Skip to content
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
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,17 @@ classifiers = [
]
dependencies = [
"aiohttp>=3.8.3",
"aleph-message>=0.4.8",
"aleph-message>=0.4.9",
"coincurve; python_version<\"3.11\"",
"coincurve>=19.0.0; python_version>=\"3.11\"",
"eth_abi>=4.0.0; python_version>=\"3.11\"",
"eth_account>=0.4.0,<0.11.0",
"jwcrypto==1.5.6",
"python-magic",
"typing_extensions",
"aioresponses>=0.7.6",
"superfluid~=0.2.1",
"superfluid@git+https://github.com/1yam/superfluid.py.git@1yam-add-base",
"eth_typing==4.3.1",

"web3==6.3.0",
]

[project.optional-dependencies]
Expand Down
196 changes: 125 additions & 71 deletions src/aleph/sdk/chains/ethereum.py
Original file line number Diff line number Diff line change
@@ -1,103 +1,167 @@
import asyncio
from decimal import Decimal
from pathlib import Path
from typing import Awaitable, Dict, Optional, Set, Union
from typing import Awaitable, Optional, Union

from aleph_message.models import Chain
from eth_account import Account
from eth_account import Account # type: ignore
from eth_account.messages import encode_defunct
from eth_account.signers.local import LocalAccount
from eth_keys.exceptions import BadSignature as EthBadSignatureError
from superfluid import Web3FlowInfo
from web3 import Web3
from web3.middleware import geth_poa_middleware
from web3.types import TxParams, TxReceipt

from aleph.sdk.exceptions import InsufficientFundsError

from ..conf import settings
from ..connectors.superfluid import Superfluid
from ..evm_utils import (
BALANCEOF_ABI,
MIN_ETH_BALANCE,
MIN_ETH_BALANCE_WEI,
get_chain_id,
get_chains_with_super_token,
get_rpc,
get_super_token_address,
get_token_address,
to_human_readable_token,
)
from ..exceptions import BadSignatureError
from ..utils import bytes_from_hex
from .common import BaseAccount, get_fallback_private_key, get_public_key

CHAINS_WITH_SUPERTOKEN: Set[Chain] = {Chain.AVAX}
CHAIN_IDS: Dict[Chain, int] = {
Chain.AVAX: settings.AVAX_CHAIN_ID,
}


def get_rpc_for_chain(chain: Chain):
"""Returns the RPC to use for a given Ethereum based blockchain"""
if not chain:
return None

if chain == Chain.AVAX:
return settings.AVAX_RPC
else:
raise ValueError(f"Unknown RPC for chain {chain}")


def get_chain_id_for_chain(chain: Chain):
"""Returns the chain ID of a given Ethereum based blockchain"""
if not chain:
return None

if chain in CHAIN_IDS:
return CHAIN_IDS[chain]
else:
raise ValueError(f"Unknown RPC for chain {chain}")


class ETHAccount(BaseAccount):
"""Interact with an Ethereum address or key pair"""
"""Interact with an Ethereum address or key pair on EVM blockchains"""

CHAIN = "ETH"
CURVE = "secp256k1"
_account: LocalAccount
_provider: Optional[Web3]
chain: Optional[Chain]
chain_id: Optional[int]
rpc: Optional[str]
superfluid_connector: Optional[Superfluid]

