# Twisted/Klein imports from twisted.logger import Logger # Other library imports from json import dumps from simplejson.errors import JSONDecodeError import requests from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend import jwt from random import choices from string import ascii_uppercase # Project imports from settings import settings class Revolut(object): """ Class to handle Revolut API calls. """ def __init__(self): """ Initialise the Revolut object. Set the logger and token. """ self.log = Logger("revolut") self.token = None def setup_auth(self): """ Function to create a new Java Web Token and use it to get a refresh/access token. """ self.create_new_jwt() self.get_access_token() def create_new_jwt(self): """ Create a new Java Web Token. """ payload = { "iss": settings.Revolut.Domain, "sub": settings.Revolut.ClientID, "aud": "https://revolut.com", "exp": int(settings.Revolut.Expiry), } with open(settings.Revolut.PrivateKey, "rb") as f: pem_bytes = f.read() # payload = {jwt_header, jwt_body} private_key = serialization.load_pem_private_key(pem_bytes, password=None, backend=default_backend()) encoded = jwt.encode(payload, private_key, algorithm="RS256") settings.Revolut.JWT = encoded settings.write() def get_access_token(self): """ Get an access token with our JWT. :return: True or False :rtype: bool """ headers = {"Content-Type": "application/x-www-form-urlencoded"} data = { "grant_type": "authorization_code", "code": settings.Revolut.AuthCode, "client_id": settings.Revolut.ClientID, "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion": settings.Revolut.JWT, } r = requests.post(f"{settings.Revolut.Base}/auth/token", data=data, headers=headers) try: parsed = r.json() except JSONDecodeError: self.log.error("Error parsing access token response: {content}", content=r.content) return False if r.status_code == 200: try: settings.Revolut.RefreshToken = parsed["refresh_token"] settings.Revolut.SetupToken = "0" settings.write() self.log.info("Refreshed refresh token") self.token = parsed["access_token"] self.log.info("Refreshed access token") except KeyError: self.log.error(f"Token authorization didn't contain refresh or access token: {parsed}", parsed=parsed) return False else: self.log.error(f"Cannot refresh token: {parsed}", parsed=parsed) return False def get_new_token(self, fail=False): """ Get a new access token with the refresh token. :param fail: whether to exit() if this fails :type fail: bool :return: True or False :rtype: bool """ headers = {"Content-Type": "application/x-www-form-urlencoded"} data = { "grant_type": "refresh_token", "refresh_token": settings.Revolut.RefreshToken, "client_id": settings.Revolut.ClientID, "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion": settings.Revolut.JWT, } r = requests.post(f"{settings.Revolut.Base}/auth/token", data=data, headers=headers) try: parsed = r.json() except JSONDecodeError: if fail: exit() return False if r.status_code == 200: if "access_token" in parsed.keys(): self.token = parsed["access_token"] self.log.info("Refreshed access token") return True else: self.log.error(f"Token refresh didn't contain access token: {parsed}", parsed=parsed) if fail: exit() return False else: self.log.error(f"Cannot refresh token: {parsed}", parsed=parsed) if fail: exit() return False def setup_webhook(self): """ Check the webhook we have set up in Revolut. Set up configured webhook if not already set up. :return: True or False :rtype: bool """ webhook = self.get_webhook() if "url" in webhook.keys(): if webhook["url"] == settings.Revolut.WebhookURL: self.log.info("Webhook exists - skipping setup: {url}", url=webhook["url"]) return True # Webhook already exists self.log.info("Setting up webhook: {url}", url=settings.Revolut.WebhookURL) headers = {"Authorization": f"Bearer {self.token}"} data = {"url": settings.Revolut.WebhookURL} r = requests.post(f"{settings.Revolut.Base}/webhook", data=dumps(data), headers=headers) if r.status_code == 204: self.log.info("Set up webhook: {url}", url=settings.Revolut.WebhookURL) return True else: parsed = r.json() self.log.info("Failed setting up webhook: {parsed}", parsed=parsed) return False def get_webhook(self): """ Get the webhook address active in Revolut. :return: dict of hook with key url or False :rtype: dict or bool """ headers = {"Authorization": f"Bearer {self.token}"} r = requests.get(f"{settings.Revolut.Base}/webhook", headers=headers) if r.status_code == 200: parsed = r.json() return parsed elif r.status_code == 404: return {} else: self.log.error("Cannot get webhook: {content}", r.content) return False def accounts(self): """ Get the details and balances of all Revolut accounts. :return: account details :rtype: dict """ headers = {"Authorization": f"Bearer {self.token}"} r = requests.get(f"{settings.Revolut.Base}/accounts", headers=headers) if r.status_code == 200: return r.json() else: self.log.error("Error getting accounts: {content}", content=r.content) return False def get_total_usd(self): rates = self.money.get_rates_all() accounts = self.accounts() if not accounts: return False total_usd = 0 for account in accounts: if account["currency"] == "USD": total_usd += account["balance"] else: total_usd += account["balance"] / rates[account["currency"]] return total_usd def convert(self, from_account_id, from_currency, to_account_id, to_currency, sell_amount): """ Convert currency. :param sell_currency: currency to sell :param buy_currency: currency to buy :param sell_amount: amount of currency to sell """ reference = "".join(choices(ascii_uppercase, k=5)) headers = {"Authorization": f"Bearer {self.token}"} data = { "from": { "account_id": from_account_id, "currency": from_currency, "amount": sell_amount, }, "to": { "account_id": to_account_id, "currency": to_currency, }, "request_id": reference, } r = requests.post(f"{settings.Revolut.Base}/exchange", headers=headers, data=dumps(data)) if r.status_code == 200: return r.json() else: self.log.error("Error converting balance: {content}", content=r.content) return False def shuffle(self, currency): """ Exchange money in all accounts to the given currency. :param currency: the currency to convert all our funds to """ accounts = self.accounts() # Find given currency account for account in accounts: if account["currency"] == currency: if account["state"] == "active" and account["public"] is True: dest_account = account # Remove this account accounts.remove(dest_account) break for account in accounts: if account["balance"] > 0: self.convert(account["id"], account["currency"], dest_account["id"], dest_account["currency"], account["balance"]) return True