Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 169 additions & 8 deletions funding/factory.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import os, re, json, logging, hashlib, asyncio
from typing import List, Set, Optional
from typing import List, Set, Optional, Union
from datetime import datetime

import json
import aiohttp
import timeago
from quart import jsonify
from peewee import PostgresqlDatabase, SqliteDatabase, ProgrammingError
from playhouse.shortcuts import ReconnectMixin
from aiocryptocurrency.coins import Coin, SUPPORTED_COINS
from aiocryptocurrency import TransactionSet, Transaction

from aiocryptocurrency.coins.nero import Wownero, Monero
from aiocryptocurrency.coins.firo import Firo
from quart import Quart, render_template, session, request, g
from quart_schema import RequestSchemaValidationError
from quart_session import Session
Expand All @@ -18,12 +21,173 @@
from funding.utils.rates import Rates
import settings

class Firo(Coin):
def __init__(self):
super(Firo, self).__init__()
self.host = '127.0.0.1'
self.port = 8888
self.basic_auth: Optional[tuple[str]] = None
self.url = None

async def send(self, address: str, amount: float) -> str:
"""returns txid"""
if amount <= 0:
raise Exception("amount cannot be zero or less")

data = {
"method": "sendtoaddress",
"params": [address, amount]
}

blob = await self._make_request(data=data)
return blob['result']

async def mintspark(self, sparkAddress: str, amount: float, memo: str = "") -> str:
"""returns txid"""
if amount <= 0:
raise Exception("amount cannot be zero or less")

params = {
sparkAddress: {
"amount": amount,
"memo": memo
}
}

data = {
"method": "mintspark",
"params": [params]
}

blob = await self._make_request(data=data)

txids = blob['result']
if isinstance(txids, list):
txids = ', '.join(txids)

return txids


async def create_address(self) -> dict:
"""Returns both a transparent address and a Spark address."""

address_data = {
"method": "getnewaddress"
}
address_blob = await self._make_request(data=address_data)
address = address_blob['result']

if address is None or not isinstance(address, str):
raise Exception("Invalid standard address result")

return {
"address": address,
}

async def check_address(self, addr_receiving: str) -> str:
"""Check address validation."""

validation_data = {
"method": "validateaddress",
"params": [addr_receiving]
}
validation_blob = await self._make_request(data=validation_data)

return validation_blob['result']

async def tx_details(self, txid: str):
if not isinstance(txid, str) or not txid:
raise Exception("bad address")

data = {
"method": "gettransaction",
"params": [txid]
}

blob = await self._make_request(data=data)
return blob['result']

async def list_txs(self, address: str = None, payment_id: str = None, minimum_confirmations: int = 3) -> Optional[TransactionSet]:
txset = TransactionSet()
if not isinstance(address, str) or not address:
raise Exception("bad address")

results = await self._make_request(data={
"method": "listreceivedbyaddress",
"params": [minimum_confirmations]
})

if not isinstance(results.get('result'), list):
return txset

try:
result = [r for r in results['result'] if r['address'] == address][0]
except Exception as ex:
return txset

for txid in result.get('txids', []):
# fetch tx details
tx = await self.tx_details(txid)

# fetch blockheight
tx['blockheight'] = await self.blockheight(tx['blockhash'])
date = datetime.fromtimestamp(tx['blocktime'])

txset.add(Transaction(amount=tx['amount'],
txid=tx['txid'],
date=date,
blockheight=tx['blockheight'],
direction='in',
confirmations=tx['confirmations']))

return txset

async def blockheight(self, blockhash: str) -> int:
"""blockhash -> blockheight"""
if not isinstance(blockhash, str) or not blockhash:
raise Exception("bad address")

data = {
"method": "getblock",
"params": [blockhash]
}
blob = await self._make_request(data=data)

height = blob['result'].get('height', 0)
return height

async def _generate_url(self) -> None:
self.url = f'http://{self.host}:{self.port}/'

async def _make_request(self, data: dict = None) -> dict:
await self._generate_url()

opts = {
"headers": {
"User-Agent": self.user_agent
}
}

