diff --git a/TNLS-Relayers/eth_interface.py b/TNLS-Relayers/eth_interface.py index d3ab119..f69edf8 100644 --- a/TNLS-Relayers/eth_interface.py +++ b/TNLS-Relayers/eth_interface.py @@ -1,10 +1,10 @@ import json -import os from copy import deepcopy from logging import getLogger, basicConfig, INFO, StreamHandler from typing import List from concurrent.futures import ThreadPoolExecutor, as_completed -from threading import Lock +from threading import Lock, Timer +from time import sleep from web3 import Web3, middleware from web3.datastructures import AttributeDict @@ -18,12 +18,9 @@ class EthInterface(BaseChainInterface): Implementaion of BaseChainInterface for eth. """ - def __init__(self, private_key="", address="", provider=None, contract_address = "", chain_id="", api_endpoint="", timeout = 1, **_kwargs): + def __init__(self, private_key="", address="", provider=None, contract_address="", chain_id="", api_endpoint="", timeout=1, sync_interval=30, **_kwargs): if provider is None: - """ - If we don't have a set provider, read it from config. - """ - + # If no provider, set a default with middleware for various blockchain scenarios provider = Web3(Web3.HTTPProvider(api_endpoint, request_kwargs={'timeout': timeout})) provider.middleware_onion.inject(geth_poa_middleware, layer=0) provider.middleware_onion.add(middleware.time_based_cache_middleware) @@ -37,13 +34,53 @@ def __init__(self, private_key="", address="", provider=None, contract_address = self.chain_id = chain_id self.nonce = self.provider.eth.get_transaction_count(self.address, 'pending') + # Set up logging basicConfig( level=INFO, format="%(asctime)s [Eth Interface: %(levelname)8.8s] %(message)s", handlers=[StreamHandler()], ) self.logger = getLogger() - pass + + # Initialize lock, executor, and sync interval + self.nonce_lock = Lock() + self.timer = None + self.sync_interval = sync_interval + self.executor = ThreadPoolExecutor(max_workers=1) + + # Schedule nonce synchronization + self.schedule_sync() + + def schedule_sync(self): + """ + Schedule nonce sync task with the executor and restart the timer + """ + try: + self.executor.submit(self.sync_nonce) + except Exception as e: + self.logger.error(f"Error during Ethereum nonce sync: {e}") + finally: + # Re-run the sync at specified intervals + self.timer = Timer(self.sync_interval, self.schedule_sync) + self.timer.start() + + def sync_nonce(self): + """ + Sync the nonce with the latest data from the provider + """ + try: + with self.nonce_lock: + self.logger.info(f"Starting Chain-id {self.chain_id} nonce sync") + sleep(1) # Introduce a delay if needed to reduce frequency of sync errors + new_nonce = self.provider.eth.get_transaction_count(self.address, 'pending') + if self.nonce is None or new_nonce >= self.nonce: + self.nonce = new_nonce + self.logger.info(f"Chain-id {self.chain_id} nonce synced") + else: + self.logger.warning( + f"New nonce {new_nonce} is not greater than or equal to the old nonce {self.nonce}.") + except Exception as e: + self.logger.error(f"Error syncing nonce: {e}") def create_transaction(self, contract_function, *args, **kwargs): """ @@ -122,12 +159,32 @@ def get_last_txs(self, block_number=None, contract_interface=None): block_number = self.get_last_block() valid_transactions = contract_interface.contract.events.logNewTask().get_logs( - fromBlock=block_number, + fromBlock=block_number ) + + if len(valid_transactions) == 0: + return [] + transaction_hashes = [event['transactionHash'].hex() for event in valid_transactions] try: - block_transactions = self.provider.eth.get_block(block_number, full_transactions=True)['transactions'] - filtered_transactions = [tx for tx in block_transactions if tx['hash'].hex() in transaction_hashes] + # Fetch block with transaction hashes only + block_data = self.provider.eth.get_block(block_number, full_transactions=False) + + # Convert transaction hashes to hexadecimal strings + transaction_hashes_hex = [tx.hex() for tx in block_data['transactions']] + + # Set of required transaction hashes in hexadecimal + required_hashes = set(transaction_hashes) # Your predefined list of hashes + + # Find matching transaction hashes + matching_hashes = set(transaction_hashes_hex).intersection(required_hashes) + + # Fetch full details for only the required transactions + filtered_transactions = [] + if matching_hashes: + # Fetch each transaction individually based on matching hashes + for hash in matching_hashes: + filtered_transactions.append(self.provider.eth.get_transaction(hash)) except Exception as e: self.logger.warning(e) return [] @@ -199,7 +256,7 @@ def call_function(self, function_name, *args): if isinstance(value, list): args[i] = tuple(value) kwargs = None - with self.lock: + with self.lock and self.interface.nonce_lock: if kwargs is None: txn = self.interface.create_transaction(function, *args) elif args is None: diff --git a/TNLS-Relayers/scrt_interface.py b/TNLS-Relayers/scrt_interface.py index 98047e8..b1b0eb8 100644 --- a/TNLS-Relayers/scrt_interface.py +++ b/TNLS-Relayers/scrt_interface.py @@ -1,9 +1,10 @@ import json from copy import deepcopy from logging import getLogger, basicConfig, DEBUG, StreamHandler -from threading import Lock +from threading import Lock, Timer +from concurrent.futures import ThreadPoolExecutor from typing import List -import asyncio +from time import sleep from secret_sdk.client.lcd import LCDClient from secret_sdk.client.lcd.api.tx import CreateTxOptions, BroadcastMode @@ -20,9 +21,7 @@ class SCRTInterface(BaseChainInterface): NOTE: the below default private key is for testing only, and does not correspond to any real account/wallet """ - def __init__(self, private_key="", - address=None, api_url="", chain_id="", provider=None, feegrant_address = None, - **kwargs): + def __init__(self, private_key="", address=None, api_url="", chain_id="", provider=None, feegrant_address=None, sync_interval=30, **kwargs): if isinstance(private_key, str): self.private_key = RawKey.from_hex(private_key) else: @@ -33,14 +32,55 @@ def __init__(self, private_key="", self.provider = provider self.address = address self.feegrant_address = feegrant_address - assert self.address == str(self.private_key.acc_address), f"Address {self.address} and private key " \ - f"{self.private_key.acc_address} mismatch" + assert self.address == str(self.private_key.acc_address), f"Address {self.address} and private key {self.private_key.acc_address} mismatch" self.wallet = self.provider.wallet(self.private_key) - account_number_and_sequence = self.wallet.account_number_and_sequence() - self.account_number = account_number_and_sequence['account_number'] - self.sequence = int(account_number_and_sequence['sequence']) self.logger = getLogger() + # Initialize account number and sequence + self.account_number = None + self.sequence = None + + self.timer = None; + + self.sequence_lock = Lock() + + self.sync_interval = sync_interval + self.executor = ThreadPoolExecutor(max_workers=1) + self.schedule_sync() + + def schedule_sync(self): + """ + Schedule the sync task with the executor and restart the timer + """ + try: + self.executor.submit(self.sync_account_number_and_sequence) + except Exception as e: + self.logger.error(f"Error during Secret sequence sync: {e}") + finally: + self.timer = Timer(self.sync_interval, self.schedule_sync) + self.timer.start() + + + def sync_account_number_and_sequence(self): + """ + Syncs the account number and sequence with the latest data from the provider + """ + try: + with self.sequence_lock: + self.logger.info("Starting Secret sequence sync") + sleep(3) + account_info = self.wallet.account_number_and_sequence() + self.account_number = account_info['account_number'] + new_sequence = int(account_info['sequence']) + if self.sequence is None or new_sequence >= self.sequence: + self.sequence = new_sequence + self.logger.info("Secret sequence synced") + else: + self.logger.warning( + f"New sequence {new_sequence} is not greater than the old sequence {self.sequence}.") + except Exception as e: + self.logger.error(f"Error syncing account number and sequence: {e}") + def sign_and_send_transaction(self, tx): """ Signs and broadcasts a transaction to the network, returns the broadcast receipt @@ -229,7 +269,7 @@ def call_function(self, function_name, *args): args = args[0] if isinstance(args, str): args = json.loads(args) - with self.lock: + with self.lock and self.interface.sequence_lock: txn = self.construct_txn(function_schema, function_name, args) transaction_result = self.interface.sign_and_send_transaction(txn) try: diff --git a/config.yml b/config.yml index ade8e98..6f71d35 100644 --- a/config.yml +++ b/config.yml @@ -105,7 +105,7 @@ #Testnet Chains "11155111": #Ethereum Sepolia - active: false + active: true type: "evm" chain_id: "11155111" api_endpoint: https://eth-sepolia-public.unifra.io @@ -116,17 +116,17 @@ "80002": #Polygon Amoy - active: false + active: true type: "evm" chain_id: "80002" - api_endpoint: https://polygon-amoy.gateway.tenderly.co + api_endpoint: https://rpc-amoy.polygon.technology contract_address: "0x8EaAB5e8551781F3E8eb745E7fcc7DAeEFd27b1f" timeout: 2 contract_schema: '[{"type":"constructor","inputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"increaseTaskId","inputs":[{"name":"_newTaskId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"initialize","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"payoutBalance","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"postExecution","inputs":[{"name":"_taskId","type":"uint256","internalType":"uint256"},{"name":"_sourceNetwork","type":"string","internalType":"string"},{"name":"_info","type":"tuple","internalType":"struct Gateway.PostExecutionInfo","components":[{"name":"payload_hash","type":"bytes32","internalType":"bytes32"},{"name":"packet_hash","type":"bytes32","internalType":"bytes32"},{"name":"callback_address","type":"bytes20","internalType":"bytes20"},{"name":"callback_selector","type":"bytes4","internalType":"bytes4"},{"name":"callback_gas_limit","type":"bytes4","internalType":"bytes4"},{"name":"packet_signature","type":"bytes","internalType":"bytes"},{"name":"result","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceOwnership","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"requestRandomness","inputs":[{"name":"_numWords","type":"uint32","internalType":"uint32"},{"name":"_callbackGasLimit","type":"uint32","internalType":"uint32"}],"outputs":[{"name":"requestId","type":"uint256","internalType":"uint256"}],"stateMutability":"payable"},{"type":"function","name":"send","inputs":[{"name":"_payloadHash","type":"bytes32","internalType":"bytes32"},{"name":"_userAddress","type":"address","internalType":"address"},{"name":"_routingInfo","type":"string","internalType":"string"},{"name":"_info","type":"tuple","internalType":"struct Gateway.ExecutionInfo","components":[{"name":"user_key","type":"bytes","internalType":"bytes"},{"name":"user_pubkey","type":"bytes","internalType":"bytes"},{"name":"routing_code_hash","type":"string","internalType":"string"},{"name":"task_destination_network","type":"string","internalType":"string"},{"name":"handle","type":"string","internalType":"string"},{"name":"nonce","type":"bytes12","internalType":"bytes12"},{"name":"callback_gas_limit","type":"uint32","internalType":"uint32"},{"name":"payload","type":"bytes","internalType":"bytes"},{"name":"payload_signature","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"taskId","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"tasks","inputs":[{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"payload_hash_reduced","type":"bytes31","internalType":"bytes31"},{"name":"completed","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"transferOwnership","inputs":[{"name":"newOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"upgradeHandler","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"OwnershipTransferred","inputs":[{"name":"previousOwner","type":"address","indexed":true,"internalType":"address"},{"name":"newOwner","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"TaskCompleted","inputs":[{"name":"taskId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"callbackSuccessful","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"type":"event","name":"logNewTask","inputs":[{"name":"task_id","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"source_network","type":"string","indexed":false,"internalType":"string"},{"name":"user_address","type":"address","indexed":false,"internalType":"address"},{"name":"routing_info","type":"string","indexed":false,"internalType":"string"},{"name":"payload_hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"info","type":"tuple","indexed":false,"internalType":"struct Gateway.ExecutionInfo","components":[{"name":"user_key","type":"bytes","internalType":"bytes"},{"name":"user_pubkey","type":"bytes","internalType":"bytes"},{"name":"routing_code_hash","type":"string","internalType":"string"},{"name":"task_destination_network","type":"string","internalType":"string"},{"name":"handle","type":"string","internalType":"string"},{"name":"nonce","type":"bytes12","internalType":"bytes12"},{"name":"callback_gas_limit","type":"uint32","internalType":"uint32"},{"name":"payload","type":"bytes","internalType":"bytes"},{"name":"payload_signature","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"error","name":"InvalidBytesLength","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidPacketSignature","inputs":[]},{"type":"error","name":"InvalidPayloadHash","inputs":[]},{"type":"error","name":"InvalidSignature","inputs":[]},{"type":"error","name":"InvalidSignatureLength","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"OwnableInvalidOwner","inputs":[{"name":"owner","type":"address","internalType":"address"}]},{"type":"error","name":"OwnableUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"}]},{"type":"error","name":"PaidRequestFeeTooLow","inputs":[]},{"type":"error","name":"TaskAlreadyCompleted","inputs":[]},{"type":"error","name":"TooManyVRFRandomWordsRequested","inputs":[]}]' wallet_address: "0xbb6B8abe049466f637b3Ac648E7Dd9850E193346" "84532": #Base Sepolia - active: false + active: true type: "evm" chain_id: "84532" api_endpoint: https://sepolia.base.org @@ -136,7 +136,7 @@ wallet_address: "0xbb6B8abe049466f637b3Ac648E7Dd9850E193346" "11155420": #Optmism Sepolia - active: false + active: true type: "evm" chain_id: "11155420" api_endpoint: https://sepolia.optimism.io @@ -146,7 +146,7 @@ wallet_address: "0xbb6B8abe049466f637b3Ac648E7Dd9850E193346" "421614": #Arbitrum Sepolia - active: False + active: true type: "evm" chain_id: "421614" api_endpoint: https://sepolia-rollup.arbitrum.io/rpc @@ -156,7 +156,7 @@ wallet_address: "0xbb6B8abe049466f637b3Ac648E7Dd9850E193346" "80085": #Berachain Artio - active: false + active: true type: "evm" chain_id: "80085" api_endpoint: https://artio.rpc.berachain.com/ @@ -176,7 +176,7 @@ wallet_address: "0xbb6B8abe049466f637b3Ac648E7Dd9850E193346" "128123": #Tezos Etherlink Testnet - active: false + active: true type: "evm" chain_id: "128123" api_endpoint: https://node.ghostnet.etherlink.com @@ -186,7 +186,7 @@ wallet_address: "0xbb6B8abe049466f637b3Ac648E7Dd9850E193346" "107107114116": #Kakarot Sepolia - active: false + active: true type: "evm" chain_id: "107107114116" api_endpoint: https://sepolia-rpc.kakarot.org @@ -196,7 +196,7 @@ wallet_address: "0xbb6B8abe049466f637b3Ac648E7Dd9850E193346" "59902": #Metis Sepolia - active: false + active: true type: "evm" chain_id: "59902" api_endpoint: https://sepolia.metisdevops.link @@ -206,7 +206,7 @@ wallet_address: "0xbb6B8abe049466f637b3Ac648E7Dd9850E193346" "1313161555": #Near Aurora Testnet - active: false + active: true type: "evm" chain_id: "1313161555" api_endpoint: https://testnet.aurora.dev @@ -216,7 +216,7 @@ wallet_address: "0xbb6B8abe049466f637b3Ac648E7Dd9850E193346" "59141": #Linea Sepolia - active: false + active: true type: "evm" chain_id: "59141" api_endpoint: https://rpc.sepolia.linea.build @@ -226,7 +226,7 @@ wallet_address: "0xbb6B8abe049466f637b3Ac648E7Dd9850E193346" "51": #XDC Apothem - active: false + active: true type: "evm" chain_id: "51" api_endpoint: https://erpc.apothem.network @@ -236,7 +236,7 @@ wallet_address: "0xbb6B8abe049466f637b3Ac648E7Dd9850E193346" "4202": #Lisk Sepolia - active: false + active: true type: "evm" chain_id: "4202" api_endpoint: https://rpc.sepolia-api.lisk.com