initial bot notification with <3
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
This commit is contained in:
commit
123e0599db
3
.env.template
Normal file
3
.env.template
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
TELEGRAM_TOKEN=
|
||||||
|
NOTIFY_CHAT_ID=
|
||||||
|
THREAD_ID=
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.venv/
|
||||||
|
.env
|
||||||
|
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
__pycache__/
|
||||||
41
README.md
Normal file
41
README.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Ghost Notifier Bot
|
||||||
|
|
||||||
|
A Telegram bot that monitors onchain events on
|
||||||
|
[ghostDAO](https://app.dao.ghostchain.io/) and sends real-time
|
||||||
|
notifications to a Telegram group using `aiogram`.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Listens to specified onchain events (e.g., swaps, bond purchases, proposals creation)
|
||||||
|
- Sends instant notifications to a configured Telegram group
|
||||||
|
- Built with `aiogram` for async Telegram bot functionality
|
||||||
|
- Usage of public RPC endpoints from [chainlist](https://chainlist.org/)
|
||||||
|
|
||||||
|
## Networks
|
||||||
|
|
||||||
|
Each network has unique name, explorer link and event details to
|
||||||
|
listen on:
|
||||||
|
|
||||||
|
- Event name that will be used for messages only
|
||||||
|
- Address is address of smart contract to listen on
|
||||||
|
- Event topic is `keccak256` hash of the event aka `topic[0]`
|
||||||
|
|
||||||
|
For more info go to `networks.json`.
|
||||||
|
|
||||||
|
## Messages
|
||||||
|
|
||||||
|
All possible messages mapped to the event name. It is using predefined
|
||||||
|
structure of message:
|
||||||
|
|
||||||
|
- Header is bold text on top of the message
|
||||||
|
- Body is main text of the message
|
||||||
|
- Link is text of the link (in out case is the link to transaction hash)
|
||||||
|
- Footer is text in the end of the message
|
||||||
|
- Button text is text of the inline button
|
||||||
|
- Button URL is the template link for the inline button
|
||||||
|
|
||||||
|
## RPCs
|
||||||
|
|
||||||
|
Becase we are aiming to use public RPC endpoints we need to have list
|
||||||
|
of enpoints in `rpcs/NETWORK_NAME.txt` where network name is described
|
||||||
|
inside `networks.json`.
|
||||||
201
main.py
Normal file
201
main.py
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from web3 import Web3
|
||||||
|
from aiogram import types, Bot
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger("GhostNotifierBot")
|
||||||
|
|
||||||
|
SLEEP_TIME=10 # seconds
|
||||||
|
CONNECTION_TIMEOUT=10 # seconds
|
||||||
|
MAX_BLOCK_DELAY=5 # blocks
|
||||||
|
|
||||||
|
class RPCManager:
|
||||||
|
def __init__(self, network_name):
|
||||||
|
self.network_name = network_name
|
||||||
|
self.current_index = 0
|
||||||
|
self.rpcs = []
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
asyncio.create_task(self._auto_update_loop())
|
||||||
|
|
||||||
|
def next_rpc(self):
|
||||||
|
if len(self.rpcs) > 0:
|
||||||
|
self.current_index = (self.current_index + 1) % len(self.rpcs)
|
||||||
|
|
||||||
|
def get_current_rpc(self):
|
||||||
|
if len(self.rpcs) == 0:
|
||||||
|
return ""
|
||||||
|
return self.rpcs[self.current_index]
|
||||||
|
|
||||||
|
def update_rpc_from_file(self):
|
||||||
|
path = os.path.join("rpcs", f"{self.network_name.lower()}.txt")
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, "r") as f:
|
||||||
|
rpcs = [line.strip() for line in f if line.strip()]
|
||||||
|
self.rpcs = rpcs
|
||||||
|
self.current_index %= len(self.rpcs)
|
||||||
|
logger.info(f"[{self.network_name}] list of RPC updated.")
|
||||||
|
else:
|
||||||
|
logger.warning(f"[{self.network_name}] file with RPCs not found at {path}.")
|
||||||
|
|
||||||
|
async def _auto_update_loop(self):
|
||||||
|
while True:
|
||||||
|
self.update_rpc_from_file()
|
||||||
|
await asyncio.sleep(1800) # 30 mins
|
||||||
|
|
||||||
|
class EventBot:
|
||||||
|
def __init__(self, token, chat_id, thread_id, messages):
|
||||||
|
self.bot = Bot(token)
|
||||||
|
self.chat_id = chat_id
|
||||||
|
self.thread_id = thread_id
|
||||||
|
self.messages = messages
|
||||||
|
|
||||||
|
def prepare_message(self, network_name, explorer_tx_link, event_name, log):
|
||||||
|
msgs = self.messages.get(event_name)
|
||||||
|
if msgs is None:
|
||||||
|
return f"{event_name} has no explanation text yet. =("
|
||||||
|
|
||||||
|
tx_link = f"{explorer_tx_link}{log['transactionHash'].to_0x_hex()}"
|
||||||
|
message = f"*{msgs['header']} #{network_name}!*\n\n"
|
||||||
|
message += f"{msgs['body']}\n[{msgs['link']}]({tx_link})\n\n"
|
||||||
|
message += f"_{msgs['footer']}_"
|
||||||
|
return message
|
||||||
|
|
||||||
|
def prepare_button(self, network_name, event_name, ftso_address, log):
|
||||||
|
msgs = self.messages.get(event_name)
|
||||||
|
if msgs is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
button_url = msgs["button_url"]
|
||||||
|
button_url = button_url.replace("/#/", f"/#/{network_name}/")
|
||||||
|
button = InlineKeyboardBuilder()
|
||||||
|
|
||||||
|
if event_name == "Swap":
|
||||||
|
button_url += ftso_address
|
||||||
|
elif event_name == "Bond" or event_name == "MarketCreated":
|
||||||
|
bond_id = int(log["topics"][1].hex(), 16)
|
||||||
|
button_url += str(bond_id)
|
||||||
|
elif event_name == "ProposalCreated":
|
||||||
|
proposal_id = int(log["topics"][1].hex(), 16)
|
||||||
|
button_url += str(proposal_id)
|
||||||
|
|
||||||
|
button.add(types.InlineKeyboardButton(
|
||||||
|
text=msgs["button_text"],
|
||||||
|
url=button_url
|
||||||
|
))
|
||||||
|
|
||||||
|
return button.as_markup()
|
||||||
|
|
||||||
|
async def monitor_network(self, network_name, network_data):
|
||||||
|
rpc_manager = RPCManager(network_name)
|
||||||
|
await rpc_manager.start()
|
||||||
|
last_block = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
checked_index = 0
|
||||||
|
current_rpc = rpc_manager.get_current_rpc()
|
||||||
|
|
||||||
|
try:
|
||||||
|
w3 = Web3(Web3.HTTPProvider(current_rpc, request_kwargs={'timeout': CONNECTION_TIMEOUT}))
|
||||||
|
if not w3.is_connected():
|
||||||
|
raise Exception(f"[{network_name}] Could not connect to RPC: {current_rpc}.")
|
||||||
|
|
||||||
|
logger.info(f"[{network_name}] Connecting to {current_rpc}.")
|
||||||
|
if last_block is None:
|
||||||
|
last_block = w3.eth.block_number
|
||||||
|
logger.info(f"[{network_name}] Starting from {last_block} based on {current_rpc}.")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
latest_block = w3.eth.block_number
|
||||||
|
|
||||||
|
if last_block >= latest_block:
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_block = min(last_block + MAX_BLOCK_DELAY, latest_block)
|
||||||
|
logger.info(f"[{network_name}] Looking for range from {last_block + 1} to {current_block}")
|
||||||
|
|
||||||
|
for index, event in enumerate(network_data["events"]):
|
||||||
|
if checked_index > index:
|
||||||
|
continue
|
||||||
|
|
||||||
|
event_name = event["name"]
|
||||||
|
logs = w3.eth.get_logs({
|
||||||
|
"fromBlock": last_block + 1,
|
||||||
|
"toBlock": current_block,
|
||||||
|
"address": Web3.to_checksum_address(event["address"]),
|
||||||
|
"topics": [event["topic"]]
|
||||||
|
})
|
||||||
|
checked_index = index + 1
|
||||||
|
|
||||||
|
logger.info(f"""[{network_name}] Found {len(logs)} {event_name} events from {current_rpc}.""")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
for log in logs:
|
||||||
|
message = self.prepare_message(
|
||||||
|
network_name,
|
||||||
|
network_data["explorer_tx_link"],
|
||||||
|
event_name,
|
||||||
|
log
|
||||||
|
)
|
||||||
|
button = self.prepare_button(
|
||||||
|
network_name,
|
||||||
|
event_name,
|
||||||
|
network_data["ftso"],
|
||||||
|
log
|
||||||
|
)
|
||||||
|
await self.send_alert(message, button)
|
||||||
|
|
||||||
|
checked_index = 0
|
||||||
|
last_block = current_block
|
||||||
|
await asyncio.sleep(SLEEP_TIME)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[{network_name}] Connection failed {current_rpc}: {e}.")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
rpc_manager.next_rpc()
|
||||||
|
|
||||||
|
async def send_alert(self, message, button_reply_markup):
|
||||||
|
try:
|
||||||
|
await self.bot.send_message(
|
||||||
|
chat_id=self.chat_id,
|
||||||
|
message_thread_id=self.thread_id,
|
||||||
|
text=message,
|
||||||
|
parse_mode="Markdown",
|
||||||
|
reply_markup=button_reply_markup
|
||||||
|
)
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[{network}] TG error during sending: {e}.")
|
||||||
|
|
||||||
|
async def start(self, networks):
|
||||||
|
tasks = [
|
||||||
|
self.monitor_network(network_name, network_data)
|
||||||
|
for network_name, network_data in networks.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info(f"Ghost Event Bot started. Monitoring {len(tasks)} networks.")
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
TG_TOKEN = os.getenv("TELEGRAM_TOKEN")
|
||||||
|
CHAT_ID = os.getenv("NOTIFY_CHAT_ID")
|
||||||
|
THREAD_ID = os.getenv("THREAD_ID") or None
|
||||||
|
|
||||||
|
with open("networks.json", "r") as f:
|
||||||
|
networks = json.load(f)
|
||||||
|
|
||||||
|
with open("messages.json", "r") as f:
|
||||||
|
messages = json.load(f)
|
||||||
|
|
||||||
|
bot = EventBot(token=TG_TOKEN, chat_id=CHAT_ID, thread_id=THREAD_ID, messages=messages)
|
||||||
|
asyncio.run(bot.start(networks))
|
||||||
50
messages.json
Normal file
50
messages.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"Swap": {
|
||||||
|
"header": "🔄 eCSPR Swap Detected on",
|
||||||
|
"body": "$eCSPR just changed hands on the DEX!",
|
||||||
|
"link": "🔗 Check the trade",
|
||||||
|
"footer": "Time to peek at your ghostDAO profits - spooky gains ahead! 👻📈",
|
||||||
|
"button_text": "View Current Price",
|
||||||
|
"button_url": "https://app.dao.ghostchain.io/#/dex/uniswap?from=0x0000000000000000000000000000000000000000&to="
|
||||||
|
},
|
||||||
|
"Bond": {
|
||||||
|
"header": "🎉 Bond Purchased on",
|
||||||
|
"body": "Someone just locked in a bond at a DISCOUNT!",
|
||||||
|
"link": "🔗 View transaction",
|
||||||
|
"footer": "Pro tip: Discounts vanish faster than ghosts at sunrise! 👻",
|
||||||
|
"button_text": "Check Discount",
|
||||||
|
"button_url": "https://app.dao.ghostchain.io/#/bonds/"
|
||||||
|
},
|
||||||
|
"Stake": {
|
||||||
|
"header": "🔥 New Staking Action Detected on",
|
||||||
|
"body": "eCSPR tokens just got staked in the latest block!",
|
||||||
|
"link": "🔗 View transaction",
|
||||||
|
"footer": "'To stake or not to stake?' The answer is always STAKE! 👻",
|
||||||
|
"button_text": "Check Staking APY",
|
||||||
|
"button_url": "https://app.dao.ghostchain.io/#/stake"
|
||||||
|
},
|
||||||
|
"MarketCreated": {
|
||||||
|
"header": "✨ Fresh Bond Alert on",
|
||||||
|
"body": "A new bond is now live with rising discount!",
|
||||||
|
"link": "🔗 See details here",
|
||||||
|
"footer": "Claim yours before the Ghosties snatch it up! 👻💰",
|
||||||
|
"button_text": "View new Bond",
|
||||||
|
"button_url": "https://app.dao.ghostchain.io/#/bonds/"
|
||||||
|
},
|
||||||
|
"Ghosted": {
|
||||||
|
"header": "🔀 eCSPR has been GHOSTed on",
|
||||||
|
"body": "eCSPR has just been bridged in ⛓️➡️👻!",
|
||||||
|
"link": "🔗 Track the transfer",
|
||||||
|
"footer": "📢 To all #CASPER Validators: Time to earn bridging fees! 👻💸",
|
||||||
|
"button_text": "Check Bridge Status",
|
||||||
|
"button_url": "https://app.dao.ghostchain.io/#/bridge"
|
||||||
|
},
|
||||||
|
"ProposalCreated": {
|
||||||
|
"header": "🗳 A new proposal just hit the ghostDAO voting floor on",
|
||||||
|
"body": "Time to make your CSPR count!",
|
||||||
|
"link": "🔗 Check the proposal",
|
||||||
|
"footer": "Go cast your vote before the proposal haunts you forever! 👻⚖️",
|
||||||
|
"button_text": "View Proposal",
|
||||||
|
"button_url": "https://app.dao.ghostchain.io/#/governance/"
|
||||||
|
}
|
||||||
|
}
|
||||||
110
networks.json
Normal file
110
networks.json
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"sepolia": {
|
||||||
|
"ftso": "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
||||||
|
"explorer_tx_link": "https://sepolia.etherscan.io/tx/",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"name": "Swap",
|
||||||
|
"address": "0xCd1505E5d169525e0241c177aF5929A92E02276D",
|
||||||
|
"topic": "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bond",
|
||||||
|
"address": "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||||
|
"topic": "0x7880508a48fd3aee88f7e15917d85e39c3ad059e51ad4aca9bb46e7b4938b961"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stake",
|
||||||
|
"address": "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
||||||
|
"topic": "0xfb1c60409e7f5b507f77c07b9d283bbd9569bcdc0b505ada7fad911130420ed8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MarketCreated",
|
||||||
|
"address": "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||||
|
"topic": "0xf5110a61238afa49549d323d552cac783a8c8104bd1bbf42c5b479d60927f033"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ghosted",
|
||||||
|
"address": "0xd735cA07984a16911222c08411A80e24EB38869B",
|
||||||
|
"topic": "0x7ab52ec05c331e6257a3d705d6bea6e4c27277351764ad139209e06b203811a6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ProposalCreated",
|
||||||
|
"address": "0xaf7Ad1b83C47405BB9aa96868bCFbb6D65e4C2a1",
|
||||||
|
"topic": "0x7d84a6263ae0d98d3329bd7b46bb4e8d6f98cd35a7adb45c274c8b7fd5ebd5e0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mordor": {
|
||||||
|
"ftso": "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
||||||
|
"explorer_tx_link": "https://etc-mordor.blockscout.com/tx/",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"name": "Swap",
|
||||||
|
"address": "0x53B13C4722081c405ce25c7A7629fC326A49a469",
|
||||||
|
"topic": "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bond",
|
||||||
|
"address": "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||||
|
"topic": "0x7880508a48fd3aee88f7e15917d85e39c3ad059e51ad4aca9bb46e7b4938b961"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stake",
|
||||||
|
"address": "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
||||||
|
"topic": "0xfb1c60409e7f5b507f77c07b9d283bbd9569bcdc0b505ada7fad911130420ed8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MarketCreated",
|
||||||
|
"address": "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||||
|
"topic": "0xf5110a61238afa49549d323d552cac783a8c8104bd1bbf42c5b479d60927f033"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ghosted",
|
||||||
|
"address": "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
|
||||||
|
"topic": "0x7ab52ec05c331e6257a3d705d6bea6e4c27277351764ad139209e06b203811a6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ProposalCreated",
|
||||||
|
"address": "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
|
||||||
|
"topic": "0x7d84a6263ae0d98d3329bd7b46bb4e8d6f98cd35a7adb45c274c8b7fd5ebd5e0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hoodi": {
|
||||||
|
"ftso": "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
||||||
|
"explorer_tx_link": "https://hoodi.etherscan.io/tx/",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"name": "Swap",
|
||||||
|
"address": "0x53B13C4722081c405ce25c7A7629fC326A49a469",
|
||||||
|
"topic": "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bond",
|
||||||
|
"address": "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||||
|
"topic": "0x7880508a48fd3aee88f7e15917d85e39c3ad059e51ad4aca9bb46e7b4938b961"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stake",
|
||||||
|
"address": "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
||||||
|
"topic": "0xfb1c60409e7f5b507f77c07b9d283bbd9569bcdc0b505ada7fad911130420ed8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MarketCreated",
|
||||||
|
"address": "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||||
|
"topic": "0xf5110a61238afa49549d323d552cac783a8c8104bd1bbf42c5b479d60927f033"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ghosted",
|
||||||
|
"address": "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
|
||||||
|
"topic": "0x7ab52ec05c331e6257a3d705d6bea6e4c27277351764ad139209e06b203811a6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ProposalCreated",
|
||||||
|
"address": "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
|
||||||
|
"topic": "0x7d84a6263ae0d98d3329bd7b46bb4e8d6f98cd35a7adb45c274c8b7fd5ebd5e0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
44
requirements.txt
Normal file
44
requirements.txt
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
aiofiles==25.1.0
|
||||||
|
aiogram==3.26.0
|
||||||
|
aiohappyeyeballs==2.6.1
|
||||||
|
aiohttp==3.13.5
|
||||||
|
aiosignal==1.4.0
|
||||||
|
annotated-types==0.7.0
|
||||||
|
async-timeout==5.0.1
|
||||||
|
attrs==26.1.0
|
||||||
|
bitarray==3.8.1
|
||||||
|
certifi==2026.2.25
|
||||||
|
charset-normalizer==3.4.7
|
||||||
|
ckzg==2.1.7
|
||||||
|
cytoolz==1.1.0
|
||||||
|
eth-account==0.13.7
|
||||||
|
eth-hash==0.8.0
|
||||||
|
eth-keyfile==0.8.1
|
||||||
|
eth-keys==0.7.0
|
||||||
|
eth-rlp==2.2.0
|
||||||
|
eth-typing==6.0.0
|
||||||
|
eth-utils==6.0.0
|
||||||
|
eth_abi==5.2.0
|
||||||
|
frozenlist==1.8.0
|
||||||
|
hexbytes==1.3.1
|
||||||
|
idna==3.11
|
||||||
|
magic-filter==1.0.12
|
||||||
|
multidict==6.7.1
|
||||||
|
parsimonious==0.10.0
|
||||||
|
propcache==0.4.1
|
||||||
|
pycryptodome==3.23.0
|
||||||
|
pydantic==2.12.5
|
||||||
|
pydantic_core==2.41.5
|
||||||
|
python-dotenv==1.2.2
|
||||||
|
pyunormalize==17.0.0
|
||||||
|
regex==2026.3.32
|
||||||
|
requests==2.33.1
|
||||||
|
rlp==4.1.0
|
||||||
|
toolz==1.1.0
|
||||||
|
types-requests==2.33.0.20260402
|
||||||
|
typing-inspection==0.4.2
|
||||||
|
typing_extensions==4.15.0
|
||||||
|
urllib3==2.6.3
|
||||||
|
web3==7.15.0
|
||||||
|
websockets==15.0.1
|
||||||
|
yarl==1.23.0
|
||||||
4
rpcs/hoodi.txt
Normal file
4
rpcs/hoodi.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
https://rpc.hoodi.ethpandaops.io
|
||||||
|
https://ethereum-hoodi.gateway.tatum.io
|
||||||
|
https://0xrpc.io/hoodi
|
||||||
|
https://rpc.sentio.xyz/hoodi
|
||||||
3
rpcs/mordor.txt
Normal file
3
rpcs/mordor.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
https://rpc.mordor.etccooperative.org
|
||||||
|
https://0xrpc.io/mordor
|
||||||
|
https://geth-mordor.etc-network.info
|
||||||
5
rpcs/sepolia.txt
Normal file
5
rpcs/sepolia.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
https://rpc.sepolia.ethpandaops.io
|
||||||
|
https://ethereum-sepolia.gateway.tatum.io
|
||||||
|
https://sepolia.gateway.tenderly.co
|
||||||
|
https://0xrpc.io/sep
|
||||||
|
https://rpc.sepolia.ethpandaops.io
|
||||||
Loading…
Reference in New Issue
Block a user