rrelayer_core 0.7.0

Core types and functionality for rrelayer - a powerful blockchain transaction relay service
Documentation
use std::error::Error;
use thiserror::Error;

use crate::shared::{internal_server_error, not_found, HttpError};
use crate::{
    gas::GasPrice,
    network::ChainId,
    postgres::{PostgresClient, PostgresError},
    provider::EvmProvider,
    relayer::types::{Relayer, RelayerId},
};

#[derive(Error, Debug)]
pub enum CreateRelayerError {
    #[error("Relayer could not be saved in DB - name: {0}, chainId: {1}: {0}")]
    CouldNotSaveRelayerDb(String, ChainId, PostgresError),

    #[error("Relayer could not update DB - name: {0}, chainId: {1}: {2}")]
    CouldNotUpdateRelayerInfoDb(String, ChainId, PostgresError),

    #[error("Relayer did not return init information - name: {0}, chainId: {1}")]
    NoSaveRelayerInitInfoReturnedDb(String, ChainId),

    #[error("Wallet error - name: {0}, chainId: {1}: {0}")]
    WalletError(String, ChainId, Box<dyn Error + Send + Sync>),

    #[error("Relayer {0} not found for cloning")]
    RelayerNotFound(RelayerId),

    #[error("Cannot clone private key relayer '{0}' - private key relayers are automatically imported on all networks")]
    CannotClonePrivateKeyRelayer(String),

    #[error("Cannot clone relayer '{0}' - provider type does not support cloning")]
    CannotCloneProviderRelayer(String),
}

impl From<CreateRelayerError> for HttpError {
    fn from(value: CreateRelayerError) -> Self {
        match value {
            CreateRelayerError::RelayerNotFound(_) => {
                not_found("Could not find relayer".to_string())
            }
            CreateRelayerError::CannotClonePrivateKeyRelayer(_) => {
                crate::shared::bad_request(value.to_string())
            }
            CreateRelayerError::CannotCloneProviderRelayer(_) => {
                crate::shared::bad_request(value.to_string())
            }
            _ => internal_server_error(Some(value.to_string())),
        }
    }
}

impl From<PostgresError> for CreateRelayerError {
    fn from(value: PostgresError) -> Self {
        CreateRelayerError::CouldNotSaveRelayerDb("Unknown".to_string(), ChainId::new(0), value)
    }
}

pub enum CreateRelayerMode {
    Clone(RelayerId),
    Create,
    PrivateKeyImport(i32),
}

