mirror of
https://git.wownero.com/wowlet/wowlet-backend.git
synced 2024-08-15 01:03:13 +00:00
42bb0c832e
- Move recurring tasks into their own class; inherits from `FeatherTask` - CCS proposals: Don't use API, it's broken - webcrawl instead until it is fixed. - Switch to hypercorn as the ASGI server, *with* support for multiple workers. You can now run feather-ws with, for example, `--workers 6`. See `Dockerfile`. - Introduce support for various coins under `BlockheightTask` - Introduce support for various Reddit communities under `RedditTask` - Introduced weightvoting whilst validating third-party RPC blockheights - where nodes are filtered based on what other nodes are commonly reporting. - Current blockheights are fetched from various block explorers and weightvoting is done to eliminate outliers under `BlockheightTask`. - Don't filter/remove bad nodes from the rpc_nodes list; correctly label them as disabled/bad nodes. - Multiple Feather instances (each for it's own coin) can now run on one machine, using only one Redis instance, as each coins has it's own Redis database index. - Configuration options inside `settings.py` can now be controlled via environment variables. - Better logging through custom log formatting and correct usage of `app.logger.*` - Fixed a bug where one task could overlap with itself if the previous one did not finish yet. This was particularly noticable inside the `RPCNodeCheckTask` where the high timeout (for Tor nodes) could cause the task to run *longer* than the recurring task interval. - Introduced a `docker-compose.yml` to combine the Feather container with Redis and Tor containers. - Blocking IO operations are now done via `aiofiles`
161 lines
5.6 KiB
Python
161 lines
5.6 KiB
Python
# SPDX-License-Identifier: BSD-3-Clause
|
|
# Copyright (c) 2020, The Monero Project.
|
|
# Copyright (c) 2020, dsc@xmr.pm
|
|
|
|
import re
|
|
from typing import Union
|
|
from collections import Counter
|
|
from functools import partial
|
|
|
|
import settings
|
|
from fapi.utils import httpget, popularity_contest
|
|
from fapi.tasks import FeatherTask
|
|
|
|
|
|
class BlockheightTask(FeatherTask):
|
|
"""
|
|
Fetch latest blockheight using webcrawling. We pick the most popular
|
|
height from a list of websites. Arguably this approach has benefits
|
|
over querying a (local) Monero RPC instance, as that requires
|
|
maintenance, while this solution assumes that (at least) 2 websites
|
|
reports the correct height.
|
|
"""
|
|
def __init__(self, interval: int = 60):
|
|
super(BlockheightTask, self).__init__(interval)
|
|
|
|
self._cache_key = "blockheights"
|
|
self._cache_expiry = 90
|
|
|
|
self._websocket_cmd = "blockheights"
|
|
|
|
self._fns = {
|
|
"xmr": {
|
|
"mainnet": [
|
|
self._blockchair,
|
|
partial(self._onion_explorer, url="https://xmrchain.net/"),
|
|
partial(self._onion_explorer, url="https://community.xmr.to/explorer/mainnet/"),
|
|
partial(self._onion_explorer, url="https://monero.exan.tech/")
|
|
],
|
|
"stagenet": [
|
|
partial(self._onion_explorer, url="https://stagenet.xmrchain.net/"),
|
|
partial(self._onion_explorer, url="https://community.xmr.to/explorer/stagenet/"),
|
|
partial(self._onion_explorer, url="https://monero-stagenet.exan.tech/")
|
|
]
|
|
},
|
|
"wow": {
|
|
"mainnet": [
|
|
partial(self._onion_explorer, url="https://explore.wownero.com/"),
|
|
]
|
|
},
|
|
"aeon": {
|
|
"mainnet": [
|
|
partial(self._onion_explorer, url="https://aeonblockexplorer.com/"),
|
|
],
|
|
"stagenet": [
|
|
partial(self._onion_explorer, url="http://162.210.173.151:8083/"),
|
|
]
|
|
},
|
|
"trtl": {
|
|
"mainnet": [
|
|
self._turtlenode,
|
|
self._turtlenetwork,
|
|
self._l33d4n
|
|
]
|
|
},
|
|
"xhv": {
|
|
"mainnet": [
|
|
partial(self._onion_explorer, url="https://explorer.havenprotocol.org/")
|
|
],
|
|
"stagenet": [
|
|
partial(self._onion_explorer, url="https://explorer.stagenet.havenprotocol.org/page/1")
|
|
]
|
|
},
|
|
"loki": {
|
|
"mainnet": [
|
|
partial(self._onion_explorer, url="https://lokiblocks.com/")
|
|
],
|
|
"testnet": [
|
|
partial(self._onion_explorer, url="https://lokitestnet.com/")
|
|
]
|
|
}
|
|
}
|
|
|
|
async def task(self) -> Union[dict, None]:
|
|
from fapi.factory import app
|
|
coin_network_types = ["mainnet", "stagenet", "testnet"]
|
|
data = {t: 0 for t in coin_network_types}
|
|
|
|
for coin_network_type in coin_network_types:
|
|
if coin_network_type not in self._fns[settings.COIN_SYMBOL]:
|
|
continue
|
|
|
|
heights = []
|
|
for fn in self._fns[settings.COIN_SYMBOL][coin_network_type]:
|
|
fn_name = fn.func.__name__ if isinstance(fn, partial) else fn.__name__
|
|
|
|
try:
|
|
result = await fn()
|
|
heights.append(result)
|
|
except Exception as ex:
|
|
app.logger.error(f"blockheight fetch failed from {fn_name}(): {ex}")
|
|
continue
|
|
|
|
if heights:
|
|
data[coin_network_type] = popularity_contest(heights)
|
|
|
|
if data["mainnet"] == 0: # only care about mainnet
|
|
app.logger.error(f"Failed to parse latest blockheight!")
|
|
return
|
|
|
|
return data
|
|
|
|
async def _blockchair(self) -> int:
|
|
re_blockheight = r"<a href=\".*\">(\d+)</a>"
|
|
|
|
url = "https://blockchair.com/monero"
|
|
content = await httpget(url, json=False, raise_for_status=True)
|
|
|
|
height = re.findall(re_blockheight, content)
|
|
height = max(map(int, height))
|
|
return height
|
|
|
|
async def _wownero(self) -> int:
|
|
url = "https://explore.wownero.com/"
|
|
return await BlockheightTask._onion_explorer(url)
|
|
|
|
async def _turtlenode(self) -> int:
|
|
url = "https://public.turtlenode.net/info"
|
|
blob = await httpget(url, json=True, raise_for_status=True)
|
|
height = int(blob.get("height", 0))
|
|
if height <= 0:
|
|
raise Exception("bad height")
|
|
return height
|
|
|
|
async def _turtlenetwork(self) -> int:
|
|
url = "https://tnnode2.turtlenetwork.eu/blocks/height"
|
|
blob = await httpget(url, json=True, raise_for_status=True)
|
|
height = int(blob.get("height", 0))
|
|
if height <= 0:
|
|
raise Exception("bad height")
|
|
return height
|
|
|
|
async def _l33d4n(self):
|
|
url = "https://blockapi.turtlepay.io/block/header/top"
|
|
blob = await httpget(url, json=True, raise_for_status=True)
|
|
height = int(blob.get("height", 0))
|
|
if height <= 0:
|
|
raise Exception("bad height")
|
|
return height
|
|
|
|
@staticmethod
|
|
async def _onion_explorer(url):
|
|
"""
|
|
Pages that are based on:
|
|
https://github.com/moneroexamples/onion-monero-blockchain-explorer
|
|
"""
|
|
re_blockheight = r"block\/(\d+)\"\>"
|
|
content = await httpget(url, json=False)
|
|
|
|
height = re.findall(re_blockheight, content)
|
|
height = max(map(int, height))
|
|
return height
|