diff --git a/TNLS-Relayers/config.yml b/TNLS-Relayers/config.yml index a54dbf1..d37f68b 100644 --- a/TNLS-Relayers/config.yml +++ b/TNLS-Relayers/config.yml @@ -1,7 +1,7 @@ #Mainnet Chains "42161": #Arbitrum One - active: true + active: false type: "evm" chain_id: "42161" api_endpoint: https://arb1.arbitrum.io/rpc @@ -9,7 +9,7 @@ timeout: 0.5 "1": #Ethereum - active: true + active: false type: "evm" chain_id: "1" api_endpoint: https://rpc.flashbots.net @@ -17,7 +17,7 @@ timeout: 1 "56": #BSC Chain - active: true + active: false type: "evm" chain_id: "56" api_endpoint: https://bsc.drpc.org @@ -25,7 +25,7 @@ timeout: 1 "137": #Polygon - active: true + active: false type: "evm" chain_id: "137" api_endpoint: https://polygon-bor.publicnode.com @@ -33,7 +33,7 @@ timeout: 1 "10": #Optimsm - active: true + active: false type: "evm" chain_id: "10" api_endpoint: https://mainnet.optimism.io @@ -41,7 +41,7 @@ timeout: 1 "43114": #Avalance C-Chain - active: true + active: false type: "evm" chain_id: "43114" api_endpoint: https://avalanche-c-chain.publicnode.com @@ -49,7 +49,7 @@ timeout: 1 "8453": #Base - active: true + active: false type: "evm" chain_id: "8453" api_endpoint: https://base.publicnode.com @@ -57,15 +57,15 @@ timeout: 1 "59144": #Linea - active: true + active: false type: "evm" chain_id: "59144" - api_endpoint: https://linea-mainnet.public.blastapi.io + api_endpoint: https://rpc.linea.build contract_address: "0xfaFCfceC4e29e9b4ECc8C0a3f7df1011580EEEf2" timeout: 1 "534352": #Scroll - active: true + active: false type: "evm" chain_id: "534352" api_endpoint: https://scroll.drpc.org @@ -73,7 +73,7 @@ timeout: 1 "1088": #Metis - active: true + active: false type: "evm" chain_id: "1088" api_endpoint: https://metis.drpc.org @@ -81,7 +81,7 @@ timeout: 1 "50": #XDC Network - active: true + active: false type: "evm" chain_id: "50" api_endpoint: https://rpc1.xinfin.network @@ -89,7 +89,7 @@ timeout: 1 "1313161554": #Near Aurora - active: true + active: false type: "evm" chain_id: "1313161554" api_endpoint: https://aurora.drpc.org @@ -97,7 +97,7 @@ timeout: 1 "1135": #Lisk - active: true + active: false type: "evm" chain_id: "1135" api_endpoint: https://rpc.api.lisk.com @@ -105,7 +105,7 @@ timeout: 1 "2016": #Mainnetz Mainnet - active: true + active: false type: "evm" chain_id: "2016" api_endpoint: https://mainnet-rpc.mainnetz.io @@ -113,7 +113,7 @@ timeout: 1 "5000": #Mantle - active: true + active: false type: "evm" chain_id: "5000" api_endpoint: https://rpc.mantle.xyz @@ -121,7 +121,7 @@ timeout: 1 "1116": #Core - active: true + active: false type: "evm" chain_id: "1116" api_endpoint: https://rpc.ankr.com/core @@ -129,7 +129,7 @@ timeout: 1 "1284": #Moonbeam - active: true + active: false type: "evm" chain_id: "1284" api_endpoint: https://rpc.api.moonbeam.network @@ -137,7 +137,7 @@ timeout: 1 "1285": #Moonriver - active: true + active: false type: "evm" chain_id: "1285" api_endpoint: https://moonriver-rpc.publicnode.com @@ -145,7 +145,7 @@ timeout: 1 "1329": #Sei - active: true + active: false type: "evm" chain_id: "1329" api_endpoint: https://evm-rpc.sei-apis.com @@ -153,7 +153,7 @@ timeout: 1 "100": #Gnosis - active: true + active: false type: "evm" chain_id: "100" api_endpoint: https://gnosis-rpc.publicnode.com @@ -161,7 +161,7 @@ timeout: 1 "90002": #Ubit Mainnet - active: true + active: false type: "evm" chain_id: "90002" api_endpoint: https://rpc.ubitscan.io/ @@ -178,7 +178,7 @@ timeout: 1 "388": #Cronos zkEVM Mainnet - active: true + active: false type: "evm" chain_id: "388" api_endpoint: "https://mainnet.zkevm.cronos.org" @@ -186,7 +186,7 @@ timeout: 1 "324": #Zksync Era Mainnet - active: true + active: false type: "evm" chain_id: "324" api_endpoint: "https://mainnet.era.zksync.io" @@ -194,7 +194,7 @@ timeout: 1 "secret-4": - active: true + active: false type: "secret" chain_id: "secret-4" api_endpoint: "https://lcd.mainnet.secretsaturn.net" @@ -246,7 +246,7 @@ timeout: 0.5 "534351": #Scroll Sepolia - active: true + active: false type: "evm" chain_id: "534351" api_endpoint: https://scroll-sepolia.drpc.org @@ -254,7 +254,7 @@ timeout: 1 "128123": #Tezos Etherlink Testnet - active: true + active: false type: "evm" chain_id: "128123" api_endpoint: https://node.ghostnet.etherlink.com @@ -262,7 +262,7 @@ timeout: 1 "1802203764": #Kakarot Sepolia - active: true + active: false type: "evm" chain_id: "1802203764" api_endpoint: https://sepolia-rpc.kakarot.org @@ -334,7 +334,7 @@ timeout: 1 "5003": #Mantle Sepolia - active: false + active: true type: "evm" chain_id: "5003" api_endpoint: "https://rpc.sepolia.mantle.xyz" @@ -342,7 +342,7 @@ timeout: 1 "44433": #UBIT Testnet - active: true + active: false type: "evm" chain_id: "44433" api_endpoint: "https://testnet-rpc.ubitscan.io/" @@ -350,7 +350,7 @@ timeout: 1 "713715": #Sei Devnet - active: false + active: true type: "evm" chain_id: "713715" api_endpoint: "https://evm-rpc-arctic-1.sei-apis.com" @@ -374,7 +374,7 @@ timeout: 1 "282": #Cronos zkEVM Testnet - active: true + active: false type: "evm" chain_id: "282" api_endpoint: "https://testnet.zkevm.cronos.org" @@ -382,7 +382,7 @@ timeout: 1 "300": #Zksync Era Testnet - active: true + active: false type: "evm" chain_id: "300" api_endpoint: "https://sepolia.era.zksync.dev" @@ -390,7 +390,7 @@ timeout: 1 "SolDN": #Solana DevNet - active: true + active: false type: "solana" chain_id: "SolDN" api_endpoint: https://rpc.ankr.com/solana_devnet @@ -398,7 +398,7 @@ timeout: 1 "SolTN": #Solana TestNet - active: true + active: false type: "solana" chain_id: "SolTN" api_endpoint: https://rpc.ankr.com/solana_testnet diff --git a/TNLS-Relayers/scrt_interface.py b/TNLS-Relayers/scrt_interface.py index 7e4befc..01fcfdc 100644 --- a/TNLS-Relayers/scrt_interface.py +++ b/TNLS-Relayers/scrt_interface.py @@ -4,7 +4,8 @@ from threading import Lock, Timer from concurrent.futures import ThreadPoolExecutor from typing import List -from time import sleep, time +from time import sleep +import requests from secret_sdk.client.lcd import LCDClient from secret_sdk.client.lcd.api.tx import CreateTxOptions, BroadcastMode @@ -22,17 +23,21 @@ class SCRTInterface(BaseChainInterface): """ def __init__(self, private_key="", 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: self.private_key = RawKey(private_key) + if provider is None: self.provider = LCDClient(url=api_url, chain_id=chain_id, **kwargs) else: self.provider = provider + self.feegrant_address = feegrant_address self.address = str(self.private_key.acc_address) self.wallet = self.provider.wallet(self.private_key) + self.api_url = api_url self.logger = getLogger() # Initialize account number and sequence @@ -59,7 +64,6 @@ def schedule_sync(self): 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 @@ -67,17 +71,30 @@ def sync_account_number_and_sequence(self): try: with self.sequence_lock: self.logger.info("Starting Secret sequence sync") - 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") + + # Replace with your wallet address + url = f'{self.api_url}/cosmos/auth/v1beta1/accounts/{self.address}' + + # Make the GET request to the URL + response = requests.get(url) + if response.status_code == 200: + data = response.json() + + # Extract account number and sequence from the JSON response + account_info = data.get('account', {}) + self.account_number = account_info.get('account_number') + new_sequence = int(account_info.get('sequence', 0)) + + 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}.") else: - self.logger.warning( - f"New sequence {new_sequence} is not greater than the old sequence {self.sequence}.") + self.logger.error(f"Failed to fetch account info: HTTP {response.status_code}") except Exception as e: - self.logger.error(f"Error syncing account number and sequence: {e}") + self.logger.error("An error occurred while syncing account number and sequence") def sign_and_send_transaction(self, tx): """ @@ -90,10 +107,10 @@ def sign_and_send_transaction(self, tx): """ max_retries = 20 + wait_interval = 3 try: # Broadcast the transaction in SYNC mode final_tx = self.provider.tx.broadcast_adapter(tx, mode=BroadcastMode.BROADCAST_MODE_ASYNC) - print(final_tx) tx_hash = final_tx.txhash self.logger.info(f"Transaction broadcasted with hash: {tx_hash}") @@ -101,7 +118,6 @@ def sign_and_send_transaction(self, tx): for attempt in range(max_retries): try: tx_result = self.provider.tx.tx_info(tx_hash) - print(tx_result) if tx_result: self.logger.info(f"Transaction included in block: {tx_result.height}") return tx_result @@ -109,7 +125,7 @@ def sign_and_send_transaction(self, tx): if 'not found' in str(e).lower(): # Transaction not yet found, wait and retry self.logger.info(f"Transaction not found, retrying... ({attempt+1}/{max_retries})") - sleep(3) + sleep(wait_interval) continue else: self.logger.error(f"LCDResponseError while fetching tx result: {e}") diff --git a/TNLS-Relayers/sol_interface.py b/TNLS-Relayers/sol_interface.py index 901a9b2..593554f 100644 --- a/TNLS-Relayers/sol_interface.py +++ b/TNLS-Relayers/sol_interface.py @@ -19,6 +19,7 @@ class LogNewTask: + # Define the data layout for the LogNewTask event using Borsh serialization layout = CStruct( "task_id" / U64, "source_network" / String, @@ -38,7 +39,7 @@ class LogNewTask: class PostExecution: - + # Define the data layout for the PostExecution event using Borsh serialization layout = CStruct( "task_id" / U64, "source_network" / String, @@ -59,13 +60,13 @@ def __init__(self, private_key="", provider=None, contract_address="", chain_id= if provider is None: provider = Client(api_endpoint, timeout) - self.provider = provider - self.private_key = private_key - self.account = Keypair.from_base58_string(private_key) - self.address = self.account.pubkey() - self.sync_interval = sync_interval - self.lock = Lock() - self.executor = ThreadPoolExecutor(max_workers=1) + self.provider = provider # Solana RPC provider client + self.private_key = private_key # User's private key in base58 string format + self.account = Keypair.from_base58_string(private_key) # Generate Keypair from private key + self.address = self.account.pubkey() # Public key of the account + self.sync_interval = sync_interval # Sync interval in seconds + self.lock = Lock() # Thread lock for synchronization + self.executor = ThreadPoolExecutor(max_workers=1) # Thread pool executor with one worker # Set up logging basicConfig( @@ -73,21 +74,24 @@ def __init__(self, private_key="", provider=None, contract_address="", chain_id= format="%(asctime)s [Solana Interface: %(levelname)8.8s] %(message)s", handlers=[StreamHandler()], ) - self.logger = getLogger() + self.logger = getLogger() # Get the logger instance def sign_and_send_transaction(self, txn): """ Sign and send a transaction to the Solana network synchronously. """ - # Sign the transaction + # Sign the transaction with the account's keypair txn.sign(self.account) - # Send the transaction - response = self.provider.send_transaction(txn, self.account, - opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) + # Send the transaction to the network + response = self.provider.send_transaction( + txn, + self.account, + opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed) + ) - # Confirm the transaction + # Confirm the transaction on the network tx_response = self.provider.confirm_transaction(response.value, commitment=Confirmed) return tx_response @@ -96,6 +100,7 @@ def get_last_block(self): Gets the most recent block number on the Solana network. """ try: + # Fetch the current slot (block number) with confirmed commitment return self.provider.get_slot(commitment=Confirmed).value except Exception as e: self.logger.error(f"Error fetching the most recent block: {e}") @@ -103,20 +108,27 @@ def get_last_block(self): def get_transactions(self, contract_interface, height): """ - Get transactions for a given address. + Get transactions for a given address since a certain block height. """ - #jump = 0 + # Set the jump interval (e.g., check every 20 blocks) jump = 20 if height % jump != 0: + # If the height is not a multiple of jump, return empty list return [] filtered_transactions = [] try: - response = self.provider.get_signatures_for_address(account=contract_interface.address, limit=10, - commitment=Confirmed) + # Get recent signatures for the given contract address + response = self.provider.get_signatures_for_address( + account=contract_interface.address, + limit=10, + commitment=Confirmed + ) if response.value: - # Filter transactions by slot height - filtered_transactions = [tx.signature for tx in response.value if tx.slot >= height-jump] + # Filter transactions by slot height (transactions within the last 'jump' slots) + filtered_transactions = [ + tx.signature for tx in response.value if tx.slot >= height - jump + ] else: return [] except Exception as e: @@ -127,9 +139,11 @@ def get_transactions(self, contract_interface, height): try: with ThreadPoolExecutor(max_workers=50) as executor: - # Create a future for each transaction - future_to_transaction = {executor.submit(self.process_transaction, signature): signature - for signature in filtered_transactions} + # Create a future for each transaction to process them concurrently + future_to_transaction = { + executor.submit(self.process_transaction, signature): signature + for signature in filtered_transactions + } for future in as_completed(future_to_transaction): result = future.result() if result is not None: @@ -142,16 +156,20 @@ def get_transactions(self, contract_interface, height): def process_transaction(self, signature): """ - Process a transaction and return its receipt. + Process a transaction and return its receipt if it contains the expected log. """ try: + # Fetch the transaction details using its signature response = self.provider.get_transaction(signature, commitment=Confirmed) if response.value: self.logger.info(f"Transaction found: {signature}") + # Extract the log messages from the transaction metadata log_messages = response.value.transaction.meta.log_messages + # Look for a specific log message indicating a new task for log in log_messages: if "LogNewTask:" in log: + # Return the transaction if it contains the "LogNewTask:" log return response.value return None else: @@ -165,15 +183,19 @@ def process_transaction(self, signature): # Base class for interaction with Solana contracts (programs) class SolanaContract: def __init__(self, interface, program_id): - self.interface = interface - self.program_id = Pubkey.from_string(program_id) + self.interface = interface # SolanaInterface instance + self.program_id = Pubkey.from_string(program_id) # Program ID (public key) of the contract + + # Find the program-derived address (PDA) for 'gateway_state' gateway_pda, gateway_bump = Pubkey.find_program_address([b'gateway_state'], self.program_id) + # Find the PDA for 'task_state' task_pda, task_bump = Pubkey.find_program_address([b'task_state'], self.program_id) - self.gateway_pda = gateway_pda - self.address = gateway_pda - self.task_pda = task_pda - self.lock = Lock() - self.logger = getLogger() + + self.gateway_pda = gateway_pda # PDA for the gateway state + self.address = gateway_pda # Address of the contract (set to gateway_pda) + self.task_pda = task_pda # PDA for the task state + self.lock = Lock() # Thread lock for synchronization + self.logger = getLogger() # Get the logger instance self.logger.info("Initialized Solana contract with program ID: %s", program_id) def call_function(self, function_name, *args): @@ -183,10 +205,10 @@ def call_function(self, function_name, *args): with self.lock: """ - Create a transaction with the given instructions and signers. - """ + Create a transaction with the given instructions and signers. + """ - # Create AccountMetas + # Create the list of AccountMeta (accounts involved in the instruction) accounts: list[AccountMeta] = [ AccountMeta(pubkey=self.gateway_pda, is_signer=False, is_writable=False), AccountMeta(pubkey=self.task_pda, is_signer=False, is_writable=True), @@ -194,24 +216,26 @@ def call_function(self, function_name, *args): AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), ] - # Parse the JSON + # Parse the JSON arguments if only one argument is provided if len(args) == 1: args = json.loads(args[0]) - # Ensure the callback_address_bytes length is a multiple of 32 - callback_address_bytes = bytes.fromhex(args[2][2][2:]) + # Ensure the callback_address_bytes length is a multiple of 32 bytes + callback_address_bytes = bytes.fromhex(args[2][2][2:]) # Remove '0x' prefix if len(callback_address_bytes) % 32 != 0: raise ValueError("callback_address_bytes length is not a multiple of 32") # Check and create callback_accounts callback_accounts: List[AccountMeta] = [] + # Iterate over the callback_address_bytes in chunks of 32 bytes for i in range(0, len(callback_address_bytes), 32): pubkey = Pubkey(callback_address_bytes[i:i + 32]) + # Skip if the pubkey is the interface address or contract address if pubkey == self.interface.address or pubkey == self.address: continue callback_accounts.append(AccountMeta(pubkey=pubkey, is_signer=False, is_writable=True)) - # Add the callback_accounts to the accounts + # Add the callback_accounts to the accounts list if callback_accounts is not None: accounts += callback_accounts @@ -225,9 +249,10 @@ def call_function(self, function_name, *args): # Add the extracted program_id as an AccountMeta accounts.append(AccountMeta(pubkey=program_id_pubkey, is_signer=False, is_writable=False)) - # The Identifier of the post execution function + # The Identifier of the post execution function (function selector) identifier = bytes([52, 46, 67, 194, 153, 197, 69, 168]) + # Build the encoded arguments for the instruction data using PostExecution layout encoded_args = PostExecution.layout.build( { "task_id": args[0], @@ -243,49 +268,62 @@ def call_function(self, function_name, *args): } ) + # Combine the function identifier and encoded arguments to form the instruction data data = identifier + encoded_args + + # Create the instruction tx = Instruction(program_id=self.program_id, data=data, accounts=accounts) + + # Extract callback_gas_limit from the arguments callback_gas_limit = int.from_bytes(bytes.fromhex(args[2][4][2:]), byteorder='big') + # Create a compute budget instruction to set the compute unit limit compute_budget_ix = set_compute_unit_limit(callback_gas_limit) - # Create the transaction + # Create the transaction with the fee payer set to the interface address transaction = Transaction(fee_payer=self.interface.address) + # Add the compute budget instruction and the main instruction to the transaction transaction.add(compute_budget_ix, tx) + # Sign and send the transaction using the interface submitted_txn = self.interface.sign_and_send_transaction(transaction) return submitted_txn def parse_event_from_txn(self, event_name, txn) -> List[Task]: """ - Parse an event from a transaction receipt. + Parse an event from a transaction receipt and extract tasks. """ task_list = [] try: + # Get the log messages from the transaction metadata log_messages = txn.transaction.meta.log_messages for log in log_messages: if "LogNewTask:" in log: + # Extract the data after "LogNewTask:" log_data = log.split("LogNewTask:")[1] + # Parse the event data using LogNewTask layout event_data = LogNewTask.layout.parse(base64.b64decode(log_data)) - args = {'task_id': event_data.task_id, - 'task_destination_network': event_data.task_destination_network, - 'source_network': event_data.source_network, - 'user_address': base64.b64encode(bytes(event_data.user_address)).decode('ASCII'), - 'routing_info': event_data.routing_info, - 'routing_code_hash': event_data.routing_code_hash, - 'payload': base64.b64encode(bytes(event_data.payload)).decode('ASCII'), - 'payload_hash': base64.b64encode(bytes(event_data.payload_hash)).decode('ASCII'), - 'payload_signature': base64.b64encode(bytes(event_data.payload_signature)).decode('ASCII'), - 'user_key': base64.b64encode(bytes(event_data.user_key)).decode('ASCII'), - 'user_pubkey': base64.b64encode(bytes(event_data.user_pubkey)).decode('ASCII'), - 'handle': event_data.handle, - 'callback_gas_limit': event_data.callback_gas_limit, - 'nonce': base64.b64encode(bytes(event_data.nonce)).decode('ASCII') + # Build the arguments for the Task object + args = { + 'task_id': event_data.task_id, + 'task_destination_network': event_data.task_destination_network, + 'source_network': event_data.source_network, + 'user_address': base64.b64encode(bytes(event_data.user_address)).decode('ASCII'), + 'routing_info': event_data.routing_info, + 'routing_code_hash': event_data.routing_code_hash, + 'payload': base64.b64encode(bytes(event_data.payload)).decode('ASCII'), + 'payload_hash': base64.b64encode(bytes(event_data.payload_hash)).decode('ASCII'), + 'payload_signature': base64.b64encode(bytes(event_data.payload_signature)).decode('ASCII'), + 'user_key': base64.b64encode(bytes(event_data.user_key)).decode('ASCII'), + 'user_pubkey': base64.b64encode(bytes(event_data.user_pubkey)).decode('ASCII'), + 'handle': event_data.handle, + 'callback_gas_limit': event_data.callback_gas_limit, + 'nonce': base64.b64encode(bytes(event_data.nonce)).decode('ASCII') } + # Create a Task object and add it to the task list task_list.append(Task(args)) return task_list except Exception as e: - self.logger.error(f"Error parsing transaction: {e}") - + self.logger.error(f"Error parsing transaction: {e}") \ No newline at end of file diff --git a/TNLS-Relayers/web_app.py b/TNLS-Relayers/web_app.py index 785ab5a..be3caeb 100644 --- a/TNLS-Relayers/web_app.py +++ b/TNLS-Relayers/web_app.py @@ -13,114 +13,167 @@ from base_interface import eth_chains, scrt_chains, solana_chains from dotenv import load_dotenv +# Load environment variables from a .env file into os.environ load_dotenv() + +# Read the Ethereum contract ABI (Application Binary Interface) from 'gateway.json' with open(f'{Path(__file__).parent.absolute()}/gateway.json', 'r') as file: eth_contract_schema = file.read() - def generate_eth_config(config_dict, provider=None): """ - Converts a config dict into a tuple of (chain_interface, contract_interface, event_name, function_name) - for ethereum - Args: - config_dict: a dict containing contract address, contract schema, and wallet address - provider: an optional API client + Converts a configuration dictionary into Ethereum-specific components needed for the relayer. - Returns: the relevant tuple of chain, contract, event, and function + Args: + config_dict (dict): Contains contract address, chain ID, API endpoint, timeout, etc. + provider (optional): Custom provider (e.g., web3 provider) + Returns: + tuple: (chain_interface, contract_interface, event_name, function_name) """ + # Retrieve the Ethereum private key from environment variables and convert it to bytes priv_key = bytes.fromhex(os.environ['ethereum-private-key']) + + # Extract configuration parameters from the config dictionary contract_address = config_dict['contract_address'] - contract_schema = eth_contract_schema + contract_schema = eth_contract_schema # Use the ABI loaded earlier chain_id = config_dict['chain_id'] api_endpoint = config_dict['api_endpoint'] timeout = config_dict['timeout'] + # Define the event name to listen for and the function name to invoke event_name = 'logNewTask' function_name = 'postExecution' - initialized_chain = EthInterface(private_key=priv_key, provider=provider, chain_id=chain_id, - api_endpoint=api_endpoint, timeout=timeout) - initialized_contract = EthContract(interface=initialized_chain, address=contract_address, - abi=contract_schema) + # Initialize the Ethereum chain interface with the provided parameters + initialized_chain = EthInterface( + private_key=priv_key, + provider=provider, + chain_id=chain_id, + api_endpoint=api_endpoint, + timeout=timeout + ) + + # Initialize the Ethereum contract interface with the chain interface and contract details + initialized_contract = EthContract( + interface=initialized_chain, + address=contract_address, + abi=contract_schema + ) + + # Create a tuple containing all necessary components eth_tuple = (initialized_chain, initialized_contract, event_name, function_name) return eth_tuple def generate_solana_config(config_dict, provider=None): """ - Converts a config dict into a tuple of (rpc_client, contract_address, wallet_address, function_name) for Solana. + Converts a configuration dictionary into Solana-specific components needed for the relayer. + Args: - config_dict: a dictionary containing relevant information such as RPC endpoint, contract address, wallet address, etc. + config_dict (dict): Contains program ID, chain ID, API endpoint, timeout, etc. + provider (optional): Custom provider (e.g., Solana RPC client) Returns: - A tuple of Solana RPC client, contract address, wallet public key, and a function name. + tuple: (chain_interface, contract_interface, event_name, function_name) """ - + # Retrieve the Solana private key from environment variables priv_key = os.environ['solana-private-key'] + + # Extract configuration parameters from the config dictionary api_endpoint = config_dict["api_endpoint"] - program_id = config_dict['program_id'] + program_id = config_dict['program_id'] # Address of the Solana program (smart contract) chain_id = config_dict['chain_id'] timeout = config_dict['timeout'] + # Define the event name to listen for and the function name to invoke event_name = 'logNewTask' function_name = 'postExecution' - initialized_chain = SolanaInterface(private_key=priv_key, provider=provider, chain_id=chain_id, - api_endpoint=api_endpoint, timeout=timeout) - initialized_contract = SolanaContract(interface=initialized_chain, program_id=program_id) + # Initialize the Solana chain interface with the provided parameters + initialized_chain = SolanaInterface( + private_key=priv_key, + provider=provider, + chain_id=chain_id, + api_endpoint=api_endpoint, + timeout=timeout + ) + # Initialize the Solana contract interface with the chain interface and program ID + initialized_contract = SolanaContract( + interface=initialized_chain, + program_id=program_id + ) + + # Create a tuple containing all necessary components solana_tuple = (initialized_chain, initialized_contract, event_name, function_name) return solana_tuple - def generate_scrt_config(config_dict, provider=None): """ - Converts a config dict into a tuple of (chain_interface, contract_interface, event_name, function_name) - for secret - Args: - config_dict: a dict containing contract address, contract schema, and wallet address - provider: an optional API client + Converts a configuration dictionary into Secret Network-specific components needed for the relayer. - Returns: the relevant tuple of chain, contract, event, and function + Args: + config_dict (dict): Contains contract address, code hash, chain ID, API endpoint, etc. + provider (optional): Custom provider (e.g., SecretJS client) + Returns: + tuple: (chain_interface, contract_interface, event_name, function_name) """ + # Retrieve the Secret Network private key from environment variables and convert it to bytes priv_key = bytes.fromhex(os.environ['secret-private-key']) + + # Extract configuration parameters from the config dictionary contract_address = config_dict['contract_address'] api_endpoint = config_dict['api_endpoint'] chain_id = config_dict['chain_id'] - code_hash = config_dict['code_hash'] + code_hash = config_dict['code_hash'] # Required for interacting with Secret contracts + + # Optional fee grant address feegrant_address = config_dict['feegrant_address'] if 'feegrant_address' in config_dict else None + + # Load the Secret contract ABI from 'secret_abi.json' with open(f'{Path(__file__).parent.absolute()}/secret_abi.json') as f: contract_schema = f.read() + + # Define the event name to listen for and determine the function name from the ABI event_name = 'wasm' function_name = list(json.loads(contract_schema).keys())[0] - if provider is None: - initialized_chain = SCRTInterface(private_key=priv_key, provider=None, - api_url=api_endpoint, chain_id=chain_id, feegrant_address=feegrant_address) - else: - initialized_chain = SCRTInterface(private_key=priv_key, provider=provider, chain_id=chain_id, - feegrant_address=feegrant_address) + # Initialize the Secret Network chain interface with the provided parameters + initialized_chain = SCRTInterface( + private_key=priv_key, + provider=provider, + chain_id=chain_id, + feegrant_address=feegrant_address, + api_url=api_endpoint + ) - initialized_chain = SCRTInterface(private_key=priv_key, provider=provider, chain_id=chain_id, - feegrant_address=feegrant_address, api_url = api_endpoint) - initialized_contract = SCRTContract(interface=initialized_chain, address=contract_address, - abi=contract_schema, code_hash=code_hash) + # Initialize the Secret Network contract interface with the chain interface and contract details + initialized_contract = SCRTContract( + interface=initialized_chain, + address=contract_address, + abi=contract_schema, + code_hash=code_hash + ) + + # Create a tuple containing all necessary components scrt_tuple = (initialized_chain, initialized_contract, event_name, function_name) return scrt_tuple - def generate_full_config(config_file, providers=None): """ - Takes in a yaml filepath and generates a config dict for eth and scrt relays + Takes in a YAML configuration file and generates configurations for all active chains. + Args: - config_file: the path to the relevant config file - providers: inject all providers if needed + config_file (str): The path to the YAML configuration file. + providers (tuple, optional): Custom providers for each chain (Ethereum, Solana, Secret). Returns: - a dict mapping scrt and eth to their respective configs - + tuple: (chains_dict, keys_dict) + - chains_dict: A dictionary mapping chain names to their configuration tuples. + - keys_dict: A dictionary for storing keys (currently empty). """ + # Set up basic logging configuration for the application basicConfig( level=INFO, format="%(asctime)s [Eth Interface: %(levelname)8.8s] %(message)s", @@ -128,122 +181,138 @@ def generate_full_config(config_file, providers=None): ) logger = getLogger() + # Load the configuration dictionary from the YAML file with open(config_file) as f: config_dict = safe_load(f) + + # If no providers are specified, default to None for each chain if providers is None: provider_eth, provider_solana, provider_scrt = None, None, None else: provider_eth, provider_solana, provider_scrt = providers - keys_dict = {} - chains_dict = {} + keys_dict = {} # Initialize an empty dictionary for keys (future use) + chains_dict = {} # Initialize an empty dictionary to store chain configurations + + # Iterate over all Ethereum chains defined in base_interface.eth_chains for chain in eth_chains: if config_dict[chain]['active']: try: + # Generate the Ethereum configuration tuple and add it to chains_dict chains_dict[chain] = generate_eth_config(config_dict[chain], provider=provider_eth) except Exception as e: logger.error(f"Error generating ETH config for chain '{chain}': {e}") + # Iterate over all Solana chains defined in base_interface.solana_chains for chain in solana_chains: if config_dict[chain]['active']: + # Generate the Solana configuration tuple and add it to chains_dict chains_dict[chain] = generate_solana_config(config_dict[chain], provider=provider_solana) + # Iterate over all Secret Network chains defined in base_interface.scrt_chains for chain in scrt_chains: if config_dict[chain]['active']: try: + # Generate the Secret Network configuration tuple and add it to chains_dict chains_dict[chain] = generate_scrt_config(config_dict[chain], provider=provider_scrt) except Exception as e: logger.error(f"Error generating SCRT config for chain '{chain}': {e}") return chains_dict, keys_dict - +# Create a Flask Blueprint for organizing routes route_blueprint = Blueprint('route_blueprint', __name__) - @route_blueprint.route('/') def index(): """ + Root endpoint that returns a string representation of the relayer. - Returns: a string form of the relayer - + Returns: + str: String form of the relayer object. """ return str(current_app.config['RELAYER']) - @route_blueprint.route('/tasks_to_routes') def task_json(): """ + Endpoint to get the status of tasks managed by the relayer. - Returns: The status of the relayer - + Returns: + str: String representation of task IDs mapped to their statuses. """ return str(current_app.config['RELAYER'].task_ids_to_statuses) - @route_blueprint.route('/networks_to_blocks') def net_to_block(): """ + Endpoint to get the latest processed block numbers for each network. - Returns: The status of the relayer - + Returns: + str: String representation of network names mapped to their latest block numbers. """ return str(current_app.config['RELAYER'].dict_of_names_to_blocks) - @route_blueprint.route('/ids_to_jsons') def id_to_json(): """ + Endpoint to get detailed information about each task. - Returns: The status of the relayer - + Returns: + str: String representation of task IDs mapped to their detailed information. """ return str(current_app.config['RELAYER'].task_ids_to_info) - @route_blueprint.route('/networks_to_addresses') def net_to_address(): """ + Endpoint to get the mapping of network names to contract addresses. - Returns: The map of names to contract addresses - - """ + Returns: + str: String representation of network names mapped to contract addresses. + """ return str(current_app.config['RELAYER'].dict_of_names_to_addresses) - @route_blueprint.route('/keys') def keys(): """ + Endpoint to get the current encryption and verification keys. - Returns: the current encryption and verification keys - + Returns: + str: String representation of the keys. """ return str(current_app.config['KEYS']) - def app_factory(config_filename=f'{Path(__file__).parent.absolute()}/config.yml', config_file_converter=generate_full_config, num_loops=None): """ - Creates a Flask app with a relayer running on the backend - Args: - config_filename: Which filepath to pull config from - config_file_converter: How to convert that config file into relayer config - num_loops: How many times the relayer should run before shutting down, None=Infinity + Factory function to create and configure the Flask app with the relayer. - Returns: a flask app + Args: + config_filename (str): Path to the configuration YAML file. + config_file_converter (function): Function to convert the config file into relayer configuration. + num_loops (int, optional): Number of times the relayer should run before shutting down. None means infinite. + Returns: + Flask: Configured Flask app instance. """ import warnings - warnings.simplefilter("ignore", UserWarning) - app = Flask(__name__) + warnings.simplefilter("ignore", UserWarning) # Ignore user warnings for cleaner output + app = Flask(__name__) # Initialize the Flask app + # Generate the relayer configuration and keys dictionary config, keys_dict = config_file_converter(config_filename) + # Initialize the relayer with the generated configuration relayer = Relayer(config, num_loops=num_loops) + # Store the relayer and keys in the app's configuration app.config['RELAYER'] = relayer app.config['KEYS'] = keys_dict + # Register the blueprint containing the defined routes app.register_blueprint(route_blueprint) + # Start the relayer's main loop (this might block execution if not run in a separate thread) relayer.run() return app - if __name__ == '__main__': + # If the script is run directly, create the app using the default configuration file app = app_factory(f'{Path(__file__).parent.absolute()}/config.yml') - app.run() + # Run the Flask app on the local development server + app.run() \ No newline at end of file