Libraries refactor and add some sinks #4

Closed
m wants to merge 136 commits from library-refactor into master
23 changed files with 4384 additions and 378 deletions

4
.gitignore vendored
View File

@ -2,5 +2,9 @@
*.swp *.swp
__pycache__/ __pycache__/
env/ env/
venv/
keys/ keys/
handler/settings.ini handler/settings.ini
handler/otp.key
handler/certs/
.vscode/

View File

@ -1,14 +1,22 @@
# Twisted/Klein imports # Twisted/Klein imports
from twisted.logger import Logger from twisted.logger import Logger
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
from twisted.internet.threads import deferToThread
# Other library imports # Other library imports
from json import loads from json import loads
from forex_python.converter import CurrencyRates from forex_python.converter import CurrencyRates
from agoradesk_py.agoradesk import AgoraDesk from agoradesk_py import AgoraDesk
from pycoingecko import CoinGeckoAPI # TODO: remove this import and defer to money
from time import sleep
from pyotp import TOTP
from datetime import datetime
# Project imports # Project imports
from settings import settings from settings import settings
import util
log = Logger("agora.global")
class Agora(object): class Agora(object):
@ -23,7 +31,8 @@ class Agora(object):
""" """
self.log = Logger("agora") self.log = Logger("agora")
self.agora = AgoraDesk(settings.Agora.Token) self.agora = AgoraDesk(settings.Agora.Token)
self.cr = CurrencyRates() self.cr = CurrencyRates() # TODO: remove this and defer to money
self.cg = CoinGeckoAPI() # TODO: remove this and defer to money
# Cache for detecting new trades # Cache for detecting new trades
self.last_dash = set() self.last_dash = set()
@ -31,11 +40,8 @@ class Agora(object):
# Cache for detecting new messages # Cache for detecting new messages
self.last_messages = {} self.last_messages = {}
def set_irc(self, irc): # Assets that cheat has been run on
self.irc = irc self.cheat_run_on = []
def set_tx(self, tx):
self.tx = tx
def setup_loop(self): def setup_loop(self):
""" """
@ -43,20 +49,36 @@ class Agora(object):
""" """
self.lc_dash = LoopingCall(self.loop_check) self.lc_dash = LoopingCall(self.loop_check)
self.lc_dash.start(int(settings.Agora.RefreshSec)) self.lc_dash.start(int(settings.Agora.RefreshSec))
if settings.Agora.Cheat == "1":
self.lc_cheat = LoopingCall(self.run_cheat_in_thread)
self.lc_cheat.start(int(settings.Agora.CheatSec))
@util.handle_exceptions
def wrap_dashboard(self):
dash = self.agora.dashboard_seller()
if dash is None:
return False
if dash is False:
return False
if dash["response"] is None:
return False
dash_tmp = {}
if not dash.items():
return False
if "data" not in dash["response"].keys():
self.log.error("Data not in dashboard response: {content}", content=dash)
return dash_tmp
if dash["response"]["data"]["contact_count"] > 0:
for contact in dash["response"]["data"]["contact_list"]:
contact_id = contact["data"]["contact_id"]
dash_tmp[contact_id] = contact
return dash_tmp
def loop_check(self): def loop_check(self):
""" """
Calls hooks to parse dashboard info and get all contact messages. Calls hooks to parse dashboard info and get all contact messages.
""" """
dash = self.agora.dashboard_seller() dash_tmp = self.wrap_dashboard()
dash_tmp = {}
if "data" not in dash["response"].keys():
self.log.error("Data not in dashboard response: {content}", content=dash)
return False
if dash["response"]["data"]["contact_count"] > 0:
for contact in dash["response"]["data"]["contact_list"]:
contact_id = contact["data"]["contact_id"]
dash_tmp[contact_id] = contact
# Call dashboard hooks # Call dashboard hooks
self.dashboard_hook(dash_tmp) self.dashboard_hook(dash_tmp)
@ -69,8 +91,25 @@ class Agora(object):
""" """
Get dashboard helper for IRC only. Get dashboard helper for IRC only.
""" """
# dash_tmp.append(f"{reference}: {buyer} {amount}{currency} {amount_xmr}XMR") dash = self.wrap_dashboard()
pass rtrn = []
if dash is False:
return False
for contact_id, contact in dash.items():
reference = self.tx.tx_to_ref(contact_id)
buyer = contact["data"]["buyer"]["username"]
amount = contact["data"]["amount"]
asset = contact["data"]["advertisement"]["asset"]
if asset == "XMR":
amount_crypto = contact["data"]["amount_xmr"]
elif asset == "BTC":
amount_crypto = contact["data"]["amount_btc"]
currency = contact["data"]["currency"]
provider = contact["data"]["advertisement"]["payment_method"]
if not contact["data"]["is_selling"]:
continue
rtrn.append(f"{reference}: {buyer} {amount}{currency} {provider} {amount_crypto}{asset}")
return rtrn
def dashboard_hook(self, dash): def dashboard_hook(self, dash):
""" """
@ -78,23 +117,32 @@ class Agora(object):
Post new trades to IRC and cache trades for the future. Post new trades to IRC and cache trades for the future.
""" """
current_trades = [] current_trades = []
if not dash:
return
if not dash.items():
return
for contact_id, contact in dash.items(): for contact_id, contact in dash.items():
reference = self.tx.tx_to_ref(contact_id) reference = self.tx.tx_to_ref(contact_id)
if reference: if reference:
current_trades.append(reference) current_trades.append(reference)
buyer = contact["data"]["buyer"]["username"] buyer = contact["data"]["buyer"]["username"]
amount = contact["data"]["amount"] amount = contact["data"]["amount"]
amount_xmr = contact["data"]["amount_xmr"] asset = contact["data"]["advertisement"]["asset"]
provider = contact["data"]["advertisement"]["payment_method"]
if asset == "XMR":
amount_crypto = contact["data"]["amount_xmr"]
elif asset == "BTC":
amount_crypto = contact["data"]["amount_btc"]
currency = contact["data"]["currency"] currency = contact["data"]["currency"]
if not contact["data"]["is_selling"]: if not contact["data"]["is_selling"]:
continue continue
if reference not in self.last_dash: if reference not in self.last_dash:
reference = self.tx.new_trade(contact_id, buyer, currency, amount, amount_xmr) reference = self.tx.new_trade(asset, contact_id, buyer, currency, amount, amount_crypto, provider)
if reference: if reference:
if reference not in current_trades: if reference not in current_trades:
current_trades.append(reference) current_trades.append(reference)
# Let us know there is a new trade # Let us know there is a new trade
self.irc.sendmsg(f"AUTO {reference}: {buyer} {amount}{currency} {amount_xmr}XMR") self.irc.sendmsg(f"AUTO {reference}: {buyer} {amount}{currency} {provider} {amount_crypto}{asset}")
# Note that we have seen this reference # Note that we have seen this reference
self.last_dash.add(reference) self.last_dash.add(reference)
@ -106,43 +154,20 @@ class Agora(object):
current_trades.append(reference) current_trades.append(reference)
self.tx.cleanup(current_trades) self.tx.cleanup(current_trades)
def dashboard_release_urls(self): @util.handle_exceptions
"""
Get information about our open trades.
Post new trades to IRC and cache trades for the future.
:return: human readable list of strings about our trades or False
:rtype: list or bool
"""
dash = self.agora.dashboard_seller()
dash_tmp = []
if "data" not in dash["response"]:
self.log.error("Data not in dashboard response: {content}", content=dash)
return False
if dash["response"]["data"]["contact_count"] > 0:
for contact in dash["response"]["data"]["contact_list"]:
contact_id = contact["data"]["contact_id"]
buyer = contact["data"]["buyer"]["username"]
amount = contact["data"]["amount"]
amount_xmr = contact["data"]["amount_xmr"]
currency = contact["data"]["currency"]
release_url = contact["actions"]["release_url"]
if not contact["data"]["is_selling"]:
continue
reference = self.tx.tx_to_ref(contact_id)
if not reference:
reference = "not_set"
dash_tmp.append(f"{reference}: {buyer} {amount}{currency} {amount_xmr}XMR {release_url}")
return dash_tmp
def get_recent_messages(self, send_irc=True): def get_recent_messages(self, send_irc=True):
""" """
Get recent messages. Get recent messages.
""" """
messages_tmp = {} messages_tmp = {}
messages = self.agora.recent_messages() messages = self.agora.recent_messages()
if messages is False:
return False
if not messages["success"]: if not messages["success"]:
return False return False
if "data" not in messages["response"]:
self.log.error("Data not in messages response: {content}", content=messages["response"])
return False
open_tx = self.tx.get_ref_map().keys() open_tx = self.tx.get_ref_map().keys()
for message in messages["response"]["data"]["message_list"]: for message in messages["response"]["data"]["message_list"]:
contact_id = message["contact_id"] contact_id = message["contact_id"]
@ -176,8 +201,11 @@ class Agora(object):
return messages_tmp return messages_tmp
@util.handle_exceptions
def enum_ad_ids(self, page=0): def enum_ad_ids(self, page=0):
ads = self.agora._api_call(api_method="ads", query_values={"page": page}) ads = self.agora._api_call(api_method="ads", query_values={"page": page})
if ads is False:
return False
ads_total = [] ads_total = []
if not ads["success"]: if not ads["success"]:
return False return False
@ -186,24 +214,239 @@ class Agora(object):
if "pagination" in ads["response"]: if "pagination" in ads["response"]:
if "next" in ads["response"]["pagination"]: if "next" in ads["response"]["pagination"]:
page += 1 page += 1
for ad in self.enum_ad_ids(page): ads_iter = self.enum_ad_ids(page)
if ads_iter is None:
return False
if ads_iter is False:
return False
for ad in ads_iter:
ads_total.append(ad) ads_total.append(ad)
return ads_total return ads_total
def enum_ads(self, page=0): @util.handle_exceptions
ads = self.agora._api_call(api_method="ads", query_values={"page": page}) def enum_ads(self, requested_asset=None, page=0):
query_values = {"page": page}
if requested_asset:
query_values["asset"] = requested_asset
ads = self.agora._api_call(api_method="ads", query_values=query_values)
if ads is False:
return False
ads_total = [] ads_total = []
if not ads["success"]: if not ads["success"]:
return False return False
for ad in ads["response"]["data"]["ad_list"]: for ad in ads["response"]["data"]["ad_list"]:
ads_total.append([ad["data"]["ad_id"], ad["data"]["countrycode"], ad["data"]["currency"]]) asset = ad["data"]["asset"]
ad_id = ad["data"]["ad_id"]
country = ad["data"]["countrycode"]
currency = ad["data"]["currency"]
provider = ad["data"]["online_provider"]
ads_total.append([asset, ad_id, country, currency, provider])
if "pagination" in ads["response"]: if "pagination" in ads["response"]:
if "next" in ads["response"]["pagination"]: if "next" in ads["response"]["pagination"]:
page += 1 page += 1
for ad in self.enum_ads(page): ads_iter = self.enum_ads(requested_asset, page)
ads_total.append([ad[0], ad[1], ad[2]]) if ads_iter is None:
return False
if ads_iter is False:
return False
for ad in ads_iter:
ads_total.append([ad[0], ad[1], ad[2], ad[3], ad[4]])
return ads_total return ads_total
@util.handle_exceptions
def enum_public_ads(self, asset, currency, providers=None, page=0):
to_return = []
if asset == "XMR":
coin = "monero"
elif asset == "BTC":
coin = "bitcoins"
if not providers:
providers = ["REVOLUT"]
# buy-monero-online, buy-bitcoin-online
# Work around Agora weirdness calling it bitcoins
if len(providers) == 1:
ads = self.agora._api_call(api_method=f"buy-{coin}-online/{currency}/{providers[0]}", query_values={"page": page})
elif len(providers) > 1:
ads = self.agora._api_call(api_method=f"buy-{coin}-online/{currency}", query_values={"page": page})
# with open("pub.json", "a") as f:
# import json
# f.write(json.dumps([page, currency, asset, ads])+"\n")
# f.close()
if ads is None:
return False
if ads is False:
return False
if ads["response"] is None:
return False
if "data" not in ads["response"]:
return False
for ad in ads["response"]["data"]["ad_list"]:
if ad["data"]["online_provider"] not in providers:
continue
date_last_seen = ad["data"]["profile"]["last_online"]
# Check if this person was seen recently
if not util.last_online_recent(date_last_seen):
continue
ad_id = ad["data"]["ad_id"]
username = ad["data"]["profile"]["username"]
temp_price = ad["data"]["temp_price"]
provider = ad["data"]["online_provider"]
if ad["data"]["currency"] != currency:
continue
to_append = [ad_id, username, temp_price, provider, asset, currency]
if to_append not in to_return:
to_return.append(to_append)
# yield [ad_id, username, temp_price, provider, asset, currency]
if "pagination" in ads["response"]:
if "next" in ads["response"]["pagination"]:
page += 1
ads_iter = self.enum_public_ads(asset, currency, providers, page)
if ads_iter is None:
return False
if ads_iter is False:
return False
for ad in ads_iter:
to_append = [ad[0], ad[1], ad[2], ad[3], ad[4], ad[5]]
if to_append not in to_return:
to_return.append(to_append)
return to_return
def run_cheat_in_thread(self, assets=None):
"""
Update prices in another thread.
"""
if not assets:
all_assets = loads(settings.Agora.AssetList)
assets_not_run = set(all_assets) ^ set(self.cheat_run_on)
if not assets_not_run:
self.cheat_run_on = []
asset = list(all_assets).pop()
self.cheat_run_on.append(asset)
else:
asset = assets_not_run.pop()
self.cheat_run_on.append(asset)
deferToThread(self.update_prices, [asset])
return asset
else:
deferToThread(self.update_prices, assets)
@util.handle_exceptions
def update_prices(self, assets=None):
# Get all public ads for the given assets
public_ads = self.get_all_public_ads(assets)
if not public_ads:
return False
# Get the ads to update
to_update = self.markets.get_new_ad_equations(public_ads, assets)
self.slow_ad_update(to_update)
# TODO: make generic and move to markets
@util.handle_exceptions
def get_all_public_ads(self, assets=None, currencies=None, providers=None):
"""
Get all public ads for our listed currencies.
:return: dict of public ads keyed by currency
:rtype: dict
"""
public_ads = {}
crypto_map = {
"XMR": "monero",
"BTC": "bitcoin",
}
if not assets:
assets = self.markets.get_all_assets()
# Get all currencies we have ads for, deduplicated
if not currencies:
currencies = self.markets.get_all_currencies()
if not providers:
providers = self.markets.get_all_providers()
# We want to get the ads for each of these currencies and return the result
rates = self.cg.get_price(ids=["monero", "bitcoin"], vs_currencies=currencies)
for asset in assets:
for currency in currencies:
cg_asset_name = crypto_map[asset]
try:
rates[cg_asset_name][currency.lower()]
except KeyError:
# self.log.error("Error getting public ads for currency {currency}", currency=currency)
continue
ads_list = self.enum_public_ads(asset, currency, providers)
if not ads_list:
continue
ads = self.money.lookup_rates(ads_list, rates=rates)
if not ads:
continue
self.write_to_es_ads("ads", ads)
if currency in public_ads:
for ad in list(ads):
if ad not in public_ads[currency]:
public_ads[currency].append(ad)
else:
public_ads[currency] = ads
return public_ads
def write_to_es_ads(self, msgtype, ads):
if settings.ES.Enabled == "1":
for ad in ads:
cast = {
"id": ad[0],
"username": ad[1],
"price": ad[2],
"provider": ad[3],
"asset": ad[4],
"currency": ad[5],
"margin": ad[6],
}
cast["type"] = msgtype
cast["ts"] = str(datetime.now().isoformat())
cast["xtype"] = "platorm"
cast["market"] = "agora"
self.es.index(index=settings.ES.MetaIndex, document=cast)
def slow_ad_update(self, ads):
"""
Slow ad equation update utilising exponential backoff in order to guarantee all ads are updated.
:param ads: our list of ads
"""
iterations = 0
throttled = 0
assets = set()
currencies = set()
while not all([x[4] for x in ads]) or iterations == 1000:
for ad_index in range(len(ads)):
ad_id, new_formula, asset, currency, actioned = ads[ad_index]
assets.add(asset)
currencies.add(currency)
if not actioned:
rtrn = self.agora.ad_equation(ad_id, new_formula)
if rtrn["success"]:
ads[ad_index][4] = True
throttled = 0
continue
else:
if "error_code" not in rtrn["response"]["error"]:
self.log.error("Error code not in return for ad {ad_id}: {response}", ad_id=ad_id, response=rtrn["response"])
return
if rtrn["response"]["error"]["error_code"] == 429:
throttled += 1
sleep_time = pow(throttled, float(settings.Agora.SleepExponent))
self.log.info(
"Throttled {x} times while updating {id}, sleeping for {sleep} seconds",
x=throttled,
id=ad_id,
sleep=sleep_time,
)
# We're running in a thread, so this is fine
sleep(sleep_time)
self.log.error("Error updating ad {ad_id}: {response}", ad_id=ad_id, response=rtrn["response"])
continue
iterations += 1
@util.handle_exceptions
def nuke_ads(self): def nuke_ads(self):
""" """
Delete all of our adverts. Delete all of our adverts.
@ -212,46 +455,20 @@ class Agora(object):
""" """
ads = self.enum_ad_ids() ads = self.enum_ad_ids()
return_ids = [] return_ids = []
if not ads: if ads is False:
return False return False
for ad_id in ads: for ad_id in ads:
rtrn = self.agora.ad_delete(ad_id) rtrn = self.agora.ad_delete(ad_id)
return_ids.append(rtrn["success"]) return_ids.append(rtrn["success"])
return all(return_ids) return all(return_ids)
def get_rates_all(self): @util.handle_exceptions
def create_ad(self, asset, countrycode, currency, provider, edit=False, ad_id=None):
""" """
Get all rates that pair with USD. Post an ad with the given asset in a country with a given currency.
:return: dictionary of USD/XXX rates
:rtype: dict
"""
rates = self.cr.get_rates("USD")
return rates
def get_acceptable_margins(self, currency, amount):
"""
Get the minimum and maximum amounts we would accept a trade for.
:param currency: currency code
:param amount: amount
:return: (min, max)
:rtype: tuple
"""
rates = self.get_rates_all()
if currency == "USD":
min_amount = amount - float(settings.Agora.AcceptableUSDMargin)
max_amount = amount + float(settings.Agora.AcceptableUSDMargin)
return (min_amount, max_amount)
amount_usd = amount / rates[currency]
min_usd = amount_usd - float(settings.Agora.AcceptableUSDMargin)
max_usd = amount_usd + float(settings.Agora.AcceptableUSDMargin)
min_local = min_usd * rates[currency]
max_local = max_usd * rates[currency]
return (min_local, max_local)
def create_ad(self, countrycode, currency):
"""
Post an ad in a country with a given currency.
Convert the min and max amounts from settings to the given currency with CurrencyRates. Convert the min and max amounts from settings to the given currency with CurrencyRates.
:param asset: the crypto asset to list (XMR or BTC)
:type asset: string
:param countrycode: country code :param countrycode: country code
:param currency: currency code :param currency: currency code
:type countrycode: string :type countrycode: string
@ -260,90 +477,95 @@ class Agora(object):
:rtype: dict :rtype: dict
""" """
ad = settings.Agora.Ad ad = settings.Agora.Ad
paymentdetails = settings.Agora.PaymentDetails
# Substitute the currency
ad = ad.replace("$CURRENCY$", currency) ad = ad.replace("$CURRENCY$", currency)
rates = self.get_rates_all() if currency == "GBP":
if currency == "USD": ad = ad.replace("$PAYMENT$", settings.Agora.GBPDetailsAd)
min_amount = float(settings.Agora.MinUSD) paymentdetailstext = paymentdetails.replace("$PAYMENT$", settings.Agora.GBPDetailsPayment)
max_amount = float(settings.Agora.MaxUSD)
else: else:
min_amount = rates[currency] * float(settings.Agora.MinUSD) ad = ad.replace("$PAYMENT$", settings.Agora.DefaultDetailsAd)
max_amount = rates[currency] * float(settings.Agora.MaxUSD) paymentdetailstext = paymentdetails.replace("$PAYMENT$", settings.Agora.DefaultDetailsPayment)
price_formula = f"coingeckoxmrusd*usd{currency.lower()}*{settings.Agora.Margin}"
# price_formula = f"coingeckoxmrusd*{settings.Agora.Margin}" # Substitute the asset
ad = settings.Agora.Ad ad = ad.replace("$ASSET$", asset)
rates = self.money.get_rates_all()
if asset == "XMR":
min_usd = float(settings.Agora.MinUSDXMR)
max_usd = float(settings.Agora.MaxUSDXMR)
elif asset == "BTC":
min_usd = float(settings.Agora.MinUSDBTC)
max_usd = float(settings.Agora.MaxUSDBTC)
if currency == "USD":
min_amount = min_usd
max_amount = max_usd
else:
min_amount = rates[currency] * min_usd
max_amount = rates[currency] * max_usd
price_formula = f"coingecko{asset.lower()}usd*usd{currency.lower()}*{settings.Agora.Margin}"
# Remove extra tabs
ad = ad.replace("\\t", "\t") ad = ad.replace("\\t", "\t")
ad = self.agora.ad_create( form = {
country_code=countrycode, "country_code": countrycode,
currency=currency, "currency": currency,
trade_type="ONLINE_SELL", "trade_type": "ONLINE_SELL",
asset="XMR", "asset": asset,
price_equation=price_formula, "price_equation": price_formula,
track_max_amount=False, "track_max_amount": False,
require_trusted_by_advertiser=False, "require_trusted_by_advertiser": False,
# verified_email_required = False, "online_provider": provider,
online_provider="REVOLUT", "msg": ad,
msg=settings.Agora.Ad, "min_amount": min_amount,
min_amount=min_amount, "max_amount": max_amount,
max_amount=max_amount, "payment_method_details": settings.Agora.PaymentMethodDetails,
payment_method_details=settings.Agora.PaymentMethodDetails, "account_info": paymentdetailstext,
# require_feedback_score = 0, }
account_info=settings.Agora.PaymentDetails, if edit:
) ad = self.agora.ad(ad_id=ad_id, **form)
else:
ad = self.agora.ad_create(**form)
return ad return ad
def dist_countries(self): def dist_countries(self, filter_asset=None):
""" """
Distribute our advert into all countries listed in the config. Distribute our advert into all countries and providers listed in the config.
Exits on errors. Exits on errors.
:return: False or dict with response :return: False or dict with response
:rtype: bool or dict :rtype: bool or dict
""" """
for currency, countrycode in loads(settings.Agora.DistList): dist_list = list(self.markets.create_distribution_list(filter_asset))
rtrn = self.create_ad(countrycode, currency) our_ads = self.enum_ads()
if not rtrn: # Let's get rid of the ad IDs and make it a tuple like dist_list
our_ads = [(x[0], x[2], x[3], x[4]) for x in our_ads]
for asset, countrycode, currency, provider in dist_list:
if (asset, countrycode, currency, provider) not in our_ads:
# Create the actual ad and pass in all the stuff
rtrn = self.create_ad(asset, countrycode, currency, provider)
# Bail on first error, let's not continue
if rtrn is False:
return False return False
yield rtrn yield rtrn
def get_combinations(self): def redist_countries(self):
""" """
Get all combinations of currencies and countries from the configuration. Redistribute our advert details into all our listed adverts.
:return: list of [country, currency] This will edit all ads and update the details. Only works if we have already run dist.
:rtype: list This will not post any new ads.
""" Exits on errors.
currencies = loads(settings.Agora.BruteCurrencies)
countries = loads(settings.Agora.BruteCountries)
combinations = [[country, currency] for country in countries for currency in currencies]
return combinations
def dist_bruteforce(self):
"""
Bruteforce all possible ads from the currencies and countries in the config.
Does not exit on errors.
:return: False or dict with response :return: False or dict with response
:rtype: bool or dict :rtype: bool or dict
""" """
combinations = self.get_combinations() our_ads = self.enum_ads()
for country, currency in combinations: for asset, ad_id, countrycode, currency, provider in our_ads:
rtrn = self.create_ad(country, currency) rtrn = self.create_ad(asset, countrycode, currency, provider, edit=True, ad_id=ad_id)
if not rtrn: # Bail on first error, let's not continue
yield False if rtrn is False:
yield rtrn return False
yield (rtrn, ad_id)
def bruteforce_fill_blanks(self):
"""
Get the ads that we want to configure but have not, and fill in the blanks.
:return: False or dict with response
:rtype: bool or dict
"""
existing_ads = self.enum_ads()
combinations = self.get_combinations()
for country, currency in combinations:
if not [country, currency] in existing_ads:
rtrn = self.create_ad(country, currency)
if not rtrn:
yield False
yield rtrn
@util.handle_exceptions
def strip_duplicate_ads(self): def strip_duplicate_ads(self):
""" """
Remove duplicate ads. Remove duplicate ads.
@ -365,6 +587,7 @@ class Agora(object):
actioned.append(rtrn["success"]) actioned.append(rtrn["success"])
return all(actioned) return all(actioned)
@util.handle_exceptions
def release_funds(self, contact_id): def release_funds(self, contact_id):
""" """
Release funds for a contact_id. Release funds for a contact_id.
@ -375,4 +598,91 @@ class Agora(object):
""" """
payload = {"tradeId": contact_id, "password": settings.Agora.Pass} payload = {"tradeId": contact_id, "password": settings.Agora.Pass}
rtrn = self.agora._api_call(api_method=f"contact_release/{contact_id}", http_method="POST", query_values=payload) rtrn = self.agora._api_call(api_method=f"contact_release/{contact_id}", http_method="POST", query_values=payload)
# Check if we can withdraw funds
self.withdraw_funds()
return rtrn return rtrn
# TODO: write test before re-enabling adding total_trades
@util.handle_exceptions
def withdraw_funds(self):
"""
Withdraw excess funds to our XMR wallets.
"""
totals_all = self.tx.get_total()
print("totals_all", totals_all)
if totals_all is False:
return False
wallet_xmr, _ = totals_all[2]
print("wallet_xmr", wallet_xmr)
# Get the wallet balances in USD
total_usd = totals_all[0][1]
print("total_usd", total_usd)
total_trades_usd = self.tx.get_open_trades_usd()
print("UNUSED total_trades_usd", total_trades_usd)
if not total_usd:
return False
# total_usd += total_trades_usd
# print("total_usd after trades add", total_usd)
profit_usd = total_usd - float(settings.Money.BaseUSD)
print("profit_usd", profit_usd)
# Get the XMR -> USD exchange rate
xmr_usd = self.cg.get_price(ids="monero", vs_currencies=["USD"])
print("xmr_usd", xmr_usd)
# Convert the USD total to XMR
profit_usd_in_xmr = float(profit_usd) / xmr_usd["monero"]["usd"]
print("profit_usd_in_xmr", profit_usd_in_xmr)
# Check profit is above zero
if not profit_usd >= 0:
return
if not float(wallet_xmr) > profit_usd_in_xmr:
# Not enough funds to withdraw
self.log.error(
"Not enough funds to withdraw {profit}, as wallet only contains {wallet}", profit=profit_usd_in_xmr, wallet=wallet_xmr
)
self.irc.sendmsg(f"Not enough funds to withdraw {profit_usd_in_xmr}, as wallet only contains {wallet_xmr}")
return
if not profit_usd >= float(settings.Money.WithdrawLimit):
# Not enough profit to withdraw
return
half = profit_usd_in_xmr / 2
print("half", half)
half_rounded = round(half, 8)
print("half_rounded", half_rounded)
# Read OTP secret
with open("otp.key", "r") as f:
otp_key = f.read()
f.close()
otp_key = otp_key.replace("\n", "")
# Get OTP code
otp_code = TOTP(otp_key)
# Set up the format for calling wallet_send_xmr
send_cast = {
"address": None,
"amount": half_rounded,
"password": settings.Agora.Pass,
"otp": otp_code.now(),
}
send_cast["address"] = settings.XMR.Wallet1
rtrn1 = self.agora.wallet_send_xmr(**send_cast)
send_cast["address"] = settings.XMR.Wallet2
rtrn2 = self.agora.wallet_send_xmr(**send_cast)
self.irc.sendmsg(f"Withdrawal: {rtrn1['success']} | {rtrn2['success']}")
self.notify.notify_withdrawal(half_rounded)

1019
handler/agoradesk_py.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,19 +2,44 @@
# Twisted/Klein imports # Twisted/Klein imports
from twisted.logger import Logger from twisted.logger import Logger
from twisted.internet import reactor from twisted.internet import reactor
from twisted.internet.task import LoopingCall, deferLater
from klein import Klein from klein import Klein
# Other library imports # Other library imports
from json import dumps, loads from json import dumps, loads
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from signal import signal, SIGINT
# Project imports # Project imports
from settings import settings from settings import settings
import util
from revolut import Revolut from revolut import Revolut
from agora import Agora from agora import Agora
from transactions import Transactions from transactions import Transactions
from irc import bot from irc import bot
from notify import Notify
from markets import Markets
from money import Money
from sinks.nordigen import Nordigen
from sinks.truelayer import TrueLayer
from sinks.fidor import Fidor
init_map = None
# TODO: extend this with more
def cleanup(sig, frame):
if init_map:
try:
init_map["tx"].lc_es_checks.stop()
init_map["agora"].lc_dash.stop()
init_map["agora"].lc_cheat.stop()
except: # noqa
pass # noqa
reactor.stop()
signal(SIGINT, cleanup) # Handle Ctrl-C and run the cleanup routine
def convert(data): def convert(data):
@ -37,9 +62,6 @@ class WebApp(object):
def __init__(self): def __init__(self):
self.log = Logger("webapp") self.log = Logger("webapp")
def set_tx(self, tx):
self.tx = tx
@app.route("/callback", methods=["POST"]) @app.route("/callback", methods=["POST"])
def callback(self, request): def callback(self, request):
content = request.content.read() content = request.content.read()
@ -49,50 +71,62 @@ class WebApp(object):
self.log.error("Failed to parse JSON callback: {content}", content=content) self.log.error("Failed to parse JSON callback: {content}", content=content)
return dumps(False) return dumps(False)
self.log.info("Callback received: {parsed}", parsed=parsed["data"]["id"]) self.log.info("Callback received: {parsed}", parsed=parsed["data"]["id"])
self.tx.transaction(parsed) # self.tx.transaction(parsed)
return dumps(True) return dumps(True)
# set up another connection to a bank
@app.route("/signin", methods=["GET"])
def signin(self, request):
auth_url = self.truelayer.create_auth_url()
return f'Please sign in <a href="{auth_url}" target="_blank">here.</a>'
# endpoint called after we finish setting up a connection above
@app.route("/callback-truelayer", methods=["POST"])
def signin_callback(self, request):
code = request.args[b"code"]
self.truelayer.handle_authcode_received(code)
return dumps(True)
@app.route("/accounts", methods=["GET"])
def balance(self, request):
accounts = self.truelayer.get_accounts()
return dumps(accounts, indent=2)
if __name__ == "__main__": if __name__ == "__main__":
# Define IRC and Agora init_map = {
irc = bot() "notify": Notify(),
agora = Agora() "irc": bot(),
"agora": Agora(),
"markets": Markets(),
"revolut": Revolut(),
"nordigen": Nordigen(),
"truelayer": TrueLayer(),
"fidor": Fidor(),
"tx": Transactions(),
"webapp": WebApp(),
"money": Money(),
}
# Merge all classes into each other
util.xmerge_attrs(init_map)
# Pass IRC to Agora and Agora to IRC # Setup the authcode -> refresh token and refresh_token -> auth_token stuff
# This is to prevent recursive dependencies # util.setup_call_loops(
agora.set_irc(irc) # token_setting=settings.Revolut.SetupToken,
irc.set_agora(agora) # function_init=init_map["revolut"].setup_auth,
# function_continuous=init_map["revolut"].get_new_token,
# delay=int(settings.Revolut.RefreshSec),
# function_post_start=init_map["revolut"].setup_webhook,
# )
# util.setup_call_loops(
# token_setting=settings.TrueLayer.SetupToken,
# function_init=init_map["truelayer"].setup_auth,
# function_continuous=init_map["truelayer"].get_new_token,
# delay=int(settings.TrueLayer.RefreshSec),
# )
# Define Revolut # Set up the loops to put data in ES
revolut = Revolut() init_map["tx"].setup_loops()
# Pass IRC to Revolut and Revolut to IRC
revolut.set_irc(irc)
irc.set_revolut(revolut)
revolut.set_agora(agora)
# Define Transactions
tx = Transactions()
# Pass Agora and IRC to Transactions and Transactions to IRC
tx.set_agora(agora)
tx.set_irc(irc)
irc.set_tx(tx)
agora.set_tx(tx)
# Define WebApp
webapp = WebApp()
webapp.set_tx(tx)
# Handle setting up JWT and request_token from an auth code
if settings.Revolut.SetupToken == "1":
deferLater(reactor, 1, revolut.setup_auth)
else:
# Schedule refreshing the access token using the refresh token
deferLater(reactor, 1, revolut.get_new_token, True)
# Check if the webhook is set up and set up if not
deferLater(reactor, 4, revolut.setup_webhook)
# Schedule repeatedly refreshing the access token
lc = LoopingCall(revolut.get_new_token)
lc.start(int(settings.Revolut.RefreshSec))
# Run the WebApp # Run the WebApp
webapp.app.run("127.0.0.1", 8080) init_map["webapp"].app.run(settings.App.BindHost, 8080)

