wallet refactor

This commit is contained in:
Georgios Konstantopoulos 2020-05-24 18:40:16 +03:00
parent 07daa1a891
commit f82f8db57f
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
10 changed files with 160 additions and 210 deletions

View File

@ -4,7 +4,7 @@
pub mod providers; pub mod providers;
pub mod wallet; pub mod signers;
/// Ethereum related datatypes /// Ethereum related datatypes
pub mod types; pub mod types;
@ -12,7 +12,4 @@ pub mod types;
/// Re-export solc for convenience /// Re-export solc for convenience
pub use solc; pub use solc;
/// JSON-RPC client
mod jsonrpc;
mod utils; mod utils;

View File

@ -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<HttpClient> for Provider {
fn from(src: HttpClient) -> Self {
Self(src)
}
}
impl TryFrom<&str> for Provider {
type Error = ParseError;
fn try_from(src: &str) -> Result<Self, Self::Error> {
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<U256, Self::Error> {
self.0.request("eth_blockNumber", None::<()>).await
}
async fn get_transaction<T: Send + Sync + Into<TxHash>>(
&self,
hash: T,
) -> Result<Transaction, Self::Error> {
let hash = hash.into();
self.0.request("eth_getTransactionByHash", Some(hash)).await
}
async fn send_transaction(&self, tx: TransactionRequest) -> Result<TxHash, Self::Error> {
self.0.request("eth_sendTransaction", Some(vec![tx])).await
}
async fn send_raw_transaction(&self, rlp: &Bytes) -> Result<TxHash, Self::Error> {
let rlp = utils::serialize(&rlp);
self.0.request("eth_sendRawTransaction", Some(rlp)).await
}
async fn get_transaction_count(
&self,
from: Address,
block: Option<BlockNumber>,
) -> Result<U256, Self::Error> {
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<U256, Self::Error>;
/// Gets a transaction by it shash
async fn get_transaction<T: Into<TxHash> + Send + Sync>(
&self,
tx_hash: T,
) -> Result<Transaction, Self::Error>;
/// Sends a transaciton request to the node
async fn send_transaction(&self, tx: TransactionRequest) -> Result<TxHash, Self::Error>;
/// Broadcasts an RLP encoded signed transaction
async fn send_raw_transaction(&self, tx: &Bytes) -> Result<TxHash, Self::Error>;
async fn get_transaction_count(
&self,
from: Address,
block: Option<BlockNumber>,
) -> Result<U256, Self::Error>;
}
#[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);
}
}

View File

