diff --git a/src/lib.rs b/src/lib.rs index f2b1aedf..2a574067 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ pub mod providers; -pub mod wallet; +pub mod signers; /// Ethereum related datatypes pub mod types; @@ -12,7 +12,4 @@ pub mod types; /// Re-export solc for convenience pub use solc; -/// JSON-RPC client -mod jsonrpc; - mod utils; diff --git a/src/providers.rs b/src/providers.rs deleted file mode 100644 index b02a6605..00000000 --- a/src/providers.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::{ - jsonrpc::{ClientError, HttpClient}, - types::{Address, BlockNumber, Bytes, Transaction, TransactionRequest, TxHash, U256}, - utils, -}; -use async_trait::async_trait; -use std::convert::TryFrom; -use url::{ParseError, Url}; - -/// An Ethereum JSON-RPC compatible backend -#[derive(Clone, Debug)] -pub struct Provider(HttpClient); - -impl From for Provider { - fn from(src: HttpClient) -> Self { - Self(src) - } -} - -impl TryFrom<&str> for Provider { - type Error = ParseError; - - fn try_from(src: &str) -> Result { - Ok(Provider(HttpClient::new(Url::parse(src)?))) - } -} - -#[async_trait] -// TODO: Figure out a way to re-use the arguments with various transports -> need a trait which has a -// `request` method -impl ProviderTrait for Provider { - type Error = ClientError; - - async fn get_block_number(&self) -> Result { - self.0.request("eth_blockNumber", None::<()>).await - } - - async fn get_transaction>( - &self, - hash: T, - ) -> Result { - let hash = hash.into(); - self.0.request("eth_getTransactionByHash", Some(hash)).await - } - - async fn send_transaction(&self, tx: TransactionRequest) -> Result { - self.0.request("eth_sendTransaction", Some(vec![tx])).await - } - - async fn send_raw_transaction(&self, rlp: &Bytes) -> Result { - let rlp = utils::serialize(&rlp); - self.0.request("eth_sendRawTransaction", Some(rlp)).await - } - - async fn get_transaction_count( - &self, - from: Address, - block: Option, - ) -> Result { - let from = utils::serialize(&from); - let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest)); - self.0 - .request("eth_getTransactionCount", Some(&[from, block])) - .await - } -} - -/// Trait for providing backend services. Different implementations for this may be used for using -/// indexers or using multiple providers at the same time -#[async_trait] -pub trait ProviderTrait { - type Error; - - async fn get_block_number(&self) -> Result; - - /// Gets a transaction by it shash - async fn get_transaction + Send + Sync>( - &self, - tx_hash: T, - ) -> Result; - - /// Sends a transaciton request to the node - async fn send_transaction(&self, tx: TransactionRequest) -> Result; - - /// Broadcasts an RLP encoded signed transaction - async fn send_raw_transaction(&self, tx: &Bytes) -> Result; - - async fn get_transaction_count( - &self, - from: Address, - block: Option, - ) -> Result; -} - -#[cfg(test)] -mod tests { - use super::*; - use ethereum_types::Address; - use std::str::FromStr; - - // TODO: Make a Ganache helper - - #[tokio::test] - async fn get_balance() { - let provider = Provider::try_from("http://localhost:8545").unwrap(); - let num = provider.get_block_number().await.unwrap(); - assert_eq!(num, U256::from(0)); - } - - #[tokio::test] - async fn send_transaction() { - let provider = Provider::try_from("http://localhost:8545").unwrap(); - let tx_req = TransactionRequest { - from: Address::from_str("e98C5Abe55bD5478717BC67DcE404B8730672298").unwrap(), - to: Some(Address::from_str("d5CB69Fb66809B7Ca203DAe8fB571DD291a86764").unwrap()), - nonce: None, - data: None, - value: Some(1000.into()), - gas_price: None, - gas: None, - }; - let tx_hash = provider.send_transaction(tx_req).await.unwrap(); - dbg!(tx_hash); - } -} diff --git a/src/jsonrpc.rs b/src/providers/http.rs similarity index 83% rename from src/jsonrpc.rs rename to src/providers/http.rs index f744bcb7..95c1907d 100644 --- a/src/jsonrpc.rs +++ b/src/providers/http.rs @@ -1,43 +1,42 @@ -//! Minimal JSON-RPC 2.0 Client +//! Minimal HTTP JSON-RPC 2.0 Client //! The request/response code is taken from [here](https://github.com/althea-net/guac_rs/blob/master/web3/src/jsonrpc) +use crate::providers::JsonRpcClient; + +use async_trait::async_trait; use reqwest::{Client, Error as ReqwestError}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::fmt; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::{ + convert::TryFrom, + fmt, + sync::atomic::{AtomicU64, Ordering}, +}; use thiserror::Error; -use url::Url; +use url::{ParseError, Url}; -/// JSON-RPC 2.0 Client +/// An HTTP Client #[derive(Debug)] -pub struct HttpClient { +pub struct Provider { id: AtomicU64, client: Client, url: Url, } -impl Clone for HttpClient { - fn clone(&self) -> Self { - Self { - id: AtomicU64::new(0), - client: self.client.clone(), - url: self.url.clone(), - } - } +#[derive(Error, Debug)] +pub enum ClientError { + #[error(transparent)] + ReqwestError(#[from] ReqwestError), + #[error(transparent)] + JsonRpcError(#[from] JsonRpcError), } -impl HttpClient { - /// Initializes a new HTTP Client - pub fn new(url: impl Into) -> Self { - Self { - id: AtomicU64::new(0), - client: Client::new(), - url: url.into(), - } - } +#[async_trait] +impl JsonRpcClient for Provider { + type Error = ClientError; /// Sends a POST request with the provided method and the params serialized as JSON - pub async fn request Deserialize<'a>>( + /// over HTTP + async fn request Deserialize<'a>>( &self, method: &str, params: Option, @@ -59,12 +58,33 @@ impl HttpClient { } } -#[derive(Error, Debug)] -pub enum ClientError { - #[error(transparent)] - ReqwestError(#[from] ReqwestError), - #[error(transparent)] - JsonRpcError(#[from] JsonRpcError), +impl Provider { + /// Initializes a new HTTP Client + pub fn new(url: impl Into) -> Self { + Self { + id: AtomicU64::new(0), + client: Client::new(), + url: url.into(), + } + } +} + +impl TryFrom<&str> for Provider { + type Error = ParseError; + + fn try_from(src: &str) -> Result { + Ok(Provider::new(Url::parse(src)?)) + } +} + +impl Clone for Provider { + fn clone(&self) -> Self { + Self { + id: AtomicU64::new(0), + client: self.client.clone(), + url: self.url.clone(), + } + } } #[derive(Serialize, Deserialize, Debug, Clone, Error)] diff --git a/src/providers/mod.rs b/src/providers/mod.rs new file mode 100644 index 00000000..2af270b9 --- /dev/null +++ b/src/providers/mod.rs @@ -0,0 +1,72 @@ +//! Ethereum compatible providers +//! Currently supported: +//! - Raw HTTP POST requests +//! +//! TODO: WebSockets, multiple backends, popular APIs etc. +mod http; + +use crate::{ + types::{Address, BlockNumber, Bytes, Transaction, TransactionRequest, TxHash, U256}, + utils, +}; + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::{error::Error, fmt::Debug}; + +/// An HTTP provider for interacting with an Ethereum-compatible blockchain +pub type HttpProvider = Provider; + +#[async_trait] +/// Implement this trait in order to plug in different backends +pub trait JsonRpcClient: Debug { + type Error: Error; + + /// Sends a request with the provided method and the params serialized as JSON + async fn request Deserialize<'a>>( + &self, + method: &str, + params: Option, + ) -> Result; +} + +/// An abstract provider for interacting with the [Ethereum JSON RPC +/// API](https://github.com/ethereum/wiki/wiki/JSON-RPC) +#[derive(Clone, Debug)] +pub struct Provider