View File

@ -1,5 +1,8 @@
# Other library imports # Other library imports
from json import dumps from json import dumps, loads
# Project imports
from settings import settings
class IRCCommands(object): class IRCCommands(object):
@ -9,14 +12,14 @@ class IRCCommands(object):
helptext = "Get all open trades." helptext = "Get all open trades."
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
""" """
Get details of open trades and post on IRC. Get details of open trades and post on IRC.
""" """
# Send IRC - we don't want to automatically send messages on IRC, even though # Send IRC - we don't want to automatically send messages on IRC, even though
# this variable seems counter-intuitive here, we are doing something with the result # this variable seems counter-intuitive here, we are doing something with the result
# then calling msg() ourselves, and we don't want extra spam in the channel. # then calling msg() ourselves, and we don't want extra spam in the channel.
trades = agora.dashboard(send_irc=False) trades = agora.get_dashboard()
if not trades: if not trades:
msg("No open trades.") msg("No open trades.")
return return
@ -26,18 +29,34 @@ class IRCCommands(object):
class create(object): class create(object):
name = "create" name = "create"
authed = True authed = True
helptext = "Create an ad. Usage: create <country> <currency>" helptext = "Create an ad. Usage: create <XMR/BTC> <country> <currency> [<provider>]"
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
""" """
Post an ad on AgoraDesk with the given country and currency code. Post an ad on AgoraDesk with the given country and currency code.
""" """
posted = agora.create_ad(spl[1], spl[2]) if length == 4:
if spl[1] not in loads(settings.Agora.AssetList):
msg(f"Not a valid asset: {spl[1]}")
return
posted = agora.create_ad(spl[1], spl[2], spl[3], "REVOLUT")
if posted["success"]: if posted["success"]:
msg(f"{posted['response']['data']['message']}: {posted['response']['data']['ad_id']}") msg(f"{posted['response']['data']['message']}: {posted['response']['data']['ad_id']}")
else: else:
msg(posted["response"]["data"]["message"]) msg(dumps(posted["response"]))
elif length == 5:
if spl[1] not in loads(settings.Agora.AssetList):
msg(f"Not a valid asset: {spl[1]}")
return
if spl[4] not in loads(settings.Agora.ProviderList):
msg(f"Not a valid provider: {spl[4]}")
return
posted = agora.create_ad(spl[1], spl[2], spl[3], spl[4])
if posted["success"]:
msg(f"{posted['response']['data']['message']}: {posted['response']['data']['ad_id']}")
else:
msg(dumps(posted["response"]))
class messages(object): class messages(object):
name = "messages" name = "messages"
@ -45,7 +64,7 @@ class IRCCommands(object):
helptext = "Get messages. Usage: messages [<reference>]" helptext = "Get messages. Usage: messages [<reference>]"
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
""" """
Get all messages for all open trades or a given trade. Get all messages for all open trades or a given trade.
""" """
@ -76,42 +95,40 @@ class IRCCommands(object):
class dist(object): class dist(object):
name = "dist" name = "dist"
authed = True authed = True
helptext = "Distribute all our chosen currency and country ad pairs." helptext = "Distribute all our chosen currency and country ad pairs. Usage: dist [<XMR/BTC>]"
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
# Distribute out our ad to all countries in the config # Distribute out our ad to all countries in the config
if length == 2:
asset = spl[1]
if asset not in loads(settings.Agora.AssetList):
msg(f"Not a valid asset: {spl[1]}")
return
for x in agora.dist_countries(filter_asset=asset):
if x["success"]:
msg(f"{x['response']['data']['message']}: {x['response']['data']['ad_id']}")
else:
msg(dumps(x["response"]))
elif length == 1:
for x in agora.dist_countries(): for x in agora.dist_countries():
if x["success"]: if x["success"]:
msg(f"{x['response']['data']['message']}: {x['response']['data']['ad_id']}") msg(f"{x['response']['data']['message']}: {x['response']['data']['ad_id']}")
else: else:
msg(x["response"]["data"]["message"]) msg(dumps(x["response"]))
class brute(object): class redist(object):
name = "brute" name = "redist"
authed = True authed = True
helptext = "Use a bruteforce algorithm to create all possible currency and country pairs." helptext = "Update all ads with details."
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
for x in agora.dist_bruteforce(): for x in agora.redist_countries():
if x["success"]: if x[0]["success"]:
msg(f"{x['response']['data']['message']}: {x['response']['data']['ad_id']}") msg(f"{x[0]['response']['data']['message']}: {x[1]}")
else: else:
msg(dumps(x)) msg(dumps(x[0]["response"]))
class fillblanks(object):
name = "fillblanks"
authed = True
helptext = "Resume a run of brute by getting all our adverts then filling the blanks."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx):
for x in agora.bruteforce_fill_blanks():
if x["success"]:
msg(f"{x['response']['data']['message']}: {x['response']['data']['ad_id']}")
else:
msg(dumps(x))
class stripdupes(object): class stripdupes(object):
name = "stripdupes" name = "stripdupes"
@ -119,39 +136,17 @@ class IRCCommands(object):
helptext = "Remove all duplicate adverts." helptext = "Remove all duplicate adverts."
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
rtrn = agora.strip_duplicate_ads() rtrn = agora.strip_duplicate_ads()
msg(dumps(rtrn)) msg(dumps(rtrn))
class find(object):
name = "find"
authed = True
helptext = "Find a transaction. Usage: find <currency> <amount>"
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx):
"""
Find a transaction received by Revolut with the given reference and amount.
"""
try:
int(spl[2])
except ValueError:
msg("Amount is not an integer.")
rtrn = tx.find_tx(spl[1], spl[2])
if rtrn == "AMOUNT_INVALID":
msg("Reference found but amount invalid.")
elif not rtrn:
msg("Reference not found.")
else:
return dumps(rtrn)
class accounts(object): class accounts(object):
name = "accounts" name = "accounts"
authed = True authed = True
helptext = "Get all account information from Revolut." helptext = "Get all account information from Revolut."
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
accounts = revolut.accounts() accounts = revolut.accounts()
accounts_posted = 0 accounts_posted = 0
if accounts is None: if accounts is None:
@ -167,18 +162,31 @@ class IRCCommands(object):
if accounts_posted == 0: if accounts_posted == 0:
msg("No accounts with balances.") msg("No accounts with balances.")
class total(object): class balance(object):
name = "total" name = "balance"
authed = True authed = True
helptext = "Get total account balance from Revolut in USD." helptext = "Get total account balance from Revolut in USD."
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
total_usd = revolut.get_total_usd() total_usd = revolut.get_total_usd()
if total_usd is False: if total_usd is False:
msg("Error getting total balance.") msg("Error getting total balance.")
msg(f"Total: {round(total_usd, 2)}USD") msg(f"Total: {round(total_usd, 2)}USD")
class total(object):
name = "total"
authed = True
helptext = "Get total account balance from Revolut and Agora."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
totals_all = tx.get_total()
totals = totals_all[0]
wallets = totals_all[1]
msg(f"Totals: SEK: {totals[0]} | USD: {totals[1]} | GBP: {totals[2]}")
msg(f"Wallets: XMR USD: {wallets[0]} | BTC USD: {wallets[1]}")
class ping(object): class ping(object):
name = "ping" name = "ping"
authed = False authed = False
@ -188,18 +196,14 @@ class IRCCommands(object):
def run(cmd, spl, length, authed, msg): def run(cmd, spl, length, authed, msg):
msg("Pong!") msg("Pong!")
class release_url(object): class summon(object):
name = "release_url" name = "summon"
authed = True authed = True
helptext = "Get release URL for all open trades." helptext = "Summon all operators."
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
trades = agora.dashboard_release_urls() notify.sendmsg("You have been summoned!")
if not trades:
msg("No trades.")
for trade in trades:
msg(trade)
class message(object): class message(object):
name = "msg" name = "msg"
@ -207,7 +211,7 @@ class IRCCommands(object):
helptext = "Send a message on a trade. Usage: msg <reference> <message...>" helptext = "Send a message on a trade. Usage: msg <reference> <message...>"
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
if length > 2: if length > 2:
full_msg = " ".join(spl[2:]) full_msg = " ".join(spl[2:])
reference = tx.ref_to_tx(spl[1]) reference = tx.ref_to_tx(spl[1])
@ -223,7 +227,7 @@ class IRCCommands(object):
helptext = "List all references" helptext = "List all references"
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
msg(f"References: {', '.join(tx.get_refs())}") msg(f"References: {', '.join(tx.get_refs())}")
class ref(object): class ref(object):
@ -232,7 +236,7 @@ class IRCCommands(object):
helptext = "Get more information about a reference. Usage: ref <reference>" helptext = "Get more information about a reference. Usage: ref <reference>"
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
if length == 2: if length == 2:
ref_data = tx.get_ref(spl[1]) ref_data = tx.get_ref(spl[1])
if not ref_data: if not ref_data:
@ -246,7 +250,7 @@ class IRCCommands(object):
helptext = "Delete a reference. Usage: del <reference>" helptext = "Delete a reference. Usage: del <reference>"
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
if length == 2: if length == 2:
ref_data = tx.get_ref(spl[1]) ref_data = tx.get_ref(spl[1])
if not ref_data: if not ref_data:
@ -261,14 +265,16 @@ class IRCCommands(object):
helptext = "Release funds for a trade. Usage: release <reference>" helptext = "Release funds for a trade. Usage: release <reference>"
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
if length == 2: if length == 2:
tx = tx.ref_to_tx(spl[1]) tx = tx.ref_to_tx(spl[1])
if not tx: if not tx:
msg(f"No such reference: {spl[1]}") msg(f"No such reference: {spl[1]}")
return return
rtrn = agora.release_funds(tx) rtrn = agora.release_funds(tx)
msg(dumps(rtrn)) message = rtrn["message"]
message_long = rtrn["response"]["data"]["message"]
msg(f"{message} - {message_long}")
class nuke(object): class nuke(object):
name = "nuke" name = "nuke"
@ -276,20 +282,216 @@ class IRCCommands(object):
helptext = "Delete all our adverts." helptext = "Delete all our adverts."
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
rtrn = agora.nuke_ads() rtrn = agora.nuke_ads()
msg(dumps(rtrn)) msg(dumps(rtrn))
class wallet(object): class wallet(object):
name = "wallet" name = "wallet"
authed = True authed = True
helptext = "Get Agora wallet balance in XMR." helptext = "Get Agora wallet balances."
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
rtrn = agora.agora.wallet_balance_xmr() rtrn_xmr = agora.agora.wallet_balance_xmr()
if not rtrn["success"]: if not rtrn_xmr["success"]:
msg("Error getting wallet details.") msg("Error getting XMR wallet details.")
return return
balance = rtrn["response"]["data"]["total"]["balance"] rtrn_btc = agora.agora.wallet_balance()
msg(f"Wallet balance: {balance}XMR") if not rtrn_btc["success"]:
msg("Error getting BTC wallet details.")
return
balance_xmr = rtrn_xmr["response"]["data"]["total"]["balance"]
balance_btc = rtrn_btc["response"]["data"]["total"]["balance"]
msg(f"XMR wallet balance: {balance_xmr}")
msg(f"BTC wallet balance: {balance_btc}")
class pubads(object):
name = "pubads"
authed = True
helptext = "View public adverts. Usage: pubads <XMR/BTC> <currency> [<provider,...>]"
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
if length == 3:
asset = spl[1]
if asset not in loads(settings.Agora.AssetList):
msg(f"Not a valid asset: {spl[1]}")
return
currency = spl[2]
rtrn = agora.get_all_public_ads(assets=[asset], currencies=[currency])
if not rtrn:
msg("No results.")
return
for ad in rtrn[currency]:
msg(f"({ad[0]}) {ad[1]} {ad[2]} {ad[3]} {ad[4]} {ad[5]} {ad[6]}")
elif length == 4:
asset = spl[1]
if asset not in loads(settings.Agora.AssetList):
msg(f"Not a valid asset: {spl[1]}")
return
providers = spl[3].split(",")
currency = spl[2]
rtrn = agora.get_all_public_ads(assets=[asset], currencies=[currency], providers=providers)
if not rtrn:
msg("No results.")
return
for ad in rtrn[currency]:
msg(f"({ad[0]}) {ad[1]} {ad[2]} {ad[3]} {ad[4]} {ad[5]} {ad[6]}")
class cheat(object):
name = "cheat"
authed = True
helptext = "Cheat the markets by manipulating our prices to exploit people. Usage: cheat [<XMR/BTC>]"
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
if length == 1:
agora.run_cheat_in_thread()
msg("Running cheat in thread.")
elif length == 2:
asset = spl[1]
if asset not in loads(settings.Agora.AssetList):
msg(f"Not a valid asset: {spl[1]}")
return
agora.run_cheat_in_thread([asset])
msg(f"Running cheat in thread for {asset}.")
class cheatnext(object):
name = "cheatnext"
authed = True
helptext = "Run the next currency for cheat."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
if length == 1:
asset = agora.run_cheat_in_thread()
msg(f"Running next asset for cheat in thread: {asset}")
class ads(object):
name = "ads"
authed = True
helptext = "Get all our ad regions"
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
ads = agora.enum_ads()
for ad in ads:
msg(f"({ad[0]}) {ad[1]} {ad[2]} {ad[3]} {ad[4]}")
class xmr(object):
name = "xmr"
authed = True
helptext = "Get current XMR price."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
xmr_prices = agora.cg.get_price(ids="monero", vs_currencies=["sek", "usd", "gbp"])
price_sek = xmr_prices["monero"]["sek"]
price_usd = xmr_prices["monero"]["usd"]
price_gbp = xmr_prices["monero"]["gbp"]
msg(f"SEK: {price_sek} | USD: {price_usd} | GBP: {price_gbp}")
class btc(object):
name = "btc"
authed = True
helptext = "Get current BTC price."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
xmr_prices = agora.cg.get_price(ids="bitcoin", vs_currencies=["sek", "usd", "gbp"])
price_sek = xmr_prices["bitcoin"]["sek"]
price_usd = xmr_prices["bitcoin"]["usd"]
price_gbp = xmr_prices["bitcoin"]["gbp"]
msg(f"SEK: {price_sek} | USD: {price_usd} | GBP: {price_gbp}")
class withdraw(object):
name = "withdraw"
authed = True
helptext = "Take profit."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
agora.withdraw_funds()
class shuffle(object):
name = "shuffle"
authed = True
helptext = "Convert all currencies in Revolut to supplied one. Usage: shuffle <currency>"
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
if length == 2:
currency = spl[1]
rtrn = revolut.shuffle(currency)
msg(dumps(rtrn))
class remaining(object):
name = "r"
authed = True
helptext = "Show how much is left before we are able to withdraw funds."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
remaining = tx.get_remaining()
msg(f"Remaining: {remaining}USD")
class total_remaining(object):
name = "tr"
authed = True
helptext = "Show how much is left before we are able to withdraw funds (including open trades)."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
remaining = tx.get_total_remaining()
msg(f"Total remaining: {remaining}USD")
class tradetotal(object):
name = "tradetotal"
authed = True
helptext = "Get total value of all open trades in USD."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
total = tx.get_open_trades_usd()
msg(f"Total trades: {total}USD")
class dollar(object):
name = "$"
authed = True
helptext = "Get total value of everything, including open trades."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
total = tx.get_total_with_trades()
msg(f"${total}")
class profit(object):
name = "profit"
authed = True
helptext = "Get total profit."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
total = tx.money.get_profit()
msg(f"Profit: {total}USD")
class tprofit(object):
name = "tprofit"
authed = True
helptext = "Get total profit with open trades."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
total = tx.money.get_profit(True)
msg(f"Profit: {total}USD")
class signin(object):
name = "signin"
authed = True
helptext = "Generate a TrueLayer signin URL."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
auth_url = agora.truelayer.create_auth_url()
msg(f"Auth URL: {auth_url}")

View File

@ -2,6 +2,7 @@
from twisted.logger import Logger from twisted.logger import Logger
from twisted.words.protocols import irc from twisted.words.protocols import irc
from twisted.internet import protocol, reactor, ssl from twisted.internet import protocol, reactor, ssl
from twisted.internet.task import deferLater
# Project imports # Project imports
from settings import settings from settings import settings
@ -37,15 +38,6 @@ class IRCBot(irc.IRCClient):
self.channel = settings.IRC.Channel self.channel = settings.IRC.Channel
def set_agora(self, agora):
self.agora = agora
def set_revolut(self, revolut):
self.revolut = revolut
def set_tx(self, tx):
self.tx = tx
def parse(self, user, host, channel, msg): def parse(self, user, host, channel, msg):
""" """
Simple handler for IRC commands. Simple handler for IRC commands.
@ -96,7 +88,7 @@ class IRCBot(irc.IRCClient):
# Check if the command required authentication # Check if the command required authentication
if obj.authed: if obj.authed:
if host in self.admins: if host in self.admins:
obj.run(cmd, spl, length, authed, msgl, self.agora, self.revolut, self.tx) obj.run(cmd, spl, length, authed, msgl, self.agora, self.revolut, self.tx, self.notify)
else: else:
# Handle authentication here instead of in the command module for security # Handle authentication here instead of in the command module for security
self.msg(channel, "Access denied.") self.msg(channel, "Access denied.")
@ -115,7 +107,7 @@ class IRCBot(irc.IRCClient):
Join our channel. Join our channel.
""" """
self.log.info("Signed on as %s" % (self.nickname)) self.log.info("Signed on as %s" % (self.nickname))
self.join(self.channel) deferLater(reactor, 2, self.join, self.channel)
def joined(self, channel): def joined(self, channel):
""" """
@ -156,7 +148,9 @@ class IRCBot(irc.IRCClient):
self.parse(user, host, channel, msg[1:]) self.parse(user, host, channel, msg[1:])
elif host in self.admins and channel == nick: elif host in self.admins and channel == nick:
if len(msg) > 0: if len(msg) > 0:
if msg.split()[0] != "!": spl = msg.split()
if len(spl) > 0:
if spl[0] != "!":
self.parse(user, host, channel, msg) self.parse(user, host, channel, msg)
def noticed(self, user, channel, msg): def noticed(self, user, channel, msg):
@ -179,15 +173,6 @@ class IRCBotFactory(protocol.ClientFactory):
def __init__(self): def __init__(self):
self.log = Logger("irc") self.log = Logger("irc")
def set_agora(self, agora):
self.agora = agora
def set_revolut(self, revolut):
self.revolut = revolut
def set_tx(self, tx):
self.tx = tx
def sendmsg(self, msg): def sendmsg(self, msg):
""" """
Passthrough function to send a message to the channel. Passthrough function to send a message to the channel.
@ -206,9 +191,10 @@ class IRCBotFactory(protocol.ClientFactory):
""" """
prcol = IRCBot(self.log) prcol = IRCBot(self.log)
self.client = prcol self.client = prcol
self.client.set_agora(self.agora) setattr(self.client, "agora", self.agora)
self.client.set_revolut(self.revolut) setattr(self.client, "revolut", self.revolut)
self.client.set_tx(self.tx) setattr(self.client, "tx", self.tx)
setattr(self.client, "notify", self.notify)
return prcol return prcol
def clientConnectionLost(self, connector, reason): def clientConnectionLost(self, connector, reason):

172
handler/markets.py Normal file
View File

@ -0,0 +1,172 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports
from json import loads
# Project imports
from settings import settings
class Markets(object):
""" "
Markets handler for generic market functions.
"""
def __init__(self):
self.log = Logger("markets")
def get_all_assets(self):
assets = loads(settings.Agora.AssetList)
return assets
def get_all_providers(self):
providers = loads(settings.Agora.ProviderList)
return providers
def get_all_currencies(self):
currencies = list(set([x[0] for x in loads(settings.Agora.DistList)]))
return currencies
def get_new_ad_equations(self, public_ads, assets=None):
"""
Update all our prices.
:param public_ads: dictionary of public ads keyed by currency
:type public_ads: dict
:return: list of ads to modify
:rtype: list
"""
to_update = []
# NOTES:
# Get all ads for each currency, with all the payment methods.
# Create a function to, in turn, filter these so it contains only one payment method. Run autoprice on this.
# Append all results to to_update. Repeat for remaining payment methods, then call slow update.
# (asset, currency, provider)
if not assets:
assets = self.get_all_assets()
currencies = self.get_all_currencies()
providers = self.get_all_providers()
brute = [(asset, currency, provider) for asset in assets for currency in currencies for provider in providers]
for asset, currency, provider in brute:
# Filter currency
try:
public_ads_currency = public_ads[currency]
except KeyError:
# self.log.error("Error getting public ads for currency {currency}", currency=currency)
if currency == "USD":
self.log.error("Error getting public ads for currency USD, aborting")
break
continue
# Filter asset
public_ads_filtered = [ad for ad in public_ads_currency if ad[4] == asset]
# Filter provider
public_ads_filtered = [ad for ad in public_ads_filtered if ad[3] == provider]
our_ads = [ad for ad in public_ads_filtered if ad[1] == settings.Agora.Username]
if not our_ads:
continue
new_margin = self.autoprice(public_ads_filtered, currency)
# self.log.info("New rate for {currency}: {rate}", currency=currency, rate=new_margin)
new_formula = f"coingecko{asset.lower()}usd*usd{currency.lower()}*{new_margin}"
for ad in our_ads:
ad_id = ad[0]
asset = ad[4]
our_margin = ad[5]
if new_margin != our_margin:
to_update.append([ad_id, new_formula, asset, currency, False])
return to_update
def autoprice(self, ads, currency):
"""
Helper function to automatically adjust the price up/down in certain markets
in order to gain the most profits and sales.
:param ads: list of ads
:type ads: list of lists
:param currency: currency of the ads
:type currency: string
:return: the rate we should use for this currency
:rtype: float
"""
# self.log.debug("Autoprice starting for {x}", x=currency)
# Find cheapest ad
# Filter by 3rd index on each ad list to find the cheapest
min_margin_ad = min(ads, key=lambda x: x[6])
# self.log.debug("Minimum margin ad: {x}", x=min_margin_ad)
# Find second cheapest that is not us
# Remove results from ads that are us
ads_without_us = [ad for ad in ads if not ad[1] == settings.Agora.Username]
# self.log.debug("Ads without us: {x}", x=ads_without_us)
# Find ads above our min that are not us
ads_above_our_min_not_us = [ad for ad in ads_without_us if ad[6] > float(settings.Agora.MinMargin)]
# self.log.debug("Ads above our min not us: {x}", x=ads_above_our_min_not_us)
# Check that this list without us is not empty
if ads_without_us:
# Find the cheapest from these
min_margin_ad_not_us = min(ads_without_us, key=lambda x: x[6])
# self.log.debug("Min margin ad not us: {x}", x=min_margin_ad_not_us)
# Lowball the lowest ad that is not ours
lowball_lowest_not_ours = min_margin_ad_not_us[6] # - 0.005
# self.log.debug("Lowball lowest not ours: {x}", x=lowball_lowest_not_ours)
# Check if the username field of the cheapest ad matches ours
if min_margin_ad[1] == settings.Agora.Username:
# self.log.debug("We are the cheapest for: {x}", x=currency)
# We are the cheapest!
# Are all of the ads ours?
all_ads_ours = all([ad[1] == settings.Agora.Username for ad in ads])
if all_ads_ours:
# self.log.debug("All ads are ours for: {x}", x=currency)
# Now we know it's safe to return the maximum value
return float(settings.Agora.MaxMargin)
else:
# self.log.debug("All ads are NOT ours for: {x}", x=currency)
# All the ads are not ours, but we are first...
# Check if the lowballed, lowest (that is not ours) ad's margin
# is less than our minimum
if lowball_lowest_not_ours < float(settings.Agora.MinMargin):
# self.log.debug("Lowball lowest not ours less than MinMargin")
return float(settings.Agora.MinMargin)
elif lowball_lowest_not_ours > float(settings.Agora.MaxMargin):
# self.log.debug("Lowball lowest not ours more than MaxMargin")
return float(settings.Agora.MaxMargin)
else:
# self.log.debug("Returning lowballed figure: {x}", x=lowball_lowest_not_ours)
return lowball_lowest_not_ours
else:
# self.log.debug("We are NOT the cheapest for: {x}", x=currency)
# We are not the cheapest :(
# Check if this list is empty
if not ads_above_our_min_not_us:
# Return the maximum margin?
return float(settings.Agora.MaxMargin)
# Find cheapest ad above our min that is not us
cheapest_ad = min(ads_above_our_min_not_us, key=lambda x: x[4])
cheapest_ad_margin = cheapest_ad[6] # - 0.005
if cheapest_ad_margin > float(settings.Agora.MaxMargin):
# self.log.debug("Cheapest ad not ours more than MaxMargin")
return float(settings.Agora.MaxMargin)
# self.log.debug("Cheapest ad above our min that is not us: {x}", x=cheapest_ad)
return cheapest_ad_margin
def create_distribution_list(self, filter_asset=None):
"""
Create a list for distribution of ads.
:return: generator of asset, countrycode, currency, provider
:rtype: generator of tuples
"""
# Iterate providers like REVOLUT, NATIONAL_BANK
for provider in loads(settings.Agora.ProviderList):
# Iterate assets like XMR, BTC
for asset in loads(settings.Agora.AssetList):
# Iterate pairs of currency and country like EUR, GB
for currency, countrycode in loads(settings.Agora.DistList):
if filter_asset:
if asset == filter_asset:
yield (asset, countrycode, currency, provider)

107
handler/money.py Normal file
View File

@ -0,0 +1,107 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports
from pycoingecko import CoinGeckoAPI
from forex_python.converter import CurrencyRates
# Project imports
from settings import settings
class Money(object):
"""
Generic class for handling money-related matters that aren't Revolut or Agora.
"""
def __init__(self):
"""
Initialise the Money object.
Set the logger.
Initialise the CoinGecko API.
"""
self.log = Logger("money")
self.cr = CurrencyRates()
self.cg = CoinGeckoAPI()
def lookup_rates(self, ads, rates=None):
"""
Lookup the rates for a list of public ads.
"""
if not rates:
rates = self.cg.get_price(ids=["monero", "bitcoin"], vs_currencies=self.markets.get_all_currencies())
# Set the price based on the asset
for ad in ads:
if ad[4] == "XMR":
coin = "monero"
elif ad[4] == "BTC":
coin = "bitcoin" # No s here
currency = ad[5]
base_currency_price = rates[coin][currency.lower()]
price = float(ad[2])
rate = round(price / base_currency_price, 2)
ad.append(rate)
return sorted(ads, key=lambda x: x[2])
def get_rates_all(self):
"""
Get all rates that pair with USD.
:return: dictionary of USD/XXX rates
:rtype: dict
"""
rates = self.cr.get_rates("USD")
return rates
def get_acceptable_margins(self, currency, amount):
"""
Get the minimum and maximum amounts we would accept a trade for.
:param currency: currency code
:param amount: amount
:return: (min, max)
:rtype: tuple
"""
rates = self.get_rates_all()
if currency == "USD":
min_amount = amount - float(settings.Agora.AcceptableUSDMargin)
max_amount = amount + float(settings.Agora.AcceptableUSDMargin)
return (min_amount, max_amount)
amount_usd = amount / rates[currency]
min_usd = amount_usd - float(settings.Agora.AcceptableUSDMargin)
max_usd = amount_usd + float(settings.Agora.AcceptableUSDMargin)
min_local = min_usd * rates[currency]
max_local = max_usd * rates[currency]
return (min_local, max_local)
def to_usd(self, amount, currency):
if currency == "USD":
return float(amount)
else:
rates = self.get_rates_all()
return float(amount) / rates[currency]
# TODO: move to money
def get_profit(self, trades=False):
"""
Check how much total profit we have made.
:return: profit in USD
:rtype: float
"""
total_usd = self.tx.get_total_usd()
if not total_usd:
return False
if trades:
trades_usd = self.tx.get_open_trades_usd()
total_usd += trades_usd
profit = total_usd - float(settings.Money.BaseUSD)
if trades:
cast_es = {
"profit_trades_usd": profit,
}
else:
cast_es = {
"profit_usd": profit,
}
self.tx.write_to_es("get_profit", cast_es)
return profit

45
handler/notify.py Normal file
View File

@ -0,0 +1,45 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports
import requests
# Project imports
from settings import settings
class Notify(object):
"""
Class to handle more robust notifications.
"""
def __init__(self):
self.log = Logger("notify")
def sendmsg(self, msg, title=None, priority=None, tags=None):
headers = {"Title": "Bot"}
if title:
headers["Title"] = title
if priority:
headers["Priority"] = priority
if tags:
headers["Tags"] = tags
requests.post(
f"{settings.Notify.Host}/{settings.Notify.Topic}",
data=msg,
headers=headers,
)
def notify_new_trade(self, amount, currency):
amount_usd = self.money.to_usd(amount, currency)
self.sendmsg(f"Total: {amount_usd}", title="New trade", tags="trades", priority="2")
def notify_complete_trade(self, amount, currency):
amount_usd = self.money.to_usd(amount, currency)
self.sendmsg(f"Total: {amount_usd}", title="Trade complete", tags="trades,profit", priority="3")
def notify_withdrawal(self, amount_usd):
self.sendmsg(f"Total: {amount_usd}", title="Withdrawal", tags="profit", priority="4")
def notify_need_topup(self, amount_usd_xmr, amount_usd_btc):
self.sendmsg(f"XMR: {amount_usd_xmr} | BTC: {amount_usd_btc}", title="Topup needed", tags="admin", priority="5")

View File

@ -8,6 +8,8 @@ import requests
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
import jwt import jwt
from random import choices
from string import ascii_uppercase
# Project imports # Project imports
from settings import settings from settings import settings
@ -26,12 +28,6 @@ class Revolut(object):
self.log = Logger("revolut") self.log = Logger("revolut")
self.token = None self.token = None
def set_irc(self, irc):
self.irc = irc
def set_agora(self, agora):
self.agora = agora
def setup_auth(self): def setup_auth(self):
""" """
Function to create a new Java Web Token and use it to get a refresh/access token. Function to create a new Java Web Token and use it to get a refresh/access token.
@ -83,9 +79,9 @@ class Revolut(object):
settings.Revolut.RefreshToken = parsed["refresh_token"] settings.Revolut.RefreshToken = parsed["refresh_token"]
settings.Revolut.SetupToken = "0" settings.Revolut.SetupToken = "0"
settings.write() settings.write()
self.log.info("Refreshed refresh token: {refresh_token}", refresh_token=settings.Revolut.RefreshToken) self.log.info("Refreshed refresh token - Revolut")
self.token = parsed["access_token"] self.token = parsed["access_token"]
self.log.info("Refreshed access token: {access_token}", access_token=self.token) self.log.info("Refreshed access token - Revolut")
except KeyError: except KeyError:
self.log.error(f"Token authorization didn't contain refresh or access token: {parsed}", parsed=parsed) self.log.error(f"Token authorization didn't contain refresh or access token: {parsed}", parsed=parsed)
return False return False
@ -119,7 +115,7 @@ class Revolut(object):
if r.status_code == 200: if r.status_code == 200:
if "access_token" in parsed.keys(): if "access_token" in parsed.keys():
self.token = parsed["access_token"] self.token = parsed["access_token"]
self.log.info("Refreshed access token: {access_token}", access_token=self.token) self.log.info("Refreshed access token - Revolut")
return True return True
else: else:
self.log.error(f"Token refresh didn't contain access token: {parsed}", parsed=parsed) self.log.error(f"Token refresh didn't contain access token: {parsed}", parsed=parsed)
@ -188,7 +184,7 @@ class Revolut(object):
return False return False
def get_total_usd(self): def get_total_usd(self):
rates = self.agora.get_rates_all() rates = self.money.get_rates_all()
accounts = self.accounts() accounts = self.accounts()
if not accounts: if not accounts:
return False return False
@ -199,3 +195,53 @@ class Revolut(object):
else: else:
total_usd += account["balance"] / rates[account["currency"]] total_usd += account["balance"] / rates[account["currency"]]
return total_usd 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

3
handler/runtest.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
pre-commit run -a
python -m unittest discover -s tests -p 'test_*.py'

23
handler/sinks/fidor.py Normal file
View File

@ -0,0 +1,23 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports
# import requests
# from json import dumps
# Project imports
# from settings import settings
class Fidor(object):
"""
Class to manage calls to the Fidor API.
"""
def __init__(self):
self.log = Logger("fidor")
def authorize(self):
"""
Perform initial authorization against Fidor API.
"""

65
handler/sinks/nordigen.py Normal file
View File

@ -0,0 +1,65 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports
import requests
from json import dumps
from simplejson.errors import JSONDecodeError
# Project imports
from settings import settings
class Nordigen(object):
"""
Class to manage calls to Open Banking APIs through Nordigen.
"""
def __init__(self):
self.log = Logger("nordigen")
self.token = None
self.get_access_token()
def get_access_token(self):
"""
Get an access token.
:return: True or False
:rtype: bool
"""
headers = {"accept": "application/json", "Content-Type": "application/json"}
data = {
"secret_id": settings.Nordigen.ID,
"secret_key": settings.Nordigen.Key,
}
path = f"{settings.Nordigen.Base}/token/new/"
r = requests.post(path, headers=headers, data=dumps(data))
try:
parsed = r.json()
except JSONDecodeError:
self.log.error("Error parsing access token response: {content}", content=r.content)
return False
if "access" in parsed:
self.token = parsed["access"]
self.log.info("Refreshed access token - Nordigen")
def get_institutions(self, country, filter_name=None):
"""
Get a list of supported institutions.
"""
if not len(country) == 2:
return False
headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"}
path = f"{settings.Nordigen.Base}/institutions/?country={country}"
r = requests.get(path, headers=headers)
try:
parsed = r.json()
except JSONDecodeError:
self.log.error("Error parsing institutions response: {content}", content=r.content)
return False
new_list = []
if filter_name:
for i in parsed:
if filter_name in i["name"]:
new_list.append(i)
return new_list
return parsed

115
handler/sinks/truelayer.py Normal file
View File

@ -0,0 +1,115 @@
# Twisted/Klein imports
from twisted.logger import Logger
from twisted.internet.task import LoopingCall
# Other library imports
import requests
from simplejson.errors import JSONDecodeError
from time import time
import urllib
# Project imports
from settings import settings
class TrueLayer(object):
"""
Class to manage calls to Open Banking APIs through TrueLayer.
"""
def __init__(self):
self.log = Logger("truelayer")
self.token = None
self.lc = LoopingCall(self.get_new_token)
self.lc.start(int(settings.TrueLayer.RefreshSec))
def setup_auth(self):
pass
def create_auth_url(self):
query = urllib.parse.urlencode(
{
"response_type": "code",
"response_mode": "form_post",
"client_id": settings.TrueLayer.ID,
"scope": "info accounts balance transactions offline_access",
"nonce": int(time()),
"redirect_uri": settings.TrueLayer.CallbackURL,
"enable_mock": "true",
}
)
auth_uri = f"{settings.TrueLayer.AuthBase}/?{query}&redirect_uri={settings.TrueLayer.CallbackURL}"
return auth_uri
def handle_authcode_received(self, authcode):
data = {
"client_id": settings.TrueLayer.ID,
"client_secret": settings.TrueLayer.Key,
"code": authcode,
"grant_type": "authorization_code",
"redirect_uri": settings.TrueLayer.CallbackURL,
}
r = requests.post(f"{settings.TrueLayer.AuthBase}/connect/token", data=data)
try:
parsed = r.json()
except JSONDecodeError:
return False
if "error" in parsed:
self.log.error("Error requesting refresh token: {error}", error=parsed["error"])
return False
settings.TrueLayer.RefreshToken = parsed["refresh_token"]
settings.TrueLayer.AuthCode = authcode
settings.write()
self.token = parsed["access_token"]
self.log.info("Retrieved access/refresh tokens - TrueLayer")
def get_new_token(self, fail=False):
"""
Exchange our refresh token for an access token.
"""
if not settings.TrueLayer.RefreshToken:
return
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "refresh_token",
"refresh_token": settings.TrueLayer.RefreshToken,
"client_id": settings.TrueLayer.ID,
"client_secret": settings.TrueLayer.Key,
}
r = requests.post(f"{settings.TrueLayer.AuthBase}/connect/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 - TrueLayer")
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 get_accounts(self):
"""
Get a list of accounts.
"""
headers = {"Authorization": f"Bearer {self.token}"}
path = f"{settings.TrueLayer.DataBase}/accounts"
r = requests.get(path, headers=headers)
try:
parsed = r.json()
except JSONDecodeError:
self.log.error("Error parsing institutions response: {content}", content=r.content)
return False
return parsed

806
handler/tests/common.py Normal file
View File

