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,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
|
data/examples/webhook.rb
ADDED
@@ -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
|