diff --git a/README.md b/README.md index 9cb48e5..215bf43 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,22 @@ # wowstash A web wallet for noobs who can't use a CLI. + +## Notes + + +``` +wownero-wallet-cli --generate-from-json e.json --restore-height 247969 --daemon-address node.suchwow.xyz:34568 --command version + +wownero-wallet-rpc --non-interactive --rpc-bind-port 8888 --daemon-address node.suchwow.xyz:34568 --wallet-file wer --rpc-login user1:mypass1 --password pass +``` + +``` +{ + "version": 1, + "filename": "", + "scan_from_height": , + "password": "", + "seed": "" +} +``` diff --git a/docker-compose.yaml b/docker-compose.yaml index f6fab16..4470664 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,7 +4,7 @@ services: image: postgres:9.6.15-alpine container_name: db ports: - - 5432:5432 + - 5433:5432 environment: POSTGRES_PASSWORD: ${DB_PASS} POSTGRES_USER: ${DB_USER} diff --git a/requirements.txt b/requirements.txt index d7c35a4..e5ccc72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ flask-bcrypt flask-login qrcode Pillow +git+https://github.com/lalanza808/MoneroPy diff --git a/wowstash/blueprints/auth/routes.py b/wowstash/blueprints/auth/routes.py index 58357cb..47ce054 100644 --- a/wowstash/blueprints/auth/routes.py +++ b/wowstash/blueprints/auth/routes.py @@ -1,9 +1,10 @@ +from os import kill from flask import request, render_template, session, redirect, url_for, flash from flask_login import login_user, logout_user, current_user +from secrets import token_urlsafe from wowstash.blueprints.auth import auth_bp from wowstash.forms import Register, Login from wowstash.models import User -from wowstash.library.jsonrpc import wallet from wowstash.factory import db, bcrypt @@ -15,25 +16,17 @@ def register(): return redirect(url_for('wallet.dashboard')) if form.validate_on_submit(): - # 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(url_for('auth.register')) - # Check if email already exists user = User.query.filter_by(email=form.email.data).first() if user: flash('This email is already registered.') return redirect(url_for('auth.login')) - # Create new subaddress - subaddress = wallet.new_address(label=form.email.data) - # Save new user user = User( email=form.email.data, password=bcrypt.generate_password_hash(form.password.data).decode('utf8'), - subaddress_index=subaddress[0] + wallet_password=token_urlsafe(16), ) db.session.add(user) db.session.commit() @@ -75,5 +68,8 @@ def login(): @auth_bp.route("/logout") def logout(): + if current_user.is_authenticated: + current_user.kill_wallet() + current_user.clear_wallet_data() logout_user() return redirect(url_for('meta.index')) diff --git a/wowstash/blueprints/meta/routes.py b/wowstash/blueprints/meta/routes.py index 349c122..91cb98c 100644 --- a/wowstash/blueprints/meta/routes.py +++ b/wowstash/blueprints/meta/routes.py @@ -1,13 +1,13 @@ from flask import request, render_template, session, redirect, url_for, make_response, jsonify from wowstash.blueprints.meta import meta_bp from wowstash.library.jsonrpc import daemon -from wowstash.library.info import info +from wowstash.library.cache import cache from wowstash.library.db import Database @meta_bp.route('/') def index(): - return render_template('meta/index.html', node=daemon.info(), info=info.get_info()) + return render_template('meta/index.html', node=daemon.info(), info=cache.get_coin_info()) @meta_bp.route('/faq') def faq(): @@ -21,7 +21,6 @@ def terms(): def privacy(): return render_template('meta/privacy.html') - @meta_bp.route('/health') def health(): return make_response(jsonify({ diff --git a/wowstash/blueprints/wallet/routes.py b/wowstash/blueprints/wallet/routes.py index 5bb0f48..aa45631 100644 --- a/wowstash/blueprints/wallet/routes.py +++ b/wowstash/blueprints/wallet/routes.py @@ -1,53 +1,115 @@ +import subprocess from io import BytesIO from base64 import b64encode from qrcode import make as qrcode_make from decimal import Decimal -from flask import request, render_template, session, redirect, url_for, current_app, flash +from flask import request, render_template, session, jsonify +from flask import redirect, url_for, current_app, flash from flask_login import login_required, current_user +from socket import socket +from datetime import datetime from wowstash.blueprints.wallet import wallet_bp -from wowstash.library.jsonrpc import wallet, daemon +from wowstash.library.jsonrpc import Wallet, to_atomic +from wowstash.library.cache import cache from wowstash.forms import Send -from wowstash.factory import login_manager, db -from wowstash.models import User, Transaction +from wowstash.factory import db +from wowstash.models import User +from wowstash import config -@wallet_bp.route("/wallet/dashboard") +@wallet_bp.route('/wallet/loading') +@login_required +def loading(): + if current_user.wallet_connected and current_user.wallet_created: + return redirect(url_for('wallet.dashboard')) + return render_template('wallet/loading.html') + +@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'] - 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) + all_transfers = list() + wallet = Wallet( + proto='http', + host='127.0.0.1', + port=current_user.wallet_port, + username=current_user.id, + password=current_user.wallet_password + ) + if not wallet.connected: + return redirect(url_for('wallet.loading')) + + address = wallet.get_address() + transfers = wallet.get_transfers() for type in transfers: for tx in transfers[type]: all_transfers.append(tx) - - qr_uri = f'wownero:{subaddress}?tx_description="{current_user.email}"' + balances = wallet.get_balances() + qr_uri = f'wownero:{address}?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, - subaddress=subaddress, + 'wallet/dashboard.html', + transfers=all_transfers, balances=balances, - all_transfers=all_transfers, + address=address, qrcode=qrcode, send_form=send_form, - txs_queued=txs_queued + user=current_user ) -@wallet_bp.route("/wallet/send", methods=["GET", "POST"]) +@wallet_bp.route('/wallet/connect') +@login_required +def connect(): + if current_user.wallet_connected is False: + tcp = socket() + tcp.bind(('', 0)) + _, port = tcp.getsockname() + tcp.close() + command = f"""wownero-wallet-rpc \ + --detach \ + --non-interactive \ + --rpc-bind-port {port} \ + --wallet-file {config.WALLET_DIR}/{current_user.id}.wallet \ + --rpc-login {current_user.id}:{current_user.wallet_password} \ + --password {current_user.wallet_password} \ + --daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \ + --daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \ + --log-file {config.WALLET_DIR}/{current_user.id}.log + """ + proc = subprocess.Popen(command.split(), stdout=subprocess.PIPE) + outs, errs = proc.communicate() + # print(outs) + if proc.returncode == 0: + print(f'Successfully started RPC for {current_user}!') + current_user.wallet_connected = True + current_user.wallet_port = port + current_user.wallet_pid = proc.pid + current_user.wallet_connect_date = datetime.now() + db.session.commit() + + return "ok" + +@wallet_bp.route('/wallet/status') +@login_required +def status(): + data = { + "created": current_user.wallet_created, + "connected": current_user.wallet_connected, + "port": current_user.wallet_port, + "date": current_user.wallet_connect_date + } + return jsonify(data) + +@wallet_bp.route('/wallet/send', methods=['GET', 'POST']) @login_required def send(): send_form = Send() - redirect_url = url_for('wallet.dashboard') + "#send" + redirect_url = url_for('wallet.dashboard') + '#send' if send_form.validate_on_submit(): address = str(send_form.address.data) + user = User.query.get(current_user.id) # Check if Wownero wallet is available if wallet.connected is False: @@ -66,21 +128,25 @@ def send(): # Make sure the amount provided is a number try: - amount = Decimal(send_form.amount.data) + amount = to_atomic(Decimal(send_form.amount.data)) except: flash('Invalid Wownero amount specified.') return redirect(redirect_url) + # Make sure the amount does not exceed the balance + if amount >= user.balance: + flash('Not enough funds to transfer that much.') + 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, + tx = TransferQueue( + user=user.id, address=address, - amount=amount, + amount=amount ) db.session.add(tx) db.session.commit() diff --git a/wowstash/config.example.py b/wowstash/config.example.py index 0198ff8..d873118 100644 --- a/wowstash/config.example.py +++ b/wowstash/config.example.py @@ -9,11 +9,7 @@ DAEMON_USER = '' DAEMON_PASS = '' # Wallet -WALLET_PROTO = 'http' -WALLET_HOST = 'localhost' -WALLET_PORT = 9999 -WALLET_USER = 'yyyyy' -WALLET_PASS = 'xxxxx' +WALLET_DIR = './data/wallets' # Security PASSWORD_SALT = 'salt here' # database salts diff --git a/wowstash/factory.py b/wowstash/factory.py index 8508a09..ff79fae 100644 --- a/wowstash/factory.py +++ b/wowstash/factory.py @@ -70,26 +70,85 @@ def create_app(): d = datetime.fromtimestamp(s) return d.strftime('%Y-%m-%d %H:%M:%S') + @app.template_filter('from_atomic') + def from_atomic(a): + from wowstash.library.jsonrpc import from_atomic + return from_atomic(a) + # 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 + @app.cli.command('create_wallets') + def create_wallets(): + import subprocess + from os import makedirs, path + from moneropy import account + from wowstash import config + from wowstash.factory import db + from wowstash.models import User + from wowstash.library.jsonrpc import daemon + + if not path.isdir(config.WALLET_DIR): + makedirs(config.WALLET_DIR) + + wallets_to_create = User.query.filter_by(wallet_created=False) + if wallets_to_create: + for u in wallets_to_create: + print(f'Creating wallet for user {u}') + seed, sk, vk, addr = account.gen_new_wallet() + command = f"""wownero-wallet-cli \ + --generate-new-wallet {config.WALLET_DIR}/{u.id}.wallet \ + --restore-height {daemon.info()['height']} \ + --password {u.wallet_password} \ + --mnemonic-language English \ + --daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \ + --daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \ + --command version + """ + proc = subprocess.Popen(command.split(), stdout=subprocess.PIPE) + proc.communicate() + if proc.returncode == 0: + print(f'Successfully created wallet for {u}!') + u.wallet_created = True + db.session.commit() + else: + print(f'Failed to create wallet for {u}.') + + @app.cli.command('refresh_wallets') + def refresh_wallets(): + import subprocess + from os import kill + from moneropy import account + from wowstash import config + from wowstash.factory import db + from wowstash.models import User + from wowstash.library.jsonrpc import daemon + + users = User.query.all() + for u in users: + print(f'Refreshing wallet for {u}') + + if u.wallet_pid is None: + break + + # first check if the pid is still there + try: + kill(u.wallet_pid, 0) + except OSError: + print('pid does not exist') + u.wallet_connected = False + u.wallet_pid = None + u.wallet_connect_date = None + u.wallet_port = None + db.session.commit() + + # then check if the user session is still active + if u.is_active is False: + print('user session inactive') + kill(u.wallet_pid, 9) + u.wallet_connected = False + u.wallet_pid = None + u.wallet_connect_date = None + u.wallet_port = None db.session.commit() - print(f'Successfully sent tx! {new_tx}') # Routes from wowstash.blueprints.auth import auth_bp diff --git a/wowstash/library/info.py b/wowstash/library/cache.py similarity index 80% rename from wowstash/library/info.py rename to wowstash/library/cache.py index d0e6085..8e457af 100644 --- a/wowstash/library/info.py +++ b/wowstash/library/cache.py @@ -6,19 +6,19 @@ from redis import Redis from wowstash import config -class CoinInfo(object): +class Cache(object): def __init__(self): self.redis = Redis(host=config.REDIS_HOST, port=config.REDIS_PORT) - def store_info(self, info): + def store_data(self, item_name, expiration_minutes, data): self.redis.setex( - "info", - timedelta(minutes=15), - value=info + item_name, + timedelta(minutes=expiration_minutes), + value=data ) - def get_info(self): - info = self.redis.get("info") + def get_coin_info(self): + info = self.redis.get("coin_info") if info: return json_loads(info) else: @@ -42,7 +42,7 @@ class CoinInfo(object): 'total_volume': r.json()['market_data']['total_volume']['usd'], 'last_updated': r.json()['last_updated'] } - self.store_info(json_dumps(info)) + self.store_data("coin_info", 15, json_dumps(info)) return info -info = CoinInfo() +cache = Cache() diff --git a/wowstash/library/jsonrpc.py b/wowstash/library/jsonrpc.py index 814a356..72a475f 100644 --- a/wowstash/library/jsonrpc.py +++ b/wowstash/library/jsonrpc.py @@ -54,22 +54,19 @@ class Wallet(JSONRPC): _address = self.make_rpc('create_address', data) return (_address['address_index'], _address['address']) - def get_address(self, account_index, subaddress_index): - data = {'account_index': account_index, 'address_index': [subaddress_index]} - subaddress = self.make_rpc('get_address', data)['addresses'][0]['address'] - return subaddress - - def get_balance(self, account_index, subaddress_index): - 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)) - - def get_transfers(self, account_index, subaddress_index): + def get_address(self, account_index=0): + data = {'account_index': account_index} + address = self.make_rpc('get_address', data)['address'] + return address + + def get_balances(self, account_index=0): + data = {'account_index': account_index} + balance = self.make_rpc('get_balance', data) + return (balance['balance'], balance['unlocked_balance']) + + def get_transfers(self, account_index=0): data = { 'account_index': account_index, - 'subaddr_indices': [subaddress_index], 'in': True, 'out': True, 'pending': True, @@ -126,11 +123,3 @@ daemon = Daemon( username=config.DAEMON_USER, password=config.DAEMON_PASS ) - -wallet = Wallet( - proto=config.WALLET_PROTO, - host=config.WALLET_HOST, - port=config.WALLET_PORT, - username=config.WALLET_USER, - password=config.WALLET_PASS -) diff --git a/wowstash/models.py b/wowstash/models.py index d7ed12b..32e91d4 100644 --- a/wowstash/models.py +++ b/wowstash/models.py @@ -1,3 +1,4 @@ +from os import kill from sqlalchemy import Column, Integer, DateTime, String, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.sql import func @@ -9,12 +10,16 @@ Base = declarative_base() class User(db.Model): __tablename__ = 'users' - id = db.Column('user_id', db.Integer, primary_key=True) - password = db.Column(db.String(120)) + id = db.Column(db.Integer, primary_key=True) 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) + password = db.Column(db.String(120)) + register_date = db.Column(db.DateTime, server_default=func.now()) + wallet_password = db.Column(db.String(120)) + wallet_created = db.Column(db.Boolean, default=False) + wallet_connected = db.Column(db.Boolean, default=False) + wallet_port = db.Column(db.Integer, nullable=True) + wallet_pid = db.Column(db.Integer, nullable=True) + wallet_connect_date = db.Column(db.DateTime, nullable=True) @property def is_authenticated(self): @@ -35,19 +40,18 @@ class User(db.Model): def get_id(self): return self.id - def __repr__(self): - return self.username - - -class Transaction(db.Model): - __tablename__ = 'transactions' + def kill_wallet(self): + try: + kill(self.wallet_pid, 9) + except Exception as e: + print('could kill:', e) - 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 clear_wallet_data(self): + self.wallet_connected = False + self.wallet_port = None + self.wallet_pid = None + self.wallet_connect_date = None + db.session.commit() def __repr__(self): - return self.id + return self.email diff --git a/wowstash/static/img/loading-cat.gif b/wowstash/static/img/loading-cat.gif new file mode 100644 index 0000000..111c44f Binary files /dev/null and b/wowstash/static/img/loading-cat.gif differ diff --git a/wowstash/templates/wallet/dashboard.html b/wowstash/templates/wallet/dashboard.html index 343a76c..dc7aa39 100644 --- a/wowstash/templates/wallet/dashboard.html +++ b/wowstash/templates/wallet/dashboard.html @@ -12,13 +12,12 @@