(P); + +// JSON RPC bindings +impl Provider

{ + pub async fn get_block_number(&self) -> Result { + self.0.request("eth_blockNumber", None::<()>).await + } + + pub async fn get_transaction>( + &self, + hash: T, + ) -> Result { + let hash = hash.into(); + self.0.request("eth_getTransactionByHash", Some(hash)).await + } + + pub async fn send_transaction(&self, tx: TransactionRequest) -> Result { + self.0.request("eth_sendTransaction", Some(tx)).await + } + + pub async fn send_raw_transaction(&self, rlp: &Bytes) -> Result { + let rlp = utils::serialize(&rlp); + self.0.request("eth_sendRawTransaction", Some(rlp)).await + } + + pub async fn get_transaction_count( + &self, + from: Address, + block: Option, + ) -> Result { + let from = utils::serialize(&from); + let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest)); + self.0 + .request("eth_getTransactionCount", Some(&[from, block])) + .await + } +} diff --git a/src/signers/client.rs b/src/signers/client.rs new file mode 100644 index 00000000..9049eb27 --- /dev/null +++ b/src/signers/client.rs @@ -0,0 +1,31 @@ +use crate::{ + providers::{JsonRpcClient, Provider}, + signers::Signer, + types::{Transaction, TxHash, UnsignedTransaction}, +}; + +#[derive(Clone, Debug)] +pub struct Client<'a, S, P> { + pub(super) provider: &'a Provider

