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.
- checksums.yaml +7 -0
- data/.gitignore +27 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +64 -0
- data/LICENSE.txt +21 -0
- data/README.md +238 -0
- data/Rakefile +6 -0
- data/bin/console +38 -0
- data/bin/setup +8 -0
- data/credentials.yml.example +7 -0
- data/lib/revolut/api.rb +55 -0
- data/lib/revolut/api/base.rb +124 -0
- data/lib/revolut/api/client.rb +29 -0
- data/lib/revolut/api/configuration.rb +26 -0
- data/lib/revolut/api/constants.rb +26 -0
- data/lib/revolut/api/errors.rb +20 -0
- data/lib/revolut/api/private/auth.rb +49 -0
- data/lib/revolut/api/private/exchange.rb +91 -0
- data/lib/revolut/api/private/transactions.rb +77 -0
- data/lib/revolut/api/private/user.rb +40 -0
- data/lib/revolut/api/public/client.rb +24 -0
- data/lib/revolut/api/railtie.rb +14 -0
- data/lib/revolut/api/response/address.rb +36 -0
- data/lib/revolut/api/response/card.rb +54 -0
- data/lib/revolut/api/response/card_issuer.rb +29 -0
- data/lib/revolut/api/response/merchant.rb +30 -0
- data/lib/revolut/api/response/pocket.rb +33 -0
- data/lib/revolut/api/response/quote.rb +52 -0
- data/lib/revolut/api/response/transaction.rb +67 -0
- data/lib/revolut/api/response/user.rb +50 -0
- data/lib/revolut/api/response/wallet.rb +42 -0
- data/lib/revolut/api/utilities.rb +28 -0
- data/lib/revolut/api/version.rb +5 -0
- data/revolut-api.gemspec +35 -0
- metadata +207 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
data/lib/revolut/api.rb
ADDED
@@ -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
|