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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +38 -0
  3. data/.gitignore +51 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +21 -0
  6. data/README.md +268 -0
  7. data/Rakefile +6 -0
  8. data/coinbase_commerce.gemspec +28 -0
  9. data/examples/charge.rb +38 -0
  10. data/examples/checkout.rb +61 -0
  11. data/examples/event.rb +26 -0
  12. data/examples/webhook.rb +35 -0
  13. data/lib/coinbase_commerce.rb +42 -0
  14. data/lib/coinbase_commerce/api_errors.rb +157 -0
  15. data/lib/coinbase_commerce/api_resources/base/api_object.rb +206 -0
  16. data/lib/coinbase_commerce/api_resources/base/api_resource.rb +25 -0
  17. data/lib/coinbase_commerce/api_resources/base/create.rb +15 -0
  18. data/lib/coinbase_commerce/api_resources/base/delete.rb +16 -0
  19. data/lib/coinbase_commerce/api_resources/base/list.rb +25 -0
  20. data/lib/coinbase_commerce/api_resources/base/save.rb +18 -0
  21. data/lib/coinbase_commerce/api_resources/base/update.rb +15 -0
  22. data/lib/coinbase_commerce/api_resources/charge.rb +14 -0
  23. data/lib/coinbase_commerce/api_resources/checkout.rb +19 -0
  24. data/lib/coinbase_commerce/api_resources/event.rb +13 -0
  25. data/lib/coinbase_commerce/api_response.rb +48 -0
  26. data/lib/coinbase_commerce/client.rb +120 -0
  27. data/lib/coinbase_commerce/util.rb +59 -0
  28. data/lib/coinbase_commerce/version.rb +3 -0
  29. data/lib/coinbase_commerce/webhooks.rb +52 -0
  30. data/spec/api_resources/base/api_object_spec.rb +156 -0
  31. data/spec/api_resources/base/api_resource_spec.rb +32 -0
  32. data/spec/api_resources/charge_spec.rb +19 -0
  33. data/spec/api_resources/checkout_spec.rb +31 -0
  34. data/spec/api_resources/event_spec.rb +12 -0
  35. data/spec/endpont_spec.rb +103 -0
  36. data/spec/error_spec.rb +58 -0
  37. data/spec/response_spec.rb +43 -0
  38. data/spec/spec_helper.rb +15 -0
  39. data/spec/webhook_spec.rb +36 -0
  40. 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,3 @@
1
+ module CoinbaseCommerce
2
+ VERSION = "0.8.7"
3
+ 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