, + pub signer: S, +} + +impl<'a, S: Signer, P: JsonRpcClient> Client<'a, S, P> { + pub async fn send_transaction(&self, tx: UnsignedTransaction) -> Result { + // sign the transaction + let signed_tx = self.signer.sign_transaction(tx.clone()); + + // broadcast it + self.provider.send_raw_transaction(&signed_tx.rlp()).await?; + + Ok(signed_tx) + } + + // TODO: Forward all other calls to the provider + pub async fn get_transaction>( + &self, + hash: T, + ) -> Result { + self.provider.get_transaction(hash).await + } +} diff --git a/src/wallet/mod.rs b/src/signers/mod.rs similarity index 100% rename from src/wallet/mod.rs rename to src/signers/mod.rs diff --git a/src/wallet/networks.rs b/src/signers/networks.rs similarity index 96% rename from src/wallet/networks.rs rename to src/signers/networks.rs index 3c655137..92a73377 100644 --- a/src/wallet/networks.rs +++ b/src/signers/networks.rs @@ -28,7 +28,7 @@ impl Network for EIP155Disabled { pub mod instantiated { use super::*; - use crate::wallet::Wallet; + use crate::signers::Wallet; pub type MainnetWallet = Wallet; pub type AnyWallet = Wallet; diff --git a/src/wallet/wallet.rs b/src/signers/wallet.rs similarity index 88% rename from src/wallet/wallet.rs rename to src/signers/wallet.rs index 4c4ea456..b484709d 100644 --- a/src/wallet/wallet.rs +++ b/src/signers/wallet.rs @@ -1,7 +1,7 @@ use crate::{ - providers::Provider, + providers::{JsonRpcClient, Provider}, + signers::{Client, Network, Signer}, types::{Address, PrivateKey, PublicKey, Signature, Transaction, UnsignedTransaction}, - wallet::{Client, Network, Signer}, }; use rand::Rng; @@ -43,8 +43,8 @@ impl Wallet { } } - /// Connects to a provider and returns a signer - pub fn connect(self, provider: &Provider) -> Client> { + /// Connects to a provider and returns a client + pub fn connect(self, provider: &Provider

) -> Client, P> { Client { signer: self, provider, diff --git a/src/types/mod.rs b/src/types/mod.rs index 9fa855bd..542c1724 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -12,7 +12,7 @@ pub use transaction::{Transaction, TransactionRequest, UnsignedTransaction}; mod keys; pub use keys::{PrivateKey, PublicKey}; -pub mod signature; +mod signature; pub use signature::Signature; mod bytes; diff --git a/src/wallet/client.rs b/src/wallet/client.rs deleted file mode 100644 index bce6bb85..00000000 --- a/src/wallet/client.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::{ - jsonrpc::ClientError, - providers::{Provider, ProviderTrait}, - types::{Transaction, TxHash, UnsignedTransaction}, - wallet::Signer, -}; - -use thiserror::Error; - -#[derive(Clone, Debug)] -pub struct Client<'a, S> { - pub(super) provider: &'a Provider, - pub signer: S, -} - -#[derive(Error, Debug)] -pub enum SignerError { - #[error(transparent)] - ClientError(#[from] ClientError), - #[error("no provider was found")] - NoProvider, -} - -impl<'a, S: Signer> Client<'a, S> { - pub async fn send_transaction( - &self, - tx: UnsignedTransaction, - ) -> Result { - // sign the transaction - let signed_tx = self.signer.sign_transaction(tx.clone()); - - // broadcast it - self.provider.send_raw_transaction(&signed_tx.rlp()).await?; - - Ok(signed_tx) - } - - // TODO: Forward all other calls to the provider - pub async fn get_transaction>( - &self, - hash: T, - ) -> Result { - self.provider.get_transaction(hash).await - } -}