forked from wowlet/wowlet-backend
commit
7785d5befe
@ -0,0 +1,4 @@
|
||||
.idea
|
||||
*.pyc
|
||||
data/fiatdb
|
||||
settings.py
|
@ -0,0 +1,32 @@
|
||||
Copyright (c) 2014-2020, The Monero Project
|
||||
Copyright (c) 2014-2020, dsc@xmr.pm
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Parts of the project are originally copyright (c) 2012-2013 The Cryptonote
|
||||
developers
|
@ -0,0 +1,7 @@
|
||||
# feather-ws
|
||||
|
||||
This is the back-end websocket server for Feather wallet.
|
||||
|
||||
- Python 3 asyncio
|
||||
- Quart web framework
|
||||
- Redis
|
@ -0,0 +1,35 @@
|
||||
{
|
||||
"mainnet": {
|
||||
"tor": [
|
||||
"zdhkwneu7lfaum2p.onion:18099",
|
||||
"fdlnlt5mr5o7lmhg.onion:18081",
|
||||
"xmkwypann4ly64gh.onion:18081",
|
||||
"xmrtolujkxnlinre.onion:18081",
|
||||
"xmrag4hf5xlabmob.onion:18081"
|
||||
],
|
||||
"clearnet": [
|
||||
"run.your.own.node.xmr.pm:18089",
|
||||
"super.fast.node.xmr.pm:18089",
|
||||
"192.110.160.146:18089",
|
||||
"uwillrunanodesoon.moneroworld.com:18089",
|
||||
"192.110.160.146:18089",
|
||||
"nodes.hashvault.pro:18081",
|
||||
"node.supportxmr.com:18081",
|
||||
"node.imonero.org:18081",
|
||||
"xmr-node-eu.cakewallet.com:18081",
|
||||
"xmr-node-usa-east.cakewallet.com:18081",
|
||||
"node.bohemianpool.com:18081",
|
||||
"node.xmr.pt:18081",
|
||||
"node.xmr.ru:18081",
|
||||
"node.xmrtbackb.one:18081",
|
||||
"node.viaxmr.com:18081"
|
||||
]
|
||||
},
|
||||
"stagenet": {
|
||||
"tor": [],
|
||||
"clearnet": [
|
||||
"run.your.own.node.xmr.pm:38089",
|
||||
"super.fast.node.xmr.pm:38089"
|
||||
]
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,3 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# Copyright (c) 2020, The Monero Project.
|
||||
# Copyright (c) 2020, dsc@xmr.pm
|
@ -0,0 +1,81 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# Copyright (c) 2020, The Monero Project.
|
||||
# Copyright (c) 2020, dsc@xmr.pm
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
|
||||
from quart import Quart
|
||||
from quart_session import Session
|
||||
import aioredis
|
||||
|
||||
import settings
|
||||
|
||||
app = None
|
||||
cache = None
|
||||
connected_websockets = set()
|
||||
api_data = {}
|
||||
nodes = {}
|
||||
user_agents = None
|
||||
txfiatdb = None
|
||||
|
||||
print("""\033[91m
|
||||
█████▒▓█████ ▄▄▄ ▄▄▄█████▓ ██░ ██ ▓█████ ██▀███
|
||||
▓██ ▒ ▓█ ▀▒████▄ ▓ ██▒ ▓▒▓██░ ██▒▓█ ▀ ▓██ ▒ ██▒
|
||||
▒████ ░ ▒███ ▒██ ▀█▄ ▒ ▓██░ ▒░▒██▀▀██░▒███ ▓██ ░▄█ ▒
|
||||
░▓█▒ ░ ▒▓█ ▄░██▄▄▄▄██░ ▓██▓ ░ ░▓█ ░██ ▒▓█ ▄ ▒██▀▀█▄
|
||||
░▒█░ ░▒████▒▓█ ▓██▒ ▒██▒ ░ ░▓█▒░██▓░▒████▒░██▓ ▒██▒
|
||||
▒ ░ ░░ ▒░ ░▒▒ ▓▒█░ ▒ ░░ ▒ ░░▒░▒░░ ▒░ ░░ ▒▓ ░▒▓░
|
||||
░ ░ ░ ░ ▒ ▒▒ ░ ░ ▒ ░▒░ ░ ░ ░ ░ ░▒ ░ ▒░
|
||||
░ ░ ░ ░ ▒ ░ ░ ░░ ░ ░ ░░ ░
|
||||
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ \033[0m
|
||||
""".strip())
|
||||
|
||||
|
||||
async def _setup_cache(app: Quart):
|
||||
global cache
|
||||
data = {
|
||||
"address": "redis://localhost"
|
||||
}
|
||||
|
||||
if settings.redis_password:
|
||||
data['password'] = settings.redis_password
|
||||
|
||||
cache = await aioredis.create_redis_pool(**data)
|
||||
app.config['SESSION_TYPE'] = 'redis'
|
||||
app.config['SESSION_REDIS'] = cache
|
||||
Session(app)
|
||||
|
||||
|
||||
def create_app():
|
||||
global app
|
||||
app = Quart(__name__)
|
||||
|
||||
@app.before_serving
|
||||
async def startup():
|
||||
global nodes, txfiatdb, user_agents
|
||||
await _setup_cache(app)
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
f = open("data/nodes.json", "r")
|
||||
nodes = json.loads(f.read())
|
||||
f.close()
|
||||
|
||||
f = open("data/user_agents.txt", "r")
|
||||
user_agents = [l.strip() for l in f.readlines() if l.strip()]
|
||||
f.close()
|
||||
|
||||
from fapi.fapi import FeatherApi
|
||||
from fapi.utils import loopyloop, TxFiatDb
|
||||
txfiatdb = TxFiatDb(settings.crypto_name, settings.crypto_block_date_start)
|
||||
loop.create_task(loopyloop(20, FeatherApi.xmrto_rates, FeatherApi.after_xmrto))
|
||||
loop.create_task(loopyloop(120, FeatherApi.crypto_rates, FeatherApi.after_crypto))
|
||||
loop.create_task(loopyloop(600, FeatherApi.fiat_rates, FeatherApi.after_fiat))
|
||||
loop.create_task(loopyloop(300, FeatherApi.ccs, FeatherApi.after_ccs))
|
||||
loop.create_task(loopyloop(900, FeatherApi.reddit, FeatherApi.after_reddit))
|
||||
loop.create_task(loopyloop(60, FeatherApi.blockheight, FeatherApi.after_blockheight))
|
||||
loop.create_task(loopyloop(60, FeatherApi.check_nodes, FeatherApi.after_check_nodes))
|
||||
loop.create_task(loopyloop(43200, txfiatdb.update))
|
||||
import fapi.routes
|
||||
|
||||
return app
|
@ -0,0 +1,380 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# Copyright (c) 2020, The Monero Project.
|
||||
# Copyright (c) 2020, dsc@xmr.pm
|
||||
|
||||
import json
|
||||
|
||||
import aiohttp
|
||||
from bs4 import BeautifulSoup
|
||||
from aiohttp_socks import ProxyType, ProxyConnector, ChainProxyConnector
|
||||
from fapi.utils import broadcast_blockheight, broadcast_nodes, httpget, BlockHeight
|
||||
|
||||
import settings
|
||||
|
||||
|
||||
class FeatherApi:
|
||||
@staticmethod
|
||||
async def redis_get(key):
|
||||
from fapi.factory import app, cache
|
||||
try:
|
||||
data = await cache.get(key)
|
||||
if data:
|
||||
return json.loads(data)
|
||||
except Exception as ex:
|
||||
app.logger.error(f"Redis error: {ex}")
|
||||
|
||||
@staticmethod
|
||||
async def xmrto_rates():
|
||||
from fapi.factory import app, cache
|
||||
xmrto_rates = await FeatherApi.redis_get("xmrto_rates")
|
||||
if xmrto_rates and app.config["DEBUG"]:
|
||||
return xmrto_rates
|
||||
|
||||
try:
|
||||
result = await httpget(settings.urls["xmrto_rates"])
|
||||
if not result:
|
||||
raise Exception("empty response")
|
||||
if "error" in result:
|
||||
raise Exception(f"${result['error']} ${result['error_msg']}")
|
||||
return result
|
||||
except Exception as ex:
|
||||
app.logger.error(f"error parsing xmrto_rates blob: {ex}")
|
||||
return xmrto_rates
|
||||
|
||||
@staticmethod
|
||||
async def after_xmrto(data):
|
||||
from fapi.factory import app, cache, api_data, connected_websockets
|
||||
if not data:
|
||||
return
|
||||
|
||||
_data = api_data.get("xmrto_rates", {})
|
||||
_data = json.dumps(_data, sort_keys=True, indent=4)
|
||||
if json.dumps(data, sort_keys=True, indent=4) == _data:
|
||||
return
|
||||
|
||||
api_data["xmrto_rates"] = data
|
||||
|
||||
@staticmethod
|
||||
async def crypto_rates():
|
||||
from fapi.factory import app, cache
|
||||
crypto_rates = await FeatherApi.redis_get("crypto_rates")
|
||||
if crypto_rates and app.config["DEBUG"]:
|
||||
return crypto_rates
|
||||
|
||||
result = None
|
||||
try:
|
||||
result = await httpget(settings.urls["crypto_rates"])
|
||||
if not result:
|
||||
raise Exception("empty response")
|
||||
crypto_rates = result
|
||||
except Exception as ex:
|
||||
app.logger.error(f"error parsing crypto_rates blob: {ex}")
|
||||
|
||||
if not result and crypto_rates:
|
||||
app.logger.warning("USING OLD CACHE FOR CRYPTO RATES")
|
||||
return crypto_rates
|
||||
|
||||
# grab WOW price while we're at it...
|
||||
|
||||
try:
|
||||
_result = await httpget(settings.urls["crypto_wow_rates"])
|
||||
if not _result:
|
||||
raise Exception("empty response")
|
||||
except Exception as ex:
|
||||
_result = {}
|
||||
if "wownero" in _result and "usd" in _result["wownero"]:
|
||||
crypto_rates.append({
|
||||
"id": "wownero",
|
||||
"symbol": "wow",
|
||||
"image": "",
|
||||
"name": "Wownero",
|
||||
"current_price": _result["wownero"]["usd"],
|
||||
"price_change_percentage_24h": 0.0
|
||||
})
|
||||
|
||||
await cache.set("crypto_rates", json.dumps(crypto_rates))
|
||||
return crypto_rates
|
||||
|
||||
@staticmethod
|
||||
async def after_crypto(data):
|
||||
from fapi.factory import app, cache, api_data, connected_websockets
|
||||
if not data:
|
||||
return
|
||||
|
||||
_data = api_data.get("crypto_rates", {})
|
||||
_data = json.dumps(_data, sort_keys=True, indent=4)
|
||||
if json.dumps(data, sort_keys=True, indent=4) == _data:
|
||||
return
|
||||
|
||||
_data = []
|
||||
for obj in data:
|
||||
_data.append({
|
||||
"id": obj['id'],
|
||||
"symbol": obj['symbol'],
|
||||
"image": obj['image'],
|
||||
"name": obj['name'],
|
||||
"current_price": obj['current_price'],
|
||||
"price_change_percentage_24h": obj['price_change_percentage_24h']
|
||||
})
|
||||
|
||||
api_data["crypto_rates"] = data
|
||||
for queue in connected_websockets:
|
||||
await queue.put({
|
||||
"cmd": "crypto_rates",
|
||||
"data": {
|
||||
"crypto_rates": api_data["crypto_rates"]
|
||||
}
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
async def fiat_rates():
|
||||
from fapi.factory import app, cache
|
||||
fiat_rates = await FeatherApi.redis_get("fiat_rates")
|
||||
if fiat_rates and app.config["DEBUG"]:
|
||||
return fiat_rates
|
||||
|
||||
try:
|
||||
result = await httpget(settings.urls["fiat_rates"], json=True)
|
||||
if not result:
|
||||
raise Exception("empty response")
|
||||
await cache.set("fiat_rates", json.dumps(result))
|
||||
return result
|
||||
except Exception as ex:
|
||||
app.logger.error(f"error parsing fiat_rates blob: {ex}")
|
||||
|
||||
# old cache
|
||||
app.logger.warning("USING OLD CACHE FOR FIAT RATES")
|
||||
return fiat_rates
|
||||
|
||||
@staticmethod
|
||||
async def after_fiat(data):
|
||||
from fapi.factory import app, cache, api_data, connected_websockets
|
||||
if not data:
|
||||
return
|
||||
|
||||
_data = api_data.get("fiat_rates", {})
|
||||
_data = json.dumps(_data, sort_keys=True, indent=4)
|
||||
if json.dumps(data, sort_keys=True, indent=4) == _data:
|
||||
return
|
||||
|
||||
api_data["fiat_rates"] = data
|
||||
for queue in connected_websockets:
|
||||
await queue.put({
|
||||
"cmd": "fiat_rates",
|
||||
"data": {
|
||||
"fiat_rates": api_data["fiat_rates"]
|
||||
}
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
async def ccs():
|
||||
# CCS JSON api is broken ;x https://hackerone.com/reports/934231
|
||||
from fapi.factory import app, cache
|
||||
ccs = await FeatherApi.redis_get("ccs")
|
||||
if ccs and app.config["DEBUG"]:
|
||||
return ccs
|
||||
|
||||
try:
|
||||
content = await httpget(f"{settings.urls['ccs']}/funding-required/", json=False)
|
||||
except Exception as ex:
|
||||
app.logger.error(f"error fetching ccs HTML: {ex}")
|
||||
return ccs
|
||||
|
||||
try:
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
except Exception as ex:
|
||||
app.logger.error(f"error parsing ccs HTML page: {ex}")
|
||||
return ccs
|
||||
|
||||
data = []
|
||||
for x in soup.findAll("a", {"class": "ffs-idea"}):
|
||||
try:
|
||||
item = {
|
||||
"state": "FUNDING-REQUIRED",
|
||||
"author": x.find("p", {"class": "author-list"}).text,
|
||||
"date": x.find("p", {"class": "date-list"}).text,
|
||||
"title": x.find("h3").text,
|
||||
"raised_amount": float(x.find("span", {"class": "progress-number-funded"}).text),
|
||||
"target_amount": float(x.find("span", {"class": "progress-number-goal"}).text),
|
||||
"contributors": 0,
|
||||
"url": f"https://ccs.getmonero.org{x.attrs['href']}"
|
||||
}
|
||||
item["percentage_funded"] = item["raised_amount"] * (100 / item["target_amount"])
|
||||
if item["percentage_funded"] >= 100:
|
||||
item["percentage_funded"] = 100.0
|
||||
try:
|
||||
item["contributors"] = int(x.find("p", {"class": "contributor"}).text.split(" ")[0])
|
||||
except:
|
||||
pass
|
||||
|
||||
href = x.attrs['href']
|
||||
|
||||
try:
|
||||
content = await httpget(f"{settings.urls['ccs']}{href}", json=False)
|
||||
try:
|
||||
soup2 = BeautifulSoup(content, "html.parser")
|
||||
except Exception as ex:
|
||||
app.logger.error(f"error parsing ccs HTML page: {ex}")
|
||||
continue
|
||||
|
||||
try:
|
||||
instructions = soup2.find("div", {"class": "instructions"})
|
||||
if not instructions:
|
||||
raise Exception("could not parse div.instructions, page probably broken")
|
||||
address = instructions.find("p", {"class": "string"}).text
|
||||
if not address.strip():
|
||||
raise Exception(f"error fetching ccs HTML: could not parse address")
|
||||
item["address"] = address.strip()
|
||||
except Exception as ex:
|
||||
app.logger.error(f"error parsing ccs address from HTML: {ex}")
|
||||
continue
|
||||
except Exception as ex:
|
||||
app.logger.error(f"error fetching ccs HTML: {ex}")
|
||||
continue
|
||||
data.append(item)
|
||||
except Exception as ex:
|
||||
app.logger.error(f"error parsing a ccs item: {ex}")
|
||||
|
||||
await cache.set("ccs", json.dumps(data))
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def after_ccs(data):
|
||||
from fapi.factory import app, cache, api_data, connected_websockets
|
||||
if not data:
|
||||
return
|
||||
|
||||
_data = api_data.get("ccs", {})
|
||||
_data = json.dumps(_data, sort_keys=True, indent=4)
|
||||
if json.dumps(data, sort_keys=True, indent=4) == _data:
|
||||
return
|
||||
|
||||
api_data["ccs"] = data
|
||||
for queue in connected_websockets:
|
||||
await queue.put({
|
||||
"cmd": "ccs",
|
||||
"data": api_data["ccs"]
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
async def reddit():
|
||||
from fapi.factory import app, cache
|
||||
reddit = await FeatherApi.redis_get("reddit")
|
||||
if reddit and app.config["DEBUG"]:
|
||||
return reddit
|
||||
|
||||
try:
|
||||
blob = await httpget(settings.urls["reddit"])
|
||||
if not blob:
|
||||
raise Exception("no data from url")
|
||||
blob = [{
|
||||
'title': z['data']['title'],
|
||||
'author': z['data']['author'],
|
||||
'url': "https://old.reddit.com/" + z['data']['permalink'],
|
||||
'comments': z['data']['num_comments']
|
||||
} for z in blob['data']['children']]
|
||||
|
||||
# success
|
||||
if blob:
|
||||
await cache.set("reddit", json.dumps(blob))
|
||||
return blob
|
||||
except Exception as ex:
|
||||
app.logger.error(f"error parsing reddit blob: {ex}")
|
||||
|
||||
# old cache
|
||||
return reddit
|
||||
|
||||
@staticmethod
|
||||
async def after_reddit(data):
|
||||
from fapi.factory import app, cache, api_data, connected_websockets
|
||||
if not data:
|
||||
return
|
||||
|
||||
_data = api_data.get("reddit", {})
|
||||
_data = json.dumps(_data, sort_keys=True, indent=4)
|
||||
if json.dumps(data, sort_keys=True, indent=4) == _data:
|
||||
return
|
||||
|
||||
api_data["reddit"] = data
|
||||
for queue in connected_websockets:
|
||||
await queue.put({
|
||||
"cmd": "reddit",
|
||||
"data": api_data["reddit"]
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
async def blockheight():
|
||||
from fapi.factory import app, cache
|
||||
data = {"mainnet": 0, "stagenet": 0}
|
||||
|
||||
for stagenet in [False, True]:
|
||||
try:
|
||||
data["mainnet" if stagenet is False else "stagenet"] = \
|
||||
await BlockHeight.xmrchain(stagenet)
|
||||
except Exception as ex:
|
||||
app.logger.error(f"Could not fetch blockheight from xmrchain")
|
||||
try:
|
||||
data["mainnet" if stagenet is False else "stagenet"] = \
|
||||
await BlockHeight.xmrto(stagenet)
|
||||
except:
|
||||
app.logger.error(f"Could not fetch blockheight from xmr.to")
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def after_blockheight(data):
|
||||
from fapi.factory import app, cache, api_data
|
||||
|
||||
changed = False
|
||||
api_data.setdefault("blockheights", {})
|
||||
if data["mainnet"] > 1 and data["mainnet"] > api_data["blockheights"].get("mainnet", 1):
|
||||
api_data["blockheights"]["mainnet"] = data["mainnet"]
|
||||
changed = True
|
||||
if data["stagenet"] > 1 and data["stagenet"] > api_data["blockheights"].get("stagenet", 1):
|
||||
api_data["blockheights"]["stagenet"] = data["stagenet"]
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
await broadcast_blockheight()
|
||||
|
||||
@staticmethod
|
||||
async def check_nodes():
|
||||
from fapi.factory import nodes, app
|
||||
data = []
|
||||
for network_type, network_name in nodes.items():
|
||||
for k, _nodes in nodes[network_type].items():
|
||||
for node in _nodes:
|
||||
timeout = aiohttp.ClientTimeout(total=5)
|
||||
d = {'timeout': timeout}
|
||||
if ".onion" in node:
|
||||
d['connector'] = ProxyConnector.from_url('socks5://127.0.0.1:9050')
|
||||
d['timeout'] = aiohttp.ClientTimeout(total=12)
|
||||
try:
|
||||
async with aiohttp.ClientSession(**d) as session:
|
||||
async with session.get(f"http://{node}/get_info") as response:
|
||||
blob = await response.json()
|
||||
for expect in ["nettype", "height"]:
|
||||
assert expect in blob
|
||||
_node = {
|
||||
"address": node,
|
||||
"height": int(blob["height"]),
|
||||
"online": True,
|
||||
"nettype": blob["nettype"],
|
||||
"type": k
|
||||
}
|
||||
except Exception as ex:
|
||||
app.logger.warning(f"node {node} not reachable")
|
||||
_node = {
|
||||
"address": node,
|
||||
"height": 0,
|
||||
"online": False,
|
||||
"nettype": network_type,
|
||||
"type": k
|
||||
}
|
||||
data.append(_node)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def after_check_nodes(data):
|
||||
from fapi.factory import api_data
|
||||
api_data["nodes"] = data
|
||||
await broadcast_nodes()
|
@ -0,0 +1,72 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# Copyright (c) 2020, The Monero Project.
|
||||
# Copyright (c) 2020, dsc@xmr.pm
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from copy import deepcopy
|
||||
|
||||
from quart import websocket, jsonify
|
||||
|
||||
from fapi.factory import app
|
||||
from fapi.wsparse import WebsocketParse
|
||||
from fapi.utils import collect_websocket
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def root():
|
||||
from fapi.factory import api_data
|
||||
return jsonify(api_data)
|
||||
|
||||
|
||||
@app.websocket('/ws')
|
||||
@collect_websocket
|
||||
async def ws(queue):
|
||||
from fapi.factory import api_data
|
||||
|
||||
# blast data on connect
|
||||
_api_data = deepcopy(api_data) # prevent race condition
|
||||
for k, v in _api_data.items():
|
||||
if not v:
|
||||
continue
|
||||
await websocket.send(json.dumps({"cmd": k, "data": v}).encode())
|
||||
_api_data = None
|
||||
|
||||
async def rx():
|
||||
while True:
|
||||
data = await websocket.receive()
|
||||
try:
|
||||
blob = json.loads(data)
|
||||
if "cmd" not in blob:
|
||||
continue
|
||||
cmd = blob.get('cmd')
|
||||
_data = blob.get('data')
|
||||
result = await WebsocketParse.parser(cmd, _data)
|
||||
if result:
|
||||
rtn = json.dumps({"cmd": cmd, "data": result}).encode()
|
||||
await websocket.send(rtn)
|
||||
except Exception as ex:
|
||||
continue
|
||||
|
||||
async def tx():
|
||||
while True:
|
||||
data = await queue.get()
|
||||
payload = json.dumps(data).encode()
|
||||
await websocket.send(payload)
|
||||
|
||||
# bidirectional async rx and tx loops
|
||||
consumer_task = asyncio.ensure_future(rx())
|
||||
producer_task = asyncio.ensure_future(tx())
|
||||
try:
|
||||
await asyncio.gather(consumer_task, producer_task)
|
||||
finally:
|
||||
consumer_task.cancel()
|
||||
producer_task.cancel()
|
||||
|
||||
|
||||
@app.errorhandler(403)
|
||||
@app.errorhandler(404)
|
||||
@app.errorhandler(405)
|
||||
@app.errorhandler(500)
|
||||
def page_not_found(e):
|
||||
return ":)", 500
|
@ -0,0 +1,163 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# Copyright (c) 2020, The Monero Project.
|
||||
# Copyright (c) 2020, dsc@xmr.pm
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import random
|
||||
from functools import wraps
|
||||
from datetime import datetime
|
||||
|
||||
import aiohttp
|
||||
|
||||
|
||||
class BlockHeight:
|
||||
@staticmethod
|
||||
async def xmrchain(stagenet: bool = False):
|
||||
re_blockheight = r"block\/(\d+)\"\>"
|
||||
url = "https://stagenet.xmrchain.net/" if stagenet else "https://xmrchain.net/"
|
||||
content = await httpget(url, json=False)
|
||||
xmrchain = re.findall(re_blockheight, content)
|
||||
current = max(map(int, xmrchain))
|
||||
return current
|
||||
|
||||
@staticmethod
|
||||
async def xmrto(stagenet: bool = False):
|
||||
re_blockheight = r"block\/(\d+)\"\>"
|
||||
url = "https://community.xmr.to/explorer/stagenet/" if stagenet else "https://community.xmr.to/explorer/mainnet/"
|
||||
content = await httpget(url, json=False)
|
||||
xmrchain = re.findall(re_blockheight, content)
|
||||
current = max(map(int, xmrchain))
|
||||
return current
|
||||
|
||||
|
||||
async def loopyloop(secs: int, func, after_func=None):
|
||||
"""
|
||||
asyncio loop
|
||||
:param secs: interval
|
||||
:param func: function to execute
|
||||
:param after_func: function to execute after completion
|
||||
:return:
|
||||
"""
|
||||
while True:
|
||||
result = await func()
|
||||
if after_func:
|
||||
await after_func(result)
|
||||
|
||||
# randomize a bit for Tor anti fingerprint reasons
|
||||
_secs = random.randrange(secs - 5, secs +5)
|
||||
await asyncio.sleep(_secs)
|
||||
|
||||
|
||||
def collect_websocket(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
from fapi.factory import connected_websockets
|
||||
queue = asyncio.Queue()
|
||||
connected_websockets.add(queue)
|
||||
try:
|
||||
return await func(queue, *args, **kwargs)
|
||||
finally:
|
||||
connected_websockets.remove(queue)
|
||||
return wrapper
|
||||
|
||||
|
||||
async def broadcast_blockheight():
|
||||
from fapi.factory import connected_websockets, api_data
|
||||
for queue in connected_websockets:
|
||||
await queue.put({
|
||||
"cmd": "blockheights",
|
||||
"data": {
|
||||
"height": api_data.get("blockheights", {})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
async def broadcast_nodes():
|
||||
from fapi.factory import connected_websockets, api_data
|
||||
for queue in connected_websockets:
|
||||
await queue.put({
|
||||
"cmd": "nodes",
|
||||
"data": api_data['nodes']
|
||||
})
|
||||
|
||||
|
||||
async def httpget(url: str, json=True):
|
||||
timeout = aiohttp.ClientTimeout(total=4)
|
||||
headers = {"User-Agent": random_agent()}
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url, headers=headers) as response:
|
||||
return await response.json() if json else await response.text()
|
||||
|
||||
|
||||
def random_agent():
|
||||
from fapi.factory import user_agents
|
||||
return random.choice(user_agents)
|
||||
|
||||
|
||||
class TxFiatDb:
|
||||
# historical fiat price db for given symbol
|
||||
def __init__(self, symbol, block_date_start):
|
||||
self.fn = "data/fiatdb"
|
||||
self.symbol = symbol
|
||||
self.block_start = block_date_start
|
||||
self._url = "https://www.coingecko.com/price_charts/69/usd/max.json"
|
||||
self.data = {}
|
||||
self.load()
|
||||
|
||||
def get(self, year: int, month: int = None):
|
||||
rtn = {}
|
||||
if year not in self.data:
|
||||
return
|
||||
if not month:
|
||||
for _m, days in self.data[year].items():
|
||||
for day, price in days.items():
|
||||
rtn[datetime(year, _m, day).strftime('%Y%m%d')] = price
|
||||
return rtn
|
||||
if month not in self.data[year]:
|
||||
return
|
||||
for day, price in self.data[year][month].items():
|
||||
rtn[datetime(year, month, day).strftime('%Y%m%d')] = price
|
||||
return rtn
|
||||
|
||||
def load(self):
|
||||
if not os.path.exists("fiatdb"):
|
||||
return {}
|
||||
f = open("fiatdb", "r")
|
||||
data = f.read()
|
||||
f.close()
|
||||
data = json.loads(data)
|
||||
|
||||
# whatever
|
||||
self.data = {int(k): {int(_k): {int(__k): __v for __k, __v in _v.items()} for _k, _v in v.items()} for k, v in data.items()}
|
||||
|
||||
def write(self):
|
||||
f = open("fiatdb", "w")
|
||||
f.write(json.dumps(self.data))
|
||||
f.close()
|
||||
|
||||
async def update(self):
|
||||
try:
|
||||
content = await httpget(self._url, json=True)
|
||||
if not "stats" in content:
|
||||
raise Exception()
|
||||
except Exception as ex:
|
||||
return
|
||||
|
||||
stats = content.get('stats')
|
||||
if not stats:
|
||||
return
|
||||
|
||||
year_start = int(self.block_start[:4])
|
||||
self.data = {z: {k: {} for k in range(1, 13)}
|
||||
for z in range(year_start, datetime.now().year + 1)}
|
||||
content = {z[0]: z[1] for z in stats}
|
||||
|
||||
for k, v in content.items():
|
||||
_date = datetime.fromtimestamp(k / 1000)
|
||||
self.data[_date.year].setdefault(_date.month, {})
|
||||
self.data[_date.year][_date.month][_date.day] = v
|
||||
|
||||
self.write()
|
@ -0,0 +1,25 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# Copyright (c) 2020, The Monero Project.
|
||||
# Copyright (c) 2020, dsc@xmr.pm
|
||||
|
||||
|
||||
class WebsocketParse:
|
||||
@staticmethod
|
||||
async def parser(cmd: str, data=None):
|
||||
if cmd == "txFiatHistory":
|
||||
return await WebsocketParse.txFiatHistory(data)
|
||||
|
||||
@staticmethod
|
||||
async def txFiatHistory(data=None):
|
||||
if not data or not isinstance(data, dict):
|
||||
return
|
||||
if "year" not in data or not isinstance(data['year'], int):
|
||||
return
|
||||
if "month" in data and not isinstance(data['month'], int):
|
||||
return
|
||||
|
||||
year = data.get('year')
|
||||
month = data.get('month')
|
||||
|
||||
from fapi.factory import txfiatdb
|
||||
return txfiatdb.get(year, month)
|
@ -0,0 +1,7 @@
|
||||
quart
|
||||
aioredis
|
||||
aiohttp
|
||||
quart_session
|
||||
beautifulsoup4
|
||||
aiohttp_socks
|
||||
python-dateutil
|
@ -0,0 +1,9 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# Copyright (c) 2020, The Monero Project.
|
||||
# Copyright (c) 2020, dsc@xmr.pm
|
||||
|
||||
from fapi.factory import create_app
|
||||
import settings
|
||||
|
||||
app = create_app()
|
||||
app.run(settings.host, port=settings.port, debug=settings.debug, use_reloader=False)
|
@ -0,0 +1,27 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# Copyright (c) 2020, The Monero Project.
|
||||
# Copyright (c) 2020, dsc@xmr.pm
|
||||
|
||||
debug = False
|
||||
host = "127.0.0.1"
|
||||
port = 1337
|
||||
redis_password = None
|
||||
rpc_url = "http://127.0.0.1:18089"
|
||||
xmrchain = "https://stagenet.xmrchain.net"
|
||||
|
||||
crypto_name = "monero"
|
||||
crypto_symbol = "xmr"
|
||||
crypto_block_date_start = "20140418"
|
||||
|
||||
urls = {
|
||||
"reddit": "https://www.reddit.com/r/monero/top.json?limit=100",
|
||||
"ccs": "https://ccs.getmonero.org",
|
||||
"fiat_rates": "https://api.exchangeratesapi.io/latest?base=USD",
|
||||
"crypto_rates": "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd",
|
||||
"crypto_wow_rates": "https://api.coingecko.com/api/v3/simple/price?ids=wownero&vs_currencies=usd"
|
||||
}
|
||||
|
||||
if debug:
|
||||
urls["xmrto_rates"] = "https://test.xmr.to/api/v3/xmr2btc/order_parameter_query/"
|
||||
else:
|
||||
urls["xmrto_rates"] = "https://xmr.to/api/v3/xmr2btc/order_parameter_query/"
|
@ -0,0 +1,22 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# Copyright (c) 2020, The Monero Project.
|
||||
# Copyright (c) 2020, dsc@xmr.pm
|
||||
|
||||
import re, os, sys, requests
|
||||
|
||||
current_height = 664767
|
||||
f = open("heights.txt", "a")
|
||||
for i in range(0, current_height, 1500):
|
||||
if i == 0:
|
||||
i = 1
|
||||
if i % (1500*8) == 0:
|
||||
print(f"[*] {current_height-i}")
|
||||
|
||||
url = f"https://stagenet.xmrchain.net/block/{i}"
|
||||
resp = requests.get(url, headers={"User-Agent": "Feather"})
|
||||
resp.raise_for_status()
|
||||
content = resp.content.decode()
|
||||
timestamp = wow = re.findall(r"\((\d{10})\)", content)[0]
|
||||
f.write(f"{i}:{timestamp}\n")
|
||||
|
||||
f.close()
|
Loading…
Reference in new issue