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
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
container_name: db
ports:
- 5432:5432
- 5433:5432
environment:
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_USER: ${DB_USER}

@ -9,3 +9,4 @@ flask-bcrypt
flask-login
qrcode
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_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'))

@ -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({

@ -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()

@ -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

@ -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

@ -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()

@ -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
)

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

@ -12,13 +12,12 @@
<div class="section-heading text-center">
<h2>Wallet Info</h2>
<h4>Address:</h4>
<p class="slim small">{{ subaddress }}</p>
<p class="slim small">{{ address }}</p>
<br>
<img src="data:image/png;base64,{{ qrcode }}" width=200 class="center">
<hr><br>
<h4>Balance</h4>
<p class="inline">{{ balances.1 }} WOW </p>
<p class="inline small">({{ balances.0 }} locked)</p>
<p class="inline">{{ balances[1] | from_atomic }} WOW ({{ balances[0] | from_atomic }} locked)</p>
<span class="dashboard-buttons">
<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>
@ -45,17 +44,19 @@
<th>Height</th>
<th>Fee</th>
</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 %}
<td>{{ tx.timestamp | datestamp }}</td>
<td>{{ tx.type }}</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.height }}</td>
<td>{{ tx.fee / 100000000000 }} WOW</td>
<td>{{ tx.fee | from_atomic }} WOW</td>
</tr>
{% endfor %}
{% endif %}
</table>
</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>
<ul>
{% 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 %}
</ul>
{% 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>