Begin adding platform support
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
from abc import ABC
|
||||
|
||||
from core.lib.db import convert, r
|
||||
from core.lib import db, notify
|
||||
|
||||
# from core.lib.money import money
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("aggregator")
|
||||
|
||||
|
||||
class AggregatorClient(ABC):
|
||||
@@ -52,18 +57,312 @@ class AggregatorClient(ABC):
|
||||
# for transaction_id in transaction_ids:
|
||||
if not transaction_ids:
|
||||
return
|
||||
r.sadd(new_key_name, *transaction_ids)
|
||||
db.r.sadd(new_key_name, *transaction_ids)
|
||||
|
||||
difference = list(r.sdiff(new_key_name, old_key_name))
|
||||
difference = list(db.r.sdiff(new_key_name, old_key_name))
|
||||
|
||||
difference = convert(difference)
|
||||
difference = db.convert(difference)
|
||||
|
||||
new_transactions = [
|
||||
x for x in transactions if x["transaction_id"] in difference
|
||||
]
|
||||
|
||||
# Rename the new key to the old key so we can run the diff again
|
||||
r.rename(new_key_name, old_key_name)
|
||||
db.r.rename(new_key_name, old_key_name)
|
||||
for transaction in new_transactions:
|
||||
transaction["subclass"] = self.name
|
||||
# self.tx.transaction(transaction)
|
||||
|
||||
def valid_transaction(self, data):
|
||||
"""
|
||||
Determine if a given transaction object is valid.
|
||||
:param data: a transaction cast
|
||||
:type data: dict
|
||||
:return: whether the transaction is valid
|
||||
:rtype: bool
|
||||
"""
|
||||
txid = data["transaction_id"]
|
||||
if "amount" not in data:
|
||||
return False
|
||||
if "currency" not in data:
|
||||
return False
|
||||
amount = data["amount"]
|
||||
if amount <= 0:
|
||||
log.info(f"Ignoring transaction with negative/zero amount: {txid}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def extract_reference(self, data):
|
||||
"""
|
||||
Extract a reference from the transaction cast.
|
||||
:param data: a transaction cast
|
||||
:type data: dict
|
||||
:return: the extracted reference or not_set
|
||||
:rtype: str
|
||||
"""
|
||||
if "reference" in data:
|
||||
return data["reference"]
|
||||
elif "meta" in data:
|
||||
if "provider_reference" in data["meta"]:
|
||||
return data["meta"]["provider_reference"]
|
||||
return "not_set"
|
||||
|
||||
def extract_sender(self, data):
|
||||
"""
|
||||
Extract a sender name from the transaction cast.
|
||||
:param data: a transaction cast
|
||||
:type data: dict
|
||||
:return: the sender name or not_set
|
||||
:rtype: str
|
||||
"""
|
||||
if "debtorName" in data:
|
||||
return data["debtorName"]
|
||||
elif "meta" in data:
|
||||
if "debtor_account_name" in data["meta"]:
|
||||
return data["meta"]["debtor_account_name"]
|
||||
elif " " in data["reference"]:
|
||||
refsplit = data["reference"].split(" ")
|
||||
if not len(refsplit) == 2:
|
||||
log.error(f"Sender cannot be extracted: {data}")
|
||||
return "not_set"
|
||||
realname, part2 = data["reference"].split(" ")
|
||||
return realname
|
||||
|
||||
return "not_set"
|
||||
|
||||
async def reference_partial_check(self, reference, txid, currency, amount):
|
||||
"""
|
||||
Perform a partial check by intersecting all parts of the split of the
|
||||
reference against the existing references, and returning a set of the matches.
|
||||
:param reference: the reference to check
|
||||
:type reference: str
|
||||
:return: matching trade ID string
|
||||
:rtype: str
|
||||
"""
|
||||
# Partial reference implementation
|
||||
# Account for silly people not removing the default string
|
||||
# Split the reference into parts
|
||||
ref_split = reference.split(" ")
|
||||
# Get all existing references
|
||||
existing_refs = await db.get_refs()
|
||||
# Get all parts of the given reference split that match the existing references
|
||||
stored_trade_reference = set(existing_refs).intersection(set(ref_split))
|
||||
if len(stored_trade_reference) > 1:
|
||||
message = (
|
||||
f"Multiple references valid for TXID {txid}: {reference}"
|
||||
f"Currency: {currency} | Amount: {amount}"
|
||||
)
|
||||
title = "Error: multiple references valid"
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
return False
|
||||
if len(stored_trade_reference) == 0:
|
||||
return None
|
||||
return stored_trade_reference.pop()
|
||||
|
||||
async def can_alt_lookup(self, amount, currency, reference):
|
||||
amount_usd = self.money.to_usd(amount, currency)
|
||||
# Amount is reliable here as it is checked by find_trade,
|
||||
# so no need for stored_trade["amount"]
|
||||
if float(amount_usd) > float(settings.Agora.AcceptableAltLookupUSD):
|
||||
message = (
|
||||
f"Amount exceeds max for {reference}"
|
||||
f"Currency: {currency} | Amount: {amount}"
|
||||
)
|
||||
title = "Amount exceeds max for {reference}"
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
return False
|
||||
return True
|
||||
|
||||
async def amount_currency_lookup(self, amount, currency, txid, reference):
|
||||
log.info(f"No reference in DB refs for {reference}")
|
||||
self.irc.sendmsg(f"No reference in DB refs for {reference}")
|
||||
# Try checking just amount and currency, as some people
|
||||
# (usually people buying small amounts)
|
||||
# are unable to put in a reference properly.
|
||||
|
||||
log.info(f"Checking against amount and currency for TXID {txid}")
|
||||
self.irc.sendmsg(f"Checking against amount and currency for TXID {txid}")
|
||||
title = f"Checking against amount and currency for TXID {txid}"
|
||||
message = (
|
||||
f"Checking against amount and currency for TXID {txid}"
|
||||
f"Currency: {currency} | Amount: {amount}"
|
||||
)
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
if not await self.can_alt_lookup(amount, currency, reference):
|
||||
return False
|
||||
stored_trade = await self.find_trade(txid, currency, amount)
|
||||
if not stored_trade:
|
||||
title = f"Failed to get reference by amount and currency: {txid}"
|
||||
message = (
|
||||
f"Failed to get reference by amount and currency: {txid}"
|
||||
f"Currency: {currency} | Amount: {amount}"
|
||||
)
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
return None
|
||||
stored_trade["amount"] = float(stored_trade["amount"]) # convert to float
|
||||
return stored_trade
|
||||
|
||||
async def normal_lookup(self, stored_trade_reference, reference, currency, amount):
|
||||
stored_trade = await db.get_ref(stored_trade_reference)
|
||||
if not stored_trade:
|
||||
title = f"No reference in DB for {reference}"
|
||||
message = (
|
||||
f"No reference in DB for {reference}"
|
||||
f"Currency: {currency} | Amount: {amount}"
|
||||
)
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
return False
|
||||
stored_trade["amount"] = float(stored_trade["amount"]) # convert to float
|
||||
return stored_trade
|
||||
|
||||
async def currency_check(self, currency, amount, reference, stored_trade):
|
||||
if not stored_trade["currency"] == currency:
|
||||
title = "Currency mismatch"
|
||||
message = (
|
||||
f"Currency mismatch, Agora: {stored_trade['currency']} "
|
||||
f"/ Sink: {currency}"
|
||||
)
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
return False
|
||||
return True
|
||||
|
||||
async def alt_amount_check(
|
||||
self, platform, amount, currency, reference, stored_trade
|
||||
):
|
||||
# If the amount does not match exactly, get the min and max values for our
|
||||
# given acceptable margins for trades
|
||||
min_amount, max_amount = await self.money.get_acceptable_margins(
|
||||
platform, currency, stored_trade["amount"]
|
||||
)
|
||||
log.info(
|
||||
(
|
||||
f"Amount does not match exactly, trying with margins: min: {min_amount}"
|
||||
f" / max: {max_amount}"
|
||||
)
|
||||
)
|
||||
self.irc.sendmsg()
|
||||
title = "Amount does not match exactly"
|
||||
message = (
|
||||
f"Amount does not match exactly, trying with margins: min: "
|
||||
f"{min_amount} / max: {max_amount}"
|
||||
)
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
if not min_amount < amount < max_amount:
|
||||
title = "Amount mismatch - not in margins"
|
||||
message = (
|
||||
f"Amount mismatch - not in margins: {stored_trade['amount']} "
|
||||
f"(min: {min_amount} / max: {max_amount}"
|
||||
)
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
return False
|
||||
return True
|
||||
|
||||
async def transaction(self, data):
|
||||
"""
|
||||
Store details of transaction and post notifications to IRC.
|
||||
Matches it up with data stored in Redis to attempt to reconcile with an Agora
|
||||
trade.
|
||||
:param data: details of transaction
|
||||
:type data: dict
|
||||
"""
|
||||
valid = self.valid_transaction(data)
|
||||
if not valid:
|
||||
return False
|
||||
ts = data["timestamp"]
|
||||
txid = data["transaction_id"]
|
||||
amount = float(data["amount"])
|
||||
currency = data["currency"]
|
||||
|
||||
reference = self.extract_reference(data)
|
||||
sender = self.extract_sender(data)
|
||||
|
||||
subclass = data["subclass"]
|
||||
to_store = {
|
||||
"subclass": subclass,
|
||||
"ts": ts,
|
||||
"txid": txid,
|
||||
"reference": reference,
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"sender": sender,
|
||||
}
|
||||
db.r.hmset(f"tx.{txid}", to_store)
|
||||
|
||||
log.info(f"Transaction processed: {dumps(to_store, indent=2)}")
|
||||
self.irc.sendmsg(
|
||||
(
|
||||
f"AUTO Incoming transaction on {subclass}: {txid} {amount}{currency} "
|
||||
f"({reference})"
|
||||
)
|
||||
)
|
||||
|
||||
stored_trade_reference = self.reference_partial_check(
|
||||
reference, txid, currency, amount
|
||||
)
|
||||
if stored_trade_reference is False: # can be None though
|
||||
return
|
||||
|
||||
stored_trade = False
|
||||
looked_up_without_reference = False
|
||||
|
||||
# Normal implementation for when we have a reference
|
||||
if stored_trade_reference:
|
||||
stored_trade = self.normal_lookup(
|
||||
stored_trade_reference, reference, currency, amount
|
||||
)
|
||||
# if not stored_trade:
|
||||
# return
|
||||
|
||||
# Amount/currency lookup implementation for when we have no reference
|
||||
else:
|
||||
if not stored_trade: # check we don't overwrite the lookup above
|
||||
stored_trade = self.amount_currency_lookup(
|
||||
amount, currency, txid, reference
|
||||
)
|
||||
if stored_trade is False:
|
||||
return
|
||||
if stored_trade:
|
||||
# Note that we have looked it up without reference so we don't use
|
||||
# +- below
|
||||
# This might be redundant given the amount checks in find_trade,
|
||||
# but better safe than sorry!
|
||||
looked_up_without_reference = True
|
||||
else:
|
||||
return
|
||||
else:
|
||||
# Stored trade reference is none, the checks below will do nothing
|
||||
return
|
||||
|
||||
# Make sure it was sent in the expected currency
|
||||
if not self.currency_check(currency, amount, reference, stored_trade):
|
||||
return
|
||||
|
||||
# Make sure the expected amount was sent
|
||||
if not stored_trade["amount"] == amount:
|
||||
if looked_up_without_reference:
|
||||
return
|
||||
platform = stored_trade["subclass"]
|
||||
if not self.alt_amount_check(
|
||||
platform, amount, currency, reference, stored_trade
|
||||
):
|
||||
return
|
||||
platform = stored_trade["subclass"]
|
||||
platform_buyer = stored_trade["buyer"]
|
||||
|
||||
# Check sender - we don't do anything with this yet
|
||||
# sender_valid = self.antifraud.check_valid_sender(
|
||||
# reference, platform, sender, platform_buyer
|
||||
# )
|
||||
# log.info(f"Trade {reference} buyer {platform_buyer} valid: {sender_valid}")
|
||||
# trade_released = self.release_map_trade(reference, txid)
|
||||
# if trade_released:
|
||||
# self.ux.notify.notify_complete_trade(amount, currency)
|
||||
# else:
|
||||
# log.error(f"Cannot release trade {reference}.")
|
||||
# return
|
||||
|
||||
rtrn = await self.release_funds(stored_trade["id"], stored_trade["reference"])
|
||||
if rtrn:
|
||||
title = "Trade complete"
|
||||
message = f"Trade complete: {amount}{currency}"
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
|
||||
1271
core/clients/platform.py
Normal file
1271
core/clients/platform.py
Normal file
File diff suppressed because it is too large
Load Diff
120
core/clients/platforms/agora.py
Normal file
120
core/clients/platforms/agora.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# Other library imports
|
||||
from pyotp import TOTP
|
||||
|
||||
from core.clients.base import BaseClient
|
||||
from core.clients.platform import LocalPlatformClient
|
||||
from core.lib.money import Money
|
||||
|
||||
|
||||
class AgoraClient(BaseClient, LocalPlatformClient):
|
||||
"""
|
||||
AgoraDesk API handler.
|
||||
"""
|
||||
|
||||
async def release_funds(self, contact_id):
|
||||
"""
|
||||
Release funds for a contact_id.
|
||||
:param contact_id: trade/contact ID
|
||||
:type contact_id: string
|
||||
:return: response dict
|
||||
:rtype: dict
|
||||
"""
|
||||
print("CALLING RELEASE FUNDS", contact_id)
|
||||
if self.instance.dummy:
|
||||
self.log.error(
|
||||
f"Running in dummy mode, not releasing funds for {contact_id}"
|
||||
)
|
||||
return
|
||||
payload = {"tradeId": contact_id, "password": self.sets.Pass}
|
||||
rtrn = await self.api._api_call(
|
||||
api_method=f"contact_release/{contact_id}",
|
||||
http_method="POST",
|
||||
query_values=payload,
|
||||
)
|
||||
|
||||
# Check if we can withdraw funds
|
||||
await self.withdraw_funds()
|
||||
|
||||
return rtrn
|
||||
|
||||
# TODO: write test before re-enabling adding total_trades
|
||||
async def withdraw_funds(self):
|
||||
"""
|
||||
Withdraw excess funds to our XMR wallets.
|
||||
"""
|
||||
print("CALLING WITHDRAW FUNDS")
|
||||
totals_all = await self.money.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]
|
||||
|
||||
# total_trades_usd = self.tx.get_open_trades_usd()
|
||||
if not total_usd:
|
||||
return False
|
||||
# total_usd += total_trades_usd
|
||||
|
||||
profit_usd = total_usd - float(settings.Money.BaseUSD)
|
||||
# Get the XMR -> USD exchange rate
|
||||
xmr_usd = self.money.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(
|
||||
(
|
||||
f"Not enough funds to withdraw {profit_usd_in_xmr}, "
|
||||
f"as wallet only contains {wallet_xmr}"
|
||||
)
|
||||
)
|
||||
self.irc.sendmsg(
|
||||
(
|
||||
f"Not enough funds to withdraw {profit_usd_in_xmr}, "
|
||||
f"as wallet only contains {wallet_xmr}"
|
||||
)
|
||||
)
|
||||
self.ux.notify.notify_need_topup(profit_usd_in_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 = await self.api.wallet_send_xmr(**send_cast)
|
||||
|
||||
send_cast["address"] = settings.XMR.Wallet2
|
||||
rtrn2 = await self.api.wallet_send_xmr(**send_cast)
|
||||
|
||||
self.irc.sendmsg(f"Withdrawal: {rtrn1['success']} | {rtrn2['success']}")
|
||||
self.ux.notify.notify_withdrawal(half_rounded)
|
||||
1031
core/clients/platforms/api/agoradesk.py
Normal file
1031
core/clients/platforms/api/agoradesk.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user