"""Blockchain ids.
Data structures and information about EVM based blockchains.
Because the same trading pair and smart contract can across multiple blockchains,
we need to have a way to identify blockchains.
See :py:class:`ChainId` enum class for passing the identity of a blockchain around.
This is based on the underlying `web3.eth.chain_id` attribute of a chain.
Trading Strategy package embeds the chain list data from `chains repository <https://github.com/ethereum-lists/chains>`_
as the submodule for the Python package. This data is used to populate some :py:class:`ChainId`
data.
"""
import enum
import json
import os
import threading
from typing import Dict, Optional
#: In-process cached chain data, so we do not need to hit FS every time we access
_chain_data: Dict[int, dict] = {}
#: Slug to chain id mapping
_slug_map: Dict[str, int] = {}
class ChainDataDoesNotExist(Exception):
"""Cannot find data for a specific chain"""
#: Prevent _ensure_chain_data_lazy_init() duplicates
#:
#: May happen in Dash application due to hot code reload
_init_lock = threading.Lock()
def _ensure_chain_data_lazy_init():
global _chain_data
global _slug_map
with _init_lock:
if _chain_data:
# Already initialized
return
for chain_id in ChainId:
chain_id = chain_id.value
path = os.path.abspath(os.path.join(os.path.dirname(__file__), "chains", "_data", "chains"))
if not os.path.exists(path):
raise RuntimeError(f"Chain data folder {path} not found. Make sure you have initialised git submodules or Python packaking is correct.\nHint: git submodule update --recursive --init")
# Ganache does not have chain data entry
dataless = _CHAIN_DATA_OVERRIDES.get(chain_id, {}).get("dataless", False)
if not dataless:
data_file = os.path.join(path, f"eip155-{chain_id}.json")
if not os.path.exists(data_file):
raise ChainDataDoesNotExist(f"Chain data for {ChainId(chain_id)} with id {chain_id} does not exist at data file location: {data_file}")
with open(data_file, "rt") as inp:
_chain_data[chain_id] = json.load(inp)
else:
_chain_data[chain_id] = {}
# Apply our own chain data records
_chain_data[chain_id].update(_CHAIN_DATA_OVERRIDES.get(chain_id, {}))
# Build slug -> chain id reverse mapping
for chain_id, data in _chain_data.items():
_slug_map[data["slug"]] = chain_id
def _get_chain_data(chain_id: int):
assert type(chain_id) == int, f"Got chain_id {type(chain_id)}"
_ensure_chain_data_lazy_init()
assert chain_id in _chain_data, f"Available chains: {_chain_data.keys()}"
return _chain_data[chain_id]
def _get_slug_map() -> Dict[str, int]:
_ensure_chain_data_lazy_init()
return _slug_map
[docs]class ChainId(enum.IntEnum):
"""Chain ids and chain metadata helper.
This class is intended to present primary key for a blockchain in datasets,
not its native chain id. This id may differ from what blockchain
assumes its own id natively.
Chain id is an integer that defines the identity of a blockchain,
all running on same or different EVM implementations. For non-EVM
blockchains we have some special logic to handle them so we can
present chain ids or all blockchains through this enum.
Chain id is a 32-bit integer.
For non-EVM chains like ones on Cosmos or Solana we use negative values,
so that they are not confused with chain.network database. Cosmos
natively uses string ids for chains instead of integers.
This class also provides various other metadata attributes besides `ChainId.value`, like `ChainId.get_slug()`.
Some of this data is handcoded, some is pulled from `chains` submodule.
For the full chain id list see:
- `chainid.network <https://chainid.network/>`_
- `chains repo <https://github.com/ethereum-lists/chains>`_
= `Cosmos chain registry <https://github.com/cosmos/chain-registry
"""
#: Ethereum mainnet chain id
ethereum = 1
#: Binance Smarrt Chain mainnet chain id
bsc = 56
#: Alias for Binance Smart Chain
binance = bsc
#: Polygon chain id
polygon = 137
#: Avalanche C-chain id
avalanche = 43114
#: Arbitrum One id
arbitrum = 42161
#: Base
base = 8453
#: Mantle
mantle = 5000
#: Ethereum Classic chain id.
#:
#: This is also the value used by EthereumTester in unit tests.
#: https://github.com/ethereum/eth-tester
ethereum_classic = 61
#: Ganache test chain.
#:
#: This is the chain id for Ganache local tester / mainnet forks.
ganache = 1337
#: Anvil test chain.
#:
#: Standalone Anvil chain launch.
#:
#: `See Foundry commit <https://github.com/foundry-rs/foundry/commit/7d6fd0ebe4caf54f1b24d379d3df2205af04fe33>`__.
anvil = 31337
#: Chain id not known
unknown = 0
#: Special chain id set for side-loaded data.
#:
#: Symbolise this data is from a centralised exchange.
#:
centralised_exchange = -1
#: Osmosis on Cosmos
#: Does not have chain registry entry,
#: beacuse Cosmos maintains its own registry
osmosis = -100
#: Python EVM test backend
#:
#: See https://github.com/ethereum/eth-tester/blob/84378ee7eb714633fbb3169378812ccfcbbd495a/eth_tester/backends/pyevm/main.py#L197
ethereum_tester = 131277322940537
@property
def data(self) -> dict:
"""Get chain data entry for this chain."""
return _get_chain_data(self.value)
[docs] def get_name(self) -> str:
"""Get full human readab name for this blockchain"""
return self.data["name"]
[docs] def get_slug(self) -> str:
"""Get URL slug for this chain"""
return self.data["slug"]
[docs] def get_homepage(self) -> str:
"""Get homepage link for this blockchain"""
# TODO: Use chain id JSON data in the future
return self.data["infoURL"]
[docs] def get_svg_icon_link(self) -> str:
"""Get an absolute SVG image link to a chain icon, transparent background"""
return self.data["svg_icon"]
[docs] def get_explorer(self) -> str:
"""Get explorer landing page for this blockchain"""
return self.data["explorers"][0]["url"]
[docs] def get_address_link(self, address) -> str:
"""Get one address link.
Use EIP3091 format.
https://eips.ethereum.org/EIPS/eip-3091
"""
return f"{self.get_explorer()}/address/{address}"
[docs] def get_tx_link(self, tx) -> str:
"""Get one tx link"""
return f"{self.get_explorer()}/tx/{tx}"
[docs] @staticmethod
def get_by_slug(slug: str) -> Optional["ChainId"]:
"""Map a slug back to the chain.
Most useful for resolving URLs.
"""
slug_map = _get_slug_map()
chain_id_value = slug_map.get(slug)
if chain_id_value is None:
return None
return ChainId(chain_id_value)
#: Override stuff we do not like in Chain data repo
#:
#: Arweave permaweb dropped
#: https://hv4gxzchk24cqfezebn3ujjz6oy2kbtztv5vghn6kpbkjc3vg4rq.arweave.net/PXhr5EdWuCgUmSBbuiU587GlBnmde1MdvlPCpIt1NyM/
#: with free AR from the faucet https://faucet.arweave.net/
#:
_CHAIN_DATA_OVERRIDES = {
1: {
"name": "Ethereum",
"slug": "ethereum",
"svg_icon": "https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg",
},
#
# BSC
#
56: {
"name": "BNB Smart Chain",
# Deployed on Arweave for good
"slug": "binance",
"svg_icon": "https://hv4gxzchk24cqfezebn3ujjz6oy2kbtztv5vghn6kpbkjc3vg4rq.arweave.net/fgp9wHyH92hION8E6CuPtUNbmiTlqsl23QbQlwA8cZQ",
},
#
# Polygon
#
137: {
"name": "Polygon",
"slug": "polygon",
"svg_icon": "https://hv4gxzchk24cqfezebn3ujjz6oy2kbtztv5vghn6kpbkjc3vg4rq.arweave.net/nLW0IfMZnhhaqdN1AbzC4d1NLZSpBlIMEHhXq-KcOws",
},
#
# Ethereum Classic / Ethereum Tester
#
61: {
"name": "Ethereum Classic",
"slug": "etc",
"svg_icon": "https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg",
"active": False,
},
#
# Ganache test chain
#
1337: {
"name": "Ganache",
"slug": "ganache",
"svg_icon": None,
"active": False,
"dataless": True,
},
#
# Avalanche
#
ChainId.avalanche.value: {
"name": "Avalanche C-chain",
"slug": "avalanche",
"svg_icon": "https://cryptologos.cc/logos/avalanche-avax-logo.svg",
},
#
# Arbitrum
#
ChainId.arbitrum.value: {
"name": "Arbitrum One",
"slug": "arbitrum",
"svg_icon": "https://cryptologos.cc/logos/arbitrum-arb-logo.svg",
},
#
# Base
#
ChainId.base.value: {
"name": "Base",
"slug": "base",
"svg_icon": "https://raw.githubusercontent.com/base-org/brand-kit/main/logo/in-product/Base_Network_Logo.svg",
},
#
# Mantle
#
ChainId.mantle.value: {
"name": "Mantle",
"slug": "mantle",
"svg_icon": "https://cryptologos.cc/logos/mantle-mnt-logo.svg",
},
#
# Osmosis
#
ChainId.osmosis.value: {
"name": "Osmosis",
"slug": "osmosis",
"svg_icon": None,
"active": False,
"dataless": True,
},
#
# Ethereum tester
#
ChainId.ethereum_tester.value: {
"name": "Ethereum tester",
"slug": "tester",
"svg_icon": None,
"active": False,
"dataless": True,
},
#
# Anvil tester
#
ChainId.anvil.value: {
"name": "Anvik",
"slug": "anvil",
"svg_icon": None,
"active": False,
"dataless": True,
},
#
# Unknown
#
ChainId.unknown.value: {
"name": "Unknown",
"slug": "unknown",
"svg_icon": None,
"active": False,
"dataless": True,
},
#
# Centralised exchange placeholder
#
ChainId.centralised_exchange.value: {
"name": "Centralised exchange",
"slug": "centralised-exchange",
"svg_icon": None,
"active": False,
"dataless": True,
},
}