diff --git a/bridges/relays/ethereum-client/Cargo.toml b/bridges/relays/ethereum-client/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..4923b8a111fbc93c8b25f9f24cc5b5a0964d8d74
--- /dev/null
+++ b/bridges/relays/ethereum-client/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "relay-ethereum-client"
+version = "0.1.0"
+authors = ["Parity Technologies <admin@parity.io>"]
+edition = "2018"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+
+[dependencies]
+codec = { package = "parity-scale-codec", version = "1.3.4" }
+ethereum-tx-sign = "3.0"
+headers-relay = { path = "../headers-relay" }
+hex = "0.4"
+jsonrpsee = { git = "https://github.com/svyatonik/jsonrpsee.git", branch = "shared-client-in-rpc-api", default-features = false, features = ["http"] }
+log = "0.4.11"
+parity-crypto = { version = "0.6", features = ["publickey"] }
+relay-utils = { path = "../utils" }
+web3 = "0.13"
diff --git a/bridges/relays/ethereum-client/src/client.rs b/bridges/relays/ethereum-client/src/client.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0042b13c6ef18f0cb38969abcb5845ea86a4c83e
--- /dev/null
+++ b/bridges/relays/ethereum-client/src/client.rs
@@ -0,0 +1,142 @@
+// Copyright 2019-2020 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+use crate::rpc::Ethereum;
+use crate::types::{
+	Address, Bytes, CallRequest, Header, HeaderWithTransactions, Receipt, SignedRawTx, Transaction, TransactionHash,
+	H256, U256,
+};
+use crate::{ConnectionParams, Error, Result};
+
+use jsonrpsee::raw::RawClient;
+use jsonrpsee::transport::http::HttpTransportClient;
+use jsonrpsee::Client as RpcClient;
+
+/// The client used to interact with an Ethereum node through RPC.
+#[derive(Clone)]
+pub struct Client {
+	client: RpcClient,
+}
+
+impl Client {
+	/// Create a new Ethereum RPC Client.
+	pub fn new(params: ConnectionParams) -> Self {
+		let uri = format!("http://{}:{}", params.host, params.port);
+		let transport = HttpTransportClient::new(&uri);
+		let raw_client = RawClient::new(transport);
+		let client: RpcClient = raw_client.into();
+
+		Self { client }
+	}
+}
+
+impl Client {
+	/// Estimate gas usage for the given call.
+	pub async fn estimate_gas(&self, call_request: CallRequest) -> Result<U256> {
+		Ok(Ethereum::estimate_gas(&self.client, call_request).await?)
+	}
+
+	/// Retrieve number of the best known block from the Ethereum node.
+	pub async fn best_block_number(&self) -> Result<u64> {
+		Ok(Ethereum::block_number(&self.client).await?.as_u64())
+	}
+
+	/// Retrieve number of the best known block from the Ethereum node.
+	pub async fn header_by_number(&self, block_number: u64) -> Result<Header> {
+		let get_full_tx_objects = false;
+		let header = Ethereum::get_block_by_number(&self.client, block_number, get_full_tx_objects).await?;
+		match header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some() {
+			true => Ok(header),
+			false => Err(Error::IncompleteHeader),
+		}
+	}
+
+	/// Retrieve block header by its hash from Ethereum node.
+	pub async fn header_by_hash(&self, hash: H256) -> Result<Header> {
+		let get_full_tx_objects = false;
+		let header = Ethereum::get_block_by_hash(&self.client, hash, get_full_tx_objects).await?;
+		match header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some() {
+			true => Ok(header),
+			false => Err(Error::IncompleteHeader),
+		}
+	}
+
+	/// Retrieve block header and its transactions by its number from Ethereum node.
+	pub async fn header_by_number_with_transactions(&self, number: u64) -> Result<HeaderWithTransactions> {
+		let get_full_tx_objects = true;
+		let header = Ethereum::get_block_by_number_with_transactions(&self.client, number, get_full_tx_objects).await?;
+
+		let is_complete_header = header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some();
+		if !is_complete_header {
+			return Err(Error::IncompleteHeader);
+		}
+
+		let is_complete_transactions = header.transactions.iter().all(|tx| tx.raw.is_some());
+		if !is_complete_transactions {
+			return Err(Error::IncompleteTransaction);
+		}
+
+		Ok(header)
+	}
+
+	/// Retrieve block header and its transactions by its hash from Ethereum node.
+	pub async fn header_by_hash_with_transactions(&self, hash: H256) -> Result<HeaderWithTransactions> {
+		let get_full_tx_objects = true;
+		let header = Ethereum::get_block_by_hash_with_transactions(&self.client, hash, get_full_tx_objects).await?;
+
+		let is_complete_header = header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some();
+		if !is_complete_header {
+			return Err(Error::IncompleteHeader);
+		}
+
+		let is_complete_transactions = header.transactions.iter().all(|tx| tx.raw.is_some());
+		if !is_complete_transactions {
+			return Err(Error::IncompleteTransaction);
+		}
+
+		Ok(header)
+	}
+
+	/// Retrieve transaction by its hash from Ethereum node.
+	pub async fn transaction_by_hash(&self, hash: H256) -> Result<Option<Transaction>> {
+		Ok(Ethereum::transaction_by_hash(&self.client, hash).await?)
+	}
+
+	/// Retrieve transaction receipt by transaction hash.
+	pub async fn transaction_receipt(&self, transaction_hash: H256) -> Result<Receipt> {
+		Ok(Ethereum::get_transaction_receipt(&self.client, transaction_hash).await?)
+	}
+
+	/// Get the nonce of the given account.
+	pub async fn account_nonce(&self, address: Address) -> Result<U256> {
+		Ok(Ethereum::get_transaction_count(&self.client, address).await?)
+	}
+
+	/// Submit an Ethereum transaction.
+	///
+	/// The transaction must already be signed before sending it through this method.
+	pub async fn submit_transaction(&self, signed_raw_tx: SignedRawTx) -> Result<TransactionHash> {
+		let transaction = Bytes(signed_raw_tx);
+		let tx_hash = Ethereum::submit_transaction(&self.client, transaction).await?;
+		log::trace!(target: "bridge", "Sent transaction to Ethereum node: {:?}", tx_hash);
+		Ok(tx_hash)
+	}
+
+	/// Call Ethereum smart contract.
+	pub async fn eth_call(&self, call_transaction: CallRequest) -> Result<Bytes> {
+		Ok(Ethereum::call(&self.client, call_transaction).await?)
+	}
+}
diff --git a/bridges/relays/ethereum-client/src/error.rs b/bridges/relays/ethereum-client/src/error.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b02e5fecf5842b91c1568afcb21d0bddc71c5388
--- /dev/null
+++ b/bridges/relays/ethereum-client/src/error.rs
@@ -0,0 +1,71 @@
+// Copyright 2019-2020 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Ethereum node RPC errors.
+
+use jsonrpsee::client::RequestError;
+use relay_utils::MaybeConnectionError;
+
+/// Result type used by Ethereum client.
+pub type Result<T> = std::result::Result<T, Error>;
+
+/// Errors that can occur only when interacting with
+/// an Ethereum node through RPC.
+#[derive(Debug)]
+pub enum Error {
+	/// An error that can occur when making an HTTP request to
+	/// an JSON-RPC client.
+	Request(RequestError),
+	/// Failed to parse response.
+	ResponseParseFailed(String),
+	/// We have received a header with missing fields.
+	IncompleteHeader,
+	/// We have received a transaction missing a `raw` field.
+	IncompleteTransaction,
+	/// An invalid Substrate block number was received from
+	/// an Ethereum node.
+	InvalidSubstrateBlockNumber,
+	/// An invalid index has been received from an Ethereum node.
+	InvalidIncompleteIndex,
+}
+
+impl From<RequestError> for Error {
+	fn from(error: RequestError) -> Self {
+		Error::Request(error)
+	}
+}
+
+impl MaybeConnectionError for Error {
+	fn is_connection_error(&self) -> bool {
+		matches!(*self, Error::Request(RequestError::TransportError(_)))
+	}
+}
+
+impl ToString for Error {
+	fn to_string(&self) -> String {
+		match self {
+			Self::Request(e) => e.to_string(),
+			Self::ResponseParseFailed(e) => e.to_string(),
+			Self::IncompleteHeader => {
+				"Incomplete Ethereum Header Received (missing some of required fields - hash, number, logs_bloom)"
+					.to_string()
+			}
+			Self::IncompleteTransaction => "Incomplete Ethereum Transaction (missing required field - raw)".to_string(),
+			Self::InvalidSubstrateBlockNumber => "Received an invalid Substrate block from Ethereum Node".to_string(),
+			Self::InvalidIncompleteIndex => "Received an invalid incomplete index from Ethereum Node".to_string(),
+		}
+	}
+}
diff --git a/bridges/relays/ethereum-client/src/lib.rs b/bridges/relays/ethereum-client/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8c5a00e01b4d7119f2fc70307a6461d666ed0af1
--- /dev/null
+++ b/bridges/relays/ethereum-client/src/lib.rs
@@ -0,0 +1,48 @@
+// Copyright 2019-2020 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Tools to interact with (Open) Ethereum node using RPC methods.
+
+#![warn(missing_docs)]
+
+mod client;
+mod error;
+mod rpc;
+mod sign;
+
+pub use crate::client::Client;
+pub use crate::error::{Error, Result};
+pub use crate::sign::{sign_and_submit_transaction, SigningParams};
+
+pub mod types;
+
+/// Ethereum connection params.
+#[derive(Debug, Clone)]
+pub struct ConnectionParams {
+	/// Ethereum RPC host.
+	pub host: String,
+	/// Ethereum RPC port.
+	pub port: u16,
+}
+
+impl Default for ConnectionParams {
+	fn default() -> Self {
+		ConnectionParams {
+			host: "localhost".into(),
+			port: 8545,
+		}
+	}
+}
diff --git a/bridges/relays/ethereum-client/src/rpc.rs b/bridges/relays/ethereum-client/src/rpc.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9739d3edbe28039c09b4a05f61f514202c5c3644
--- /dev/null
+++ b/bridges/relays/ethereum-client/src/rpc.rs
@@ -0,0 +1,53 @@
+// Copyright 2019-2020 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Ethereum node RPC interface.
+
+// The compiler doesn't think we're using the
+// code from rpc_api!
+#![allow(dead_code)]
+#![allow(unused_variables)]
+
+use crate::types::{
+	Address, Bytes, CallRequest, Header, HeaderWithTransactions, Receipt, Transaction, TransactionHash, H256, U256, U64,
+};
+
+jsonrpsee::rpc_api! {
+	pub(crate) Ethereum {
+		#[rpc(method = "eth_estimateGas", positional_params)]
+		fn estimate_gas(call_request: CallRequest) -> U256;
+		#[rpc(method = "eth_blockNumber", positional_params)]
+		fn block_number() -> U64;
+		#[rpc(method = "eth_getBlockByNumber", positional_params)]
+		fn get_block_by_number(block_number: U64, full_tx_objs: bool) -> Header;
+		#[rpc(method = "eth_getBlockByHash", positional_params)]
+		fn get_block_by_hash(hash: H256, full_tx_objs: bool) -> Header;
+		#[rpc(method = "eth_getBlockByNumber", positional_params)]
+		fn get_block_by_number_with_transactions(number: U64, full_tx_objs: bool) -> HeaderWithTransactions;
+		#[rpc(method = "eth_getBlockByHash", positional_params)]
+		fn get_block_by_hash_with_transactions(hash: H256, full_tx_objs: bool) -> HeaderWithTransactions;
+		#[rpc(method = "eth_getTransactionByHash", positional_params)]
+		fn transaction_by_hash(hash: H256) -> Option<Transaction>;
+		#[rpc(method = "eth_getTransactionReceipt", positional_params)]
+		fn get_transaction_receipt(transaction_hash: H256) -> Receipt;
+		#[rpc(method = "eth_getTransactionCount", positional_params)]
+		fn get_transaction_count(address: Address) -> U256;
+		#[rpc(method = "eth_submitTransaction", positional_params)]
+		fn submit_transaction(transaction: Bytes) -> TransactionHash;
+		#[rpc(method = "eth_call", positional_params)]
+		fn call(transaction_call: CallRequest) -> Bytes;
+	}
+}
diff --git a/bridges/relays/ethereum-client/src/sign.rs b/bridges/relays/ethereum-client/src/sign.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f5b80a34e5a7b40a09b70d7dabc2648ac890b4fa
--- /dev/null
+++ b/bridges/relays/ethereum-client/src/sign.rs
@@ -0,0 +1,85 @@
+// Copyright 2019-2020 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity Bridges Common is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+use crate::types::{Address, CallRequest, U256};
+use crate::{Client, Result};
+
+use parity_crypto::publickey::KeyPair;
+
+/// Ethereum signing params.
+#[derive(Clone, Debug)]
+pub struct SigningParams {
+	/// Ethereum chain id.
+	pub chain_id: u64,
+	/// Ethereum transactions signer.
+	pub signer: KeyPair,
+	/// Gas price we agree to pay.
+	pub gas_price: U256,
+}
+
+impl Default for SigningParams {
+	fn default() -> Self {
+		SigningParams {
+			chain_id: 0x11, // Parity dev chain
+			// account that has a lot of ether when we run instant seal engine
+			// address: 0x00a329c0648769a73afac7f9381e08fb43dbea72
+			// secret: 0x4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7
+			signer: KeyPair::from_secret_slice(
+				&hex::decode("4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7")
+					.expect("secret is hardcoded, thus valid; qed"),
+			)
+			.expect("secret is hardcoded, thus valid; qed"),
+			gas_price: 8_000_000_000u64.into(), // 8 Gwei
+		}
+	}
+}
+
+/// Sign and submit tranaction using given Ethereum client.
+pub async fn sign_and_submit_transaction(
+	client: &Client,
+	params: &SigningParams,
+	contract_address: Option<Address>,
+	nonce: Option<U256>,
+	double_gas: bool,
+	encoded_call: Vec<u8>,
+) -> Result<()> {
+	let nonce = if let Some(n) = nonce {
+		n
+	} else {
+		let address: Address = params.signer.address().as_fixed_bytes().into();
+		client.account_nonce(address).await?
+	};
+
+	let call_request = CallRequest {
+		to: contract_address,
+		data: Some(encoded_call.clone().into()),
+		..Default::default()
+	};
+	let gas = client.estimate_gas(call_request).await?;
+
+	let raw_transaction = ethereum_tx_sign::RawTransaction {
+		nonce,
+		to: contract_address,
+		value: U256::zero(),
+		gas: if double_gas { gas.saturating_mul(2.into()) } else { gas },
+		gas_price: params.gas_price,
+		data: encoded_call,
+	}
+	.sign(&params.signer.secret().as_fixed_bytes().into(), &params.chain_id);
+
+	let _ = client.submit_transaction(raw_transaction).await?;
+	Ok(())
+}
diff --git a/bridges/relays/ethereum/src/ethereum_types.rs b/bridges/relays/ethereum-client/src/types.rs
similarity index 57%
rename from bridges/relays/ethereum/src/ethereum_types.rs
rename to bridges/relays/ethereum-client/src/types.rs
index bb780d69680192e3bbe2cbae7f2ccf0471e9c5db..f64362ade0e887542cc39853f56abebef45b820c 100644
--- a/bridges/relays/ethereum/src/ethereum_types.rs
+++ b/bridges/relays/ethereum-client/src/types.rs
@@ -14,11 +14,9 @@
 // You should have received a copy of the GNU General Public License
 // along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
 
