Initial commit

tempfix
dsc 4 years ago committed by Sander Ferdinand
commit 7785d5befe

4
.gitignore vendored

@ -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…
Cancel
Save