coinbase_commerce 0.8.7

Sign up to get free protection for your applications and to get access to all the features.
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