-use crate::substrate_types::{into_substrate_ethereum_header, into_substrate_ethereum_receipts};
+//! Common types that are used in relay <-> Ethereum node communications.
 
-use codec::Encode;
-use headers_relay::sync_types::{HeadersSyncPipeline, QueuedHeader, SourceHeader};
-use relay_utils::HeaderId;
+use headers_relay::sync_types::SourceHeader;
 
 pub use web3::types::{Address, Bytes, CallRequest, H256, U128, U256, U64};
 
@@ -26,6 +24,9 @@ pub use web3::types::{Address, Bytes, CallRequest, H256, U128, U256, U64};
 /// both number and hash fields filled.
 pub const HEADER_ID_PROOF: &str = "checked on retrieval; qed";
 
+/// Ethereum transaction hash type.
+pub type HeaderHash = H256;
+
 /// Ethereum transaction hash type.
 pub type TransactionHash = H256;
 
@@ -37,10 +38,11 @@ pub type Header = web3::types::Block<H256>;
 
 /// Ethereum header type used in headers sync.
 #[derive(Clone, Debug, PartialEq)]
-pub struct EthereumSyncHeader(Header);
+pub struct SyncHeader(Header);
 
-impl std::ops::Deref for EthereumSyncHeader {
+impl std::ops::Deref for SyncHeader {
 	type Target = Header;
+
 	fn deref(&self) -> &Self::Target {
 		&self.0
 	}
@@ -53,52 +55,26 @@ pub type HeaderWithTransactions = web3::types::Block<Transaction>;
 pub type Receipt = web3::types::TransactionReceipt;
 
 /// Ethereum header ID.
-pub type EthereumHeaderId = HeaderId<H256, u64>;
-
-/// Queued ethereum header ID.
-pub type QueuedEthereumHeader = QueuedHeader<EthereumHeadersSyncPipeline>;
+pub type HeaderId = relay_utils::HeaderId<H256, u64>;
 
 /// A raw Ethereum transaction that's been signed.
 pub type SignedRawTx = Vec<u8>;
 
-/// Ethereum synchronization pipeline.
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(test, derive(PartialEq))]
-pub struct EthereumHeadersSyncPipeline;
-
-impl HeadersSyncPipeline for EthereumHeadersSyncPipeline {
-	const SOURCE_NAME: &'static str = "Ethereum";
-	const TARGET_NAME: &'static str = "Substrate";
-
-	type Hash = H256;
-	type Number = u64;
-	type Header = EthereumSyncHeader;
-	type Extra = Vec<Receipt>;
-	type Completion = ();
-
-	fn estimate_size(source: &QueuedHeader<Self>) -> usize {
-		into_substrate_ethereum_header(source.header()).encode().len()
-			+ into_substrate_ethereum_receipts(source.extra())
-				.map(|extra| extra.encode().len())
-				.unwrap_or(0)
-	}
-}
-
-impl From<Header> for EthereumSyncHeader {
+impl From<Header> for SyncHeader {
 	fn from(header: Header) -> Self {
 		Self(header)
 	}
 }
 
-impl SourceHeader<H256, u64> for EthereumSyncHeader {
-	fn id(&self) -> EthereumHeaderId {
-		HeaderId(
+impl SourceHeader<H256, u64> for SyncHeader {
+	fn id(&self) -> HeaderId {
+		relay_utils::HeaderId(
 			self.number.expect(HEADER_ID_PROOF).as_u64(),
 			self.hash.expect(HEADER_ID_PROOF),
 		)
 	}
 
-	fn parent_id(&self) -> EthereumHeaderId {
-		HeaderId(self.number.expect(HEADER_ID_PROOF).as_u64() - 1, self.parent_hash)
+	fn parent_id(&self) -> HeaderId {
+		relay_utils::HeaderId(self.number.expect(HEADER_ID_PROOF).as_u64() - 1, self.parent_hash)
 	}
 }
diff --git a/bridges/relays/ethereum/Cargo.toml b/bridges/relays/ethereum/Cargo.toml
index 1bedea7aedaa0278bdccb17f3ee733fe3f37ce27..ab6872dabf206a8c581206fe96c858bbc749b0e7 100644
--- a/bridges/relays/ethereum/Cargo.toml
+++ b/bridges/relays/ethereum/Cargo.toml
@@ -17,7 +17,6 @@ env_logger = "0.7.0"
 ethabi = "12.0"
 ethabi-contract = "11.0"
 ethabi-derive = "12.0"
-ethereum-tx-sign = "3.0"
 exchange-relay = { path = "../exchange-relay" }
 futures = "0.3.5"
 headers-relay = { path = "../headers-relay" }
@@ -27,11 +26,11 @@ log = "0.4.11"
 messages-relay = { path = "../messages-relay" }
 num-traits = "0.2"
 parity-crypto = { version = "0.6", features = ["publickey"] }
+relay-ethereum-client = { path = "../ethereum-client" }
 relay-utils = { path = "../utils" }
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0.57"
 time = "0.2"
-web3 = "0.13"
 
 [dependencies.jsonrpsee]
 git = "https://github.com/svyatonik/jsonrpsee.git"
diff --git a/bridges/relays/ethereum/src/ethereum_client.rs b/bridges/relays/ethereum/src/ethereum_client.rs
index 5635197d6404ad33648fbc6f12ba55704065801b..d9357c560b08d60c82c3eb26bad89612a647849f 100644
--- a/bridges/relays/ethereum/src/ethereum_client.rs
+++ b/bridges/relays/ethereum/src/ethereum_client.rs
@@ -14,22 +14,18 @@
 // You should have received a copy of the GNU General Public License
 // along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
 
-use crate::ethereum_types::{
-	Address, Bytes, CallRequest, EthereumHeaderId, Header, HeaderWithTransactions, Receipt, SignedRawTx, Transaction,
-	TransactionHash, H256, U256,
-};
-use crate::rpc::{Ethereum, EthereumRpc};
-use crate::rpc_errors::{EthereumNodeError, RpcError};
+use crate::rpc_errors::RpcError;
 use crate::substrate_types::{GrandpaJustification, Hash as SubstrateHash, QueuedSubstrateHeader, SubstrateHeaderId};
 
 use async_trait::async_trait;
 use codec::{Decode, Encode};
 use ethabi::FunctionOutputDecoder;
 use headers_relay::sync_types::SubmittedHeaders;
-use jsonrpsee::raw::RawClient;
-use jsonrpsee::transport::http::HttpTransportClient;
-use jsonrpsee::Client;
-use parity_crypto::publickey::KeyPair;
+use relay_ethereum_client::{
+	sign_and_submit_transaction,
+	types::{Address, CallRequest, HeaderId as EthereumHeaderId, Receipt, H256, U256},
+	Client as EthereumClient, Error as EthereumNodeError, SigningParams as EthereumSigningParams,
+};
 use relay_utils::{HeaderId, MaybeConnectionError};
 use std::collections::HashSet;
 
@@ -38,159 +34,10 @@ ethabi_contract::use_contract!(bridge_contract, "res/substrate-bridge-abi.json")
 
 type RpcResult<T> = std::result::Result<T, RpcError>;
 
-/// Ethereum connection params.
-#[derive(Debug, Clone)]
-pub struct EthereumConnectionParams {
-	/// Ethereum RPC host.
-	pub host: String,
-	/// Ethereum RPC port.
-	pub port: u16,
-}
-
-impl Default for EthereumConnectionParams {
-	fn default() -> Self {
-		EthereumConnectionParams {
-			host: "localhost".into(),
-			port: 8545,
-		}
-	}
-}
-
-/// Ethereum signing params.
-#[derive(Clone, Debug)]
-pub struct EthereumSigningParams {
-	/// Ethereum chain id.
-	pub chain_id: u64,
-	/// Ethereum transactions signer.
-	pub signer: KeyPair,
-	/// Gas price we agree to pay.
-	pub gas_price: U256,
-}
-
-impl Default for EthereumSigningParams {
-	fn default() -> Self {
-		EthereumSigningParams {
-			chain_id: 0x11, // Parity dev chain
-			// account that has a lot of ether when we run instant seal engine
-			// address: 0x00a329c0648769a73afac7f9381e08fb43dbea72
-			// secret: 0x4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7
-			signer: KeyPair::from_secret_slice(
-				&hex::decode("4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7")
-					.expect("secret is hardcoded, thus valid; qed"),
-			)
-			.expect("secret is hardcoded, thus valid; qed"),
-			gas_price: 8_000_000_000u64.into(), // 8 Gwei
-		}
-	}
-}
-
-/// The client used to interact with an Ethereum node through RPC.
-pub struct EthereumRpcClient {
-	client: Client,
-}
-
-impl EthereumRpcClient {
-	/// Create a new Ethereum RPC Client.
-	pub fn new(params: EthereumConnectionParams) -> Self {
-		let uri = format!("http://{}:{}", params.host, params.port);
-		let transport = HttpTransportClient::new(&uri);
-		let raw_client = RawClient::new(transport);
-		let client: Client = raw_client.into();
-
-		Self { client }
-	}
-}
-
-#[async_trait]
-impl EthereumRpc for EthereumRpcClient {
-	async fn estimate_gas(&self, call_request: CallRequest) -> RpcResult<U256> {
-		Ok(Ethereum::estimate_gas(&self.client, call_request).await?)
-	}
-
-	async fn best_block_number(&self) -> RpcResult<u64> {
-		Ok(Ethereum::block_number(&self.client).await?.as_u64())
-	}
-
-	async fn header_by_number(&self, block_number: u64) -> RpcResult<Header> {
-		let get_full_tx_objects = false;
-		let header = Ethereum::get_block_by_number(&self.client, block_number, get_full_tx_objects).await?;
-		match header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some() {
-			true => Ok(header),
-			false => Err(RpcError::Ethereum(EthereumNodeError::IncompleteHeader)),
-		}
-	}
-
-	async fn header_by_hash(&self, hash: H256) -> RpcResult<Header> {
-		let get_full_tx_objects = false;
-		let header = Ethereum::get_block_by_hash(&self.client, hash, get_full_tx_objects).await?;
-		match header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some() {
-			true => Ok(header),
-			false => Err(RpcError::Ethereum(EthereumNodeError::IncompleteHeader)),
-		}
-	}
-
-	async fn header_by_number_with_transactions(&self, number: u64) -> RpcResult<HeaderWithTransactions> {
-		let get_full_tx_objects = true;
-		let header = Ethereum::get_block_by_number_with_transactions(&self.client, number, get_full_tx_objects).await?;
-
-		let is_complete_header = header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some();
-		if !is_complete_header {
-			return Err(RpcError::Ethereum(EthereumNodeError::IncompleteHeader));
-		}
-
-		let is_complete_transactions = header.transactions.iter().all(|tx| tx.raw.is_some());
-		if !is_complete_transactions {
-			return Err(RpcError::Ethereum(EthereumNodeError::IncompleteTransaction));
-		}
-
-		Ok(header)
-	}
-
-	async fn header_by_hash_with_transactions(&self, hash: H256) -> RpcResult<HeaderWithTransactions> {
-		let get_full_tx_objects = true;
-		let header = Ethereum::get_block_by_hash_with_transactions(&self.client, hash, get_full_tx_objects).await?;
-
-		let is_complete_header = header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some();
-		if !is_complete_header {
-			return Err(RpcError::Ethereum(EthereumNodeError::IncompleteHeader));
-		}
-
-		let is_complete_transactions = header.transactions.iter().all(|tx| tx.raw.is_some());
-		if !is_complete_transactions {
-			return Err(RpcError::Ethereum(EthereumNodeError::IncompleteTransaction));
-		}
-
-		Ok(header)
-	}
-
-	async fn transaction_by_hash(&self, hash: H256) -> RpcResult<Option<Transaction>> {
-		Ok(Ethereum::transaction_by_hash(&self.client, hash).await?)
-	}
-
-	async fn transaction_receipt(&self, transaction_hash: H256) -> RpcResult<Receipt> {
-		Ok(Ethereum::get_transaction_receipt(&self.client, transaction_hash).await?)
-	}
-
-	async fn account_nonce(&self, address: Address) -> RpcResult<U256> {
-		Ok(Ethereum::get_transaction_count(&self.client, address).await?)
-	}
-
-	async fn submit_transaction(&self, signed_raw_tx: SignedRawTx) -> RpcResult<TransactionHash> {
-		let transaction = Bytes(signed_raw_tx);
-		let tx_hash = Ethereum::submit_transaction(&self.client, transaction).await?;
-		log::trace!(target: "bridge", "Sent transaction to Ethereum node: {:?}", tx_hash);
-		Ok(tx_hash)
-	}
-
-	async fn eth_call(&self, call_transaction: CallRequest) -> RpcResult<Bytes> {
-		Ok(Ethereum::call(&self.client, call_transaction).await?)
-	}
-}
-
 /// A trait which contains methods that work by using multiple low-level RPCs, or more complicated
 /// interactions involving, for example, an Ethereum contract.
 #[async_trait]
-pub trait EthereumHighLevelRpc: EthereumRpc {
+pub trait EthereumHighLevelRpc {
 	/// Returns best Substrate block that PoA chain knows of.
 	async fn best_substrate_block(&self, contract_address: Address) -> RpcResult<SubstrateHeaderId>;
 
@@ -240,7 +87,7 @@ pub trait EthereumHighLevelRpc: EthereumRpc {
 }
 
 #[async_trait]
-impl EthereumHighLevelRpc for EthereumRpcClient {
+impl EthereumHighLevelRpc for EthereumClient {
 	async fn best_substrate_block(&self, contract_address: Address) -> RpcResult<SubstrateHeaderId> {
 		let (encoded_call, call_decoder) = bridge_contract::functions::best_known_header::call();
 		let call_request = CallRequest {
@@ -293,7 +140,7 @@ impl EthereumHighLevelRpc for EthereumRpcClient {
 					submitted: Vec::new(),
 					incomplete: Vec::new(),
 					rejected: headers.iter().rev().map(|header| header.id()).collect(),
-					fatal_error: Some(error),
+					fatal_error: Some(error.into()),
 				}
 			}
 		};
@@ -302,9 +149,7 @@ impl EthereumHighLevelRpc for EthereumRpcClient {
 		// cloning `jsonrpsee::Client` only clones reference to background threads
 		submit_substrate_headers(
 			EthereumHeadersSubmitter {
-				client: EthereumRpcClient {
-					client: self.client.clone(),
-				},
+				client: self.clone(),
 				params,
 				contract_address,
 				nonce,
@@ -369,32 +214,9 @@ impl EthereumHighLevelRpc for EthereumRpcClient {
 		double_gas: bool,
 		encoded_call: Vec<u8>,
 	) -> RpcResult<()> {
-		let nonce = if let Some(n) = nonce {
-			n
-		} else {
-			let address: Address = params.signer.address().as_fixed_bytes().into();
-			self.account_nonce(address).await?
-		};
-
-		let call_request = CallRequest {
-			to: contract_address,
-			data: Some(encoded_call.clone().into()),
-			..Default::default()
-		};
-		let gas = self.estimate_gas(call_request).await?;
-
-		let raw_transaction = ethereum_tx_sign::RawTransaction {
-			nonce,
-			to: contract_address,
-			value: U256::zero(),
-			gas: if double_gas { gas.saturating_mul(2.into()) } else { gas },
-			gas_price: params.gas_price,
-			data: encoded_call,
-		}
-		.sign(&params.signer.secret().as_fixed_bytes().into(), &params.chain_id);
-
-		let _ = self.submit_transaction(raw_transaction).await?;
-		Ok(())
+		sign_and_submit_transaction(self, params, contract_address, nonce, double_gas, encoded_call)
+			.await
+			.map_err(Into::into)
 	}
 
 	async fn transaction_receipts(
@@ -524,7 +346,7 @@ trait HeadersSubmitter {
 
 /// Implementation of Substrate headers submitter that sends headers to running Ethereum node.
 struct EthereumHeadersSubmitter {
-	client: EthereumRpcClient,
+	client: EthereumClient,
 	params: EthereumSigningParams,
 	contract_address: Address,
 	nonce: U256,
diff --git a/bridges/relays/ethereum/src/ethereum_deploy_contract.rs b/bridges/relays/ethereum/src/ethereum_deploy_contract.rs
index b04632fc7eae0a2fa875aaa68bbe41cee9b43b05..17f12ba45eb1b565b7c9db8e6e007873f577ef0b 100644
--- a/bridges/relays/ethereum/src/ethereum_deploy_contract.rs
+++ b/bridges/relays/ethereum/src/ethereum_deploy_contract.rs
@@ -14,9 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
 
-use crate::ethereum_client::{
-	bridge_contract, EthereumConnectionParams, EthereumHighLevelRpc, EthereumRpcClient, EthereumSigningParams,
-};
+use crate::ethereum_client::{bridge_contract, EthereumHighLevelRpc};
 use crate::instances::BridgeInstance;
 use crate::rpc::SubstrateRpc;
 use crate::substrate_client::{SubstrateConnectionParams, SubstrateRpcClient};
@@ -24,6 +22,9 @@ use crate::substrate_types::{Hash as SubstrateHash, Header as SubstrateHeader, S
 
 use codec::{Decode, Encode};
 use num_traits::Zero;
+use relay_ethereum_client::{
+	Client as EthereumClient, ConnectionParams as EthereumConnectionParams, SigningParams as EthereumSigningParams,
+};
 use relay_utils::HeaderId;
 
 /// Ethereum synchronization parameters.
@@ -63,7 +64,7 @@ pub fn run(params: EthereumDeployContractParams) {
 	} = params;
 
 	let result = local_pool.run_until(async move {
-		let eth_client = EthereumRpcClient::new(eth_params);
+		let eth_client = EthereumClient::new(eth_params);
 		let sub_client = SubstrateRpcClient::new(sub_params, instance).await?;
 
 		let (initial_header_id, initial_header) = prepare_initial_header(&sub_client, sub_initial_header).await?;
@@ -137,7 +138,7 @@ async fn prepare_initial_authorities_set(
 
 /// Deploy bridge contract to Ethereum chain.
 async fn deploy_bridge_contract(
-	eth_client: &EthereumRpcClient,
+	eth_client: &EthereumClient,
 	params: &EthereumSigningParams,
 	contract_code: Vec<u8>,
 	initial_header: Vec<u8>,
diff --git a/bridges/relays/ethereum/src/ethereum_exchange.rs b/bridges/relays/ethereum/src/ethereum_exchange.rs
index e5a821bf3a098f50f12855ef549b79b1eebf4366..59c9a42ca68c4010567b576d2412d26af2a427b0 100644
--- a/bridges/relays/ethereum/src/ethereum_exchange.rs
+++ b/bridges/relays/ethereum/src/ethereum_exchange.rs
@@ -16,13 +16,8 @@
 
 //! Relaying proofs of PoA -> Substrate exchange transactions.
 
-use crate::ethereum_client::{EthereumConnectionParams, EthereumRpcClient};
-use crate::ethereum_types::{
-	EthereumHeaderId, HeaderWithTransactions as EthereumHeaderWithTransactions, Transaction as EthereumTransaction,
-	TransactionHash as EthereumTransactionHash, H256,
-};
 use crate::instances::BridgeInstance;
-use crate::rpc::{EthereumRpc, SubstrateRpc};
+use crate::rpc::SubstrateRpc;
 use crate::rpc_errors::RpcError;
 use crate::substrate_client::{
 	SubmitEthereumExchangeTransactionProof, SubstrateConnectionParams, SubstrateRpcClient, SubstrateSigningParams,
@@ -36,6 +31,13 @@ use exchange_relay::exchange::{
 	TransactionProofPipeline,
 };
 use exchange_relay::exchange_loop::{run as run_loop, InMemoryStorage};
+use relay_ethereum_client::{
+	types::{
+		HeaderId as EthereumHeaderId, HeaderWithTransactions as EthereumHeaderWithTransactions,
+		Transaction as EthereumTransaction, TransactionHash as EthereumTransactionHash, H256, HEADER_ID_PROOF,
+	},
+	Client as EthereumClient, ConnectionParams as EthereumConnectionParams,
+};
 use relay_utils::{metrics::MetricsParams, HeaderId};
 use rialto_runtime::exchange::EthereumTransactionInclusionProof;
 use std::time::Duration;
@@ -92,8 +94,8 @@ impl SourceBlock for EthereumSourceBlock {
 
 	fn id(&self) -> EthereumHeaderId {
 		HeaderId(
-			self.0.number.expect(crate::ethereum_types::HEADER_ID_PROOF).as_u64(),
-			self.0.hash.expect(crate::ethereum_types::HEADER_ID_PROOF),
+			self.0.number.expect(HEADER_ID_PROOF).as_u64(),
+			self.0.hash.expect(HEADER_ID_PROOF),
 		)
 	}
 
@@ -120,7 +122,7 @@ impl SourceTransaction for EthereumSourceTransaction {
 
 /// Ethereum node as transactions proof source.
 struct EthereumTransactionsSource {
-	client: EthereumRpcClient,
+	client: EthereumClient,
 }
 
 #[async_trait]
@@ -136,6 +138,7 @@ impl SourceClient<EthereumToSubstrateExchange> for EthereumTransactionsSource {
 			.header_by_hash_with_transactions(hash)
 			.await
 			.map(EthereumSourceBlock)
+			.map_err(Into::into)
 	}
 
 	async fn block_by_number(&self, number: u64) -> Result<EthereumSourceBlock, Self::Error> {
@@ -143,6 +146,7 @@ impl SourceClient<EthereumToSubstrateExchange> for EthereumTransactionsSource {
 			.header_by_number_with_transactions(number)
 			.await
 			.map(EthereumSourceBlock)
+			.map_err(Into::into)
 	}
 
 	async fn transaction_block(
@@ -278,7 +282,7 @@ fn run_single_transaction_relay(params: EthereumExchangeParams, eth_tx_hash: H25
 	} = params;
 
 	let result = local_pool.run_until(async move {
-		let eth_client = EthereumRpcClient::new(eth_params);
+		let eth_client = EthereumClient::new(eth_params);
 		let sub_client = SubstrateRpcClient::new(sub_params, instance).await?;
 
 		let source = EthereumTransactionsSource { client: eth_client };
@@ -321,7 +325,7 @@ fn run_auto_transactions_relay_loop(params: EthereumExchangeParams, eth_start_wi
 	} = params;
 
 	let do_run_loop = move || -> Result<(), String> {
-		let eth_client = EthereumRpcClient::new(eth_params);
+		let eth_client = EthereumClient::new(eth_params);
 		let sub_client = async_std::task::block_on(SubstrateRpcClient::new(sub_params, instance))
 			.map_err(|err| format!("Error starting Substrate client: {:?}", err))?;
 
diff --git a/bridges/relays/ethereum/src/ethereum_exchange_submit.rs b/bridges/relays/ethereum/src/ethereum_exchange_submit.rs
index 4309b6e894d24d11d94bde78d197ad0475eb06ae..519fdba8cb99af606cdc0eaa5de25b4fab6bf731 100644
--- a/bridges/relays/ethereum/src/ethereum_exchange_submit.rs
+++ b/bridges/relays/ethereum/src/ethereum_exchange_submit.rs
@@ -16,14 +16,14 @@
 
 //! Submitting Ethereum -> Substrate exchange transactions.
 
-use crate::ethereum_client::{EthereumConnectionParams, EthereumRpcClient, EthereumSigningParams};
-use crate::ethereum_types::{CallRequest, U256};
-use crate::rpc::EthereumRpc;
-
 use bp_eth_poa::{
 	signatures::{SecretKey, SignTransaction},
 	UnsignedTransaction,
 };
+use relay_ethereum_client::{
+	types::{CallRequest, U256},
+	Client as EthereumClient, ConnectionParams as EthereumConnectionParams, SigningParams as EthereumSigningParams,
+};
 use rialto_runtime::exchange::LOCK_FUNDS_ADDRESS;
 
 /// Ethereum exchange transaction params.
@@ -54,7 +54,7 @@ pub fn run(params: EthereumExchangeSubmitParams) {
 	} = params;
 
 	let result: Result<_, String> = local_pool.run_until(async move {
-		let eth_client = EthereumRpcClient::new(eth_params);
+		let eth_client = EthereumClient::new(eth_params);
 
 		let eth_signer_address = eth_sign.signer.address();
 		let sub_recipient_encoded = sub_recipient;
diff --git a/bridges/relays/ethereum/src/ethereum_sync_loop.rs b/bridges/relays/ethereum/src/ethereum_sync_loop.rs
index 10779845cbf24300ba574652e36f574384f98211..d5a4ec9b8e103a2c1d7a9974ccd1ffdd00a4c045 100644
--- a/bridges/relays/ethereum/src/ethereum_sync_loop.rs
+++ b/bridges/relays/ethereum/src/ethereum_sync_loop.rs
@@ -16,26 +16,27 @@
 
 //! Ethereum PoA -> Substrate synchronization.
 
-use crate::ethereum_client::{EthereumConnectionParams, EthereumHighLevelRpc, EthereumRpcClient};
-use crate::ethereum_types::{
-	EthereumHeaderId, EthereumHeadersSyncPipeline, EthereumSyncHeader as Header, QueuedEthereumHeader, Receipt,
-};
+use crate::ethereum_client::EthereumHighLevelRpc;
 use crate::instances::BridgeInstance;
-use crate::rpc::{EthereumRpc, SubstrateRpc};
+use crate::rpc::SubstrateRpc;
 use crate::rpc_errors::RpcError;
 use crate::substrate_client::{
 	SubmitEthereumHeaders, SubstrateConnectionParams, SubstrateRpcClient, SubstrateSigningParams,
 };
-use crate::substrate_types::into_substrate_ethereum_header;
+use crate::substrate_types::{into_substrate_ethereum_header, into_substrate_ethereum_receipts};
 
 use async_trait::async_trait;
+use codec::Encode;
 use headers_relay::{
 	sync::{HeadersSyncParams, TargetTransactionMode},
 	sync_loop::{SourceClient, TargetClient},
-	sync_types::{SourceHeader, SubmittedHeaders},
+	sync_types::{HeadersSyncPipeline, QueuedHeader, SourceHeader, SubmittedHeaders},
+};
+use relay_ethereum_client::{
+	types::{HeaderHash, HeaderId as EthereumHeaderId, Receipt, SyncHeader as Header},
+	Client as EthereumClient, ConnectionParams as EthereumConnectionParams,
 };
 use relay_utils::metrics::MetricsParams;
-use web3::types::H256;
 
 use std::fmt::Debug;
 use std::{collections::HashSet, time::Duration};
@@ -78,14 +79,40 @@ pub struct EthereumSyncParams {
 	pub instance: Box<dyn BridgeInstance>,
 }
 
+/// Ethereum synchronization pipeline.
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
+pub struct EthereumHeadersSyncPipeline;
+
+impl HeadersSyncPipeline for EthereumHeadersSyncPipeline {
+	const SOURCE_NAME: &'static str = "Ethereum";
+	const TARGET_NAME: &'static str = "Substrate";
+
+	type Hash = HeaderHash;
+	type Number = u64;
+	type Header = Header;
+	type Extra = Vec<Receipt>;
+	type Completion = ();
+
+	fn estimate_size(source: &QueuedHeader<Self>) -> usize {
+		into_substrate_ethereum_header(source.header()).encode().len()
+			+ into_substrate_ethereum_receipts(source.extra())
+				.map(|extra| extra.encode().len())
+				.unwrap_or(0)
+	}
+}
+
+/// Queued ethereum header ID.
+pub type QueuedEthereumHeader = QueuedHeader<EthereumHeadersSyncPipeline>;
+
 /// Ethereum client as headers source.
 struct EthereumHeadersSource {
 	/// Ethereum node client.
-	client: EthereumRpcClient,
+	client: EthereumClient,
 }
 
 impl EthereumHeadersSource {
-	fn new(client: EthereumRpcClient) -> Self {
+	fn new(client: EthereumClient) -> Self {
 		Self { client }
 	}
 }
@@ -95,15 +122,23 @@ impl SourceClient<EthereumHeadersSyncPipeline> for EthereumHeadersSource {
 	type Error = RpcError;
 
 	async fn best_block_number(&self) -> Result<u64, Self::Error> {
-		self.client.best_block_number().await
+		self.client.best_block_number().await.map_err(Into::into)
 	}
 
-	async fn header_by_hash(&self, hash: H256) -> Result<Header, Self::Error> {
-		self.client.header_by_hash(hash).await.map(Into::into)
+	async fn header_by_hash(&self, hash: HeaderHash) -> Result<Header, Self::Error> {
+		self.client
+			.header_by_hash(hash)
+			.await
+			.map(Into::into)
+			.map_err(Into::into)
 	}
 
 	async fn header_by_number(&self, number: u64) -> Result<Header, Self::Error> {
-		self.client.header_by_number(number).await.map(Into::into)
+		self.client
+			.header_by_number(number)
+			.await
+			.map(Into::into)
+			.map_err(Into::into)
 	}
 
 	async fn header_completion(&self, id: EthereumHeaderId) -> Result<(EthereumHeaderId, Option<()>), Self::Error> {
@@ -192,7 +227,7 @@ pub fn run(params: EthereumSyncParams) -> Result<(), RpcError> {
 		instance,
 	} = params;
 
-	let eth_client = EthereumRpcClient::new(eth_params);
+	let eth_client = EthereumClient::new(eth_params);
 	let sub_client = async_std::task::block_on(async { SubstrateRpcClient::new(sub_params, instance).await })?;
 
 	let sign_sub_transactions = match sync_params.target_tx_mode {
diff --git a/bridges/relays/ethereum/src/instances.rs b/bridges/relays/ethereum/src/instances.rs
index 6fa44740e89c7f3036fa9dca946a3bd29332da12..d2f06c25043424f470db78257a45c576bdeab2a8 100644
--- a/bridges/relays/ethereum/src/instances.rs
+++ b/bridges/relays/ethereum/src/instances.rs
@@ -23,7 +23,7 @@
 //!
 //! This module helps by preparing the correct `Call`s for each of the different pallet instances.
 
-use crate::ethereum_types::QueuedEthereumHeader;
+use crate::ethereum_sync_loop::QueuedEthereumHeader;
 use crate::substrate_types::{into_substrate_ethereum_header, into_substrate_ethereum_receipts};
 
 use rialto_runtime::exchange::EthereumTransactionInclusionProof as Proof;
diff --git a/bridges/relays/ethereum/src/main.rs b/bridges/relays/ethereum/src/main.rs
index 85deab4f1ad4bc27244c80641a7fb26a2172d936..14400c630f9e5dc3a0d6ef19bcb9ed487f361fa8 100644
--- a/bridges/relays/ethereum/src/main.rs
+++ b/bridges/relays/ethereum/src/main.rs
@@ -21,7 +21,6 @@ mod ethereum_deploy_contract;
 mod ethereum_exchange;
 mod ethereum_exchange_submit;
 mod ethereum_sync_loop;
-mod ethereum_types;
 mod instances;
 mod rpc;
 mod rpc_errors;
@@ -29,7 +28,6 @@ mod substrate_client;
 mod substrate_sync_loop;
 mod substrate_types;
 
-use ethereum_client::{EthereumConnectionParams, EthereumSigningParams};
 use ethereum_deploy_contract::EthereumDeployContractParams;
 use ethereum_exchange::EthereumExchangeParams;
 use ethereum_exchange_submit::EthereumExchangeSubmitParams;
@@ -44,6 +42,7 @@ use substrate_client::{SubstrateConnectionParams, SubstrateSigningParams};
 use substrate_sync_loop::SubstrateSyncParams;
 
 use headers_relay::sync::HeadersSyncParams;
+use relay_ethereum_client::{ConnectionParams as EthereumConnectionParams, SigningParams as EthereumSigningParams};
 use std::io::Write;
 
 fn main() {
@@ -250,13 +249,14 @@ fn ethereum_sync_params(matches: &clap::ArgMatches) -> Result<EthereumSyncParams
 fn substrate_sync_params(matches: &clap::ArgMatches) -> Result<SubstrateSyncParams, String> {
 	use crate::substrate_sync_loop::consts::*;
 
-	let eth_contract_address: ethereum_types::Address = if let Some(eth_contract) = matches.value_of("eth-contract") {
-		eth_contract.parse().map_err(|e| format!("{}", e))?
-	} else {
-		"731a10897d267e19b34503ad902d0a29173ba4b1"
-			.parse()
-			.expect("address is hardcoded, thus valid; qed")
-	};
+	let eth_contract_address: relay_ethereum_client::types::Address =
+		if let Some(eth_contract) = matches.value_of("eth-contract") {
+			eth_contract.parse().map_err(|e| format!("{}", e))?
+		} else {
+			"731a10897d267e19b34503ad902d0a29173ba4b1"
+				.parse()
+				.expect("address is hardcoded, thus valid; qed")
+		};
 
 	let params = SubstrateSyncParams {
 		sub_params: substrate_connection_params(matches)?,
@@ -313,7 +313,10 @@ fn ethereum_deploy_contract_params(matches: &clap::ArgMatches) -> Result<Ethereu
 
 fn ethereum_exchange_submit_params(matches: &clap::ArgMatches) -> Result<EthereumExchangeSubmitParams, String> {
 	let eth_nonce = if let Some(eth_nonce) = matches.value_of("eth-nonce") {
-		Some(ethereum_types::U256::from_dec_str(&eth_nonce).map_err(|e| format!("Failed to parse eth-nonce: {}", e))?)
+		Some(
+			relay_ethereum_client::types::U256::from_dec_str(&eth_nonce)
+				.map_err(|e| format!("Failed to parse eth-nonce: {}", e))?,
+		)
 	} else {
 		None
 	};
diff --git a/bridges/relays/ethereum/src/rpc.rs b/bridges/relays/ethereum/src/rpc.rs
index 64b70093f12d42b88a063a43ef27ad8cc148e3fb..ee463e38cecf51b5beaa08a0775c607053136b2c 100644
--- a/bridges/relays/ethereum/src/rpc.rs
+++ b/bridges/relays/ethereum/src/rpc.rs
@@ -23,11 +23,6 @@
 #![allow(unused_variables)]
 use std::result;
 
-use crate::ethereum_types::{
-	Address as EthAddress, Bytes, CallRequest, EthereumHeaderId, Header as EthereumHeader,
-	HeaderWithTransactions as EthereumHeaderWithTransactions, Receipt, SignedRawTx, Transaction as EthereumTransaction,
-	TransactionHash as EthereumTxHash, H256, U256, U64,
-};
 use crate::rpc_errors::RpcError;
 use crate::substrate_types::{
 	Hash as SubstrateHash, Header as SubstrateHeader, Number as SubBlockNumber, SignedBlock as SubstrateBlock,
@@ -35,36 +30,12 @@ use crate::substrate_types::{
 
 use async_trait::async_trait;
 use bp_eth_poa::AuraHeader as SubstrateEthereumHeader;
+use relay_ethereum_client::types::{Bytes, HeaderId as EthereumHeaderId};
 
 type Result<T> = result::Result<T, RpcError>;
 type GrandpaAuthorityList = Vec<u8>;
 
 jsonrpsee::rpc_api! {
-	pub(crate) Ethereum {
-		#[rpc(method = "eth_estimateGas", positional_params)]
-		fn estimate_gas(call_request: CallRequest) -> U256;
-		#[rpc(method = "eth_blockNumber", positional_params)]
-		fn block_number() -> U64;
-		#[rpc(method = "eth_getBlockByNumber", positional_params)]
-		fn get_block_by_number(block_number: U64, full_tx_objs: bool) -> EthereumHeader;
-		#[rpc(method = "eth_getBlockByHash", positional_params)]
-		fn get_block_by_hash(hash: H256, full_tx_objs: bool) -> EthereumHeader;
-		#[rpc(method = "eth_getBlockByNumber", positional_params)]
-		fn get_block_by_number_with_transactions(number: U64, full_tx_objs: bool) -> EthereumHeaderWithTransactions;
-		#[rpc(method = "eth_getBlockByHash", positional_params)]
-		fn get_block_by_hash_with_transactions(hash: H256, full_tx_objs: bool) -> EthereumHeaderWithTransactions;
-		#[rpc(method = "eth_getTransactionByHash", positional_params)]
-		fn transaction_by_hash(hash: H256) -> Option<EthereumTransaction>;
-		#[rpc(method = "eth_getTransactionReceipt", positional_params)]
-		fn get_transaction_receipt(transaction_hash: H256) -> Receipt;
-		#[rpc(method = "eth_getTransactionCount", positional_params)]
-		fn get_transaction_count(address: EthAddress) -> U256;
-		#[rpc(method = "eth_submitTransaction", positional_params)]
-		fn submit_transaction(transaction: Bytes) -> EthereumTxHash;
-		#[rpc(method = "eth_call", positional_params)]
-		fn call(transaction_call: CallRequest) -> Bytes;
-	}
-
 	pub(crate) Substrate {
 		#[rpc(method = "chain_getHeader", positional_params)]
 		fn chain_get_header(block_hash: Option<SubstrateHash>) -> SubstrateHeader;
@@ -81,35 +52,6 @@ jsonrpsee::rpc_api! {
 	}
 }
 
-/// The API for the supported Ethereum RPC methods.
-#[async_trait]
-pub trait EthereumRpc {
-	/// Estimate gas usage for the given call.
-	async fn estimate_gas(&self, call_request: CallRequest) -> Result<U256>;
-	/// Retrieve number of the best known block from the Ethereum node.
-	async fn best_block_number(&self) -> Result<u64>;
-	/// Retrieve block header by its number from Ethereum node.
-	async fn header_by_number(&self, block_number: u64) -> Result<EthereumHeader>;
-	/// Retrieve block header by its hash from Ethereum node.
-	async fn header_by_hash(&self, hash: H256) -> Result<EthereumHeader>;
-	/// Retrieve block header and its transactions by its number from Ethereum node.
-	async fn header_by_number_with_transactions(&self, block_number: u64) -> Result<EthereumHeaderWithTransactions>;
-	/// Retrieve block header and its transactions by its hash from Ethereum node.
-	async fn header_by_hash_with_transactions(&self, hash: H256) -> Result<EthereumHeaderWithTransactions>;
-	/// Retrieve transaction by its hash from Ethereum node.
-	async fn transaction_by_hash(&self, hash: H256) -> Result<Option<EthereumTransaction>>;
-	/// Retrieve transaction receipt by transaction hash.
-	async fn transaction_receipt(&self, transaction_hash: H256) -> Result<Receipt>;
-	/// Get the nonce of the given account.
-	async fn account_nonce(&self, address: EthAddress) -> Result<U256>;
-	/// Submit an Ethereum transaction.
-	///
-	/// The transaction must already be signed before sending it through this method.
-	async fn submit_transaction(&self, signed_raw_tx: SignedRawTx) -> Result<EthereumTxHash>;
-	/// Submit a call to an Ethereum smart contract.
-	async fn eth_call(&self, call_transaction: CallRequest) -> Result<Bytes>;
-}
-
 /// The API for the supported Substrate RPC methods.
 #[async_trait]
 pub trait SubstrateRpc {
diff --git a/bridges/relays/ethereum/src/rpc_errors.rs b/bridges/relays/ethereum/src/rpc_errors.rs
index 747ec151f1e43c4094f51d0a3c7c0a2127d3ac05..5e01031968fc73ee176434de442572c876948df6 100644
--- a/bridges/relays/ethereum/src/rpc_errors.rs
+++ b/bridges/relays/ethereum/src/rpc_errors.rs
@@ -15,6 +15,7 @@
 // along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
 
 use jsonrpsee::client::RequestError;
+use relay_ethereum_client::Error as EthereumNodeError;
 use relay_utils::MaybeConnectionError;
 
 /// Contains common errors that can occur when
@@ -76,7 +77,11 @@ impl From<ethabi::Error> for RpcError {
 
 impl MaybeConnectionError for RpcError {
 	fn is_connection_error(&self) -> bool {
-		matches!(*self, RpcError::Request(RequestError::TransportError(_)))
+		match self {
+			RpcError::Request(RequestError::TransportError(_)) => true,
+			RpcError::Ethereum(ref error) => error.is_connection_error(),
+			_ => false,
+		}
 	}
 }
 
@@ -86,38 +91,6 @@ impl From<codec::Error> for RpcError {
 	}
 }
 
-/// Errors that can occur only when interacting with
-/// an Ethereum node through RPC.
-#[derive(Debug)]
-pub enum EthereumNodeError {
-	/// Failed to parse response.
-	ResponseParseFailed(String),
-	/// We have received a header with missing fields.
-	IncompleteHeader,
-	/// We have received a transaction missing a `raw` field.
-	IncompleteTransaction,
-	/// An invalid Substrate block number was received from
-	/// an Ethereum node.
-	InvalidSubstrateBlockNumber,
-	/// An invalid index has been received from an Ethereum node.
-	InvalidIncompleteIndex,
-}
-
-impl ToString for EthereumNodeError {
-	fn to_string(&self) -> String {
-		match self {
-			Self::ResponseParseFailed(e) => e.to_string(),
-			Self::IncompleteHeader => {
-				"Incomplete Ethereum Header Received (missing some of required fields - hash, number, logs_bloom)"
-					.to_string()
-			}
-			Self::IncompleteTransaction => "Incomplete Ethereum Transaction (missing required field - raw)".to_string(),
-			Self::InvalidSubstrateBlockNumber => "Received an invalid Substrate block from Ethereum Node".to_string(),
-			Self::InvalidIncompleteIndex => "Received an invalid incomplete index from Ethereum Node".to_string(),
-		}
-	}
-}
-
 /// Errors that can occur only when interacting with
 /// a Substrate node through RPC.
 #[derive(Debug)]
diff --git a/bridges/relays/ethereum/src/substrate_client.rs b/bridges/relays/ethereum/src/substrate_client.rs
index aadde0ec14f204a15450ee9016e8cd68f9cc631f..4b1765aa2be05dca964c3207ecb519be05cd9c65 100644
--- a/bridges/relays/ethereum/src/substrate_client.rs
+++ b/bridges/relays/ethereum/src/substrate_client.rs
@@ -14,7 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
 
-use crate::ethereum_types::{Bytes, EthereumHeaderId, QueuedEthereumHeader, H256};
+use crate::ethereum_sync_loop::QueuedEthereumHeader;
 use crate::instances::BridgeInstance;
 use crate::rpc::{Substrate, SubstrateRpc};
 use crate::rpc_errors::RpcError;
@@ -28,6 +28,7 @@ use jsonrpsee::raw::RawClient;
 use jsonrpsee::transport::http::HttpTransportClient;
 use jsonrpsee::Client;
 use num_traits::Zero;
+use relay_ethereum_client::types::{Bytes, HeaderId as EthereumHeaderId, H256};
 use relay_utils::HeaderId;
 use sp_core::crypto::Pair;
 use sp_runtime::traits::IdentifyAccount;
diff --git a/bridges/relays/ethereum/src/substrate_sync_loop.rs b/bridges/relays/ethereum/src/substrate_sync_loop.rs
index ce3a5ae4d31411fa72858973e88528c272bb73fe..8c3e0fb91d1938582d5020a2a08676165a2876c1 100644
--- a/bridges/relays/ethereum/src/substrate_sync_loop.rs
+++ b/bridges/relays/ethereum/src/substrate_sync_loop.rs
@@ -16,10 +16,7 @@
 
 //! Substrate -> Ethereum synchronization.
 
-use crate::ethereum_client::{
-	EthereumConnectionParams, EthereumHighLevelRpc, EthereumRpcClient, EthereumSigningParams,
-};
-use crate::ethereum_types::Address;
+use crate::ethereum_client::EthereumHighLevelRpc;
 use crate::instances::BridgeInstance;
 use crate::rpc::SubstrateRpc;
 use crate::rpc_errors::RpcError;
@@ -35,6 +32,10 @@ use headers_relay::{
 	sync_loop::{SourceClient, TargetClient},
 	sync_types::{SourceHeader, SubmittedHeaders},
 };
+use relay_ethereum_client::{
+	types::Address, Client as EthereumClient, ConnectionParams as EthereumConnectionParams,
+	SigningParams as EthereumSigningParams,
+};
 use relay_utils::metrics::MetricsParams;
 
 use std::fmt::Debug;
@@ -125,7 +126,7 @@ impl SourceClient<SubstrateHeadersSyncPipeline> for SubstrateHeadersSource {
 /// Ethereum client as Substrate headers target.
 struct EthereumHeadersTarget {
 	/// Ethereum node client.
-	client: EthereumRpcClient,
+	client: EthereumClient,
 	/// Bridge contract address.
 	contract: Address,
 	/// Ethereum signing params.
@@ -133,7 +134,7 @@ struct EthereumHeadersTarget {
 }
 
 impl EthereumHeadersTarget {
-	fn new(client: EthereumRpcClient, contract: Address, sign_params: EthereumSigningParams) -> Self {
+	fn new(client: EthereumClient, contract: Address, sign_params: EthereumSigningParams) -> Self {
 		Self {
 			client,
 			contract,
@@ -194,7 +195,7 @@ pub fn run(params: SubstrateSyncParams) -> Result<(), RpcError> {
 		instance,
 	} = params;
 
-	let eth_client = EthereumRpcClient::new(eth_params);
+	let eth_client = EthereumClient::new(eth_params);
 	let sub_client = async_std::task::block_on(async { SubstrateRpcClient::new(sub_params, instance).await })?;
 
 	let target = EthereumHeadersTarget::new(eth_client, eth_contract_address, eth_sign);
diff --git a/bridges/relays/ethereum/src/substrate_types.rs b/bridges/relays/ethereum/src/substrate_types.rs
index 4f5328b778763fbbec8107b070ca83ad1adadf3e..793358be03a476d1ddf07012c5a1a4018179b75e 100644
--- a/bridges/relays/ethereum/src/substrate_types.rs
+++ b/bridges/relays/ethereum/src/substrate_types.rs
@@ -14,10 +14,6 @@
 // You should have received a copy of the GNU General Public License
 // along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
 
-use crate::ethereum_types::{
-	Header as EthereumHeader, Receipt as EthereumReceipt, HEADER_ID_PROOF as ETHEREUM_HEADER_ID_PROOF,
-};
-
 use codec::Encode;
 use headers_relay::sync_types::{HeadersSyncPipeline, QueuedHeader, SourceHeader};
 use relay_utils::HeaderId;
@@ -26,6 +22,9 @@ pub use bp_eth_poa::{
 	Address, AuraHeader as SubstrateEthereumHeader, Bloom, Bytes, LogEntry as SubstrateEthereumLogEntry,
 	Receipt as SubstrateEthereumReceipt, TransactionOutcome as SubstrateEthereumTransactionOutcome, H256, U256,
 };
+use relay_ethereum_client::types::{
+	Header as EthereumHeader, Receipt as EthereumReceipt, HEADER_ID_PROOF as ETHEREUM_HEADER_ID_PROOF,
+};
 
 /// Substrate header hash.
 pub type Hash = rialto_runtime::Hash;