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,61 @@
1
+ require 'coinbase_commerce'
2
+
3
+ client = CoinbaseCommerce::Client.new(api_key: 'your_api_key')
4
+
5
+ # create checkout
6
+ data = {
7
+ "name": "The Sovereign Individual",
8
+ "description": "Mastering the Transition to the Information Age",
9
+ "pricing_type": "fixed_price",
10
+ "local_price": {
11
+ "amount": "1.00",
12
+ "currency": "USD"
13
+ },
14
+ "requested_info": ["name", "email"]
15
+ }
16
+ checkout = client.checkout.create(data)
17
+
18
+ # or retrieve it if you know checkout id
19
+ checkout = client.checkout.retrieve checkout.id
20
+
21
+ # update checkout with modify method
22
+ upd_checkout = client.checkout.modify(checkout.id, "local_price": {
23
+ "amount": "10000.00",
24
+ "currency": "USD"
25
+ })
26
+
27
+ # or with save method if you already have checkout object
28
+ upd_checkout.name
29
+ upd_checkout.description = "foo"
30
+ upd_checkout.to_hash
31
+ upd_checkout.name = "bar"
32
+ amount = "1000.00"
33
+ upd_checkout.local_price.amount = amount
34
+ upd_checkout.save
35
+
36
+ # get checkouts list
37
+ checkouts_list = client.checkout.list
38
+
39
+ # in case you need provide additional params
40
+ checkouts_list = client.checkout.list(limit: 10)
41
+
42
+ # or get results from another page
43
+ checkouts_list = client.checkout.list(starting_after: checkout.id, limit: 3)
44
+
45
+ # checkout list could be iterated like
46
+ checkouts_list.data.each do |ch|
47
+ # work with each checkout
48
+ puts ch.id
49
+ end
50
+
51
+ # iterate over all checkouts and modify them with per-page limitation
52
+ client.checkout.auto_paging limit: 20 do |ch|
53
+ puts ch.id
54
+ ch.name = 'name updated'
55
+ ch.save
56
+ # also could be deleted by
57
+ # ch.delete
58
+ end
59
+
60
+ # delete checkout
61
+ upd_checkout.delete
data/examples/event.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'coinbase_commerce'
2
+
3
+ client = CoinbaseCommerce::Client.new(api_key: 'your_api_key')
4
+
5
+ # get events list
6
+ events_list = client.event.list
7
+
8
+ # in case you need provide additional params
9
+ events_list = client.event.list(limit: 10)
10
+
11
+ # or get results from another page
12
+ event = events_list.data[0]
13
+ events_list = client.event.list(starting_after: event.id, limit: 3)
14
+
15
+ # event list could be iterated like
16
+ events_list.data.each do |event|
17
+ puts event.id
18
+ end
19
+
20
+ # retrieve single event
21
+ event = client.event.retrieve event.id
22
+
23
+ # iterate over all events with per-page limitation
24
+ client.event.auto_paging limit: 20 do |event|
25
+ puts event.id
26
+ end
@@ -0,0 +1,35 @@
1
+ # Sinatra server example to test webhooks
2
+ # You may need tunnels to localhost webhook development tool and debugging tool.
3
+ # f.e. you could try ngrok
4
+
5
+ require 'sinatra'
6
+ require 'coinbase_commerce'
7
+
8
+ set :port, 5000
9
+ WEBHOOK_SECRET = 'your_webhook_secret'
10
+
11
+ # Using Sinatra
12
+ post '/webhooks' do
13
+ payload = request.body.read
14
+ sig_header = request.env['HTTP_X_CC_WEBHOOK_SIGNATURE']
15
+
16
+ begin
17
+ event = CoinbaseCommerce::Webhook.construct_event(payload, sig_header, WEBHOOK_SECRET)
18
+ # event handle
19
+ puts "Received event id=#{event.id}, type=#{event.type}"
20
+ status 200
21
+ # errors handle
22
+ rescue JSON::ParserError => e
23
+ puts "json parse error"
24
+ status 400
25
+ return
26
+ rescue CoinbaseCommerce::Errors::SignatureVerificationError => e
27
+ puts "signature verification error"
28
+ status 400
29
+ return
30
+ rescue CoinbaseCommerce::Errors::WebhookInvalidPayload => e
31
+ puts "missing request or headers data"
32
+ status 400
33
+ return
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ # general
2
+ require "json"
3
+ require "uri"
4
+ require "faraday"
5
+ require "openssl"
6
+
7
+ # version
8
+ require "coinbase_commerce/version"
9
+
10
+ # client
11
+ require "coinbase_commerce/client"
12
+
13
+ # api response and errors
14
+ require "coinbase_commerce/api_errors"
15
+ require "coinbase_commerce/api_response"
16
+
17
+ # api base object
18
+ require "coinbase_commerce/api_resources/base/api_object"
19
+
20
+ # api resource base model
21
+ require "coinbase_commerce/api_resources/base/api_resource"
22
+
23
+ # api base operations
24
+ require "coinbase_commerce/api_resources/base/create"
25
+ require "coinbase_commerce/api_resources/base/update"
26
+ require "coinbase_commerce/api_resources/base/save"
27
+ require "coinbase_commerce/api_resources/base/list"
28
+ require "coinbase_commerce/api_resources/base/delete"
29
+
30
+ # api resources
31
+ require "coinbase_commerce/api_resources/checkout"
32
+ require "coinbase_commerce/api_resources/charge"
33
+ require "coinbase_commerce/api_resources/event"
34
+
35
+ # webhooks
36
+ require "coinbase_commerce/webhooks"
37
+
38
+ # utils
39
+ require "coinbase_commerce/util"
40
+
41
+ module CoinbaseCommerce
42
+ end
@@ -0,0 +1,157 @@
1
+ module CoinbaseCommerce
2
+ module Errors
3
+ class APIError < StandardError
4
+ attr_reader :message
5
+
6
+ # Response contains a CoinbaseCommerceResponse object
7
+ attr_accessor :response
8
+
9
+ attr_reader :http_body
10
+ attr_reader :http_headers
11
+ attr_reader :http_status
12
+ attr_reader :json_body
13
+ attr_reader :request_id
14
+
15
+ # Initializes a API error.
16
+ def initialize(message = nil, http_status: nil, http_body: nil,
17
+ json_body: nil, http_headers: nil)
18
+ @message = message
19
+ @http_status = http_status
20
+ @http_body = http_body
21
+ @http_headers = http_headers || {}
22
+ @json_body = json_body
23
+ @request_id = @http_headers["x-request-id"]
24
+ end
25
+
26
+ def to_s
27
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
28
+ id_string = @request_id.nil? ? "" : "(Request #{@request_id}) "
29
+ "#{status_string}#{id_string}#{@message}"
30
+ end
31
+ end
32
+
33
+ # in case error connecting to coinbase commerce server
34
+ class APIConnectionError < APIError
35
+ end
36
+
37
+ # Status 400
38
+ class BadRequestError < APIError
39
+ end
40
+
41
+ class ParamRequiredError < APIError
42
+ end
43
+
44
+ class InvalidRequestError < APIError
45
+ end
46
+
47
+ # Status 401
48
+ class AuthenticationError < APIError
49
+ end
50
+
51
+ # Status 404
52
+ class ResourceNotFoundError < APIError
53
+ end
54
+
55
+ # Status 422
56
+ class ValidationError < APIError
57
+ end
58
+
59
+ # Status 429
60
+ class RateLimitExceededError < APIError
61
+ end
62
+
63
+ # Status 500
64
+ class InternalServerError < APIError
65
+ end
66
+
67
+ # Status 503
68
+ class ServiceUnavailableError < APIError
69
+ end
70
+
71
+ # Webhook errors
72
+ class WebhookError < APIError
73
+ attr_accessor :sig_header
74
+
75
+ def initialize(message, sig_header, http_body: nil)
76
+ super(message, http_body: http_body)
77
+ @sig_header = sig_header
78
+ end
79
+ end
80
+
81
+ class SignatureVerificationError < WebhookError
82
+ end
83
+
84
+ class WebhookInvalidPayload < WebhookError
85
+ end
86
+
87
+ # Errors handling
88
+ def self.handle_error_response(http_resp)
89
+ begin
90
+ resp = CoinbaseCommerceResponse.from_faraday_hash(http_resp)
91
+ error_data = resp.data[:error]
92
+
93
+ raise APIError, "Unknown error" unless error_data
94
+ rescue JSON::ParserError, APIError
95
+ raise general_api_error(http_resp[:status], http_resp[:body])
96
+ end
97
+ error = specific_api_error(resp, error_data)
98
+ error.response = resp
99
+ raise(error)
100
+ end
101
+
102
+ def self.general_api_error(status, body)
103
+ APIError.new("Invalid response object from API: #{body.inspect} " +
104
+ "(HTTP response code: #{status} http_body: #{body}")
105
+ end
106
+
107
+ def self.specific_api_error(resp, error_data)
108
+ opts = {
109
+ http_body: resp.http_body,
110
+ http_headers: resp.http_headers,
111
+ http_status: resp.http_status,
112
+ json_body: resp.data,
113
+ }
114
+ case resp.http_status
115
+ when 400
116
+ # in case of known error code
117
+ case error_data[:type]
118
+ when 'param_required'
119
+ ParamRequiredError.new(error_data[:message], opts)
120
+ when 'validation_error'
121
+ ValidationError.new(error_data[:message], opts)
122
+ when 'invalid_request'
123
+ InvalidRequestError.new(error_data[:message], opts)
124
+ else
125
+ InvalidRequestError.new(error_data[:message], opts)
126
+ end
127
+ when 401 then
128
+ AuthenticationError.new(error_data[:message], opts)
129
+ when 404
130
+ ResourceNotFoundError.new(error_data[:message], opts)
131
+ when 429
132
+ RateLimitExceededError.new(error_data[:message], opts)
133
+ when 500
134
+ InternalServerError.new(error_data[:message], opts)
135
+ when 503
136
+ ServiceUnavailableError.new(error_data[:message], opts)
137
+ else
138
+ APIError.new(error_data[:message], opts)
139
+ end
140
+ end
141
+
142
+ def self.handle_network_error(e, api_base = nil)
143
+ api_base ||= @api_uri
144
+ case e
145
+ when Faraday::ConnectionFailed
146
+ message = "Unexpected error communicating when trying to connect to Coinbase Commerce."
147
+ when Faraday::SSLError
148
+ message = "Could not establish a secure connection to Coinbase Commerce."
149
+ when Faraday::TimeoutError
150
+ message = "Could not connect to Coinbase Commerce (#{api_base})."
151
+ else
152
+ message = "Unexpected error communicating with Coinbase Commerce."
153
+ end
154
+ raise APIConnectionError, message + "\n\n(Network error: #{e.message})"
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,206 @@
1
+ module CoinbaseCommerce
2
+ module APIResources
3
+ module Base
4
+ # Base APIObject class
5
+ # Used to work and display with all the data
6
+ # that Coinbase Commerce API returns
7
+ class APIObject
8
+ include Enumerable
9
+
10
+ def initialize(id = nil, client = nil)
11
+ @data = {}
12
+ @data[:id] = id if id
13
+ @client = client
14
+ @unsaved_values = Set.new
15
+ @transient_values = Set.new
16
+ end
17
+
18
+ # Base object options section
19
+ def [](k)
20
+ @data[k.to_sym]
21
+ end
22
+
23
+ def []=(k, v)
24
+ send(:"#{k}=", v)
25
+ end
26
+
27
+ def keys
28
+ @data.keys
29
+ end
30
+
31
+ def values
32
+ @data.values
33
+ end
34
+
35
+ def each(&blk)
36
+ @data.each(&blk)
37
+ end
38
+
39
+ def to_s(*_args)
40
+ JSON.pretty_generate(to_hash)
41
+ end
42
+
43
+ def to_hash
44
+ @data.each_with_object({}) do |(key, value), output|
45
+ case value
46
+ when Array
47
+ output[key] = value.map {|v| v.respond_to?(:to_hash) ? v.to_hash : v}
48
+ else
49
+ output[key] = value.respond_to?(:to_hash) ? value.to_hash : value
50
+ end
51
+ end
52
+ end
53
+
54
+ def to_json(*_a)
55
+ JSON.generate(@data)
56
+ end
57
+
58
+ def inspect
59
+ item_id = respond_to?(:id) && !id.nil? ? "id=#{id}" : "No ID"
60
+ "#{self.class}: #{item_id}> Serialized: " + JSON.pretty_generate(@data)
61
+ end
62
+
63
+ def respond_to_missing?(symbol, include_private = false)
64
+ @data && @data.key?(symbol) || super
65
+ end
66
+
67
+ def method_missing(name, *args)
68
+ if name.to_s.end_with?("=")
69
+
70
+ attr = name.to_s[0...-1].to_sym
71
+ val = args.first
72
+ add_accessors([attr], attr => val)
73
+
74
+ begin
75
+ mth = method(name)
76
+ rescue NameError
77
+ raise NoMethodError, "Cannot set #{attr} on this object."
78
+ end
79
+
80
+ return mth.call(args[0])
81
+
82
+ elsif @data.key?(name)
83
+ return @data[name]
84
+ end
85
+
86
+ begin
87
+ super
88
+ rescue NoMethodError => e
89
+ raise unless @transient_values.include?(name)
90
+ raise NoMethodError, e.message + " Available attributes: #{@data.keys.join(', ')}"
91
+ end
92
+ end
93
+
94
+ # Object serialize section
95
+ def serialize_params(options = {})
96
+ update_hash = {}
97
+
98
+ @data.each do |k, v|
99
+ if options[:push] || @unsaved_values.include?(k) || v.is_a?(APIObject)
100
+ push = options[:push] || @unsaved_values.include?(k)
101
+ update_hash[k.to_sym] = serialize_params_value(@data[k], push)
102
+ end
103
+ end
104
+
105
+ update_hash.reject! {|_, v| v.nil? || v.empty?}
106
+ update_hash
107
+ end
108
+
109
+ def serialize_params_value(value, push)
110
+ if value.nil?
111
+ ""
112
+ elsif value.is_a?(Array)
113
+ value.map {|v| serialize_params_value(v, push)}
114
+
115
+ elsif value.is_a?(Hash)
116
+ Util.convert_to_api_object(value, @opts).serialize_params
117
+
118
+ elsif value.is_a?(APIObject)
119
+ value.serialize_params(push: push)
120
+ else
121
+ value
122
+ end
123
+ end
124
+
125
+ # Object initialize/update section
126
+ def self.create_from(values, client = nil)
127
+ values = Util.symbolize_names(values)
128
+ new(values[:id], client).send(:initialize_from, values)
129
+ end
130
+
131
+ def initialize_from(values, partial = false)
132
+ removed = partial ? Set.new : Set.new(@data.keys - values.keys)
133
+ added = Set.new(values.keys - @data.keys)
134
+
135
+ remove_accessors(removed)
136
+ add_accessors(added, values)
137
+
138
+ removed.each do |k|
139
+ @data.delete(k)
140
+ @transient_values.add(k)
141
+ @unsaved_values.delete(k)
142
+ end
143
+
144
+ update_attributes(values)
145
+ values.each_key do |k|
146
+ @transient_values.delete(k)
147
+ @unsaved_values.delete(k)
148
+ end
149
+
150
+ self
151
+ end
152
+
153
+ def update_attributes(values)
154
+ values.each do |k, v|
155
+ add_accessors([k], values) unless metaclass.method_defined?(k.to_sym)
156
+ @data[k] = Util.convert_to_api_object(v, @client)
157
+ @unsaved_values.add(k)
158
+ end
159
+ end
160
+
161
+
162
+ protected
163
+
164
+ def metaclass
165
+ class << self
166
+ self
167
+ end
168
+ end
169
+
170
+ def remove_accessors(keys)
171
+ metaclass.instance_eval do
172
+ keys.each do |k|
173
+ # Remove methods for the accessor's reader and writer.
174
+ [k, :"#{k}=", :"#{k}?"].each do |method_name|
175
+ remove_method(method_name) if method_defined?(method_name)
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ def add_accessors(keys, values)
182
+ metaclass.instance_eval do
183
+ keys.each do |k|
184
+ if k == :method
185
+ define_method(k) {|*args| args.empty? ? @data[k] : super(*args)}
186
+ else
187
+ define_method(k) {@data[k]}
188
+ end
189
+
190
+ define_method(:"#{k}=") do |v|
191
+ if v != ""
192
+ @data[k] = Util.convert_to_api_object(v, @opts)
193
+ @unsaved_values.add(k)
194
+ end
195
+ end
196
+
197
+ if [FalseClass, TrueClass].include?(values[k].class)
198
+ define_method(:"#{k}?") {@data[k]}
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end