Skip to content

Commit

Permalink
Upgrade to web3py, fixes on ETHAccount/PAYG, EVM configs (#154)
Browse files Browse the repository at this point in the history
* Remove eth_typing / eth_account -> web3 6.3.0 + temp dependencies

* New config

* Rewrite for EVM chains

* Fix circular import

* mypy

* Fix superfluid + export utils methods in evm_utils

* Fix eth_typing

* fix: unit test

* fix: unit test

* Fix mock tests

* Add get_chains_with_holding

---------

Co-authored-by: 1yam <[email protected]>
  • Loading branch information
philogicae and 1yam authored Aug 26, 2024
1 parent fcb3730 commit 2d4ded1
Show file tree
Hide file tree
Showing 7 changed files with 376 additions and 242 deletions.
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

0 comments on commit 2d4ded1

Please sign in to comment.