Remove legacy codebase

master
Mark Veidemanis 1 year ago
parent 85c64efc78
commit f41e69b003
Signed by: m
GPG Key ID: 5ACFCEED46C0904F

@ -1,141 +0,0 @@
#!/usr/bin/env python3
# Twisted/Klein imports
# Other library imports
from json import dumps
from signal import SIGINT, signal
from sys import argv
import lib.antifraud
import lib.logstash
import lib.markets
import lib.money
import lib.transactions
# New style classes
import sinks
import sources
import util
import ux
from klein import Klein
# Project imports
from settings import settings
from twisted.internet import reactor
from twisted.internet.protocol import Factory
init_map = None
Factory.noisy = False
# TODO: extend this with more
def cleanup(sig, frame):
to_cleanup = []
if hasattr(init_map["money"], "lc_es_checks"):
to_cleanup.append(init_map["money"].lc_es_checks.stop)
if hasattr(init_map["sources"].agora, "lc_dash"):
to_cleanup.append(init_map["sources"].agora.lc_dash.stop)
if hasattr(init_map["sources"].agora, "lc_cheat"):
to_cleanup.append(init_map["sources"].agora.lc_cheat.stop)
if hasattr(init_map["sources"].lbtc, "lc_dash"):
to_cleanup.append(init_map["sources"].lbtc.lc_dash.stop)
if hasattr(init_map["sources"].lbtc, "lc_cheat"):
to_cleanup.append(init_map["sources"].lbtc.lc_cheat.stop)
if hasattr(init_map["sinks"], "truelayer"):
if hasattr(init_map["sinks"].truelayer, "lc"):
to_cleanup.append(init_map["sinks"].truelayer.lc.stop)
if hasattr(init_map["sinks"].truelayer, "lc_tx"):
to_cleanup.append(init_map["sinks"].truelayer.lc_tx.stop)
if hasattr(init_map["sinks"], "nordigen"):
if hasattr(init_map["sinks"].nordigen, "lc"):
to_cleanup.append(init_map["sinks"].nordigen.lc.stop)
if hasattr(init_map["sinks"].nordigen, "lc_tx"):
to_cleanup.append(init_map["sinks"].nordigen.lc_tx.stop)
if init_map:
for func in to_cleanup:
try:
func()
except: # noqa
print(f"Exception when stopping {func}")
pass # noqa
reactor.stop()
signal(SIGINT, cleanup) # Handle Ctrl-C and run the cleanup routine
class WebApp(util.Base):
"""
Our Klein webapp.
"""
app = Klein()
# @app.route("/callback", methods=["POST"])
# def callback(self, request):
# content = request.content.read()
# try:
# parsed = loads(content)
# except JSONDecodeError:
# self.log.error(f"Failed to parse JSON callback: {content}")
# return dumps(False)
# self.log.info("Callback received: {parsed}", parsed=parsed["data"]["id"])
# # self.tx.transaction(parsed)
# return dumps(True)
# set up another connection to a bank
@app.route("/signin/<string:account>", methods=["GET"])
def signin(self, request, account):
auth_url = self.sinks.truelayer.create_auth_url(account)
return f'Please sign in to {account} <a href="{auth_url}" target="_blank">here.</a>'
# endpoint called after we finish setting up a connection above
@app.route("/callback-truelayer", methods=["POST"])
def signin_callback_truelayer(self, request):
code = request.args[b"code"]
self.sinks.truelayer.handle_authcode_received(code)
return dumps(True)
@app.route("/callback-nordigen", methods=["GET"])
def signin_callback_nordigen(self, request):
# code = request.args[b"code"]
# self.sinks.truelayer.handle_authcode_received(code)
return dumps(True)
@app.route("/callback-verify", methods=["POST"])
def callback_verify(self, request):
# code = request.args[b"code"]
rtrn = self.ux.verify.handle_callback(request)
return dumps(rtrn)
@app.route("/accounts/<string:account>", methods=["GET"])
def balance(self, request, account):
accounts = self.sinks.truelayer.get_accounts(account)
return dumps(accounts, indent=2)
if __name__ == "__main__":
if "--debug" in argv:
util.debug = True
init_map = {
"ux": ux.UX(),
"markets": lib.markets.Markets(),
"sources": sources.Sources(),
"sinks": sinks.Sinks(),
"tx": lib.transactions.Transactions(),
"webapp": WebApp(),
"money": lib.money.Money(),
"antifraud": lib.antifraud.AntiFraud(),
}
# Merge all classes into each other
util.xmerge_attrs(init_map)
# Let the classes know they have been merged
for class_name, class_instance in init_map.items():
if hasattr(class_instance, "__xmerged__"):
class_instance.__xmerged__()
# Set up the loops to put data in ES
# init_map["tx"].setup_loops()
lib.logstash.init_logstash()
# Run the WebApp
init_map["webapp"].app.run(settings.App.BindHost, 8080)

@ -1,139 +0,0 @@
# Other library imports
import util
from redis import StrictRedis
# Project imports
from settings import settings
log = util.get_logger("DB")
# Define the Redis endpoint to the socket
r = StrictRedis(unix_socket_path=settings.DB.RedisSocket, db=int(settings.DB.DB))
def get_refs():
"""
Get all reference IDs for trades.
:return: list of trade IDs
:rtype: list
"""
references = []
ref_keys = r.keys("trade.*.reference")
for key in ref_keys:
references.append(r.get(key))
return util.convert(references)
def tx_to_ref(tx):
"""
Convert a trade ID to a reference.
:param tx: trade ID
:type tx: string
:return: reference
:rtype: string
"""
refs = get_refs()
for reference in refs:
ref_data = util.convert(r.hgetall(f"trade.{reference}"))
if not ref_data:
continue
if ref_data["id"] == tx:
return reference
def ref_to_tx(reference):
"""
Convert a reference to a trade ID.
:param reference: trade reference
:type reference: string
:return: trade ID
:rtype: string
"""
ref_data = util.convert(r.hgetall(f"trade.{reference}"))
if not ref_data:
return False
return ref_data["id"]
def get_ref_map():
"""
Get all reference IDs for trades.
:return: dict of references keyed by TXID
:rtype: dict
"""
references = {}
ref_keys = r.keys("trade.*.reference")
for key in ref_keys:
tx = util.convert(key).split(".")[1]
references[tx] = r.get(key)
return util.convert(references)
def get_ref(reference):
"""
Get the trade information for a reference.
:param reference: trade reference
:type reference: string
:return: dict of trade information
:rtype: dict
"""
ref_data = r.hgetall(f"trade.{reference}")
ref_data = util.convert(ref_data)
if "subclass" not in ref_data:
ref_data["subclass"] = "agora"
if not ref_data:
return False
return ref_data
def get_tx(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 get_subclass(reference):
obj = r.hget(f"trade.{reference}", "subclass")
subclass = util.convert(obj)
return subclass
def del_ref(reference):
"""
Delete a given reference from the Redis database.
:param reference: trade reference to delete
:type reference: string
"""
tx = ref_to_tx(reference)
r.delete(f"trade.{reference}")
r.delete(f"trade.{tx}.reference")
def cleanup(subclass, references):
"""
Reconcile the internal reference database with a given list of references.
Delete all internal references not present in the list and clean up artifacts.
:param references: list of references to reconcile against
:type references: list
"""
messages = []
for tx, reference in get_ref_map().items():
if reference not in references:
if get_subclass(reference) == subclass:
logmessage = (
f"[{reference}] ({subclass}): Archiving trade reference. TX: {tx}"
)
messages.append(logmessage)
log.info(logmessage)
r.rename(f"trade.{tx}.reference", f"archive.trade.{tx}.reference")
r.rename(f"trade.{reference}", f"archive.trade.{reference}")
return messages

File diff suppressed because it is too large Load Diff

@ -1,113 +0,0 @@
# Project imports
import db
import util
class AntiFraud(util.Base):
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}"
db.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 = db.r.smembers(key)
if not senders:
return None
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 senders is None: # no senders yet, assume it's valid
return True
if platform_buyer in senders:
return True
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 = db.get_ref(reference)
if not stored_trade:
return None
stored_tx = db.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 = db.r.hget(f"trade.{reference}", "tx")
if existing_tx is None:
return None
elif existing_tx == b"":
db.r.hset(f"trade.{reference}", "tx", txid)
return True
else: # Already a mapped transaction
return False
def user_verification_successful(self, uid):
"""
A user has successfully completed verification.
"""
self.log.info(f"User has completed verification: {uid}")
trade_list = self.markets.find_trades_by_uid(uid)
for platform, trade_id, reference, currency in trade_list:
self.markets.send_bank_details(platform, currency, trade_id)
self.markets.send_reference(platform, trade_id, reference)
def send_verification_url(self, platform, uid, trade_id):
send_setting, post_message = self.markets.get_send_settings(platform)
if send_setting == "1":
auth_url = self.ux.verify.create_applicant_and_get_link(uid)
if platform == "lbtc":
auth_url = auth_url.replace("https://", "") # hack
post_message(
trade_id,
f"Hi! To continue the trade, please complete the verification form: {auth_url}",
)
antifraud = AntiFraud()

