synapse_fi 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6e6933e283961c5db4a8f009d272a7aa74e501c3
4
+ data.tar.gz: 79d68318e948ea23958fe7ea0000ee9503038e54
5
+ SHA512:
6
+ metadata.gz: 4d5d0707a08845b5c7469300982035c6b441eabfc5b01645cc8dc457a5264567c09a2340847e72e6ebad884c37c0842250c90ba886be3b0f3bda89533d011260
7
+ data.tar.gz: 2931dd52da4c6375de02b4b5b6bd54b0e12b38326525f793db6d105479df8b43088bbd3b49243709116810d208ffb6c414d2710d1b5bf149f772fae6c9e388c8
data/.DS_Store ADDED
Binary file
data/.env.sample ADDED
@@ -0,0 +1,9 @@
1
+ # For development in a console
2
+ CLIENT_ID=your_sandbox_client_id
3
+ CLIENT_SECRET=your_sandbox_client_secret
4
+ FINGERPRINT=your_sandbox_fingerprint
5
+
6
+ # For running tests
7
+ TEST_CLIENT_ID=your_sandbox_client_id
8
+ TEST_CLIENT_SECRET=your_sandbox_client_secret
9
+
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .env
2
+ test
3
+
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+ source 'https://rubygems.org'
3
+
4
+
5
+ # Specify your gem's dependencies in your *.gemspec file
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,43 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ ansi (1.5.0)
5
+ builder (3.2.3)
6
+ domain_name (0.5.20180417)
7
+ unf (>= 0.0.5, < 1.0.0)
8
+ dotenv (2.5.0)
9
+ http-cookie (1.0.3)
10
+ domain_name (~> 0.5)
11
+ mime-types (3.2.2)
12
+ mime-types-data (~> 3.2015)
13
+ mime-types-data (3.2018.0812)
14
+ minitest (5.11.3)
15
+ minitest-reporters (1.3.5)
16
+ ansi
17
+ builder
18
+ minitest (>= 5.0)
19
+ ruby-progressbar
20
+ netrc (0.11.0)
21
+ rest-client (2.0.2)
22
+ http-cookie (>= 1.0.2, < 2.0)
23
+ mime-types (>= 1.16, < 4.0)
24
+ netrc (~> 0.8)
25
+ ruby-progressbar (1.10.0)
26
+ synapse_pay_rest (3.4.3)
27
+ rest-client (~> 2.0)
28
+ unf (0.1.4)
29
+ unf_ext
30
+ unf_ext (0.0.7.5)
31
+
32
+ PLATFORMS
33
+ ruby
34
+
35
+ DEPENDENCIES
36
+ bundler
37
+ dotenv
38
+ minitest
39
+ minitest-reporters
40
+ synapse_pay_rest
41
+
42
+ BUNDLED WITH
43
+ 1.17.1
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # SynapseFI-Ruby-v2
2
+
3
+ Native API library for SynapsePay REST v3.x
4
+
5
+ Not all API endpoints are supported.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'synapse_fi'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ ```bash
18
+ $ bundle
19
+ ```
20
+
21
+ Or install it yourself by executing:
22
+
23
+ ```bash
24
+ $ gem install synapse_fi
25
+ ```
26
+ ## Documentation
27
+
28
+ - [API docs](http://docs.synapsefi.com/v3.1)
29
+ - [Samples demonstrating common operations](samples.md)
30
+
31
+ ## Contributing
32
+
33
+ For minor issues, please open a pull request. For larger changes or features, please email hello@synapsepay.com. Please document and test any public constants/methods.
34
+
35
+ ## License
36
+
37
+ [MIT License](LICENSE)
38
+
39
+
Binary file
@@ -0,0 +1,355 @@
1
+ require 'json'
2
+ require_relative './http_request'
3
+ require_relative './user'
4
+ require_relative './users'
5
+ require_relative './transaction'
6
+ require_relative './transactions'
7
+ require_relative './node'
8
+ require_relative './nodes'
9
+ require_relative './subscription'
10
+ require_relative './subscriptions'
11
+ require_relative './subnet'
12
+ require_relative './subnets'
13
+ require 'pp'
14
+
15
+
16
+
17
+ module Synapse
18
+ # Initializes various wrapper settings such as development mode and request
19
+ # header values
20
+
21
+ class Client
22
+
23
+ VALID_QUERY_PARAMS = [:query, :page, :per_page, :full_dehydrate, :radius, :zip, :lat, :lon, :limit, :currency].freeze
24
+
25
+ attr_accessor :http_client
26
+
27
+ attr_reader :client_id
28
+
29
+ # Alias for #http_client
30
+ alias_method :client, :http_client
31
+
32
+
33
+ # @param client_id [String] should be stored in environment variable
34
+ # @param client_secret [String] should be stored in environment variable
35
+ # @param ip_address [String] user's IP address
36
+ # @param fingerprint [String] a hashed value, either unique to user or static
37
+ # @param development_mode [String] default true
38
+ # @param raise_for_202 [Boolean]
39
+ # @param logging [Boolean] (optional) logs to stdout when true
40
+ # @param log_to [String] (optional) file path to log to file (logging must be true)
41
+ def initialize(client_id:, client_secret:, ip_address:, fingerprint:nil,development_mode: true, raise_for_202:nil, **options)
42
+ base_url = if development_mode
43
+ 'https://uat-api.synapsefi.com/v3.1'
44
+ else
45
+ 'https://api.synapsefi.com/v3.1'
46
+ end
47
+ @client_id = client_id
48
+ @client_secret = client_secret
49
+ @http_client = HTTPClient.new(base_url: base_url,
50
+ client_id: client_id,
51
+ client_secret: client_secret,
52
+ fingerprint: fingerprint,
53
+ ip_address: ip_address,
54
+ raise_for_202: raise_for_202,
55
+ **options)
56
+
57
+ end
58
+
59
+ # Queries Synapse API to create a new user
60
+ # @param payload [Hash]
61
+ # @param idempotency_key [String] (optional)
62
+ # @return[Synapse::User]
63
+ # @see https://docs.synapsepay.com/docs/create-a-user payload structure
64
+ def create_user(payload:, **options)
65
+ raise ArgumentError, 'client must be a Synapse::Client' unless self.is_a?(Client)
66
+ response = client.post(user_path,payload, options)
67
+
68
+
69
+ user = User.new(
70
+ user_id: response['_id'],
71
+ refresh_token: response['refresh_token'],
72
+ client: client,
73
+ full_dehydrate: "no",
74
+ payload: response
75
+ )
76
+ user
77
+ end
78
+
79
+ # Update headers in HTTPClient class
80
+ # for API request headers
81
+ # @param fingerprint [Hash]
82
+ # @param idemopotency_key [Hash]
83
+ # @param ip_address [Hash]
84
+ def update_headers(fingerprint:nil, idemopotency_key:nil, ip_address:nil)
85
+ client.update_headers(fingerprint: fingerprint, idemopotency_key: idemopotency_key, ip_address: ip_address)
86
+ end
87
+
88
+ # Queries Synapse API for a user by user_id
89
+ # @param user_id [String] id of the user to find
90
+ # @param full_dehydrate [String] (optional) if true, returns all KYC on user
91
+ # @see https://docs.synapsefi.com/docs/get-user
92
+ # @return [Synapse::User]
93
+ def get_user(user_id:, **options)
94
+ raise ArgumentError, 'client must be a Synapse::Client' unless self.is_a?(Client)
95
+ raise ArgumentError, 'user_id must be a String' unless user_id.is_a?(String)
96
+
97
+ options[:full_dehydrate] = "yes" if options[:full_dehydrate] == true
98
+ options[:full_dehydrate] = "no" if options[:full_dehydrate] == false
99
+
100
+ path = user_path(user_id: user_id, full_dehydrate: options[:full_dehydrate])
101
+ response = client.get(path)
102
+
103
+ user = User.new(
104
+ user_id: response['_id'],
105
+ refresh_token: response['refresh_token'],
106
+ client: client,
107
+ full_dehydrate: options[:full_dehydrate] == "yes" ? true : false,
108
+ payload: response
109
+ )
110
+ user
111
+ end
112
+
113
+ # Queries Synapse API for platform users
114
+ # @param query [String] (optional) response will be filtered to
115
+ # users with matching name/email
116
+ # @param page [Integer] (optional) response will default to 1
117
+ # @param per_page [Integer] (optional) response will default to 20
118
+ # @note users created this way are not automatically OAuthed
119
+ # @return [Array<Synapse::Users>]
120
+ def get_users(**options)
121
+ path = user_path(options)
122
+ response = client.get(path)
123
+ return [] if response["users"].empty?
124
+ users = response["users"].map { |user_data| User.new(user_id: user_data['_id'], refresh_token: user_data['refresh_token'], client: client, full_dehydrate: "no", payload: user_data)}
125
+ users = Users.new(limit: response["limit"], page: response["page"], page_count: response["page_count"], user_count: response["user_count"], payload: users, http_client: client)
126
+
127
+ users
128
+ end
129
+
130
+ # Queries Synapse for all transactions on platform
131
+ # @param page [Integer] (optional) response will default to 1
132
+ # @param per_page [Integer] (optional) response will default to 20
133
+ # @return [Array<Synapse::Transactions>]
134
+ def get_all_transaction(**options)
135
+ path = '/trans'
136
+
137
+ params = VALID_QUERY_PARAMS.map do |p|
138
+ options[p] ? "#{p}=#{options[p]}" : nil
139
+ end.compact
140
+
141
+ path += '?' + params.join('&') if params.any?
142
+
143
+ trans = client.get(path)
144
+
145
+ return [] if trans["trans"].empty?
146
+ response = trans["trans"].map { |trans_data| Transaction.new(trans_id: trans_data['_id'], payload: trans_data)}
147
+ trans = Transactions.new(limit: trans["limit"], page: trans["page"], page_count: trans["page_count"], trans_count: trans["trans_count"], payload: response)
148
+ trans
149
+
150
+ end
151
+
152
+ # Queries Synapse API for all nodes belonging to platform
153
+ # @param page [Integer] (optional) response will default to 1
154
+ # @param per_page [Integer] (optional) response will default to 20
155
+ # @return [Array<Synapse::Nodes>]
156
+ def get_all_nodes(**options)
157
+ [options[:page], options[:per_page]].each do |arg|
158
+ if arg && (!arg.is_a?(Integer) || arg < 1)
159
+ raise ArgumentError, "#{arg} must be nil or an Integer >= 1"
160
+ end
161
+ end
162
+ path = nodes_path(options: options)
163
+ nodes = client.get(path)
164
+
165
+ return [] if nodes["nodes"].empty?
166
+ response = nodes["nodes"].map { |node_data| Node.new(node_id: node_data['_id'], user_id: node_data['user_id'], payload: node_data, full_dehydrate: "no")}
167
+ nodes = Nodes.new(limit: nodes["limit"], page: nodes["page"], page_count: nodes["page_count"], nodes_count: nodes["node_count"], payload: response)
168
+ end
169
+
170
+ # Queries Synapse API for all institutions available for bank logins
171
+ # @param page [Integer] (optional) response will default to 1
172
+ # @param per_page [Integer] (optional) response will default to 20
173
+ # @return API response [Hash]
174
+ def get_all_institutions(**options)
175
+ client.get(institutions_path(options))
176
+ end
177
+
178
+ # Queries Synapse API to create a webhook subscriptions for platform
179
+ # @param scope [Array<String>]
180
+ # @param idempotency_key [String] (optional)
181
+ # @param url [String]
182
+ # @see https://docs.synapsefi.com/docs/create-subscription
183
+ # @return [Synapse::Subscription]
184
+ def create_subscriptions(scope:, url:, **options)
185
+ payload = {
186
+ 'scope' => scope,
187
+ 'url' => url,
188
+ }
189
+
190
+ response = client.post(subscriptions_path , payload, options)
191
+
192
+ subscriptions = Subscription.new(subscription_id: response["_id"], url: response["url"], payload: response)
193
+ end
194
+
195
+ # Queries Synapse API for all platform subscriptions
196
+ # @param page [Integer] (optional) response will default to 1
197
+ # @param per_page [Integer] (optional) response will default to 20
198
+ # @return [Array<Synapse::Subscriptions>]
199
+ def get_all_subscriptions(**options)
200
+ subscriptions = client.get(subscriptions_path(options))
201
+
202
+ return [] if subscriptions["subscriptions"].empty?
203
+ response = subscriptions["subscriptions"].map { |subscription_data| Subscription.new(subscription_id: subscription_data["_id"], url: subscription_data["url"], payload: subscription_data)}
204
+ subscriptions = Subscriptions.new(limit: subscriptions["limit"], page: subscriptions["page"], page_count: subscriptions["page_count"], subscriptions_count: subscriptions["subscription_count"], payload: response)
205
+ end
206
+
207
+
208
+ # Queries Synapse API for a subscription by subscription_id
209
+ # @param subscription_id [String]
210
+ # @return [Synapse::Subscription]
211
+ def get_subscription(subscription_id:)
212
+ path = subscriptions_path + "/#{subscription_id}"
213
+ response = client.get(path)
214
+ subscription = Subscription.new(subscription_id: response["_id"], url: response["url"], payload: response)
215
+ end
216
+
217
+ # updates subscription platform subscription
218
+ # @param subscription_id [String]
219
+ # @param is_active [boolean]
220
+ # @param url [String]
221
+ # @param scope [Array<String>]
222
+ # see https://docs.synapsefi.com/docs/update-subscription
223
+ # @return [Synapse::Subscription]
224
+ def update_subscriptions(subscription_id:, url:nil, scope:nil, is_active:nil)
225
+ path = subscriptions_path + "/#{subscription_id}"
226
+
227
+ payload = {}
228
+
229
+ payload["url"] = url if url
230
+ payload["scope"] = scope if scope
231
+ payload["is_active"] = is_active if is_active
232
+
233
+ response = client.patch(path, payload)
234
+ subscriptions = Subscription.new(subscription_id: response["_id"], url: response["url"], payload: response)
235
+ end
236
+
237
+
238
+ # Issues public key for client
239
+ # @param client [Synapse::Client]
240
+ # @param scope [String]
241
+ # @see https://docs.synapsefi.com/docs/issuing-public-key
242
+ # @note valid scope "OAUTH|POST,USERS|POST,USERS|GET,USER|GET,USER|PATCH,SUBSCRIPTIONS|GET,SUBSCRIPTIONS|POST,SUBSCRIPTION|GET,SUBSCRIPTION|PATCH,CLIENT|REPORTS,CLIENT|CONTROLS"
243
+ def issue_public_key(scope:)
244
+ raise ArgumentError, 'scope must be a string' unless scope.is_a?(String)
245
+ path = '/client?issue_public_key=YES'
246
+ path += "&scope=#{scope}"
247
+ response = client.get(path)
248
+ response[ "public_key_obj"]
249
+ end
250
+
251
+ # Queries Synapse API for ATMS nearby
252
+ # @param zip [String]
253
+ # @param radius [String]
254
+ # @param lat [String]
255
+ # @param lon [String]
256
+ # @see https://docs.synapsefi.com/docs/locate-atms
257
+ # @return [Hash]
258
+ def locate_atm(**options)
259
+ params = VALID_QUERY_PARAMS.map do |p|
260
+ options[p] ? "#{p}=#{options[p]}" : nil
261
+ end.compact
262
+
263
+ path = "/nodes/atms?"
264
+ path += params.join('&') if params.any?
265
+ atms = client.get(path)
266
+ atms
267
+ end
268
+
269
+ # Queries Synapse API for Crypto Currencies Quotes
270
+ # @return API response [Hash]
271
+ def get_crypto_quotes()
272
+ path = '/nodes/crypto-quotes'
273
+ params = VALID_QUERY_PARAMS.map do |p|
274
+ options[p] ? "#{p}=#{options[p]}" : nil
275
+ end.compact
276
+
277
+ path += '?' + params.join('&') if params.any?
278
+ quotes = client.get(path)
279
+ quotes
280
+ end
281
+
282
+ # Queries Synapse API for Crypto Currencies Market data
283
+ # @param limit [Integer]
284
+ # @param currency [String]
285
+ # @return API response [Hash]
286
+ def get_crypto_market_data(**options)
287
+ path = '/nodes/crypto-market-watch'
288
+
289
+ params = VALID_QUERY_PARAMS.map do |p|
290
+ options[p] ? "#{p}=#{options[p]}" : nil
291
+ end.compact
292
+
293
+ path += '?' + params.join('&') if params.any?
294
+
295
+ data = client.get(path)
296
+ data
297
+ end
298
+
299
+
300
+ private
301
+
302
+ def user_path(user_id: nil, **options)
303
+ path = "/users"
304
+ path += "/#{user_id}" if user_id
305
+
306
+ params = VALID_QUERY_PARAMS.map do |p|
307
+ options[p] ? "#{p}=#{options[p]}" : nil
308
+ end.compact
309
+
310
+ path += '?' + params.join('&') if params.any?
311
+ path
312
+ end
313
+
314
+ def transactions_path(user_id: nil, node_id: nil, **options)
315
+ path = "/users/#{user_id}/trans"
316
+ params = VALID_QUERY_PARAMS.map do |p|
317
+ options[p] ? "#{p}=#{options[p]}" : nil
318
+ end.compact
319
+
320
+ path += '?' + params.join('&') if params.any?
321
+ path
322
+ end
323
+
324
+ def nodes_path(**options)
325
+ path = "/nodes"
326
+ params = VALID_QUERY_PARAMS.map do |p|
327
+ options[p] ? "#{p}=#{options[p]}" : nil
328
+ end.compact
329
+
330
+ path += '?' + params.join('&') if params.any?
331
+ path
332
+ end
333
+
334
+ def institutions_path(**options)
335
+ path = "/institutions"
336
+ params = VALID_QUERY_PARAMS.map do |p|
337
+ options[p] ? "#{p}=#{options[p]}" : nil
338
+ end.compact
339
+
340
+ path += '?' + params.join('&') if params.any?
341
+ path
342
+ end
343
+
344
+ def subscriptions_path(**options)
345
+ path = "/subscriptions"
346
+ params = VALID_QUERY_PARAMS.map do |p|
347
+ options[p] ? "#{p}=#{options[p]}" : nil
348
+ end.compact
349
+
350
+ path += '?' + params.join('&') if params.any?
351
+ path
352
+ end
353
+ end
354
+ end
355
+
@@ -0,0 +1,138 @@
1
+ module Synapse
2
+ # Custom class for handling HTTP and API errors.
3
+ class Error < StandardError
4
+ # Raised on a 4xx HTTP status code
5
+ ClientError = Class.new(self)
6
+
7
+ # Raised on the HTTP status code 202
8
+ Accepted = Class.new(ClientError)
9
+
10
+ # Raised on the HTTP status code 400
11
+ BadRequest = Class.new(ClientError)
12
+
13
+ # Raised on the HTTP status code 401
14
+ Unauthorized = Class.new(ClientError)
15
+
16
+ # Raised on the HTTP status code 402
17
+ RequestDeclined = Class.new(ClientError)
18
+
19
+ # Raised on the HTTP status code 403
20
+ # Forbidden = Class.new(ClientError)
21
+ # '403' => Synapse::Error::Forbidden,
22
+
23
+ # Raised on the HTTP status code 404
24
+ NotFound = Class.new(ClientError)
25
+
26
+ # Raised on the HTTP status code 406
27
+ # NotAcceptable = Class.new(ClientError)
28
+ # '406' => Synapse::Error::NotAcceptable,
29
+
30
+ # Raised on the HTTP status code 409
31
+ Conflict = Class.new(ClientError)
32
+
33
+ # Raised on the HTTP status code 415
34
+ # UnsupportedMediaType = Class.new(ClientError)
35
+ # '415' => Synapse::Error::UnsupportedMediaType,
36
+
37
+ # Raised on the HTTP status code 422
38
+ # UnprocessableEntity = Class.new(ClientError)
39
+ # '422' => Synapse::Error::UnprocessableEntity,
40
+
41
+ # Raised on the HTTP status code 429
42
+ TooManyRequests = Class.new(ClientError)
43
+
44
+ # Raised on a 5xx HTTP status code
45
+ ServerError = Class.new(self)
46
+
47
+ # Raised on the HTTP status code 500
48
+ InternalServerError = Class.new(ServerError)
49
+
50
+ # Raised on the HTTP status code 502
51
+ # BadGateway = Class.new(ServerError)
52
+ # '502' => Synapse::Error::BadGateway,
53
+
54
+ # Raised on the HTTP status code 503
55
+ ServiceUnavailable = Class.new(ServerError)
56
+
57
+ # Raised on the HTTP status code 504
58
+ # GatewayTimeout = Class.new(ServerError)
59
+ # '504' => Synapse::Error::GatewayTimeout
60
+
61
+ # HTTP status code to Error subclass mapping
62
+ #
63
+ # @todo doesn't do well when there's an html response from nginx for bad gateway/timeout
64
+
65
+ ERRORS = {
66
+ '202' => Synapse::Error::Accepted,
67
+ '400' => Synapse::Error::BadRequest,
68
+ '401' => Synapse::Error::Unauthorized,
69
+ '402' => Synapse::Error::RequestDeclined,
70
+ '404' => Synapse::Error::NotFound,
71
+ '409' => Synapse::Error::Conflict,
72
+ '429' => Synapse::Error::TooManyRequests,
73
+ '500' => Synapse::Error::InternalServerError,
74
+ '503' => Synapse::Error::ServiceUnavailable,
75
+ }.freeze
76
+
77
+ # The SynapsePay API Error Code
78
+ #
79
+ # @return [Integer]
80
+ attr_reader :code, :http_code
81
+
82
+ # The JSON HTTP response in Hash form
83
+ # @return [Hash]
84
+ attr_reader :response, :message
85
+
86
+ class << self
87
+ # Create a new error from an HTTP response
88
+ # @param body [String]
89
+ # @param code [Integer]
90
+ # @param http_code [Integer]
91
+ # @return [Synapse::Error]
92
+ def from_response(body)
93
+ message, error_code, http_code = parse_error(body)
94
+ http_code = http_code.to_s
95
+ klass = ERRORS[http_code] || Synapse::Error
96
+ klass.new(message: message, code: error_code, response: body, http_code: http_code)
97
+ end
98
+
99
+ private
100
+
101
+ def parse_error(body)
102
+
103
+ if body.nil? || body.empty?
104
+ ['', nil, nil]
105
+
106
+ elsif body['mfa'] && body.is_a?(Hash)
107
+ ["#{body['mfa']["message"] } acces_token: #{body['mfa']["access_token"]}", body['error_code'], body['http_code']]
108
+ elsif body[:mfa] && body.is_a?(Hash)
109
+ ["#{body[:mfa][:message] } acces_token: #{body[:mfa][:access_token]}", body[:error_code], body[:http_code]]
110
+
111
+ elsif body['message'] && body.is_a?(Hash)
112
+ [body["message"]["en"], body['error_code'], body['http_code']]
113
+ elsif body[:message] && body.is_a?(Hash)
114
+ [body[:message][:en], body[:error_code], body[:http_code]]
115
+
116
+ elsif body.is_a?(Hash) && body['error'].is_a?(Hash)
117
+ [body['error']['en'], body['error_code'], body['http_code']]
118
+ elsif body.is_a?(Hash) && body[:error].is_a?(Hash)
119
+ [body[:error][:en], body[:error_code], body[:http_code]]
120
+
121
+ end
122
+ end
123
+ end
124
+
125
+ # Initializes a new Error object
126
+ # @param message [Exception, String]
127
+ # @param code [Integer]
128
+ # @param response [Hash]
129
+ # @return [Synapse::Error]
130
+ def initialize(message: '', code: nil, response: {}, http_code:)
131
+ super(message)
132
+ @code = code
133
+ @response = response
134
+ @message = message
135
+ @http_code = http_code
136
+ end
137
+ end
138
+ end