Wallet Info

Address:

-

{{ subaddress }}

+

{{ address }}




Balance

-

{{ balances.1 }} WOW

-

({{ balances.0 }} locked)

+

{{ balances[1] | from_atomic }} WOW ({{ balances[0] | from_atomic }} locked)

List @@ -45,17 +44,19 @@ Height Fee - {% for tx in all_transfers | sort(attribute='timestamp', reverse=True) %} + {% if transfers %} + {% for tx in transfers | sort(attribute='timestamp', reverse=True) %} {% if tx.type == 'pool' %}{% else %}{% endif %} {{ tx.timestamp | datestamp }} {{ tx.type }} {{ tx.txid | truncate(12) }} - {% if tx.type == 'out' %}{{ tx.destinations.0.amount / 100000000000 }}{% else %}{{ tx.amount / 100000000000 }}{% endif %} WOW + {{ tx.amount | from_atomic }} WOW {{ tx.confirmations }} {{ tx.height }} - {{ tx.fee / 100000000000 }} WOW + {{ tx.fee | from_atomic }} WOW {% endfor %} + {% endif %}
@@ -69,7 +70,7 @@

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 }}
  • +
  • {{ tx.amount | from_atomic }} - {{ tx.address }}
  • {% endfor %}
{% else %} diff --git a/wowstash/templates/wallet/loading.html b/wowstash/templates/wallet/loading.html new file mode 100644 index 0000000..2e85a2e --- /dev/null +++ b/wowstash/templates/wallet/loading.html @@ -0,0 +1,67 @@ + + + + {% include 'head.html' %} + + + + {% include 'navbar.html' %} + +
+
+
+ {% if current_user.wallet_created == False %} +

Your wallet is being created

+ {% else %} +

Your wallet is connecting

+ {% endif %} +

Go smoke a fatty. This page should auto-refresh when it's ready...if not, click the button below

+ + + + +
+
+
+ + + + {% include 'footer.html' %} + + {% include 'scripts.html' %} + + + +