Initial commit
This commit is contained in:
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