diff --git a/handler/app.py b/handler/app.py deleted file mode 100755 index 969858e..0000000 --- a/handler/app.py +++ /dev/null @@ -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/", methods=["GET"]) - def signin(self, request, account): - auth_url = self.sinks.truelayer.create_auth_url(account) - return f'Please sign in to {account} here.' - - # 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/", 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) diff --git a/handler/db.py b/handler/db.py deleted file mode 100644 index dfe9c46..0000000 --- a/handler/db.py +++ /dev/null @@ -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 diff --git a/handler/lib/agoradesk_py.py b/handler/lib/agoradesk_py.py deleted file mode 100644 index 1b7257d..0000000 --- a/handler/lib/agoradesk_py.py +++ /dev/null @@ -1,1072 +0,0 @@ -"""See https://agoradesk.com/api-docs/v1.""" -# pylint: disable=too-many-lines -# Large API. Lots of lines can't be avoided. -import json -import logging -from typing import Any, Dict, List, Optional, Union - -import arrow -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://agoradesk.com/api/v1/" - - -class AgoraDesk: - """AgoraDesk / LocalMonero API object. - - Documentation: https://agoradesk.com/api-docs/v1 - """ - - # pylint: disable=too-many-public-methods - # API provides this many methods, I can't change that - - def __init__(self, api_key: Optional[str], debug: Optional[bool] = False) -> None: - self.api_key = "" - if api_key: - self.api_key = api_key - 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 AgoraDesk API with api_key %s", self.api_key) - - @inlineCallbacks - def callback_api_call(self, response, result): - logger.debug(response) - try: - text = yield response.content() - except: # noqa - self.log.error("Error with API call") - return - 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, - ) -> Dict[str, Any]: - api_call_url = URI_API + api_method - - headers = { - "Content-Type": "application/json", - "User-Agent": f"agoradesk_py/{__version__} " - f"https://codeberg.org/MarvinsCryptoTools/agoradesk_py", - "Authorization": self.api_key, - } - - logger.debug("API Call URL: %s", api_call_url) - logger.debug("Headers : %s", 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=json.dumps(query_values).encode("ascii"), - ) - 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 - - # except httpx.ConnectError as error: - # result["message"] = str(error) - # result["status"] = 600 - # result["response"] = {"error": {"message": error}} - # return result - # except json.decoder.JSONDecodeError: - # result["message"] = "Not JSON" - # if response: - # result["status"] = response.status_code - # result["response"] = {"error": {"message": response.text}} - # return result - # except httpx.ReadTimeout: - # result["message"] = "Read timed out" - # if response: - # result["status"] = response.status_code - # result["response"] = {"error": {"message": response.text}} - # return result - - # Account related API Methods - # =========================== - - def account_info(self, username: str) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getUserByUsername - """ - return self._api_call(api_method=f"account_info/{username}") - - # def dashboard(self) -> Dict[str, Any]: - # """See Agoradesk API. - - # https://agoradesk.com/api-docs/v1#operation/getUserDashboard - # """ - # return self._api_call(api_method="dashboard") - - def dashboard_buyer(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getUserDashboardBuyer - """ - return self._api_call(api_method="dashboard/buyer") - - def dashboard(self) -> Dict[str, Any]: - """See Agoradesk 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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getUserDashboardCanceled - """ - return self._api_call(api_method="dashboard/canceled") - - def dashboard_closed(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getUserDashboardClosed - """ - return self._api_call(api_method="dashboard/closed") - - def dashboard_released(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getUserDashboardReleased - """ - - return self._api_call(api_method="dashboard/released") - - def logout(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/logout - """ - - return self._api_call(api_method="logout", http_method="POST") - - def myself(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getTokenOwnerUserData - """ - - return self._api_call(api_method="myself") - - def notifications(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getUserNotifications - """ - - return self._api_call(api_method="notifications") - - def notifications_mark_as_read(self, notification_id: str) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/markNotificationRead - """ - - return self._api_call( - api_method=f"notifications/mark_as_read/{notification_id}", - http_method="POST", - ) - - def recent_messages(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getRecemtMessages - """ - - return self._api_call(api_method="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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/setUserFeedback - """ - - 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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/markPaid - """ - 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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/cancelTrade - """ - 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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getTradeMessages - """ - 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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/startTrade - """ - 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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getTradeById and - https://agoradesk.com/api-docs/v1#operation/getTradesInBulk - """ - 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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/sendChatMessage - """ - payload = {"msg": msg} - return self._api_call( - api_method=f"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, - asset: str, - price_equation: str, - track_max_amount: bool, - require_trusted_by_advertiser: bool, - verified_email_required: Optional[bool] = None, - online_provider: Optional[str] = None, - msg: Optional[str] = None, - min_amount: Optional[float] = None, - max_amount: Optional[float] = None, - limit_to_fiat_amounts: Optional[str] = None, - payment_method_details: Optional[str] = None, - first_time_limit_asset: Optional[float] = None, - require_feedback_score: Optional[int] = None, - account_info: Optional[str] = None, - payment_window_minutes: Optional[int] = None, - floating: Optional[bool] = None, - lat: Optional[float] = None, - lon: Optional[float] = None, - ) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/createAd - """ - - # 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, - "asset": asset, - "price_equation": price_equation, - "track_max_amount": 1 if track_max_amount else 0, - "require_trusted_by_advertiser": 1 if require_trusted_by_advertiser else 0, - } - if verified_email_required: - params["verified_email_required"] = 1 if verified_email_required else 0 - 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 limit_to_fiat_amounts: - params["limit_to_fiat_amounts"] = limit_to_fiat_amounts - if payment_method_details: - params["payment_method_detail"] = payment_method_details - if first_time_limit_asset: - params["first_time_limit_asset"] = first_time_limit_asset - if require_feedback_score: - params["require_feedback_score"] = require_feedback_score - if account_info: - params["account_info"] = account_info - if payment_window_minutes: - params["payment_window_minutes"] = payment_window_minutes - if floating: - params["floating"] = 1 if floating else 0 - if lat: - params["lat"] = lat - if lon: - params["lon"] = lon - - return self._api_call( - api_method="ad-create", - http_method="POST", - query_values=params, - ) - - def ad( - self, - ad_id: str, - country_code: Optional[str] = None, - currency: Optional[str] = None, - trade_type: Optional[str] = None, - asset: Optional[str] = None, - price_equation: Optional[str] = None, - track_max_amount: Optional[bool] = None, - require_trusted_by_advertiser: Optional[bool] = None, - verified_email_required: Optional[bool] = None, - online_provider: Optional[str] = None, - msg: Optional[str] = None, - min_amount: Optional[float] = None, - max_amount: Optional[float] = None, - limit_to_fiat_amounts: Optional[str] = None, - payment_method_details: Optional[str] = None, - first_time_limit_asset: Optional[float] = None, - require_feedback_score: Optional[int] = None, - account_info: Optional[str] = None, - payment_window_minutes: Optional[int] = None, - floating: Optional[bool] = None, - lat: Optional[float] = None, - lon: Optional[float] = None, - visible: Optional[bool] = None, - ) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/editAd - """ - - # 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, Union[str, float, bool]] = {} - if country_code: - params["countrycode"] = country_code - if currency: - params["currency"] = currency - if trade_type: - params["trade_type"] = trade_type - if asset: - params["asset"] = asset - if price_equation: - params["price_equation"] = price_equation - if track_max_amount: - params["track_max_amount"] = 1 if track_max_amount else 0 - if require_trusted_by_advertiser: - params["require_trusted_by_advertiser"] = ( - 1 if require_trusted_by_advertiser else 0 - ) - if verified_email_required: - params["verified_email_required"] = 1 if verified_email_required else 0 - 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 limit_to_fiat_amounts: - params["limit_to_fiat_amounts"] = limit_to_fiat_amounts - if payment_method_details: - params["payment_method_detail"] = payment_method_details - if first_time_limit_asset: - params["first_time_limit_asset"] = first_time_limit_asset - if require_feedback_score: - params["require_feedback_score"] = require_feedback_score - if account_info: - params["account_info"] = account_info - if payment_window_minutes: - params["payment_window_minutes"] = payment_window_minutes - if floating: - params["floating"] = 1 if floating else 0 - if lat: - params["lat"] = lat - if lon: - params["lon"] = lon - if visible: - params["visible"] = True if visible else False - - return self._api_call( - api_method=f"ad/{ad_id}", - http_method="POST", - query_values=params, - ) - - def ad_equation(self, ad_id: str, price_equation: str) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/updateFormula - """ - return self._api_call( - api_method=f"ad-equation/{ad_id}", - http_method="POST", - query_values={"price_equation": price_equation}, - ) - - def ad_delete(self, ad_id: str) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/deleteAd - """ - return self._api_call(api_method=f"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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getYourAds - """ - - # 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="ads") - - return self._api_call(api_method="ads", query_values=params) - - def ad_get(self, ad_ids: List[str]) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getAdById and - https://agoradesk.com/api-docs/v1#operation/getAdsInBulk - """ - 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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/paymentMethods and - https://agoradesk.com/api-docs/v1#operation/countryHasPaymentMethod - """ - 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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/countryCodes - """ - return self._api_call(api_method="countrycodes") - - def currencies(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/currencyCodes - """ - return self._api_call(api_method="currencies") - - def equation(self, price_equation: str, currency: str) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/priceFormula - """ - return self._api_call( - api_method="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}", - query_values=params, - ) - - @staticmethod - def _generic_search_parameters(amount, page): - params = None - if amount and page is not None: - params = {"amount": f"{amount}"} - elif amount and page is not None: - params = {"amount": f"{amount}", "page": f"{page}"} - elif not amount and page is not None: - params = {"page": f"{page}"} - return params - - def buy_monero_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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getOnlineSellXmrByCurrencyCode and - https://agoradesk.com/api-docs/v1#operation/getOnlineSellXmrByCurrencyCodeAndCountryCode and # noqa: E501 pylint: disable=line-too-long - https://agoradesk.com/api-docs/v1#operation/getOnlineSellXmrByCurrencyCodeAndPaymentMethodCode and # noqa: E501 pylint: disable=line-too-long - https://agoradesk.com/api-docs/v1#operation/getOnlineSellXmrByCurrencyCodeAndCountryCodeAndPaymentMethodCode # noqa: E501 pylint: disable=line-too-long - """ - - # pylint: disable=too-many-arguments - - return self._generic_online( - direction="buy", - main_currency="monero", - exchange_currency=currency_code, - country_code=country_code, - payment_method=payment_method, - amount=amount, - page=page, - ) - - def buy_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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getOnlineSellBtcByCurrencyCode - https://agoradesk.com/api-docs/v1#operation/getOnlineSellBtcByCurrencyCodeAndCountryCode and # noqa: E501 pylint: disable=line-too-long - https://agoradesk.com/api-docs/v1#operation/getOnlineSellBtcByCurrencyCodeAndPaymentMethodCode and # noqa: E501 pylint: disable=line-too-long - https://agoradesk.com/api-docs/v1#operation/getOnlineSellBtcByCurrencyCodeAndCountryCodeAndPaymentMethodCode # noqa: E501 pylint: disable=line-too-long - """ - - # 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_monero_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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getOnlineBuyXmrByCurrencyCode - https://agoradesk.com/api-docs/v1#operation/getOnlineBuyXmrByCurrencyCodeAndCountryCode and # noqa: E501 pylint: disable=line-too-long - https://agoradesk.com/api-docs/v1#operation/getOnlineBuyXmrByCurrencyCodeAndPaymentMethodCode and # noqa: E501 pylint: disable=line-too-long - https://agoradesk.com/api-docs/v1#operation/getOnlineBuyXmrByCurrencyCodeAndCountryCodeAndPaymentMethodCode # noqa: E501 pylint: disable=line-too-long - """ - - # pylint: disable=too-many-arguments - - return self._generic_online( - direction="sell", - main_currency="monero", - 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 Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getOnlineBuyBtcByCurrencyCode - https://agoradesk.com/api-docs/v1#operation/getOnlineBuyBtcByCurrencyCodeAndCountryCode and # noqa: E501 pylint: disable=line-too-long - https://agoradesk.com/api-docs/v1#operation/getOnlineBuyBtcByCurrencyCodeAndPaymentMethodCode and # noqa: E501 pylint: disable=line-too-long - https://agoradesk.com/api-docs/v1#operation/getOnlineBuyBtcByCurrencyCodeAndCountryCodeAndPaymentMethodCode # noqa: E501 pylint: disable=line-too-long - """ - - # 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, - ) - - def _generic_cash( - self, - direction: str, - main_currency: str, - exchange_currency: str, - country_code: str, - lat: str, - lon: str, - amount: Optional[float] = None, - page: Optional[int] = None, - ) -> Dict[str, Any]: - # pylint: disable=too-many-arguments - - params = self._generic_search_parameters(amount, page) - - return self._api_call( - api_method=f"{direction}-{main_currency}-with-cash/" - f"{exchange_currency}/{country_code}/{lat}/{lon}", - query_values=params, - ) - - def buy_monero_with_cash( - self, - currency_code: str, - country_code: str, - lat: str, - lon: str, - amount: Optional[float] = None, - page: Optional[int] = None, - ) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getLocalSellXmrByCurrencyCodeAndCountryCode # noqa: E501 pylint: disable=line-too-long - """ - - # pylint: disable=too-many-arguments - - return self._generic_cash( - direction="buy", - main_currency="monero", - exchange_currency=currency_code, - country_code=country_code, - lat=lat, - lon=lon, - amount=amount, - page=page, - ) - - def buy_bitcoins_with_cash( - self, - currency_code: str, - country_code: str, - lat: str, - lon: str, - amount: Optional[float] = None, - page: Optional[int] = None, - ) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getLocalSellBtcByCurrencyCodeAndCountryCode # noqa: E501 pylint: disable=line-too-long - """ - - # pylint: disable=too-many-arguments - - return self._generic_cash( - direction="buy", - main_currency="bitcoins", - exchange_currency=currency_code, - country_code=country_code, - lat=lat, - lon=lon, - amount=amount, - page=page, - ) - - def sell_monero_with_cash( - self, - currency_code: str, - country_code: str, - lat: str, - lon: str, - amount: Optional[float] = None, - page: Optional[int] = None, - ) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getLocalBuyXmrByCurrencyCodeAndCountryCode # noqa: E501 pylint: disable=line-too-long - """ - - # pylint: disable=too-many-arguments - - return self._generic_cash( - direction="sell", - main_currency="monero", - exchange_currency=currency_code, - country_code=country_code, - lat=lat, - lon=lon, - amount=amount, - page=page, - ) - - def sell_bitcoins_with_cash( - self, - currency_code: str, - country_code: str, - lat: str, - lon: str, - amount: Optional[float] = None, - page: Optional[int] = None, - ) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getLocalBuyBtcByCurrencyCodeAndCountryCode # noqa: E501 pylint: disable=line-too-long - """ - - # pylint: disable=too-many-arguments - - return self._generic_cash( - direction="sell", - main_currency="bitcoins", - exchange_currency=currency_code, - country_code=country_code, - lat=lat, - lon=lon, - amount=amount, - page=page, - ) - - # Statistics related API Methods - # ============================== - - def moneroaverage( - self, currency: Optional[str] = "ticker-all-currencies" - ) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getXmrTicker and - https://agoradesk.com/api-docs/v1#operation/getXmrTickerByCurrencyCode - """ - return self._api_call(api_method=f"moneroaverage/{currency}") - - # Wallet related API Methods - # =========================== - - def wallet(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getBtcWallet - """ - return self._api_call(api_method="wallet") - - def wallet_balance(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getBtcWalletBalance - """ - return self._api_call(api_method="wallet-balance") - - def wallet_xmr(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getXmrWallet - """ - return self._api_call(api_method="wallet/XMR") - - def wallet_balance_xmr(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getXmrWalletBalance - """ - return self._api_call(api_method="wallet-balance/XMR") - - def wallet_addr(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getBtcAddress - """ - return self._api_call(api_method="wallet-addr") - - def wallet_addr_xmr(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getXMRAddress - """ - return self._api_call(api_method="wallet-addr/XMR") - - def fees(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getBtcFee - """ - return self._api_call(api_method="fees") - - def fees_xmr(self) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/getXmrFee - """ - return self._api_call(api_method="fees/XMR") - - def wallet_send( - self, - address: str, - amount: float, - password: str, - fee_level: str, - otp: Optional[int] = None, - ) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/withdrawBtc - """ - # pylint: disable=too-many-arguments - - params = { - "address": address, - "amount": amount, - "password": password, - "fee_level": fee_level, - } - if otp: - params["otp"] = otp - - return self._api_call( - api_method="wallet-send", http_method="POST", query_values=params - ) - - def wallet_send_xmr( - self, - address: str, - amount: float, - password: str, - # fee_level: str, - otp: Optional[int] = None, - ) -> Dict[str, Any]: - """See Agoradesk API. - - https://agoradesk.com/api-docs/v1#operation/withdrawXmr - """ - # pylint: disable=too-many-arguments - - params = { - "address": address, - "amount": amount, - "password": password, - # "fee_level": fee_level, - } - if otp: - params["otp"] = otp - - return self._api_call( - api_method="wallet-send/XMR", - http_method="POST", - query_values=params, - ) diff --git a/handler/lib/antifraud.py b/handler/lib/antifraud.py deleted file mode 100644 index 24e5560..0000000 --- a/handler/lib/antifraud.py +++ /dev/null @@ -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() diff --git a/handler/lib/localbitcoins_py.py b/handler/lib/localbitcoins_py.py deleted file mode 100644 index ddaad04..0000000 --- a/handler/lib/localbitcoins_py.py +++ /dev/null @@ -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, - ) diff --git a/handler/lib/logstash.py b/handler/lib/logstash.py deleted file mode 100644 index 67cb39b..0000000 --- a/handler/lib/logstash.py +++ /dev/null @@ -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 diff --git a/handler/lib/markets.py b/handler/lib/markets.py deleted file mode 100644 index bf9a55a..0000000 --- a/handler/lib/markets.py +++ /dev/null @@ -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 diff --git a/handler/lib/money.py b/handler/lib/money.py deleted file mode 100644 index abfb8c0..0000000 --- a/handler/lib/money.py +++ /dev/null @@ -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 diff --git a/handler/lib/serde/nordigen.py b/handler/lib/serde/nordigen.py deleted file mode 100644 index ac0e2a6..0000000 --- a/handler/lib/serde/nordigen.py +++ /dev/null @@ -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() diff --git a/handler/lib/serde/truelayer.py b/handler/lib/serde/truelayer.py deleted file mode 100644 index ccc15fe..0000000 --- a/handler/lib/serde/truelayer.py +++ /dev/null @@ -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() diff --git a/handler/lib/transactions.py b/handler/lib/transactions.py deleted file mode 100644 index 72f7d21..0000000 --- a/handler/lib/transactions.py +++ /dev/null @@ -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] diff --git a/handler/requirements.txt b/handler/requirements.txt deleted file mode 100644 index a2c099f..0000000 --- a/handler/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -twisted -redis -pyOpenSSL -Klein -ConfigObject -service_identity -forex_python -simplejson -requests -arrow -httpx -pre-commit -pycoingecko -PyOTP -opensearch -serde[ext] diff --git a/handler/runtest.sh b/handler/runtest.sh deleted file mode 100755 index 471fb54..0000000 --- a/handler/runtest.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -pre-commit run -a -python -m unittest discover -s tests -p 'test_*.py' diff --git a/handler/settings.example.ini b/handler/settings.example.ini deleted file mode 100644 index c626898..0000000 --- a/handler/settings.example.ini +++ /dev/null @@ -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 won’t 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 diff --git a/handler/settings.py b/handler/settings.py deleted file mode 100644 index 01b8dcb..0000000 --- a/handler/settings.py +++ /dev/null @@ -1,5 +0,0 @@ -# Other library imports -from ConfigObject import ConfigObject - -# Load the configuration -settings = ConfigObject(filename="settings.ini") diff --git a/handler/sinks/__init__.py b/handler/sinks/__init__.py deleted file mode 100644 index eff0d16..0000000 --- a/handler/sinks/__init__.py +++ /dev/null @@ -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 diff --git a/handler/sinks/nordigen.py b/handler/sinks/nordigen.py deleted file mode 100644 index d5bfba1..0000000 --- a/handler/sinks/nordigen.py +++ /dev/null @@ -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 diff --git a/handler/sinks/truelayer.py b/handler/sinks/truelayer.py deleted file mode 100644 index d5580fe..0000000 --- a/handler/sinks/truelayer.py +++ /dev/null @@ -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 diff --git a/handler/sources/__init__.py b/handler/sources/__init__.py deleted file mode 100644 index b58f5dd..0000000 --- a/handler/sources/__init__.py +++ /dev/null @@ -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. - """ diff --git a/handler/sources/agora.py b/handler/sources/agora.py deleted file mode 100644 index ab727b3..0000000 --- a/handler/sources/agora.py +++ /dev/null @@ -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) diff --git a/handler/sources/local.py b/handler/sources/local.py deleted file mode 100644 index d55acb2..0000000 --- a/handler/sources/local.py +++ /dev/null @@ -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) diff --git a/handler/sources/localbitcoins.py b/handler/sources/localbitcoins.py deleted file mode 100644 index 5634390..0000000 --- a/handler/sources/localbitcoins.py +++ /dev/null @@ -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) diff --git a/handler/tests/test_agora.py b/handler/tests/test_agora.py deleted file mode 100644 index 406a3d1..0000000 --- a/handler/tests/test_agora.py +++ /dev/null @@ -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) diff --git a/handler/tests/test_lbtc.py b/handler/tests/test_lbtc.py deleted file mode 100644 index 85de189..0000000 --- a/handler/tests/test_lbtc.py +++ /dev/null @@ -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) diff --git a/handler/tests/test_markets.py b/handler/tests/test_markets.py deleted file mode 100644 index 3a9ddaa..0000000 --- a/handler/tests/test_markets.py +++ /dev/null @@ -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) diff --git a/handler/tests/test_money.py b/handler/tests/test_money.py deleted file mode 100644 index 405742d..0000000 --- a/handler/tests/test_money.py +++ /dev/null @@ -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 diff --git a/handler/tests/test_transactions.py b/handler/tests/test_transactions.py deleted file mode 100644 index d80bbc1..0000000 --- a/handler/tests/test_transactions.py +++ /dev/null @@ -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 diff --git a/handler/util.py b/handler/util.py deleted file mode 100644 index 08df40d..0000000 --- a/handler/util.py +++ /dev/null @@ -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. 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 diff --git a/handler/ux/__init__.py b/handler/ux/__init__.py deleted file mode 100644 index 7d43bc2..0000000 --- a/handler/ux/__init__.py +++ /dev/null @@ -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) diff --git a/handler/ux/commands.py b/handler/ux/commands.py deleted file mode 100644 index 7f484f2..0000000 --- a/handler/ux/commands.py +++ /dev/null @@ -1,1190 +0,0 @@ -# Other library imports -from json import dumps, loads - -import db - -# Project imports -from settings import settings - - -class GenericCommands(object): - class trades(object): - @staticmethod - def got_trades(trades, msg): - if not trades: - msg("No open trades.") - return - for trade_id in trades: - msg(trade_id) - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, caller): - """ - Get details of open trades and post on IRC. - """ - - trades = caller.get_dashboard_irc() - trades.addCallback(GenericCommands.trades.got_trades, msg) - - class create(object): - @staticmethod - def run( - cmd, - spl, - length, - authed, - msg, - agora, - tx, - ux, - asset_list, - provider_list, - caller, - ): - """ - Post an ad on AgoraDesk/LBTC with the given country and currency code. - """ - if length == 4: - asset = spl[1] - country = spl[2] - currency = spl[3] - if asset not in loads(asset_list): - msg(f"Not a valid asset: {asset}") - return - _, account_info = tx.markets.get_valid_account_details(caller.platform) - posted = caller.create_ad( - asset, - country, - currency, - "NATIONAL_BANK", - payment_details=account_info[currency], - ) - if posted["success"]: - msg( - f"{posted['response']['data']['message']}: {posted['response']['data']['ad_id']}" - ) - else: - msg(dumps(posted["response"])) - elif length == 5: - asset = spl[1] - country = spl[2] - currency = spl[3] - provider = spl[4] - if asset not in loads(asset_list): - msg(f"Not a valid asset: {asset}") - return - if provider not in loads(provider_list): - msg(f"Not a valid provider: {provider}") - return - _, account_info = tx.markets.get_valid_account_details(caller.platform) - posted = caller.create_ad( - asset, - country, - currency, - provider, - payment_details=account_info[currency], - ) - if posted["success"]: - msg( - f"{posted['response']['data']['message']}: {posted['response']['data']['ad_id']}" - ) - else: - msg(dumps(posted["response"])) - - class messages(object): - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, caller): - """ - Get all messages for all open trades or a given trade. - """ - if length == 1: - m = caller.api.recent_messages() - m.addCallback(caller.got_recent_messages) - - elif length == 2: - tx = tx.ref_to_tx(spl[1]) - if not tx: - msg(f"No such reference: {spl[1]}") - return - messages = caller.get_messages(spl[1], send_irc=False) - if not messages: - msg("No messages.") - for message in messages: - msg(f"{spl[1]}: {message}") - - class dist(object): - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, asset_list, caller): - # Distribute out our ad to all countries in the config - if length == 2: - asset = spl[1] - if asset not in loads(asset_list): - msg(f"Not a valid asset: {spl[1]}") - return - for x in caller.dist_countries(filter_asset=asset): - if x["success"]: - msg( - f"{x['response']['data']['message']}: {x['response']['data']['ad_id']}" - ) - else: - msg(dumps(x["response"])) - elif length == 1: - for x in caller.dist_countries(): - if x["success"]: - msg( - f"{x['response']['data']['message']}: {x['response']['data']['ad_id']}" - ) - else: - msg(dumps(x["response"])) - - class redist(object): - @staticmethod - def got_redist(redist_output, msg): - for x in redist_output: - if x[0]["success"]: - msg(f"{x[0]['response']['data']['message']}: {x[1]}") - else: - msg(dumps(x)) - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, caller): - c = caller.redist_countries() - c.addCallback(GenericCommands.redist.got_redist, msg) - - class stripdupes(object): - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, caller): - rtrn = caller.strip_duplicate_ads() - msg(dumps(rtrn)) - - class message(object): - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, caller): - if length > 2: - full_msg = " ".join(spl[2:]) - reference = tx.ref_to_tx(spl[1]) - if not reference: - msg(f"No such reference: {spl[1]}") - return - rtrn = caller.contact_message_post(reference, full_msg) - msg(f"Sent {full_msg} to {reference}: {rtrn}") - - class release(object): - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, caller): - if length == 2: - tx = tx.ref_to_tx(spl[1]) - if not tx: - msg(f"No such reference: {spl[1]}") - return - rtrn = caller.release_funds(tx) - message = rtrn["message"] - message_long = rtrn["response"]["data"]["message"] - msg(f"{message} - {message_long}") - - class pubads(object): - @staticmethod - def got_pubads(pubads_output, currency, msg): - if not pubads_output: - msg("No results.") - return - for ad in pubads_output[currency]: - msg(f"({ad[0]}) {ad[1]} {ad[2]} {ad[3]} {ad[4]} {ad[5]} {ad[6]}") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, asset_list, caller): - if length == 3: - asset = spl[1] - if asset not in loads(asset_list): - msg(f"Not a valid asset: {spl[1]}") - return - currency = spl[2] - c = caller.get_all_public_ads(assets=[asset], currencies=[currency]) - c.addCallback(GenericCommands.pubads.got_pubads, currency, msg) - - elif length == 4: - asset = spl[1] - if asset not in loads(asset_list): - msg(f"Not a valid asset: {spl[1]}") - return - providers = spl[3].split(",") - currency = spl[2] - c = caller.get_all_public_ads( - assets=[asset], currencies=[currency], providers=providers - ) - c.addCallback(GenericCommands.pubads.got_pubads, currency, msg) - - class cheat(object): - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, asset_list, caller): - if length == 1: - caller.run_cheat_in_thread() - msg("Running cheat in thread.") - elif length == 2: - asset = spl[1] - if asset not in loads(asset_list): - msg(f"Not a valid asset: {spl[1]}") - return - caller.run_cheat_in_thread([asset]) - msg(f"Running cheat in thread for {asset}.") - - class cheatnext(object): - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, caller): - if length == 1: - asset = caller.run_cheat_in_thread() - msg(f"Running next asset for cheat in thread: {asset}") - - class ads(object): - @staticmethod - def got_ads(ads_output, msg): - if not ads_output: - msg("Could not get ads.") - return - for ad in ads_output: - msg(f"({ad[0]}) {ad[1]} {ad[2]} {ad[3]} {ad[4]}") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, caller): - c = caller.enum_ads() - c.addCallback(GenericCommands.ads.got_ads, msg) - - class withdraw(object): - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, caller): - caller.withdraw_funds() - - class nuke(object): - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, caller): - rtrn = caller.nuke_ads() - msg(dumps(rtrn)) - - class wallet(object): - @staticmethod - def got_wallet(wallet_output, msg, asset): - if not wallet_output["success"]: - msg(f"Error getting {asset} wallet details.") - return - if not wallet_output["response"]: - msg(f"Error getting {asset} wallet details.") - return - balance = wallet_output["response"]["data"]["total"]["balance"] - msg(f"{asset} wallet balance: {balance}") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux, xmr, caller): - if xmr: - rtrn_xmr = caller.wallet_balance_xmr() - rtrn_xmr.addCallback(GenericCommands.wallet.got_wallet, msg, "XMR") - - else: - rtrn_btc = caller.wallet_balance() - rtrn_btc.addCallback(GenericCommands.wallet.got_wallet, msg, "BTC") - - -class IRCCommands(object): - class atrades(object): - authed = True - name = "atrades" - helptext = "Get all open trades for Agora." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.trades.run( - cmd, spl, length, authed, msg, agora, tx, ux, agora - ) - - class ltrades(object): - authed = True - name = "ltrades" - helptext = "Get all open trades for LBTC." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.trades.run( - cmd, spl, length, authed, msg, agora, tx, ux, tx.lbtc - ) - - class acreate(object): - name = "acreate" - authed = True - helptext = "Create an ad on Agora. Usage: acreate []" - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.create.run( - cmd, - spl, - length, - authed, - msg, - agora, - tx, - ux, - settings.Agora.AssetList, - settings.Agora.ProviderList, - agora, - ) - - class lcreate(object): - name = "lcreate" - authed = True - helptext = "Create an ad on LBTC. Usage: lcreate []" - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.create.run( - cmd, - spl, - length, - authed, - msg, - agora, - tx, - ux, - settings.LocalBitcoins.AssetList, - settings.LocalBitcoins.ProviderList, - tx.lbtc, - ) - - class amessages(object): - authed = True - name = "amessages" - helptext = "Get messages for Agora. Usage: amessages []" - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.messages.run( - cmd, spl, length, authed, msg, agora, tx, ux, agora - ) - - class lmessages(object): - authed = True - name = "lmessages" - helptext = "Get messages for LBTC. Usage: lmessages []" - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.messages.run( - cmd, spl, length, authed, msg, agora, tx, ux, tx.lbtc - ) - - class adist(object): - authed = True - name = "adist" - helptext = "Distribute all our chosen currency and country ad pairs for Agora. Usage: adist []" - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.dist.run( - cmd, - spl, - length, - authed, - msg, - agora, - tx, - ux, - settings.Agora.AssetList, - agora, - ) - - class ldist(object): - authed = True - name = "ldist" - helptext = "Distribute all our chosen currency and country ad pairs for LBTC. Usage: ldist []" - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.dist.run( - cmd, - spl, - length, - authed, - msg, - agora, - tx, - ux, - settings.LocalBitcoins.AssetList, - tx.lbtc, - ) - - class aredist(object): - authed = True - name = "aredist" - helptext = "Update all ads with details for Agora." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.redist.run( - cmd, spl, length, authed, msg, agora, tx, ux, agora - ) - - class lredist(object): - authed = True - name = "lredist" - helptext = "Update all ads with details for LBTC." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.redist.run( - cmd, spl, length, authed, msg, agora, tx, ux, tx.lbtc - ) - - class astripdupes(object): - authed = True - name = "astripdupes" - helptext = "Remove all duplicate adverts for Agora." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.stripdupes.run( - cmd, spl, length, authed, msg, agora, tx, ux, agora - ) - - class lstripdupes(object): - authed = True - name = "lstripdupes" - helptext = "Remove all duplicate adverts for LBTC." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.stripdupes.run( - cmd, spl, length, authed, msg, agora, tx, ux, tx.lbtc - ) - - class total(object): - name = "total" - authed = True - helptext = "Get total account balance from Sinks and Agora." - - @staticmethod - def got_total(total_output, msg): - if not total_output: - msg("Error getting total output") - return - totals = total_output[0] - wallets = total_output[1] - msg(f"Totals: SEK: {totals[0]} | USD: {totals[1]} | GBP: {totals[2]}") - msg(f"Wallets: XMR USD: {wallets[0]} | BTC USD: {wallets[1]}") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - totals_all = tx.money.get_total() - totals_all.addCallback(IRCCommands.total.got_total, msg) - - class ping(object): - name = "ping" - authed = False - helptext = "Pong!" - - @staticmethod - def run(cmd, spl, length, authed, msg): - msg("Pong!") - - class summon(object): - name = "summon" - authed = True - helptext = "Summon all operators." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - ux.notify.sendmsg("You have been summoned!") - - class amessage(object): - authed = True - name = "amsg" - helptext = "Send a message on a trade. Usage: amsg " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.message.run( - cmd, spl, length, authed, msg, agora, tx, ux, agora.agora - ) - - class lmessage(object): - authed = True - name = "lmsg" - helptext = "Send a message on a trade. Usage: lmsg " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.message.run( - cmd, spl, length, authed, msg, agora, tx, ux, tx.lbtc.lbtc - ) - - class refs(object): - name = "refs" - authed = True - helptext = "List all references" - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - msg(f"References: {', '.join(db.get_refs())}") - - class ref(object): - name = "ref" - authed = True - helptext = "Get more information about a reference. Usage: ref " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 2: - ref_data = db.get_ref(spl[1]) - if not ref_data: - msg(f"No such reference: {spl[1]}") - return - msg(f"{spl[1]}: {dumps(ref_data)}") - - class delete(object): - name = "del" - authed = True - helptext = "Delete a reference. Usage: del " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 2: - ref_data = db.get_ref(spl[1]) - if not ref_data: - msg(f"No such reference: {spl[1]}") - return - db.del_ref(spl[1]) - msg(f"Deleted reference: {spl[1]}") - - class arelease(object): - authed = True - name = "arelease" - helptext = "Release funds for a trade. Usage: arelease " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.release.run( - cmd, spl, length, authed, msg, agora, tx, ux, agora - ) - - class lrelease(object): - authed = True - name = "lrelease" - helptext = "Release funds for a trade. Usage: lrelease " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.release.run( - cmd, spl, length, authed, msg, agora, tx, ux, tx.lbtc - ) - - class map(object): - name = "map" - authed = True - helptext = "Release funds for a trade. Usage: map " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 3: - reference = spl[1] - txid = spl[2] - is_released = tx.release_map_trade(reference, txid) - if is_released is None: - msg("Trade or TX invalid") - return - elif is_released is True: - msg("Trade released") - return - elif is_released is False: - msg("Could not release trade") - return - - class anuke(object): - authed = True - name = "anuke" - helptext = "Delete all our adverts for Agora." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.nuke.run( - cmd, spl, length, authed, msg, agora, tx, ux, agora - ) - - class lnuke(object): - authed = True - name = "lnuke" - helptext = "Delete all our adverts for Agora." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.nuke.run( - cmd, spl, length, authed, msg, agora, tx, ux, tx.lbtc - ) - - class awallet(object): - authed = True - name = "awallet" - helptext = "Get Agora wallet balances." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.wallet.run( - cmd, spl, length, authed, msg, agora, tx, ux, True, agora.api - ) - - class lwallet(object): - authed = True - name = "lwallet" - helptext = "Get LBTC wallet balances." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.wallet.run( - cmd, spl, length, authed, msg, agora, tx, ux, False, tx.lbtc.api - ) - - class apubads(object): - authed = True - name = "apubads" - helptext = "View public adverts for Agora. Usage: apubads []" - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.pubads.run( - cmd, - spl, - length, - authed, - msg, - agora, - tx, - ux, - settings.Agora.AssetList, - agora, - ) - - class lpubads(object): - authed = True - name = "lpubads" - helptext = "View public adverts for LBTC. Usage: lpubads []" - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.pubads.run( - cmd, - spl, - length, - authed, - msg, - agora, - tx, - ux, - settings.LocalBitcoins.AssetList, - tx.lbtc, - ) - - class acheat(object): - authed = True - name = "acheat" - helptext = "Cheat the markets by manipulating our prices to exploit people on Agora. Usage: acheat []" - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.cheat.run( - cmd, - spl, - length, - authed, - msg, - agora, - tx, - ux, - settings.Agora.AssetList, - agora, - ) - - class lcheat(object): - authed = True - name = "lcheat" - helptext = "Cheat the markets by manipulating our prices to exploit people on LBTC. Usage: lcheat []" - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.cheat.run( - cmd, - spl, - length, - authed, - msg, - agora, - tx, - ux, - settings.LocalBitcoins.AssetList, - tx.lbtc, - ) - - class acheatnext(object): - authed = True - name = "acheatnext" - helptext = "Run the next currency for cheat on Agora." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.cheatnext.run( - cmd, spl, length, authed, msg, agora, tx, ux, agora - ) - - class lcheatnext(object): - authed = True - name = "lcheatnext" - helptext = "Run the next currency for cheat on LBTC." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.cheatnext.run( - cmd, spl, length, authed, msg, agora, tx, ux, tx.lbtc - ) - - class aads(object): - authed = True - name = "aads" - helptext = "Get all our ad regions for Agora." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.ads.run(cmd, spl, length, authed, msg, agora, tx, ux, agora) - - class lads(object): - authed = True - name = "lads" - helptext = "Get all our ad regions for LBTC." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.ads.run( - cmd, spl, length, authed, msg, agora, tx, ux, tx.lbtc - ) - - class xmr(object): - name = "xmr" - authed = True - helptext = "Get current XMR price." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - xmr_prices = agora.money.cg.get_price( - ids="monero", vs_currencies=["sek", "usd", "gbp"] - ) - price_sek = xmr_prices["monero"]["sek"] - price_usd = xmr_prices["monero"]["usd"] - price_gbp = xmr_prices["monero"]["gbp"] - msg(f"SEK: {price_sek} | USD: {price_usd} | GBP: {price_gbp}") - - class btc(object): - name = "btc" - authed = True - helptext = "Get current BTC price." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - xmr_prices = agora.money.cg.get_price( - ids="bitcoin", vs_currencies=["sek", "usd", "gbp"] - ) - price_sek = xmr_prices["bitcoin"]["sek"] - price_usd = xmr_prices["bitcoin"]["usd"] - price_gbp = xmr_prices["bitcoin"]["gbp"] - msg(f"SEK: {price_sek} | USD: {price_usd} | GBP: {price_gbp}") - - class awithdraw(object): - authed = True - name = "awithdraw" - helptext = "Take profit for Agora." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.withdraw.run( - cmd, spl, length, authed, msg, agora, tx, ux, agora - ) - - class lwithdraw(object): - authed = True - name = "lwithdraw" - helptext = "Take profit for LBTC." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - GenericCommands.withdraw.run( - cmd, spl, length, authed, msg, agora, tx, ux, tx.lbtc - ) - - class remaining(object): - name = "r" - authed = True - helptext = "Show how much is left before we are able to withdraw funds." - - @staticmethod - def got_remaining(data_remaining, msg): - msg(f"Remaining: {data_remaining}USD") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - remaining = tx.money.get_remaining() - remaining.addCallback(IRCCommands.remaining.got_remaining, msg) - - class total_remaining(object): - name = "tr" - authed = True - helptext = "Show how much is left before we are able to withdraw funds (including open trades)." - - @staticmethod - def got_total_remaining(data_remaining, msg): - msg(f"Total remaining: {data_remaining}USD") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - remaining = tx.money.get_total_remaining() - remaining.addCallback(IRCCommands.total_remaining.got_total_remaining, msg) - - class tradetotal(object): - name = "tradetotal" - authed = True - helptext = "Get total value of all open trades in USD." - - @staticmethod - def got_tradetotal(data_tradetotal, msg): - msg(f"Total trades: {data_tradetotal}USD") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - total = tx.money.get_open_trades_usd() - total.addCallback(IRCCommands.tradetotal.got_tradetotal, msg) - - class dollar(object): - name = "$" - authed = True - helptext = "Get total value of everything, including open trades." - - @staticmethod - def got_dollar(data_dollar, msg): - msg(f"${data_dollar}") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - total = tx.money.get_total_with_trades() - total.addCallback(IRCCommands.dollar.got_dollar, msg) - - class profit(object): - name = "profit" - authed = True - helptext = "Get total profit." - - @staticmethod - def got_profit(data_profit, msg): - msg(f"Profit: {data_profit}USD") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - total = tx.money.get_profit() - total.addCallback(IRCCommands.profit.got_profit, msg) - - class tprofit(object): - name = "tprofit" - authed = True - helptext = "Get total profit with open trades." - - @staticmethod - def got_tprofit(data_tprofit, msg): - msg(f"Total profit: {data_tprofit}USD") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - total = tx.money.get_profit(True) - total.addCallback(IRCCommands.tprofit.got_tprofit, msg) - - class signin(object): - name = "signin" - authed = True - helptext = "Generate a TrueLayer signin URL. Usage: signin " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 2: - account = spl[1] - auth_url = tx.sinks.truelayer.create_auth_url(account) - msg(f"Auth URL for {account}: {auth_url}") - - class nsignin(object): - name = "nsignin" - authed = True - helptext = "Generate a Nordigen signin URL. Usage: nsignin " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length >= 3: - country = spl[1] - bank_name = " ".join(spl[2:]) - auth_url = tx.sinks.nordigen.create_auth_url(country, bank_name) - if not auth_url: - msg("Could not find bank.") - return - msg(f"Auth URL for {bank_name}: {auth_url}") - - class accounts(object): - name = "accounts" - authed = True - helptext = "Get a list of acccounts from TrueLayer. Usage: accounts " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 2: - account = spl[1] - accounts = tx.sinks.truelayer.get_accounts(account) - for account in accounts["results"]: - msg( - f"{account['account_id']} {account['display_name']} {account['currency']}" - ) - - class naccounts(object): - name = "naccounts" - authed = True - helptext = "Get a list of acccounts from Nordigen. Usage: naccounts" - - @staticmethod - def got_accounts(accounts, msg): - for name, accounts in accounts.items(): - for account in accounts: - msg(dumps(account)) - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 1: - accounts = tx.sinks.nordigen.get_all_account_info() - accounts.addCallback(IRCCommands.naccounts.got_accounts, msg) - - class transactions(object): - name = "transactions" - authed = True - helptext = "Get a list of transactions from TrueLayer. Usage: transactions " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 3: - account = spl[1] - account_id = spl[2] - transactions = tx.sinks.truelayer.get_transactions(account, account_id) - for transaction in transactions: - txid = transaction["transaction_id"] - # ptxid = transaction["meta"]["provider_transaction_id"] - txtype = transaction["transaction_type"] - timestamp = transaction["timestamp"] - amount = transaction["amount"] - currency = transaction["currency"] - description = transaction["description"] - msg(f"{timestamp} {txid} {txtype} {amount}{currency} {description}") - - class ntransactions(object): - name = "ntransactions" - authed = True - helptext = "Get a list of transactions from Nordigen. Usage: ntransactions " - - @staticmethod - def got_transactions(transactions, msg): - for transaction in transactions: - if "transaction_id" in transaction: - txid = transaction["transaction_id"] - else: - txid = "not_set" - timestamp = transaction["timestamp"] - amount = transaction["amount"] - currency = transaction["currency"] - reference = transaction["reference"] - msg(f"{timestamp} {txid} {amount}{currency} {reference}") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 2: - account_id = spl[1] - transactions = tx.sinks.nordigen.get_transactions(account_id) - transactions.addCallback( - IRCCommands.ntransactions.got_transactions, msg - ) - - class nreqs(object): - name = "nreqs" - authed = True - helptext = "Get a list of requisitions from Nordigen." - - @staticmethod - def got_requisitions(reqs, msg): - for req in reqs: - id = req["id"] - institution_id = req["institution_id"] - redirect = req["link"] - msg(f"{id} {institution_id} {redirect}") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - reqs = tx.sinks.nordigen.get_requisitions() - reqs.addCallback(IRCCommands.nreqs.got_requisitions, msg) - - class ndelreq(object): - name = "ndelreq" - authed = True - helptext = "Delete a requisition from Nordigen." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 2: - requisition_id = spl[1] - rtrn = tx.sinks.nordigen.delete_requisition(requisition_id) - msg(f"{rtrn['summary']}") - - class mapaccount(object): - name = "mapaccount" - authed = True - helptext = "Enable an account_id at a bank for use in TrueLayer. Usage: mapaccount " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 3: - bank = spl[1] - account_id = spl[2] - account_name = tx.sinks.truelayer.map_account(bank, account_id) - if not account_name: - msg("Failed to map the account") - return - msg(f"Mapped account ID {account_id} at bank {bank} to {account_name}") - - class nmapaccount(object): - name = "nmapaccount" - authed = True - helptext = "Enable an account_id at a bank for use in Nordigen. Usage: nmapaccount " - - @staticmethod - def got_map_account(account_name, account_id, msg): - if not account_name: - msg("Failed to map the account") - return - msg(f"Mapped account ID {account_id} to {account_name}") - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 2: - account_id = spl[1] - c = tx.sinks.nordigen.map_account(account_id) - c.addCallback(IRCCommands.nmapaccount.got_map_account, account_id, msg) - - class nunmapaccount(object): - name = "nunmapaccount" - authed = True - helptext = "Disable an account_id at a bank for use in Nordigen. Usage: nunmapaccount " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 2: - account_id = spl[1] - tx.sinks.nordigen.unmap_account(account_id) - msg(f"Unmapped account ID {account_id}") - - class unmapped(object): - name = "unmapped" - authed = True - helptext = "Get unmapped accounts for a bank. Usage: unmapped " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 2: - bank = spl[1] - accounts_active = [] - for bank, accounts in tx.sinks.truelayer.banks.items(): - for account in accounts: - accounts_active.append(account) - accounts_all = tx.sinks.truelayer.get_accounts(bank) - accounts_unmapped = [ - x["account_id"] - for x in accounts_all["results"] - if x["account_id"] not in accounts_active - ] - msg(f"Unmapped accounts: {', '.join(accounts_unmapped)}") - - class distdetails(object): - name = "distdetails" - authed = True - helptext = "Distribute account details among all ads." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - currencies = tx.sinks.currencies - tx.markets.distribute_account_details() - msg(f"Distributing account details for currencies: {', '.join(currencies)}") - - class authlink(object): - name = "authlink" - authed = True - helptext = ( - "Create a URL for identity verification. Usage: authlink " - ) - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 2: - identifier = spl[1] - auth_url = ux.verify.create_applicant_and_get_link(identifier) - msg(f"Verification URL: {auth_url}") - - class checkauth(object): - name = "checkauth" - authed = True - helptext = ( - "Check the authentication for an identifier. Usage: checkauth " - ) - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 2: - identifier = spl[1] - verified = ux.verify.get_external_user_id_status(identifier) - msg(f"Verification status: {verified}") - - class bankdetails(object): - name = "bankdetails" - authed = True - helptext = "Get the bank details for a platform. Usage: bankdetails " - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 3: - platform = spl[1] - currency = spl[2] - account_info = tx.markets.get_matching_account_details( - platform, currency - ) - bank_details = tx.markets.format_payment_details( - currency, account_info, real=True - ) - if not bank_details: - msg("Could not get bank details.") - return - msg(bank_details) - - class id(object): - name = "id" - authed = True - helptext = ( - "Get the identity information of a user. Usage: id " - ) - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - if length == 3: - platform = spl[1] - username = spl[2] - uid = tx.ux.verify.create_uid(platform, username) - first, last = tx.ux.verify.get_external_user_id_details(uid) - msg(f"Name: {first} {last}") - - class balance(object): - name = "balance" - authed = True - helptext = "Get the total balance of all fiat accounts." - - @staticmethod - def run(cmd, spl, length, authed, msg, agora, tx, ux): - total = ux.sinks.get_total_usd() - msg(f"Fiat balance (USD): {total}") diff --git a/handler/ux/irc.py b/handler/ux/irc.py deleted file mode 100644 index abdeab7..0000000 --- a/handler/ux/irc.py +++ /dev/null @@ -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 diff --git a/handler/ux/notify.py b/handler/ux/notify.py deleted file mode 100644 index 9071a93..0000000 --- a/handler/ux/notify.py +++ /dev/null @@ -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", - ) diff --git a/handler/ux/verify.py b/handler/ux/verify.py deleted file mode 100644 index 156898e..0000000 --- a/handler/ux/verify.py +++ /dev/null @@ -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