Libraries refactor and add some sinks #4
|
@ -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/
|
||||||
|
|
618
handler/agora.py
618
handler/agora.py
|
@ -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)
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
120
handler/app.py
120
handler/app.py
|
@ -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)
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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")
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
pre-commit run -a
|
||||||
|
python -m unittest discover -s tests -p 'test_*.py'
|
|
@ -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.
|
||||||
|
"""
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue