From 5d1585260758a35142bbd66b0fcb05a25b1e7863 Mon Sep 17 00:00:00 2001 From: lza_menace Date: Fri, 11 Sep 2020 13:59:48 -0700 Subject: [PATCH] setting up transfers --- bin/{shell => cmd} | 4 +- wowstash/blueprints/wallet/routes.py | 81 ++++++++++++++++++++++-- wowstash/cli.py | 19 ++++-- wowstash/config.example.py | 2 +- wowstash/factory.py | 33 ++++++++-- wowstash/forms.py | 4 ++ wowstash/library/jsonrpc.py | 16 +++++ wowstash/models.py | 17 ++++- wowstash/static/css/main.css | 7 +- wowstash/templates/auth/login.html | 1 - wowstash/templates/wallet/dashboard.html | 40 +++++++++++- 11 files changed, 200 insertions(+), 24 deletions(-) rename bin/{shell => cmd} (64%) diff --git a/bin/shell b/bin/cmd similarity index 64% rename from bin/shell rename to bin/cmd index f0101c5..d7f7cf4 100755 --- a/bin/shell +++ b/bin/cmd @@ -1,7 +1,7 @@ #!/bin/bash source .venv/bin/activate -export FLASK_APP=wowstash/run.py +export FLASK_APP=wowstash/app.py export FLASK_SECRETS=config.py export FLASK_DEBUG=1 -flask eviscerate +flask $1 diff --git a/wowstash/blueprints/wallet/routes.py b/wowstash/blueprints/wallet/routes.py index cbf8c7c..7dd8cfc 100644 --- a/wowstash/blueprints/wallet/routes.py +++ b/wowstash/blueprints/wallet/routes.py @@ -1,38 +1,107 @@ from io import BytesIO from base64 import b64encode from qrcode import make as qrcode_make -from flask import request, render_template, session, redirect, url_for, current_app +from decimal import Decimal +from flask import request, render_template, session, redirect, url_for, current_app, flash from flask_login import login_required, current_user from wowstash.blueprints.wallet import wallet_bp from wowstash.library.jsonrpc import wallet, daemon -from wowstash.factory import login_manager -from wowstash.models import User +from wowstash.forms import Send +from wowstash.factory import login_manager, db +from wowstash.models import User, Transaction @wallet_bp.route("/wallet/dashboard") @login_required def dashboard(): all_transfers = list() + send_form = Send() _address_qr = BytesIO() user = User.query.get(current_user.id) wallet_height = wallet.height()['height'] - daemon_height = daemon.height()['height'] subaddress = wallet.get_address(0, user.subaddress_index) balances = wallet.get_balance(0, user.subaddress_index) transfers = wallet.get_transfers(0, user.subaddress_index) + txs_queued = Transaction.query.filter_by(from_user=user.id) for type in transfers: for tx in transfers[type]: all_transfers.append(tx) + # data = {'account_index': account_index, 'address_indices': [subaddress_index]} + # _balance = self.make_rpc('get_balance', data) + # locked = from_atomic(_balance['per_subaddress'][0]['balance']) + # unlocked = from_atomic(_balance['per_subaddress'][0]['unlocked_balance']) + # return (float(locked), float(unlocked)) + from wowstash.library.jsonrpc import from_atomic + from pprint import pprint + bal = wallet.make_rpc('get_balance', {'account_index': 0, 'address_indices': [user.subaddress_index]}) + # print(from_atomic(bal)) + pprint(bal) + # pprint(transfers) + # pprint(balances) + qr_uri = f'wownero:{subaddress}?tx_description="{current_user.email}"' address_qr = qrcode_make(qr_uri).save(_address_qr) qrcode = b64encode(_address_qr.getvalue()).decode() return render_template( "wallet/dashboard.html", wallet_height=wallet_height, - daemon_height=daemon_height, subaddress=subaddress, balances=balances, all_transfers=all_transfers, - qrcode=qrcode + qrcode=qrcode, + send_form=send_form, + txs_queued=txs_queued ) + +@wallet_bp.route("/wallet/send", methods=["GET", "POST"]) +@login_required +def send(): + send_form = Send() + redirect_url = url_for('wallet.dashboard') + "#send" + if send_form.validate_on_submit(): + address = str(send_form.address.data) + + # Check if Wownero wallet is available + if wallet.connected is False: + flash('Wallet RPC interface is unavailable at this time. Try again later.') + return redirect(redirect_url) + + # Check if user funds flag is locked + if current_user.funds_locked: + flash('You currently have transactions pending and transfers are locked. Try again later.') + return redirect(redirect_url) + + # Quick n dirty check to see if address is WOW + if len(address) not in [97, 108]: + flash('Invalid Wownero address provided.') + return redirect(redirect_url) + + # Make sure the amount provided is a number + try: + amount = Decimal(send_form.amount.data) + except: + flash('Invalid Wownero amount specified.') + return redirect(redirect_url) + + # Lock user funds + user = User.query.get(current_user.id) + user.funds_locked = True + db.session.commit() + + # Queue the transaction + tx = Transaction( + from_user=user.id, + address=address, + amount=amount, + ) + db.session.add(tx) + db.session.commit() + + # Redirect back + flash('Successfully queued transfer.') + return redirect(redirect_url) + else: + for field, errors in send_form.errors.items(): + flash(f'{send_form[field].label}: {", ".join(errors)}') + return redirect(redirect_url) diff --git a/wowstash/cli.py b/wowstash/cli.py index 4e3e8f2..7def856 100644 --- a/wowstash/cli.py +++ b/wowstash/cli.py @@ -1,12 +1,23 @@ +from wowstash.library.jsonrpc import wallet +from wowstash.models import Transaction +from wowstash.factory import db -@app.errorhandler(404) +# @app.errorhandler(404) def not_found(error): return make_response(jsonify({ 'error': 'Page not found' }), 404) -@app.cli.command('initdb') -def initdb(): - from wowstash.models import * +# @app.cli.command('initdb') +def init_db(): db.create_all() + +# @app.cli.command('send_transfers') +def send_transfers(): + txes = Transaction.query.all() + for i in txes: + print(i) + # tx = wallet.transfer( + # 0, current_user.subaddress_index, address, amount + # ) diff --git a/wowstash/config.example.py b/wowstash/config.example.py index 63385b4..0198ff8 100644 --- a/wowstash/config.example.py +++ b/wowstash/config.example.py @@ -20,7 +20,7 @@ PASSWORD_SALT = 'salt here' # database salts SECRET_KEY = 'secret session key here' # encrypts the session token # Session -PERMANENT_SESSION_LIFETIME = 1800 # 30 minute session expiry +PERMANENT_SESSION_LIFETIME = 1800 # 60 minute session expiry SESSION_TYPE = 'redis' SESSION_COOKIE_NAME = 'wowstash' SESSION_COOKIE_SECURE = False diff --git a/wowstash/factory.py b/wowstash/factory.py index 9de6276..8508a09 100644 --- a/wowstash/factory.py +++ b/wowstash/factory.py @@ -1,5 +1,6 @@ from flask import Flask from flask_sqlalchemy import SQLAlchemy +from flask_wtf.csrf import CSRFProtect from flask_session import Session from flask_bcrypt import Bcrypt from flask_login import LoginManager @@ -14,10 +15,11 @@ bcrypt = None def _setup_db(app: Flask): global db - uri = 'postgresql+psycopg2://{user}:{pw}@{url}/{db}'.format( + uri = 'postgresql+psycopg2://{user}:{pw}@{host}:{port}/{db}'.format( user=config.DB_USER, pw=config.DB_PASS, - url=config.DB_HOST, + host=config.DB_HOST, + port=config.DB_PORT, db=config.DB_NAME ) app.config['SQLALCHEMY_DATABASE_URI'] = uri @@ -27,8 +29,6 @@ def _setup_db(app: Flask): db.create_all() def _setup_session(app: Flask): - app.config['SESSION_TYPE'] = 'redis' - app.config['SESSION_COOKIE_NAME'] = app.config.get('SESSION_COOKIE_NAME', 'wowstash') app.config['SESSION_REDIS'] = Redis( host=app.config['REDIS_HOST'], port=app.config['REDIS_PORT'] @@ -52,6 +52,7 @@ def create_app(): _setup_db(app) _setup_session(app) _setup_bcrypt(app) + CSRFProtect(app) login_manager = LoginManager() login_manager.init_app(app) @@ -60,7 +61,8 @@ def create_app(): @login_manager.user_loader def load_user(user_id): from wowstash.models import User - return User.query.get(user_id) + user = User.query.get(user_id) + return user # template filters @app.template_filter('datestamp') @@ -68,6 +70,27 @@ def create_app(): d = datetime.fromtimestamp(s) return d.strftime('%Y-%m-%d %H:%M:%S') + # commands + @app.cli.command('send_transfers') + def send_transfers(): + from wowstash.models import Transaction, User + from wowstash.library.jsonrpc import wallet + from decimal import Decimal + + txes = Transaction.query.filter_by(sent=False) + for tx in txes: + user = User.query.get(tx.from_user) + new_tx = wallet.transfer( + 0, user.subaddress_index, tx.address, Decimal(tx.amount) + ) + if 'message' in new_tx: + print(f'Failed to send tx {tx.id}. Reason: {new_tx["message"]}') + else: + tx.sent = True + user.funds_locked = False + db.session.commit() + print(f'Successfully sent tx! {new_tx}') + # Routes from wowstash.blueprints.auth import auth_bp from wowstash.blueprints.wallet import wallet_bp diff --git a/wowstash/forms.py b/wowstash/forms.py index 5591855..9b14cfe 100644 --- a/wowstash/forms.py +++ b/wowstash/forms.py @@ -13,3 +13,7 @@ class Register(FlaskForm): class Login(FlaskForm): email = StringField('Email Address:', validators=[DataRequired()], render_kw={"placeholder": "Email", "class": "form-control", "type": "email"}) password = StringField('Password:', validators=[DataRequired()], render_kw={"placeholder": "Password", "class": "form-control", "type": "password"}) + +class Send(FlaskForm): + address = StringField('Destination Address:', validators=[DataRequired()], render_kw={"placeholder": "Wownero address", "class": "form-control"}) + amount = StringField('Amount:', validators=[DataRequired()], render_kw={"placeholder": "Amount to send", "class": "form-control"}) diff --git a/wowstash/library/jsonrpc.py b/wowstash/library/jsonrpc.py index 2fc0957..814a356 100644 --- a/wowstash/library/jsonrpc.py +++ b/wowstash/library/jsonrpc.py @@ -78,6 +78,22 @@ class Wallet(JSONRPC): } return self.make_rpc('get_transfers', data) + def transfer(self, account_index, subaddress_index, dest_address, amount): + data = { + 'account_index': account_index, + 'subaddr_indices': [subaddress_index], + 'destinations': [{'address': dest_address, 'amount': to_atomic(amount)}], + 'priority': 0, + 'unlock_time': 0, + 'get_tx_key': True, + 'get_tx_hex': True, + 'new_algorithm': True, + 'do_not_relay': False, + 'ring_size': 22 + } + transfer = self.make_rpc('transfer', data) + return transfer + class Daemon(JSONRPC): def __init__(self, **kwargs): diff --git a/wowstash/models.py b/wowstash/models.py index 9d16c03..d7ed12b 100644 --- a/wowstash/models.py +++ b/wowstash/models.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, DateTime, String +from sqlalchemy import Column, Integer, DateTime, String, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.sql import func from wowstash.factory import db @@ -14,6 +14,7 @@ class User(db.Model): email = db.Column(db.String(50), unique=True, index=True) subaddress_index = db.Column(db.Integer) registered_on = db.Column(db.DateTime, server_default=func.now()) + funds_locked = db.Column(db.Boolean, default=False) @property def is_authenticated(self): @@ -36,3 +37,17 @@ class User(db.Model): def __repr__(self): return self.username + + +class Transaction(db.Model): + __tablename__ = 'transactions' + + id = db.Column('tx_id', db.Integer, primary_key=True) + from_user = db.Column(db.Integer, ForeignKey(User.id)) + sent = db.Column(db.Boolean, default=False) + address = db.Column(db.String(120)) + amount = db.Column(db.String(120)) + date = db.Column(db.DateTime, server_default=func.now()) + + def __repr__(self): + return self.id diff --git a/wowstash/static/css/main.css b/wowstash/static/css/main.css index 2b83023..05f4551 100644 --- a/wowstash/static/css/main.css +++ b/wowstash/static/css/main.css @@ -525,10 +525,15 @@ header.masthead .header-content-lg { .dashboard-buttons { width: 100%; - margin: 2em auto; + margin: 3em auto; display: block; } .dashboard-button { display: inline; } + +.send-form { + width: 60%; + margin: 0 auto; +} diff --git a/wowstash/templates/auth/login.html b/wowstash/templates/auth/login.html index 6be7d5c..b6d25a0 100644 --- a/wowstash/templates/auth/login.html +++ b/wowstash/templates/auth/login.html @@ -31,7 +31,6 @@