@ -1,884 +0,0 @@
"""See https://agoradesk.com/api-docs/v1."""
# pylint: disable=too-many-lines
# Large API. Lots of lines can't be avoided.
import hashlib
import hmac as hmac_lib
import json
import logging
import sys
import time
from typing import Any, Dict, List, Optional, Union
from urllib.parse import urlparse
import arrow
import requests
import treq
# Project imports
import util
from twisted.internet.defer import inlineCallbacks
__author__ = "marvin8"
__copyright__ = "(C) 2021 https://codeberg.org/MarvinsCryptoTools/agoradesk_py"
__version__ = "0.1.0"
# set logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logging.getLogger("requests.packages.urllib3").setLevel(logging.INFO)
logging.getLogger("urllib3.connectionpool").setLevel(logging.INFO)
logger = util.get_logger(__name__)
URI_API = "https://localbitcoins.com/"
SERVER = "https://localbitcoins.com"
class LocalBitcoins:
"""LocalBitcoins API object.
Documentation: https://localbitcoins.com/api-docs/
"""
# pylint: disable=too-many-public-methods
# API provides this many methods, I can't change that
def __init__(
self,
hmac_key: Optional[str],
hmac_secret: Optional[str],
debug: Optional[bool] = False,
) -> None:
self.hmac_key = ""
self.hmac_secret = ""
if hmac_key:
self.hmac_key = hmac_key.encode("ascii")
if hmac_secret:
self.hmac_secret = hmac_secret.encode("ascii")
self.debug = debug
if self.debug:
logging.getLogger("requests.packages.urllib3").setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
logger.debug(
"creating instance of LocalBitcoins API with api_key %s",
self.hmac_key,
)
def sign_payload(self, nonce, url, params_encoded):
"""
Sign the payload with our HMAC keys.
"""
# Calculate signature
message = nonce + self.hmac_key + url.encode("ascii")
if params_encoded:
if sys.version_info >= (3, 0) and isinstance(params_encoded, str):
message += params_encoded.encode("ascii")
else:
message += params_encoded
signature = (
hmac_lib.new(self.hmac_secret, msg=message, digestmod=hashlib.sha256)
.hexdigest()
.upper()
)
return signature
def encode_params(self, http_method, api_call_url, query_values):
if http_method == "POST":
api_request = requests.Request(
"POST", api_call_url, data=query_values
).prepare()
params_encoded = api_request.body
# GET method
else:
api_request = requests.Request(
"GET", api_call_url, params=query_values
).prepare()
params_encoded = urlparse(api_request.url).query
return (api_request, params_encoded)
@inlineCallbacks
def callback_api_call(self, response, result):
logger.debug(response)
text = yield response.content()
try:
result["response"] = json.loads(text)
except json.decoder.JSONDecodeError:
result["success"] = "ERROR"
result["message"] = "Error parsing JSON."
return result
result["status"] = response.code
if response.code == 200:
result["success"] = True
result["message"] = "OK"
else:
result["message"] = "API ERROR"
return result
def _api_call(
self,
api_method: str,
http_method: Optional[str] = "GET",
query_values: Optional[Dict[str, Any]] = None,
files: Optional[Any] = None,
) -> Dict[str, Any]:
api_call_url = URI_API + api_method
url = api_call_url
if url.startswith(SERVER):
url = url[len(SERVER) :] # noqa
# HMAC crypto stuff
api_request, params_encoded = self.encode_params(
http_method, api_call_url, query_values
)
nonce = str(int(time.time() * 1000)).encode("ascii")
signature = self.sign_payload(nonce, url, params_encoded)
headers = {
"Apiauth-Key": self.hmac_key,
"Apiauth-Nonce": nonce,
"Apiauth-Signature": signature,
}
api_request.headers["Apiauth-Key"] = self.hmac_key
api_request.headers["Apiauth-Nonce"] = nonce
api_request.headers["Apiauth-Signature"] = signature
logger.debug("API Call URL: %s", api_call_url)
logger.debug("Headers : %s", api_request.headers)
logger.debug("HTTP Method : %s", http_method)
logger.debug("Query Values: %s", query_values)
logger.debug("Query Values as Json:\n%s", json.dumps(query_values))
result: Dict[str, Any] = {
"success": False,
"message": "Invalid Method",
"response": None,
"status": None,
}
response = None
if http_method == "POST":
if query_values:
# response = httpx.post(
# url=api_call_url,
# headers=headers,
# content=json.dumps(query_values),
# )
response = treq.post(
api_call_url,
headers=headers,
data=query_values,
)
else:
# response = httpx.post(
# url=api_call_url,
# headers=headers,
# )
response = treq.post(
api_call_url,
headers=headers,
)
else:
# response = httpx.get(url=api_call_url, headers=headers, params=query_values)
response = treq.get(api_call_url, headers=headers, params=query_values)
if response:
response.addCallback(self.callback_api_call, result)
return response
# Account related API Methods
# ===========================
def account_info(self, username: str) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#account_info
"""
return self._api_call(api_method=f"api/account_info/{username}/")
def dashboard(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#dashboard
"""
return self._api_call(api_method="api/dashboard/")
# def dashboard_buyer(self) -> Dict[str, Any]:
# """See LocalBitcoins API.
#
# https://agoradesk.com/api-docs/v1#operation/getUserDashboardBuyer
# """
# return self._api_call(api_method="dashboard/buyer")
# def dashboard_seller(self) -> Dict[str, Any]:
# """See LocalBitcoins API.
#
# https://agoradesk.com/api-docs/v1#operation/getUserDashboardSeller
# """
# return self._api_call(api_method="dashboard/seller")
def dashboard_canceled(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#dashboard-canceled
"""
return self._api_call(api_method="api/dashboard/canceled/")
def dashboard_closed(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#dashboard-closed
"""
return self._api_call(api_method="api/dashboard/closed/")
def dashboard_released(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#dashboard-released
"""
return self._api_call(api_method="api/dashboard/released/")
def logout(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#logout
"""
return self._api_call(api_method="api/logout/", http_method="POST")
def myself(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#myself
"""
return self._api_call(api_method="api/myself/")
def notifications(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#notifications
"""
return self._api_call(api_method="api/notifications/")
def notifications_mark_as_read(self, notification_id: str) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#notifications-read
"""
return self._api_call(
api_method=f"notifications/mark_as_read/{notification_id}/",
http_method="POST",
)
def recent_messages(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#recent-messages
"""
return self._api_call(api_method="api/recent_messages/")
# Trade related API Methods
# ===========================
# post/feedback/{username} • Give feedback to a user
def feedback(
self, username: str, feedback: str, msg: Optional[str]
) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#feedback
"""
params = {"feedback": feedback}
if msg:
params["msg"] = msg
return self._api_call(
api_method=f"feedback/{username}/",
http_method="POST",
query_values=params,
)
# Todo:
# post/trade/contact_release/{trade_id} • Release trade escrow
# post/contact_fund/{trade_id} • Fund a trade
# post/contact_dispute/{trade_id} • Start a trade dispute
# post/contact_mark_as_paid/{trade_id} • Mark a trade as paid
def contact_mark_as_paid(self, trade_id: str) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#contact-paid
"""
return self._api_call(
api_method=f"contact_mark_as_paid/{trade_id}/", http_method="POST"
)
# post/contact_cancel/{trade_id} • Cancel the trade
def contact_cancel(
self,
trade_id: str,
) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#contact-cancel
"""
return self._api_call(
api_method=f"contact_cancel/{trade_id}",
http_method="POST",
)
# Todo:
# post/contact_escrow/{trade_id} • Enable escrow
# get/contact_messages/{trade_id} • Get trade messages
def contact_messages(
self, trade_id: str, after: Optional[arrow.Arrow] = None
) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#contact-message
"""
if after:
reply = self._api_call(
api_method=f"contact_messages/{trade_id}/",
query_values={"after": after.to("UTC").isoformat()},
)
else:
reply = self._api_call(api_method=f"contact_messages/{trade_id}/")
return reply
# post/contact_create/{ad_id} • Start a trade
def contact_create(
self,
ad_id: str,
amount: float,
msg: Optional[str] = None,
) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#contact-create
"""
payload: Dict[str, Any] = {"amount": amount}
if msg:
payload["msg"] = msg
return self._api_call(
api_method=f"contact_create/{ad_id}/",
http_method="POST",
query_values=payload,
)
# get/contact_info/{trade_id} • Get a trade by trade ID
def contact_info(self, trade_ids: Union[str, List[str]]) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#contact-info-id and
https://localbitcoins.com/api-docs/#contact-info
"""
api_method = "contact_info/"
if isinstance(trade_ids, list):
params = "?contacts="
for trade_id in trade_ids:
params += f"{trade_id},"
params = params[0:-1]
else:
params = f"/{trade_ids}"
api_method += params
return self._api_call(api_method=api_method)
# Todo: Add image upload functionality
# post/contact_message_post/{trade_id} • Send a chat message/attachment
def contact_message_post(
self, trade_id: str, msg: Optional[str] = None
) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#contact-post
"""
payload = {"msg": msg}
return self._api_call(
api_method=f"api/contact_message_post/{trade_id}/",
http_method="POST",
query_values=payload,
)
# Todo:
# get/contact_message_attachment/{trade_id}/{attachment_id}
# Advertisement related API Methods
# ================================
def ad_create(
self,
country_code: str,
currency: str,
trade_type: str,
price_equation: str,
track_max_amount: bool,
require_trusted_by_advertiser: bool,
bank_name: str,
sms_verification_required: Optional[bool] = None,
require_identification: Optional[bool] = None,
online_provider: Optional[str] = None,
msg: Optional[str] = None,
min_amount: Optional[float] = None,
max_amount: Optional[float] = None,
account_info: Optional[str] = None,
first_time_limit_btc: Optional[float] = None,
require_feedback_score: Optional[int] = None,
city: Optional[str] = None,
location_string: Optional[str] = None,
opening_hours: Optional[dict] = None,
visible: Optional[bool] = True,
require_trade_volume: Optional[float] = None,
volume_coefficient_btc: Optional[float] = None,
reference_type: Optional[str] = None,
display_reference: Optional[bool] = None,
) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#ad-create
"""
# pylint: disable=too-many-arguments
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# API takes this many arguments, I can't change that
# Too many locals and too many branches goes hand in hand
# with too many arguments
params: Dict[str, Any] = {
"countrycode": country_code,
"currency": currency,
"trade_type": trade_type,
"price_equation": price_equation,
"track_max_amount": track_max_amount,
"require_trusted_by_advertiser": require_trusted_by_advertiser,
}
if sms_verification_required:
params["sms_verification_required"] = True
if require_identification:
params["require_identification"] = True
if online_provider:
params["online_provider"] = online_provider
if msg:
params["msg"] = msg
if min_amount:
params["min_amount"] = min_amount
if max_amount:
params["max_amount"] = max_amount
if first_time_limit_btc:
params["first_time_limit_btc"] = first_time_limit_btc
if require_feedback_score:
params["require_feedback_score"] = require_feedback_score
if account_info:
params["account_info"] = account_info
if opening_hours:
params["opening_hours"] = opening_hours
if city:
params["city"] = city
if location_string:
params["location_string"] = location_string
if bank_name:
params["bank_name"] = bank_name
if visible:
params["visible"] = visible
if require_trade_volume:
params["require_trade_volume"] = require_trade_volume
if volume_coefficient_btc:
params["volume_coefficient_btc"] = volume_coefficient_btc
if reference_type:
params["reference_type"] = reference_type
if display_reference is not None:
params["display_reference"] = display_reference
return self._api_call(
api_method="api/ad-create/",
http_method="POST",
query_values=params,
)
def ad(
self,
ad_id: str,
country_code: str,
currency: str,
trade_type: str,
price_equation: str,
track_max_amount: bool,
require_trusted_by_advertiser: bool,
bank_name: str,
sms_verification_required: Optional[bool] = None,
require_identification: Optional[bool] = None,
online_provider: Optional[str] = None,
msg: Optional[str] = None,
min_amount: Optional[float] = None,
max_amount: Optional[float] = None,
account_info: Optional[str] = None,
first_time_limit_btc: Optional[float] = None,
require_feedback_score: Optional[int] = None,
city: Optional[str] = None,
location_string: Optional[str] = None,
opening_hours: Optional[dict] = None,
visible: Optional[bool] = True,
require_trade_volume: Optional[float] = None,
volume_coefficient_btc: Optional[float] = None,
reference_type: Optional[str] = None,
display_reference: Optional[bool] = None,
) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#ad-id
"""
# pylint: disable=invalid-name
# Don't want to change the name of the method from what the API call is
# pylint: disable=too-many-arguments
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# API takes this many arguments, I can't change that
# Too many locals and too many branches goes hand in hand
# with too many arguments
params: Dict[str, Any] = {
"countrycode": country_code,
"currency": currency,
"trade_type": trade_type,
"price_equation": price_equation,
"track_max_amount": track_max_amount,
"require_trusted_by_advertiser": require_trusted_by_advertiser,
}
if sms_verification_required:
params["sms_verification_required"] = True
if require_identification:
params["require_identification"] = True
if online_provider:
params["online_provider"] = online_provider
if msg:
params["msg"] = msg
if min_amount:
params["min_amount"] = min_amount
if max_amount:
params["max_amount"] = max_amount
if first_time_limit_btc:
params["first_time_limit_btc"] = first_time_limit_btc
if require_feedback_score:
params["require_feedback_score"] = require_feedback_score
if account_info:
params["account_info"] = account_info
if opening_hours:
params["opening_hours"] = opening_hours
if city:
params["city"] = city
if location_string:
params["location_string"] = location_string
if bank_name:
params["bank_name"] = bank_name
if visible:
params["visible"] = visible
if require_trade_volume:
params["require_trade_volume"] = require_trade_volume
if volume_coefficient_btc:
params["volume_coefficient_btc"] = volume_coefficient_btc
if reference_type:
params["reference_type"] = reference_type
if display_reference is not None:
params["display_reference"] = display_reference
return self._api_call(
api_method=f"api/ad/{ad_id}/",
http_method="POST",
query_values=params,
)
def ad_equation(self, ad_id: str, price_equation: str) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#ad-equation-id
"""
return self._api_call(
api_method=f"api/ad-equation/{ad_id}/",
http_method="POST",
query_values={"price_equation": price_equation},
)
def ad_delete(self, ad_id: str) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#ad-delete
"""
return self._api_call(api_method=f"api/ad-delete/{ad_id}/", http_method="POST")
def ads(
self,
country_code: Optional[str] = None,
currency: Optional[str] = None,
trade_type: Optional[str] = None,
visible: Optional[bool] = None,
asset: Optional[str] = None,
payment_method_code: Optional[str] = None,
page: Optional[int] = None,
) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#ads
"""
# pylint: disable=too-many-arguments
# API takes this many arguments, I can't change that
params = {}
if country_code:
params["countrycode"] = country_code
if currency:
params["currency"] = currency
if trade_type:
params["trade_type"] = trade_type
if visible is not None and visible:
params["visible"] = "1"
elif visible is not None and not visible:
params["visible"] = "0"
if asset:
params["asset"] = asset
if payment_method_code:
params["payment_method_code"] = payment_method_code
if page:
params["page"] = page
if len(params) == 0:
return self._api_call(api_method="api/ads/")
return self._api_call(api_method="api/ads/", query_values=params)
def ad_get(self, ad_ids: List[str]) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#ad-get and
hhttps://localbitcoins.com/api-docs/#ad-get-id
"""
api_method = "ad-get/"
params = None
ids = str(ad_ids)[1:-1].replace(" ", "").replace("'", "")
if len(ad_ids) == 1:
api_method += f"/{ids}"
else:
params = {"ads": ids}
return self._api_call(api_method=api_method, query_values=params)
def payment_methods(self, country_code: Optional[str] = None) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#payment-methods and
https://localbitcoins.com/api-docs/#payment_methods-cc
"""
api_method = "payment_methods/"
if country_code:
api_method += f"/{country_code}"
return self._api_call(api_method=api_method)
def country_codes(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#countrycodes
"""
return self._api_call(api_method="api/countrycodes/")
def currencies(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#currencies
"""
return self._api_call(api_method="api/currencies/")
def equation(self, price_equation: str, currency: str) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#equation
"""
return self._api_call(
api_method="api/equation/",
http_method="POST",
query_values={
"price_equation": price_equation,
"currency": currency,
},
)
# Public ad search related API Methods
# ====================================
def _generic_online(
self,
direction: str,
main_currency: str,
exchange_currency: str,
country_code: Optional[str] = None,
payment_method: Optional[str] = None,
amount: Optional[float] = None,
page: Optional[int] = None,
) -> Dict[str, Any]:
# pylint: disable=too-many-arguments
add_to_api_method = ""
if country_code:
add_to_api_method = f"/{country_code}"
if payment_method:
add_to_api_method += f"/{payment_method}"
params = self._generic_search_parameters(amount, page)
return self._api_call(
api_method=f"{direction}-{main_currency}-online/"
f"{exchange_currency}{add_to_api_method}/.json",
query_values=params,
)
@staticmethod
def _generic_search_parameters(amount, page):
params = None
if amount and not page:
params = {"amount": f"{amount}"}
elif amount and page:
params = {"amount": f"{amount}", "page": f"{page}"}
elif not amount and page:
params = {"page": f"{page}"}
return params
def buy_bitcoins_online( # TODO: check fields
self,
currency_code: str,
country_code: Optional[str] = None,
payment_method: Optional[str] = None,
amount: Optional[float] = None,
page: Optional[int] = None,
) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#online-buy1 and
https://localbitcoins.com/api-docs/#online-buy2 and
https://localbitcoins.com/api-docs/#online-buy3 and
https://localbitcoins.com/api-docs/#online-buy4 and
https://localbitcoins.com/api-docs/#online-buy5 and
https://localbitcoins.com/api-docs/#online-buy6
"""
# pylint: disable=too-many-arguments
return self._generic_online(
direction="buy",
main_currency="bitcoins",
exchange_currency=currency_code,
country_code=country_code,
payment_method=payment_method,
amount=amount,
page=page,
)
def sell_bitcoins_online(
self,
currency_code: str,
country_code: Optional[str] = None,
payment_method: Optional[str] = None,
amount: Optional[float] = None,
page: Optional[int] = None,
) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#online-sell1 and
https://localbitcoins.com/api-docs/#online-sell2 and
https://localbitcoins.com/api-docs/#online-sell3 and
https://localbitcoins.com/api-docs/#online-sell4 and
https://localbitcoins.com/api-docs/#online-sell5 and
https://localbitcoins.com/api-docs/#online-sell6
"""
# pylint: disable=too-many-arguments
return self._generic_online(
direction="sell",
main_currency="bitcoins",
exchange_currency=currency_code,
country_code=country_code,
payment_method=payment_method,
amount=amount,
page=page,
)
# Statistics related API Methods
# ==============================
def bitcoinaverage(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#ticker-all
"""
return self._api_call(api_method="api/bitcoinaverage/ticket-all-currencies/")
# Wallet related API Methods
# ===========================
def wallet(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#wallet
"""
return self._api_call(api_method="api/wallet/")
def wallet_balance(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#wallet-balance
"""
return self._api_call(api_method="api/wallet-balance/")
def wallet_addr(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#wallet-addr
"""
return self._api_call(api_method="api/wallet-addr/")
def fees(self) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#fees
"""
return self._api_call(api_method="api/fees/")
def wallet_send_pin(
self,
address: str,
amount: float,
password: str,
fee_level: str,
pincode: Optional[int] = None,
) -> Dict[str, Any]:
"""See LocalBitcoins API.
https://localbitcoins.com/api-docs/#wallet-send
"""
# pylint: disable=too-many-arguments
params = {
"address": address,
"amount": amount,
"password": password,
"fee_level": fee_level,
}
if pincode:
params["pincode"] = pincode
return self._api_call(
api_method="api/wallet-send-pin/",
http_method="POST",
query_values=params,
)

@ -1,31 +0,0 @@
# Other library imports
import logging
from json import dumps
import logstash
# Project imports
from settings import settings
logger = None
def init_logstash():
global logger
logger = logging.getLogger("ingest")
logger.setLevel(logging.INFO)
logger.addHandler(
logstash.TCPLogstashHandler(
settings.Logstash.Host,
int(settings.Logstash.Port),
version=1,
)
)
def send_logstash(text):
global logger
if logger is not None:
logger.info(dumps(text))
return True
return False

@ -1,411 +0,0 @@
# Other library imports
from json import loads
import db
import util
# Project imports
from settings import settings
class Markets(util.Base):
""" "
Markets handler for generic market functions.
"""
def find_trades_by_uid(self, uid):
"""
Find a list of trade IDs and references by a customer UID.
:return: tuple of (platform, trade_id, reference, currency)
"""
platform, username = self.ux.verify.get_uid(uid)
refs = db.get_refs()
matching_trades = []
for reference in refs:
ref_data = db.get_ref(reference)
tx_platform = ref_data["subclass"]
tx_username = ref_data["buyer"]
trade_id = ref_data["id"]
currency = ref_data["currency"]
if tx_platform == platform and tx_username == username:
to_append = (platform, trade_id, reference, currency)
matching_trades.append(to_append)
return matching_trades
def get_send_settings(self, platform):
if platform == "agora":
send_setting = settings.Agora.Send
post_message = self.agora.api.contact_message_post
elif platform == "lbtc":
send_setting = settings.LocalBitcoins.Send
post_message = self.lbtc.api.contact_message_post
return (send_setting, post_message)
def send_reference(self, platform, trade_id, reference):
"""
Send the reference to a customer.
"""
send_setting, post_message = self.get_send_settings(platform)
if send_setting == "1":
post_message(
trade_id,
f"When sending the payment please use reference code: {reference}",
)
def send_bank_details(self, platform, currency, trade_id):
"""
Send the bank details to a trade.
"""
send_setting, post_message = self.get_send_settings(platform)
self.log.info(f"Sending bank details/reference for {platform}/{trade_id}")
if send_setting == "1":
account_info = self.get_matching_account_details(platform, currency)
formatted_account_info = self.format_payment_details(
currency, account_info, real=True
)
if not formatted_account_info:
self.log.error(f"Payment info invalid: {formatted_account_info}")
return
post_message(
trade_id,
f"Payment details: \n{formatted_account_info}",
)
def get_all_assets(self, platform):
sets = util.get_settings(platform)
assets = loads(sets.AssetList)
return assets
def get_all_providers(self, platform):
sets = util.get_settings(platform)
providers = loads(sets.ProviderList)
return providers
def get_all_currencies(self, platform):
sets = util.get_settings(platform)
currencies = list(set([x[0] for x in loads(sets.DistList)]))
return currencies
def get_new_ad_equations(self, platform, public_ads, assets=None):
"""
Update all our prices.
:param public_ads: dictionary of public ads keyed by currency
:type public_ads: dict
:return: list of ads to modify
:rtype: list
"""
sets = util.get_settings(platform)
username = sets.Username
min_margin = sets.MinMargin
max_margin = sets.MaxMargin
to_update = []
# NOTES:
# Get all ads for each currency, with all the payment methods.
# Create a function to, in turn, filter these so it contains only one payment method. Run autoprice on this.
# Append all results to to_update. Repeat for remaining payment methods, then call slow update.
# (asset, currency, provider)
if not assets:
assets = self.get_all_assets(platform)
currencies = self.get_all_currencies(platform)
providers = self.get_all_providers(platform)
if platform == "lbtc":
providers = [
self.sources.lbtc.map_provider(x, reverse=True) for x in providers
]
sinks_currencies = self.sinks.currencies
supported_currencies = [
currency for currency in currencies if currency in sinks_currencies
]
currencies = supported_currencies
brute = [
(asset, currency, provider)
for asset in assets
for currency in currencies
for provider in providers
]
for asset, currency, provider in brute:
# Filter currency
try:
public_ads_currency = public_ads[currency]
except KeyError:
# self.log.error("Error getting public ads for currency {currency}", currency=currency)
if currency == "GBP":
self.log.error(
"Error getting public ads for currency GBP, aborting"
)
break
continue
# Filter asset
public_ads_filtered = [ad for ad in public_ads_currency if ad[4] == asset]
# Filter provider
public_ads_filtered = [
ad for ad in public_ads_filtered if ad[3] == provider
]
our_ads = [ad for ad in public_ads_filtered if ad[1] == username]
if not our_ads:
self.log.warning(
f"No ads found in {platform} public listing for {asset} {currency} {provider}"
)
continue
new_margin = self.autoprice(
username, min_margin, max_margin, public_ads_filtered, currency
)
# self.log.info("New rate for {currency}: {rate}", currency=currency, rate=new_margin)
if platform == "agora":
new_formula = (
f"coingecko{asset.lower()}usd*usd{currency.lower()}*{new_margin}"
)
elif platform == "lbtc":
new_formula = f"btc_in_usd*{new_margin}*USD_in_{currency}"
for ad in our_ads:
ad_id = ad[0]
asset = ad[4]
our_margin = ad[5]
if new_margin != our_margin:
to_update.append([str(ad_id), new_formula, asset, currency, False])
return to_update
def autoprice(self, username, min_margin, max_margin, ads, currency):
"""
Helper function to automatically adjust the price up/down in certain markets
in order to gain the most profits and sales.
:param ads: list of ads
:type ads: list of lists
:param currency: currency of the ads
:type currency: string
:return: the rate we should use for this currency
:rtype: float
"""
# self.log.debug("Autoprice starting for {x}", x=currency)
# Find cheapest ad
# Filter by 3rd index on each ad list to find the cheapest
min_margin_ad = min(ads, key=lambda x: x[6])
# self.log.debug("Minimum margin ad: {x}", x=min_margin_ad)
# Find second cheapest that is not us
# Remove results from ads that are us
ads_without_us = [ad for ad in ads if not ad[1] == username]
# self.log.debug("Ads without us: {x}", x=ads_without_us)
# Find ads above our min that are not us
ads_above_our_min_not_us = [
ad for ad in ads_without_us if ad[6] > float(min_margin)
]
# self.log.debug("Ads above our min not us: {x}", x=ads_above_our_min_not_us)
# Check that this list without us is not empty
if ads_without_us:
# Find the cheapest from these
min_margin_ad_not_us = min(ads_without_us, key=lambda x: x[6])
# self.log.debug("Min margin ad not us: {x}", x=min_margin_ad_not_us)
# Lowball the lowest ad that is not ours
lowball_lowest_not_ours = min_margin_ad_not_us[6] # - 0.005
# self.log.debug("Lowball lowest not ours: {x}", x=lowball_lowest_not_ours)
# Check if the username field of the cheapest ad matches ours
if min_margin_ad[1] == username:
# self.log.debug("We are the cheapest for: {x}", x=currency)
# We are the cheapest!
# Are all of the ads ours?
all_ads_ours = all([ad[1] == username for ad in ads])
if all_ads_ours:
# self.log.debug("All ads are ours for: {x}", x=currency)
# Now we know it's safe to return the maximum value
return float(max_margin)
else:
# self.log.debug("All ads are NOT ours for: {x}", x=currency)
# All the ads are not ours, but we are first...
# Check if the lowballed, lowest (that is not ours) ad's margin
# is less than our minimum
if lowball_lowest_not_ours < float(min_margin):
# self.log.debug("Lowball lowest not ours less than MinMargin")
return float(min_margin)
elif lowball_lowest_not_ours > float(max_margin):
# self.log.debug("Lowball lowest not ours more than MaxMargin")
return float(max_margin)
else:
# self.log.debug("Returning lowballed figure: {x}", x=lowball_lowest_not_ours)
return lowball_lowest_not_ours
else:
# self.log.debug("We are NOT the cheapest for: {x}", x=currency)
# We are not the cheapest :(
# Check if this list is empty
if not ads_above_our_min_not_us:
# Return the maximum margin?
return float(max_margin)
# Find cheapest ad above our min that is not us
cheapest_ad = min(ads_above_our_min_not_us, key=lambda x: x[4])
cheapest_ad_margin = cheapest_ad[6] # - 0.005
if cheapest_ad_margin > float(max_margin):
# self.log.debug("Cheapest ad not ours more than MaxMargin")
return float(max_margin)
# self.log.debug("Cheapest ad above our min that is not us: {x}", x=cheapest_ad)
return cheapest_ad_margin
def create_distribution_list(self, platform, filter_asset=None):
"""
Create a list for distribution of ads.
:return: generator of asset, countrycode, currency, provider
:rtype: generator of tuples
"""
sets = util.get_settings(platform)
# Iterate providers like REVOLUT, NATIONAL_BANK
for provider in loads(sets.ProviderList):
# Iterate assets like XMR, BTC
for asset in loads(sets.AssetList):
# Iterate pairs of currency and country like EUR, GB
for currency, countrycode in loads(sets.DistList):
if filter_asset:
if asset == filter_asset:
yield (asset, countrycode, currency, provider)
else:
yield (asset, countrycode, currency, provider)
def get_valid_account_details(self, platform):
currencies = self.sinks.currencies
account_info = self.sinks.account_info
all_currencies = self.get_all_currencies(platform)
supported_currencies = [
currency for currency in currencies if currency in all_currencies
]
currency_account_info_map = {}
for currency in supported_currencies:
for bank, accounts in account_info.items():
for account in accounts:
if account["currency"] == currency:
currency_account_info_map[currency] = account["account_number"]
currency_account_info_map[currency]["bank"] = bank.split("_")[0]
currency_account_info_map[currency]["recipient"] = account[
"recipient"
]
return (supported_currencies, currency_account_info_map)
def get_matching_account_details(self, platform, currency):
(
supported_currencies,
currency_account_info_map,
) = self.get_valid_account_details(platform)
if currency not in supported_currencies:
return False
return currency_account_info_map[currency]
def _distribute_account_details(self, platform, currencies=None, account_info=None):
"""
Distribute account details for ads.
We will disable ads we can't support.
"""
if platform == "agora":
caller = self.agora
elif platform == "lbtc":
caller = self.lbtc
if not currencies:
currencies = self.sinks.currencies
if not account_info:
account_info = self.sinks.account_info
(
supported_currencies,
currency_account_info_map,
) = self.get_valid_account_details(platform)
# not_supported = [currency for currency in all_currencies if currency not in supported_currencies]
our_ads = caller.enum_ads()
supported_ads = [ad for ad in our_ads if ad[3] in supported_currencies]
not_supported_ads = [ad for ad in our_ads if ad[3] not in supported_currencies]
for ad in supported_ads:
asset = ad[0]
countrycode = ad[2]
currency = ad[3]
provider = ad[4]
payment_details = currency_account_info_map[currency]
ad_id = ad[1]
caller.create_ad(
asset,
countrycode,
currency,
provider,
payment_details,
visible=True,
edit=True,
ad_id=ad_id,
)
for ad in not_supported_ads:
asset = ad[0]
countrycode = ad[2]
currency = ad[3]
provider = ad[4]
ad_id = ad[1]
caller.create_ad(
asset,
countrycode,
currency,
provider,
payment_details=False,
visible=False,
edit=True,
ad_id=ad_id,
)
def distribute_account_details(self, currencies=None, account_info=None):
"""
Helper to distribute the account details for all platforms.
"""
platforms = ("agora", "lbtc")
for platform in platforms:
self._distribute_account_details(
platform, currencies=currencies, account_info=account_info
)
def format_ad(self, asset, currency, payment_details_text):
"""
Format the ad.
"""
ad = settings.Platform.Ad
# Substitute the currency
ad = ad.replace("$CURRENCY$", currency)
# Substitute the asset
ad = ad.replace("$ASSET$", asset)
# Substitute the payment details
ad = ad.replace("$PAYMENT$", payment_details_text)
# Strip extra tabs
ad = ad.replace("\\t", "\t")
return ad
def format_payment_details(self, currency, payment_details, real=False):
"""
Format the payment details.
"""
if not payment_details:
return False
if real:
payment = settings.Platform.PaymentDetailsReal
else:
payment = settings.Platform.PaymentDetails
payment_text = ""
for field, value in payment_details.items():
formatted_name = field.replace("_", " ")
formatted_name = formatted_name.capitalize()
payment_text += f"* {formatted_name}: **{value}**"
if field != list(payment_details.keys())[-1]: # No trailing newline
payment_text += "\n"
payment = payment.replace("$PAYMENT$", payment_text)
payment = payment.replace("$CURRENCY$", currency)
return payment

@ -1,505 +0,0 @@
# Twisted imports
import logging
from datetime import datetime
import urllib3
import util
from forex_python.converter import CurrencyRates
from lib.logstash import send_logstash
from opensearchpy import OpenSearch
# Other library imports
from pycoingecko import CoinGeckoAPI
# Project imports
from settings import settings
from twisted.internet.defer import inlineCallbacks
from twisted.internet.task import LoopingCall
# TODO: secure ES traffic properly
urllib3.disable_warnings()
tracer = logging.getLogger("opensearch")
tracer.setLevel(logging.CRITICAL)
tracer = logging.getLogger("elastic_transport.transport")
tracer.setLevel(logging.CRITICAL)
class Money(util.Base):
"""
Generic class for handling money-related matters that aren't Revolut or Agora.
"""
def __init__(self):
"""
Initialise the Money object.
Set the logger.
Initialise the CoinGecko API.
"""
super().__init__()
self.cr = CurrencyRates()
self.cg = CoinGeckoAPI()
if settings.ES.Enabled == "1":
# self.es = Elasticsearch(
# f"https://{settings.ES.Host}:9200",
# verify_certs=False,
# basic_auth=(settings.ES.Username, settings.ES.Pass),
# # ssl_assert_fingerprint=("6b264fd2fd107d45652d8add1750a8a78f424542e13b056d0548173006260710"),
# ca_certs="certs/ca.crt",
# )
# TODO: implement ca certs https://opensearch.org/docs/latest/clients/python/
auth = (settings.ES.Username, settings.ES.Pass)
self.es = OpenSearch(
hosts=[{"host": settings.ES.Host, "port": 9200}],
http_compress=False, # enables gzip compression for request bodies
http_auth=auth,
# client_cert = client_cert_path,
# client_key = client_key_path,
use_ssl=True,
verify_certs=False,
ssl_assert_hostname=False,
ssl_show_warn=False,
# a_certs=ca_certs_path,
)
@inlineCallbacks
def run_checks_in_thread(self):
"""
Run all the balance checks that output into ES in another thread.
"""
total = self.get_total()
remaining = self.get_remaining()
profit = self.get_profit()
profit_with_trades = self.get_profit(True)
open_trades = self.get_open_trades_usd()
total_remaining = self.get_total_remaining()
total_with_trades = self.get_total_with_trades()
# This will make them all run concurrently, hopefully not hitting rate limits
for x in (
total,
remaining,
profit,
profit_with_trades,
open_trades,
total_remaining,
total_with_trades,
):
yield x
def setup_loops(self):
"""
Set up the LoopingCalls to get the balance so we have data in ES.
"""
if settings.ES.Enabled == "1" or settings.Logstash.Enabled == "1":
self.lc_es_checks = LoopingCall(self.run_checks_in_thread)
delay = int(settings.ES.RefreshSec)
self.lc_es_checks.start(delay)
if settings.ES.Enabled == "1":
self.agora.es = self.es
self.lbtc.es = self.es
def write_to_es(self, msgtype, cast):
cast["type"] = "money"
cast["ts"] = str(datetime.now().isoformat())
cast["xtype"] = msgtype
if settings.ES.Enabled == "1":
self.es.index(index=settings.ES.Index, body=cast)
elif settings.Logstash.Enabled == "1":
send_logstash(cast)
def lookup_rates(self, platform, ads, rates=None):
"""
Lookup the rates for a list of public ads.
"""
if not rates:
rates = self.cg.get_price(
ids=["monero", "bitcoin"],
vs_currencies=self.markets.get_all_currencies(platform),
)
# Set the price based on the asset
for ad in ads:
if ad[4] == "XMR":
coin = "monero"
elif ad[4] == "BTC":
coin = "bitcoin" # No s here
currency = ad[5]
base_currency_price = rates[coin][currency.lower()]
price = float(ad[2])
rate = round(price / base_currency_price, 2)
ad.append(rate)
# TODO: sort?
return sorted(ads, key=lambda x: x[2])
def get_rates_all(self):
"""
Get all rates that pair with USD.
:return: dictionary of USD/XXX rates
:rtype: dict
"""
rates = self.cr.get_rates("USD")
return rates
def get_acceptable_margins(self, platform, currency, amount):
"""
Get the minimum and maximum amounts we would accept a trade for.
:param currency: currency code
:param amount: amount
:return: (min, max)
:rtype: tuple
"""
sets = util.get_settings(platform)
rates = self.get_rates_all()
if currency == "USD":
min_amount = amount - float(sets.AcceptableUSDMargin)
max_amount = amount + float(sets.AcceptableUSDMargin)
return (min_amount, max_amount)
amount_usd = amount / rates[currency]
min_usd = amount_usd - float(sets.AcceptableUSDMargin)
max_usd = amount_usd + float(sets.AcceptableUSDMargin)
min_local = min_usd * rates[currency]
max_local = max_usd * rates[currency]
return (min_local, max_local)
def get_minmax(self, platform, asset, currency):
sets = util.get_settings(platform)
rates = self.get_rates_all()
if currency not in rates and not currency == "USD":
self.log.error(f"Can't create ad without rates: {currency}")
return
if asset == "XMR":
min_usd = float(sets.MinUSDXMR)
max_usd = float(sets.MaxUSDXMR)
elif asset == "BTC":
min_usd = float(sets.MinUSDBTC)
max_usd = float(sets.MaxUSDBTC)
if currency == "USD":
min_amount = min_usd
max_amount = max_usd
else:
min_amount = rates[currency] * min_usd
max_amount = rates[currency] * max_usd
return (min_amount, max_amount)
def to_usd(self, amount, currency):
if currency == "USD":
return float(amount)
else:
rates = self.get_rates_all()
return float(amount) / rates[currency]
def multiple_to_usd(self, currency_map):
"""
Convert multiple curencies to USD while saving API calls.
"""
rates = self.get_rates_all()
cumul = 0
for currency, amount in currency_map.items():
if currency == "USD":
cumul += float(amount)
else:
cumul += float(amount) / rates[currency]
return cumul
@inlineCallbacks
def get_profit(self, trades=False):
"""
Check how much total profit we have made.
:return: profit in USD
:rtype: float
"""
total_usd = yield self.get_total_usd()
if not total_usd:
return False
if trades:
trades_usd = yield self.get_open_trades_usd()
total_usd += trades_usd
profit = total_usd - float(settings.Money.BaseUSD)
if trades:
cast_es = {
"profit_trades_usd": profit,
}
else:
cast_es = {
"profit_usd": profit,
}
self.write_to_es("get_profit", cast_es)
return profit
@inlineCallbacks
def get_total_usd(self):
"""
Get total USD in all our accounts, bank and trading.
:return: value in USD
:rtype float:
"""
total_sinks_usd = self.sinks.get_total_usd()
agora_wallet_xmr = yield self.agora.api.wallet_balance_xmr()
agora_wallet_btc = yield self.agora.api.wallet_balance()
lbtc_wallet_btc = yield self.lbtc.api.wallet_balance()
total_xmr_agora = agora_wallet_xmr["response"]["data"]["total"]["balance"]
total_btc_agora = agora_wallet_btc["response"]["data"]["total"]["balance"]
total_btc_lbtc = lbtc_wallet_btc["response"]["data"]["total"]["balance"]
# Get the XMR -> USD exchange rate
xmr_usd = self.cg.get_price(ids="monero", vs_currencies=["USD"])
# Get the BTC -> USD exchange rate
btc_usd = self.cg.get_price(ids="bitcoin", vs_currencies=["USD"])
# Convert the Agora BTC total to USD
total_usd_agora_btc = float(total_btc_agora) * btc_usd["bitcoin"]["usd"]
# Convert the LBTC BTC total to USD
total_usd_lbtc_btc = float(total_btc_lbtc) * btc_usd["bitcoin"]["usd"]
# Convert the Agora XMR total to USD
total_usd_agora_xmr = float(total_xmr_agora) * xmr_usd["monero"]["usd"]
# Add it all up
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
total_usd_lbtc = total_usd_lbtc_btc
total_usd = total_usd_agora + total_usd_lbtc + total_sinks_usd
cast_es = {
"price_usd": total_usd,
"total_usd_agora_xmr": total_usd_agora_xmr,
"total_usd_agora_btc": total_usd_agora_btc,
"total_usd_lbtc_btc": total_usd_lbtc_btc,
"total_xmr_agora": total_xmr_agora,
"total_btc_agora": total_btc_agora,
"total_btc_lbtc": total_btc_lbtc,
"xmr_usd": xmr_usd["monero"]["usd"],
"btc_usd": btc_usd["bitcoin"]["usd"],
"total_sinks_usd": total_sinks_usd,
"total_usd_agora": total_usd_agora,
}
self.write_to_es("get_total_usd", cast_es)
return total_usd
# TODO: possibly refactor this into smaller functions which don't return as much stuff
# check if this is all really needed in the corresponding withdraw function
@inlineCallbacks
def get_total(self):
"""
Get all the values corresponding to the amount of money we hold.
:return: ((total SEK, total USD, total GBP), (total XMR USD, total BTC USD), (total XMR, total BTC))
:rtype: tuple(tuple(float, float, float), tuple(float, float), tuple(float, float))
"""
total_sinks_usd = self.sinks.get_total_usd()
agora_wallet_xmr = yield self.agora.api.wallet_balance_xmr()
agora_wallet_btc = yield self.agora.api.wallet_balance()
lbtc_wallet_btc = yield self.lbtc.api.wallet_balance()
if not agora_wallet_xmr["success"]:
return False
if not agora_wallet_btc["success"]:
return False
if not lbtc_wallet_btc["success"]:
return False
if not agora_wallet_xmr["response"]:
return False
if not agora_wallet_btc["response"]:
return False
if not lbtc_wallet_btc["response"]:
return False
total_xmr_agora = agora_wallet_xmr["response"]["data"]["total"]["balance"]
total_btc_agora = agora_wallet_btc["response"]["data"]["total"]["balance"]
total_btc_lbtc = lbtc_wallet_btc["response"]["data"]["total"]["balance"]
# Get the XMR -> USD exchange rate
xmr_usd = self.cg.get_price(ids="monero", vs_currencies=["USD"])
# Get the BTC -> USD exchange rate
btc_usd = self.cg.get_price(ids="bitcoin", vs_currencies=["USD"])
# Convert the Agora XMR total to USD
total_usd_agora_xmr = float(total_xmr_agora) * xmr_usd["monero"]["usd"]
# Convert the Agora BTC total to USD
total_usd_agora_btc = float(total_btc_agora) * btc_usd["bitcoin"]["usd"]
# Convert the LBTC BTC total to USD
total_usd_lbtc_btc = float(total_btc_lbtc) * btc_usd["bitcoin"]["usd"]
# Add it all up
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
total_usd_lbtc = total_usd_lbtc_btc
total_usd = total_usd_agora + total_usd_lbtc + total_sinks_usd
total_btc_usd = total_usd_agora_btc + total_usd_lbtc_btc
total_xmr_usd = total_usd_agora_xmr
total_xmr = total_xmr_agora
total_btc = total_btc_lbtc + total_btc_agora
# Convert the total USD price to GBP and SEK
rates = self.get_rates_all()
price_sek = rates["SEK"] * total_usd
price_usd = total_usd
price_gbp = rates["GBP"] * total_usd
cast = (
(
price_sek,
price_usd,
price_gbp,
), # Total prices in our 3 favourite currencies
(
total_xmr_usd,
total_btc_usd,
), # Total USD balance in only Agora
(total_xmr, total_btc),
) # Total XMR and BTC balance in Agora
cast_es = {
"price_sek": price_sek,
"price_usd": price_usd,
"price_gbp": price_gbp,
"total_usd_agora_xmr": total_usd_agora_xmr,
"total_usd_agora_btc": total_usd_agora_btc,
"total_usd_lbtc_btc": total_usd_lbtc_btc,
"total_xmr_agora": total_xmr_agora,
"total_btc_agora": total_btc_agora,
"total_btc_lbtc": total_btc_lbtc,
"xmr_usd": xmr_usd["monero"]["usd"],
"btc_usd": btc_usd["bitcoin"]["usd"],
"total_sinks_usd": total_sinks_usd,
"total_usd_agora": total_usd_agora,
}
self.write_to_es("get_total", cast_es)
return cast
@inlineCallbacks
def get_remaining(self):
"""
Check how much profit we need to make in order to withdraw.
:return: profit remaining in USD
:rtype: float
"""
total_usd = yield self.get_total_usd()
if not total_usd:
return False
withdraw_threshold = float(settings.Money.BaseUSD) + float(
settings.Money.WithdrawLimit
)
remaining = withdraw_threshold - total_usd
cast_es = {
"remaining_usd": remaining,
}
self.write_to_es("get_remaining", cast_es)
return remaining
def open_trades_usd_parse_dash(self, platform, dash, rates):
cumul_usd = 0
for contact_id, contact in dash.items():
# We need created at in order to look up the historical prices
created_at = contact["data"]["created_at"]
# Reformat the date how CoinGecko likes
# 2022-05-02T11:17:14+00:00
if "+" in created_at:
date_split = created_at.split("+")
date_split[1].replace(".", "")
date_split[1].replace(":", "")
created_at = "+".join(date_split)
date_parsed = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S%z")
else:
date_parsed = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ")
date_formatted = date_parsed.strftime("%d-%m-%Y")
# Get the historical rates for the right asset, extract the price
if platform == "agora":
asset = contact["data"]["advertisement"]["asset"]
elif platform == "lbtc":
asset = "BTC"
if asset == "XMR":
amount_crypto = contact["data"]["amount_xmr"]
history = self.cg.get_coin_history_by_id(
id="monero", date=date_formatted
)
if "market_data" not in history:
return False
crypto_usd = float(history["market_data"]["current_price"]["usd"])
elif asset == "BTC":
amount_crypto = contact["data"]["amount_btc"]
history = self.cg.get_coin_history_by_id(
id="bitcoin", date=date_formatted
)
crypto_usd = float(history["market_data"]["current_price"]["usd"])
# Convert crypto to fiat
amount = float(amount_crypto) * crypto_usd
currency = contact["data"]["currency"]
if not contact["data"]["is_selling"]:
continue
if currency == "USD":
cumul_usd += float(amount)
else:
rate = rates[currency]
amount_usd = float(amount) / rate
cumul_usd += amount_usd
return cumul_usd
@inlineCallbacks
def get_open_trades_usd(self):
"""
Get total value of open trades in USD.
:return: total trade value
:rtype: float
"""
dash_agora = self.agora.wrap_dashboard()
dash_lbtc = self.lbtc.wrap_dashboard()
dash_agora = yield dash_agora
dash_lbtc = yield dash_lbtc
if dash_agora is False:
return False
if dash_lbtc is False:
return False
rates = self.get_rates_all()
cumul_usd_agora = self.open_trades_usd_parse_dash("agora", dash_agora, rates)
cumul_usd_lbtc = self.open_trades_usd_parse_dash("lbtc", dash_lbtc, rates)
cumul_usd = cumul_usd_agora + cumul_usd_lbtc
cast_es = {
"trades_usd": cumul_usd,
}
self.write_to_es("get_open_trades_usd", cast_es)
return cumul_usd
@inlineCallbacks
def get_total_remaining(self):
"""
Check how much profit we need to make in order to withdraw, taking into account open trade value.
:return: profit remaining in USD
:rtype: float
"""
total_usd = yield self.get_total_usd()
total_trades_usd = yield self.get_open_trades_usd()
if not total_usd:
return False
total_usd += total_trades_usd
withdraw_threshold = float(settings.Money.BaseUSD) + float(
settings.Money.WithdrawLimit
)
remaining = withdraw_threshold - total_usd
cast_es = {
"total_remaining_usd": remaining,
}
self.write_to_es("get_total_remaining", cast_es)
return remaining
@inlineCallbacks
def get_total_with_trades(self):
total_usd = yield self.get_total_usd()
if not total_usd:
return False
total_trades_usd = yield self.get_open_trades_usd()
total_with_trades = total_usd + total_trades_usd
cast_es = {
"total_with_trades": total_with_trades,
}
self.write_to_es("get_total_with_trades", cast_es)
return total_with_trades

@ -1,125 +0,0 @@
from serde import Model, fields
class Amount(Model):
amount: fields.Str()
currency: fields.Str()
class TransactionBBAN(Model):
bban: fields.Optional(fields.Str())
class ExchangeInstructedAmount(Model):
amount: fields.Str()
currency: fields.Str()
class TransactionCurrencyExchange(Model):
exchangeRate: fields.Str()
instructedAmount: fields.Nested(ExchangeInstructedAmount)
sourceCurrency: fields.Str()
targetCurrency: fields.Str()
unitCurrency: fields.Str()
class Transaction(Model):
bookingDate: fields.Date()
creditorAccount: fields.Optional(fields.Nested(TransactionBBAN))
creditorName: fields.Optional(fields.Str())
debtorName: fields.Optional(fields.Str())
currencyExchange: fields.Optional(fields.Nested(TransactionCurrencyExchange))
proprietaryBankTransactionCode: fields.Optional(fields.Str())
remittanceInformationUnstructured: fields.Optional(fields.Str())
transactionAmount: fields.Nested(Amount)
transactionId: fields.Optional(fields.Str())
class Pending(Transaction):
pass
class Booked(Transaction):
pass
class Transactions(Model):
pending: fields.List(Pending)
booked: fields.List(Booked)
class TXRoot(Model):
transactions: fields.Optional(fields.Nested(Transactions))
class AccessToken(Model):
access: fields.Str()
class Institution(Model):
id: fields.Str()
name: fields.Str()
bic: fields.Str()
transaction_total_days: fields.Str()
countries: fields.List()
logo: fields.Url()
class Institutions(Model):
institutions: fields.List(Institution)
class Agreement(Model):
id: fields.Str()
created: fields.DateTime()
redirect: fields.Url()
status: fields.Str()
institution_id: fields.Str()
agreement: fields.Str()
reference: fields.Str()
accounts: fields.List()
link: fields.Url()
ssn: fields.Literal(None) # ?
account_selection: fields.Bool()
redirect_immediate: fields.Bool()
class Requisitions(Model):
count: fields.Int()
next: fields.Literal(None) # wrong
previous: fields.Literal(None) # wrong
results: fields.List(Agreement)
class Account(Model):
resourceId: fields.Optional(fields.Str())
iban: fields.Optional(fields.Str())
bban: fields.Optional(fields.Str())
currency: fields.Str()
ownerName: fields.Optional(fields.Str())
status: fields.Optional(fields.Str())
details: fields.Optional(fields.Str())
class AccountDetails(Model):
account: fields.Optional(fields.Nested(Account))
class AccountBalanceAmount(Model):
amount: fields.Str()
currency: fields.Str()
class AccountBalances(Model):
balanceAmount: fields.Nested(AccountBalanceAmount)
balanceType: fields.Str()
referenceDate: fields.Date()
class AccountBalancesRoot(Model):
balances = fields.Optional(fields.List(AccountBalances))
class RequisitionResponse(Model):
summary: fields.Str()
detail: fields.Str()

@ -1,14 +0,0 @@
from serde import Model, fields
class AccountBalances(Model):
currency: fields.Str()
available: fields.Float()
current: fields.Float()
overdraft: fields.Float()
update_timestamp: fields.DateTime()
class AccountBalancesRoot(Model):
results: fields.List(AccountBalances)
status: fields.Str()

@ -1,447 +0,0 @@
# Twisted/Klein imports
# Other library imports
from json import dumps
from random import choices
from string import ascii_uppercase
import db
import util
# Project imports
from settings import settings
from twisted.internet.defer import inlineCallbacks
class Transactions(util.Base):
"""
Handler class for incoming Revolut transactions.
"""
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:
self.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:
self.log.error(f"Sender cannot be extracted: {data}")
return "not_set"
realname, part2 = data["reference"].split(" ")
return realname
return "not_set"
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 = 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:
self.log.error(f"Multiple references valid for TXID {txid}: {reference}")
self.irc.sendmsg(f"Multiple references valid for TXID {txid}: {reference}")
self.ux.notify.notify_tx_lookup_failed(
currency, amount, reference, "MULTIPLE_REFS_MATCH"
)
return False
if len(stored_trade_reference) == 0:
return None
return stored_trade_reference.pop()
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):
self.log.info(
"Not checking against amount and currency as amount exceeds MAX"
)
self.irc.sendmsg(
"Not checking against amount and currency as amount exceeds MAX"
)
# Close here if the amount exceeds the allowable limit for no reference
self.ux.notify.notify_tx_lookup_failed(
currency, amount, reference, "EXCEEDS_MAX"
)
return False
return True
def amount_currency_lookup(self, amount, currency, txid, reference):
self.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.
self.log.info(f"Checking against amount and currency for TXID {txid}")
self.irc.sendmsg(f"Checking against amount and currency for TXID {txid}")
if not self.can_alt_lookup(amount, currency, reference):
return False
stored_trade = self.find_trade(txid, currency, amount)
if not stored_trade:
self.log.info(
f"Failed to get reference by amount and currency: {txid} {currency} {amount}"
)
self.irc.sendmsg(
f"Failed to get reference by amount and currency: {txid} {currency} {amount}"
)
self.ux.notify.notify_tx_lookup_failed(
currency, amount, reference, "ALT_LOOKUP_FAILED"
)
return None
stored_trade["amount"] = float(stored_trade["amount"]) # convert to float
return stored_trade
def normal_lookup(self, stored_trade_reference, reference, currency, amount):
stored_trade = db.get_ref(stored_trade_reference)
if not stored_trade:
self.log.info(f"No reference in DB for {reference}")
self.irc.sendmsg(f"No reference in DB for {reference}")
self.ux.notify.notify_tx_lookup_failed(
currency, amount, reference, "NOREF", stored_trade_reference
)
return False
stored_trade["amount"] = float(stored_trade["amount"]) # convert to float
return stored_trade
def currency_check(self, currency, amount, reference, stored_trade):
if not stored_trade["currency"] == currency:
self.log.info(
f"Currency mismatch, Agora: {stored_trade['currency']} / Sink: {currency}"
)
self.irc.sendmsg(
f"Currency mismatch, Agora: {stored_trade['currency']} / Sink: {currency}"
)
self.ux.notify.notify_tx_lookup_failed(
currency,
amount,
reference,
"CURRENCY_MISMATCH",
stored_trade["id"],
)
return False
return True
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 = self.money.get_acceptable_margins(
platform, currency, stored_trade["amount"]
)
self.log.info(
f"Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}"
)
self.irc.sendmsg(
f"Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}"
)
if not min_amount < amount < max_amount:
self.log.info(
"Amount mismatch - not in margins: {stored_trade['amount']} (min: {min_amount} / max: {max_amount}"
)
self.irc.sendmsg(
f"Amount mismatch - not in margins: {stored_trade['amount']} (min: {min_amount} / max: {max_amount}"
)
self.ux.notify.notify_tx_lookup_failed(
currency,
amount,
reference,
"AMOUNT_MARGIN_MISMATCH",
stored_trade["id"],
)
return False
return True
@inlineCallbacks
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)
self.log.info(f"Transaction processed: {dumps(to_store, indent=2)}")
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
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 at all
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
)
self.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:
# self.log.error(f"Cannot release trade {reference}.")
# return
rtrn = yield self.release_funds(stored_trade["id"], stored_trade["reference"])
if rtrn:
self.ux.notify.notify_complete_trade(amount, currency)
@inlineCallbacks
def release_funds(self, trade_id, reference):
stored_trade = db.get_ref(reference)
platform = stored_trade["subclass"]
logmessage = f"All checks passed, releasing funds for {trade_id} {reference}"
self.log.info(logmessage)
self.irc.sendmsg(logmessage)
if platform == "agora":
release = self.agora.release_funds
post_message = self.agora.api.contact_message_post
elif platform == "lbtc":
release = self.lbtc.release_funds
post_message = self.lbtc.api.contact_message_post
rtrn = yield release(trade_id)
if rtrn["message"] == "OK":
post_message(trade_id, "Thanks! Releasing now :)")
return True
else:
logmessage = f"Release funds unsuccessful: {rtrn['message']}"
self.log.error(logmessage)
self.irc.sendmsg(logmessage)
self.ux.notify.notify_release_unsuccessful(trade_id)
return
# Parse the escrow release response
message = rtrn["message"]
# message_long = rtrn["response"]["data"]["message"]
self.irc.sendmsg(f"{dumps(message)}")
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 = db.get_ref(reference)
if not stored_trade:
self.log.error(f"Could not get stored trade for {reference}.")
return None
tx_obj = db.get_tx(tx)
if not tx_obj:
self.log.error(f"Could not get TX for {tx}.")
return None
platform = stored_trade["subclass"]
platform_buyer = stored_trade["buyer"]
bank_sender = tx_obj["sender"]
trade_id = stored_trade["id"]
is_updated = self.antifraud.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)
self.antifraud.add_bank_sender(platform, platform_buyer, bank_sender)
return True
elif is_updated is False:
# Already mapped
self.log.error(
f"Trade {reference} already has a TX mapped, cannot map {tx}."
)
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.
"""
reference = "".join(choices(ascii_uppercase, k=5))
reference = f"PGN-{reference}"
existing_ref = db.r.get(f"trade.{trade_id}.reference")
if not existing_ref:
to_store = {
"id": trade_id,
"tx": "",
"asset": asset,
"buyer": buyer,
"currency": currency,
"amount": amount,
"amount_crypto": amount_crypto,
"reference": reference,
"provider": provider,
"subclass": subclass,
}
self.log.info(f"Storing trade information: {str(to_store)}")
db.r.hmset(f"trade.{reference}", to_store)
db.r.set(f"trade.{trade_id}.reference", reference)
self.irc.sendmsg(f"Generated reference for {trade_id}: {reference}")
self.ux.notify.notify_new_trade(amount, currency)
uid = self.ux.verify.create_uid(subclass, buyer)
verified = self.ux.verify.get_external_user_id_status(uid)
if verified != "GREEN":
self.log.info(f"UID {uid} is not verified, sending link.")
self.antifraud.send_verification_url(subclass, uid, trade_id)
else: # User is verified
self.log.info(f"UID {uid} is verified.")
self.markets.send_bank_details(subclass, currency, trade_id)
self.markets.send_reference(subclass, trade_id, reference)
if existing_ref:
return util.convert(existing_ref)
else:
return reference
def find_trade(self, txid, currency, amount):
"""
Get a trade reference that matches the given currency and amount.
Only works if there is one result.
:param txid: Sink transaction ID
: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 = db.get_refs()
matching_refs = []
# TODO: use get_ref_map in this function instead of calling get_ref multiple times
for ref in refs:
stored_trade = db.get_ref(ref)
if stored_trade["currency"] == currency and float(
stored_trade["amount"]
) == float(amount):
matching_refs.append(stored_trade)
if len(matching_refs) != 1:
self.log.error(
f"Find trade returned multiple results for TXID {txid}: {matching_refs}"
)
return False
return matching_refs[0]

@ -1,16 +0,0 @@
twisted
redis
pyOpenSSL
Klein
ConfigObject
service_identity
forex_python
simplejson
requests
arrow
httpx
pre-commit
pycoingecko
PyOTP
opensearch
serde[ext]

@ -1,3 +0,0 @@
#!/bin/sh
pre-commit run -a
python -m unittest discover -s tests -p 'test_*.py'

@ -1,139 +0,0 @@
[App]
bindhost = 0.0.0.0
[Nordigen]
base = https://ob.nordigen.com/api/v2
id =
key =
access =
[TrueLayer]
authbase = https://auth.truelayer.com
database = https://api.truelayer.com/data/v1
id =
key =
refreshkeys = {}
maps =
tokenrefreshsec = 600
refreshsec = 10
authcode =
refreshtoken =
setuptoken = 0
callbackurl =
[Agora]
base = https://agoradesk.com/api/v1
token =
pass =
username = topmonero
refreshsec = 10
feedbackscore = 0
paymentmethoddetails = ✅ONLINE NOW 🥷NO KYC 🚀INSTANT
send = 0
cheat = 0
cheatsec = 300
ad = Hello and welcome!
Please start a trade and wait until I send the reference/note, then complete the payment using the payment details.
Instructions below are for the Revolut app, but you may send with instant SEPA or bank transfer in the UK.
Step-by-step instructions in Revolut app:
* Go to **Send** on balance page
* Click **New** in top right corner
* Select **Add a bank recipient**
* Select **Business**
* Set **Country of recipient's bank** to **"United Kingdom"**
$PAYMENT$
* Set **Company name** to **"PATHOGEN LIMITED"**
* Leave e-mail blank
* Click **Add recipient**
* If you are asked for address information, please use **24 Holborn Viaduct, London, England, EC1A 2BN**
* **IMPORTANT:** Set **Reference** to the ID provided in chat (example: **$ASSET$-XXXXX**), without this I wont know which trade to release!
* Send the payment
As soon as the money is received (around 5 seconds average) I'll release the $ASSET$! :)
paymentdetails = * Company name: **PATHOGEN LIMITED**
$PAYMENT$
* Please send in **$CURRENCY$**
* If you are asked for address information, please use **24 Holborn Viaduct, London, England, EC1A 2BN**
* The post code is **EC1A 2BN**
distlist = [["AUD", "AU"],
["BGN", "BG"],
["CAD", "CA"],
["CHF", "CH"],
["CZK", "CZ"],
["DKK", "DK"],
["GBP", "GB"],
["USD", "GB"],
["EUR", "GB"],
["USD", "US"],
["GBP", "US"],
["EUR", "US"],
["HKD", "HK"],
["HRK", "HR"],
["HUF", "HU"],
["ISK", "IS"],
["JPY", "JP"],
["MXN", "MX"],
["NOK", "NO"],
["NZD", "NZ"],
["PLN", "PL"],
["RON", "RO"],
["RUB", "RU"],
["SEK", "SE"],
["EUR", "SE"],
["SGD", "SG"],
["THB", "TH"],
["TRY", "TR"],
["ZAR", "ZA"]]
margin = 1.20
maxmargin = 1.30
minmargin = 1.10
minusdxmr = 10
maxusdxmr = 4000
minusdbtc = 10
maxusdbtc = 4000
acceptableusdmargin = 1
acceptablealtlookupusd = 160
assetlist = ["XMR", "BTC"]
providerlist = ["NATIONAL_BANK", "TRANSFERWISE", "REVOLUT"]
sleepexponent = 1.9
[IRC]
host = zm.is
port = 6697
nick = bot-dev
pass =
cert = ../keys/irc.pem
channel = #trades-dev
admins = xf/staff/m
xf/users/mkuj
highlight = m
max
prefix = ~
[DB]
redissocket = /var/run/redis/redis.sock
db = 0
[XMR]
wallet1 =
wallet2 =
[Money]
baseusd = 2800
withdrawlimit = 200
[Notify]
host = https://ntfy.sh
topic =
[ES]
host = 10.1.0.1
username = elastic
pass =
index = main
metaindex = meta
refreshsec = 300
enabled = 0

@ -1,5 +0,0 @@
# Other library imports
from ConfigObject import ConfigObject
# Load the configuration
settings = ConfigObject(filename="settings.ini")

@ -1,123 +0,0 @@
# Other library imports
# import requests
# from json import dumps
import sinks.nordigen
import sinks.truelayer
import util
from db import r
# Project imports
from settings import settings
class Sinks(util.Base):
"""
Class to manage calls to various sinks.
"""
def __init__(self):
super().__init__()
self.account_info = {}
self.currencies = []
def __irc_started__(self):
self.log.debug("IRC hook called.")
self.startup()
self.log.debug("Finished initialising subclasses.")
def all_sinks_authenticated(self): # TODO: fix
self.money.setup_loops()
def startup(self):
"""
We NEED the other libraries, and we initialise fast, so don't make
any race conditions by relying on something that might not be there.
"""
if settings.Nordigen.enabled == "1":
self.nordigen = sinks.nordigen.Nordigen(self)
if settings.TrueLayer.enabled == "1":
self.truelayer = sinks.truelayer.TrueLayer(self)
# setattr(self.truelayer, "sinks", self)
def got_transactions(self, subclass, account_id, transactions):
if not transactions:
return False
transaction_ids = [x["transaction_id"] for x in transactions]
new_key_name = f"new.transactions.{subclass}.{account_id}"
old_key_name = f"transactions.{subclass}.{account_id}"
# for transaction_id in transaction_ids:
if not transaction_ids:
return
r.sadd(new_key_name, *transaction_ids)
difference = list(r.sdiff(new_key_name, old_key_name))
difference = util.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)
for transaction in new_transactions:
transaction["subclass"] = subclass
self.tx.transaction(transaction)
def got_account_info(self, subclass, account_infos):
"""
Called when we get account information from an API provider.
:param subclass: class name that called it, truelayer, fidor, etc
:param account_infos: dict of dicts of account information
:param account_infos: dict
"""
if not account_infos:
self.log.error(f"No accounts provided for {subclass}") #
return
for bank, accounts in account_infos.items():
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)
self.log.warning(f"Potentially useless bank account: {account}")
currencies = [
account["currency"]
for bank, accounts in account_infos.items()
for account in accounts
]
for bank, accounts in account_infos.items():
self.account_info[bank] = []
for account in accounts:
self.account_info[bank].append(account)
# self.account_info = account_infos
self.currencies = currencies
# parsed_details =
# {"EUR": {"IBAN": "xxx", "BIC": "xxx"},
# "GBP": {"SORT": "04-04-04", "ACCOUNT": "1922-2993"}}
# self.markets.distribute_account_details(currencies, account_infos)
def get_total_usd(self):
"""
Get the total balance of our accounts in USD.
"""
total = 0
if settings.Nordigen.enabled == "1":
total_nordigen = self.nordigen.get_total_map()
total_nordigen_usd = self.money.multiple_to_usd(total_nordigen)
total += total_nordigen_usd
if settings.TrueLayer.enabled == "1":
total_truelayer = self.truelayer.get_total_map()
total_truelayer_usd = self.money.multiple_to_usd(total_truelayer)
total += total_truelayer_usd
return total

