tips table/total, pending txes, verification

master
lza_menace 2 years ago
parent d937a12317
commit e3a3e7aa19

@ -5,3 +5,14 @@ Post your best neroChans. Get tipped.
No middle-men. p2p tips. No middle-men. p2p tips.
No passwords. Wallet signing authentication. No passwords. Wallet signing authentication.
## Dev Stuff
Stagenet txes for testing/validation:
| recipient | tx_id | tx_key |
| --- | --- | --- |
| 78TanhCTvw4V8HkY3vD49A5EiyeGCzCHQUm59sByukTcffZPf3QHoK8PDg8WpMUc6VGwqxTu65HvwCUfB2jZutb6NKpjArk | 077b8654dd95fdfbd6d97808e2a9ad37cf767fb2f9da4cb0e1e6427c8587f6ee | be64bd151bd01cb4f8572a3c9731d0dff726079213e9f7017957799edc46630b |
| 78TanhCTvw4V8HkY3vD49A5EiyeGCzCHQUm59sByukTcffZPf3QHoK8PDg8WpMUc6VGwqxTu65HvwCUfB2jZutb6NKpjArk | 46fd71389ed54f195d359b84897bb89b37bb8da0bbe72ef22b552c8786346805 | b683e96770c76a1a23253873ad8a2ebb1832e14d90a05fb49a9e6e22e73d630a |
| | | |

@ -8,5 +8,4 @@ XMR_WALLET_NETWORK=stagenet
SITE_NAME=nerochan SITE_NAME=nerochan
SECRET_KEY=xxxxxxxxxxxxxxxxxxx SECRET_KEY=xxxxxxxxxxxxxxxxxxx
STATS_TOKEN=xxxxxxxxxxxxxxxxxxxx
SERVER_NAME=127.0.0.1:5000 SERVER_NAME=127.0.0.1:5000

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