if self.basic_auth:
opts['auth'] = await self._make_basic_auth()

async with aiohttp.ClientSession(**opts) as session:
async with session.post(self.url, json=data) as resp:
if resp.status == 401:
raise Exception("Unauthorized")
blob = await resp.json()
if 'result' not in blob:
if blob:
blob = json.dumps(blob, indent=4, sort_keys=True)
raise Exception(f"Invalid response: {blob}")
return blob

cache = None
peewee = None
rates = Rates()
app: Optional[Quart] = None
openid: Optional[OpenID] = None
crypto_provider: Optional[Firo] = None
crypto_provider: Optional[Firo] = Firo()
coin: Optional[dict] = None
discourse = Discourse()
proposal_task = None
Expand All @@ -47,7 +211,6 @@ def __init__(self, *args, **kwargs):
port=settings.DB_PORT
)


async def _setup_postgres(app: Quart):
import peewee
import funding.models.database
Expand Down Expand Up @@ -115,7 +278,6 @@ async def _setup_crypto(app: Quart):
if settings.COIN_RPC_AUTH:
crypto_provider.basic_auth = settings.COIN_RPC_AUTH


async def _setup_cache(app: Quart):
global cache
app.config['SESSION_TYPE'] = 'redis'
Expand Down Expand Up @@ -148,7 +310,6 @@ async def page_not_found(e):
def create_app():
global app
app = Quart(__name__)

app.logger.setLevel(logging.INFO)
app.secret_key = settings.APP_SECRET

Expand Down Expand Up @@ -197,7 +358,7 @@ def template_variables():

@app.errorhandler(RequestSchemaValidationError)
async def handle_request_validation_error(error):
return {"errors": error.validation_error.json()}, 400
return jsonify({"errors": str(error)}), 400

@app.before_serving
async def startup():
Expand Down
7 changes: 7 additions & 0 deletions funding/models/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,13 @@ async def upsert(cls, data: ProposalUpsert):
if proposal._is_new:
proposal.slug = proposal.generate_slug(data.title)

blob = await crypto_provider.check_address(data.addr_receiving)
if data.addr_receiving[0] == 'a' and not blob['isvalid']:
raise Exception("Invalid Address")

elif data.addr_receiving[0] == 's' and not blob['isvalidSpark']:
raise Exception("Invalid Spark Address")

proposal.set_addr_receiving(data.addr_receiving, user)
await proposal.set_category(data.category, user)
await proposal.set_status(data.status, user)
Expand Down
1 change: 0 additions & 1 deletion funding/proposals/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

bp_proposals_api = Blueprint('bp_proposals_api', __name__, url_prefix='/api/proposals')


@bp_proposals_api.post("/upsert")
@validate_request(ProposalUpsert, source=DataSource.JSON)
@login_required
Expand Down
2 changes: 1 addition & 1 deletion funding/proposals/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class ProposalUpsert(BaseModel):
category: ProposalCategory
status: Optional[ProposalStatus]
discourse_topic_id: Optional[int]
addr_receiving: constr(min_length=8, max_length=128)
addr_receiving: constr(min_length=8, max_length=255)

class Config:
use_enum_values = False
10 changes: 7 additions & 3 deletions funding/proposals/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ async def funds(slug: str = None):

return await render_template("proposals/funds.html", proposal=proposal, crumbs=crumbs)


@bp_proposals.post("/<path:slug>/funds/transfer")
@validate_request(ProposalFundsTransfer, source=DataSource.FORM)
@admin_required
Expand Down Expand Up @@ -103,9 +102,14 @@ async def funds_transfer(data: ProposalFundsTransfer, slug: str = None):

destination = data.destination.strip()
try:
txid = await crypto_provider.send(
address=destination,
if (destination[0] == 's'):
txid = await crypto_provider.mintspark(
sparkAddress=destination,
amount=amount)
else:
txid = await crypto_provider.send(
address=destination,
amount=amount)
except Exception as ex:
return f"Error sending to '{destination}': {ex}"

Expand Down