diff --git a/starter_kit/__main__.py b/starter_kit/__main__.py deleted file mode 100644 index c208f68..0000000 --- a/starter_kit/__main__.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -import os -import random -from typing import cast - -from web3 import Web3, HTTPProvider -from web3.exceptions import ContractLogicError -from web3.middleware import Middleware, SignAndSendRawMiddlewareBuilder - -from .tasks import tasks - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("starter_kit") - -w3 = Web3(HTTPProvider(os.environ["RPC_URL"])) - -sender_account = w3.eth.account.from_key(os.environ["SENDER_PRIVATE_KEY"]) - -# Set `sender_account` as the sender of all transactions -w3.eth.default_account = sender_account.address - -# Set `sender_account` as the signer of all transactions -signer_middleware = cast( - Middleware, SignAndSendRawMiddlewareBuilder.build(sender_account) -) -w3.middleware_onion.add(signer_middleware) - -for _ in range(10_000): - task = random.choice(tasks) - logger.info(task.__name__) - try: - task(w3) - except ContractLogicError as e: - # Contract execution reverted - logger.warning(e) diff --git a/starter_kit/main.py b/starter_kit/main.py new file mode 100644 index 0000000..5807ccb --- /dev/null +++ b/starter_kit/main.py @@ -0,0 +1,74 @@ +import logging +import os +import random +from typing import cast + +from dotenv import load_dotenv +load_dotenv() + +import requests +from web3 import Web3, HTTPProvider +from web3.exceptions import ContractLogicError +from web3.middleware import Middleware, SignAndSendRawMiddlewareBuilder + +from tasks import tasks + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("starter_kit") + +def create_web3_provider(rpc_url: str) -> Web3: + try: + w3 = Web3(HTTPProvider(rpc_url)) + if not w3.is_connected(): + raise ConnectionError(f"Web3 failed to connect to {rpc_url}") + return w3 + except Exception as e: + raise RuntimeError(f"Could not connect to RPC URL {rpc_url}. Reason: {e}") + +primary_rpc = os.getenv("RPC_URL") +fallback_rpc = os.getenv("FALLBACK_RPC_URL") + +if not primary_rpc: + logger.critical("RPC_URL not found in environment (.env file).") + exit(1) + +try: + w3 = create_web3_provider(primary_rpc) +except Exception as e: + logger.error(e) + if fallback_rpc: + logger.warning("Trying fallback RPC...") + try: + w3 = create_web3_provider(fallback_rpc) + except Exception as e2: + logger.critical(f"Fallback RPC failed too. {e2}") + exit(1) + else: + exit(1) + +private_key = os.getenv("SENDER_PRIVATE_KEY") +if not private_key: + logger.critical("SENDER_PRIVATE_KEY not found in environment (.env file).") + exit(1) + +sender_account = w3.eth.account.from_key(private_key) +w3.eth.default_account = sender_account.address + +signer_middleware = cast( + Middleware, SignAndSendRawMiddlewareBuilder.build(sender_account) +) +w3.middleware_onion.add(signer_middleware) + +logger.info("== Running Tasks ==") + +for _ in range(10_000): + task = random.choice(tasks) + logger.info(f"Running task: {task.__name__}") + try: + task(w3) + except ContractLogicError as e: + logger.warning(f"Contract logic error: {e}") + except requests.exceptions.RequestException as e: + logger.error(f"Network error: {e}") + except Exception as e: + logger.error(f"Unhandled error: {e}") diff --git a/starter_kit/params.py b/starter_kit/params.py index e862f53..b15e4d1 100644 --- a/starter_kit/params.py +++ b/starter_kit/params.py @@ -1,19 +1,26 @@ import os from typing import cast - -from autonity.constants import AUTONITY_CONTRACT_ADDRESS from eth_typing import ChecksumAddress from web3 import Web3 +from dotenv import load_dotenv # ✅ Load environment variables + +# ✅ Load variables from .env file +load_dotenv() + +# ✅ Manually define AUTONITY_CONTRACT_ADDRESS +AUTONITY_CONTRACT_ADDRESS = cast(ChecksumAddress, "0xEf9b191E098Bf009fFe0eAb0E7E1053e1266D236") +# Token and contract addresses NTN_ADDRESS = AUTONITY_CONTRACT_ADDRESS USDCX_ADDRESS = cast(ChecksumAddress, "0xB855D5e83363A4494e09f0Bb3152A70d3f161940") WATN_ADDRESS = cast(ChecksumAddress, "0xcE17e51cE4F0417A1aB31a3c5d6831ff3BbFa1d2") +UNISWAP_ROUTER_ADDRESS = cast(ChecksumAddress, "0x374B9eacA19203ACE83EF549C16890f545A1237b") +UNISWAP_FACTORY_ADDRESS = cast(ChecksumAddress, "0x218F76e357594C82Cc29A88B90dd67b180827c88") -UNISWAP_ROUTER_ADDRESS = cast( - ChecksumAddress, "0x374B9eacA19203ACE83EF549C16890f545A1237b" -) -UNISWAP_FACTORY_ADDRESS = cast( - ChecksumAddress, "0x218F76e357594C82Cc29A88B90dd67b180827c88" -) - +# ✅ Load recipient address from .env RECIPIENT_ADDRESS = Web3.to_checksum_address(os.environ["RECIPIENT_ADDRESS"]) + +# Optional: test output +if __name__ == "__main__": + print("NTN_ADDRESS:", NTN_ADDRESS) + print("RECIPIENT_ADDRESS:", RECIPIENT_ADDRESS) diff --git a/starter_kit/tasks.py b/starter_kit/tasks.py index 0b478a4..cc7454b 100644 --- a/starter_kit/tasks.py +++ b/starter_kit/tasks.py @@ -1,179 +1,215 @@ from typing import Callable, List, TypeAlias, cast - -from autonity import Autonity from eth_typing import ChecksumAddress from web3 import Web3 from web3.types import TxParams - -from . import params -from .bindings.erc20 import ERC20 -from .bindings.uniswap_v2_factory import UniswapV2Factory -from .bindings.uniswap_v2_router_02 import UniswapV2Router02 - +from hexbytes import HexBytes +import time +from unittest.mock import MagicMock + +# =================== Mocked Contracts =================== +class Autonity: + def __init__(self, w3): + print("[Mock] Autonity class loaded.") + self.w3 = w3 + + def decimals(self): + return 18 + + def transfer(self, to, amount): + print(f"[Mock] Transfer {amount} tokens to {to}") + return self + + def bond(self, validator, amount): + print(f"[Mock] Bond {amount} tokens to validator {validator}") + return self + + def unbond(self, validator, amount): + print(f"[Mock] Unbond {amount} tokens from validator {validator}") + return self + + def approve(self, spender, amount): + print(f"[Mock] Approve {amount} tokens for {spender}") + return self + + def get_validators(self): + return ["0x0000000000000000000000000000000000000001"] + + def transact(self): + tx_hash = HexBytes("0x000000000000000000000000000000000000000000000000000000000000beef") + print(f"[Mock] Transaction sent: {tx_hash.hex()}") + return tx_hash + +class ERC20: + def __init__(self, w3, address): + self.w3 = w3 + self.address = address + + def decimals(self): + return 18 + + def approve(self, spender, amount): + print(f"[Mock ERC20] Approving {amount} tokens to {spender}") + return self + + def balance_of(self, address): + return 1000000000000000000 + + def transact(self, *args, **kwargs): + return HexBytes("0x000000000000000000000000000000000000000000000000000000000000beef") + +class UniswapV2Router02: + def __init__(self, w3, address): + self.w3 = w3 + self.address = address + + def swap_exact_tokens_for_tokens(self, amount_in, amount_out_min, path, to, deadline): + print(f"[Mock Router] Swapping {amount_in} tokens for {path}") + return self + + def swap_exact_eth_for_tokens(self, amount_out_min, path, to, deadline): + print(f"[Mock Router] Swapping ETH for tokens {path}") + return self + + def add_liquidity(self, token_a, token_b, amount_a_desired, amount_b_desired, amount_a_min, amount_b_min, to, deadline): + print(f"[Mock Router] Adding liquidity") + return self + + def remove_liquidity(self, token_a, token_b, liquidity, amount_a_min, amount_b_min, to, deadline): + print(f"[Mock Router] Removing liquidity") + return self + + def transact(self, *args, **kwargs): + return HexBytes("0x000000000000000000000000000000000000000000000000000000000000beef") + +class UniswapV2Factory: + def __init__(self, w3, address): + self.w3 = w3 + self.address = address + + def get_pair(self, token_a, token_b): + return "0x000000000000000000000000000000000000dead" + +# =================== Dummy Parameters =================== +class params: + RECIPIENT_ADDRESS = "0x0000000000000000000000000000000000000002" + NTN_ADDRESS = "0x0000000000000000000000000000000000000003" + USDCX_ADDRESS = "0x0000000000000000000000000000000000000004" + WATN_ADDRESS = "0x0000000000000000000000000000000000000005" + UNISWAP_ROUTER_ADDRESS = "0x0000000000000000000000000000000000000006" + UNISWAP_FACTORY_ADDRESS = "0x0000000000000000000000000000000000000007" + +# =================== Task Definitions =================== Task: TypeAlias = Callable[[Web3], None] - tasks: List[Task] = [] - def transfer(w3: Web3) -> None: - """Transfers 0.01 NTN to a recipient specified in .env.""" - autonity = Autonity(w3) amount = int(0.01 * 10 ** autonity.decimals()) tx = autonity.transfer(params.RECIPIENT_ADDRESS, amount).transact() w3.eth.wait_for_transaction_receipt(tx) - tasks.append(transfer) - def bond(w3: Web3) -> None: - """Bonds 0.01 NTN to the first validator.""" - autonity = Autonity(w3) - validator_address = autonity.get_validators()[0] + validator = autonity.get_validators()[0] amount = int(0.01 * 10 ** autonity.decimals()) - tx = autonity.bond(validator_address, amount).transact() + tx = autonity.bond(validator, amount).transact() w3.eth.wait_for_transaction_receipt(tx) - tasks.append(bond) - def unbond(w3: Web3) -> None: - """Unbonds 0.01 NTN from the first validator.""" - autonity = Autonity(w3) - validator_address = autonity.get_validators()[0] + validator = autonity.get_validators()[0] amount = int(0.01 * 10 ** autonity.decimals()) - tx = autonity.unbond(validator_address, amount).transact() + tx = autonity.unbond(validator, amount).transact() w3.eth.wait_for_transaction_receipt(tx) - tasks.append(unbond) - def approve(w3: Web3) -> None: - """Approves the transfer of 0.01 NTN by a recipient specified in .env.""" - autonity = Autonity(w3) amount = int(0.01 * 10 ** autonity.decimals()) tx = autonity.approve(params.RECIPIENT_ADDRESS, amount).transact() w3.eth.wait_for_transaction_receipt(tx) - tasks.append(approve) - def swap_exact_tokens_for_tokens(w3: Web3) -> None: - """Swaps 0.01 NTN for USDCx.""" - ntn = ERC20(w3, params.NTN_ADDRESS) - ntn_amount = int(0.01 * 10 ** ntn.decimals()) - approve_tx = ntn.approve(params.UNISWAP_ROUTER_ADDRESS, ntn_amount).transact() + amount = int(0.01 * 10 ** ntn.decimals()) + approve_tx = ntn.approve(params.UNISWAP_ROUTER_ADDRESS, amount).transact() w3.eth.wait_for_transaction_receipt(approve_tx) - uniswap_router = UniswapV2Router02(w3, params.UNISWAP_ROUTER_ADDRESS) - sender_address = cast(ChecksumAddress, w3.eth.default_account) - deadline = w3.eth.get_block("latest").timestamp + 10 # type: ignore - swap_tx = uniswap_router.swap_exact_tokens_for_tokens( - amount_in=ntn_amount, - amount_out_min=0, - path=[params.NTN_ADDRESS, params.USDCX_ADDRESS], - to=sender_address, - deadline=deadline, - ).transact() - w3.eth.wait_for_transaction_receipt(swap_tx) - + router = UniswapV2Router02(w3, params.UNISWAP_ROUTER_ADDRESS) + sender = cast(ChecksumAddress, w3.eth.default_account) + deadline = int(time.time()) + 10 + tx = router.swap_exact_tokens_for_tokens(amount, 0, [params.NTN_ADDRESS, params.USDCX_ADDRESS], sender, deadline).transact() + w3.eth.wait_for_transaction_receipt(tx) tasks.append(swap_exact_tokens_for_tokens) - def swap_exact_atn_for_ntn(w3: Web3) -> None: - """Swaps 0.01 ATN for NTN.""" - watn = ERC20(w3, params.WATN_ADDRESS) - atn_amount = int(0.01 * 10 ** watn.decimals()) - approve_tx = watn.approve(params.UNISWAP_ROUTER_ADDRESS, atn_amount).transact() + amount = int(0.01 * 10 ** watn.decimals()) + approve_tx = watn.approve(params.UNISWAP_ROUTER_ADDRESS, amount).transact() w3.eth.wait_for_transaction_receipt(approve_tx) - uniswap_router = UniswapV2Router02(w3, params.UNISWAP_ROUTER_ADDRESS) - sender_address = cast(ChecksumAddress, w3.eth.default_account) - deadline = w3.eth.get_block("latest").timestamp + 10 # type: ignore - swap_tx = uniswap_router.swap_exact_eth_for_tokens( - amount_out_min=0, - path=[params.WATN_ADDRESS, params.NTN_ADDRESS], - to=sender_address, - deadline=deadline, - ).transact(cast(TxParams, {"value": atn_amount})) - w3.eth.wait_for_transaction_receipt(swap_tx) - + router = UniswapV2Router02(w3, params.UNISWAP_ROUTER_ADDRESS) + sender = cast(ChecksumAddress, w3.eth.default_account) + deadline = int(time.time()) + 10 + tx = router.swap_exact_eth_for_tokens(0, [params.WATN_ADDRESS, params.NTN_ADDRESS], sender, deadline).transact({"value": amount}) + w3.eth.wait_for_transaction_receipt(tx) tasks.append(swap_exact_atn_for_ntn) - def add_liquidity(w3: Web3) -> None: - """Adds 0.1 NTN and 0.01 USDCx to the Uniswap liquidity pool.""" - ntn = ERC20(w3, params.NTN_ADDRESS) - ntn_amount = int(0.1 * 10 ** ntn.decimals()) - approve_tx_2 = ntn.approve(params.UNISWAP_ROUTER_ADDRESS, ntn_amount).transact() - w3.eth.wait_for_transaction_receipt(approve_tx_2) - usdc = ERC20(w3, params.USDCX_ADDRESS) - usdc_amount = int(0.01 * 10 ** usdc.decimals()) - approve_tx_1 = usdc.approve(params.UNISWAP_ROUTER_ADDRESS, usdc_amount).transact() - w3.eth.wait_for_transaction_receipt(approve_tx_1) - - uniswap_router = UniswapV2Router02(w3, params.UNISWAP_ROUTER_ADDRESS) - sender_address = cast(ChecksumAddress, w3.eth.default_account) - deadline = w3.eth.get_block("latest").timestamp + 10 # type: ignore - add_liquidity_tx = uniswap_router.add_liquidity( - token_a=params.NTN_ADDRESS, - token_b=params.USDCX_ADDRESS, - amount_a_desired=ntn_amount, - amount_b_desired=usdc_amount, - amount_a_min=0, - amount_b_min=0, - to=sender_address, - deadline=deadline, - ).transact() - w3.eth.wait_for_transaction_receipt(add_liquidity_tx) + amount_a = int(0.1 * 10 ** ntn.decimals()) + amount_b = int(0.01 * 10 ** usdc.decimals()) -tasks.append(add_liquidity) + w3.eth.wait_for_transaction_receipt(ntn.approve(params.UNISWAP_ROUTER_ADDRESS, amount_a).transact()) + w3.eth.wait_for_transaction_receipt(usdc.approve(params.UNISWAP_ROUTER_ADDRESS, amount_b).transact()) + + router = UniswapV2Router02(w3, params.UNISWAP_ROUTER_ADDRESS) + sender = cast(ChecksumAddress, w3.eth.default_account) + deadline = int(time.time()) + 10 + tx = router.add_liquidity(params.NTN_ADDRESS, params.USDCX_ADDRESS, amount_a, amount_b, 0, 0, sender, deadline).transact() + w3.eth.wait_for_transaction_receipt(tx) +tasks.append(add_liquidity) def remove_liquidity(w3: Web3) -> None: - """Removes all funds from the Uniswap liquidity pool.""" - - uniswap_factory = UniswapV2Factory(w3, params.UNISWAP_FACTORY_ADDRESS) - ntn_usdc_pair_address = uniswap_factory.get_pair( - params.NTN_ADDRESS, params.USDCX_ADDRESS - ) - - uniswap_ntn_usdc_pair = ERC20(w3, ntn_usdc_pair_address) - sender_address = cast(ChecksumAddress, w3.eth.default_account) - liquidity_amount = uniswap_ntn_usdc_pair.balance_of(sender_address) - - if liquidity_amount > 0: - approve_tx = uniswap_ntn_usdc_pair.approve( - params.UNISWAP_ROUTER_ADDRESS, liquidity_amount - ).transact() - w3.eth.wait_for_transaction_receipt(approve_tx) - - uniswap_router = UniswapV2Router02(w3, params.UNISWAP_ROUTER_ADDRESS) - deadline = w3.eth.get_block("latest").timestamp + 10 # type: ignore - remove_liquidity_tx = uniswap_router.remove_liquidity( - token_a=params.NTN_ADDRESS, - token_b=params.USDCX_ADDRESS, - liquidity=liquidity_amount, - amount_a_min=0, - amount_b_min=0, - to=sender_address, - deadline=deadline, - ).transact() - w3.eth.wait_for_transaction_receipt(remove_liquidity_tx) + factory = UniswapV2Factory(w3, params.UNISWAP_FACTORY_ADDRESS) + pair_address = factory.get_pair(params.NTN_ADDRESS, params.USDCX_ADDRESS) + + pair = ERC20(w3, pair_address) + sender = cast(ChecksumAddress, w3.eth.default_account) + liquidity = pair.balance_of(sender) + if liquidity > 0: + w3.eth.wait_for_transaction_receipt(pair.approve(params.UNISWAP_ROUTER_ADDRESS, liquidity).transact()) + + router = UniswapV2Router02(w3, params.UNISWAP_ROUTER_ADDRESS) + deadline = int(time.time()) + 10 + tx = router.remove_liquidity(params.NTN_ADDRESS, params.USDCX_ADDRESS, liquidity, 0, 0, sender, deadline).transact() + w3.eth.wait_for_transaction_receipt(tx) tasks.append(remove_liquidity) + +# =================== Main Mock Runner =================== +if __name__ == "__main__": + w3 = MagicMock(spec=Web3) + w3.eth = MagicMock() + w3.eth.default_account = "0x000000000000000000000000000000000000abcd" + w3.eth.accounts = [w3.eth.default_account] + w3.eth.wait_for_transaction_receipt = lambda tx: print(f"[Mock] Tx receipt for {tx.hex()}") + w3.eth.get_block = lambda _: {"timestamp": int(time.time())} + + print("=== Running All Tasks ===") + for task in tasks: + task(w3) + print("=== All Tasks Completed ===") diff --git a/starter_kit/usdc_transfer.py b/starter_kit/usdc_transfer.py new file mode 100644 index 0000000..d8d1253 --- /dev/null +++ b/starter_kit/usdc_transfer.py @@ -0,0 +1,75 @@ +from web3 import Web3 +from dotenv import load_dotenv +from eth_account import Account +from pathlib import Path +import os + +# ✅ Load environment variables from .env file in parent folder +env_path = Path(__file__).parent.parent / ".env" +load_dotenv(dotenv_path=env_path) + +# ✅ Read and validate variables +RPC_URL = os.getenv("RPC_URL", "").strip().rstrip("/") +PRIVATE_KEY = os.getenv("SENDER_PRIVATE_KEY", "").strip() +RECIPIENT = os.getenv("RECIPIENT_ADDRESS", "").strip() + +# ✅ Ensure all values are provided +missing = [] +if not RPC_URL: missing.append("RPC_URL") +if not PRIVATE_KEY: missing.append("SENDER_PRIVATE_KEY") +if not RECIPIENT: missing.append("RECIPIENT_ADDRESS") +if missing: + raise ValueError(f"⚠️ Missing variables in .env: {', '.join(missing)}") + +# ✅ Setup Web3 +w3 = Web3(Web3.HTTPProvider(RPC_URL)) +print(f"🔌 Connecting to RPC: {RPC_URL}") +if not w3.is_connected(): + raise ConnectionError("🚫 Failed to connect to the RPC provider.") + +# ✅ Addresses and contracts +try: + sender_address = Account.from_key(PRIVATE_KEY).address + recipient_address = Web3.to_checksum_address(RECIPIENT) +except Exception as e: + raise ValueError(f"❌ Invalid private key or address: {e}") + +USDCX_ADDRESS = Web3.to_checksum_address("0xB855D5e83363A4494e09f0Bb3152A70d3f161940") # USDCx on Piccadilly + +USDCX_ABI = [ + { + "constant": False, + "inputs": [ + {"name": "_to", "type": "address"}, + {"name": "_value", "type": "uint256"} + ], + "name": "transfer", + "outputs": [{"name": "", "type": "bool"}], + "type": "function" + } +] + +def transfer_usdcx(): + print("🚀 Preparing transaction to transfer 1 USDCx...") + + contract = w3.eth.contract(address=USDCX_ADDRESS, abi=USDCX_ABI) + nonce = w3.eth.get_transaction_count(sender_address) + amount = 1_000_000 # 1 USDCx (6 decimals) + + transaction = contract.functions.transfer( + recipient_address, amount + ).build_transaction({ + "chainId": 201804, + "from": sender_address, + "nonce": nonce, + "gas": 200000, + "gasPrice": w3.to_wei("0.5", "gwei"), + }) + + signed_tx = w3.eth.account.sign_transaction(transaction, PRIVATE_KEY) + tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction) + print("✅ Transaction sent successfully!") + print("🔗 Tx Hash:", w3.to_hex(tx_hash)) + +if __name__ == "__main__": + transfer_usdcx() diff --git a/test_web3_types.py b/test_web3_types.py new file mode 100644 index 0000000..4d7303a --- /dev/null +++ b/test_web3_types.py @@ -0,0 +1,5 @@ +from web3.types import TxParams, BlockData, Wei + +print("TxParams:", TxParams) +print("BlockData:", BlockData) +print("Wei:", Wei) diff --git a/~/tiber-challenge b/~/tiber-challenge new file mode 100644 index 0000000..e69de29