Implement sender-based anti-fraud system

This commit is contained in:
Mark Veidemanis 2022-04-11 20:56:20 +01:00
parent fec536616d
commit 94429d0aaa
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
6 changed files with 165 additions and 9 deletions

View File

@ -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]

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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 <reference> <txid>"
@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"

View File

@ -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",
)