pluto/core/clients/aggregator.py

372 lines
14 KiB
Python
Raw Normal View History

2023-03-09 16:44:16 +00:00
from abc import ABC
2023-03-11 11:51:19 +00:00
import orjson
2023-03-09 23:27:16 +00:00
from core.lib import db, notify
# from core.lib.money import money
from core.util import logs
log = logs.get_logger("aggregator")
2023-03-09 16:44:16 +00:00
class AggregatorClient(ABC):
def store_account_info(self, account_infos):
# account_infos = {
# bank: accounts
# for bank, accounts in account_info.items()
# for account in accounts
# #if account["account_id"] in self.banks
# }
# For each bank
for bank, accounts in account_infos.items():
# Iterate the accounts
for index, account in enumerate(list(accounts)):
if "account_number" not in account:
account_infos[bank][index]["account_number"] = {}
fields = ["sort_code", "number", "iban"]
for field in fields:
if field in account:
account_infos[bank][index]["account_number"][
field
] = account[field]
del account_infos[bank][index][field]
# if len(account["account_number"]) == 1:
# account_infos[bank].remove(account)
currencies = [
account["currency"]
for bank, accounts in account_infos.items()
for account in accounts
]
for bank, accounts in account_infos.items():
if not self.instance.account_info:
self.instance.account_info = {}
self.instance.account_info[bank] = []
for account in accounts:
self.instance.account_info[bank].append(account)
# self.account_info = account_infos
self.currencies = currencies
self.instance.currencies = currencies
self.instance.save()
async def process_transactions(self, account_id, transactions):
if not transactions:
return False
transaction_ids = [x["transaction_id"] for x in transactions]
new_key_name = f"new.transactions.{self.instance.id}.{self.name}.{account_id}"
old_key_name = f"transactions.{self.instance.id}.{self.name}.{account_id}"
# for transaction_id in transaction_ids:
if not transaction_ids:
return
2023-03-09 23:27:16 +00:00
db.r.sadd(new_key_name, *transaction_ids)
2023-03-09 23:27:16 +00:00
difference = list(db.r.sdiff(new_key_name, old_key_name))
2023-03-09 23:27:16 +00:00
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
2023-03-09 23:27:16 +00:00
db.r.rename(new_key_name, old_key_name)
for transaction in new_transactions:
transaction["subclass"] = self.name
# self.tx.transaction(transaction)
2023-03-09 23:27:16 +00:00
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()
2023-03-11 11:51:19 +00:00
# TODO: pass platform here
# 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
2023-03-09 23:27:16 +00:00
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)
2023-03-11 11:51:19 +00:00
log.info(f"Transaction processed: {orjson.dumps(to_store, indent=2)}")
2023-03-09 23:27:16 +00:00
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"]
2023-03-11 11:51:19 +00:00
# platform_buyer = stored_trade["buyer"]
2023-03-09 23:27:16 +00:00
# Check sender - we don't do anything with this yet
2023-03-11 11:51:19 +00:00
# sender_valid = antifraud.check_valid_sender(
2023-03-09 23:27:16 +00:00
# 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)