diff --git a/src/bin/hive_chain.rs b/src/bin/hive_chain.rs index 8e8da1e09..331079a96 100644 --- a/src/bin/hive_chain.rs +++ b/src/bin/hive_chain.rs @@ -68,7 +68,8 @@ async fn main() -> eyre::Result<()> { std::env::set_var("STARKNET_NETWORK", STARKNET_RPC_URL); // Prepare the relayer - let relayer_balance = starknet_provider.balance_at(args.relayer_address, BlockId::Tag(BlockTag::Latest)).await?; + let relayer_balance = + starknet_provider.balance_at_native(args.relayer_address, BlockId::Tag(BlockTag::Latest)).await?; let relayer_balance = into_via_try_wrapper!(relayer_balance)?; let relayer = Relayer::new( diff --git a/src/models/felt.rs b/src/models/felt.rs index 264636a55..a88c34350 100644 --- a/src/models/felt.rs +++ b/src/models/felt.rs @@ -52,6 +52,28 @@ impl TryFrom for Address { } } +impl TryFrom for u64 { + type Error = EthereumDataFormatError; + + fn try_from(felt: Felt252Wrapper) -> Result { + match felt.to_be_digits() { + [0, 0, 0, d] => Ok(d), + _ => Err(EthereumDataFormatError::Primitive), + } + } +} + +impl TryFrom for u128 { + type Error = EthereumDataFormatError; + + fn try_from(felt: Felt252Wrapper) -> Result { + match felt.to_be_digits() { + [0, 0, d1, d2] => Ok(Self::from(d1) << 64 | Self::from(d2)), + _ => Err(EthereumDataFormatError::Primitive), + } + } +} + impl From for Felt252Wrapper { fn from(value: B256) -> Self { Self(Felt::from_bytes_be(value.as_ref())) @@ -191,4 +213,36 @@ mod tests { // When assert_eq!(Felt252Wrapper::from(hash).0, Felt::ZERO,); } + + #[test] + fn test_u64_try_from_felt_should_pass() { + let value = Felt::from(u64::MAX); + + let value = u64::try_from(Felt252Wrapper::from(value)).unwrap(); + assert_eq!(u64::MAX, value); + } + + #[test] + fn test_u64_try_from_felt_should_fail() { + let value = Felt::from(u64::MAX) + Felt::ONE; + + let value = u64::try_from(Felt252Wrapper::from(value)); + assert!(value.is_err()); + } + + #[test] + fn test_u128_try_from_felt_should_pass() { + let value = Felt::from(u128::MAX); + + let value = u128::try_from(Felt252Wrapper::from(value)).unwrap(); + assert_eq!(u128::MAX, value); + } + + #[test] + fn test_u128_try_from_felt_should_fail() { + let value = Felt::from(u128::MAX) + Felt::ONE; + + let value = u128::try_from(Felt252Wrapper::from(value)); + assert!(value.is_err()); + } } diff --git a/src/pool/mempool.rs b/src/pool/mempool.rs index a26f48068..3b0f5ffb8 100644 --- a/src/pool/mempool.rs +++ b/src/pool/mempool.rs @@ -167,7 +167,7 @@ impl AccountM // Get the balance of the address for the Pending block. self.eth_client .starknet_provider() - .balance_at(account_address, starknet::core::types::BlockId::Tag(BlockTag::Pending)) + .strk_balance_at(account_address, starknet::core::types::BlockId::Tag(BlockTag::Pending)) .await .map_err(Into::into) } diff --git a/src/providers/eth_provider/starknet/mod.rs b/src/providers/eth_provider/starknet/mod.rs index 386d7efa7..8a9136940 100644 --- a/src/providers/eth_provider/starknet/mod.rs +++ b/src/providers/eth_provider/starknet/mod.rs @@ -4,10 +4,13 @@ pub mod relayer; use cainome::rs::abigen_legacy; use starknet::core::types::Felt; -use std::sync::LazyLock; abigen_legacy!(ERC20, "./.kakarot/artifacts/ERC20.json"); /// Starknet native token address -pub static STARKNET_NATIVE_TOKEN: LazyLock = - LazyLock::new(|| Felt::from_hex("0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7").unwrap()); +pub static STARKNET_NATIVE_TOKEN: Felt = + Felt::from_hex_unchecked("0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"); + +/// Starknet eth token address +pub static STARKNET_ETH: Felt = + Felt::from_hex_unchecked("0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"); diff --git a/src/providers/eth_provider/starknet/relayer.rs b/src/providers/eth_provider/starknet/relayer.rs index 4314d57c9..e765590d1 100644 --- a/src/providers/eth_provider/starknet/relayer.rs +++ b/src/providers/eth_provider/starknet/relayer.rs @@ -1,16 +1,16 @@ use crate::{ constants::STARKNET_CHAIN_ID, - models::transaction::transaction_data_to_starknet_calldata, + models::{felt::Felt252Wrapper, transaction::transaction_data_to_starknet_calldata}, providers::eth_provider::{ database::{ethereum::EthereumTransactionStore, types::transaction::EthStarknetHashes, Database}, - error::{SignatureError, TransactionError}, + error::{KakarotError, SignatureError, TransactionError}, provider::EthApiResult, starknet::kakarot_core::{starknet_address, EXECUTE_FROM_OUTSIDE}, }, }; use reth_primitives::TransactionSigned; use starknet::{ - accounts::{Account, ConnectedAccount, ExecutionEncoding, ExecutionV1, SingleOwnerAccount}, + accounts::{Account, ConnectedAccount, ExecutionEncoding, ExecutionV3, SingleOwnerAccount}, core::types::{BlockTag, Felt, NonZeroFelt}, providers::Provider, signers::{LocalWallet, SigningKey}, @@ -37,7 +37,7 @@ static RELAYER_SIGNER: LazyLock = LazyLock::new(|| { pub struct Relayer { /// The account used to sign and broadcast the transaction account: SingleOwnerAccount, - /// The balance of the relayer + /// The balance of the relayer in STRK balance: Felt, /// The database used to store the relayer's transaction hashes map (Ethereum -> Starknet) database: Option>, @@ -76,7 +76,7 @@ where // Construct the call let call = starknet::core::types::Call { to: eoa_address, selector: *EXECUTE_FROM_OUTSIDE, calldata }; - let mut execution = ExecutionV1::new(vec![call], &self.account); + let mut execution = ExecutionV3::new(vec![call], &self.account); // Fetch the relayer nonce from the Starknet provider let relayer_nonce = self @@ -88,9 +88,31 @@ where execution = execution.nonce(relayer_nonce); - // We set the max fee to the balance of the account / 5. This means that the account could + // Fetch the current gas price from the Starknet provider + // TODO: fetch the gas price in background and cache it + let fri_gas_price = self + .account + .provider() + .get_block_with_tx_hashes(starknet::core::types::BlockId::Tag(BlockTag::Pending)) + .await + .map_err(KakarotError::from)? + .l1_gas_price() + .price_in_fri; + + // We set the gas to the balance of the account / 5. This means that the account could // send up to 5 transactions before hitting a feeder gateway error. - execution = execution.max_fee(self.balance.floor_div(&NonZeroFelt::from_felt_unchecked(5.into()))); + let max_fee = self.balance.floor_div(&NonZeroFelt::from_felt_unchecked(5.into())); + + let max_gas_price = fri_gas_price.double(); + let max_gas = max_fee.floor_div(&NonZeroFelt::from_felt_unchecked(if max_gas_price == Felt::ZERO { + Felt::from(1) + } else { + max_gas_price + })); + + execution = execution + .gas(Felt252Wrapper::from(max_gas).try_into()?) + .gas_price(Felt252Wrapper::from(max_gas_price).try_into()?); let prepared = execution.prepared().map_err(|_| SignatureError::SigningFailure)?; let res = prepared.send().await.map_err(|err| TransactionError::Broadcast(err.into()))?; diff --git a/src/providers/eth_provider/state.rs b/src/providers/eth_provider/state.rs index 293a4059d..9c9382cfb 100644 --- a/src/providers/eth_provider/state.rs +++ b/src/providers/eth_provider/state.rs @@ -66,7 +66,7 @@ where // Convert the optional Ethereum block ID to a Starknet block ID. let starknet_block_id = self.to_starknet_block_id(block_id).await?; // Get the balance of the address at the given block ID. - self.starknet_provider().balance_at(starknet_address(address), starknet_block_id).await.map_err(Into::into) + self.starknet_provider().eth_balance_at(starknet_address(address), starknet_block_id).await.map_err(Into::into) } async fn storage_at( diff --git a/src/providers/sn_provider/starknet_provider.rs b/src/providers/sn_provider/starknet_provider.rs index 02d4fffd5..3f820e5a7 100644 --- a/src/providers/sn_provider/starknet_provider.rs +++ b/src/providers/sn_provider/starknet_provider.rs @@ -2,7 +2,7 @@ use crate::{ into_via_wrapper, providers::eth_provider::{ error::ExecutionError, - starknet::{ERC20Reader, STARKNET_NATIVE_TOKEN}, + starknet::{ERC20Reader, STARKNET_ETH, STARKNET_NATIVE_TOKEN}, utils::{class_hash_not_declared, contract_not_found}, }, }; @@ -38,16 +38,48 @@ where Self { provider } } - /// Retrieves the balance of a Starknet address for a specified block. + /// Retrieves the balance of the parent token for a Starknet address. /// - /// This method interacts with the Starknet native token contract to query the balance of the given - /// address at a specific block. + /// # Arguments + /// * `token_address` - The address of the token contract + /// * `address` - The address to check the balance for + /// * `block_id` - The block ID at which to check the balance /// - /// If the contract is not deployed or the class hash is not declared, a balance of 0 is returned - /// instead of an error. - pub async fn balance_at(&self, address: Felt, block_id: BlockId) -> Result { - // Create a new `ERC20Reader` instance for the Starknet native token - let eth_contract = ERC20Reader::new(*STARKNET_NATIVE_TOKEN, &self.provider); + /// # Returns + /// * `Result` - The balance as a U256 value or an execution error + pub async fn eth_balance_at(&self, address: Felt, block_id: BlockId) -> Result { + self.balance_at(STARKNET_ETH, address, block_id).await + } + + /// Retrieves the balance of the native Starknet token for an address. + /// + /// # Arguments + /// * `token_address` - The address of the token contract + /// * `address` - The address to check the balance for + /// * `block_id` - The block ID at which to check the balance + /// + /// # Returns + /// * `Result` - The balance as a U256 value or an execution error + pub async fn strk_balance_at(&self, address: Felt, block_id: BlockId) -> Result { + self.balance_at(STARKNET_NATIVE_TOKEN, address, block_id).await + } + + /// Internal method to retrieve the balance of any ERC20 token for a Starknet address. + /// + /// # Arguments + /// * `token_address` - The address of the ERC20 token contract + /// * `address` - The address to check the balance for + /// * `block_id` - The block ID at which to check the balance + /// + /// # Returns + /// * `Result` - The balance as a U256 value or an execution error + /// + /// # Notes + /// - Returns a balance of 0 if the contract is not deployed or the class hash is not declared + /// - The balance is returned as a U256, combining low and high 128-bit components + async fn balance_at(&self, token_address: Felt, address: Felt, block_id: BlockId) -> Result { + // Create a new `ERC20Reader` instance for the Starknet parent native token + let eth_contract = ERC20Reader::new(token_address, &self.provider); // Call the `balanceOf` method on the contract for the given address and block ID, awaiting the result let span = tracing::span!(tracing::Level::INFO, "sn::balance"); @@ -55,8 +87,6 @@ where // Check if the contract was not found or the class hash not declared, // returning a default balance of 0 if true. - // The native token contract should be deployed on Kakarot, so this should not happen - // We want to avoid errors in this case and return a default balance of 0 if contract_not_found(&res) || class_hash_not_declared(&res) { return Ok(Default::default()); } diff --git a/src/test_utils/eoa.rs b/src/test_utils/eoa.rs index 0aab5ed1b..0b16b1249 100644 --- a/src/test_utils/eoa.rs +++ b/src/test_utils/eoa.rs @@ -152,7 +152,7 @@ impl KakarotEOA

{ let relayer_balance = self .eth_client .starknet_provider() - .balance_at(self.relayer.address(), BlockId::Tag(BlockTag::Latest)) + .balance_at_native(self.relayer.address(), BlockId::Tag(BlockTag::Latest)) .await?; let relayer_balance = into_via_try_wrapper!(relayer_balance)?; @@ -226,7 +226,7 @@ impl KakarotEOA

{ let relayer_balance = self .eth_client .starknet_provider() - .balance_at(self.relayer.address(), BlockId::Tag(BlockTag::Latest)) + .balance_at_native(self.relayer.address(), BlockId::Tag(BlockTag::Latest)) .await?; let relayer_balance = into_via_try_wrapper!(relayer_balance)?; diff --git a/tests/tests/eth_provider.rs b/tests/tests/eth_provider.rs index 2a150b656..351cb1d8c 100644 --- a/tests/tests/eth_provider.rs +++ b/tests/tests/eth_provider.rs @@ -746,7 +746,7 @@ async fn test_send_raw_transaction(#[future] katana_empty: Katana, _setup: ()) { // Prepare the relayer let relayer_balance = eth_client .starknet_provider() - .balance_at(katana.eoa.relayer.address(), BlockId::Tag(BlockTag::Latest)) + .balance_at_native(katana.eoa.relayer.address(), BlockId::Tag(BlockTag::Latest)) .await .expect("Failed to get relayer balance"); let relayer_balance = into_via_try_wrapper!(relayer_balance).expect("Failed to convert balance"); @@ -1018,7 +1018,7 @@ async fn test_send_raw_transaction_pre_eip_155(#[future] katana_empty: Katana, _ let relayer_balance = katana .eth_client .starknet_provider() - .balance_at(katana.eoa.relayer.address(), BlockId::Tag(BlockTag::Latest)) + .balance_at_native(katana.eoa.relayer.address(), BlockId::Tag(BlockTag::Latest)) .await .expect("Failed to get relayer balance"); let relayer_balance = into_via_try_wrapper!(relayer_balance).expect("Failed to convert balance"); @@ -1391,7 +1391,7 @@ async fn test_transaction_by_hash(#[future] katana_empty: Katana, _setup: ()) { let relayer_balance = katana_empty .eth_client .starknet_provider() - .balance_at(katana_empty.eoa.relayer.address(), BlockId::Tag(BlockTag::Latest)) + .balance_at_native(katana_empty.eoa.relayer.address(), BlockId::Tag(BlockTag::Latest)) .await .expect("Failed to get relayer balance"); let relayer_balance = into_via_try_wrapper!(relayer_balance).expect("Failed to convert balance");