import click import arrow from os.path import isfile from time import sleep from flask import Flask, url_for from flask_sqlalchemy import SQLAlchemy from flask_wtf.csrf import CSRFProtect from redis import Redis from datetime import datetime, timezone, timedelta from app import config from app.library.mattermost import post_webhook db = SQLAlchemy() def _setup_db(app: Flask): uri = 'postgresql+psycopg2://{user}:{pw}@{host}:{port}/{db}'.format( user=config.DB_USER, pw=config.DB_PASS, host=config.DB_HOST, port=config.DB_PORT, db=config.DB_NAME ) app.config['SQLALCHEMY_DATABASE_URI'] = uri app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) def create_app(): app = Flask(__name__) app.config.from_envvar('FLASK_SECRETS') # Setup backends _setup_db(app) with app.app_context(): # todo - find a way to move filters into a new file # Template filters @app.template_filter('pst') def pst(s): d = arrow.get(s).to('US/Pacific').format('YYYY-MM-DD HH:mm:ss') return d @app.template_filter('from_atomic_wow') def from_atomic(v): from app.library.crypto import wownero return wownero.as_real(wownero.from_atomic(v)) @app.template_filter('from_atomic_xmr') def from_atomic(v): from app.library.crypto import monero return monero.as_real(monero.from_atomic(v)) @app.template_filter('from_atomic_usd') def to_ausd(v): from app.models import Swap return Swap().from_ausd(v) @app.template_filter('ts') def from_ts(v): from datetime import datetime return datetime.fromtimestamp(v) @app.template_filter('wow_block_explorer') def wow_block_explorer(v): return f'https://wownero.club/transaction/{v}' @app.template_filter('xmr_block_explorer') def xmr_block_explorer(v): return f'https://www.exploremonero.com/transaction/{v}' # todo - find a way to move into a new file # CLI @app.cli.command('init') def init(): import app.models db.create_all() @app.cli.command('store') def store(): from app.library.crypto import wow_wallet, xmr_wallet a, b = wow_wallet.make_wallet_rpc('store'), xmr_wallet.make_wallet_rpc('store') return (a, b) @app.cli.command('show') @click.argument('swap_id') def show_swap(swap_id): from pprint import pprint from app.models import Swap s = Swap.query.get(swap_id) if s: pprint(s.show_details()) else: print('Swap ID does not exist') @app.cli.command('delete') @click.argument('swap_id') def show_swap(swap_id): from app.models import Swap s = Swap.query.get(swap_id) if s: print(s.show_details()) db.session.delete(s) db.session.commit() print(f'Swap {s.id} was deleted') post_webhook(f'Deleted swap {s.id} from console') else: print('Swap ID does not exist') @app.cli.command('list') def list_swaps(): from app.models import Swap s = Swap.query.all() for i in s: print(i.id) @app.cli.command('wow_transfer') @click.argument('address') @click.argument('amount') def wow_transfer(address, amount): from app.library.crypto import wow_wallet, wownero tx = wow_wallet.transfer(address, wownero.to_atomic(wownero.as_real(amount))) post_webhook(f'Sent {amount} WOW to `{address}` from console. Tx ID: `{tx["tx_hash"]}`') print(tx) @app.cli.command('xmr_transfer') @click.argument('address') @click.argument('amount') def xmr_transfer(address, amount): from app.library.crypto import xmr_wallet, monero tx = xmr_wallet.transfer(address, monero.to_atomic(monero.as_real(amount))) post_webhook(f'Sent {amount} XMR to `{address}` from console. Tx ID: `{tx["tx_hash"]}`') print(tx) @app.cli.command('process_expired') def process_expired(): from app.models import Swap _hours = config.SWAP_EXPIRATION_HOURS for swap in Swap.query.filter(Swap.funds_received == False): elapsed = swap.hours_elapsed() details = swap.show_details() if elapsed >= _hours: db.session.delete(swap) db.session.commit() print(f'Swap {swap.id} has not received funds in over {_hours} hours ({elapsed} hours) and was deleted: {details}') post_webhook(f'Deleted swap {swap.id}; {round(elapsed)} hours old with no incoming transfers.') sleep(5) @app.cli.command('process_incoming') def process_incoming(): from app.library.crypto import wow_wallet, xmr_wallet from app.models import Swap for swap in Swap.query.filter(Swap.funds_received == False): swap_tx_amounts = list() if swap.wow_to_xmr: incoming_wallet = wow_wallet else: incoming_wallet = xmr_wallet txes = incoming_wallet.incoming_transfers(swap.swap_address_index) if 'transfers' in txes: for tx in txes['transfers']: if tx['unlocked']: swap_tx_amounts.append(tx['amount']) amount_expected = swap.receive_amount_atomic amount_received = sum(swap_tx_amounts) if amount_received >= amount_expected: url = url_for('swap.get_swap', swap_id=swap.id, _external=True) msg = f'Funds received for swap [{swap.id}]({url}) ({swap.show_details()["receive"]})' swap.funds_received = True db.session.add(swap) db.session.commit() print(msg) post_webhook(msg) @app.cli.command('process_outgoing') def process_outgoing(): from datetime import datetime from app.library.crypto import wow_wallet, xmr_wallet, wownero, monero from app.models import Swap from time import sleep for swap in Swap.query.filter(Swap.funds_received == True, Swap.completed == False): print(f'Planning to send {swap.show_details()["send"]} for swap {swap.id}') sleep(10) if swap.wow_to_xmr: outgoing_wallet = xmr_wallet dest_address = swap.return_xmr_address crypto = monero else: outgoing_wallet = wow_wallet dest_address = swap.return_wow_address crypto = wownero available_funds = outgoing_wallet.balances()[0] if available_funds > swap.send_amount_atomic: res = outgoing_wallet.transfer(dest_address, swap.send_amount_atomic) if not 'message' in res: url = url_for('swap.get_swap', swap_id=swap.id, _external=True) res['address'] = dest_address msg = 'Sent {amount} {coin} to return address for swap [{id}]({url}): `{tx_id}` - `{res}`'.format( amount=crypto.from_atomic(swap.send_amount_atomic), coin=swap.send_coin().upper(), id=swap.id, tx_id=res['tx_hash'], url=url, res=res ) print(msg) post_webhook(msg) swap.completed = True swap.completed_date = datetime.utcnow() swap.send_tx_id = res['tx_hash'] swap.funds_sent = True db.session.add(swap) db.session.commit() else: print('There was an error sending funds for swap {id}: {msg}'.format( id=swap.id, msg=res['message'] )) if not isfile(f'/tmp/{swap.id}'): post_webhook(f'Unable to send funds for swap {swap.id}: {res["message"]}') with open(f'/tmp/{swap.id}', 'w') as f: f.write(swap.id) else: print('Not enough funds to transfer for swap {id}. Available: {avail}, Required: {req}'.format( id=swap.id, avail=crypto.from_atomic(available_funds), req=crypto.from_atomic(swap.send_amount_atomic) )) if not isfile(f'/tmp/{swap.id}'): post_webhook(f'Unable to send funds for swap {swap.id}: Not enough') with open(f'/tmp/{swap.id}', 'w') as f: f.write(swap.id) # Routes/blueprints from app.routes import meta, swap, api app.register_blueprint(meta.bp) app.register_blueprint(swap.bp) app.register_blueprint(api.bp) return app