Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add skale-allocator project #53

Merged
merged 11 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions python/scripts/full_check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ echo "Run pylint"
pylint src
echo "Run mypy"
mypy --strict src
echo "Run flake8"
flake8 src
31 changes: 23 additions & 8 deletions python/src/skale_contracts/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -24,14 +25,15 @@
"stateMutability": "view",
"payable": False,
"inputs": [],
"outputs": [ { "type": "string", "name": "" } ]
"outputs": [{"type": "string", "name": ""}]
}


@dataclass
class InstanceData:
"""Contains instance data"""
data: dict[str, str]

@classmethod
def from_json(cls, data: str) -> InstanceData:
"""Create InstanceData object from json string"""
Expand Down Expand Up @@ -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())
7 changes: 6 additions & 1 deletion python/src/skale_contracts/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class NetworkMetadata:
chain_id: int
path: str


@dataclass
class MetadataFile:
"""Represents file with metadata"""
Expand All @@ -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]
Expand All @@ -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.
"""
Expand Down
13 changes: 11 additions & 2 deletions python/src/skale_contracts/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
19 changes: 14 additions & 5 deletions python/src/skale_contracts/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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
Expand Down
25 changes: 10 additions & 15 deletions python/src/skale_contracts/project_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
4 changes: 0 additions & 4 deletions python/src/skale_contracts/project_metadata.py

This file was deleted.

3 changes: 2 additions & 1 deletion python/src/skale_contracts/projects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
74 changes: 52 additions & 22 deletions python/src/skale_contracts/projects/ima.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,6 +12,7 @@


if TYPE_CHECKING:
from eth_typing import Address, ChecksumAddress
from web3.contract.contract import Contract

MESSAGE_PROXY_ABI = [
Expand All @@ -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):
Expand All @@ -45,24 +45,30 @@ 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()
)

@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(
Expand All @@ -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)

Expand All @@ -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")
Expand All @@ -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:
Expand Down
Loading
Loading