impl PostgresClient {
    pub async fn create_relayer(
        &self,
        name: &str,
        chain_id: &ChainId,
        evm_provider: &EvmProvider,
        mode: CreateRelayerMode,
    ) -> Result<Relayer, CreateRelayerError> {
        let new_relayer_id = RelayerId::new();

        match &mode {
            CreateRelayerMode::Clone(clone_relayer_id) => {
                let source_relayer = self
                    .get_relayer(clone_relayer_id)
                    .await
                    .map_err(|e| {
                        CreateRelayerError::CouldNotSaveRelayerDb(name.to_string(), *chain_id, e)
                    })?
                    .ok_or_else(|| CreateRelayerError::RelayerNotFound(*clone_relayer_id))?;

                // Prevent cloning private key relayers since they are auto-imported on all networks
                if source_relayer.is_private_key {
                    return Err(CreateRelayerError::CannotClonePrivateKeyRelayer(
                        source_relayer.name.clone(),
                    ));
                }

                // Prevent cloning providers that don't support cloning (e.g., Fireblocks)
                if !evm_provider.can_clone() {
                    return Err(CreateRelayerError::CannotCloneProviderRelayer(
                        source_relayer.name.clone(),
                    ));
                }

                let wallet_index = source_relayer.wallet_index;
                let address = evm_provider
                    .create_wallet(source_relayer.wallet_index_type().index())
                    .await
                    .map_err(|e| {
                        CreateRelayerError::WalletError(name.to_string(), *chain_id, Box::new(e))
                    })?;

                self.execute(
                    "INSERT INTO relayer.record (id, name, chain_id, wallet_index, max_gas_price_cap, paused, eip_1559_enabled, address, is_private_key)
                     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
                    &[
                        &new_relayer_id,
                        &name,
                        chain_id,
                        &wallet_index,
                        &source_relayer.max_gas_price,
                        &source_relayer.paused,
                        &source_relayer.eip_1559_enabled,
                        &address,
                        &source_relayer.is_private_key,
                    ],
                )
                .await
                .map_err(|e| {
                    CreateRelayerError::CouldNotSaveRelayerDb(name.to_string(), *chain_id, e)
                })?;
            }
            CreateRelayerMode::Create => {
                let evm_provider_clone = evm_provider.clone();
                let new_relayer_id_val = new_relayer_id;
                let name_val = name.to_string();
                let chain_id_val = *chain_id;

                self.with_transaction(move |tx| {
                    Box::pin(async move {
                        let query = "
                            WITH new_wallet_index AS (
                                SELECT COALESCE(MAX(wallet_index), -1) + 1 AS wallet_index
                                FROM relayer.record
                                WHERE chain_id = $3
                            )
                            INSERT INTO relayer.record (id, name, chain_id, wallet_index, is_private_key)
                            SELECT $1, $2, $3, wallet_index, false
                            FROM new_wallet_index
                            RETURNING wallet_index";

                        let rows = tx.query(query, &[&new_relayer_id_val, &name_val, &chain_id_val]).await.map_err(PostgresError::PgError)?;

                        let wallet_index: i32 = rows.first()
                            .map(|row| row.get("wallet_index"))
                            .unwrap_or_else(|| panic!("No wallet index returned"));

                        let address = evm_provider_clone.create_wallet(wallet_index as u32).await
                            .unwrap_or_else(|e| panic!("Wallet creation failed: {}", e));

                        tx.execute(
                            "UPDATE relayer.record SET address = $1 WHERE chain_id = $2 AND wallet_index = $3",
                            &[&address, &chain_id_val, &wallet_index],
                        )
                        .await.map_err(PostgresError::PgError)?;

                        Ok(())
                    })
                })
                .await
                .map_err(|e| CreateRelayerError::CouldNotSaveRelayerDb(name.to_string(), *chain_id, e))?
            }
            CreateRelayerMode::PrivateKeyImport(wallet_index) => {
                // Convert negative wallet index to positive private key index for address lookup
                let private_key_index = (-wallet_index - 1) as u32;
                let address = evm_provider.get_address(private_key_index).await.map_err(|e| {
                    CreateRelayerError::WalletError(name.to_string(), *chain_id, Box::new(e))
                })?;

                self.execute(
                    "INSERT INTO relayer.record (id, name, chain_id, wallet_index, address, is_private_key)
                     VALUES ($1, $2, $3, $4, $5, $6)",
                    &[&new_relayer_id, &name, chain_id, wallet_index, &address, &true],
                )
                .await
                .map_err(|e| CreateRelayerError::CouldNotSaveRelayerDb(name.to_string(), *chain_id, e))?;
            }
        };

        let relayer = self.get_relayer(&new_relayer_id).await.map_err(|e| {
            CreateRelayerError::CouldNotSaveRelayerDb(name.to_string(), *chain_id, e)
        })?;

        match relayer {
            Some(relayer) => Ok(relayer),
            None => Err(CreateRelayerError::NoSaveRelayerInitInfoReturnedDb(
                name.to_string(),
                *chain_id,
            )),
        }
    }

    pub async fn delete_relayer(&self, relayer_id: &RelayerId) -> Result<(), PostgresError> {
        let _ = self
            .execute(
                "
                UPDATE relayer.record
                SET deleted = TRUE
                WHERE id = $1
                ",
                &[relayer_id],
            )
            .await?;

        Ok(())
    }

    pub async fn pause_relayer(&self, relayer_id: &RelayerId) -> Result<(), PostgresError> {
        let _ = self
            .execute(
                "
                UPDATE relayer.record
                SET paused = TRUE
                WHERE id = $1
                ",
                &[relayer_id],
            )
            .await?;

        Ok(())
    }

    pub async fn unpause_relayer(&self, relayer_id: &RelayerId) -> Result<(), PostgresError> {
        let _ = self
            .execute(
                "
                UPDATE relayer.record
                SET paused = FALSE
                WHERE id = $1
                ",
                &[relayer_id],
            )
            .await?;

        Ok(())
    }

    pub async fn update_relayer_max_gas_price(
        &self,
        relayer_id: &RelayerId,
        cap: Option<GasPrice>,
    ) -> Result<(), PostgresError> {
        let _ = self
            .execute(
                "
                UPDATE relayer.record
                SET max_gas_price_cap = $1
                WHERE id = $2
                ",
                &[&cap, relayer_id],
            )
            .await?;

        Ok(())
    }

    pub async fn update_relayer_eip_1559_status(
        &self,
        relayer_id: &RelayerId,
        enable: &bool,
    ) -> Result<(), PostgresError> {
        let _ = self
            .execute(
                "
                UPDATE relayer.record
                SET eip_1559_enabled = $1
                WHERE id = $2
                ",
                &[enable, relayer_id],
            )
            .await?;

        Ok(())
    }
}