diff --git a/python/scripts/full_check.sh b/python/scripts/full_check.sh index 391145b..efd36c6 100755 --- a/python/scripts/full_check.sh +++ b/python/scripts/full_check.sh @@ -10,3 +10,5 @@ echo "Run pylint" pylint src echo "Run mypy" mypy --strict src +echo "Run flake8" +flake8 src diff --git a/python/src/skale_contracts/instance.py b/python/src/skale_contracts/instance.py index 3c2a2f1..19b9b9a 100644 --- a/python/src/skale_contracts/instance.py +++ b/python/src/skale_contracts/instance.py @@ -3,8 +3,9 @@ from __future__ import annotations from abc import ABC, abstractmethod import json -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, cast from attr import dataclass +from eth_typing import ChecksumAddress from parver import Version as PyVersion from semver.version import Version as SemVersion @@ -24,7 +25,7 @@ "stateMutability": "view", "payable": False, "inputs": [], - "outputs": [ { "type": "string", "name": "" } ] + "outputs": [{"type": "string", "name": ""}] } @@ -32,6 +33,7 @@ class InstanceData: """Contains instance data""" data: dict[str, str] + @classmethod def from_json(cls, data: str) -> InstanceData: """Create InstanceData object from json string""" @@ -79,20 +81,33 @@ def version(self) -> str: def abi(self) -> SkaleAbi: """Get abi file of the project instance""" if self._abi is None: - self._abi = json.loads(self._project.download_abi_file(self.version)) + self._abi = json.loads( + self._project.download_abi_file(self.version) + ) return self._abi @abstractmethod - def get_contract_address(self, name: str) -> Address: + def get_contract_address( + self, + name: str, + *args: str | Address | ChecksumAddress + ) -> Address: """Get address of the contract by it's name""" - def get_contract(self, name: str) -> Contract: + def get_contract( + self, + name: str, + *args: str | Address | ChecksumAddress + ) -> Contract: """Get Contract object of the contract by it's name""" - address = self.get_contract_address(name) + address = self.get_contract_address(name, *args) return self.web3.eth.contract(address=address, abi=self.abi[name]) # protected - @abstractmethod def _get_version(self) -> str: - pass + contract = self.web3.eth.contract( + address=self.address, + abi=[DEFAULT_GET_VERSION_FUNCTION] + ) + return cast(str, contract.functions.version().call()) diff --git a/python/src/skale_contracts/metadata.py b/python/src/skale_contracts/metadata.py index 924db1a..c4a1287 100644 --- a/python/src/skale_contracts/metadata.py +++ b/python/src/skale_contracts/metadata.py @@ -16,6 +16,7 @@ class NetworkMetadata: chain_id: int path: str + @dataclass class MetadataFile: """Represents file with metadata""" @@ -33,6 +34,7 @@ def from_json(cls, data: str) -> MetadataFile: path=network['path'])) return cls(networks) + class Metadata: """Class to manage SKALE contracts metadata""" networks: list[NetworkMetadata] @@ -49,7 +51,10 @@ def download(self) -> None: metadata = MetadataFile.from_json(metadata_response.text) self.networks = metadata.networks - def get_network_by_chain_id(self, chain_id: int) -> Optional[NetworkMetadata]: + def get_network_by_chain_id( + self, + chain_id: int + ) -> Optional[NetworkMetadata]: """Get network metadata by it's chain id. Returns None if there is no such network in the metadata. """ diff --git a/python/src/skale_contracts/network.py b/python/src/skale_contracts/network.py index 1603ca9..789658d 100644 --- a/python/src/skale_contracts/network.py +++ b/python/src/skale_contracts/network.py @@ -15,7 +15,11 @@ class Network: """Represents blockchain with deployed smart contracts projects""" - def __init__(self, skale_contracts: SkaleContracts, provider: BaseProvider): + def __init__( + self, + skale_contracts: SkaleContracts, + provider: BaseProvider + ): self.web3 = Web3(provider) self._skale_contracts = skale_contracts @@ -39,7 +43,12 @@ def as_listed(self) -> ListedNetwork: class ListedNetwork(Network): """Network that is listed in the metadata""" - def __init__(self, skale_contracts: SkaleContracts, provider: BaseProvider, path: str): + def __init__( + self, + skale_contracts: SkaleContracts, + provider: BaseProvider, + path: str + ): super().__init__(skale_contracts, provider) self.path = path diff --git a/python/src/skale_contracts/project.py b/python/src/skale_contracts/project.py index 8be3bf7..59599ef 100644 --- a/python/src/skale_contracts/project.py +++ b/python/src/skale_contracts/project.py @@ -11,22 +11,30 @@ if TYPE_CHECKING: from eth_typing import Address from .network import Network - from .project_metadata import ProjectMetadata class Project(ABC): """Represents set of smart contracts known as project""" - def __init__(self, network: Network, metadata: ProjectMetadata) -> None: + def __init__(self, network: Network) -> None: super().__init__() self.network = network - self._metadata = metadata + + @staticmethod + @abstractmethod + def name() -> str: + """Name of the project""" @property @abstractmethod def github_repo(self) -> str: """URL of github repo with the project""" + @property + def folder(self) -> str: + """Folder name with instances json files""" + return self.name() + def get_instance(self, alias_or_address: str) -> Instance: """Create instance object based on alias or address""" if self.network.web3.is_address(alias_or_address): @@ -53,7 +61,8 @@ def download_abi_file(self, version: str) -> str: def get_abi_url(self, version: str) -> str: """Calculate URL of ABI file""" - return f'{self.github_repo}releases/download/{version}/{self.get_abi_filename(version)}' + filename = self.get_abi_filename(version) + return f'{self.github_repo}releases/download/{version}/{filename}' @abstractmethod def get_abi_filename(self, version: str) -> str: @@ -63,7 +72,7 @@ def get_instance_data_url(self, alias: str) -> str: """Get URL of a file containing address for provided alias""" if self.network.is_listed(): return f'{REPOSITORY_URL}{self.network.as_listed().path}/' + \ - f'{self._metadata.path}/{alias}.json' + f'{self.folder}/{alias}.json' raise ValueError('Network is unknown') @abstractmethod diff --git a/python/src/skale_contracts/project_factory.py b/python/src/skale_contracts/project_factory.py index de376d5..035b846 100644 --- a/python/src/skale_contracts/project_factory.py +++ b/python/src/skale_contracts/project_factory.py @@ -2,30 +2,25 @@ from __future__ import annotations from typing import TYPE_CHECKING -from attr import dataclass +import inspect +from .project import Project from . import projects -from .project_metadata import ProjectMetadata if TYPE_CHECKING: - from .project import Project from .network import Network -@dataclass -class Projects: - """Contains all known projects""" - skale_manager = ProjectMetadata(name='skale-manager', path='skale-manager') - mainnet_ima = ProjectMetadata(name='mainnet-ima', path='mainnet-ima') - schain_ima = ProjectMetadata(name='schain-ima', path='schain-ima') +projects_dict = { + project_type.name(): project_type + for _, project_type + in inspect.getmembers(projects, inspect.isclass) + if issubclass(project_type, Project) +} def create_project(network: Network, name: str) -> Project: """Create Project object based on it's name""" - if name == Projects.skale_manager.name: - return projects.SkaleManager(network, Projects.skale_manager) - if name == Projects.mainnet_ima.name: - return projects.MainnetIma(network, Projects.mainnet_ima) - if name == Projects.schain_ima.name: - return projects.SchainIma(network, Projects.schain_ima) + if name in projects_dict: + return projects_dict[name](network) raise ValueError(f'Project with name {name} is unknown') diff --git a/python/src/skale_contracts/project_metadata.py b/python/src/skale_contracts/project_metadata.py deleted file mode 100644 index 96435a8..0000000 --- a/python/src/skale_contracts/project_metadata.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Tools for project metadata processing""" -from collections import namedtuple - -ProjectMetadata = namedtuple('ProjectMetadata', ['name', 'path']) diff --git a/python/src/skale_contracts/projects/__init__.py b/python/src/skale_contracts/projects/__init__.py index 7eb124c..cc88ec4 100644 --- a/python/src/skale_contracts/projects/__init__.py +++ b/python/src/skale_contracts/projects/__init__.py @@ -3,5 +3,6 @@ MainnetImaProject as MainnetIma, \ SchainImaProject as SchainIma from .skale_manager import SkaleManagerProject as SkaleManager +from .skale_allocator import SkaleAllocatorProject as SkaleAllocator -__all__ = ['MainnetIma', 'SchainIma', 'SkaleManager'] +__all__ = ['MainnetIma', 'SchainIma', 'SkaleAllocator', 'SkaleManager'] diff --git a/python/src/skale_contracts/projects/ima.py b/python/src/skale_contracts/projects/ima.py index 8574662..dd2a0d7 100644 --- a/python/src/skale_contracts/projects/ima.py +++ b/python/src/skale_contracts/projects/ima.py @@ -1,8 +1,7 @@ """Module connects IMA to the SKALE contracts library""" from __future__ import annotations -from typing import cast, TYPE_CHECKING -from eth_typing import Address +from typing import TYPE_CHECKING from eth_utils.address import to_canonical_address from skale_contracts.constants import PREDEPLOYED_ALIAS @@ -13,6 +12,7 @@ if TYPE_CHECKING: + from eth_typing import Address, ChecksumAddress from web3.contract.contract import Contract MESSAGE_PROXY_ABI = [ @@ -24,10 +24,10 @@ class ImaInstance(Instance): """Represents instance of IMA""" def __init__(self, project: Project, address: Address) -> None: super().__init__(project, address) - self.message_proxy = self.web3.eth.contract(address=address, abi=MESSAGE_PROXY_ABI) - - def _get_version(self) -> str: - return cast(str, self.message_proxy.functions.version().call()) + self.message_proxy = self.web3.eth.contract( + address=address, + abi=MESSAGE_PROXY_ABI + ) class ImaProject(Project): @@ -45,16 +45,21 @@ def __init__(self, project: Project, address: Address) -> None: super().__init__(project, address) self._contract_manager: Contract | None = None - def get_contract_address(self, name: str) -> Address: + def get_contract_address( + self, + name: str, *args: str | Address | ChecksumAddress + ) -> Address: if name == 'MessageProxyForMainnet': return self.address if name == 'CommunityPool': return to_canonical_address( - self.get_contract("MessageProxyForMainnet").functions.communityPool().call() + self.get_contract("MessageProxyForMainnet") + .functions.communityPool().call() ) if name == 'Linker': return to_canonical_address( - self.get_contract("MessageProxyForMainnet").functions.linker().call() + self.get_contract("MessageProxyForMainnet") + .functions.linker().call() ) return to_canonical_address( self.contract_manager.functions.getContract(name).call() @@ -62,7 +67,8 @@ def get_contract_address(self, name: str) -> Address: @property def contract_manager(self) -> Contract: - """ContractManager contract of a skale-manager instance associated with the IMA""" + """ContractManager contract of a skale-manager instance +associated with the IMA""" if self._contract_manager is None: self._contract_manager = self.web3.eth.contract( address=to_canonical_address( @@ -77,6 +83,10 @@ def contract_manager(self) -> Contract: class MainnetImaProject(ImaProject): """Represents mainnet part of IMA project""" + @staticmethod + def name() -> str: + return 'mainnet-ima' + def create_instance(self, address: Address) -> Instance: return MainnetImaInstance(self, address) @@ -89,19 +99,33 @@ class SchainImaInstance(ImaInstance): PREDEPLOYED: dict[str, Address] = { name: to_canonical_address(address) for name, address in { - 'ProxyAdmin': '0xd2aAa00000000000000000000000000000000000', - 'MessageProxyForSchain': '0xd2AAa00100000000000000000000000000000000', - 'KeyStorage': '0xd2aaa00200000000000000000000000000000000', - 'CommunityLocker': '0xD2aaa00300000000000000000000000000000000', - 'TokenManagerEth': '0xd2AaA00400000000000000000000000000000000', - 'TokenManagerERC20': '0xD2aAA00500000000000000000000000000000000', - 'TokenManagerERC721': '0xD2aaa00600000000000000000000000000000000', - 'TokenManagerLinker': '0xD2aAA00800000000000000000000000000000000', - 'TokenManagerERC1155': '0xD2aaA00900000000000000000000000000000000', - 'TokenManagerERC721WithMetadata': '0xd2AaA00a00000000000000000000000000000000' + 'ProxyAdmin': + '0xd2aAa00000000000000000000000000000000000', + 'MessageProxyForSchain': + '0xd2AAa00100000000000000000000000000000000', + 'KeyStorage': + '0xd2aaa00200000000000000000000000000000000', + 'CommunityLocker': + '0xD2aaa00300000000000000000000000000000000', + 'TokenManagerEth': + '0xd2AaA00400000000000000000000000000000000', + 'TokenManagerERC20': + '0xD2aAA00500000000000000000000000000000000', + 'TokenManagerERC721': + '0xD2aaa00600000000000000000000000000000000', + 'TokenManagerLinker': + '0xD2aAA00800000000000000000000000000000000', + 'TokenManagerERC1155': + '0xD2aaA00900000000000000000000000000000000', + 'TokenManagerERC721WithMetadata': + '0xd2AaA00a00000000000000000000000000000000' }.items()} - def get_contract_address(self, name: str) -> Address: + def get_contract_address( + self, + name: str, + *args: str | Address | ChecksumAddress + ) -> Address: if name in self.PREDEPLOYED: return self.PREDEPLOYED[name] raise RuntimeError(f"Can't get address of {name} contract") @@ -110,9 +134,15 @@ def get_contract_address(self, name: str) -> Address: class SchainImaProject(ImaProject): """Represents schain part of IMA project""" + @staticmethod + def name() -> str: + return 'schain-ima' + def get_instance(self, alias_or_address: str) -> Instance: if alias_or_address == PREDEPLOYED_ALIAS: - return self.create_instance(SchainImaInstance.PREDEPLOYED['MessageProxyForSchain']) + return self.create_instance( + SchainImaInstance.PREDEPLOYED['MessageProxyForSchain'] + ) return super().get_instance(alias_or_address) def create_instance(self, address: Address) -> Instance: diff --git a/python/src/skale_contracts/projects/skale_allocator.py b/python/src/skale_contracts/projects/skale_allocator.py new file mode 100644 index 0000000..dcc1350 --- /dev/null +++ b/python/src/skale_contracts/projects/skale_allocator.py @@ -0,0 +1,57 @@ +"""Module connects skale-allocator project to the SKALE contracts library""" + +from __future__ import annotations +from typing import TYPE_CHECKING +from eth_utils.address import to_canonical_address + +from skale_contracts.instance import Instance +from skale_contracts.project import Project + + +if TYPE_CHECKING: + from eth_typing import Address, ChecksumAddress + + +class SkaleAllocatorInstance(Instance): + """Represents instance of skale-allocator""" + def __init__(self, project: Project, address: Address) -> None: + super().__init__(project, address) + self.allocator = self.get_contract("Allocator") + + def get_contract_address( + self, + name: str, + *args: str | Address | ChecksumAddress + ) -> Address: + if name == 'Allocator': + return self.address + if name == 'Escrow': + if len(args) > 0: + beneficiary = args[0] + if self.web3.is_address(beneficiary): + return self._get_escrow(to_canonical_address(beneficiary)) + raise ValueError('Beneficiary is not set') + raise ValueError(f'Contract ${name} is not found') + + def _get_escrow(self, beneficiary: Address) -> Address: + return to_canonical_address( + self.allocator.functions.getEscrowAddress(beneficiary).call() + ) + + +class SkaleAllocatorProject(Project): + """Represents skale-allocator project""" + + @staticmethod + def name() -> str: + return 'skale-allocator' + + @property + def github_repo(self) -> str: + return 'https://github.com/skalenetwork/skale-allocator/' + + def create_instance(self, address: Address) -> Instance: + return SkaleAllocatorInstance(self, address) + + def get_abi_filename(self, version: str) -> str: + return f'skale-allocator-{version}-abi.json' diff --git a/python/src/skale_contracts/projects/skale_manager.py b/python/src/skale_contracts/projects/skale_manager.py index 9cf8840..1068e85 100644 --- a/python/src/skale_contracts/projects/skale_manager.py +++ b/python/src/skale_contracts/projects/skale_manager.py @@ -1,7 +1,7 @@ """Module connects skale-manager project to the SKALE contracts library""" from __future__ import annotations -from typing import cast, TYPE_CHECKING +from typing import TYPE_CHECKING from eth_utils.address import to_canonical_address from skale_contracts.instance import Instance, DEFAULT_GET_VERSION_FUNCTION @@ -9,14 +9,18 @@ if TYPE_CHECKING: - from eth_typing import Address + from eth_typing import Address, ChecksumAddress from web3.contract.contract import Contract SKALE_MANAGER_ABI = [ { "inputs": [], "name": "contractManager", - "outputs": [{ "internalType": "contract IContractManager", "name": "", "type": "address" }], + "outputs": [{ + "internalType": "contract IContractManager", + "name": "", + "type": "address" + }], "stateMutability": "view", "type": "function" }, DEFAULT_GET_VERSION_FUNCTION @@ -24,9 +28,17 @@ CONTRACT_MANAGER_ABI = [ { - "inputs": [{ "internalType": "string", "name": "name", "type": "string" }], + "inputs": [{ + "internalType": "string", + "name": "name", + "type": "string" + }], "name": "getContract", - "outputs": [{ "internalType": "address", "name": "contractAddress", "type": "address" }], + "outputs": [{ + "internalType": "address", + "name": "contractAddress", + "type": "address" + }], "stateMutability": "view", "type": "function" } @@ -37,8 +49,12 @@ class SkaleManagerInstance(Instance): """Represents instance of skale-manager""" def __init__(self, project: Project, address: Address) -> None: super().__init__(project, address) - self.skale_manager = self.web3.eth.contract(address=address, abi=SKALE_MANAGER_ABI) - contract_manager_address: Address = self.skale_manager.functions.contractManager().call() + self.skale_manager = self.web3.eth.contract( + address=address, + abi=SKALE_MANAGER_ABI + ) + contract_manager_address: Address = \ + self.skale_manager.functions.contractManager().call() self.contract_manager: Contract = self.web3.eth.contract( address=contract_manager_address, abi=CONTRACT_MANAGER_ABI @@ -48,14 +64,17 @@ def __init__(self, project: Project, address: Address) -> None: 'TimeHelpersWithDebug': 'TimeHelpers' } - def get_contract_address(self, name: str) -> Address: + def get_contract_address( + self, + name: str, + *args: str | Address | ChecksumAddress + ) -> Address: return to_canonical_address( - self.contract_manager.functions.getContract(self._actual_name(name)).call() + self.contract_manager.functions.getContract( + self._actual_name(name) + ).call() ) - def _get_version(self) -> str: - return cast(str, self.skale_manager.functions.version().call()) - def _actual_name(self, name: str) -> str: if name in self.custom_names: return self.custom_names[name] @@ -65,6 +84,10 @@ def _actual_name(self, name: str) -> str: class SkaleManagerProject(Project): """Represents skale-manager project""" + @staticmethod + def name() -> str: + return 'skale-manager' + @property def github_repo(self) -> str: return 'https://github.com/skalenetwork/skale-manager/' diff --git a/python/test/requirements.txt b/python/test/requirements.txt index ec45407..29b2fa0 100644 --- a/python/test/requirements.txt +++ b/python/test/requirements.txt @@ -1,3 +1,4 @@ +flake8 mypy pylint types-requests