major refactor - start shelling out as a prototype to process per user

mm-logging
lza_menace 4 years ago
parent 549193f0be
commit 8324dc3444

@ -1,3 +1,22 @@
# wowstash # wowstash
A web wallet for noobs who can't use a CLI. 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": "<user specific name>",
"scan_from_height": <height>,
"password": "<password>",
"seed": "<seed phrase>"
}
```

@ -4,7 +4,7 @@ services:
image: postgres:9.6.15-alpine image: postgres:9.6.15-alpine
container_name: db container_name: db
ports: ports:
- 5432:5432 - 5433:5432
environment: environment:
POSTGRES_PASSWORD: ${DB_PASS} POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_USER: ${DB_USER} POSTGRES_USER: ${DB_USER}

@ -9,3 +9,4 @@ flask-bcrypt
flask-login flask-login
qrcode qrcode
Pillow Pillow
git+https://github.com/lalanza808/MoneroPy

@ -1,9 +1,10 @@
from os import kill
from flask import request, render_template, session, redirect, url_for, flash from flask import request, render_template, session, redirect, url_for, flash
from flask_login import login_user, logout_user, current_user from flask_login import login_user, logout_user, current_user
from secrets import token_urlsafe
from wowstash.blueprints.auth import auth_bp from wowstash.blueprints.auth import auth_bp
from wowstash.forms import Register, Login from wowstash.forms import Register, Login
from wowstash.models import User from wowstash.models import User
from wowstash.library.jsonrpc import wallet
from wowstash.factory import db, bcrypt from wowstash.factory import db, bcrypt
@ -15,25 +16,17 @@ def register():
return redirect(url_for('wallet.dashboard')) return redirect(url_for('wallet.dashboard'))
if form.validate_on_submit(): 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 # Check if email already exists
user = User.query.filter_by(email=form.email.data).first() user = User.query.filter_by(email=form.email.data).first()
if user: if user:
flash('This email is already registered.') flash('This email is already registered.')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
# Create new subaddress
subaddress = wallet.new_address(label=form.email.data)
# Save new user # Save new user
user = User( user = User(
email=form.email.data, email=form.email.data,
password=bcrypt.generate_password_hash(form.password.data).decode('utf8'), 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.add(user)
db.session.commit() db.session.commit()
@ -75,5 +68,8 @@ def login():
@auth_bp.route("/logout") @auth_bp.route("/logout")
def logout(): def logout():
if current_user.is_authenticated:
current_user.kill_wallet()
current_user.clear_wallet_data()
logout_user() logout_user()
return redirect(url_for('meta.index')) return redirect(url_for('meta.index'))

@ -1,13 +1,13 @@
from flask import request, render_template, session, redirect, url_for, make_response, jsonify from flask import request, render_template, session, redirect, url_for, make_response, jsonify
from wowstash.blueprints.meta import meta_bp from wowstash.blueprints.meta import meta_bp
from wowstash.library.jsonrpc import daemon from wowstash.library.jsonrpc import daemon
from wowstash.library.info import info from wowstash.library.cache import cache
from wowstash.library.db import Database from wowstash.library.db import Database
@meta_bp.route('/') @meta_bp.route('/')
def index(): 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') @meta_bp.route('/faq')
def faq(): def faq():
@ -21,7 +21,6 @@ def terms():
def privacy(): def privacy():
return render_template('meta/privacy.html') return render_template('meta/privacy.html')
@meta_bp.route('/health') @meta_bp.route('/health')
def health(): def health():
return make_response(jsonify({ return make_response(jsonify({

@ -1,53 +1,115 @@
import subprocess
from io import BytesIO from io import BytesIO
from base64 import b64encode from base64 import b64encode
from qrcode import make as qrcode_make from qrcode import make as qrcode_make
from decimal import Decimal 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 flask_login import login_required, current_user
from socket import socket
from datetime import datetime
from wowstash.blueprints.wallet import wallet_bp 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.forms import Send
from wowstash.factory import login_manager, db from wowstash.factory import db
from wowstash.models import User, Transaction 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 @login_required
def dashboard(): def dashboard():
all_transfers = list()
send_form = Send() send_form = Send()
_address_qr = BytesIO() _address_qr = BytesIO()
user = User.query.get(current_user.id) all_transfers = list()
wallet_height = wallet.height()['height'] wallet = Wallet(
subaddress = wallet.get_address(0, user.subaddress_index) proto='http',
balances = wallet.get_balance(0, user.subaddress_index) host='127.0.0.1',
transfers = wallet.get_transfers(0, user.subaddress_index) port=current_user.wallet_port,
txs_queued = Transaction.query.filter_by(from_user=user.id) 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 type in transfers:
for tx in transfers[type]: for tx in transfers[type]:
all_transfers.append(tx) all_transfers.append(tx)
balances = wallet.get_balances()
qr_uri = f'wownero:{subaddress}?tx_description="{current_user.email}"' qr_uri = f'wownero:{address}?tx_description="{current_user.email}"'
address_qr = qrcode_make(qr_uri).save(_address_qr) address_qr = qrcode_make(qr_uri).save(_address_qr)
qrcode = b64encode(_address_qr.getvalue()).decode() qrcode = b64encode(_address_qr.getvalue()).decode()
return render_template( return render_template(
"wallet/dashboard.html", 'wallet/dashboard.html',
wallet_height=wallet_height, transfers=all_transfers,
subaddress=subaddress,
balances=balances, balances=balances,
all_transfers=all_transfers, address=address,
qrcode=qrcode, qrcode=qrcode,
send_form=send_form, 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 @login_required
def send(): def send():
send_form = Send() send_form = Send()
redirect_url = url_for('wallet.dashboard') + "#send" redirect_url = url_for('wallet.dashboard') + '#send'
if send_form.validate_on_submit(): if send_form.validate_on_submit():
address = str(send_form.address.data) address = str(send_form.address.data)
user = User.query.get(current_user.id)
# Check if Wownero wallet is available # Check if Wownero wallet is available
if wallet.connected is False: if wallet.connected is False:
@ -66,21 +128,25 @@ def send():
# Make sure the amount provided is a number # Make sure the amount provided is a number
try: try:
amount = Decimal(send_form.amount.data) amount = to_atomic(Decimal(send_form.amount.data))
except: except:
flash('Invalid Wownero amount specified.') flash('Invalid Wownero amount specified.')
return redirect(redirect_url) 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 # Lock user funds
user = User.query.get(current_user.id)
user.funds_locked = True user.funds_locked = True
db.session.commit() db.session.commit()
# Queue the transaction # Queue the transaction
tx = Transaction( tx = TransferQueue(
from_user=user.id, user=user.id,
address=address, address=address,
amount=amount, amount=amount
) )
db.session.add(tx) db.session.add(tx)
db.session.commit() db.session.commit()

@ -9,11 +9,7 @@ DAEMON_USER = ''
DAEMON_PASS = '' DAEMON_PASS = ''
# Wallet # Wallet
WALLET_PROTO = 'http' WALLET_DIR = './data/wallets'
WALLET_HOST = 'localhost'
WALLET_PORT = 9999
WALLET_USER = 'yyyyy'
WALLET_PASS = 'xxxxx'
# Security # Security
PASSWORD_SALT = 'salt here' # database salts PASSWORD_SALT = 'salt here' # database salts

@ -70,26 +70,85 @@ def create_app():
d = datetime.fromtimestamp(s) d = datetime.fromtimestamp(s)
return d.strftime('%Y-%m-%d %H:%M:%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 # commands
@app.cli.command('send_transfers') @app.cli.command('create_wallets')
def send_transfers(): def create_wallets():
from wowstash.models import Transaction, User import subprocess
from wowstash.library.jsonrpc import wallet from os import makedirs, path
from decimal import Decimal from moneropy import account
from wowstash import config
txes = Transaction.query.filter_by(sent=False) from wowstash.factory import db
for tx in txes: from wowstash.models import User
user = User.query.get(tx.from_user) from wowstash.library.jsonrpc import daemon
new_tx = wallet.transfer(
0, user.subaddress_index, tx.address, Decimal(tx.amount) if not path.isdir(config.WALLET_DIR):
) makedirs(config.WALLET_DIR)
if 'message' in new_tx:
print(f'Failed to send tx {tx.id}. Reason: {new_tx["message"]}') wallets_to_create = User.query.filter_by(wallet_created=False)
else: if wallets_to_create:
tx.sent = True for u in wallets_to_create:
user.funds_locked = False 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() db.session.commit()
print(f'Successfully sent tx! {new_tx}')
# Routes # Routes
from wowstash.blueprints.auth import auth_bp from wowstash.blueprints.auth import auth_bp

@ -6,19 +6,19 @@ from redis import Redis
from wowstash import config from wowstash import config
class CoinInfo(object): class Cache(object):
def __init__(self): def __init__(self):
self.redis = Redis(host=config.REDIS_HOST, port=config.REDIS_PORT) 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( self.redis.setex(
"info", item_name,
timedelta(minutes=15), timedelta(minutes=expiration_minutes),
value=info value=data
) )
def get_info(self): def get_coin_info(self):
info = self.redis.get("info") info = self.redis.get("coin_info")
if info: if info:
return json_loads(info) return json_loads(info)
else: else:
@ -42,7 +42,7 @@ class CoinInfo(object):
'total_volume': r.json()['market_data']['total_volume']['usd'], 'total_volume': r.json()['market_data']['total_volume']['usd'],
'last_updated': r.json()['last_updated'] 'last_updated': r.json()['last_updated']
} }
self.store_info(json_dumps(info)) self.store_data("coin_info", 15, json_dumps(info))
return info return info
info = CoinInfo() cache = Cache()

@ -54,22 +54,19 @@ class Wallet(JSONRPC):
_address = self.make_rpc('create_address', data) _address = self.make_rpc('create_address', data)
return (_address['address_index'], _address['address']) return (_address['address_index'], _address['address'])
def get_address(self, account_index, subaddress_index): def get_address(self, account_index=0):
data = {'account_index': account_index, 'address_index': [subaddress_index]} data = {'account_index': account_index}
subaddress = self.make_rpc('get_address', data)['addresses'][0]['address'] address = self.make_rpc('get_address', data)['address']
return subaddress return address
def get_balance(self, account_index, subaddress_index): def get_balances(self, account_index=0):
data = {'account_index': account_index, 'address_indices': [subaddress_index]} data = {'account_index': account_index}
_balance = self.make_rpc('get_balance', data) balance = self.make_rpc('get_balance', data)
locked = from_atomic(_balance['per_subaddress'][0]['balance']) return (balance['balance'], balance['unlocked_balance'])
unlocked = from_atomic(_balance['per_subaddress'][0]['unlocked_balance'])
return (float(locked), float(unlocked)) def get_transfers(self, account_index=0):
def get_transfers(self, account_index, subaddress_index):
data = { data = {
'account_index': account_index, 'account_index': account_index,
'subaddr_indices': [subaddress_index],
'in': True, 'in': True,
'out': True, 'out': True,
'pending': True, 'pending': True,
@ -126,11 +123,3 @@ daemon = Daemon(
username=config.DAEMON_USER, username=config.DAEMON_USER,
password=config.DAEMON_PASS 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
)

@ -1,3 +1,4 @@
from os import kill
from sqlalchemy import Column, Integer, DateTime, String, ForeignKey from sqlalchemy import Column, Integer, DateTime, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func from sqlalchemy.sql import func
@ -9,12 +10,16 @@ Base = declarative_base()
class User(db.Model): class User(db.Model):
__tablename__ = 'users' __tablename__ = 'users'
id = db.Column('user_id', db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
password = db.Column(db.String(120))
email = db.Column(db.String(50), unique=True, index=True) email = db.Column(db.String(50), unique=True, index=True)
subaddress_index = db.Column(db.Integer) password = db.Column(db.String(120))
registered_on = db.Column(db.DateTime, server_default=func.now()) register_date = db.Column(db.DateTime, server_default=func.now())
funds_locked = db.Column(db.Boolean, default=False) 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 @property
def is_authenticated(self): def is_authenticated(self):
@ -35,19 +40,18 @@ class User(db.Model):
def get_id(self): def get_id(self):
return self.id return self.id
def __repr__(self): def kill_wallet(self):
return self.username try:
kill(self.wallet_pid, 9)
except Exception as e:
class Transaction(db.Model): print('could kill:', e)
__tablename__ = 'transactions'
id = db.Column('tx_id', db.Integer, primary_key=True) def clear_wallet_data(self):
from_user = db.Column(db.Integer, ForeignKey(User.id)) self.wallet_connected = False
sent = db.Column(db.Boolean, default=False) self.wallet_port = None
address = db.Column(db.String(120)) self.wallet_pid = None
amount = db.Column(db.String(120)) self.wallet_connect_date = None
date = db.Column(db.DateTime, server_default=func.now()) db.session.commit()
def __repr__(self): def __repr__(self):
return self.id return self.email

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

@ -12,13 +12,12 @@
<div class="section-heading text-center"> <div class="section-heading text-center">
<h2>Wallet Info</h2> <h2>Wallet Info</h2>
<h4>Address:</h4> <h4>Address:</h4>
<p class="slim small">{{ subaddress }}</p> <p class="slim small">{{ address }}</p>
<br> <br>
<img src="data:image/png;base64,{{ qrcode }}" width=200 class="center"> <img src="data:image/png;base64,{{ qrcode }}" width=200 class="center">
<hr><br> <hr><br>
<h4>Balance</h4> <h4>Balance</h4>
<p class="inline">{{ balances.1 }} WOW </p> <p class="inline">{{ balances[1] | from_atomic }} WOW ({{ balances[0] | from_atomic }} locked)</p>
<p class="inline small">({{ balances.0 }} locked)</p>
<span class="dashboard-buttons"> <span class="dashboard-buttons">
<div class="col-sm-6 dashboard-button"> <div class="col-sm-6 dashboard-button">
<a class="btn btn-lg btn-link btn-outline btn-xl js-scroll-trigger" href="#transfers">List</a> <a class="btn btn-lg btn-link btn-outline btn-xl js-scroll-trigger" href="#transfers">List</a>
@ -45,17 +44,19 @@
<th>Height</th> <th>Height</th>
<th>Fee</th> <th>Fee</th>
</tr> </tr>
{% 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' %}<tr class="table-warning">{% else %}<tr>{% endif %} {% if tx.type == 'pool' %}<tr class="table-warning">{% else %}<tr>{% endif %}
<td>{{ tx.timestamp | datestamp }}</td> <td>{{ tx.timestamp | datestamp }}</td>
<td>{{ tx.type }}</td> <td>{{ tx.type }}</td>
<td><a href="https://wownero.club/transaction/{{ tx.txid }}" target="_blank">{{ tx.txid | truncate(12) }}</a></td> <td><a href="https://wownero.club/transaction/{{ tx.txid }}" target="_blank">{{ tx.txid | truncate(12) }}</a></td>
<td>{% if tx.type == 'out' %}{{ tx.destinations.0.amount / 100000000000 }}{% else %}{{ tx.amount / 100000000000 }}{% endif %} WOW</td> <td>{{ tx.amount | from_atomic }} WOW</td>
<td>{{ tx.confirmations }}</td> <td>{{ tx.confirmations }}</td>
<td>{{ tx.height }}</td> <td>{{ tx.height }}</td>
<td>{{ tx.fee / 100000000000 }} WOW</td> <td>{{ tx.fee | from_atomic }} WOW</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% endif %}
</table> </table>
</div> </div>
</div> </div>
@ -69,7 +70,7 @@
<p>Sending funds is currently locked due to a transfer already in progress. Please try again in a few minutes. Pending transfers:</p> <p>Sending funds is currently locked due to a transfer already in progress. Please try again in a few minutes. Pending transfers:</p>
<ul> <ul>
{% for tx in txs_queued %} {% for tx in txs_queued %}
<li class="slim small">{{ tx.amount }} - {{ tx.address }}</li> <li class="slim small">{{ tx.amount | from_atomic }} - {{ tx.address }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
{% include 'head.html' %}
<body id="page-top">
{% include 'navbar.html' %}
<section class="section2">
<div class="container">
<div class="section-heading text-center">
{% if current_user.wallet_created == False %}
<h2>Your wallet is being created</h2>
{% else %}
<h2>Your wallet is connecting</h2>
{% endif %}
<p>Go smoke a fatty. This page should auto-refresh when it's ready...if not, click the button below</p>
<img src="/static/img/loading-cat.gif" width=300>
<span class="dashboard-buttons">
<div class="col-sm-12 dashboard-button">
<a class="btn btn-lg btn-link btn-outline btn-xl" href="{{ url_for('wallet.dashboard') }}">Check Again</a>
</div>
</span>
</div>
</div>
</section>
<script>
function check_wallet_status(attrib) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4){
let res = JSON.parse(xhr.responseText);
if (res[attrib] == true) {
window.location.href = "{{ url_for('wallet.dashboard') }}"
}
}
};
xhr.open('GET', '{{ url_for("wallet.status") }}');
xhr.send();
}
{% if current_user.wallet_connected == False %}
document.addEventListener("DOMContentLoaded", function(){
var xhr = new XMLHttpRequest();
xhr.open('GET', '{{ url_for("wallet.connect") }}');
xhr.send();
});
{% endif %}
window.setInterval(function(){
{% if current_user.wallet_connected == False %}
check_wallet_status('connected');
{% else %}
check_wallet_status('created');
{% endif %}
}, 5000);
</script>
{% include 'footer.html' %}
{% include 'scripts.html' %}
</body>
</html>