diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 4e32a239..61074b57 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-12, macos-13, macos-14, ubuntu-22.04, ubuntu-24.04] + os: [macos-13, macos-14, ubuntu-22.04, ubuntu-24.04] runs-on: ${{ matrix.os }} steps: diff --git a/.gitignore b/.gitignore index f18f4bd6..5ab655ca 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ MANIFEST # Per-project virtualenvs .venv*/ +venv/* **/device.key # environment variables diff --git a/pyproject.toml b/pyproject.toml index 409694cf..d6644265 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dynamic = [ "version" ] dependencies = [ "aiohttp>=3.8.3", "aioresponses>=0.7.6", - "aleph-message>=0.4.9", + "aleph-message>=0.5", "aleph-superfluid>=0.2.1", "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version<'3.11'", diff --git a/src/aleph/sdk/account.py b/src/aleph/sdk/account.py index 9bfafcd3..0bf54201 100644 --- a/src/aleph/sdk/account.py +++ b/src/aleph/sdk/account.py @@ -7,8 +7,10 @@ from aleph.sdk.chains.common import get_fallback_private_key from aleph.sdk.chains.ethereum import ETHAccount +from aleph.sdk.chains.evm import EVMAccount from aleph.sdk.chains.remote import RemoteAccount from aleph.sdk.chains.solana import SOLAccount +from aleph.sdk.chains.substrate import DOTAccount from aleph.sdk.conf import load_main_configuration, settings from aleph.sdk.evm_utils import get_chains_with_super_token from aleph.sdk.types import AccountFromPrivateKey @@ -18,10 +20,24 @@ T = TypeVar("T", bound=AccountFromPrivateKey) chain_account_map: Dict[Chain, Type[T]] = { # type: ignore - Chain.ETH: ETHAccount, + Chain.ARBITRUM: EVMAccount, Chain.AVAX: ETHAccount, Chain.BASE: ETHAccount, + Chain.BLAST: EVMAccount, + Chain.BOB: EVMAccount, + Chain.CYBER: EVMAccount, + Chain.DOT: DOTAccount, + Chain.ETH: ETHAccount, + Chain.FRAXTAL: EVMAccount, + Chain.LINEA: EVMAccount, + Chain.LISK: EVMAccount, + Chain.METIS: EVMAccount, + Chain.MODE: EVMAccount, + Chain.OPTIMISM: EVMAccount, + Chain.POL: EVMAccount, Chain.SOL: SOLAccount, + Chain.WORLDCHAIN: EVMAccount, + Chain.ZORA: EVMAccount, } @@ -43,7 +59,7 @@ def account_from_hex_string( return account_type(bytes.fromhex(private_key_str)) # type: ignore account_type = load_chain_account_type(chain) - account = account_type(bytes.fromhex(private_key_str)) + account = account_type(bytes.fromhex(private_key_str), chain) if chain in get_chains_with_super_token(): account.switch_chain(chain) return account # type: ignore @@ -62,7 +78,7 @@ def account_from_file( return account_type(private_key) # type: ignore account_type = load_chain_account_type(chain) - account = account_type(private_key) + account = account_type(private_key, chain) if chain in get_chains_with_super_token(): account.switch_chain(chain) return account @@ -76,28 +92,29 @@ def _load_account( ) -> AccountFromPrivateKey: """Load an account from a private key string or file, or from the configuration file.""" - # Loads configuration if no account_type is specified - if not account_type: - config = load_main_configuration(settings.CONFIG_FILE) + config = load_main_configuration(settings.CONFIG_FILE) + chain_to_use = settings.DEFAULT_CHAIN + + if not chain: if config and hasattr(config, "chain"): - account_type = load_chain_account_type(config.chain) + chain_to_use = config.chain logger.debug( f"Detected {config.chain} account for path {settings.CONFIG_FILE}" ) - else: - account_type = account_type = load_chain_account_type( - Chain.ETH - ) # Defaults to ETHAccount - logger.warning( - f"No main configuration data found in {settings.CONFIG_FILE}, defaulting to {account_type and account_type.__name__}" - ) + + # Loads configuration if no account_type is specified + if not account_type: + account_type = load_chain_account_type(chain_to_use) + logger.warning( + f"No main configuration data found in {settings.CONFIG_FILE}, defaulting to {account_type and account_type.__name__}" + ) # Loads private key from a string if private_key_str: - return account_from_hex_string(private_key_str, account_type, chain) + return account_from_hex_string(private_key_str, account_type, chain_to_use) # Loads private key from a file elif private_key_path and private_key_path.is_file(): - return account_from_file(private_key_path, account_type, chain) + return account_from_file(private_key_path, account_type, chain_to_use) # For ledger keys elif settings.REMOTE_CRYPTO_HOST: logger.debug("Using remote account") @@ -112,7 +129,7 @@ def _load_account( else: new_private_key = get_fallback_private_key() account = account_from_hex_string( - bytes.hex(new_private_key), account_type, chain + bytes.hex(new_private_key), account_type, chain_to_use ) logger.info( f"Generated fallback private key with address {account.get_address()}" diff --git a/src/aleph/sdk/chains/evm.py b/src/aleph/sdk/chains/evm.py new file mode 100644 index 00000000..5bf66ef1 --- /dev/null +++ b/src/aleph/sdk/chains/evm.py @@ -0,0 +1,48 @@ +from decimal import Decimal +from pathlib import Path +from typing import Awaitable, Optional + +from aleph_message.models import Chain +from eth_account import Account # type: ignore + +from .common import get_fallback_private_key +from .ethereum import ETHAccount + + +class EVMAccount(ETHAccount): + def __init__(self, private_key: bytes, chain: Optional[Chain] = None): + super().__init__(private_key, chain) + # Decide if we have to send also the specified chain value or always use ETH + # if chain: + # self.CHAIN = chain + + @staticmethod + def from_mnemonic(mnemonic: str, chain: Optional[Chain] = None) -> "EVMAccount": + Account.enable_unaudited_hdwallet_features() + return EVMAccount( + private_key=Account.from_mnemonic(mnemonic=mnemonic).key, chain=chain + ) + + def get_token_balance(self) -> Decimal: + raise ValueError(f"Token not implemented for this chain {self.CHAIN}") + + def get_super_token_balance(self) -> Decimal: + raise ValueError(f"Super token not implemented for this chain {self.CHAIN}") + + def create_flow(self, receiver: str, flow: Decimal) -> Awaitable[str]: + raise ValueError(f"Flow creation not implemented for this chain {self.CHAIN}") + + def get_flow(self, receiver: str): + raise ValueError(f"Get flow not implemented for this chain {self.CHAIN}") + + def update_flow(self, receiver: str, flow: Decimal) -> Awaitable[str]: + raise ValueError(f"Flow update not implemented for this chain {self.CHAIN}") + + def delete_flow(self, receiver: str) -> Awaitable[str]: + raise ValueError(f"Flow deletion not implemented for this chain {self.CHAIN}") + + +def get_fallback_account( + path: Optional[Path] = None, chain: Optional[Chain] = None +) -> ETHAccount: + return ETHAccount(private_key=get_fallback_private_key(path=path), chain=chain) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 5fe4cd4b..846d82ab 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -99,10 +99,9 @@ class Settings(BaseSettings): active=False, ), # MAINNETS - Chain.ETH: ChainInfo( - chain_id=1, - rpc="https://eth-mainnet.public.blastapi.io", - token="0x27702a26126e0B3702af63Ee09aC4d1A084EF628", + Chain.ARBITRUM: ChainInfo( + chain_id=42161, + rpc="https://arbitrum-one.publicnode.com", ), Chain.AVAX: ChainInfo( chain_id=43114, @@ -116,12 +115,65 @@ class Settings(BaseSettings): token="0xc0Fbc4967259786C743361a5885ef49380473dCF", super_token="0xc0Fbc4967259786C743361a5885ef49380473dCF", ), + Chain.BLAST: ChainInfo( + chain_id=81457, + rpc="https://blastl2-mainnet.public.blastapi.io", + ), + Chain.BOB: ChainInfo( + chain_id=60808, + rpc="https://bob-mainnet.public.blastapi.io", + ), Chain.BSC: ChainInfo( chain_id=56, rpc="https://binance.llamarpc.com", token="0x82D2f8E02Afb160Dd5A480a617692e62de9038C4", active=False, ), + Chain.CYBER: ChainInfo( + chain_id=7560, + rpc="https://rpc.cyber.co", + ), + Chain.ETH: ChainInfo( + chain_id=1, + rpc="https://eth-mainnet.public.blastapi.io", + token="0x27702a26126e0B3702af63Ee09aC4d1A084EF628", + ), + Chain.FRAXTAL: ChainInfo( + chain_id=252, + rpc="https://rpc.frax.com", + ), + Chain.LINEA: ChainInfo( + chain_id=59144, + rpc="https://linea-rpc.publicnode.com", + ), + Chain.LISK: ChainInfo( + chain_id=1135, + rpc="https://rpc.api.lisk.com", + ), + Chain.METIS: ChainInfo( + chain_id=1088, + rpc="https://metis.drpc.org", + ), + Chain.MODE: ChainInfo( + chain_id=34443, + rpc="https://mode.drpc.org", + ), + Chain.OPTIMISM: ChainInfo( + chain_id=10, + rpc="https://optimism-rpc.publicnode.com", + ), + Chain.POL: ChainInfo( + chain_id=137, + rpc="https://polygon.gateway.tenderly.co", + ), + Chain.WORLDCHAIN: ChainInfo( + chain_id=480, + rpc="https://worldchain-mainnet.gateway.tenderly.co", + ), + Chain.ZORA: ChainInfo( + chain_id=7777777, + rpc="https://rpc.zora.energy/", + ), } # Add all placeholders to allow easy dynamic setup of CHAINS CHAINS_SEPOLIA_ACTIVE: Optional[bool] @@ -135,6 +187,8 @@ class Settings(BaseSettings): CHAINS_BASE_RPC: Optional[str] CHAINS_BSC_RPC: Optional[str] + DEFAULT_CHAIN: Chain = Chain.ETH + # Dns resolver DNS_IPFS_DOMAIN = "ipfs.public.aleph.sh" DNS_PROGRAM_DOMAIN = "program.public.aleph.sh" diff --git a/src/aleph/sdk/evm_utils.py b/src/aleph/sdk/evm_utils.py index c7166cec..4d2026ef 100644 --- a/src/aleph/sdk/evm_utils.py +++ b/src/aleph/sdk/evm_utils.py @@ -79,10 +79,16 @@ def get_super_token_address( return None -def get_chains_with_holding() -> List[Union[Chain, str]]: +def get_compatible_chains() -> List[Union[Chain, str]]: return [chain for chain, info in settings.CHAINS.items() if info.active] +def get_chains_with_holding() -> List[Union[Chain, str]]: + return [ + chain for chain, info in settings.CHAINS.items() if info.active and info.token + ] + + def get_chains_with_super_token() -> List[Union[Chain, str]]: return [ chain diff --git a/src/aleph/sdk/types.py b/src/aleph/sdk/types.py index dab90379..8f70dbd8 100644 --- a/src/aleph/sdk/types.py +++ b/src/aleph/sdk/types.py @@ -6,7 +6,7 @@ __all__ = ("StorageEnum", "Account", "AccountFromPrivateKey", "GenericMessage") -from aleph_message.models import AlephMessage +from aleph_message.models import AlephMessage, Chain class StorageEnum(str, Enum): @@ -35,7 +35,7 @@ def get_public_key(self) -> str: ... class AccountFromPrivateKey(Account, Protocol): """Only accounts that are initialized from a private key string are supported.""" - def __init__(self, private_key: bytes): ... + def __init__(self, private_key: bytes, chain: Chain): ... async def sign_raw(self, buffer: bytes) -> bytes: ... @@ -77,6 +77,6 @@ class ChainInfo(BaseModel): chain_id: int rpc: str - token: str + token: Optional[str] = None super_token: Optional[str] = None active: bool = True