revolut-api 0.1.0

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