From a1fd0e24f170622ae2ebfa7f94ac621635302a05 Mon Sep 17 00:00:00 2001 From: Nick Pavlov Date: Wed, 17 Apr 2024 15:37:05 +0300 Subject: [PATCH 1/5] fix(chain-connector): make connector generic --- Cargo.lock | 1 + crates/chain-connector/Cargo.toml | 1 + crates/chain-connector/src/connector.rs | 379 +++++++++++++----------- crates/chain-connector/src/lib.rs | 1 + crates/chain-data/src/utils.rs | 1 + crates/chain-listener/src/listener.rs | 8 +- crates/core-manager/src/strict.rs | 11 + nox/src/node.rs | 6 +- 8 files changed, 227 insertions(+), 181 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd937c65ed..6d4134ba46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1304,6 +1304,7 @@ version = "0.1.0" dependencies = [ "alloy-primitives", "alloy-sol-types", + "async-trait", "ccp-shared", "chain-data", "clarity", diff --git a/crates/chain-connector/Cargo.toml b/crates/chain-connector/Cargo.toml index 0e18882ab9..f7b17da68c 100644 --- a/crates/chain-connector/Cargo.toml +++ b/crates/chain-connector/Cargo.toml @@ -28,6 +28,7 @@ alloy-sol-types = { workspace = true } alloy-primitives = { workspace = true } const-hex = { workspace = true } serde = { workspace = true } +async-trait = { workspace = true } [dev-dependencies] mockito = { workspace = true } diff --git a/crates/chain-connector/src/connector.rs b/crates/chain-connector/src/connector.rs index 42045b3d65..fbc807a1d9 100644 --- a/crates/chain-connector/src/connector.rs +++ b/crates/chain-connector/src/connector.rs @@ -12,6 +12,7 @@ use ccp_shared::types::{Difficulty, GlobalNonce, CUID}; use clarity::{Transaction, Uint256}; use eyre::eyre; use futures::FutureExt; +use jsonrpsee::core::async_trait; use jsonrpsee::core::client::{BatchResponse, ClientT}; use jsonrpsee::core::params::{ArrayParams, BatchRequestBuilder}; use jsonrpsee::http_client::HttpClientBuilder; @@ -36,7 +37,42 @@ use crate::Offer::{ComputePeer, ComputeUnit}; const BASE_FEE_PREMIUM_DIVIDER: U256 = uint!(8_U256); -pub struct ChainConnector { +#[async_trait] +pub trait ChainConnector: Send + Sync { + async fn get_current_commitment_id(&self) -> Result, ConnectorError>; + + async fn get_cc_init_params(&self) -> eyre::Result; //TODO: make error type + + async fn get_compute_units(&self) -> Result, ConnectorError>; + + async fn get_commitment_status( + &self, + commitment_id: CommitmentId, + ) -> Result; + + async fn get_global_nonce(&self) -> Result; + + async fn submit_proof(&self, proof: CCProof) -> Result; + + async fn get_deal_statuses( + &self, + deal_ids: Vec, + ) -> Result>, ConnectorError>; + + async fn exit_deal(&self, cu_id: &CUID) -> Result; + + async fn get_tx_statuses( + &self, + tx_hashes: Vec, + ) -> Result, ConnectorError>>, ConnectorError>; + + async fn get_tx_receipts( + &self, + tx_hashes: Vec, + ) -> Result>, ConnectorError>; +} + +pub struct HttpChainConnector { client: Arc, config: ChainConfig, tx_nonce_mutex: Arc>, @@ -53,7 +89,7 @@ pub struct CCInitParams { pub max_proofs_per_epoch: U256, } -impl ChainConnector { +impl HttpChainConnector { pub fn new( config: ChainConfig, host_id: PeerId, @@ -221,90 +257,70 @@ impl ChainConnector { Ok(resp) } - pub async fn get_current_commitment_id(&self) -> Result, ConnectorError> { - let peer_id = peer_id_to_bytes(self.host_id); - let data: String = Offer::getComputePeerCall { - peerId: peer_id.into(), - } - .abi_encode() - .encode_hex(); - let resp: String = process_response( - self.client - .request( - "eth_call", - rpc_params![ - json!({ - "data": data, - "to": self.config.market_contract_address, - }), - "latest" - ], - ) - .await, - )?; - let compute_peer = ::abi_decode(&decode_hex(&resp)?, true)?; - Ok(CommitmentId::new(compute_peer.commitmentId.0)) - } - - pub async fn get_commitment_status( - &self, - commitment_id: CommitmentId, - ) -> Result { - let data: String = Capacity::getStatusCall { - commitmentId: commitment_id.0.into(), - } - .abi_encode() - .encode_hex(); + fn difficulty_params(&self) -> ArrayParams { + let data: String = Core::difficultyCall {}.abi_encode().encode_hex(); - let resp: String = process_response( - self.client - .request( - "eth_call", - rpc_params![ - json!({ - "data": data, - "to": self.config.cc_contract_address, - }), - "latest" - ], - ) - .await, - )?; - Ok(::abi_decode( - &decode_hex(&resp)?, - true, - )?) + rpc_params![ + json!({"data": data, "to": self.config.core_contract_address}), + "latest" + ] } - pub async fn get_global_nonce(&self) -> Result { - let resp: String = process_response( - self.client - .request("eth_call", self.global_nonce_params()) - .await, - )?; - - let bytes: FixedBytes<32> = FixedBytes::from_str(&resp)?; - Ok(GlobalNonce::new(bytes.0)) + fn init_timestamp_params(&self) -> ArrayParams { + let data: String = Core::initTimestampCall {}.abi_encode().encode_hex(); + rpc_params![ + json!({"data": data, "to": self.config.core_contract_address}), + "latest" + ] + } + fn global_nonce_params(&self) -> ArrayParams { + let data: String = Capacity::getGlobalNonceCall {}.abi_encode().encode_hex(); + rpc_params![ + json!({"data": data, "to": self.config.cc_contract_address}), + "latest" + ] + } + fn current_epoch_params(&self) -> ArrayParams { + let data: String = Core::currentEpochCall {}.abi_encode().encode_hex(); + rpc_params![ + json!({"data": data, "to": self.config.core_contract_address}), + "latest" + ] + } + fn epoch_duration_params(&self) -> ArrayParams { + let data: String = Core::epochDurationCall {}.abi_encode().encode_hex(); + rpc_params![ + json!({"data": data, "to": self.config.core_contract_address}), + "latest" + ] } - pub async fn submit_proof(&self, proof: CCProof) -> Result { - let data = Capacity::submitProofCall { - unitId: proof.cu_id.as_ref().into(), - localUnitNonce: proof.local_nonce.as_ref().into(), - resultHash: proof.result_hash.as_ref().into(), - } - .abi_encode(); + fn min_proofs_per_epoch_params(&self) -> ArrayParams { + let data: String = Core::minProofsPerEpochCall {}.abi_encode().encode_hex(); + rpc_params![ + json!({"data": data, "to": self.config.core_contract_address}), + "latest" + ] + } - self.send_tx(data, &self.config.cc_contract_address).await + fn max_proofs_per_epoch_params(&self) -> ArrayParams { + let data: String = Core::maxProofsPerEpochCall {}.abi_encode().encode_hex(); + rpc_params![ + json!({"data": data, "to": self.config.core_contract_address}), + "latest" + ] } +} - pub async fn get_compute_units(&self) -> Result, ConnectorError> { - let data: String = Offer::getComputeUnitsCall { - peerId: peer_id_to_bytes(self.host_id).into(), +#[async_trait] +impl ChainConnector for HttpChainConnector { + async fn get_current_commitment_id(&self) -> Result, ConnectorError> { + let peer_id = peer_id_to_bytes(self.host_id); + let data: String = Offer::getComputePeerCall { + peerId: peer_id.into(), } .abi_encode() .encode_hex(); - let resp: String = process_response( self.client .request( @@ -319,13 +335,11 @@ impl ChainConnector { ) .await, )?; - let bytes = decode_hex(&resp)?; - let compute_units = as SolType>::abi_decode(&bytes, true)?; - - Ok(compute_units) + let compute_peer = ::abi_decode(&decode_hex(&resp)?, true)?; + Ok(CommitmentId::new(compute_peer.commitmentId.0)) } - pub async fn get_cc_init_params(&self) -> eyre::Result { + async fn get_cc_init_params(&self) -> eyre::Result { let mut batch = BatchRequestBuilder::new(); batch.insert("eth_call", self.difficulty_params())?; @@ -392,13 +406,89 @@ impl ChainConnector { }) } - pub async fn get_deal_statuses<'a, I>( + async fn get_compute_units(&self) -> Result, ConnectorError> { + let data: String = Offer::getComputeUnitsCall { + peerId: peer_id_to_bytes(self.host_id).into(), + } + .abi_encode() + .encode_hex(); + + let resp: String = process_response( + self.client + .request( + "eth_call", + rpc_params![ + json!({ + "data": data, + "to": self.config.market_contract_address, + }), + "latest" + ], + ) + .await, + )?; + let bytes = decode_hex(&resp)?; + let compute_units = as SolType>::abi_decode(&bytes, true)?; + + Ok(compute_units) + } + + async fn get_commitment_status( + &self, + commitment_id: CommitmentId, + ) -> Result { + let data: String = Capacity::getStatusCall { + commitmentId: commitment_id.0.into(), + } + .abi_encode() + .encode_hex(); + + let resp: String = process_response( + self.client + .request( + "eth_call", + rpc_params![ + json!({ + "data": data, + "to": self.config.cc_contract_address, + }), + "latest" + ], + ) + .await, + )?; + Ok(::abi_decode( + &decode_hex(&resp)?, + true, + )?) + } + + async fn get_global_nonce(&self) -> Result { + let resp: String = process_response( + self.client + .request("eth_call", self.global_nonce_params()) + .await, + )?; + + let bytes: FixedBytes<32> = FixedBytes::from_str(&resp)?; + Ok(GlobalNonce::new(bytes.0)) + } + + async fn submit_proof(&self, proof: CCProof) -> Result { + let data = Capacity::submitProofCall { + unitId: proof.cu_id.as_ref().into(), + localUnitNonce: proof.local_nonce.as_ref().into(), + resultHash: proof.result_hash.as_ref().into(), + } + .abi_encode(); + + self.send_tx(data, &self.config.cc_contract_address).await + } + + async fn get_deal_statuses( &self, - deal_ids: I, - ) -> Result>, ConnectorError> - where - I: Iterator, - { + deal_ids: Vec, + ) -> Result>, ConnectorError> { let mut batch = BatchRequestBuilder::new(); for deal_id in deal_ids { let data: String = Deal::getStatusCall {}.abi_encode().encode_hex(); @@ -433,34 +523,20 @@ impl ChainConnector { Ok(statuses) } - - pub async fn get_tx_receipts<'a, I>( - &self, - tx_hashes: I, - ) -> Result>, ConnectorError> - where - I: Iterator, - { - let mut batch = BatchRequestBuilder::new(); - for tx_hash in tx_hashes { - batch.insert("eth_getTransactionReceipt", rpc_params![tx_hash])?; - } - let resp: BatchResponse = self.client.batch_request(batch).await?; - let mut receipts = vec![]; - for receipt in resp.into_iter() { - let receipt = receipt.map_err(|e| ConnectorError::RpcError(e.to_owned().into())); - receipts.push(receipt); + async fn exit_deal(&self, cu_id: &CUID) -> Result { + let data = Offer::returnComputeUnitFromDealCall { + unitId: cu_id.as_ref().into(), } - Ok(receipts) + .abi_encode(); + + self.send_tx(data, &self.config.market_contract_address) + .await } - pub async fn get_tx_statuses<'a, I>( + async fn get_tx_statuses( &self, - tx_hashes: I, - ) -> Result, ConnectorError>>, ConnectorError> - where - I: Iterator, - { + tx_hashes: Vec, + ) -> Result, ConnectorError>>, ConnectorError> { let mut statuses = vec![]; for receipt in self.get_tx_receipts(tx_hashes).await? { @@ -479,68 +555,21 @@ impl ChainConnector { Ok(statuses) } - pub async fn exit_deal(&self, cu_id: &CUID) -> Result { - let data = Offer::returnComputeUnitFromDealCall { - unitId: cu_id.as_ref().into(), + async fn get_tx_receipts( + &self, + tx_hashes: Vec, + ) -> Result>, ConnectorError> { + let mut batch = BatchRequestBuilder::new(); + for tx_hash in tx_hashes { + batch.insert("eth_getTransactionReceipt", rpc_params![tx_hash])?; } - .abi_encode(); - - self.send_tx(data, &self.config.market_contract_address) - .await - } - - fn difficulty_params(&self) -> ArrayParams { - let data: String = Core::difficultyCall {}.abi_encode().encode_hex(); - - rpc_params![ - json!({"data": data, "to": self.config.core_contract_address}), - "latest" - ] - } - - fn init_timestamp_params(&self) -> ArrayParams { - let data: String = Core::initTimestampCall {}.abi_encode().encode_hex(); - rpc_params![ - json!({"data": data, "to": self.config.core_contract_address}), - "latest" - ] - } - fn global_nonce_params(&self) -> ArrayParams { - let data: String = Capacity::getGlobalNonceCall {}.abi_encode().encode_hex(); - rpc_params![ - json!({"data": data, "to": self.config.cc_contract_address}), - "latest" - ] - } - fn current_epoch_params(&self) -> ArrayParams { - let data: String = Core::currentEpochCall {}.abi_encode().encode_hex(); - rpc_params![ - json!({"data": data, "to": self.config.core_contract_address}), - "latest" - ] - } - fn epoch_duration_params(&self) -> ArrayParams { - let data: String = Core::epochDurationCall {}.abi_encode().encode_hex(); - rpc_params![ - json!({"data": data, "to": self.config.core_contract_address}), - "latest" - ] - } - - fn min_proofs_per_epoch_params(&self) -> ArrayParams { - let data: String = Core::minProofsPerEpochCall {}.abi_encode().encode_hex(); - rpc_params![ - json!({"data": data, "to": self.config.core_contract_address}), - "latest" - ] - } - - fn max_proofs_per_epoch_params(&self) -> ArrayParams { - let data: String = Core::maxProofsPerEpochCall {}.abi_encode().encode_hex(); - rpc_params![ - json!({"data": data, "to": self.config.core_contract_address}), - "latest" - ] + let resp: BatchResponse = self.client.batch_request(batch).await?; + let mut receipts = vec![]; + for receipt in resp.into_iter() { + let receipt = receipt.map_err(|e| ConnectorError::RpcError(e.to_owned().into())); + receipts.push(receipt); + } + Ok(receipts) } } @@ -563,10 +592,12 @@ mod tests { use chain_data::peer_id_from_hex; use hex_utils::decode_hex; - use crate::{is_commitment_not_active, CCStatus, ChainConnector, CommitmentId, ConnectorError}; + use crate::{ + is_commitment_not_active, CCStatus, CommitmentId, ConnectorError, HttpChainConnector, + }; - fn get_connector(url: &str) -> Arc { - let (connector, _) = ChainConnector::new( + fn get_connector(url: &str) -> Arc { + let (connector, _) = HttpChainConnector::new( server_config::ChainConfig { http_endpoint: url.to_string(), cc_contract_address: "0x0E62f5cfA5189CA34E79CCB03829C064405790aD".to_string(), diff --git a/crates/chain-connector/src/lib.rs b/crates/chain-connector/src/lib.rs index 7e7503123a..0caaed297a 100644 --- a/crates/chain-connector/src/lib.rs +++ b/crates/chain-connector/src/lib.rs @@ -7,5 +7,6 @@ mod function; pub use connector::CCInitParams; pub use connector::ChainConnector; +pub use connector::HttpChainConnector; pub use error::ConnectorError; pub use function::*; diff --git a/crates/chain-data/src/utils.rs b/crates/chain-data/src/utils.rs index b16dd196fa..a3e51dc7d9 100644 --- a/crates/chain-data/src/utils.rs +++ b/crates/chain-data/src/utils.rs @@ -10,6 +10,7 @@ pub fn parse_peer_id(bytes: Vec) -> Result { PeerId::from_bytes(&peer_id) } +/// This code works only for PeerId generated from ed25519 public key, the size assumptions are wrong pub fn peer_id_to_bytes(peer_id: PeerId) -> [u8; 32] { let peer_id = peer_id.to_bytes(); // peer_id is 38 bytes but we need 32 for chain diff --git a/crates/chain-listener/src/listener.rs b/crates/chain-listener/src/listener.rs index 48cc718e71..d3702b1900 100644 --- a/crates/chain-listener/src/listener.rs +++ b/crates/chain-listener/src/listener.rs @@ -54,7 +54,7 @@ pub struct ChainListener { config: ChainConfig, listener_config: ChainListenerConfig, - chain_connector: Arc, + chain_connector: Arc, // To subscribe to chain events ws_client: WsClient, @@ -117,7 +117,7 @@ impl ChainListener { ws_client: WsClient, listener_config: ChainListenerConfig, host_id: PeerId, - chain_connector: Arc, + chain_connector: Arc, core_manager: Arc, ccp_client: Option, persisted_proof_id_dir: PathBuf, @@ -1204,7 +1204,7 @@ impl ChainListener { } let statuses = retry(ExponentialBackoff::default(), || async { - let s = self.chain_connector.get_deal_statuses(self.active_deals.keys()).await.map_err(|err| { + let s = self.chain_connector.get_deal_statuses(self.active_deals.keys().cloned().collect()).await.map_err(|err| { tracing::warn!(target: "chain-listener", "Failed to poll deal statuses: {err}; Retrying..."); eyre!("Failed to poll deal statuses: {err}; Retrying...") })?; @@ -1260,7 +1260,7 @@ impl ChainListener { } let statuses = retry(ExponentialBackoff::default(), || async { - let s = self.chain_connector.get_tx_statuses(self.pending_proof_txs.iter().map(|(tx, _)| tx)).await.map_err(|err| { + let s = self.chain_connector.get_tx_statuses(self.pending_proof_txs.iter().map(|(tx, _)| tx).cloned().collect()).await.map_err(|err| { tracing::warn!(target: "chain-listener", "Failed to poll pending proof txs statuses: {err}"); eyre!("Failed to poll pending proof txs statuses: {err}; Retrying...") })?; diff --git a/crates/core-manager/src/strict.rs b/crates/core-manager/src/strict.rs index aeefa6404e..5f26192e07 100644 --- a/crates/core-manager/src/strict.rs +++ b/crates/core-manager/src/strict.rs @@ -347,6 +347,7 @@ impl PersistentCoreManagerFunctions for StrictCoreManager { mod tests { use ccp_shared::types::{LogicalCoreId, PhysicalCoreId, CUID}; use hex::FromHex; + use std::collections::BTreeSet; use crate::manager::CoreManagerFunctions; use crate::persistence::PersistentCoreManagerState; @@ -397,6 +398,15 @@ mod tests { }) .unwrap(); assert_eq!(assignment_1, assignment_2); + assert_eq!( + assignment_1 + .cuid_cores + .keys() + .cloned() + .collect::>(), + BTreeSet::from_iter(unit_ids) + ); + assert_eq!(assignment_1, assignment_2); assert_eq!(assignment_1, assignment_3); } } @@ -442,6 +452,7 @@ mod tests { }) .unwrap(); assert_eq!(assignment.physical_core_ids.len(), 2); + assert_eq!(assignment.cuid_cores.len(), 2); let after_assignment = manager.state.read(); diff --git a/nox/src/node.rs b/nox/src/node.rs index 69f9e81b27..b1c52c469f 100644 --- a/nox/src/node.rs +++ b/nox/src/node.rs @@ -41,7 +41,7 @@ use aquamarine::{ AquaRuntime, AquamarineApi, AquamarineApiError, AquamarineBackend, DataStoreConfig, RemoteRoutingEffects, VmPoolConfig, }; -use chain_connector::ChainConnector; +use chain_connector::HttpChainConnector; use chain_listener::ChainListener; use config_utils::to_peer_id; use connection_pool::ConnectionPoolT; @@ -112,7 +112,7 @@ pub struct Node { } async fn setup_listener( - connector: Option>, + connector: Option>, config: &ResolvedConfig, core_manager: Arc, chain_listener_metrics: Option, @@ -393,7 +393,7 @@ impl Node { let connector = if let Some(chain_config) = config.chain_config.clone() { let host_id = scopes.get_host_peer_id(); let (chain_connector, chain_builtins) = - ChainConnector::new(chain_config.clone(), host_id).map_err(|err| { + HttpChainConnector::new(chain_config.clone(), host_id).map_err(|err| { log::error!( "Error connecting to http endpoint {}, error: {err}", chain_config.http_endpoint From 319b1629a8a4d2c0de39da75a12f423020e48817 Mon Sep 17 00:00:00 2001 From: Nick Pavlov Date: Wed, 17 Apr 2024 15:38:19 +0300 Subject: [PATCH 2/5] fix(chain-connector): make connector generic --- crates/chain-data/src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/chain-data/src/utils.rs b/crates/chain-data/src/utils.rs index a3e51dc7d9..54881f5aaf 100644 --- a/crates/chain-data/src/utils.rs +++ b/crates/chain-data/src/utils.rs @@ -10,7 +10,7 @@ pub fn parse_peer_id(bytes: Vec) -> Result { PeerId::from_bytes(&peer_id) } -/// This code works only for PeerId generated from ed25519 public key, the size assumptions are wrong +/// This code works only for PeerId generated from ed25519 public key, the size assumptions is wrong pub fn peer_id_to_bytes(peer_id: PeerId) -> [u8; 32] { let peer_id = peer_id.to_bytes(); // peer_id is 38 bytes but we need 32 for chain From 7940e9a164ff691bd7c758dc493e64798d97ba30 Mon Sep 17 00:00:00 2001 From: Nick Pavlov Date: Wed, 17 Apr 2024 15:49:01 +0300 Subject: [PATCH 3/5] fix tests --- crates/chain-connector/src/connector.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/chain-connector/src/connector.rs b/crates/chain-connector/src/connector.rs index fbc807a1d9..5356de7c25 100644 --- a/crates/chain-connector/src/connector.rs +++ b/crates/chain-connector/src/connector.rs @@ -592,11 +592,9 @@ mod tests { use chain_data::peer_id_from_hex; use hex_utils::decode_hex; - use crate::{ - is_commitment_not_active, CCStatus, CommitmentId, ConnectorError, HttpChainConnector, - }; + use crate::{is_commitment_not_active, CCStatus, CommitmentId, ConnectorError, HttpChainConnector, ChainConnector}; - fn get_connector(url: &str) -> Arc { + fn get_connector(url: &str) -> Arc { let (connector, _) = HttpChainConnector::new( server_config::ChainConfig { http_endpoint: url.to_string(), From e38a157b71b728a888c263f091a2a49f31a8e2c4 Mon Sep 17 00:00:00 2001 From: Nick Pavlov Date: Wed, 17 Apr 2024 15:49:08 +0300 Subject: [PATCH 4/5] fix tests --- crates/chain-connector/src/connector.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/chain-connector/src/connector.rs b/crates/chain-connector/src/connector.rs index 5356de7c25..2320f1d30a 100644 --- a/crates/chain-connector/src/connector.rs +++ b/crates/chain-connector/src/connector.rs @@ -592,7 +592,10 @@ mod tests { use chain_data::peer_id_from_hex; use hex_utils::decode_hex; - use crate::{is_commitment_not_active, CCStatus, CommitmentId, ConnectorError, HttpChainConnector, ChainConnector}; + use crate::{ + is_commitment_not_active, CCStatus, ChainConnector, CommitmentId, ConnectorError, + HttpChainConnector, + }; fn get_connector(url: &str) -> Arc { let (connector, _) = HttpChainConnector::new( From e26fa6263b9d835e78bc2a4293b97b85320be65b Mon Sep 17 00:00:00 2001 From: Nick Pavlov Date: Wed, 17 Apr 2024 16:26:54 +0300 Subject: [PATCH 5/5] fix tests --- crates/chain-connector/src/connector.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/chain-connector/src/connector.rs b/crates/chain-connector/src/connector.rs index 2320f1d30a..4e3be72ca7 100644 --- a/crates/chain-connector/src/connector.rs +++ b/crates/chain-connector/src/connector.rs @@ -597,7 +597,7 @@ mod tests { HttpChainConnector, }; - fn get_connector(url: &str) -> Arc { + fn get_connector(url: &str) -> Arc { let (connector, _) = HttpChainConnector::new( server_config::ChainConfig { http_endpoint: url.to_string(),