@ -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) //! 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 reqwest::{Client, Error as ReqwestError};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::fmt; use std::{
use std::sync::atomic::{AtomicU64, Ordering}; convert::TryFrom,
fmt,
sync::atomic::{AtomicU64, Ordering},
};
use thiserror::Error; use thiserror::Error;
use url::Url; use url::{ParseError, Url};
/// JSON-RPC 2.0 Client /// An HTTP Client
#[derive(Debug)] #[derive(Debug)]
pub struct HttpClient { pub struct Provider {
id: AtomicU64, id: AtomicU64,
client: Client, client: Client,
url: Url, url: Url,
} }
impl Clone for HttpClient { #[derive(Error, Debug)]
fn clone(&self) -> Self { pub enum ClientError {
Self { #[error(transparent)]
id: AtomicU64::new(0), ReqwestError(#[from] ReqwestError),
client: self.client.clone(), #[error(transparent)]
url: self.url.clone(), JsonRpcError(#[from] JsonRpcError),
}
}
} }
impl HttpClient { #[async_trait]
/// Initializes a new HTTP Client impl JsonRpcClient for Provider {
pub fn new(url: impl Into<Url>) -> Self { type Error = ClientError;
Self {
id: AtomicU64::new(0),
client: Client::new(),
url: url.into(),
}
}
/// Sends a POST request with the provided method and the params serialized as JSON /// Sends a POST request with the provided method and the params serialized as JSON
pub async fn request<T: Serialize, R: for<'a> Deserialize<'a>>( /// over HTTP
async fn request<T: Serialize + Send + Sync, R: for<'a> Deserialize<'a>>(
&self, &self,
method: &str, method: &str,
params: Option<T>, params: Option<T>,
@ -59,12 +58,33 @@ impl HttpClient {
} }
} }
#[derive(Error, Debug)] impl Provider {
pub enum ClientError { /// Initializes a new HTTP Client
#[error(transparent)] pub fn new(url: impl Into<Url>) -> Self {
ReqwestError(#[from] ReqwestError), Self {
#[error(transparent)] id: AtomicU64::new(0),
JsonRpcError(#[from] JsonRpcError), client: Client::new(),
url: url.into(),
}
}
}
impl TryFrom<&str> for Provider {
type Error = ParseError;
fn try_from(src: &str) -> Result<Self, Self::Error> {
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)] #[derive(Serialize, Deserialize, Debug, Clone, Error)]

72
src/providers/mod.rs Normal file
View File

@ -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<http::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<T: Serialize + Send + Sync, R: for<'a> Deserialize<'a>>(
&self,
method: &str,
params: Option<T>,
) -> Result<R, Self::Error>;
}
/// 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>(P);
// JSON RPC bindings
impl<P: JsonRpcClient> Provider<P> {
pub async fn get_block_number(&self) -> Result<U256, P::Error> {
self.0.request("eth_blockNumber", None::<()>).await
}
pub async fn get_transaction<T: Send + Sync + Into<TxHash>>(
&self,
hash: T,
) -> Result<Transaction, P::Error> {
let hash = hash.into();
self.0.request("eth_getTransactionByHash", Some(hash)).await
}
pub async fn send_transaction(&self, tx: TransactionRequest) -> Result<TxHash, P::Error> {
self.0.request("eth_sendTransaction", Some(tx)).await
}
pub async fn send_raw_transaction(&self, rlp: &Bytes) -> Result<TxHash, P::Error> {
let rlp = utils::serialize(&rlp);
self.0.request("eth_sendRawTransaction", Some(rlp)).await
}
pub async fn get_transaction_count(
&self,
from: Address,
block: Option<BlockNumber>,
) -> Result<U256, P::Error> {
let from = utils::serialize(&from);
let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest));
self.0
.request("eth_getTransactionCount", Some(&[from, block]))
.await
}
}

31
src/signers/client.rs Normal file
View File

@ -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<P>,
pub signer: S,
}
impl<'a, S: Signer, P: JsonRpcClient> Client<'a, S, P> {
pub async fn send_transaction(&self, tx: UnsignedTransaction) -> Result<Transaction, P::Error> {
// 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<T: Send + Sync + Into<TxHash>>(
&self,
hash: T,
) -> Result<Transaction, P::Error> {
self.provider.get_transaction(hash).await
}
}

View File

@ -28,7 +28,7 @@ impl Network for EIP155Disabled {
pub mod instantiated { pub mod instantiated {
use super::*; use super::*;
use crate::wallet::Wallet; use crate::signers::Wallet;
pub type MainnetWallet = Wallet<Mainnet>; pub type MainnetWallet = Wallet<Mainnet>;
pub type AnyWallet = Wallet<EIP155Disabled>; pub type AnyWallet = Wallet<EIP155Disabled>;

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
providers::Provider, providers::{JsonRpcClient, Provider},
signers::{Client, Network, Signer},
types::{Address, PrivateKey, PublicKey, Signature, Transaction, UnsignedTransaction}, types::{Address, PrivateKey, PublicKey, Signature, Transaction, UnsignedTransaction},
wallet::{Client, Network, Signer},
}; };
use rand::Rng; use rand::Rng;
@ -43,8 +43,8 @@ impl<N: Network> Wallet<N> {
} }
} }
/// Connects to a provider and returns a signer /// Connects to a provider and returns a client
pub fn connect(self, provider: &Provider) -> Client<Wallet<N>> { pub fn connect<P: JsonRpcClient>(self, provider: &Provider<P>) -> Client<Wallet<N>, P> {
Client { Client {
signer: self, signer: self,
provider, provider,

View File

@ -12,7 +12,7 @@ pub use transaction::{Transaction, TransactionRequest, UnsignedTransaction};
mod keys; mod keys;
pub use keys::{PrivateKey, PublicKey}; pub use keys::{PrivateKey, PublicKey};
pub mod signature; mod signature;
pub use signature::Signature; pub use signature::Signature;
mod bytes; mod bytes;

View File

@ -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<Transaction, SignerError> {
// 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<T: Send + Sync + Into<TxHash>>(
&self,
hash: T,
) -> Result<Transaction, ClientError> {
self.provider.get_transaction(hash).await
}
}