Skip to content

Commit ec7e2ab

Browse files
committed
Spark address support
1 parent 579a7ec commit ec7e2ab

File tree

5 files changed

+168
-13
lines changed

5 files changed

+168
-13
lines changed

funding/factory.py

Lines changed: 153 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import os, re, json, logging, hashlib, asyncio
2-
from typing import List, Set, Optional
2+
from typing import List, Set, Optional, Union
33
from datetime import datetime
4-
4+
import json
5+
import aiohttp
56
import timeago
7+
from quart import jsonify
68
from peewee import PostgresqlDatabase, SqliteDatabase, ProgrammingError
79
from playhouse.shortcuts import ReconnectMixin
810
from aiocryptocurrency.coins import Coin, SUPPORTED_COINS
11+
from aiocryptocurrency import TransactionSet, Transaction
12+
913
from aiocryptocurrency.coins.nero import Wownero, Monero
10-
from aiocryptocurrency.coins.firo import Firo
1114
from quart import Quart, render_template, session, request, g
1215
from quart_schema import RequestSchemaValidationError
1316
from quart_session import Session
@@ -18,12 +21,157 @@
1821
from funding.utils.rates import Rates
1922
import settings
2023

24+
class Firo(Coin):
25+
def __init__(self):
26+
super(Firo, self).__init__()
27+
self.host = '127.0.0.1'
28+
self.port = 8888
29+
self.basic_auth: Optional[tuple[str]] = None
30+
self.url = None
31+
32+
async def send(self, address: str, amount: float) -> str:
33+
"""returns txid"""
34+
if amount <= 0:
35+
raise Exception("amount cannot be zero or less")
36+
37+
data = {
38+
"method": "sendtoaddress",
39+
"params": [address, amount]
40+
}
41+
42+
blob = await self._make_request(data=data)
43+
return blob['result']
44+
45+
async def mintspark(self, sparkAddress: str, amount: float, memo: str = "") -> str:
46+
"""returns txid"""
47+
if amount <= 0:
48+
raise Exception("amount cannot be zero or less")
49+
50+
params = {
51+
sparkAddress: {
52+
"amount": amount,
53+
"memo": memo
54+
}
55+
}
56+
57+
data = {
58+
"method": "mintspark",
59+
"params": [params]
60+
}
61+
62+
blob = await self._make_request(data=data)
63+
return blob['result']
64+
65+
66+
async def create_address(self) -> dict:
67+
"""Returns both a transparent address and a Spark address."""
68+
69+
address_data = {
70+
"method": "getnewaddress"
71+
}
72+
address_blob = await self._make_request(data=address_data)
73+
address = address_blob['result']
74+
75+
if address is None or not isinstance(address, str):
76+
raise Exception("Invalid standard address result")
77+
78+
return {
79+
"address": address,
80+
}
81+
82+
async def tx_details(self, txid: str):
83+
if not isinstance(txid, str) or not txid:
84+
raise Exception("bad address")
85+
86+
data = {
87+
"method": "gettransaction",
88+
"params": [txid]
89+
}
90+
91+
blob = await self._make_request(data=data)
92+
return blob['result']
93+
94+
async def list_txs(self, address: str = None, payment_id: str = None, minimum_confirmations: int = 3) -> Optional[TransactionSet]:
95+
txset = TransactionSet()
96+
if not isinstance(address, str) or not address:
97+
raise Exception("bad address")
98+
99+
results = await self._make_request(data={
100+
"method": "listreceivedbyaddress",
101+
"params": [minimum_confirmations]
102+
})
103+
104+
if not isinstance(results.get('result'), list):
105+
return txset
106+
107+
try:
108+
result = [r for r in results['result'] if r['address'] == address][0]
109+
except Exception as ex:
110+
return txset
111+
112+
for txid in result.get('txids', []):
113+
# fetch tx details
114+
tx = await self.tx_details(txid)
115+
116+
# fetch blockheight
117+
tx['blockheight'] = await self.blockheight(tx['blockhash'])
118+
date = datetime.fromtimestamp(tx['blocktime'])
119+
120+
txset.add(Transaction(amount=tx['amount'],
121+
txid=tx['txid'],
122+
date=date,
123+
blockheight=tx['blockheight'],
124+
direction='in',
125+
confirmations=tx['confirmations']))
126+
127+
return txset
128+
129+
async def blockheight(self, blockhash: str) -> int:
130+
"""blockhash -> blockheight"""
131+
if not isinstance(blockhash, str) or not blockhash:
132+
raise Exception("bad address")
133+
134+
data = {
135+
"method": "getblock",
136+
"params": [blockhash]
137+
}
138+
blob = await self._make_request(data=data)
139+
140+
height = blob['result'].get('height', 0)
141+
return height
142+
143+
async def _generate_url(self) -> None:
144+
self.url = f'http://{self.host}:{self.port}/'
145+
146+
async def _make_request(self, data: dict = None) -> dict:
147+
await self._generate_url()
148+
149+
opts = {
150+
"headers": {
151+
"User-Agent": self.user_agent
152+
}
153+
}
154+
155+
if self.basic_auth:
156+
opts['auth'] = await self._make_basic_auth()
157+
158+
async with aiohttp.ClientSession(**opts) as session:
159+
async with session.post(self.url, json=data) as resp:
160+
if resp.status == 401:
161+
raise Exception("Unauthorized")
162+
blob = await resp.json()
163+
if 'result' not in blob:
164+
if blob:
165+
blob = json.dumps(blob, indent=4, sort_keys=True)
166+
raise Exception(f"Invalid response: {blob}")
167+
return blob
168+
21169
cache = None
22170
peewee = None
23171
rates = Rates()
24172
app: Optional[Quart] = None
25173
openid: Optional[OpenID] = None
26-
crypto_provider: Optional[Firo] = None
174+
crypto_provider: Optional[Firo] = Firo()
27175
coin: Optional[dict] = None
28176
discourse = Discourse()
29177
proposal_task = None
@@ -47,7 +195,6 @@ def __init__(self, *args, **kwargs):
47195
port=settings.DB_PORT
48196
)
49197

