Finish implementation and tests for the cheat system #3
|
@ -4,3 +4,4 @@ __pycache__/
|
|||
env/
|
||||
keys/
|
||||
handler/settings.ini
|
||||
handler/otp.key
|
||||
|
|
616
handler/agora.py
616
handler/agora.py
|
@ -1,15 +1,44 @@
|
|||
# Twisted/Klein imports
|
||||
from twisted.logger import Logger
|
||||
from twisted.internet.task import LoopingCall
|
||||
from twisted.internet.threads import deferToThread
|
||||
|
||||
# Other library imports
|
||||
from json import loads
|
||||
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
|
||||
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):
|
||||
"""
|
||||
|
@ -24,6 +53,7 @@ class Agora(object):
|
|||
self.log = Logger("agora")
|
||||
self.agora = AgoraDesk(settings.Agora.Token)
|
||||
self.cr = CurrencyRates()
|
||||
self.cg = CoinGeckoAPI()
|
||||
|
||||
# Cache for detecting new trades
|
||||
self.last_dash = set()
|
||||
|
@ -31,11 +61,8 @@ class Agora(object):
|
|||
# Cache for detecting new messages
|
||||
self.last_messages = {}
|
||||
|
||||
def set_irc(self, irc):
|
||||
self.irc = irc
|
||||
|
||||
def set_tx(self, tx):
|
||||
self.tx = tx
|
||||
# Assets that cheat has been run on
|
||||
self.cheat_run_on = []
|
||||
|
||||
def setup_loop(self):
|
||||
"""
|
||||
|
@ -43,20 +70,36 @@ class Agora(object):
|
|||
"""
|
||||
self.lc_dash = LoopingCall(self.loop_check)
|
||||
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):
|
||||
"""
|
||||
Calls hooks to parse dashboard info and get all contact messages.
|
||||
"""
|
||||
dash = self.agora.dashboard_seller()
|
||||
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
|
||||
dash_tmp = self.wrap_dashboard()
|
||||
|
||||
# Call dashboard hooks
|
||||
self.dashboard_hook(dash_tmp)
|
||||
|
@ -69,8 +112,24 @@ class Agora(object):
|
|||
"""
|
||||
Get dashboard helper for IRC only.
|
||||
"""
|
||||
# dash_tmp.append(f"{reference}: {buyer} {amount}{currency} {amount_xmr}XMR")
|
||||
pass
|
||||
dash = self.wrap_dashboard()
|
||||
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):
|
||||
"""
|
||||
|
@ -78,23 +137,31 @@ class Agora(object):
|
|||
Post new trades to IRC and cache trades for the future.
|
||||
"""
|
||||
current_trades = []
|
||||
if not dash:
|
||||
return
|
||||
if not dash.items():
|
||||
return
|
||||
for contact_id, contact in dash.items():
|
||||
reference = self.tx.tx_to_ref(contact_id)
|
||||
if reference:
|
||||
current_trades.append(reference)
|
||||
buyer = contact["data"]["buyer"]["username"]
|
||||
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"]
|
||||
if not contact["data"]["is_selling"]:
|
||||
continue
|
||||
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 not in current_trades:
|
||||
current_trades.append(reference)
|
||||
# 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
|
||||
self.last_dash.add(reference)
|
||||
|
||||
|
@ -106,6 +173,7 @@ class Agora(object):
|
|||
current_trades.append(reference)
|
||||
self.tx.cleanup(current_trades)
|
||||
|
||||
@handle_exceptions
|
||||
def dashboard_release_urls(self):
|
||||
"""
|
||||
Get information about our open trades.
|
||||
|
@ -114,6 +182,8 @@ class Agora(object):
|
|||
:rtype: list or bool
|
||||
"""
|
||||
dash = self.agora.dashboard_seller()
|
||||
if dash is False:
|
||||
return False
|
||||
dash_tmp = []
|
||||
if "data" not in dash["response"]:
|
||||
self.log.error("Data not in dashboard response: {content}", content=dash)
|
||||
|
@ -123,7 +193,11 @@ class Agora(object):
|
|||
contact_id = contact["data"]["contact_id"]
|
||||
buyer = contact["data"]["buyer"]["username"]
|
||||
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"]
|
||||
release_url = contact["actions"]["release_url"]
|
||||
if not contact["data"]["is_selling"]:
|
||||
|
@ -131,18 +205,24 @@ class Agora(object):
|
|||
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}")
|
||||
dash_tmp.append(f"{reference}: {buyer} {amount}{currency} {amount_crypto}{asset} {release_url}")
|
||||
|
||||
return dash_tmp
|
||||
|
||||
@handle_exceptions
|
||||
def get_recent_messages(self, send_irc=True):
|
||||
"""
|
||||
Get recent messages.
|
||||
"""
|
||||
messages_tmp = {}
|
||||
messages = self.agora.recent_messages()
|
||||
if messages is False:
|
||||
return False
|
||||
if not messages["success"]:
|
||||
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()
|
||||
for message in messages["response"]["data"]["message_list"]:
|
||||
contact_id = message["contact_id"]
|
||||
|
@ -176,8 +256,11 @@ class Agora(object):
|
|||
|
||||
return messages_tmp
|
||||
|
||||
@handle_exceptions
|
||||
def enum_ad_ids(self, page=0):
|
||||
ads = self.agora._api_call(api_method="ads", query_values={"page": page})
|
||||
if ads is False:
|
||||
return False
|
||||
ads_total = []
|
||||
if not ads["success"]:
|
||||
return False
|
||||
|
@ -186,24 +269,246 @@ class Agora(object):
|
|||
if "pagination" in ads["response"]:
|
||||
if "next" in ads["response"]["pagination"]:
|
||||
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)
|
||||
return ads_total
|
||||
|
||||
def enum_ads(self, page=0):
|
||||
ads = self.agora._api_call(api_method="ads", query_values={"page": page})
|
||||
@handle_exceptions
|
||||
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 = []
|
||||
if not ads["success"]:
|
||||
return False
|
||||
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 "next" in ads["response"]["pagination"]:
|
||||
page += 1
|
||||
for ad in self.enum_ads(page):
|
||||
ads_total.append([ad[0], ad[1], ad[2]])
|
||||
ads_iter = self.enum_ads(requested_asset, page)
|
||||
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
|
||||
|
||||
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):
|
||||
"""
|
||||
Delete all of our adverts.
|
||||
|
@ -212,7 +517,7 @@ class Agora(object):
|
|||
"""
|
||||
ads = self.enum_ad_ids()
|
||||
return_ids = []
|
||||
if not ads:
|
||||
if ads is False:
|
||||
return False
|
||||
for ad_id in ads:
|
||||
rtrn = self.agora.ad_delete(ad_id)
|
||||
|
@ -248,10 +553,13 @@ class Agora(object):
|
|||
max_local = max_usd * rates[currency]
|
||||
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.
|
||||
:param asset: the crypto asset to list (XMR or BTC)
|
||||
:type asset: string
|
||||
:param countrycode: country code
|
||||
:param currency: currency code
|
||||
:type countrycode: string
|
||||
|
@ -260,90 +568,116 @@ class Agora(object):
|
|||
:rtype: dict
|
||||
"""
|
||||
ad = settings.Agora.Ad
|
||||
paymentdetails = settings.Agora.PaymentDetails
|
||||
|
||||
# Substitute the currency
|
||||
ad = ad.replace("$CURRENCY$", currency)
|
||||
rates = self.get_rates_all()
|
||||
if currency == "USD":
|
||||
min_amount = float(settings.Agora.MinUSD)
|
||||
max_amount = float(settings.Agora.MaxUSD)
|
||||
if currency == "GBP":
|
||||
ad = ad.replace("$PAYMENT$", settings.Agora.GBPDetailsAd)
|
||||
paymentdetailstext = paymentdetails.replace("$PAYMENT$", settings.Agora.GBPDetailsPayment)
|
||||
else:
|
||||
min_amount = rates[currency] * float(settings.Agora.MinUSD)
|
||||
max_amount = rates[currency] * float(settings.Agora.MaxUSD)
|
||||
price_formula = f"coingeckoxmrusd*usd{currency.lower()}*{settings.Agora.Margin}"
|
||||
ad = ad.replace("$PAYMENT$", settings.Agora.DefaultDetailsAd)
|
||||
paymentdetailstext = paymentdetails.replace("$PAYMENT$", settings.Agora.DefaultDetailsPayment)
|
||||
|
||||
# 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}"
|
||||
ad = settings.Agora.Ad
|
||||
|
||||
# Remove extra tabs
|
||||
ad = ad.replace("\\t", "\t")
|
||||
ad = self.agora.ad_create(
|
||||
country_code=countrycode,
|
||||
currency=currency,
|
||||
trade_type="ONLINE_SELL",
|
||||
asset="XMR",
|
||||
price_equation=price_formula,
|
||||
track_max_amount=False,
|
||||
require_trusted_by_advertiser=False,
|
||||
# verified_email_required = False,
|
||||
online_provider="REVOLUT",
|
||||
msg=settings.Agora.Ad,
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
payment_method_details=settings.Agora.PaymentMethodDetails,
|
||||
# require_feedback_score = 0,
|
||||
account_info=settings.Agora.PaymentDetails,
|
||||
)
|
||||
|
||||
form = {
|
||||
"country_code": countrycode,
|
||||
"currency": currency,
|
||||
"trade_type": "ONLINE_SELL",
|
||||
"asset": asset,
|
||||
"price_equation": price_formula,
|
||||
"track_max_amount": False,
|
||||
"require_trusted_by_advertiser": False,
|
||||
"online_provider": provider,
|
||||
"msg": ad,
|
||||
"min_amount": min_amount,
|
||||
"max_amount": max_amount,
|
||||
"payment_method_details": settings.Agora.PaymentMethodDetails,
|
||||
"account_info": paymentdetailstext,
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
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):
|
||||
"""
|
||||
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.
|
||||
:return: False or dict with response
|
||||
:rtype: bool or dict
|
||||
"""
|
||||
for currency, countrycode in loads(settings.Agora.DistList):
|
||||
rtrn = self.create_ad(countrycode, currency)
|
||||
if not rtrn:
|
||||
return False
|
||||
yield rtrn
|
||||
|
||||
def get_combinations(self):
|
||||
"""
|
||||
Get all combinations of currencies and countries from the configuration.
|
||||
:return: list of [country, currency]
|
||||
:rtype: list
|
||||
"""
|
||||
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
|
||||
dist_list = list(self.create_distribution_list())
|
||||
our_ads = self.enum_ads()
|
||||
# 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
|
||||
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):
|
||||
"""
|
||||
Remove duplicate ads.
|
||||
|
@ -365,6 +699,7 @@ class Agora(object):
|
|||
actioned.append(rtrn["success"])
|
||||
return all(actioned)
|
||||
|
||||
@handle_exceptions
|
||||
def release_funds(self, contact_id):
|
||||
"""
|
||||
Release funds for a contact_id.
|
||||
|
@ -375,4 +710,83 @@ class Agora(object):
|
|||
"""
|
||||
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)
|
||||
|
||||
# Check if we can withdraw funds
|
||||
self.withdraw_funds()
|
||||
|
||||
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]
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -15,6 +15,8 @@ from revolut import Revolut
|
|||
from agora import Agora
|
||||
from transactions import Transactions
|
||||
from irc import bot
|
||||
from notify import Notify
|
||||
from markets import Markets
|
||||
|
||||
|
||||
def convert(data):
|
||||
|
@ -37,9 +39,6 @@ class WebApp(object):
|
|||
def __init__(self):
|
||||
self.log = Logger("webapp")
|
||||
|
||||
def set_tx(self, tx):
|
||||
self.tx = tx
|
||||
|
||||
@app.route("/callback", methods=["POST"])
|
||||
def callback(self, request):
|
||||
content = request.content.read()
|
||||
|
@ -54,45 +53,33 @@ class WebApp(object):
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Define IRC and Agora
|
||||
irc = bot()
|
||||
agora = Agora()
|
||||
init_map = {
|
||||
"notify": Notify(),
|
||||
"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
|
||||
if settings.Revolut.SetupToken == "1":
|
||||
deferLater(reactor, 1, revolut.setup_auth)
|
||||
deferLater(reactor, 1, init_map["revolut"].setup_auth)
|
||||
else:
|
||||
# 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
|
||||
deferLater(reactor, 4, revolut.setup_webhook)
|
||||
deferLater(reactor, 4, init_map["revolut"].setup_webhook)
|
||||
# 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))
|
||||
|
||||
# Run the WebApp
|
||||
webapp.app.run("127.0.0.1", 8080)
|
||||
init_map["webapp"].app.run("127.0.0.1", 8080)
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
# Other library imports
|
||||
from json import dumps
|
||||
from json import dumps, loads
|
||||
|
||||
# Project imports
|
||||
from settings import settings
|
||||
|
||||
|
||||
class IRCCommands(object):
|
||||
|
@ -9,14 +12,14 @@ class IRCCommands(object):
|
|||
helptext = "Get all open trades."
|
||||
|
||||
@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.
|
||||
"""
|
||||
# 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
|
||||
# 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:
|
||||
msg("No open trades.")
|
||||
return
|
||||
|
@ -26,18 +29,22 @@ class IRCCommands(object):
|
|||
class create(object):
|
||||
name = "create"
|
||||
authed = True
|
||||
helptext = "Create an ad. Usage: create <country> <currency>"
|
||||
helptext = "Create an ad. Usage: create <XMR/BTC> <country> <currency>"
|
||||
|
||||
@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.
|
||||
"""
|
||||
posted = agora.create_ad(spl[1], spl[2])
|
||||
if posted["success"]:
|
||||
msg(f"{posted['response']['data']['message']}: {posted['response']['data']['ad_id']}")
|
||||
else:
|
||||
msg(posted["response"]["data"]["message"])
|
||||
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"]:
|
||||
msg(f"{posted['response']['data']['message']}: {posted['response']['data']['ad_id']}")
|
||||
else:
|
||||
msg(dumps(posted["response"]))
|
||||
|
||||
class messages(object):
|
||||
name = "messages"
|
||||
|
@ -45,7 +52,7 @@ class IRCCommands(object):
|
|||
helptext = "Get messages. Usage: messages [<reference>]"
|
||||
|
||||
@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.
|
||||
"""
|
||||
|
@ -79,39 +86,26 @@ class IRCCommands(object):
|
|||
helptext = "Distribute all our chosen currency and country ad pairs."
|
||||
|
||||
@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
|
||||
for x in agora.dist_countries():
|
||||
if x["success"]:
|
||||
msg(f"{x['response']['data']['message']}: {x['response']['data']['ad_id']}")
|
||||
else:
|
||||
msg(x["response"]["data"]["message"])
|
||||
msg(dumps(x["response"]))
|
||||
|
||||
class brute(object):
|
||||
name = "brute"
|
||||
class redist(object):
|
||||
name = "redist"
|
||||
authed = True
|
||||
helptext = "Use a bruteforce algorithm to create all possible currency and country pairs."
|
||||
helptext = "Update all ads with details."
|
||||
|
||||
@staticmethod
|
||||
def run(cmd, spl, length, authed, msg, agora, revolut, tx):
|
||||
for x in agora.dist_bruteforce():
|
||||
if x["success"]:
|
||||
msg(f"{x['response']['data']['message']}: {x['response']['data']['ad_id']}")
|
||||
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
|
||||
for x in agora.redist_countries():
|
||||
if x[0]["success"]:
|
||||
msg(f"{x[0]['response']['data']['message']}: {x[1]}")
|
||||
else:
|
||||
msg(dumps(x))
|
||||
|
||||
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))
|
||||
msg(dumps(x[0]["response"]))
|
||||
|
||||
class stripdupes(object):
|
||||
name = "stripdupes"
|
||||
|
@ -119,7 +113,7 @@ class IRCCommands(object):
|
|||
helptext = "Remove all duplicate adverts."
|
||||
|
||||
@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()
|
||||
msg(dumps(rtrn))
|
||||
|
||||
|
@ -129,7 +123,7 @@ class IRCCommands(object):
|
|||
helptext = "Find a transaction. Usage: find <currency> <amount>"
|
||||
|
||||
@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.
|
||||
"""
|
||||
|
@ -151,7 +145,7 @@ class IRCCommands(object):
|
|||
helptext = "Get all account information from Revolut."
|
||||
|
||||
@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_posted = 0
|
||||
if accounts is None:
|
||||
|
@ -167,18 +161,31 @@ class IRCCommands(object):
|
|||
if accounts_posted == 0:
|
||||
msg("No accounts with balances.")
|
||||
|
||||
class total(object):
|
||||
name = "total"
|
||||
class balance(object):
|
||||
name = "balance"
|
||||
authed = True
|
||||
helptext = "Get total account balance from Revolut in USD."
|
||||
|
||||
@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()
|
||||
if total_usd is False:
|
||||
msg("Error getting total balance.")
|
||||
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):
|
||||
name = "ping"
|
||||
authed = False
|
||||
|
@ -188,13 +195,22 @@ class IRCCommands(object):
|
|||
def run(cmd, spl, length, authed, msg):
|
||||
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):
|
||||
name = "release_url"
|
||||
authed = True
|
||||
helptext = "Get release URL for all open trades."
|
||||
|
||||
@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()
|
||||
if not trades:
|
||||
msg("No trades.")
|
||||
|
@ -207,7 +223,7 @@ class IRCCommands(object):
|
|||
helptext = "Send a message on a trade. Usage: msg <reference> <message...>"
|
||||
|
||||
@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:
|
||||
full_msg = " ".join(spl[2:])
|
||||
reference = tx.ref_to_tx(spl[1])
|
||||
|
@ -223,7 +239,7 @@ class IRCCommands(object):
|
|||
helptext = "List all references"
|
||||
|
||||
@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())}")
|
||||
|
||||
class ref(object):
|
||||
|
@ -232,7 +248,7 @@ class IRCCommands(object):
|
|||
helptext = "Get more information about a reference. Usage: ref <reference>"
|
||||
|
||||
@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:
|
||||
ref_data = tx.get_ref(spl[1])
|
||||
if not ref_data:
|
||||
|
@ -246,7 +262,7 @@ class IRCCommands(object):
|
|||
helptext = "Delete a reference. Usage: del <reference>"
|
||||
|
||||
@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:
|
||||
ref_data = tx.get_ref(spl[1])
|
||||
if not ref_data:
|
||||
|
@ -261,14 +277,16 @@ class IRCCommands(object):
|
|||
helptext = "Release funds for a trade. Usage: release <reference>"
|
||||
|
||||
@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:
|
||||
tx = tx.ref_to_tx(spl[1])
|
||||
if not tx:
|
||||
msg(f"No such reference: {spl[1]}")
|
||||
return
|
||||
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):
|
||||
name = "nuke"
|
||||
|
@ -276,20 +294,150 @@ class IRCCommands(object):
|
|||
helptext = "Delete all our adverts."
|
||||
|
||||
@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()
|
||||
msg(dumps(rtrn))
|
||||
|
||||
class wallet(object):
|
||||
name = "wallet"
|
||||
authed = True
|
||||
helptext = "Get Agora wallet balance in XMR."
|
||||
helptext = "Get Agora wallet balances."
|
||||
|
||||
@staticmethod
|
||||
def run(cmd, spl, length, authed, msg, agora, revolut, tx):
|
||||
rtrn = agora.agora.wallet_balance_xmr()
|
||||
if not rtrn["success"]:
|
||||
msg("Error getting wallet details.")
|
||||
def run(cmd, spl, length, authed, msg, agora, revolut, tx, notify):
|
||||
rtrn_xmr = agora.agora.wallet_balance_xmr()
|
||||
if not rtrn_xmr["success"]:
|
||||
msg("Error getting XMR wallet details.")
|
||||
return
|
||||
balance = rtrn["response"]["data"]["total"]["balance"]
|
||||
msg(f"Wallet balance: {balance}XMR")
|
||||
rtrn_btc = agora.agora.wallet_balance()
|
||||
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")
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from twisted.logger import Logger
|
||||
from twisted.words.protocols import irc
|
||||
from twisted.internet import protocol, reactor, ssl
|
||||
from twisted.internet.task import deferLater
|
||||
|
||||
# Project imports
|
||||
from settings import settings
|
||||
|
@ -37,15 +38,6 @@ class IRCBot(irc.IRCClient):
|
|||
|
||||
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):
|
||||
"""
|
||||
Simple handler for IRC commands.
|
||||
|
@ -96,7 +88,7 @@ class IRCBot(irc.IRCClient):
|
|||
# Check if the command required authentication
|
||||
if obj.authed:
|
||||
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:
|
||||
# Handle authentication here instead of in the command module for security
|
||||
self.msg(channel, "Access denied.")
|
||||
|
@ -115,7 +107,7 @@ class IRCBot(irc.IRCClient):
|
|||
Join our channel.
|
||||
"""
|
||||
self.log.info("Signed on as %s" % (self.nickname))
|
||||
self.join(self.channel)
|
||||
deferLater(reactor, 2, self.join, self.channel)
|
||||
|
||||
def joined(self, channel):
|
||||
"""
|
||||
|
@ -156,8 +148,10 @@ class IRCBot(irc.IRCClient):
|
|||
self.parse(user, host, channel, msg[1:])
|
||||
elif host in self.admins and channel == nick:
|
||||
if len(msg) > 0:
|
||||
if msg.split()[0] != "!":
|
||||
self.parse(user, host, channel, msg)
|
||||
spl = msg.split()
|
||||
if len(spl) > 0:
|
||||
if spl[0] != "!":
|
||||
self.parse(user, host, channel, msg)
|
||||
|
||||
def noticed(self, user, channel, msg):
|
||||
"""
|
||||
|
@ -179,15 +173,6 @@ class IRCBotFactory(protocol.ClientFactory):
|
|||
def __init__(self):
|
||||
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):
|
||||
"""
|
||||
Passthrough function to send a message to the channel.
|
||||
|
@ -206,9 +191,10 @@ class IRCBotFactory(protocol.ClientFactory):
|
|||
"""
|
||||
prcol = IRCBot(self.log)
|
||||
self.client = prcol
|
||||
self.client.set_agora(self.agora)
|
||||
self.client.set_revolut(self.revolut)
|
||||
self.client.set_tx(self.tx)
|
||||
setattr(self.client, "agora", self.agora)
|
||||
setattr(self.client, "revolut", self.revolut)
|
||||
setattr(self.client, "tx", self.tx)
|
||||
setattr(self.client, "notify", self.notify)
|
||||
return prcol
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
|
|
|
@ -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
|
|
@ -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")
|
|
@ -8,6 +8,8 @@ import requests
|
|||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
import jwt
|
||||
from random import choices
|
||||
from string import ascii_uppercase
|
||||
|
||||
# Project imports
|
||||
from settings import settings
|
||||
|
@ -26,12 +28,6 @@ class Revolut(object):
|
|||
self.log = Logger("revolut")
|
||||
self.token = None
|
||||
|
||||
def set_irc(self, irc):
|
||||
self.irc = irc
|
||||
|
||||
def set_agora(self, agora):
|
||||
self.agora = agora
|
||||
|
||||
def setup_auth(self):
|
||||
"""
|
||||
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.SetupToken = "0"
|
||||
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.log.info("Refreshed access token: {access_token}", access_token=self.token)
|
||||
self.log.info("Refreshed access token")
|
||||
except KeyError:
|
||||
self.log.error(f"Token authorization didn't contain refresh or access token: {parsed}", parsed=parsed)
|
||||
return False
|
||||
|
@ -119,7 +115,7 @@ class Revolut(object):
|
|||
if r.status_code == 200:
|
||||
if "access_token" in parsed.keys():
|
||||
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
|
||||
else:
|
||||
self.log.error(f"Token refresh didn't contain access token: {parsed}", parsed=parsed)
|
||||
|
@ -199,3 +195,53 @@ class Revolut(object):
|
|||
else:
|
||||
total_usd += account["balance"] / rates[account["currency"]]
|
||||
return total_usd
|
||||
|
||||
def convert(self, from_account_id, from_currency, to_account_id, to_currency, sell_amount):
|
||||
"""
|
||||
Convert currency.
|
||||
:param sell_currency: currency to sell
|
||||
:param buy_currency: currency to buy
|
||||
:param sell_amount: amount of currency to sell
|
||||
"""
|
||||
reference = "".join(choices(ascii_uppercase, k=5))
|
||||
headers = {"Authorization": f"Bearer {self.token}"}
|
||||
data = {
|
||||
"from": {
|
||||
"account_id": from_account_id,
|
||||
"currency": from_currency,
|
||||
"amount": sell_amount,
|
||||
},
|
||||
"to": {
|
||||
"account_id": to_account_id,
|
||||
"currency": to_currency,
|
||||
},
|
||||
"request_id": reference,
|
||||
}
|
||||
r = requests.post(f"{settings.Revolut.Base}/exchange", headers=headers, data=dumps(data))
|
||||
if r.status_code == 200:
|
||||
return r.json()
|
||||
else:
|
||||
self.log.error("Error converting balance: {content}", content=r.content)
|
||||
return False
|
||||
|
||||
def shuffle(self, currency):
|
||||
"""
|
||||
Exchange money in all accounts to the given currency.
|
||||
:param currency: the currency to convert all our funds to
|
||||
"""
|
||||
accounts = self.accounts()
|
||||
|
||||
# Find given currency account
|
||||
for account in accounts:
|
||||
if account["currency"] == currency:
|
||||
if account["state"] == "active" and account["public"] is True:
|
||||
dest_account = account
|
||||
# Remove this account
|
||||
accounts.remove(dest_account)
|
||||
break
|
||||
|
||||
for account in accounts:
|
||||
if account["balance"] > 0:
|
||||
self.convert(account["id"], account["currency"], dest_account["id"], dest_account["currency"], account["balance"])
|
||||
|
||||
return True
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
|
@ -24,12 +24,6 @@ class Transactions(object):
|
|||
"""
|
||||
self.log = Logger("transactions")
|
||||
|
||||
def set_agora(self, agora):
|
||||
self.agora = agora
|
||||
|
||||
def set_irc(self, irc):
|
||||
self.irc = irc
|
||||
|
||||
def transaction(self, data):
|
||||
"""
|
||||
Store details of transaction and post notifications to IRC.
|
||||
|
@ -41,8 +35,47 @@ class Transactions(object):
|
|||
ts = data["timestamp"]
|
||||
|
||||
inside = data["data"]
|
||||
|
||||
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"]
|
||||
if "reference" in inside:
|
||||
reference = inside["reference"]
|
||||
|
@ -57,11 +90,15 @@ class Transactions(object):
|
|||
account_type = "not_given"
|
||||
|
||||
amount = leg["amount"]
|
||||
if amount <= 0:
|
||||
self.log.info("Ignoring transaction with negative/zero amount: {id}", id=txid)
|
||||
return
|
||||
currency = leg["currency"]
|
||||
description = leg["description"]
|
||||
|
||||
to_store = {
|
||||
"event": event,
|
||||
"trade_id": "",
|
||||
"ts": ts,
|
||||
"txid": txid,
|
||||
"txtype": txtype,
|
||||
|
@ -71,10 +108,10 @@ class Transactions(object):
|
|||
"amount": amount,
|
||||
"currency": currency,
|
||||
"description": description,
|
||||
"valid": 0, # All checks passed and we can release escrow?
|
||||
}
|
||||
|
||||
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}")
|
||||
|
||||
# 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}")
|
||||
return
|
||||
# Make sure the account type was Revolut, as these are completed instantly
|
||||
if not account_type == "revolut":
|
||||
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}")
|
||||
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))
|
||||
# if not account_type == "revolut":
|
||||
# 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}")
|
||||
# return
|
||||
|
||||
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.
|
||||
Store details in Redis, generate a reference and optionally let the customer know the reference.
|
||||
"""
|
||||
reference = "".join(choices(ascii_uppercase, k=5))
|
||||
reference = f"XMR-{reference}"
|
||||
reference = f"{asset}-{reference}"
|
||||
existing_ref = r.get(f"trade.{trade_id}.reference")
|
||||
if not existing_ref:
|
||||
r.set(f"trade.{trade_id}.reference", reference)
|
||||
to_store = {
|
||||
"id": trade_id,
|
||||
"asset": asset,
|
||||
"buyer": buyer,
|
||||
"currency": currency,
|
||||
"amount": amount,
|
||||
"amount_xmr": amount_xmr,
|
||||
"amount_crypto": amount_crypto,
|
||||
"reference": reference,
|
||||
}
|
||||
self.log.info("Storing trade information: {info}", info=str(to_store))
|
||||
r.hmset(f"trade.{reference}", to_store)
|
||||
self.irc.sendmsg(f"Generated reference for {trade_id}: {reference}")
|
||||
self.notify.notify_new_trade(amount, currency)
|
||||
if settings.Agora.Send == "1":
|
||||
self.agora.agora.contact_message_post(trade_id, f"Hi! When sending the payment please use reference code: {reference}")
|
||||
if existing_ref:
|
||||
|
@ -319,3 +379,82 @@ class Transactions(object):
|
|||
if not ref_data:
|
||||
return False
|
||||
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
|
||||
|
|
|
@ -1,4 +1,15 @@
|
|||
flask
|
||||
flask_sqlalchemy
|
||||
twisted
|
||||
redis
|
||||
pyOpenSSL
|
||||
Klein
|
||||
ConfigObject
|
||||
service_identity
|
||||
forex_python
|
||||
simplejson
|
||||
requests
|
||||
PyJWT
|
||||
arrow
|
||||
httpx
|
||||
pre-commit
|
||||
pycoingecko
|
||||
PyOTP
|
||||
|
|
Loading…
Reference in New Issue