coinbase_commerce 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|