Remove legacy codebase
This commit is contained in:
parent
85c64efc78
commit
f41e69b003
141
handler/app.py
141
handler/app.py
|
@ -1,141 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# Twisted/Klein imports
|
|
||||||
# Other library imports
|
|
||||||
from json import dumps
|
|
||||||
from signal import SIGINT, signal
|
|
||||||
from sys import argv
|
|
||||||
|
|
||||||
import lib.antifraud
|
|
||||||
import lib.logstash
|
|
||||||
import lib.markets
|
|
||||||
import lib.money
|
|
||||||
import lib.transactions
|
|
||||||
|
|
||||||
# New style classes
|
|
||||||
import sinks
|
|
||||||
import sources
|
|
||||||
import util
|
|
||||||
import ux
|
|
||||||
from klein import Klein
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
from twisted.internet import reactor
|
|
||||||
from twisted.internet.protocol import Factory
|
|
||||||
|
|
||||||
init_map = None
|
|
||||||
Factory.noisy = False
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: extend this with more
|
|
||||||
def cleanup(sig, frame):
|
|
||||||
to_cleanup = []
|
|
||||||
if hasattr(init_map["money"], "lc_es_checks"):
|
|
||||||
to_cleanup.append(init_map["money"].lc_es_checks.stop)
|
|
||||||
if hasattr(init_map["sources"].agora, "lc_dash"):
|
|
||||||
to_cleanup.append(init_map["sources"].agora.lc_dash.stop)
|
|
||||||
if hasattr(init_map["sources"].agora, "lc_cheat"):
|
|
||||||
to_cleanup.append(init_map["sources"].agora.lc_cheat.stop)
|
|
||||||
if hasattr(init_map["sources"].lbtc, "lc_dash"):
|
|
||||||
to_cleanup.append(init_map["sources"].lbtc.lc_dash.stop)
|
|
||||||
if hasattr(init_map["sources"].lbtc, "lc_cheat"):
|
|
||||||
to_cleanup.append(init_map["sources"].lbtc.lc_cheat.stop)
|
|
||||||
if hasattr(init_map["sinks"], "truelayer"):
|
|
||||||
if hasattr(init_map["sinks"].truelayer, "lc"):
|
|
||||||
to_cleanup.append(init_map["sinks"].truelayer.lc.stop)
|
|
||||||
if hasattr(init_map["sinks"].truelayer, "lc_tx"):
|
|
||||||
to_cleanup.append(init_map["sinks"].truelayer.lc_tx.stop)
|
|
||||||
if hasattr(init_map["sinks"], "nordigen"):
|
|
||||||
if hasattr(init_map["sinks"].nordigen, "lc"):
|
|
||||||
to_cleanup.append(init_map["sinks"].nordigen.lc.stop)
|
|
||||||
if hasattr(init_map["sinks"].nordigen, "lc_tx"):
|
|
||||||
to_cleanup.append(init_map["sinks"].nordigen.lc_tx.stop)
|
|
||||||
if init_map:
|
|
||||||
for func in to_cleanup:
|
|
||||||
try:
|
|
||||||
func()
|
|
||||||
except: # noqa
|
|
||||||
print(f"Exception when stopping {func}")
|
|
||||||
pass # noqa
|
|
||||||
reactor.stop()
|
|
||||||
|
|
||||||
|
|
||||||
signal(SIGINT, cleanup) # Handle Ctrl-C and run the cleanup routine
|
|
||||||
|
|
||||||
|
|
||||||
class WebApp(util.Base):
|
|
||||||
"""
|
|
||||||
Our Klein webapp.
|
|
||||||
"""
|
|
||||||
|
|
||||||
app = Klein()
|
|
||||||
|
|
||||||
# @app.route("/callback", methods=["POST"])
|
|
||||||
# def callback(self, request):
|
|
||||||
# content = request.content.read()
|
|
||||||
# try:
|
|
||||||
# parsed = loads(content)
|
|
||||||
# except JSONDecodeError:
|
|
||||||
# self.log.error(f"Failed to parse JSON callback: {content}")
|
|
||||||
# return dumps(False)
|
|
||||||
# self.log.info("Callback received: {parsed}", parsed=parsed["data"]["id"])
|
|
||||||
# # self.tx.transaction(parsed)
|
|
||||||
# return dumps(True)
|
|
||||||
|
|
||||||
# set up another connection to a bank
|
|
||||||
@app.route("/signin/<string:account>", methods=["GET"])
|
|
||||||
def signin(self, request, account):
|
|
||||||
auth_url = self.sinks.truelayer.create_auth_url(account)
|
|
||||||
return f'Please sign in to {account} <a href="{auth_url}" target="_blank">here.</a>'
|
|
||||||
|
|
||||||
# endpoint called after we finish setting up a connection above
|
|
||||||
@app.route("/callback-truelayer", methods=["POST"])
|
|
||||||
def signin_callback_truelayer(self, request):
|
|
||||||
code = request.args[b"code"]
|
|
||||||
self.sinks.truelayer.handle_authcode_received(code)
|
|
||||||
return dumps(True)
|
|
||||||
|
|
||||||
@app.route("/callback-nordigen", methods=["GET"])
|
|
||||||
def signin_callback_nordigen(self, request):
|
|
||||||
# code = request.args[b"code"]
|
|
||||||
# self.sinks.truelayer.handle_authcode_received(code)
|
|
||||||
return dumps(True)
|
|
||||||
|
|
||||||
@app.route("/callback-verify", methods=["POST"])
|
|
||||||
def callback_verify(self, request):
|
|
||||||
# code = request.args[b"code"]
|
|
||||||
rtrn = self.ux.verify.handle_callback(request)
|
|
||||||
return dumps(rtrn)
|
|
||||||
|
|
||||||
@app.route("/accounts/<string:account>", methods=["GET"])
|
|
||||||
def balance(self, request, account):
|
|
||||||
accounts = self.sinks.truelayer.get_accounts(account)
|
|
||||||
return dumps(accounts, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if "--debug" in argv:
|
|
||||||
util.debug = True
|
|
||||||
init_map = {
|
|
||||||
"ux": ux.UX(),
|
|
||||||
"markets": lib.markets.Markets(),
|
|
||||||
"sources": sources.Sources(),
|
|
||||||
"sinks": sinks.Sinks(),
|
|
||||||
"tx": lib.transactions.Transactions(),
|
|
||||||
"webapp": WebApp(),
|
|
||||||
"money": lib.money.Money(),
|
|
||||||
"antifraud": lib.antifraud.AntiFraud(),
|
|
||||||
}
|
|
||||||
# Merge all classes into each other
|
|
||||||
util.xmerge_attrs(init_map)
|
|
||||||
|
|
||||||
# Let the classes know they have been merged
|
|
||||||
for class_name, class_instance in init_map.items():
|
|
||||||
if hasattr(class_instance, "__xmerged__"):
|
|
||||||
class_instance.__xmerged__()
|
|
||||||
|
|
||||||
# Set up the loops to put data in ES
|
|
||||||
# init_map["tx"].setup_loops()
|
|
||||||
lib.logstash.init_logstash()
|
|
||||||
# Run the WebApp
|
|
||||||
init_map["webapp"].app.run(settings.App.BindHost, 8080)
|
|
139
handler/db.py
139
handler/db.py
|
@ -1,139 +0,0 @@
|
||||||
# Other library imports
|
|
||||||
import util
|
|
||||||
from redis import StrictRedis
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
log = util.get_logger("DB")
|
|
||||||
|
|
||||||
# Define the Redis endpoint to the socket
|
|
||||||
r = StrictRedis(unix_socket_path=settings.DB.RedisSocket, db=int(settings.DB.DB))
|
|
||||||
|
|
||||||
|
|
||||||
def get_refs():
|
|
||||||
"""
|
|
||||||
Get all reference IDs for trades.
|
|
||||||
:return: list of trade IDs
|
|
||||||
:rtype: list
|
|
||||||
"""
|
|
||||||
references = []
|
|
||||||
ref_keys = r.keys("trade.*.reference")
|
|
||||||
for key in ref_keys:
|
|
||||||
references.append(r.get(key))
|
|
||||||
return util.convert(references)
|
|
||||||
|
|
||||||
|
|
||||||
def tx_to_ref(tx):
|
|
||||||
"""
|
|
||||||
Convert a trade ID to a reference.
|
|
||||||
:param tx: trade ID
|
|
||||||
:type tx: string
|
|
||||||
:return: reference
|
|
||||||
:rtype: string
|
|
||||||
"""
|
|
||||||
refs = get_refs()
|
|
||||||
for reference in refs:
|
|
||||||
ref_data = util.convert(r.hgetall(f"trade.{reference}"))
|
|
||||||
if not ref_data:
|
|
||||||
continue
|
|
||||||
if ref_data["id"] == tx:
|
|
||||||
return reference
|
|
||||||
|
|
||||||
|
|
||||||
def ref_to_tx(reference):
|
|
||||||
"""
|
|
||||||
Convert a reference to a trade ID.
|
|
||||||
:param reference: trade reference
|
|
||||||
:type reference: string
|
|
||||||
:return: trade ID
|
|
||||||
:rtype: string
|
|
||||||
"""
|
|
||||||
ref_data = util.convert(r.hgetall(f"trade.{reference}"))
|
|
||||||
if not ref_data:
|
|
||||||
return False
|
|
||||||
return ref_data["id"]
|
|
||||||
|
|
||||||
|
|
||||||
def get_ref_map():
|
|
||||||
"""
|
|
||||||
Get all reference IDs for trades.
|
|
||||||
:return: dict of references keyed by TXID
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
references = {}
|
|
||||||
ref_keys = r.keys("trade.*.reference")
|
|
||||||
for key in ref_keys:
|
|
||||||
tx = util.convert(key).split(".")[1]
|
|
||||||
references[tx] = r.get(key)
|
|
||||||
return util.convert(references)
|
|
||||||
|
|
||||||
|
|
||||||
def get_ref(reference):
|
|
||||||
"""
|
|
||||||
Get the trade information for a reference.
|
|
||||||
:param reference: trade reference
|
|
||||||
:type reference: string
|
|
||||||
:return: dict of trade information
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
ref_data = r.hgetall(f"trade.{reference}")
|
|
||||||
ref_data = util.convert(ref_data)
|
|
||||||
if "subclass" not in ref_data:
|
|
||||||
ref_data["subclass"] = "agora"
|
|
||||||
if not ref_data:
|
|
||||||
return False
|
|
||||||
return ref_data
|
|
||||||
|
|
||||||
|
|
||||||
def get_tx(tx):
|
|
||||||
"""
|
|
||||||
Get the transaction information for a transaction ID.
|
|
||||||
:param reference: trade reference
|
|
||||||
:type reference: string
|
|
||||||
:return: dict of trade information
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
tx_data = r.hgetall(f"tx.{tx}")
|
|
||||||
tx_data = util.convert(tx_data)
|
|
||||||
if not tx_data:
|
|
||||||
return False
|
|
||||||
return tx_data
|
|
||||||
|
|
||||||
|
|
||||||
def get_subclass(reference):
|
|
||||||
obj = r.hget(f"trade.{reference}", "subclass")
|
|
||||||
subclass = util.convert(obj)
|
|
||||||
return subclass
|
|
||||||
|
|
||||||
|
|
||||||
def del_ref(reference):
|
|
||||||
"""
|
|
||||||
Delete a given reference from the Redis database.
|
|
||||||
:param reference: trade reference to delete
|
|
||||||
:type reference: string
|
|
||||||
"""
|
|
||||||
tx = ref_to_tx(reference)
|
|
||||||
r.delete(f"trade.{reference}")
|
|
||||||
r.delete(f"trade.{tx}.reference")
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup(subclass, references):
|
|
||||||
"""
|
|
||||||
Reconcile the internal reference database with a given list of references.
|
|
||||||
Delete all internal references not present in the list and clean up artifacts.
|
|
||||||
:param references: list of references to reconcile against
|
|
||||||
:type references: list
|
|
||||||
"""
|
|
||||||
messages = []
|
|
||||||
for tx, reference in get_ref_map().items():
|
|
||||||
if reference not in references:
|
|
||||||
if get_subclass(reference) == subclass:
|
|
||||||
logmessage = (
|
|
||||||
f"[{reference}] ({subclass}): Archiving trade reference. TX: {tx}"
|
|
||||||
)
|
|
||||||
messages.append(logmessage)
|
|
||||||
log.info(logmessage)
|
|
||||||
r.rename(f"trade.{tx}.reference", f"archive.trade.{tx}.reference")
|
|
||||||
r.rename(f"trade.{reference}", f"archive.trade.{reference}")
|
|
||||||
return messages
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,113 +0,0 @@
|
||||||
# Project imports
|
|
||||||
import db
|
|
||||||
import util
|
|
||||||
|
|
||||||
|
|
||||||
class AntiFraud(util.Base):
|
|
||||||
def add_bank_sender(self, platform, platform_buyer, bank_sender):
|
|
||||||
"""
|
|
||||||
Add the bank senders into Redis.
|
|
||||||
:param platform: name of the platform - freeform
|
|
||||||
:param platform_buyer: the username of the buyer on the platform
|
|
||||||
:param bank_sender: the sender name from the bank
|
|
||||||
"""
|
|
||||||
key = f"namemap.{platform}.{platform_buyer}"
|
|
||||||
db.r.sadd(key, bank_sender)
|
|
||||||
|
|
||||||
def get_previous_senders(self, platform, platform_buyer):
|
|
||||||
"""
|
|
||||||
Get all the previous bank sender names for the given buyer on the platform.
|
|
||||||
:param platform: name of the platform - freeform
|
|
||||||
:param platform_buyer: the username of the buyer on the platform
|
|
||||||
:return: set of previous buyers
|
|
||||||
:rtype: set
|
|
||||||
"""
|
|
||||||
key = f"namemap.{platform}.{platform_buyer}"
|
|
||||||
senders = db.r.smembers(key)
|
|
||||||
if not senders:
|
|
||||||
return None
|
|
||||||
senders = util.convert(senders)
|
|
||||||
return senders
|
|
||||||
|
|
||||||
def check_valid_sender(self, reference, platform, bank_sender, platform_buyer):
|
|
||||||
"""
|
|
||||||
Check that either:
|
|
||||||
* The platform buyer has never had a recognised transaction before
|
|
||||||
* The bank sender name matches a previous transaction from the platform buyer
|
|
||||||
:param reference: the trade reference
|
|
||||||
:param platform: name of the platform - freeform
|
|
||||||
:param bank_sender: the sender of the bank transaction
|
|
||||||
:param platform_buyer: the username of the buyer on the platform
|
|
||||||
:return: whether the sender is valid
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
senders = self.get_previous_senders(platform, platform_buyer)
|
|
||||||
if senders is None: # no senders yet, assume it's valid
|
|
||||||
return True
|
|
||||||
if platform_buyer in senders:
|
|
||||||
return True
|
|
||||||
self.ux.notify.notify_sender_name_mismatch(
|
|
||||||
reference, platform_buyer, bank_sender
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def check_tx_sender(self, tx, reference):
|
|
||||||
"""
|
|
||||||
Check whether the sender of a given transaction is authorised based on the previous
|
|
||||||
transactions of the username that originated the trade reference.
|
|
||||||
:param tx: the transaction ID
|
|
||||||
:param reference: the trade reference
|
|
||||||
"""
|
|
||||||
stored_trade = db.get_ref(reference)
|
|
||||||
if not stored_trade:
|
|
||||||
return None
|
|
||||||
stored_tx = db.get_tx(tx)
|
|
||||||
if not stored_tx:
|
|
||||||
return None
|
|
||||||
bank_sender = stored_tx["sender"]
|
|
||||||
platform_buyer = stored_trade["buyer"]
|
|
||||||
platform = stored_trade["subclass"]
|
|
||||||
is_allowed = self.check_valid_sender(
|
|
||||||
reference, platform, bank_sender, platform_buyer
|
|
||||||
)
|
|
||||||
if is_allowed is True:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def update_trade_tx(self, reference, txid):
|
|
||||||
"""
|
|
||||||
Update a trade to point to a given transaction ID.
|
|
||||||
Return False if the trade already has a mapped transaction.
|
|
||||||
"""
|
|
||||||
existing_tx = db.r.hget(f"trade.{reference}", "tx")
|
|
||||||
if existing_tx is None:
|
|
||||||
return None
|
|
||||||
elif existing_tx == b"":
|
|
||||||
db.r.hset(f"trade.{reference}", "tx", txid)
|
|
||||||
return True
|
|
||||||
else: # Already a mapped transaction
|
|
||||||
return False
|
|
||||||
|
|
||||||
def user_verification_successful(self, uid):
|
|
||||||
"""
|
|
||||||
A user has successfully completed verification.
|
|
||||||
"""
|
|
||||||
self.log.info(f"User has completed verification: {uid}")
|
|
||||||
trade_list = self.markets.find_trades_by_uid(uid)
|
|
||||||
for platform, trade_id, reference, currency in trade_list:
|
|
||||||
self.markets.send_bank_details(platform, currency, trade_id)
|
|
||||||
self.markets.send_reference(platform, trade_id, reference)
|
|
||||||
|
|
||||||
def send_verification_url(self, platform, uid, trade_id):
|
|
||||||
send_setting, post_message = self.markets.get_send_settings(platform)
|
|
||||||
if send_setting == "1":
|
|
||||||
auth_url = self.ux.verify.create_applicant_and_get_link(uid)
|
|
||||||
if platform == "lbtc":
|
|
||||||
auth_url = auth_url.replace("https://", "") # hack
|
|
||||||
post_message(
|
|
||||||
trade_id,
|
|
||||||
f"Hi! To continue the trade, please complete the verification form: {auth_url}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
antifraud = AntiFraud()
|
|
|
@ -1,884 +0,0 @@
|
||||||
"""See https://agoradesk.com/api-docs/v1."""
|
|
||||||
# pylint: disable=too-many-lines
|
|
||||||
# Large API. Lots of lines can't be avoided.
|
|
||||||
import hashlib
|
|
||||||
import hmac as hmac_lib
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from typing import Any, Dict, List, Optional, Union
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import arrow
|
|
||||||
import requests
|
|
||||||
import treq
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
import util
|
|
||||||
from twisted.internet.defer import inlineCallbacks
|
|
||||||
|
|
||||||
__author__ = "marvin8"
|
|
||||||
__copyright__ = "(C) 2021 https://codeberg.org/MarvinsCryptoTools/agoradesk_py"
|
|
||||||
__version__ = "0.1.0"
|
|
||||||
|
|
||||||
# set logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
||||||
)
|
|
||||||
logging.getLogger("requests.packages.urllib3").setLevel(logging.INFO)
|
|
||||||
logging.getLogger("urllib3.connectionpool").setLevel(logging.INFO)
|
|
||||||
logger = util.get_logger(__name__)
|
|
||||||
|
|
||||||
URI_API = "https://localbitcoins.com/"
|
|
||||||
SERVER = "https://localbitcoins.com"
|
|
||||||
|
|
||||||
|
|
||||||
class LocalBitcoins:
|
|
||||||
"""LocalBitcoins API object.
|
|
||||||
|
|
||||||
Documentation: https://localbitcoins.com/api-docs/
|
|
||||||
"""
|
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods
|
|
||||||
# API provides this many methods, I can't change that
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hmac_key: Optional[str],
|
|
||||||
hmac_secret: Optional[str],
|
|
||||||
debug: Optional[bool] = False,
|
|
||||||
) -> None:
|
|
||||||
self.hmac_key = ""
|
|
||||||
self.hmac_secret = ""
|
|
||||||
if hmac_key:
|
|
||||||
self.hmac_key = hmac_key.encode("ascii")
|
|
||||||
if hmac_secret:
|
|
||||||
self.hmac_secret = hmac_secret.encode("ascii")
|
|
||||||
self.debug = debug
|
|
||||||
|
|
||||||
if self.debug:
|
|
||||||
logging.getLogger("requests.packages.urllib3").setLevel(logging.DEBUG)
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
else:
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
"creating instance of LocalBitcoins API with api_key %s",
|
|
||||||
self.hmac_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
def sign_payload(self, nonce, url, params_encoded):
|
|
||||||
"""
|
|
||||||
Sign the payload with our HMAC keys.
|
|
||||||
"""
|
|
||||||
# Calculate signature
|
|
||||||
message = nonce + self.hmac_key + url.encode("ascii")
|
|
||||||
if params_encoded:
|
|
||||||
if sys.version_info >= (3, 0) and isinstance(params_encoded, str):
|
|
||||||
message += params_encoded.encode("ascii")
|
|
||||||
else:
|
|
||||||
message += params_encoded
|
|
||||||
signature = (
|
|
||||||
hmac_lib.new(self.hmac_secret, msg=message, digestmod=hashlib.sha256)
|
|
||||||
.hexdigest()
|
|
||||||
.upper()
|
|
||||||
)
|
|
||||||
return signature
|
|
||||||
|
|
||||||
def encode_params(self, http_method, api_call_url, query_values):
|
|
||||||
if http_method == "POST":
|
|
||||||
api_request = requests.Request(
|
|
||||||
"POST", api_call_url, data=query_values
|
|
||||||
).prepare()
|
|
||||||
params_encoded = api_request.body
|
|
||||||
|
|
||||||
# GET method
|
|
||||||
else:
|
|
||||||
api_request = requests.Request(
|
|
||||||
"GET", api_call_url, params=query_values
|
|
||||||
).prepare()
|
|
||||||
params_encoded = urlparse(api_request.url).query
|
|
||||||
return (api_request, params_encoded)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def callback_api_call(self, response, result):
|
|
||||||
logger.debug(response)
|
|
||||||
text = yield response.content()
|
|
||||||
try:
|
|
||||||
result["response"] = json.loads(text)
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
result["success"] = "ERROR"
|
|
||||||
result["message"] = "Error parsing JSON."
|
|
||||||
return result
|
|
||||||
result["status"] = response.code
|
|
||||||
if response.code == 200:
|
|
||||||
result["success"] = True
|
|
||||||
result["message"] = "OK"
|
|
||||||
else:
|
|
||||||
result["message"] = "API ERROR"
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _api_call(
|
|
||||||
self,
|
|
||||||
api_method: str,
|
|
||||||
http_method: Optional[str] = "GET",
|
|
||||||
query_values: Optional[Dict[str, Any]] = None,
|
|
||||||
files: Optional[Any] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
api_call_url = URI_API + api_method
|
|
||||||
|
|
||||||
url = api_call_url
|
|
||||||
if url.startswith(SERVER):
|
|
||||||
url = url[len(SERVER) :] # noqa
|
|
||||||
|
|
||||||
# HMAC crypto stuff
|
|
||||||
api_request, params_encoded = self.encode_params(
|
|
||||||
http_method, api_call_url, query_values
|
|
||||||
)
|
|
||||||
nonce = str(int(time.time() * 1000)).encode("ascii")
|
|
||||||
signature = self.sign_payload(nonce, url, params_encoded)
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Apiauth-Key": self.hmac_key,
|
|
||||||
"Apiauth-Nonce": nonce,
|
|
||||||
"Apiauth-Signature": signature,
|
|
||||||
}
|
|
||||||
|
|
||||||
api_request.headers["Apiauth-Key"] = self.hmac_key
|
|
||||||
api_request.headers["Apiauth-Nonce"] = nonce
|
|
||||||
api_request.headers["Apiauth-Signature"] = signature
|
|
||||||
|
|
||||||
logger.debug("API Call URL: %s", api_call_url)
|
|
||||||
logger.debug("Headers : %s", api_request.headers)
|
|
||||||
logger.debug("HTTP Method : %s", http_method)
|
|
||||||
logger.debug("Query Values: %s", query_values)
|
|
||||||
logger.debug("Query Values as Json:\n%s", json.dumps(query_values))
|
|
||||||
|
|
||||||
result: Dict[str, Any] = {
|
|
||||||
"success": False,
|
|
||||||
"message": "Invalid Method",
|
|
||||||
"response": None,
|
|
||||||
"status": None,
|
|
||||||
}
|
|
||||||
response = None
|
|
||||||
if http_method == "POST":
|
|
||||||
if query_values:
|
|
||||||
# response = httpx.post(
|
|
||||||
# url=api_call_url,
|
|
||||||
# headers=headers,
|
|
||||||
# content=json.dumps(query_values),
|
|
||||||
# )
|
|
||||||
response = treq.post(
|
|
||||||
api_call_url,
|
|
||||||
headers=headers,
|
|
||||||
data=query_values,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# response = httpx.post(
|
|
||||||
# url=api_call_url,
|
|
||||||
# headers=headers,
|
|
||||||
# )
|
|
||||||
response = treq.post(
|
|
||||||
api_call_url,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# response = httpx.get(url=api_call_url, headers=headers, params=query_values)
|
|
||||||
response = treq.get(api_call_url, headers=headers, params=query_values)
|
|
||||||
if response:
|
|
||||||
response.addCallback(self.callback_api_call, result)
|
|
||||||
return response
|
|
||||||
|
|
||||||
# Account related API Methods
|
|
||||||
# ===========================
|
|
||||||
|
|
||||||
def account_info(self, username: str) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#account_info
|
|
||||||
"""
|
|
||||||
return self._api_call(api_method=f"api/account_info/{username}/")
|
|
||||||
|
|
||||||
def dashboard(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#dashboard
|
|
||||||
"""
|
|
||||||
return self._api_call(api_method="api/dashboard/")
|
|
||||||
|
|
||||||
# def dashboard_buyer(self) -> Dict[str, Any]:
|
|
||||||
# """See LocalBitcoins API.
|
|
||||||
#
|
|
||||||
# https://agoradesk.com/api-docs/v1#operation/getUserDashboardBuyer
|
|
||||||
# """
|
|
||||||
# return self._api_call(api_method="dashboard/buyer")
|
|
||||||
|
|
||||||
# def dashboard_seller(self) -> Dict[str, Any]:
|
|
||||||
# """See LocalBitcoins API.
|
|
||||||
#
|
|
||||||
# https://agoradesk.com/api-docs/v1#operation/getUserDashboardSeller
|
|
||||||
# """
|
|
||||||
# return self._api_call(api_method="dashboard/seller")
|
|
||||||
|
|
||||||
def dashboard_canceled(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#dashboard-canceled
|
|
||||||
"""
|
|
||||||
return self._api_call(api_method="api/dashboard/canceled/")
|
|
||||||
|
|
||||||
def dashboard_closed(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#dashboard-closed
|
|
||||||
"""
|
|
||||||
return self._api_call(api_method="api/dashboard/closed/")
|
|
||||||
|
|
||||||
def dashboard_released(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#dashboard-released
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._api_call(api_method="api/dashboard/released/")
|
|
||||||
|
|
||||||
def logout(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#logout
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._api_call(api_method="api/logout/", http_method="POST")
|
|
||||||
|
|
||||||
def myself(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#myself
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._api_call(api_method="api/myself/")
|
|
||||||
|
|
||||||
def notifications(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#notifications
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._api_call(api_method="api/notifications/")
|
|
||||||
|
|
||||||
def notifications_mark_as_read(self, notification_id: str) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#notifications-read
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._api_call(
|
|
||||||
api_method=f"notifications/mark_as_read/{notification_id}/",
|
|
||||||
http_method="POST",
|
|
||||||
)
|
|
||||||
|
|
||||||
def recent_messages(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#recent-messages
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._api_call(api_method="api/recent_messages/")
|
|
||||||
|
|
||||||
# Trade related API Methods
|
|
||||||
# ===========================
|
|
||||||
|
|
||||||
# post/feedback/{username} • Give feedback to a user
|
|
||||||
def feedback(
|
|
||||||
self, username: str, feedback: str, msg: Optional[str]
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#feedback
|
|
||||||
"""
|
|
||||||
|
|
||||||
params = {"feedback": feedback}
|
|
||||||
if msg:
|
|
||||||
params["msg"] = msg
|
|
||||||
return self._api_call(
|
|
||||||
api_method=f"feedback/{username}/",
|
|
||||||
http_method="POST",
|
|
||||||
query_values=params,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Todo:
|
|
||||||
# post/trade/contact_release/{trade_id} • Release trade escrow
|
|
||||||
# post/contact_fund/{trade_id} • Fund a trade
|
|
||||||
# post/contact_dispute/{trade_id} • Start a trade dispute
|
|
||||||
|
|
||||||
# post/contact_mark_as_paid/{trade_id} • Mark a trade as paid
|
|
||||||
def contact_mark_as_paid(self, trade_id: str) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#contact-paid
|
|
||||||
"""
|
|
||||||
return self._api_call(
|
|
||||||
api_method=f"contact_mark_as_paid/{trade_id}/", http_method="POST"
|
|
||||||
)
|
|
||||||
|
|
||||||
# post/contact_cancel/{trade_id} • Cancel the trade
|
|
||||||
def contact_cancel(
|
|
||||||
self,
|
|
||||||
trade_id: str,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#contact-cancel
|
|
||||||
"""
|
|
||||||
return self._api_call(
|
|
||||||
api_method=f"contact_cancel/{trade_id}",
|
|
||||||
http_method="POST",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Todo:
|
|
||||||
# post/contact_escrow/{trade_id} • Enable escrow
|
|
||||||
|
|
||||||
# get/contact_messages/{trade_id} • Get trade messages
|
|
||||||
def contact_messages(
|
|
||||||
self, trade_id: str, after: Optional[arrow.Arrow] = None
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#contact-message
|
|
||||||
"""
|
|
||||||
if after:
|
|
||||||
reply = self._api_call(
|
|
||||||
api_method=f"contact_messages/{trade_id}/",
|
|
||||||
query_values={"after": after.to("UTC").isoformat()},
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
reply = self._api_call(api_method=f"contact_messages/{trade_id}/")
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
# post/contact_create/{ad_id} • Start a trade
|
|
||||||
def contact_create(
|
|
||||||
self,
|
|
||||||
ad_id: str,
|
|
||||||
amount: float,
|
|
||||||
msg: Optional[str] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#contact-create
|
|
||||||
"""
|
|
||||||
payload: Dict[str, Any] = {"amount": amount}
|
|
||||||
if msg:
|
|
||||||
payload["msg"] = msg
|
|
||||||
return self._api_call(
|
|
||||||
api_method=f"contact_create/{ad_id}/",
|
|
||||||
http_method="POST",
|
|
||||||
query_values=payload,
|
|
||||||
)
|
|
||||||
|
|
||||||
# get/contact_info/{trade_id} • Get a trade by trade ID
|
|
||||||
def contact_info(self, trade_ids: Union[str, List[str]]) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#contact-info-id and
|
|
||||||
https://localbitcoins.com/api-docs/#contact-info
|
|
||||||
"""
|
|
||||||
api_method = "contact_info/"
|
|
||||||
if isinstance(trade_ids, list):
|
|
||||||
params = "?contacts="
|
|
||||||
for trade_id in trade_ids:
|
|
||||||
params += f"{trade_id},"
|
|
||||||
params = params[0:-1]
|
|
||||||
else:
|
|
||||||
params = f"/{trade_ids}"
|
|
||||||
api_method += params
|
|
||||||
return self._api_call(api_method=api_method)
|
|
||||||
|
|
||||||
# Todo: Add image upload functionality
|
|
||||||
# post/contact_message_post/{trade_id} • Send a chat message/attachment
|
|
||||||
def contact_message_post(
|
|
||||||
self, trade_id: str, msg: Optional[str] = None
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#contact-post
|
|
||||||
"""
|
|
||||||
payload = {"msg": msg}
|
|
||||||
return self._api_call(
|
|
||||||
api_method=f"api/contact_message_post/{trade_id}/",
|
|
||||||
http_method="POST",
|
|
||||||
query_values=payload,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Todo:
|
|
||||||
# get/contact_message_attachment/{trade_id}/{attachment_id}
|
|
||||||
|
|
||||||
# Advertisement related API Methods
|
|
||||||
# ================================
|
|
||||||
|
|
||||||
def ad_create(
|
|
||||||
self,
|
|
||||||
country_code: str,
|
|
||||||
currency: str,
|
|
||||||
trade_type: str,
|
|
||||||
price_equation: str,
|
|
||||||
track_max_amount: bool,
|
|
||||||
require_trusted_by_advertiser: bool,
|
|
||||||
bank_name: str,
|
|
||||||
sms_verification_required: Optional[bool] = None,
|
|
||||||
require_identification: Optional[bool] = None,
|
|
||||||
online_provider: Optional[str] = None,
|
|
||||||
msg: Optional[str] = None,
|
|
||||||
min_amount: Optional[float] = None,
|
|
||||||
max_amount: Optional[float] = None,
|
|
||||||
account_info: Optional[str] = None,
|
|
||||||
first_time_limit_btc: Optional[float] = None,
|
|
||||||
require_feedback_score: Optional[int] = None,
|
|
||||||
city: Optional[str] = None,
|
|
||||||
location_string: Optional[str] = None,
|
|
||||||
opening_hours: Optional[dict] = None,
|
|
||||||
visible: Optional[bool] = True,
|
|
||||||
require_trade_volume: Optional[float] = None,
|
|
||||||
volume_coefficient_btc: Optional[float] = None,
|
|
||||||
reference_type: Optional[str] = None,
|
|
||||||
display_reference: Optional[bool] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#ad-create
|
|
||||||
"""
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
# pylint: disable=too-many-locals
|
|
||||||
# pylint: disable=too-many-branches
|
|
||||||
# API takes this many arguments, I can't change that
|
|
||||||
# Too many locals and too many branches goes hand in hand
|
|
||||||
# with too many arguments
|
|
||||||
params: Dict[str, Any] = {
|
|
||||||
"countrycode": country_code,
|
|
||||||
"currency": currency,
|
|
||||||
"trade_type": trade_type,
|
|
||||||
"price_equation": price_equation,
|
|
||||||
"track_max_amount": track_max_amount,
|
|
||||||
"require_trusted_by_advertiser": require_trusted_by_advertiser,
|
|
||||||
}
|
|
||||||
if sms_verification_required:
|
|
||||||
params["sms_verification_required"] = True
|
|
||||||
if require_identification:
|
|
||||||
params["require_identification"] = True
|
|
||||||
if online_provider:
|
|
||||||
params["online_provider"] = online_provider
|
|
||||||
if msg:
|
|
||||||
params["msg"] = msg
|
|
||||||
if min_amount:
|
|
||||||
params["min_amount"] = min_amount
|
|
||||||
if max_amount:
|
|
||||||
params["max_amount"] = max_amount
|
|
||||||
if first_time_limit_btc:
|
|
||||||
params["first_time_limit_btc"] = first_time_limit_btc
|
|
||||||
if require_feedback_score:
|
|
||||||
params["require_feedback_score"] = require_feedback_score
|
|
||||||
if account_info:
|
|
||||||
params["account_info"] = account_info
|
|
||||||
if opening_hours:
|
|
||||||
params["opening_hours"] = opening_hours
|
|
||||||
if city:
|
|
||||||
params["city"] = city
|
|
||||||
if location_string:
|
|
||||||
params["location_string"] = location_string
|
|
||||||
if bank_name:
|
|
||||||
params["bank_name"] = bank_name
|
|
||||||
if visible:
|
|
||||||
params["visible"] = visible
|
|
||||||
if require_trade_volume:
|
|
||||||
params["require_trade_volume"] = require_trade_volume
|
|
||||||
if volume_coefficient_btc:
|
|
||||||
params["volume_coefficient_btc"] = volume_coefficient_btc
|
|
||||||
if reference_type:
|
|
||||||
params["reference_type"] = reference_type
|
|
||||||
if display_reference is not None:
|
|
||||||
params["display_reference"] = display_reference
|
|
||||||
|
|
||||||
return self._api_call(
|
|
||||||
api_method="api/ad-create/",
|
|
||||||
http_method="POST",
|
|
||||||
query_values=params,
|
|
||||||
)
|
|
||||||
|
|
||||||
def ad(
|
|
||||||
self,
|
|
||||||
ad_id: str,
|
|
||||||
country_code: str,
|
|
||||||
currency: str,
|
|
||||||
trade_type: str,
|
|
||||||
price_equation: str,
|
|
||||||
track_max_amount: bool,
|
|
||||||
require_trusted_by_advertiser: bool,
|
|
||||||
bank_name: str,
|
|
||||||
sms_verification_required: Optional[bool] = None,
|
|
||||||
require_identification: Optional[bool] = None,
|
|
||||||
online_provider: Optional[str] = None,
|
|
||||||
msg: Optional[str] = None,
|
|
||||||
min_amount: Optional[float] = None,
|
|
||||||
max_amount: Optional[float] = None,
|
|
||||||
account_info: Optional[str] = None,
|
|
||||||
first_time_limit_btc: Optional[float] = None,
|
|
||||||
require_feedback_score: Optional[int] = None,
|
|
||||||
city: Optional[str] = None,
|
|
||||||
location_string: Optional[str] = None,
|
|
||||||
opening_hours: Optional[dict] = None,
|
|
||||||
visible: Optional[bool] = True,
|
|
||||||
require_trade_volume: Optional[float] = None,
|
|
||||||
volume_coefficient_btc: Optional[float] = None,
|
|
||||||
reference_type: Optional[str] = None,
|
|
||||||
display_reference: Optional[bool] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#ad-id
|
|
||||||
"""
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
# Don't want to change the name of the method from what the API call is
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
# pylint: disable=too-many-locals
|
|
||||||
# pylint: disable=too-many-branches
|
|
||||||
# API takes this many arguments, I can't change that
|
|
||||||
# Too many locals and too many branches goes hand in hand
|
|
||||||
# with too many arguments
|
|
||||||
params: Dict[str, Any] = {
|
|
||||||
"countrycode": country_code,
|
|
||||||
"currency": currency,
|
|
||||||
"trade_type": trade_type,
|
|
||||||
"price_equation": price_equation,
|
|
||||||
"track_max_amount": track_max_amount,
|
|
||||||
"require_trusted_by_advertiser": require_trusted_by_advertiser,
|
|
||||||
}
|
|
||||||
if sms_verification_required:
|
|
||||||
params["sms_verification_required"] = True
|
|
||||||
if require_identification:
|
|
||||||
params["require_identification"] = True
|
|
||||||
if online_provider:
|
|
||||||
params["online_provider"] = online_provider
|
|
||||||
if msg:
|
|
||||||
params["msg"] = msg
|
|
||||||
if min_amount:
|
|
||||||
params["min_amount"] = min_amount
|
|
||||||
if max_amount:
|
|
||||||
params["max_amount"] = max_amount
|
|
||||||
if first_time_limit_btc:
|
|
||||||
params["first_time_limit_btc"] = first_time_limit_btc
|
|
||||||
if require_feedback_score:
|
|
||||||
params["require_feedback_score"] = require_feedback_score
|
|
||||||
if account_info:
|
|
||||||
params["account_info"] = account_info
|
|
||||||
if opening_hours:
|
|
||||||
params["opening_hours"] = opening_hours
|
|
||||||
if city:
|
|
||||||
params["city"] = city
|
|
||||||
if location_string:
|
|
||||||
params["location_string"] = location_string
|
|
||||||
if bank_name:
|
|
||||||
params["bank_name"] = bank_name
|
|
||||||
if visible:
|
|
||||||
params["visible"] = visible
|
|
||||||
if require_trade_volume:
|
|
||||||
params["require_trade_volume"] = require_trade_volume
|
|
||||||
if volume_coefficient_btc:
|
|
||||||
params["volume_coefficient_btc"] = volume_coefficient_btc
|
|
||||||
if reference_type:
|
|
||||||
params["reference_type"] = reference_type
|
|
||||||
if display_reference is not None:
|
|
||||||
params["display_reference"] = display_reference
|
|
||||||
return self._api_call(
|
|
||||||
api_method=f"api/ad/{ad_id}/",
|
|
||||||
http_method="POST",
|
|
||||||
query_values=params,
|
|
||||||
)
|
|
||||||
|
|
||||||
def ad_equation(self, ad_id: str, price_equation: str) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#ad-equation-id
|
|
||||||
"""
|
|
||||||
return self._api_call(
|
|
||||||
api_method=f"api/ad-equation/{ad_id}/",
|
|
||||||
http_method="POST",
|
|
||||||
query_values={"price_equation": price_equation},
|
|
||||||
)
|
|
||||||
|
|
||||||
def ad_delete(self, ad_id: str) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#ad-delete
|
|
||||||
"""
|
|
||||||
return self._api_call(api_method=f"api/ad-delete/{ad_id}/", http_method="POST")
|
|
||||||
|
|
||||||
def ads(
|
|
||||||
self,
|
|
||||||
country_code: Optional[str] = None,
|
|
||||||
currency: Optional[str] = None,
|
|
||||||
trade_type: Optional[str] = None,
|
|
||||||
visible: Optional[bool] = None,
|
|
||||||
asset: Optional[str] = None,
|
|
||||||
payment_method_code: Optional[str] = None,
|
|
||||||
page: Optional[int] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#ads
|
|
||||||
"""
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
# API takes this many arguments, I can't change that
|
|
||||||
|
|
||||||
params = {}
|
|
||||||
if country_code:
|
|
||||||
params["countrycode"] = country_code
|
|
||||||
if currency:
|
|
||||||
params["currency"] = currency
|
|
||||||
if trade_type:
|
|
||||||
params["trade_type"] = trade_type
|
|
||||||
if visible is not None and visible:
|
|
||||||
params["visible"] = "1"
|
|
||||||
elif visible is not None and not visible:
|
|
||||||
params["visible"] = "0"
|
|
||||||
if asset:
|
|
||||||
params["asset"] = asset
|
|
||||||
if payment_method_code:
|
|
||||||
params["payment_method_code"] = payment_method_code
|
|
||||||
if page:
|
|
||||||
params["page"] = page
|
|
||||||
|
|
||||||
if len(params) == 0:
|
|
||||||
return self._api_call(api_method="api/ads/")
|
|
||||||
|
|
||||||
return self._api_call(api_method="api/ads/", query_values=params)
|
|
||||||
|
|
||||||
def ad_get(self, ad_ids: List[str]) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#ad-get and
|
|
||||||
hhttps://localbitcoins.com/api-docs/#ad-get-id
|
|
||||||
"""
|
|
||||||
api_method = "ad-get/"
|
|
||||||
params = None
|
|
||||||
ids = str(ad_ids)[1:-1].replace(" ", "").replace("'", "")
|
|
||||||
|
|
||||||
if len(ad_ids) == 1:
|
|
||||||
api_method += f"/{ids}"
|
|
||||||
else:
|
|
||||||
params = {"ads": ids}
|
|
||||||
return self._api_call(api_method=api_method, query_values=params)
|
|
||||||
|
|
||||||
def payment_methods(self, country_code: Optional[str] = None) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#payment-methods and
|
|
||||||
https://localbitcoins.com/api-docs/#payment_methods-cc
|
|
||||||
"""
|
|
||||||
api_method = "payment_methods/"
|
|
||||||
if country_code:
|
|
||||||
api_method += f"/{country_code}"
|
|
||||||
return self._api_call(api_method=api_method)
|
|
||||||
|
|
||||||
def country_codes(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#countrycodes
|
|
||||||
"""
|
|
||||||
return self._api_call(api_method="api/countrycodes/")
|
|
||||||
|
|
||||||
def currencies(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#currencies
|
|
||||||
"""
|
|
||||||
return self._api_call(api_method="api/currencies/")
|
|
||||||
|
|
||||||
def equation(self, price_equation: str, currency: str) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#equation
|
|
||||||
"""
|
|
||||||
return self._api_call(
|
|
||||||
api_method="api/equation/",
|
|
||||||
http_method="POST",
|
|
||||||
query_values={
|
|
||||||
"price_equation": price_equation,
|
|
||||||
"currency": currency,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Public ad search related API Methods
|
|
||||||
# ====================================
|
|
||||||
|
|
||||||
def _generic_online(
|
|
||||||
self,
|
|
||||||
direction: str,
|
|
||||||
main_currency: str,
|
|
||||||
exchange_currency: str,
|
|
||||||
country_code: Optional[str] = None,
|
|
||||||
payment_method: Optional[str] = None,
|
|
||||||
amount: Optional[float] = None,
|
|
||||||
page: Optional[int] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
|
|
||||||
add_to_api_method = ""
|
|
||||||
if country_code:
|
|
||||||
add_to_api_method = f"/{country_code}"
|
|
||||||
if payment_method:
|
|
||||||
add_to_api_method += f"/{payment_method}"
|
|
||||||
|
|
||||||
params = self._generic_search_parameters(amount, page)
|
|
||||||
|
|
||||||
return self._api_call(
|
|
||||||
api_method=f"{direction}-{main_currency}-online/"
|
|
||||||
f"{exchange_currency}{add_to_api_method}/.json",
|
|
||||||
query_values=params,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _generic_search_parameters(amount, page):
|
|
||||||
params = None
|
|
||||||
if amount and not page:
|
|
||||||
params = {"amount": f"{amount}"}
|
|
||||||
elif amount and page:
|
|
||||||
params = {"amount": f"{amount}", "page": f"{page}"}
|
|
||||||
elif not amount and page:
|
|
||||||
params = {"page": f"{page}"}
|
|
||||||
return params
|
|
||||||
|
|
||||||
def buy_bitcoins_online( # TODO: check fields
|
|
||||||
self,
|
|
||||||
currency_code: str,
|
|
||||||
country_code: Optional[str] = None,
|
|
||||||
payment_method: Optional[str] = None,
|
|
||||||
amount: Optional[float] = None,
|
|
||||||
page: Optional[int] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#online-buy1 and
|
|
||||||
https://localbitcoins.com/api-docs/#online-buy2 and
|
|
||||||
https://localbitcoins.com/api-docs/#online-buy3 and
|
|
||||||
https://localbitcoins.com/api-docs/#online-buy4 and
|
|
||||||
https://localbitcoins.com/api-docs/#online-buy5 and
|
|
||||||
https://localbitcoins.com/api-docs/#online-buy6
|
|
||||||
"""
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
|
|
||||||
return self._generic_online(
|
|
||||||
direction="buy",
|
|
||||||
main_currency="bitcoins",
|
|
||||||
exchange_currency=currency_code,
|
|
||||||
country_code=country_code,
|
|
||||||
payment_method=payment_method,
|
|
||||||
amount=amount,
|
|
||||||
page=page,
|
|
||||||
)
|
|
||||||
|
|
||||||
def sell_bitcoins_online(
|
|
||||||
self,
|
|
||||||
currency_code: str,
|
|
||||||
country_code: Optional[str] = None,
|
|
||||||
payment_method: Optional[str] = None,
|
|
||||||
amount: Optional[float] = None,
|
|
||||||
page: Optional[int] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#online-sell1 and
|
|
||||||
https://localbitcoins.com/api-docs/#online-sell2 and
|
|
||||||
https://localbitcoins.com/api-docs/#online-sell3 and
|
|
||||||
https://localbitcoins.com/api-docs/#online-sell4 and
|
|
||||||
https://localbitcoins.com/api-docs/#online-sell5 and
|
|
||||||
https://localbitcoins.com/api-docs/#online-sell6
|
|
||||||
"""
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
|
|
||||||
return self._generic_online(
|
|
||||||
direction="sell",
|
|
||||||
main_currency="bitcoins",
|
|
||||||
exchange_currency=currency_code,
|
|
||||||
country_code=country_code,
|
|
||||||
payment_method=payment_method,
|
|
||||||
amount=amount,
|
|
||||||
page=page,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Statistics related API Methods
|
|
||||||
# ==============================
|
|
||||||
|
|
||||||
def bitcoinaverage(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#ticker-all
|
|
||||||
"""
|
|
||||||
return self._api_call(api_method="api/bitcoinaverage/ticket-all-currencies/")
|
|
||||||
|
|
||||||
# Wallet related API Methods
|
|
||||||
# ===========================
|
|
||||||
|
|
||||||
def wallet(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#wallet
|
|
||||||
"""
|
|
||||||
return self._api_call(api_method="api/wallet/")
|
|
||||||
|
|
||||||
def wallet_balance(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#wallet-balance
|
|
||||||
"""
|
|
||||||
return self._api_call(api_method="api/wallet-balance/")
|
|
||||||
|
|
||||||
def wallet_addr(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#wallet-addr
|
|
||||||
"""
|
|
||||||
return self._api_call(api_method="api/wallet-addr/")
|
|
||||||
|
|
||||||
def fees(self) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#fees
|
|
||||||
"""
|
|
||||||
return self._api_call(api_method="api/fees/")
|
|
||||||
|
|
||||||
def wallet_send_pin(
|
|
||||||
self,
|
|
||||||
address: str,
|
|
||||||
amount: float,
|
|
||||||
password: str,
|
|
||||||
fee_level: str,
|
|
||||||
pincode: Optional[int] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""See LocalBitcoins API.
|
|
||||||
|
|
||||||
https://localbitcoins.com/api-docs/#wallet-send
|
|
||||||
"""
|
|
||||||
# pylint: disable=too-many-arguments
|
|
||||||
|
|
||||||
params = {
|
|
||||||
"address": address,
|
|
||||||
"amount": amount,
|
|
||||||
"password": password,
|
|
||||||
"fee_level": fee_level,
|
|
||||||
}
|
|
||||||
if pincode:
|
|
||||||
params["pincode"] = pincode
|
|
||||||
|
|
||||||
return self._api_call(
|
|
||||||
api_method="api/wallet-send-pin/",
|
|
||||||
http_method="POST",
|
|
||||||
query_values=params,
|
|
||||||
)
|
|
|
@ -1,31 +0,0 @@
|
||||||
# Other library imports
|
|
||||||
import logging
|
|
||||||
from json import dumps
|
|
||||||
|
|
||||||
import logstash
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
logger = None
|
|
||||||
|
|
||||||
|
|
||||||
def init_logstash():
|
|
||||||
global logger
|
|
||||||
logger = logging.getLogger("ingest")
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
logger.addHandler(
|
|
||||||
logstash.TCPLogstashHandler(
|
|
||||||
settings.Logstash.Host,
|
|
||||||
int(settings.Logstash.Port),
|
|
||||||
version=1,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def send_logstash(text):
|
|
||||||
global logger
|
|
||||||
if logger is not None:
|
|
||||||
logger.info(dumps(text))
|
|
||||||
return True
|
|
||||||
return False
|
|
|
@ -1,411 +0,0 @@
|
||||||
# Other library imports
|
|
||||||
from json import loads
|
|
||||||
|
|
||||||
import db
|
|
||||||
import util
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
|
|
||||||
class Markets(util.Base):
|
|
||||||
""" "
|
|
||||||
Markets handler for generic market functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def find_trades_by_uid(self, uid):
|
|
||||||
"""
|
|
||||||
Find a list of trade IDs and references by a customer UID.
|
|
||||||
:return: tuple of (platform, trade_id, reference, currency)
|
|
||||||
"""
|
|
||||||
platform, username = self.ux.verify.get_uid(uid)
|
|
||||||
refs = db.get_refs()
|
|
||||||
matching_trades = []
|
|
||||||
for reference in refs:
|
|
||||||
ref_data = db.get_ref(reference)
|
|
||||||
tx_platform = ref_data["subclass"]
|
|
||||||
tx_username = ref_data["buyer"]
|
|
||||||
trade_id = ref_data["id"]
|
|
||||||
currency = ref_data["currency"]
|
|
||||||
if tx_platform == platform and tx_username == username:
|
|
||||||
to_append = (platform, trade_id, reference, currency)
|
|
||||||
matching_trades.append(to_append)
|
|
||||||
return matching_trades
|
|
||||||
|
|
||||||
def get_send_settings(self, platform):
|
|
||||||
if platform == "agora":
|
|
||||||
send_setting = settings.Agora.Send
|
|
||||||
post_message = self.agora.api.contact_message_post
|
|
||||||
elif platform == "lbtc":
|
|
||||||
send_setting = settings.LocalBitcoins.Send
|
|
||||||
post_message = self.lbtc.api.contact_message_post
|
|
||||||
|
|
||||||
return (send_setting, post_message)
|
|
||||||
|
|
||||||
def send_reference(self, platform, trade_id, reference):
|
|
||||||
"""
|
|
||||||
Send the reference to a customer.
|
|
||||||
"""
|
|
||||||
send_setting, post_message = self.get_send_settings(platform)
|
|
||||||
if send_setting == "1":
|
|
||||||
post_message(
|
|
||||||
trade_id,
|
|
||||||
f"When sending the payment please use reference code: {reference}",
|
|
||||||
)
|
|
||||||
|
|
||||||
def send_bank_details(self, platform, currency, trade_id):
|
|
||||||
"""
|
|
||||||
Send the bank details to a trade.
|
|
||||||
"""
|
|
||||||
send_setting, post_message = self.get_send_settings(platform)
|
|
||||||
self.log.info(f"Sending bank details/reference for {platform}/{trade_id}")
|
|
||||||
if send_setting == "1":
|
|
||||||
account_info = self.get_matching_account_details(platform, currency)
|
|
||||||
formatted_account_info = self.format_payment_details(
|
|
||||||
currency, account_info, real=True
|
|
||||||
)
|
|
||||||
if not formatted_account_info:
|
|
||||||
self.log.error(f"Payment info invalid: {formatted_account_info}")
|
|
||||||
return
|
|
||||||
post_message(
|
|
||||||
trade_id,
|
|
||||||
f"Payment details: \n{formatted_account_info}",
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_all_assets(self, platform):
|
|
||||||
sets = util.get_settings(platform)
|
|
||||||
assets = loads(sets.AssetList)
|
|
||||||
return assets
|
|
||||||
|
|
||||||
def get_all_providers(self, platform):
|
|
||||||
sets = util.get_settings(platform)
|
|
||||||
providers = loads(sets.ProviderList)
|
|
||||||
return providers
|
|
||||||
|
|
||||||
def get_all_currencies(self, platform):
|
|
||||||
sets = util.get_settings(platform)
|
|
||||||
currencies = list(set([x[0] for x in loads(sets.DistList)]))
|
|
||||||
return currencies
|
|
||||||
|
|
||||||
def get_new_ad_equations(self, platform, public_ads, assets=None):
|
|
||||||
"""
|
|
||||||
Update all our prices.
|
|
||||||
:param public_ads: dictionary of public ads keyed by currency
|
|
||||||
:type public_ads: dict
|
|
||||||
:return: list of ads to modify
|
|
||||||
:rtype: list
|
|
||||||
"""
|
|
||||||
sets = util.get_settings(platform)
|
|
||||||
username = sets.Username
|
|
||||||
min_margin = sets.MinMargin
|
|
||||||
max_margin = sets.MaxMargin
|
|
||||||
|
|
||||||
to_update = []
|
|
||||||
|
|
||||||
# NOTES:
|
|
||||||
# Get all ads for each currency, with all the payment methods.
|
|
||||||
# Create a function to, in turn, filter these so it contains only one payment method. Run autoprice on this.
|
|
||||||
# Append all results to to_update. Repeat for remaining payment methods, then call slow update.
|
|
||||||
|
|
||||||
# (asset, currency, provider)
|
|
||||||
if not assets:
|
|
||||||
assets = self.get_all_assets(platform)
|
|
||||||
currencies = self.get_all_currencies(platform)
|
|
||||||
providers = self.get_all_providers(platform)
|
|
||||||
if platform == "lbtc":
|
|
||||||
providers = [
|
|
||||||
self.sources.lbtc.map_provider(x, reverse=True) for x in providers
|
|
||||||
]
|
|
||||||
sinks_currencies = self.sinks.currencies
|
|
||||||
supported_currencies = [
|
|
||||||
currency for currency in currencies if currency in sinks_currencies
|
|
||||||
]
|
|
||||||
currencies = supported_currencies
|
|
||||||
|
|
||||||
brute = [
|
|
||||||
(asset, currency, provider)
|
|
||||||
for asset in assets
|
|
||||||
for currency in currencies
|
|
||||||
for provider in providers
|
|
||||||
]
|
|
||||||
for asset, currency, provider in brute:
|
|
||||||
# Filter currency
|
|
||||||
try:
|
|
||||||
public_ads_currency = public_ads[currency]
|
|
||||||
except KeyError:
|
|
||||||
# self.log.error("Error getting public ads for currency {currency}", currency=currency)
|
|
||||||
if currency == "GBP":
|
|
||||||
self.log.error(
|
|
||||||
"Error getting public ads for currency GBP, aborting"
|
|
||||||
)
|
|
||||||
break
|
|
||||||
continue
|
|
||||||
# Filter asset
|
|
||||||
public_ads_filtered = [ad for ad in public_ads_currency if ad[4] == asset]
|
|
||||||
|
|
||||||
# Filter provider
|
|
||||||
public_ads_filtered = [
|
|
||||||
ad for ad in public_ads_filtered if ad[3] == provider
|
|
||||||
]
|
|
||||||
our_ads = [ad for ad in public_ads_filtered if ad[1] == username]
|
|
||||||
if not our_ads:
|
|
||||||
self.log.warning(
|
|
||||||
f"No ads found in {platform} public listing for {asset} {currency} {provider}"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
new_margin = self.autoprice(
|
|
||||||
username, min_margin, max_margin, public_ads_filtered, currency
|
|
||||||
)
|
|
||||||
# self.log.info("New rate for {currency}: {rate}", currency=currency, rate=new_margin)
|
|
||||||
if platform == "agora":
|
|
||||||
new_formula = (
|
|
||||||
f"coingecko{asset.lower()}usd*usd{currency.lower()}*{new_margin}"
|
|
||||||
)
|
|
||||||
elif platform == "lbtc":
|
|
||||||
new_formula = f"btc_in_usd*{new_margin}*USD_in_{currency}"
|
|
||||||
for ad in our_ads:
|
|
||||||
ad_id = ad[0]
|
|
||||||
asset = ad[4]
|
|
||||||
our_margin = ad[5]
|
|
||||||
if new_margin != our_margin:
|
|
||||||
to_update.append([str(ad_id), new_formula, asset, currency, False])
|
|
||||||
|
|
||||||
return to_update
|
|
||||||
|
|
||||||
def autoprice(self, username, min_margin, max_margin, ads, currency):
|
|
||||||
"""
|
|
||||||
Helper function to automatically adjust the price up/down in certain markets
|
|
||||||
in order to gain the most profits and sales.
|
|
||||||
:param ads: list of ads
|
|
||||||
:type ads: list of lists
|
|
||||||
:param currency: currency of the ads
|
|
||||||
:type currency: string
|
|
||||||
:return: the rate we should use for this currency
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
# self.log.debug("Autoprice starting for {x}", x=currency)
|
|
||||||
# Find cheapest ad
|
|
||||||
# Filter by 3rd index on each ad list to find the cheapest
|
|
||||||
min_margin_ad = min(ads, key=lambda x: x[6])
|
|
||||||
# self.log.debug("Minimum margin ad: {x}", x=min_margin_ad)
|
|
||||||
|
|
||||||
# Find second cheapest that is not us
|
|
||||||
# Remove results from ads that are us
|
|
||||||
ads_without_us = [ad for ad in ads if not ad[1] == username]
|
|
||||||
# self.log.debug("Ads without us: {x}", x=ads_without_us)
|
|
||||||
# Find ads above our min that are not us
|
|
||||||
ads_above_our_min_not_us = [
|
|
||||||
ad for ad in ads_without_us if ad[6] > float(min_margin)
|
|
||||||
]
|
|
||||||
# self.log.debug("Ads above our min not us: {x}", x=ads_above_our_min_not_us)
|
|
||||||
# Check that this list without us is not empty
|
|
||||||
if ads_without_us:
|
|
||||||
# Find the cheapest from these
|
|
||||||
min_margin_ad_not_us = min(ads_without_us, key=lambda x: x[6])
|
|
||||||
# self.log.debug("Min margin ad not us: {x}", x=min_margin_ad_not_us)
|
|
||||||
# Lowball the lowest ad that is not ours
|
|
||||||
lowball_lowest_not_ours = min_margin_ad_not_us[6] # - 0.005
|
|
||||||
# self.log.debug("Lowball lowest not ours: {x}", x=lowball_lowest_not_ours)
|
|
||||||
|
|
||||||
# Check if the username field of the cheapest ad matches ours
|
|
||||||
if min_margin_ad[1] == username:
|
|
||||||
# self.log.debug("We are the cheapest for: {x}", x=currency)
|
|
||||||
# We are the cheapest!
|
|
||||||
# Are all of the ads ours?
|
|
||||||
all_ads_ours = all([ad[1] == username for ad in ads])
|
|
||||||
if all_ads_ours:
|
|
||||||
# self.log.debug("All ads are ours for: {x}", x=currency)
|
|
||||||
# Now we know it's safe to return the maximum value
|
|
||||||
return float(max_margin)
|
|
||||||
else:
|
|
||||||
# self.log.debug("All ads are NOT ours for: {x}", x=currency)
|
|
||||||
# All the ads are not ours, but we are first...
|
|
||||||
# Check if the lowballed, lowest (that is not ours) ad's margin
|
|
||||||
# is less than our minimum
|
|
||||||
if lowball_lowest_not_ours < float(min_margin):
|
|
||||||
# self.log.debug("Lowball lowest not ours less than MinMargin")
|
|
||||||
return float(min_margin)
|
|
||||||
elif lowball_lowest_not_ours > float(max_margin):
|
|
||||||
# self.log.debug("Lowball lowest not ours more than MaxMargin")
|
|
||||||
return float(max_margin)
|
|
||||||
else:
|
|
||||||
# self.log.debug("Returning lowballed figure: {x}", x=lowball_lowest_not_ours)
|
|
||||||
return lowball_lowest_not_ours
|
|
||||||
else:
|
|
||||||
# self.log.debug("We are NOT the cheapest for: {x}", x=currency)
|
|
||||||
# We are not the cheapest :(
|
|
||||||
# Check if this list is empty
|
|
||||||
if not ads_above_our_min_not_us:
|
|
||||||
# Return the maximum margin?
|
|
||||||
return float(max_margin)
|
|
||||||
# Find cheapest ad above our min that is not us
|
|
||||||
cheapest_ad = min(ads_above_our_min_not_us, key=lambda x: x[4])
|
|
||||||
cheapest_ad_margin = cheapest_ad[6] # - 0.005
|
|
||||||
if cheapest_ad_margin > float(max_margin):
|
|
||||||
# self.log.debug("Cheapest ad not ours more than MaxMargin")
|
|
||||||
return float(max_margin)
|
|
||||||
# self.log.debug("Cheapest ad above our min that is not us: {x}", x=cheapest_ad)
|
|
||||||
return cheapest_ad_margin
|
|
||||||
|
|
||||||
def create_distribution_list(self, platform, filter_asset=None):
|
|
||||||
"""
|
|
||||||
Create a list for distribution of ads.
|
|
||||||
:return: generator of asset, countrycode, currency, provider
|
|
||||||
:rtype: generator of tuples
|
|
||||||
"""
|
|
||||||
sets = util.get_settings(platform)
|
|
||||||
|
|
||||||
# Iterate providers like REVOLUT, NATIONAL_BANK
|
|
||||||
for provider in loads(sets.ProviderList):
|
|
||||||
# Iterate assets like XMR, BTC
|
|
||||||
for asset in loads(sets.AssetList):
|
|
||||||
# Iterate pairs of currency and country like EUR, GB
|
|
||||||
for currency, countrycode in loads(sets.DistList):
|
|
||||||
if filter_asset:
|
|
||||||
if asset == filter_asset:
|
|
||||||
yield (asset, countrycode, currency, provider)
|
|
||||||
else:
|
|
||||||
yield (asset, countrycode, currency, provider)
|
|
||||||
|
|
||||||
def get_valid_account_details(self, platform):
|
|
||||||
currencies = self.sinks.currencies
|
|
||||||
account_info = self.sinks.account_info
|
|
||||||
all_currencies = self.get_all_currencies(platform)
|
|
||||||
supported_currencies = [
|
|
||||||
currency for currency in currencies if currency in all_currencies
|
|
||||||
]
|
|
||||||
currency_account_info_map = {}
|
|
||||||
for currency in supported_currencies:
|
|
||||||
for bank, accounts in account_info.items():
|
|
||||||
for account in accounts:
|
|
||||||
if account["currency"] == currency:
|
|
||||||
currency_account_info_map[currency] = account["account_number"]
|
|
||||||
currency_account_info_map[currency]["bank"] = bank.split("_")[0]
|
|
||||||
currency_account_info_map[currency]["recipient"] = account[
|
|
||||||
"recipient"
|
|
||||||
]
|
|
||||||
return (supported_currencies, currency_account_info_map)
|
|
||||||
|
|
||||||
def get_matching_account_details(self, platform, currency):
|
|
||||||
(
|
|
||||||
supported_currencies,
|
|
||||||
currency_account_info_map,
|
|
||||||
) = self.get_valid_account_details(platform)
|
|
||||||
if currency not in supported_currencies:
|
|
||||||
return False
|
|
||||||
return currency_account_info_map[currency]
|
|
||||||
|
|
||||||
def _distribute_account_details(self, platform, currencies=None, account_info=None):
|
|
||||||
"""
|
|
||||||
Distribute account details for ads.
|
|
||||||
We will disable ads we can't support.
|
|
||||||
"""
|
|
||||||
if platform == "agora":
|
|
||||||
caller = self.agora
|
|
||||||
elif platform == "lbtc":
|
|
||||||
caller = self.lbtc
|
|
||||||
|
|
||||||
if not currencies:
|
|
||||||
currencies = self.sinks.currencies
|
|
||||||
if not account_info:
|
|
||||||
account_info = self.sinks.account_info
|
|
||||||
(
|
|
||||||
supported_currencies,
|
|
||||||
currency_account_info_map,
|
|
||||||
) = self.get_valid_account_details(platform)
|
|
||||||
|
|
||||||
# not_supported = [currency for currency in all_currencies if currency not in supported_currencies]
|
|
||||||
|
|
||||||
our_ads = caller.enum_ads()
|
|
||||||
|
|
||||||
supported_ads = [ad for ad in our_ads if ad[3] in supported_currencies]
|
|
||||||
|
|
||||||
not_supported_ads = [ad for ad in our_ads if ad[3] not in supported_currencies]
|
|
||||||
|
|
||||||
for ad in supported_ads:
|
|
||||||
asset = ad[0]
|
|
||||||
countrycode = ad[2]
|
|
||||||
currency = ad[3]
|
|
||||||
provider = ad[4]
|
|
||||||
payment_details = currency_account_info_map[currency]
|
|
||||||
ad_id = ad[1]
|
|
||||||
caller.create_ad(
|
|
||||||
asset,
|
|
||||||
countrycode,
|
|
||||||
currency,
|
|
||||||
provider,
|
|
||||||
payment_details,
|
|
||||||
visible=True,
|
|
||||||
edit=True,
|
|
||||||
ad_id=ad_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
for ad in not_supported_ads:
|
|
||||||
asset = ad[0]
|
|
||||||
countrycode = ad[2]
|
|
||||||
currency = ad[3]
|
|
||||||
provider = ad[4]
|
|
||||||
ad_id = ad[1]
|
|
||||||
caller.create_ad(
|
|
||||||
asset,
|
|
||||||
countrycode,
|
|
||||||
currency,
|
|
||||||
provider,
|
|
||||||
payment_details=False,
|
|
||||||
visible=False,
|
|
||||||
edit=True,
|
|
||||||
ad_id=ad_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
def distribute_account_details(self, currencies=None, account_info=None):
|
|
||||||
"""
|
|
||||||
Helper to distribute the account details for all platforms.
|
|
||||||
"""
|
|
||||||
platforms = ("agora", "lbtc")
|
|
||||||
for platform in platforms:
|
|
||||||
self._distribute_account_details(
|
|
||||||
platform, currencies=currencies, account_info=account_info
|
|
||||||
)
|
|
||||||
|
|
||||||
def format_ad(self, asset, currency, payment_details_text):
|
|
||||||
"""
|
|
||||||
Format the ad.
|
|
||||||
"""
|
|
||||||
ad = settings.Platform.Ad
|
|
||||||
|
|
||||||
# Substitute the currency
|
|
||||||
ad = ad.replace("$CURRENCY$", currency)
|
|
||||||
|
|
||||||
# Substitute the asset
|
|
||||||
ad = ad.replace("$ASSET$", asset)
|
|
||||||
|
|
||||||
# Substitute the payment details
|
|
||||||
ad = ad.replace("$PAYMENT$", payment_details_text)
|
|
||||||
|
|
||||||
# Strip extra tabs
|
|
||||||
ad = ad.replace("\\t", "\t")
|
|
||||||
return ad
|
|
||||||
|
|
||||||
def format_payment_details(self, currency, payment_details, real=False):
|
|
||||||
"""
|
|
||||||
Format the payment details.
|
|
||||||
"""
|
|
||||||
if not payment_details:
|
|
||||||
return False
|
|
||||||
if real:
|
|
||||||
payment = settings.Platform.PaymentDetailsReal
|
|
||||||
else:
|
|
||||||
payment = settings.Platform.PaymentDetails
|
|
||||||
|
|
||||||
payment_text = ""
|
|
||||||
for field, value in payment_details.items():
|
|
||||||
formatted_name = field.replace("_", " ")
|
|
||||||
formatted_name = formatted_name.capitalize()
|
|
||||||
payment_text += f"* {formatted_name}: **{value}**"
|
|
||||||
if field != list(payment_details.keys())[-1]: # No trailing newline
|
|
||||||
payment_text += "\n"
|
|
||||||
|
|
||||||
payment = payment.replace("$PAYMENT$", payment_text)
|
|
||||||
payment = payment.replace("$CURRENCY$", currency)
|
|
||||||
|
|
||||||
return payment
|
|
|
@ -1,505 +0,0 @@
|
||||||
# Twisted imports
|
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import urllib3
|
|
||||||
import util
|
|
||||||
from forex_python.converter import CurrencyRates
|
|
||||||
from lib.logstash import send_logstash
|
|
||||||
from opensearchpy import OpenSearch
|
|
||||||
|
|
||||||
# Other library imports
|
|
||||||
from pycoingecko import CoinGeckoAPI
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
from twisted.internet.defer import inlineCallbacks
|
|
||||||
from twisted.internet.task import LoopingCall
|
|
||||||
|
|
||||||
# TODO: secure ES traffic properly
|
|
||||||
urllib3.disable_warnings()
|
|
||||||
|
|
||||||
tracer = logging.getLogger("opensearch")
|
|
||||||
tracer.setLevel(logging.CRITICAL)
|
|
||||||
tracer = logging.getLogger("elastic_transport.transport")
|
|
||||||
tracer.setLevel(logging.CRITICAL)
|
|
||||||
|
|
||||||
|
|
||||||
class Money(util.Base):
|
|
||||||
"""
|
|
||||||
Generic class for handling money-related matters that aren't Revolut or Agora.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
Initialise the Money object.
|
|
||||||
Set the logger.
|
|
||||||
Initialise the CoinGecko API.
|
|
||||||
"""
|
|
||||||
super().__init__()
|
|
||||||
self.cr = CurrencyRates()
|
|
||||||
self.cg = CoinGeckoAPI()
|
|
||||||
if settings.ES.Enabled == "1":
|
|
||||||
# self.es = Elasticsearch(
|
|
||||||
# f"https://{settings.ES.Host}:9200",
|
|
||||||
# verify_certs=False,
|
|
||||||
# basic_auth=(settings.ES.Username, settings.ES.Pass),
|
|
||||||
# # ssl_assert_fingerprint=("6b264fd2fd107d45652d8add1750a8a78f424542e13b056d0548173006260710"),
|
|
||||||
# ca_certs="certs/ca.crt",
|
|
||||||
# )
|
|
||||||
# TODO: implement ca certs https://opensearch.org/docs/latest/clients/python/
|
|
||||||
auth = (settings.ES.Username, settings.ES.Pass)
|
|
||||||
self.es = OpenSearch(
|
|
||||||
hosts=[{"host": settings.ES.Host, "port": 9200}],
|
|
||||||
http_compress=False, # enables gzip compression for request bodies
|
|
||||||
http_auth=auth,
|
|
||||||
# client_cert = client_cert_path,
|
|
||||||
# client_key = client_key_path,
|
|
||||||
use_ssl=True,
|
|
||||||
verify_certs=False,
|
|
||||||
ssl_assert_hostname=False,
|
|
||||||
ssl_show_warn=False,
|
|
||||||
# a_certs=ca_certs_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def run_checks_in_thread(self):
|
|
||||||
"""
|
|
||||||
Run all the balance checks that output into ES in another thread.
|
|
||||||
"""
|
|
||||||
total = self.get_total()
|
|
||||||
remaining = self.get_remaining()
|
|
||||||
profit = self.get_profit()
|
|
||||||
profit_with_trades = self.get_profit(True)
|
|
||||||
open_trades = self.get_open_trades_usd()
|
|
||||||
total_remaining = self.get_total_remaining()
|
|
||||||
total_with_trades = self.get_total_with_trades()
|
|
||||||
# This will make them all run concurrently, hopefully not hitting rate limits
|
|
||||||
for x in (
|
|
||||||
total,
|
|
||||||
remaining,
|
|
||||||
profit,
|
|
||||||
profit_with_trades,
|
|
||||||
open_trades,
|
|
||||||
total_remaining,
|
|
||||||
total_with_trades,
|
|
||||||
):
|
|
||||||
yield x
|
|
||||||
|
|
||||||
def setup_loops(self):
|
|
||||||
"""
|
|
||||||
Set up the LoopingCalls to get the balance so we have data in ES.
|
|
||||||
"""
|
|
||||||
if settings.ES.Enabled == "1" or settings.Logstash.Enabled == "1":
|
|
||||||
self.lc_es_checks = LoopingCall(self.run_checks_in_thread)
|
|
||||||
delay = int(settings.ES.RefreshSec)
|
|
||||||
self.lc_es_checks.start(delay)
|
|
||||||
if settings.ES.Enabled == "1":
|
|
||||||
self.agora.es = self.es
|
|
||||||
self.lbtc.es = self.es
|
|
||||||
|
|
||||||
def write_to_es(self, msgtype, cast):
|
|
||||||
cast["type"] = "money"
|
|
||||||
cast["ts"] = str(datetime.now().isoformat())
|
|
||||||
cast["xtype"] = msgtype
|
|
||||||
if settings.ES.Enabled == "1":
|
|
||||||
self.es.index(index=settings.ES.Index, body=cast)
|
|
||||||
elif settings.Logstash.Enabled == "1":
|
|
||||||
send_logstash(cast)
|
|
||||||
|
|
||||||
def lookup_rates(self, platform, ads, rates=None):
|
|
||||||
"""
|
|
||||||
Lookup the rates for a list of public ads.
|
|
||||||
"""
|
|
||||||
if not rates:
|
|
||||||
rates = self.cg.get_price(
|
|
||||||
ids=["monero", "bitcoin"],
|
|
||||||
vs_currencies=self.markets.get_all_currencies(platform),
|
|
||||||
)
|
|
||||||
# Set the price based on the asset
|
|
||||||
for ad in ads:
|
|
||||||
if ad[4] == "XMR":
|
|
||||||
coin = "monero"
|
|
||||||
elif ad[4] == "BTC":
|
|
||||||
coin = "bitcoin" # No s here
|
|
||||||
currency = ad[5]
|
|
||||||
base_currency_price = rates[coin][currency.lower()]
|
|
||||||
price = float(ad[2])
|
|
||||||
rate = round(price / base_currency_price, 2)
|
|
||||||
ad.append(rate)
|
|
||||||
# TODO: sort?
|
|
||||||
return sorted(ads, key=lambda x: x[2])
|
|
||||||
|
|
||||||
def get_rates_all(self):
|
|
||||||
"""
|
|
||||||
Get all rates that pair with USD.
|
|
||||||
:return: dictionary of USD/XXX rates
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
rates = self.cr.get_rates("USD")
|
|
||||||
return rates
|
|
||||||
|
|
||||||
def get_acceptable_margins(self, platform, currency, amount):
|
|
||||||
"""
|
|
||||||
Get the minimum and maximum amounts we would accept a trade for.
|
|
||||||
:param currency: currency code
|
|
||||||
:param amount: amount
|
|
||||||
:return: (min, max)
|
|
||||||
:rtype: tuple
|
|
||||||
"""
|
|
||||||
sets = util.get_settings(platform)
|
|
||||||
rates = self.get_rates_all()
|
|
||||||
if currency == "USD":
|
|
||||||
min_amount = amount - float(sets.AcceptableUSDMargin)
|
|
||||||
max_amount = amount + float(sets.AcceptableUSDMargin)
|
|
||||||
return (min_amount, max_amount)
|
|
||||||
amount_usd = amount / rates[currency]
|
|
||||||
min_usd = amount_usd - float(sets.AcceptableUSDMargin)
|
|
||||||
max_usd = amount_usd + float(sets.AcceptableUSDMargin)
|
|
||||||
min_local = min_usd * rates[currency]
|
|
||||||
max_local = max_usd * rates[currency]
|
|
||||||
return (min_local, max_local)
|
|
||||||
|
|
||||||
def get_minmax(self, platform, asset, currency):
|
|
||||||
sets = util.get_settings(platform)
|
|
||||||
rates = self.get_rates_all()
|
|
||||||
if currency not in rates and not currency == "USD":
|
|
||||||
self.log.error(f"Can't create ad without rates: {currency}")
|
|
||||||
return
|
|
||||||
if asset == "XMR":
|
|
||||||
min_usd = float(sets.MinUSDXMR)
|
|
||||||
max_usd = float(sets.MaxUSDXMR)
|
|
||||||
elif asset == "BTC":
|
|
||||||
min_usd = float(sets.MinUSDBTC)
|
|
||||||
max_usd = float(sets.MaxUSDBTC)
|
|
||||||
if currency == "USD":
|
|
||||||
min_amount = min_usd
|
|
||||||
max_amount = max_usd
|
|
||||||
else:
|
|
||||||
min_amount = rates[currency] * min_usd
|
|
||||||
max_amount = rates[currency] * max_usd
|
|
||||||
|
|
||||||
return (min_amount, max_amount)
|
|
||||||
|
|
||||||
def to_usd(self, amount, currency):
|
|
||||||
if currency == "USD":
|
|
||||||
return float(amount)
|
|
||||||
else:
|
|
||||||
rates = self.get_rates_all()
|
|
||||||
return float(amount) / rates[currency]
|
|
||||||
|
|
||||||
def multiple_to_usd(self, currency_map):
|
|
||||||
"""
|
|
||||||
Convert multiple curencies to USD while saving API calls.
|
|
||||||
"""
|
|
||||||
rates = self.get_rates_all()
|
|
||||||
cumul = 0
|
|
||||||
for currency, amount in currency_map.items():
|
|
||||||
if currency == "USD":
|
|
||||||
cumul += float(amount)
|
|
||||||
else:
|
|
||||||
cumul += float(amount) / rates[currency]
|
|
||||||
return cumul
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_profit(self, trades=False):
|
|
||||||
"""
|
|
||||||
Check how much total profit we have made.
|
|
||||||
:return: profit in USD
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
total_usd = yield self.get_total_usd()
|
|
||||||
if not total_usd:
|
|
||||||
return False
|
|
||||||
if trades:
|
|
||||||
trades_usd = yield self.get_open_trades_usd()
|
|
||||||
total_usd += trades_usd
|
|
||||||
|
|
||||||
profit = total_usd - float(settings.Money.BaseUSD)
|
|
||||||
if trades:
|
|
||||||
cast_es = {
|
|
||||||
"profit_trades_usd": profit,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
cast_es = {
|
|
||||||
"profit_usd": profit,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.write_to_es("get_profit", cast_es)
|
|
||||||
return profit
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_total_usd(self):
|
|
||||||
"""
|
|
||||||
Get total USD in all our accounts, bank and trading.
|
|
||||||
:return: value in USD
|
|
||||||
:rtype float:
|
|
||||||
"""
|
|
||||||
total_sinks_usd = self.sinks.get_total_usd()
|
|
||||||
agora_wallet_xmr = yield self.agora.api.wallet_balance_xmr()
|
|
||||||
agora_wallet_btc = yield self.agora.api.wallet_balance()
|
|
||||||
lbtc_wallet_btc = yield self.lbtc.api.wallet_balance()
|
|
||||||
|
|
||||||
total_xmr_agora = agora_wallet_xmr["response"]["data"]["total"]["balance"]
|
|
||||||
total_btc_agora = agora_wallet_btc["response"]["data"]["total"]["balance"]
|
|
||||||
total_btc_lbtc = lbtc_wallet_btc["response"]["data"]["total"]["balance"]
|
|
||||||
# Get the XMR -> USD exchange rate
|
|
||||||
xmr_usd = self.cg.get_price(ids="monero", vs_currencies=["USD"])
|
|
||||||
|
|
||||||
# Get the BTC -> USD exchange rate
|
|
||||||
btc_usd = self.cg.get_price(ids="bitcoin", vs_currencies=["USD"])
|
|
||||||
|
|
||||||
# Convert the Agora BTC total to USD
|
|
||||||
total_usd_agora_btc = float(total_btc_agora) * btc_usd["bitcoin"]["usd"]
|
|
||||||
|
|
||||||
# Convert the LBTC BTC total to USD
|
|
||||||
total_usd_lbtc_btc = float(total_btc_lbtc) * btc_usd["bitcoin"]["usd"]
|
|
||||||
|
|
||||||
# Convert the Agora XMR total to USD
|
|
||||||
total_usd_agora_xmr = float(total_xmr_agora) * xmr_usd["monero"]["usd"]
|
|
||||||
|
|
||||||
# Add it all up
|
|
||||||
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
|
|
||||||
total_usd_lbtc = total_usd_lbtc_btc
|
|
||||||
total_usd = total_usd_agora + total_usd_lbtc + total_sinks_usd
|
|
||||||
cast_es = {
|
|
||||||
"price_usd": total_usd,
|
|
||||||
"total_usd_agora_xmr": total_usd_agora_xmr,
|
|
||||||
"total_usd_agora_btc": total_usd_agora_btc,
|
|
||||||
"total_usd_lbtc_btc": total_usd_lbtc_btc,
|
|
||||||
"total_xmr_agora": total_xmr_agora,
|
|
||||||
"total_btc_agora": total_btc_agora,
|
|
||||||
"total_btc_lbtc": total_btc_lbtc,
|
|
||||||
"xmr_usd": xmr_usd["monero"]["usd"],
|
|
||||||
"btc_usd": btc_usd["bitcoin"]["usd"],
|
|
||||||
"total_sinks_usd": total_sinks_usd,
|
|
||||||
"total_usd_agora": total_usd_agora,
|
|
||||||
}
|
|
||||||
self.write_to_es("get_total_usd", cast_es)
|
|
||||||
return total_usd
|
|
||||||
|
|
||||||
# TODO: possibly refactor this into smaller functions which don't return as much stuff
|
|
||||||
# check if this is all really needed in the corresponding withdraw function
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_total(self):
|
|
||||||
"""
|
|
||||||
Get all the values corresponding to the amount of money we hold.
|
|
||||||
:return: ((total SEK, total USD, total GBP), (total XMR USD, total BTC USD), (total XMR, total BTC))
|
|
||||||
:rtype: tuple(tuple(float, float, float), tuple(float, float), tuple(float, float))
|
|
||||||
"""
|
|
||||||
total_sinks_usd = self.sinks.get_total_usd()
|
|
||||||
agora_wallet_xmr = yield self.agora.api.wallet_balance_xmr()
|
|
||||||
agora_wallet_btc = yield self.agora.api.wallet_balance()
|
|
||||||
lbtc_wallet_btc = yield self.lbtc.api.wallet_balance()
|
|
||||||
if not agora_wallet_xmr["success"]:
|
|
||||||
return False
|
|
||||||
if not agora_wallet_btc["success"]:
|
|
||||||
return False
|
|
||||||
if not lbtc_wallet_btc["success"]:
|
|
||||||
return False
|
|
||||||
if not agora_wallet_xmr["response"]:
|
|
||||||
return False
|
|
||||||
if not agora_wallet_btc["response"]:
|
|
||||||
return False
|
|
||||||
if not lbtc_wallet_btc["response"]:
|
|
||||||
return False
|
|
||||||
total_xmr_agora = agora_wallet_xmr["response"]["data"]["total"]["balance"]
|
|
||||||
total_btc_agora = agora_wallet_btc["response"]["data"]["total"]["balance"]
|
|
||||||
total_btc_lbtc = lbtc_wallet_btc["response"]["data"]["total"]["balance"]
|
|
||||||
# Get the XMR -> USD exchange rate
|
|
||||||
xmr_usd = self.cg.get_price(ids="monero", vs_currencies=["USD"])
|
|
||||||
|
|
||||||
# Get the BTC -> USD exchange rate
|
|
||||||
btc_usd = self.cg.get_price(ids="bitcoin", vs_currencies=["USD"])
|
|
||||||
|
|
||||||
# Convert the Agora XMR total to USD
|
|
||||||
total_usd_agora_xmr = float(total_xmr_agora) * xmr_usd["monero"]["usd"]
|
|
||||||
|
|
||||||
# Convert the Agora BTC total to USD
|
|
||||||
total_usd_agora_btc = float(total_btc_agora) * btc_usd["bitcoin"]["usd"]
|
|
||||||
|
|
||||||
# Convert the LBTC BTC total to USD
|
|
||||||
total_usd_lbtc_btc = float(total_btc_lbtc) * btc_usd["bitcoin"]["usd"]
|
|
||||||
|
|
||||||
# Add it all up
|
|
||||||
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
|
|
||||||
total_usd_lbtc = total_usd_lbtc_btc
|
|
||||||
total_usd = total_usd_agora + total_usd_lbtc + total_sinks_usd
|
|
||||||
|
|
||||||
total_btc_usd = total_usd_agora_btc + total_usd_lbtc_btc
|
|
||||||
total_xmr_usd = total_usd_agora_xmr
|
|
||||||
|
|
||||||
total_xmr = total_xmr_agora
|
|
||||||
total_btc = total_btc_lbtc + total_btc_agora
|
|
||||||
|
|
||||||
# Convert the total USD price to GBP and SEK
|
|
||||||
rates = self.get_rates_all()
|
|
||||||
price_sek = rates["SEK"] * total_usd
|
|
||||||
price_usd = total_usd
|
|
||||||
price_gbp = rates["GBP"] * total_usd
|
|
||||||
|
|
||||||
cast = (
|
|
||||||
(
|
|
||||||
price_sek,
|
|
||||||
price_usd,
|
|
||||||
price_gbp,
|
|
||||||
), # Total prices in our 3 favourite currencies
|
|
||||||
(
|
|
||||||
total_xmr_usd,
|
|
||||||
total_btc_usd,
|
|
||||||
), # Total USD balance in only Agora
|
|
||||||
(total_xmr, total_btc),
|
|
||||||
) # Total XMR and BTC balance in Agora
|
|
||||||
|
|
||||||
cast_es = {
|
|
||||||
"price_sek": price_sek,
|
|
||||||
"price_usd": price_usd,
|
|
||||||
"price_gbp": price_gbp,
|
|
||||||
"total_usd_agora_xmr": total_usd_agora_xmr,
|
|
||||||
"total_usd_agora_btc": total_usd_agora_btc,
|
|
||||||
"total_usd_lbtc_btc": total_usd_lbtc_btc,
|
|
||||||
"total_xmr_agora": total_xmr_agora,
|
|
||||||
"total_btc_agora": total_btc_agora,
|
|
||||||
"total_btc_lbtc": total_btc_lbtc,
|
|
||||||
"xmr_usd": xmr_usd["monero"]["usd"],
|
|
||||||
"btc_usd": btc_usd["bitcoin"]["usd"],
|
|
||||||
"total_sinks_usd": total_sinks_usd,
|
|
||||||
"total_usd_agora": total_usd_agora,
|
|
||||||
}
|
|
||||||
self.write_to_es("get_total", cast_es)
|
|
||||||
return cast
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_remaining(self):
|
|
||||||
"""
|
|
||||||
Check how much profit we need to make in order to withdraw.
|
|
||||||
:return: profit remaining in USD
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
total_usd = yield self.get_total_usd()
|
|
||||||
if not total_usd:
|
|
||||||
return False
|
|
||||||
|
|
||||||
withdraw_threshold = float(settings.Money.BaseUSD) + float(
|
|
||||||
settings.Money.WithdrawLimit
|
|
||||||
)
|
|
||||||
remaining = withdraw_threshold - total_usd
|
|
||||||
cast_es = {
|
|
||||||
"remaining_usd": remaining,
|
|
||||||
}
|
|
||||||
self.write_to_es("get_remaining", cast_es)
|
|
||||||
return remaining
|
|
||||||
|
|
||||||
def open_trades_usd_parse_dash(self, platform, dash, rates):
|
|
||||||
cumul_usd = 0
|
|
||||||
for contact_id, contact in dash.items():
|
|
||||||
# We need created at in order to look up the historical prices
|
|
||||||
created_at = contact["data"]["created_at"]
|
|
||||||
|
|
||||||
# Reformat the date how CoinGecko likes
|
|
||||||
# 2022-05-02T11:17:14+00:00
|
|
||||||
if "+" in created_at:
|
|
||||||
date_split = created_at.split("+")
|
|
||||||
date_split[1].replace(".", "")
|
|
||||||
date_split[1].replace(":", "")
|
|
||||||
created_at = "+".join(date_split)
|
|
||||||
date_parsed = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S%z")
|
|
||||||
else:
|
|
||||||
date_parsed = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
||||||
|
|
||||||
date_formatted = date_parsed.strftime("%d-%m-%Y")
|
|
||||||
|
|
||||||
# Get the historical rates for the right asset, extract the price
|
|
||||||
if platform == "agora":
|
|
||||||
asset = contact["data"]["advertisement"]["asset"]
|
|
||||||
elif platform == "lbtc":
|
|
||||||
asset = "BTC"
|
|
||||||
if asset == "XMR":
|
|
||||||
amount_crypto = contact["data"]["amount_xmr"]
|
|
||||||
history = self.cg.get_coin_history_by_id(
|
|
||||||
id="monero", date=date_formatted
|
|
||||||
)
|
|
||||||
if "market_data" not in history:
|
|
||||||
return False
|
|
||||||
crypto_usd = float(history["market_data"]["current_price"]["usd"])
|
|
||||||
elif asset == "BTC":
|
|
||||||
amount_crypto = contact["data"]["amount_btc"]
|
|
||||||
history = self.cg.get_coin_history_by_id(
|
|
||||||
id="bitcoin", date=date_formatted
|
|
||||||
)
|
|
||||||
crypto_usd = float(history["market_data"]["current_price"]["usd"])
|
|
||||||
# Convert crypto to fiat
|
|
||||||
amount = float(amount_crypto) * crypto_usd
|
|
||||||
currency = contact["data"]["currency"]
|
|
||||||
if not contact["data"]["is_selling"]:
|
|
||||||
continue
|
|
||||||
if currency == "USD":
|
|
||||||
cumul_usd += float(amount)
|
|
||||||
else:
|
|
||||||
rate = rates[currency]
|
|
||||||
amount_usd = float(amount) / rate
|
|
||||||
cumul_usd += amount_usd
|
|
||||||
return cumul_usd
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_open_trades_usd(self):
|
|
||||||
"""
|
|
||||||
Get total value of open trades in USD.
|
|
||||||
:return: total trade value
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
dash_agora = self.agora.wrap_dashboard()
|
|
||||||
dash_lbtc = self.lbtc.wrap_dashboard()
|
|
||||||
dash_agora = yield dash_agora
|
|
||||||
dash_lbtc = yield dash_lbtc
|
|
||||||
if dash_agora is False:
|
|
||||||
return False
|
|
||||||
if dash_lbtc is False:
|
|
||||||
return False
|
|
||||||
|
|
||||||
rates = self.get_rates_all()
|
|
||||||
cumul_usd_agora = self.open_trades_usd_parse_dash("agora", dash_agora, rates)
|
|
||||||
cumul_usd_lbtc = self.open_trades_usd_parse_dash("lbtc", dash_lbtc, rates)
|
|
||||||
cumul_usd = cumul_usd_agora + cumul_usd_lbtc
|
|
||||||
|
|
||||||
cast_es = {
|
|
||||||
"trades_usd": cumul_usd,
|
|
||||||
}
|
|
||||||
self.write_to_es("get_open_trades_usd", cast_es)
|
|
||||||
return cumul_usd
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_total_remaining(self):
|
|
||||||
"""
|
|
||||||
Check how much profit we need to make in order to withdraw, taking into account open trade value.
|
|
||||||
:return: profit remaining in USD
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
total_usd = yield self.get_total_usd()
|
|
||||||
total_trades_usd = yield self.get_open_trades_usd()
|
|
||||||
if not total_usd:
|
|
||||||
return False
|
|
||||||
total_usd += total_trades_usd
|
|
||||||
withdraw_threshold = float(settings.Money.BaseUSD) + float(
|
|
||||||
settings.Money.WithdrawLimit
|
|
||||||
)
|
|
||||||
remaining = withdraw_threshold - total_usd
|
|
||||||
|
|
||||||
cast_es = {
|
|
||||||
"total_remaining_usd": remaining,
|
|
||||||
}
|
|
||||||
self.write_to_es("get_total_remaining", cast_es)
|
|
||||||
return remaining
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_total_with_trades(self):
|
|
||||||
total_usd = yield self.get_total_usd()
|
|
||||||
if not total_usd:
|
|
||||||
return False
|
|
||||||
total_trades_usd = yield self.get_open_trades_usd()
|
|
||||||
total_with_trades = total_usd + total_trades_usd
|
|
||||||
cast_es = {
|
|
||||||
"total_with_trades": total_with_trades,
|
|
||||||
}
|
|
||||||
self.write_to_es("get_total_with_trades", cast_es)
|
|
||||||
return total_with_trades
|
|
|
@ -1,125 +0,0 @@
|
||||||
from serde import Model, fields
|
|
||||||
|
|
||||||
|
|
||||||
class Amount(Model):
|
|
||||||
amount: fields.Str()
|
|
||||||
currency: fields.Str()
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionBBAN(Model):
|
|
||||||
bban: fields.Optional(fields.Str())
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeInstructedAmount(Model):
|
|
||||||
amount: fields.Str()
|
|
||||||
currency: fields.Str()
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionCurrencyExchange(Model):
|
|
||||||
exchangeRate: fields.Str()
|
|
||||||
instructedAmount: fields.Nested(ExchangeInstructedAmount)
|
|
||||||
sourceCurrency: fields.Str()
|
|
||||||
targetCurrency: fields.Str()
|
|
||||||
unitCurrency: fields.Str()
|
|
||||||
|
|
||||||
|
|
||||||
class Transaction(Model):
|
|
||||||
bookingDate: fields.Date()
|
|
||||||
creditorAccount: fields.Optional(fields.Nested(TransactionBBAN))
|
|
||||||
creditorName: fields.Optional(fields.Str())
|
|
||||||
debtorName: fields.Optional(fields.Str())
|
|
||||||
currencyExchange: fields.Optional(fields.Nested(TransactionCurrencyExchange))
|
|
||||||
proprietaryBankTransactionCode: fields.Optional(fields.Str())
|
|
||||||
remittanceInformationUnstructured: fields.Optional(fields.Str())
|
|
||||||
transactionAmount: fields.Nested(Amount)
|
|
||||||
transactionId: fields.Optional(fields.Str())
|
|
||||||
|
|
||||||
|
|
||||||
class Pending(Transaction):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Booked(Transaction):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Transactions(Model):
|
|
||||||
pending: fields.List(Pending)
|
|
||||||
booked: fields.List(Booked)
|
|
||||||
|
|
||||||
|
|
||||||
class TXRoot(Model):
|
|
||||||
transactions: fields.Optional(fields.Nested(Transactions))
|
|
||||||
|
|
||||||
|
|
||||||
class AccessToken(Model):
|
|
||||||
access: fields.Str()
|
|
||||||
|
|
||||||
|
|
||||||
class Institution(Model):
|
|
||||||
id: fields.Str()
|
|
||||||
name: fields.Str()
|
|
||||||
bic: fields.Str()
|
|
||||||
transaction_total_days: fields.Str()
|
|
||||||
countries: fields.List()
|
|
||||||
logo: fields.Url()
|
|
||||||
|
|
||||||
|
|
||||||
class Institutions(Model):
|
|
||||||
institutions: fields.List(Institution)
|
|
||||||
|
|
||||||
|
|
||||||
class Agreement(Model):
|
|
||||||
id: fields.Str()
|
|
||||||
created: fields.DateTime()
|
|
||||||
redirect: fields.Url()
|
|
||||||
status: fields.Str()
|
|
||||||
institution_id: fields.Str()
|
|
||||||
agreement: fields.Str()
|
|
||||||
reference: fields.Str()
|
|
||||||
accounts: fields.List()
|
|
||||||
link: fields.Url()
|
|
||||||
ssn: fields.Literal(None) # ?
|
|
||||||
account_selection: fields.Bool()
|
|
||||||
redirect_immediate: fields.Bool()
|
|
||||||
|
|
||||||
|
|
||||||
class Requisitions(Model):
|
|
||||||
count: fields.Int()
|
|
||||||
next: fields.Literal(None) # wrong
|
|
||||||
previous: fields.Literal(None) # wrong
|
|
||||||
results: fields.List(Agreement)
|
|
||||||
|
|
||||||
|
|
||||||
class Account(Model):
|
|
||||||
resourceId: fields.Optional(fields.Str())
|
|
||||||
iban: fields.Optional(fields.Str())
|
|
||||||
bban: fields.Optional(fields.Str())
|
|
||||||
currency: fields.Str()
|
|
||||||
ownerName: fields.Optional(fields.Str())
|
|
||||||
status: fields.Optional(fields.Str())
|
|
||||||
details: fields.Optional(fields.Str())
|
|
||||||
|
|
||||||
|
|
||||||
class AccountDetails(Model):
|
|
||||||
account: fields.Optional(fields.Nested(Account))
|
|
||||||
|
|
||||||
|
|
||||||
class AccountBalanceAmount(Model):
|
|
||||||
amount: fields.Str()
|
|
||||||
currency: fields.Str()
|
|
||||||
|
|
||||||
|
|
||||||
class AccountBalances(Model):
|
|
||||||
balanceAmount: fields.Nested(AccountBalanceAmount)
|
|
||||||
balanceType: fields.Str()
|
|
||||||
referenceDate: fields.Date()
|
|
||||||
|
|
||||||
|
|
||||||
class AccountBalancesRoot(Model):
|
|
||||||
balances = fields.Optional(fields.List(AccountBalances))
|
|
||||||
|
|
||||||
|
|
||||||
class RequisitionResponse(Model):
|
|
||||||
summary: fields.Str()
|
|
||||||
detail: fields.Str()
|
|
|
@ -1,14 +0,0 @@
|
||||||
from serde import Model, fields
|
|
||||||
|
|
||||||
|
|
||||||
class AccountBalances(Model):
|
|
||||||
currency: fields.Str()
|
|
||||||
available: fields.Float()
|
|
||||||
current: fields.Float()
|
|
||||||
overdraft: fields.Float()
|
|
||||||
update_timestamp: fields.DateTime()
|
|
||||||
|
|
||||||
|
|
||||||
class AccountBalancesRoot(Model):
|
|
||||||
results: fields.List(AccountBalances)
|
|
||||||
status: fields.Str()
|
|
|
@ -1,447 +0,0 @@
|
||||||
# Twisted/Klein imports
|
|
||||||
# Other library imports
|
|
||||||
from json import dumps
|
|
||||||
from random import choices
|
|
||||||
from string import ascii_uppercase
|
|
||||||
|
|
||||||
import db
|
|
||||||
import util
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
from twisted.internet.defer import inlineCallbacks
|
|
||||||
|
|
||||||
|
|
||||||
class Transactions(util.Base):
|
|
||||||
"""
|
|
||||||
Handler class for incoming Revolut transactions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def valid_transaction(self, data):
|
|
||||||
"""
|
|
||||||
Determine if a given transaction object is valid.
|
|
||||||
:param data: a transaction cast
|
|
||||||
:type data: dict
|
|
||||||
:return: whether the transaction is valid
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
txid = data["transaction_id"]
|
|
||||||
if "amount" not in data:
|
|
||||||
return False
|
|
||||||
if "currency" not in data:
|
|
||||||
return False
|
|
||||||
amount = data["amount"]
|
|
||||||
if amount <= 0:
|
|
||||||
self.log.info(f"Ignoring transaction with negative/zero amount: {txid}")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def extract_reference(self, data):
|
|
||||||
"""
|
|
||||||
Extract a reference from the transaction cast.
|
|
||||||
:param data: a transaction cast
|
|
||||||
:type data: dict
|
|
||||||
:return: the extracted reference or not_set
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
if "reference" in data:
|
|
||||||
return data["reference"]
|
|
||||||
elif "meta" in data:
|
|
||||||
if "provider_reference" in data["meta"]:
|
|
||||||
return data["meta"]["provider_reference"]
|
|
||||||
return "not_set"
|
|
||||||
|
|
||||||
def extract_sender(self, data):
|
|
||||||
"""
|
|
||||||
Extract a sender name from the transaction cast.
|
|
||||||
:param data: a transaction cast
|
|
||||||
:type data: dict
|
|
||||||
:return: the sender name or not_set
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
if "debtorName" in data:
|
|
||||||
return data["debtorName"]
|
|
||||||
elif "meta" in data:
|
|
||||||
if "debtor_account_name" in data["meta"]:
|
|
||||||
return data["meta"]["debtor_account_name"]
|
|
||||||
elif " " in data["reference"]:
|
|
||||||
refsplit = data["reference"].split(" ")
|
|
||||||
if not len(refsplit) == 2:
|
|
||||||
self.log.error(f"Sender cannot be extracted: {data}")
|
|
||||||
return "not_set"
|
|
||||||
realname, part2 = data["reference"].split(" ")
|
|
||||||
return realname
|
|
||||||
|
|
||||||
return "not_set"
|
|
||||||
|
|
||||||
def reference_partial_check(self, reference, txid, currency, amount):
|
|
||||||
"""
|
|
||||||
Perform a partial check by intersecting all parts of the split of the
|
|
||||||
reference against the existing references, and returning a set of the matches.
|
|
||||||
:param reference: the reference to check
|
|
||||||
:type reference: str
|
|
||||||
:return: matching trade ID string
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
# Partial reference implementation
|
|
||||||
# Account for silly people not removing the default string
|
|
||||||
# Split the reference into parts
|
|
||||||
ref_split = reference.split(" ")
|
|
||||||
# Get all existing references
|
|
||||||
existing_refs = db.get_refs()
|
|
||||||
# Get all parts of the given reference split that match the existing references
|
|
||||||
stored_trade_reference = set(existing_refs).intersection(set(ref_split))
|
|
||||||
if len(stored_trade_reference) > 1:
|
|
||||||
self.log.error(f"Multiple references valid for TXID {txid}: {reference}")
|
|
||||||
self.irc.sendmsg(f"Multiple references valid for TXID {txid}: {reference}")
|
|
||||||
self.ux.notify.notify_tx_lookup_failed(
|
|
||||||
currency, amount, reference, "MULTIPLE_REFS_MATCH"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
if len(stored_trade_reference) == 0:
|
|
||||||
return None
|
|
||||||
return stored_trade_reference.pop()
|
|
||||||
|
|
||||||
def can_alt_lookup(self, amount, currency, reference):
|
|
||||||
amount_usd = self.money.to_usd(amount, currency)
|
|
||||||
# Amount is reliable here as it is checked by find_trade, so no need for stored_trade["amount"]
|
|
||||||
if float(amount_usd) > float(settings.Agora.AcceptableAltLookupUSD):
|
|
||||||
self.log.info(
|
|
||||||
"Not checking against amount and currency as amount exceeds MAX"
|
|
||||||
)
|
|
||||||
self.irc.sendmsg(
|
|
||||||
"Not checking against amount and currency as amount exceeds MAX"
|
|
||||||
)
|
|
||||||
# Close here if the amount exceeds the allowable limit for no reference
|
|
||||||
self.ux.notify.notify_tx_lookup_failed(
|
|
||||||
currency, amount, reference, "EXCEEDS_MAX"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def amount_currency_lookup(self, amount, currency, txid, reference):
|
|
||||||
self.log.info(f"No reference in DB refs for {reference}")
|
|
||||||
self.irc.sendmsg(f"No reference in DB refs for {reference}")
|
|
||||||
# Try checking just amount and currency, as some people (usually people buying small amounts)
|
|
||||||
# are unable to put in a reference properly.
|
|
||||||
|
|
||||||
self.log.info(f"Checking against amount and currency for TXID {txid}")
|
|
||||||
self.irc.sendmsg(f"Checking against amount and currency for TXID {txid}")
|
|
||||||
if not self.can_alt_lookup(amount, currency, reference):
|
|
||||||
return False
|
|
||||||
stored_trade = self.find_trade(txid, currency, amount)
|
|
||||||
if not stored_trade:
|
|
||||||
self.log.info(
|
|
||||||
f"Failed to get reference by amount and currency: {txid} {currency} {amount}"
|
|
||||||
)
|
|
||||||
self.irc.sendmsg(
|
|
||||||
f"Failed to get reference by amount and currency: {txid} {currency} {amount}"
|
|
||||||
)
|
|
||||||
self.ux.notify.notify_tx_lookup_failed(
|
|
||||||
currency, amount, reference, "ALT_LOOKUP_FAILED"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
stored_trade["amount"] = float(stored_trade["amount"]) # convert to float
|
|
||||||
return stored_trade
|
|
||||||
|
|
||||||
def normal_lookup(self, stored_trade_reference, reference, currency, amount):
|
|
||||||
stored_trade = db.get_ref(stored_trade_reference)
|
|
||||||
if not stored_trade:
|
|
||||||
self.log.info(f"No reference in DB for {reference}")
|
|
||||||
self.irc.sendmsg(f"No reference in DB for {reference}")
|
|
||||||
self.ux.notify.notify_tx_lookup_failed(
|
|
||||||
currency, amount, reference, "NOREF", stored_trade_reference
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
stored_trade["amount"] = float(stored_trade["amount"]) # convert to float
|
|
||||||
return stored_trade
|
|
||||||
|
|
||||||
def currency_check(self, currency, amount, reference, stored_trade):
|
|
||||||
if not stored_trade["currency"] == currency:
|
|
||||||
self.log.info(
|
|
||||||
f"Currency mismatch, Agora: {stored_trade['currency']} / Sink: {currency}"
|
|
||||||
)
|
|
||||||
self.irc.sendmsg(
|
|
||||||
f"Currency mismatch, Agora: {stored_trade['currency']} / Sink: {currency}"
|
|
||||||
)
|
|
||||||
self.ux.notify.notify_tx_lookup_failed(
|
|
||||||
currency,
|
|
||||||
amount,
|
|
||||||
reference,
|
|
||||||
"CURRENCY_MISMATCH",
|
|
||||||
stored_trade["id"],
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def alt_amount_check(self, platform, amount, currency, reference, stored_trade):
|
|
||||||
# If the amount does not match exactly, get the min and max values for our given acceptable margins for trades
|
|
||||||
min_amount, max_amount = self.money.get_acceptable_margins(
|
|
||||||
platform, currency, stored_trade["amount"]
|
|
||||||
)
|
|
||||||
self.log.info(
|
|
||||||
f"Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}"
|
|
||||||
)
|
|
||||||
self.irc.sendmsg(
|
|
||||||
f"Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}"
|
|
||||||
)
|
|
||||||
if not min_amount < amount < max_amount:
|
|
||||||
self.log.info(
|
|
||||||
"Amount mismatch - not in margins: {stored_trade['amount']} (min: {min_amount} / max: {max_amount}"
|
|
||||||
)
|
|
||||||
self.irc.sendmsg(
|
|
||||||
f"Amount mismatch - not in margins: {stored_trade['amount']} (min: {min_amount} / max: {max_amount}"
|
|
||||||
)
|
|
||||||
self.ux.notify.notify_tx_lookup_failed(
|
|
||||||
currency,
|
|
||||||
amount,
|
|
||||||
reference,
|
|
||||||
"AMOUNT_MARGIN_MISMATCH",
|
|
||||||
stored_trade["id"],
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def transaction(self, data):
|
|
||||||
"""
|
|
||||||
Store details of transaction and post notifications to IRC.
|
|
||||||
Matches it up with data stored in Redis to attempt to reconcile with an Agora trade.
|
|
||||||
:param data: details of transaction
|
|
||||||
:type data: dict
|
|
||||||
"""
|
|
||||||
valid = self.valid_transaction(data)
|
|
||||||
if not valid:
|
|
||||||
return False
|
|
||||||
ts = data["timestamp"]
|
|
||||||
txid = data["transaction_id"]
|
|
||||||
amount = float(data["amount"])
|
|
||||||
currency = data["currency"]
|
|
||||||
|
|
||||||
reference = self.extract_reference(data)
|
|
||||||
sender = self.extract_sender(data)
|
|
||||||
|
|
||||||
subclass = data["subclass"]
|
|
||||||
to_store = {
|
|
||||||
"subclass": subclass,
|
|
||||||
"ts": ts,
|
|
||||||
"txid": txid,
|
|
||||||
"reference": reference,
|
|
||||||
"amount": amount,
|
|
||||||
"currency": currency,
|
|
||||||
"sender": sender,
|
|
||||||
}
|
|
||||||
db.r.hmset(f"tx.{txid}", to_store)
|
|
||||||
|
|
||||||
self.log.info(f"Transaction processed: {dumps(to_store, indent=2)}")
|
|
||||||
self.irc.sendmsg(
|
|
||||||
f"AUTO Incoming transaction on {subclass}: {txid} {amount}{currency} ({reference})"
|
|
||||||
)
|
|
||||||
|
|
||||||
stored_trade_reference = self.reference_partial_check(
|
|
||||||
reference, txid, currency, amount
|
|
||||||
)
|
|
||||||
if stored_trade_reference is False: # can be None though
|
|
||||||
return
|
|
||||||
|
|
||||||
stored_trade = False
|
|
||||||
looked_up_without_reference = False
|
|
||||||
|
|
||||||
# Normal implementation for when we have a reference
|
|
||||||
if stored_trade_reference:
|
|
||||||
stored_trade = self.normal_lookup(
|
|
||||||
stored_trade_reference, reference, currency, amount
|
|
||||||
)
|
|
||||||
# if not stored_trade:
|
|
||||||
# return
|
|
||||||
|
|
||||||
# Amount/currency lookup implementation for when we have no reference
|
|
||||||
else:
|
|
||||||
if not stored_trade: # check we don't overwrite the lookup above
|
|
||||||
stored_trade = self.amount_currency_lookup(
|
|
||||||
amount, currency, txid, reference
|
|
||||||
)
|
|
||||||
if stored_trade is False:
|
|
||||||
return
|
|
||||||
if stored_trade:
|
|
||||||
# Note that we have looked it up without reference so we don't use +- below
|
|
||||||
# This might be redundant given the amount checks in find_trade, but better safe than sorry!
|
|
||||||
looked_up_without_reference = True
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
# Stored trade reference is none, the checks below will do nothing at all
|
|
||||||
return
|
|
||||||
|
|
||||||
# Make sure it was sent in the expected currency
|
|
||||||
if not self.currency_check(currency, amount, reference, stored_trade):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Make sure the expected amount was sent
|
|
||||||
if not stored_trade["amount"] == amount:
|
|
||||||
if looked_up_without_reference:
|
|
||||||
return
|
|
||||||
platform = stored_trade["subclass"]
|
|
||||||
if not self.alt_amount_check(
|
|
||||||
platform, amount, currency, reference, stored_trade
|
|
||||||
):
|
|
||||||
return
|
|
||||||
platform = stored_trade["subclass"]
|
|
||||||
platform_buyer = stored_trade["buyer"]
|
|
||||||
|
|
||||||
# Check sender - we don't do anything with this yet
|
|
||||||
sender_valid = self.antifraud.check_valid_sender(
|
|
||||||
reference, platform, sender, platform_buyer
|
|
||||||
)
|
|
||||||
self.log.info(f"Trade {reference} buyer {platform_buyer} valid: {sender_valid}")
|
|
||||||
# trade_released = self.release_map_trade(reference, txid)
|
|
||||||
# if trade_released:
|
|
||||||
# self.ux.notify.notify_complete_trade(amount, currency)
|
|
||||||
# else:
|
|
||||||
# self.log.error(f"Cannot release trade {reference}.")
|
|
||||||
# return
|
|
||||||
|
|
||||||
rtrn = yield self.release_funds(stored_trade["id"], stored_trade["reference"])
|
|
||||||
if rtrn:
|
|
||||||
self.ux.notify.notify_complete_trade(amount, currency)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def release_funds(self, trade_id, reference):
|
|
||||||
stored_trade = db.get_ref(reference)
|
|
||||||
platform = stored_trade["subclass"]
|
|
||||||
logmessage = f"All checks passed, releasing funds for {trade_id} {reference}"
|
|
||||||
self.log.info(logmessage)
|
|
||||||
self.irc.sendmsg(logmessage)
|
|
||||||
if platform == "agora":
|
|
||||||
release = self.agora.release_funds
|
|
||||||
post_message = self.agora.api.contact_message_post
|
|
||||||
elif platform == "lbtc":
|
|
||||||
release = self.lbtc.release_funds
|
|
||||||
post_message = self.lbtc.api.contact_message_post
|
|
||||||
|
|
||||||
rtrn = yield release(trade_id)
|
|
||||||
if rtrn["message"] == "OK":
|
|
||||||
post_message(trade_id, "Thanks! Releasing now :)")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logmessage = f"Release funds unsuccessful: {rtrn['message']}"
|
|
||||||
self.log.error(logmessage)
|
|
||||||
self.irc.sendmsg(logmessage)
|
|
||||||
self.ux.notify.notify_release_unsuccessful(trade_id)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Parse the escrow release response
|
|
||||||
message = rtrn["message"]
|
|
||||||
# message_long = rtrn["response"]["data"]["message"]
|
|
||||||
self.irc.sendmsg(f"{dumps(message)}")
|
|
||||||
|
|
||||||
def release_map_trade(self, reference, tx):
|
|
||||||
"""
|
|
||||||
Map a trade to a transaction and release if no other TX is
|
|
||||||
mapped to the same trade.
|
|
||||||
"""
|
|
||||||
stored_trade = db.get_ref(reference)
|
|
||||||
if not stored_trade:
|
|
||||||
self.log.error(f"Could not get stored trade for {reference}.")
|
|
||||||
return None
|
|
||||||
tx_obj = db.get_tx(tx)
|
|
||||||
if not tx_obj:
|
|
||||||
self.log.error(f"Could not get TX for {tx}.")
|
|
||||||
return None
|
|
||||||
platform = stored_trade["subclass"]
|
|
||||||
platform_buyer = stored_trade["buyer"]
|
|
||||||
bank_sender = tx_obj["sender"]
|
|
||||||
trade_id = stored_trade["id"]
|
|
||||||
is_updated = self.antifraud.update_trade_tx(reference, tx)
|
|
||||||
if is_updated is None:
|
|
||||||
return None
|
|
||||||
elif is_updated is True:
|
|
||||||
# We mapped the trade successfully
|
|
||||||
self.release_funds(trade_id, reference)
|
|
||||||
self.antifraud.add_bank_sender(platform, platform_buyer, bank_sender)
|
|
||||||
return True
|
|
||||||
elif is_updated is False:
|
|
||||||
# Already mapped
|
|
||||||
self.log.error(
|
|
||||||
f"Trade {reference} already has a TX mapped, cannot map {tx}."
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def new_trade(
|
|
||||||
self,
|
|
||||||
subclass,
|
|
||||||
asset,
|
|
||||||
trade_id,
|
|
||||||
buyer,
|
|
||||||
currency,
|
|
||||||
amount,
|
|
||||||
amount_crypto,
|
|
||||||
provider,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Called when we have a new trade in Agora.
|
|
||||||
Store details in Redis, generate a reference and optionally let the customer know the reference.
|
|
||||||
"""
|
|
||||||
reference = "".join(choices(ascii_uppercase, k=5))
|
|
||||||
reference = f"PGN-{reference}"
|
|
||||||
existing_ref = db.r.get(f"trade.{trade_id}.reference")
|
|
||||||
if not existing_ref:
|
|
||||||
to_store = {
|
|
||||||
"id": trade_id,
|
|
||||||
"tx": "",
|
|
||||||
"asset": asset,
|
|
||||||
"buyer": buyer,
|
|
||||||
"currency": currency,
|
|
||||||
"amount": amount,
|
|
||||||
"amount_crypto": amount_crypto,
|
|
||||||
"reference": reference,
|
|
||||||
"provider": provider,
|
|
||||||
"subclass": subclass,
|
|
||||||
}
|
|
||||||
self.log.info(f"Storing trade information: {str(to_store)}")
|
|
||||||
db.r.hmset(f"trade.{reference}", to_store)
|
|
||||||
db.r.set(f"trade.{trade_id}.reference", reference)
|
|
||||||
self.irc.sendmsg(f"Generated reference for {trade_id}: {reference}")
|
|
||||||
self.ux.notify.notify_new_trade(amount, currency)
|
|
||||||
uid = self.ux.verify.create_uid(subclass, buyer)
|
|
||||||
verified = self.ux.verify.get_external_user_id_status(uid)
|
|
||||||
if verified != "GREEN":
|
|
||||||
self.log.info(f"UID {uid} is not verified, sending link.")
|
|
||||||
self.antifraud.send_verification_url(subclass, uid, trade_id)
|
|
||||||
else: # User is verified
|
|
||||||
self.log.info(f"UID {uid} is verified.")
|
|
||||||
self.markets.send_bank_details(subclass, currency, trade_id)
|
|
||||||
self.markets.send_reference(subclass, trade_id, reference)
|
|
||||||
if existing_ref:
|
|
||||||
return util.convert(existing_ref)
|
|
||||||
else:
|
|
||||||
return reference
|
|
||||||
|
|
||||||
def find_trade(self, txid, currency, amount):
|
|
||||||
"""
|
|
||||||
Get a trade reference that matches the given currency and amount.
|
|
||||||
Only works if there is one result.
|
|
||||||
:param txid: Sink transaction ID
|
|
||||||
:param currency: currency
|
|
||||||
:param amount: amount
|
|
||||||
:type txid: string
|
|
||||||
:type currency: string
|
|
||||||
:type amount: int
|
|
||||||
:return: matching trade object or False
|
|
||||||
:rtype: dict or bool
|
|
||||||
"""
|
|
||||||
refs = db.get_refs()
|
|
||||||
matching_refs = []
|
|
||||||
# TODO: use get_ref_map in this function instead of calling get_ref multiple times
|
|
||||||
for ref in refs:
|
|
||||||
stored_trade = db.get_ref(ref)
|
|
||||||
if stored_trade["currency"] == currency and float(
|
|
||||||
stored_trade["amount"]
|
|
||||||
) == float(amount):
|
|
||||||
matching_refs.append(stored_trade)
|
|
||||||
if len(matching_refs) != 1:
|
|
||||||
self.log.error(
|
|
||||||
f"Find trade returned multiple results for TXID {txid}: {matching_refs}"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
return matching_refs[0]
|
|
|
@ -1,16 +0,0 @@
|
||||||
twisted
|
|
||||||
redis
|
|
||||||
pyOpenSSL
|
|
||||||
Klein
|
|
||||||
ConfigObject
|
|
||||||
service_identity
|
|
||||||
forex_python
|
|
||||||
simplejson
|
|
||||||
requests
|
|
||||||
arrow
|
|
||||||
httpx
|
|
||||||
pre-commit
|
|
||||||
pycoingecko
|
|
||||||
PyOTP
|
|
||||||
opensearch
|
|
||||||
serde[ext]
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
pre-commit run -a
|
|
||||||
python -m unittest discover -s tests -p 'test_*.py'
|
|
|
@ -1,139 +0,0 @@
|
||||||
[App]
|
|
||||||
bindhost = 0.0.0.0
|
|
||||||
|
|
||||||
[Nordigen]
|
|
||||||
base = https://ob.nordigen.com/api/v2
|
|
||||||
id =
|
|
||||||
key =
|
|
||||||
access =
|
|
||||||
|
|
||||||
[TrueLayer]
|
|
||||||
authbase = https://auth.truelayer.com
|
|
||||||
database = https://api.truelayer.com/data/v1
|
|
||||||
id =
|
|
||||||
key =
|
|
||||||
refreshkeys = {}
|
|
||||||
maps =
|
|
||||||
tokenrefreshsec = 600
|
|
||||||
refreshsec = 10
|
|
||||||
authcode =
|
|
||||||
refreshtoken =
|
|
||||||
setuptoken = 0
|
|
||||||
callbackurl =
|
|
||||||
|
|
||||||
[Agora]
|
|
||||||
base = https://agoradesk.com/api/v1
|
|
||||||
token =
|
|
||||||
pass =
|
|
||||||
username = topmonero
|
|
||||||
refreshsec = 10
|
|
||||||
feedbackscore = 0
|
|
||||||
paymentmethoddetails = ✅ONLINE NOW 🥷NO KYC 🚀INSTANT
|
|
||||||
send = 0
|
|
||||||
cheat = 0
|
|
||||||
cheatsec = 300
|
|
||||||
ad = Hello and welcome!
|
|
||||||
|
|
||||||
Please start a trade and wait until I send the reference/note, then complete the payment using the payment details.
|
|
||||||
|
|
||||||
Instructions below are for the Revolut app, but you may send with instant SEPA or bank transfer in the UK.
|
|
||||||
|
|
||||||
Step-by-step instructions in Revolut app:
|
|
||||||
* Go to **Send** on balance page
|
|
||||||
* Click **New** in top right corner
|
|
||||||
* Select **Add a bank recipient**
|
|
||||||
* Select **Business**
|
|
||||||
* Set **Country of recipient's bank** to **"United Kingdom"**
|
|
||||||
$PAYMENT$
|
|
||||||
* Set **Company name** to **"PATHOGEN LIMITED"**
|
|
||||||
* Leave e-mail blank
|
|
||||||
* Click **Add recipient**
|
|
||||||
* If you are asked for address information, please use **24 Holborn Viaduct, London, England, EC1A 2BN**
|
|
||||||
* **IMPORTANT:** Set **Reference** to the ID provided in chat (example: **$ASSET$-XXXXX**), without this I 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
|
|
|
@ -1,5 +0,0 @@
|
||||||
# Other library imports
|
|
||||||
from ConfigObject import ConfigObject
|
|
||||||
|
|
||||||
# Load the configuration
|
|
||||||
settings = ConfigObject(filename="settings.ini")
|
|
|
@ -1,123 +0,0 @@
|
||||||
# Other library imports
|
|
||||||
# import requests
|
|
||||||
# from json import dumps
|
|
||||||
|
|
||||||
import sinks.nordigen
|
|
||||||
import sinks.truelayer
|
|
||||||
import util
|
|
||||||
from db import r
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
|
|
||||||
class Sinks(util.Base):
|
|
||||||
"""
|
|
||||||
Class to manage calls to various sinks.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.account_info = {}
|
|
||||||
self.currencies = []
|
|
||||||
|
|
||||||
def __irc_started__(self):
|
|
||||||
self.log.debug("IRC hook called.")
|
|
||||||
self.startup()
|
|
||||||
self.log.debug("Finished initialising subclasses.")
|
|
||||||
|
|
||||||
def all_sinks_authenticated(self): # TODO: fix
|
|
||||||
self.money.setup_loops()
|
|
||||||
|
|
||||||
def startup(self):
|
|
||||||
"""
|
|
||||||
We NEED the other libraries, and we initialise fast, so don't make
|
|
||||||
any race conditions by relying on something that might not be there.
|
|
||||||
"""
|
|
||||||
if settings.Nordigen.enabled == "1":
|
|
||||||
self.nordigen = sinks.nordigen.Nordigen(self)
|
|
||||||
if settings.TrueLayer.enabled == "1":
|
|
||||||
self.truelayer = sinks.truelayer.TrueLayer(self)
|
|
||||||
# setattr(self.truelayer, "sinks", self)
|
|
||||||
|
|
||||||
def got_transactions(self, subclass, account_id, transactions):
|
|
||||||
if not transactions:
|
|
||||||
return False
|
|
||||||
transaction_ids = [x["transaction_id"] for x in transactions]
|
|
||||||
new_key_name = f"new.transactions.{subclass}.{account_id}"
|
|
||||||
old_key_name = f"transactions.{subclass}.{account_id}"
|
|
||||||
# for transaction_id in transaction_ids:
|
|
||||||
if not transaction_ids:
|
|
||||||
return
|
|
||||||
r.sadd(new_key_name, *transaction_ids)
|
|
||||||
|
|
||||||
difference = list(r.sdiff(new_key_name, old_key_name))
|
|
||||||
|
|
||||||
difference = util.convert(difference)
|
|
||||||
|
|
||||||
new_transactions = [
|
|
||||||
x for x in transactions if x["transaction_id"] in difference
|
|
||||||
]
|
|
||||||
|
|
||||||
# Rename the new key to the old key so we can run the diff again
|
|
||||||
r.rename(new_key_name, old_key_name)
|
|
||||||
for transaction in new_transactions:
|
|
||||||
transaction["subclass"] = subclass
|
|
||||||
self.tx.transaction(transaction)
|
|
||||||
|
|
||||||
def got_account_info(self, subclass, account_infos):
|
|
||||||
"""
|
|
||||||
Called when we get account information from an API provider.
|
|
||||||
:param subclass: class name that called it, truelayer, fidor, etc
|
|
||||||
:param account_infos: dict of dicts of account information
|
|
||||||
:param account_infos: dict
|
|
||||||
"""
|
|
||||||
if not account_infos:
|
|
||||||
self.log.error(f"No accounts provided for {subclass}") #
|
|
||||||
return
|
|
||||||
for bank, accounts in account_infos.items():
|
|
||||||
for index, account in enumerate(list(accounts)):
|
|
||||||
if "account_number" not in account:
|
|
||||||
account_infos[bank][index]["account_number"] = {}
|
|
||||||
fields = ["sort_code", "number", "iban"]
|
|
||||||
for field in fields:
|
|
||||||
if field in account:
|
|
||||||
account_infos[bank][index]["account_number"][
|
|
||||||
field
|
|
||||||
] = account[field]
|
|
||||||
del account_infos[bank][index][field]
|
|
||||||
if len(account["account_number"]) == 1:
|
|
||||||
account_infos[bank].remove(account)
|
|
||||||
self.log.warning(f"Potentially useless bank account: {account}")
|
|
||||||
currencies = [
|
|
||||||
account["currency"]
|
|
||||||
for bank, accounts in account_infos.items()
|
|
||||||
for account in accounts
|
|
||||||
]
|
|
||||||
for bank, accounts in account_infos.items():
|
|
||||||
self.account_info[bank] = []
|
|
||||||
for account in accounts:
|
|
||||||
self.account_info[bank].append(account)
|
|
||||||
# self.account_info = account_infos
|
|
||||||
self.currencies = currencies
|
|
||||||
|
|
||||||
# parsed_details =
|
|
||||||
# {"EUR": {"IBAN": "xxx", "BIC": "xxx"},
|
|
||||||
# "GBP": {"SORT": "04-04-04", "ACCOUNT": "1922-2993"}}
|
|
||||||
# self.markets.distribute_account_details(currencies, account_infos)
|
|
||||||
|
|
||||||
def get_total_usd(self):
|
|
||||||
"""
|
|
||||||
Get the total balance of our accounts in USD.
|
|
||||||
"""
|
|
||||||
total = 0
|
|
||||||
if settings.Nordigen.enabled == "1":
|
|
||||||
total_nordigen = self.nordigen.get_total_map()
|
|
||||||
total_nordigen_usd = self.money.multiple_to_usd(total_nordigen)
|
|
||||||
total += total_nordigen_usd
|
|
||||||
if settings.TrueLayer.enabled == "1":
|
|
||||||
total_truelayer = self.truelayer.get_total_map()
|
|
||||||
total_truelayer_usd = self.money.multiple_to_usd(total_truelayer)
|
|
||||||
total += total_truelayer_usd
|
|
||||||
|
|
||||||
return total
|
|
|
@ -1,488 +0,0 @@
|
||||||
# Twisted/Klein imports
|
|
||||||
from hashlib import sha256
|
|
||||||
from json import dumps, loads
|
|
||||||
|
|
||||||
# Other library imports
|
|
||||||
import requests
|
|
||||||
import treq
|
|
||||||
import util
|
|
||||||
from lib.serde.nordigen import (
|
|
||||||
AccessToken,
|
|
||||||
AccountBalancesRoot,
|
|
||||||
AccountDetails,
|
|
||||||
Agreement,
|
|
||||||
Institutions,
|
|
||||||
RequisitionResponse,
|
|
||||||
Requisitions,
|
|
||||||
TXRoot,
|
|
||||||
)
|
|
||||||
from serde import ValidationError
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
from simplejson.errors import JSONDecodeError
|
|
||||||
from twisted.internet.defer import inlineCallbacks
|
|
||||||
from twisted.internet.task import LoopingCall
|
|
||||||
|
|
||||||
|
|
||||||
class Nordigen(util.Base):
|
|
||||||
"""
|
|
||||||
Class to manage calls to Open Banking APIs through Nordigen.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, sinks):
|
|
||||||
super().__init__()
|
|
||||||
self.sinks = sinks
|
|
||||||
self.token = None
|
|
||||||
self.banks = {}
|
|
||||||
self.authed = False
|
|
||||||
self.requisitions = None
|
|
||||||
|
|
||||||
# Get the banks from the config and cache them
|
|
||||||
self.log.debug("Getting mapped accounts.")
|
|
||||||
self.get_mapped_accounts()
|
|
||||||
self.log.debug("Finished getting mapped accounts.")
|
|
||||||
|
|
||||||
self.log.debug("Creating loop to get access token.")
|
|
||||||
self.lc = LoopingCall(self.get_access_token)
|
|
||||||
self.lc.start(int(settings.Nordigen.TokenRefreshSec))
|
|
||||||
self.log.debug("Finished creating loops.")
|
|
||||||
|
|
||||||
def __authed__(self):
|
|
||||||
"""
|
|
||||||
Called when we have received the access token.
|
|
||||||
"""
|
|
||||||
self.log.info("Connection authenticated.")
|
|
||||||
# self.get_requisitions()
|
|
||||||
d = self.get_all_account_info()
|
|
||||||
d.addCallback(self.got_all_account_info)
|
|
||||||
self.sinks.all_sinks_authenticated()
|
|
||||||
|
|
||||||
def got_all_account_info(self, account_infos):
|
|
||||||
# Filter for added accounts since we only do that for TrueLayer
|
|
||||||
account_infos = {
|
|
||||||
bank: accounts
|
|
||||||
for bank, accounts in account_infos.items()
|
|
||||||
for account in accounts
|
|
||||||
if account["account_id"] in self.banks
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sinks.got_account_info("nordigen", account_infos)
|
|
||||||
|
|
||||||
self.lc_tx = LoopingCall(self.transaction_loop)
|
|
||||||
self.lc_tx.start(int(settings.Nordigen.RefreshSec))
|
|
||||||
|
|
||||||
def transaction_loop(self):
|
|
||||||
for account_id in self.banks:
|
|
||||||
transactions = self.get_transactions(account_id)
|
|
||||||
transactions.addCallback(self.got_transactions, account_id)
|
|
||||||
|
|
||||||
def got_transactions(self, transactions, account_id):
|
|
||||||
self.sinks.got_transactions("nordigen", account_id, transactions)
|
|
||||||
|
|
||||||
def generic_deferred(self, response, dest_func):
|
|
||||||
"""
|
|
||||||
Generic function to take a treq response and fire a callback with
|
|
||||||
its content to dest_func.
|
|
||||||
:param response: a treq response
|
|
||||||
:param dest_func: function to call with the response data
|
|
||||||
"""
|
|
||||||
self.log.debug(f"Generic deferred received: {response}")
|
|
||||||
content = response.content()
|
|
||||||
content.addCallback(dest_func)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_access_token(self):
|
|
||||||
"""
|
|
||||||
Get an access token.
|
|
||||||
:return: True or False
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
headers = {
|
|
||||||
"accept": "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
data = {
|
|
||||||
"secret_id": settings.Nordigen.ID,
|
|
||||||
"secret_key": settings.Nordigen.Key,
|
|
||||||
}
|
|
||||||
path = f"{settings.Nordigen.Base}/token/new/"
|
|
||||||
self.log.debug("Getting new access token.")
|
|
||||||
d = yield treq.post(path, headers=headers, data=data)
|
|
||||||
content = yield d.content()
|
|
||||||
try:
|
|
||||||
obj = AccessToken.from_json(content)
|
|
||||||
except ValidationError as err:
|
|
||||||
self.log.error(f"Validation error: {err}")
|
|
||||||
return
|
|
||||||
parsed = obj.to_dict()
|
|
||||||
self.token = parsed["access"]
|
|
||||||
self.log.info("Refreshed access token")
|
|
||||||
if not self.authed:
|
|
||||||
self.__authed__()
|
|
||||||
self.authed = True
|
|
||||||
|
|
||||||
def get_institutions(self, country, filter_name=None):
|
|
||||||
"""
|
|
||||||
Get a list of supported institutions.
|
|
||||||
:param country: country to query
|
|
||||||
:param filter_name: return only results with this in the name
|
|
||||||
:return: list of institutions
|
|
||||||
:rtype: list
|
|
||||||
"""
|
|
||||||
if not len(country) == 2:
|
|
||||||
return False
|
|
||||||
headers = {
|
|
||||||
"accept": "application/json",
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
}
|
|
||||||
path = f"{settings.Nordigen.Base}/institutions/?country={country}"
|
|
||||||
r = requests.get(path, headers=headers)
|
|
||||||
try:
|
|
||||||
parsed_pre = r.json()
|
|
||||||
except JSONDecodeError:
|
|
||||||
self.log.error(f"Error parsing institutions response: {r.content}")
|
|
||||||
return False
|
|
||||||
parsed = {"institutions": parsed_pre}
|
|
||||||
try:
|
|
||||||
obj = Institutions.from_dict(parsed)
|
|
||||||
except ValidationError as err:
|
|
||||||
self.log.error(f"Validation error: {err}")
|
|
||||||
return
|
|
||||||
parsed = obj.to_dict()["institutions"]
|
|
||||||
new_list = []
|
|
||||||
if filter_name:
|
|
||||||
for i in parsed:
|
|
||||||
if filter_name in i["name"]:
|
|
||||||
new_list.append(i)
|
|
||||||
return new_list
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
def build_link(self, institution_id):
|
|
||||||
"""Create a link to access an institution.
|
|
||||||
:param institution_id: ID of the institution
|
|
||||||
"""
|
|
||||||
headers = {
|
|
||||||
"accept": "application/json",
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
}
|
|
||||||
path = f"{settings.Nordigen.Base}/requisitions/"
|
|
||||||
data = {
|
|
||||||
"institution_id": institution_id,
|
|
||||||
"redirect": settings.Nordigen.CallbackURL,
|
|
||||||
}
|
|
||||||
r = requests.post(path, headers=headers, data=data)
|
|
||||||
try:
|
|
||||||
obj = Agreement.from_json(r.content)
|
|
||||||
except ValidationError as err:
|
|
||||||
self.log.error(f"Validation error: {err}")
|
|
||||||
return
|
|
||||||
parsed = obj.to_dict()
|
|
||||||
if "link" in parsed:
|
|
||||||
return parsed["link"]
|
|
||||||
return False
|
|
||||||
|
|
||||||
def create_auth_url(self, country, bank_name):
|
|
||||||
"""Helper to look up a bank and create a link.
|
|
||||||
:param country: country
|
|
||||||
:param bank_name: bank name string to search"""
|
|
||||||
institutions = self.get_institutions(country, filter_name=bank_name)
|
|
||||||
# We were not precise enough to have one result
|
|
||||||
if not len(institutions) == 1:
|
|
||||||
return False
|
|
||||||
institution = institutions[0]
|
|
||||||
link = self.build_link(institution["id"])
|
|
||||||
if not link:
|
|
||||||
return False
|
|
||||||
return link
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_requisitions(self):
|
|
||||||
"""
|
|
||||||
Get a list of active accounts.
|
|
||||||
"""
|
|
||||||
headers = {
|
|
||||||
"accept": "application/json",
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
}
|
|
||||||
path = f"{settings.Nordigen.Base}/requisitions"
|
|
||||||
d = yield treq.get(path, headers=headers)
|
|
||||||
content = yield d.content()
|
|
||||||
try:
|
|
||||||
obj = Requisitions.from_json(content)
|
|
||||||
except ValidationError as err:
|
|
||||||
self.log.error(f"Validation error: {err}")
|
|
||||||
return
|
|
||||||
parsed = obj.to_dict()
|
|
||||||
if "results" in parsed:
|
|
||||||
return parsed["results"]
|
|
||||||
else:
|
|
||||||
self.log.error(f"Results not in requisitions response: {parsed}")
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def delete_requisition(self, requisition_id):
|
|
||||||
"""
|
|
||||||
Delete a requisision ID.
|
|
||||||
"""
|
|
||||||
headers = {
|
|
||||||
"accept": "application/json",
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
}
|
|
||||||
path = f"{settings.Nordigen.Base}/requisitions/{requisition_id}/"
|
|
||||||
r = requests.delete(path, headers=headers)
|
|
||||||
try:
|
|
||||||
obj = RequisitionResponse.from_json(r.content)
|
|
||||||
except ValidationError as err:
|
|
||||||
self.log.error(f"Validation error: {err}")
|
|
||||||
return
|
|
||||||
parsed = obj.to_dict()
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_accounts(self, requisition):
|
|
||||||
"""
|
|
||||||
Get a list of accounts for a requisition.
|
|
||||||
:param requisition: requisition ID"""
|
|
||||||
headers = {
|
|
||||||
"accept": "application/json",
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
}
|
|
||||||
path = f"{settings.Nordigen.Base}/requisitions/{requisition}/"
|
|
||||||
d = yield treq.get(path, headers=headers)
|
|
||||||
content = yield d.content()
|
|
||||||
try:
|
|
||||||
obj = Agreement.from_json(content)
|
|
||||||
except ValidationError as err:
|
|
||||||
self.log.error(f"Validation error: {err}")
|
|
||||||
return
|
|
||||||
parsed = obj.to_dict()
|
|
||||||
if "accounts" in parsed:
|
|
||||||
return parsed["accounts"]
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_ownernames(self):
|
|
||||||
"""
|
|
||||||
Get list of supplementary owner names.
|
|
||||||
"""
|
|
||||||
ownernames = loads(settings.Nordigen.OwnerNames)
|
|
||||||
return ownernames
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_account(self, account_id):
|
|
||||||
"""
|
|
||||||
Get details of an account.
|
|
||||||
:param requisition: requisition ID"""
|
|
||||||
headers = {
|
|
||||||
"accept": "application/json",
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
}
|
|
||||||
path = f"{settings.Nordigen.Base}/accounts/{account_id}/details/"
|
|
||||||
d = yield treq.get(path, headers=headers)
|
|
||||||
content = yield d.content()
|
|
||||||
try:
|
|
||||||
obj = AccountDetails.from_json(content)
|
|
||||||
except ValidationError as err:
|
|
||||||
self.log.error(f"Validation error: {err}")
|
|
||||||
return
|
|
||||||
parsed_obj = obj.to_dict()
|
|
||||||
if "account" not in parsed_obj:
|
|
||||||
return False
|
|
||||||
parsed = parsed_obj["account"]
|
|
||||||
if "bban" in parsed and parsed["currency"] == "GBP":
|
|
||||||
sort_code = parsed["bban"][0:6]
|
|
||||||
account_number = parsed["bban"][6:]
|
|
||||||
if "ownerName" not in parsed:
|
|
||||||
ownernames = self.get_ownernames()
|
|
||||||
if account_id in ownernames:
|
|
||||||
parsed["ownerName"] = ownernames[account_id]
|
|
||||||
self.log.info(
|
|
||||||
f"Found supplementary owner name for {account_id}: {ownernames[account_id]}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.log.error(f"No owner name in parsed, cannot use: {account_id}")
|
|
||||||
return False
|
|
||||||
recipient = parsed["ownerName"]
|
|
||||||
del parsed["bban"]
|
|
||||||
if "iban" in parsed:
|
|
||||||
del parsed["iban"]
|
|
||||||
sort_code = "-".join(list(map("".join, zip(*[iter(sort_code)] * 2))))
|
|
||||||
parsed["sort_code"] = sort_code
|
|
||||||
parsed["number"] = account_number
|
|
||||||
parsed["recipient"] = recipient
|
|
||||||
# Let's add the account ID so we can reference it later
|
|
||||||
parsed["account_id"] = account_id
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
def get_mapped_accounts(self):
|
|
||||||
existing_entry = loads(settings.Nordigen.Maps)
|
|
||||||
self.banks = existing_entry
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def map_account(self, account_id): # TODO: inlineCallbacks?
|
|
||||||
"""
|
|
||||||
Map an account_id at a bank to an account_name.
|
|
||||||
This enables the account for fetching.
|
|
||||||
Data type: {"monzo": [account, ids, here],
|
|
||||||
"revolut": [account, ids, here]}
|
|
||||||
"""
|
|
||||||
account_data = yield self.get_account(account_id)
|
|
||||||
currency = account_data["currency"]
|
|
||||||
|
|
||||||
existing_entry = loads(settings.Nordigen.Maps)
|
|
||||||
if account_id in existing_entry:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
existing_entry.append(account_id)
|
|
||||||
|
|
||||||
settings.Nordigen.Maps = dumps(existing_entry)
|
|
||||||
self.banks = existing_entry
|
|
||||||
settings.write()
|
|
||||||
|
|
||||||
return currency
|
|
||||||
|
|
||||||
def unmap_account(self, account_id):
|
|
||||||
"""
|
|
||||||
Unmap an account_id at a bank to an account_name.
|
|
||||||
This disables the account for fetching.
|
|
||||||
Data type: {"monzo": [account, ids, here],
|
|
||||||
"revolut": [account, ids, here]}
|
|
||||||
"""
|
|
||||||
|
|
||||||
existing_entry = loads(settings.Nordigen.Maps)
|
|
||||||
if account_id not in existing_entry:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
existing_entry.remove(account_id)
|
|
||||||
|
|
||||||
settings.Nordigen.Maps = dumps(existing_entry)
|
|
||||||
self.banks = existing_entry
|
|
||||||
settings.write()
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_all_account_info(self):
|
|
||||||
to_return = {}
|
|
||||||
requisitions = yield self.get_requisitions()
|
|
||||||
if not requisitions:
|
|
||||||
self.log.error("Could not get requisitions.")
|
|
||||||
return {}
|
|
||||||
for req in requisitions:
|
|
||||||
if not req["accounts"]:
|
|
||||||
continue
|
|
||||||
accounts = yield self.get_accounts(req["id"])
|
|
||||||
for account_id in accounts:
|
|
||||||
account_info = yield self.get_account(account_id)
|
|
||||||
if not account_info:
|
|
||||||
continue
|
|
||||||
if req["institution_id"] in to_return:
|
|
||||||
to_return[req["institution_id"]].append(account_info)
|
|
||||||
else:
|
|
||||||
to_return[req["institution_id"]] = [account_info]
|
|
||||||
return to_return
|
|
||||||
|
|
||||||
def normalise_transactions(self, transactions):
|
|
||||||
for transaction in transactions:
|
|
||||||
# Rename ID
|
|
||||||
if "transactionId" in transaction:
|
|
||||||
transaction["transaction_id"] = transaction["transactionId"]
|
|
||||||
del transaction["transactionId"]
|
|
||||||
else:
|
|
||||||
# No transaction ID. This is a problem for our implementation
|
|
||||||
|
|
||||||
tx_hash = sha256(
|
|
||||||
dumps(transaction, sort_keys=True).encode("utf8")
|
|
||||||
).hexdigest()
|
|
||||||
transaction["transaction_id"] = tx_hash
|
|
||||||
|
|
||||||
# Rename timestamp
|
|
||||||
transaction["timestamp"] = transaction["bookingDate"]
|
|
||||||
del transaction["bookingDate"]
|
|
||||||
|
|
||||||
transaction["amount"] = float(transaction["transactionAmount"]["amount"])
|
|
||||||
transaction["currency"] = transaction["transactionAmount"]["currency"]
|
|
||||||
del transaction["transactionAmount"]
|
|
||||||
|
|
||||||
transaction["reference"] = transaction["remittanceInformationUnstructured"]
|
|
||||||
del transaction["remittanceInformationUnstructured"]
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_transactions(self, account_id):
|
|
||||||
"""
|
|
||||||
Get all transactions for an account.
|
|
||||||
:param account_id: account to fetch transactions for
|
|
||||||
:return: list of transactions
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
headers = {
|
|
||||||
"accept": "application/json",
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
}
|
|
||||||
path = f"{settings.Nordigen.Base}/accounts/{account_id}/transactions/"
|
|
||||||
d = yield treq.get(path, headers=headers)
|
|
||||||
content = yield d.content()
|
|
||||||
try:
|
|
||||||
obj = TXRoot.from_json(content)
|
|
||||||
except ValidationError as err:
|
|
||||||
self.log.error(f"Validation error: {err}")
|
|
||||||
return
|
|
||||||
parsed_obj = obj.to_dict()
|
|
||||||
if "transactions" not in parsed_obj:
|
|
||||||
self.log.warning(f"No transactions for account: {account_id}")
|
|
||||||
return {}
|
|
||||||
parsed = parsed_obj["transactions"]["booked"]
|
|
||||||
self.normalise_transactions(parsed)
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
def get_balance(self, account_id):
|
|
||||||
"""
|
|
||||||
Get the balance and currency of an account.
|
|
||||||
:param account_id: the account ID
|
|
||||||
:return: tuple of (currency, amount)
|
|
||||||
:rtype: tuple
|
|
||||||
"""
|
|
||||||
headers = {
|
|
||||||
"accept": "application/json",
|
|
||||||
"Authorization": f"Bearer {self.token}",
|
|
||||||
}
|
|
||||||
path = f"{settings.Nordigen.Base}/accounts/{account_id}/balances/"
|
|
||||||
r = requests.get(path, headers=headers)
|
|
||||||
try:
|
|
||||||
obj = AccountBalancesRoot.from_json(r.content)
|
|
||||||
except ValidationError as err:
|
|
||||||
self.log.error(f"Validation error: {err}")
|
|
||||||
return (False, False)
|
|
||||||
parsed = obj.to_dict()
|
|
||||||
total = 0
|
|
||||||
currency = None
|
|
||||||
if "balances" not in parsed:
|
|
||||||
self.log.error(f"No balances: {parsed}")
|
|
||||||
return (False, False)
|
|
||||||
for entry in parsed["balances"]:
|
|
||||||
if currency:
|
|
||||||
if not currency == entry["balanceAmount"]["currency"]:
|
|
||||||
self.log.error("Different currencies in balance query.")
|
|
||||||
return (False, False)
|
|
||||||
if not entry["balanceType"] == "expected":
|
|
||||||
continue
|
|
||||||
total += float(entry["balanceAmount"]["amount"])
|
|
||||||
currency = entry["balanceAmount"]["currency"]
|
|
||||||
return (currency, total)
|
|
||||||
|
|
||||||
def get_total_map(self):
|
|
||||||
"""
|
|
||||||
Return a dictionary keyed by currencies with the amounts as values.
|
|
||||||
:return: dict keyed by currency, values are amounts
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
totals = {}
|
|
||||||
for account_id in self.banks:
|
|
||||||
currency, amount = self.get_balance(account_id)
|
|
||||||
if not amount:
|
|
||||||
continue
|
|
||||||
if not currency:
|
|
||||||
continue
|
|
||||||
if currency in totals:
|
|
||||||
totals[currency] += amount
|
|
||||||
else:
|
|
||||||
totals[currency] = amount
|
|
||||||
return totals
|
|
|
@ -1,341 +0,0 @@
|
||||||
# Twisted/Klein imports
|
|
||||||
import urllib
|
|
||||||
from json import dumps, loads
|
|
||||||
from time import time
|
|
||||||
|
|
||||||
# Other library imports
|
|
||||||
import requests
|
|
||||||
import util
|
|
||||||
from lib.serde.truelayer import AccountBalancesRoot
|
|
||||||
from serde import ValidationError
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
from simplejson.errors import JSONDecodeError
|
|
||||||
from twisted.internet.task import LoopingCall
|
|
||||||
|
|
||||||
|
|
||||||
class TrueLayer(util.Base):
|
|
||||||
"""
|
|
||||||
Class to manage calls to Open Banking APIs through TrueLayer.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, sinks):
|
|
||||||
super().__init__()
|
|
||||||
self.sinks = sinks
|
|
||||||
self.tokens = {}
|
|
||||||
self.banks = {}
|
|
||||||
self.refresh_tokens = {}
|
|
||||||
self.authed = False
|
|
||||||
|
|
||||||
# Get the banks from the config and cache them
|
|
||||||
self.get_mapped_accounts()
|
|
||||||
|
|
||||||
# account we are authenticating - where to store the refresh keys
|
|
||||||
self.current_authcode_bank = None
|
|
||||||
self.lc = LoopingCall(self.get_new_tokens_all)
|
|
||||||
# self.get_new_tokens_all()
|
|
||||||
# self.get_new_token(bank)
|
|
||||||
# -> set self.tokens[bank] = access_token
|
|
||||||
self.lc.start(int(settings.TrueLayer.TokenRefreshSec))
|
|
||||||
|
|
||||||
def __authed__(self):
|
|
||||||
"""
|
|
||||||
Called when we have received all the API tokens.
|
|
||||||
"""
|
|
||||||
# Get the account information and pass it to the main function
|
|
||||||
self.log.info("All accounts authenticated: " + ", ".join(self.tokens.keys()))
|
|
||||||
account_infos = self.get_all_account_info()
|
|
||||||
self.sinks.got_account_info("truelayer", account_infos)
|
|
||||||
|
|
||||||
self.lc_tx = LoopingCall(self.transaction_loop)
|
|
||||||
self.lc_tx.start(int(settings.TrueLayer.RefreshSec))
|
|
||||||
|
|
||||||
def transaction_loop(self):
|
|
||||||
for bank in self.banks:
|
|
||||||
for account_id in self.banks[bank]:
|
|
||||||
# account_data = self.get_account(bank, account_id)
|
|
||||||
transactions = self.get_transactions(bank, account_id)
|
|
||||||
self.sinks.got_transactions("truelayer", account_id, transactions)
|
|
||||||
|
|
||||||
def add_refresh_token(self, refresh_token):
|
|
||||||
"""
|
|
||||||
Add an API key to the configuration.
|
|
||||||
Data type: {"monzo": refresh_token,
|
|
||||||
"revolut": refresh_token}
|
|
||||||
"""
|
|
||||||
account = self.current_authcode_bank
|
|
||||||
if not account:
|
|
||||||
return False
|
|
||||||
existing_entry = loads(settings.TrueLayer.RefreshKeys)
|
|
||||||
existing_entry[account] = refresh_token
|
|
||||||
settings.TrueLayer.RefreshKeys = dumps(existing_entry)
|
|
||||||
# Set the cached entry
|
|
||||||
self.refresh_tokens = existing_entry
|
|
||||||
settings.write()
|
|
||||||
|
|
||||||
# def get_refresh_tokens(self):
|
|
||||||
# existing_entry = loads(settings.TrueLayer.RefreshKeys)
|
|
||||||
# return existing_entry
|
|
||||||
|
|
||||||
def get_key(self, bank):
|
|
||||||
if bank in self.tokens:
|
|
||||||
return self.tokens[bank]
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def create_auth_url(self, bank):
|
|
||||||
query = urllib.parse.urlencode(
|
|
||||||
{
|
|
||||||
"response_type": "code",
|
|
||||||
"response_mode": "form_post",
|
|
||||||
"client_id": settings.TrueLayer.ID,
|
|
||||||
"scope": "info accounts balance transactions offline_access",
|
|
||||||
"nonce": int(time()),
|
|
||||||
"redirect_uri": settings.TrueLayer.CallbackURL,
|
|
||||||
"enable_mock": "true",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
auth_uri = f"{settings.TrueLayer.AuthBase}/?{query}&redirect_uri={settings.TrueLayer.CallbackURL}"
|
|
||||||
self.current_authcode_bank = bank
|
|
||||||
return auth_uri
|
|
||||||
|
|
||||||
def handle_authcode_received(self, authcode):
|
|
||||||
data = {
|
|
||||||
"client_id": settings.TrueLayer.ID,
|
|
||||||
"client_secret": settings.TrueLayer.Key,
|
|
||||||
"code": authcode,
|
|
||||||
"grant_type": "authorization_code",
|
|
||||||
"redirect_uri": settings.TrueLayer.CallbackURL,
|
|
||||||
}
|
|
||||||
r = requests.post(f"{settings.TrueLayer.AuthBase}/connect/token", data=data)
|
|
||||||
try:
|
|
||||||
parsed = r.json()
|
|
||||||
except JSONDecodeError:
|
|
||||||
return False
|
|
||||||
if "error" in parsed:
|
|
||||||
self.log.error("Error requesting refresh token: {parsed['error']}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Extract the access tokens
|
|
||||||
refresh_token = parsed["refresh_token"]
|
|
||||||
access_token = parsed["access_token"]
|
|
||||||
|
|
||||||
# Add the refresh token
|
|
||||||
self.add_refresh_token(refresh_token)
|
|
||||||
|
|
||||||
# Add the access
|
|
||||||
if self.current_authcode_bank:
|
|
||||||
self.tokens[self.current_authcode_bank] = access_token
|
|
||||||
else:
|
|
||||||
self.log.error("Received an authcode we didn't ask for")
|
|
||||||
return
|
|
||||||
self.log.info(
|
|
||||||
f"Retrieved access/refresh tokens for {self.current_authcode_bank}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_new_tokens_all(self):
|
|
||||||
refresh_tokens = loads(settings.TrueLayer.RefreshKeys)
|
|
||||||
# Set the cached entry
|
|
||||||
self.refresh_tokens = refresh_tokens
|
|
||||||
|
|
||||||
for bank in refresh_tokens:
|
|
||||||
rtrn = self.get_new_token(bank)
|
|
||||||
if not rtrn:
|
|
||||||
self.log.error(f"Error getting token for {bank}")
|
|
||||||
return
|
|
||||||
|
|
||||||
def get_new_token(self, bank):
|
|
||||||
"""
|
|
||||||
Exchange our refresh token for an access token.
|
|
||||||
:param account: account to refresh the token for
|
|
||||||
:type account:
|
|
||||||
"""
|
|
||||||
if bank not in self.refresh_tokens:
|
|
||||||
self.log.error(f"Bank {bank} not in refresh tokens")
|
|
||||||
return
|
|
||||||
|
|
||||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
||||||
data = {
|
|
||||||
"grant_type": "refresh_token",
|
|
||||||
"refresh_token": self.refresh_tokens[bank],
|
|
||||||
"client_id": settings.TrueLayer.ID,
|
|
||||||
"client_secret": settings.TrueLayer.Key,
|
|
||||||
}
|
|
||||||
r = requests.post(
|
|
||||||
f"{settings.TrueLayer.AuthBase}/connect/token",
|
|
||||||
data=data,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
parsed = r.json()
|
|
||||||
except JSONDecodeError:
|
|
||||||
self.log.error(f"Failed to decode JSON: {r.content}")
|
|
||||||
return False
|
|
||||||
if r.status_code == 200:
|
|
||||||
if "access_token" in parsed.keys():
|
|
||||||
self.tokens[bank] = parsed["access_token"]
|
|
||||||
# self.log.info(f"Refreshed access token for {bank}")
|
|
||||||
if (
|
|
||||||
len(self.refresh_tokens.keys()) == len(self.tokens.keys())
|
|
||||||
and not self.authed
|
|
||||||
):
|
|
||||||
# We are now fully authenticated and ready to start loops!
|
|
||||||
self.__authed__()
|
|
||||||
self.authed = True
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.log.error(f"Token refresh didn't contain access token: {parsed}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
self.log.error(f"Cannot refresh token: {parsed}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_accounts(self, bank):
|
|
||||||
"""
|
|
||||||
Get a list of accounts.
|
|
||||||
"""
|
|
||||||
token = self.get_key(bank)
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
|
||||||
path = f"{settings.TrueLayer.DataBase}/accounts"
|
|
||||||
r = requests.get(path, headers=headers)
|
|
||||||
try:
|
|
||||||
parsed = r.json()
|
|
||||||
except JSONDecodeError:
|
|
||||||
self.log.error(
|
|
||||||
"Error parsing accounts response: {content}", content=r.content
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
def _get_account(self, bank, account_id):
|
|
||||||
token = self.get_key(bank)
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
|
||||||
path = f"{settings.TrueLayer.DataBase}/accounts/{account_id}"
|
|
||||||
r = requests.get(path, headers=headers)
|
|
||||||
try:
|
|
||||||
parsed = r.json()
|
|
||||||
except JSONDecodeError:
|
|
||||||
self.log.error(f"Error parsing accounts response: {r.content}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
def get_mapped_accounts(self):
|
|
||||||
existing_entry = loads(settings.TrueLayer.Maps)
|
|
||||||
self.banks = existing_entry
|
|
||||||
|
|
||||||
def get_all_account_info(self):
|
|
||||||
to_return = {}
|
|
||||||
for bank in self.banks:
|
|
||||||
for account_id in self.banks[bank]:
|
|
||||||
account_data = self.get_account(bank, account_id)
|
|
||||||
if bank in to_return:
|
|
||||||
to_return[bank].append(account_data)
|
|
||||||
else:
|
|
||||||
to_return[bank] = [account_data]
|
|
||||||
return to_return
|
|
||||||
|
|
||||||
def get_account(self, bank, account_id):
|
|
||||||
account_data = self._get_account(bank, account_id)
|
|
||||||
if "results" not in account_data:
|
|
||||||
return False
|
|
||||||
if not len(account_data["results"]) == 1:
|
|
||||||
return False
|
|
||||||
if not len(account_data) == 2:
|
|
||||||
return False
|
|
||||||
if not account_data["status"] == "Succeeded":
|
|
||||||
return False
|
|
||||||
base = account_data["results"][0]
|
|
||||||
return base
|
|
||||||
|
|
||||||
def map_account(self, bank, account_id):
|
|
||||||
"""
|
|
||||||
Map an account_id at a bank to an account_name.
|
|
||||||
This enables the account for fetching.
|
|
||||||
Data type: {"monzo": [account, ids, here],
|
|
||||||
"revolut": [account, ids, here]}
|
|
||||||
"""
|
|
||||||
account_data = self.get_account(bank, account_id)
|
|
||||||
currency = account_data["currency"]
|
|
||||||
|
|
||||||
existing_entry = loads(settings.TrueLayer.Maps)
|
|
||||||
if bank in existing_entry:
|
|
||||||
if account_id not in existing_entry[bank]:
|
|
||||||
existing_entry[bank].append(account_id)
|
|
||||||
else:
|
|
||||||
existing_entry[bank] = [account_id]
|
|
||||||
settings.TrueLayer.Maps = dumps(existing_entry)
|
|
||||||
self.banks = existing_entry
|
|
||||||
settings.write()
|
|
||||||
|
|
||||||
return currency
|
|
||||||
|
|
||||||
def get_transactions(self, bank, account_id):
|
|
||||||
"""
|
|
||||||
Get a list of transactions from an account.
|
|
||||||
:param account_id: account to fetch transactions for
|
|
||||||
:return: list of transactions
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
token = self.get_key(bank)
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
|
||||||
path = f"{settings.TrueLayer.DataBase}/accounts/{account_id}/transactions"
|
|
||||||
r = requests.get(path, headers=headers)
|
|
||||||
try:
|
|
||||||
parsed = r.json()
|
|
||||||
except JSONDecodeError:
|
|
||||||
self.log.error(f"Error parsing transactions response: {r.content}")
|
|
||||||
return (False, False)
|
|
||||||
if "results" in parsed:
|
|
||||||
return parsed["results"]
|
|
||||||
else:
|
|
||||||
return (False, False)
|
|
||||||
|
|
||||||
def get_balance(self, bank, account_id):
|
|
||||||
"""
|
|
||||||
Get the balance of an account.
|
|
||||||
:param bank: the bank to check
|
|
||||||
:param account_id: the account ID
|
|
||||||
:return: tuple of (currency, amount)
|
|
||||||
:rtype: tuple
|
|
||||||
"""
|
|
||||||
token = self.get_key(bank)
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
|
||||||
path = f"{settings.TrueLayer.DataBase}/accounts/{account_id}/balance"
|
|
||||||
r = requests.get(path, headers=headers)
|
|
||||||
try:
|
|
||||||
obj = AccountBalancesRoot.from_json(r.content)
|
|
||||||
except ValidationError as err:
|
|
||||||
self.log.error(f"Validation error: {err}")
|
|
||||||
return
|
|
||||||
parsed = obj.to_dict()["results"]
|
|
||||||
total = 0
|
|
||||||
currency = None
|
|
||||||
for entry in parsed:
|
|
||||||
if currency:
|
|
||||||
if not currency == entry["currency"]:
|
|
||||||
self.log.error("Different currencies in balance query.")
|
|
||||||
return
|
|
||||||
total += entry["available"]
|
|
||||||
currency = entry["currency"]
|
|
||||||
return (currency, total)
|
|
||||||
|
|
||||||
def get_total_map(self):
|
|
||||||
"""
|
|
||||||
Return a dictionary keyed by currencies with the amounts as values.
|
|
||||||
:return: dict keyed by currency, values are amounts
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
totals = {}
|
|
||||||
for bank in self.banks:
|
|
||||||
for account_id in self.banks[bank]:
|
|
||||||
currency, amount = self.get_balance(bank, account_id)
|
|
||||||
if not amount:
|
|
||||||
continue
|
|
||||||
if currency in totals:
|
|
||||||
totals[currency] += amount
|
|
||||||
else:
|
|
||||||
totals[currency] = amount
|
|
||||||
return totals
|
|
|
@ -1,47 +0,0 @@
|
||||||
# Project imports
|
|
||||||
# from settings import settings
|
|
||||||
import sources.agora
|
|
||||||
import sources.localbitcoins
|
|
||||||
import util
|
|
||||||
|
|
||||||
|
|
||||||
class Sources(util.Base):
|
|
||||||
"""
|
|
||||||
Class to manage calls to various sources.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.agora = sources.agora.Agora()
|
|
||||||
self.lbtc = sources.localbitcoins.LBTC()
|
|
||||||
|
|
||||||
def __irc_started__(self):
|
|
||||||
self.log.debug("IRC hook called.")
|
|
||||||
self.agora.setup_loop()
|
|
||||||
self.lbtc.setup_loop()
|
|
||||||
self.log.debug("Finished setting up loops.")
|
|
||||||
|
|
||||||
def __xmerged__(self):
|
|
||||||
"""
|
|
||||||
Called when xmerge has been completed in the webapp.
|
|
||||||
Merge all instances into child classes.
|
|
||||||
"""
|
|
||||||
init_map = {
|
|
||||||
"ux": self.ux,
|
|
||||||
"agora": self.agora,
|
|
||||||
"lbtc": self.lbtc,
|
|
||||||
"markets": self.markets,
|
|
||||||
"sinks": self.sinks,
|
|
||||||
"sources": self,
|
|
||||||
"tx": self.tx,
|
|
||||||
"webapp": self.webapp,
|
|
||||||
"money": self.money,
|
|
||||||
"irc": self.irc,
|
|
||||||
"notify": self.notify,
|
|
||||||
}
|
|
||||||
util.xmerge_attrs(init_map)
|
|
||||||
|
|
||||||
def get_total_wallets(self):
|
|
||||||
"""
|
|
||||||
Get the total crypto in our wallets.
|
|
||||||
"""
|
|
|
@ -1,136 +0,0 @@
|
||||||
# Twisted/Klein imports
|
|
||||||
import sources.local
|
|
||||||
|
|
||||||
# Other library imports
|
|
||||||
from pyotp import TOTP
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
from twisted.internet.defer import inlineCallbacks
|
|
||||||
|
|
||||||
|
|
||||||
class Agora(sources.local.Local):
|
|
||||||
"""
|
|
||||||
AgoraDesk API handler.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
Initialise the AgoraDesk API.
|
|
||||||
Initialise the last_dash storage for detecting new trades.
|
|
||||||
"""
|
|
||||||
self.platform = "agora"
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# Cache for detecting new trades
|
|
||||||
self.last_dash = set()
|
|
||||||
|
|
||||||
# Cache for detecting new messages
|
|
||||||
self.last_messages = {}
|
|
||||||
|
|
||||||
# Assets that cheat has been run on
|
|
||||||
self.cheat_run_on = []
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def release_funds(self, contact_id):
|
|
||||||
"""
|
|
||||||
Release funds for a contact_id.
|
|
||||||
:param contact_id: trade/contact ID
|
|
||||||
:type contact_id: string
|
|
||||||
:return: response dict
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
print("CALLING RELEASE FUNDS", contact_id)
|
|
||||||
if self.sets.Dummy == "1":
|
|
||||||
self.log.error(
|
|
||||||
f"Running in dummy mode, not releasing funds for {contact_id}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
payload = {"tradeId": contact_id, "password": self.sets.Pass}
|
|
||||||
rtrn = yield self.api._api_call(
|
|
||||||
api_method=f"contact_release/{contact_id}",
|
|
||||||
http_method="POST",
|
|
||||||
query_values=payload,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if we can withdraw funds
|
|
||||||
yield self.withdraw_funds()
|
|
||||||
|
|
||||||
return rtrn
|
|
||||||
|
|
||||||
# TODO: write test before re-enabling adding total_trades
|
|
||||||
@inlineCallbacks
|
|
||||||
def withdraw_funds(self):
|
|
||||||
"""
|
|
||||||
Withdraw excess funds to our XMR wallets.
|
|
||||||
"""
|
|
||||||
print("CALLING WITHDRAW FUNDS")
|
|
||||||
totals_all = yield self.money.get_total()
|
|
||||||
if totals_all is False:
|
|
||||||
return False
|
|
||||||
|
|
||||||
wallet_xmr, _ = totals_all[2]
|
|
||||||
|
|
||||||
# Get the wallet balances in USD
|
|
||||||
total_usd = totals_all[0][1]
|
|
||||||
|
|
||||||
# total_trades_usd = self.tx.get_open_trades_usd()
|
|
||||||
if not total_usd:
|
|
||||||
return False
|
|
||||||
# total_usd += total_trades_usd
|
|
||||||
|
|
||||||
profit_usd = total_usd - float(settings.Money.BaseUSD)
|
|
||||||
# Get the XMR -> USD exchange rate
|
|
||||||
xmr_usd = self.money.cg.get_price(ids="monero", vs_currencies=["USD"])
|
|
||||||
|
|
||||||
# Convert the USD total to XMR
|
|
||||||
profit_usd_in_xmr = float(profit_usd) / xmr_usd["monero"]["usd"]
|
|
||||||
|
|
||||||
# Check profit is above zero
|
|
||||||
if not profit_usd >= 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not float(wallet_xmr) > profit_usd_in_xmr:
|
|
||||||
# Not enough funds to withdraw
|
|
||||||
self.log.error(
|
|
||||||
f"Not enough funds to withdraw {profit_usd_in_xmr}, as wallet only contains {wallet_xmr}"
|
|
||||||
)
|
|
||||||
self.irc.sendmsg(
|
|
||||||
f"Not enough funds to withdraw {profit_usd_in_xmr}, as wallet only contains {wallet_xmr}"
|
|
||||||
)
|
|
||||||
self.ux.notify.notify_need_topup(profit_usd_in_xmr)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not profit_usd >= float(settings.Money.WithdrawLimit):
|
|
||||||
# Not enough profit to withdraw
|
|
||||||
return
|
|
||||||
|
|
||||||
half = profit_usd_in_xmr / 2
|
|
||||||
|
|
||||||
half_rounded = round(half, 8)
|
|
||||||
|
|
||||||
# Read OTP secret
|
|
||||||
with open("otp.key", "r") as f:
|
|
||||||
otp_key = f.read()
|
|
||||||
f.close()
|
|
||||||
otp_key = otp_key.replace("\n", "")
|
|
||||||
|
|
||||||
# Get OTP code
|
|
||||||
otp_code = TOTP(otp_key)
|
|
||||||
|
|
||||||
# Set up the format for calling wallet_send_xmr
|
|
||||||
send_cast = {
|
|
||||||
"address": None,
|
|
||||||
"amount": half_rounded,
|
|
||||||
"password": settings.Agora.Pass,
|
|
||||||
"otp": otp_code.now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
send_cast["address"] = settings.XMR.Wallet1
|
|
||||||
rtrn1 = yield self.api.wallet_send_xmr(**send_cast)
|
|
||||||
|
|
||||||
send_cast["address"] = settings.XMR.Wallet2
|
|
||||||
rtrn2 = yield self.api.wallet_send_xmr(**send_cast)
|
|
||||||
|
|
||||||
self.irc.sendmsg(f"Withdrawal: {rtrn1['success']} | {rtrn2['success']}")
|
|
||||||
self.ux.notify.notify_withdrawal(half_rounded)
|
|
|
@ -1,719 +0,0 @@
|
||||||
# Twisted/Klein imports
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Other library imports
|
|
||||||
from json import loads
|
|
||||||
from time import sleep # TODO: async
|
|
||||||
|
|
||||||
import db
|
|
||||||
import util
|
|
||||||
from lib.agoradesk_py import AgoraDesk
|
|
||||||
from lib.localbitcoins_py import LocalBitcoins
|
|
||||||
from lib.logstash import send_logstash
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
from twisted.internet.defer import inlineCallbacks
|
|
||||||
from twisted.internet.task import LoopingCall
|
|
||||||
|
|
||||||
|
|
||||||
class Local(util.Base):
|
|
||||||
"""
|
|
||||||
Initialise the Local API library for LBTC and Agora.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
if self.platform == "agora":
|
|
||||||
self.api = AgoraDesk(settings.Agora.Token)
|
|
||||||
self.sets = settings.Agora
|
|
||||||
elif self.platform == "lbtc":
|
|
||||||
self.api = LocalBitcoins(
|
|
||||||
settings.LocalBitcoins.Token, settings.LocalBitcoins.Secret
|
|
||||||
)
|
|
||||||
self.sets = settings.LocalBitcoins
|
|
||||||
else:
|
|
||||||
self.log.error("Platform not defined.")
|
|
||||||
|
|
||||||
def setup_loop(self):
|
|
||||||
"""
|
|
||||||
Set up the LoopingCall to get all active trades and messages.
|
|
||||||
"""
|
|
||||||
self.log.debug("Setting up loops.")
|
|
||||||
self.lc_dash = LoopingCall(self.loop_check)
|
|
||||||
self.lc_dash.start(int(self.sets.RefreshSec))
|
|
||||||
if settings.Agora.Cheat == "1":
|
|
||||||
self.lc_cheat = LoopingCall(self.run_cheat_in_thread)
|
|
||||||
self.lc_cheat.start(int(self.sets.CheatSec))
|
|
||||||
self.log.debug("Finished setting up loops.")
|
|
||||||
|
|
||||||
def map_provider(self, provider, reverse=False):
|
|
||||||
provider_map = {"NATIONAL_BANK": "national-bank-transfer"}
|
|
||||||
if reverse:
|
|
||||||
try:
|
|
||||||
return next(
|
|
||||||
key for key, value in provider_map.items() if value == provider
|
|
||||||
)
|
|
||||||
except StopIteration:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
return provider_map[provider]
|
|
||||||
except KeyError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def got_dashboard(self, dash):
|
|
||||||
dash_tmp = yield self.wrap_dashboard(dash)
|
|
||||||
self.dashboard_hook(dash_tmp)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def wrap_dashboard(self, dash=None): # backwards compatibility with TX
|
|
||||||
if not dash:
|
|
||||||
dash = yield self.api.dashboard()
|
|
||||||
# if dash["response"] is None:
|
|
||||||
# return False
|
|
||||||
dash_tmp = {}
|
|
||||||
if not dash:
|
|
||||||
return False
|
|
||||||
if not dash["response"]:
|
|
||||||
return False
|
|
||||||
if "data" not in dash["response"]:
|
|
||||||
# self.log.error(f"Data not in dashboard response: {dash}")
|
|
||||||
return dash_tmp
|
|
||||||
if dash["response"]["data"]["contact_count"] > 0:
|
|
||||||
for contact in dash["response"]["data"]["contact_list"]:
|
|
||||||
contact_id = contact["data"]["contact_id"]
|
|
||||||
dash_tmp[contact_id] = contact
|
|
||||||
return dash_tmp
|
|
||||||
|
|
||||||
def loop_check(self):
|
|
||||||
"""
|
|
||||||
Calls hooks to parse dashboard info and get all contact messages.
|
|
||||||
"""
|
|
||||||
d = self.api.dashboard()
|
|
||||||
d.addCallback(self.got_dashboard)
|
|
||||||
|
|
||||||
# Get recent messages
|
|
||||||
m = self.api.recent_messages()
|
|
||||||
m.addCallback(self.got_recent_messages)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_dashboard_irc(self):
|
|
||||||
"""
|
|
||||||
Get dashboard helper for IRC only.
|
|
||||||
"""
|
|
||||||
dash = yield self.wrap_dashboard()
|
|
||||||
rtrn = []
|
|
||||||
if dash is False:
|
|
||||||
return False
|
|
||||||
for contact_id, contact in dash.items():
|
|
||||||
reference = db.tx_to_ref(contact_id)
|
|
||||||
buyer = contact["data"]["buyer"]["username"]
|
|
||||||
amount = contact["data"]["amount"]
|
|
||||||
if self.platform == "agora":
|
|
||||||
asset = contact["data"]["advertisement"]["asset"]
|
|
||||||
elif self.platform == "lbtc":
|
|
||||||
asset = "BTC"
|
|
||||||
if asset == "XMR":
|
|
||||||
amount_crypto = contact["data"]["amount_xmr"]
|
|
||||||
elif asset == "BTC":
|
|
||||||
amount_crypto = contact["data"]["amount_btc"]
|
|
||||||
currency = contact["data"]["currency"]
|
|
||||||
provider = contact["data"]["advertisement"]["payment_method"]
|
|
||||||
if not contact["data"]["is_selling"]:
|
|
||||||
continue
|
|
||||||
rtrn.append(
|
|
||||||
(
|
|
||||||
f"[#] [{reference}] ({self.platform}) <{buyer}>"
|
|
||||||
f" {amount}{currency} {provider} {amount_crypto}{asset}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return rtrn
|
|
||||||
|
|
||||||
def dashboard_hook(self, dash):
|
|
||||||
"""
|
|
||||||
Get information about our open trades.
|
|
||||||
Post new trades to IRC and cache trades for the future.
|
|
||||||
"""
|
|
||||||
current_trades = []
|
|
||||||
if not dash:
|
|
||||||
return
|
|
||||||
if not dash.items():
|
|
||||||
return
|
|
||||||
for contact_id, contact in dash.items():
|
|
||||||
reference = db.tx_to_ref(str(contact_id))
|
|
||||||
if reference:
|
|
||||||
current_trades.append(reference)
|
|
||||||
buyer = contact["data"]["buyer"]["username"]
|
|
||||||
amount = contact["data"]["amount"]
|
|
||||||
if self.platform == "agora":
|
|
||||||
asset = contact["data"]["advertisement"]["asset"]
|
|
||||||
elif self.platform == "lbtc":
|
|
||||||
asset = "BTC"
|
|
||||||
provider = contact["data"]["advertisement"]["payment_method"]
|
|
||||||
if asset == "XMR":
|
|
||||||
amount_crypto = contact["data"]["amount_xmr"]
|
|
||||||
elif asset == "BTC":
|
|
||||||
amount_crypto = contact["data"]["amount_btc"]
|
|
||||||
currency = contact["data"]["currency"]
|
|
||||||
if not contact["data"]["is_selling"]:
|
|
||||||
continue
|
|
||||||
if reference not in self.last_dash:
|
|
||||||
reference = self.tx.new_trade(
|
|
||||||
self.platform,
|
|
||||||
asset,
|
|
||||||
contact_id,
|
|
||||||
buyer,
|
|
||||||
currency,
|
|
||||||
amount,
|
|
||||||
amount_crypto,
|
|
||||||
provider,
|
|
||||||
)
|
|
||||||
if reference:
|
|
||||||
if reference not in current_trades:
|
|
||||||
current_trades.append(reference)
|
|
||||||
# Let us know there is a new trade
|
|
||||||
self.irc.sendmsg(
|
|
||||||
(
|
|
||||||
f"[#] [{reference}] ({self.platform}) <{buyer}>"
|
|
||||||
f" {amount}{currency} {provider} {amount_crypto}{asset}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# Note that we have seen this reference
|
|
||||||
self.last_dash.add(reference)
|
|
||||||
|
|
||||||
# Purge old trades from cache
|
|
||||||
for ref in list(self.last_dash): # We're removing from the list on the fly
|
|
||||||
if ref not in current_trades:
|
|
||||||
self.last_dash.remove(ref)
|
|
||||||
if reference and reference not in current_trades:
|
|
||||||
current_trades.append(reference)
|
|
||||||
messages = db.cleanup(self.platform, current_trades)
|
|
||||||
for message in messages:
|
|
||||||
self.ux.irc.sendmsg(message)
|
|
||||||
|
|
||||||
def got_recent_messages(self, messages, send_irc=True):
|
|
||||||
"""
|
|
||||||
Get recent messages.
|
|
||||||
"""
|
|
||||||
messages_tmp = {}
|
|
||||||
if not messages:
|
|
||||||
return False
|
|
||||||
if not messages["success"]:
|
|
||||||
return False
|
|
||||||
if not messages["response"]:
|
|
||||||
return False
|
|
||||||
if "data" not in messages["response"]:
|
|
||||||
self.log.error(f"Data not in messages response: {messages['response']}")
|
|
||||||
return False
|
|
||||||
open_tx = db.get_ref_map().keys()
|
|
||||||
for message in messages["response"]["data"]["message_list"]:
|
|
||||||
contact_id = str(message["contact_id"])
|
|
||||||
username = message["sender"]["username"]
|
|
||||||
msg = message["msg"]
|
|
||||||
if contact_id not in open_tx:
|
|
||||||
continue
|
|
||||||
reference = db.tx_to_ref(contact_id)
|
|
||||||
if reference in messages_tmp:
|
|
||||||
messages_tmp[reference].append([username, msg])
|
|
||||||
else:
|
|
||||||
messages_tmp[reference] = [[username, msg]]
|
|
||||||
|
|
||||||
# Send new messages on IRC
|
|
||||||
if send_irc:
|
|
||||||
for user, message in messages_tmp[reference][::-1]:
|
|
||||||
if reference in self.last_messages:
|
|
||||||
if not [user, message] in self.last_messages[reference]:
|
|
||||||
self.irc.sendmsg(
|
|
||||||
f"[{reference}] ({self.platform}) <{user}> {message}"
|
|
||||||
)
|
|
||||||
# Append sent messages to last_messages so we don't send them again
|
|
||||||
self.last_messages[reference].append([user, message])
|
|
||||||
else:
|
|
||||||
self.last_messages[reference] = [[user, message]]
|
|
||||||
for x in messages_tmp[reference]:
|
|
||||||
self.irc.sendmsg(
|
|
||||||
f"[{reference}] ({self.platform}) <{user}> {message}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Purge old trades from cache
|
|
||||||
for ref in list(self.last_messages): # We're removing from the list on the fly
|
|
||||||
if ref not in messages_tmp:
|
|
||||||
del self.last_messages[ref]
|
|
||||||
|
|
||||||
return messages_tmp
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def enum_ad_ids(self, page=0):
|
|
||||||
if self.platform == "lbtc" and page == 0:
|
|
||||||
page = 1
|
|
||||||
ads = yield self.api.ads(page=page)
|
|
||||||
# ads = yield self.api._api_call(api_method="ads", query_values={"page": page})
|
|
||||||
if ads is False:
|
|
||||||
return False
|
|
||||||
ads_total = []
|
|
||||||
if not ads["success"]:
|
|
||||||
return False
|
|
||||||
for ad in ads["response"]["data"]["ad_list"]:
|
|
||||||
ads_total.append(ad["data"]["ad_id"])
|
|
||||||
if "pagination" in ads["response"]:
|
|
||||||
if "next" in ads["response"]["pagination"]:
|
|
||||||
page += 1
|
|
||||||
ads_iter = yield self.enum_ad_ids(page)
|
|
||||||
if ads_iter is None:
|
|
||||||
return False
|
|
||||||
if ads_iter is False:
|
|
||||||
return False
|
|
||||||
for ad in ads_iter:
|
|
||||||
ads_total.append(ad)
|
|
||||||
return ads_total
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def enum_ads(self, requested_asset=None, page=0):
|
|
||||||
if self.platform == "lbtc" and page == 0:
|
|
||||||
page = 1
|
|
||||||
query_values = {"page": page}
|
|
||||||
if requested_asset:
|
|
||||||
query_values["asset"] = requested_asset
|
|
||||||
# ads = yield self.api._api_call(api_method="ads", query_values=query_values)
|
|
||||||
ads = yield self.api.ads(page=page)
|
|
||||||
if ads is False:
|
|
||||||
return False
|
|
||||||
ads_total = []
|
|
||||||
if not ads["success"]:
|
|
||||||
return False
|
|
||||||
if not ads["response"]:
|
|
||||||
return False
|
|
||||||
for ad in ads["response"]["data"]["ad_list"]:
|
|
||||||
if self.platform == "agora":
|
|
||||||
asset = ad["data"]["asset"]
|
|
||||||
elif self.platform == "lbtc":
|
|
||||||
asset = "BTC"
|
|
||||||
ad_id = ad["data"]["ad_id"]
|
|
||||||
country = ad["data"]["countrycode"]
|
|
||||||
currency = ad["data"]["currency"]
|
|
||||||
provider = ad["data"]["online_provider"]
|
|
||||||
ads_total.append([asset, ad_id, country, currency, provider])
|
|
||||||
if "pagination" in ads["response"]:
|
|
||||||
if "next" in ads["response"]["pagination"]:
|
|
||||||
page += 1
|
|
||||||
ads_iter = yield self.enum_ads(requested_asset, page)
|
|
||||||
if ads_iter is None:
|
|
||||||
return False
|
|
||||||
if ads_iter is False:
|
|
||||||
return False
|
|
||||||
for ad in ads_iter:
|
|
||||||
ads_total.append([ad[0], ad[1], ad[2], ad[3], ad[4]])
|
|
||||||
return ads_total
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def enum_public_ads(self, asset, currency, providers=None, page=0):
|
|
||||||
if self.platform == "lbtc" and page == 0:
|
|
||||||
page = 1
|
|
||||||
to_return = []
|
|
||||||
# if asset == "XMR":
|
|
||||||
# coin = "monero"
|
|
||||||
# elif asset == "BTC":
|
|
||||||
# coin = "bitcoins"
|
|
||||||
if not providers:
|
|
||||||
providers = ["NATIONAL_BANK"]
|
|
||||||
# buy-monero-online, buy-bitcoin-online
|
|
||||||
# Work around Agora weirdness calling it bitcoins
|
|
||||||
# ads = yield self.api._api_call(
|
|
||||||
# api_method=f"buy-{coin}-online/{currency}",
|
|
||||||
# query_values={"page": page},
|
|
||||||
# )
|
|
||||||
if asset == "XMR":
|
|
||||||
ads = yield self.api.buy_monero_online(currency_code=currency, page=page)
|
|
||||||
elif asset == "BTC":
|
|
||||||
ads = yield self.api.buy_bitcoins_online(currency_code=currency, page=page)
|
|
||||||
# with open("pub.json", "a") as f:
|
|
||||||
# import json
|
|
||||||
# f.write(json.dumps([page, currency, asset, ads])+"\n")
|
|
||||||
# f.close()
|
|
||||||
if ads is None:
|
|
||||||
return False
|
|
||||||
if ads is False:
|
|
||||||
return False
|
|
||||||
if ads["response"] is None:
|
|
||||||
return False
|
|
||||||
if "data" not in ads["response"]:
|
|
||||||
return False
|
|
||||||
for ad in ads["response"]["data"]["ad_list"]:
|
|
||||||
provider = ad["data"]["online_provider"]
|
|
||||||
if self.platform == "lbtc":
|
|
||||||
provider_test = self.map_provider(provider)
|
|
||||||
else:
|
|
||||||
provider_test = provider
|
|
||||||
if provider_test not in providers:
|
|
||||||
continue
|
|
||||||
date_last_seen = ad["data"]["profile"]["last_online"]
|
|
||||||
# Check if this person was seen recently
|
|
||||||
if not util.last_online_recent(date_last_seen):
|
|
||||||
continue
|
|
||||||
ad_id = str(ad["data"]["ad_id"])
|
|
||||||
username = ad["data"]["profile"]["username"]
|
|
||||||
temp_price = ad["data"]["temp_price"]
|
|
||||||
if ad["data"]["currency"] != currency:
|
|
||||||
continue
|
|
||||||
to_append = [ad_id, username, temp_price, provider, asset, currency]
|
|
||||||
if to_append not in to_return:
|
|
||||||
to_return.append(to_append)
|
|
||||||
# yield [ad_id, username, temp_price, provider, asset, currency]
|
|
||||||
if "pagination" in ads["response"]:
|
|
||||||
if "next" in ads["response"]["pagination"]:
|
|
||||||
page += 1
|
|
||||||
ads_iter = yield self.enum_public_ads(asset, currency, providers, page)
|
|
||||||
if ads_iter is None:
|
|
||||||
return False
|
|
||||||
if ads_iter is False:
|
|
||||||
return False
|
|
||||||
for ad in ads_iter:
|
|
||||||
to_append = [ad[0], ad[1], ad[2], ad[3], ad[4], ad[5]]
|
|
||||||
if to_append not in to_return:
|
|
||||||
to_return.append(to_append)
|
|
||||||
return to_return
|
|
||||||
|
|
||||||
def run_cheat_in_thread(self, assets=None):
|
|
||||||
"""
|
|
||||||
Update prices in another thread.
|
|
||||||
"""
|
|
||||||
if not assets:
|
|
||||||
all_assets = loads(self.sets.AssetList)
|
|
||||||
assets_not_run = set(all_assets) ^ set(self.cheat_run_on)
|
|
||||||
if not assets_not_run:
|
|
||||||
self.cheat_run_on = []
|
|
||||||
asset = list(all_assets).pop()
|
|
||||||
self.cheat_run_on.append(asset)
|
|
||||||
else:
|
|
||||||
asset = assets_not_run.pop()
|
|
||||||
self.cheat_run_on.append(asset)
|
|
||||||
self.update_prices([asset])
|
|
||||||
return asset
|
|
||||||
else:
|
|
||||||
# deferToThread(self.update_prices, assets)
|
|
||||||
self.update_prices(assets)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def update_prices(self, assets=None):
|
|
||||||
# Get all public ads for the given assets
|
|
||||||
public_ads = yield self.get_all_public_ads(assets)
|
|
||||||
if not public_ads:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Get the ads to update
|
|
||||||
to_update = self.markets.get_new_ad_equations(self.platform, public_ads, assets)
|
|
||||||
self.slow_ad_update(to_update)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def get_all_public_ads(self, assets=None, currencies=None, providers=None):
|
|
||||||
"""
|
|
||||||
Get all public ads for our listed currencies.
|
|
||||||
:return: dict of public ads keyed by currency
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
public_ads = {}
|
|
||||||
crypto_map = {
|
|
||||||
"XMR": "monero",
|
|
||||||
"BTC": "bitcoin",
|
|
||||||
}
|
|
||||||
|
|
||||||
if not assets:
|
|
||||||
assets = self.markets.get_all_assets(self.platform)
|
|
||||||
# Get all currencies we have ads for, deduplicated
|
|
||||||
if not currencies:
|
|
||||||
currencies = self.markets.get_all_currencies(self.platform)
|
|
||||||
if not providers:
|
|
||||||
providers = self.markets.get_all_providers(self.platform)
|
|
||||||
sinks_currencies = self.sinks.currencies
|
|
||||||
supported_currencies = [
|
|
||||||
currency for currency in currencies if currency in sinks_currencies
|
|
||||||
]
|
|
||||||
currencies = supported_currencies
|
|
||||||
# We want to get the ads for each of these currencies and return the result
|
|
||||||
rates = self.money.cg.get_price(
|
|
||||||
ids=["monero", "bitcoin"], vs_currencies=currencies
|
|
||||||
)
|
|
||||||
for asset in assets:
|
|
||||||
for currency in currencies:
|
|
||||||
cg_asset_name = crypto_map[asset]
|
|
||||||
try:
|
|
||||||
rates[cg_asset_name][currency.lower()]
|
|
||||||
except KeyError:
|
|
||||||
self.log.debug(f"Error getting public ads for currency: {currency}")
|
|
||||||
continue
|
|
||||||
ads_list = yield self.enum_public_ads(asset, currency, providers)
|
|
||||||
if not ads_list:
|
|
||||||
self.log.debug("Error getting ads list.")
|
|
||||||
continue
|
|
||||||
ads = self.money.lookup_rates(self.platform, ads_list, rates=rates)
|
|
||||||
if not ads:
|
|
||||||
self.log.debug("Error lookup up rates.")
|
|
||||||
continue
|
|
||||||
self.log.debug("Writing to ES.")
|
|
||||||
self.write_to_es_ads("ads", ads)
|
|
||||||
if currency in public_ads:
|
|
||||||
for ad in list(ads):
|
|
||||||
if ad not in public_ads[currency]:
|
|
||||||
public_ads[currency].append(ad)
|
|
||||||
else:
|
|
||||||
public_ads[currency] = ads
|
|
||||||
|
|
||||||
return public_ads
|
|
||||||
|
|
||||||
def write_to_es_ads(self, msgtype, ads):
|
|
||||||
for ad in ads:
|
|
||||||
cast = {
|
|
||||||
"id": ad[0],
|
|
||||||
"username": ad[1],
|
|
||||||
"price": ad[2],
|
|
||||||
"provider": ad[3],
|
|
||||||
"asset": ad[4],
|
|
||||||
"currency": ad[5],
|
|
||||||
"margin": ad[6],
|
|
||||||
"ts": str(datetime.now().isoformat()),
|
|
||||||
"xtype": msgtype,
|
|
||||||
"market": self.platform,
|
|
||||||
"type": "platform",
|
|
||||||
}
|
|
||||||
if settings.ES.Enabled == "1":
|
|
||||||
self.es.index(index=settings.ES.MetaIndex, body=cast)
|
|
||||||
elif settings.Logstash.Enabled == "1":
|
|
||||||
send_logstash(cast)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def slow_ad_update(self, ads):
|
|
||||||
"""
|
|
||||||
Slow ad equation update utilising exponential backoff in order to guarantee all ads are updated.
|
|
||||||
:param ads: our list of ads
|
|
||||||
"""
|
|
||||||
iterations = 0
|
|
||||||
throttled = 0
|
|
||||||
assets = set()
|
|
||||||
currencies = set()
|
|
||||||
while not all([x[4] for x in ads]) or iterations == 1000:
|
|
||||||
for ad_index in range(len(ads)):
|
|
||||||
ad_id, new_formula, asset, currency, actioned = ads[ad_index]
|
|
||||||
assets.add(asset)
|
|
||||||
currencies.add(currency)
|
|
||||||
if not actioned:
|
|
||||||
rtrn = yield self.api.ad_equation(ad_id, new_formula)
|
|
||||||
if rtrn["success"]:
|
|
||||||
ads[ad_index][4] = True
|
|
||||||
throttled = 0
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if "error_code" not in rtrn["response"]["error"]:
|
|
||||||
self.log.error(
|
|
||||||
f"Error code not in return for ad {ad_id}: {rtrn['response']}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if rtrn["response"]["error"]["error_code"] == 429:
|
|
||||||
throttled += 1
|
|
||||||
sleep_time = pow(throttled, float(self.sets.SleepExponent))
|
|
||||||
self.log.info(
|
|
||||||
f"Throttled {throttled} times while updating {ad_id}, sleeping for {sleep_time} seconds"
|
|
||||||
)
|
|
||||||
# We're running in a thread, so this is fine
|
|
||||||
sleep(sleep_time)
|
|
||||||
self.log.error(f"Error updating ad {ad_id}: {rtrn['response']}")
|
|
||||||
continue
|
|
||||||
iterations += 1
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def nuke_ads(self):
|
|
||||||
"""
|
|
||||||
Delete all of our adverts.
|
|
||||||
:return: True or False
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
ads = yield self.enum_ad_ids()
|
|
||||||
return_ids = []
|
|
||||||
if ads is False:
|
|
||||||
return False
|
|
||||||
for ad_id in ads:
|
|
||||||
rtrn = yield self.api.ad_delete(ad_id)
|
|
||||||
return_ids.append(rtrn["success"])
|
|
||||||
return all(return_ids)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def create_ad(
|
|
||||||
self,
|
|
||||||
asset,
|
|
||||||
countrycode,
|
|
||||||
currency,
|
|
||||||
provider,
|
|
||||||
payment_details,
|
|
||||||
visible=None,
|
|
||||||
edit=False,
|
|
||||||
ad_id=None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Post an ad with the given asset in a country with a given currency.
|
|
||||||
Convert the min and max amounts from settings to the given currency with CurrencyRates.
|
|
||||||
:param asset: the crypto asset to list (XMR or BTC)
|
|
||||||
:type asset: string
|
|
||||||
:param countrycode: country code
|
|
||||||
:param currency: currency code
|
|
||||||
:param payment_details: the payment details
|
|
||||||
:type countrycode: string
|
|
||||||
:type currency: string
|
|
||||||
:type payment_details: dict
|
|
||||||
:return: data about created object or error
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
|
|
||||||
if payment_details:
|
|
||||||
payment_details_text = self.markets.format_payment_details(
|
|
||||||
currency, payment_details
|
|
||||||
)
|
|
||||||
ad_text = self.markets.format_ad(asset, currency, payment_details_text)
|
|
||||||
min_amount, max_amount = self.money.get_minmax(
|
|
||||||
self.platform, asset, currency
|
|
||||||
)
|
|
||||||
if self.platform == "lbtc":
|
|
||||||
bank_name = payment_details["bank"]
|
|
||||||
|
|
||||||
if self.platform == "agora":
|
|
||||||
price_formula = (
|
|
||||||
f"coingecko{asset.lower()}usd*usd{currency.lower()}*{self.sets.Margin}"
|
|
||||||
)
|
|
||||||
elif self.platform == "lbtc":
|
|
||||||
price_formula = f"btc_in_usd*{self.sets.Margin}*USD_in_{currency}"
|
|
||||||
|
|
||||||
form = {
|
|
||||||
"country_code": countrycode,
|
|
||||||
"currency": currency,
|
|
||||||
"trade_type": "ONLINE_SELL",
|
|
||||||
# "asset": asset,
|
|
||||||
"price_equation": price_formula,
|
|
||||||
"track_max_amount": False,
|
|
||||||
"require_trusted_by_advertiser": False,
|
|
||||||
"online_provider": provider,
|
|
||||||
"require_feedback_score": int(self.sets.FeedbackScore),
|
|
||||||
}
|
|
||||||
if self.platform == "agora":
|
|
||||||
form["asset"] = asset
|
|
||||||
form["payment_method_details"] = settings.Platform.PaymentMethodDetails
|
|
||||||
form["online_provider"] = provider
|
|
||||||
elif self.platform == "lbtc":
|
|
||||||
form["online_provider"] = self.map_provider(provider, reverse=True)
|
|
||||||
|
|
||||||
if visible is False:
|
|
||||||
form["visible"] = False
|
|
||||||
elif visible is True:
|
|
||||||
form["visible"] = False
|
|
||||||
if payment_details:
|
|
||||||
form["account_info"] = payment_details_text
|
|
||||||
form["msg"] = ad_text
|
|
||||||
form["min_amount"] = round(min_amount, 2)
|
|
||||||
form["max_amount"] = round(max_amount, 2)
|
|
||||||
if self.platform == "lbtc":
|
|
||||||
form["bank_name"] = bank_name
|
|
||||||
if edit:
|
|
||||||
ad = yield self.api.ad(ad_id=ad_id, **form)
|
|
||||||
else:
|
|
||||||
ad = yield self.api.ad_create(**form)
|
|
||||||
return ad
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def dist_countries(self, filter_asset=None):
|
|
||||||
"""
|
|
||||||
Distribute our advert into all countries and providers listed in the config.
|
|
||||||
Exits on errors.
|
|
||||||
:return: False or dict with response
|
|
||||||
:rtype: bool or dict
|
|
||||||
"""
|
|
||||||
dist_list = list(
|
|
||||||
self.markets.create_distribution_list(self.platform, filter_asset)
|
|
||||||
)
|
|
||||||
our_ads = yield self.enum_ads()
|
|
||||||
(
|
|
||||||
supported_currencies,
|
|
||||||
account_info,
|
|
||||||
) = self.markets.get_valid_account_details(self.platform)
|
|
||||||
# Let's get rid of the ad IDs and make it a tuple like dist_list
|
|
||||||
our_ads = [(x[0], x[2], x[3], x[4]) for x in our_ads]
|
|
||||||
if not our_ads:
|
|
||||||
self.log.error("Could not get our ads.")
|
|
||||||
return False
|
|
||||||
to_return = []
|
|
||||||
for asset, countrycode, currency, provider in dist_list:
|
|
||||||
if (asset, countrycode, currency, provider) not in our_ads:
|
|
||||||
if currency in supported_currencies:
|
|
||||||
# Create the actual ad and pass in all the stuff
|
|
||||||
rtrn = yield self.create_ad(
|
|
||||||
asset,
|
|
||||||
countrycode,
|
|
||||||
currency,
|
|
||||||
provider,
|
|
||||||
payment_details=account_info[currency],
|
|
||||||
)
|
|
||||||
# Bail on first error, let's not continue
|
|
||||||
if rtrn is False:
|
|
||||||
return False
|
|
||||||
to_return.append(rtrn)
|
|
||||||
return to_return
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def redist_countries(self):
|
|
||||||
"""
|
|
||||||
Redistribute our advert details into all our listed adverts.
|
|
||||||
This will edit all ads and update the details. Only works if we have already run dist.
|
|
||||||
This will not post any new ads.
|
|
||||||
Exits on errors.
|
|
||||||
:return: False or dict with response
|
|
||||||
:rtype: bool or dict
|
|
||||||
"""
|
|
||||||
our_ads = yield self.enum_ads()
|
|
||||||
(
|
|
||||||
supported_currencies,
|
|
||||||
account_info,
|
|
||||||
) = self.markets.get_valid_account_details(self.platform)
|
|
||||||
if not our_ads:
|
|
||||||
self.log.error("Could not get our ads.")
|
|
||||||
return False
|
|
||||||
to_return = []
|
|
||||||
for asset, ad_id, countrycode, currency, provider in our_ads:
|
|
||||||
if currency in supported_currencies:
|
|
||||||
rtrn = yield self.create_ad(
|
|
||||||
asset,
|
|
||||||
countrycode,
|
|
||||||
currency,
|
|
||||||
provider,
|
|
||||||
payment_details=account_info[currency],
|
|
||||||
edit=True,
|
|
||||||
ad_id=ad_id,
|
|
||||||
)
|
|
||||||
# Bail on first error, let's not continue
|
|
||||||
if rtrn is False:
|
|
||||||
return False
|
|
||||||
to_return.append((rtrn, ad_id))
|
|
||||||
return to_return
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def strip_duplicate_ads(self):
|
|
||||||
"""
|
|
||||||
Remove duplicate ads.
|
|
||||||
:return: list of duplicate ads
|
|
||||||
:rtype: list
|
|
||||||
"""
|
|
||||||
existing_ads = yield self.enum_ads()
|
|
||||||
_size = len(existing_ads)
|
|
||||||
repeated = []
|
|
||||||
for i in range(_size):
|
|
||||||
k = i + 1
|
|
||||||
for j in range(k, _size):
|
|
||||||
if (
|
|
||||||
existing_ads[i] == existing_ads[j]
|
|
||||||
and existing_ads[i] not in repeated
|
|
||||||
):
|
|
||||||
repeated.append(existing_ads[i])
|
|
||||||
|
|
||||||
actioned = []
|
|
||||||
for ad_id, country, currency in repeated:
|
|
||||||
rtrn = yield self.api.ad_delete(ad_id)
|
|
||||||
actioned.append(rtrn["success"])
|
|
||||||
|
|
||||||
return all(actioned)
|
|
|
@ -1,136 +0,0 @@
|
||||||
# Other library imports
|
|
||||||
import sources.local
|
|
||||||
import util
|
|
||||||
from pyotp import TOTP
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
|
|
||||||
class LBTC(sources.local.Local):
|
|
||||||
"""
|
|
||||||
LocalBitcoins API handler.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
Initialise the LocalBitcoins API.
|
|
||||||
Initialise the last_dash storage for detecting new trades.
|
|
||||||
"""
|
|
||||||
self.platform = "lbtc"
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# Cache for detecting new trades
|
|
||||||
self.last_dash = set()
|
|
||||||
|
|
||||||
# Cache for detecting new messages
|
|
||||||
self.last_messages = {}
|
|
||||||
|
|
||||||
# Assets that cheat has been run on
|
|
||||||
self.cheat_run_on = []
|
|
||||||
|
|
||||||
@util.handle_exceptions
|
|
||||||
def release_funds(self, contact_id):
|
|
||||||
"""
|
|
||||||
Release funds for a contact_id.
|
|
||||||
:param contact_id: trade/contact ID
|
|
||||||
:type contact_id: string
|
|
||||||
:return: response dict
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
if self.sets.Dummy == "1":
|
|
||||||
self.log.error(
|
|
||||||
f"Running in dummy mode, not releasing funds for {contact_id}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
payload = {
|
|
||||||
"tradeId": contact_id,
|
|
||||||
"password": self.sets.Pass,
|
|
||||||
}
|
|
||||||
rtrn = self.api._api_call(
|
|
||||||
api_method=f"contact_release/{contact_id}",
|
|
||||||
http_method="POST",
|
|
||||||
query_values=payload,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if we can withdraw funds
|
|
||||||
self.withdraw_funds()
|
|
||||||
|
|
||||||
return rtrn
|
|
||||||
|
|
||||||
# TODO: rewrite to handle BTC
|
|
||||||
@util.handle_exceptions
|
|
||||||
def withdraw_funds(self):
|
|
||||||
"""
|
|
||||||
Withdraw excess funds to our XMR wallets.
|
|
||||||
"""
|
|
||||||
totals_all = self.money.get_total()
|
|
||||||
if totals_all is False:
|
|
||||||
return False
|
|
||||||
|
|
||||||
wallet_xmr, _ = totals_all[2]
|
|
||||||
|
|
||||||
# Get the wallet balances in USD
|
|
||||||
total_usd = totals_all[0][1]
|
|
||||||
|
|
||||||
# total_trades_usd = self.tx.get_open_trades_usd()
|
|
||||||
if not total_usd:
|
|
||||||
return False
|
|
||||||
# total_usd += total_trades_usd
|
|
||||||
|
|
||||||
profit_usd = total_usd - float(settings.Money.BaseUSD)
|
|
||||||
# Get the XMR -> USD exchange rate
|
|
||||||
xmr_usd = self.money.cg.get_price(ids="bitcoin", vs_currencies=["USD"])
|
|
||||||
|
|
||||||
# Convert the USD total to XMR
|
|
||||||
profit_usd_in_xmr = float(profit_usd) / xmr_usd["bitcoin"]["usd"]
|
|
||||||
|
|
||||||
# Check profit is above zero
|
|
||||||
if not profit_usd >= 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not float(wallet_xmr) > profit_usd_in_xmr:
|
|
||||||
# Not enough funds to withdraw
|
|
||||||
self.log.error(
|
|
||||||
f"Not enough funds to withdraw {profit_usd_in_xmr}, as wallet only contains {wallet_xmr}"
|
|
||||||
)
|
|
||||||
self.irc.sendmsg(
|
|
||||||
f"Not enough funds to withdraw {profit_usd_in_xmr}, as wallet only contains {wallet_xmr}"
|
|
||||||
)
|
|
||||||
self.ux.notify.notify_need_topup(profit_usd_in_xmr)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
if not profit_usd >= float(settings.Money.WithdrawLimit):
|
|
||||||
# Not enough profit to withdraw
|
|
||||||
return
|
|
||||||
|
|
||||||
half = profit_usd_in_xmr / 2
|
|
||||||
|
|
||||||
half_rounded = round(half, 8)
|
|
||||||
|
|
||||||
# Read OTP secret
|
|
||||||
with open("otp.key", "r") as f:
|
|
||||||
otp_key = f.read()
|
|
||||||
f.close()
|
|
||||||
otp_key = otp_key.replace("\n", "")
|
|
||||||
|
|
||||||
# Get OTP code
|
|
||||||
otp_code = TOTP(otp_key)
|
|
||||||
|
|
||||||
# Set up the format for calling wallet_send_xmr
|
|
||||||
send_cast = {
|
|
||||||
"address": None,
|
|
||||||
"amount": half_rounded,
|
|
||||||
"password": settings.LocalBitcoins.Pass,
|
|
||||||
"otp": otp_code.now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
send_cast["address"] = settings.XMR.Wallet1
|
|
||||||
rtrn1 = self.api.wallet_send_xmr(**send_cast)
|
|
||||||
|
|
||||||
send_cast["address"] = settings.XMR.Wallet2
|
|
||||||
rtrn2 = self.api.wallet_send_xmr(**send_cast)
|
|
||||||
|
|
||||||
self.irc.sendmsg(f"Withdrawal: {rtrn1['success']} | {rtrn2['success']}")
|
|
||||||
self.ux.notify.notify_withdrawal(half_rounded)
|
|
|
@ -1,284 +0,0 @@
|
||||||
import logging
|
|
||||||
from copy import deepcopy
|
|
||||||
from json import loads
|
|
||||||
from unittest import TestCase
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
import lib.markets
|
|
||||||
import lib.money
|
|
||||||
import settings
|
|
||||||
import sources.agora
|
|
||||||
import util
|
|
||||||
from tests.common import cg_prices, expected_to_update, fake_public_ads
|
|
||||||
from twisted.internet.defer import inlineCallbacks
|
|
||||||
|
|
||||||
|
|
||||||
class TestAgora(TestCase):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.test_return_data = {}
|
|
||||||
with open("tests/data/agora_ads.json", "r") as f:
|
|
||||||
for line in f.readlines():
|
|
||||||
parsed = loads(line)
|
|
||||||
self.test_return_data[(parsed[2], parsed[1], str(parsed[0]))] = parsed[
|
|
||||||
3
|
|
||||||
]
|
|
||||||
|
|
||||||
super().__init__(*args, *kwargs)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
logging.disable(logging.CRITICAL)
|
|
||||||
self.markets = lib.markets.Markets()
|
|
||||||
self.agora = sources.agora.Agora()
|
|
||||||
self.money = lib.money.Money()
|
|
||||||
setattr(self.agora, "markets", self.markets)
|
|
||||||
setattr(self.money, "markets", self.markets)
|
|
||||||
setattr(self.agora, "money", self.money)
|
|
||||||
self.markets.sinks = MagicMock()
|
|
||||||
self.markets.sinks.currencies = [
|
|
||||||
"AUD",
|
|
||||||
"CAD",
|
|
||||||
"CHF",
|
|
||||||
"CZK",
|
|
||||||
"DKK",
|
|
||||||
"EUR",
|
|
||||||
"GBP",
|
|
||||||
"HKD",
|
|
||||||
"HUF",
|
|
||||||
"JPY",
|
|
||||||
"MXN",
|
|
||||||
"NOK",
|
|
||||||
"NZD",
|
|
||||||
"PLN",
|
|
||||||
"RUB",
|
|
||||||
"SEK",
|
|
||||||
"SGD",
|
|
||||||
"THB",
|
|
||||||
"TRY",
|
|
||||||
"USD",
|
|
||||||
"ZAR",
|
|
||||||
]
|
|
||||||
self.agora.sinks = MagicMock()
|
|
||||||
self.agora.es = MagicMock()
|
|
||||||
self.agora.es.index = MagicMock()
|
|
||||||
self.agora.sinks.currencies = self.markets.sinks.currencies
|
|
||||||
|
|
||||||
self.all_providers = [
|
|
||||||
"XOOM",
|
|
||||||
"CRYPTOCURRENCY",
|
|
||||||
"VIPPS",
|
|
||||||
"PAYSAFECARD",
|
|
||||||
"PAYPAL",
|
|
||||||
"WU",
|
|
||||||
"SQUARE_CASH",
|
|
||||||
"CASH_DEPOSIT",
|
|
||||||
"ADVCASH",
|
|
||||||
"TRANSFERWISE",
|
|
||||||
"GIFT_CARD_CODE_GLOBAL",
|
|
||||||
"NETELLER",
|
|
||||||
"INTERNATIONAL_WIRE_SWIFT",
|
|
||||||
"CASH_BY_MAIL",
|
|
||||||
"SEPA",
|
|
||||||
"OTHER",
|
|
||||||
"REVOLUT",
|
|
||||||
"NATIONAL_BANK",
|
|
||||||
"MONEYBOOKERS",
|
|
||||||
"CREDITCARD",
|
|
||||||
"APPLE_PAY",
|
|
||||||
"ZELLE",
|
|
||||||
"PERFECT_MONEY",
|
|
||||||
"CASHIERS_CHECK",
|
|
||||||
"GOOGLEWALLET",
|
|
||||||
"STRIKE",
|
|
||||||
"SPECIFIC_BANK",
|
|
||||||
"CHIPPER_CASH",
|
|
||||||
"REMITLY",
|
|
||||||
"WORLDREMIT",
|
|
||||||
"PAYEER",
|
|
||||||
"MOBILE_TOP_UP",
|
|
||||||
"VIRTUAL_VISA_MASTERCARD",
|
|
||||||
"VANILLA",
|
|
||||||
"MONEYGRAM",
|
|
||||||
"VENMO",
|
|
||||||
"SERVE2SERVE",
|
|
||||||
"WEBMONEY",
|
|
||||||
]
|
|
||||||
|
|
||||||
def mock_enum_public_ads_api_call(self, api_method, query_values):
|
|
||||||
if "buy-monero-online" in api_method:
|
|
||||||
asset = "XMR"
|
|
||||||
elif "buy-bitcoins-online" in api_method:
|
|
||||||
asset = "BTC"
|
|
||||||
|
|
||||||
spl = api_method.split("/")
|
|
||||||
currency = spl[1]
|
|
||||||
|
|
||||||
page = str(query_values["page"])
|
|
||||||
return self.test_return_data[(asset, currency, page)]
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def test_get_all_public_ads(self):
|
|
||||||
# Override enum_public_ads
|
|
||||||
self.agora.api._api_call = self.mock_enum_public_ads_api_call
|
|
||||||
util.last_online_recent = MagicMock()
|
|
||||||
util.last_online_recent.return_value = True
|
|
||||||
|
|
||||||
# Override get_price
|
|
||||||
self.agora.money.cg.get_price = MagicMock()
|
|
||||||
self.agora.money.cg.get_price.return_value = cg_prices
|
|
||||||
|
|
||||||
self.agora.markets.get_all_providers = MagicMock()
|
|
||||||
self.agora.markets.get_all_providers.return_value = self.all_providers
|
|
||||||
|
|
||||||
public_ads = yield self.agora.get_all_public_ads()
|
|
||||||
self.assertDictEqual(public_ads, fake_public_ads)
|
|
||||||
|
|
||||||
for currency, ads in public_ads.items():
|
|
||||||
ad_ids = [ad[0] for ad in ads]
|
|
||||||
ad_ids_dedup = set(ad_ids)
|
|
||||||
# Make sure there's no duplicate ads
|
|
||||||
self.assertEqual(len(ad_ids), len(ad_ids_dedup))
|
|
||||||
|
|
||||||
@patch("twisted.internet.threads.deferToThread")
|
|
||||||
def test_run_cheat_in_thread(self, defer):
|
|
||||||
asset1 = self.agora.run_cheat_in_thread()
|
|
||||||
|
|
||||||
asset2 = self.agora.run_cheat_in_thread()
|
|
||||||
self.assertEqual(set([asset1, asset2]), set(["XMR", "BTC"]))
|
|
||||||
|
|
||||||
asset3 = self.agora.run_cheat_in_thread()
|
|
||||||
|
|
||||||
asset4 = self.agora.run_cheat_in_thread()
|
|
||||||
|
|
||||||
self.assertEqual(set([asset3, asset4]), set(["XMR", "BTC"]))
|
|
||||||
|
|
||||||
self.assertNotEqual(asset1, asset2)
|
|
||||||
self.assertNotEqual(asset3, asset4)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def test_update_prices(self):
|
|
||||||
# Override the providers
|
|
||||||
settings.settings.Agora.MinMargin = 1.17
|
|
||||||
settings.settings.Agora.MaxMargin = 1.3
|
|
||||||
settings.settings.Agora.ProviderList = '["REVOLUT", "NATIONAL_BANK"]'
|
|
||||||
|
|
||||||
# Override enum_public_ads
|
|
||||||
self.agora.api._api_call = self.mock_enum_public_ads_api_call
|
|
||||||
util.last_online_recent = MagicMock()
|
|
||||||
util.last_online_recent.return_value = True
|
|
||||||
|
|
||||||
# Override get_price
|
|
||||||
self.agora.money.cg.get_price = MagicMock()
|
|
||||||
self.agora.money.cg.get_price.return_value = cg_prices
|
|
||||||
|
|
||||||
self.agora.slow_ad_update = MagicMock()
|
|
||||||
yield self.agora.update_prices()
|
|
||||||
call_args = self.agora.slow_ad_update.call_args_list[0][0][0]
|
|
||||||
self.assertCountEqual(call_args, expected_to_update)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def test_enum_public_ads(self):
|
|
||||||
# Override enum_public_ads
|
|
||||||
self.agora.api._api_call = self.mock_enum_public_ads_api_call
|
|
||||||
util.last_online_recent = MagicMock()
|
|
||||||
util.last_online_recent.return_value = True
|
|
||||||
|
|
||||||
enum_ads_return = yield self.agora.enum_public_ads(
|
|
||||||
"XMR", "USD", self.all_providers
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ensure there are no duplicates
|
|
||||||
enum_ads_return_ids = [
|
|
||||||
(x[0], x[1], x[2], x[3], x[4], x[5]) for x in enum_ads_return
|
|
||||||
]
|
|
||||||
enum_ads_return_ids_dedup = set(enum_ads_return_ids)
|
|
||||||
self.assertEqual(len(enum_ads_return_ids), len(enum_ads_return_ids_dedup))
|
|
||||||
|
|
||||||
expected_return = []
|
|
||||||
# ['94b399e2-2c96-480c-b399-e22c96180cf2', 'Jorge', '272.00', 'SEPA', 'XMR', 'USD']
|
|
||||||
for asset, currency, page in self.test_return_data:
|
|
||||||
if not asset == "XMR":
|
|
||||||
continue
|
|
||||||
if not currency == "USD":
|
|
||||||
continue
|
|
||||||
content = self.test_return_data[(asset, currency, page)]
|
|
||||||
ads = content["response"]["data"]["ad_list"]
|
|
||||||
for ad in ads:
|
|
||||||
ad_id = ad["data"]["ad_id"]
|
|
||||||
username = ad["data"]["profile"]["username"]
|
|
||||||
temp_price = ad["data"]["temp_price"]
|
|
||||||
provider = ad["data"]["online_provider"]
|
|
||||||
asset = "XMR"
|
|
||||||
currency = ad["data"]["currency"]
|
|
||||||
to_append = [
|
|
||||||
ad_id,
|
|
||||||
username,
|
|
||||||
temp_price,
|
|
||||||
provider,
|
|
||||||
asset,
|
|
||||||
currency,
|
|
||||||
]
|
|
||||||
if to_append not in expected_return:
|
|
||||||
expected_return.append(to_append)
|
|
||||||
|
|
||||||
self.assertCountEqual(enum_ads_return, expected_return)
|
|
||||||
self.assertNotEqual(enum_ads_return[0][0], enum_ads_return[1][0])
|
|
||||||
|
|
||||||
ad_ids = [x[0] for x in enum_ads_return]
|
|
||||||
ad_ids_dedup = set(ad_ids)
|
|
||||||
self.assertEqual(len(ad_ids), len(ad_ids_dedup))
|
|
||||||
|
|
||||||
def test_lookup_rates(self):
|
|
||||||
# Override enum_public_ads
|
|
||||||
self.agora.api._api_call = self.mock_enum_public_ads_api_call
|
|
||||||
util.last_online_recent = MagicMock()
|
|
||||||
util.last_online_recent.return_value = True
|
|
||||||
|
|
||||||
# Override get_price
|
|
||||||
self.money.cg.get_price = MagicMock()
|
|
||||||
self.money.cg.get_price.return_value = cg_prices
|
|
||||||
|
|
||||||
enum_ads_return = self.agora.enum_public_ads("XMR", "USD", self.all_providers)
|
|
||||||
|
|
||||||
expected_return = []
|
|
||||||
# Let's manually calculate what it's supposed to look like
|
|
||||||
price_xmr = cg_prices["monero"]["usd"]
|
|
||||||
for ad in deepcopy(enum_ads_return):
|
|
||||||
price = float(ad[2])
|
|
||||||
margin = round(price / price_xmr, 2)
|
|
||||||
ad.append(margin)
|
|
||||||
expected_return.append(ad)
|
|
||||||
|
|
||||||
lookup_rates_return = self.agora.money.lookup_rates(
|
|
||||||
"agora", enum_ads_return
|
|
||||||
) # TODO: do this properly
|
|
||||||
self.assertCountEqual(lookup_rates_return, expected_return)
|
|
||||||
|
|
||||||
def test_lookup_rates_not_usd(self):
|
|
||||||
"""
|
|
||||||
Above test only tests USD which does not take into account Forex.
|
|
||||||
Let's test both, and additionaly specify our own rates.
|
|
||||||
"""
|
|
||||||
# Override enum_public_ads
|
|
||||||
self.agora.api._api_call = self.mock_enum_public_ads_api_call
|
|
||||||
util.last_online_recent = MagicMock()
|
|
||||||
util.last_online_recent.return_value = True
|
|
||||||
|
|
||||||
# Override get_price
|
|
||||||
self.agora.money.cg.get_price = MagicMock()
|
|
||||||
self.agora.money.cg.get_price.return_value = cg_prices
|
|
||||||
|
|
||||||
enum_ads_return = self.agora.enum_public_ads("XMR", "EUR", self.all_providers)
|
|
||||||
|
|
||||||
expected_return = []
|
|
||||||
# Let's manually calculate what it's supposed to look like
|
|
||||||
price_xmr = cg_prices["monero"]["eur"]
|
|
||||||
for ad in deepcopy(enum_ads_return):
|
|
||||||
price = float(ad[2])
|
|
||||||
margin = round(price / price_xmr, 2)
|
|
||||||
ad.append(margin)
|
|
||||||
expected_return.append(ad)
|
|
||||||
# Test specifying rates=
|
|
||||||
lookup_rates_return = self.agora.money.lookup_rates(
|
|
||||||
"agora", enum_ads_return, rates=cg_prices
|
|
||||||
)
|
|
||||||
self.assertCountEqual(lookup_rates_return, expected_return)
|
|
|
@ -1,228 +0,0 @@
|
||||||
import logging
|
|
||||||
from copy import deepcopy
|
|
||||||
from json import loads
|
|
||||||
from unittest import TestCase
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
import lib.markets
|
|
||||||
import lib.money
|
|
||||||
import settings
|
|
||||||
import sources
|
|
||||||
import sources.localbitcoins
|
|
||||||
import util
|
|
||||||
from tests.common import cg_prices, expected_to_update_lbtc, fake_public_ads_lbtc
|
|
||||||
from twisted.internet.defer import inlineCallbacks
|
|
||||||
|
|
||||||
|
|
||||||
class TestLBTC(TestCase):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.test_return_data = {}
|
|
||||||
with open("tests/data/lbtc_ads.json", "r") as f:
|
|
||||||
for line in f.readlines():
|
|
||||||
parsed = loads(line)
|
|
||||||
self.test_return_data[(parsed[2], parsed[1], str(parsed[0]))] = parsed[
|
|
||||||
3
|
|
||||||
]
|
|
||||||
|
|
||||||
super().__init__(*args, *kwargs)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
logging.disable(logging.CRITICAL)
|
|
||||||
self.markets = lib.markets.Markets()
|
|
||||||
self.lbtc = sources.localbitcoins.LBTC()
|
|
||||||
self.money = lib.money.Money()
|
|
||||||
self.sources = sources.Sources()
|
|
||||||
setattr(self.markets, "sources", self.sources)
|
|
||||||
setattr(self.lbtc, "markets", self.markets)
|
|
||||||
setattr(self.money, "markets", self.markets)
|
|
||||||
setattr(self.lbtc, "money", self.money)
|
|
||||||
self.markets.sinks = MagicMock()
|
|
||||||
self.markets.sinks.currencies = [
|
|
||||||
"GBP",
|
|
||||||
]
|
|
||||||
self.lbtc.sinks = MagicMock()
|
|
||||||
self.lbtc.es = MagicMock()
|
|
||||||
self.lbtc.es.index = MagicMock()
|
|
||||||
self.lbtc.sinks.currencies = self.markets.sinks.currencies
|
|
||||||
|
|
||||||
self.all_providers = [
|
|
||||||
"national-bank-transfer",
|
|
||||||
]
|
|
||||||
|
|
||||||
def mock_enum_public_ads_api_call(self, api_method, query_values):
|
|
||||||
if "buy-bitcoins-online" in api_method:
|
|
||||||
asset = "BTC"
|
|
||||||
|
|
||||||
spl = api_method.split("/")
|
|
||||||
currency = spl[1]
|
|
||||||
|
|
||||||
page = str(query_values["page"])
|
|
||||||
return self.test_return_data[(asset, currency, page)]
|
|
||||||
|
|
||||||
def test_get_all_public_ads(self):
|
|
||||||
# Override enum_public_ads
|
|
||||||
self.lbtc.api._api_call = self.mock_enum_public_ads_api_call
|
|
||||||
util.last_online_recent = MagicMock()
|
|
||||||
util.last_online_recent.return_value = True
|
|
||||||
|
|
||||||
# Override get_price
|
|
||||||
self.lbtc.money.cg.get_price = MagicMock()
|
|
||||||
self.lbtc.money.cg.get_price.return_value = cg_prices
|
|
||||||
|
|
||||||
self.lbtc.markets.get_all_providers = MagicMock()
|
|
||||||
self.lbtc.markets.get_all_providers.return_value = self.all_providers
|
|
||||||
|
|
||||||
public_ads = yield self.lbtc.get_all_public_ads()
|
|
||||||
self.assertDictEqual(public_ads, fake_public_ads_lbtc)
|
|
||||||
|
|
||||||
for currency, ads in public_ads.items():
|
|
||||||
ad_ids = [ad[0] for ad in ads]
|
|
||||||
ad_ids_dedup = set(ad_ids)
|
|
||||||
# Make sure there's no duplicate ads
|
|
||||||
self.assertEqual(len(ad_ids), len(ad_ids_dedup))
|
|
||||||
|
|
||||||
@patch("twisted.internet.threads.deferToThread")
|
|
||||||
def test_run_cheat_in_thread(self, defer):
|
|
||||||
asset1 = self.lbtc.run_cheat_in_thread()
|
|
||||||
|
|
||||||
asset2 = self.lbtc.run_cheat_in_thread()
|
|
||||||
self.assertEqual(set([asset1, asset2]), set(["BTC"]))
|
|
||||||
|
|
||||||
asset3 = self.lbtc.run_cheat_in_thread()
|
|
||||||
|
|
||||||
asset4 = self.lbtc.run_cheat_in_thread()
|
|
||||||
|
|
||||||
self.assertEqual(set([asset3, asset4]), set(["BTC"]))
|
|
||||||
|
|
||||||
# Only one asset so far for BTC
|
|
||||||
# self.assertNotEqual(asset1, asset2)
|
|
||||||
# self.assertNotEqual(asset3, asset4)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def test_update_prices(self):
|
|
||||||
settings.settings.LocalBitcoins.MinMargin = 1.10
|
|
||||||
settings.settings.LocalBitcoins.MaxMargin = 1.3
|
|
||||||
settings.settings.LocalBitcoins.Username = "Harrey"
|
|
||||||
# Override the providers
|
|
||||||
settings.settings.LocalBitcoins.ProviderList = '["national-bank-transfer"]'
|
|
||||||
|
|
||||||
# Override enum_public_ads
|
|
||||||
self.lbtc.api._api_call = self.mock_enum_public_ads_api_call
|
|
||||||
util.last_online_recent = MagicMock()
|
|
||||||
util.last_online_recent.return_value = True
|
|
||||||
|
|
||||||
# Override get_price
|
|
||||||
self.lbtc.money.cg.get_price = MagicMock()
|
|
||||||
self.lbtc.money.cg.get_price.return_value = cg_prices
|
|
||||||
|
|
||||||
self.lbtc.slow_ad_update = MagicMock()
|
|
||||||
yield self.lbtc.update_prices()
|
|
||||||
call_args = self.lbtc.slow_ad_update.call_args_list[0][0][0]
|
|
||||||
self.assertCountEqual(call_args, expected_to_update_lbtc)
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def test_enum_public_ads(self):
|
|
||||||
# Override enum_public_ads
|
|
||||||
self.lbtc.api._api_call = self.mock_enum_public_ads_api_call
|
|
||||||
util.last_online_recent = MagicMock()
|
|
||||||
util.last_online_recent.return_value = True
|
|
||||||
|
|
||||||
enum_ads_return = yield self.lbtc.enum_public_ads(
|
|
||||||
"BTC", "GBP", self.all_providers
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ensure there are no duplicates
|
|
||||||
enum_ads_return_ids = [
|
|
||||||
(x[0], x[1], x[2], x[3], x[4], x[5]) for x in enum_ads_return
|
|
||||||
]
|
|
||||||
enum_ads_return_ids_dedup = set(enum_ads_return_ids)
|
|
||||||
self.assertEqual(len(enum_ads_return_ids), len(enum_ads_return_ids_dedup))
|
|
||||||
|
|
||||||
expected_return = []
|
|
||||||
# ['94b399e2-2c96-480c-b399-e22c96180cf2', 'Jorge', '272.00', 'SEPA', 'XMR', 'USD']
|
|
||||||
for asset, currency, page in self.test_return_data:
|
|
||||||
if not asset == "BTC":
|
|
||||||
continue
|
|
||||||
if not currency == "GBP":
|
|
||||||
continue
|
|
||||||
content = self.test_return_data[(asset, currency, page)]
|
|
||||||
ads = content["response"]["data"]["ad_list"]
|
|
||||||
for ad in ads:
|
|
||||||
ad_id = str(ad["data"]["ad_id"])
|
|
||||||
username = ad["data"]["profile"]["username"]
|
|
||||||
temp_price = ad["data"]["temp_price"]
|
|
||||||
provider = ad["data"]["online_provider"]
|
|
||||||
asset = "BTC"
|
|
||||||
currency = ad["data"]["currency"]
|
|
||||||
to_append = [
|
|
||||||
ad_id,
|
|
||||||
username,
|
|
||||||
temp_price,
|
|
||||||
provider,
|
|
||||||
asset,
|
|
||||||
currency,
|
|
||||||
]
|
|
||||||
if to_append not in expected_return:
|
|
||||||
expected_return.append(to_append)
|
|
||||||
self.assertCountEqual(enum_ads_return, expected_return)
|
|
||||||
self.assertNotEqual(enum_ads_return[0][0], enum_ads_return[1][0])
|
|
||||||
|
|
||||||
ad_ids = [x[0] for x in enum_ads_return]
|
|
||||||
ad_ids_dedup = set(ad_ids)
|
|
||||||
self.assertEqual(len(ad_ids), len(ad_ids_dedup))
|
|
||||||
|
|
||||||
def test_lookup_rates(self):
|
|
||||||
# Override enum_public_ads
|
|
||||||
self.lbtc.api._api_call = self.mock_enum_public_ads_api_call
|
|
||||||
util.last_online_recent = MagicMock()
|
|
||||||
util.last_online_recent.return_value = True
|
|
||||||
|
|
||||||
# Override get_price
|
|
||||||
self.money.cg.get_price = MagicMock()
|
|
||||||
self.money.cg.get_price.return_value = cg_prices
|
|
||||||
|
|
||||||
enum_ads_return = self.lbtc.enum_public_ads("BTC", "GBP", self.all_providers)
|
|
||||||
|
|
||||||
expected_return = []
|
|
||||||
# Let's manually calculate what it's supposed to look like
|
|
||||||
price_btc = cg_prices["bitcoin"]["gbp"]
|
|
||||||
for ad in deepcopy(enum_ads_return):
|
|
||||||
price = float(ad[2])
|
|
||||||
margin = round(price / price_btc, 2)
|
|
||||||
ad.append(margin)
|
|
||||||
expected_return.append(ad)
|
|
||||||
|
|
||||||
lookup_rates_return = self.lbtc.money.lookup_rates(
|
|
||||||
"lbtc", enum_ads_return
|
|
||||||
) # TODO: do this properly
|
|
||||||
self.assertCountEqual(lookup_rates_return, expected_return)
|
|
||||||
|
|
||||||
def test_lookup_rates_not_usd(self):
|
|
||||||
"""
|
|
||||||
Above test only tests USD which does not take into account Forex.
|
|
||||||
Let's test both, and additionaly specify our own rates.
|
|
||||||
"""
|
|
||||||
# Override enum_public_ads
|
|
||||||
self.lbtc.api._api_call = self.mock_enum_public_ads_api_call
|
|
||||||
util.last_online_recent = MagicMock()
|
|
||||||
util.last_online_recent.return_value = True
|
|
||||||
|
|
||||||
# Override get_price
|
|
||||||
self.lbtc.money.cg.get_price = MagicMock()
|
|
||||||
self.lbtc.money.cg.get_price.return_value = cg_prices
|
|
||||||
|
|
||||||
enum_ads_return = self.lbtc.enum_public_ads("BTC", "GBP", self.all_providers)
|
|
||||||
|
|
||||||
expected_return = []
|
|
||||||
# Let's manually calculate what it's supposed to look like
|
|
||||||
price_btc = cg_prices["bitcoin"]["gbp"]
|
|
||||||
for ad in deepcopy(enum_ads_return):
|
|
||||||
price = float(ad[2])
|
|
||||||
margin = round(price / price_btc, 2)
|
|
||||||
ad.append(margin)
|
|
||||||
expected_return.append(ad)
|
|
||||||
# Test specifying rates=
|
|
||||||
lookup_rates_return = self.lbtc.money.lookup_rates(
|
|
||||||
"lbtc", enum_ads_return, rates=cg_prices
|
|
||||||
)
|
|
||||||
self.assertCountEqual(lookup_rates_return, expected_return)
|
|
|
@ -1,152 +0,0 @@
|
||||||
import logging
|
|
||||||
from unittest import TestCase
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import lib.markets
|
|
||||||
import settings
|
|
||||||
from sources.agora import Agora
|
|
||||||
from tests.common import expected_to_update, fake_public_ads
|
|
||||||
|
|
||||||
|
|
||||||
class TestMarkets(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
logging.disable(logging.CRITICAL)
|
|
||||||
self.markets = lib.markets.Markets()
|
|
||||||
self.agora = Agora()
|
|
||||||
self.markets.sinks = MagicMock()
|
|
||||||
self.markets.sinks.currencies = [
|
|
||||||
"AUD",
|
|
||||||
"CAD",
|
|
||||||
"CHF",
|
|
||||||
"CZK",
|
|
||||||
"DKK",
|
|
||||||
"EUR",
|
|
||||||
"GBP",
|
|
||||||
"HKD",
|
|
||||||
"HUF",
|
|
||||||
"JPY",
|
|
||||||
"MXN",
|
|
||||||
"NOK",
|
|
||||||
"NZD",
|
|
||||||
"PLN",
|
|
||||||
"RUB",
|
|
||||||
"SEK",
|
|
||||||
"SGD",
|
|
||||||
"THB",
|
|
||||||
"TRY",
|
|
||||||
"USD",
|
|
||||||
"ZAR",
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_autoprice(self):
|
|
||||||
ads = [
|
|
||||||
[
|
|
||||||
"2b6dba4d-c9db-48f2-adba-4dc9dba8f2a0",
|
|
||||||
"Xpoterlolipop",
|
|
||||||
"182.80",
|
|
||||||
"NATIONAL_BANK",
|
|
||||||
"XMR",
|
|
||||||
"USD",
|
|
||||||
1.18,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"57e3e8d6-45fe-40da-a3e8-d645fe20da46",
|
|
||||||
"SecureMole",
|
|
||||||
"183.26",
|
|
||||||
"NATIONAL_BANK",
|
|
||||||
"XMR",
|
|
||||||
"USD",
|
|
||||||
1.19,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"87af6467-be02-476e-af64-67be02676e9a",
|
|
||||||
"topmonero",
|
|
||||||
"183.42",
|
|
||||||
"NATIONAL_BANK",
|
|
||||||
"XMR",
|
|
||||||
"USD",
|
|
||||||
1.19,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"65b452e3-a29f-4233-b452-e3a29fe23369",
|
|
||||||
"topmonero",
|
|
||||||
"183.42",
|
|
||||||
"NATIONAL_BANK",
|
|
||||||
"XMR",
|
|
||||||
"USD",
|
|
||||||
1.19,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"d2c6645c-6d56-4094-8664-5c6d5640941b",
|
|
||||||
"topmonero",
|
|
||||||
"183.42",
|
|
||||||
"NATIONAL_BANK",
|
|
||||||
"XMR",
|
|
||||||
"USD",
|
|
||||||
1.19,
|
|
||||||
],
|
|
||||||
]
|
|
||||||
currency = "EUR"
|
|
||||||
margin = self.markets.autoprice("topmonero", 1.1, 1.3, ads, currency)
|
|
||||||
expected_margin = 1.18
|
|
||||||
self.assertEqual(margin, expected_margin)
|
|
||||||
|
|
||||||
def test_get_new_ad_equation(self):
|
|
||||||
self.maxDiff = None
|
|
||||||
settings.settings.Agora.MinMargin = 1.17
|
|
||||||
settings.settings.Agora.MaxMargin = 1.3
|
|
||||||
# 437 should be 1.3 but is 1.21
|
|
||||||
to_update = self.markets.get_new_ad_equations("agora", fake_public_ads)
|
|
||||||
self.assertCountEqual(to_update, expected_to_update)
|
|
||||||
res_xmr = self.markets.get_new_ad_equations("agora", fake_public_ads, ["XMR"])
|
|
||||||
expected_xmr_to_update = [x for x in expected_to_update if x[2] == "XMR"]
|
|
||||||
self.assertCountEqual(res_xmr, expected_xmr_to_update)
|
|
||||||
|
|
||||||
res_btc = self.markets.get_new_ad_equations("agora", fake_public_ads, ["BTC"])
|
|
||||||
expected_btc_to_update = [x for x in expected_to_update if x[2] == "BTC"]
|
|
||||||
self.assertCountEqual(res_btc, expected_btc_to_update)
|
|
||||||
|
|
||||||
res_both = self.markets.get_new_ad_equations(
|
|
||||||
"agora", fake_public_ads, ["XMR", "BTC"]
|
|
||||||
)
|
|
||||||
self.assertCountEqual(res_both, expected_to_update)
|
|
||||||
|
|
||||||
def test_format_ad(self):
|
|
||||||
settings.settings.Platform.Ad = """* Set **Country of recipient's bank** to **"United Kingdom"**
|
|
||||||
$PAYMENT$
|
|
||||||
* Set **Company name** to **"PATHOGEN LIMITED"**"""
|
|
||||||
payment_details = {
|
|
||||||
"sort_code": "02-03-04",
|
|
||||||
"account_number": "0023-0045",
|
|
||||||
}
|
|
||||||
payment_details_text = self.markets.format_payment_details(
|
|
||||||
"GBP", payment_details
|
|
||||||
)
|
|
||||||
ad_text = self.markets.format_ad("XMR", "GBP", payment_details_text)
|
|
||||||
expected = """* Set **Country of recipient's bank** to **"United Kingdom"**
|
|
||||||
Payment details will be released after verification has passed.
|
|
||||||
If you've already completed verification, they will be sent immediately.
|
|
||||||
* Set **Company name** to **"PATHOGEN LIMITED"**"""
|
|
||||||
|
|
||||||
self.assertEqual(ad_text, expected)
|
|
||||||
|
|
||||||
def test_format_payment_details(self):
|
|
||||||
payment_details = {
|
|
||||||
"sort_code": "02-03-04",
|
|
||||||
"account_number": "0023-0045",
|
|
||||||
}
|
|
||||||
payment_details_text = self.markets.format_payment_details(
|
|
||||||
"GBP", payment_details
|
|
||||||
)
|
|
||||||
|
|
||||||
expected = """Payment details will be released after verification has passed.
|
|
||||||
If you've already completed verification, they will be sent immediately."""
|
|
||||||
self.assertEqual(payment_details_text, expected)
|
|
||||||
|
|
||||||
expected_real = """* Sort code: **02-03-04**
|
|
||||||
* Account number: **0023-0045**
|
|
||||||
Please send in GBP."""
|
|
||||||
payment_details_text_real = self.markets.format_payment_details(
|
|
||||||
"GBP", payment_details, real=True
|
|
||||||
)
|
|
||||||
self.assertEqual(payment_details_text_real, expected_real)
|
|
|
@ -1,26 +0,0 @@
|
||||||
import logging
|
|
||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
import lib.money
|
|
||||||
|
|
||||||
|
|
||||||
class TestMoney(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
logging.disable(logging.CRITICAL)
|
|
||||||
self.money = lib.money.Money()
|
|
||||||
|
|
||||||
def test_lookup_rates(self):
|
|
||||||
# Move from Agora tests
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_rates_all(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_acceptable_margins(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_to_usd(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_profit(self):
|
|
||||||
pass
|
|
|
@ -1,294 +0,0 @@
|
||||||
import logging
|
|
||||||
from copy import deepcopy
|
|
||||||
from unittest import TestCase
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import lib.antifraud
|
|
||||||
import lib.money
|
|
||||||
import lib.transactions
|
|
||||||
|
|
||||||
|
|
||||||
class TestTransactions(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
logging.disable(logging.CRITICAL)
|
|
||||||
self.transactions = lib.transactions.Transactions()
|
|
||||||
self.test_data = {
|
|
||||||
"timestamp": "2022-03-14T19:34:13.501Z",
|
|
||||||
"description": "Received Rebiere Matthieu",
|
|
||||||
"transaction_type": "CREDIT",
|
|
||||||
"transaction_category": "CREDIT",
|
|
||||||
"transaction_classification": [],
|
|
||||||
"amount": 1,
|
|
||||||
"currency": "GBP",
|
|
||||||
"transaction_id": "ec4df5248c750c30301a1da71024ac0b",
|
|
||||||
"provider_transaction_id": "27373011.TU9ORVRBUllfQUNUSVZJVFk6OjI1MDE4MDQyOjpUUkFOU0ZFUjo6MzgwMDM2NDY2",
|
|
||||||
"normalised_provider_transaction_id": "txn-c8c12c308789bd980",
|
|
||||||
"meta": {
|
|
||||||
"provider_reference": "TEST-1",
|
|
||||||
"transaction_type": "Credit",
|
|
||||||
"provider_id": "27373011.TU9ORVRBUllfQUNUSVZJVFk6OjI1MDE4MDQyOjpUUkFOU0ZFUjo6MzgwMDM2NDY2",
|
|
||||||
"counter_party_preferred_name": "Rebiere Matthieu",
|
|
||||||
},
|
|
||||||
"subclass": "truelayer",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Mock redis calls
|
|
||||||
lib.transactions.db.r.hgetall = self.mock_hgetall
|
|
||||||
lib.transactions.db.r.hmset = self.mock_hmset
|
|
||||||
lib.transactions.db.r.keys = self.mock_keys
|
|
||||||
lib.transactions.db.r.get = self.mock_get
|
|
||||||
|
|
||||||
# Mock some callbacks
|
|
||||||
self.transactions.irc = MagicMock()
|
|
||||||
self.transactions.irc.sendmsg = MagicMock()
|
|
||||||
self.transactions.release_funds = MagicMock()
|
|
||||||
self.transactions.ux = MagicMock()
|
|
||||||
self.transactions.ux.notify = MagicMock()
|
|
||||||
self.transactions.ux.notify.notify_complete_trade = MagicMock()
|
|
||||||
self.transactions.antifraud = lib.antifraud.AntiFraud()
|
|
||||||
|
|
||||||
# Mock the rates
|
|
||||||
self.transactions.money = MagicMock()
|
|
||||||
self.transactions.money.to_usd = self.mock_to_usd
|
|
||||||
self.transactions.money.get_rates_all = MagicMock()
|
|
||||||
self.transactions.money.get_rates_all.return_value = {"GBP": 0.8}
|
|
||||||
|
|
||||||
# Don't mock the functions we want to test
|
|
||||||
self.money = lib.money.Money()
|
|
||||||
self.money.get_rates_all = MagicMock()
|
|
||||||
self.money.get_rates_all.return_value = {"GBP": 0.8}
|
|
||||||
self.transactions.money.get_acceptable_margins = (
|
|
||||||
self.money.get_acceptable_margins
|
|
||||||
)
|
|
||||||
|
|
||||||
self.trades = {
|
|
||||||
1: {
|
|
||||||
"id": "uuid1",
|
|
||||||
"buyer": "test_buyer_1",
|
|
||||||
"currency": "GBP",
|
|
||||||
"amount": "1",
|
|
||||||
"amount_xmr": "0.3",
|
|
||||||
"reference": "TEST-1",
|
|
||||||
"subclass": "agora",
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
"id": "uuid2",
|
|
||||||
"buyer": "test_buyer_2",
|
|
||||||
"currency": "GBP",
|
|
||||||
"amount": "1",
|
|
||||||
"amount_xmr": "0.3",
|
|
||||||
"reference": "TEST-2",
|
|
||||||
"subclass": "agora",
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
"id": "uuid3",
|
|
||||||
"buyer": "test_buyer_3",
|
|
||||||
"currency": "GBP",
|
|
||||||
"amount": "1000",
|
|
||||||
"amount_xmr": "3",
|
|
||||||
"reference": "TEST-3",
|
|
||||||
"subclass": "agora",
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
"id": "uuid4",
|
|
||||||
"buyer": "test_buyer_4",
|
|
||||||
"currency": "GBP",
|
|
||||||
"amount": "10",
|
|
||||||
"amount_xmr": "0.5",
|
|
||||||
"reference": "TEST-4",
|
|
||||||
"subclass": "agora",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
self.return_trades = [1, 2, 3]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def test_data_copy(self):
|
|
||||||
return deepcopy(self.test_data)
|
|
||||||
|
|
||||||
def data_custom(self, amount, currency, reference):
|
|
||||||
test_data = self.test_data_copy
|
|
||||||
test_data["meta"]["provider_reference"] = reference
|
|
||||||
test_data["amount"] = amount
|
|
||||||
test_data["currency"] = currency
|
|
||||||
return test_data
|
|
||||||
|
|
||||||
def mock_hgetall(self, string):
|
|
||||||
ref = string.split(".")[1]
|
|
||||||
for num, trade in self.trades.items():
|
|
||||||
if trade["reference"] == ref:
|
|
||||||
return trade
|
|
||||||
|
|
||||||
def mock_hmset(self, string, data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def mock_keys(self, string):
|
|
||||||
return [v["id"] for k, v in self.trades.items() if k in self.return_trades]
|
|
||||||
|
|
||||||
def mock_get(self, string):
|
|
||||||
for num, trade in self.trades.items():
|
|
||||||
if trade["id"] == string:
|
|
||||||
return trade["reference"]
|
|
||||||
|
|
||||||
def mock_to_usd(self, amount, currency):
|
|
||||||
if currency == "GBP":
|
|
||||||
return amount * 1.3
|
|
||||||
elif currency == "USD":
|
|
||||||
return amount
|
|
||||||
# fuck it who cares
|
|
||||||
elif currency == "SEK":
|
|
||||||
return 100
|
|
||||||
elif currency == "EUR":
|
|
||||||
return 10
|
|
||||||
|
|
||||||
def test_transaction(self):
|
|
||||||
self.transactions.transaction(self.test_data)
|
|
||||||
self.transactions.release_funds.assert_called_once_with("uuid1", "TEST-1")
|
|
||||||
|
|
||||||
self.transactions.release_funds = MagicMock()
|
|
||||||
ref_2 = self.test_data_copy
|
|
||||||
ref_2["meta"]["provider_reference"] = "TEST-2"
|
|
||||||
self.transactions.transaction(ref_2)
|
|
||||||
self.transactions.release_funds.assert_called_once_with("uuid2", "TEST-2")
|
|
||||||
|
|
||||||
def test_transaction_invalid(self):
|
|
||||||
invalid_data = self.test_data_copy
|
|
||||||
invalid_data = self.data_custom(2000, "SEK", "sss")
|
|
||||||
self.transactions.transaction(invalid_data)
|
|
||||||
self.transactions.release_funds.assert_not_called()
|
|
||||||
|
|
||||||
def test_transaction_malformed(self):
|
|
||||||
malformed_data = self.test_data_copy
|
|
||||||
del malformed_data["amount"]
|
|
||||||
self.transactions.transaction(malformed_data)
|
|
||||||
self.transactions.release_funds.assert_not_called()
|
|
||||||
|
|
||||||
malformed_data = self.test_data_copy
|
|
||||||
del malformed_data["currency"]
|
|
||||||
self.transactions.transaction(malformed_data)
|
|
||||||
self.transactions.release_funds.assert_not_called()
|
|
||||||
|
|
||||||
def test_transaction_no_reference_fail(self):
|
|
||||||
no_reference_fail = self.data_custom(1, "GBP", "none")
|
|
||||||
no_reference_fail["meta"]["provider_reference"] = "none"
|
|
||||||
self.transactions.transaction(no_reference_fail)
|
|
||||||
self.transactions.release_funds.assert_not_called()
|
|
||||||
|
|
||||||
def test_transaction_no_reference_pass(self):
|
|
||||||
no_reference_pass = self.data_custom(1, "GBP", "none")
|
|
||||||
no_reference_pass["meta"]["provider_reference"] = "THIS_ONE_FAILS"
|
|
||||||
self.return_trades = [1]
|
|
||||||
self.transactions.transaction(no_reference_pass)
|
|
||||||
self.transactions.release_funds.assert_called_with("uuid1", "TEST-1")
|
|
||||||
|
|
||||||
def test_transaction_large(self):
|
|
||||||
exceeds_max = self.data_custom(1000, "GBP", "TEST-3")
|
|
||||||
self.transactions.transaction(exceeds_max)
|
|
||||||
self.transactions.release_funds.assert_called_once_with("uuid3", "TEST-3")
|
|
||||||
|
|
||||||
def test_transaction_no_reference_exceeds_max(self):
|
|
||||||
exceeds_max = self.data_custom(1000, "GBP", "noref")
|
|
||||||
self.transactions.transaction(exceeds_max)
|
|
||||||
self.transactions.release_funds.assert_not_called()
|
|
||||||
|
|
||||||
def test_transaction_wrong_currency(self):
|
|
||||||
wrong_currency = self.data_custom(1, "EUR", "TEST-1")
|
|
||||||
self.transactions.transaction(wrong_currency)
|
|
||||||
self.transactions.release_funds.assert_not_called()
|
|
||||||
|
|
||||||
wrong_currency = self.data_custom(1, "EUR", "none")
|
|
||||||
self.transactions.transaction(wrong_currency)
|
|
||||||
self.transactions.release_funds.assert_not_called()
|
|
||||||
|
|
||||||
def test_transaction_wrong_amount(self):
|
|
||||||
self.transactions.money.get_acceptable_margins = MagicMock()
|
|
||||||
self.transactions.money.get_acceptable_margins.return_value = (0.8, 1.8)
|
|
||||||
wrong_amount = self.data_custom(10, "GBP", "TEST-1")
|
|
||||||
self.transactions.transaction(wrong_amount)
|
|
||||||
self.transactions.release_funds.assert_not_called()
|
|
||||||
|
|
||||||
wrong_amount = self.data_custom(10, "GBP", "none")
|
|
||||||
self.transactions.transaction(wrong_amount)
|
|
||||||
self.transactions.release_funds.assert_not_called()
|
|
||||||
|
|
||||||
# def test_transaction_pending(self):
|
|
||||||
# pending_tx = self.test_data_copy
|
|
||||||
# pending_tx["data"]["state"] = "pending"
|
|
||||||
# self.transactions.transaction(pending_tx)
|
|
||||||
# self.transactions.release_funds.assert_not_called()
|
|
||||||
|
|
||||||
def test_transaction_too_low(self):
|
|
||||||
too_low = self.data_custom(5, "GBP", "TEST-1")
|
|
||||||
self.transactions.transaction(too_low)
|
|
||||||
self.transactions.release_funds.assert_not_called()
|
|
||||||
|
|
||||||
def test_transaction_too_high(self):
|
|
||||||
too_high = self.data_custom(15, "GBP", "TEST-1")
|
|
||||||
self.transactions.transaction(too_high)
|
|
||||||
self.transactions.release_funds.assert_not_called()
|
|
||||||
|
|
||||||
# def test_transaction_pending_then_completed(self):
|
|
||||||
# pass
|
|
||||||
|
|
||||||
def test_transaction_store_incomplete_trade(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_transaction_release_incomplete_trade(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_transaction_card_payment(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_transaction_negative_amount(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_release_funds(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_new_trade(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_find_trade(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_refs(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_ref_map(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_ref(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_del_ref(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_cleanup(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_tx_to_ref(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_ref_to_tx(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_total_usd(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_total(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_write_to_es(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_remaining(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_open_trades_usd(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_total_remaining(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_total_with_trades(self):
|
|
||||||
pass
|
|
179
handler/util.py
179
handler/util.py
|
@ -1,179 +0,0 @@
|
||||||
# Other library imports
|
|
||||||
import logging
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
from httpx import ReadError, ReadTimeout, RemoteProtocolError
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
log = logging.getLogger("util")
|
|
||||||
|
|
||||||
debug = False
|
|
||||||
|
|
||||||
# Color definitions
|
|
||||||
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
|
|
||||||
COLORS = {
|
|
||||||
"WARNING": YELLOW,
|
|
||||||
"INFO": WHITE,
|
|
||||||
"DEBUG": BLUE,
|
|
||||||
"CRITICAL": YELLOW,
|
|
||||||
"ERROR": RED,
|
|
||||||
}
|
|
||||||
RESET_SEQ = "\033[0m"
|
|
||||||
COLOR_SEQ = "\033[1;%dm"
|
|
||||||
BOLD_SEQ = "\033[1m"
|
|
||||||
|
|
||||||
|
|
||||||
def formatter_message(message, use_color=True):
|
|
||||||
if use_color:
|
|
||||||
message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ)
|
|
||||||
else:
|
|
||||||
message = message.replace("$RESET", "").replace("$BOLD", "")
|
|
||||||
return message
|
|
||||||
|
|
||||||
|
|
||||||
class ColoredFormatter(logging.Formatter):
|
|
||||||
def __init__(self, msg, use_color=True):
|
|
||||||
logging.Formatter.__init__(self, msg)
|
|
||||||
self.use_color = use_color
|
|
||||||
|
|
||||||
def format(self, record):
|
|
||||||
levelname = record.levelname
|
|
||||||
if self.use_color and levelname in COLORS:
|
|
||||||
levelname_color = (
|
|
||||||
COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ
|
|
||||||
)
|
|
||||||
record.levelname = levelname_color
|
|
||||||
return logging.Formatter.format(self, record)
|
|
||||||
|
|
||||||
|
|
||||||
def get_logger(name):
|
|
||||||
# Define the logging format
|
|
||||||
FORMAT = "%(asctime)s %(levelname)18s $BOLD%(name)13s$RESET - %(message)s"
|
|
||||||
COLOR_FORMAT = formatter_message(FORMAT, True)
|
|
||||||
color_formatter = ColoredFormatter(COLOR_FORMAT)
|
|
||||||
# formatter = logging.Formatter(
|
|
||||||
|
|
||||||
# Why is this so complicated?
|
|
||||||
ch = logging.StreamHandler()
|
|
||||||
ch.setLevel(logging.DEBUG)
|
|
||||||
# ch.setFormatter(formatter)
|
|
||||||
ch.setFormatter(color_formatter)
|
|
||||||
|
|
||||||
# Define the logger on the base class
|
|
||||||
log = logging.getLogger(name)
|
|
||||||
if debug:
|
|
||||||
log.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
# Add the handler and stop it being silly and printing everything twice
|
|
||||||
log.addHandler(ch)
|
|
||||||
log.propagate = False
|
|
||||||
return log
|
|
||||||
|
|
||||||
|
|
||||||
class Base(object):
|
|
||||||
def __init__(self):
|
|
||||||
name = self.__class__.__name__
|
|
||||||
|
|
||||||
# Set up all the logging stuff
|
|
||||||
self.log = get_logger(name)
|
|
||||||
|
|
||||||
self.log.info("Class initialised")
|
|
||||||
|
|
||||||
|
|
||||||
def xmerge_attrs(init_map):
|
|
||||||
"""
|
|
||||||
Given a dictionary of strings and classes, set all corresponding class.<string> attributes
|
|
||||||
on each class, to every other class.
|
|
||||||
"a": A(), "b": B() -> A.b = B_instance, B.a = A_instance
|
|
||||||
:param init_map: dict of class names to classes
|
|
||||||
"""
|
|
||||||
for classname, object_instance in init_map.items():
|
|
||||||
# notify, Notify
|
|
||||||
for classname_inside, object_instance_inside in init_map.items():
|
|
||||||
if not classname == classname_inside:
|
|
||||||
# irc, bot
|
|
||||||
setattr(object_instance, classname_inside, object_instance_inside)
|
|
||||||
|
|
||||||
|
|
||||||
def convert(data):
|
|
||||||
"""
|
|
||||||
Recursively convert a dictionary.
|
|
||||||
"""
|
|
||||||
if isinstance(data, bytes):
|
|
||||||
return data.decode("ascii")
|
|
||||||
if isinstance(data, dict):
|
|
||||||
return dict(map(convert, data.items()))
|
|
||||||
if isinstance(data, tuple):
|
|
||||||
return map(convert, data)
|
|
||||||
if isinstance(data, list):
|
|
||||||
return list(map(convert, data))
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def last_online_recent(date):
|
|
||||||
"""
|
|
||||||
Check if the last online date was recent.
|
|
||||||
:param date: date last online
|
|
||||||
:type date: string
|
|
||||||
:return: bool indicating whether the date was recent enough
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
if "+" in date:
|
|
||||||
# for LBTC
|
|
||||||
# 2022-04-16T08:53:58+00:00
|
|
||||||
date_split = date.split("+")
|
|
||||||
date_split[1].replace(".", "")
|
|
||||||
date_split[1].replace(":", "")
|
|
||||||
date = "+".join(date_split)
|
|
||||||
date_string = "%Y-%m-%dT%H:%M:%S%z"
|
|
||||||
now = datetime.now(timezone.utc)
|
|
||||||
else:
|
|
||||||
date_string = "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
||||||
now = datetime.now()
|
|
||||||
date_parsed = datetime.strptime(date, date_string)
|
|
||||||
sec_ago_date = (now - date_parsed).total_seconds()
|
|
||||||
return sec_ago_date < 172800
|
|
||||||
|
|
||||||
|
|
||||||
def handle_exceptions(func):
|
|
||||||
"""
|
|
||||||
Wrapper helper to handle Agora API errors.
|
|
||||||
:param func: function to wrap
|
|
||||||
:rtype: func
|
|
||||||
:return: the wrapped function
|
|
||||||
"""
|
|
||||||
|
|
||||||
def inner_function(*args, **kwargs):
|
|
||||||
"""
|
|
||||||
Inner wrapper helper.
|
|
||||||
:rtype: any or bool
|
|
||||||
:return: False or the normal return
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
rtrn = func(*args, **kwargs)
|
|
||||||
except (ReadTimeout, ReadError, RemoteProtocolError):
|
|
||||||
return False
|
|
||||||
if isinstance(rtrn, dict):
|
|
||||||
if "success" in rtrn:
|
|
||||||
if "message" in rtrn:
|
|
||||||
if not rtrn["success"] and rtrn["message"] == "API ERROR":
|
|
||||||
if "error_code" in rtrn["response"]["error"]:
|
|
||||||
code = rtrn["response"]["error"]["error_code"]
|
|
||||||
if not code == 136:
|
|
||||||
log.error(f"API error: {code}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
log.error(f"API error: {rtrn['response']['error']}")
|
|
||||||
return False
|
|
||||||
return rtrn
|
|
||||||
|
|
||||||
return inner_function
|
|
||||||
|
|
||||||
|
|
||||||
def get_settings(platform):
|
|
||||||
if platform == "agora":
|
|
||||||
return settings.Agora
|
|
||||||
elif platform == "lbtc":
|
|
||||||
return settings.LocalBitcoins
|
|
|
@ -1,44 +0,0 @@
|
||||||
# Other library imports
|
|
||||||
# import requests
|
|
||||||
# from json import dumps
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
# from settings import settings
|
|
||||||
import util
|
|
||||||
import ux.commands
|
|
||||||
import ux.irc
|
|
||||||
import ux.notify
|
|
||||||
import ux.verify
|
|
||||||
|
|
||||||
|
|
||||||
class UX(object):
|
|
||||||
"""
|
|
||||||
Class to manage calls to various user interfaces.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.irc = ux.irc.bot()
|
|
||||||
self.notify = ux.notify.Notify()
|
|
||||||
self.verify = ux.verify.Verify()
|
|
||||||
|
|
||||||
def __xmerged__(self):
|
|
||||||
"""
|
|
||||||
Called when xmerge has been completed in the webapp.
|
|
||||||
Merge all instances into child classes.
|
|
||||||
"""
|
|
||||||
init_map = {
|
|
||||||
"ux": self,
|
|
||||||
"markets": self.markets,
|
|
||||||
"sinks": self.sinks,
|
|
||||||
"sources": self.sources,
|
|
||||||
"tx": self.tx,
|
|
||||||
"webapp": self.webapp,
|
|
||||||
"money": self.money,
|
|
||||||
"irc": self.irc,
|
|
||||||
"notify": self.notify,
|
|
||||||
"verify": self.verify,
|
|
||||||
"antifraud": self.antifraud,
|
|
||||||
}
|
|
||||||
util.xmerge_attrs(init_map)
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,248 +0,0 @@
|
||||||
# Twisted/Klein imports
|
|
||||||
import util
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
from twisted.internet import protocol, reactor, ssl
|
|
||||||
from twisted.words.protocols import irc
|
|
||||||
from ux.commands import IRCCommands
|
|
||||||
|
|
||||||
|
|
||||||
class IRCBot(irc.IRCClient):
|
|
||||||
def __init__(self, log):
|
|
||||||
"""
|
|
||||||
Initialise IRC bot.
|
|
||||||
:param log: logger instance
|
|
||||||
:type log: Logger
|
|
||||||
"""
|
|
||||||
self.log = log
|
|
||||||
self.cmd = IRCCommands()
|
|
||||||
# Parse the commands into "commandname": "commandclass"
|
|
||||||
self.cmdhash = {
|
|
||||||
getattr(self.cmd, x).name: x for x in dir(self.cmd) if not x.startswith("_")
|
|
||||||
}
|
|
||||||
self.nickname = settings.IRC.Nick
|
|
||||||
self.password = settings.IRC.Pass
|
|
||||||
self.realname = self.nickname
|
|
||||||
self.username = self.nickname
|
|
||||||
|
|
||||||
# Don't give away information about our client
|
|
||||||
self.userinfo = None
|
|
||||||
self.fingerReply = None
|
|
||||||
self.versionName = None
|
|
||||||
self.sourceURL = None
|
|
||||||
self.lineRate = None # Don't throttle messages, we may need to send a lot
|
|
||||||
|
|
||||||
self.prefix = settings.IRC.Prefix
|
|
||||||
self.admins = (settings.IRC.Admins).split("\n")
|
|
||||||
self.highlight = (settings.IRC.Highlight).split("\n")
|
|
||||||
|
|
||||||
self.channel = settings.IRC.Channel
|
|
||||||
|
|
||||||
def parse(self, user, host, channel, msg):
|
|
||||||
"""
|
|
||||||
Simple handler for IRC commands.
|
|
||||||
:param user: full user string with host
|
|
||||||
:param host: user's hostname
|
|
||||||
:param channel: channel the message was received on
|
|
||||||
:param msg: the message
|
|
||||||
:type user: string
|
|
||||||
:type host: string
|
|
||||||
:type channel: string
|
|
||||||
:type msg: string
|
|
||||||
"""
|
|
||||||
spl = msg.split()
|
|
||||||
# nick = user.split("!")[0]
|
|
||||||
|
|
||||||
cmd = spl[0]
|
|
||||||
length = len(spl)
|
|
||||||
|
|
||||||
# Check if user is authenticated
|
|
||||||
authed = host in self.admins
|
|
||||||
if cmd == "help" and length == 2 and authed:
|
|
||||||
if spl[1] in self.cmdhash:
|
|
||||||
cmdname = self.cmdhash[spl[1]]
|
|
||||||
obj = getattr(self.cmd, cmdname)
|
|
||||||
helptext = getattr(obj, "helptext")
|
|
||||||
self.msg(channel, helptext)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
self.msg(channel, f"No such command: {spl[1]}")
|
|
||||||
return
|
|
||||||
if cmd == "helpall" and authed:
|
|
||||||
for command in self.cmdhash:
|
|
||||||
cmdname = self.cmdhash[command]
|
|
||||||
obj = getattr(self.cmd, cmdname)
|
|
||||||
helptext = getattr(obj, "helptext")
|
|
||||||
self.msg(channel, f"{cmdname}: {helptext}")
|
|
||||||
return
|
|
||||||
|
|
||||||
if cmd in self.cmdhash:
|
|
||||||
# Get the class name of the referenced command
|
|
||||||
cmdname = self.cmdhash[cmd]
|
|
||||||
# Get the class name
|
|
||||||
obj = getattr(self.cmd, cmdname)
|
|
||||||
|
|
||||||
def msgl(x):
|
|
||||||
self.msg(channel, x)
|
|
||||||
|
|
||||||
# Check if the command required authentication
|
|
||||||
if obj.authed:
|
|
||||||
if host in self.admins:
|
|
||||||
obj.run(
|
|
||||||
cmd,
|
|
||||||
spl,
|
|
||||||
length,
|
|
||||||
authed,
|
|
||||||
msgl,
|
|
||||||
self.agora,
|
|
||||||
self.tx,
|
|
||||||
self.ux,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Handle authentication here instead of in the command module for security
|
|
||||||
self.msg(channel, "Access denied.")
|
|
||||||
else:
|
|
||||||
# Run an unauthenticated command, without passing through secure library calls
|
|
||||||
obj.run(cmd, spl, len(spl), authed, msgl)
|
|
||||||
return
|
|
||||||
self.msg(channel, "Command not found.")
|
|
||||||
if authed:
|
|
||||||
# Give user command hints if they are authenticated
|
|
||||||
self.msg(channel, f"Commands loaded: {', '.join(self.cmdhash.keys())}")
|
|
||||||
|
|
||||||
def signedOn(self):
|
|
||||||
"""
|
|
||||||
Called when we have signed on to IRC.
|
|
||||||
Join our channel.
|
|
||||||
"""
|
|
||||||
self.log.info(f"Signed on as {self.nickname}")
|
|
||||||
self.join(self.channel)
|
|
||||||
|
|
||||||
def joined(self, channel):
|
|
||||||
"""
|
|
||||||
Called when we have joined a channel.
|
|
||||||
Setup the Agora LoopingCall to get trades.
|
|
||||||
This is here to ensure the IRC client is initialised enough to send the trades.
|
|
||||||
:param channel: channel we joined
|
|
||||||
:type channel: string
|
|
||||||
"""
|
|
||||||
self.sinks.__irc_started__()
|
|
||||||
self.sources.__irc_started__()
|
|
||||||
self.log.info(f"Joined channel {channel}")
|
|
||||||
|
|
||||||
def privmsg(self, user, channel, msg):
|
|
||||||
"""
|
|
||||||
Called on received PRIVMSGs.
|
|
||||||
Pass through identified commands to the parse function.
|
|
||||||
:param user: full user string with host
|
|
||||||
:param channel: channel the message was received on
|
|
||||||
:param msg: the message
|
|
||||||
:type user: string
|
|
||||||
:type channel: string
|
|
||||||
:type msg: string
|
|
||||||
"""
|
|
||||||
nick = user.split("!")[0]
|
|
||||||
|
|
||||||
if channel == self.nickname:
|
|
||||||
channel = nick
|
|
||||||
|
|
||||||
host = user.split("!")[1]
|
|
||||||
host = host.split("@")[1]
|
|
||||||
|
|
||||||
ident = user.split("!")[1]
|
|
||||||
ident = ident.split("@")[0]
|
|
||||||
self.log.info(f"({channel}) {user}: {msg}")
|
|
||||||
if msg[0] == self.prefix:
|
|
||||||
if len(msg) > 1:
|
|
||||||
if msg.split()[0] != "!":
|
|
||||||
self.parse(user, host, channel, msg[1:])
|
|
||||||
elif host in self.admins and channel == nick:
|
|
||||||
if len(msg) > 0:
|
|
||||||
spl = msg.split()
|
|
||||||
if len(spl) > 0:
|
|
||||||
if spl[0] != "!":
|
|
||||||
self.parse(user, host, channel, msg)
|
|
||||||
|
|
||||||
def noticed(self, user, channel, msg):
|
|
||||||
"""
|
|
||||||
Called on received NOTICEs.
|
|
||||||
:param user: full user string with host
|
|
||||||
:param channel: channel the notice was received on
|
|
||||||
:param msg: the message
|
|
||||||
:type user: string
|
|
||||||
:type channel: string
|
|
||||||
:type msg: string
|
|
||||||
"""
|
|
||||||
nick = user.split("!")[0]
|
|
||||||
if channel == self.nickname:
|
|
||||||
channel = nick
|
|
||||||
# self.log.info("[%s] %s: %s" % (channel, user, msg))
|
|
||||||
|
|
||||||
|
|
||||||
class IRCBotFactory(protocol.ClientFactory):
|
|
||||||
def __init__(self):
|
|
||||||
self.log = util.get_logger("IRC")
|
|
||||||
self.log.info("Class initialised")
|
|
||||||
|
|
||||||
def sendmsg(self, msg):
|
|
||||||
"""
|
|
||||||
Passthrough function to send a message to the channel.
|
|
||||||
"""
|
|
||||||
if self.client:
|
|
||||||
self.client.msg(self.client.channel, msg)
|
|
||||||
else:
|
|
||||||
self.log.error(f"Trying to send a message without connected client: {msg}")
|
|
||||||
return
|
|
||||||
|
|
||||||
def buildProtocol(self, addr):
|
|
||||||
"""
|
|
||||||
Custom override for the Twisted buildProtocol so we can access the Protocol instance.
|
|
||||||
Passes through the Agora instance to IRC.
|
|
||||||
:return: IRCBot Protocol instance
|
|
||||||
"""
|
|
||||||
# Pass through the logger
|
|
||||||
prcol = IRCBot(self.log)
|
|
||||||
self.client = prcol
|
|
||||||
setattr(self.client, "agora", self.agora)
|
|
||||||
setattr(self.client, "sinks", self.sinks)
|
|
||||||
setattr(self.client, "sources", self.sources)
|
|
||||||
setattr(self.client, "tx", self.tx)
|
|
||||||
setattr(self.client, "ux", self.ux)
|
|
||||||
return prcol
|
|
||||||
|
|
||||||
def clientConnectionLost(self, connector, reason):
|
|
||||||
"""
|
|
||||||
Called when connection to IRC server lost. Reconnect.
|
|
||||||
:param connector: connector object
|
|
||||||
:param reason: reason connection lost
|
|
||||||
:type connector: object
|
|
||||||
:type reason: string
|
|
||||||
"""
|
|
||||||
self.log.error(f"Lost connection: {reason}, reconnecting")
|
|
||||||
connector.connect()
|
|
||||||
|
|
||||||
def clientConnectionFailed(self, connector, reason):
|
|
||||||
"""
|
|
||||||
Called when connection to IRC server failed. Reconnect.
|
|
||||||
:param connector: connector object
|
|
||||||
:param reason: reason connection failed
|
|
||||||
:type connector: object
|
|
||||||
:type reason: string
|
|
||||||
"""
|
|
||||||
self.log.error(f"Could not connect: {reason}")
|
|
||||||
connector.connect()
|
|
||||||
|
|
||||||
|
|
||||||
def bot():
|
|
||||||
"""
|
|
||||||
Load the certificates, start the Bot Factory and connect it to the IRC server.
|
|
||||||
:return: Factory instance
|
|
||||||
:rtype: Factory
|
|
||||||
"""
|
|
||||||
# Load the certificates
|
|
||||||
context = ssl.DefaultOpenSSLContextFactory(settings.IRC.Cert, settings.IRC.Cert)
|
|
||||||
# Define the factory instance
|
|
||||||
factory = IRCBotFactory()
|
|
||||||
reactor.connectSSL(settings.IRC.Host, int(settings.IRC.Port), factory, context)
|
|
||||||
return factory
|
|
|
@ -1,86 +0,0 @@
|
||||||
# Other library imports
|
|
||||||
import requests
|
|
||||||
import util
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
|
|
||||||
class Notify(util.Base):
|
|
||||||
"""
|
|
||||||
Class to handle more robust notifications.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def sendmsg(self, msg, title=None, priority=None, tags=None):
|
|
||||||
if settings.Notify.Enabled == "0":
|
|
||||||
return
|
|
||||||
headers = {"Title": "Bot"}
|
|
||||||
if title:
|
|
||||||
headers["Title"] = title
|
|
||||||
if priority:
|
|
||||||
headers["Priority"] = priority
|
|
||||||
if tags:
|
|
||||||
headers["Tags"] = tags
|
|
||||||
requests.post(
|
|
||||||
f"{settings.Notify.Host}/{settings.Notify.Topic}",
|
|
||||||
data=msg,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
def notify_new_trade(self, amount, currency):
|
|
||||||
amount_usd = self.money.to_usd(amount, currency)
|
|
||||||
self.sendmsg(
|
|
||||||
f"Total: {amount_usd}",
|
|
||||||
title="New trade",
|
|
||||||
tags="trades",
|
|
||||||
priority="2",
|
|
||||||
)
|
|
||||||
|
|
||||||
def notify_complete_trade(self, amount, currency):
|
|
||||||
amount_usd = self.money.to_usd(amount, currency)
|
|
||||||
self.sendmsg(
|
|
||||||
f"Total: {amount_usd}",
|
|
||||||
title="Trade complete",
|
|
||||||
tags="trades,profit",
|
|
||||||
priority="3",
|
|
||||||
)
|
|
||||||
|
|
||||||
def notify_withdrawal(self, amount_usd):
|
|
||||||
self.sendmsg(
|
|
||||||
f"Total: {amount_usd}",
|
|
||||||
title="Withdrawal",
|
|
||||||
tags="profit",
|
|
||||||
priority="4",
|
|
||||||
)
|
|
||||||
|
|
||||||
def notify_need_topup(self, amount_usd_xmr):
|
|
||||||
self.sendmsg(
|
|
||||||
f"XMR: {amount_usd_xmr}",
|
|
||||||
title="Topup needed",
|
|
||||||
tags="admin",
|
|
||||||
priority="5",
|
|
||||||
)
|
|
||||||
|
|
||||||
def notify_tx_lookup_failed(self, currency, amount, reference, code, trade_id=None):
|
|
||||||
self.sendmsg(
|
|
||||||
f"Unknown TX [{code}]: {amount}{currency} ({reference}) for {trade_id}",
|
|
||||||
title=code,
|
|
||||||
tags="tx",
|
|
||||||
priority="5",
|
|
||||||
)
|
|
||||||
|
|
||||||
def notify_release_unsuccessful(self, trade_id):
|
|
||||||
self.sendmsg(
|
|
||||||
f"Release unsuccessful for {trade_id}",
|
|
||||||
title="Unsuccessful release",
|
|
||||||
tags="tx",
|
|
||||||
priority="5",
|
|
||||||
)
|
|
||||||
|
|
||||||
def notify_sender_name_mismatch(self, trade_id, platform_username, bank_sender):
|
|
||||||
self.sendmsg(
|
|
||||||
f"Sender name mismatch for {trade_id}: Username: {platform_username}, Sender: {bank_sender}",
|
|
||||||
title="Sender name mismatch",
|
|
||||||
tags="fraud",
|
|
||||||
priority="5",
|
|
||||||
)
|
|
|
@ -1,233 +0,0 @@
|
||||||
# Other library imports
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import util
|
|
||||||
|
|
||||||
# Project imports
|
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
|
|
||||||
class Verify(util.Base):
|
|
||||||
"""
|
|
||||||
Class to handle user verification.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def create_uid(self, platform, username):
|
|
||||||
return f"{platform}|{username}"
|
|
||||||
|
|
||||||
def get_uid(self, external_user_id):
|
|
||||||
"""
|
|
||||||
Get the platform and username from the external user ID.
|
|
||||||
"""
|
|
||||||
spl = external_user_id.split("|")
|
|
||||||
if not len(spl) == 2:
|
|
||||||
self.log.error(f"Split invalid, cannot get customer: {spl}")
|
|
||||||
return False
|
|
||||||
platform, username = spl
|
|
||||||
return (platform, username)
|
|
||||||
|
|
||||||
def verification_successful(self, external_user_id):
|
|
||||||
"""
|
|
||||||
Called when verification has been successfully passed.
|
|
||||||
"""
|
|
||||||
self.antifraud.user_verification_successful(external_user_id)
|
|
||||||
|
|
||||||
def update_verification_status(
|
|
||||||
self, external_user_id, review_status, review_answer=None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Update the authentication status of a external user ID.
|
|
||||||
"""
|
|
||||||
if review_status == "completed" and review_answer == "GREEN":
|
|
||||||
self.verification_successful(external_user_id)
|
|
||||||
|
|
||||||
def verify_webhook_signature(self, content, payload_digest):
|
|
||||||
if type(content) == str:
|
|
||||||
content = content.encode("utf-8")
|
|
||||||
# hmac needs bytes
|
|
||||||
signature = hmac.new(
|
|
||||||
settings.Verify.WebHookSecret.encode("utf-8"),
|
|
||||||
content,
|
|
||||||
digestmod=hashlib.sha1,
|
|
||||||
).hexdigest()
|
|
||||||
|
|
||||||
return signature == payload_digest
|
|
||||||
|
|
||||||
def process_callback(self, content_json):
|
|
||||||
if "externalUserId" in content_json:
|
|
||||||
external_user_id = content_json["externalUserId"]
|
|
||||||
else:
|
|
||||||
self.log.warning("Useless callback received. No external user ID.")
|
|
||||||
return False
|
|
||||||
if "reviewStatus" in content_json:
|
|
||||||
review_status = content_json["reviewStatus"]
|
|
||||||
else:
|
|
||||||
self.log.warning("Useless callback received. No review status.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
review_answer = None
|
|
||||||
if review_status == "completed":
|
|
||||||
if "reviewResult" in content_json:
|
|
||||||
if "reviewAnswer" in content_json["reviewResult"]:
|
|
||||||
review_answer = content_json["reviewResult"]["reviewAnswer"]
|
|
||||||
|
|
||||||
self.update_verification_status(
|
|
||||||
external_user_id, review_status, review_answer=review_answer
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def handle_callback(self, request):
|
|
||||||
"""
|
|
||||||
Handle a webhook callback.
|
|
||||||
"""
|
|
||||||
content = request.content.read()
|
|
||||||
payload_digest = request.getHeader("x-payload-digest")
|
|
||||||
if not self.verify_webhook_signature(content, payload_digest):
|
|
||||||
self.log.error("Webhook is not signed. Aborting.")
|
|
||||||
return False
|
|
||||||
content_json = json.loads(content)
|
|
||||||
rtrn = self.process_callback(content_json)
|
|
||||||
return rtrn
|
|
||||||
|
|
||||||
def get_external_user_id_details(self, external_user_id):
|
|
||||||
# /resources/applicants/-;externalUserId={externalUserId}/one
|
|
||||||
url = f"{settings.Verify.Base}/resources/applicants/-;externalUserId={external_user_id}/one"
|
|
||||||
resp = self.sign_request(requests.Request("GET", url))
|
|
||||||
s = requests.Session()
|
|
||||||
response = s.send(resp)
|
|
||||||
info = response.json()
|
|
||||||
if "info" in info:
|
|
||||||
if {"firstName", "lastName"}.issubset(set(info["info"].keys())):
|
|
||||||
first_name = info["info"]["firstName"]
|
|
||||||
last_name = info["info"]["lastName"]
|
|
||||||
if first_name.startswith("MR "):
|
|
||||||
first_name = first_name[3:]
|
|
||||||
return (first_name, last_name)
|
|
||||||
|
|
||||||
def create_applicant_and_get_link(self, external_user_id):
|
|
||||||
"""
|
|
||||||
Create the applicant and return the authentication link.
|
|
||||||
"""
|
|
||||||
# applicant_id = self.create_applicant(external_user_id)
|
|
||||||
auth_url = self.get_authentication_link(external_user_id)
|
|
||||||
return auth_url
|
|
||||||
|
|
||||||
def get_authentication_link(self, external_user_id):
|
|
||||||
"""
|
|
||||||
Get an external authentication link for a user.
|
|
||||||
"""
|
|
||||||
# /resources/sdkIntegrations/levels/{levelName}/websdkLink?ttlInSecs={lifetime}&externalUserId={externalUserId}&lang={locale}
|
|
||||||
url = (
|
|
||||||
f"{settings.Verify.Base}/resources/sdkIntegrations/levels/{settings.Verify.LevelName}"
|
|
||||||
f"/websdkLink?ttlInSecs=36000&externalUserId={external_user_id}"
|
|
||||||
)
|
|
||||||
resp = self.sign_request(requests.Request("POST", url))
|
|
||||||
s = requests.Session()
|
|
||||||
response = s.send(resp)
|
|
||||||
verification_url = response.json()["url"]
|
|
||||||
return verification_url
|
|
||||||
|
|
||||||
def get_applicant_status(self, applicant_id):
|
|
||||||
"""
|
|
||||||
Get the status of an applicant.
|
|
||||||
"""
|
|
||||||
# url = settings.Verify.Base + '/resources/applicants/' + applicant_id + '/requiredIdDocsStatus'
|
|
||||||
url = f"{settings.Verify.Base}/resources/applicants/'{applicant_id}/requiredIdDocsStatus"
|
|
||||||
resp = self.sign_request(requests.Request("GET", url))
|
|
||||||
s = requests.Session()
|
|
||||||
response = s.send(resp)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def get_external_user_id_status(self, external_user_id):
|
|
||||||
"""
|
|
||||||
Get the status of an applicant by the external user ID.
|
|
||||||
"""
|
|
||||||
url = (
|
|
||||||
settings.Verify.Base
|
|
||||||
+ f"/resources/applicants/-;externalUserId={external_user_id}/one"
|
|
||||||
)
|
|
||||||
resp = self.sign_request(requests.Request("GET", url))
|
|
||||||
s = requests.Session()
|
|
||||||
response = s.send(resp)
|
|
||||||
response_json = response.json()
|
|
||||||
if "review" in response_json:
|
|
||||||
if "reviewResult" in response_json["review"]:
|
|
||||||
if "reviewAnswer" in response_json["review"]["reviewResult"]:
|
|
||||||
return response_json["review"]["reviewResult"]["reviewAnswer"]
|
|
||||||
return
|
|
||||||
|
|
||||||
def create_applicant(self, external_user_id):
|
|
||||||
"""
|
|
||||||
Create an applicant.
|
|
||||||
"""
|
|
||||||
body = {"externalUserId": external_user_id}
|
|
||||||
params = {"levelName": settings.Verify.LevelName}
|
|
||||||
headers = {"Content-Type": "application/json", "Content-Encoding": "utf-8"}
|
|
||||||
resp = self.sign_request(
|
|
||||||
requests.Request(
|
|
||||||
"POST",
|
|
||||||
f"{settings.Verify.Base}/resources/applicants?levelName={settings.Verify.LevelName}",
|
|
||||||
params=params,
|
|
||||||
data=json.dumps(body),
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
s = requests.Session()
|
|
||||||
response = s.send(resp)
|
|
||||||
applicant_id = response.json()["id"]
|
|
||||||
return applicant_id
|
|
||||||
|
|
||||||
def get_access_token(self, external_user_id, level_name):
|
|
||||||
"""
|
|
||||||
Get an access token for an external user ID.
|
|
||||||
"""
|
|
||||||
params = {
|
|
||||||
"userId": external_user_id,
|
|
||||||
"ttlInSecs": "600",
|
|
||||||
"levelName": level_name,
|
|
||||||
}
|
|
||||||
headers = {"Content-Type": "application/json", "Content-Encoding": "utf-8"}
|
|
||||||
resp = self.sign_request(
|
|
||||||
requests.Request(
|
|
||||||
"POST",
|
|
||||||
f"{settings.Verify.Base}/resources/accessTokens",
|
|
||||||
params=params,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
s = requests.Session()
|
|
||||||
response = s.send(resp)
|
|
||||||
token = response.json()["token"]
|
|
||||||
|
|
||||||
return token
|
|
||||||
|
|
||||||
def sign_request(self, request: requests.Request) -> requests.PreparedRequest:
|
|
||||||
"""
|
|
||||||
Sign a request.
|
|
||||||
"""
|
|
||||||
prepared_request = request.prepare()
|
|
||||||
now = int(time.time())
|
|
||||||
method = request.method.upper()
|
|
||||||
path_url = prepared_request.path_url # includes encoded query params
|
|
||||||
# could be None so we use an empty **byte** string here
|
|
||||||
body = b"" if prepared_request.body is None else prepared_request.body
|
|
||||||
if type(body) == str:
|
|
||||||
body = body.encode("utf-8")
|
|
||||||
data_to_sign = (
|
|
||||||
str(now).encode("utf-8")
|
|
||||||
+ method.encode("utf-8")
|
|
||||||
+ path_url.encode("utf-8")
|
|
||||||
+ body
|
|
||||||
)
|
|
||||||
# hmac needs bytes
|
|
||||||
signature = hmac.new(
|
|
||||||
settings.Verify.Key.encode("utf-8"), data_to_sign, digestmod=hashlib.sha256
|
|
||||||
)
|
|
||||||
prepared_request.headers["X-App-Token"] = settings.Verify.Token
|
|
||||||
prepared_request.headers["X-App-Access-Ts"] = str(now)
|
|
||||||
prepared_request.headers["X-App-Access-Sig"] = signature.hexdigest()
|
|
||||||
return prepared_request
|
|
Loading…
Reference in New Issue