Compare commits

..

No commits in common. 'master' and 'newcss' have entirely different histories.

@ -36,8 +36,8 @@ Download application and configure.
```
sudo apt install libjpeg-dev libpng-dev python-virtualenv python3 redis-server postgresql-server postgresql-server-dev-*
git clone https://git.wownero.com/wownero/wownero-funding-system.git
cd wownero-funding-system
git clone https://github.com/skftn/wownero-wfs.git
cd wownero-wfs
virtualenv -p /usr/bin/python3
source venv/bin/activate
pip uninstall pillow

@ -1,13 +1,12 @@
import re
import requests
from flask import jsonify, send_from_directory, Response
from flask import jsonify, send_from_directory, Response, request
from flask_yoloapi import endpoint, parameter
import settings
from funding.bin.utils import get_ip
from funding.bin.qr import QrCodeGenerator
from funding.factory import app, cache
from funding.orm import Proposal
from funding.factory import app, db_session
from funding.orm.orm import Proposal, User
@app.route('/api/1/proposals')
@ -68,31 +67,27 @@ def api_qr_generate(address):
return send_from_directory('static/qr', '%s.png' % address)
@app.route('/api/1/wowlite')
@app.route('/api/1/wowlight')
@endpoint.api(
parameter('version', type=str, location='args', required=True)
)
@cache.cached(timeout=600 * 6, make_cache_key=lambda version: f"api_wowlight_version_check_{version}")
def api_wowlight_version_check(version: str) -> bool:
def api_wowlight_version_check(version):
"""
Checks incoming wow-lite wallet version, returns False when the version is too old and needs to be upgraded.
Checks incoming wowlight wallet version, returns False when the version is
too old and needs to be upgraded (due to hard-forks)
:param version:
:return: bool
"""
url = "https://raw.githubusercontent.com/wownero/wow-lite-wallet/master/src/renderer/components/Landing/LandingPage.vue"
try:
resp = requests.get(url, headers={"User-Agent": "Mozilla 5.0"})
resp.raise_for_status()
content = resp.content.decode()
except:
return True # default to true
versions = {
'0.1.0': False,
'0.1.1': False,
'0.1.2': True
}
# parse latest version
current = next(re.finditer(r"wowlite\?version=(\d+.\d+.\d+)", content), None)
if not current:
if version not in versions:
return False
return version == current.group(1)
return versions[version]
@app.route('/api/1/wow/supply')

@ -0,0 +1,191 @@
from datetime import datetime
import requests
from requests.auth import HTTPDigestAuth
import settings
from funding.orm.orm import User
class Daemon:
def __init__(self, url=None, username=None, password=None):
self.url = url
self.username = username
self.password = password
if url is None:
self.url = settings.RPC_LOCATION
if username is None:
self.username = settings.RPC_USERNAME
if password is None:
self.password = settings.RPC_PASSWORD
self.headers = {"User-Agent": "Mozilla"}
def create_address(self, account_index, label_name):
data = {
'method': 'create_address',
'params': {'account_index': account_index, 'label': 'p_%s' % label_name},
'jsonrpc': '2.0',
'id': '0'
}
try:
result = self._make_request(data)
return result['result']
except:
return
def create_account(self, pid):
data = {
'method': 'create_account',
'params': {'label': 'p_%s' % pid},
'jsonrpc': '2.0',
'id': '0'
}
try:
result = self._make_request(data)
return result['result']
except:
return
def get_accounts(self, proposal_id:int = None):
data = {
'method': 'get_accounts',
'jsonrpc': '2.0',
'id': '0'
}
try:
result = self._make_request(data)
result = result['result']
if isinstance(proposal_id, int):
account_user = [acc for acc in result.get('subaddress_accounts', []) if acc['label'] == 'p_%d' % proposal_id]
if account_user:
return account_user[0]
else:
return
return result
except Exception as ex:
return
def get_address(self, account_index: int, proposal_id: int = None):
data = {
'method': 'getaddress',
'params': {'account_index': account_index},
'jsonrpc': '2.0',
'id': '0'
}
try:
result = self._make_request(data)
addresses = result['result']['addresses']
if isinstance(proposal_id, int):
address = [addy for addy in addresses if addy['label'] == 'p_%d' % proposal_id]
if address:
return address[0]
else:
return
return addresses
except:
return
def get_transfers_in(self, proposal):
account = self.get_accounts(proposal.id)
if not account:
raise Exception('wallet error; pid not found found')
index = account['account_index']
address = self.get_address(index, proposal_id=proposal.id)
if not address:
print('Could not fetch transfers_in for proposal id %d' % proposal.id)
return {'sum': [], 'txs': []}
data = {
"method": "get_transfers",
"params": {"pool": True, "in": True, "account_index": index},
"jsonrpc": "2.0",
"id": "0",
}
data = self._make_request(data)
data = data['result']
data = data.get('in', []) + data.get('pool', [])
# filter by current proposal
txs = [tx for tx in data if tx.get('address') == address['address']]
for d in txs:
d['amount_human'] = float(d['amount'])/1e11
return {
'sum': sum([float(z['amount'])/1e11 for z in txs]),
'txs': txs
}
def get_transfers_in_simple(self):
data = {
"method": "get_transfers",
"params": {"pool": True, "in": True},
"jsonrpc": "2.0",
"id": "0",
}
data = self._make_request(data)
data = data['result']
data = data.get('in', []) + data.get('pool', [])
for d in data:
d['datetime'] = datetime.fromtimestamp(d['timestamp'])
d['amount_human'] = float(d['amount'])/1e11
# most recent tx first
data = sorted(data, key=lambda k: k['datetime'], reverse=True)
return {
'sum': sum([float(z['amount'])/1e11 for z in data]),
'txs': data
}
def get_transfers_out(self, proposal):
account = self.get_accounts(proposal.id)
if not account:
raise Exception('wallet error; pid not found found')
index = account['account_index']
address = self.get_address(index, proposal_id=proposal.id)
if not address:
print('Could not fetch transfers_in for proposal id %d' % proposal.id)
return {'sum': [], 'txs': []}
data = {
"method": "get_transfers",
"params": {"pool": False, "out": True, "account_index": index},
"jsonrpc": "2.0",
"id": "0",
}
data = self._make_request(data)
data = data['result']
data = data.get('out', []) + data.get('pool', [])
# filter by current proposal
txs = [tx for tx in data if tx.get('address') == address['address']]
for d in txs:
d['amount_human'] = float(d['amount'])/1e11
return {
'sum': sum([float(z['amount'])/1e11 for z in txs]),
'txs': txs
}
def _make_request(self, data):
options = {'json': data, 'headers': self.headers}
if self.username and self.password:
options['auth'] = HTTPDigestAuth(settings.RPC_USERNAME, settings.RPC_PASSWORD)
r = requests.post(self.url, **options)
r.raise_for_status()
return r.json()

@ -34,8 +34,8 @@ class QrCodeGenerator:
:param color_to: gradient to color
:return:
"""
if len(address) not in settings.COIN_ADDRESS_LENGTH:
raise Exception(f'faulty address length, should be: {" or ".join(map(str, settings.COIN_ADDRESS_LENGTH))}')
if len(address) != settings.COIN_ADDRESS_LENGTH:
raise Exception('faulty address length')
if not dest:
dest = os.path.join(self.base, '%s.png' % address)

@ -1,64 +1,79 @@
import os
import json
from datetime import datetime, date
import pyqrcode
import requests
from flask import request
from flask import g, request
from flask.json import JSONEncoder
import settings
from funding.factory import cache
def json_encoder(obj):
if isinstance(obj, (datetime, date)):
return obj.isoformat()
raise TypeError("Type %s not serializable" % type(obj))
raise TypeError ("Type %s not serializable" % type(obj))
class Summary:
@staticmethod
@cache.cached(timeout=600, key_prefix="fetch_prices")
def fetch_prices():
return {
if hasattr(g, 'funding_prices') and g.coin_prices:
return g.coin_prices
from funding.factory import cache
cache_key = 'funding_prices'
data = cache.get(cache_key)
if data:
return data
data = {
'coin-btc': coin_btc_value(),
'btc-usd': price_cmc_btc_usd()
}
cache.set(cache_key, data=data, expiry=1200)
g.coin_prices = data
return data
@staticmethod
@cache.cached(timeout=600, key_prefix="funding_stats")
def fetch_stats():
from funding.factory import db
from funding.orm import Proposal, User
def fetch_stats(purge=False):
from funding.factory import db_session
from funding.orm.orm import Proposal, User, Comment
from funding.factory import cache
cache_key = 'funding_stats'
data = cache.get(cache_key)
if data and not purge:
return data
data = {}
categories = settings.FUNDING_CATEGORIES
statuses = settings.FUNDING_STATUSES.keys()
for cat in categories:
q = db.session.query(Proposal)
q = db_session.query(Proposal)
q = q.filter(Proposal.category == cat)
res = q.count()
data.setdefault('cats', {})
data['cats'][cat] = res
for status in statuses:
q = db.session.query(Proposal)
q = db_session.query(Proposal)
q = q.filter(Proposal.status == status)
res = q.count()
data.setdefault('statuses', {})
data['statuses'][status] = res
data.setdefault('users', {})
data['users']['count'] = db.session.query(User.id).count()
data['users']['count'] = db_session.query(User.id).count()
cache.set(cache_key, data=data, expiry=300)
return data
def price_cmc_btc_usd():
headers = {'User-Agent': 'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'}
try:
r = requests.get('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd', headers=headers)
print('request coinmarketcap')
r = requests.get('https://api.coinmarketcap.com/v2/ticker/1/?convert=USD', headers=headers)
r.raise_for_status()
data = r.json()
btc = next(c for c in data if c['symbol'] == 'btc')
return btc['current_price']
return r.json().get('data', {}).get('quotes', {}).get('USD', {}).get('price')
except:
return
@ -66,6 +81,7 @@ def price_cmc_btc_usd():
def coin_btc_value():
headers = {'User-Agent': 'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'}
try:
print('request TO')
r = requests.get('https://tradeogre.com/api/v1/ticker/BTC-WOW', headers=headers)
r.raise_for_status()
return float(r.json().get('high'))

@ -1,16 +1,17 @@
from flask import request
from datetime import datetime
from flask import session, g, request
import settings
from funding.bin.utils import Summary
from funding.factory import app, db
from funding.orm import User, Comment
from funding.factory import app, db_session
from funding.orm.orm import Proposal, User, Comment
@app.context_processor
def templating():
from flask_login import current_user
recent_comments = db.session.query(Comment).filter(Comment.automated == False).order_by(Comment.date_added.desc()).limit(8).all()
from flask.ext.login import current_user
recent_comments = db_session.query(Comment).filter(Comment.automated == False).order_by(Comment.date_added.desc()).limit(8).all()
summary_data = Summary.fetch_stats()
newest_users = db.session.query(User).filter(User.admin == False).order_by(User.registered_on.desc()).limit(5).all()
newest_users = db_session.query(User).filter(User.admin == False).order_by(User.registered_on.desc()).limit(5).all()
return dict(logged_in=current_user.is_authenticated,
current_user=current_user,
funding_categories=settings.FUNDING_CATEGORIES,
@ -27,6 +28,8 @@ def before_request():
@app.after_request
def after_request(res):
if hasattr(g, 'funding_prices'):
delattr(g, 'funding_prices')
res.headers.add('Accept-Ranges', 'bytes')
if request.full_path.startswith('/api/'):
@ -40,6 +43,11 @@ def after_request(res):
return res
@app.teardown_appcontext
def shutdown_session(**kwargs):
db_session.remove()
@app.errorhandler(404)
def error(err):
return 'Error', 404

@ -45,3 +45,17 @@ class JsonRedis(RedisSessionInterface):
redis=redis.Redis(**redis_args()),
key_prefix=key_prefix,
use_signer=use_signer)
class WowCache:
def __init__(self):
self._cache = redis.StrictRedis(**redis_args())
def get(self, key):
try:
return json.loads(self._cache.get(key))
except:
return {}
def set(self, key: str, data: dict, expiry=300):
self._cache.set(key, json.dumps(data, default=json_encoder), ex=expiry)

@ -1,94 +1,56 @@
# -*- coding: utf-8 -*-
import settings
from werkzeug.contrib.fixers import ProxyFix
from flask import Flask
from flask_caching import Cache
from flask_session import Session
from flask_sqlalchemy import SQLAlchemy
from werkzeug.middleware.proxy_fix import ProxyFix
import redis
app = None
sentry = None
cache = None
db = None
db_session = None
bcrypt = None
def _setup_cache(app: Flask):
global cache
cache_config = {
"CACHE_TYPE": "redis",
"CACHE_DEFAULT_TIMEOUT": 60,
"CACHE_KEY_PREFIX": "wow_cache_",
"CACHE_REDIS_PORT": settings.REDIS_PORT
}
if settings.REDIS_PASSWD:
cache_config["CACHE_REDIS_PASSWORD"] = settings.REDIS_PASSWD
app.config.from_mapping(cache_config)
cache = Cache(app)
def _setup_session(app: Flask):
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_COOKIE_NAME'] = 'bar'
app.config['SESSION_REDIS'] = redis.from_url(settings.REDIS_URI)
Session(app) # defaults to timedelta(days=31)
def _setup_db(app: Flask):
global db
uri = 'postgresql+psycopg2://{user}:{pw}@{url}/{db}'.format(
user=settings.PSQL_USER,
pw=settings.PSQL_PASS,
url=settings.PSQL_HOST,
db=settings.PSQL_DB)
app.config['SQLALCHEMY_DATABASE_URI'] = uri
db = SQLAlchemy(app)
import funding.orm
db.create_all()
def create_app():
global app
global db
global db_session
global sentry
global cache
global bcrypt
app = Flask(import_name=__name__,
static_folder='static',
template_folder='templates')
from funding.orm.connect import create_session
db_session = create_session()
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app)
app.config.from_object(settings)
app.config['PERMANENT_SESSION_LIFETIME'] = 2678400
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 30
app.secret_key = settings.SECRET
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
_setup_cache(app)
_setup_session(app)
_setup_db(app)
# flask-login
from flask_login import LoginManager
from flask.ext.login import LoginManager
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
from flask_bcrypt import Bcrypt
from flask.ext.bcrypt import Bcrypt
bcrypt = Bcrypt(app)
@login_manager.user_loader
def load_user(_id):
from funding.orm import User
from funding.orm.orm import User
return User.query.get(int(_id))
# session init
from funding.cache import JsonRedis, WowCache
app.session_interface = JsonRedis(key_prefix=app.config['SESSION_PREFIX'], use_signer=False)
cache = WowCache()
# import routes
from funding import routes
from funding import api
from funding.bin import utils_request
app.app_context().push()
return app
return app

@ -0,0 +1,18 @@
from datetime import datetime
import sqlalchemy as sa
from sqlalchemy.orm import scoped_session, sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base
import settings
def create_session():
from funding.orm.orm import base
engine = sa.create_engine(settings.SQLALCHEMY_DATABASE_URI, echo=False, encoding="latin")
session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
base.query = session.query_property()
base.metadata.create_all(bind=engine)
return session

@ -1,40 +1,26 @@
from datetime import datetime
import string
import random
import requests
from sqlalchemy.orm import relationship, backref
import sqlalchemy as sa
from sqlalchemy.orm import scoped_session, sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.types import Float
from sqlalchemy_json import MutableJson
import settings
from funding.factory import db, cache
base = declarative_base(name="Model")
class User(db.Model):
class User(base):
__tablename__ = "users"
id = db.Column('user_id', db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, index=True)
password = db.Column(db.String(60))
email = db.Column(db.String(50), unique=True, index=True)
registered_on = db.Column(db.DateTime)
admin = db.Column(db.Boolean, default=False)
id = sa.Column('user_id', sa.Integer, primary_key=True)
username = sa.Column(sa.String(20), unique=True, index=True)
password = sa.Column(sa.String(60))
email = sa.Column(sa.String(50), unique=True, index=True)
registered_on = sa.Column(sa.DateTime)
admin = sa.Column(sa.Boolean, default=False)
proposals = relationship('Proposal', back_populates="user")
comments = relationship("Comment", back_populates="user")
uuid = db.Column(UUID(as_uuid=True), unique=True)
def __init__(self, username, password, email, uuid=None):
def __init__(self, username, password, email):
from funding.factory import bcrypt
self.username = username
if password:
self.password = bcrypt.generate_password_hash(password).decode('utf8')
self.uuid = uuid
self.password = bcrypt.generate_password_hash(password).decode('utf8')
self.email = email
self.registered_on = datetime.utcnow()
@ -61,50 +47,47 @@ class User(db.Model):
return self.username
@classmethod
def add(cls, username, password=None, email=None, uuid=None):
from funding.factory import db
def add(cls, username, password, email):
from funding.factory import db_session
from funding.validation import val_username, val_email
try:
# validate incoming username/email
val_username(username)
if email:
val_email(email)
val_email(email)
user = User(username=username, password=password, email=email, uuid=uuid)
db.session.add(user)
db.session.commit()
db.session.flush()
user = User(username, password, email)
db_session.add(user)
db_session.commit()
db_session.flush()
return user
except Exception as ex:
db.session.rollback()
db_session.rollback()
raise
class Proposal(db.Model):
class Proposal(base):
__tablename__ = "proposals"
id = db.Column(db.Integer, primary_key=True)
archived = db.Column(db.Boolean, default=False)
headline = db.Column(db.VARCHAR, nullable=False)
content = db.Column(db.VARCHAR, nullable=False)
category = db.Column(db.VARCHAR, nullable=False)
date_added = db.Column(db.TIMESTAMP, default=datetime.now)
html = db.Column(db.VARCHAR)
last_edited = db.Column(db.TIMESTAMP)
id = sa.Column(sa.Integer, primary_key=True)
headline = sa.Column(sa.VARCHAR, nullable=False)
content = sa.Column(sa.VARCHAR, nullable=False)
category = sa.Column(sa.VARCHAR, nullable=False)
date_added = sa.Column(sa.TIMESTAMP, default=datetime.now)
html = sa.Column(sa.VARCHAR)
last_edited = sa.Column(sa.TIMESTAMP)
# the FFS target
funds_target = db.Column(db.Float, nullable=False)
funds_target = sa.Column(sa.Float, nullable=False)
# the FFS progress (cached)
funds_progress = db.Column(db.Float, nullable=False, default=0)
funds_progress = sa.Column(sa.Float, nullable=False, default=0)
# the FFS withdrawal amount (paid to the author)
funds_withdrew = db.Column(db.Float, nullable=False, default=0)
funds_withdrew = sa.Column(sa.Float, nullable=False, default=0)
# the FFS receiving and withdrawal addresses
addr_donation = db.Column(db.VARCHAR)
addr_receiving = db.Column(db.VARCHAR)
payment_id = db.Column(db.VARCHAR)
addr_donation = sa.Column(sa.VARCHAR)
addr_receiving = sa.Column(sa.VARCHAR)
# proposal status:
# 0: disabled
@ -112,9 +95,9 @@ class Proposal(db.Model):
# 2: funding required
# 3: wip
# 4: completed
status = db.Column(db.INTEGER, default=1)
status = sa.Column(sa.INTEGER, default=1)
user_id = db.Column(db.Integer, db.ForeignKey('users.user_id'))
user_id = sa.Column(sa.Integer, sa.ForeignKey('users.user_id'))
user = relationship("User", back_populates="proposals")
payouts = relationship("Payout", back_populates="proposal")
@ -148,12 +131,17 @@ class Proposal(db.Model):
@classmethod
def find_by_id(cls, pid: int):
from funding.factory import db
from funding.factory import db_session
q = cls.query
q = q.filter(Proposal.id == pid)
result = q.first()
if not result:
return
# check if we have a valid addr_donation generated. if not, make one.
if not result.addr_donation and result.status == 2:
from funding.bin.daemon import Daemon
Proposal.generate_donation_addr(result)
return result
@property
@ -166,21 +154,21 @@ class Proposal(db.Model):
@property
def comment_count(self):
from funding.factory import db
q = db.session.query(db.func.count(Comment.id))
from funding.factory import db_session
q = db_session.query(sa.func.count(Comment.id))
q = q.filter(Comment.proposal_id == self.id)
return q.scalar()
def get_comments(self):
from funding.factory import db
q = db.session.query(Comment)
from funding.factory import db_session
q = db_session.query(Comment)
q = q.filter(Comment.proposal_id == self.id)
q = q.filter(Comment.replied_to.is_(None))
q = q.filter(Comment.replied_to == None)
q = q.order_by(Comment.date_added.desc())
comments = q.all()
for c in comments:
q = db.session.query(Comment)
q = db_session.query(Comment)
q = q.filter(Comment.proposal_id == self.id)
q = q.filter(Comment.replied_to == c.id)
_c = q.all()
@ -190,62 +178,34 @@ class Proposal(db.Model):
return self
@property
def spends(self):
amount = sum([p.amount for p in self.payouts])
pct = amount / 100 * self.balance['sum']
return {"amount": amount, "pct": pct}
@property
@cache.cached(timeout=60, make_cache_key=lambda p: f"proposal_balance_{p.id}")
def balance(self):
"""This property retrieves the current funding status
of this proposal. It uses Redis cache to not spam the
daemon too much. Returns a nice dictionary containing
all relevant proposal funding info"""
from funding.bin.utils import Summary, coin_to_usd
from funding.factory import db
rtn = {'sum': 0.0, 'txs': [], 'pct': 0.0, 'available': 0}
if self.archived:
return rtn
try:
r = requests.get(f'http://{settings.RPC_HOST}:{settings.RPC_PORT}/json_rpc', json={
"jsonrpc": "2.0",
"id": "0",
"method": "get_payments",
"params": {
"payment_id": self.payment_id
}
})
r.raise_for_status()
blob = r.json()
assert 'result' in blob
assert 'payments' in blob['result']
assert isinstance(blob['result']['payments'], list)
except Exception as ex:
return rtn
txs = blob['result']['payments']
for tx in txs:
tx['amount_human'] = float(tx['amount'])/1e11
tx['txid'] = tx['tx_hash']
tx['type'] = 'in'
from funding.factory import cache, db_session
rtn = {'sum': 0.0, 'txs': [], 'pct': 0.0}
data = {
'sum': sum([float(z['amount']) / 1e11 for z in txs]),
'txs': txs
}
if not isinstance(data, dict):
print('error; get_transfers_in; %d' % self.id)
return rtn
cache_key = 'coin_balance_pid_%d' % self.id
data = cache.get(cache_key)
if not data:
from funding.bin.daemon import Daemon
try:
data = Daemon().get_transfers_in(proposal=self)
if not isinstance(data, dict):
print('error; get_transfers_in; %d' % self.id)
return rtn
cache.set(cache_key, data=data, expiry=60)
except Exception as ex:
print('error; get_transfers_in; %d' % self.id)
return rtn
prices = Summary.fetch_prices()
for tx in data['txs']:
if prices:
tx['amount_usd'] = coin_to_usd(amt=tx['amount_human'], btc_per_coin=prices['coin-btc'], usd_per_btc=prices['btc-usd'])
tx['datetime'] = datetime.fromtimestamp(tx['timestamp'])
if data.get('sum', 0.0):
data['pct'] = 100 / float(self.funds_target / data.get('sum', 0.0))
@ -256,8 +216,8 @@ class Proposal(db.Model):
if data['pct'] != self.funds_progress:
self.funds_progress = data['pct']
db.session.commit()
db.session.flush()
db_session.commit()
db_session.flush()
if data['available']:
data['remaining_pct'] = 100 / float(data['sum'] / data['available'])
@ -266,8 +226,74 @@ class Proposal(db.Model):
return data
@property
def spends(self):
from funding.bin.utils import Summary, coin_to_usd
from funding.factory import cache, db_session
rtn = {'sum': 0.0, 'txs': [], 'pct': 0.0}
cache_key = 'coin_spends_pid_%d' % self.id
data = cache.get(cache_key)
if not data:
from funding.bin.daemon import Daemon
try:
data = Daemon().get_transfers_out(proposal=self)
if not isinstance(data, dict):
print('error; get_transfers_out; %d' % self.id)
return rtn
cache.set(cache_key, data=data, expiry=60)
except:
print('error; get_transfers_out; %d' % self.id)
return rtn
data['remaining_pct'] = 0.0
prices = Summary.fetch_prices()
for tx in data['txs']:
if prices:
tx['amount_usd'] = coin_to_usd(amt=tx['amount_human'], btc_per_coin=prices['coin-btc'], usd_per_btc=prices['btc-usd'])
tx['datetime'] = datetime.fromtimestamp(tx['timestamp'])
if data.get('sum', 0.0):
data['pct'] = 100 / float(self.funds_target / data.get('sum', 0.0))
data['spent'] = data['sum']
else:
data['pct'] = 0.0
data['spent'] = 0.0
cache_key_in = 'coin_balance_pid_%d' % self.id
data_in = cache.get(cache_key_in)
if data_in and data['spent']:
data['remaining_pct'] = 100 / float(data_in['sum'] / data['spent'])
return data
@staticmethod
def generate_donation_addr(cls):
from funding.factory import db_session
from funding.bin.daemon import Daemon
if cls.addr_donation:
return cls.addr_donation
# check if the current user has an account in the wallet
account = Daemon().get_accounts(cls.id)
if not account:
account = Daemon().create_account(cls.id)
index = account['account_index']
address = account.get('address') or account.get('base_address')
if not address:
raise Exception('Cannot generate account/address for pid %d' % cls.id)
# assign donation address, commit to db
cls.addr_donation = address
db_session.commit()
db_session.flush()
return address
@classmethod
def find_by_args(cls, status: int = None, cat: str = None, limit: int = 20, offset=0):
from funding.factory import db_session
if isinstance(status, int) and status not in settings.FUNDING_STATUSES.keys():
raise NotImplementedError('invalid status')
if isinstance(cat, str) and cat not in settings.FUNDING_CATEGORIES:
@ -287,82 +313,71 @@ class Proposal(db.Model):
@classmethod
def search(cls, key: str):
key_ilike = f"%{key.replace('%', '')}%"
key_ilike = '%' + key.replace('%', '') + '%'
q = Proposal.query
q = q.filter(db.or_(
q = q.filter(sa.or_(
Proposal.headline.ilike(key_ilike),
Proposal.content.ilike(key_ilike)))
return q.all()
class Payout(db.Model):
class Payout(base):
__tablename__ = "payouts"
id = db.Column(db.Integer, primary_key=True)
id = sa.Column(sa.Integer, primary_key=True)
proposal_id = db.Column(db.Integer, db.ForeignKey('proposals.id'))
proposal_id = sa.Column(sa.Integer, sa.ForeignKey('proposals.id'))
proposal = relationship("Proposal", back_populates="payouts")
amount = db.Column(db.Integer, nullable=False)
to_address = db.Column(db.VARCHAR, nullable=False)
date_sent = db.Column(db.TIMESTAMP, default=datetime.now)
amount = sa.Column(sa.Integer, nullable=False)
to_address = sa.Column(sa.VARCHAR, nullable=False)
ix_proposal_id = db.Index("ix_proposal_id", proposal_id)
ix_proposal_id = sa.Index("ix_proposal_id", proposal_id)
@classmethod
def add(cls, proposal_id, amount, to_address):
# @TODO: validate that we can make this payout; check previous payouts
from flask_login import current_user
from flask.ext.login import current_user
if not current_user.admin:
raise Exception("user must be admin to add a payout")
from funding.factory import db
from funding.factory import db_session
try:
payout = Payout(propsal_id=proposal_id, amount=amount, to_address=to_address)
db.session.add(payout)
db.session.commit()
db.session.flush()
db_session.add(payout)
db_session.commit()
db_session.flush()
return payout
except Exception as ex:
db.session.rollback()
db_session.rollback()
raise
@staticmethod
def get_payouts(proposal_id):
from funding.factory import db
return db.session.query(Payout).filter(Payout.proposal_id == proposal_id).all()
@property
def as_tx(self):
return {
"block_height": "-",
"type": "out",
"amount_human": self.amount,
"amount": self.amount
}
from funding.factory import db_session
return db_session.query(Payout).filter(Payout.proposal_id == proposal_id).all()
class Comment(db.Model):
class Comment(base):
__tablename__ = "comments"
id = db.Column(db.Integer, primary_key=True)
id = sa.Column(sa.Integer, primary_key=True)
proposal_id = db.Column(db.Integer, db.ForeignKey('proposals.id'))
proposal_id = sa.Column(sa.Integer, sa.ForeignKey('proposals.id'))
proposal = relationship("Proposal", back_populates="comments")
user_id = db.Column(db.Integer, db.ForeignKey('users.user_id'), nullable=False)
user_id = sa.Column(sa.Integer, sa.ForeignKey('users.user_id'), nullable=False)
user = relationship("User", back_populates="comments")
date_added = db.Column(db.TIMESTAMP, default=datetime.now)
date_added = sa.Column(sa.TIMESTAMP, default=datetime.now)
message = db.Column(db.VARCHAR, nullable=False)
replied_to = db.Column(db.ForeignKey("comments.id"))
message = sa.Column(sa.VARCHAR, nullable=False)
replied_to = sa.Column(sa.ForeignKey("comments.id"))
locked = db.Column(db.Boolean, default=False)
locked = sa.Column(sa.Boolean, default=False)
automated = db.Column(db.Boolean, default=False)
automated = sa.Column(sa.Boolean, default=False)
ix_comment_replied_to = db.Index("ix_comment_replied_to", replied_to)
ix_comment_proposal_id = db.Index("ix_comment_proposal_id", proposal_id)
ix_comment_replied_to = sa.Index("ix_comment_replied_to", replied_to)
ix_comment_proposal_id = sa.Index("ix_comment_proposal_id", proposal_id)
@property
def message_html(self):
@ -375,30 +390,28 @@ class Comment(db.Model):
@staticmethod
def find_by_id(cid: int):
from funding.factory import db
return db.session.query(Comment).filter(Comment.id == cid).first()
from funding.factory import db_session
return db_session.query(Comment).filter(Comment.id == cid).first()
@staticmethod
def remove(cid: int):
from funding.factory import db
from flask_login import current_user
comment = Comment.get(cid=cid)
if current_user.id != comment.user_id and not current_user.admin:
from funding.factory import db_session
from flask.ext.login import current_user
if current_user.id != user_id and not current_user.admin:
raise Exception("no rights to remove this comment")
comment = Comment.get(cid=cid)
try:
comment.delete()
db.session.commit()
db.session.flush()
db_session.commit()
db_session.flush()
except:
db.session.rollback()
db_session.rollback()
raise
@staticmethod
def lock(cid: int):
from funding.factory import db
from flask_login import current_user
from funding.factory import db_session
from flask.ext.login import current_user
if not current_user.admin:
raise Exception("admin required")
comment = Comment.find_by_id(cid=cid)
@ -406,17 +419,17 @@ class Comment(db.Model):
raise Exception("comment by that id not found")
comment.locked = True
try:
db.session.commit()
db.session.flush()
db_session.commit()
db_session.flush()
return comment
except:
db.session.rollback()
db_session.rollback()
raise
@classmethod
def add_comment(cls, pid: int, user_id: int, message: str, cid: int = None, message_id: int = None, automated=False):
from flask_login import current_user
from funding.factory import db
from flask.ext.login import current_user
from funding.factory import db_session
if not message:
raise Exception("empty message")
@ -435,7 +448,7 @@ class Comment(db.Model):
comment.replied_to = parent.id
else:
try:
user = db.session.query(User).filter(User.id == user_id).first()
user = db_session.query(User).filter(User.id == user_id).first()
if not user:
raise Exception("no user by that id")
comment = next(c for c in user.comments if c.id == message_id)
@ -447,10 +460,10 @@ class Comment(db.Model):
raise Exception("unknown error")
try:
comment.message = message
db.session.add(comment)
db.session.commit()
db.session.flush()
db_session.add(comment)
db_session.commit()
db_session.flush()
except Exception as ex:
db.session.rollback()
db_session.rollback()
raise Exception(str(ex))
return comment
return comment

@ -1,15 +1,13 @@
import uuid
from datetime import datetime
import requests
from flask import request, redirect, render_template, url_for, flash, make_response, send_from_directory, jsonify, session
from flask_login import login_user , logout_user , current_user
from flask import request, redirect, Response, abort, render_template, url_for, flash, make_response, send_from_directory, jsonify
from flask.ext.login import login_user , logout_user , current_user, login_required, current_user
from dateutil.parser import parse as dateutil_parse
from flask_yoloapi import endpoint, parameter
import settings
from funding.factory import app, db, cache
from funding.orm import Proposal, User, Comment
from funding.factory import app, db_session
from funding.orm.orm import Proposal, User, Comment
@app.route('/')
@ -29,7 +27,7 @@ def api():
@app.route('/proposal/add/disclaimer')
def proposal_add_disclaimer():
return make_response(render_template('proposal/disclaimer.html'))
return make_response(render_template(('proposal/disclaimer.html')))
@app.route('/proposal/add')
@ -65,7 +63,7 @@ def proposal_comment(pid, text, cid):
@app.route('/proposal/<int:pid>/comment/<int:cid>')
def propsal_comment_reply(cid, pid):
from funding.orm import Comment
from funding.orm.orm import Comment
c = Comment.find_by_id(cid)
if not c or c.replied_to:
return redirect(url_for('proposal', pid=pid))
@ -81,9 +79,9 @@ def propsal_comment_reply(cid, pid):
@app.route('/proposal/<int:pid>')
def proposal(pid):
p = Proposal.find_by_id(pid=pid)
p.get_comments()
if not p:
return make_response(redirect(url_for('proposals')))
p.get_comments()
return make_response(render_template(('proposal/proposal.html'), proposal=p))
@ -157,9 +155,9 @@ def proposal_api_add(title, content, pid, funds_target, addr_receiving, category
except Exception as ex:
return make_response(jsonify('letters detected'),500)
if funds_target < 1:
return make_response(jsonify('Proposal asking less than 1 error :)'), 500)
if len(addr_receiving) not in settings.COIN_ADDRESS_LENGTH:
return make_response(jsonify(f'Faulty address, should be of length: {" or ".join(map(str, settings.COIN_ADDRESS_LENGTH))}'), 500)
return make_response(jsonify('Proposal asking less than 1 error :)'), 500)
if len(addr_receiving) != settings.COIN_ADDRESS_LENGTH:
return make_response(jsonify('Faulty address, should be of length 72'), 500)
p = Proposal(headline=title, content=content, category='misc', user=current_user)
p.html = html
@ -169,32 +167,18 @@ def proposal_api_add(title, content, pid, funds_target, addr_receiving, category
p.category = category
p.status = status
# generate integrated address
try:
r = requests.get(f'http://{settings.RPC_HOST}:{settings.RPC_PORT}/json_rpc', json={
"jsonrpc": "2.0",
"id": "0",
"method": "make_integrated_address"
})
r.raise_for_status()
blob = r.json()
assert 'result' in blob
assert 'integrated_address' in blob['result']
assert 'payment_id' in blob['result']
except Exception as ex:
raise
p.addr_donation = blob['result']['integrated_address']
p.payment_id = blob['result']['payment_id']
db_session.add(p)
db_session.commit()
db_session.flush()
db.session.add(p)
p.addr_donation = Proposal.generate_donation_addr(p)
db.session.commit()
db.session.flush()
db_session.commit()
db_session.flush()
# reset cached stuffz
cache.delete('funding_stats')
# reset cached statistics
from funding.bin.utils import Summary
Summary.fetch_stats(purge=True)
return make_response(jsonify({'url': url_for('proposal', pid=p.id)}))
@ -205,7 +189,7 @@ def proposal_edit(pid):
if not p:
return make_response(redirect(url_for('proposals')))
return make_response(render_template('proposal/edit.html', proposal=p))
return make_response(render_template(('proposal/edit.html'), proposal=p))
@app.route('/search')
@ -221,7 +205,7 @@ def search(key=None):
@app.route('/user/<path:name>')
def user(name):
q = db.session.query(User)
q = db_session.query(User)
q = q.filter(User.username == name)
user = q.first()
return render_template('user.html', user=user)
@ -256,7 +240,8 @@ def proposals(status, page, cat):
@app.route('/donate')
def donate():
return "devfund page currently not working :D"
from funding.bin.daemon import Daemon
from funding.factory import cache, db_session
data_default = {'sum': 0, 'txs': []}
cache_key = 'devfund_txs_in'
@ -295,100 +280,18 @@ def register():
try:
user = User.add(username, password, email)
flash('Successfully registered. No confirmation email required. You can login!')
cache.delete('funding_stats') # reset cached stuffz
return redirect(url_for('login'))
except Exception as ex:
flash('Could not register user. Probably a duplicate username or email that already exists.', 'error')
return make_response(render_template('register.html'))
if settings.OPENID_ENABLED:
@app.route("/wow-auth/")
def wow_auth():
assert "state" in request.args
assert "session_state" in request.args
assert "code" in request.args
# verify state
if not session.get('auth_state'):
return "session error", 500
if request.args['state'] != session['auth_state']:
return "attack detected :)", 500
# with this authorization code we can fetch an access token
url = f"{settings.OPENID_URL}/token"
data = {
"grant_type": "authorization_code",
"code": request.args["code"],
"redirect_uri": settings.OPENID_REDIRECT_URI,
"client_id": settings.OPENID_CLIENT_ID,
"client_secret": settings.OPENID_CLIENT_SECRET,
"state": request.args['state']
}
try:
resp = requests.post(url, data=data)
resp.raise_for_status()
except:
return "something went wrong :( #1", 500
data = resp.json()
assert "access_token" in data
assert data.get("token_type") == "bearer"
access_token = data['access_token']
# fetch user information with the access token
url = f"{settings.OPENID_URL}/userinfo"
try:
resp = requests.post(url, headers={"Authorization": f"Bearer {access_token}"})
resp.raise_for_status()
user_profile = resp.json()
except:
return "something went wrong :( #2", 500
username = user_profile.get("preferred_username")
sub = user_profile.get("sub")
if not username:
return "something went wrong :( #3", 500
sub_uuid = uuid.UUID(sub)
user = User.query.filter_by(username=username).first()
if user:
if not user.uuid:
user.uuid = sub_uuid
db.session.commit()
db.session.flush()
else:
user = User.add(username=username,
password=None, email=None, uuid=sub_uuid)
login_user(user)
response = redirect(request.args.get('next') or url_for('index'))
response.headers['X-Set-Cookie'] = True
return response
@app.route('/login', methods=['GET', 'POST'])
@endpoint.api(
parameter('username', type=str, location='form', required=False),
parameter('password', type=str, location='form', required=False)
parameter('username', type=str, location='form'),
parameter('password', type=str, location='form')
)
def login(username, password):
if settings.OPENID_ENABLED:
state = uuid.uuid4().hex
session['auth_state'] = state
url = f"{settings.OPENID_URL}/auth?" \
f"client_id={settings.OPENID_CLIENT_ID}&" \
f"redirect_uri={settings.OPENID_REDIRECT_URI}&" \
f"response_type=code&" \
f"state={state}"
return redirect(url)
if not username or not password:
flash('Enter username/password pl0x')
return make_response(render_template('login.html'))
if request.method == 'GET':
return make_response(render_template('login.html'))
@ -409,9 +312,10 @@ def logout():
logout_user()
response = redirect(request.args.get('next') or url_for('login'))
response.headers['X-Set-Cookie'] = True
flash('Logout successfully')
return response
@app.route('/static/<path:path>')
def static_route(path):
return send_from_directory('static', path)
return send_from_directory('static', path)

@ -0,0 +1,8 @@
from flask.ext.login import login_required
from wowfunding.factory import app, db_session
@app.route('/admin/index')
@login_required
def admin_home():
return 'yep'

@ -60,7 +60,6 @@ body {
color: #ff0000;
font-size: 18px;
margin-bottom: 0;
user-select: all;
}
.proposal_content blockquote {
@ -651,6 +650,10 @@ ul.b {
color: #999;
}
.tx_item .height {
float:right
}
.tx_item .height b {
font-size:14px;
}

@ -26,7 +26,7 @@
When you encounter problems; please visit #wownero on chat.freenode.org
</p>
<p>
AEON (<a target="_blank" href="https://github.com/camthegeek">camthegeek</a>) has contributed commits <a href="http://github.com/skftn/wownero-wfs">upstream</a>; WOW is grateful.
AEON (<a target="_blank" href="https://github.com/camthegeek">camthegeek</a>) has contributed commits to <a href="http://github.com/skftn/wownero-wfs">upstream</a>; WOW is grateful.
</p>
</div>

@ -64,7 +64,7 @@
<!-- Footer -->
<footer class="bg-dark footer">
<div class="container">
<p class="m-0 text-center text-white">WOW 2018-2020</p>
<p class="m-0 text-center text-white">WOW 2018</p>
</div>
</footer>

@ -27,7 +27,7 @@
</div>
<hr>
<div class="row">
<div class="col-lg-4">
<div class="col-lg-3">
<form class="form-horizontal" action="" method=post>
<div class="form-group">
<label class="sr-only" for="inlineFormInput">Name</label>
@ -46,13 +46,10 @@
<div class="form-group">
<button type="submit" class="btn btn-primary btn-sm">Login</button>
</div>
<div class="form-group">
<small>Forgot your password? Out of luck! Create a new account lol</small>
</div>
</form>
</div>
<div class="col-lg-5">
<a href="/register">Register here</a>
<a href="/register">Or register here</a>
</div>
</div>
</div>

@ -1,28 +1,20 @@
{% macro tx_item(tx) %}
<li class="list-group-item tx_item">
<span class="datetime">
{{tx['datetime'].strftime('%Y-%m-%d %H:%M')}}
</span>
<span class="height">
<b>Blockheight</b>:
{% if tx['type'] == 'pool' %}
soon^tm
{% elif tx['type'] == 'out' %}
<small>hidden</small>
{% else %}
{{tx['block_height']}}
{% endif %}
{% if tx['type'] == 'pool' %}
soon^tm
{% else %}
{{tx['height']}}
{% endif %}
</span>
<br>
{% if tx['type'] in ['in', 'pool'] %}
<a target="_blank" href="https://explore.wownero.com/tx/{{tx['txid']}}">{{tx['txid'][:32]}}...</a>
{% else %}
{% set lulz = [
'vodka', 'hookers', 'booze', 'strippers', 'new lambo',
'new ferrari', 'new villa', 'new vacation home', 'new tesla',
'new watch', 'new home cinema set', 'throphy wife', 'drugs']
%}
<a style="font-size:11px;" href="#">Sent to author. Enjoy the {{ lulz|random }}!</a>
{% endif %}
<span class="amount{% if tx['type'] in ['pool', 'in'] %} in{% endif %}">
{% if tx['type'] in ['in', 'pool'] %}
+

@ -97,13 +97,11 @@
{{proposal.balance['available']|round(3) or 0 }} WOW Raised
{% set remaining = proposal.funds_target - proposal.balance['available']|float|round(3) %}
<small>
{% if remaining > 0 %}
({{ (proposal.funds_target - proposal.balance['available']|float|round(3)|int) }} WOW until goal)
{% elif remaining < 0 %}
({{ (proposal.balance['available']-proposal.funds_target|float|round(3)|int) }} WOW past goal!)
{% endif %}
</small>
<div class="progress">
<div class="progress-bar progress-warning progress-bar" style="width: {{proposal.balance['pct']}}%;">
@ -115,16 +113,15 @@
<br/>
<div class="col-lg-8">
{{ proposal.spends['amount'] }} WOW Paid out <small>({{ proposal.spends['pct']|round(1) }}%)</small>
{{proposal.spends['spent']|round(3) or 0}} WOW Paid out
<div class="progress">
<!-- @todo: spends remaining pct -->
<div class="progress-bar progress-warning progress-bar" style="width: {{ proposal.spends['pct'] }}%;">
<div class="progress-bar progress-warning progress-bar" style="width: {{proposal.spends['remaining_pct']}}%;">
</div>
</div>
<hr>
</div>
<div class="col-lg-8">
{{ (proposal.balance['available']-proposal.spends['amount']) |round(3) or 0}} WOW Available for payout :-)
{{(proposal.balance['available']-proposal.spends['spent']) |round(3) or 0}} WOW Available to Payout
<div class="progress">
<div class="progress-bar progress-warning progress-bar" style="width: {{proposal.balance['remaining_pct']}}%;">
</div>
@ -188,15 +185,15 @@
<!-- /.row -->
{% endif %}
{% if proposal.payouts %}
{% if proposal.spends['txs'] %}
<div class="row">
<div class="col-md-12">
<div class="card my-6" id="incoming_txs">
<h5 class="card-header">Outgoing transactions</h5>
<h5 class="card-header">Outgoing transactions <small>({{proposal.spends['txs']|length}})</small></h5>
<div class="card-body">
<ul class="list-group">
{% for payout in proposal.payouts %}
{{ tx_item(payout.as_tx) }}
{% for tx in proposal.spends['txs'] %}
{{ tx_item(tx) }}
{% endfor %}
</ul>
</div>

@ -1,5 +1,5 @@
sqlalchemy==1.3.4
flask
sqlalchemy==1.2.7
flask==0.12.3
flask-yoloapi==0.1.5
flask_session
flask-login
@ -12,6 +12,3 @@ requests
pyqrcode
pypng
pillow-simd
Flask-Caching
flask-sqlalchemy
sqlalchemy_json

@ -7,19 +7,11 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SECRET = ''
DEBUG = True
COIN_ADDRESS_LENGTH = [97, 108]
COIN_ADDRESS_LENGTH = 97
COINCODE = ''
PSQL_HOST = "127.0.0.1:5432"
PSQL_DB = ''
PSQL_USER = 'postgres'
PSQL_USER = ''
PSQL_PASS = ''
OPENID_ENABLED = False
OPENID_REALM = "master"
OPENID_URL = f"https://login.wownero.com/auth/realms/{OPENID_REALM}/protocol/openid-connect"
OPENID_CLIENT_ID = ""
OPENID_CLIENT_SECRET = ""
OPENID_REDIRECT_URI = "http://0.0.0.0:5004/wow-auth/"
PSQL_DB = ''
SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql://{user}:{pw}@localhost/{db}').format(user=PSQL_USER, pw=PSQL_PASS, db=PSQL_DB)
@ -29,10 +21,6 @@ SESSION_PREFIX = os.environ.get('{coincode}_SESSION_PREFIX', 'session:').format(
REDIS_HOST = os.environ.get('REDIS_HOST', '127.0.0.1')
REDIS_PORT = int(os.environ.get('REDIS_PORT', 6379))
REDIS_PASSWD = os.environ.get('REDIS_PASSWD', None)
REDIS_URI = "redis://"
if REDIS_PASSWD:
REDIS_URI += f":{REDIS_PASSWD}"
REDIS_URI += f"@{REDIS_HOST}:{REDIS_PORT}"
BIND_HOST = os.environ.get("BIND_HOST", "0.0.0.0")
if not BIND_HOST:

Loading…
Cancel
Save