Click here if you need to register.

- diff --git a/wowstash/templates/wallet/dashboard.html b/wowstash/templates/wallet/dashboard.html index 5db37c2..343a76c 100644 --- a/wowstash/templates/wallet/dashboard.html +++ b/wowstash/templates/wallet/dashboard.html @@ -21,10 +21,10 @@

({{ balances.0 }} locked)

@@ -50,7 +50,7 @@ {{ tx.timestamp | datestamp }} {{ tx.type }} {{ tx.txid | truncate(12) }} - {{ tx.amount / 100000000000 }} + {% if tx.type == 'out' %}{{ tx.destinations.0.amount / 100000000000 }}{% else %}{{ tx.amount / 100000000000 }}{% endif %} WOW {{ tx.confirmations }} {{ tx.height }} {{ tx.fee / 100000000000 }} WOW @@ -61,6 +61,40 @@ +
+
+
+

Send

+ {% if current_user.funds_locked %} +

Sending funds is currently locked due to a transfer already in progress. Please try again in a few minutes. Pending transfers:

+
    + {% for tx in txs_queued %} +
  • {{ tx.amount }} - {{ tx.address }}
  • + {% endfor %} +
+ {% else %} +
+ {{ send_form.csrf_token }} + {% for f in send_form %} + {% if f.name != 'csrf_token' %} +
+ {{ f.label }} + {{ f }} +
+ {% endif %} + {% endfor %} +
    + {% for field, errors in send_form.errors.items() %} +
  • {{ send_form[field].label }}: {{ ', '.join(errors) }}
  • + {% endfor %} +
+ +
+ {% endif %} +
+
+
+ {% include 'footer.html' %} {% include 'scripts.html' %}