coinbase_commerce 0.8.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +38 -0
- data/.gitignore +51 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +268 -0
- data/Rakefile +6 -0
- data/coinbase_commerce.gemspec +28 -0
- data/examples/charge.rb +38 -0
- data/examples/checkout.rb +61 -0
- data/examples/event.rb +26 -0
- data/examples/webhook.rb +35 -0
- data/lib/coinbase_commerce.rb +42 -0
- data/lib/coinbase_commerce/api_errors.rb +157 -0
- data/lib/coinbase_commerce/api_resources/base/api_object.rb +206 -0
- data/lib/coinbase_commerce/api_resources/base/api_resource.rb +25 -0
- data/lib/coinbase_commerce/api_resources/base/create.rb +15 -0
- data/lib/coinbase_commerce/api_resources/base/delete.rb +16 -0
- data/lib/coinbase_commerce/api_resources/base/list.rb +25 -0
- data/lib/coinbase_commerce/api_resources/base/save.rb +18 -0
- data/lib/coinbase_commerce/api_resources/base/update.rb +15 -0
- data/lib/coinbase_commerce/api_resources/charge.rb +14 -0
- data/lib/coinbase_commerce/api_resources/checkout.rb +19 -0
- data/lib/coinbase_commerce/api_resources/event.rb +13 -0
- data/lib/coinbase_commerce/api_response.rb +48 -0
- data/lib/coinbase_commerce/client.rb +120 -0
- data/lib/coinbase_commerce/util.rb +59 -0
- data/lib/coinbase_commerce/version.rb +3 -0
- data/lib/coinbase_commerce/webhooks.rb +52 -0
- data/spec/api_resources/base/api_object_spec.rb +156 -0
- data/spec/api_resources/base/api_resource_spec.rb +32 -0
- data/spec/api_resources/charge_spec.rb +19 -0
- data/spec/api_resources/checkout_spec.rb +31 -0
- data/spec/api_resources/event_spec.rb +12 -0
- data/spec/endpont_spec.rb +103 -0
- data/spec/error_spec.rb +58 -0
- data/spec/response_spec.rb +43 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/webhook_spec.rb +36 -0
- metadata +161 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module CoinbaseCommerce
|
2
|
+
module APIResources
|
3
|
+
module Base
|
4
|
+
# Base resource class
|
5
|
+
# if you need to add additional API resource, inherit from APIResource
|
6
|
+
class APIResource < APIObject
|
7
|
+
class << self
|
8
|
+
attr_accessor :client
|
9
|
+
end
|
10
|
+
|
11
|
+
@client = nil
|
12
|
+
|
13
|
+
def self.retrieve(id, params = {})
|
14
|
+
resp = @client.request(:get, "#{self::RESOURCE_PATH}/#{id}", params)
|
15
|
+
Util.convert_to_api_object(resp.data, @client, self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def refresh(params = {})
|
19
|
+
resp = @client.request(:get, "#{self.class::RESOURCE_PATH}/#{self[:id]}", params)
|
20
|
+
initialize_from(resp.data)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoinbaseCommerce
|
4
|
+
module APIResources
|
5
|
+
module Base
|
6
|
+
# create operations mixin
|
7
|
+
module Create
|
8
|
+
def create(params = {})
|
9
|
+
response = @client.request(:post, "#{self::RESOURCE_PATH}", params)
|
10
|
+
Util.convert_to_api_object(response.data, @client, self)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoinbaseCommerce
|
4
|
+
module APIResources
|
5
|
+
module Base
|
6
|
+
# delete opertaions mixin
|
7
|
+
module Delete
|
8
|
+
def delete
|
9
|
+
response = @client.request(:delete, "#{self.class::RESOURCE_PATH}/#{self[:id]}")
|
10
|
+
initialize_from(response.data)
|
11
|
+
self
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoinbaseCommerce
|
4
|
+
module APIResources
|
5
|
+
module Base
|
6
|
+
# list operations mixin
|
7
|
+
module List
|
8
|
+
def list(params = {})
|
9
|
+
resp = @client.request(:get, "#{self::RESOURCE_PATH}", params)
|
10
|
+
Util.convert_to_api_object(resp.data, @client, self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def auto_paging(params = {}, &blk)
|
14
|
+
loop do
|
15
|
+
page = list(params)
|
16
|
+
last_id = page.data.empty? ? nil : page.data.last.id
|
17
|
+
break if last_id.nil?
|
18
|
+
params[:starting_after] = last_id
|
19
|
+
page.data.each(&blk)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoinbaseCommerce
|
4
|
+
module APIResources
|
5
|
+
module Base
|
6
|
+
# save operations mixin
|
7
|
+
module Save
|
8
|
+
def save
|
9
|
+
values = serialize_params(self)
|
10
|
+
values.delete(:id)
|
11
|
+
resp = @client.request(:put, "#{self.class::RESOURCE_PATH}/#{self[:id]}", self)
|
12
|
+
initialize_from(resp.data)
|
13
|
+
self
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoinbaseCommerce
|
4
|
+
module APIResources
|
5
|
+
module Base
|
6
|
+
# update operations mixin
|
7
|
+
module Update
|
8
|
+
def modify(id, params = {})
|
9
|
+
resp = @client.request(:put, "#{self::RESOURCE_PATH}/#{id}", params)
|
10
|
+
Util.convert_to_api_object(resp.data, @client, self)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module CoinbaseCommerce
|
2
|
+
module APIResources
|
3
|
+
# Class that allows you to work with Charge resource
|
4
|
+
class Charge < Base::APIResource
|
5
|
+
# class methods
|
6
|
+
extend Base::List
|
7
|
+
extend Base::Create
|
8
|
+
|
9
|
+
# class constants
|
10
|
+
OBJECT_NAME = "charge".freeze
|
11
|
+
RESOURCE_PATH = "charges".freeze
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module CoinbaseCommerce
|
2
|
+
module APIResources
|
3
|
+
# Class that allows you to work with Checkout resource
|
4
|
+
class Checkout < Base::APIResource
|
5
|
+
# class methods
|
6
|
+
extend Base::List
|
7
|
+
extend Base::Create
|
8
|
+
extend Base::Update
|
9
|
+
|
10
|
+
# instance methods
|
11
|
+
include Base::Save
|
12
|
+
include Base::Delete
|
13
|
+
|
14
|
+
# class constants
|
15
|
+
OBJECT_NAME = "checkout".freeze
|
16
|
+
RESOURCE_PATH = "checkouts".freeze
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module CoinbaseCommerce
|
2
|
+
module APIResources
|
3
|
+
# Class that allows you to work with Event resource
|
4
|
+
class Event < Base::APIResource
|
5
|
+
# class methods
|
6
|
+
extend Base::List
|
7
|
+
|
8
|
+
# class constants
|
9
|
+
OBJECT_NAME = "event".freeze
|
10
|
+
RESOURCE_PATH = "events".freeze
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module CoinbaseCommerce
|
2
|
+
class CoinbaseCommerceResponse
|
3
|
+
attr_accessor :data
|
4
|
+
|
5
|
+
attr_accessor :http_body
|
6
|
+
|
7
|
+
attr_accessor :http_headers
|
8
|
+
|
9
|
+
attr_accessor :http_status
|
10
|
+
|
11
|
+
attr_accessor :request_id
|
12
|
+
|
13
|
+
# Initializes a CoinbaseCommerceResponse object
|
14
|
+
# from a Hash like the kind returned as part of a Faraday exception.
|
15
|
+
def self.from_faraday_hash(http_resp)
|
16
|
+
resp = CoinbaseCommerceResponse.new
|
17
|
+
resp.data = JSON.parse(http_resp[:body], symbolize_names: true)
|
18
|
+
resp.http_body = http_resp[:body]
|
19
|
+
resp.http_headers = http_resp[:headers]
|
20
|
+
resp.http_status = http_resp[:status]
|
21
|
+
resp.request_id = http_resp[:headers]["x-request-id"]
|
22
|
+
resp
|
23
|
+
end
|
24
|
+
|
25
|
+
# Initializes a CoinbaseCommerceResponse object
|
26
|
+
# from a Faraday HTTP response object.
|
27
|
+
def self.from_faraday_response(http_resp)
|
28
|
+
resp = CoinbaseCommerceResponse.new
|
29
|
+
resp.data = JSON.parse(http_resp.body, symbolize_names: true)
|
30
|
+
resp.http_body = http_resp.body
|
31
|
+
resp.http_headers = http_resp.headers
|
32
|
+
resp.http_status = http_resp.status
|
33
|
+
resp.request_id = http_resp.headers["x-request-id"]
|
34
|
+
|
35
|
+
# unpack nested data field if it exist
|
36
|
+
if resp.data.is_a? Hash and resp.data.fetch(:data, nil).is_a? Hash
|
37
|
+
resp.data.update(resp.data.delete(:data))
|
38
|
+
end
|
39
|
+
|
40
|
+
# warn in there warnings in response
|
41
|
+
if resp.data.is_a? Hash and resp.data.fetch(:warnings, nil).is_a? Array
|
42
|
+
warn(resp.data[:warnings].first.to_s)
|
43
|
+
end
|
44
|
+
|
45
|
+
resp
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoinbaseCommerce
|
4
|
+
BASE_API_URL = "https://api.commerce.coinbase.com/"
|
5
|
+
API_VERSION = "2018-03-22"
|
6
|
+
|
7
|
+
class Client
|
8
|
+
# API Client for the Coinbase API.
|
9
|
+
# Entry point for making requests to the Coinbase API.
|
10
|
+
# Full API docs available here: https://commerce.coinbase.com/docs/api/
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
# set API key and API URL
|
14
|
+
check_api_key!(options[:api_key])
|
15
|
+
@api_key = options[:api_key]
|
16
|
+
@api_uri = URI.parse(options[:api_url] || BASE_API_URL)
|
17
|
+
@api_ver = options[:api_ver] || API_VERSION
|
18
|
+
# create client obj
|
19
|
+
@conn = Faraday.new do |c|
|
20
|
+
c.use Faraday::Request::Multipart
|
21
|
+
c.use Faraday::Request::UrlEncoded
|
22
|
+
c.use Faraday::Response::RaiseError
|
23
|
+
c.adapter Faraday.default_adapter
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Set client-resource relations with all API resources
|
28
|
+
# provide client instance to each resource
|
29
|
+
def charge
|
30
|
+
APIResources::Charge.client = self
|
31
|
+
APIResources::Charge
|
32
|
+
end
|
33
|
+
|
34
|
+
def checkout
|
35
|
+
APIResources::Checkout.client = self
|
36
|
+
APIResources::Checkout
|
37
|
+
end
|
38
|
+
|
39
|
+
def event
|
40
|
+
APIResources::Event.client = self
|
41
|
+
APIResources::Event
|
42
|
+
end
|
43
|
+
|
44
|
+
def api_url(url = "", api_base = nil)
|
45
|
+
(api_base || CoinbaseCommerce::BASE_API_URL) + url
|
46
|
+
end
|
47
|
+
|
48
|
+
def request_headers(api_key)
|
49
|
+
{
|
50
|
+
"User-Agent" => "CoinbaseCommerce/#{CoinbaseCommerce::VERSION}",
|
51
|
+
"Accept" => "application/json",
|
52
|
+
"X-CC-Api-Key" => api_key,
|
53
|
+
"X-CC-Version" => @api_ver,
|
54
|
+
"Content-Type" => "application/json",
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def check_api_key!(api_key)
|
59
|
+
raise AuthenticationError, "No API key provided" unless api_key
|
60
|
+
end
|
61
|
+
|
62
|
+
# request section
|
63
|
+
def request(method, path, params = {})
|
64
|
+
@last_response = nil
|
65
|
+
url = api_url(path, @api_uri)
|
66
|
+
headers = request_headers(@api_key)
|
67
|
+
|
68
|
+
body = nil
|
69
|
+
query_params = nil
|
70
|
+
|
71
|
+
case method.to_s.downcase.to_sym
|
72
|
+
when :get, :head, :delete
|
73
|
+
query_params = params
|
74
|
+
else
|
75
|
+
body = params.to_json
|
76
|
+
end
|
77
|
+
|
78
|
+
u = URI.parse(path)
|
79
|
+
unless u.query.nil?
|
80
|
+
query_params ||= {}
|
81
|
+
query_params = Hash[URI.decode_www_form(u.query)].merge(query_params)
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
http_resp = execute_request_with_rescues(@api_uri) do
|
86
|
+
@conn.run_request(method, url, body, headers) do |req|
|
87
|
+
req.params = query_params unless query_params.nil?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
begin
|
92
|
+
resp = CoinbaseCommerceResponse.from_faraday_response(http_resp)
|
93
|
+
rescue JSON::ParserError
|
94
|
+
raise Errors.general_api_error(http_resp.status, http_resp.body)
|
95
|
+
end
|
96
|
+
|
97
|
+
@last_response = resp
|
98
|
+
resp
|
99
|
+
end
|
100
|
+
|
101
|
+
# сollect errors during request execution if they occurred
|
102
|
+
def execute_request_with_rescues(api_base)
|
103
|
+
begin
|
104
|
+
resp = yield
|
105
|
+
rescue StandardError => e
|
106
|
+
case e
|
107
|
+
when Faraday::ClientError
|
108
|
+
if e.response
|
109
|
+
Errors.handle_error_response(e.response)
|
110
|
+
else
|
111
|
+
Errors.handle_network_error(e, api_base)
|
112
|
+
end
|
113
|
+
else
|
114
|
+
raise
|
115
|
+
end
|
116
|
+
end
|
117
|
+
resp
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module CoinbaseCommerce
|
2
|
+
module Util
|
3
|
+
|
4
|
+
def self.object_classes
|
5
|
+
# Class mappings for api responce fetching
|
6
|
+
@object_classes ||= {
|
7
|
+
# API Resources
|
8
|
+
APIResources::Checkout::OBJECT_NAME => APIResources::Checkout,
|
9
|
+
APIResources::Charge::OBJECT_NAME => APIResources::Charge,
|
10
|
+
APIResources::Event::OBJECT_NAME => APIResources::Event,
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def self.convert_to_api_object(data, client = nil, klass = nil)
|
16
|
+
# Converts a hash of fields or an array of hashes into a
|
17
|
+
# appropriate APIResources of APIObjects form
|
18
|
+
case data
|
19
|
+
when Array
|
20
|
+
data.map {|i| convert_to_api_object(i, client, klass)}
|
21
|
+
when Hash
|
22
|
+
# If class received in params, create instance
|
23
|
+
if klass
|
24
|
+
klass.create_from(data, client)
|
25
|
+
else
|
26
|
+
# Try converting to a known object class.
|
27
|
+
# If none available, fall back to generic APIObject
|
28
|
+
klass = object_classes.fetch(data[:resource], APIResources::Base::APIObject)
|
29
|
+
# Provide client relation only for APIResource objects
|
30
|
+
klass != APIResources::Base::APIObject ? klass.create_from(data, client) : klass.create_from(data)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
data
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.symbolize_names(object)
|
38
|
+
# Convert object key and values to symbols if its possible
|
39
|
+
case object
|
40
|
+
when Hash
|
41
|
+
new_hash = {}
|
42
|
+
object.each do |key, value|
|
43
|
+
key = (
|
44
|
+
begin
|
45
|
+
key.to_sym
|
46
|
+
rescue StandardError
|
47
|
+
key
|
48
|
+
end) || key
|
49
|
+
new_hash[key] = symbolize_names(value)
|
50
|
+
end
|
51
|
+
new_hash
|
52
|
+
when Array
|
53
|
+
object.map {|value| symbolize_names(value)}
|
54
|
+
else
|
55
|
+
object
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoinbaseCommerce
|
4
|
+
module Webhook
|
5
|
+
# Analyze and construct appropriate event object based on webhook notification
|
6
|
+
def self.construct_event(payload, sig_header, secret)
|
7
|
+
data = JSON.parse(payload, symbolize_names: true)
|
8
|
+
if data.key?(:event)
|
9
|
+
WebhookSignature.verify_header(payload, sig_header, secret)
|
10
|
+
CoinbaseCommerce::APIResources::Event.create_from(data[:event])
|
11
|
+
else
|
12
|
+
raise CoinbaseCommerce::Errors::WebhookInvalidPayload.new("no event in payload",
|
13
|
+
sig_header, http_body: payload)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module WebhookSignature
|
18
|
+
def self.verify_header(payload, sig_header, secret)
|
19
|
+
unless [payload, sig_header, secret].all?
|
20
|
+
raise CoinbaseCommerce::Errors::WebhookInvalidPayload.new(
|
21
|
+
"Missing payload or signature",
|
22
|
+
sig_header, http_body: payload)
|
23
|
+
end
|
24
|
+
expected_sig = compute_signature(payload, secret)
|
25
|
+
unless secure_compare(expected_sig, sig_header)
|
26
|
+
raise CoinbaseCommerce::Errors::SignatureVerificationError.new(
|
27
|
+
"No signatures found matching the expected signature for payload",
|
28
|
+
sig_header, http_body: payload
|
29
|
+
)
|
30
|
+
end
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.secure_compare(a, b)
|
35
|
+
return false unless a.bytesize == b.bytesize
|
36
|
+
|
37
|
+
l = a.unpack "C#{a.bytesize}"
|
38
|
+
res = 0
|
39
|
+
b.each_byte {|byte| res |= byte ^ l.shift}
|
40
|
+
res.zero?
|
41
|
+
end
|
42
|
+
|
43
|
+
private_class_method :secure_compare
|
44
|
+
|
45
|
+
def self.compute_signature(payload, secret)
|
46
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload)
|
47
|
+
end
|
48
|
+
|
49
|
+
private_class_method :compute_signature
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|