50-
51198
async def _setup_postgres(app: Quart):
52199
import peewee
53200
import funding.models.database
@@ -115,7 +262,6 @@ async def _setup_crypto(app: Quart):
115262
if settings.COIN_RPC_AUTH:
116263
crypto_provider.basic_auth = settings.COIN_RPC_AUTH
117264

118-
119265
async def _setup_cache(app: Quart):
120266
global cache
121267
app.config['SESSION_TYPE'] = 'redis'
@@ -148,7 +294,6 @@ async def page_not_found(e):
148294
def create_app():
149295
global app
150296
app = Quart(__name__)
151-
152297
app.logger.setLevel(logging.INFO)
153298
app.secret_key = settings.APP_SECRET
154299

@@ -197,7 +342,7 @@ def template_variables():
197342

198343
@app.errorhandler(RequestSchemaValidationError)
199344
async def handle_request_validation_error(error):
200-
return {"errors": error.validation_error.json()}, 400
345+
return jsonify({"errors": str(error)}), 400
201346

202347
@app.before_serving
203348
async def startup():

funding/models/database.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,13 @@ async def upsert(cls, data: ProposalUpsert):
445445
if proposal._is_new:
446446
proposal.slug = proposal.generate_slug(data.title)
447447

448+
blob = await crypto_provider.check_address(data.addr_receiving)
449+
if data.addr_receiving[0] == 'a' and not blob['isvalid']:
450+
raise Exception("Invalid Address")
451+
452+
elif data.addr_receiving[0] == 's' and not blob['isvalidSpark']:
453+
raise Exception("Invalid Spark Address")
454+
448455
proposal.set_addr_receiving(data.addr_receiving, user)
449456
await proposal.set_category(data.category, user)
450457
await proposal.set_status(data.status, user)

funding/proposals/api.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

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

20-
2120
@bp_proposals_api.post("/upsert")
2221
@validate_request(ProposalUpsert, source=DataSource.JSON)
2322
@login_required

funding/proposals/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class ProposalUpsert(BaseModel):
5555
category: ProposalCategory
5656
status: Optional[ProposalStatus]
5757
discourse_topic_id: Optional[int]
58-
addr_receiving: constr(min_length=8, max_length=128)
58+
addr_receiving: constr(min_length=8, max_length=255)
5959

6060
class Config:
6161
use_enum_values = False

funding/proposals/routes.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ async def funds(slug: str = None):
7373

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

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

104103
destination = data.destination.strip()
105104
try:
106-
txid = await crypto_provider.send(
107-
address=destination,
105+
if (destination[0] == 's'):
106+
txid = await crypto_provider.mintspark(
107+
sparkAddress=destination,
108108
amount=amount)
109+
else:
110+
txid = await crypto_provider.send(
111+
address=destination,
112+
amount=amount)
109113
except Exception as ex:
110114
return f"Error sending to '{destination}': {ex}"
111115

0 commit comments

Comments
 (0)