@ -1,10 +1,13 @@
import click import click
import lorem import lorem
from datetime import datetime, timedelta, timezone
from os import path, makedirs from os import path, makedirs
from urllib.request import urlopen from urllib.request import urlopen
from nerochan.models import User, Artwork from nerochan.helpers import make_wallet_rpc, daemon
from nerochan.models import User, Artwork, Transaction
def cli(app): def cli(app):
@ -14,6 +17,30 @@ def cli(app):
from nerochan.models import db from nerochan.models import db
model = peewee.Model.__subclasses__() model = peewee.Model.__subclasses__()
db.create_tables(model) db.create_tables(model)
@app.cli.command('verify_tips')
def verify_tips():
txes = Transaction.select().where(Transaction.verified == False)
for tx in txes:
data = {
'txid': tx.tx_id,
'tx_key': tx.tx_key,
'address': tx.to_address
}
try:
res = make_wallet_rpc('check_tx_key', data)
if res['in_pool'] is False:
txdata = daemon.transactions([tx.tx_id])[0]
d = txdata.timestamp.astimezone(timezone.utc)
tx.atomic_xmr = res['received']
tx.tx_date = d
tx.verified = True
tx.save()
click.echo(f'[+] Found valid tip {tx.tx_id}')
except Exception as e:
# delete if it fails for over 8 hours
if tx.create_date <= datetime.utcnow() - timedelta(hours=8):
pass
@app.cli.command('generate_data') @app.cli.command('generate_data')
def generate_data(): def generate_data():
@ -74,7 +101,7 @@ def cli(app):
click.echo(f'[+] Downloaded {art}') click.echo(f'[+] Downloaded {art}')
if not Artwork.select().where(Artwork.image == bn).first(): if not Artwork.select().where(Artwork.image == bn).first():
artwork = Artwork( artwork = Artwork(
creator=_user, user=_user,
image=bn, image=bn,
approved=True, approved=True,
title=lorem.sentence(), title=lorem.sentence(),

@ -17,6 +17,8 @@ XMR_WALLET_RPC_USER = getenv('XMR_WALLET_RPC_USER')
XMR_WALLET_RPC_PASS = getenv('XMR_WALLET_RPC_PASS') XMR_WALLET_RPC_PASS = getenv('XMR_WALLET_RPC_PASS')
XMR_WALLET_RPC_PORT = getenv('XMR_WALLET_RPC_PORT', 8000) XMR_WALLET_RPC_PORT = getenv('XMR_WALLET_RPC_PORT', 8000)
XMR_WALLET_NETWORK = getenv('XMR_WALLET_NETWORK') XMR_WALLET_NETWORK = getenv('XMR_WALLET_NETWORK')
XMR_DAEMON_URI = getenv('XMR_DAEMON_URI')
XMR_DAEMON_HOST, XMR_DAEMON_PORT = XMR_DAEMON_URI.split('://')[1].split(':')
REDIS_HOST = getenv('REDIS_HOST', 'localhost') REDIS_HOST = getenv('REDIS_HOST', 'localhost')
REDIS_PORT = getenv('REDIS_PORT', 6379) REDIS_PORT = getenv('REDIS_PORT', 6379)
DATA_PATH = getenv('DATA_PATH', f'{getcwd()}/data') DATA_PATH = getenv('DATA_PATH', f'{getcwd()}/data')

@ -1,8 +1,10 @@
from datetime import datetime from datetime import datetime
import arrow import arrow
from monero import numbers from monero.numbers import from_atomic
from flask import Blueprint, current_app from flask import Blueprint
from nerochan import config
bp = Blueprint('filters', 'filters') bp = Blueprint('filters', 'filters')
@ -18,8 +20,15 @@ def from_ts(v):
@bp.app_template_filter('xmr_block_explorer') @bp.app_template_filter('xmr_block_explorer')
def xmr_block_explorer(v): def xmr_block_explorer(v):
return f'https://www.exploremonero.com/transaction/{v}' if config.XMR_WALLET_NETWORK == 'stagenet':
return f'https://stagenet.xmrchain.net/tx/{v}'
@bp.app_template_filter('from_atomic') else:
def from_atomic(amt): return f'https://www.exploremonero.com/transaction/{v}'
return numbers.from_atomic(amt)
@bp.app_template_filter('atomic')
def atomic(amt):
return float(from_atomic(amt))
@bp.app_template_filter('shorten')
def shorten(s):
return s[:4] + '...' + s[-5:]

@ -21,27 +21,19 @@ def is_valid_xmr_address(form, field):
class UserRegistration(FlaskForm): class UserRegistration(FlaskForm):
handle = StringField('Handle:', validators=[DataRequired()], render_kw={'placeholder': 'online handle', 'class': 'form-control', 'type': 'text'}) handle = StringField('Handle:', validators=[DataRequired()], render_kw={'placeholder': 'online handle', 'class': 'u-full-width', 'type': 'text'})
wallet_address = StringField('Wallet Address:', validators=[DataRequired(), is_valid_xmr_address], render_kw={'placeholder': 'monero wallet address', 'class': 'form-control', 'type': 'text'}) wallet_address = StringField('Wallet Address:', validators=[DataRequired(), is_valid_xmr_address], render_kw={'placeholder': 'monero wallet address', 'class': 'u-full-width', 'type': 'text'})
class UserLogin(FlaskForm): class UserLogin(FlaskForm):
handle = StringField('Handle:', validators=[DataRequired()], render_kw={'placeholder': 'online handle', 'class': 'form-control', 'type': 'text'}) handle = StringField('Handle:', validators=[DataRequired()], render_kw={'placeholder': 'online handle', 'class': 'u-full-width', 'type': 'text'})
class UserChallenge(FlaskForm): class UserChallenge(FlaskForm):
signature = StringField('Signature:', validators=[DataRequired()], render_kw={'placeholder': 'signed data', 'class': 'form-control', 'type': 'text'}) signature = StringField('Signature:', validators=[DataRequired()], render_kw={'placeholder': 'signed data', 'class': 'u-full-width', 'type': 'text'})
class ConfirmPlatformSubscription(FlaskForm): class ConfirmTip(FlaskForm):
tx_id = StringField('TX ID:', validators=[DataRequired()], render_kw={'placeholder': 'TX ID', 'class': 'form-control', 'type': 'text'}) tx_id = StringField('TX ID:', validators=[DataRequired()], render_kw={'placeholder': 'TX ID', 'class': 'u-full-width', 'type': 'text'})
tx_key = StringField('TX Key:', validators=[DataRequired()], render_kw={'placeholder': 'TX Key', 'class': 'form-control', 'type': 'text'}) tx_key = StringField('TX Key:', validators=[DataRequired()], render_kw={'placeholder': 'TX Key', 'class': 'u-full-width', 'type': 'text'})
class ConfirmCreatorSubscription(ConfirmPlatformSubscription):
wallet_address = StringField('Wallet Address:', validators=[DataRequired(), is_valid_xmr_address], render_kw={'placeholder': 'monero wallet address', 'class': 'form-control', 'type': 'text'})
class CreateSubscription(FlaskForm):
price_xmr = FloatField('Price (XMR):', validators=[DataRequired()], render_kw={'placeholder': '.5', 'class': 'form-control', 'type': 'text'})
number_days = FloatField('Length (Days)', validators=[DataRequired()], render_kw={'placeholder': '30', 'class': 'form-control', 'type': 'text'})

@ -1,19 +1,34 @@
import peewee as pw
import playhouse.postgres_ext as pwpg
from monero.wallet import Wallet from monero.wallet import Wallet
from monero.daemon import Daemon
from nerochan import config from nerochan import config
daemon = Daemon(
host=config.XMR_DAEMON_HOST,
port=config.XMR_DAEMON_PORT,
timeout=3
)
wallet = Wallet(
port=config.XMR_WALLET_RPC_PORT,
user=config.XMR_WALLET_RPC_USER,
password=config.XMR_WALLET_RPC_PASS,
timeout=3
)
def make_wallet_rpc(method, data={}): def make_wallet_rpc(method, data={}):
try: try:
w = Wallet( w = wallet
port=config.XMR_WALLET_RPC_PORT,
user=config.XMR_WALLET_RPC_USER,
password=config.XMR_WALLET_RPC_PASS,
timeout=3
)
res = w._backend.raw_request(method, data) res = w._backend.raw_request(method, data)
return res return res
except Exception as e: except Exception as e:
raise e raise e
def make_daemon_rpc(method, data={}):
try:
d = daemon
res = d._backend.raw_request(method, data)
return res
except Exception as e:
raise e

@ -58,7 +58,7 @@ class User(pw.Model):
class Profile(pw.Model): class Profile(pw.Model):
""" """
Profile model is for users to provide metadata about Profile model is for users to provide metadata about
themselves; Creators for their fans or even just the general public. themselves; artists for their fans or even just the general public.
Links to social media, contact info, portfolio sites, etc Links to social media, contact info, portfolio sites, etc
should go in here. should go in here.
""" """
@ -77,10 +77,10 @@ class Profile(pw.Model):
class Artwork(pw.Model): class Artwork(pw.Model):
""" """
Artwork model is any uploaded content from a creator. Artwork model is any uploaded content from a user.
""" """
id = pw.AutoField() id = pw.AutoField()
creator = pw.ForeignKeyField(User) user = pw.ForeignKeyField(User)
image = pw.CharField() image = pw.CharField()
upload_date = pw.DateTimeField(default=datetime.utcnow) upload_date = pw.DateTimeField(default=datetime.utcnow)
last_edit_date = pw.DateTimeField(default=datetime.utcnow) last_edit_date = pw.DateTimeField(default=datetime.utcnow)
@ -135,9 +135,13 @@ class Transaction(pw.Model):
""" """
id = pw.AutoField() id = pw.AutoField()
tx_id = pw.CharField(unique=True) tx_id = pw.CharField(unique=True)
atomic_xmr = pw.BigIntegerField() tx_key = pw.CharField(unique=True)
atomic_xmr = pw.BigIntegerField(null=True)
to_address = pw.CharField() to_address = pw.CharField()
content = pw.ForeignKeyField(Artwork) artwork = pw.ForeignKeyField(Artwork)
verified = pw.BooleanField(default=False)
create_date = pw.DateTimeField(default=datetime.utcnow)
tx_date = pw.DateTimeField(null=True)
class Meta: class Meta:
database = db database = db

@ -1,7 +1,9 @@
from flask import Blueprint, render_template, flash, redirect, url_for from flask import Blueprint, render_template, flash, redirect, url_for
from flask_login import login_required from flask_login import login_required
from nerochan.models import Artwork, User from nerochan.helpers import make_wallet_rpc
from nerochan.forms import ConfirmTip
from nerochan.models import Artwork, Transaction
bp = Blueprint('artwork', 'artwork', url_prefix='/artwork') bp = Blueprint('artwork', 'artwork', url_prefix='/artwork')
@ -10,13 +12,43 @@ bp = Blueprint('artwork', 'artwork', url_prefix='/artwork')
def list(): def list():
return 'show all artwork' return 'show all artwork'
@bp.route('/<int:id>') @bp.route('/<int:id>', methods=['GET', 'POST'])
def show(id): def show(id):
form = ConfirmTip()
artwork = Artwork.get_or_none(id) artwork = Artwork.get_or_none(id)
if not artwork: if not artwork:
flash('That artwork does not exist.', 'warning') flash('That artwork does not exist.', 'warning')
return redirect(url_for('main.index')) return redirect(url_for('main.index'))
return render_template('artwork/show.html', artwork=artwork) if form.validate_on_submit():
# Create a tx object to verify later
try:
tx = Transaction(
tx_id=form.tx_id.data,
tx_key=form.tx_key.data,
to_address=artwork.user.wallet_address,
artwork=artwork.id
)
tx.save()
except Exception as e:
pass
return redirect(url_for('artwork.show', id=artwork.id))
txes_pending = Transaction.select().where(
Transaction.artwork == artwork,
Transaction.verified == False
).count()
txes = Transaction.select().where(
Transaction.artwork == artwork,
Transaction.verified == True
).order_by(Transaction.tx_date.desc())
total = sum([i.atomic_xmr for i in txes])
return render_template(
'artwork/show.html',
artwork=artwork,
txes_pending=txes_pending,
txes=txes,
total=total,
form=form
)
@bp.route('/new', methods=['GET', 'POST']) @bp.route('/new', methods=['GET', 'POST'])
@login_required @login_required

@ -8,6 +8,12 @@ a, a:visited {
color: white; color: white;
} }
.container-wide {
max-width: 90%;
margin-left: 3em;
margin-right: 3em;
}
.artworkLink img { .artworkLink img {
border-radius: 2px; border-radius: 2px;
margin: 1em; margin: 1em;
@ -93,7 +99,7 @@ input[type="text"] {
margin-right: 35px; margin-right: 35px;
text-decoration: none; text-decoration: none;
line-height: 6.5rem; line-height: 6.5rem;
color: #222; color: #fff;
transition: all .2s ease; transition: all .2s ease;
} }

@ -7,7 +7,7 @@
<div class="row"> <div class="row">
<h2 class="no-margin"><strong>{{ artwork.title }}</strong></h2> <h2 class="no-margin"><strong>{{ artwork.title }}</strong></h2>
<h6 class="no-margin"> <h6 class="no-margin">
Posted by <a href="{{ url_for('user.show', handle=artwork.creator.handle) }}">{{ artwork.creator.handle }}</a> - {{ artwork.upload_date | humanize }} posted by <a href="{{ url_for('user.show', handle=artwork.user.handle) }}">{{ artwork.user.handle }}</a> - {{ artwork.upload_date | humanize }}
</h6> </h6>
<p class="artworkDescription">{{ artwork.description }}</p> <p class="artworkDescription">{{ artwork.description }}</p>
</div> </div>
@ -15,46 +15,52 @@
<div class="row"> <div class="row">
<div class="column one-half"> <div class="column one-half">
<a href="{{ img }}"> <a href="{{ img }}">
<img src="{{ img }}" width="100%"> <img src="{{ img }}" width="100%" style="padding-bottom: 2em;">
</a> </a>
</div> </div>
<div class="column one-half"> <div class="column one-half">
<h5>Send a Tip</h5> <h5>Send a Tip</h5>
<p class="walletAddress">{{ artwork.creator.wallet_address }}</p> <p class="walletAddress">{{ artwork.user.wallet_address }}</p>
<form method="get"> <form method="post">
{{ form.csrf_token }}
<div class="row"> <div class="row">
<div class="six columns"> <div class="six columns">
<label for="txID">TX ID</label> {{ form.tx_id.label }}
<input class="u-full-width" type="text" placeholder="..." id="txID" name="txID"> {{ form.tx_id }}
</div> </div>
<div class="six columns"> <div class="six columns">
<label for="txKey">TX Key</label> {{ form.tx_key.label }}
<input class="u-full-width" type="text" placeholder="..." id="txKey" name="txKey"> {{ form.tx_key }}
</div> </div>
</div> </div>
<input class="button-primary" type="submit" value="Submit"> <input class="button-primary" type="submit" value="Submit">
</form> </form>
<table class="u-full-width"> {% if txes_pending %}
<thead> <h6>{{ txes_pending }} tips pending</h6>
<tr> {% endif %}
<th>TXID</th> {% if txes %}
<th>XMR</th> <table class="u-full-width">
<th>Date</th> <thead>
</tr> <tr>
</thead> <th>TXID</th>
<tbody> <th>XMR</th>
<tr> <th>Date</th>
<td>e599...5429</td> </tr>
<td>.05</td> </thead>
<td>3 days ago</td> <tbody>
</tr> {% for tx in txes %}
<tr> <tr>
<td>681a...e264</td> <td><a href="{{ tx.tx_id | xmr_block_explorer }}" target="_blank">{{ tx.tx_id | shorten }}</a></td>
<td>.25</td> <td>{{ tx.atomic_xmr | atomic }}</td>
<td>28 days ago</td> <td>{{ tx.tx_date | humanize }}</td>
</tr> </tr>
</tbody> {% endfor %}
</table> </tbody>
</table>
<p>Total Received: <strong>{{ total | atomic }} XMR</strong></p>
{% else %}
<p>No tips confirmed yet...</p>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

@ -4,7 +4,8 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<h2>Latest Artworks</h2> <h2>artworks</h2>
{% if feed['artwork'] %}
{%- for _artwork in feed['artwork'] | batch(4) %} {%- for _artwork in feed['artwork'] | batch(4) %}
{%- for artwork in _artwork %} {%- for artwork in _artwork %}
<a class="artworkLink" href="{{ url_for('artwork.show', id=artwork.id) }}"> <a class="artworkLink" href="{{ url_for('artwork.show', id=artwork.id) }}">
@ -13,12 +14,19 @@
{%- endfor %} {%- endfor %}
{%- endfor %} {%- endfor %}
<p><a href="{{ url_for('artwork.list') }}">...view all</a></p> <p><a href="{{ url_for('artwork.list') }}">...view all</a></p>
{% else %}
<p>There's nothing here yet...</p>
{% endif %}
</div> </div>
<div class="row"> <div class="row">
<h2>Latest Artists</h2> <h2>artists</h2>
{% if feed['users'] %}
{% for user in feed['users'] %} {% for user in feed['users'] %}
<p><a href="{{ url_for('user.show', handle=user.handle) }}">{{ user.handle }}</a></p> <p><a href="{{ url_for('user.show', handle=user.handle) }}">{{ user.handle }}</a></p>
{% endfor %} {% endfor %}
{% else %}
<p>There's nothing here yet...</p>
{% endif %}
</div> </div>
</div> </div>

Loading…
Cancel
Save