def __init__(
self,
private_key: bytes,
chain: Optional[Chain] = None,
rpc: Optional[str] = None,
chain_id: Optional[int] = None,
):
self.private_key = private_key
self._account = Account.from_key(self.private_key)
self.chain = chain
rpc = rpc or get_rpc_for_chain(chain)
chain_id = chain_id or get_chain_id_for_chain(chain)
self.superfluid_connector = (
Superfluid(
rpc=rpc,
chain_id=chain_id,
account=self._account,
)
if chain in CHAINS_WITH_SUPERTOKEN
else None
self._account: LocalAccount = Account.from_key(self.private_key)
self.connect_chain(chain=chain)

@staticmethod
def from_mnemonic(mnemonic: str, chain: Optional[Chain] = None) -> "ETHAccount":
Account.enable_unaudited_hdwallet_features()
return ETHAccount(
private_key=Account.from_mnemonic(mnemonic=mnemonic).key, chain=chain
)

def get_address(self) -> str:
return self._account.address

def get_public_key(self) -> str:
return "0x" + get_public_key(private_key=self._account.key).hex()

async def sign_raw(self, buffer: bytes) -> bytes:
"""Sign a raw buffer."""
msghash = encode_defunct(text=buffer.decode("utf-8"))
sig = self._account.sign_message(msghash)
return sig["signature"]

def get_address(self) -> str:
return self._account.address
def connect_chain(self, chain: Optional[Chain] = None):
self.chain = chain
if self.chain:
self.chain_id = get_chain_id(self.chain)
self.rpc = get_rpc(self.chain)
self._provider = Web3(Web3.HTTPProvider(self.rpc))
if chain == Chain.BSC:
self._provider.middleware_onion.inject(
geth_poa_middleware, "geth_poa", layer=0
)
else:
self.chain_id = None
self.rpc = None
self._provider = None

if chain in get_chains_with_super_token() and self._provider:
self.superfluid_connector = Superfluid(self)
else:
self.superfluid_connector = None

def switch_chain(self, chain: Optional[Chain] = None):
self.connect_chain(chain=chain)

def can_transact(self, block=True) -> bool:
balance = self.get_eth_balance()
valid = balance > MIN_ETH_BALANCE_WEI if self.chain else False
if not valid and block:
raise InsufficientFundsError(
required_funds=MIN_ETH_BALANCE,
available_funds=to_human_readable_token(balance),
)
return valid

async def _sign_and_send_transaction(self, tx_params: TxParams) -> str:
"""
Sign and broadcast a transaction using the provided ETHAccount
@param tx_params - Transaction parameters
@returns - str - Transaction hash
"""
self.can_transact()

def sign_and_send() -> TxReceipt:
if self._provider is None:
raise ValueError("Provider not connected")
signed_tx = self._provider.eth.account.sign_transaction(
tx_params, self._account.key
)
tx_hash = self._provider.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = self._provider.eth.wait_for_transaction_receipt(
tx_hash, settings.TX_TIMEOUT
)
return tx_receipt

def get_public_key(self) -> str:
return "0x" + get_public_key(private_key=self._account.key).hex()
loop = asyncio.get_running_loop()
tx_receipt = await loop.run_in_executor(None, sign_and_send)
return tx_receipt["transactionHash"].hex()

@staticmethod
def from_mnemonic(mnemonic: str) -> "ETHAccount":
Account.enable_unaudited_hdwallet_features()
return ETHAccount(private_key=Account.from_mnemonic(mnemonic=mnemonic).key)
def get_eth_balance(self) -> Decimal:
return Decimal(
self._provider.eth.get_balance(self._account.address)
if self._provider
else 0
)

def get_token_balance(self) -> Decimal:
if self.chain and self._provider:
contact_address = get_token_address(self.chain)
if contact_address:
contract = self._provider.eth.contract(
address=contact_address, abi=BALANCEOF_ABI
)
return Decimal(contract.functions.balanceOf(self.get_address()).call())
return Decimal(0)

def get_super_token_balance(self) -> Decimal:
if self.chain and self._provider:
contact_address = get_super_token_address(self.chain)
if contact_address:
contract = self._provider.eth.contract(
address=contact_address, abi=BALANCEOF_ABI
)
return Decimal(contract.functions.balanceOf(self.get_address()).call())
return Decimal(0)

def create_flow(self, receiver: str, flow: Decimal) -> Awaitable[str]:
"""Creat a Superfluid flow between this account and the receiver address."""
if not self.superfluid_connector:
raise ValueError("Superfluid connector is required to create a flow")
return self.superfluid_connector.create_flow(
sender=self.get_address(), receiver=receiver, flow=flow
)
return self.superfluid_connector.create_flow(receiver=receiver, flow=flow)

def get_flow(self, receiver: str) -> Awaitable[Web3FlowInfo]:
"""Get the Superfluid flow between this account and the receiver address."""
Expand All @@ -111,29 +175,19 @@ def update_flow(self, receiver: str, flow: Decimal) -> Awaitable[str]:
"""Update the Superfluid flow between this account and the receiver address."""
if not self.superfluid_connector:
raise ValueError("Superfluid connector is required to update a flow")
return self.superfluid_connector.update_flow(
sender=self.get_address(), receiver=receiver, flow=flow
)
return self.superfluid_connector.update_flow(receiver=receiver, flow=flow)

def delete_flow(self, receiver: str) -> Awaitable[str]:
"""Delete the Superfluid flow between this account and the receiver address."""
if not self.superfluid_connector:
raise ValueError("Superfluid connector is required to delete a flow")
return self.superfluid_connector.delete_flow(
sender=self.get_address(), receiver=receiver
)

def update_superfluid_connector(self, rpc: str, chain_id: int):
"""Update the Superfluid connector after initialisation."""
self.superfluid_connector = Superfluid(
rpc=rpc,
chain_id=chain_id,
account=self._account,
)
return self.superfluid_connector.delete_flow(receiver=receiver)


def get_fallback_account(path: Optional[Path] = None) -> ETHAccount:
return ETHAccount(private_key=get_fallback_private_key(path=path))
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)


