Finish implementation and tests for the cheat system #3

Closed
m wants to merge 67 commits from cheat-refactor into master
14 changed files with 2415 additions and 247 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ __pycache__/
env/ env/
keys/ keys/
handler/settings.ini handler/settings.ini
handler/otp.key

View File

@ -1,15 +1,44 @@
# 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 httpx import ReadTimeout, ReadError
from pycoingecko import CoinGeckoAPI
from datetime import datetime
from time import sleep
from pyotp import TOTP
# Project imports # Project imports
from settings import settings from settings import settings
log = Logger("agora.global")
def handle_exceptions(func):
def inner_function(*args, **kwargs):
try:
rtrn = func(*args, **kwargs)
except (ReadTimeout, ReadError):
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"]:
log.error("API error: {code}", code=rtrn["response"]["error"]["error_code"])
return False
else:
log.error("API error: {code}", code=rtrn["response"]["error"])
return False
return rtrn
return inner_function
class Agora(object): class Agora(object):
""" """
@ -24,6 +53,7 @@ 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()
self.cg = CoinGeckoAPI()
# Cache for detecting new trades # Cache for detecting new trades
self.last_dash = set() self.last_dash = set()
@ -31,11 +61,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 +70,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._update_prices, None, None)
self.lc_cheat.start(int(settings.Agora.CheatSec))
@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 +112,24 @@ 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"]
if not contact["data"]["is_selling"]:
continue
rtrn.append(f"{reference}: {buyer} {amount}{currency} {amount_crypto}{asset}")
return rtrn
def dashboard_hook(self, dash): def dashboard_hook(self, dash):
""" """
@ -78,23 +137,31 @@ 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"]
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)
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} {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,6 +173,7 @@ class Agora(object):
current_trades.append(reference) current_trades.append(reference)
self.tx.cleanup(current_trades) self.tx.cleanup(current_trades)
@handle_exceptions
def dashboard_release_urls(self): def dashboard_release_urls(self):
""" """
Get information about our open trades. Get information about our open trades.
@ -114,6 +182,8 @@ class Agora(object):
:rtype: list or bool :rtype: list or bool
""" """
dash = self.agora.dashboard_seller() dash = self.agora.dashboard_seller()
if dash is False:
return False
dash_tmp = [] dash_tmp = []
if "data" not in dash["response"]: if "data" not in dash["response"]:
self.log.error("Data not in dashboard response: {content}", content=dash) self.log.error("Data not in dashboard response: {content}", content=dash)
@ -123,7 +193,11 @@ class Agora(object):
contact_id = contact["data"]["contact_id"] contact_id = contact["data"]["contact_id"]
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"]
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"]
release_url = contact["actions"]["release_url"] release_url = contact["actions"]["release_url"]
if not contact["data"]["is_selling"]: if not contact["data"]["is_selling"]:
@ -131,18 +205,24 @@ class Agora(object):
reference = self.tx.tx_to_ref(contact_id) reference = self.tx.tx_to_ref(contact_id)
if not reference: if not reference:
reference = "not_set" reference = "not_set"
dash_tmp.append(f"{reference}: {buyer} {amount}{currency} {amount_xmr}XMR {release_url}") dash_tmp.append(f"{reference}: {buyer} {amount}{currency} {amount_crypto}{asset} {release_url}")
return dash_tmp return dash_tmp
@handle_exceptions
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 +256,11 @@ class Agora(object):
return messages_tmp return messages_tmp
@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 +269,246 @@ 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): @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
def last_online_recent(self, 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
@handle_exceptions
def enum_public_ads(self, coin, currency, providers=None, page=0):
if not providers:
providers = ["REVOLUT"]
# buy-monero-online, buy-bitcoin-online
# Work around Agora weirdness calling it bitcoins
if coin == "bitcoin":
coin = "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})
if ads is None:
return False
if ads is False:
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 self.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"]
yield [ad_id, username, temp_price, provider]
if "pagination" in ads["response"]:
if "next" in ads["response"]["pagination"]:
page += 1
ads_iter = self.enum_public_ads(coin, currency, providers, page)
if ads_iter is None:
return False
if ads_iter is False:
return False
for ad in ads_iter:
yield [ad[0], ad[1], ad[2], ad[3]]
def wrap_public_ads(self, asset, currency, providers=None, rates=None):
"""
Wrapper to sort public ads.
"""
if asset == "XMR":
coin = "monero"
elif asset == "BTC":
coin = "bitcoin"
ads_obj = self.enum_public_ads(coin, currency.upper(), providers)
ads = list(ads_obj)
if ads is False:
return False
# ads = list(ads_obj)
if ads is False:
return False
if not rates:
# Set the price based on the asset
base_currency_price = self.cg.get_price(ids=coin, vs_currencies=currency)[coin][currency.lower()]
else:
base_currency_price = rates
for ad in ads:
price = float(ad[2])
rate = round(price / base_currency_price, 4)
ad.append(asset)
ad.append(rate)
return sorted(ads, key=lambda x: x[2])
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)
@handle_exceptions
def update_prices(self, assets=None):
# Get all public ads for the given assets
public_ads = self.get_all_public_ads(assets)
# Get the ads to update
to_update = self.markets.get_new_ad_equations(public_ads, assets)
self.slow_ad_update(to_update)
@handle_exceptions
def get_all_public_ads(self, assets=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
currencies = self.markets.get_all_currencies()
providers = self.markets.get_all_providers()
# We want to get the ads for each of these currencies and return the result
rates_crypto = 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_current_crypto = rates_crypto[cg_asset_name][currency.lower()]
except KeyError:
self.log.error("Error getting public ads for currency {currency}", currency=currency)
continue
ads = self.wrap_public_ads(asset, currency, providers=providers, rates=rates_current_crypto)
if not ads:
continue
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 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
"""
self.log.info("Beginning slow ad update for {num} ads", num=len(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)
self.log.error("ASSET {a}", a=asset)
if not actioned:
rtrn = self.agora.ad_equation(ad_id, new_formula)
if rtrn["success"]:
ads[ad_index][4] = True
throttled = 0
self.log.info("Successfully updated ad: {id}", id=ad_id)
continue
else:
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
if iterations == 0:
self.log.info("Slow ad update finished, no ads to update")
self.irc.sendmsg("Slow ad update finished, no ads to update")
else:
self.log.info(
"Slow ad update completed with {iterations} iterations: [{assets}] | [{currencies}]",
iterations=iterations,
assets=", ".join(assets),
currencies=", ".join(currencies),
)
self.irc.sendmsg(f"Slow ad update completed with {iterations} iterations: [{', '.join(assets)}] | [{', '.join(currencies)}]")
@handle_exceptions
def nuke_ads(self): def nuke_ads(self):
""" """
Delete all of our adverts. Delete all of our adverts.
@ -212,7 +517,7 @@ 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)
@ -248,10 +553,13 @@ class Agora(object):
max_local = max_usd * rates[currency] max_local = max_usd * rates[currency]
return (min_local, max_local) return (min_local, max_local)
def create_ad(self, countrycode, currency): @handle_exceptions
def create_ad(self, asset, countrycode, currency, provider, edit=False, ad_id=None):
""" """
Post an ad in a country with a given currency. Post an ad with the given asset 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 +568,116 @@ 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}"
# Substitute the asset
ad = ad.replace("$ASSET$", asset)
rates = self.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}"
# price_formula = f"coingeckoxmrusd*{settings.Agora.Margin}" # price_formula = f"coingeckoxmrusd*{settings.Agora.Margin}"
ad = settings.Agora.Ad
# Remove extra tabs
ad = ad.replace("\\t", "\t") ad = ad.replace("\\t", "\t")
ad = self.agora.ad_create(
country_code=countrycode, form = {
currency=currency, "country_code": countrycode,
trade_type="ONLINE_SELL", "currency": currency,
asset="XMR", "trade_type": "ONLINE_SELL",
price_equation=price_formula, "asset": asset,
track_max_amount=False, "price_equation": price_formula,
require_trusted_by_advertiser=False, "track_max_amount": False,
# verified_email_required = False, "require_trusted_by_advertiser": False,
online_provider="REVOLUT", "online_provider": provider,
msg=settings.Agora.Ad, "msg": 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,
# require_feedback_score = 0, "account_info": paymentdetailstext,
account_info=settings.Agora.PaymentDetails, }
)
# Dirty hack to test
# if asset == "BTC":
# del form["min_amount"]
if edit:
ad = self.agora.ad(ad_id=ad_id, **form)
else:
ad = self.agora.ad_create(**form)
return ad return ad
def create_distribution_list(self):
"""
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):
yield (asset, countrycode, currency, provider)
def dist_countries(self): def dist_countries(self):
""" """
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.create_distribution_list())
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
return False our_ads = [(x[0], x[2], x[3], x[4]) for x in our_ads]
yield rtrn for asset, countrycode, currency, provider in dist_list:
if (asset, countrycode, currency, provider) not in our_ads:
def get_combinations(self): # Create the actual ad and pass in all the stuff
""" rtrn = self.create_ad(asset, countrycode, currency, provider)
Get all combinations of currencies and countries from the configuration. # Bail on first error, let's not continue
:return: list of [country, currency] if rtrn is False:
:rtype: list return False
"""
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
:rtype: bool or dict
"""
combinations = self.get_combinations()
for country, currency in combinations:
rtrn = self.create_ad(country, currency)
if not rtrn:
yield False
yield rtrn
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 yield rtrn
def redist_countries(self):
"""
Redistribute our advert details into all our listed adverts.
This will edit all ads and update the details. Only works if we have already run dist.
This will not post any new ads.
Exits on errors.
:return: False or dict with response
:rtype: bool or dict
"""
our_ads = self.enum_ads()
for asset, ad_id, countrycode, currency, provider in our_ads:
rtrn = self.create_ad(asset, countrycode, currency, provider, edit=True, ad_id=ad_id)
# Bail on first error, let's not continue
if rtrn is False:
return False
yield (rtrn, ad_id)
@handle_exceptions
def strip_duplicate_ads(self): def strip_duplicate_ads(self):
""" """
Remove duplicate ads. Remove duplicate ads.
@ -365,6 +699,7 @@ class Agora(object):
actioned.append(rtrn["success"]) actioned.append(rtrn["success"])
return all(actioned) return all(actioned)
@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 +710,83 @@ 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
@handle_exceptions
def withdraw_funds(self):
"""
Withdraw excess funds to our XMR/BTC wallets.
"""
totals_all = self.tx.get_total()
if totals_all is False:
return False
wallet_xmr, _ = totals_all[2]
# Get the wallet balances in USD
total_usd = totals_all[0][1]
profit_usd = total_usd - float(settings.Money.BaseUSD)
# Get the XMR -> USD exchange rate
xmr_usd = self.cg.get_price(ids="monero", vs_currencies=["USD"])
# Convert the USD total to XMR
profit_usd_in_xmr = float(profit_usd) / xmr_usd["monero"]["usd"]
# 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
half_rounded = round(half, 8)
# 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(profit_usd / 2)
def to_usd(self, amount, currency):
if currency == "USD":
return float(amount)
else:
rates = self.get_rates_all()
return float(amount) / rates[currency]

1019
handler/agoradesk_py.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,8 @@ 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
def convert(data): def convert(data):
@ -37,9 +39,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()
@ -54,45 +53,33 @@ class WebApp(object):
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(),
"tx": Transactions(),
"webapp": WebApp(),
}
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)
# Pass IRC to Agora and Agora to IRC
# This is to prevent recursive dependencies
agora.set_irc(irc)
irc.set_agora(agora)
# Define Revolut
revolut = Revolut()
# 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 # Handle setting up JWT and request_token from an auth code
if settings.Revolut.SetupToken == "1": if settings.Revolut.SetupToken == "1":
deferLater(reactor, 1, revolut.setup_auth) deferLater(reactor, 1, init_map["revolut"].setup_auth)
else: else:
# Schedule refreshing the access token using the refresh token # Schedule refreshing the access token using the refresh token
deferLater(reactor, 1, revolut.get_new_token, True) deferLater(reactor, 1, init_map["revolut"].get_new_token, True)
# Check if the webhook is set up and set up if not # Check if the webhook is set up and set up if not
deferLater(reactor, 4, revolut.setup_webhook) deferLater(reactor, 4, init_map["revolut"].setup_webhook)
# Schedule repeatedly refreshing the access token # Schedule repeatedly refreshing the access token
lc = LoopingCall(revolut.get_new_token) lc = LoopingCall(init_map["revolut"].get_new_token)
lc.start(int(settings.Revolut.RefreshSec)) 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("127.0.0.1", 8080)

View File

@ -1,5 +1,8 @@
# Other library imports # Other library imports
from json import dumps from json import dumps, loads
# Project imports
from settings import settings
class IRCCommands(object): class IRCCommands(object):
@ -9,14 +12,14 @@ class IRCCommands(object):
helptext = "Get all open trades." helptext = "Get all open trades."
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
""" """
Get details of open trades and post on IRC. Get details of open trades and post on IRC.
""" """
# Send IRC - we don't want to automatically send messages on IRC, even though # Send IRC - we don't want to automatically send messages on IRC, even though
# this variable seems counter-intuitive here, we are doing something with the result # this variable seems counter-intuitive here, we are doing something with the result
# then calling msg() ourselves, and we don't want extra spam in the channel. # then calling msg() ourselves, and we don't want extra spam in the channel.
trades = agora.dashboard(send_irc=False) trades = agora.get_dashboard()
if not trades: if not trades:
msg("No open trades.") msg("No open trades.")
return return
@ -26,18 +29,22 @@ 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>"
@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 posted["success"]: if spl[1] not in loads(settings.Agora.AssetList):
msg(f"{posted['response']['data']['message']}: {posted['response']['data']['ad_id']}") msg(f"Not a valid asset: {spl[1]}")
else: return
msg(posted["response"]["data"]["message"]) posted = agora.create_ad(spl[1], spl[2], spl[3], "REVOLUT")
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 +52,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.
""" """
@ -79,39 +86,26 @@ class IRCCommands(object):
helptext = "Distribute all our chosen currency and country ad pairs." helptext = "Distribute all our chosen currency and country ad pairs."
@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
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,7 +113,7 @@ 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))
@ -129,7 +123,7 @@ class IRCCommands(object):
helptext = "Find a transaction. Usage: find <currency> <amount>" helptext = "Find a transaction. Usage: find <currency> <amount>"
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx): def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
""" """
Find a transaction received by Revolut with the given reference and amount. Find a transaction received by Revolut with the given reference and amount.
""" """
@ -151,7 +145,7 @@ class IRCCommands(object):
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 +161,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,13 +195,22 @@ class IRCCommands(object):
def run(cmd, spl, length, authed, msg): def run(cmd, spl, length, authed, msg):
msg("Pong!") msg("Pong!")
class summon(object):
name = "summon"
authed = True
helptext = "Summon all operators."
@staticmethod
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
notify.sendmsg("You have been summoned!")
class release_url(object): class release_url(object):
name = "release_url" name = "release_url"
authed = True authed = True
helptext = "Get release URL for all open trades." helptext = "Get release URL for 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):
trades = agora.dashboard_release_urls() trades = agora.dashboard_release_urls()
if not trades: if not trades:
msg("No trades.") msg("No trades.")
@ -207,7 +223,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 +239,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 +248,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 +262,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 +277,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 +294,150 @@ 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>"
@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.wrap_public_ads(asset, currency)
for ad in rtrn:
msg(f"({ad[0]}) {ad[1]} {ad[2]} {ad[4]}")
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.wrap_public_ads(asset, currency, providers)
for ad in rtrn:
msg(f"({ad[0]}) {ad[1]} {ad[2]} {ad[3]} {ad[4]}")
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._update_prices(None, None)
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")

View File

@ -2,6 +2,7 @@
from twisted.logger import Logger from twisted.logger import Logger
from twisted.words.protocols import irc from twisted.words.protocols import irc
from twisted.internet import protocol, reactor, ssl from twisted.internet import protocol, reactor, ssl
from twisted.internet.task import deferLater
# Project imports # Project imports
from settings import settings from settings import settings
@ -37,15 +38,6 @@ class IRCBot(irc.IRCClient):
self.channel = settings.IRC.Channel self.channel = settings.IRC.Channel
def set_agora(self, agora):
self.agora = agora
def set_revolut(self, revolut):
self.revolut = revolut
def set_tx(self, tx):
self.tx = tx
def parse(self, user, host, channel, msg): def parse(self, user, host, channel, msg):
""" """
Simple handler for IRC commands. Simple handler for IRC commands.
@ -96,7 +88,7 @@ class IRCBot(irc.IRCClient):
# Check if the command required authentication # Check if the command required authentication
if obj.authed: if obj.authed:
if host in self.admins: if host in self.admins:
obj.run(cmd, spl, length, authed, msgl, self.agora, self.revolut, self.tx) obj.run(cmd, spl, length, authed, msgl, self.agora, self.revolut, self.tx, self.notify)
else: else:
# Handle authentication here instead of in the command module for security # Handle authentication here instead of in the command module for security
self.msg(channel, "Access denied.") self.msg(channel, "Access denied.")
@ -115,7 +107,7 @@ class IRCBot(irc.IRCClient):
Join our channel. Join our channel.
""" """
self.log.info("Signed on as %s" % (self.nickname)) self.log.info("Signed on as %s" % (self.nickname))
self.join(self.channel) deferLater(reactor, 2, self.join, self.channel)
def joined(self, channel): def joined(self, channel):
""" """
@ -156,8 +148,10 @@ 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()
self.parse(user, host, channel, msg) if len(spl) > 0:
if spl[0] != "!":
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):

152
handler/markets.py Normal file
View File

@ -0,0 +1,152 @@
# 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)
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)
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[5])
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[5] > 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[5])
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[5] # - 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[5] # - 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

45
handler/notify.py Normal file
View File

@ -0,0 +1,45 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports
import requests
# Project imports
from settings import settings
class Notify(object):
"""
Class to handle more robust notifications.
"""
def __init__(self):
self.log = Logger("notify")
def sendmsg(self, msg, title=None, priority=None, tags=None):
headers = {"Title": "Bot"}
if title:
headers["Title"] = title
if priority:
headers["Priority"] = priority
if tags:
headers["Tags"] = tags
requests.post(
f"{settings.Notify.Host}/{settings.Notify.Topic}",
data=msg,
headers=headers,
)
def notify_new_trade(self, amount, currency):
amount_usd = self.agora.to_usd(amount, currency)
self.sendmsg(f"Total: {amount_usd}", title="New trade", tags="trades")
def notify_complete_trade(self, amount, currency):
amount_usd = self.agora.to_usd(amount, currency)
self.sendmsg(f"Total: {amount_usd}", title="Trade complete", tags="trades,profit")
def notify_withdrawal(self, amount_usd):
self.sendmsg(f"Total: {amount_usd}", title="Withdrawal", tags="profit")
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")

View File

@ -8,6 +8,8 @@ import requests
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
import jwt import jwt
from random import choices
from string import ascii_uppercase
# Project imports # Project imports
from settings import settings from settings import settings
@ -26,12 +28,6 @@ class Revolut(object):
self.log = Logger("revolut") self.log = Logger("revolut")
self.token = None self.token = None
def set_irc(self, irc):
self.irc = irc
def set_agora(self, agora):
self.agora = agora
def setup_auth(self): def setup_auth(self):
""" """
Function to create a new Java Web Token and use it to get a refresh/access token. Function to create a new Java Web Token and use it to get a refresh/access token.
@ -83,9 +79,9 @@ class Revolut(object):
settings.Revolut.RefreshToken = parsed["refresh_token"] settings.Revolut.RefreshToken = parsed["refresh_token"]
settings.Revolut.SetupToken = "0" settings.Revolut.SetupToken = "0"
settings.write() settings.write()
self.log.info("Refreshed refresh token: {refresh_token}", refresh_token=settings.Revolut.RefreshToken) self.log.info("Refreshed refresh token")
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")
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")
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)
@ -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

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

@ -0,0 +1,115 @@
fake_public_ads = {
"JPY": [["b048bad8-3aaa-4727-88ba-d83aaa1727b6", "topmonero", "26978.66", "REVOLUT", "XMR", 1.32]],
"DKK": [["ef1455dc-4629-4827-9455-dc46291827f0", "topmonero", "1521.50", "REVOLUT", "XMR", 1.32]],
"CHF": [["fb04a8e2-3c69-45f3-84a8-e23c6975f380", "topmonero", "215.74", "REVOLUT", "XMR", 1.32]],
"SEK": [
["f0e840b9-29ab-4a6f-a840-b929ab7a6fde", "topmonero", "2053.68", "REVOLUT", "XMR", 1.27],
["2252a3f7-6d6b-400b-92a3-f76d6bb00b50", "SwishaMonero", "2053.68", "REVOLUT", "XMR", 1.27],
],
"CZK": [["80aa52ef-a5d3-462c-aa52-efa5d3862cbe", "topmonero", "4960.76", "REVOLUT", "XMR", 1.32]],
"PLN": [["b5be2881-4491-4a60-be28-814491ca606a", "topmonero", "926.48", "REVOLUT", "XMR", 1.32]],
"EUR": [
["87af6467-be02-476e-af64-67be02676e9a", "topmonero", "187.11", "REVOLUT", "XMR", 1.21],
["57e3e8d6-45fe-40da-a3e8-d645fe20da46", "SecureMole", "187.11", "REVOLUT", "XMR", 1.21],
["d2c6645c-6d56-4094-8664-5c6d5640941b", "topmonero", "187.11", "REVOLUT", "XMR", 1.21],
["65b452e3-a29f-4233-b452-e3a29fe23369", "topmonero", "187.11", "REVOLUT", "XMR", 1.21],
],
"NOK": [["9c3d9fb6-c74c-4a35-bd9f-b6c74c7a3504", "topmonero", "2058.76", "REVOLUT", "XMR", 1.32]],
"THB": [["9a7bd726-6229-4fda-bbd7-2662295fda98", "topmonero", "7668.74", "REVOLUT", "XMR", 1.31]],
"ZAR": [["92877e53-823e-4ac5-877e-53823e2ac545", "topmonero", "3583.09", "REVOLUT", "XMR", 1.3]],
"MXN": [["bf12756b-0138-49ca-9275-6b0138f9caf1", "topmonero", "4815.61", "REVOLUT", "XMR", 1.31]],
"TRY": [["3c5068ce-fcf9-40cc-9068-cefcf920cc02", "topmonero", "3166.12", "REVOLUT", "XMR", 1.31]],
"RUB": [["24a9cad1-0bce-46f3-a9ca-d10bce86f34f", "topmonero", "17513.23", "REVOLUT", "XMR", 1.31]],
"SGD": [["ac9eb9c8-88c7-4add-9eb9-c888c72addb2", "topmonero", "313.83", "REVOLUT", "XMR", 1.32]],
"HKD": [["b23aa3b3-4c91-42e4-baa3-b34c9162e4e0", "topmonero", "1818.76", "REVOLUT", "XMR", 1.32]],
"AUD": [["68b50d95-91d2-4f21-b50d-9591d22f218d", "topmonero", "327.16", "REVOLUT", "XMR", 1.31]],
"HUF": [["f886768f-b9c9-4cf7-8676-8fb9c9ccf7ee", "topmonero", "72350.54", "REVOLUT", "XMR", 1.32]],
"USD": [
["24871dd9-54e8-4962-871d-d954e82962b1", "topmonero", "224.36", "REVOLUT", "XMR", 1.27],
["b5f80385-73cb-4f98-b803-8573cb6f9846", "SecureMole", "224.36", "REVOLUT", "XMR", 1.27],
["92add2b6-ccd5-4351-add2-b6ccd53351e4", "topmonero", "224.36", "REVOLUT", "XMR", 1.27],
["04be2471-72a5-4ab6-be24-7172a57ab64a", "EmmanuelMuema", "232.98", "REVOLUT", "XMR", 1.31],
["bb0d817d-5d7d-4e76-8d81-7d5d7d9e76fd", "EmmanuelMuema", "233.34", "REVOLUT", "XMR", 1.32],
["a53a9770-69ba-43fc-ba97-7069bad3fc1a", "EmmanuelMuema", "235.13", "REVOLUT", "XMR", 1.33],
["9c7e4b21-359c-4953-be4b-21359c095370", "EmmanuelMuema", "236.93", "REVOLUT", "XMR", 1.34],
["88c72c50-3162-4c91-872c-503162dc9176", "Alphabay", "358.80", "REVOLUT", "XMR", 2.02],
],
"GBP": [
["15e821b8-e570-4b0f-a821-b8e5709b0ffc", "SecureMole", "167.95", "REVOLUT", "XMR", 1.28],
["071ab272-ba37-4a14-9ab2-72ba37fa1484", "Boozymad89", "167.95", "REVOLUT", "XMR", 1.28],
["6727d9e5-c038-43f5-a7d9-e5c038c3f5be", "topmonero", "168.22", "REVOLUT", "XMR", 1.28],
["850f28eb-ce63-4ca7-8f28-ebce63dca707", "topmonero", "168.22", "REVOLUT", "XMR", 1.28],
["ca4feeb9-22d5-456d-8fee-b922d5c56d27", "Boozymad89", "177.49", "REVOLUT", "XMR", 1.36],
["78db95b7-e090-48e2-9b95-b7e09098e2d9", "Crypto_Hood", "38150.99", "REVOLUT", "BTC", 1.18],
],
"NZD": [["0addd244-c5cb-40bf-9dd2-44c5cbd0bff4", "topmonero", "351.40", "REVOLUT", "XMR", 1.31]],
"CAD": [["388442b4-0cb7-48f3-8442-b40cb7f8f39d", "topmonero", "296.50", "REVOLUT", "XMR", 1.32]],
}
expected_to_update = [
["fb04a8e2-3c69-45f3-84a8-e23c6975f380", "coingeckoxmrusd*usdchf*1.3", "XMR", "CHF", False],
["bf12756b-0138-49ca-9275-6b0138f9caf1", "coingeckoxmrusd*usdmxn*1.3", "XMR", "MXN", False],
["ef1455dc-4629-4827-9455-dc46291827f0", "coingeckoxmrusd*usddkk*1.3", "XMR", "DKK", False],
["388442b4-0cb7-48f3-8442-b40cb7f8f39d", "coingeckoxmrusd*usdcad*1.3", "XMR", "CAD", False],
["b5be2881-4491-4a60-be28-814491ca606a", "coingeckoxmrusd*usdpln*1.3", "XMR", "PLN", False],
["3c5068ce-fcf9-40cc-9068-cefcf920cc02", "coingeckoxmrusd*usdtry*1.3", "XMR", "TRY", False],
["b23aa3b3-4c91-42e4-baa3-b34c9162e4e0", "coingeckoxmrusd*usdhkd*1.3", "XMR", "HKD", False],
["f886768f-b9c9-4cf7-8676-8fb9c9ccf7ee", "coingeckoxmrusd*usdhuf*1.3", "XMR", "HUF", False],
["0addd244-c5cb-40bf-9dd2-44c5cbd0bff4", "coingeckoxmrusd*usdnzd*1.3", "XMR", "NZD", False],
["24a9cad1-0bce-46f3-a9ca-d10bce86f34f", "coingeckoxmrusd*usdrub*1.3", "XMR", "RUB", False],
["80aa52ef-a5d3-462c-aa52-efa5d3862cbe", "coingeckoxmrusd*usdczk*1.3", "XMR", "CZK", False],
["ac9eb9c8-88c7-4add-9eb9-c888c72addb2", "coingeckoxmrusd*usdsgd*1.3", "XMR", "SGD", False],
["68b50d95-91d2-4f21-b50d-9591d22f218d", "coingeckoxmrusd*usdaud*1.3", "XMR", "AUD", False],
["b048bad8-3aaa-4727-88ba-d83aaa1727b6", "coingeckoxmrusd*usdjpy*1.3", "XMR", "JPY", False],
["9c3d9fb6-c74c-4a35-bd9f-b6c74c7a3504", "coingeckoxmrusd*usdnok*1.3", "XMR", "NOK", False],
["9a7bd726-6229-4fda-bbd7-2662295fda98", "coingeckoxmrusd*usdthb*1.3", "XMR", "THB", 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,
},
}

View File

@ -0,0 +1,67 @@
from unittest import TestCase
from unittest.mock import MagicMock, patch
from tests.common import fake_public_ads, cg_prices, expected_to_update
from agora import Agora
from markets import Markets
class TestAgora(TestCase):
def setUp(self):
self.markets = Markets()
self.agora = Agora()
setattr(self.agora, "markets", self.markets)
def mock_wrap_public_ads(self, asset, currency, providers, rates):
try:
return fake_public_ads[currency]
except KeyError:
return
def test_get_all_public_ads(self):
self.agora.cg.get_price = MagicMock()
self.agora.cg.get_price.return_value = cg_prices
self.agora.wrap_public_ads = self.mock_wrap_public_ads
public_ads = self.agora.get_all_public_ads()
self.assertDictEqual(public_ads, fake_public_ads)
def test_get_all_public_ads_only_one(self):
self.agora.cg.get_price = MagicMock()
self.agora.cg.get_price.return_value = cg_prices
self.agora.wrap_public_ads = self.mock_wrap_public_ads
public_ads = self.agora.get_all_public_ads()
for currency, ads in public_ads.items():
ad_ids = [ad[0] for ad in ads]
len_ad_ids = len(ad_ids)
ad_ids_dedup = set(ad_ids)
len_ad_ids_dedup = len(ad_ids_dedup)
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):
self.agora.cg.get_price = MagicMock()
self.agora.cg.get_price.return_value = cg_prices
self.agora.wrap_public_ads = self.mock_wrap_public_ads
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)

View File

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

View File

@ -24,12 +24,6 @@ class Transactions(object):
""" """
self.log = Logger("transactions") self.log = Logger("transactions")
def set_agora(self, agora):
self.agora = agora
def set_irc(self, irc):
self.irc = irc
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.
@ -41,8 +35,47 @@ class Transactions(object):
ts = data["timestamp"] ts = data["timestamp"]
inside = data["data"] inside = data["data"]
txid = inside["id"] txid = inside["id"]
txtype = inside["type"] try:
txtype = inside["type"]
except KeyError:
self.log.error("Typeless event: {id}", id=txid)
return
if txtype == "card_payment":
self.log.info("Ignoring card payment: {id}", id=txid)
return
if "type" not in inside:
stored_trade = r.hgetall(f"tx.{txid}")
print("stored trade", stored_trade)
if not stored_trade:
self.log.error("Could not find entry in DB for typeless transaction: {id}", id=txid)
return
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:
print("valid not in stored_trade", stored_trade)
if stored_trade["valid"] == 1:
print("WOULD RELEASE ESCROW FROM SECONDARY NOTINSIDE UPDATE", stored_trade["trade_id"], stored_trade["txid"])
return
# If type not in inside and we haven't hit any more returns
return
state = inside["state"] state = inside["state"]
if "reference" in inside: if "reference" in inside:
reference = inside["reference"] reference = inside["reference"]
@ -57,11 +90,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,10 +108,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
@ -168,37 +205,60 @@ 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):
""" """
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,
} }
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:
@ -319,3 +379,82 @@ class Transactions(object):
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):
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
return total_usd
def get_total(self):
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
# Convert the total USD price to GBP and SEK
rates = self.agora.get_rates_all()
price_sek = rates["SEK"] * total_usd
price_usd = total_usd
price_gbp = rates["GBP"] * total_usd
return (
(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
def get_remaining(self):
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
return remaining

View File

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