revolut-api 0.1.0

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.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "revolut/api"
5
+
6
+ require "yaml"
7
+
8
+ cfg_path = File.join(File.dirname(__FILE__), "../credentials.yml")
9
+
10
+ if ::File.exists?(cfg_path)
11
+ cfg = YAML.load_file(cfg_path)
12
+
13
+ Revolut::Api.configure do |config|
14
+ config.user_id = cfg["user_id"]
15
+ config.access_token = cfg["access_token"]
16
+
17
+ config.user_agent = cfg["user_agent"]
18
+
19
+ config.device_id = cfg["device_id"]
20
+ config.device_model = cfg["device_model"]
21
+
22
+ config.api_version = cfg["api_version"]
23
+ config.client_version = cfg["client_version"]
24
+
25
+ config.verbose = true
26
+ end
27
+ end
28
+
29
+ # You can add fixtures and/or initialization code here to make experimenting
30
+ # with your gem easier. You can also use a different console, if you like.
31
+
32
+ # (If you use this, don't forget to add pry to your Gemfile!)
33
+ require "pry"
34
+ Pry.config.history.file = File.join(__FILE__, "../.pry_history")
35
+ Pry.start
36
+
37
+ #require "irb"
38
+ #IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ user_id: "foobar-2313221-barfoo-321322-foobar"
2
+ access_token: "foobarbarfoo-token"
3
+ client_version: "5.12.1"
4
+ api_version: "1"
5
+ user_agent: "Revolut/com.revolut.revolut (iPhone; iOS 11.1)"
6
+ device_id: "SOME-DEVICE-ID"
7
+ device_model: "iPhone8,1"
@@ -0,0 +1,55 @@
1
+ require "base64"
2
+ require "json"
3
+ require "securerandom"
4
+
5
+ require "faraday"
6
+ require "faraday_middleware"
7
+
8
+ require "revolut/api/version"
9
+
10
+ require "revolut/api/constants"
11
+ require "revolut/api/errors"
12
+ require "revolut/api/utilities"
13
+ require "revolut/api/configuration"
14
+
15
+ require "revolut/api/response/user"
16
+ require "revolut/api/response/address"
17
+ require "revolut/api/response/wallet"
18
+ require "revolut/api/response/pocket"
19
+ require "revolut/api/response/card_issuer"
20
+ require "revolut/api/response/card"
21
+ require "revolut/api/response/transaction"
22
+ require "revolut/api/response/merchant"
23
+ require "revolut/api/response/quote"
24
+
25
+ require "revolut/api/base"
26
+
27
+ require "revolut/api/private/auth"
28
+ require "revolut/api/private/user"
29
+ require "revolut/api/private/exchange"
30
+ require "revolut/api/private/transactions"
31
+
32
+ require "revolut/api/client"
33
+ require "revolut/api/public/client"
34
+
35
+ require 'revolut/api/railtie' if defined?(Rails)
36
+
37
+ module Revolut
38
+ module Api
39
+ class << self
40
+ attr_writer :configuration
41
+ end
42
+
43
+ def self.configuration
44
+ @configuration ||= ::Revolut::Api::Configuration.new
45
+ end
46
+
47
+ def self.reset
48
+ @configuration = ::Revolut::Api::Configuration.new
49
+ end
50
+
51
+ def self.configure
52
+ yield(configuration)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,124 @@
1
+ module Revolut
2
+ module Api
3
+ class Base
4
+ attr_accessor :host, :configuration, :headers, :memoized
5
+
6
+ def initialize(host: "api.revolut.com", configuration: ::Revolut::Api.configuration)
7
+ self.host = host
8
+
9
+ self.configuration = configuration
10
+
11
+ self.headers = {
12
+ 'Host' => self.host
13
+ }
14
+
15
+ self.memoized = {}
16
+ end
17
+
18
+ include ::Revolut::Api::Errors
19
+
20
+ def to_uri(path)
21
+ "https://#{self.host}/#{path}"
22
+ end
23
+
24
+ def check_configuration!
25
+ %w(user_id access_token user_agent device_id device_model).each do |config_key|
26
+ raise ::Revolut::Api::MissingConfigurationError, "You need to specify the #{config_key.gsub("_", " ")}!" if ::Revolut::Api.configuration.send(config_key).to_s.empty?
27
+ end
28
+ end
29
+
30
+ def quotes(from: [], to: [], endpoint: "quote", options: {})
31
+ from = (from.is_a?(Array) ? from : split_to_array(from)).collect(&:upcase)
32
+ to = (to.is_a?(Array) ? to : split_to_array(to)).collect(&:upcase)
33
+ args = []
34
+
35
+ from.each do |f|
36
+ to.each do |t|
37
+ args << "#{f.to_s.upcase}#{t.to_s.upcase}"
38
+ end
39
+ end
40
+
41
+ params = {symbol: args}
42
+
43
+ options[:params_encoder] = ::Faraday::FlatParamsEncoder
44
+
45
+ response = get(endpoint, params: params, options: options)
46
+ data = []
47
+
48
+ if response && response.is_a?(Array) && response.any?
49
+ response.each do |hash|
50
+ data << ::Revolut::Api::Response::Quote.new(hash)
51
+ end
52
+ end
53
+
54
+ return data
55
+ end
56
+
57
+ def split_to_array(string)
58
+ string.include?(",") ? string.split(",") : [string]
59
+ end
60
+
61
+ def get(path, params: {}, options: {})
62
+ request path, method: :get, params: params, options: options
63
+ end
64
+
65
+ def post(path, params: {}, data: {}, options: {})
66
+ request path, method: :post, params: params, data: data, options: options
67
+ end
68
+
69
+ def patch(path, params: {}, data: {}, options: {})
70
+ request path, method: :patch, params: params, data: data, options: options
71
+ end
72
+
73
+ def request(path, method: :get, params: {}, data: {}, options: {})
74
+ check_configuration! if options.fetch(:check_configuration, true)
75
+
76
+ authenticate = options.fetch(:authenticate, true)
77
+ params_encoder = options.fetch(:params_encoder, nil)
78
+ user_agent = options.fetch(:user_agent, self.configuration.user_agent)
79
+
80
+ self.headers.merge!(user_agent: user_agent) unless user_agent.to_s.empty?
81
+
82
+ opts = {url: to_uri(path)}
83
+ opts.merge!(request: { params_encoder: params_encoder }) unless params_encoder.nil?
84
+
85
+ connection = Faraday.new(opts) do |builder|
86
+ builder.headers = self.headers
87
+
88
+ builder.request :basic_auth, self.configuration.user_id, self.configuration.access_token if authenticate && authable?
89
+ builder.request :json
90
+
91
+ builder.response :json
92
+ builder.response :logger if self.configuration.verbose
93
+
94
+ builder.adapter :net_http
95
+ end
96
+
97
+ response = case method
98
+ when :get
99
+ connection.get do |request|
100
+ request.params = params if params && !params.empty?
101
+ end&.body
102
+ when :post, :patch
103
+ connection.send(method) do |request|
104
+ request.body = data
105
+ request.params = params if params && !params.empty?
106
+ end&.body
107
+ end
108
+
109
+ error?(response)
110
+
111
+ return response
112
+ end
113
+
114
+ def authable?
115
+ !self.configuration.user_id.to_s.empty? && !self.configuration.access_token.to_s.empty?
116
+ end
117
+
118
+ def log(message)
119
+ puts "[Revolut::Api] - #{Time.now}: #{message}" if !message.to_s.empty? && self.configuration.verbose
120
+ end
121
+
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,29 @@
1
+ module Revolut
2
+ module Api
3
+ class Client < ::Revolut::Api::Base
4
+
5
+ def initialize(host: "api.revolut.com", configuration: ::Revolut::Api.configuration)
6
+ super(host: host, configuration: configuration)
7
+
8
+ set_headers
9
+ end
10
+
11
+ def set_headers
12
+ self.headers.merge!({
13
+ 'X-Client-Version' => self.configuration.client_version,
14
+ 'X-Api-Version' => self.configuration.api_version,
15
+ 'X-Device-Id' => self.configuration.device_id,
16
+ 'X-Device-Model' => self.configuration.device_model
17
+ })
18
+
19
+ self.headers.delete_if { |key, value| value.to_s.empty? }
20
+ end
21
+
22
+ include ::Revolut::Api::Private::Auth
23
+ include ::Revolut::Api::Private::User
24
+ include ::Revolut::Api::Private::Exchange
25
+ include ::Revolut::Api::Private::Transactions
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ module Revolut
2
+ module Api
3
+ class Configuration
4
+ attr_accessor :user_id, :access_token, :user_agent
5
+ attr_accessor :device_id, :device_model
6
+ attr_accessor :api_version, :client_version
7
+ attr_accessor :verbose
8
+
9
+ def initialize
10
+ self.user_id = nil
11
+ self.access_token = nil
12
+
13
+ self.user_agent = "Revolut/com.revolut.revolut (iPhone; iOS 11.1)"
14
+
15
+ self.device_id = SecureRandom.uuid
16
+ self.device_model = "iPhone8,1"
17
+
18
+ self.api_version = "1"
19
+ self.client_version = "5.12.1"
20
+
21
+ self.verbose = false
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module Revolut
2
+ module Api
3
+ class Constants
4
+
5
+ CRYPTOS = [
6
+ 'BTC',
7
+ 'LTC',
8
+ 'ETH',
9
+ 'XRP'
10
+ ]
11
+
12
+ BASE_UNITS = {
13
+ fiat: 100.0,
14
+ crypto: 100_000_000.0
15
+ }
16
+
17
+ TIME = {
18
+ epoch_base: 1_000,
19
+ one_month: 2629746
20
+ }
21
+
22
+ PUBLIC_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15"
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ module Revolut
2
+ module Api
3
+ class MissingConfigurationError < StandardError; end
4
+ class AuthorizationError < StandardError; end
5
+
6
+ module Errors
7
+ MAPPING = {
8
+ "The request should be authorized." => -> { raise ::Revolut::Api::AuthorizationError.new("Authorization failed!") },
9
+ }
10
+
11
+ def error?(response)
12
+ if response.is_a?(Hash) && response.has_key?("message")
13
+ message = response.fetch("message", nil)
14
+ ::Revolut::Api::Errors::MAPPING.fetch(message, nil)&.call
15
+ end
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,49 @@
1
+ module Revolut
2
+ module Api
3
+ module Private
4
+ module Auth
5
+
6
+ def signin(phone:, password:)
7
+ data = {
8
+ phone: phone,
9
+ password: password
10
+ }
11
+
12
+ options = {
13
+ check_configuration: false,
14
+ authenticate: false
15
+ }
16
+
17
+ post("signin", data: data, options: options)
18
+ end
19
+
20
+ def confirm_signin(phone:, code:)
21
+ data = {
22
+ phone: phone,
23
+ code: code.gsub("-", "")
24
+ }
25
+
26
+ options = {
27
+ check_configuration: false,
28
+ authenticate: false
29
+ }
30
+
31
+ response = post("signin/confirm", data: data, options: options)
32
+
33
+ auth_data = {id: response&.dig("user", "id"), access_token: response&.fetch("accessToken", nil)}
34
+ auth_data.delete_if { |key, value| value.to_s.empty? }
35
+
36
+ if !auth_data.empty?
37
+ self.configuration.user_id = auth_data.fetch(:id, nil)
38
+ self.configuration.access_token = auth_data.fetch(:access_token, nil)
39
+ else
40
+ auth_data = nil
41
+ end
42
+
43
+ return auth_data
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,91 @@
1
+ module Revolut
2
+ module Api
3
+ module Private
4
+ module Exchange
5
+
6
+ def exchange(from:, to:, amount:, side: :sell)
7
+ status, success = nil, nil
8
+ transactions = []
9
+ error = {}
10
+ from = from.to_s.upcase
11
+ to = to.to_s.upcase
12
+
13
+ if amount.eql?(:all)
14
+ pocket = self.wallet.pocket(from)
15
+
16
+ if pocket
17
+ amount = pocket.balance
18
+ log "Will exchange total balance #{from} to #{to}. Balance: #{amount}."
19
+ end
20
+ end
21
+
22
+ if !from.to_s.empty? && !to.to_s.empty? && !amount.nil?
23
+ log "Will exchange #{amount} #{from} to #{to}"
24
+
25
+ payload = {
26
+ "fromCcy" => from,
27
+ "toCcy" => to,
28
+ "rateTimestamp" => Time.now.utc.to_i,
29
+ }
30
+
31
+ if side.eql?(:sell)
32
+ payload.merge!("fromAmount" => ::Revolut::Api::Utilities.convert_to_integer_amount(from, amount))
33
+ elsif side.eql?(:buy)
34
+ payload.merge!("toAmount" => ::Revolut::Api::Utilities.convert_to_integer_amount(to, amount))
35
+ end
36
+
37
+ data = post("exchange", data: payload.to_json)
38
+
39
+ if data && data.is_a?(Array) && data.any?
40
+ data.each do |response|
41
+ transactions << ::Revolut::Api::Response::Transaction.new(response)
42
+ end
43
+
44
+ complete_count = 0
45
+ pending_count = 0
46
+
47
+ transactions.each do |transaction|
48
+ complete_count += 1 if transaction.completed?
49
+ pending_count += 1 if transaction.pending?
50
+ end
51
+
52
+ success = (complete_count == data.count || pending_count == data.count)
53
+ status = :completed if complete_count == data.count
54
+ status = :pending if pending_count == data.count
55
+
56
+ log "Successfully exchanged #{amount} #{from} to #{to}!" if status.eql?(:completed)
57
+ log "Exchange from #{amount} #{from} to #{to} is currently pending" if status.eql?(:pending)
58
+
59
+ elsif data && data.is_a?(Hash)
60
+ error_message = data.fetch("message", nil)
61
+ error_code = data.fetch("code", nil)
62
+
63
+ status = :failed
64
+ success = false
65
+ error[:message] = error_message if !error_message.to_s.empty?
66
+ error[:code] = error_code if !error_code.to_s.empty?
67
+
68
+ log "Error occurred while trying to exchange #{amount} #{from} to #{to}. Error (#{error_code}): #{error_message}"
69
+ end
70
+ else
71
+ log "Missing some data required for the exchange. From: #{from}. To: #{to}. Amount: #{amount}."
72
+ end
73
+
74
+ return {status: status, success: success, transactions: transactions, error: error}
75
+ end
76
+
77
+ def quote(from:, to:, amount:, side: :sell)
78
+ params = {
79
+ amount: ::Revolut::Api::Utilities.convert_to_integer_amount(from, amount),
80
+ side: side.to_s.upcase
81
+ }
82
+
83
+ endpoint = "quote/#{from.to_s.upcase}#{to.to_s.upcase}"
84
+
85
+ return ::Revolut::Api::Response::Quote.new(get(endpoint, params: params))
86
+ end
87
+
88
+ end
89
+ end
90
+ end
91
+ end