diff --git a/pyproject.toml b/pyproject.toml index 4c41904a..9ecc2a62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/src/aleph/sdk/chains/ethereum.py b/src/aleph/sdk/chains/ethereum.py index 294f47da..32f459b7 100644 --- a/src/aleph/sdk/chains/ethereum.py +++ b/src/aleph/sdk/chains/ethereum.py @@ -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.""" @@ -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( diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 70378088..38afc381 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -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 @@ -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" diff --git a/src/aleph/sdk/connectors/superfluid.py b/src/aleph/sdk/connectors/superfluid.py index 2c0b9fb6..4b7274f8 100644 --- a/src/aleph/sdk/connectors/superfluid.py +++ b/src/aleph/sdk/connectors/superfluid.py @@ -1,61 +1,17 @@ from __future__ import annotations -import asyncio from decimal import Decimal -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING -from eth_utils import to_normalized_address, to_wei +from eth_utils import to_normalized_address from superfluid import CFA_V1, Operation, Web3FlowInfo -from web3 import Web3 -from web3.types import TxParams -from aleph.sdk.conf import settings +from aleph.sdk.exceptions import InsufficientFundsError -if TYPE_CHECKING: - from aleph.sdk.chains.ethereum import LocalAccount - - -async def sign_and_send_transaction( - account: LocalAccount, tx_params: TxParams, rpc: str -) -> str: - """ - Sign and broadcast a transaction using the provided ETHAccount - - @param tx_params - Transaction parameters - @param rpc - RPC URL - @returns - str - The transaction hash - """ - web3 = Web3(Web3.HTTPProvider(rpc)) - - def sign_and_send(): - signed_txn = account.sign_transaction(tx_params) - transaction_hash = web3.eth.send_raw_transaction(signed_txn.rawTransaction) - return transaction_hash.hex() - - # Sending a transaction is done over HTTP(S) and implemented using a blocking - # API in `web3.eth`. This runs it in a non-blocking asyncio executor. - loop = asyncio.get_running_loop() - transaction_hash = await loop.run_in_executor(None, sign_and_send) - return transaction_hash +from ..evm_utils import get_super_token_address, to_human_readable_token, to_wei_token - -async def execute_operation_with_account( - account: LocalAccount, operation: Operation -) -> str: - """ - Execute an operation using the provided ETHAccount - - @param operation - Operation instance from the library - @returns - str - The transaction hash - @returns - str - The transaction hash - """ - populated_transaction = operation._get_populated_transaction_request( - operation.rpc, account.key - ) - transaction_hash = await sign_and_send_transaction( - account, populated_transaction, operation.rpc - ) - return transaction_hash +if TYPE_CHECKING: + from aleph.sdk.chains.ethereum import ETHAccount class Superfluid: @@ -63,28 +19,52 @@ class Superfluid: Wrapper around the Superfluid APIs in order to CRUD Superfluid flows between two accounts. """ - account: Optional[LocalAccount] + account: ETHAccount + normalized_address: str + super_token: str + cfaV1Instance: CFA_V1 + MIN_4_HOURS = 60 * 60 * 4 - def __init__( - self, - rpc=settings.AVAX_RPC, - chain_id=settings.AVAX_CHAIN_ID, - account: Optional[LocalAccount] = None, - ): - self.cfaV1Instance = CFA_V1(rpc, chain_id) + def __init__(self, account: ETHAccount): self.account = account - - async def create_flow(self, sender: str, receiver: str, flow: Decimal) -> str: + self.normalized_address = to_normalized_address(account.get_address()) + if account.chain: + self.super_token = str(get_super_token_address(account.chain)) + self.cfaV1Instance = CFA_V1(account.rpc, account.chain_id) + + async def _execute_operation_with_account(self, operation: Operation) -> str: + """ + Execute an operation using the provided ETHAccount + @param operation - Operation instance from the library + @returns - str - Transaction hash + """ + populated_transaction = operation._get_populated_transaction_request( + self.account.rpc, self.account._account.key + ) + return await self.account._sign_and_send_transaction(populated_transaction) + + def can_start_flow(self, flow: Decimal, block=True) -> bool: + valid = False + if self.account.can_transact(block=block): + balance = self.account.get_super_token_balance() + MIN_FLOW_4H = to_wei_token(flow) * Decimal(self.MIN_4_HOURS) + valid = balance > MIN_FLOW_4H + if not valid and block: + raise InsufficientFundsError( + required_funds=float(MIN_FLOW_4H), + available_funds=to_human_readable_token(balance), + ) + return valid + + async def create_flow(self, receiver: str, flow: Decimal) -> str: """Create a Superfluid flow between two addresses.""" - if not self.account: - raise ValueError("An account is required to create a flow") - return await execute_operation_with_account( - account=self.account, + self.can_start_flow(flow) + return await self._execute_operation_with_account( operation=self.cfaV1Instance.create_flow( - sender=to_normalized_address(sender), + sender=self.normalized_address, receiver=to_normalized_address(receiver), - super_token=settings.AVAX_ALEPH_SUPER_TOKEN, - flow_rate=to_wei(Decimal(flow), "ether"), + super_token=self.super_token, + flow_rate=int(to_wei_token(flow)), ), ) @@ -93,32 +73,26 @@ async def get_flow(self, sender: str, receiver: str) -> Web3FlowInfo: return self.cfaV1Instance.get_flow( sender=to_normalized_address(sender), receiver=to_normalized_address(receiver), - super_token=settings.AVAX_ALEPH_SUPER_TOKEN, + super_token=self.super_token, ) - async def delete_flow(self, sender: str, receiver: str) -> str: + async def delete_flow(self, receiver: str) -> str: """Delete the Supefluid flow between two addresses.""" - if not self.account: - raise ValueError("An account is required to delete a flow") - return await execute_operation_with_account( - account=self.account, + return await self._execute_operation_with_account( operation=self.cfaV1Instance.delete_flow( - sender=to_normalized_address(sender), + sender=self.normalized_address, receiver=to_normalized_address(receiver), - super_token=settings.AVAX_ALEPH_SUPER_TOKEN, + super_token=self.super_token, ), ) - async def update_flow(self, sender: str, receiver: str, flow: Decimal) -> str: + async def update_flow(self, receiver: str, flow: Decimal) -> str: """Update the flow of a Superfluid flow between two addresses.""" - if not self.account: - raise ValueError("An account is required to update a flow") - return await execute_operation_with_account( - account=self.account, + return await self._execute_operation_with_account( operation=self.cfaV1Instance.update_flow( - sender=to_normalized_address(sender), + sender=self.normalized_address, receiver=to_normalized_address(receiver), - super_token=settings.AVAX_ALEPH_SUPER_TOKEN, - flow_rate=to_wei(Decimal(flow), "ether"), + super_token=self.super_token, + flow_rate=int(to_wei_token(flow)), ), ) diff --git a/src/aleph/sdk/evm_utils.py b/src/aleph/sdk/evm_utils.py new file mode 100644 index 00000000..c7166cec --- /dev/null +++ b/src/aleph/sdk/evm_utils.py @@ -0,0 +1,91 @@ +from decimal import Decimal +from typing import List, Optional, Union + +from aleph_message.models import Chain +from eth_utils import to_wei +from web3 import Web3 +from web3.types import ChecksumAddress + +from .conf import settings + +MIN_ETH_BALANCE: float = 0.005 +MIN_ETH_BALANCE_WEI = Decimal(to_wei(MIN_ETH_BALANCE, "ether")) +BALANCEOF_ABI = """[{ + "name": "balanceOf", + "inputs": [{"name": "account", "type": "address"}], + "outputs": [{"name": "balance", "type": "uint256"}], + "constant": true, + "payable": false, + "stateMutability": "view", + "type": "function" +}]""" + + +def to_human_readable_token(amount: Decimal) -> float: + return float(amount / (Decimal(10) ** Decimal(settings.TOKEN_DECIMALS))) + + +def to_wei_token(amount: Decimal) -> Decimal: + return amount * Decimal(10) ** Decimal(settings.TOKEN_DECIMALS) + + +def get_chain_id(chain: Union[Chain, str, None]) -> Optional[int]: + """Returns the CHAIN_ID of a given EVM blockchain""" + if chain: + if chain in settings.CHAINS and settings.CHAINS[chain].chain_id: + return settings.CHAINS[chain].chain_id + else: + raise ValueError(f"Unknown RPC for chain {chain}") + return None + + +def get_rpc(chain: Union[Chain, str, None]) -> Optional[str]: + """Returns the RPC to use for a given EVM blockchain""" + if chain: + if chain in settings.CHAINS and settings.CHAINS[chain].rpc: + return settings.CHAINS[chain].rpc + else: + raise ValueError(f"Unknown RPC for chain {chain}") + return None + + +def get_token_address(chain: Union[Chain, str, None]) -> Optional[ChecksumAddress]: + if chain: + if chain in settings.CHAINS: + address = settings.CHAINS[chain].super_token + if address: + try: + return Web3.to_checksum_address(address) + except ValueError: + raise ValueError(f"Invalid token address {address}") + else: + raise ValueError(f"Unknown token for chain {chain}") + return None + + +def get_super_token_address( + chain: Union[Chain, str, None] +) -> Optional[ChecksumAddress]: + if chain: + if chain in settings.CHAINS: + address = settings.CHAINS[chain].super_token + if address: + try: + return Web3.to_checksum_address(address) + except ValueError: + raise ValueError(f"Invalid token address {address}") + else: + raise ValueError(f"Unknown super_token for chain {chain}") + return None + + +def get_chains_with_holding() -> List[Union[Chain, str]]: + return [chain for chain, info in settings.CHAINS.items() if info.active] + + +def get_chains_with_super_token() -> List[Union[Chain, str]]: + return [ + chain + for chain, info in settings.CHAINS.items() + if info.active and info.super_token + ] diff --git a/src/aleph/sdk/types.py b/src/aleph/sdk/types.py index cf9e6fa8..081a3465 100644 --- a/src/aleph/sdk/types.py +++ b/src/aleph/sdk/types.py @@ -1,6 +1,6 @@ from abc import abstractmethod from enum import Enum -from typing import Dict, Protocol, TypeVar +from typing import Dict, Optional, Protocol, TypeVar from pydantic import BaseModel @@ -64,3 +64,15 @@ class SEVMeasurement(BaseModel): sev_info: SEVInfo launch_measure: str + + +class ChainInfo(BaseModel): + """ + A chain information. + """ + + chain_id: int + rpc: str + token: str + super_token: Optional[str] = None + active: bool = True diff --git a/tests/unit/test_superfluid.py b/tests/unit/test_superfluid.py index 92c83f2c..c2f853bd 100644 --- a/tests/unit/test_superfluid.py +++ b/tests/unit/test_superfluid.py @@ -1,15 +1,12 @@ import random from decimal import Decimal -from unittest import mock -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch import pytest from aleph_message.models import Chain from eth_utils import to_checksum_address -from superfluid import Operation, Web3FlowInfo from aleph.sdk.chains.ethereum import ETHAccount -from aleph.sdk.conf import settings def generate_fake_eth_address(): @@ -20,19 +17,38 @@ def generate_fake_eth_address(): @pytest.fixture def mock_superfluid(): - with patch("aleph.sdk.connectors.superfluid.CFA_V1") as MockCFA_V1: - yield MockCFA_V1.return_value + with patch("aleph.sdk.connectors.superfluid.Superfluid") as MockSuperfluid: + mock_superfluid = MockSuperfluid.return_value + + # Mock methods for the Superfluid connector + mock_superfluid.create_flow = AsyncMock(return_value="0xTransactionHash") + mock_superfluid.delete_flow = AsyncMock(return_value="0xTransactionHash") + mock_superfluid.update_flow = AsyncMock(return_value="0xTransactionHash") + + # Mock get_flow to return a mock Web3FlowInfo + mock_flow_info = {"timestamp": 0, "flowRate": 0, "deposit": 0, "owedDeposit": 0} + mock_superfluid.get_flow = AsyncMock(return_value=mock_flow_info) + + yield mock_superfluid @pytest.fixture def eth_account(mock_superfluid): private_key = b"\x01" * 32 - return ETHAccount( + account = ETHAccount( private_key, chain=Chain.AVAX, - rpc=settings.AVAX_RPC, - chain_id=settings.AVAX_CHAIN_ID, ) + with patch.object( + account, "get_super_token_balance", new_callable=AsyncMock + ) as mock_get_balance: + mock_get_balance.return_value = Decimal("1") + with patch.object( + account, "can_transact", new_callable=AsyncMock + ) as mock_can_transact: + mock_can_transact.return_value = True + account.superfluid_connector = mock_superfluid + yield account @pytest.mark.asyncio @@ -42,92 +58,43 @@ async def test_initialization(eth_account): @pytest.mark.asyncio async def test_create_flow(eth_account, mock_superfluid): - mock_operation = AsyncMock(spec=Operation) - mock_superfluid.create_flow.return_value = mock_operation - - sender = eth_account.get_address() receiver = generate_fake_eth_address() - flow = Decimal("10.0") - - with patch( - "aleph.sdk.connectors.superfluid.execute_operation_with_account", - return_value="0xTransactionHash", - ) as mock_execute: - tx_hash = await eth_account.create_flow(receiver, flow) - assert tx_hash == "0xTransactionHash" - mock_execute.assert_called_once_with( - account=eth_account._account, operation=mock_operation - ) - mock_superfluid.create_flow.assert_called_once_with( - sender=sender.lower(), - receiver=receiver.lower(), - super_token=settings.AVAX_ALEPH_SUPER_TOKEN, - flow_rate=mock.ANY, - ) + flow = Decimal("0.00000005") + + tx_hash = await eth_account.create_flow(receiver, flow) + + assert tx_hash == "0xTransactionHash" + mock_superfluid.create_flow.assert_awaited_once() @pytest.mark.asyncio async def test_delete_flow(eth_account, mock_superfluid): - mock_operation = AsyncMock(spec=Operation) - mock_superfluid.delete_flow.return_value = mock_operation - - sender = eth_account.get_address() receiver = generate_fake_eth_address() - with patch( - "aleph.sdk.connectors.superfluid.execute_operation_with_account", - return_value="0xTransactionHash", - ) as mock_execute: - tx_hash = await eth_account.delete_flow(receiver) - assert tx_hash == "0xTransactionHash" - mock_execute.assert_called_once_with( - account=eth_account._account, operation=mock_operation - ) - mock_superfluid.delete_flow.assert_called_once_with( - sender=sender.lower(), - receiver=receiver.lower(), - super_token=settings.AVAX_ALEPH_SUPER_TOKEN, - ) + tx_hash = await eth_account.delete_flow(receiver) + + assert tx_hash == "0xTransactionHash" + mock_superfluid.delete_flow.assert_awaited_once() @pytest.mark.asyncio async def test_update_flow(eth_account, mock_superfluid): - mock_operation = AsyncMock(spec=Operation) - mock_superfluid.update_flow.return_value = mock_operation - - sender = eth_account.get_address() receiver = generate_fake_eth_address() - flow = Decimal(15.0) - - with patch( - "aleph.sdk.connectors.superfluid.execute_operation_with_account", - return_value="0xTransactionHash", - ) as mock_execute: - tx_hash = await eth_account.update_flow(receiver, flow) - assert tx_hash == "0xTransactionHash" - mock_execute.assert_called_once_with( - account=eth_account._account, operation=mock_operation - ) - mock_superfluid.update_flow.assert_called_once_with( - sender=sender.lower(), - receiver=receiver.lower(), - super_token=settings.AVAX_ALEPH_SUPER_TOKEN, - flow_rate=mock.ANY, - ) + flow = Decimal("0.005") + + tx_hash = await eth_account.update_flow(receiver, flow) + + assert tx_hash == "0xTransactionHash" + mock_superfluid.update_flow.assert_awaited_once() @pytest.mark.asyncio async def test_get_flow(eth_account, mock_superfluid): - mock_flow_info = MagicMock(spec=Web3FlowInfo) - mock_superfluid.get_flow.return_value = mock_flow_info - - sender = eth_account.get_address() receiver = generate_fake_eth_address() flow_info = await eth_account.get_flow(receiver) - assert flow_info == mock_flow_info - mock_superfluid.get_flow.assert_called_once_with( - sender=sender.lower(), - receiver=receiver.lower(), - super_token=settings.AVAX_ALEPH_SUPER_TOKEN, - ) + + assert flow_info["timestamp"] == 0 + assert flow_info["flowRate"] == 0 + assert flow_info["deposit"] == 0 + assert flow_info["owedDeposit"] == 0