@ -0,0 +1,806 @@
expected_to_update = [
["2caa4afa-a1c7-4683-aa4a-faa1c7a683dc", "coingeckoxmrusd*usdrub*1.3", "XMR", "RUB", False],
["dd1148de-ba6e-4824-9148-deba6e8824e0", "coingeckoxmrusd*usdhkd*1.3", "XMR", "HKD", False],
["929450a0-9a86-4133-9450-a09a86613363", "coingeckoxmrusd*usdtry*1.3", "XMR", "TRY", False],
["262f54e5-faa1-4656-af54-e5faa106569e", "coingeckoxmrusd*usdgbp*1.15", "XMR", "GBP", False],
["71911d6a-ef24-4b8a-911d-6aef24fb8a42", "coingeckoxmrusd*usdgbp*1.15", "XMR", "GBP", False],
["47c3d48b-385c-4d48-83d4-8b385c3d48d8", "coingeckoxmrusd*usdnok*1.3", "XMR", "NOK", False],
["1f48b508-cdd9-4e49-88b5-08cdd99e49c2", "coingeckoxmrusd*usdnzd*1.3", "XMR", "NZD", False],
["2a769b75-6408-4823-b69b-75640828231b", "coingeckoxmrusd*usdhuf*1.3", "XMR", "HUF", False],
["2bcfb7a6-7ba6-4ea5-8fb7-a67ba69ea59f", "coingeckoxmrusd*usdchf*1.3", "XMR", "CHF", False],
["3bc93ad9-bc51-4939-893a-d9bc51e9395a", "coingeckoxmrusd*usdczk*1.3", "XMR", "CZK", False],
["f3663e72-12e1-4b87-a63e-7212e1ab87b0", "coingeckoxmrusd*usdpln*1.3", "XMR", "PLN", False],
["8577c575-42d1-4ebc-b7c5-7542d17ebc82", "coingeckoxmrusd*usdjpy*1.3", "XMR", "JPY", False],
["82423582-fe58-432d-8235-82fe58f32d0f", "coingeckoxmrusd*usdthb*1.3", "XMR", "THB", False],
["5223d44f-b620-42a5-a3d4-4fb620c2a530", "coingeckoxmrusd*usdsek*1.22", "XMR", "SEK", False],
["2f767f92-f1bd-4e3e-b67f-92f1bd2e3ed8", "coingeckoxmrusd*usdusd*1.12", "XMR", "USD", False],
["6ca63cef-783b-40cd-a63c-ef783b90cdc7", "coingeckoxmrusd*usdusd*1.12", "XMR", "USD", False],
["2db9190b-7f46-41cd-b919-0b7f4661cd9e", "coingeckoxmrusd*usdcad*1.3", "XMR", "CAD", False],
["f035c709-31f9-4c2b-b5c7-0931f9bc2b20", "coingeckoxmrusd*usdsgd*1.3", "XMR", "SGD", False],
["64cdcaca-0f61-4139-8dca-ca0f61e1390f", "coingeckoxmrusd*usdmxn*1.3", "XMR", "MXN", False],
["0f3fe35f-808f-4bae-bfe3-5f808ffbaee7", "coingeckoxmrusd*usdaud*1.26", "XMR", "AUD", False],
["7034f552-271f-4f88-b4f5-52271f4f8839", "coingeckoxmrusd*usdeur*1.18", "XMR", "EUR", False],
["3359fcab-4e02-4ea0-99fc-ab4e024ea0da", "coingeckoxmrusd*usdeur*1.18", "XMR", "EUR", False],
["f70b6711-5b7e-4c5c-8b67-115b7e3c5c7a", "coingeckoxmrusd*usdeur*1.18", "XMR", "EUR", False],
["473a3803-701d-45ee-ba38-03701dc5ee4d", "coingeckoxmrusd*usddkk*1.3", "XMR", "DKK", False],
["7b93d58b-7721-45be-93d5-8b772115bed3", "coingeckoxmrusd*usdzar*1.3", "XMR", "ZAR", False],
]
cg_prices = {
"bitcoin": {
"eur": 38164,
"czk": 924828,
"gbp": 32252,
"cad": 55384,
"dkk": 284076,
"nzd": 65925,
"usd": 43640,
"nok": 384190,
"pln": 173208,
"zar": 678716,
"huf": 13509146,
"sgd": 58674,
"rub": 3301769,
"jpy": 5021747,
"thb": 1439257,
"chf": 40315,
"aud": 61394,
"try": 593510,
"hkd": 340058,
"mxn": 902166,
"sek": 399011,
},
"monero": {
"eur": 154.97,
"czk": 3755.44,
"gbp": 130.97,
"cad": 224.9,
"dkk": 1153.54,
"nzd": 267.7,
"usd": 177.21,
"nok": 1560.08,
"pln": 703.34,
"zar": 2756.05,
"huf": 54856,
"sgd": 238.26,
"rub": 13407.45,
"jpy": 20392,
"thb": 5844.37,
"chf": 163.71,
"aud": 249.3,
"try": 2410.06,
"hkd": 1380.87,
"mxn": 3663.41,
"sek": 1620.26,
},
}
fake_public_ads = {
"AUD": [
["d7b9fbfd-4569-4431-b9fb-fd4569c43156", "Monero-Australia", "263.50", "CRYPTOCURRENCY", "XMR", "AUD", 1.06],
["aa4e2b2d-be22-45b8-8e2b-2dbe2235b807", "Monero-Australia", "264.77", "OTHER", "XMR", "AUD", 1.06],
["6d72615c-0a99-4093-b261-5c0a99409364", "Moneroeh", "266.28", "CRYPTOCURRENCY", "XMR", "AUD", 1.07],
["16a56e53-ab07-43b0-a56e-53ab07d3b032", "Select", "267.37", "CRYPTOCURRENCY", "XMR", "AUD", 1.07],
["99094468-3fbb-4709-8944-683fbbd709fc", "xmrTraders", "272.59", "CASH_BY_MAIL", "XMR", "AUD", 1.09],
["ecd168af-b74d-4321-9168-afb74d032138", "Select", "273.80", "CRYPTOCURRENCY", "XMR", "AUD", 1.1],
["f314252d-9eeb-416f-9425-2d9eebf16fb6", "VivekBlogger", "275.12", "CRYPTOCURRENCY", "XMR", "AUD", 1.1],
["95c64b58-3e6a-4004-864b-583e6a000487", "cointrades", "277.64", "CASH_DEPOSIT", "XMR", "AUD", 1.11],
["91b352bf-2aad-4e97-b352-bf2aad1e9766", "supermonday888", "280.16", "OTHER", "XMR", "AUD", 1.12],
["b0cc1ccb-ac72-4641-8c1c-cbac722641f1", "supermonday888", "280.16", "CASH_DEPOSIT", "XMR", "AUD", 1.12],
["29213ed9-285b-467e-a13e-d9285ba67eba", "XMRCoops", "282.69", "NATIONAL_BANK", "XMR", "AUD", 1.13],
["bff51fb3-47dc-4527-b51f-b347dcd527ba", "pmr", "282.69", "OTHER", "XMR", "AUD", 1.13],
["4fee811d-3f8e-48c8-ae81-1d3f8e28c8c7", "XMRCoops", "283.95", "CRYPTOCURRENCY", "XMR", "AUD", 1.14],
["8d612032-9e16-435c-a120-329e16e35c7f", "XMRCoops", "285.21", "CASH_DEPOSIT", "XMR", "AUD", 1.14],
["1d53b058-508f-487e-93b0-58508f887e84", "tlbig", "287.48", "NATIONAL_BANK", "XMR", "AUD", 1.15],
["61dd4ff4-92dd-4133-9d4f-f492dd813396", "Emu", "287.74", "NATIONAL_BANK", "XMR", "AUD", 1.15],
["750e5672-1bc5-4daf-8e56-721bc5cdaf78", "XMRCoops", "290.24", "CASH_BY_MAIL", "XMR", "AUD", 1.16],
["9d61f2bf-9a4c-48ae-a1f2-bf9a4cd8ae8a", "tlbig", "290.26", "CASH_DEPOSIT", "XMR", "AUD", 1.16],
["f7d7842d-f886-4d6f-9784-2df8865d6fe6", "tlbig", "290.26", "CASH_BY_MAIL", "XMR", "AUD", 1.16],
["e5a2ecac-a77d-4250-a2ec-aca77d025009", "cointrades", "290.26", "CASH_BY_MAIL", "XMR", "AUD", 1.16],
["d869ac03-1e6a-4bf4-a9ac-031e6a1bf403", "Select", "294.89", "PAYPAL", "XMR", "AUD", 1.18],
["46ee27e4-8bf7-4faf-ae27-e48bf73fafcd", "XMRCoops", "297.08", "OTHER", "XMR", "AUD", 1.19],
["3c777f16-4225-4386-b77f-164225f3869e", "XMRCoops", "297.71", "OTHER", "XMR", "AUD", 1.19],
["60089e22-8fd1-4d42-889e-228fd18d4265", "Sbudubuda", "302.88", "CASH_DEPOSIT", "XMR", "AUD", 1.21],
["2e9c04a6-b489-4029-9c04-a6b48910297d", "jeffguy", "312.98", "REVOLUT", "XMR", "AUD", 1.26],
["0f3fe35f-808f-4bae-bfe3-5f808ffbaee7", "topmonero", "313.31", "REVOLUT", "XMR", "AUD", 1.26],
["0afb9d71-5065-4c2a-bb9d-715065ec2a9f", "VivekBlogger", "318.02", "WU", "XMR", "AUD", 1.28],
["151dbefc-6a1e-4c81-9dbe-fc6a1eac8152", "Bitpal", "327.11", "PAYPAL", "XMR", "AUD", 1.31],
["b8937671-7979-44ea-9376-717979d4ea15", "Sbudubuda", "330.64", "CASH_BY_MAIL", "XMR", "AUD", 1.33],
["811bb879-800c-441a-9bb8-79800c741a86", "yakinikun", "333.93", "PAYPAL", "XMR", "AUD", 1.34],
["896e5326-16fe-487f-ae53-2616fea87f85", "EASY", "333.93", "PAYPAL", "XMR", "AUD", 1.34],
["9f5d4833-712b-4e5d-9d48-33712bce5d17", "COMPRATUDO", "340.74", "NATIONAL_BANK", "XMR", "AUD", 1.37],
["c7a51333-625f-447f-a513-33625fb47fdf", "Select", "343.24", "GIFT_CARD_CODE_GLOBAL", "XMR", "AUD", 1.38],
["49d53e96-852f-4b48-953e-96852fdb489e", "VivekBlogger", "381.13", "PAYPAL", "XMR", "AUD", 1.53],
["7ea4712a-e932-4eb1-a471-2ae9320eb17c", "Dax", "418.00", "NATIONAL_BANK", "XMR", "AUD", 1.68],
["e1da42f3-7081-470d-9a42-f37081370dd3", "MCWILSON700", "479.56", "PAYPAL", "XMR", "AUD", 1.92],
["f2271081-60cc-4ca7-a710-8160ccaca705", "MCWILSON700", "479.56", "XOOM", "XMR", "AUD", 1.92],
["ebc582a5-9112-4304-8582-a59112d30448", "Smithaye", "64671.41", "CRYPTOCURRENCY", "BTC", "AUD", 1.05],
["4a5fbc5a-333e-4d7a-9fbc-5a333e0d7aaa", "Select", "65580.47", "CRYPTOCURRENCY", "BTC", "AUD", 1.07],
["0f62d796-a679-4fbc-a2d7-96a6798fbcea", "supermonday888", "73212.92", "CASH_DEPOSIT", "BTC", "AUD", 1.19],
["3a97e998-64ef-4fa2-97e9-9864efdfa253", "Select", "76861.36", "PAYPAL", "BTC", "AUD", 1.25],
["bb51f1a5-3ae4-4676-91f1-a53ae4167601", "Dax", "99780.00", "NATIONAL_BANK", "BTC", "AUD", 1.63],
],
"SEK": [
["777c8026-c01d-4f2c-bc80-26c01dbf2c3e", "Moneroeh", "1793.57", "CRYPTOCURRENCY", "XMR", "SEK", 1.11],
["53578d42-4038-4b28-978d-4240386b2881", "VivekBlogger", "1827.10", "CRYPTOCURRENCY", "XMR", "SEK", 1.13],
["902a87ec-5061-49c6-aa87-ec5061e9c6a1", "XMRCoops", "1885.76", "CRYPTOCURRENCY", "XMR", "SEK", 1.16],
["2ff9284b-92cd-4576-b928-4b92cda576b3", "KnutValentinee", "1894.15", "SEPA", "XMR", "SEK", 1.17],
["60f65deb-2bd3-4d40-b65d-eb2bd32d4074", "libertyCrypto", "1927.67", "CASH_BY_MAIL", "XMR", "SEK", 1.19],
["d3812058-1b95-42ec-8120-581b95a2ecd4", "XMRCoops", "1977.96", "OTHER", "XMR", "SEK", 1.22],
["f9daf7da-b3da-47b0-9af7-dab3da27b005", "isse0202", "1977.96", "REVOLUT", "XMR", "SEK", 1.22],
["5223d44f-b620-42a5-a3d4-4fb620c2a530", "topmonero", "1981.81", "REVOLUT", "XMR", "SEK", 1.22],
["2252a3f7-6d6b-400b-92a3-f76d6bb00b50", "SwishaMonero", "2095.29", "REVOLUT", "XMR", "SEK", 1.29],
["2d72efa7-cef3-400c-b2ef-a7cef3200c15", "VivekBlogger", "2112.06", "WU", "XMR", "SEK", 1.3],
["a3059ae1-ca32-4566-859a-e1ca32956672", "yakinikun", "2217.66", "PAYPAL", "XMR", "SEK", 1.37],
["f5ecef47-7bb6-433e-acef-477bb6633ef5", "EASY", "2217.66", "PAYPAL", "XMR", "SEK", 1.37],
["da7b28a3-df32-4126-bb28-a3df32812653", "Dax", "2470.00", "NATIONAL_BANK", "XMR", "SEK", 1.52],
["53797e1b-2e0c-4dd1-b97e-1b2e0c8dd1d6", "VivekBlogger", "2531.11", "PAYPAL", "XMR", "SEK", 1.56],
["1b8d962b-500c-45af-8d96-2b500c85afdf", "Dax", "693700.00", "NATIONAL_BANK", "BTC", "SEK", 1.74],
],
"CAD": [
["b4f74d8a-d92f-4276-b74d-8ad92fe276c4", "Chicks", "240.38", "CASH_BY_MAIL", "XMR", "CAD", 1.07],
["c79c4049-1c35-4d8a-9c40-491c359d8a24", "Moneroeh", "242.67", "CRYPTOCURRENCY", "XMR", "CAD", 1.08],
["53c0860f-7ee2-4322-8086-0f7ee2a3222a", "VivekBlogger", "249.53", "CRYPTOCURRENCY", "XMR", "CAD", 1.11],
["dc4681c8-4155-4691-8681-c84155769163", "XMRCoops", "251.60", "CRYPTOCURRENCY", "XMR", "CAD", 1.12],
["5f42560b-3c42-4fbd-8256-0b3c42ffbdcf", "Select", "251.78", "CRYPTOCURRENCY", "XMR", "CAD", 1.12],
["ee503341-cdf5-4bae-9033-41cdf53baed6", "Select", "286.12", "PAYPAL", "XMR", "CAD", 1.27],
["76ce7a0a-5764-42f8-8e7a-0a5764c2f878", "VivekBlogger", "288.45", "WU", "XMR", "CAD", 1.28],
["2db9190b-7f46-41cd-b919-0b7f4661cd9e", "topmonero", "297.61", "REVOLUT", "XMR", "CAD", 1.32],
["f1464e18-f75a-4628-864e-18f75ae62836", "COMPRATUDO", "297.61", "CRYPTOCURRENCY", "XMR", "CAD", 1.32],
["9e332536-1a13-42e8-b325-361a13a2e8f5", "EASY", "302.88", "PAYPAL", "XMR", "CAD", 1.35],
["95744327-8aa3-4d23-b443-278aa30d2308", "yakinikun", "302.88", "PAYPAL", "XMR", "CAD", 1.35],
["30c33fe4-7bca-46a7-833f-e47bcac6a722", "VivekBlogger", "345.69", "PAYPAL", "XMR", "CAD", 1.54],
["525a6232-4896-4390-9a62-3248962390f8", "Dax", "349.00", "INTERNATIONAL_WIRE_SWIFT", "XMR", "CAD", 1.55],
["0280d6f4-19a5-4a8a-80d6-f419a59a8a26", "MCWILSON700", "412.08", "PAYPAL", "XMR", "CAD", 1.83],
["dff5afcc-0f39-4da5-b5af-cc0f397da5a6", "MCWILSON700", "434.97", "XOOM", "XMR", "CAD", 1.93],
["24bb0a8f-9ac1-4fdb-bb0a-8f9ac1dfdb58", "Chicks", "57551.09", "CASH_BY_MAIL", "BTC", "CAD", 1.04],
["505f6224-f039-4366-9f62-24f03963668e", "Dax", "87500.00", "INTERNATIONAL_WIRE_SWIFT", "BTC", "CAD", 1.58],
],
"DKK": [
["ef95850b-aae9-4d0c-9585-0baae92d0cd1", "Moneroeh", "1257.27", "CRYPTOCURRENCY", "XMR", "DKK", 1.09],
["48ab8c4d-04d3-4273-ab8c-4d04d362736b", "VivekBlogger", "1280.77", "CRYPTOCURRENCY", "XMR", "DKK", 1.11],
["e31a513a-cd01-4bf7-9a51-3acd019bf7ed", "XMRCoops", "1321.90", "CRYPTOCURRENCY", "XMR", "DKK", 1.15],
["83ee628e-a0f1-4690-ae62-8ea0f136907f", "KnutValentinee", "1421.77", "INTERNATIONAL_WIRE_SWIFT", "XMR", "DKK", 1.23],
["73e51f5f-ec63-4098-a51f-5fec63109802", "KnutValentinee", "1421.77", "INTERNATIONAL_WIRE_SWIFT", "XMR", "DKK", 1.23],
["65bf7220-e5f0-4efb-bf72-20e5f09efba1", "KnutValentinee", "1468.78", "SEPA", "XMR", "DKK", 1.27],
["16e78c0e-6879-4ff0-a78c-0e6879bff0f1", "VivekBlogger", "1480.53", "WU", "XMR", "DKK", 1.28],
["89f5bbcf-517e-4afa-b5bb-cf517edafae4", "Dax", "1490.00", "NATIONAL_BANK", "XMR", "DKK", 1.29],
["473a3803-701d-45ee-ba38-03701dc5ee4d", "topmonero", "1527.53", "REVOLUT", "XMR", "DKK", 1.32],
["41d4b3c7-fc3c-4ee9-94b3-c7fc3cdee907", "yakinikun", "1554.55", "PAYPAL", "XMR", "DKK", 1.35],
["205ac967-5d8d-459e-9ac9-675d8db59e88", "EASY", "1554.55", "PAYPAL", "XMR", "DKK", 1.35],
["94f52982-e92b-4bb4-b529-82e92b5bb494", "VivekBlogger", "1774.28", "PAYPAL", "XMR", "DKK", 1.54],
["a2e926ee-960b-4449-a926-ee960ba449dd", "Dax", "387000.00", "NATIONAL_BANK", "BTC", "DKK", 1.36],
],
"GBP": [
["8a3f81a8-0f76-4b03-bf81-a80f769b03f4", "Hakhlaque", "136.77", "CRYPTOCURRENCY", "XMR", "GBP", 1.04],
["ba882fd4-0cc7-48ae-882f-d40cc728aed2", "NewNamesProfile", "138.16", "CRYPTOCURRENCY", "XMR", "GBP", 1.05],
["c4ada210-42fa-4266-ada2-1042fad26683", "Moneroeh", "140.82", "CRYPTOCURRENCY", "XMR", "GBP", 1.08],
["84a10227-fce0-4282-a102-27fce0828255", "Chicks", "142.81", "CASH_BY_MAIL", "XMR", "GBP", 1.09],
["c56f28cf-3024-4d90-af28-cf30244d90a9", "XMRCoops", "144.74", "CRYPTOCURRENCY", "XMR", "GBP", 1.11],
["c0c7ac98-01fe-433e-87ac-9801fed33e77", "Select", "144.78", "CRYPTOCURRENCY", "XMR", "GBP", 1.11],
["7fa6f5a5-68bd-48ec-a6f5-a568bd28eccd", "VivekBlogger", "144.81", "CRYPTOCURRENCY", "XMR", "GBP", 1.11],
["e59fd3cb-4312-4cb2-9fd3-cb43122cb2b0", "InstaCrypto", "146.14", "CASH_BY_MAIL", "XMR", "GBP", 1.12],
["c378be42-5ab5-42df-b8be-425ab5e2dfbc", "Kevlar", "148.53", "NATIONAL_BANK", "XMR", "GBP", 1.13],
["402b5955-6b1d-4d77-ab59-556b1d2d7769", "10poplar", "148.53", "NATIONAL_BANK", "XMR", "GBP", 1.13],
["0be0bbeb-0435-4679-a0bb-eb043546793d", "wiefix", "148.79", "NATIONAL_BANK", "XMR", "GBP", 1.14],
["560cdb59-251c-4ced-8cdb-59251ceceddb", "Boozymad89", "148.79", "NATIONAL_BANK", "XMR", "GBP", 1.14],
["15e821b8-e570-4b0f-a821-b8e5709b0ffc", "SecureMole", "150.12", "REVOLUT", "XMR", "GBP", 1.15],
["071ab272-ba37-4a14-9ab2-72ba37fa1484", "Boozymad89", "150.12", "REVOLUT", "XMR", "GBP", 1.15],
["262f54e5-faa1-4656-af54-e5faa106569e", "topmonero", "150.35", "REVOLUT", "XMR", "GBP", 1.15],
["71911d6a-ef24-4b8a-911d-6aef24fb8a42", "topmonero", "150.35", "REVOLUT", "XMR", "GBP", 1.15],
["91870f8b-6038-4777-870f-8b60385777ba", "KnutValentinee", "152.11", "SEPA", "XMR", "GBP", 1.16],
["f3a89e8c-8708-4779-a89e-8c870817795f", "jeffguy", "152.78", "REVOLUT", "XMR", "GBP", 1.17],
["c7b587d8-26c4-4c02-b587-d826c4fc0228", "NewNamesProfile", "152.78", "NATIONAL_BANK", "XMR", "GBP", 1.17],
["702e8869-134e-4f34-ae88-69134e8f3455", "tradingdirect", "152.78", "CASH_BY_MAIL", "XMR", "GBP", 1.17],
["18666a0d-bb79-48af-a66a-0dbb7998af8e", "Boozymad89", "154.09", "NATIONAL_BANK", "XMR", "GBP", 1.18],
["6d81245a-8e65-4cc3-8124-5a8e652cc3ad", "37inglenook", "154.09", "NATIONAL_BANK", "XMR", "GBP", 1.18],
["150989af-cdcc-4674-8989-afcdcc967441", "Senti", "155.43", "NATIONAL_BANK", "XMR", "GBP", 1.19],
["73aca869-4b40-4e94-aca8-694b40fe9463", "Boozymad89", "159.42", "NATIONAL_BANK", "XMR", "GBP", 1.22],
["cc2ab0b2-aa95-45bf-aab0-b2aa9525bfc5", "10poplar", "159.42", "NATIONAL_BANK", "XMR", "GBP", 1.22],
["2cce8a67-d2b3-4067-8e8a-67d2b33067fb", "NewNamesProfile", "159.42", "NATIONAL_BANK", "XMR", "GBP", 1.22],
["281eee00-6f25-4868-9eee-006f25c868bb", "XMRCoops", "159.42", "OTHER", "XMR", "GBP", 1.22],
["5e90f6c5-ed34-46b8-90f6-c5ed34e6b829", "MattUK", "159.42", "GIFT_CARD_CODE_GLOBAL", "XMR", "GBP", 1.22],
["50d685b7-bdb8-4860-9685-b7bdb8d86081", "Pellerin", "160.75", "CREDITCARD", "XMR", "GBP", 1.23],
["590c4824-12f9-454f-8c48-2412f9f54f74", "Power", "161.92", "PAYPAL", "XMR", "GBP", 1.24],
["c1e26b15-3f77-46ea-a26b-153f7796ea92", "SecureMole", "161.92", "PAYPAL", "XMR", "GBP", 1.24],
["7a22da88-19b4-4ed3-a2da-8819b42ed34c", "Select", "161.92", "PAYPAL", "XMR", "GBP", 1.24],
["80f0c67e-7334-4d5f-b0c6-7e73348d5f38", "NewNamesProfile", "161.94", "PAYPAL", "XMR", "GBP", 1.24],
["a154792a-5336-4695-9479-2a53360695b4", "Senti", "162.08", "SQUARE_CASH", "XMR", "GBP", 1.24],
["c8d261b4-13c1-4200-9261-b413c1b200ca", "10poplar", "163.41", "SQUARE_CASH", "XMR", "GBP", 1.25],
["a705131c-bf10-427c-8513-1cbf10127c32", "37inglenook", "163.41", "SQUARE_CASH", "XMR", "GBP", 1.25],
["19bb0806-e2cd-4925-bb08-06e2cdd925b8", "Markantonio", "164.07", "PAYPAL", "XMR", "GBP", 1.25],
["e7c2fea8-2db6-4650-82fe-a82db62650c3", "Power", "164.73", "TRANSFERWISE", "XMR", "GBP", 1.26],
["5bca1a21-46dc-41da-8a1a-2146dc91daf1", "Bitpal", "164.73", "MONEYBOOKERS", "XMR", "GBP", 1.26],
["cca14a7b-9813-43cb-a14a-7b981303cb47", "Power", "165.00", "SQUARE_CASH", "XMR", "GBP", 1.26],
["ed15a9f1-f420-46a4-95a9-f1f42046a431", "NewNamesProfile", "166.06", "TRANSFERWISE", "XMR", "GBP", 1.27],
["ecd37a1c-14b5-4182-937a-1c14b53182fe", "NewNamesProfile", "167.38", "SQUARE_CASH", "XMR", "GBP", 1.28],
["b6186a14-6bd3-46bc-986a-146bd326bccc", "Bitpal", "167.39", "PAYPAL", "XMR", "GBP", 1.28],
["de5f9fd0-7192-4a2f-9f9f-d071928a2f86", "VivekBlogger", "167.39", "WU", "XMR", "GBP", 1.28],
["9493e7ac-6cf5-47a2-93e7-ac6cf597a2d5", "NewNamesProfile", "172.71", "SEPA", "XMR", "GBP", 1.32],
["a80a2f06-ee65-44c7-8a2f-06ee65e4c71b", "notahamster", "172.71", "GIFT_CARD_CODE_GLOBAL", "XMR", "GBP", 1.32],
["56b01cf3-912b-4c44-b01c-f3912b2c4467", "slacker111", "172.71", "CASH_BY_MAIL", "XMR", "GBP", 1.32],
["a18fcdca-45c0-464c-8fcd-ca45c0c64c37", "Power", "172.71", "SEPA", "XMR", "GBP", 1.32],
["ca4feeb9-22d5-456d-8fee-b922d5c56d27", "Boozymad89", "175.36", "REVOLUT", "XMR", "GBP", 1.34],
["ee0895e5-8993-4b5f-8895-e589930b5f3d", "yakinikun", "175.76", "PAYPAL", "XMR", "GBP", 1.34],
["75b635d5-1ca7-4606-b635-d51ca7160664", "EASY", "175.76", "PAYPAL", "XMR", "GBP", 1.34],
["34a20689-f788-4f73-a206-89f7884f7375", "Dax", "186.00", "CREDITCARD", "XMR", "GBP", 1.42],
["b3b5a4e7-2071-46a9-b5a4-e7207196a917", "Dax", "195.00", "NATIONAL_BANK", "XMR", "GBP", 1.49],
["8935e17b-81e8-4f9c-b5e1-7b81e8bf9c8f", "VivekBlogger", "200.60", "PAYPAL", "XMR", "GBP", 1.53],
["03d7342a-cf82-4496-9734-2acf82b496ee", "VivekBlogger", "200.60", "PAYPAL", "XMR", "GBP", 1.53],
["f3bfc6d0-7f8c-42a8-bfc6-d07f8c82a81f", "VivekBlogger", "200.60", "PAYPAL", "XMR", "GBP", 1.53],
["dc3a0bcc-f191-4f6f-ba0b-ccf1916f6f6d", "MCWILSON700", "252.42", "XOOM", "XMR", "GBP", 1.93],
["055d8971-b511-4d40-9d89-71b511dd40e8", "MCWILSON700", "252.42", "PAYPAL", "XMR", "GBP", 1.93],
["fca30f93-a091-44d1-a30f-93a091d4d178", "Crypto_Hood", "32755.02", "NATIONAL_BANK", "BTC", "GBP", 1.02],
["7a59c09d-2e8e-4638-99c0-9d2e8e0638a4", "Chicks", "34200.10", "CASH_BY_MAIL", "BTC", "GBP", 1.06],
["71b1b2c4-3b97-4e7c-b1b2-c43b978e7c5d", "Crypto_Hood", "38214.19", "CASH_BY_MAIL", "BTC", "GBP", 1.18],
["7eaffcbb-69f0-447b-affc-bb69f0647b67", "Crypto_Hood", "38535.32", "OTHER", "BTC", "GBP", 1.19],
["78db95b7-e090-48e2-9b95-b7e09098e2d9", "Crypto_Hood", "40140.96", "REVOLUT", "BTC", "GBP", 1.24],
["10c14420-0c03-43ab-8144-200c03c3abb1", "Crypto_Hood", "41104.34", "PAYPAL", "BTC", "GBP", 1.27],
["5af65e8f-9b6b-4fd3-b65e-8f9b6b5fd3c8", "EmmanuelMuema", "41698.43", "PAYPAL", "BTC", "GBP", 1.29],
["efaeea05-b8f7-49ac-aeea-05b8f7c9ac6f", "Crypto_Hood", "48169.15", "SQUARE_CASH", "BTC", "GBP", 1.49],
["f919710a-20b0-4d4d-9971-0a20b04d4d2b", "Dax", "48900.00", "CREDITCARD", "BTC", "GBP", 1.52],
["b333f748-5e77-4d2c-b3f7-485e77dd2ce2", "Dax", "49500.00", "NATIONAL_BANK", "BTC", "GBP", 1.53],
],
"HUF": [
["7befe24a-4ca6-4934-afe2-4a4ca6393486", "Moneroeh", "59865.17", "CRYPTOCURRENCY", "XMR", "HUF", 1.09],
["186b0ba9-26d7-4052-ab0b-a926d7f0526e", "KnutValentinee", "72173.89", "SEPA", "XMR", "HUF", 1.32],
["2a769b75-6408-4823-b69b-75640828231b", "topmonero", "72733.38", "REVOLUT", "XMR", "HUF", 1.33],
["d3da7425-84b3-4a99-9a74-2584b39a993b", "EASY", "74020.20", "PAYPAL", "XMR", "HUF", 1.35],
["abbb7c6d-c95c-44d5-bb7c-6dc95cb4d530", "yakinikun", "74020.20", "PAYPAL", "XMR", "HUF", 1.35],
["0b2b69c8-c75c-42bc-ab69-c8c75c32bc17", "VivekBlogger", "84482.62", "PAYPAL", "XMR", "HUF", 1.54],
],
"NOK": [
["b2941e74-b7b2-4711-941e-74b7b2a7113e", "Moneroeh", "1701.30", "CRYPTOCURRENCY", "XMR", "NOK", 1.09],
["db96e90e-ad7f-4972-96e9-0ead7f8972db", "Moneroeh", "1701.30", "CRYPTOCURRENCY", "XMR", "NOK", 1.09],
["1f8e8630-0f24-48fc-8e86-300f24f8fca1", "VivekBlogger", "1733.10", "CRYPTOCURRENCY", "XMR", "NOK", 1.11],
["2d831dae-bd69-4bd9-831d-aebd69bbd93b", "XMRCoops", "1788.75", "CRYPTOCURRENCY", "XMR", "NOK", 1.15],
["7988900b-1f3d-4cfb-8890-0b1f3dfcfb97", "KnutValentinee", "1892.10", "NATIONAL_BANK", "XMR", "NOK", 1.21],
["7cdf89eb-51a2-4cea-9f89-eb51a23cea53", "KnutValentinee", "1900.05", "VIPPS", "XMR", "NOK", 1.22],
["81721cdf-ae2c-4255-b21c-dfae2cc2553d", "KnutValentinee", "1923.90", "SEPA", "XMR", "NOK", 1.23],
["40a078fa-d53e-4ac6-a078-fad53ebac6ae", "KnutValentinee", "1923.90", "NATIONAL_BANK", "XMR", "NOK", 1.23],
["a1e6936e-a661-4072-a693-6ea661a0720f", "KnutValentinee", "1928.67", "VIPPS", "XMR", "NOK", 1.24],
["ee6c9180-2310-4982-ac91-80231089829f", "KnutValentinee", "1931.85", "SEPA", "XMR", "NOK", 1.24],
["5ea617dd-d0fe-4fa3-a617-ddd0feafa32d", "Dax", "1939.80", "NATIONAL_BANK", "XMR", "NOK", 1.24],
["3c03af5b-de8b-4efd-83af-5bde8bdefd15", "VivekBlogger", "2003.40", "WU", "XMR", "NOK", 1.28],
["47c3d48b-385c-4d48-83d4-8b385c3d48d8", "topmonero", "2067.00", "REVOLUT", "XMR", "NOK", 1.32],
["82d39e00-4c3b-4855-939e-004c3bf855a1", "yakinikun", "2103.57", "PAYPAL", "XMR", "NOK", 1.35],
["e5ffacc9-706f-4fd4-bfac-c9706fcfd442", "EASY", "2103.57", "PAYPAL", "XMR", "NOK", 1.35],
["828c0693-5c20-454c-8c06-935c20c54cc5", "VivekBlogger", "2400.90", "PAYPAL", "XMR", "NOK", 1.54],
["a3d7e2b3-f208-4063-97e2-b3f208f063d3", "Dax", "496000.00", "NATIONAL_BANK", "BTC", "NOK", 1.29],
],
"MXN": [
["75474da5-8395-4f55-874d-a58395ef5556", "Moneroeh", "3957.41", "CRYPTOCURRENCY", "XMR", "MXN", 1.08],
["64cdcaca-0f61-4139-8dca-ca0f61e1390f", "topmonero", "4808.07", "REVOLUT", "XMR", "MXN", 1.31],
["29d85cda-5fc4-489e-985c-da5fc4b89ef2", "yakinikun", "4893.14", "PAYPAL", "XMR", "MXN", 1.34],
["a33c95f4-2b93-468c-bc95-f42b93e68c72", "EASY", "4893.14", "PAYPAL", "XMR", "MXN", 1.34],
],
"JPY": [
["b6413848-0a04-4c9d-8138-480a043c9d9a", "Moneroeh", "22320.46", "CRYPTOCURRENCY", "XMR", "JPY", 1.09],
["40daa9ca-5eaa-4e58-9aa9-ca5eaade582d", "VivekBlogger", "22737.67", "CRYPTOCURRENCY", "XMR", "JPY", 1.12],
["f74b0e2c-8f39-4f94-8b0e-2c8f392f9459", "XMRCoops", "23467.78", "CRYPTOCURRENCY", "XMR", "JPY", 1.15],
["b56becaa-5013-4d36-abec-aa5013fd364e", "VivekBlogger", "26283.91", "WU", "XMR", "JPY", 1.29],
["8577c575-42d1-4ebc-b7c5-7542d17ebc82", "topmonero", "27118.32", "REVOLUT", "XMR", "JPY", 1.33],
["6add9ffa-25be-4f26-9d9f-fa25be5f26e0", "yakinikun", "27598.10", "PAYPAL", "XMR", "JPY", 1.35],
["f4d45ad4-4b13-4f5f-945a-d44b137f5fa7", "EASY", "27598.10", "PAYPAL", "XMR", "JPY", 1.35],
["fd6e1b2c-c23b-4f02-ae1b-2cc23bbf02c9", "Dax", "29670.00", "NATIONAL_BANK", "XMR", "JPY", 1.45],
["241487a6-9589-4160-9487-a69589616009", "VivekBlogger", "31498.97", "PAYPAL", "XMR", "JPY", 1.54],
["d4c78133-9c8e-4379-8781-339c8e53798a", "kek", "21903.26", "NATIONAL_BANK", "BTC", "JPY", 0.0],
["90d1d9a8-6ba3-4928-91d9-a86ba3d9282d", "Dax", "9676170.00", "NATIONAL_BANK", "BTC", "JPY", 1.93],
],
"HKD": [
["9d84b509-4339-4467-84b5-094339c4672e", "Moneroeh", "1501.17", "CRYPTOCURRENCY", "XMR", "HKD", 1.09],
["481afdd8-9e5f-42d5-9afd-d89e5f42d53d", "KnutValentinee", "1690.57", "INTERNATIONAL_WIRE_SWIFT", "XMR", "HKD", 1.22],
["927cccb9-0c7d-4ae5-bccc-b90c7daae56d", "Dax", "1697.59", "NATIONAL_BANK", "XMR", "HKD", 1.23],
["9dba4868-5d52-4e99-ba48-685d522e9906", "XMRCoops", "1753.71", "CRYPTOCURRENCY", "XMR", "HKD", 1.27],
["dd1148de-ba6e-4824-9148-deba6e8824e0", "topmonero", "1823.85", "REVOLUT", "XMR", "HKD", 1.32],
["20dab57c-85a1-402d-9ab5-7c85a1602dfb", "yakinikun", "1856.12", "PAYPAL", "XMR", "HKD", 1.34],
["d1863f48-2045-4b6a-863f-482045cb6acc", "EASY", "1856.12", "PAYPAL", "XMR", "HKD", 1.34],
["0974289b-ca2b-4fe4-b428-9bca2b0fe485", "Dax", "448000.00", "NATIONAL_BANK", "BTC", "HKD", 1.32],
],
"SGD": [
["a8fdb943-dea6-4b9b-bdb9-43dea6db9b5b", "Moneroeh", "258.75", "CRYPTOCURRENCY", "XMR", "SGD", 1.09],
["f035c709-31f9-4c2b-b5c7-0931f9bc2b20", "topmonero", "314.37", "REVOLUT", "XMR", "SGD", 1.32],
["082aefcb-7e2f-414c-aaef-cb7e2f414c78", "yakinikun", "319.93", "PAYPAL", "XMR", "SGD", 1.34],
["dd96e523-14d8-4dc4-96e5-2314d80dc499", "EASY", "319.93", "PAYPAL", "XMR", "SGD", 1.34],
["5f7e569c-5303-4f1c-be56-9c5303af1c09", "Dax", "419.00", "NATIONAL_BANK", "XMR", "SGD", 1.76],
["21b5b9a1-cb31-4893-b5b9-a1cb318893ab", "burpMonero", "440.00", "OTHER", "XMR", "SGD", 1.85],
["6a299a37-69b9-47f0-a99a-3769b927f08a", "Dax", "99763.00", "NATIONAL_BANK", "BTC", "SGD", 1.7],
],
"EUR": [
["fed66e9a-195e-45ac-966e-9a195e95acd3", "cryptuser", "153.20", "CASH_BY_MAIL", "XMR", "EUR", 0.99],
["96c88799-95e6-4726-8887-9995e6f7268a", "Hakhlaque", "162.67", "CRYPTOCURRENCY", "XMR", "EUR", 1.05],
["c966e1ea-60a8-4348-a6e1-ea60a813481a", "MalMen", "163.52", "SEPA", "XMR", "EUR", 1.06],
["5674c7c6-0318-417f-b4c7-c60318017f06", "MalMen", "163.83", "NATIONAL_BANK", "XMR", "EUR", 1.06],
["61bf7e67-2a03-4e4c-bf7e-672a039e4c7d", "duckduck", "164.25", "SEPA", "XMR", "EUR", 1.06],
["9eea3824-f2e7-4348-aa38-24f2e763480f", "Kikillbill", "165.83", "REVOLUT", "XMR", "EUR", 1.07],
["82419d43-676c-4cf6-819d-43676cacf6d0", "M0m0", "165.83", "SEPA", "XMR", "EUR", 1.07],
["825c2d91-1ce5-48e1-9c2d-911ce558e159", "MalMen", "166.79", "SEPA", "XMR", "EUR", 1.08],
["87c23b9e-ce21-469b-823b-9ece21269b4e", "Moneroeh", "168.04", "CRYPTOCURRENCY", "XMR", "EUR", 1.08],
["d3c1d82f-71a1-4a8f-81d8-2f71a1da8f0c", "mickeycrab", "168.20", "NATIONAL_BANK", "XMR", "EUR", 1.09],
["ed312f59-ad3a-4b8e-b12f-59ad3abb8e8f", "Moneroeh", "168.52", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["ae3baa59-f333-44c9-bbaa-59f33334c90b", "XMRCoops", "168.97", "OTHER", "XMR", "EUR", 1.09],
["b663e568-6e5c-445c-a3e5-686e5cf45cea", "XMRCoops", "168.97", "OTHER", "XMR", "EUR", 1.09],
["ae37dd20-1248-4d98-b7dd-2012488d98d0", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["bdd11897-9931-4cc1-9118-979931ccc124", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["0e2fa5db-4b33-436f-afa5-db4b33036f1a", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["706aff29-ae26-4568-aaff-29ae26156802", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["613f57df-2ca6-4482-bf57-df2ca684821c", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["7f891412-e428-4b22-8914-12e4287b22eb", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["447d52f3-fa90-4e1c-bd52-f3fa904e1ca9", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["2586d869-f678-4225-86d8-69f678f22593", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["a1aa5479-2152-414b-aa54-792152714b82", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["5ef11f6b-1f16-4541-b11f-6b1f16854198", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["4407e482-9fe0-4008-87e4-829fe0c00835", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["337751cc-1bc0-4f77-b751-cc1bc0af777e", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["03465a61-b9ce-403d-865a-61b9ce403d41", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["5e701223-3717-4483-b012-233717648374", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["aa2ddd3f-4466-4be2-addd-3f44661be2cb", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["f74154bf-f076-47ae-8154-bff07647ae3e", "Moneroeh", "168.99", "CRYPTOCURRENCY", "XMR", "EUR", 1.09],
["dee3013e-ac0e-4dcd-a301-3eac0e2dcdd6", "lamsamid", "168.99", "SEPA", "XMR", "EUR", 1.09],
["634dc943-7c3c-4d97-8dc9-437c3c6d9714", "Chicks", "170.57", "CASH_BY_MAIL", "XMR", "EUR", 1.1],
["d486e7a2-0b82-4dfa-86e7-a20b820dfaa6", "KnutValentinee", "171.36", "SEPA", "XMR", "EUR", 1.11],
["2a8452d4-dcea-4146-8452-d4dceac146c3", "VivekBlogger", "172.15", "CRYPTOCURRENCY", "XMR", "EUR", 1.11],
["27e908a8-9a68-4f78-a908-a89a68bf784a", "VivekBlogger", "172.15", "CRYPTOCURRENCY", "XMR", "EUR", 1.11],
["59c91f5b-7f3a-4b37-891f-5b7f3adb375c", "VivekBlogger", "172.15", "CRYPTOCURRENCY", "XMR", "EUR", 1.11],
["6ff7d2e6-6c36-4fdf-b7d2-e66c365fdf98", "VivekBlogger", "172.15", "CRYPTOCURRENCY", "XMR", "EUR", 1.11],
["2eacda51-34ca-4579-acda-5134caa579ef", "VivekBlogger", "172.15", "CRYPTOCURRENCY", "XMR", "EUR", 1.11],
["3b9e1d7c-0ab0-4979-9e1d-7c0ab0c979e7", "VivekBlogger", "172.15", "CRYPTOCURRENCY", "XMR", "EUR", 1.11],
["139f8e92-0088-4c87-9f8e-9200883c87d2", "VivekBlogger", "172.15", "CRYPTOCURRENCY", "XMR", "EUR", 1.11],
["be3f0fbf-cf8c-4f83-bf0f-bfcf8c6f833b", "VivekBlogger", "172.15", "CRYPTOCURRENCY", "XMR", "EUR", 1.11],
["5c60551d-a6bc-4be9-a055-1da6bcbbe994", "VivekBlogger", "172.15", "CRYPTOCURRENCY", "XMR", "EUR", 1.11],
["e1297cc2-9ebf-4759-a97c-c29ebfc759fc", "postman", "173.71", "CASH_BY_MAIL", "XMR", "EUR", 1.12],
["e3210144-b23a-4bcc-a101-44b23a8bcc41", "Swisswatcher", "173.73", "CASH_BY_MAIL", "XMR", "EUR", 1.12],
["795df0a7-0a3f-43cd-9df0-a70a3fd3cd77", "Power", "173.87", "SEPA", "XMR", "EUR", 1.12],
["fca3c142-df5f-45e2-a3c1-42df5f35e21f", "NuBIt", "175.00", "SEPA", "XMR", "EUR", 1.13],
["0eabd463-7a3c-4d5b-abd4-637a3c4d5bc5", "chriys", "175.31", "CASH_BY_MAIL", "XMR", "EUR", 1.13],
["006bc5c7-5e60-414f-abc5-c75e60614f02", "NeedMoneroXMR", "175.31", "CASH_BY_MAIL", "XMR", "EUR", 1.13],
["293c4d78-4296-4961-bc4d-78429629610d", "KnutValentinee", "176.41", "SEPA", "XMR", "EUR", 1.14],
["279e361f-d99a-4258-9e36-1fd99ad258e9", "KnutValentinee", "176.57", "SEPA", "XMR", "EUR", 1.14],
["3fc4e754-d275-4910-84e7-54d275e910fd", "KnutValentinee", "176.89", "SEPA", "XMR", "EUR", 1.14],
["c6d5ed31-ce4b-48f7-95ed-31ce4b78f746", "KnutValentinee", "176.89", "SEPA", "XMR", "EUR", 1.14],
["dbffbffc-066d-4cec-bfbf-fc066d1cecab", "KnutValentinee", "176.89", "SEPA", "XMR", "EUR", 1.14],
["f6bd6290-1662-471b-bd62-901662471bd2", "KnutValentinee", "176.89", "SEPA", "XMR", "EUR", 1.14],
["999aabd8-2535-4889-9aab-d8253528892e", "Matthias2309", "176.89", "SEPA", "XMR", "EUR", 1.14],
["647bf3a3-149d-49f1-bbf3-a3149d59f1e5", "KnutValentinee", "176.89", "SEPA", "XMR", "EUR", 1.14],
["bfcb8de2-0fbc-4b53-8b8d-e20fbc2b5345", "KnutValentinee", "176.89", "SEPA", "XMR", "EUR", 1.14],
["01b61e5b-e371-47e5-b61e-5be371f7e5bd", "KnutValentinee", "176.89", "SEPA", "XMR", "EUR", 1.14],
["579720f7-69c6-4b95-9720-f769c6db95b5", "freemarkets", "176.89", "CASH_BY_MAIL", "XMR", "EUR", 1.14],
["f08ba8b2-1246-46cb-8ba8-b2124616cbb4", "KnutValentinee", "177.52", "SEPA", "XMR", "EUR", 1.15],
["8289a5c3-1994-480b-89a5-c31994d80b78", "KnutValentinee", "177.68", "SEPA", "XMR", "EUR", 1.15],
["73d139ff-84f3-4c7c-9139-ff84f3bc7cb0", "XMRCoops", "177.68", "CRYPTOCURRENCY", "XMR", "EUR", 1.15],
["62baffd4-8759-48ca-baff-d48759b8caa4", "XMRCoops", "177.68", "CRYPTOCURRENCY", "XMR", "EUR", 1.15],
["435cddc0-d9a9-41d0-9cdd-c0d9a981d0b0", "XMRCoops", "177.68", "CRYPTOCURRENCY", "XMR", "EUR", 1.15],
["5a180496-9454-413e-9804-969454f13e7e", "XMRCoops", "177.68", "CRYPTOCURRENCY", "XMR", "EUR", 1.15],
["15322e5a-a056-46b2-b22e-5aa05626b20f", "XMRCoops", "177.68", "CRYPTOCURRENCY", "XMR", "EUR", 1.15],
["b20ceaef-2e83-4aad-8cea-ef2e83daadf2", "XMRCoops", "177.68", "CRYPTOCURRENCY", "XMR", "EUR", 1.15],
["32d90dc8-0821-4aed-990d-c808218aedae", "XMRCoops", "177.68", "CRYPTOCURRENCY", "XMR", "EUR", 1.15],
["88dade03-ae90-470d-9ade-03ae90270d15", "XMRCoops", "177.68", "CRYPTOCURRENCY", "XMR", "EUR", 1.15],
["ea4f1f4a-168d-486a-8f1f-4a168dc86a49", "XMRCoops", "177.68", "CRYPTOCURRENCY", "XMR", "EUR", 1.15],
["0323bc04-9e3f-47cd-a3bc-049e3fe7cdf8", "XMRCoops", "177.68", "CRYPTOCURRENCY", "XMR", "EUR", 1.15],
["8ee55f50-c45d-42b4-a55f-50c45de2b484", "XMRCoops", "177.68", "CRYPTOCURRENCY", "XMR", "EUR", 1.15],
["a0e5906c-7f48-469f-a590-6c7f48f69f53", "XMRCoops", "177.68", "CRYPTOCURRENCY", "XMR", "EUR", 1.15],
["3a16bdaf-5b84-410a-96bd-af5b84110a6c", "KnutValentinee", "177.99", "SEPA", "XMR", "EUR", 1.15],
["c4090828-6989-4d14-8908-286989ad14cc", "KnutValentinee", "177.99", "SEPA", "XMR", "EUR", 1.15],
["1026486a-54ae-4fda-a648-6a54ae6fda3f", "KnutValentinee", "177.99", "SEPA", "XMR", "EUR", 1.15],
["26764c9b-167c-44cd-b64c-9b167c94cd51", "KnutValentinee", "178.15", "SEPA", "XMR", "EUR", 1.15],
["d331f0c4-919a-49e7-b1f0-c4919a79e7df", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["8a341935-623b-42d7-b419-35623b92d777", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["f2632eb3-0698-4e85-a32e-b306988e85d9", "RC19", "178.23", "NATIONAL_BANK", "XMR", "EUR", 1.15],
["c942d7f0-47c2-44f5-82d7-f047c2e4f5da", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["10060758-fada-4b3b-8607-58fada8b3ba8", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["758f3353-8848-4d8c-8f33-538848dd8c12", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["0baab891-46ce-4a74-aab8-9146ceda740f", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["eb99846a-9c64-4e20-9984-6a9c643e20bb", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["2dd2eddb-7e9e-4640-92ed-db7e9ec640a1", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["8bc7b52d-40ee-4766-87b5-2d40ee2766ac", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["74f7feeb-cd20-46a2-b7fe-ebcd2096a22f", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["42760ad8-ef7b-4802-b60a-d8ef7b2802ae", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["89629b67-0821-4053-a29b-670821105369", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["5ebe8acf-5d21-484d-be8a-cf5d21584d7c", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["71d134ca-6909-4cd7-9134-ca69097cd700", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["6398f39a-61b7-4d5f-98f3-9a61b74d5fbb", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["e2fd522c-7057-467a-bd52-2c7057267a7f", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["d2703d09-c823-4a15-b03d-09c8238a1520", "RC19", "178.23", "SEPA", "XMR", "EUR", 1.15],
["b5edf385-b7eb-462a-adf3-85b7eb762af6", "KnutValentinee", "178.31", "SEPA", "XMR", "EUR", 1.15],
["15495a6e-1df2-4c4f-895a-6e1df2ac4f66", "KnutValentinee", "178.47", "SEPA", "XMR", "EUR", 1.15],
["566c4974-c320-4dcb-ac49-74c3205dcb09", "Swisswatcher", "181.62", "OTHER", "XMR", "EUR", 1.17],
["7034f552-271f-4f88-b4f5-52271f4f8839", "topmonero", "182.79", "REVOLUT", "XMR", "EUR", 1.18],
["3359fcab-4e02-4ea0-99fc-ab4e024ea0da", "topmonero", "182.79", "REVOLUT", "XMR", "EUR", 1.18],
["57e3e8d6-45fe-40da-a3e8-d645fe20da46", "SecureMole", "182.79", "REVOLUT", "XMR", "EUR", 1.18],
["f70b6711-5b7e-4c5c-8b67-115b7e3c5c7a", "topmonero", "182.79", "REVOLUT", "XMR", "EUR", 1.18],
["15d09e00-b143-4fe1-909e-00b143cfe142", "jeffguy", "184.63", "REVOLUT", "XMR", "EUR", 1.19],
["7e9dd73a-2847-4b39-9dd7-3a28476b39cd", "KnutValentinee", "186.36", "SEPA", "XMR", "EUR", 1.2],
["4097ffa8-a700-4c6c-97ff-a8a700ac6c8c", "XMRCoops", "189.49", "OTHER", "XMR", "EUR", 1.22],
["ba483593-990a-491f-8835-93990a791fc1", "XMRCoops", "189.52", "OTHER", "XMR", "EUR", 1.22],
["337f1815-707d-4e6a-bf18-15707d6e6a8f", "andromuj", "189.52", "SEPA", "XMR", "EUR", 1.22],
["47a006de-8e04-4815-a006-de8e04781565", "edk", "190.00", "SEPA", "XMR", "EUR", 1.23],
["30db5bf7-1e24-4bc6-9b5b-f71e248bc67f", "KnutValentinee", "190.31", "INTERNATIONAL_WIRE_SWIFT", "XMR", "EUR", 1.23],
["179af82c-531d-4887-9af8-2c531d388782", "KnutValentinee", "191.10", "INTERNATIONAL_WIRE_SWIFT", "XMR", "EUR", 1.23],
["16e8a04e-f861-4314-a8a0-4ef861b3148b", "Pellerin", "191.10", "CREDITCARD", "XMR", "EUR", 1.23],
["85f8d228-14bc-4af5-b8d2-2814bc0af5ee", "Pellerin", "191.10", "CREDITCARD", "XMR", "EUR", 1.23],
["beed8a20-2df2-4495-ad8a-202df2449540", "Pellerin", "191.10", "CREDITCARD", "XMR", "EUR", 1.23],
["471ccfb1-0e20-4205-9ccf-b10e20d2056d", "Pellerin", "191.10", "CREDITCARD", "XMR", "EUR", 1.23],
["d05504be-b262-437b-9504-beb262a37ba1", "Pellerin", "191.10", "CREDITCARD", "XMR", "EUR", 1.23],
["5b5cb732-39ea-4412-9cb7-3239ea44128d", "KnutValentinee", "191.10", "INTERNATIONAL_WIRE_SWIFT", "XMR", "EUR", 1.23],
["f2fed716-c1b1-403a-bed7-16c1b1703ab1", "KnutValentinee", "191.10", "INTERNATIONAL_WIRE_SWIFT", "XMR", "EUR", 1.23],
["a6f949d4-9700-4bf8-b949-d497003bf88e", "KnutValentinee", "191.10", "INTERNATIONAL_WIRE_SWIFT", "XMR", "EUR", 1.23],
["ef2bf435-ad9f-4776-abf4-35ad9f777615", "KnutValentinee", "192.68", "SEPA", "XMR", "EUR", 1.24],
["4be06752-7cab-489f-a067-527cab689fab", "KnutValentinee", "194.26", "SEPA", "XMR", "EUR", 1.25],
["2155aa9d-b871-45c3-95aa-9db87135c3f4", "KnutValentinee", "194.26", "SEPA", "XMR", "EUR", 1.25],
["f1755ab2-4a29-4a1a-b55a-b24a292a1a7b", "KnutValentinee", "195.05", "SEPA", "XMR", "EUR", 1.26],
["35ef3119-1914-4b76-af31-1919146b76e9", "Bitpal", "195.84", "MONEYBOOKERS", "XMR", "EUR", 1.26],
["e91e3ffc-c6ca-4d5c-9e3f-fcc6ca6d5cc2", "SecureMole", "195.84", "TRANSFERWISE", "XMR", "EUR", 1.26],
["7217ba8d-cf7e-4f1e-97ba-8dcf7e2f1ea1", "KnutValentinee", "195.84", "SEPA", "XMR", "EUR", 1.26],
["3f1825d0-739b-4bd3-9825-d0739b1bd32c", "KnutValentinee", "195.84", "SEPA", "XMR", "EUR", 1.26],
["83f1656c-6de4-4b13-b165-6c6de40b13d7", "KnutValentinee", "196.63", "SEPA", "XMR", "EUR", 1.27],
["b0f489cf-8cea-4c07-b489-cf8cea1c0793", "KnutValentinee", "196.63", "SEPA", "XMR", "EUR", 1.27],
["71e510aa-7ef8-42a6-a510-aa7ef8c2a629", "jeffguy", "197.26", "PAYPAL", "XMR", "EUR", 1.27],
["29ef0fe8-1664-4ddd-af0f-e81664fdddc2", "Bitpal", "197.42", "MONEYBOOKERS", "XMR", "EUR", 1.27],
["f8447096-20fa-445d-8470-9620faa45dff", "Dax", "198.70", "SEPA", "XMR", "EUR", 1.28],
["d8f2de73-18ea-4659-b2de-7318ea7659ef", "VivekBlogger", "199.00", "WU", "XMR", "EUR", 1.28],
["ae0e40ec-c535-4252-8e40-ecc535925245", "VivekBlogger", "199.00", "WU", "XMR", "EUR", 1.28],
["01f52ba1-49f3-4974-b52b-a149f3c97423", "VivekBlogger", "199.00", "WU", "XMR", "EUR", 1.28],
["d03ea54a-745b-4920-bea5-4a745bd92028", "VivekBlogger", "199.00", "WU", "XMR", "EUR", 1.28],
["d666dab0-d037-4d17-a6da-b0d0374d17ce", "VivekBlogger", "199.00", "WU", "XMR", "EUR", 1.28],
["e8a77e95-65c9-4c71-a77e-9565c9fc71c1", "VivekBlogger", "199.00", "WU", "XMR", "EUR", 1.28],
["f3be1d5c-c842-4c0b-be1d-5cc842ec0ba5", "VivekBlogger", "199.00", "WU", "XMR", "EUR", 1.28],
["47a8c8e1-275e-4022-a8c8-e1275e60227a", "VivekBlogger", "199.00", "WU", "XMR", "EUR", 1.28],
["65bc21b8-3e93-48cf-bc21-b83e9398cfc7", "Bitpal", "201.37", "PAYPAL", "XMR", "EUR", 1.3],
["d5697aa3-9565-4225-a97a-a395651225ee", "Power", "201.37", "PAYPAL", "XMR", "EUR", 1.3],
["a6676d8e-1a43-4136-a76d-8e1a43e13670", "EASY", "201.84", "PAYPAL", "XMR", "EUR", 1.3],
["afed7764-bc02-4a58-ad77-64bc02ca5848", "yakinikun", "201.84", "PAYPAL", "XMR", "EUR", 1.3],
["ddeb55f4-c08a-4417-ab55-f4c08ae41744", "SecureMole", "201.84", "PAYPAL", "XMR", "EUR", 1.3],
["24f5b07b-17ef-4ced-b5b0-7b17efaced39", "KnutValentinee", "202.16", "NATIONAL_BANK", "XMR", "EUR", 1.3],
["02d65f65-7463-46f2-965f-65746306f2a3", "KnutValentinee", "202.16", "NATIONAL_BANK", "XMR", "EUR", 1.3],
["eef710a1-45dd-42c7-b710-a145dd62c702", "KnutValentinee", "203.42", "VIPPS", "XMR", "EUR", 1.31],
["a24af15c-3a17-4bd8-8af1-5c3a177bd8f4", "KnutValentinee", "203.74", "VIPPS", "XMR", "EUR", 1.31],
["9cb46b4e-2d40-4516-b46b-4e2d40a516de", "COMPRATUDO", "205.31", "CRYPTOCURRENCY", "XMR", "EUR", 1.32],
["54d5afc8-c0e7-45b0-95af-c8c0e775b09a", "KnutValentinee", "205.31", "SEPA", "XMR", "EUR", 1.32],
["7f8e36ad-11a1-49ed-8e36-ad11a169ed71", "KnutValentinee", "205.31", "SEPA", "XMR", "EUR", 1.32],
["6ce62792-d93e-4aba-a627-92d93e5aba6c", "KnutValentinee", "205.31", "SEPA", "XMR", "EUR", 1.32],
["0360ea65-5d3b-4768-a0ea-655d3bd768cd", "KnutValentinee", "205.31", "SEPA", "XMR", "EUR", 1.32],
["1c488904-f015-49b3-8889-04f01599b315", "KnutValentinee", "205.31", "INTERNATIONAL_WIRE_SWIFT", "XMR", "EUR", 1.32],
["8f5734df-7a7c-40e9-9734-df7a7c10e918", "Power", "205.31", "TRANSFERWISE", "XMR", "EUR", 1.32],
["e6199e4a-5fdd-47d4-999e-4a5fdd77d481", "Power", "205.31", "NATIONAL_BANK", "XMR", "EUR", 1.32],
["cffdcec5-c042-438d-bdce-c5c042738d73", "COMPRATUDO", "205.31", "CRYPTOCURRENCY", "XMR", "EUR", 1.32],
["cbf9e638-cee0-424d-b9e6-38cee0124dd3", "KnutValentinee", "205.31", "SEPA", "XMR", "EUR", 1.32],
["cdb5a7b3-8eb5-4903-b5a7-b38eb55903ac", "KnutValentinee", "205.31", "SEPA", "XMR", "EUR", 1.32],
["1d4bba97-2e75-4c7a-8bba-972e75fc7a1b", "COMPRATUDO", "205.31", "CRYPTOCURRENCY", "XMR", "EUR", 1.32],
["e3333ade-1d21-4d75-b33a-de1d21fd75a5", "COMPRATUDO", "205.31", "CRYPTOCURRENCY", "XMR", "EUR", 1.32],
["9eebd8ac-21a1-4b75-abd8-ac21a11b758a", "KnutValentinee", "205.31", "INTERNATIONAL_WIRE_SWIFT", "XMR", "EUR", 1.32],
["b1a60c0e-f749-47f6-a60c-0ef74987f6c5", "KnutValentinee", "205.31", "INTERNATIONAL_WIRE_SWIFT", "XMR", "EUR", 1.32],
["599eb17a-962b-4470-9eb1-7a962b7470a9", "KnutValentinee", "208.47", "CASH_BY_MAIL", "XMR", "EUR", 1.35],
["114ad2f8-b377-4f2a-8ad2-f8b3775f2a8e", "KnutValentinee", "213.21", "CASH_DEPOSIT", "XMR", "EUR", 1.38],
["eb202f4a-5f8f-4081-a02f-4a5f8f4081d1", "COMPRATUDO", "213.21", "NATIONAL_BANK", "XMR", "EUR", 1.38],
["b7a49893-8512-4719-a498-9385128719bf", "COMPRATUDO", "213.21", "NATIONAL_BANK", "XMR", "EUR", 1.38],
["38df20b7-4097-45b2-9f20-b7409755b2ec", "COMPRATUDO", "213.21", "ADVCASH", "XMR", "EUR", 1.38],
["282fc9c2-811b-4a30-afc9-c2811b3a3020", "COMPRATUDO", "213.21", "ADVCASH", "XMR", "EUR", 1.38],
["a23b6fca-a3af-40c9-bb6f-caa3af80c911", "COMPRATUDO", "213.21", "SEPA", "XMR", "EUR", 1.38],
["09816590-b5e5-4921-8165-90b5e569219e", "COMPRATUDO", "213.21", "TRANSFERWISE", "XMR", "EUR", 1.38],
["71fba09f-59c6-4e21-bba0-9f59c6ae21be", "COMPRATUDO", "213.21", "TRANSFERWISE", "XMR", "EUR", 1.38],
["ea90d7aa-0d1d-4808-90d7-aa0d1d2808ff", "COMPRATUDO", "213.21", "ADVCASH", "XMR", "EUR", 1.38],
["bdc38992-22cb-4e2c-8389-9222cb9e2cad", "COMPRATUDO", "213.21", "TRANSFERWISE", "XMR", "EUR", 1.38],
["a75bc0f1-1701-4400-9bc0-f11701e40053", "COMPRATUDO", "213.21", "NATIONAL_BANK", "XMR", "EUR", 1.38],
["b691a006-87f2-4d8e-91a0-0687f2cd8ea0", "COMPRATUDO", "213.21", "TRANSFERWISE", "XMR", "EUR", 1.38],
["7c4facad-f504-4a91-8fac-adf5044a91f1", "COMPRATUDO", "213.21", "SEPA", "XMR", "EUR", 1.38],
["4c0a0d34-a37e-4d81-8a0d-34a37e1d81e0", "COMPRATUDO", "213.21", "ADVCASH", "XMR", "EUR", 1.38],
["4c4440c7-d681-4b5e-8440-c7d6810b5e6b", "Dax", "219.00", "CREDITCARD", "XMR", "EUR", 1.41],
["fa0f2cf2-e817-4efe-8f2c-f2e8179efe2b", "Dax", "219.00", "NATIONAL_BANK", "XMR", "EUR", 1.41],
["d5aa9539-0aab-4bbf-aa95-390aab8bbf74", "luca_babulli", "220.96", "CASH_BY_MAIL", "XMR", "EUR", 1.43],
["feac92e1-459f-4d29-ac92-e1459f1d291b", "Dax", "234.00", "CASH_DEPOSIT", "XMR", "EUR", 1.51],
["d7b56002-83ab-429a-b560-0283abe29a3c", "manascrypto", "235.32", "PAYSAFECARD", "XMR", "EUR", 1.52],
["fedc1efe-2fab-445b-9c1e-fe2fab145ba1", "strawberries", "236.90", "CRYPTOCURRENCY", "XMR", "EUR", 1.53],
["ed23b3e8-2526-448b-a3b3-e82526048bf3", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["751b8fc4-0fdb-4085-9b8f-c40fdbd08585", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["f5fe23ec-3272-407b-be23-ec3272c07bbf", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["d1964bfc-9a3f-4f86-964b-fc9a3fff8639", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["cfc47038-257d-4b1e-8470-38257dcb1e30", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["31390b83-4ecf-46dc-b90b-834ecf46dc9e", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["b91e884e-86c3-42e7-9e88-4e86c362e718", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["d8db0fe1-cf2d-4497-9b0f-e1cf2d449763", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["6404aba0-f2ac-48a6-84ab-a0f2aca8a659", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["aaaafb1d-e3e4-449a-aafb-1de3e4549a2d", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["6cfb5120-d31f-4533-bb51-20d31ff53310", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["db6c8143-8683-4ee1-ac81-4386834ee1f4", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["7f823ed1-25f5-48b6-823e-d125f548b6e8", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["17ea8a58-2210-46b2-aa8a-58221076b212", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["f300d8d3-4faf-472c-80d8-d34fafa72cf9", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["bbeb2314-61f3-4219-ab23-1461f3f219c5", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["4b27f562-e2ec-4839-a7f5-62e2ec38397c", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["49f93c7a-eb93-40a8-b93c-7aeb93f0a885", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["05eecd5d-c4b5-4d24-aecd-5dc4b57d249f", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["dfbf3da0-bf27-4354-bf3d-a0bf272354bf", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["4270e746-2f80-4c11-b0e7-462f800c118a", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["f8fcbb0a-3bf6-4899-bcbb-0a3bf6d899a7", "VivekBlogger", "238.48", "PAYPAL", "XMR", "EUR", 1.54],
["8b4e6392-c3c7-46dc-8e63-92c3c786dc1e", "luca_babulli", "251.21", "NETELLER", "XMR", "EUR", 1.62],
["14128e28-d88c-4877-928e-28d88c98777e", "luca_babulli", "253.26", "WU", "XMR", "EUR", 1.63],
["2f3a7508-88c5-4b45-ba75-0888c51b45f2", "luca_babulli", "257.36", "PAYPAL", "XMR", "EUR", 1.66],
["60c88ebe-0b34-40ec-888e-be0b3420ec75", "luca_babulli", "259.41", "CREDITCARD", "XMR", "EUR", 1.67],
["141d63f2-cc84-4919-9d63-f2cc84e919ec", "luca_babulli", "269.66", "CREDITCARD", "XMR", "EUR", 1.74],
["c0d7f23b-cc26-4d85-97f2-3bcc265d8537", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["2993cebc-195c-4c5b-93ce-bc195c4c5b5c", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["5a9286b0-4141-400e-9286-b04141200eb5", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["c812f35c-7b09-452a-92f3-5c7b09552ac2", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["728e71a7-2e9d-4f0a-8e71-a72e9d9f0a49", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["e39971c0-78d4-42c3-9971-c078d4c2c33e", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["ff361a0b-903e-44f7-b61a-0b903ef4f712", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["94b399e2-2c96-480c-b399-e22c96180cf2", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["f3249712-c189-43a9-a497-12c189e3a9f8", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["354f59ae-c281-4700-8f59-aec281970075", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["6ce55e8a-10f4-4d66-a55e-8a10f4ad669c", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["69ae1799-283a-4c2b-ae17-99283a8c2b93", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["d11043b9-27a9-4d69-9043-b927a90d6981", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["f09c52ba-f003-43bd-9c52-baf00303bd5d", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["e35c51ff-d2f8-4840-9c51-ffd2f8984083", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["16ff709c-461c-403a-bf70-9c461c103a51", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["f203fdf9-3b5a-47b1-83fd-f93b5a97b13c", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["92121c5b-c07b-436a-921c-5bc07b036a53", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["e07f1fef-3e67-4e9d-bf1f-ef3e671e9dbc", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["844fd027-8b9e-4bad-8fd0-278b9ecbadcb", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["1b6359c0-a2ef-4875-a359-c0a2ef1875bd", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["f0158ec8-92c1-4466-958e-c892c1146601", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["fab7060b-2a7c-4ca1-b706-0b2a7ceca1e4", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["b8b12954-49f4-4d5f-b129-5449f41d5fb5", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["c8b80cc1-44ba-47de-b80c-c144ba87de63", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["626e497e-3ddf-4337-ae49-7e3ddf4337ef", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["e98a86af-54aa-4879-8a86-af54aa98791a", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["8a30a234-a13f-41c0-b0a2-34a13f51c095", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["aafb3dce-da7c-4958-bb3d-ceda7c2958ce", "Jorge", "272.00", "SEPA", "XMR", "EUR", 1.76],
["2f147a42-48c5-441a-947a-4248c5541a48", "postman", "282.70", "GIFT_CARD_CODE_GLOBAL", "XMR", "EUR", 1.82],
["09e678f2-276f-414d-a678-f2276f114d46", "luca_babulli", "289.16", "CREDITCARD", "XMR", "EUR", 1.87],
["078d582e-9be2-480c-8d58-2e9be2380c6e", "yoy", "296.00", "PAYPAL", "XMR", "EUR", 1.91],
["80757975-3d13-46fb-b579-753d1366fba6", "MCWILSON700", "300.08", "PAYPAL", "XMR", "EUR", 1.94],
["1a605e62-46c6-42e7-a05e-6246c6e2e7f4", "MCWILSON700", "300.08", "XOOM", "XMR", "EUR", 1.94],
["c9fbaaa2-6434-4b39-bbaa-a26434cb3979", "sanjurjo", "341.00", "NATIONAL_BANK", "XMR", "EUR", 2.2],
["225150af-4e90-4b05-9150-af4e90db0526", "Ahmed003", "473.80", "GIFT_CARD_CODE_GLOBAL", "XMR", "EUR", 3.06],
["16c2bd7b-551c-4c3f-82bd-7b551c4c3f9a", "MalMen", "39437.57", "NATIONAL_BANK", "BTC", "EUR", 1.03],
["69986ca9-f0eb-4ac0-986c-a9f0eb3ac057", "MalMen", "40616.87", "SEPA", "BTC", "EUR", 1.06],
["85e7cb6f-6f23-452d-a7cb-6f6f23752daa", "Chicks", "41230.31", "CASH_BY_MAIL", "BTC", "EUR", 1.08],
["1c4b5b14-ec53-4065-8b5b-14ec5300657c", "TheKaii", "46574.98", "SPECIFIC_BANK", "BTC", "EUR", 1.22],
["6c18d1c7-7619-4479-98d1-c77619747958", "TheKaii", "46574.98", "NATIONAL_BANK", "BTC", "EUR", 1.22],
["de745aa8-aa79-424a-b45a-a8aa79624a1a", "TheKaii", "46574.98", "SEPA", "BTC", "EUR", 1.22],
["940306fe-3445-49c7-8306-fe344519c711", "TheKaii", "46574.98", "SEPA", "BTC", "EUR", 1.22],
["65dde899-70ed-4fb6-9de8-9970edcfb664", "TheKaii", "46574.98", "SEPA", "BTC", "EUR", 1.22],
["179db5e3-a306-40f9-9db5-e3a30690f953", "TheKaii", "46574.98", "NATIONAL_BANK", "BTC", "EUR", 1.22],
["d91f445d-0cb3-4fae-9f44-5d0cb34fae3a", "TheKaii", "46574.98", "SPECIFIC_BANK", "BTC", "EUR", 1.22],
["9928ea57-79f6-4e99-a8ea-5779f6fe99f5", "TheKaii", "46574.98", "SEPA", "BTC", "EUR", 1.22],
["42a53e79-bdf0-4b52-a53e-79bdf0cb52a9", "TheKaii", "46574.98", "SEPA", "BTC", "EUR", 1.22],
["34a7f150-ab27-43b6-a7f1-50ab2733b6f6", "TheKaii", "46574.98", "SEPA", "BTC", "EUR", 1.22],
["91b80d22-044e-46f8-b80d-22044ec6f854", "Crypto_Hood", "46574.98", "WU", "BTC", "EUR", 1.22],
["24648fb0-6a87-4fc8-a48f-b06a871fc8df", "EmmanuelMuema", "48861.74", "PAYPAL", "BTC", "EUR", 1.28],
["a9b4a046-fbd3-40ce-b4a0-46fbd350cee4", "EmmanuelMuema", "48862.50", "PAYPAL", "BTC", "EUR", 1.28],
["3ac35173-e890-4251-8351-73e890725145", "EmmanuelMuema", "48862.88", "PAYPAL", "BTC", "EUR", 1.28],
["e5e7a1ff-2274-4667-a7a1-ff2274f667a4", "EmmanuelMuema", "48863.64", "PAYPAL", "BTC", "EUR", 1.28],
["e4099bf9-5ee5-40d2-899b-f95ee5f0d2c0", "EmmanuelMuema", "49602.35", "XOOM", "BTC", "EUR", 1.3],
["d229188e-bf21-4a4d-a918-8ebf21fa4dbc", "Crypto_Hood", "51537.89", "PAYPAL", "BTC", "EUR", 1.35],
["0f66a064-89eb-46dd-a6a0-6489eb96dd6c", "Crypto_Hood", "53446.70", "PAYPAL", "BTC", "EUR", 1.4],
["808efc29-1cea-4801-8efc-291ceac80192", "Crypto_Hood", "53446.70", "PAYPAL", "BTC", "EUR", 1.4],
["eac8d85b-f16c-420f-88d8-5bf16c920f79", "Dax", "56500.00", "NATIONAL_BANK", "BTC", "EUR", 1.48],
["a294be17-2b54-4640-94be-172b54f6401a", "Dax", "56800.00", "SEPA", "BTC", "EUR", 1.49],
["ece2517d-db7b-4a94-a251-7ddb7b1a9415", "Dax", "56910.00", "CREDITCARD", "BTC", "EUR", 1.49],
["6fecdc11-9acb-4269-acdc-119acb92695b", "Dax", "59700.00", "CASH_DEPOSIT", "BTC", "EUR", 1.56],
],
"NZD": [
["fcfc7b8a-3569-48cc-bc7b-8a356948cc6c", "Monero-Australia", "286.88", "CRYPTOCURRENCY", "XMR", "NZD", 1.07],
["d4e38b3c-3c9f-4ca0-a38b-3c3c9f1ca09f", "Monero-Australia", "288.23", "OTHER", "XMR", "NZD", 1.08],
["1b5d9ed4-16ba-4d94-9d9e-d416ba4d94e4", "Moneroeh", "289.59", "CRYPTOCURRENCY", "XMR", "NZD", 1.08],
["6419fb03-484e-4598-99fb-03484eb59869", "VivekBlogger", "295.00", "CRYPTOCURRENCY", "XMR", "NZD", 1.1],
["044e84c5-4217-4497-8e84-c54217449700", "jwang", "297.71", "NATIONAL_BANK", "XMR", "NZD", 1.11],
["781597f8-59c6-40f2-9597-f859c6e0f2dd", "KnutValentinee", "338.30", "INTERNATIONAL_WIRE_SWIFT", "XMR", "NZD", 1.26],
["53bc53ba-f2d8-4b6d-bc53-baf2d84b6df9", "VivekBlogger", "341.01", "WU", "XMR", "NZD", 1.27],
["1f48b508-cdd9-4e49-88b5-08cdd99e49c2", "topmonero", "351.83", "REVOLUT", "XMR", "NZD", 1.31],
["127c2881-99fe-4f3b-bc28-8199fe2f3b91", "yakinikun", "358.06", "PAYPAL", "XMR", "NZD", 1.34],
["ee1da643-a8cb-4287-9da6-43a8cb3287f2", "EASY", "358.06", "PAYPAL", "XMR", "NZD", 1.34],
["34787129-6032-4eef-b871-296032deef96", "VivekBlogger", "408.67", "PAYPAL", "XMR", "NZD", 1.53],
["acf18d26-bd10-4236-b18d-26bd10723645", "MCWILSON700", "514.22", "PAYPAL", "XMR", "NZD", 1.92],
],
"PLN": [
["88427811-1651-4fb1-8278-1116517fb145", "VivekBlogger", "1073.80", "PAYPAL", "XMR", "PLN", 1.53],
["fa821209-7ca5-45a3-8212-097ca5c5a335", "Moneroeh", "760.90", "CRYPTOCURRENCY", "XMR", "PLN", 1.08],
["939a5d47-8cc7-424c-9a5d-478cc7024cd3", "VivekBlogger", "775.13", "CRYPTOCURRENCY", "XMR", "PLN", 1.1],
["4c87ac82-3ffa-40af-87ac-823ffad0af8f", "KnutValentinee", "923.04", "SEPA", "XMR", "PLN", 1.31],
["f3663e72-12e1-4b87-a63e-7212e1ab87b0", "topmonero", "924.46", "REVOLUT", "XMR", "PLN", 1.31],
["0bc0b5cb-5472-40e6-80b5-cb5472f0e6d8", "yakinikun", "940.82", "PAYPAL", "XMR", "PLN", 1.34],
["61cb9c96-010e-477c-8b9c-96010ef77c23", "EASY", "940.82", "PAYPAL", "XMR", "PLN", 1.34],
["d1711a86-bc5a-41d4-b11a-86bc5aa1d43e", "manascrypto", "995.57", "PAYSAFECARD", "XMR", "PLN", 1.42],
],
"CHF": [
["14a42f88-96c7-4be1-a42f-8896c7fbe19c", "Moneroeh", "178.45", "CRYPTOCURRENCY", "XMR", "CHF", 1.09],
["501011fd-5f12-467c-9011-fd5f12867c30", "Moneroeh", "178.45", "CRYPTOCURRENCY", "XMR", "CHF", 1.09],
["4028814a-064a-4346-a881-4a064a5346f3", "VivekBlogger", "181.79", "CRYPTOCURRENCY", "XMR", "CHF", 1.11],
["bd49e62e-5413-481e-89e6-2e5413481e1e", "Swisswatcher", "183.46", "NATIONAL_BANK", "XMR", "CHF", 1.12],
["23a9cfb1-37c7-43d0-a9cf-b137c7d3d056", "Swisswatcher", "183.46", "CASH_BY_MAIL", "XMR", "CHF", 1.12],
["9f000a01-d04f-4645-800a-01d04fc64519", "Swisswatcher", "186.79", "OTHER", "XMR", "CHF", 1.14],
["4f1e0bbd-c22c-4c6d-9e0b-bdc22c9c6d45", "XMRCoops", "187.63", "CRYPTOCURRENCY", "XMR", "CHF", 1.15],
["e704c072-53e9-457f-84c0-7253e9e57f85", "KnutValentinee", "196.97", "SEPA", "XMR", "CHF", 1.2],
["ac05d59a-e7e5-46c6-85d5-9ae7e516c698", "Dax", "208.00", "NATIONAL_BANK", "XMR", "CHF", 1.27],
["db3e9fe7-63f4-4ff7-be9f-e763f4aff7e5", "Dax", "209.00", "CREDITCARD", "XMR", "CHF", 1.28],
["89ee1335-7ddc-4d5a-ae13-357ddc7d5a14", "NuBIt", "210.00", "NATIONAL_BANK", "XMR", "CHF", 1.28],
["8b360e7d-ec02-415c-b60e-7dec02e15cfe", "VivekBlogger", "210.14", "WU", "XMR", "CHF", 1.28],
["2bcfb7a6-7ba6-4ea5-8fb7-a67ba69ea59f", "topmonero", "216.81", "REVOLUT", "XMR", "CHF", 1.32],
["a31256b5-03ae-4ecd-9256-b503ae8ecdcb", "yakinikun", "220.65", "PAYPAL", "XMR", "CHF", 1.35],
["6646ffef-9300-4efb-86ff-ef93007efb1d", "EASY", "220.65", "PAYPAL", "XMR", "CHF", 1.35],
["353ce186-1732-4010-bce1-86173280102f", "VivekBlogger", "251.84", "PAYPAL", "XMR", "CHF", 1.54],
["927700e4-0258-4b98-b700-e40258fb98db", "Swisswatcher", "44345.48", "OTHER", "BTC", "CHF", 1.1],
["2c836d2c-b052-48fe-836d-2cb05258fefa", "Swisswatcher", "44748.63", "MOBILE_TOP_UP", "BTC", "CHF", 1.11],
["af3d12c3-8dde-4e32-bd12-c38dde9e32ad", "Dax", "59500.00", "NATIONAL_BANK", "BTC", "CHF", 1.48],
],
"ZAR": [
["be5cd08c-b885-4ce7-9cd0-8cb8859ce72c", "Moneroeh", "2912.69", "CRYPTOCURRENCY", "XMR", "ZAR", 1.06],
["7b93d58b-7721-45be-93d5-8b772115bed3", "topmonero", "3538.78", "REVOLUT", "XMR", "ZAR", 1.28],
],
"USD": [
["930011f4-b81b-4482-8011-f4b81b748212", "akz55", "179.81", "CASH_BY_MAIL", "XMR", "USD", 1.01],
["89080ba9-44ba-48f2-880b-a944bae8f26b", "Shazi", "181.61", "SQUARE_CASH", "XMR", "USD", 1.02],
["4f84daed-d692-486d-84da-edd692586d06", "intcryptominer", "183.41", "CASH_BY_MAIL", "XMR", "USD", 1.03],
["7866cf04-3817-4c9e-a6cf-0438170c9ef2", "Hakhlaque", "184.25", "CRYPTOCURRENCY", "XMR", "USD", 1.04],
["5aadfcbe-4102-4df5-adfc-be4102adf54a", "shenyun", "185.20", "CASH_BY_MAIL", "XMR", "USD", 1.05],
["5188679c-149e-442a-8867-9c149e042a17", "opticbit", "185.38", "CASH_BY_MAIL", "XMR", "USD", 1.05],
["11ec65d6-2b17-4df5-ac65-d62b17ddf5ad", "Hakhlaque", "186.62", "CRYPTOCURRENCY", "XMR", "USD", 1.05],
["b1cf5796-82f5-49fc-8f57-9682f569fcd7", "abitofcoin", "186.79", "CASH_BY_MAIL", "XMR", "USD", 1.05],
["de4f061f-7c32-4061-8f06-1f7c3220611f", "CryptoBismol", "187.00", "NATIONAL_BANK", "XMR", "USD", 1.06],
["6e93daa5-b807-479b-93da-a5b807179b07", "CryptoBismol", "187.00", "VENMO", "XMR", "USD", 1.06],
["6afb14d0-ba95-4c7f-bb14-d0ba95dc7f57", "Select", "187.34", "CRYPTOCURRENCY", "XMR", "USD", 1.06],
["70f557d8-4430-44bc-b557-d8443054bc12", "opticbit", "188.80", "CRYPTOCURRENCY", "XMR", "USD", 1.07],
["1bdedbac-ca32-4f5c-9edb-acca321f5ce8", "Moneroeh", "188.80", "CRYPTOCURRENCY", "XMR", "USD", 1.07],
["32d05c38-e2ff-4c23-905c-38e2ff0c234b", "xmr36d", "188.80", "CASH_BY_MAIL", "XMR", "USD", 1.07],
["263ae794-9d12-4d42-bae7-949d12cd428b", "MoneroMan01", "188.80", "CASH_BY_MAIL", "XMR", "USD", 1.07],
["97a805cc-3f4b-422f-a805-cc3f4b722f20", "dscotese", "189.08", "CASH_BY_MAIL", "XMR", "USD", 1.07],
["ba73358c-0c43-428a-b335-8c0c43528ae5", "scottemick", "189.71", "CASH_BY_MAIL", "XMR", "USD", 1.07],
["17d76cba-6422-4457-976c-ba6422645792", "abitofcoin", "190.58", "CASH_DEPOSIT", "XMR", "USD", 1.08],
["8769171c-6e1d-4f85-a917-1c6e1dcf859e", "Chicks", "190.60", "WU", "XMR", "USD", 1.08],
["b91615fd-3355-44b9-9615-fd335524b983", "XMRCoops", "192.38", "OTHER", "XMR", "USD", 1.09],
["415ad6fb-20ea-4ca9-9ad6-fb20eacca914", "XMRCoops", "192.38", "OTHER", "XMR", "USD", 1.09],
["909785c9-6614-45b9-9785-c96614c5b94d", "XMRCoops", "192.38", "OTHER", "XMR", "USD", 1.09],
["355a5f41-5a8a-4647-9a5f-415a8a164720", "XMRCoops", "192.38", "OTHER", "XMR", "USD", 1.09],
["7e6fac17-947b-49fa-afac-17947ba9faf2", "Moneroeh", "192.40", "CRYPTOCURRENCY", "XMR", "USD", 1.09],
["0a6c0733-bf5e-4a9c-ac07-33bf5eba9ca2", "Moneroeh", "192.40", "CRYPTOCURRENCY", "XMR", "USD", 1.09],
["bf9aa452-0245-4c84-9aa4-5202456c8463", "Chicks", "192.40", "CASH_BY_MAIL", "XMR", "USD", 1.09],
["b74ada20-fe3d-4e53-8ada-20fe3dce53b3", "xmr4eva", "193.30", "CASH_BY_MAIL", "XMR", "USD", 1.09],
["aa645d9e-48c5-410d-a45d-9e48c5c10dd7", "Select", "195.96", "CRYPTOCURRENCY", "XMR", "USD", 1.11],
["ea13ccde-b65a-4d2e-93cc-deb65a5d2e59", "VivekBlogger", "195.99", "CRYPTOCURRENCY", "XMR", "USD", 1.11],
["24af83d6-e656-4fdb-af83-d6e656bfdbe5", "cypherist", "197.43", "ZELLE", "XMR", "USD", 1.11],
["eb95a37c-9adb-46c5-95a3-7c9adb76c503", "wiefix", "197.65", "ZELLE", "XMR", "USD", 1.12],
["8b4c7b35-acb6-4355-8c7b-35acb673558b", "Swisswatcher", "197.79", "PERFECT_MONEY", "XMR", "USD", 1.12],
["348290f8-2428-48b1-8290-f8242888b192", "cypherist", "197.79", "REVOLUT", "XMR", "USD", 1.12],
["e671d6ac-43e7-48ba-b1d6-ac43e728ba49", "xmrtoad", "197.79", "CASH_BY_MAIL", "XMR", "USD", 1.12],
["2297d690-fc9d-4e3f-97d6-90fc9d0e3f89", "cypherist", "199.23", "APPLE_PAY", "XMR", "USD", 1.12],
["5ec23e3f-3e9d-45dc-823e-3f3e9d35dc53", "cypherist", "199.41", "GOOGLEWALLET", "XMR", "USD", 1.13],
["17a55c43-0055-4c8a-a55c-4300550c8af1", "Kdmccoy529", "199.59", "ZELLE", "XMR", "USD", 1.13],
["f8935f6b-1537-48e4-935f-6b153748e476", "edk", "200.00", "WU", "XMR", "USD", 1.13],
["faa2a43b-c129-408a-a2a4-3bc129f08aaf", "lightlyanonymous", "201.39", "CASH_BY_MAIL", "XMR", "USD", 1.14],
["5195aebd-0978-436e-95ae-bd0978a36eda", "xmr4eva", "201.39", "CASH_DEPOSIT", "XMR", "USD", 1.14],
["b6416c55-8779-4336-816c-558779a3366a", "XMRCoops", "202.29", "CRYPTOCURRENCY", "XMR", "USD", 1.14],
["81b75d2c-5444-46a6-b75d-2c5444f6a63c", "cypherist", "204.98", "SERVE2SERVE", "XMR", "USD", 1.16],
["dfe30c2f-3d44-43f5-a30c-2f3d4473f539", "Swisswatcher", "206.78", "WU", "XMR", "USD", 1.17],
["bae2defc-7b9b-462a-a2de-fc7b9ba62ad2", "SecureMole", "206.78", "NATIONAL_BANK", "XMR", "USD", 1.17],
["220b926c-5b84-42f2-8b92-6c5b8432f282", "Swisswatcher", "206.78", "OTHER", "XMR", "USD", 1.17],
["e2368d44-a32c-429e-b68d-44a32cd29e39", "Kdmccoy529", "210.02", "SQUARE_CASH", "XMR", "USD", 1.19],
["cac4a3b0-4a66-47da-84a3-b04a6637da6f", "Shazi", "215.00", "SQUARE_CASH", "XMR", "USD", 1.21],
["4e96acd6-0047-489a-96ac-d60047489a08", "MCWILSON700", "215.77", "REMITLY", "XMR", "USD", 1.22],
["2fa5891f-f9f8-4d61-a589-1ff9f80d6190", "OliverFerret", "215.77", "ZELLE", "XMR", "USD", 1.22],
["6ccba46d-1fc4-4dec-8ba4-6d1fc43decc3", "19900518", "215.77", "WEBMONEY", "XMR", "USD", 1.22],
["b3624e09-db17-466f-a24e-09db17466f6e", "OliverFerret", "215.77", "SQUARE_CASH", "XMR", "USD", 1.22],
["8e130946-7823-4d1c-9309-4678230d1c45", "chaslopz", "215.77", "SQUARE_CASH", "XMR", "USD", 1.22],
["111de00e-ffb7-4e6a-9de0-0effb7ce6a46", "Bitcapital", "215.77", "CASH_DEPOSIT", "XMR", "USD", 1.22],
["467555f7-3a4e-4e72-b555-f73a4eae723a", "KnutValentinee", "217.57", "INTERNATIONAL_WIRE_SWIFT", "XMR", "USD", 1.23],
["1ed73e7a-8e5f-4986-973e-7a8e5fb9863b", "Pellerin", "217.57", "CREDITCARD", "XMR", "USD", 1.23],
["5b9d2429-43b2-42eb-9d24-2943b2e2eb13", "Select", "218.43", "PAYPAL", "XMR", "USD", 1.23],
["84923585-32dd-452e-9235-8532dd452eb6", "SecureMole", "218.43", "PAYPAL", "XMR", "USD", 1.23],
["3dc33639-d7db-4fad-8336-39d7dbdfad91", "SouthSalez", "220.27", "SQUARE_CASH", "XMR", "USD", 1.24],
["5a8f07bc-05f5-40c4-8f07-bc05f530c4d7", "Markantonio", "221.17", "TRANSFERWISE", "XMR", "USD", 1.25],
["2420235d-fd97-4097-a023-5dfd97f097e1", "SecureMole", "221.17", "TRANSFERWISE", "XMR", "USD", 1.25],
["7e718590-0d24-408c-b185-900d24208ccc", "Bitcapital", "221.17", "NATIONAL_BANK", "XMR", "USD", 1.25],
["8600ba3a-6aac-43c2-80ba-3a6aacc3c2b5", "yakinikun", "221.71", "PAYPAL", "XMR", "USD", 1.25],
["4d6bbd05-583c-4e7f-abbd-05583c8e7f2f", "EASY", "221.71", "PAYPAL", "XMR", "USD", 1.25],
["18ff526f-7f3e-4aec-bf52-6f7f3e5aec2d", "Markantonio", "222.07", "PAYPAL", "XMR", "USD", 1.25],
["6012cabd-d618-4c37-92ca-bdd6186c37e0", "Bitpal", "222.41", "PAYPAL", "XMR", "USD", 1.26],
["a29da647-5870-4f39-9da6-4758704f39b0", "yakinikun", "222.96", "PAYPAL", "XMR", "USD", 1.26],
["2572e85a-980b-49c3-b2e8-5a980bb9c3b1", "Markantonio", "222.96", "WORLDREMIT", "XMR", "USD", 1.26],
["b5f80385-73cb-4f98-b803-8573cb6f9846", "SecureMole", "224.08", "REVOLUT", "XMR", "USD", 1.26],
["2f767f92-f1bd-4e3e-b67f-92f1bd2e3ed8", "topmonero", "224.08", "REVOLUT", "XMR", "USD", 1.26],
["6ca63cef-783b-40cd-a63c-ef783b90cdc7", "topmonero", "224.08", "REVOLUT", "XMR", "USD", 1.26],
["2361f270-6325-4aaf-a1f2-706325baaf57", "jeffguy", "224.58", "REVOLUT", "XMR", "USD", 1.27],
["dd8c2ad6-2f7d-4fdf-8c2a-d62f7d5fdfd4", "Bitpal", "224.76", "MONEYBOOKERS", "XMR", "USD", 1.27],
["d1eac0cd-6219-4bc9-aac0-cd62198bc9b2", "19900518", "224.76", "MONEYGRAM", "XMR", "USD", 1.27],
["a38915a4-eb27-4d84-8915-a4eb276d84d1", "19900518", "224.76", "GIFT_CARD_CODE_GLOBAL", "XMR", "USD", 1.27],
["4ea2fcd6-95cb-4d7a-a2fc-d695cbed7a8c", "Bitcapital", "224.76", "CASH_BY_MAIL", "XMR", "USD", 1.27],
["b66ff938-ed1e-485e-aff9-38ed1e285e68", "Bitcapital", "224.76", "CASHIERS_CHECK", "XMR", "USD", 1.27],
["bc092047-3b32-4f38-8920-473b32ff3849", "VivekBlogger", "226.56", "WU", "XMR", "USD", 1.28],
["00974890-254d-4fcd-9748-90254d4fcd8f", "SriHari", "228.18", "MONEYGRAM", "XMR", "USD", 1.29],
["07eaf980-14db-41f7-aaf9-8014db21f71e", "Markantonio", "228.36", "XOOM", "XMR", "USD", 1.29],
["7999f97e-f93d-4a24-99f9-7ef93dba24a9", "COMPRATUDO", "233.75", "CRYPTOCURRENCY", "XMR", "USD", 1.32],
["6631d344-baed-4378-b1d3-44baedc378bc", "COMPRATUDO", "233.75", "CRYPTOCURRENCY", "XMR", "USD", 1.32],
["f535dbdd-7a7a-4c0f-b5db-dd7a7a4c0fe5", "Sbudubuda", "242.74", "PAYPAL", "XMR", "USD", 1.37],
["29942c59-73ef-4440-942c-5973ef84400e", "strawberries", "269.72", "CRYPTOCURRENCY", "XMR", "USD", 1.52],
["353dc0a9-c147-4a2a-bdc0-a9c1474a2a38", "VivekBlogger", "271.51", "PAYPAL", "XMR", "USD", 1.53],
["584bb34c-6960-4a1a-8bb3-4c69606a1a38", "VivekBlogger", "271.51", "PAYPAL", "XMR", "USD", 1.53],
["959cbb44-eff2-42a7-9cbb-44eff292a7bc", "VivekBlogger", "271.51", "PAYPAL", "XMR", "USD", 1.53],
["4e8c96d5-b803-4e01-8c96-d5b803fe0176", "VivekBlogger", "271.51", "PAYPAL", "XMR", "USD", 1.53],
["2f0bd5b5-495c-4b06-8bd5-b5495cdb06dd", "VivekBlogger", "271.51", "PAYPAL", "XMR", "USD", 1.53],
["6c576caa-c5b5-4308-976c-aac5b5c308a1", "luca_babulli", "295.66", "CREDITCARD", "XMR", "USD", 1.67],
["8dd5fe2d-157f-42ab-95fe-2d157f72ab6c", "STEELROB916", "314.67", "SQUARE_CASH", "XMR", "USD", 1.78],
["f59fb82d-6d80-4774-9fb8-2d6d80d77443", "Dax", "315.00", "INTERNATIONAL_WIRE_SWIFT", "XMR", "USD", 1.78],
["a75da905-1a50-449c-9da9-051a50f49c68", "burpMonero", "336.00", "OTHER", "XMR", "USD", 1.9],
["4ab1ecfd-04f7-4baf-b1ec-fd04f7abafdf", "MCWILSON700", "341.64", "PAYPAL", "XMR", "USD", 1.93],
["7570836e-eb2c-4831-b083-6eeb2cc83147", "MCWILSON700", "341.64", "XOOM", "XMR", "USD", 1.93],
["74efb4d0-27b4-4dcf-afb4-d027b4edcfab", "pannuo", "449.52", "VANILLA", "XMR", "USD", 2.54],
["8446c3f3-369f-446a-86c3-f3369fd46ad8", "blackoct", "43464.00", "CRYPTOCURRENCY", "BTC", "USD", 1.0],
["a691d616-f91b-4461-91d6-16f91b046198", "51mesa", "44550.60", "CASH_BY_MAIL", "BTC", "USD", 1.02],
["d8591935-bca3-426a-9919-35bca3026af5", "shenyun", "44767.92", "CASH_BY_MAIL", "BTC", "USD", 1.03],
["7d562f0b-35d7-47d3-962f-0b35d787d350", "opticbit", "44811.38", "CASH_BY_MAIL", "BTC", "USD", 1.03],
["777f7aee-429f-48d9-bf7a-ee429f08d979", "abitofcoin", "45115.63", "CASH_BY_MAIL", "BTC", "USD", 1.03],
["12515237-73f4-4906-9152-3773f419065a", "VermontCrypto", "45202.56", "CASH_BY_MAIL", "BTC", "USD", 1.04],
["7c4ff6e5-de65-4ea0-8ff6-e5de653ea0c0", "opticbit", "45680.66", "CRYPTOCURRENCY", "BTC", "USD", 1.05],
["5654de11-8ffc-4782-94de-118ffcb782ea", "dscotese", "45991.30", "CASH_BY_MAIL", "BTC", "USD", 1.05],
["3a29d630-8f6e-4a43-a9d6-308f6e1a43be", "abitofcoin", "46067.49", "CASH_DEPOSIT", "BTC", "USD", 1.06],
["35e823fb-4163-4345-a823-fb4163534539", "Chicks", "46506.48", "CASH_BY_MAIL", "BTC", "USD", 1.07],
["7730b01f-b0b5-455b-b0b0-1fb0b5855b92", "LysanderSpooner", "46936.77", "STRIKE", "BTC", "USD", 1.08],
["9668ac27-5b43-4452-a8ac-275b433452f2", "scottemick", "46941.12", "CASH_BY_MAIL", "BTC", "USD", 1.08],
["7a8c8e1a-6fab-4051-8c8e-1a6fab80517b", "EmmanuelMuema", "47797.36", "CRYPTOCURRENCY", "BTC", "USD", 1.1],
["face155f-5c06-43dc-8e15-5f5c0653dc08", "EmmanuelMuema", "47801.71", "CRYPTOCURRENCY", "BTC", "USD", 1.1],
["19496677-27a7-4ca5-8966-7727a74ca5b4", "Select", "47806.05", "CRYPTOCURRENCY", "BTC", "USD", 1.1],
["e4d02cf6-adac-4e1a-902c-f6adac1e1a71", "Swisswatcher", "50418.24", "WU", "BTC", "USD", 1.16],
["b1c5c47a-c928-4f22-85c4-7ac9285f22c5", "19900518", "52156.80", "WEBMONEY", "BTC", "USD", 1.2],
["e1446a95-cb9e-4175-846a-95cb9e417548", "OliverFerret", "52156.80", "SQUARE_CASH", "BTC", "USD", 1.2],
["520807a4-d67e-49ae-8807-a4d67e99aeb0", "SouthSalez", "52834.84", "SQUARE_CASH", "BTC", "USD", 1.21],
["22e720f6-8c21-438b-a720-f68c21138bf5", "Bitcapital", "53460.72", "CASHIERS_CHECK", "BTC", "USD", 1.23],
["37570fae-6606-4d6b-970f-ae66067d6bce", "Bitcapital", "53460.72", "NATIONAL_BANK", "BTC", "USD", 1.23],
["76fefea3-097b-4466-befe-a3097b7466ae", "19900518", "54330.00", "GIFT_CARD_CODE_GLOBAL", "BTC", "USD", 1.24],
["7a17b641-60d0-4e69-97b6-4160d0ae692b", "Kdmccoy529", "54330.00", "ZELLE", "BTC", "USD", 1.24],
["c2df24b3-3681-44f5-9f24-b3368194f52e", "Bitcapital", "54330.00", "CASH_BY_MAIL", "BTC", "USD", 1.24],
["a36aa1c1-67f2-4d9b-aaa1-c167f27d9bbb", "Bitcapital", "54330.00", "CASH_DEPOSIT", "BTC", "USD", 1.24],
["7e179959-8eae-4b8e-9799-598eaeab8e1c", "EmmanuelMuema", "55625.23", "PAYPAL", "BTC", "USD", 1.27],
["33a94c12-378a-49f6-a94c-12378a89f667", "EmmanuelMuema", "55629.57", "PAYPAL", "BTC", "USD", 1.27],
["93e45e67-8f47-415b-a45e-678f47715b93", "EmmanuelMuema", "56420.62", "XOOM", "BTC", "USD", 1.29],
["ddb3dd89-9bc2-4c83-b3dd-899bc20c83da", "Legitworld", "56937.84", "WORLDREMIT", "BTC", "USD", 1.3],
["152a8104-88bd-4d50-aa81-0488bdad508c", "Legitworld", "56937.84", "SQUARE_CASH", "BTC", "USD", 1.3],
["05d18c04-85f0-43e0-918c-0485f083e0e8", "Crypto_Hood", "57368.13", "SQUARE_CASH", "BTC", "USD", 1.31],
["ae4319fe-6af5-413d-8319-fe6af5813da6", "Kdmccoy529", "57372.48", "SQUARE_CASH", "BTC", "USD", 1.31],
["a8ecab22-caa0-4cf9-acab-22caa01cf91a", "Crypto_Hood", "58676.40", "SQUARE_CASH", "BTC", "USD", 1.34],
["cfcea560-2898-4c6c-8ea5-6028980c6c0f", "Crypto_Hood", "63022.80", "PAYPAL", "BTC", "USD", 1.44],
["f5e220f6-002a-453c-a220-f6002a253c0c", "Dax", "76809.00", "INTERNATIONAL_WIRE_SWIFT", "BTC", "USD", 1.76],
],
"THB": [
["628822d9-674a-4d90-8822-d9674abd90e1", "Moneroeh", "6288.97", "CRYPTOCURRENCY", "XMR", "THB", 1.08],
["82423582-fe58-432d-8235-82fe58f32d0f", "topmonero", "7640.80", "REVOLUT", "XMR", "THB", 1.31],
["a9a9f2ff-7fc9-4823-a9f2-ff7fc93823da", "EASY", "7775.98", "PAYPAL", "XMR", "THB", 1.33],
["ba577351-9024-435c-9773-519024a35ce5", "yakinikun", "7775.98", "PAYPAL", "XMR", "THB", 1.33],
],
"RUB": [
["0a46bc2a-0808-4aec-86bc-2a0808daec4a", "Moneroeh", "14415.71", "CRYPTOCURRENCY", "XMR", "RUB", 1.08],
["4425627e-bc7c-4973-a562-7ebc7c09732f", "Sieterayos", "14819.89", "SPECIFIC_BANK", "XMR", "RUB", 1.11],
["f479ff19-1303-4c90-b9ff-1913031c9074", "YuriyLavrentiev", "15224.06", "NATIONAL_BANK", "XMR", "RUB", 1.14],
["2caa4afa-a1c7-4683-aa4a-faa1c7a683dc", "topmonero", "17514.41", "REVOLUT", "XMR", "RUB", 1.31],
["37dcf4ed-a74a-4767-9cf4-eda74a776700", "COMPRATUDO", "17514.41", "CRYPTOCURRENCY", "XMR", "RUB", 1.31],
["87398967-6291-41aa-b989-676291e1aa28", "EASY", "17824.28", "PAYPAL", "XMR", "RUB", 1.33],
["7e101259-153f-4305-9012-59153fe305c4", "yakinikun", "17824.28", "PAYPAL", "XMR", "RUB", 1.33],
["b9463aa1-f6e4-4207-863a-a1f6e4d207da", "XMRCoops", "18188.04", "CRYPTOCURRENCY", "XMR", "RUB", 1.36],
["2701a6db-0d2c-4e24-81a6-db0d2cae2492", "piknik86", "22700.00", "OTHER", "XMR", "RUB", 1.69],
["4ad0457a-13d3-4605-9045-7a13d3e605b4", "strawberries", "22903.46", "CRYPTOCURRENCY", "XMR", "RUB", 1.71],
["3874622d-f1f9-450d-b462-2df1f9c50d4c", "strawberries", "22903.46", "NATIONAL_BANK", "XMR", "RUB", 1.71],
["2e41b954-4295-4791-81b9-54429587912d", "strawberries", "22903.46", "SPECIFIC_BANK", "XMR", "RUB", 1.71],
["1ed29864-fe20-4ebb-9298-64fe20debbb0", "orangeline", "22903.46", "SPECIFIC_BANK", "XMR", "RUB", 1.71],
["a5aba438-64fb-47a8-aba4-3864fba7a81a", "orangeline", "22903.46", "NATIONAL_BANK", "XMR", "RUB", 1.71],
["09e2bc20-4452-4dcf-a2bc-2044526dcf6c", "yura023", "3370609.08", "NATIONAL_BANK", "BTC", "RUB", 1.02],
["14d8cc43-5bce-41a3-98cc-435bcef1a390", "strawberries", "4233615.27", "SPECIFIC_BANK", "BTC", "RUB", 1.28],
["111758c3-7225-449c-9758-c37225a49c27", "strawberries", "4233615.27", "NATIONAL_BANK", "BTC", "RUB", 1.28],
],
"TRY": [
["0d3d707e-6ee2-4cc0-bd70-7e6ee23cc04a", "Moneroeh", "2605.06", "CRYPTOCURRENCY", "XMR", "TRY", 1.08],
["5450ebe9-49ac-4917-90eb-e949ac4917b9", "Horixon", "2629.41", "NATIONAL_BANK", "XMR", "TRY", 1.09],
["dbe8d37b-bbcc-40f1-a8d3-7bbbcca0f141", "Horixon", "2629.41", "CASH_DEPOSIT", "XMR", "TRY", 1.09],
["e028af52-0c6c-45d1-a8af-520c6cf5d17e", "Horixon", "2653.75", "CASH_DEPOSIT", "XMR", "TRY", 1.1],
["853e2190-dfd9-400f-be21-90dfd9300fba", "Horixon", "2653.75", "OTHER", "XMR", "TRY", 1.1],
["2b202548-6bab-4231-a025-486bab8231bc", "Horixon", "2726.79", "OTHER", "XMR", "TRY", 1.13],
["4a651892-7432-477e-a518-927432277ef6", "KnutValentinee", "2921.56", "INTERNATIONAL_WIRE_SWIFT", "XMR", "TRY", 1.21],
["929450a0-9a86-4133-9450-a09a86613363", "topmonero", "3165.03", "REVOLUT", "XMR", "TRY", 1.31],
],
"CZK": [
["eb425203-49b7-4b66-8252-0349b7bb66d7", "Moneroeh", "4115.56", "CRYPTOCURRENCY", "XMR", "CZK", 1.1],
["a2029b29-8136-48eb-829b-298136e8eb0a", "KnutValentinee", "4807.89", "SEPA", "XMR", "CZK", 1.28],
["3bc93ad9-bc51-4939-893a-d9bc51e9395a", "topmonero", "5000.21", "REVOLUT", "XMR", "CZK", 1.33],
["c944467d-c318-42d9-8446-7dc31812d9d4", "yakinikun", "5088.68", "PAYPAL", "XMR", "CZK", 1.36],
["d9612937-11c8-4911-a129-3711c879118e", "EASY", "5088.68", "PAYPAL", "XMR", "CZK", 1.36],
["167232e5-70d6-414a-b232-e570d6814a6a", "VivekBlogger", "5807.94", "PAYPAL", "XMR", "CZK", 1.55],
],
}

File diff suppressed because one or more lines are too long

227
handler/tests/test_agora.py Normal file
View File

@ -0,0 +1,227 @@
from unittest import TestCase
from unittest.mock import MagicMock, patch
from json import loads
from copy import deepcopy
from tests.common import fake_public_ads, cg_prices, expected_to_update
from agora import Agora
from markets import Markets
from money import Money
import util
class TestAgora(TestCase):
def __init__(self, *args, **kwargs):
self.test_return_data = {}
with open("tests/data/api_call_ads_return.json", "r") as f:
for line in f.readlines():
parsed = loads(line)
self.test_return_data[(parsed[2], parsed[1], str(parsed[0]))] = parsed[3]
super().__init__(*args, *kwargs)
def setUp(self):
self.markets = Markets()
self.agora = Agora()
self.money = Money()
setattr(self.agora, "markets", self.markets)
setattr(self.money, "markets", self.markets)
setattr(self.agora, "money", self.money)
self.all_providers = [
"XOOM",
"CRYPTOCURRENCY",
"VIPPS",
"PAYSAFECARD",
"PAYPAL",
"WU",
"SQUARE_CASH",
"CASH_DEPOSIT",
"ADVCASH",
"TRANSFERWISE",
"GIFT_CARD_CODE_GLOBAL",
"NETELLER",
"INTERNATIONAL_WIRE_SWIFT",
"CASH_BY_MAIL",
"SEPA",
"OTHER",
"REVOLUT",
"NATIONAL_BANK",
"MONEYBOOKERS",
"CREDITCARD",
"APPLE_PAY",
"ZELLE",
"PERFECT_MONEY",
"CASHIERS_CHECK",
"GOOGLEWALLET",
"STRIKE",
"SPECIFIC_BANK",
"CHIPPER_CASH",
"REMITLY",
"WORLDREMIT",
"PAYEER",
"MOBILE_TOP_UP",
"VIRTUAL_VISA_MASTERCARD",
"VANILLA",
"MONEYGRAM",
"VENMO",
"SERVE2SERVE",
"WEBMONEY",
]
def mock_enum_public_ads_api_call(self, api_method, query_values):
if "buy-monero-online" in api_method:
asset = "XMR"
elif "buy-bitcoins-online" in api_method:
asset = "BTC"
spl = api_method.split("/")
currency = spl[1]
page = str(query_values["page"])
return self.test_return_data[(asset, currency, page)]
def test_get_all_public_ads(self):
# Override enum_public_ads
self.agora.agora._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
# Override get_price
self.agora.cg.get_price = MagicMock()
self.agora.cg.get_price.return_value = cg_prices
self.agora.markets.get_all_providers = MagicMock()
self.agora.markets.get_all_providers.return_value = self.all_providers
public_ads = self.agora.get_all_public_ads()
self.assertDictEqual(public_ads, fake_public_ads)
for currency, ads in public_ads.items():
ad_ids = [ad[0] for ad in ads]
ad_ids_dedup = set(ad_ids)
# Make sure there's no duplicate ads
self.assertEqual(len(ad_ids), len(ad_ids_dedup))
@patch("twisted.internet.threads.deferToThread")
def test_run_cheat_in_thread(self, defer):
asset1 = self.agora.run_cheat_in_thread()
asset2 = self.agora.run_cheat_in_thread()
self.assertEqual(set([asset1, asset2]), set(["XMR", "BTC"]))
asset3 = self.agora.run_cheat_in_thread()
asset4 = self.agora.run_cheat_in_thread()
self.assertEqual(set([asset3, asset4]), set(["XMR", "BTC"]))
self.assertNotEqual(asset1, asset2)
self.assertNotEqual(asset3, asset4)
def test_update_prices(self):
# Override enum_public_ads
self.agora.agora._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
# Override get_price
self.agora.cg.get_price = MagicMock()
self.agora.cg.get_price.return_value = cg_prices
self.agora.slow_ad_update = MagicMock()
self.agora.update_prices()
call_args = self.agora.slow_ad_update.call_args_list[0][0][0]
self.assertCountEqual(call_args, expected_to_update)
def test_enum_public_ads(self):
# Override enum_public_ads
self.agora.agora._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
enum_ads_return = self.agora.enum_public_ads("XMR", "USD", self.all_providers)
# Ensure there are no duplicates
enum_ads_return_ids = [(x[0], x[1], x[2], x[3], x[4], x[5]) for x in enum_ads_return]
enum_ads_return_ids_dedup = set(enum_ads_return_ids)
self.assertEqual(len(enum_ads_return_ids), len(enum_ads_return_ids_dedup))
expected_return = []
# ['94b399e2-2c96-480c-b399-e22c96180cf2', 'Jorge', '272.00', 'SEPA', 'XMR', 'USD']
for asset, currency, page in self.test_return_data:
if not asset == "XMR":
continue
if not currency == "USD":
continue
content = self.test_return_data[(asset, currency, page)]
ads = content["response"]["data"]["ad_list"]
for ad in ads:
ad_id = ad["data"]["ad_id"]
username = ad["data"]["profile"]["username"]
temp_price = ad["data"]["temp_price"]
provider = ad["data"]["online_provider"]
asset = "XMR"
currency = ad["data"]["currency"]
to_append = [ad_id, username, temp_price, provider, asset, currency]
if to_append not in expected_return:
expected_return.append(to_append)
self.assertCountEqual(enum_ads_return, expected_return)
self.assertNotEqual(enum_ads_return[0][0], enum_ads_return[1][0])
ad_ids = [x[0] for x in enum_ads_return]
ad_ids_dedup = set(ad_ids)
self.assertEqual(len(ad_ids), len(ad_ids_dedup))
def test_lookup_rates(self):
# Override enum_public_ads
self.agora.agora._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
# Override get_price
self.money.cg.get_price = MagicMock()
self.money.cg.get_price.return_value = cg_prices
enum_ads_return = self.agora.enum_public_ads("XMR", "USD", self.all_providers)
expected_return = []
# Let's manually calculate what it's supposed to look like
price_xmr = cg_prices["monero"]["usd"]
for ad in deepcopy(enum_ads_return):
price = float(ad[2])
margin = round(price / price_xmr, 2)
ad.append(margin)
expected_return.append(ad)
lookup_rates_return = self.agora.money.lookup_rates(enum_ads_return) # TODO: do this properly
self.assertCountEqual(lookup_rates_return, expected_return)
def test_lookup_rates_not_usd(self):
"""
Above test only tests USD which does not take into account Forex.
Let's test both, and additionaly specify our own rates.
"""
# Override enum_public_ads
self.agora.agora._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
# Override get_price
self.agora.cg.get_price = MagicMock()
self.agora.cg.get_price.return_value = cg_prices
enum_ads_return = self.agora.enum_public_ads("XMR", "EUR", self.all_providers)
expected_return = []
# Let's manually calculate what it's supposed to look like
price_xmr = cg_prices["monero"]["eur"]
for ad in deepcopy(enum_ads_return):
price = float(ad[2])
margin = round(price / price_xmr, 2)
ad.append(margin)
expected_return.append(ad)
# Test specifying rates=
lookup_rates_return = self.agora.money.lookup_rates(enum_ads_return, rates=cg_prices)
self.assertCountEqual(lookup_rates_return, expected_return)

View File

@ -0,0 +1,38 @@
from unittest import TestCase
from tests.common import fake_public_ads, expected_to_update
from markets import Markets
from agora import Agora
class TestMarkets(TestCase):
def setUp(self):
self.markets = Markets()
self.agora = Agora()
def test_autoprice(self):
ads = [
["2b6dba4d-c9db-48f2-adba-4dc9dba8f2a0", "Xpoterlolipop", "182.80", "REVOLUT", "XMR", "USD", 1.18],
["57e3e8d6-45fe-40da-a3e8-d645fe20da46", "SecureMole", "183.26", "REVOLUT", "XMR", "USD", 1.19],
["87af6467-be02-476e-af64-67be02676e9a", "topmonero", "183.42", "REVOLUT", "XMR", "USD", 1.19],
["65b452e3-a29f-4233-b452-e3a29fe23369", "topmonero", "183.42", "REVOLUT", "XMR", "USD", 1.19],
["d2c6645c-6d56-4094-8664-5c6d5640941b", "topmonero", "183.42", "REVOLUT", "XMR", "USD", 1.19],
]
currency = "EUR"
margin = self.markets.autoprice(ads, currency)
expected_margin = 1.18
self.assertEqual(margin, expected_margin)
def test_get_new_ad_equation(self):
to_update = self.markets.get_new_ad_equations(fake_public_ads)
self.assertCountEqual(to_update, expected_to_update)
res_xmr = self.markets.get_new_ad_equations(fake_public_ads, ["XMR"])
expected_xmr_to_update = [x for x in expected_to_update if x[2] == "XMR"]
self.assertCountEqual(res_xmr, expected_xmr_to_update)
res_btc = self.markets.get_new_ad_equations(fake_public_ads, ["BTC"])
expected_btc_to_update = [x for x in expected_to_update if x[2] == "BTC"]
self.assertCountEqual(res_btc, expected_btc_to_update)
res_both = self.markets.get_new_ad_equations(fake_public_ads, ["XMR", "BTC"])
self.assertCountEqual(res_both, expected_to_update)

View File

@ -0,0 +1,23 @@
from unittest import TestCase
from money import Money
class TestMoney(TestCase):
def setUp(self):
self.money = Money()
def test_lookup_rates(self):
# Move from Agora tests
pass
def test_get_rates_all(self):
pass
def test_get_acceptable_margins(self):
pass
def test_to_usd(self):
pass
def test_get_profit(self):
pass

View File

@ -0,0 +1,278 @@
from unittest import TestCase
from unittest.mock import MagicMock
from copy import deepcopy
import transactions
import money
class TestTransactions(TestCase):
def setUp(self):
self.transactions = transactions.Transactions()
self.test_data = {
"event": "TransactionCreated",
"timestamp": "2022-02-24T20:26:15.232342Z",
"data": {
"id": "6217e9e7-43e1-a809-8500-0a5b0170e6e4",
"type": "transfer",
"state": "completed",
"request_id": "8a15213e-a7d2-4738-bfb5-b1d037b75a57",
"created_at": "2022-02-24T20:26:15.238218Z",
"updated_at": "2022-02-24T20:26:15.238218Z",
"completed_at": "2022-02-24T20:26:15.238453Z",
"reference": "TEST-1",
"legs": [
{
"leg_id": "80b35daf-409c-41be-8755-15982b7633a6",
"account_id": "7185593b-d9ad-4456-920e-d9db109a5172",
"counterparty": {"account_type": "revolut"},
"amount": 1,
"currency": "GBP",
"description": "From Mark Veidemanis",
"balance": 3832.3,
}
],
},
}
# Mock redis calls
transactions.r.hgetall = self.mock_hgetall
transactions.r.hmset = self.mock_hmset
transactions.r.keys = self.mock_keys
transactions.r.get = self.mock_get
# Mock some callbacks
self.transactions.irc = MagicMock()
self.transactions.irc.sendmsg = MagicMock()
self.transactions.release_funds = MagicMock()
self.transactions.notify = MagicMock()
self.transactions.notify.notify_complete_trade = MagicMock()
# Mock the rates
self.transactions.money = MagicMock()
self.transactions.money.get_rates_all = MagicMock()
self.transactions.money.get_rates_all.return_value = {"GBP": 0.8}
# Don't mock the functions we want to test
self.money = money.Money()
self.money.get_rates_all = MagicMock()
self.money.get_rates_all.return_value = {"GBP": 0.8}
self.transactions.money.get_acceptable_margins = self.money.get_acceptable_margins
self.trades = {
1: {
"id": "uuid1",
"buyer": "test_buyer_1",
"currency": "GBP",
"amount": "1",
"amount_xmr": "0.3",
"reference": "TEST-1",
},
2: {
"id": "uuid2",
"buyer": "test_buyer_2",
"currency": "GBP",
"amount": "1",
"amount_xmr": "0.3",
"reference": "TEST-2",
},
3: {
"id": "uuid3",
"buyer": "test_buyer_3",
"currency": "GBP",
"amount": "1000",
"amount_xmr": "3",
"reference": "TEST-3",
},
4: {
"id": "uuid4",
"buyer": "test_buyer_4",
"currency": "GBP",
"amount": "10",
"amount_xmr": "0.5",
"reference": "TEST-4",
},
}
self.return_trades = [1, 2, 3]
@property
def test_data_copy(self):
return deepcopy(self.test_data)
def data_custom(self, amount, currency, reference):
test_data = self.test_data_copy
test_data["data"]["reference"] = reference
test_data["data"]["legs"][0]["amount"] = amount
test_data["data"]["legs"][0]["currency"] = currency
return test_data
def mock_hgetall(self, string):
ref = string.split(".")[1]
for num, trade in self.trades.items():
if trade["reference"] == ref:
return trade
def mock_hmset(self, string, data):
print("HMSET", string, data)
def mock_keys(self, string):
return [v["id"] for k, v in self.trades.items() if k in self.return_trades]
def mock_get(self, string):
for num, trade in self.trades.items():
if trade["id"] == string:
return trade["reference"]
def test_transaction(self):
self.transactions.transaction(self.test_data)
self.transactions.release_funds.assert_called_once_with("uuid1", "TEST-1")
self.transactions.release_funds = MagicMock()
ref_2 = self.test_data_copy
ref_2["data"]["reference"] = "TEST-2"
self.transactions.transaction(ref_2)
self.transactions.release_funds.assert_called_once_with("uuid2", "TEST-2")
def test_transaction_invalid(self):
invalid_data = self.test_data_copy
invalid_data = self.data_custom(2000, "SEK", "sss")
self.transactions.transaction(invalid_data)
self.transactions.release_funds.assert_not_called()
def test_transaction_malformed(self):
malformed_data = self.test_data_copy
del malformed_data["data"]
self.transactions.transaction(malformed_data)
self.transactions.release_funds.assert_not_called()
malformed_data = self.test_data_copy
del malformed_data["data"]["type"]
self.transactions.transaction(malformed_data)
self.transactions.release_funds.assert_not_called()
def test_transaction_no_reference_fail(self):
no_reference_fail = self.data_custom(1, "GBP", "none")
no_reference_fail["data"]["reference"] = "none"
self.transactions.transaction(no_reference_fail)
self.transactions.release_funds.assert_not_called()
def test_transaction_no_reference_pass(self):
no_reference_pass = self.data_custom(1, "GBP", "none")
no_reference_pass["data"]["reference"] = "none"
self.return_trades = [1]
self.transactions.transaction(no_reference_pass)
self.transactions.release_funds.assert_called_with("uuid1", "TEST-1")
def test_transaction_large(self):
exceeds_max = self.data_custom(1000, "GBP", "TEST-3")
self.transactions.transaction(exceeds_max)
self.transactions.release_funds.assert_called_once_with("uuid3", "TEST-3")
def test_transaction_no_reference_exceeds_max(self):
exceeds_max = self.data_custom(1000, "GBP", "noref")
self.transactions.transaction(exceeds_max)
self.transactions.release_funds.assert_not_called()
def test_transaction_wrong_currency(self):
wrong_currency = self.data_custom(1, "EUR", "TEST-1")
self.transactions.transaction(wrong_currency)
self.transactions.release_funds.assert_not_called()
wrong_currency = self.data_custom(1, "EUR", "none")
self.transactions.transaction(wrong_currency)
self.transactions.release_funds.assert_not_called()
def test_transaction_wrong_amount(self):
self.transactions.money.get_acceptable_margins = MagicMock()
self.transactions.money.get_acceptable_margins.return_value = (0.8, 1.8)
wrong_amount = self.data_custom(10, "GBP", "TEST-1")
self.transactions.transaction(wrong_amount)
self.transactions.release_funds.assert_not_called()
wrong_amount = self.data_custom(10, "GBP", "none")
self.transactions.transaction(wrong_amount)
self.transactions.release_funds.assert_not_called()
def test_transaction_pending(self):
pending_tx = self.test_data_copy
pending_tx["data"]["state"] = "pending"
self.transactions.transaction(pending_tx)
self.transactions.release_funds.assert_not_called()
def test_transaction_too_low(self):
too_low = self.data_custom(5, "GBP", "TEST-1")
self.transactions.transaction(too_low)
self.transactions.release_funds.assert_not_called()
def test_transaction_too_high(self):
too_high = self.data_custom(15, "GBP", "TEST-1")
self.transactions.transaction(too_high)
self.transactions.release_funds.assert_not_called()
# def test_transaction_pending_then_completed(self):
# pass
def test_transaction_store_incomplete_trade(self):
pass
def test_transaction_release_incomplete_trade(self):
pass
def test_transaction_card_payment(self):
pass
def test_transaction_negative_amount(self):
pass
def test_release_funds(self):
pass
def test_new_trade(self):
pass
def test_find_trade(self):
pass
def test_get_refs(self):
pass
def test_get_ref_map(self):
pass
def test_get_ref(self):
pass
def test_del_ref(self):
pass
def test_cleanup(self):
pass
def test_tx_to_ref(self):
pass
def test_ref_to_tx(self):
pass
def test_get_total_usd(self):
pass
def test_get_total(self):
pass
def test_write_to_es(self):
pass
def test_get_remaining(self):
pass
def test_get_open_trades_usd(self):
pass
def test_get_total_remaining(self):
pass
def get_total_with_trades(self):
pass

View File

@ -1,16 +1,30 @@
# Twisted/Klein imports # Twisted/Klein imports
from twisted.logger import Logger from twisted.logger import Logger
from twisted.internet.task import LoopingCall
from twisted.internet.threads import deferToThread
# Other library imports # Other library imports
from json import dumps from json import dumps
from random import choices from random import choices
from string import ascii_uppercase from string import ascii_uppercase
from elasticsearch import Elasticsearch
from datetime import datetime
import urllib3
import logging
# Project imports # Project imports
from settings import settings from settings import settings
from db import r from db import r
from util import convert from util import convert
# TODO: secure ES traffic properly
urllib3.disable_warnings()
tracer = logging.getLogger("elasticsearch")
tracer.setLevel(logging.CRITICAL)
tracer = logging.getLogger("elastic_transport.transport")
tracer.setLevel(logging.CRITICAL)
class Transactions(object): class Transactions(object):
""" """
@ -23,13 +37,38 @@ class Transactions(object):
Set the logger. Set the logger.
""" """
self.log = Logger("transactions") self.log = Logger("transactions")
if settings.ES.Enabled == "1":
self.es = Elasticsearch(
f"https://{settings.ES.Host}:9200",
verify_certs=False,
basic_auth=(settings.ES.Username, settings.ES.Pass),
# ssl_assert_fingerprint=("6b264fd2fd107d45652d8add1750a8a78f424542e13b056d0548173006260710"),
ca_certs="certs/ca.crt",
)
def set_agora(self, agora): def run_checks_in_thread(self):
self.agora = agora """
Run all the balance checks that output into ES in another thread.
"""
deferToThread(self.get_total)
deferToThread(self.get_remaining)
deferToThread(self.money.get_profit)
deferToThread(self.money.get_profit, True)
deferToThread(self.get_open_trades_usd)
deferToThread(self.get_total_remaining)
deferToThread(self.get_total_with_trades)
def set_irc(self, irc): def setup_loops(self):
self.irc = irc """
Set up the LoopingCalls to get the balance so we have data in ES.
"""
if settings.ES.Enabled == "1":
self.lc_es_checks = LoopingCall(self.run_checks_in_thread)
delay = int(settings.ES.RefreshSec)
self.lc_es_checks.start(delay)
self.agora.es = self.es
# TODO: write tests then refactor, this is terribly complicated!
def transaction(self, data): def transaction(self, data):
""" """
Store details of transaction and post notifications to IRC. Store details of transaction and post notifications to IRC.
@ -40,9 +79,54 @@ class Transactions(object):
event = data["event"] event = data["event"]
ts = data["timestamp"] ts = data["timestamp"]
if "data" not in data:
return
inside = data["data"] inside = data["data"]
txid = inside["id"] txid = inside["id"]
if "type" not in inside:
# stored_trade here is actually TX
stored_trade = r.hgetall(f"tx.{txid}")
if not stored_trade:
self.log.error("Could not find entry in DB for typeless transaction: {id}", id=txid)
return
print("BEFORE CONVERT STORED TRADE", stored_trade)
stored_trade = convert(stored_trade)
if "old_state" in inside:
if "new_state" in inside:
# We don't care unless we're being told a transaction is now completed
if not inside["new_state"] == "completed":
return
# We don't care unless the existing trade is pending
if not stored_trade["state"] == "pending":
return
# Check the old state is what we also think it is
if inside["old_state"] == stored_trade["state"]:
# Set the state to the new state
stored_trade["state"] = inside["new_state"]
# Store the updated state
r.hmset(f"tx.{txid}", stored_trade)
# Check it's all been previously validated
if "valid" not in stored_trade:
self.log.error("Valid not in stored trade for {txid}, aborting.", txid=txid)
return
if stored_trade["valid"] == "1":
# Make it invalid immediately, as we're going to release now
stored_trade["valid"] = "0"
r.hmset(f"tx.{txid}", stored_trade)
reference = self.tx_to_ref(stored_trade["trade_id"])
self.release_funds(stored_trade["trade_id"], reference)
self.notify.notify_complete_trade(stored_trade["amount"], stored_trade["currency"])
return
# If type not in inside and we haven't hit any more returns
return
else:
txtype = inside["type"] txtype = inside["type"]
if txtype == "card_payment":
self.log.info("Ignoring card payment: {id}", id=txid)
return
state = inside["state"] state = inside["state"]
if "reference" in inside: if "reference" in inside:
reference = inside["reference"] reference = inside["reference"]
@ -57,11 +141,15 @@ class Transactions(object):
account_type = "not_given" account_type = "not_given"
amount = leg["amount"] amount = leg["amount"]
if amount <= 0:
self.log.info("Ignoring transaction with negative/zero amount: {id}", id=txid)
return
currency = leg["currency"] currency = leg["currency"]
description = leg["description"] description = leg["description"]
to_store = { to_store = {
"event": event, "event": event,
"trade_id": "",
"ts": ts, "ts": ts,
"txid": txid, "txid": txid,
"txtype": txtype, "txtype": txtype,
@ -71,12 +159,10 @@ class Transactions(object):
"amount": amount, "amount": amount,
"currency": currency, "currency": currency,
"description": description, "description": description,
"valid": 0, # All checks passed and we can release escrow?
} }
self.log.info("Transaction processed: {formatted}", formatted=dumps(to_store, indent=2)) self.log.info("Transaction processed: {formatted}", formatted=dumps(to_store, indent=2))
r.hmset(f"tx.{txid}", to_store)
self.irc.sendmsg(f"AUTO Incoming transaction: {amount}{currency} ({reference}) - {state} - {description}") self.irc.sendmsg(f"AUTO Incoming transaction: {amount}{currency} ({reference}) - {state} - {description}")
# Partial reference implementation # Partial reference implementation
# Account for silly people not removing the default string # Account for silly people not removing the default string
# Split the reference into parts # Split the reference into parts
@ -115,7 +201,7 @@ class Transactions(object):
if currency == "USD": if currency == "USD":
amount_usd = amount amount_usd = amount
else: else:
rates = self.agora.get_rates_all() rates = self.money.get_rates_all()
amount_usd = amount / rates[currency] amount_usd = amount / rates[currency]
# Amount is reliable here as it is checked by find_trade, so no need for stored_trade["amount"] # Amount is reliable here as it is checked by find_trade, so no need for stored_trade["amount"]
if float(amount_usd) > float(settings.Agora.AcceptableAltLookupUSD): if float(amount_usd) > float(settings.Agora.AcceptableAltLookupUSD):
@ -151,14 +237,14 @@ class Transactions(object):
if looked_up_without_reference: if looked_up_without_reference:
return return
# If the amount does not match exactly, get the min and max values for our given acceptable margins for trades # If the amount does not match exactly, get the min and max values for our given acceptable margins for trades
min_amount, max_amount = self.agora.get_acceptable_margins(currency, amount) min_amount, max_amount = self.money.get_acceptable_margins(currency, stored_trade["amount"])
self.log.info( self.log.info(
"Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}", "Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}",
min_amount=min_amount, min_amount=min_amount,
max_amount=max_amount, max_amount=max_amount,
) )
self.irc.sendmsg(f"Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}") self.irc.sendmsg(f"Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}")
if not min_amount < stored_trade["amount"] < max_amount: if not min_amount < amount < max_amount:
self.log.info( self.log.info(
"Amount mismatch - not in margins: {amount} (min: {min_amount} / max: {max_amount}", "Amount mismatch - not in margins: {amount} (min: {min_amount} / max: {max_amount}",
amount=stored_trade["amount"], amount=stored_trade["amount"],
@ -168,37 +254,61 @@ class Transactions(object):
self.irc.sendmsg(f"Amount mismatch - not in margins: {stored_trade['amount']} (min: {min_amount} / max: {max_amount}") self.irc.sendmsg(f"Amount mismatch - not in margins: {stored_trade['amount']} (min: {min_amount} / max: {max_amount}")
return return
# Make sure the account type was Revolut, as these are completed instantly # Make sure the account type was Revolut, as these are completed instantly
if not account_type == "revolut": # if not account_type == "revolut":
self.log.info("Account type is not Revolut: {account_type}", account_type=account_type) # self.log.info("Account type is not Revolut: {account_type}", account_type=account_type)
self.irc.sendmsg(f"Account type is not Revolut: {account_type}") # self.irc.sendmsg(f"Account type is not Revolut: {account_type}")
return # return
self.log.info("All checks passed, releasing funds for {trade_id} {reference}", trade_id=stored_trade["id"], reference=reference)
self.irc.sendmsg(f"All checks passed, releasing funds for {stored_trade['id']} / {reference}")
# rtrn = self.agora.release_funds(stored_trade["id"])
# self.agora.agora.contact_message_post(stored_trade["id"], "Thanks! Releasing now :)")
# self.irc.sendmsg(dumps(rtrn))
def new_trade(self, trade_id, buyer, currency, amount, amount_xmr): # We have made it this far without hitting any of the returns, so let's set valid = True
# This will let us instantly release if the type is pending, and it is subsequently updated to completed with a callback.
to_store["valid"] = 1
# Store the trade ID so we can release it easily
to_store["trade_id"] = stored_trade["id"]
if not state == "completed":
self.log.info("Storing incomplete trade: {id}", id=txid)
r.hmset(f"tx.{txid}", to_store)
# Don't procees further if state is not "completed"
return
r.hmset(f"tx.{txid}", to_store)
self.release_funds(stored_trade["id"], stored_trade["reference"])
self.notify.notify_complete_trade(amount, currency)
def release_funds(self, trade_id, reference):
self.log.info("All checks passed, releasing funds for {trade_id} {reference}", trade_id=trade_id, reference=reference)
self.irc.sendmsg(f"All checks passed, releasing funds for {trade_id} / {reference}")
rtrn = self.agora.release_funds(trade_id)
self.agora.agora.contact_message_post(trade_id, "Thanks! Releasing now :)")
# Parse the escrow release response
message = rtrn["message"]
message_long = rtrn["response"]["data"]["message"]
self.irc.sendmsg(f"{message} - {message_long}")
def new_trade(self, asset, trade_id, buyer, currency, amount, amount_crypto, provider):
""" """
Called when we have a new trade in Agora. Called when we have a new trade in Agora.
Store details in Redis, generate a reference and optionally let the customer know the reference. Store details in Redis, generate a reference and optionally let the customer know the reference.
""" """
reference = "".join(choices(ascii_uppercase, k=5)) reference = "".join(choices(ascii_uppercase, k=5))
reference = f"XMR-{reference}" reference = f"{asset}-{reference}"
existing_ref = r.get(f"trade.{trade_id}.reference") existing_ref = r.get(f"trade.{trade_id}.reference")
if not existing_ref: if not existing_ref:
r.set(f"trade.{trade_id}.reference", reference) r.set(f"trade.{trade_id}.reference", reference)
to_store = { to_store = {
"id": trade_id, "id": trade_id,
"asset": asset,
"buyer": buyer, "buyer": buyer,
"currency": currency, "currency": currency,
"amount": amount, "amount": amount,
"amount_xmr": amount_xmr, "amount_crypto": amount_crypto,
"reference": reference, "reference": reference,
"provider": provider,
} }
self.log.info("Storing trade information: {info}", info=str(to_store)) self.log.info("Storing trade information: {info}", info=str(to_store))
r.hmset(f"trade.{reference}", to_store) r.hmset(f"trade.{reference}", to_store)
self.irc.sendmsg(f"Generated reference for {trade_id}: {reference}") self.irc.sendmsg(f"Generated reference for {trade_id}: {reference}")
self.notify.notify_new_trade(amount, currency)
if settings.Agora.Send == "1": if settings.Agora.Send == "1":
self.agora.agora.contact_message_post(trade_id, f"Hi! When sending the payment please use reference code: {reference}") self.agora.agora.contact_message_post(trade_id, f"Hi! When sending the payment please use reference code: {reference}")
if existing_ref: if existing_ref:
@ -206,26 +316,6 @@ class Transactions(object):
else: else:
return reference return reference
def find_tx(self, reference, amount):
"""
Find transactions that match the given reference and amount.
:param reference: transaction reference in Revolut
:param amount: transaction amount
:type reference: string
:type amount: int
:return: transaction details or AMOUNT_INVALID, or False
:rtype: dict or string or bool
"""
all_transactions = r.scan(0, match="tx.*")
for tx_iter in all_transactions[1]:
tx_obj = r.hgetall(tx_iter)
if tx_obj[b"reference"] == str.encode(reference):
if tx_obj[b"amount"] == str.encode(amount):
return convert(tx_obj)
else:
return "AMOUNT_INVALID"
return False
def find_trade(self, txid, currency, amount): def find_trade(self, txid, currency, amount):
""" """
Get a trade reference that matches the given currency and amount. Get a trade reference that matches the given currency and amount.
@ -241,6 +331,7 @@ class Transactions(object):
""" """
refs = self.get_refs() refs = self.get_refs()
matching_refs = [] matching_refs = []
# TODO: use get_ref_map in this function instead of calling get_ref multiple times
for ref in refs: for ref in refs:
stored_trade = self.get_ref(ref) stored_trade = self.get_ref(ref)
if stored_trade["currency"] == currency and float(stored_trade["amount"]) == float(amount): if stored_trade["currency"] == currency and float(stored_trade["amount"]) == float(amount):
@ -277,9 +368,11 @@ class Transactions(object):
def get_ref(self, reference): def get_ref(self, reference):
""" """
Get a reference ID for a single trade. Get the trade information for a reference.
:return: trade ID :param reference: trade reference
:rtype: string :type reference: string
:return: dict of trade information
:rtype: dict
""" """
ref_data = r.hgetall(f"trade.{reference}") ref_data = r.hgetall(f"trade.{reference}")
ref_data = convert(ref_data) ref_data = convert(ref_data)
@ -290,22 +383,34 @@ class Transactions(object):
def del_ref(self, reference): def del_ref(self, reference):
""" """
Delete a given reference from the Redis database. Delete a given reference from the Redis database.
:param reference: trade reference to delete
:type reference: string
""" """
tx = self.ref_to_tx(reference) tx = self.ref_to_tx(reference)
r.delete(f"trade.{reference}") r.delete(f"trade.{reference}")
r.delete(f"trade.{tx}.reference") r.delete(f"trade.{tx}.reference")
def cleanup(self, references): def cleanup(self, references):
"""
Reconcile the internal reference database with a given list of references.
Delete all internal references not present in the list and clean up artifacts.
:param references: list of references to reconcile against
:type references: list
"""
for tx, reference in self.get_ref_map().items(): for tx, reference in self.get_ref_map().items():
if reference not in references: if reference not in references:
self.log.info("Archiving trade reference: {reference} / TX: {tx}", reference=reference, tx=tx) self.log.info("Archiving trade reference: {reference} / TX: {tx}", reference=reference, tx=tx)
r.rename(f"trade.{tx}.reference", f"archive.trade.{tx}.reference") r.rename(f"trade.{tx}.reference", f"archive.trade.{tx}.reference")
r.rename(f"trade.{reference}", f"archive.trade.{reference}") r.rename(f"trade.{reference}", f"archive.trade.{reference}")
def del_tx(self, txid):
pass
def tx_to_ref(self, tx): def tx_to_ref(self, tx):
"""
Convert a trade ID to a reference.
:param tx: trade ID
:type tx: string
:return: reference
:rtype: string
"""
refs = self.get_refs() refs = self.get_refs()
for reference in refs: for reference in refs:
ref_data = convert(r.hgetall(f"trade.{reference}")) ref_data = convert(r.hgetall(f"trade.{reference}"))
@ -315,7 +420,231 @@ class Transactions(object):
return reference return reference
def ref_to_tx(self, reference): def ref_to_tx(self, reference):
"""
Convert a reference to a trade ID.
:param reference: trade reference
:type reference: string
:return: trade ID
:rtype: string
"""
ref_data = convert(r.hgetall(f"trade.{reference}")) ref_data = convert(r.hgetall(f"trade.{reference}"))
if not ref_data: if not ref_data:
return False return False
return ref_data["id"] return ref_data["id"]
def get_total_usd(self):
"""
Get total USD in all our accounts, bank and trading.
:return: value in USD
:rtype float:
"""
total_usd_revolut = self.revolut.get_total_usd()
if total_usd_revolut is False:
return False
agora_wallet_xmr = self.agora.agora.wallet_balance_xmr()
if not agora_wallet_xmr["success"]:
return False
agora_wallet_btc = self.agora.agora.wallet_balance()
if not agora_wallet_btc["success"]:
return False
total_xmr_agora = agora_wallet_xmr["response"]["data"]["total"]["balance"]
total_btc_agora = agora_wallet_btc["response"]["data"]["total"]["balance"]
# Get the XMR -> USD exchange rate
xmr_usd = self.agora.cg.get_price(ids="monero", vs_currencies=["USD"])
# Get the BTC -> USD exchange rate
btc_usd = self.agora.cg.get_price(ids="bitcoin", vs_currencies=["USD"])
# Convert the Agora XMR total to USD
total_usd_agora_xmr = float(total_xmr_agora) * xmr_usd["monero"]["usd"]
# Convert the Agora BTC total to USD
total_usd_agora_btc = float(total_btc_agora) * btc_usd["bitcoin"]["usd"]
# Add it all up
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
total_usd = total_usd_agora + total_usd_revolut
cast_es = {
"price_usd": total_usd,
"total_usd_agora_xmr": total_usd_agora_xmr,
"total_usd_agora_btc": total_usd_agora_btc,
"total_xmr_agora": total_xmr_agora,
"total_btc_agora": total_btc_agora,
"xmr_usd": xmr_usd["monero"]["usd"],
"btc_usd": btc_usd["bitcoin"]["usd"],
"total_usd_revolut": total_usd_revolut,
"total_usd_agora": total_usd_agora,
}
self.write_to_es("get_total_usd", cast_es)
return total_usd
# TODO: possibly refactor this into smaller functions which don't return as much stuff
# check if this is all really needed in the corresponding withdraw function
def get_total(self):
"""
Get all the values corresponding to the amount of money we hold.
:return: ((total SEK, total USD, total GBP), (total XMR USD, total BTC USD), (total XMR, total BTC))
:rtype: tuple(tuple(float, float, float), tuple(float, float), tuple(float, float))
"""
total_usd_revolut = self.revolut.get_total_usd()
if total_usd_revolut is False:
self.log.error("Could not get USD total.")
return False
agora_wallet_xmr = self.agora.agora.wallet_balance_xmr()
if not agora_wallet_xmr["success"]:
self.log.error("Could not get Agora XMR wallet total.")
return False
agora_wallet_btc = self.agora.agora.wallet_balance()
if not agora_wallet_btc["success"]:
self.log.error("Could not get Agora BTC wallet total.")
return False
total_xmr_agora = agora_wallet_xmr["response"]["data"]["total"]["balance"]
total_btc_agora = agora_wallet_btc["response"]["data"]["total"]["balance"]
# Get the XMR -> USD exchange rate
xmr_usd = self.agora.cg.get_price(ids="monero", vs_currencies=["USD"])
# Get the BTC -> USD exchange rate
btc_usd = self.agora.cg.get_price(ids="bitcoin", vs_currencies=["USD"])
# Convert the Agora XMR total to USD
total_usd_agora_xmr = float(total_xmr_agora) * xmr_usd["monero"]["usd"]
# Convert the Agora BTC total to USD
total_usd_agora_btc = float(total_btc_agora) * btc_usd["bitcoin"]["usd"]
# Add it all up
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
total_usd = total_usd_agora + total_usd_revolut
# Convert the total USD price to GBP and SEK
rates = self.money.get_rates_all()
price_sek = rates["SEK"] * total_usd
price_usd = total_usd
price_gbp = rates["GBP"] * total_usd
cast = (
(price_sek, price_usd, price_gbp), # Total prices in our 3 favourite currencies
(total_usd_agora_xmr, total_usd_agora_btc), # Total USD balance in only Agora
(total_xmr_agora, total_btc_agora),
) # Total XMR and BTC balance in Agora
cast_es = {
"price_sek": price_sek,
"price_usd": price_usd,
"price_gbp": price_gbp,
"total_usd_agora_xmr": total_usd_agora_xmr,
"total_usd_agora_btc": total_usd_agora_btc,
"total_xmr_agora": total_xmr_agora,
"total_btc_agora": total_btc_agora,
"xmr_usd": xmr_usd["monero"]["usd"],
"btc_usd": btc_usd["bitcoin"]["usd"],
"total_usd_revolut": total_usd_revolut,
"total_usd_agora": total_usd_agora,
}
self.write_to_es("get_total", cast_es)
return cast
def write_to_es(self, msgtype, cast):
if settings.ES.Enabled == "1":
cast["type"] = msgtype
cast["ts"] = str(datetime.now().isoformat())
cast["xtype"] = "tx"
self.es.index(index=settings.ES.Index, document=cast)
def get_remaining(self):
"""
Check how much profit we need to make in order to withdraw.
:return: profit remaining in USD
:rtype: float
"""
total_usd = self.get_total_usd()
if not total_usd:
return False
withdraw_threshold = float(settings.Money.BaseUSD) + float(settings.Money.WithdrawLimit)
remaining = withdraw_threshold - total_usd
cast_es = {
"remaining_usd": remaining,
}
self.write_to_es("get_remaining", cast_es)
return remaining
def get_open_trades_usd(self):
"""
Get total value of open trades in USD.
:return: total trade value
:rtype: float
"""
dash = self.agora.wrap_dashboard()
if dash is False:
return False
rates = self.money.get_rates_all()
cumul_usd = 0
for contact_id, contact in dash.items():
# We need created at in order to look up the historical prices
created_at = contact["data"]["created_at"]
# Reformat the date how CoinGecko likes
date_parsed = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ")
date_formatted = date_parsed.strftime("%d-%m-%Y")
# Get the historical rates for the right asset, extract the price
asset = contact["data"]["advertisement"]["asset"]
if asset == "XMR":
amount_crypto = contact["data"]["amount_xmr"]
history = self.agora.cg.get_coin_history_by_id(id="monero", date=date_formatted)
crypto_usd = float(history["market_data"]["current_price"]["usd"])
elif asset == "BTC":
amount_crypto = contact["data"]["amount_btc"]
history = self.agora.cg.get_coin_history_by_id(id="bitcoin", date=date_formatted)
crypto_usd = float(history["market_data"]["current_price"]["usd"])
# Convert crypto to fiat
amount = float(amount_crypto) * crypto_usd
currency = contact["data"]["currency"]
if not contact["data"]["is_selling"]:
continue
if currency == "USD":
cumul_usd += float(amount)
else:
rate = rates[currency]
amount_usd = float(amount) / rate
cumul_usd += amount_usd
cast_es = {
"trades_usd": cumul_usd,
}
self.write_to_es("get_open_trades_usd", cast_es)
return cumul_usd
def get_total_remaining(self):
"""
Check how much profit we need to make in order to withdraw, taking into account open trade value.
:return: profit remaining in USD
:rtype: float
"""
total_usd = self.get_total_usd()
total_trades_usd = self.get_open_trades_usd()
if not total_usd:
return False
total_usd += total_trades_usd
withdraw_threshold = float(settings.Money.BaseUSD) + float(settings.Money.WithdrawLimit)
remaining = withdraw_threshold - total_usd
cast_es = {
"total_remaining_usd": remaining,
}
self.write_to_es("get_total_remaining", cast_es)
return remaining
def get_total_with_trades(self):
total_usd = self.get_total_usd()
if not total_usd:
return False
total_trades_usd = self.get_open_trades_usd()
total_with_trades = total_usd + total_trades_usd
cast_es = {
"total_with_trades": total_with_trades,
}
self.write_to_es("get_total_with_trades", cast_es)
return total_with_trades

View File

@ -1,3 +1,50 @@
# Twisted/Klein imports
from twisted.logger import Logger
from twisted.internet import reactor
from twisted.internet.task import LoopingCall, deferLater
# Other library imports
from httpx import ReadTimeout, ReadError, RemoteProtocolError
from datetime import datetime
log = Logger("util.global")
def xmerge_attrs(init_map):
"""
Given a dictionary of strings and classes, set all corresponding class.<string> attributes
on each class, to every other class.
"a": A(), "b": B() -> A.b = B_instance, B.a = A_instance
:param init_map: dict of class names to classes
"""
for classname, object_instance in init_map.items():
# notify, Notify
for classname_inside, object_instance_inside in init_map.items():
if not classname == classname_inside:
# irc, bot
setattr(object_instance, classname_inside, object_instance_inside)
def setup_call_loops(token_setting, function_init, function_continuous, delay, function_post_start=None):
"""
Setup the loops for dealing with access, refresh and auth tokens for various providers.
:param token_setting: the setting for whether to do the initial authentication
:param function_init: the initial authentication function
:param function_continuous: the ongoing authentication function (refresh_token -> access_token)
:param delay: time in seconds to wait between calls to function_continuous
:param function_post_start: an optional function to run after the access token is obtained
"""
if token_setting == "1":
deferLater(reactor, 1, function_init)
else:
deferLater(reactor, 1, function_continuous, True)
if function_post_start:
deferLater(reactor, 4, function_post_start)
lc = LoopingCall(function_continuous)
lc.start(delay)
def convert(data): def convert(data):
""" """
Recursively convert a dictionary. Recursively convert a dictionary.
@ -11,3 +58,53 @@ def convert(data):
if isinstance(data, list): if isinstance(data, list):
return list(map(convert, data)) return list(map(convert, data))
return data return data
def last_online_recent(date):
"""
Check if the last online date was recent.
:param date: date last online
:type date: string
:return: bool indicating whether the date was recent enough
:rtype: bool
"""
date_parsed = datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ")
now = datetime.now()
sec_ago_date = (now - date_parsed).total_seconds()
# self.log.debug("Seconds ago date for {date} ^ {now}: {x}", date=date, now=str(now), x=sec_ago_date)
return sec_ago_date < 172800
def handle_exceptions(func):
"""
Wrapper helper to handle Agora API errors.
:param func: function to wrap
:rtype: func
:return: the wrapped function
"""
def inner_function(*args, **kwargs):
"""
Inner wrapper helper.
:rtype: any or bool
:return: False or the normal return
"""
try:
rtrn = func(*args, **kwargs)
except (ReadTimeout, ReadError, RemoteProtocolError):
return False
if isinstance(rtrn, dict):
if "success" in rtrn:
if "message" in rtrn:
if not rtrn["success"] and rtrn["message"] == "API ERROR":
if "error_code" in rtrn["response"]["error"]:
code = rtrn["response"]["error"]["error_code"]
if not code == 136:
log.error("API error: {code}", code=code)
return False
else:
log.error("API error: {code}", code=rtrn["response"]["error"])
return False
return rtrn
return inner_function

View File

@ -1,4 +1,15 @@
flask twisted
flask_sqlalchemy redis
pyOpenSSL
Klein
ConfigObject
service_identity
forex_python
simplejson
requests requests
PyJWT
arrow
httpx
pre-commit pre-commit
pycoingecko
PyOTP