@ -1,488 +0,0 @@
# Twisted/Klein imports
from hashlib import sha256
from json import dumps, loads
# Other library imports
import requests
import treq
import util
from lib.serde.nordigen import (
AccessToken,
AccountBalancesRoot,
AccountDetails,
Agreement,
Institutions,
RequisitionResponse,
Requisitions,
TXRoot,
)
from serde import ValidationError
# Project imports
from settings import settings
from simplejson.errors import JSONDecodeError
from twisted.internet.defer import inlineCallbacks
from twisted.internet.task import LoopingCall
class Nordigen(util.Base):
"""
Class to manage calls to Open Banking APIs through Nordigen.
"""
def __init__(self, sinks):
super().__init__()
self.sinks = sinks
self.token = None
self.banks = {}
self.authed = False
self.requisitions = None
# Get the banks from the config and cache them
self.log.debug("Getting mapped accounts.")
self.get_mapped_accounts()
self.log.debug("Finished getting mapped accounts.")
self.log.debug("Creating loop to get access token.")
self.lc = LoopingCall(self.get_access_token)
self.lc.start(int(settings.Nordigen.TokenRefreshSec))
self.log.debug("Finished creating loops.")
def __authed__(self):
"""
Called when we have received the access token.
"""
self.log.info("Connection authenticated.")
# self.get_requisitions()
d = self.get_all_account_info()
d.addCallback(self.got_all_account_info)
self.sinks.all_sinks_authenticated()
def got_all_account_info(self, account_infos):
# Filter for added accounts since we only do that for TrueLayer
account_infos = {
bank: accounts
for bank, accounts in account_infos.items()
for account in accounts
if account["account_id"] in self.banks
}
self.sinks.got_account_info("nordigen", account_infos)
self.lc_tx = LoopingCall(self.transaction_loop)
self.lc_tx.start(int(settings.Nordigen.RefreshSec))
def transaction_loop(self):
for account_id in self.banks:
transactions = self.get_transactions(account_id)
transactions.addCallback(self.got_transactions, account_id)
def got_transactions(self, transactions, account_id):
self.sinks.got_transactions("nordigen", account_id, transactions)
def generic_deferred(self, response, dest_func):
"""
Generic function to take a treq response and fire a callback with
its content to dest_func.
:param response: a treq response
:param dest_func: function to call with the response data
"""
self.log.debug(f"Generic deferred received: {response}")
content = response.content()
content.addCallback(dest_func)
@inlineCallbacks
def get_access_token(self):
"""
Get an access token.
:return: True or False
:rtype: bool
"""
headers = {
"accept": "application/json",
"Content-Type": "application/json",
}
data = {
"secret_id": settings.Nordigen.ID,
"secret_key": settings.Nordigen.Key,
}
path = f"{settings.Nordigen.Base}/token/new/"
self.log.debug("Getting new access token.")
d = yield treq.post(path, headers=headers, data=data)
content = yield d.content()
try:
obj = AccessToken.from_json(content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()
self.token = parsed["access"]
self.log.info("Refreshed access token")
if not self.authed:
self.__authed__()
self.authed = True
def get_institutions(self, country, filter_name=None):
"""
Get a list of supported institutions.
:param country: country to query
:param filter_name: return only results with this in the name
:return: list of institutions
:rtype: list
"""
if not len(country) == 2:
return False
headers = {
"accept": "application/json",
"Authorization": f"Bearer {self.token}",
}
path = f"{settings.Nordigen.Base}/institutions/?country={country}"
r = requests.get(path, headers=headers)
try:
parsed_pre = r.json()
except JSONDecodeError:
self.log.error(f"Error parsing institutions response: {r.content}")
return False
parsed = {"institutions": parsed_pre}
try:
obj = Institutions.from_dict(parsed)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()["institutions"]
new_list = []
if filter_name:
for i in parsed:
if filter_name in i["name"]:
new_list.append(i)
return new_list
return parsed
def build_link(self, institution_id):
"""Create a link to access an institution.
:param institution_id: ID of the institution
"""
headers = {
"accept": "application/json",
"Authorization": f"Bearer {self.token}",
}
path = f"{settings.Nordigen.Base}/requisitions/"
data = {
"institution_id": institution_id,
"redirect": settings.Nordigen.CallbackURL,
}
r = requests.post(path, headers=headers, data=data)
try:
obj = Agreement.from_json(r.content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()
if "link" in parsed:
return parsed["link"]
return False
def create_auth_url(self, country, bank_name):
"""Helper to look up a bank and create a link.
:param country: country
:param bank_name: bank name string to search"""
institutions = self.get_institutions(country, filter_name=bank_name)
# We were not precise enough to have one result
if not len(institutions) == 1:
return False
institution = institutions[0]
link = self.build_link(institution["id"])
if not link:
return False
return link
@inlineCallbacks
def get_requisitions(self):
"""
Get a list of active accounts.
"""
headers = {
"accept": "application/json",
"Authorization": f"Bearer {self.token}",
}
path = f"{settings.Nordigen.Base}/requisitions"
d = yield treq.get(path, headers=headers)
content = yield d.content()
try:
obj = Requisitions.from_json(content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()
if "results" in parsed:
return parsed["results"]
else:
self.log.error(f"Results not in requisitions response: {parsed}")
return False
def delete_requisition(self, requisition_id):
"""
Delete a requisision ID.
"""
headers = {
"accept": "application/json",
"Authorization": f"Bearer {self.token}",
}
path = f"{settings.Nordigen.Base}/requisitions/{requisition_id}/"
r = requests.delete(path, headers=headers)
try:
obj = RequisitionResponse.from_json(r.content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()
return parsed
@inlineCallbacks
def get_accounts(self, requisition):
"""
Get a list of accounts for a requisition.
:param requisition: requisition ID"""
headers = {
"accept": "application/json",
"Authorization": f"Bearer {self.token}",
}
path = f"{settings.Nordigen.Base}/requisitions/{requisition}/"
d = yield treq.get(path, headers=headers)
content = yield d.content()
try:
obj = Agreement.from_json(content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()
if "accounts" in parsed:
return parsed["accounts"]
return False
def get_ownernames(self):
"""
Get list of supplementary owner names.
"""
ownernames = loads(settings.Nordigen.OwnerNames)
return ownernames
@inlineCallbacks
def get_account(self, account_id):
"""
Get details of an account.
:param requisition: requisition ID"""
headers = {
"accept": "application/json",
"Authorization": f"Bearer {self.token}",
}
path = f"{settings.Nordigen.Base}/accounts/{account_id}/details/"
d = yield treq.get(path, headers=headers)
content = yield d.content()
try:
obj = AccountDetails.from_json(content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed_obj = obj.to_dict()
if "account" not in parsed_obj:
return False
parsed = parsed_obj["account"]
if "bban" in parsed and parsed["currency"] == "GBP":
sort_code = parsed["bban"][0:6]
account_number = parsed["bban"][6:]
if "ownerName" not in parsed:
ownernames = self.get_ownernames()
if account_id in ownernames:
parsed["ownerName"] = ownernames[account_id]
self.log.info(
f"Found supplementary owner name for {account_id}: {ownernames[account_id]}"
)
else:
self.log.error(f"No owner name in parsed, cannot use: {account_id}")
return False
recipient = parsed["ownerName"]
del parsed["bban"]
if "iban" in parsed:
del parsed["iban"]
sort_code = "-".join(list(map("".join, zip(*[iter(sort_code)] * 2))))
parsed["sort_code"] = sort_code
parsed["number"] = account_number
parsed["recipient"] = recipient
# Let's add the account ID so we can reference it later
parsed["account_id"] = account_id
return parsed
def get_mapped_accounts(self):
existing_entry = loads(settings.Nordigen.Maps)
self.banks = existing_entry
@inlineCallbacks
def map_account(self, account_id): # TODO: inlineCallbacks?
"""
Map an account_id at a bank to an account_name.
This enables the account for fetching.
Data type: {"monzo": [account, ids, here],
"revolut": [account, ids, here]}
"""
account_data = yield self.get_account(account_id)
currency = account_data["currency"]
existing_entry = loads(settings.Nordigen.Maps)
if account_id in existing_entry:
return
else:
existing_entry.append(account_id)
settings.Nordigen.Maps = dumps(existing_entry)
self.banks = existing_entry
settings.write()
return currency
def unmap_account(self, account_id):
"""
Unmap an account_id at a bank to an account_name.
This disables the account for fetching.
Data type: {"monzo": [account, ids, here],
"revolut": [account, ids, here]}
"""
existing_entry = loads(settings.Nordigen.Maps)
if account_id not in existing_entry:
return
else:
existing_entry.remove(account_id)
settings.Nordigen.Maps = dumps(existing_entry)
self.banks = existing_entry
settings.write()
@inlineCallbacks
def get_all_account_info(self):
to_return = {}
requisitions = yield self.get_requisitions()
if not requisitions:
self.log.error("Could not get requisitions.")
return {}
for req in requisitions:
if not req["accounts"]:
continue
accounts = yield self.get_accounts(req["id"])
for account_id in accounts:
account_info = yield self.get_account(account_id)
if not account_info:
continue
if req["institution_id"] in to_return:
to_return[req["institution_id"]].append(account_info)
else:
to_return[req["institution_id"]] = [account_info]
return to_return
def normalise_transactions(self, transactions):
for transaction in transactions:
# Rename ID
if "transactionId" in transaction:
transaction["transaction_id"] = transaction["transactionId"]
del transaction["transactionId"]
else:
# No transaction ID. This is a problem for our implementation
tx_hash = sha256(
dumps(transaction, sort_keys=True).encode("utf8")
).hexdigest()
transaction["transaction_id"] = tx_hash
# Rename timestamp
transaction["timestamp"] = transaction["bookingDate"]
del transaction["bookingDate"]
transaction["amount"] = float(transaction["transactionAmount"]["amount"])
transaction["currency"] = transaction["transactionAmount"]["currency"]
del transaction["transactionAmount"]
transaction["reference"] = transaction["remittanceInformationUnstructured"]
del transaction["remittanceInformationUnstructured"]
@inlineCallbacks
def get_transactions(self, account_id):
"""
Get all transactions for an account.
:param account_id: account to fetch transactions for
:return: list of transactions
:rtype: dict
"""
headers = {
"accept": "application/json",
"Authorization": f"Bearer {self.token}",
}
path = f"{settings.Nordigen.Base}/accounts/{account_id}/transactions/"
d = yield treq.get(path, headers=headers)
content = yield d.content()
try:
obj = TXRoot.from_json(content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed_obj = obj.to_dict()
if "transactions" not in parsed_obj:
self.log.warning(f"No transactions for account: {account_id}")
return {}
parsed = parsed_obj["transactions"]["booked"]
self.normalise_transactions(parsed)
return parsed
def get_balance(self, account_id):
"""
Get the balance and currency of an account.
:param account_id: the account ID
:return: tuple of (currency, amount)
:rtype: tuple
"""
headers = {
"accept": "application/json",
"Authorization": f"Bearer {self.token}",
}
path = f"{settings.Nordigen.Base}/accounts/{account_id}/balances/"
r = requests.get(path, headers=headers)
try:
obj = AccountBalancesRoot.from_json(r.content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return (False, False)
parsed = obj.to_dict()
total = 0
currency = None
if "balances" not in parsed:
self.log.error(f"No balances: {parsed}")
return (False, False)
for entry in parsed["balances"]:
if currency:
if not currency == entry["balanceAmount"]["currency"]:
self.log.error("Different currencies in balance query.")
return (False, False)
if not entry["balanceType"] == "expected":
continue
total += float(entry["balanceAmount"]["amount"])
currency = entry["balanceAmount"]["currency"]
return (currency, total)
def get_total_map(self):
"""
Return a dictionary keyed by currencies with the amounts as values.
:return: dict keyed by currency, values are amounts
:rtype: dict
"""
totals = {}
for account_id in self.banks:
currency, amount = self.get_balance(account_id)
if not amount:
continue
if not currency:
continue
if currency in totals:
totals[currency] += amount
else:
totals[currency] = amount
return totals

@ -1,341 +0,0 @@
# Twisted/Klein imports
import urllib
from json import dumps, loads
from time import time
# Other library imports
import requests
import util
from lib.serde.truelayer import AccountBalancesRoot
from serde import ValidationError
# Project imports
from settings import settings
from simplejson.errors import JSONDecodeError
from twisted.internet.task import LoopingCall
class TrueLayer(util.Base):
"""
Class to manage calls to Open Banking APIs through TrueLayer.
"""
def __init__(self, sinks):
super().__init__()
self.sinks = sinks
self.tokens = {}
self.banks = {}
self.refresh_tokens = {}
self.authed = False
# Get the banks from the config and cache them
self.get_mapped_accounts()
# account we are authenticating - where to store the refresh keys
self.current_authcode_bank = None
self.lc = LoopingCall(self.get_new_tokens_all)
# self.get_new_tokens_all()
# self.get_new_token(bank)
# -> set self.tokens[bank] = access_token
self.lc.start(int(settings.TrueLayer.TokenRefreshSec))
def __authed__(self):
"""
Called when we have received all the API tokens.
"""
# Get the account information and pass it to the main function
self.log.info("All accounts authenticated: " + ", ".join(self.tokens.keys()))
account_infos = self.get_all_account_info()
self.sinks.got_account_info("truelayer", account_infos)
self.lc_tx = LoopingCall(self.transaction_loop)
self.lc_tx.start(int(settings.TrueLayer.RefreshSec))
def transaction_loop(self):
for bank in self.banks:
for account_id in self.banks[bank]:
# account_data = self.get_account(bank, account_id)
transactions = self.get_transactions(bank, account_id)
self.sinks.got_transactions("truelayer", account_id, transactions)
def add_refresh_token(self, refresh_token):
"""
Add an API key to the configuration.
Data type: {"monzo": refresh_token,
"revolut": refresh_token}
"""
account = self.current_authcode_bank
if not account:
return False
existing_entry = loads(settings.TrueLayer.RefreshKeys)
existing_entry[account] = refresh_token
settings.TrueLayer.RefreshKeys = dumps(existing_entry)
# Set the cached entry
self.refresh_tokens = existing_entry
settings.write()
# def get_refresh_tokens(self):
# existing_entry = loads(settings.TrueLayer.RefreshKeys)
# return existing_entry
def get_key(self, bank):
if bank in self.tokens:
return self.tokens[bank]
else:
return False
def create_auth_url(self, bank):
query = urllib.parse.urlencode(
{
"response_type": "code",
"response_mode": "form_post",
"client_id": settings.TrueLayer.ID,
"scope": "info accounts balance transactions offline_access",
"nonce": int(time()),
"redirect_uri": settings.TrueLayer.CallbackURL,
"enable_mock": "true",
}
)
auth_uri = f"{settings.TrueLayer.AuthBase}/?{query}&redirect_uri={settings.TrueLayer.CallbackURL}"
self.current_authcode_bank = bank
return auth_uri
def handle_authcode_received(self, authcode):
data = {
"client_id": settings.TrueLayer.ID,
"client_secret": settings.TrueLayer.Key,
"code": authcode,
"grant_type": "authorization_code",
"redirect_uri": settings.TrueLayer.CallbackURL,
}
r = requests.post(f"{settings.TrueLayer.AuthBase}/connect/token", data=data)
try:
parsed = r.json()
except JSONDecodeError:
return False
if "error" in parsed:
self.log.error("Error requesting refresh token: {parsed['error']}")
return False
# Extract the access tokens
refresh_token = parsed["refresh_token"]
access_token = parsed["access_token"]
# Add the refresh token
self.add_refresh_token(refresh_token)
# Add the access
if self.current_authcode_bank:
self.tokens[self.current_authcode_bank] = access_token
else:
self.log.error("Received an authcode we didn't ask for")
return
self.log.info(
f"Retrieved access/refresh tokens for {self.current_authcode_bank}"
)
def get_new_tokens_all(self):
refresh_tokens = loads(settings.TrueLayer.RefreshKeys)
# Set the cached entry
self.refresh_tokens = refresh_tokens
for bank in refresh_tokens:
rtrn = self.get_new_token(bank)
if not rtrn:
self.log.error(f"Error getting token for {bank}")
return
def get_new_token(self, bank):
"""
Exchange our refresh token for an access token.
:param account: account to refresh the token for
:type account:
"""
if bank not in self.refresh_tokens:
self.log.error(f"Bank {bank} not in refresh tokens")
return
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "refresh_token",
"refresh_token": self.refresh_tokens[bank],
"client_id": settings.TrueLayer.ID,
"client_secret": settings.TrueLayer.Key,
}
r = requests.post(
f"{settings.TrueLayer.AuthBase}/connect/token",
data=data,
headers=headers,
)
try:
parsed = r.json()
except JSONDecodeError:
self.log.error(f"Failed to decode JSON: {r.content}")
return False
if r.status_code == 200:
if "access_token" in parsed.keys():
self.tokens[bank] = parsed["access_token"]
# self.log.info(f"Refreshed access token for {bank}")
if (
len(self.refresh_tokens.keys()) == len(self.tokens.keys())
and not self.authed
):
# We are now fully authenticated and ready to start loops!
self.__authed__()
self.authed = True
return True
else:
self.log.error(f"Token refresh didn't contain access token: {parsed}")
return False
else:
self.log.error(f"Cannot refresh token: {parsed}")
return False
def get_accounts(self, bank):
"""
Get a list of accounts.
"""
token = self.get_key(bank)
headers = {"Authorization": f"Bearer {token}"}
path = f"{settings.TrueLayer.DataBase}/accounts"
r = requests.get(path, headers=headers)
try:
parsed = r.json()
except JSONDecodeError:
self.log.error(
"Error parsing accounts response: {content}", content=r.content
)
return False
return parsed
def _get_account(self, bank, account_id):
token = self.get_key(bank)
headers = {"Authorization": f"Bearer {token}"}
path = f"{settings.TrueLayer.DataBase}/accounts/{account_id}"
r = requests.get(path, headers=headers)
try:
parsed = r.json()
except JSONDecodeError:
self.log.error(f"Error parsing accounts response: {r.content}")
return False
return parsed
def get_mapped_accounts(self):
existing_entry = loads(settings.TrueLayer.Maps)
self.banks = existing_entry
def get_all_account_info(self):
to_return = {}
for bank in self.banks:
for account_id in self.banks[bank]:
account_data = self.get_account(bank, account_id)
if bank in to_return:
to_return[bank].append(account_data)
else:
to_return[bank] = [account_data]
return to_return
def get_account(self, bank, account_id):
account_data = self._get_account(bank, account_id)
if "results" not in account_data:
return False
if not len(account_data["results"]) == 1:
return False
if not len(account_data) == 2:
return False
if not account_data["status"] == "Succeeded":
return False
base = account_data["results"][0]
return base
def map_account(self, bank, account_id):
"""
Map an account_id at a bank to an account_name.
This enables the account for fetching.
Data type: {"monzo": [account, ids, here],
"revolut": [account, ids, here]}
"""
account_data = self.get_account(bank, account_id)
currency = account_data["currency"]
existing_entry = loads(settings.TrueLayer.Maps)
if bank in existing_entry:
if account_id not in existing_entry[bank]:
existing_entry[bank].append(account_id)
else:
existing_entry[bank] = [account_id]
settings.TrueLayer.Maps = dumps(existing_entry)
self.banks = existing_entry
settings.write()
return currency
def get_transactions(self, bank, account_id):
"""
Get a list of transactions from an account.
:param account_id: account to fetch transactions for
:return: list of transactions
:rtype: dict
"""
token = self.get_key(bank)
headers = {"Authorization": f"Bearer {token}"}
path = f"{settings.TrueLayer.DataBase}/accounts/{account_id}/transactions"
r = requests.get(path, headers=headers)
try:
parsed = r.json()
except JSONDecodeError:
self.log.error(f"Error parsing transactions response: {r.content}")
return (False, False)
if "results" in parsed:
return parsed["results"]
else:
return (False, False)
def get_balance(self, bank, account_id):
"""
Get the balance of an account.
:param bank: the bank to check
:param account_id: the account ID
:return: tuple of (currency, amount)
:rtype: tuple
"""
token = self.get_key(bank)
headers = {"Authorization": f"Bearer {token}"}
path = f"{settings.TrueLayer.DataBase}/accounts/{account_id}/balance"
r = requests.get(path, headers=headers)
try:
obj = AccountBalancesRoot.from_json(r.content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()["results"]
total = 0
currency = None
for entry in parsed:
if currency:
if not currency == entry["currency"]:
self.log.error("Different currencies in balance query.")
return
total += entry["available"]
currency = entry["currency"]
return (currency, total)
def get_total_map(self):
"""
Return a dictionary keyed by currencies with the amounts as values.
:return: dict keyed by currency, values are amounts
:rtype: dict
"""
totals = {}
for bank in self.banks:
for account_id in self.banks[bank]:
currency, amount = self.get_balance(bank, account_id)
if not amount:
continue
if currency in totals:
totals[currency] += amount
else:
totals[currency] = amount
return totals

@ -1,47 +0,0 @@
# Project imports
# from settings import settings
import sources.agora
import sources.localbitcoins
import util
class Sources(util.Base):
"""
Class to manage calls to various sources.
"""
def __init__(self):
super().__init__()
self.agora = sources.agora.Agora()
self.lbtc = sources.localbitcoins.LBTC()
def __irc_started__(self):
self.log.debug("IRC hook called.")
self.agora.setup_loop()
self.lbtc.setup_loop()
self.log.debug("Finished setting up loops.")
def __xmerged__(self):
"""
Called when xmerge has been completed in the webapp.
Merge all instances into child classes.
"""
init_map = {
"ux": self.ux,
"agora": self.agora,
"lbtc": self.lbtc,
"markets": self.markets,
"sinks": self.sinks,
"sources": self,
"tx": self.tx,
"webapp": self.webapp,
"money": self.money,
"irc": self.irc,
"notify": self.notify,
}
util.xmerge_attrs(init_map)
def get_total_wallets(self):
"""
Get the total crypto in our wallets.
"""

@ -1,136 +0,0 @@
# Twisted/Klein imports
import sources.local
# Other library imports
from pyotp import TOTP
# Project imports
from settings import settings
from twisted.internet.defer import inlineCallbacks
class Agora(sources.local.Local):
"""
AgoraDesk API handler.
"""
def __init__(self):
"""
Initialise the AgoraDesk API.
Initialise the last_dash storage for detecting new trades.
"""
self.platform = "agora"
super().__init__()
# Cache for detecting new trades
self.last_dash = set()
# Cache for detecting new messages
self.last_messages = {}
# Assets that cheat has been run on
self.cheat_run_on = []
@inlineCallbacks
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.sets.Dummy == "1":
self.log.error(
f"Running in dummy mode, not releasing funds for {contact_id}"
)
return
payload = {"tradeId": contact_id, "password": self.sets.Pass}
rtrn = yield self.api._api_call(
api_method=f"contact_release/{contact_id}",
http_method="POST",
query_values=payload,
)
# Check if we can withdraw funds
yield self.withdraw_funds()
return rtrn
# TODO: write test before re-enabling adding total_trades
@inlineCallbacks
def withdraw_funds(self):
"""
Withdraw excess funds to our XMR wallets.
"""
print("CALLING WITHDRAW FUNDS")
totals_all = yield 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}, as wallet only contains {wallet_xmr}"
)
self.irc.sendmsg(
f"Not enough funds to withdraw {profit_usd_in_xmr}, 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 = yield self.api.wallet_send_xmr(**send_cast)
send_cast["address"] = settings.XMR.Wallet2
rtrn2 = yield self.api.wallet_send_xmr(**send_cast)
self.irc.sendmsg(f"Withdrawal: {rtrn1['success']} | {rtrn2['success']}")
self.ux.notify.notify_withdrawal(half_rounded)

@ -1,719 +0,0 @@
# Twisted/Klein imports
from datetime import datetime
# Other library imports
from json import loads
from time import sleep # TODO: async
import db
import util
from lib.agoradesk_py import AgoraDesk
from lib.localbitcoins_py import LocalBitcoins
from lib.logstash import send_logstash
# Project imports
from settings import settings
from twisted.internet.defer import inlineCallbacks
from twisted.internet.task import LoopingCall
class Local(util.Base):
"""
Initialise the Local API library for LBTC and Agora.
"""
def __init__(self):
super().__init__()
if self.platform == "agora":
self.api = AgoraDesk(settings.Agora.Token)
self.sets = settings.Agora
elif self.platform == "lbtc":
self.api = LocalBitcoins(
settings.LocalBitcoins.Token, settings.LocalBitcoins.Secret
)
self.sets = settings.LocalBitcoins
else:
self.log.error("Platform not defined.")
def setup_loop(self):
"""
Set up the LoopingCall to get all active trades and messages.
"""
self.log.debug("Setting up loops.")
self.lc_dash = LoopingCall(self.loop_check)
self.lc_dash.start(int(self.sets.RefreshSec))
if settings.Agora.Cheat == "1":
self.lc_cheat = LoopingCall(self.run_cheat_in_thread)
self.lc_cheat.start(int(self.sets.CheatSec))
self.log.debug("Finished setting up loops.")
def map_provider(self, provider, reverse=False):
provider_map = {"NATIONAL_BANK": "national-bank-transfer"}
if reverse:
try:
return next(
key for key, value in provider_map.items() if value == provider
)
except StopIteration:
return False
else:
try:
return provider_map[provider]
except KeyError:
return False
@inlineCallbacks
def got_dashboard(self, dash):
dash_tmp = yield self.wrap_dashboard(dash)
self.dashboard_hook(dash_tmp)
@inlineCallbacks
def wrap_dashboard(self, dash=None): # backwards compatibility with TX
if not dash:
dash = yield self.api.dashboard()
# if dash["response"] is None:
# return False
dash_tmp = {}
if not dash:
return False
if not dash["response"]:
return False
if "data" not in dash["response"]:
# self.log.error(f"Data not in dashboard response: {dash}")
return dash_tmp
if dash["response"]["data"]["contact_count"] > 0:
for contact in dash["response"]["data"]["contact_list"]:
contact_id = contact["data"]["contact_id"]
dash_tmp[contact_id] = contact
return dash_tmp
def loop_check(self):
"""
Calls hooks to parse dashboard info and get all contact messages.
"""
d = self.api.dashboard()
d.addCallback(self.got_dashboard)
# Get recent messages
m = self.api.recent_messages()
m.addCallback(self.got_recent_messages)
@inlineCallbacks
def get_dashboard_irc(self):
"""
Get dashboard helper for IRC only.
"""
dash = yield self.wrap_dashboard()
rtrn = []
if dash is False:
return False
for contact_id, contact in dash.items():
reference = db.tx_to_ref(contact_id)
buyer = contact["data"]["buyer"]["username"]
amount = contact["data"]["amount"]
if self.platform == "agora":
asset = contact["data"]["advertisement"]["asset"]
elif self.platform == "lbtc":
asset = "BTC"
if asset == "XMR":
amount_crypto = contact["data"]["amount_xmr"]
elif asset == "BTC":
amount_crypto = contact["data"]["amount_btc"]
currency = contact["data"]["currency"]
provider = contact["data"]["advertisement"]["payment_method"]
if not contact["data"]["is_selling"]:
continue
rtrn.append(
(
f"[#] [{reference}] ({self.platform}) <{buyer}>"
f" {amount}{currency} {provider} {amount_crypto}{asset}"
)
)
return rtrn
def dashboard_hook(self, dash):
"""
Get information about our open trades.
Post new trades to IRC and cache trades for the future.
"""
current_trades = []
if not dash:
return
if not dash.items():
return
for contact_id, contact in dash.items():
reference = db.tx_to_ref(str(contact_id))
if reference:
current_trades.append(reference)
buyer = contact["data"]["buyer"]["username"]
amount = contact["data"]["amount"]
if self.platform == "agora":
asset = contact["data"]["advertisement"]["asset"]
elif self.platform == "lbtc":
asset = "BTC"
provider = contact["data"]["advertisement"]["payment_method"]
if asset == "XMR":
amount_crypto = contact["data"]["amount_xmr"]
elif asset == "BTC":
amount_crypto = contact["data"]["amount_btc"]
currency = contact["data"]["currency"]
if not contact["data"]["is_selling"]:
continue
if reference not in self.last_dash:
reference = self.tx.new_trade(
self.platform,
asset,
contact_id,
buyer,
currency,
amount,
amount_crypto,
provider,
)
if reference:
if reference not in current_trades:
current_trades.append(reference)
# Let us know there is a new trade
self.irc.sendmsg(
(
f"[#] [{reference}] ({self.platform}) <{buyer}>"
f" {amount}{currency} {provider} {amount_crypto}{asset}"
)
)
# Note that we have seen this reference
self.last_dash.add(reference)
# Purge old trades from cache
for ref in list(self.last_dash): # We're removing from the list on the fly
if ref not in current_trades:
self.last_dash.remove(ref)
if reference and reference not in current_trades:
current_trades.append(reference)
messages = db.cleanup(self.platform, current_trades)
for message in messages:
self.ux.irc.sendmsg(message)
def got_recent_messages(self, messages, send_irc=True):
"""
Get recent messages.
"""
messages_tmp = {}
if not messages:
return False
if not messages["success"]:
return False
if not messages["response"]:
return False
if "data" not in messages["response"]:
self.log.error(f"Data not in messages response: {messages['response']}")
return False
open_tx = db.get_ref_map().keys()
for message in messages["response"]["data"]["message_list"]:
contact_id = str(message["contact_id"])
username = message["sender"]["username"]
msg = message["msg"]
if contact_id not in open_tx:
continue
reference = db.tx_to_ref(contact_id)
if reference in messages_tmp:
messages_tmp[reference].append([username, msg])
else:
messages_tmp[reference] = [[username, msg]]
# Send new messages on IRC
if send_irc:
for user, message in messages_tmp[reference][::-1]:
if reference in self.last_messages:
if not [user, message] in self.last_messages[reference]:
self.irc.sendmsg(
f"[{reference}] ({self.platform}) <{user}> {message}"
)
# Append sent messages to last_messages so we don't send them again
self.last_messages[reference].append([user, message])
else:
self.last_messages[reference] = [[user, message]]
for x in messages_tmp[reference]:
self.irc.sendmsg(
f"[{reference}] ({self.platform}) <{user}> {message}"
)
# Purge old trades from cache
for ref in list(self.last_messages): # We're removing from the list on the fly
if ref not in messages_tmp:
del self.last_messages[ref]
return messages_tmp
@inlineCallbacks
def enum_ad_ids(self, page=0):
if self.platform == "lbtc" and page == 0:
page = 1
ads = yield self.api.ads(page=page)
# ads = yield self.api._api_call(api_method="ads", query_values={"page": page})
if ads is False:
return False
ads_total = []
if not ads["success"]:
return False
for ad in ads["response"]["data"]["ad_list"]:
ads_total.append(ad["data"]["ad_id"])
if "pagination" in ads["response"]:
if "next" in ads["response"]["pagination"]:
page += 1
ads_iter = yield self.enum_ad_ids(page)
if ads_iter is None:
return False
if ads_iter is False:
return False
for ad in ads_iter:
ads_total.append(ad)
return ads_total
@inlineCallbacks
def enum_ads(self, requested_asset=None, page=0):
if self.platform == "lbtc" and page == 0:
page = 1
query_values = {"page": page}
if requested_asset:
query_values["asset"] = requested_asset
# ads = yield self.api._api_call(api_method="ads", query_values=query_values)
ads = yield self.api.ads(page=page)
if ads is False:
return False
ads_total = []
if not ads["success"]:
return False
if not ads["response"]:
return False
for ad in ads["response"]["data"]["ad_list"]:
if self.platform == "agora":
asset = ad["data"]["asset"]
elif self.platform == "lbtc":
asset = "BTC"
ad_id = ad["data"]["ad_id"]
country = ad["data"]["countrycode"]
currency = ad["data"]["currency"]
provider = ad["data"]["online_provider"]
ads_total.append([asset, ad_id, country, currency, provider])
if "pagination" in ads["response"]:
if "next" in ads["response"]["pagination"]:
page += 1
ads_iter = yield self.enum_ads(requested_asset, page)
if ads_iter is None:
return False
if ads_iter is False:
return False
for ad in ads_iter:
ads_total.append([ad[0], ad[1], ad[2], ad[3], ad[4]])
return ads_total
@inlineCallbacks
def enum_public_ads(self, asset, currency, providers=None, page=0):
if self.platform == "lbtc" and page == 0:
page = 1
to_return = []
# if asset == "XMR":
# coin = "monero"
# elif asset == "BTC":
# coin = "bitcoins"
if not providers:
providers = ["NATIONAL_BANK"]
# buy-monero-online, buy-bitcoin-online
# Work around Agora weirdness calling it bitcoins
# ads = yield self.api._api_call(
# api_method=f"buy-{coin}-online/{currency}",
# query_values={"page": page},
# )
if asset == "XMR":
ads = yield self.api.buy_monero_online(currency_code=currency, page=page)
elif asset == "BTC":
ads = yield self.api.buy_bitcoins_online(currency_code=currency, page=page)
# with open("pub.json", "a") as f:
# import json
# f.write(json.dumps([page, currency, asset, ads])+"\n")
# f.close()
if ads is None:
return False
if ads is False:
return False
if ads["response"] is None:
return False
if "data" not in ads["response"]:
return False
for ad in ads["response"]["data"]["ad_list"]:
provider = ad["data"]["online_provider"]
if self.platform == "lbtc":
provider_test = self.map_provider(provider)
else:
provider_test = provider
if provider_test not in providers:
continue
date_last_seen = ad["data"]["profile"]["last_online"]
# Check if this person was seen recently
if not util.last_online_recent(date_last_seen):
continue
ad_id = str(ad["data"]["ad_id"])
username = ad["data"]["profile"]["username"]
temp_price = ad["data"]["temp_price"]
if ad["data"]["currency"] != currency:
continue
to_append = [ad_id, username, temp_price, provider, asset, currency]
if to_append not in to_return:
to_return.append(to_append)
# yield [ad_id, username, temp_price, provider, asset, currency]
if "pagination" in ads["response"]:
if "next" in ads["response"]["pagination"]:
page += 1
ads_iter = yield self.enum_public_ads(asset, currency, providers, page)
if ads_iter is None:
return False
if ads_iter is False:
return False
for ad in ads_iter:
to_append = [ad[0], ad[1], ad[2], ad[3], ad[4], ad[5]]
if to_append not in to_return:
to_return.append(to_append)
return to_return
def run_cheat_in_thread(self, assets=None):
"""
Update prices in another thread.
"""
if not assets:
all_assets = loads(self.sets.AssetList)
assets_not_run = set(all_assets) ^ set(self.cheat_run_on)
if not assets_not_run:
self.cheat_run_on = []
asset = list(all_assets).pop()
self.cheat_run_on.append(asset)
else:
asset = assets_not_run.pop()
self.cheat_run_on.append(asset)
self.update_prices([asset])
return asset
else:
# deferToThread(self.update_prices, assets)
self.update_prices(assets)
@inlineCallbacks
def update_prices(self, assets=None):
# Get all public ads for the given assets
public_ads = yield self.get_all_public_ads(assets)
if not public_ads:
return False
# Get the ads to update
to_update = self.markets.get_new_ad_equations(self.platform, public_ads, assets)
self.slow_ad_update(to_update)
@inlineCallbacks
def get_all_public_ads(self, assets=None, currencies=None, providers=None):
"""
Get all public ads for our listed currencies.
:return: dict of public ads keyed by currency
:rtype: dict
"""
public_ads = {}
crypto_map = {
"XMR": "monero",
"BTC": "bitcoin",
}
if not assets:
assets = self.markets.get_all_assets(self.platform)
# Get all currencies we have ads for, deduplicated
if not currencies:
currencies = self.markets.get_all_currencies(self.platform)
if not providers:
providers = self.markets.get_all_providers(self.platform)
sinks_currencies = self.sinks.currencies
supported_currencies = [
currency for currency in currencies if currency in sinks_currencies
]
currencies = supported_currencies
# We want to get the ads for each of these currencies and return the result
rates = self.money.cg.get_price(
ids=["monero", "bitcoin"], vs_currencies=currencies
)
for asset in assets:
for currency in currencies:
cg_asset_name = crypto_map[asset]
try:
rates[cg_asset_name][currency.lower()]
except KeyError:
self.log.debug(f"Error getting public ads for currency: {currency}")
continue
ads_list = yield self.enum_public_ads(asset, currency, providers)
if not ads_list:
self.log.debug("Error getting ads list.")
continue
ads = self.money.lookup_rates(self.platform, ads_list, rates=rates)
if not ads:
self.log.debug("Error lookup up rates.")
continue
self.log.debug("Writing to ES.")
self.write_to_es_ads("ads", ads)
if currency in public_ads:
for ad in list(ads):
if ad not in public_ads[currency]:
public_ads[currency].append(ad)
else:
public_ads[currency] = ads
return public_ads
def write_to_es_ads(self, msgtype, ads):
for ad in ads:
cast = {
"id": ad[0],
"username": ad[1],
"price": ad[2],
"provider": ad[3],
"asset": ad[4],
"currency": ad[5],
"margin": ad[6],
"ts": str(datetime.now().isoformat()),
"xtype": msgtype,
"market": self.platform,
"type": "platform",
}
if settings.ES.Enabled == "1":
self.es.index(index=settings.ES.MetaIndex, body=cast)
elif settings.Logstash.Enabled == "1":
send_logstash(cast)
@inlineCallbacks
def slow_ad_update(self, ads):
"""
Slow ad equation update utilising exponential backoff in order to guarantee all ads are updated.
:param ads: our list of ads
"""
iterations = 0
throttled = 0
assets = set()
currencies = set()
while not all([x[4] for x in ads]) or iterations == 1000:
for ad_index in range(len(ads)):
ad_id, new_formula, asset, currency, actioned = ads[ad_index]
assets.add(asset)
currencies.add(currency)
if not actioned:
rtrn = yield self.api.ad_equation(ad_id, new_formula)
if rtrn["success"]:
ads[ad_index][4] = True
throttled = 0
continue
else:
if "error_code" not in rtrn["response"]["error"]:
self.log.error(
f"Error code not in return for ad {ad_id}: {rtrn['response']}"
)
return
if rtrn["response"]["error"]["error_code"] == 429:
throttled += 1
sleep_time = pow(throttled, float(self.sets.SleepExponent))
self.log.info(
f"Throttled {throttled} times while updating {ad_id}, sleeping for {sleep_time} seconds"
)
# We're running in a thread, so this is fine
sleep(sleep_time)
self.log.error(f"Error updating ad {ad_id}: {rtrn['response']}")
continue
iterations += 1
@inlineCallbacks
def nuke_ads(self):
"""
Delete all of our adverts.
:return: True or False
:rtype: bool
"""
ads = yield self.enum_ad_ids()
return_ids = []
if ads is False:
return False
for ad_id in ads:
rtrn = yield self.api.ad_delete(ad_id)
return_ids.append(rtrn["success"])
return all(return_ids)
@inlineCallbacks
def create_ad(
self,
asset,
countrycode,
currency,
provider,
payment_details,
visible=None,
edit=False,
ad_id=None,
):
"""
Post an ad with the given asset in a country with a given currency.
Convert the min and max amounts from settings to the given currency with CurrencyRates.
:param asset: the crypto asset to list (XMR or BTC)
:type asset: string
:param countrycode: country code
:param currency: currency code
:param payment_details: the payment details
:type countrycode: string
:type currency: string
:type payment_details: dict
:return: data about created object or error
:rtype: dict
"""
if payment_details:
payment_details_text = self.markets.format_payment_details(
currency, payment_details
)
ad_text = self.markets.format_ad(asset, currency, payment_details_text)
min_amount, max_amount = self.money.get_minmax(
self.platform, asset, currency
)
if self.platform == "lbtc":
bank_name = payment_details["bank"]
if self.platform == "agora":
price_formula = (
f"coingecko{asset.lower()}usd*usd{currency.lower()}*{self.sets.Margin}"
)
elif self.platform == "lbtc":
price_formula = f"btc_in_usd*{self.sets.Margin}*USD_in_{currency}"
form = {
"country_code": countrycode,
"currency": currency,
"trade_type": "ONLINE_SELL",
# "asset": asset,
"price_equation": price_formula,
"track_max_amount": False,
"require_trusted_by_advertiser": False,
"online_provider": provider,
"require_feedback_score": int(self.sets.FeedbackScore),
}
if self.platform == "agora":
form["asset"] = asset
form["payment_method_details"] = settings.Platform.PaymentMethodDetails
form["online_provider"] = provider
elif self.platform == "lbtc":
form["online_provider"] = self.map_provider(provider, reverse=True)
if visible is False:
form["visible"] = False
elif visible is True:
form["visible"] = False
if payment_details:
form["account_info"] = payment_details_text
form["msg"] = ad_text
form["min_amount"] = round(min_amount, 2)
form["max_amount"] = round(max_amount, 2)
if self.platform == "lbtc":
form["bank_name"] = bank_name
if edit:
ad = yield self.api.ad(ad_id=ad_id, **form)
else:
ad = yield self.api.ad_create(**form)
return ad
@inlineCallbacks
def dist_countries(self, filter_asset=None):
"""
Distribute our advert into all countries and providers listed in the config.
Exits on errors.
:return: False or dict with response
:rtype: bool or dict
"""
dist_list = list(
self.markets.create_distribution_list(self.platform, filter_asset)
)
our_ads = yield self.enum_ads()
(
supported_currencies,
account_info,
) = self.markets.get_valid_account_details(self.platform)
# Let's get rid of the ad IDs and make it a tuple like dist_list
our_ads = [(x[0], x[2], x[3], x[4]) for x in our_ads]
if not our_ads:
self.log.error("Could not get our ads.")
return False
to_return = []
for asset, countrycode, currency, provider in dist_list:
if (asset, countrycode, currency, provider) not in our_ads:
if currency in supported_currencies:
# Create the actual ad and pass in all the stuff
rtrn = yield self.create_ad(
asset,
countrycode,
currency,
provider,
payment_details=account_info[currency],
)
# Bail on first error, let's not continue
if rtrn is False:
return False
to_return.append(rtrn)
return to_return
@inlineCallbacks
def redist_countries(self):
"""
Redistribute our advert details into all our listed adverts.
This will edit all ads and update the details. Only works if we have already run dist.
This will not post any new ads.
Exits on errors.
:return: False or dict with response
:rtype: bool or dict
"""
our_ads = yield self.enum_ads()
(
supported_currencies,
account_info,
) = self.markets.get_valid_account_details(self.platform)
if not our_ads:
self.log.error("Could not get our ads.")
return False
to_return = []
for asset, ad_id, countrycode, currency, provider in our_ads:
if currency in supported_currencies:
rtrn = yield self.create_ad(
asset,
countrycode,
currency,
provider,
payment_details=account_info[currency],
edit=True,
ad_id=ad_id,
)
# Bail on first error, let's not continue
if rtrn is False:
return False
to_return.append((rtrn, ad_id))
return to_return
@inlineCallbacks
def strip_duplicate_ads(self):
"""
Remove duplicate ads.
:return: list of duplicate ads
:rtype: list
"""
existing_ads = yield self.enum_ads()
_size = len(existing_ads)
repeated = []
for i in range(_size):
k = i + 1
for j in range(k, _size):
if (
existing_ads[i] == existing_ads[j]
and existing_ads[i] not in repeated
):
repeated.append(existing_ads[i])
actioned = []
for ad_id, country, currency in repeated:
rtrn = yield self.api.ad_delete(ad_id)
actioned.append(rtrn["success"])
return all(actioned)

@ -1,136 +0,0 @@
# Other library imports
import sources.local
import util
from pyotp import TOTP
# Project imports
from settings import settings
class LBTC(sources.local.Local):
"""
LocalBitcoins API handler.
"""
def __init__(self):
"""
Initialise the LocalBitcoins API.
Initialise the last_dash storage for detecting new trades.
"""
self.platform = "lbtc"
super().__init__()
# Cache for detecting new trades
self.last_dash = set()
# Cache for detecting new messages
self.last_messages = {}
# Assets that cheat has been run on
self.cheat_run_on = []
@util.handle_exceptions
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
"""
if self.sets.Dummy == "1":
self.log.error(
f"Running in dummy mode, not releasing funds for {contact_id}"
)
return
payload = {
"tradeId": contact_id,
"password": self.sets.Pass,
}
rtrn = self.api._api_call(
api_method=f"contact_release/{contact_id}",
http_method="POST",
query_values=payload,
)
# Check if we can withdraw funds
self.withdraw_funds()
return rtrn
# TODO: rewrite to handle BTC
@util.handle_exceptions
def withdraw_funds(self):
"""
Withdraw excess funds to our XMR wallets.
"""
totals_all = 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="bitcoin", vs_currencies=["USD"])
# Convert the USD total to XMR
profit_usd_in_xmr = float(profit_usd) / xmr_usd["bitcoin"]["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}, as wallet only contains {wallet_xmr}"
)
self.irc.sendmsg(
f"Not enough funds to withdraw {profit_usd_in_xmr}, 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.LocalBitcoins.Pass,
"otp": otp_code.now(),
}
send_cast["address"] = settings.XMR.Wallet1
rtrn1 = self.api.wallet_send_xmr(**send_cast)
send_cast["address"] = settings.XMR.Wallet2
rtrn2 = self.api.wallet_send_xmr(**send_cast)
self.irc.sendmsg(f"Withdrawal: {rtrn1['success']} | {rtrn2['success']}")
self.ux.notify.notify_withdrawal(half_rounded)

@ -1,284 +0,0 @@
import logging
from copy import deepcopy
from json import loads
from unittest import TestCase
from unittest.mock import MagicMock, patch
import lib.markets
import lib.money
import settings
import sources.agora
import util
from tests.common import cg_prices, expected_to_update, fake_public_ads
from twisted.internet.defer import inlineCallbacks
class TestAgora(TestCase):
def __init__(self, *args, **kwargs):
self.test_return_data = {}
with open("tests/data/agora_ads.json", "r") as f:
for line in f.readlines():
parsed = loads(line)
self.test_return_data[(parsed[2], parsed[1], str(parsed[0]))] = parsed[
3
]
super().__init__(*args, *kwargs)
def setUp(self):
logging.disable(logging.CRITICAL)
self.markets = lib.markets.Markets()
self.agora = sources.agora.Agora()
self.money = lib.money.Money()
setattr(self.agora, "markets", self.markets)
setattr(self.money, "markets", self.markets)
setattr(self.agora, "money", self.money)
self.markets.sinks = MagicMock()
self.markets.sinks.currencies = [
"AUD",
"CAD",
"CHF",
"CZK",
"DKK",
"EUR",
"GBP",
"HKD",
"HUF",
"JPY",
"MXN",
"NOK",
"NZD",
"PLN",
"RUB",
"SEK",
"SGD",
"THB",
"TRY",
"USD",
"ZAR",
]
self.agora.sinks = MagicMock()
self.agora.es = MagicMock()
self.agora.es.index = MagicMock()
self.agora.sinks.currencies = self.markets.sinks.currencies
self.all_providers = [
"XOOM",
"CRYPTOCURRENCY",
"VIPPS",
"PAYSAFECARD",
"PAYPAL",
"WU",
"SQUARE_CASH",
"CASH_DEPOSIT",
"ADVCASH",
"TRANSFERWISE",
"GIFT_CARD_CODE_GLOBAL",
"NETELLER",
"INTERNATIONAL_WIRE_SWIFT",
"CASH_BY_MAIL",
"SEPA",
"OTHER",
"REVOLUT",
"NATIONAL_BANK",
"MONEYBOOKERS",
"CREDITCARD",
"APPLE_PAY",
"ZELLE",
"PERFECT_MONEY",
"CASHIERS_CHECK",
"GOOGLEWALLET",
"STRIKE",
"SPECIFIC_BANK",
"CHIPPER_CASH",
"REMITLY",
"WORLDREMIT",
"PAYEER",
"MOBILE_TOP_UP",
"VIRTUAL_VISA_MASTERCARD",
"VANILLA",
"MONEYGRAM",
"VENMO",
"SERVE2SERVE",
"WEBMONEY",
]
def mock_enum_public_ads_api_call(self, api_method, query_values):
if "buy-monero-online" in api_method:
asset = "XMR"
elif "buy-bitcoins-online" in api_method:
asset = "BTC"
spl = api_method.split("/")
currency = spl[1]
page = str(query_values["page"])
return self.test_return_data[(asset, currency, page)]
@inlineCallbacks
def test_get_all_public_ads(self):
# Override enum_public_ads
self.agora.api._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
# Override get_price
self.agora.money.cg.get_price = MagicMock()
self.agora.money.cg.get_price.return_value = cg_prices
self.agora.markets.get_all_providers = MagicMock()
self.agora.markets.get_all_providers.return_value = self.all_providers
public_ads = yield self.agora.get_all_public_ads()
self.assertDictEqual(public_ads, fake_public_ads)
for currency, ads in public_ads.items():
ad_ids = [ad[0] for ad in ads]
ad_ids_dedup = set(ad_ids)
# Make sure there's no duplicate ads
self.assertEqual(len(ad_ids), len(ad_ids_dedup))
@patch("twisted.internet.threads.deferToThread")
def test_run_cheat_in_thread(self, defer):
asset1 = self.agora.run_cheat_in_thread()
asset2 = self.agora.run_cheat_in_thread()
self.assertEqual(set([asset1, asset2]), set(["XMR", "BTC"]))
asset3 = self.agora.run_cheat_in_thread()
asset4 = self.agora.run_cheat_in_thread()
self.assertEqual(set([asset3, asset4]), set(["XMR", "BTC"]))
self.assertNotEqual(asset1, asset2)
self.assertNotEqual(asset3, asset4)
@inlineCallbacks
def test_update_prices(self):
# Override the providers
settings.settings.Agora.MinMargin = 1.17
settings.settings.Agora.MaxMargin = 1.3
settings.settings.Agora.ProviderList = '["REVOLUT", "NATIONAL_BANK"]'
# Override enum_public_ads
self.agora.api._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
# Override get_price
self.agora.money.cg.get_price = MagicMock()
self.agora.money.cg.get_price.return_value = cg_prices
self.agora.slow_ad_update = MagicMock()
yield self.agora.update_prices()
call_args = self.agora.slow_ad_update.call_args_list[0][0][0]
self.assertCountEqual(call_args, expected_to_update)
@inlineCallbacks
def test_enum_public_ads(self):
# Override enum_public_ads
self.agora.api._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
enum_ads_return = yield self.agora.enum_public_ads(
"XMR", "USD", self.all_providers
)
# Ensure there are no duplicates
enum_ads_return_ids = [
(x[0], x[1], x[2], x[3], x[4], x[5]) for x in enum_ads_return
]
enum_ads_return_ids_dedup = set(enum_ads_return_ids)
self.assertEqual(len(enum_ads_return_ids), len(enum_ads_return_ids_dedup))
expected_return = []
# ['94b399e2-2c96-480c-b399-e22c96180cf2', 'Jorge', '272.00', 'SEPA', 'XMR', 'USD']
for asset, currency, page in self.test_return_data:
if not asset == "XMR":
continue
if not currency == "USD":
continue
content = self.test_return_data[(asset, currency, page)]
ads = content["response"]["data"]["ad_list"]
for ad in ads:
ad_id = ad["data"]["ad_id"]
username = ad["data"]["profile"]["username"]
temp_price = ad["data"]["temp_price"]
provider = ad["data"]["online_provider"]
asset = "XMR"
currency = ad["data"]["currency"]
to_append = [
ad_id,
username,
temp_price,
provider,
asset,
currency,
]
if to_append not in expected_return:
expected_return.append(to_append)
self.assertCountEqual(enum_ads_return, expected_return)
self.assertNotEqual(enum_ads_return[0][0], enum_ads_return[1][0])
ad_ids = [x[0] for x in enum_ads_return]
ad_ids_dedup = set(ad_ids)
self.assertEqual(len(ad_ids), len(ad_ids_dedup))
def test_lookup_rates(self):
# Override enum_public_ads
self.agora.api._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
# Override get_price
self.money.cg.get_price = MagicMock()
self.money.cg.get_price.return_value = cg_prices
enum_ads_return = self.agora.enum_public_ads("XMR", "USD", self.all_providers)
expected_return = []
# Let's manually calculate what it's supposed to look like
price_xmr = cg_prices["monero"]["usd"]
for ad in deepcopy(enum_ads_return):
price = float(ad[2])
margin = round(price / price_xmr, 2)
ad.append(margin)
expected_return.append(ad)
lookup_rates_return = self.agora.money.lookup_rates(
"agora", enum_ads_return
) # TODO: do this properly
self.assertCountEqual(lookup_rates_return, expected_return)
def test_lookup_rates_not_usd(self):
"""
Above test only tests USD which does not take into account Forex.
Let's test both, and additionaly specify our own rates.
"""
# Override enum_public_ads
self.agora.api._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
# Override get_price
self.agora.money.cg.get_price = MagicMock()
self.agora.money.cg.get_price.return_value = cg_prices
enum_ads_return = self.agora.enum_public_ads("XMR", "EUR", self.all_providers)
expected_return = []
# Let's manually calculate what it's supposed to look like
price_xmr = cg_prices["monero"]["eur"]
for ad in deepcopy(enum_ads_return):
price = float(ad[2])
margin = round(price / price_xmr, 2)
ad.append(margin)
expected_return.append(ad)
# Test specifying rates=
lookup_rates_return = self.agora.money.lookup_rates(
"agora", enum_ads_return, rates=cg_prices
)
self.assertCountEqual(lookup_rates_return, expected_return)

@ -1,228 +0,0 @@
import logging
from copy import deepcopy
from json import loads
from unittest import TestCase
from unittest.mock import MagicMock, patch
import lib.markets
import lib.money
import settings
import sources
import sources.localbitcoins
import util
from tests.common import cg_prices, expected_to_update_lbtc, fake_public_ads_lbtc
from twisted.internet.defer import inlineCallbacks
class TestLBTC(TestCase):
def __init__(self, *args, **kwargs):
self.test_return_data = {}
with open("tests/data/lbtc_ads.json", "r") as f:
for line in f.readlines():
parsed = loads(line)
self.test_return_data[(parsed[2], parsed[1], str(parsed[0]))] = parsed[
3
]
super().__init__(*args, *kwargs)
def setUp(self):
logging.disable(logging.CRITICAL)
self.markets = lib.markets.Markets()
self.lbtc = sources.localbitcoins.LBTC()
self.money = lib.money.Money()
self.sources = sources.Sources()
setattr(self.markets, "sources", self.sources)
setattr(self.lbtc, "markets", self.markets)
setattr(self.money, "markets", self.markets)
setattr(self.lbtc, "money", self.money)
self.markets.sinks = MagicMock()
self.markets.sinks.currencies = [
"GBP",
]
self.lbtc.sinks = MagicMock()
self.lbtc.es = MagicMock()
self.lbtc.es.index = MagicMock()
self.lbtc.sinks.currencies = self.markets.sinks.currencies
self.all_providers = [
"national-bank-transfer",
]
def mock_enum_public_ads_api_call(self, api_method, query_values):
if "buy-bitcoins-online" in api_method:
asset = "BTC"
spl = api_method.split("/")
currency = spl[1]
page = str(query_values["page"])
return self.test_return_data[(asset, currency, page)]
def test_get_all_public_ads(self):
# Override enum_public_ads
self.lbtc.api._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
# Override get_price
self.lbtc.money.cg.get_price = MagicMock()
self.lbtc.money.cg.get_price.return_value = cg_prices
self.lbtc.markets.get_all_providers = MagicMock()
self.lbtc.markets.get_all_providers.return_value = self.all_providers
public_ads = yield self.lbtc.get_all_public_ads()
self.assertDictEqual(public_ads, fake_public_ads_lbtc)
for currency, ads in public_ads.items():
ad_ids = [ad[0] for ad in ads]
ad_ids_dedup = set(ad_ids)
# Make sure there's no duplicate ads
self.assertEqual(len(ad_ids), len(ad_ids_dedup))
@patch("twisted.internet.threads.deferToThread")
def test_run_cheat_in_thread(self, defer):
asset1 = self.lbtc.run_cheat_in_thread()
asset2 = self.lbtc.run_cheat_in_thread()
self.assertEqual(set([asset1, asset2]), set(["BTC"]))
asset3 = self.lbtc.run_cheat_in_thread()
asset4 = self.lbtc.run_cheat_in_thread()
self.assertEqual(set([asset3, asset4]), set(["BTC"]))
# Only one asset so far for BTC
# self.assertNotEqual(asset1, asset2)
# self.assertNotEqual(asset3, asset4)
@inlineCallbacks
def test_update_prices(self):
settings.settings.LocalBitcoins.MinMargin = 1.10
settings.settings.LocalBitcoins.MaxMargin = 1.3
settings.settings.LocalBitcoins.Username = "Harrey"
# Override the providers
settings.settings.LocalBitcoins.ProviderList = '["national-bank-transfer"]'
# Override enum_public_ads
self.lbtc.api._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
# Override get_price
self.lbtc.money.cg.get_price = MagicMock()
self.lbtc.money.cg.get_price.return_value = cg_prices
self.lbtc.slow_ad_update = MagicMock()
yield self.lbtc.update_prices()
call_args = self.lbtc.slow_ad_update.call_args_list[0][0][0]
self.assertCountEqual(call_args, expected_to_update_lbtc)
@inlineCallbacks
def test_enum_public_ads(self):
# Override enum_public_ads
self.lbtc.api._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
enum_ads_return = yield self.lbtc.enum_public_ads(
"BTC", "GBP", self.all_providers
)
# Ensure there are no duplicates
enum_ads_return_ids = [
(x[0], x[1], x[2], x[3], x[4], x[5]) for x in enum_ads_return
]
enum_ads_return_ids_dedup = set(enum_ads_return_ids)
self.assertEqual(len(enum_ads_return_ids), len(enum_ads_return_ids_dedup))
expected_return = []
# ['94b399e2-2c96-480c-b399-e22c96180cf2', 'Jorge', '272.00', 'SEPA', 'XMR', 'USD']
for asset, currency, page in self.test_return_data:
if not asset == "BTC":
continue
if not currency == "GBP":
continue
content = self.test_return_data[(asset, currency, page)]
ads = content["response"]["data"]["ad_list"]
for ad in ads:
ad_id = str(ad["data"]["ad_id"])
username = ad["data"]["profile"]["username"]
temp_price = ad["data"]["temp_price"]
provider = ad["data"]["online_provider"]
asset = "BTC"
currency = ad["data"]["currency"]
to_append = [
ad_id,
username,
temp_price,
provider,
asset,
currency,
]
if to_append not in expected_return:
expected_return.append(to_append)
self.assertCountEqual(enum_ads_return, expected_return)
self.assertNotEqual(enum_ads_return[0][0], enum_ads_return[1][0])
ad_ids = [x[0] for x in enum_ads_return]
ad_ids_dedup = set(ad_ids)
self.assertEqual(len(ad_ids), len(ad_ids_dedup))
def test_lookup_rates(self):
# Override enum_public_ads
self.lbtc.api._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
# Override get_price
self.money.cg.get_price = MagicMock()
self.money.cg.get_price.return_value = cg_prices
enum_ads_return = self.lbtc.enum_public_ads("BTC", "GBP", self.all_providers)
expected_return = []
# Let's manually calculate what it's supposed to look like
price_btc = cg_prices["bitcoin"]["gbp"]
for ad in deepcopy(enum_ads_return):
price = float(ad[2])
margin = round(price / price_btc, 2)
ad.append(margin)
expected_return.append(ad)
lookup_rates_return = self.lbtc.money.lookup_rates(
"lbtc", enum_ads_return
) # TODO: do this properly
self.assertCountEqual(lookup_rates_return, expected_return)
def test_lookup_rates_not_usd(self):
"""
Above test only tests USD which does not take into account Forex.
Let's test both, and additionaly specify our own rates.
"""
# Override enum_public_ads
self.lbtc.api._api_call = self.mock_enum_public_ads_api_call
util.last_online_recent = MagicMock()
util.last_online_recent.return_value = True
# Override get_price
self.lbtc.money.cg.get_price = MagicMock()
self.lbtc.money.cg.get_price.return_value = cg_prices
enum_ads_return = self.lbtc.enum_public_ads("BTC", "GBP", self.all_providers)
expected_return = []
# Let's manually calculate what it's supposed to look like
price_btc = cg_prices["bitcoin"]["gbp"]
for ad in deepcopy(enum_ads_return):
price = float(ad[2])
margin = round(price / price_btc, 2)
ad.append(margin)
expected_return.append(ad)
# Test specifying rates=
lookup_rates_return = self.lbtc.money.lookup_rates(
"lbtc", enum_ads_return, rates=cg_prices
)
self.assertCountEqual(lookup_rates_return, expected_return)

@ -1,152 +0,0 @@
import logging
from unittest import TestCase
from unittest.mock import MagicMock
import lib.markets
import settings
from sources.agora import Agora
from tests.common import expected_to_update, fake_public_ads
class TestMarkets(TestCase):
def setUp(self):
logging.disable(logging.CRITICAL)
self.markets = lib.markets.Markets()
self.agora = Agora()
self.markets.sinks = MagicMock()
self.markets.sinks.currencies = [
"AUD",
"CAD",
"CHF",
"CZK",
"DKK",
"EUR",
"GBP",
"HKD",
"HUF",
"JPY",
"MXN",
"NOK",
"NZD",
"PLN",
"RUB",
"SEK",
"SGD",
"THB",
"TRY",
"USD",
"ZAR",
]
def test_autoprice(self):
ads = [
[
"2b6dba4d-c9db-48f2-adba-4dc9dba8f2a0",
"Xpoterlolipop",
"182.80",
"NATIONAL_BANK",
"XMR",
"USD",
1.18,
],
[
"57e3e8d6-45fe-40da-a3e8-d645fe20da46",
"SecureMole",
"183.26",
"NATIONAL_BANK",
"XMR",
"USD",
1.19,
],
[
"87af6467-be02-476e-af64-67be02676e9a",
"topmonero",
"183.42",
"NATIONAL_BANK",
"XMR",
"USD",
1.19,
],
[
"65b452e3-a29f-4233-b452-e3a29fe23369",
"topmonero",
"183.42",
"NATIONAL_BANK",
"XMR",
"USD",
1.19,
],
[
"d2c6645c-6d56-4094-8664-5c6d5640941b",
"topmonero",
"183.42",
"NATIONAL_BANK",
"XMR",
"USD",
1.19,
],
]
currency = "EUR"
margin = self.markets.autoprice("topmonero", 1.1, 1.3, ads, currency)
expected_margin = 1.18
self.assertEqual(margin, expected_margin)
def test_get_new_ad_equation(self):
self.maxDiff = None
settings.settings.Agora.MinMargin = 1.17
settings.settings.Agora.MaxMargin = 1.3
# 437 should be 1.3 but is 1.21
to_update = self.markets.get_new_ad_equations("agora", fake_public_ads)
self.assertCountEqual(to_update, expected_to_update)
res_xmr = self.markets.get_new_ad_equations("agora", fake_public_ads, ["XMR"])
expected_xmr_to_update = [x for x in expected_to_update if x[2] == "XMR"]
self.assertCountEqual(res_xmr, expected_xmr_to_update)
res_btc = self.markets.get_new_ad_equations("agora", fake_public_ads, ["BTC"])
expected_btc_to_update = [x for x in expected_to_update if x[2] == "BTC"]
self.assertCountEqual(res_btc, expected_btc_to_update)
res_both = self.markets.get_new_ad_equations(
"agora", fake_public_ads, ["XMR", "BTC"]
)
self.assertCountEqual(res_both, expected_to_update)
def test_format_ad(self):
settings.settings.Platform.Ad = """* Set **Country of recipient's bank** to **"United Kingdom"**
$PAYMENT$
* Set **Company name** to **"PATHOGEN LIMITED"**"""
payment_details = {
"sort_code": "02-03-04",
"account_number": "0023-0045",
}
payment_details_text = self.markets.format_payment_details(
"GBP", payment_details
)
ad_text = self.markets.format_ad("XMR", "GBP", payment_details_text)
expected = """* Set **Country of recipient's bank** to **"United Kingdom"**
Payment details will be released after verification has passed.
If you've already completed verification, they will be sent immediately.
* Set **Company name** to **"PATHOGEN LIMITED"**"""
self.assertEqual(ad_text, expected)
def test_format_payment_details(self):
payment_details = {
"sort_code": "02-03-04",
"account_number": "0023-0045",
}
payment_details_text = self.markets.format_payment_details(
"GBP", payment_details
)
expected = """Payment details will be released after verification has passed.
If you've already completed verification, they will be sent immediately."""
self.assertEqual(payment_details_text, expected)
expected_real = """* Sort code: **02-03-04**
* Account number: **0023-0045**
Please send in GBP."""
payment_details_text_real = self.markets.format_payment_details(
"GBP", payment_details, real=True
)
self.assertEqual(payment_details_text_real, expected_real)

@ -1,26 +0,0 @@
import logging
from unittest import TestCase
import lib.money
class TestMoney(TestCase):
def setUp(self):
logging.disable(logging.CRITICAL)
self.money = lib.money.Money()
def test_lookup_rates(self):
# Move from Agora tests
pass
def test_get_rates_all(self):
pass
def test_get_acceptable_margins(self):
pass
def test_to_usd(self):
pass
def test_get_profit(self):
pass

@ -1,294 +0,0 @@
import logging
from copy import deepcopy
from unittest import TestCase
from unittest.mock import MagicMock
import lib.antifraud
import lib.money
import lib.transactions
class TestTransactions(TestCase):
def setUp(self):
logging.disable(logging.CRITICAL)
self.transactions = lib.transactions.Transactions()
self.test_data = {
"timestamp": "2022-03-14T19:34:13.501Z",
"description": "Received Rebiere Matthieu",
"transaction_type": "CREDIT",
"transaction_category": "CREDIT",
"transaction_classification": [],
"amount": 1,
"currency": "GBP",
"transaction_id": "ec4df5248c750c30301a1da71024ac0b",
"provider_transaction_id": "27373011.TU9ORVRBUllfQUNUSVZJVFk6OjI1MDE4MDQyOjpUUkFOU0ZFUjo6MzgwMDM2NDY2",
"normalised_provider_transaction_id": "txn-c8c12c308789bd980",
"meta": {
"provider_reference": "TEST-1",
"transaction_type": "Credit",
"provider_id": "27373011.TU9ORVRBUllfQUNUSVZJVFk6OjI1MDE4MDQyOjpUUkFOU0ZFUjo6MzgwMDM2NDY2",
"counter_party_preferred_name": "Rebiere Matthieu",
},
"subclass": "truelayer",
}
# Mock redis calls
lib.transactions.db.r.hgetall = self.mock_hgetall
lib.transactions.db.r.hmset = self.mock_hmset
lib.transactions.db.r.keys = self.mock_keys
lib.transactions.db.r.get = self.mock_get
# Mock some callbacks
self.transactions.irc = MagicMock()
self.transactions.irc.sendmsg = MagicMock()
self.transactions.release_funds = MagicMock()
self.transactions.ux = MagicMock()
self.transactions.ux.notify = MagicMock()
self.transactions.ux.notify.notify_complete_trade = MagicMock()
self.transactions.antifraud = lib.antifraud.AntiFraud()
# Mock the rates
self.transactions.money = MagicMock()
self.transactions.money.to_usd = self.mock_to_usd
self.transactions.money.get_rates_all = MagicMock()
self.transactions.money.get_rates_all.return_value = {"GBP": 0.8}
# Don't mock the functions we want to test
self.money = lib.money.Money()
self.money.get_rates_all = MagicMock()
self.money.get_rates_all.return_value = {"GBP": 0.8}
self.transactions.money.get_acceptable_margins = (
self.money.get_acceptable_margins
)
self.trades = {
1: {
"id": "uuid1",
"buyer": "test_buyer_1",
"currency": "GBP",
"amount": "1",
"amount_xmr": "0.3",
"reference": "TEST-1",
"subclass": "agora",
},
2: {
"id": "uuid2",
"buyer": "test_buyer_2",
"currency": "GBP",
"amount": "1",
"amount_xmr": "0.3",
"reference": "TEST-2",
"subclass": "agora",
},
3: {
"id": "uuid3",
"buyer": "test_buyer_3",
"currency": "GBP",
"amount": "1000",
"amount_xmr": "3",
"reference": "TEST-3",
"subclass": "agora",
},
4: {
"id": "uuid4",
"buyer": "test_buyer_4",
"currency": "GBP",
"amount": "10",
"amount_xmr": "0.5",
"reference": "TEST-4",
"subclass": "agora",
},
}
self.return_trades = [1, 2, 3]
@property
def test_data_copy(self):
return deepcopy(self.test_data)
def data_custom(self, amount, currency, reference):
test_data = self.test_data_copy
test_data["meta"]["provider_reference"] = reference
test_data["amount"] = amount
test_data["currency"] = currency
return test_data
def mock_hgetall(self, string):
ref = string.split(".")[1]
for num, trade in self.trades.items():
if trade["reference"] == ref:
return trade
def mock_hmset(self, string, data):
pass
def mock_keys(self, string):
return [v["id"] for k, v in self.trades.items() if k in self.return_trades]
def mock_get(self, string):
for num, trade in self.trades.items():
if trade["id"] == string:
return trade["reference"]
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
def test_transaction(self):
self.transactions.transaction(self.test_data)
self.transactions.release_funds.assert_called_once_with("uuid1", "TEST-1")
self.transactions.release_funds = MagicMock()
ref_2 = self.test_data_copy
ref_2["meta"]["provider_reference"] = "TEST-2"
self.transactions.transaction(ref_2)
self.transactions.release_funds.assert_called_once_with("uuid2", "TEST-2")
def test_transaction_invalid(self):
invalid_data = self.test_data_copy
invalid_data = self.data_custom(2000, "SEK", "sss")
self.transactions.transaction(invalid_data)
self.transactions.release_funds.assert_not_called()
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()
def test_transaction_no_reference_fail(self):
no_reference_fail = self.data_custom(1, "GBP", "none")
no_reference_fail["meta"]["provider_reference"] = "none"
self.transactions.transaction(no_reference_fail)
self.transactions.release_funds.assert_not_called()
def test_transaction_no_reference_pass(self):
no_reference_pass = self.data_custom(1, "GBP", "none")
no_reference_pass["meta"]["provider_reference"] = "THIS_ONE_FAILS"
self.return_trades = [1]
self.transactions.transaction(no_reference_pass)
self.transactions.release_funds.assert_called_with("uuid1", "TEST-1")
def test_transaction_large(self):
exceeds_max = self.data_custom(1000, "GBP", "TEST-3")
self.transactions.transaction(exceeds_max)
self.transactions.release_funds.assert_called_once_with("uuid3", "TEST-3")
def test_transaction_no_reference_exceeds_max(self):
exceeds_max = self.data_custom(1000, "GBP", "noref")
self.transactions.transaction(exceeds_max)
self.transactions.release_funds.assert_not_called()
def test_transaction_wrong_currency(self):
wrong_currency = self.data_custom(1, "EUR", "TEST-1")
self.transactions.transaction(wrong_currency)
self.transactions.release_funds.assert_not_called()
wrong_currency = self.data_custom(1, "EUR", "none")
self.transactions.transaction(wrong_currency)
self.transactions.release_funds.assert_not_called()
def test_transaction_wrong_amount(self):
self.transactions.money.get_acceptable_margins = MagicMock()
self.transactions.money.get_acceptable_margins.return_value = (0.8, 1.8)
wrong_amount = self.data_custom(10, "GBP", "TEST-1")
self.transactions.transaction(wrong_amount)
self.transactions.release_funds.assert_not_called()
wrong_amount = self.data_custom(10, "GBP", "none")
self.transactions.transaction(wrong_amount)
self.transactions.release_funds.assert_not_called()
# 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()
def test_transaction_too_low(self):
too_low = self.data_custom(5, "GBP", "TEST-1")
self.transactions.transaction(too_low)
self.transactions.release_funds.assert_not_called()
def test_transaction_too_high(self):
too_high = self.data_custom(15, "GBP", "TEST-1")
self.transactions.transaction(too_high)
self.transactions.release_funds.assert_not_called()
# def test_transaction_pending_then_completed(self):
# pass
def test_transaction_store_incomplete_trade(self):
pass
def test_transaction_release_incomplete_trade(self):
pass
def test_transaction_card_payment(self):
pass
def test_transaction_negative_amount(self):
pass
def test_release_funds(self):
pass
def test_new_trade(self):
pass
def test_find_trade(self):
pass
def test_get_refs(self):
pass
def test_get_ref_map(self):
pass
def test_get_ref(self):
pass
def test_del_ref(self):
pass
def test_cleanup(self):
pass
def test_tx_to_ref(self):
pass
def test_ref_to_tx(self):
pass
def test_get_total_usd(self):
pass
def test_get_total(self):
pass
def test_write_to_es(self):
pass
def test_get_remaining(self):
pass
def test_get_open_trades_usd(self):
pass
def test_get_total_remaining(self):
pass
def get_total_with_trades(self):
pass

@ -1,179 +0,0 @@
# Other library imports
import logging
from datetime import datetime, timezone
from httpx import ReadError, ReadTimeout, RemoteProtocolError
# Project imports
from settings import settings
log = logging.getLogger("util")
debug = False
# Color definitions
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
COLORS = {
"WARNING": YELLOW,
"INFO": WHITE,
"DEBUG": BLUE,
"CRITICAL": YELLOW,
"ERROR": RED,
}
RESET_SEQ = "\033[0m"
COLOR_SEQ = "\033[1;%dm"
BOLD_SEQ = "\033[1m"
def formatter_message(message, use_color=True):
if use_color:
message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ)
else:
message = message.replace("$RESET", "").replace("$BOLD", "")
return message
class ColoredFormatter(logging.Formatter):
def __init__(self, msg, use_color=True):
logging.Formatter.__init__(self, msg)
self.use_color = use_color
def format(self, record):
levelname = record.levelname
if self.use_color and levelname in COLORS:
levelname_color = (
COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ
)
record.levelname = levelname_color
return logging.Formatter.format(self, record)
def get_logger(name):
# Define the logging format
FORMAT = "%(asctime)s %(levelname)18s $BOLD%(name)13s$RESET - %(message)s"
COLOR_FORMAT = formatter_message(FORMAT, True)
color_formatter = ColoredFormatter(COLOR_FORMAT)
# formatter = logging.Formatter(
# Why is this so complicated?
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# ch.setFormatter(formatter)
ch.setFormatter(color_formatter)
# Define the logger on the base class
log = logging.getLogger(name)
if debug:
log.setLevel(logging.DEBUG)
# Add the handler and stop it being silly and printing everything twice
log.addHandler(ch)
log.propagate = False
return log
class Base(object):
def __init__(self):
name = self.__class__.__name__
# Set up all the logging stuff
self.log = get_logger(name)
self.log.info("Class initialised")
def xmerge_attrs(init_map):
"""
Given a dictionary of strings and classes, set all corresponding class.<string> attributes
on each class, to every other class.
"a": A(), "b": B() -> A.b = B_instance, B.a = A_instance
:param init_map: dict of class names to classes
"""
for classname, object_instance in init_map.items():
# notify, Notify
for classname_inside, object_instance_inside in init_map.items():
if not classname == classname_inside:
# irc, bot
setattr(object_instance, classname_inside, object_instance_inside)
def convert(data):
"""
Recursively convert a dictionary.
"""
if isinstance(data, bytes):
return data.decode("ascii")
if isinstance(data, dict):
return dict(map(convert, data.items()))
if isinstance(data, tuple):
return map(convert, data)
if isinstance(data, list):
return list(map(convert, data))
return data
def last_online_recent(date):
"""
Check if the last online date was recent.
:param date: date last online
:type date: string
:return: bool indicating whether the date was recent enough
:rtype: bool
"""
if "+" in date:
# for LBTC
# 2022-04-16T08:53:58+00:00
date_split = date.split("+")
date_split[1].replace(".", "")
date_split[1].replace(":", "")
date = "+".join(date_split)
date_string = "%Y-%m-%dT%H:%M:%S%z"
now = datetime.now(timezone.utc)
else:
date_string = "%Y-%m-%dT%H:%M:%S.%fZ"
now = datetime.now()
date_parsed = datetime.strptime(date, date_string)
sec_ago_date = (now - date_parsed).total_seconds()
return sec_ago_date < 172800
def handle_exceptions(func):
"""
Wrapper helper to handle Agora API errors.
:param func: function to wrap
:rtype: func
:return: the wrapped function
"""
def inner_function(*args, **kwargs):
"""
Inner wrapper helper.
:rtype: any or bool
:return: False or the normal return
"""
try:
rtrn = func(*args, **kwargs)
except (ReadTimeout, ReadError, RemoteProtocolError):
return False
if isinstance(rtrn, dict):
if "success" in rtrn:
if "message" in rtrn:
if not rtrn["success"] and rtrn["message"] == "API ERROR":
if "error_code" in rtrn["response"]["error"]:
code = rtrn["response"]["error"]["error_code"]
if not code == 136:
log.error(f"API error: {code}")
return False
else:
log.error(f"API error: {rtrn['response']['error']}")
return False
return rtrn
return inner_function
def get_settings(platform):
if platform == "agora":
return settings.Agora
elif platform == "lbtc":
return settings.LocalBitcoins

@ -1,44 +0,0 @@
# Other library imports
# import requests
# from json import dumps
# Project imports
# from settings import settings
import util
import ux.commands
import ux.irc
import ux.notify
import ux.verify
class UX(object):
"""
Class to manage calls to various user interfaces.
"""
def __init__(self):
super().__init__()
self.irc = ux.irc.bot()
self.notify = ux.notify.Notify()
self.verify = ux.verify.Verify()
def __xmerged__(self):
"""
Called when xmerge has been completed in the webapp.
Merge all instances into child classes.
"""
init_map = {
"ux": self,
"markets": self.markets,
"sinks": self.sinks,
"sources": self.sources,
"tx": self.tx,
"webapp": self.webapp,
"money": self.money,
"irc": self.irc,
"notify": self.notify,
"verify": self.verify,
"antifraud": self.antifraud,
}
util.xmerge_attrs(init_map)

File diff suppressed because it is too large Load Diff

@ -1,248 +0,0 @@
# Twisted/Klein imports
import util
# Project imports
from settings import settings
from twisted.internet import protocol, reactor, ssl
from twisted.words.protocols import irc
from ux.commands import IRCCommands
class IRCBot(irc.IRCClient):
def __init__(self, log):
"""
Initialise IRC bot.
:param log: logger instance
:type log: Logger
"""
self.log = log
self.cmd = IRCCommands()
# Parse the commands into "commandname": "commandclass"
self.cmdhash = {
getattr(self.cmd, x).name: x for x in dir(self.cmd) if not x.startswith("_")
}
self.nickname = settings.IRC.Nick
self.password = settings.IRC.Pass
self.realname = self.nickname
self.username = self.nickname
# Don't give away information about our client
self.userinfo = None
self.fingerReply = None
self.versionName = None
self.sourceURL = None
self.lineRate = None # Don't throttle messages, we may need to send a lot
self.prefix = settings.IRC.Prefix
self.admins = (settings.IRC.Admins).split("\n")
self.highlight = (settings.IRC.Highlight).split("\n")
self.channel = settings.IRC.Channel
def parse(self, user, host, channel, msg):
"""
Simple handler for IRC commands.
:param user: full user string with host
:param host: user's hostname
:param channel: channel the message was received on
:param msg: the message
:type user: string
:type host: string
:type channel: string
:type msg: string
"""
spl = msg.split()
# nick = user.split("!")[0]
cmd = spl[0]
length = len(spl)
# Check if user is authenticated
authed = host in self.admins
if cmd == "help" and length == 2 and authed:
if spl[1] in self.cmdhash:
cmdname = self.cmdhash[spl[1]]
obj = getattr(self.cmd, cmdname)
helptext = getattr(obj, "helptext")
self.msg(channel, helptext)
return
else:
self.msg(channel, f"No such command: {spl[1]}")
return
if cmd == "helpall" and authed:
for command in self.cmdhash:
cmdname = self.cmdhash[command]
obj = getattr(self.cmd, cmdname)
helptext = getattr(obj, "helptext")
self.msg(channel, f"{cmdname}: {helptext}")
return
if cmd in self.cmdhash:
# Get the class name of the referenced command
cmdname = self.cmdhash[cmd]
# Get the class name
obj = getattr(self.cmd, cmdname)
def msgl(x):
self.msg(channel, x)
# Check if the command required authentication
if obj.authed:
if host in self.admins:
obj.run(
cmd,
spl,
length,
authed,
msgl,
self.agora,
self.tx,
self.ux,
)
else:
# Handle authentication here instead of in the command module for security
self.msg(channel, "Access denied.")
else:
# Run an unauthenticated command, without passing through secure library calls
obj.run(cmd, spl, len(spl), authed, msgl)
return
self.msg(channel, "Command not found.")
if authed:
# Give user command hints if they are authenticated
self.msg(channel, f"Commands loaded: {', '.join(self.cmdhash.keys())}")
def signedOn(self):
"""
Called when we have signed on to IRC.
Join our channel.
"""
self.log.info(f"Signed on as {self.nickname}")
self.join(self.channel)
def joined(self, channel):
"""
Called when we have joined a channel.
Setup the Agora LoopingCall to get trades.
This is here to ensure the IRC client is initialised enough to send the trades.
:param channel: channel we joined
:type channel: string
"""
self.sinks.__irc_started__()
self.sources.__irc_started__()
self.log.info(f"Joined channel {channel}")
def privmsg(self, user, channel, msg):
"""
Called on received PRIVMSGs.
Pass through identified commands to the parse function.
:param user: full user string with host
:param channel: channel the message was received on
:param msg: the message
:type user: string
:type channel: string
:type msg: string
"""
nick = user.split("!")[0]
if channel == self.nickname:
channel = nick
host = user.split("!")[1]
host = host.split("@")[1]
ident = user.split("!")[1]
ident = ident.split("@")[0]
self.log.info(f"({channel}) {user}: {msg}")
if msg[0] == self.prefix:
if len(msg) > 1:
if msg.split()[0] != "!":
self.parse(user, host, channel, msg[1:])
elif host in self.admins and channel == nick:
if len(msg) > 0:
spl = msg.split()
if len(spl) > 0:
if spl[0] != "!":
self.parse(user, host, channel, msg)
def noticed(self, user, channel, msg):
"""
Called on received NOTICEs.
:param user: full user string with host
:param channel: channel the notice was received on
:param msg: the message
:type user: string
:type channel: string
:type msg: string
"""
nick = user.split("!")[0]
if channel == self.nickname:
channel = nick
# self.log.info("[%s] %s: %s" % (channel, user, msg))
class IRCBotFactory(protocol.ClientFactory):
def __init__(self):
self.log = util.get_logger("IRC")
self.log.info("Class initialised")
def sendmsg(self, msg):
"""
Passthrough function to send a message to the channel.
"""
if self.client:
self.client.msg(self.client.channel, msg)
else:
self.log.error(f"Trying to send a message without connected client: {msg}")
return
def buildProtocol(self, addr):
"""
Custom override for the Twisted buildProtocol so we can access the Protocol instance.
Passes through the Agora instance to IRC.
:return: IRCBot Protocol instance
"""
# Pass through the logger
prcol = IRCBot(self.log)
self.client = prcol
setattr(self.client, "agora", self.agora)
setattr(self.client, "sinks", self.sinks)
setattr(self.client, "sources", self.sources)
setattr(self.client, "tx", self.tx)
setattr(self.client, "ux", self.ux)
return prcol
def clientConnectionLost(self, connector, reason):
"""
Called when connection to IRC server lost. Reconnect.
:param connector: connector object
:param reason: reason connection lost
:type connector: object
:type reason: string
"""
self.log.error(f"Lost connection: {reason}, reconnecting")
connector.connect()
def clientConnectionFailed(self, connector, reason):
"""
Called when connection to IRC server failed. Reconnect.
:param connector: connector object
:param reason: reason connection failed
:type connector: object
:type reason: string
"""
self.log.error(f"Could not connect: {reason}")
connector.connect()
def bot():
"""
Load the certificates, start the Bot Factory and connect it to the IRC server.
:return: Factory instance
:rtype: Factory
"""
# Load the certificates
context = ssl.DefaultOpenSSLContextFactory(settings.IRC.Cert, settings.IRC.Cert)
# Define the factory instance
factory = IRCBotFactory()
reactor.connectSSL(settings.IRC.Host, int(settings.IRC.Port), factory, context)
return factory

@ -1,86 +0,0 @@
# Other library imports
import requests
import util
# Project imports
from settings import settings
class Notify(util.Base):
"""
Class to handle more robust notifications.
"""
def sendmsg(self, msg, title=None, priority=None, tags=None):
if settings.Notify.Enabled == "0":
return
headers = {"Title": "Bot"}
if title:
headers["Title"] = title
if priority:
headers["Priority"] = priority
if tags:
headers["Tags"] = tags
requests.post(
f"{settings.Notify.Host}/{settings.Notify.Topic}",
data=msg,
headers=headers,
)
def notify_new_trade(self, amount, currency):
amount_usd = self.money.to_usd(amount, currency)
self.sendmsg(
f"Total: {amount_usd}",
title="New trade",
tags="trades",
priority="2",
)
def notify_complete_trade(self, amount, currency):
amount_usd = self.money.to_usd(amount, currency)
self.sendmsg(
f"Total: {amount_usd}",
title="Trade complete",
tags="trades,profit",
priority="3",
)
def notify_withdrawal(self, amount_usd):
self.sendmsg(
f"Total: {amount_usd}",
title="Withdrawal",
tags="profit",
priority="4",
)
def notify_need_topup(self, amount_usd_xmr):
self.sendmsg(
f"XMR: {amount_usd_xmr}",
title="Topup needed",
tags="admin",
priority="5",
)
def notify_tx_lookup_failed(self, currency, amount, reference, code, trade_id=None):
self.sendmsg(
f"Unknown TX [{code}]: {amount}{currency} ({reference}) for {trade_id}",
title=code,
tags="tx",
priority="5",
)
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",
)

@ -1,233 +0,0 @@
# Other library imports
import hashlib
import hmac
import json
import time
import requests
import util
# Project imports
from settings import settings
class Verify(util.Base):
"""
Class to handle user verification.
"""
def create_uid(self, platform, username):
return f"{platform}|{username}"
def get_uid(self, external_user_id):
"""
Get the platform and username from the external user ID.
"""
spl = external_user_id.split("|")
if not len(spl) == 2:
self.log.error(f"Split invalid, cannot get customer: {spl}")
return False
platform, username = spl
return (platform, username)
def verification_successful(self, external_user_id):
"""
Called when verification has been successfully passed.
"""
self.antifraud.user_verification_successful(external_user_id)
def update_verification_status(
self, external_user_id, review_status, review_answer=None
):
"""
Update the authentication status of a external user ID.
"""
if review_status == "completed" and review_answer == "GREEN":
self.verification_successful(external_user_id)
def verify_webhook_signature(self, content, payload_digest):
if type(content) == str:
content = content.encode("utf-8")
# hmac needs bytes
signature = hmac.new(
settings.Verify.WebHookSecret.encode("utf-8"),
content,
digestmod=hashlib.sha1,
).hexdigest()
return signature == payload_digest
def process_callback(self, content_json):
if "externalUserId" in content_json:
external_user_id = content_json["externalUserId"]
else:
self.log.warning("Useless callback received. No external user ID.")
return False
if "reviewStatus" in content_json:
review_status = content_json["reviewStatus"]
else:
self.log.warning("Useless callback received. No review status.")
return False
review_answer = None
if review_status == "completed":
if "reviewResult" in content_json:
if "reviewAnswer" in content_json["reviewResult"]:
review_answer = content_json["reviewResult"]["reviewAnswer"]
self.update_verification_status(
external_user_id, review_status, review_answer=review_answer
)
return True
def handle_callback(self, request):
"""
Handle a webhook callback.
"""
content = request.content.read()
payload_digest = request.getHeader("x-payload-digest")
if not self.verify_webhook_signature(content, payload_digest):
self.log.error("Webhook is not signed. Aborting.")
return False
content_json = json.loads(content)
rtrn = self.process_callback(content_json)
return rtrn
def get_external_user_id_details(self, external_user_id):
# /resources/applicants/-;externalUserId={externalUserId}/one
url = f"{settings.Verify.Base}/resources/applicants/-;externalUserId={external_user_id}/one"
resp = self.sign_request(requests.Request("GET", url))
s = requests.Session()
response = s.send(resp)
info = response.json()
if "info" in info:
if {"firstName", "lastName"}.issubset(set(info["info"].keys())):
first_name = info["info"]["firstName"]
last_name = info["info"]["lastName"]
if first_name.startswith("MR "):
first_name = first_name[3:]
return (first_name, last_name)
def create_applicant_and_get_link(self, external_user_id):
"""
Create the applicant and return the authentication link.
"""
# applicant_id = self.create_applicant(external_user_id)
auth_url = self.get_authentication_link(external_user_id)
return auth_url
def get_authentication_link(self, external_user_id):
"""
Get an external authentication link for a user.
"""
# /resources/sdkIntegrations/levels/{levelName}/websdkLink?ttlInSecs={lifetime}&externalUserId={externalUserId}&lang={locale}
url = (
f"{settings.Verify.Base}/resources/sdkIntegrations/levels/{settings.Verify.LevelName}"
f"/websdkLink?ttlInSecs=36000&externalUserId={external_user_id}"
)
resp = self.sign_request(requests.Request("POST", url))
s = requests.Session()
response = s.send(resp)
verification_url = response.json()["url"]
return verification_url
def get_applicant_status(self, applicant_id):
"""
Get the status of an applicant.
"""
# url = settings.Verify.Base + '/resources/applicants/' + applicant_id + '/requiredIdDocsStatus'
url = f"{settings.Verify.Base}/resources/applicants/'{applicant_id}/requiredIdDocsStatus"
resp = self.sign_request(requests.Request("GET", url))
s = requests.Session()
response = s.send(resp)
return response
def get_external_user_id_status(self, external_user_id):
"""
Get the status of an applicant by the external user ID.
"""
url = (
settings.Verify.Base
+ f"/resources/applicants/-;externalUserId={external_user_id}/one"
)
resp = self.sign_request(requests.Request("GET", url))
s = requests.Session()
response = s.send(resp)
response_json = response.json()
if "review" in response_json:
if "reviewResult" in response_json["review"]:
if "reviewAnswer" in response_json["review"]["reviewResult"]:
return response_json["review"]["reviewResult"]["reviewAnswer"]
return
def create_applicant(self, external_user_id):
"""
Create an applicant.
"""
body = {"externalUserId": external_user_id}
params = {"levelName": settings.Verify.LevelName}
headers = {"Content-Type": "application/json", "Content-Encoding": "utf-8"}
resp = self.sign_request(
requests.Request(
"POST",
f"{settings.Verify.Base}/resources/applicants?levelName={settings.Verify.LevelName}",
params=params,
data=json.dumps(body),
headers=headers,
)
)
s = requests.Session()
response = s.send(resp)
applicant_id = response.json()["id"]
return applicant_id
def get_access_token(self, external_user_id, level_name):
"""
Get an access token for an external user ID.
"""
params = {
"userId": external_user_id,
"ttlInSecs": "600",
"levelName": level_name,
}
headers = {"Content-Type": "application/json", "Content-Encoding": "utf-8"}
resp = self.sign_request(
requests.Request(
"POST",
f"{settings.Verify.Base}/resources/accessTokens",
params=params,
headers=headers,
)
)
s = requests.Session()
response = s.send(resp)
token = response.json()["token"]
return token
def sign_request(self, request: requests.Request) -> requests.PreparedRequest:
"""
Sign a request.
"""
prepared_request = request.prepare()
now = int(time.time())
method = request.method.upper()
path_url = prepared_request.path_url # includes encoded query params
# could be None so we use an empty **byte** string here
body = b"" if prepared_request.body is None else prepared_request.body
if type(body) == str:
body = body.encode("utf-8")
data_to_sign = (
str(now).encode("utf-8")
+ method.encode("utf-8")
+ path_url.encode("utf-8")
+ body
)
# hmac needs bytes
signature = hmac.new(
settings.Verify.Key.encode("utf-8"), data_to_sign, digestmod=hashlib.sha256
)
prepared_request.headers["X-App-Token"] = settings.Verify.Token
prepared_request.headers["X-App-Access-Ts"] = str(now)
prepared_request.headers["X-App-Access-Sig"] = signature.hexdigest()
return prepared_request
Loading…
Cancel
Save