From 94429d0aaaa73040bdeffde49a4fd3b18052acb8 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Mon, 11 Apr 2022 20:56:20 +0100 Subject: [PATCH] Implement sender-based anti-fraud system --- handler/sinks/__init__.py | 1 + handler/sources/agora.py | 2 +- handler/sources/localbitcoins.py | 2 +- handler/transactions.py | 134 ++++++++++++++++++++++++++++++- handler/ux/commands.py | 27 ++++++- handler/ux/notify.py | 8 ++ 6 files changed, 165 insertions(+), 9 deletions(-) diff --git a/handler/sinks/__init__.py b/handler/sinks/__init__.py index 0f8b0eb..8c0a11f 100644 --- a/handler/sinks/__init__.py +++ b/handler/sinks/__init__.py @@ -37,6 +37,7 @@ class Sinks(util.Base): # setattr(self.truelayer, "sinks", self) def got_transactions(self, subclass, account_id, transactions): + print("GOT TX", transactions[0:10]) if not transactions: return False transaction_ids = [x["transaction_id"] for x in transactions] diff --git a/handler/sources/agora.py b/handler/sources/agora.py index 5707ec4..58035ae 100644 --- a/handler/sources/agora.py +++ b/handler/sources/agora.py @@ -130,7 +130,7 @@ class Agora(util.Base): if not contact["data"]["is_selling"]: continue if reference not in self.last_dash: - reference = self.tx.new_trade(asset, contact_id, buyer, currency, amount, amount_crypto, provider) + reference = self.tx.new_trade("agora", asset, contact_id, buyer, currency, amount, amount_crypto, provider) if reference: if reference not in current_trades: current_trades.append(reference) diff --git a/handler/sources/localbitcoins.py b/handler/sources/localbitcoins.py index 0a5cee7..4a81a4a 100644 --- a/handler/sources/localbitcoins.py +++ b/handler/sources/localbitcoins.py @@ -124,7 +124,7 @@ class LBTC(util.Base): if not contact["data"]["is_selling"]: continue if reference not in self.last_dash: - reference = self.tx.new_trade(asset, contact_id, buyer, currency, amount, amount_crypto, provider) + reference = self.tx.new_trade("lbtc", asset, contact_id, buyer, currency, amount, amount_crypto, provider) if reference: if reference not in current_trades: current_trades.append(reference) diff --git a/handler/transactions.py b/handler/transactions.py index e21a933..61db9e9 100644 --- a/handler/transactions.py +++ b/handler/transactions.py @@ -202,6 +202,84 @@ class Transactions(util.Base): return False return True + def add_bank_sender(self, platform, platform_buyer, bank_sender): + """ + Add the bank senders into Redis. + :param platform: name of the platform - freeform + :param platform_buyer: the username of the buyer on the platform + :param bank_sender: the sender name from the bank + """ + key = f"namemap.{platform}.{platform_buyer}" + r.sadd(key, bank_sender) + + def get_previous_senders(self, platform, platform_buyer): + """ + Get all the previous bank sender names for the given buyer on the platform. + :param platform: name of the platform - freeform + :param platform_buyer: the username of the buyer on the platform + :return: set of previous buyers + :rtype: set + """ + key = f"namemap.{platform}.{platform_buyer}" + senders = r.smembers(key) + senders = util.convert(senders) + return senders + + def check_valid_sender(self, reference, platform, bank_sender, platform_buyer): + """ + Check that either: + * The platform buyer has never had a recognised transaction before + * The bank sender name matches a previous transaction from the platform buyer + :param reference: the trade reference + :param platform: name of the platform - freeform + :param bank_sender: the sender of the bank transaction + :param platform_buyer: the username of the buyer on the platform + :return: whether the sender is valid + :rtype: bool + """ + senders = self.get_previous_senders(platform, platform_buyer) + if platform_buyer in senders: + print("Platform buyer is in senders!") + return True + print("Platform buyer is not in senders") + self.ux.notify.notify_sender_name_mismatch(reference, platform_buyer, bank_sender) + return False + + def check_tx_sender(self, tx, reference): + """ + Check whether the sender of a given transaction is authorised based on the previous + transactions of the username that originated the trade reference. + :param tx: the transaction ID + :param reference: the trade reference + """ + stored_trade = self.get_ref(reference) + if not stored_trade: + return None + stored_tx = self.get_tx(tx) + if not stored_tx: + return None + bank_sender = stored_tx["sender"] + platform_buyer = stored_trade["buyer"] + platform = stored_trade["subclass"] + is_allowed = self.check_valid_sender(reference, platform, bank_sender, platform_buyer) + if is_allowed is True: + return True + return False + + def update_trade_tx(self, reference, txid): + """ + Update a trade to point to a given transaction ID. + Return False if the trade already has a mapped transaction. + """ + existing_tx = r.hget(f"trade.{reference}", "tx") + if existing_tx is None: + return None + elif existing_tx == "": + r.hset(f"trade.{reference}", "tx", txid) + return True + else: # Already a mapped transaction + return False + def transaction(self, data): """ Store details of transaction and post notifications to IRC. @@ -223,7 +301,6 @@ class Transactions(util.Base): subclass = data["subclass"] to_store = { - "trade_id": "", "subclass": subclass, "ts": ts, "txid": txid, @@ -232,8 +309,10 @@ class Transactions(util.Base): "currency": currency, "sender": sender, } + r.hmset(f"tx.{txid}", to_store) + self.log.info(f"Transaction processed: {dumps(to_store, indent=2)}") - self.irc.sendmsg(f"AUTO Incoming transaction on {subclass}: {amount}{currency} ({reference})") + self.irc.sendmsg(f"AUTO Incoming transaction on {subclass}: {txid} {amount}{currency} ({reference})") stored_trade_reference = self.reference_partial_check(reference, txid, currency, amount) if stored_trade_reference is False: # can be None though @@ -274,8 +353,13 @@ class Transactions(util.Base): return if not self.alt_amount_check(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.check_valid_sender(reference, platform, sender, platform_buyer) + print("Sender valid for trade: ", sender_valid) - r.hmset(f"tx.{txid}", to_store) self.release_funds(stored_trade["id"], stored_trade["reference"]) self.ux.notify.notify_complete_trade(amount, currency) @@ -298,7 +382,33 @@ class Transactions(util.Base): # message_long = rtrn["response"]["data"]["message"] self.irc.sendmsg(f"{dumps(message)}") - def new_trade(self, asset, trade_id, buyer, currency, amount, amount_crypto, provider): + def release_map_trade(self, reference, tx): + """ + Map a trade to a transaction and release if no other TX is + mapped to the same trade. + """ + stored_trade = self.get_ref(reference) + if not stored_trade: + return None + tx_obj = self.get_tx(tx) + if not tx_obj: + return None + platform = stored_trade["subclass"] + platform_buyer = stored_trade["buyer"] + bank_sender = tx_obj["sender"] + trade_id = stored_trade["id"] + is_updated = self.update_trade_tx(reference, tx) + if is_updated is None: + return None + elif is_updated is True: + # We mapped the trade successfully + self.release_funds(trade_id, reference) + print("Adding mapped bank sender", platform_buyer, bank_sender) + self.add_bank_sender(platform, platform_buyer, bank_sender) + return True + return False + + def new_trade(self, subclass, asset, trade_id, buyer, currency, amount, amount_crypto, provider): """ Called when we have a new trade in Agora. Store details in Redis, generate a reference and optionally let the customer know the reference. @@ -310,6 +420,7 @@ class Transactions(util.Base): r.set(f"trade.{trade_id}.reference", reference) to_store = { "id": trade_id, + "tx": "", "asset": asset, "buyer": buyer, "currency": currency, @@ -317,6 +428,7 @@ class Transactions(util.Base): "amount_crypto": amount_crypto, "reference": reference, "provider": provider, + "subclass": subclass, } self.log.info(f"Storing trade information: {str(to_store)}") r.hmset(f"trade.{reference}", to_store) @@ -393,6 +505,20 @@ class Transactions(util.Base): return False return ref_data + def get_tx(self, tx): + """ + Get the transaction information for a transaction ID. + :param reference: trade reference + :type reference: string + :return: dict of trade information + :rtype: dict + """ + tx_data = r.hgetall(f"tx.{tx}") + tx_data = util.convert(tx_data) + if not tx_data: + return False + return tx_data + def del_ref(self, reference): """ Delete a given reference from the Redis database. diff --git a/handler/ux/commands.py b/handler/ux/commands.py index 29080de..b3f72e5 100644 --- a/handler/ux/commands.py +++ b/handler/ux/commands.py @@ -251,6 +251,27 @@ class IRCCommands(object): message_long = rtrn["response"]["data"]["message"] msg(f"{message} - {message_long}") + class map(object): + name = "map" + authed = True + helptext = "Release funds for a trade. Usage: map " + + @staticmethod + def run(cmd, spl, length, authed, msg, agora, tx, ux): + if length == 2: + reference = spl[1] + txid = spl[2] + is_released = tx.release_map_trade(reference, txid) + if is_released is None: + msg("Trade or TX invalid") + return + elif is_released is True: + msg(f"Trade released") + return + elif is_released is False: + msg(f"Could not release trade") + return + class nuke(object): name = "nuke" authed = True @@ -502,7 +523,7 @@ class IRCCommands(object): def run(cmd, spl, length, authed, msg, agora, tx, ux): if length == 2: account = spl[1] - auth_url = tx.truelayer.create_auth_url(account) + auth_url = tx.sinks.truelayer.create_auth_url(account) msg(f"Auth URL for {account}: {auth_url}") class nsignin(object): @@ -560,13 +581,13 @@ class IRCCommands(object): transactions = tx.sinks.truelayer.get_transactions(account, account_id) for transaction in transactions: txid = transaction["transaction_id"] - ptxid = transaction["meta"]["provider_transaction_id"] + # ptxid = transaction["meta"]["provider_transaction_id"] txtype = transaction["transaction_type"] timestamp = transaction["timestamp"] amount = transaction["amount"] currency = transaction["currency"] description = transaction["description"] - msg(f"{timestamp} {txid} {ptxid} {txtype} {amount}{currency} {description}") + msg(f"{timestamp} {txid} {txtype} {amount}{currency} {description}") class ntransactions(object): name = "ntransactions" diff --git a/handler/ux/notify.py b/handler/ux/notify.py index 4ed9f0b..37561a7 100644 --- a/handler/ux/notify.py +++ b/handler/ux/notify.py @@ -44,3 +44,11 @@ class Notify(util.Base): def notify_release_unsuccessful(self, trade_id): self.sendmsg(f"Release unsuccessful for {trade_id}", title="Unsuccessful release", tags="tx", priority="5") + + def notify_sender_name_mismatch(self, trade_id, platform_username, bank_sender): + self.sendmsg( + f"Sender name mismatch for {trade_id}: Username: {platform_username}, Sender: {bank_sender}", + title="Sender name mismatch", + tags="fraud", + priority="5", + )