Implement transaction handling

This commit is contained in:
Mark Veidemanis 2023-03-13 18:49:47 +00:00
parent 780adf3bc1
commit 7d1bd75f48
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
12 changed files with 1098 additions and 318 deletions

View File

@ -1,10 +1,8 @@
from abc import ABC from abc import ABC
import orjson from core.clients.platforms.agora import AgoraClient
from core.lib import notify
from core.lib import db, notify from core.lib.money import money
# from core.lib.money import money
from core.util import logs from core.util import logs
log = logs.get_logger("aggregator") log = logs.get_logger("aggregator")
@ -53,29 +51,55 @@ class AggregatorClient(ABC):
async def process_transactions(self, account_id, transactions): async def process_transactions(self, account_id, transactions):
if not transactions: if not transactions:
return False 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
await db.r.sadd(new_key_name, *transaction_ids)
difference = list(await db.r.sdiff(new_key_name, old_key_name)) platforms = self.platforms
for transaction in transactions:
transaction_id = transaction["transaction_id"]
tx_obj = self.instance.get_transaction(
account_id,
transaction_id,
)
if tx_obj is None:
tx_cast = {
"transaction_id": transaction_id,
"recipient": transaction["creditorName"],
"sender": transaction["debtorName"],
"amount": transaction["amount"],
"currency": transaction["currency"],
"note": transaction["reference"],
}
tx_obj = self.instance.add_transaction(
account_id,
tx_cast,
)
# New transaction
await self.transaction(platforms, tx_obj)
else:
# Transaction exists
continue
# 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
# await db.r.sadd(new_key_name, *transaction_ids)
difference = db.convert(difference) # difference = list(await db.r.sdiff(new_key_name, old_key_name))
new_transactions = [ # difference = db.convert(difference)
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 # new_transactions = [
await db.r.rename(new_key_name, old_key_name) # x for x in transactions if x["transaction_id"] in difference
for transaction in new_transactions: # ]
transaction["subclass"] = self.name
# self.tx.transaction(transaction)
def valid_transaction(self, data): # # Rename the new key to the old key so we can run the diff again
# await 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, tx_obj):
""" """
Determine if a given transaction object is valid. Determine if a given transaction object is valid.
:param data: a transaction cast :param data: a transaction cast
@ -83,56 +107,58 @@ class AggregatorClient(ABC):
:return: whether the transaction is valid :return: whether the transaction is valid
:rtype: bool :rtype: bool
""" """
txid = data["transaction_id"] txid = tx_obj.transaction_id
if "amount" not in data: if tx_obj.amount is None:
return False return False
if "currency" not in data: if tx_obj.currency is None:
return False return False
amount = data["amount"] amount = tx_obj.amount
if amount <= 0: if amount <= 0:
log.info(f"Ignoring transaction with negative/zero amount: {txid}") log.info(f"Ignoring transaction with negative/zero amount: {txid}")
return False return False
return True return True
def extract_reference(self, data): # def extract_reference(self, data):
""" # """
Extract a reference from the transaction cast. # Extract a reference from the transaction cast.
:param data: a transaction cast # :param data: a transaction cast
:type data: dict # :type data: dict
:return: the extracted reference or not_set # :return: the extracted reference or not_set
:rtype: str # :rtype: str
""" # """
if "reference" in data: # if "reference" in data:
return data["reference"] # return data["reference"]
elif "meta" in data: # elif "meta" in data:
if "provider_reference" in data["meta"]: # if "provider_reference" in data["meta"]:
return data["meta"]["provider_reference"] # return data["meta"]["provider_reference"]
return "not_set" # return "not_set"
def extract_sender(self, data): # def extract_sender(self, data):
""" # """
Extract a sender name from the transaction cast. # Extract a sender name from the transaction cast.
:param data: a transaction cast # :param data: a transaction cast
:type data: dict # :type data: dict
:return: the sender name or not_set # :return: the sender name or not_set
:rtype: str # :rtype: str
""" # """
if "debtorName" in data: # if "debtorName" in data:
return data["debtorName"] # return data["debtorName"]
elif "meta" in data: # elif "meta" in data:
if "debtor_account_name" in data["meta"]: # if "debtor_account_name" in data["meta"]:
return data["meta"]["debtor_account_name"] # return data["meta"]["debtor_account_name"]
elif " " in data["reference"]: # elif " " in data["reference"]:
refsplit = data["reference"].split(" ") # refsplit = data["reference"].split(" ")
if not len(refsplit) == 2: # if not len(refsplit) == 2:
log.error(f"Sender cannot be extracted: {data}") # log.error(f"Sender cannot be extracted: {data}")
return "not_set" # return "not_set"
realname, part2 = data["reference"].split(" ") # realname, part2 = data["reference"].split(" ")
return realname # return realname
return "not_set" # return "not_set"
async def reference_partial_check(self, reference, txid, currency, amount): async def reference_partial_check(
self, platform, reference, txid, currency, amount
):
""" """
Perform a partial check by intersecting all parts of the split of the 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. reference against the existing references, and returning a set of the matches.
@ -146,9 +172,10 @@ class AggregatorClient(ABC):
# Split the reference into parts # Split the reference into parts
ref_split = reference.split(" ") ref_split = reference.split(" ")
# Get all existing references # Get all existing references
existing_refs = await db.get_refs() existing_refs = platform.references
# Get all parts of the given reference split that match the existing references # Get all parts of the given reference split that match the existing references
stored_trade_reference = set(existing_refs).intersection(set(ref_split)) # stored_trade_reference = set(existing_refs).intersection(set(ref_split))
stored_trade_reference = [x for x in existing_refs if x in ref_split]
if len(stored_trade_reference) > 1: if len(stored_trade_reference) > 1:
message = ( message = (
f"Multiple references valid for TXID {txid}: {reference}" f"Multiple references valid for TXID {txid}: {reference}"
@ -162,38 +189,59 @@ class AggregatorClient(ABC):
return stored_trade_reference.pop() return stored_trade_reference.pop()
# TODO: pass platform here # TODO: pass platform here
# async def can_alt_lookup(self, amount, currency, reference): async def can_alt_lookup(self, platform, amount, currency, reference):
# amount_usd = self.money.to_usd(amount, currency) amount_usd = await money.to_usd(amount, currency)
# # Amount is reliable here as it is checked by find_trade, # Amount is reliable here as it is checked by find_trade,
# # so no need for stored_trade["amount"] # so no need for stored_trade["amount"]
# if float(amount_usd) > float(settings.Agora.AcceptableAltLookupUSD): if amount_usd > platform.no_reference_amount_check_max_usd:
# message = ( message = (
# f"Amount exceeds max for {reference}" f"Amount exceeds max for {reference}"
# f"Currency: {currency} | Amount: {amount}" f"Currency: {currency} | Amount: {amount}"
# ) )
# title = "Amount exceeds max for {reference}" title = "Amount exceeds max for {reference}"
# await notify.sendmsg(self.instance.user, message, title=title) await notify.sendmsg(self.instance.user, message, title=title)
# return False return False
# return True return True
async def amount_currency_lookup(self, amount, currency, txid, reference): def find_trade(self, platform, txid, currency, amount):
log.info(f"No reference in DB refs for {reference}") """
self.irc.sendmsg(f"No reference in DB refs for {reference}") Get a trade reference that matches the given currency and amount.
# Try checking just amount and currency, as some people Only works if there is one result.
# (usually people buying small amounts) :param txid: Sink transaction ID
# are unable to put in a reference properly. :param currency: currency
:param amount: amount
:type txid: string
:type currency: string
:type amount: int
:return: matching trade object or False
:rtype: dict or bool
"""
refs = platform.references
matching_refs = []
# TODO: use get_ref_map in this function instead of calling get_ref multiple
# times
for ref in refs:
stored_trade = platform.get_trade_by_reference(ref)
if stored_trade.currency == currency and stored_trade.amount_fiat == amount:
matching_refs.append(stored_trade)
if len(matching_refs) != 1:
log.error(
f"Find trade returned multiple results for TXID {txid}: {matching_refs}"
)
return False
return matching_refs[0]
log.info(f"Checking against amount and currency for TXID {txid}") async def amount_currency_lookup(self, platform, amount, currency, txid, ref):
self.irc.sendmsg(f"Checking against amount and currency for TXID {txid}")
title = f"Checking against amount and currency for TXID {txid}" title = f"Checking against amount and currency for TXID {txid}"
message = ( message = (
f"Checking against amount and currency for TXID {txid}" f"Checking against amount and currency for TXID {txid}"
f"Currency: {currency} | Amount: {amount}" f"Currency: {currency} | Amount: {amount}"
) )
await notify.sendmsg(self.instance.user, message, title=title) await notify.sendmsg(self.instance.user, message, title=title)
if not await self.can_alt_lookup(amount, currency, reference):
if not await self.can_alt_lookup(platform, amount, currency, ref):
return False return False
stored_trade = await self.find_trade(txid, currency, amount) stored_trade = self.find_trade(platform, txid, currency, amount)
if not stored_trade: if not stored_trade:
title = f"Failed to get reference by amount and currency: {txid}" title = f"Failed to get reference by amount and currency: {txid}"
message = ( message = (
@ -202,11 +250,12 @@ class AggregatorClient(ABC):
) )
await notify.sendmsg(self.instance.user, message, title=title) await notify.sendmsg(self.instance.user, message, title=title)
return None return None
stored_trade["amount"] = float(stored_trade["amount"]) # convert to float
return stored_trade return stored_trade
async def normal_lookup(self, stored_trade_reference, reference, currency, amount): async def normal_lookup(
stored_trade = await db.get_ref(stored_trade_reference) self, platform, stored_trade_reference, reference, currency, amount
):
stored_trade = platform.get_trade_by_reference(stored_trade_reference)
if not stored_trade: if not stored_trade:
title = f"No reference in DB for {reference}" title = f"No reference in DB for {reference}"
message = ( message = (
@ -215,27 +264,25 @@ class AggregatorClient(ABC):
) )
await notify.sendmsg(self.instance.user, message, title=title) await notify.sendmsg(self.instance.user, message, title=title)
return False return False
stored_trade["amount"] = float(stored_trade["amount"]) # convert to float # stored_trade["amount"] = float(stored_trade["amount"]) # convert to float
return stored_trade return stored_trade
async def currency_check(self, currency, amount, reference, stored_trade): async def currency_check(self, currency, stored_trade):
if not stored_trade["currency"] == currency: if not stored_trade.currency == currency:
title = "Currency mismatch" title = "Currency mismatch"
message = ( message = (
f"Currency mismatch, Agora: {stored_trade['currency']} " f"Currency mismatch, Agora: {stored_trade.currency} "
f"/ Sink: {currency}" f"/ Sink: {currency}"
) )
await notify.sendmsg(self.instance.user, message, title=title) await notify.sendmsg(self.instance.user, message, title=title)
return False return False
return True return True
async def alt_amount_check( async def alt_amount_check(self, platform, amount, currency, stored_trade):
self, platform, amount, currency, reference, stored_trade
):
# If the amount does not match exactly, get the min and max values for our # If the amount does not match exactly, get the min and max values for our
# given acceptable margins for trades # given acceptable margins for trades
min_amount, max_amount = await self.money.get_acceptable_margins( min_amount, max_amount = await money.get_acceptable_margins(
platform, currency, stored_trade["amount"] platform, currency, stored_trade.amount_fiat
) )
log.info( log.info(
( (
@ -243,7 +290,6 @@ class AggregatorClient(ABC):
f" / max: {max_amount}" f" / max: {max_amount}"
) )
) )
self.irc.sendmsg()
title = "Amount does not match exactly" title = "Amount does not match exactly"
message = ( message = (
f"Amount does not match exactly, trying with margins: min: " f"Amount does not match exactly, trying with margins: min: "
@ -253,14 +299,14 @@ class AggregatorClient(ABC):
if not min_amount < amount < max_amount: if not min_amount < amount < max_amount:
title = "Amount mismatch - not in margins" title = "Amount mismatch - not in margins"
message = ( message = (
f"Amount mismatch - not in margins: {stored_trade['amount']} " f"Amount mismatch - not in margins: {stored_trade.amount_fiat} "
f"(min: {min_amount} / max: {max_amount}" f"(min: {min_amount} / max: {max_amount}"
) )
await notify.sendmsg(self.instance.user, message, title=title) await notify.sendmsg(self.instance.user, message, title=title)
return False return False
return True return True
async def transaction(self, data): async def transaction(self, platforms, tx_obj):
""" """
Store details of transaction and post notifications to IRC. Store details of transaction and post notifications to IRC.
Matches it up with data stored in Redis to attempt to reconcile with an Agora Matches it up with data stored in Redis to attempt to reconcile with an Agora
@ -268,50 +314,38 @@ class AggregatorClient(ABC):
:param data: details of transaction :param data: details of transaction
:type data: dict :type data: dict
""" """
valid = self.valid_transaction(data) valid = self.valid_transaction(tx_obj)
if not valid: if not valid:
return False return False
ts = data["timestamp"] txid = tx_obj.transaction_id
txid = data["transaction_id"] amount = tx_obj.amount
amount = float(data["amount"]) currency = tx_obj.currency
currency = data["currency"]
reference = self.extract_reference(data) reference = tx_obj.note
sender = self.extract_sender(data)
subclass = data["subclass"] # reference = self.extract_reference(data)
to_store = { # sender = tx_obj.sender
"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: {orjson.dumps(to_store, indent=2)}") log.info(f"Transaction processed: {tx_obj}")
self.irc.sendmsg( await notify.sendmsg(
( self.instance.user,
f"AUTO Incoming transaction on {subclass}: {txid} {amount}{currency} " (f"Transaction: {txid} {amount}{currency}: {reference}"),
f"({reference})" title="Incoming transaction",
) )
) for platform in platforms:
stored_trade_reference = await self.reference_partial_check(
stored_trade_reference = self.reference_partial_check( platform, reference, txid, currency, amount
reference, txid, currency, amount
) )
if stored_trade_reference is False: # can be None though if stored_trade_reference is False: # can be None though
return continue
stored_trade = False stored_trade = False
looked_up_without_reference = False looked_up_without_reference = False
# Normal implementation for when we have a reference # Normal implementation for when we have a reference
if stored_trade_reference: if stored_trade_reference:
stored_trade = self.normal_lookup( stored_trade = await self.normal_lookup(
stored_trade_reference, reference, currency, amount platform, stored_trade_reference, reference, currency, amount
) )
# if not stored_trade: # if not stored_trade:
# return # return
@ -319,52 +353,53 @@ class AggregatorClient(ABC):
# Amount/currency lookup implementation for when we have no reference # Amount/currency lookup implementation for when we have no reference
else: else:
if not stored_trade: # check we don't overwrite the lookup above if not stored_trade: # check we don't overwrite the lookup above
stored_trade = self.amount_currency_lookup( stored_trade = await self.amount_currency_lookup(
amount, currency, txid, reference platform, amount, currency, txid, reference
) )
if stored_trade is False: if stored_trade is False:
return continue
if stored_trade: if stored_trade:
# Note that we have looked it up without reference so we don't use # Note that we have looked it up without reference so we don't
# +- below # use +- below
# This might be redundant given the amount checks in find_trade, # This might be redundant given the checks in find_trade,
# but better safe than sorry! # but better safe than sorry!
looked_up_without_reference = True looked_up_without_reference = True
else: else:
return continue
else: else:
# Stored trade reference is none, the checks below will do nothing # Stored trade reference is none, the checks below will do nothing
return continue
# Make sure it was sent in the expected currency # Make sure it was sent in the expected currency
if not self.currency_check(currency, amount, reference, stored_trade): if not await self.currency_check(currency, stored_trade):
return continue
# Make sure the expected amount was sent # Make sure the expected amount was sent
if not stored_trade["amount"] == amount: if not stored_trade.amount_fiat == amount:
if looked_up_without_reference: if looked_up_without_reference:
return continue
platform = stored_trade["subclass"] if not await self.alt_amount_check(
if not self.alt_amount_check( platform, amount, currency, stored_trade
platform, amount, currency, reference, stored_trade
): ):
return continue
platform = stored_trade["subclass"]
# platform_buyer = stored_trade["buyer"] # platform_buyer = stored_trade["buyer"]
# Check sender - we don't do anything with this yet # Check sender - we don't do anything with this yet
# sender_valid = antifraud.check_valid_sender( # sender_valid = antifraud.check_valid_sender(
# reference, platform, sender, platform_buyer # reference, platform, sender, platform_buyer
# ) # )
# log.info(f"Trade {reference} buyer {platform_buyer} valid: {sender_valid}") # log.info(f"Trade {reference} buyer {platform_buyer}
# trade_released = self.release_map_trade(reference, txid) # valid: {sender_valid}")
instance = await AgoraClient(platform)
rtrn = await instance.release_map_trade(stored_trade, tx_obj)
# if trade_released: # if trade_released:
# self.ux.notify.notify_complete_trade(amount, currency) # self.ux.notify.notify_complete_trade(amount, currency)
# else: # else:
# log.error(f"Cannot release trade {reference}.") # log.error(f"Cannot release trade {reference}.")
# return # return
rtrn = await self.release_funds(stored_trade["id"], stored_trade["reference"]) # rtrn = await platform.release_funds(stored_trade["id"],
# stored_trade["reference"])
if rtrn: if rtrn:
title = "Trade complete" title = "Trade complete"
message = f"Trade complete: {amount}{currency}" message = f"Trade complete: {amount}{currency}"

View File

@ -306,7 +306,7 @@ class NordigenClient(BaseClient, AggregatorClient):
self.normalise_transactions(parsed, state="booked") self.normalise_transactions(parsed, state="booked")
if process: if process:
await self.process_transactions(parsed) await self.process_transactions(account_id, parsed)
if pending: if pending:
parsed_pending = response["pending"] parsed_pending = response["pending"]
self.normalise_transactions(parsed_pending, state="pending") self.normalise_transactions(parsed_pending, state="pending")

View File

@ -778,72 +778,68 @@ class LocalPlatformClient(ABC):
return all(actioned) return all(actioned)
async def release_funds(self, trade_id, reference): async def release_trade_escrow(self, trade_id, reference):
# stored_trade = await db.get_ref(reference) # stored_trade = await db.get_ref(reference)
logmessage = f"All checks passed, releasing funds for {trade_id} {reference}" logmessage = f"All checks passed, releasing funds for {trade_id} {reference}"
log.info(logmessage) log.info(logmessage)
title = "Releasing escrow" title = "Releasing escrow"
await notify.sendmsg(self.instance.user, logmessage, title=title) await notify.sendmsg(self.instance.user, logmessage, title=title)
release = self.release_funds
post_message = self.api.contact_message_post
rtrn = await release(trade_id) # THIS IS NOT A COMMENT
if rtrn["message"] == "OK": # THIS IS FOR SECURITY
post_message(trade_id, "Thanks! Releasing now :)") # WHEN IT HAS BEEN CONFIRMED TO WORK
return True # THIS CAN BE UNCOMMENTED
else: # rtrn = await self.release_funds(trade_id)
logmessage = f"Release funds unsuccessful: {rtrn['message']}" # if rtrn["message"] == "OK":
title = "Release unsuccessful" # await self.api.contact_message_post(trade_id, "Thanks! Releasing now :)")
log.error(logmessage) # return True
await notify.sendmsg(self.instance.user, logmessage, title=title) # else:
return # logmessage = f"Release funds unsuccessful: {rtrn['message']}"
# title = "Release unsuccessful"
# log.error(logmessage)
# await notify.sendmsg(self.instance.user, logmessage, title=title)
# return
# UNCOMMENT TO HERE
# # Parse the escrow release response async def update_trade_tx(self, stored_trade, tx_obj):
# message = rtrn["message"]
# # message_long = rtrn["response"]["data"]["message"]
# self.irc.sendmsg(f"{dumps(message)}")
async def update_trade_tx(self, reference, txid):
""" """
Update a trade to point to a given transaction ID. Update a trade to point to a given transaction ID.
Return False if the trade already has a mapped transaction. Return False if the transaction already has a mapped trade.
""" """
existing_tx = await db.r.hget(f"trade.{reference}", "tx")
if existing_tx is None: if tx_obj.reconciled:
return None
elif existing_tx == b"":
await db.r.hset(f"trade.{reference}", "tx", txid)
return True
else: # Already a mapped transaction
return False return False
async def release_map_trade(self, reference, tx): if tx_obj in stored_trade.linked.all():
return False
stored_trade.linked.add(tx_obj)
stored_trade.save()
tx_obj.reconciled = True
tx_obj.save()
return True
async def release_map_trade(self, stored_trade, tx_obj):
""" """
Map a trade to a transaction and release if no other TX is Map a trade to a transaction and release if no other TX is
mapped to the same trade. mapped to the same trade.
""" """
stored_trade = await db.get_ref(reference) platform_buyer = stored_trade.buyer
if not stored_trade: bank_sender = tx_obj.sender
log.error(f"Could not get stored trade for {reference}.") trade_id = stored_trade.contact_id
return None is_updated = await self.update_trade_tx(stored_trade, tx_obj)
tx_obj = await db.get_tx(tx) if is_updated is True:
if not tx_obj:
log.error(f"Could not get TX for {tx}.")
return None
platform_buyer = stored_trade["buyer"]
bank_sender = tx_obj["sender"]
trade_id = stored_trade["id"]
is_updated = await self.update_trade_tx(reference, tx)
if is_updated is None:
return None
elif is_updated is True:
# We mapped the trade successfully # We mapped the trade successfully
self.release_funds(trade_id, reference) await self.release_trade_escrow(trade_id, stored_trade.reference)
antifraud.add_bank_sender(platform_buyer, bank_sender) await antifraud.add_bank_sender(platform_buyer, bank_sender)
return True return True
elif is_updated is False: else:
# Already mapped # Already mapped
log.error(f"Trade {reference} already has a TX mapped, cannot map {tx}.") log.error(
f"Trade {stored_trade} already has a TX mapped, cannot map {tx_obj}."
)
return False return False
async def new_trade( async def new_trade(

View File

@ -3,6 +3,9 @@ from pyotp import TOTP
from core.clients.base import BaseClient from core.clients.base import BaseClient
from core.clients.platform import LocalPlatformClient from core.clients.platform import LocalPlatformClient
from core.util import logs
log = logs.get_logger("agora")
class AgoraClient(LocalPlatformClient, BaseClient): class AgoraClient(LocalPlatformClient, BaseClient):
@ -20,15 +23,12 @@ class AgoraClient(LocalPlatformClient, BaseClient):
""" """
print("CALLING RELEASE FUNDS", contact_id) print("CALLING RELEASE FUNDS", contact_id)
if self.instance.dummy: if self.instance.dummy:
self.log.error( log.error(f"Running in dummy mode, not releasing funds for {contact_id}")
f"Running in dummy mode, not releasing funds for {contact_id}"
)
return return
payload = {"tradeId": contact_id, "password": self.sets.Pass}
rtrn = await self.api._api_call( rtrn = await self.api.contact_release(
api_method=f"contact_release/{contact_id}", contact_id,
http_method="POST", self.instance.password,
query_values=payload,
) )
# Check if we can withdraw funds # Check if we can withdraw funds

View File

@ -88,7 +88,6 @@ class AgoraDesk:
async with session.post(api_call_url, **cast) as response_raw: async with session.post(api_call_url, **cast) as response_raw:
response = await response_raw.json() response = await response_raw.json()
status_code = response_raw.status status_code = response_raw.status
else: else:
cast["params"] = query_values cast["params"] = query_values
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
@ -226,11 +225,23 @@ class AgoraDesk:
) )
# Todo: # Todo:
# post/trade/contact_release/{trade_id} • Release trade escrow
# post/contact_fund/{trade_id} • Fund a trade # post/contact_fund/{trade_id} • Fund a trade
# post/contact_dispute/{trade_id} • Start a trade dispute # post/contact_dispute/{trade_id} • Start a trade dispute
# post/contact_mark_as_paid/{trade_id} • Mark a trade as paid # post/contact_mark_as_paid/{trade_id} • Mark a trade as paid
async def contact_release(self, trade_id: str, password: str) -> Dict[str, Any]:
"""See Agoradesk API documentation.
https://agoradesk.com/api-docs/v1#operation/releaseEscrow
"""
payload = {"tradeId": trade_id, "password": password}
return await self._api_call(
api_method=f"contact_release/{trade_id}",
http_method="POST",
query_values=payload,
)
async def contact_mark_as_paid(self, trade_id: str) -> Dict[str, Any]: async def contact_mark_as_paid(self, trade_id: str) -> Dict[str, Any]:
"""See Agoradesk API. """See Agoradesk API.

View File

@ -1,20 +1,21 @@
# Project imports # Project imports
from core.lib import db # , notify # from core.lib import db # , notify
from core.util import logs from core.util import logs
log = logs.get_logger("antifraud") log = logs.get_logger("antifraud")
class AntiFraud(object): class AntiFraud(object):
async def add_bank_sender(self, platform, platform_buyer, bank_sender): async def add_bank_sender(self, platform_buyer, bank_sender):
""" """
Add the bank senders into Redis. Add the bank senders into Redis.
:param platform: name of the platform - freeform :param platform: name of the platform - freeform
:param platform_buyer: the username of the buyer on the platform :param platform_buyer: the username of the buyer on the platform
:param bank_sender: the sender name from the bank :param bank_sender: the sender name from the bank
""" """
key = f"namemap.{platform}.{platform_buyer}" # key = f"namemap.{platform}.{platform_buyer}"
await db.r.sadd(key, bank_sender) # await db.r.sadd(key, bank_sender)
# TODO
async def get_previous_senders(self, platform, platform_buyer): async def get_previous_senders(self, platform, platform_buyer):
""" """
@ -24,12 +25,13 @@ class AntiFraud(object):
:return: set of previous buyers :return: set of previous buyers
:rtype: set :rtype: set
""" """
key = f"namemap.{platform}.{platform_buyer}" # key = f"namemap.{platform}.{platform_buyer}"
senders = await db.r.smembers(key) # senders = await db.r.smembers(key)
if not senders: # if not senders:
return None # return None
senders = db.convert(senders) # senders = db.convert(senders)
return senders # return senders
# TODO
async def check_valid_sender( async def check_valid_sender(
self, reference, platform, bank_sender, platform_buyer self, reference, platform, bank_sender, platform_buyer
@ -45,22 +47,23 @@ class AntiFraud(object):
:return: whether the sender is valid :return: whether the sender is valid
:rtype: bool :rtype: bool
""" """
senders = await self.get_previous_senders(platform, platform_buyer) # senders = await self.get_previous_senders(platform, platform_buyer)
if senders is None: # no senders yet, assume it's valid # if senders is None: # no senders yet, assume it's valid
return True # return True
if platform_buyer in senders: # if platform_buyer in senders:
return True # return True
self.ux.notify.notify_sender_name_mismatch( # self.ux.notify.notify_sender_name_mismatch(
reference, platform_buyer, bank_sender # reference, platform_buyer, bank_sender
)
# title = "Sender name mismatch"
# message = (
# f"Sender name mismatch for {reference}:\n"
# f"Platform buyer: {platform_buyer}"
# f"Bank sender: {bank_sender}"
# ) # )
# await notify.sendmsg(self.instance.) # TODO # # title = "Sender name mismatch"
return False # # message = (
# # f"Sender name mismatch for {reference}:\n"
# # f"Platform buyer: {platform_buyer}"
# # f"Bank sender: {bank_sender}"
# # )
# # await notify.sendmsg(self.instance.) # TODO
# return False
# TODO
async def check_tx_sender(self, tx, reference): async def check_tx_sender(self, tx, reference):
""" """
@ -69,21 +72,22 @@ class AntiFraud(object):
:param tx: the transaction ID :param tx: the transaction ID
:param reference: the trade reference :param reference: the trade reference
""" """
stored_trade = await db.get_ref(reference) # stored_trade = await db.get_ref(reference)
if not stored_trade: # if not stored_trade:
return None # return None
stored_tx = await db.get_tx(tx) # stored_tx = await db.get_tx(tx)
if not stored_tx: # if not stored_tx:
return None # return None
bank_sender = stored_tx["sender"] # bank_sender = stored_tx["sender"]
platform_buyer = stored_trade["buyer"] # platform_buyer = stored_trade["buyer"]
platform = stored_trade["subclass"] # platform = stored_trade["subclass"]
is_allowed = await self.check_valid_sender( # is_allowed = await self.check_valid_sender(
reference, platform, bank_sender, platform_buyer # reference, platform, bank_sender, platform_buyer
) # )
if is_allowed is True: # if is_allowed is True:
return True # return True
return False # return False
# TODO
# def user_verification_successful(self, uid): # def user_verification_successful(self, uid):
# """ # """

View File

@ -111,26 +111,25 @@ class Money(object):
return rates return rates
# TODO: pass platform # TODO: pass platform
# async def get_acceptable_margins(self, platform, currency, amount): async def get_acceptable_margins(self, platform, currency, amount):
# """ """
# Get the minimum and maximum amounts we would accept a trade for. Get the minimum and maximum amounts we would accept a trade for.
# :param currency: currency code :param currency: currency code
# :param amount: amount :param amount: amount
# :return: (min, max) :return: (min, max)
# :rtype: tuple :rtype: tuple
# """ """
# sets = util.get_settings(platform) rates = await self.get_rates_all()
# rates = await self.get_rates_all() if currency == "USD":
# if currency == "USD": min_amount = amount - platform.accept_within_usd
# min_amount = amount - float(sets.AcceptableUSDMargin) max_amount = amount + platform.accept_within_usd
# max_amount = amount + float(sets.AcceptableUSDMargin) return (min_amount, max_amount)
# return (min_amount, max_amount) amount_usd = amount / rates[currency]
# amount_usd = amount / rates[currency] min_usd = amount_usd - platform.accept_within_usd
# min_usd = amount_usd - float(sets.AcceptableUSDMargin) max_usd = amount_usd + platform.accept_within_usd
# max_usd = amount_usd + float(sets.AcceptableUSDMargin) min_local = min_usd * rates[currency]
# min_local = min_usd * rates[currency] max_local = max_usd * rates[currency]
# max_local = max_usd * rates[currency] return (min_local, max_local)
# return (min_local, max_local)
async def get_minmax(self, min_usd, max_usd, asset, currency): async def get_minmax(self, min_usd, max_usd, asset, currency):
rates = await self.get_rates_all() rates = await self.get_rates_all()

View File

@ -16,11 +16,20 @@ INTERVALS_PLATFORM = [x[0] for x in INTERVAL_CHOICES]
async def aggregator_job(): async def aggregator_job():
aggregators = Aggregator.objects.filter(enabled=True, fetch_accounts=True) aggregators = Aggregator.objects.filter(enabled=True)
for aggregator in aggregators: for aggregator in aggregators:
if aggregator.service == "nordigen": if aggregator.service == "nordigen":
instance = await NordigenClient(aggregator) instance = await NordigenClient(aggregator)
if aggregator.fetch_accounts is True:
await instance.get_all_account_info(store=True) await instance.get_all_account_info(store=True)
fetch_tasks = []
for bank, accounts in aggregator.account_info.items():
for account in accounts:
account_id = account["account_id"]
task = instance.get_transactions(account_id, process=True)
fetch_tasks.append(task)
await asyncio.gather(*fetch_tasks)
else: else:
raise NotImplementedError(f"No such client library: {aggregator.service}") raise NotImplementedError(f"No such client library: {aggregator.service}")
aggregator.fetch_accounts = False aggregator.fetch_accounts = False

View File

@ -0,0 +1,26 @@
# Generated by Django 4.1.7 on 2023-03-12 19:21
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0021_alter_trade_ad_id'),
]
operations = [
migrations.AddField(
model_name='transaction',
name='transaction_id',
field=models.CharField(default='NONE', max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='transaction',
name='ts_added',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.7 on 2023-03-13 09:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0022_transaction_transaction_id_transaction_ts_added'),
]
operations = [
migrations.AlterField(
model_name='trade',
name='linked',
field=models.ManyToManyField(blank=True, to='core.transaction'),
),
]

View File

@ -80,7 +80,40 @@ class Aggregator(models.Model):
@classmethod @classmethod
def get_for_platform(cls, platform): def get_for_platform(cls, platform):
return cls.objects.filter(user=platform.user, enabled=True) aggregators = []
ads = Ad.objects.filter(
platforms=platform,
enabled=True,
)
print("ADS", ads)
for ad in ads:
for aggregator in ad.aggregators.all():
if aggregator not in aggregators:
aggregators.append(aggregator)
print("RET", aggregators)
return aggregators
@property
def platforms(self):
"""
Get platforms for this aggregator.
Do this by looking up Ads with the aggregator.
Then, join them all together.
"""
platforms = []
ads = Ad.objects.filter(
aggregators=self,
enabled=True,
)
print("ADS", ads)
for ad in ads:
for platform in ad.platforms.all():
if platform not in platforms:
platforms.append(platform)
print("RET", platforms)
return platforms
@classmethod @classmethod
def get_currencies_for_platform(cls, platform): def get_currencies_for_platform(cls, platform):
@ -104,6 +137,23 @@ class Aggregator(models.Model):
account_info[bank].append(account) account_info[bank].append(account)
return account_info return account_info
def add_transaction(self, account_id, tx_data):
return Transaction.objects.create(
aggregator=self,
account_id=account_id,
reconciled=False,
**tx_data,
)
def get_transaction(self, account_id, tx_id):
transaction = Transaction.objects.filter(
account_id=account_id,
transaction_id=tx_id,
).first()
if not transaction:
return None
return transaction
class Platform(models.Model): class Platform(models.Model):
""" """
@ -208,6 +258,13 @@ class Platform(models.Model):
return references return references
def get_trade_by_reference(self, reference):
return Trade.objects.filter(
platform=self,
open=True,
reference=reference,
).first()
@property @property
def trades(self): def trades(self):
""" """
@ -249,6 +306,43 @@ class Platform(models.Model):
log.info(msg) log.info(msg)
return messages return messages
@classmethod
def get_for_aggregator(cls, aggregator):
platforms = []
ads = Ad.objects.filter(
aggregators=aggregator,
enabled=True,
)
print("ADS", ads)
for ad in ads:
for platform in ad.platforms.all():
if platform not in platforms:
platforms.append(platform)
print("RET", platforms)
return platforms
@property
def aggregators(self):
"""
Get aggregators for this platform.
Do this by looking up Ads with the platform.
Then, join them all together.
"""
aggregators = []
ads = Ad.objects.filter(
platforms=self,
enabled=True,
)
print("ADS", ads)
for ad in ads:
for aggregator in ad.aggregators.all():
if aggregator not in aggregators:
aggregators.append(aggregator)
print("RET", aggregators)
return aggregators
class Asset(models.Model): class Asset(models.Model):
code = models.CharField(max_length=64) code = models.CharField(max_length=64)
@ -320,8 +414,9 @@ class Transaction(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
aggregator = models.ForeignKey(Aggregator, on_delete=models.CASCADE) aggregator = models.ForeignKey(Aggregator, on_delete=models.CASCADE)
account_id = models.CharField(max_length=255) account_id = models.CharField(max_length=255)
transaction_id = models.CharField(max_length=255)
ts_added = ... ts_added = models.DateTimeField(auto_now_add=True)
recipient = models.CharField(max_length=255, null=True, blank=True) recipient = models.CharField(max_length=255, null=True, blank=True)
sender = models.CharField(max_length=255, null=True, blank=True) sender = models.CharField(max_length=255, null=True, blank=True)
@ -354,7 +449,7 @@ class Trade(models.Model):
open = models.BooleanField(default=True) open = models.BooleanField(default=True)
linked = models.ManyToManyField(Transaction) linked = models.ManyToManyField(Transaction, blank=True)
reconciled = models.BooleanField(default=False) reconciled = models.BooleanField(default=False)
released = models.BooleanField(default=False) released = models.BooleanField(default=False)

View File

@ -0,0 +1,587 @@
import logging
from unittest.mock import patch
from django.test import TransactionTestCase
from core.clients.aggregator import AggregatorClient
from core.models import Aggregator, Platform, Trade, Transaction, User
class TestTransactions(TransactionTestCase):
def setUp(self):
logging.disable(logging.CRITICAL)
self.user = User.objects.create_user(
username="testuser", email="test@example.com", password="test"
)
self.aggregator = Aggregator.objects.create(
user=self.user,
name="Test",
service="nordigen",
secret_id="a",
secret_key="a",
)
self.agg_client = AggregatorClient()
self.agg_client.instance = self.aggregator
self.platform = Platform.objects.create(
user=self.user,
name="Test",
service="agora",
token="a",
password="a",
otp_token="a",
username="myuser",
)
self.transaction = Transaction.objects.create(
aggregator=self.aggregator,
account_id="my account id",
transaction_id="BANKTX",
amount=1,
currency="GBP",
note="TEST-1",
)
self.trades = {
1: {
"contact_id": "uuid1",
"buyer": "test_buyer_1",
"currency": "GBP",
"asset": "XMR",
"amount_fiat": 1,
"amount_crypto": 0.3,
"reference": "TEST-1",
"provider": "REVOLUT",
},
2: {
"contact_id": "uuid2",
"buyer": "test_buyer_2",
"currency": "GBP",
"asset": "XMR",
"amount_fiat": 1,
"amount_crypto": 0.3,
"reference": "TEST-2",
"provider": "REVOLUT",
},
3: {
"contact_id": "uuid3",
"buyer": "test_buyer_3",
"currency": "GBP",
"asset": "XMR",
"amount_fiat": 1000,
"amount_crypto": 3,
"reference": "TEST-3",
"provider": "REVOLUT",
},
4: {
"contact_id": "uuid4",
"buyer": "test_buyer_4",
"currency": "GBP",
"asset": "XMR",
"amount_fiat": 10,
"amount_crypto": 0.5,
"reference": "TEST-4",
"provider": "REVOLUT",
},
5: { # to conflict with 1
"contact_id": "uuid1",
"buyer": "test_buyer_2",
"currency": "GBP",
"asset": "XMR",
"amount_fiat": 1,
"amount_crypto": 0.3,
"reference": "TEST-1",
"provider": "REVOLUT",
},
}
def create_trades(self, *numbers):
for trade_key in self.trades.keys():
if trade_key in numbers:
Trade.objects.create(
platform=self.platform,
# open=True,
**self.trades[trade_key],
)
def mock_to_usd(self, amount, currency):
if currency == "GBP":
return amount * 1.3
elif currency == "USD":
return amount
# fuck it who cares
elif currency == "SEK":
return 100
elif currency == "EUR":
return 10
@patch("core.lib.notify.sendmsg")
async def test_reference_partial_check(self, _):
self.create_trades(1, 2, 3, 4)
result = await self.agg_client.reference_partial_check(
self.platform,
"TEST-1",
"for notifications only",
"GBP",
1,
)
self.assertEqual(result, "TEST-1")
@patch("core.lib.notify.sendmsg")
async def test_reference_partial_check_subset(self, _):
self.create_trades(1, 2, 3, 4)
result = await self.agg_client.reference_partial_check(
self.platform,
"the TEST-1 in string",
"for notifications only",
"GBP",
1,
)
self.assertEqual(result, "TEST-1")
@patch("core.lib.notify.sendmsg")
async def test_reference_partial_check_multiple_match(self, _):
self.create_trades(1, 2, 3, 4, 5)
result = await self.agg_client.reference_partial_check(
self.platform,
"TEST-1",
"for notifications only",
"GBP",
1,
)
self.assertEqual(result, False)
@patch("core.lib.notify.sendmsg")
async def test_reference_partial_check_none(self, _):
# self.create_trades(1, 2, 3, 4, 5)
result = await self.agg_client.reference_partial_check(
self.platform,
"TEST-1",
"for notifications only",
"GBP",
1,
)
self.assertEqual(result, None)
@patch("core.lib.notify.sendmsg")
async def test_reference_partial_check_none_match(self, _):
self.create_trades(1, 2, 3, 4, 5)
result = await self.agg_client.reference_partial_check(
self.platform,
"NOWHERE",
"for notifications only",
"GBP",
1,
)
self.assertEqual(result, None)
def test_valid_transaction(self):
result = self.agg_client.valid_transaction(self.transaction)
self.assertEqual(result, True)
def test_valid_transaction_fail(self):
self.transaction.amount = -100
result = self.agg_client.valid_transaction(self.transaction)
self.assertEqual(result, False)
self.transaction.amount = 1
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=100)
async def test_can_alt_lookup(self, *args):
result = await self.agg_client.can_alt_lookup(
self.platform,
999999999, # ignored
"GBP",
"IGNORED",
)
self.assertEqual(result, True)
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=500)
async def test_can_alt_lookup_fail(self, *args):
result = await self.agg_client.can_alt_lookup(
self.platform,
999999999, # ignored
"GBP",
"IGNORED",
)
self.assertEqual(result, False)
def test_find_trade(self):
self.create_trades(1, 3, 4)
result = self.agg_client.find_trade(
self.platform,
"BANKTX",
"GBP",
1,
)
self.assertEqual(result.reference, "TEST-1")
self.assertEqual(result.currency, "GBP")
self.assertEqual(result.amount_fiat, 1)
def test_find_trade_fail_two_match(self):
self.create_trades(1, 2, 3, 4) # 2 trades with same amount and currency
result = self.agg_client.find_trade(
self.platform,
"BANKTX",
"GBP",
1,
)
self.assertEqual(result, False)
def test_find_trade_fail_two_match_alt(self):
self.create_trades(1, 3, 4)
result = self.agg_client.find_trade(
self.platform,
"BANKTX",
"GBP",
88,
)
self.assertEqual(result, False)
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=100)
async def test_amount_currency_lookup(self, *args):
self.create_trades(1, 3, 4)
result = await self.agg_client.amount_currency_lookup(
self.platform,
1,
"GBP",
"BANKTX",
"TEST-1",
)
self.assertEqual(result.reference, "TEST-1")
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=100)
async def test_amount_currency_lookup_fail(self, *args):
self.create_trades(1, 3, 4)
result = await self.agg_client.amount_currency_lookup(
self.platform,
88,
"GBP",
"BANKTX",
"TEST-1",
)
self.assertEqual(result, None)
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=500)
async def test_amount_currency_lookup_fail_too_high(self, *args):
self.create_trades(1, 3, 4)
result = await self.agg_client.amount_currency_lookup(
self.platform,
1,
"GBP",
"BANKTX",
"TEST-1",
)
self.assertEqual(result, False)
@patch("core.lib.notify.sendmsg")
async def test_normal_lookup(self, _):
self.create_trades(1, 2, 3, 4)
result = await self.agg_client.normal_lookup(
self.platform,
"TEST-1",
"BANKTX",
"GBP",
1,
)
self.assertEqual(result.reference, "TEST-1")
self.assertEqual(result.currency, "GBP")
self.assertEqual(result.amount_fiat, 1)
@patch("core.lib.notify.sendmsg")
async def test_currency_check(self, _):
self.create_trades(1)
trade = Trade.objects.all().first()
result = await self.agg_client.currency_check(
"GBP",
trade,
)
self.assertEqual(result, True)
@patch("core.lib.notify.sendmsg")
async def test_currency_check_fail(self, _):
self.create_trades(1)
trade = Trade.objects.all().first()
result = await self.agg_client.currency_check(
"AYZ",
trade,
)
self.assertEqual(result, False)
@patch("core.lib.notify.sendmsg")
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
)
async def test_alt_amount_check(self, *args):
self.create_trades(1)
trade = Trade.objects.all().first()
result = await self.agg_client.alt_amount_check(
self.platform,
1.123,
"GBP",
trade,
)
self.assertEqual(result, True)
@patch("core.lib.notify.sendmsg")
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
)
async def test_alt_amount_check_fail(self, *args):
self.create_trades(1)
trade = Trade.objects.all().first()
result = await self.agg_client.alt_amount_check(
self.platform,
1.501,
"GBP",
trade,
)
self.assertEqual(result, False)
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=1)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction(self, release, gam, to_usd, sendmsg):
self.create_trades(1, 2, 3, 4)
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_called_once_with("uuid1", "TEST-1")
self.assertEqual(self.transaction.reconciled, True)
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=1)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction_second(self, release, gam, to_usd, sendmsg):
self.create_trades(1, 2, 3, 4)
self.transaction.note = "TEST-2"
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_called_once_with("uuid2", "TEST-2")
self.assertEqual(self.transaction.reconciled, True)
self.transaction.note = "TEST-1"
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=1)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction_invalid(self, release, gam, to_usd, sendmsg):
self.create_trades(1, 2, 3, 4)
self.transaction.amount = -1
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_not_called()
self.assertEqual(self.transaction.reconciled, False)
self.transaction.amount = 1
# def test_transaction_malformed(self):
# malformed_data = self.test_data_copy
# del malformed_data["amount"]
# self.transactions.transaction(malformed_data)
# self.transactions.release_funds.assert_not_called()
# malformed_data = self.test_data_copy
# del malformed_data["currency"]
# self.transactions.transaction(malformed_data)
# self.transactions.release_funds.assert_not_called()
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=1)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction_no_reference_fail(self, release, gam, to_usd, sendmsg):
self.create_trades(1, 2, 3, 4)
self.transaction.note = "none"
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_not_called()
self.assertEqual(self.transaction.reconciled, False)
self.transaction.note = "TEST-1"
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=1)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction_no_reference_pass(self, release, gam, to_usd, sendmsg):
self.create_trades(1)
self.transaction.note = "none"
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_called_with("uuid1", "TEST-1")
self.assertEqual(self.transaction.reconciled, True)
self.transaction.note = "TEST-1"
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=1000)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction_large(self, release, gam, to_usd, sendmsg):
self.create_trades(1, 2, 3, 4)
self.transaction.amount = 1000
self.transaction.note = "TEST-3"
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_called_once_with("uuid3", "TEST-3")
self.assertEqual(self.transaction.reconciled, True)
self.transaction_amount_fiat = 1
self.transaction.note = "TEST-1"
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=1000)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction_no_reference_exceeds_max(
self, release, gam, to_usd, sendmsg
):
self.create_trades(1, 2, 3, 4)
self.transaction.amount = 1000
self.transaction.note = "noref"
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_not_called()
self.assertEqual(self.transaction.reconciled, False)
self.transaction.amount = 1
self.transaction.note = "TEST-1"
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=100)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction_wrong_currency(self, release, gam, to_usd, sendmsg):
self.create_trades(1, 2, 3, 4)
self.transaction.currency = "EUR"
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_not_called()
self.assertEqual(self.transaction.reconciled, False)
self.transaction.currency = "GBP"
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=100)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction_wrong_currency_noref(
self, release, gam, to_usd, sendmsg
):
self.transaction.currency = "EUR"
self.transaction.note = "none"
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_not_called()
self.assertEqual(self.transaction.reconciled, False)
self.transaction.currency = "GBP"
self.transaction.note = "TEST-1"
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=100)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.8, 1.8)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction_wrong_amount(self, release, gam, to_usd, sendmsg):
self.create_trades(1, 2, 3, 4)
self.transaction.amount = 10
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_not_called()
self.assertEqual(self.transaction.reconciled, False)
self.transaction.amount = 1
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=100)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.8, 1.8)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction_wrong_amount_noref(self, release, gam, to_usd, sendmsg):
self.transaction.amount = 10
self.transaction.note = "none"
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_not_called()
self.assertEqual(self.transaction.reconciled, False)
self.transaction.amount = 1
self.transaction.note = "TEST-1"
# def test_transaction_pending(self):
# pending_tx = self.test_data_copy
# pending_tx["data"]["state"] = "pending"
# self.transactions.transaction(pending_tx)
# self.transactions.release_funds.assert_not_called()
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=100)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction_too_low(self, release, gam, to_usd, sendmsg):
self.create_trades(1, 2, 3, 4)
self.transaction.amount = 5
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_not_called()
self.assertEqual(self.transaction.reconciled, False)
self.transaction.amount = 1
@patch("core.lib.notify.sendmsg")
@patch("core.clients.aggregator.money.to_usd", return_value=100)
@patch(
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1)
)
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
async def test_transaction_too_high(self, release, gam, to_usd, sendmsg):
self.create_trades(1, 2, 3, 4)
self.transaction.amount = 15
await self.agg_client.transaction([self.platform], self.transaction)
release.assert_not_called()
self.assertEqual(self.transaction.reconciled, False)
self.transaction.amount = 1