diff --git a/integration/chain_orchestrator.test.ts b/integration/chain_orchestrator.test.ts index d173669a..a33ac92a 100644 --- a/integration/chain_orchestrator.test.ts +++ b/integration/chain_orchestrator.test.ts @@ -31,37 +31,6 @@ describe('Chain abstraction orchestrator', () => { let orchestration_id = ""; - it('bridging available', async () => { - // Sending USDC to Optimism, but having the USDC balance on Base chain - const data_encoded = erc20Interface.encodeFunctionData('transfer', [ - receiver_address, - amount_to_send, - ]); - - let transactionObj = { - transaction: { - from: from_address_with_funds, - to: usdc_contract_optimism, - value: "0x00", // Zero native tokens - gas: "0x00", - gasPrice: "0x00", - data: data_encoded, - nonce: "0x00", - maxFeePerGas: "0x00", - maxPriorityFeePerGas: "0x00", - chainId: chain_id_optimism, - } - } - - let resp: any = await httpClient.post( - `${baseUrl}/v1/ca/orchestrator/check?projectId=${projectId}`, - transactionObj - ) - expect(resp.status).toBe(200) - expect(typeof resp.data.requiresMultiChain).toBe('boolean') - expect(resp.data.requiresMultiChain).toBe(true) - }) - it('bridging unavailable (insufficient funds)', async () => { // Having the USDC balance on Base chain less then the amount to send const amount_to_send_in_decimals = usdc_funds_on_base + 10_000_000 @@ -86,12 +55,11 @@ describe('Chain abstraction orchestrator', () => { } let resp: any = await httpClient.post( - `${baseUrl}/v1/ca/orchestrator/check?projectId=${projectId}`, + `${baseUrl}/v1/ca/orchestrator/route?projectId=${projectId}`, transactionObj ) expect(resp.status).toBe(200) - expect(typeof resp.data.requiresMultiChain).toBe('boolean') - expect(resp.data.requiresMultiChain).toBe(false) + expect(resp.data.error).toBe("INSUFFICIENT_FUNDS") }) it('bridging unavailable (empty wallet)', async () => { @@ -119,18 +87,16 @@ describe('Chain abstraction orchestrator', () => { } let resp: any = await httpClient.post( - `${baseUrl}/v1/ca/orchestrator/check?projectId=${projectId}`, + `${baseUrl}/v1/ca/orchestrator/route?projectId=${projectId}`, transactionObj ) expect(resp.status).toBe(200) - expect(typeof resp.data.requiresMultiChain).toBe('boolean') - expect(resp.data.requiresMultiChain).toBe(false) + expect(resp.data.error).toBe("INSUFFICIENT_FUNDS") }) - it('bridging routes (no routes)', async () => { + it('bridging routes (no bridging needed)', async () => { // Sending USDC to Optimism, having the USDC balance on Base chain - // with MIN_AMOUNT_NOT_MET error expected - const amount_to_send_in_decimals = 20_000 // Less then minimum amount required + const amount_to_send_in_decimals = 20_000 // Less then bridging needed amount const data_encoded = erc20Interface.encodeFunctionData('transfer', [ receiver_address, amount_to_send_in_decimals, @@ -155,7 +121,9 @@ describe('Chain abstraction orchestrator', () => { `${baseUrl}/v1/ca/orchestrator/route?projectId=${projectId}`, transactionObj ) - expect(resp.status).toBe(400) + expect(resp.status).toBe(200) + expect(resp.data.transactions.length).toBe(0) + }) it('bridging routes (routes available)', async () => { @@ -188,8 +156,8 @@ describe('Chain abstraction orchestrator', () => { const data = resp.data expect(typeof data.orchestrationId).toBe('string') - // Expecting 3 transactions in the route - expect(data.transactions.length).toBe(3) + // Expecting 2 transactions in the route + expect(data.transactions.length).toBe(2) // First transaction expected to be the approval transaction const approvalTransaction = data.transactions[0] @@ -205,10 +173,6 @@ describe('Chain abstraction orchestrator', () => { expect(bridgingTransaction.nonce).not.toBe("0x00") expect(bridgingTransaction.gas).toBe(gas_estimate) - // Last transaction expected to be the initial one - const initialTransaction = data.transactions[2] - expect(initialTransaction.data).toBe(transactionObj.transaction.data) - // Check the metadata fundingFrom const fundingFrom = data.metadata.fundingFrom[0] expect(fundingFrom.chainId).toBe(chain_id_base) @@ -226,6 +190,7 @@ describe('Chain abstraction orchestrator', () => { expect(resp.status).toBe(200) const data = resp.data expect(typeof data.status).toBe('string') - expect(data.status).toBe('pending') + expect(data.status).toBe('PENDING') + expect(data.checkIn).toBe(3000) }) }) diff --git a/src/error.rs b/src/error.rs index 6ef99f04..0837992d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -239,15 +239,6 @@ pub enum RpcError { #[error("ABI decoding error: {0}")] AbiDecodingError(String), - #[error("No bridging needed")] - NoBridgingNeeded, - - #[error("No bridging available")] - NoBridgingAvailable, - - #[error("No routes available for the bridging")] - NoBridgingRoutesAvailable, - #[error("Orchestration ID is not found: {0}")] OrchestrationIdNotFound(String), } @@ -634,30 +625,6 @@ impl IntoResponse for RpcError { )), ) .into_response(), - Self::NoBridgingNeeded => ( - StatusCode::BAD_REQUEST, - Json(new_error_response( - "".to_string(), - "No bridging needed".to_string(), - )), - ) - .into_response(), - Self::NoBridgingAvailable => ( - StatusCode::BAD_REQUEST, - Json(new_error_response( - "".to_string(), - "No bridging available".to_string(), - )), - ) - .into_response(), - Self::NoBridgingRoutesAvailable => ( - StatusCode::BAD_REQUEST, - Json(new_error_response( - "".to_string(), - "No bridging routes available".to_string(), - )), - ) - .into_response(), Self::OrchestrationIdNotFound(id) => ( StatusCode::BAD_REQUEST, Json(new_error_response( diff --git a/src/handlers/chain_agnostic/check.rs b/src/handlers/chain_agnostic/check.rs deleted file mode 100644 index fb905557..00000000 --- a/src/handlers/chain_agnostic/check.rs +++ /dev/null @@ -1,172 +0,0 @@ -use { - super::{ - super::HANDLER_TASK_METRICS, check_bridging_for_erc20_transfer, BRIDGING_AMOUNT_MULTIPLIER, - }, - crate::{ - analytics::MessageSource, - error::RpcError, - state::AppState, - utils::crypto::{ - convert_alloy_address_to_h160, decode_erc20_function_type, decode_erc20_transfer_data, - get_balance, get_erc20_balance, Erc20FunctionType, - }, - }, - alloy::primitives::{Address, U256}, - axum::{ - extract::{Query, State}, - response::{IntoResponse, Response}, - Json, - }, - serde::{Deserialize, Serialize}, - std::{str::FromStr, sync::Arc}, - tracing::{debug, error}, - wc::future::FutureExt, -}; - -#[derive(Debug, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct QueryParams { - pub project_id: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CheckTransactionRequest { - transaction: Transaction, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Transaction { - from: String, - to: String, - value: String, - gas: String, - gas_price: String, - data: String, - nonce: String, - max_fee_per_gas: String, - max_priority_fee_per_gas: String, - chain_id: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RequiresMultiChainResponse { - requires_multi_chain: bool, -} - -pub async fn handler( - state: State>, - query_params: Query, - Json(request_payload): Json, -) -> Result { - handler_internal(state, query_params, request_payload) - .with_metrics(HANDLER_TASK_METRICS.with_name("ca_check")) - .await -} - -#[tracing::instrument(skip(state), level = "debug")] -async fn handler_internal( - state: State>, - Query(query_params): Query, - request_payload: CheckTransactionRequest, -) -> Result { - state - .validate_project_access_and_quota(&query_params.project_id.clone()) - .await?; - - let from_address = Address::from_str(&request_payload.transaction.from) - .map_err(|_| RpcError::InvalidAddress)?; - let to_address = - Address::from_str(&request_payload.transaction.to).map_err(|_| RpcError::InvalidAddress)?; - - // Check the native token balance - let native_token_balance = get_balance( - &request_payload.transaction.chain_id, - convert_alloy_address_to_h160(from_address), - &query_params.project_id.clone(), - MessageSource::ChainAgnosticCheck, - ) - .await?; - let transfer_value_string = request_payload.transaction.value; - let transfer_value = U256::from_str(&transfer_value_string) - .map_err(|_| RpcError::InvalidValue(transfer_value_string))?; - - // check if the transaction value is non zero so it's a native token transfer - if transfer_value > U256::ZERO { - debug!( - "The transaction is a native token transfer with value: {:?}", - transfer_value - ); - // If the native token balance is greater than the transfer value, we don't need multi-chain bridging - if U256::from_be_bytes(native_token_balance.into()) > transfer_value { - return Ok(Json(RequiresMultiChainResponse { - requires_multi_chain: false, - }) - .into_response()); - } - } - - // Check if the ERC20 function is the `transfer` function - let transaction_data = hex::decode(request_payload.transaction.data.trim_start_matches("0x")) - .map_err(|e| RpcError::WrongHexFormat(e.to_string()))?; - if decode_erc20_function_type(&transaction_data)? != Erc20FunctionType::Transfer { - error!("The transaction data is not a transfer function"); - return Ok(Json(RequiresMultiChainResponse { - requires_multi_chain: false, - }) - .into_response()); - } - - // Decode the ERC20 transfer function data - let (_erc20_receiver, erc20_transfer_value) = decode_erc20_transfer_data(&transaction_data)?; - - // Get the current balance of the ERC20 token and check if it's enough for the transfer - // without bridging or calculate the top-up value - let erc20_balance = get_erc20_balance( - &request_payload.transaction.chain_id, - convert_alloy_address_to_h160(to_address), - convert_alloy_address_to_h160(from_address), - &query_params.project_id, - MessageSource::ChainAgnosticCheck, - ) - .await?; - let erc20_balance = U256::from_be_bytes(erc20_balance.into()); - if erc20_balance >= erc20_transfer_value { - // The balance is sufficient for the transfer no need for bridging - return Ok(Json(RequiresMultiChainResponse { - requires_multi_chain: false, - }) - .into_response()); - } - let erc20_topup_value = erc20_transfer_value - erc20_balance; - // Multiply the topup value by the bridging percent multiplier - let erc20_topup_value = erc20_topup_value - + (erc20_topup_value * U256::from(BRIDGING_AMOUNT_MULTIPLIER)) / U256::from(100); - - // Check for possible bridging by iterating over supported assets - if let Some((bridge_chain_id, _, bridge_contract)) = - check_bridging_for_erc20_transfer(query_params.project_id, erc20_topup_value, from_address) - .await? - { - // Skip bridging if that's the same chainId and contract address - if bridge_chain_id == request_payload.transaction.chain_id && bridge_contract == to_address - { - return Ok(Json(RequiresMultiChainResponse { - requires_multi_chain: false, - }) - .into_response()); - } - - return Ok(Json(RequiresMultiChainResponse { - requires_multi_chain: true, - }) - .into_response()); - } - - // No sufficient balances found for the transfer or bridging - Ok(Json(RequiresMultiChainResponse { - requires_multi_chain: false, - }) - .into_response()) -} diff --git a/src/handlers/chain_agnostic/mod.rs b/src/handlers/chain_agnostic/mod.rs index 6821556c..9e660f69 100644 --- a/src/handlers/chain_agnostic/mod.rs +++ b/src/handlers/chain_agnostic/mod.rs @@ -7,7 +7,6 @@ use { std::{collections::HashMap, str::FromStr}, }; -pub mod check; pub mod route; pub mod status; @@ -36,11 +35,12 @@ pub struct StorageBridgingItem { contract: Address, amount_expected: U256, status: BridgingStatus, + error_reason: Option, } /// Bridging status #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "UPPERCASE")] pub enum BridgingStatus { Pending, Completed, diff --git a/src/handlers/chain_agnostic/route.rs b/src/handlers/chain_agnostic/route.rs index b79e2f64..7a2ee69c 100644 --- a/src/handlers/chain_agnostic/route.rs +++ b/src/handlers/chain_agnostic/route.rs @@ -25,7 +25,7 @@ use { sync::Arc, time::{SystemTime, UNIX_EPOCH}, }, - tracing::{debug, error}, + tracing::debug, uuid::Uuid, wc::future::FutureExt, }; @@ -65,9 +65,9 @@ pub struct RequiresMultiChainResponse { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RouteResponse { - orchestration_id: String, + orchestration_id: Option, transactions: Vec, - metadata: Metadata, + metadata: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -85,6 +85,30 @@ pub struct FundingMetadata { amount: String, } +/// Bridging check error response that should be returned as a normal HTTP 200 response +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ErrorResponse { + error: BridgingError, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum BridgingError { + NoRoutesAvailable, + InsufficientFunds, + InsufficientGasFunds, +} + +const NO_BRIDING_NEEDED_RESPONSE: Json = Json(RouteResponse { + transactions: vec![], + orchestration_id: None, + metadata: None, +}); + +// Default gas estimate +// Using default with 6x increase +const DEFAULT_GAS: i64 = 0x029a6b * 0x6; + pub async fn handler( state: State>, query_params: Query, @@ -107,37 +131,25 @@ async fn handler_internal( let from_address = request_payload.transaction.from; let to_address = request_payload.transaction.to; - - // Check the native token balance - let native_token_balance = get_balance( - &request_payload.transaction.chain_id, - convert_alloy_address_to_h160(from_address), - &query_params.project_id.clone(), - MessageSource::ChainAgnosticCheck, - ) - .await?; let transfer_value_string = request_payload.transaction.value.clone(); let transfer_value = U256::from_str(&transfer_value_string) .map_err(|_| RpcError::InvalidValue(transfer_value_string))?; - // check if the transaction value is non zero so it's a native token transfer + // Check if the transaction value is non zero it's a native token transfer if transfer_value > U256::ZERO { debug!( "The transaction is a native token transfer with value: {:?}", transfer_value ); - // If the native token balance is greater than the transfer value, we don't need multi-chain bridging - if U256::from_be_bytes(native_token_balance.into()) > transfer_value { - return Err(RpcError::NoBridgingNeeded); - } + return Ok(NO_BRIDING_NEEDED_RESPONSE.into_response()); } // Check if the ERC20 function is the `transfer` function let transaction_data = hex::decode(request_payload.transaction.data.trim_start_matches("0x")) .map_err(|e| RpcError::WrongHexFormat(e.to_string()))?; if decode_erc20_function_type(&transaction_data)? != Erc20FunctionType::Transfer { - error!("The transaction data is not a transfer function"); - return Err(RpcError::NoBridgingNeeded); + debug!("The transaction data is not a transfer function"); + return Ok(NO_BRIDING_NEEDED_RESPONSE.into_response()); } // Decode the ERC20 transfer function data @@ -155,179 +167,194 @@ async fn handler_internal( .await?; let erc20_balance = U256::from_be_bytes(erc20_balance.into()); if erc20_balance >= erc20_transfer_value { - return Err(RpcError::NoBridgingNeeded); + return Ok(NO_BRIDING_NEEDED_RESPONSE.into_response()); } - let erc20_topup_value = erc20_transfer_value - erc20_balance; + // Multiply the topup value by the bridging percent multiplier + let erc20_topup_value = erc20_transfer_value - erc20_balance; let erc20_topup_value = erc20_topup_value + (erc20_topup_value * U256::from(BRIDGING_AMOUNT_MULTIPLIER)) / U256::from(100); - // Check for possible bridging by iterating over supported assets - if let Some((bridge_chain_id, bridge_token_symbol, bridge_contract)) = + // Check for possible bridging funds by iterating over supported assets + // or return an insufficient funds error + let Some((bridge_chain_id, bridge_token_symbol, bridge_contract)) = check_bridging_for_erc20_transfer( query_params.project_id.clone(), erc20_topup_value, from_address, ) .await? - { - // Skip bridging if that's the same chainId and contract address - if bridge_chain_id == request_payload.transaction.chain_id && bridge_contract == to_address - { - return Err(RpcError::NoBridgingNeeded); - } + else { + return Ok(Json(ErrorResponse { + error: BridgingError::InsufficientFunds, + }) + .into_response()); + }; - // Get Quotes for the bridging - let quotes = state - .providers - .chain_orchestrator_provider - .get_bridging_quotes( - bridge_chain_id.clone(), - bridge_contract, - request_payload.transaction.chain_id.clone(), - to_address, - erc20_topup_value, - from_address, - ) - .await?; + // Skip bridging if that's the same chainId and contract address + if bridge_chain_id == request_payload.transaction.chain_id && bridge_contract == to_address { + return Ok(NO_BRIDING_NEEDED_RESPONSE.into_response()); + } - // Build bridging transaction - let mut routes = Vec::new(); - let best_route = quotes.first().ok_or(RpcError::NoBridgingRoutesAvailable)?; - let bridge_tx = state - .providers - .chain_orchestrator_provider - .build_bridging_tx(best_route.clone()) - .await?; + // Check the native token balance for the bridging chainId for gas fees paying + let native_token_balance = get_balance( + &bridge_chain_id, + convert_alloy_address_to_h160(from_address), + &query_params.project_id.clone(), + MessageSource::ChainAgnosticCheck, + ) + .await?; + if U256::from_be_bytes(native_token_balance.into()) == U256::ZERO { + return Ok(Json(ErrorResponse { + error: BridgingError::InsufficientGasFunds, + }) + .into_response()); + } - // Getting the current nonce for the address - let mut current_nonce = get_nonce( - format!("eip155:{}", bridge_tx.chain_id).as_str(), + // Get Quotes for the bridging + let quotes = state + .providers + .chain_orchestrator_provider + .get_bridging_quotes( + bridge_chain_id.clone(), + bridge_contract, + request_payload.transaction.chain_id.clone(), + to_address, + erc20_topup_value, from_address, - &query_params.project_id.clone(), - MessageSource::ChainAgnosticCheck, ) .await?; - // Getting the current gas price - let gas_price = get_gas_price( - &bridge_chain_id.clone(), - &query_params.project_id.clone(), - MessageSource::ChainAgnosticCheck, - ) + // Build bridging transaction + let mut routes = Vec::new(); + let Some(best_route) = quotes.first() else { + return Ok(Json(ErrorResponse { + error: BridgingError::NoRoutesAvailable, + }) + .into_response()); + }; + let bridge_tx = state + .providers + .chain_orchestrator_provider + .build_bridging_tx(best_route.clone()) .await?; - // Default gas estimate - // Using default with 6x increase - // Todo: Implement gas estimation using `eth_estimateGas` - let gas = 0x029a6b * 0x6; + // Getting the current nonce for the address + let mut current_nonce = get_nonce( + format!("eip155:{}", bridge_tx.chain_id).as_str(), + from_address, + &query_params.project_id.clone(), + MessageSource::ChainAgnosticCheck, + ) + .await?; + + // Getting the current gas price + let gas_price = get_gas_price( + &bridge_chain_id.clone(), + &query_params.project_id.clone(), + MessageSource::ChainAgnosticCheck, + ) + .await?; + + // TODO: Implement gas estimation using `eth_estimateGas` for each transaction - // Check for the allowance - if let Some(approval_data) = bridge_tx.approval_data { - let allowance = state + // Check for the allowance + if let Some(approval_data) = bridge_tx.approval_data { + let allowance = state + .providers + .chain_orchestrator_provider + .check_allowance( + format!("eip155:{}", bridge_tx.chain_id), + approval_data.owner, + approval_data.allowance_target, + approval_data.approval_token_address, + ) + .await?; + + // Check if the approval transaction injection is needed + if approval_data.minimum_approval_amount >= allowance { + let approval_tx = state .providers .chain_orchestrator_provider - .check_allowance( + .build_approval_tx( format!("eip155:{}", bridge_tx.chain_id), approval_data.owner, approval_data.allowance_target, approval_data.approval_token_address, + erc20_topup_value, ) .await?; - // Check if the approval transaction injection is needed - if approval_data.minimum_approval_amount >= allowance { - let approval_tx = state - .providers - .chain_orchestrator_provider - .build_approval_tx( - format!("eip155:{}", bridge_tx.chain_id), - approval_data.owner, - approval_data.allowance_target, - approval_data.approval_token_address, - erc20_topup_value, - ) - .await?; - - routes.push(Transaction { - from: approval_tx.from, - to: approval_tx.to, - value: "0x00".to_string(), - gas_price: format!("0x{:x}", gas_price), - gas: format!("0x{:x}", gas), - data: approval_tx.data, - nonce: format!("0x{:x}", current_nonce), - max_fee_per_gas: request_payload.transaction.max_fee_per_gas.clone(), - max_priority_fee_per_gas: request_payload - .transaction - .max_priority_fee_per_gas - .clone(), - chain_id: format!("eip155:{}", bridge_tx.chain_id), - }); - current_nonce += 1; - } + routes.push(Transaction { + from: approval_tx.from, + to: approval_tx.to, + value: "0x00".to_string(), + gas_price: format!("0x{:x}", gas_price), + gas: format!("0x{:x}", DEFAULT_GAS), + data: approval_tx.data, + nonce: format!("0x{:x}", current_nonce), + max_fee_per_gas: request_payload.transaction.max_fee_per_gas.clone(), + max_priority_fee_per_gas: request_payload + .transaction + .max_priority_fee_per_gas + .clone(), + chain_id: format!("eip155:{}", bridge_tx.chain_id), + }); + current_nonce += 1; } - - // Push bridging transaction - routes.push(Transaction { - from: from_address, - to: bridge_tx.tx_target, - value: bridge_tx.value, - gas_price: format!("0x{:x}", gas_price), - gas: format!("0x{:x}", gas), - data: bridge_tx.tx_data, - nonce: format!("0x{:x}", current_nonce), - max_fee_per_gas: request_payload.transaction.max_fee_per_gas.clone(), - max_priority_fee_per_gas: request_payload.transaction.max_priority_fee_per_gas.clone(), - chain_id: format!("eip155:{}", bridge_tx.chain_id), - }); - current_nonce += 1; - - // Push initial transaction last after all bridging transactions - routes.push(Transaction { - nonce: format!("0x{:x}", current_nonce), - ..request_payload.transaction.clone() - }); - let orchestration_id = Uuid::new_v4().to_string(); - - // Save the bridging transaction to the IRN - let bridging_status_item = StorageBridgingItem { - created_at: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as usize, - chain_id: request_payload.transaction.chain_id, - wallet: from_address, - contract: to_address, - amount_expected: erc20_transfer_value, // The total transfer amount expected - status: BridgingStatus::Pending, - }; - let irn_client = state.irn.as_ref().ok_or(RpcError::IrnNotConfigured)?; - let irn_call_start = SystemTime::now(); - irn_client - .set( - orchestration_id.clone(), - serde_json::to_string(&bridging_status_item)?.into(), - ) - .await?; - state - .metrics - .add_irn_latency(irn_call_start, OperationType::Set); - - return Ok(Json(RouteResponse { - orchestration_id, - transactions: routes, - metadata: Metadata { - funding_from: vec![FundingMetadata { - chain_id: bridge_chain_id, - token_contract: bridge_contract, - symbol: bridge_token_symbol, - amount: format!("0x{:x}", erc20_topup_value), - }], - }, - }) - .into_response()); } - Err(RpcError::NoBridgingNeeded) + // Push bridging transaction + routes.push(Transaction { + from: from_address, + to: bridge_tx.tx_target, + value: bridge_tx.value, + gas_price: format!("0x{:x}", gas_price), + gas: format!("0x{:x}", DEFAULT_GAS), + data: bridge_tx.tx_data, + nonce: format!("0x{:x}", current_nonce), + max_fee_per_gas: request_payload.transaction.max_fee_per_gas.clone(), + max_priority_fee_per_gas: request_payload.transaction.max_priority_fee_per_gas.clone(), + chain_id: format!("eip155:{}", bridge_tx.chain_id), + }); + + // Save the bridging transaction to the IRN + let orchestration_id = Uuid::new_v4().to_string(); + let bridging_status_item = StorageBridgingItem { + created_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as usize, + chain_id: request_payload.transaction.chain_id, + wallet: from_address, + contract: to_address, + amount_expected: erc20_transfer_value, // The total transfer amount expected + status: BridgingStatus::Pending, + error_reason: None, + }; + let irn_client = state.irn.as_ref().ok_or(RpcError::IrnNotConfigured)?; + let irn_call_start = SystemTime::now(); + irn_client + .set( + orchestration_id.clone(), + serde_json::to_string(&bridging_status_item)?.into(), + ) + .await?; + state + .metrics + .add_irn_latency(irn_call_start, OperationType::Set); + + return Ok(Json(RouteResponse { + orchestration_id: Some(orchestration_id), + transactions: routes, + metadata: Some(Metadata { + funding_from: vec![FundingMetadata { + chain_id: bridge_chain_id, + token_contract: bridge_contract, + symbol: bridge_token_symbol, + amount: format!("0x{:x}", erc20_topup_value), + }], + }), + }) + .into_response()); } diff --git a/src/handlers/chain_agnostic/status.rs b/src/handlers/chain_agnostic/status.rs index a5397428..8f9d0717 100644 --- a/src/handlers/chain_agnostic/status.rs +++ b/src/handlers/chain_agnostic/status.rs @@ -16,6 +16,9 @@ use { wc::future::FutureExt, }; +/// The status polling interval in ms for the client +const STATUS_POLLING_INTERVAL: usize = 3000; // 3 seconds + #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct QueryParams { @@ -28,6 +31,9 @@ pub struct QueryParams { pub struct StatusResponse { status: BridgingStatus, created_at: usize, + error_reason: Option, + /// Polling interval in ms for the client + check_in: Option, } pub async fn handler( @@ -70,6 +76,8 @@ async fn handler_internal( return Ok(Json(StatusResponse { status: bridging_status_item.status, created_at: bridging_status_item.created_at, + error_reason: bridging_status_item.error_reason, + check_in: None, }) .into_response()); } @@ -89,6 +97,8 @@ async fn handler_internal( return Ok(Json(StatusResponse { status: bridging_status_item.status, created_at: bridging_status_item.created_at, + check_in: Some(STATUS_POLLING_INTERVAL), + error_reason: None, }) .into_response()); } else { @@ -109,6 +119,8 @@ async fn handler_internal( return Ok(Json(StatusResponse { status: bridging_status_item.status, created_at: bridging_status_item.created_at, + check_in: Some(STATUS_POLLING_INTERVAL), + error_reason: None, }) .into_response()); } diff --git a/src/lib.rs b/src/lib.rs index 6a5d86ae..4260c1c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -351,7 +351,6 @@ pub async fn bootstrap(config: Config) -> RpcResult<()> { // Wallet .route("/v1/wallet", post(handlers::wallet::handler::handler)) // Chain agnostic orchestration - .route("/v1/ca/orchestrator/check", post(handlers::chain_agnostic::check::handler)) .route("/v1/ca/orchestrator/route", post(handlers::chain_agnostic::route::handler)) .route("/v1/ca/orchestrator/status", get(handlers::chain_agnostic::status::handler)) // Health diff --git a/src/providers/bungee.rs b/src/providers/bungee.rs index 5474c906..88496f03 100644 --- a/src/providers/bungee.rs +++ b/src/providers/bungee.rs @@ -157,7 +157,7 @@ impl ChainOrchestrationProvider for BungeeProvider { "No bridging routes available from Bungee provider. Bridges errors: {:?}", body.result.bridge_route_errors ); - return Err(RpcError::NoBridgingRoutesAvailable); + return Ok(vec![]); } Ok(body.result.routes)