diff --git a/Makefile b/Makefile index 7b7dfb4..f2f1ff5 100644 --- a/Makefile +++ b/Makefile @@ -10,12 +10,18 @@ setup: ## Establish local environment with dependencies installed python3 -m venv .venv .venv/bin/pip install -r requirements.txt +build: ## Build containers + docker-compose build + up: ## Start containers docker-compose up -d down: ## Stop containers docker-compose down +dbshell: + docker-compose exec db psql -U xmrbackers + shell: ## Start Quart CLI shell QUART_APP=app.py QUART_SECRETS=config.py QUART_DEBUG=0 QUART_ENV=production .venv/bin/quart shell diff --git a/env-example b/env-example index 9538deb..6990720 100644 --- a/env-example +++ b/env-example @@ -5,8 +5,6 @@ DB_HOST=localhost PLATFORM_WALLET=xxxxxx -XMR_WALLET_PATH=/data/xmr-wallet -XMR_WALLET_PASS=xxxxxxxxxxxxxxxxxxx XMR_WALLET_RPC_USER=xxxxxxxxxx XMR_WALLET_RPC_PASS=xxxxxxxxxxxxxxxxxxx XMR_WALLET_RPC_PORT=8000 diff --git a/requirements.txt b/requirements.txt index b84c968..9ad6311 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ qrcode redis peewee requests -SQLAlchemy WTForms quart==0.17.0 monero diff --git a/xmrbackers/forms.py b/xmrbackers/forms.py index 4beae00..7c193f2 100644 --- a/xmrbackers/forms.py +++ b/xmrbackers/forms.py @@ -35,9 +35,3 @@ class UserChallenge(FlaskForm): class ConfirmSubscription(FlaskForm): tx_id = StringField('TX ID:', validators=[DataRequired()], render_kw={"placeholder": "TX ID", "class": "form-control", "type": "text"}) tx_key = StringField('TX Key:', validators=[DataRequired()], render_kw={"placeholder": "TX Key", "class": "form-control", "type": "text"}) - wallet_address = StringField('XMR Address:', validators=[DataRequired()], render_kw={"placeholder": "XMR Address", "class": "form-control", "type": "text"}) - - -class ConfirmCreatorSubscription(FlaskForm): - tx_id = StringField('TX ID:', validators=[DataRequired()], render_kw={"placeholder": "TX ID", "class": "form-control", "type": "text"}) - tx_key = StringField('TX Key:', validators=[DataRequired()], render_kw={"placeholder": "TX Key", "class": "form-control", "type": "text"}) \ No newline at end of file diff --git a/xmrbackers/helpers.py b/xmrbackers/helpers.py index 09260c6..d355546 100644 --- a/xmrbackers/helpers.py +++ b/xmrbackers/helpers.py @@ -6,26 +6,26 @@ from monero.wallet import Wallet from xmrbackers import config -def check_tx_key(tx_id, tx_key, wallet_address): - try: - check_data = { - 'txid': tx_id, - 'tx_key': tx_key, - 'address': wallet_address - } - w = Wallet(port=config.XMR_WALLET_RPC_PORT, user=config.XMR_WALLET_RPC_USER, password=config.XMR_WALLET_RPC_PASS) - res = w._backend.raw_request('check_tx_key', check_data) - return res - except: - raise Exception('there was a problem i dont feel like writing good code for right now') +# def check_tx_key(tx_id, tx_key, wallet_address): +# try: +# check_data = { +# 'txid': tx_id, +# 'tx_key': tx_key, +# 'address': wallet_address +# } +# w = Wallet(port=config.XMR_WALLET_RPC_PORT, user=config.XMR_WALLET_RPC_USER, password=config.XMR_WALLET_RPC_PASS) +# res = w._backend.raw_request('check_tx_key', check_data) +# return res +# except: +# raise Exception('there was a problem i dont feel like writing good code for right now') def make_wallet_rpc(method, data={}): try: w = Wallet( - port=config.XMR_WALLET_RPC_PORT, - user=config.XMR_WALLET_RPC_USER, - password=config.XMR_WALLET_RPC_PASS, - timeout=5 + 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) return res @@ -101,4 +101,4 @@ class EnumIntField(pw.IntegerField): return value.value def python_value(self, value): - return self.enum_class(value) \ No newline at end of file + return self.enum_class(value) diff --git a/xmrbackers/models.py b/xmrbackers/models.py index 5be8ae7..bdeb197 100644 --- a/xmrbackers/models.py +++ b/xmrbackers/models.py @@ -97,6 +97,7 @@ class Transaction(pw.Model): id = pw.AutoField() tx_id = pw.CharField(unique=True) atomic_xmr = pw.BigIntegerField() + to_address = pw.CharField() class Meta: database = db @@ -104,7 +105,7 @@ class Transaction(pw.Model): class CreatorSubscription(pw.Model): """ - CreatorSubscription model is for tracking subscriptions of creators to the platform. + CreatorSubscription model is for tracking subscriptions of creators to the platform. The first subscription is a flat fee, the following will be based on usage/consumption and will be re-negotiated every N days. """ @@ -146,8 +147,8 @@ class SubscriptionMeta(pw.Model): class Subscription(pw.Model): """ Subscription model gets created when users/backers can confirm payment via - the `check_tx_key` RPC method to the creator's wallet. Once a subscription - is in place and is associated with a user, that user is then elligible to + the `check_tx_key` RPC method to the creator's wallet. Once a subscription + is in place and is associated with a user, that user is then elligible to view that creator's content until the subscription expires (number_hours). """ id = pw.AutoField() @@ -164,7 +165,7 @@ class Subscription(pw.Model): class Content(pw.Model): """ - Content model represents any uploaded content from a creator which is only + Content model represents any uploaded content from a creator which is only viewable by backers with an active subscription. """ id = pw.AutoField() diff --git a/xmrbackers/routes/creator.py b/xmrbackers/routes/creator.py index 3b1ed76..e695605 100644 --- a/xmrbackers/routes/creator.py +++ b/xmrbackers/routes/creator.py @@ -1,10 +1,11 @@ from quart import Blueprint, render_template, flash, redirect, url_for from flask_login import current_user, login_required from monero.wallet import Wallet +from monero.address import address +from monero.numbers import to_atomic, from_atomic -from xmrbackers.models import User, Profile, Content -from xmrbackers.models import Subscription, SubscriptionMeta, UserRole -from xmrbackers.helpers import check_tx_key +from xmrbackers.models import * +from xmrbackers.helpers import make_wallet_rpc from xmrbackers import config, forms @@ -19,42 +20,107 @@ async def all(): ) return await render_template('creator/creators.html', creators=creators) -@bp.route('/creators/join') +@bp.route('/creators/join', methods=['GET', 'POST']) @login_required async def join(): - form = forms.ConfirmCreatorSubscription() + form = forms.ConfirmSubscription() + valid_address = False + + try: + address(config.PLATFORM_WALLET) + valid_address = True + except: + pass + + if not config.PLATFORM_WALLET or valid_address is False: + await flash('Platform operator has not setup wallet yet. Try later.', 'warning') + return redirect(url_for('main.index')) if UserRole.creator in current_user.roles: await flash('You already are a creator!', 'warning') - - if not config.PLATFORM_WALLET: - await flash('Platform operator has not setup wallet yet. Try later.', 'warning') - - return render_template( - 'creator/join.html', + return redirect(url_for('main.index')) + + if form.validate_on_submit(): + try: + data = { + 'txid': form.tx_id.data, + 'tx_key': form.tx_key.data, + 'address': config.PLATFORM_WALLET + } + res = make_wallet_rpc('check_tx_key', data) + existing_tx = Transaction.select().where( + Transaction.tx_id == form.tx_id.data.lower() + ).first() + existing_sub = CreatorSubscription.select().where( + CreatorSubscription.tx == existing_tx + ).first() + for i in ['confirmations', 'in_pool', 'received']: + assert i in res + if res['in_pool']: + await flash('That transaction is still in the mempool. You need to wait a few more minutes for it to clear. Try again in a bit.', 'warning') + elif res['received'] < to_atomic(config.CREATOR_SUBSCRIPTION_FEE_XMR): + await flash(f'Not enought XMR sent. {from_atomic(res["received"])} XMR sent, expected {config.CREATOR_SUBSCRIPTION_FEE_XMR} XMR.', 'error') + elif existing_tx: + if existing_sub: + await flash('This transaction was already used for another subscription.', 'warning') + else: + await flash('Adding creator subscription.', 'success') + c = CreatorSubscription( + user=current_user, + tx=existing_tx, + atomic_xmr=res['received'] + ) + c.save() + current_user.roles.append(UserRole.creator) + current_user.save() + elif res['received'] >= to_atomic(config.CREATOR_SUBSCRIPTION_FEE_XMR): + await flash('Success! Welcome to the creator club!', 'success') + t = Transaction( + tx_id=form.tx_id.data.lower(), + atomic_xmr=res['received'], + to_address=config.PLATFORM_WALLET + ) + t.save() + c = CreatorSubscription( + user=current_user, + tx=t, + atomic_xmr=res['received'] + ) + c.save() + current_user.roles.append(UserRole.creator) + current_user.save() + return redirect(url_for('creator.show', handle=current_user.handle)) + else: + await flash('Something went wrong. No idea what, though. Check with admin.', 'error') + except Exception as e: + await flash(f'seems bad: {e}', 'error') + + return await render_template( + 'creator/join.html', new_fee_xmr=config.CREATOR_SUBSCRIPTION_FEE_XMR, form=form ) - - return redirect(url_for('main.index')) @bp.route('/creator/') async def show(handle): - creator = User.select().where(User.handle == handle, User.roles.contains_any(UserRole.creator)) - if creator: - posts = Content.select().where( - Content.creator == creator, - Content.hidden == False - ).order_by(Content.post_date.desc()) - return await render_template( - 'creator/creator.html', - creator=creator, - posts=posts - ) - else: + creator = User.select().where( + User.handle == handle, + User.roles.contains_any(UserRole.creator) + ) + if not creator: await flash('That creator does not exist.') - return redirect(url_for('meta.index')) + return redirect(url_for('main.index')) + posts = Content.select().where( + Content.creator == creator, + Content.hidden == False + ).order_by(Content.post_date.desc()) + return await render_template( + 'creator/show.html', + creator=creator, + posts=posts + ) + # @bp.route('/creator//subscription') # async def subscription(username): diff --git a/xmrbackers/templates/creator/creator.html b/xmrbackers/templates/creator/creator.html deleted file mode 100644 index f66ebd7..0000000 --- a/xmrbackers/templates/creator/creator.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - {% include 'includes/head.html' %} - - -
- - {% include 'includes/header.html' %} - - {% if creator %} -

{{ creator.user.username }}

-

Bio: {{ creator.bio }}

- {% endif %} - - {% if posts %} -

Posts

- {% for post in posts %} -

{{ post.id }} - {{ post.title }} - {{ post.post_date | humanize }}

- {% endfor %} - {% endif %} - - {% include 'includes/footer.html' %} - -
- - {% include 'includes/scripts.html' %} - - - diff --git a/xmrbackers/templates/creator/join.html b/xmrbackers/templates/creator/join.html index 1df135a..c1cad6c 100644 --- a/xmrbackers/templates/creator/join.html +++ b/xmrbackers/templates/creator/join.html @@ -27,4 +27,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/xmrbackers/templates/creator/show.html b/xmrbackers/templates/creator/show.html new file mode 100644 index 0000000..99d4256 --- /dev/null +++ b/xmrbackers/templates/creator/show.html @@ -0,0 +1,15 @@ +{% extends 'includes/base.html' %} + +{% block content %} + +

{{ creator.handle }}

+

Bio: {{ creator.bio }}

+ + {% if posts %} +

Posts

+ {% for post in posts %} +

{{ post.id }} - {{ post.title }} - {{ post.post_date | humanize }}

+ {% endfor %} + {% endif %} + +{% endblock %}