def verify_signature(
Expand Down
45 changes: 41 additions & 4 deletions src/aleph/sdk/conf.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import os
from pathlib import Path
from shutil import which
from typing import Optional
from typing import Dict, Optional, Union

from aleph_message.models import Chain
from pydantic import BaseSettings, Field

from aleph.sdk.types import ChainInfo


class Settings(BaseSettings):
CONFIG_HOME: Optional[str] = None
Expand Down Expand Up @@ -38,9 +41,43 @@ class Settings(BaseSettings):

CODE_USES_SQUASHFS: bool = which("mksquashfs") is not None # True if command exists

AVAX_RPC: str = "https://api.avax.network/ext/bc/C/rpc"
AVAX_CHAIN_ID: int = 43114
AVAX_ALEPH_SUPER_TOKEN = "0xc0Fbc4967259786C743361a5885ef49380473dCF" # mainnet
# Web3Provider settings
TOKEN_DECIMALS = 18
TX_TIMEOUT = 60 * 3
CHAINS: Dict[Union[Chain, str], ChainInfo] = {
# TESTNETS
"SEPOLIA": ChainInfo(
chain_id=11155111,
rpc="https://eth-sepolia.public.blastapi.io",
token="0xc4bf5cbdabe595361438f8c6a187bdc330539c60",
super_token="0x22064a21fee226d8ffb8818e7627d5ff6d0fc33a",
active=False,
),
# MAINNETS
Chain.ETH: ChainInfo(
chain_id=1,
rpc="https://eth-mainnet.public.blastapi.io",
token="0x27702a26126e0B3702af63Ee09aC4d1A084EF628",
),
Chain.AVAX: ChainInfo(
chain_id=43114,
rpc="https://api.avax.network/ext/bc/C/rpc",
token="0xc0Fbc4967259786C743361a5885ef49380473dCF",
super_token="0xc0Fbc4967259786C743361a5885ef49380473dCF",
),
Chain.BASE: ChainInfo(
chain_id=8453,
rpc="https://base-mainnet.public.blastapi.io",
token="0xc0Fbc4967259786C743361a5885ef49380473dCF",
super_token="0xc0Fbc4967259786C743361a5885ef49380473dCF",
),
Chain.BSC: ChainInfo(
chain_id=56,
rpc="https://binance.llamarpc.com",
token="0x82D2f8E02Afb160Dd5A480a617692e62de9038C4",
active=False,
),
}

# Dns resolver
DNS_IPFS_DOMAIN = "ipfs.public.aleph.sh"
Expand Down
Loading