From 1a49a62a8145a13fd87154b352af08e3210b1a7e Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Fri, 22 May 2020 21:37:21 +0300 Subject: [PATCH] init --- .gitignore | 2 + Cargo.toml | 19 ++++++ README.md | 44 +++++++++++++ examples/transfer_eth.rs | 15 +++++ src/jsonrpc.rs | 138 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 16 +++++ src/network.rs | 18 +++++ src/primitives.rs | 2 + src/providers.rs | 50 ++++++++++++++ src/transaction.rs | 0 src/wallet.rs | 23 +++++++ 11 files changed, 327 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 examples/transfer_eth.rs create mode 100644 src/jsonrpc.rs create mode 100644 src/lib.rs create mode 100644 src/network.rs create mode 100644 src/primitives.rs create mode 100644 src/providers.rs create mode 100644 src/transaction.rs create mode 100644 src/wallet.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..96ef6c0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..54c58e8f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ethers" +version = "0.1.0" +authors = ["Georgios Konstantopoulos "] +edition = "2018" + +[dependencies] +solc = { git = "https://github.com/paritytech/rust_solc "} +ethereum-types = "0.9.2" +url = "2.1.1" +once_cell = "1.4.0" +async-trait = "0.1.31" +reqwest = { version = "0.10.4", features = ["json"] } +serde = { version = "1.0.110", features = ["derive"] } +serde_json = "1.0.53" +thiserror = "1.0.19" + +[dev-dependencies] +tokio = { version = "0.2.21", features = ["macros"] } diff --git a/README.md b/README.md new file mode 100644 index 00000000..174647a2 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +Provider model + +should be usable both for tests and for normal actions + + +Features +- [ ] Keep your private keys in your client, safe and sound +- [ ] Import and export JSON wallets (Geth, Parity and crowdsale) +- [ ] Import and export BIP 39 mnemonic phrases (12 word backup phrases) and HD Wallets (English, Italian, Japanese, Korean, Simplified Chinese, Traditional Chinese; more coming soon) +- [ ] Meta-classes create JavaScript objects from any contract ABI, including ABIv2 and Human-Readable ABI +- [ ] Connect to Ethereum nodes over JSON-RPC, INFURA, Etherscan, Alchemy, Cloudflare or MetaMask. +- [ ] ENS names are first-class citizens; they can be used anywhere an Ethereum addresses can be used +- [ ] Tiny (~88kb compressed; 284kb uncompressed) +- [ ] Complete functionality for all your Ethereum needs +- [ ] Extensive documentation +- [ ] Large collection of test cases which are maintained and added to +- [ ] Fully TypeScript ready, with definition files and full TypeScript source +- [ ] MIT License (including ALL dependencies); completely open source to do with as you please +- [ ] Compatible with ethers.js and Metamask web3 providers via WASM +- Calls by default are made async -> provide a synchronous API + + +- Provider +- Signer +- Contract +- Choice of BigNumber library? Probably the ones in ethabi +- Human readable ABI very important +- https://docs-beta.ethers.io/getting-started/ +- Supports IN-EVM Testing -> SUPER fast tests which are ALSO typesafe +- build.rs type safe methods + +This library is inspired by the APIs of Riemann Ether and Ethers https://github.com/summa-tx/riemann-ether#development + +- Listening to events via HTTP polls, while WS push/pulls -> look at rust-web3 + +- Async std futures 1.0 RPC calls for everything `rpc_impl!` using reqwest and mockito + +https://github.com/ethers-io/ethers.js/blob/ethers-v5-beta/packages/contracts/src.ts/index.ts#L721 + +- Wallet + +ethers::wallet::new().connect(provider) +::get_default_provider() + diff --git a/examples/transfer_eth.rs b/examples/transfer_eth.rs new file mode 100644 index 00000000..7234899b --- /dev/null +++ b/examples/transfer_eth.rs @@ -0,0 +1,15 @@ +use ethers::providers::{Provider, ProviderTrait}; +use ethers::wallet::Signer; +use std::convert::TryFrom; + +#[tokio::main] +async fn main() { + let provider = + Provider::try_from("https://mainnet.infura.io/v3/4aebe67796c64b95ab20802677b7bb55") + .unwrap(); + + let num = provider.get_block_number().await.unwrap(); + dbg!(num); + + // let signer = Signer::random().connect(&provider); +} diff --git a/src/jsonrpc.rs b/src/jsonrpc.rs new file mode 100644 index 00000000..92dd84cd --- /dev/null +++ b/src/jsonrpc.rs @@ -0,0 +1,138 @@ +//! Minimal 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 reqwest::{Client, Error as ReqwestError}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::fmt; +use std::sync::atomic::{AtomicU64, Ordering}; +use thiserror::Error; +use url::Url; + +#[derive(Debug)] +/// JSON-RPC 2.0 Client +pub struct HttpClient { + id: AtomicU64, + client: Client, + url: Url, +} + +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(), + } + } + + /// Sends a POST request with the provided method and the params serialized as JSON + pub async fn request Deserialize<'a>>( + &self, + method: &str, + params: Option, + ) -> Result { + let next_id = self.id.load(Ordering::SeqCst) + 1; + self.id.store(next_id, Ordering::SeqCst); + + let payload = Request::new(next_id, method, params); + + let res = self + .client + .post(self.url.as_ref()) + .json(&payload) + .send() + .await?; + let res = res.json::>().await?; + + Ok(res.data.into_result()?) + } +} + +#[derive(Error, Debug)] +pub enum ClientError { + #[error(transparent)] + ReqwestError(#[from] ReqwestError), + #[error(transparent)] + JsonRpcError(#[from] JsonRpcError), +} + +#[derive(Serialize, Deserialize, Debug, Clone, Error)] +/// A JSON-RPC 2.0 error +pub struct JsonRpcError { + /// The error code + pub code: i64, + /// The error message + pub message: String, + /// Additional data + pub data: Option, +} + +impl fmt::Display for JsonRpcError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "(code: {}, message: {}, data: {:?})", + self.code, self.message, self.data + ) + } +} + +#[derive(Serialize, Deserialize, Debug)] +/// A JSON-RPC request +struct Request<'a, T> { + id: u64, + jsonrpc: &'a str, + method: &'a str, + params: Option, +} + +impl<'a, T> Request<'a, T> { + /// Creates a new JSON RPC request + fn new(id: u64, method: &'a str, params: Option) -> Self { + Self { + id, + jsonrpc: "2.0", + method, + params, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct Response { + id: u64, + jsonrpc: String, + #[serde(flatten)] + data: ResponseData, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +enum ResponseData { + Error { error: JsonRpcError }, + Success { result: R }, +} + +impl ResponseData { + /// Consume response and return value + fn into_result(self) -> Result { + match self { + ResponseData::Success { result } => Ok(result), + ResponseData::Error { error } => Err(error), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn response() { + let response: Response = + serde_json::from_str(r#"{"jsonrpc": "2.0", "result": 19, "id": 1}"#).unwrap(); + assert_eq!(response.id, 1); + assert_eq!(response.data.into_result().unwrap(), 19); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..b51bfb72 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,16 @@ +//! ethers-rs +//! +//! ethers-rs is a port of [ethers-js](github.com/ethers-io/ethers.js) in Rust. + +mod network; + +pub mod providers; + +pub mod wallet; + +pub mod primitives; + +mod jsonrpc; + +/// Re-export solc +pub use solc; diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 00000000..8c4b6595 --- /dev/null +++ b/src/network.rs @@ -0,0 +1,18 @@ +/// Parameters for instantiating a network +use ethereum_types::Address; + +trait Network { + const NAME: &'static str; + const CHAIN_ID: u32; + const ENS: Option
; +} + +#[derive(Clone, Debug)] +pub struct Mainnet; + +impl Network for Mainnet { + const NAME: &'static str = "mainnet"; + const CHAIN_ID: u32 = 1; + // TODO: Replace with ENS address + const ENS: Option
= None; +} diff --git a/src/primitives.rs b/src/primitives.rs new file mode 100644 index 00000000..2b00875b --- /dev/null +++ b/src/primitives.rs @@ -0,0 +1,2 @@ +/// A signature; +pub struct Signature([u8; 65]); diff --git a/src/providers.rs b/src/providers.rs new file mode 100644 index 00000000..2f0c444d --- /dev/null +++ b/src/providers.rs @@ -0,0 +1,50 @@ +use crate::jsonrpc::{ClientError, HttpClient}; +use async_trait::async_trait; +use ethereum_types::U256; +use std::convert::TryFrom; +use url::{ParseError, Url}; + +/// An Ethereum JSON-RPC compatible backend +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] +impl ProviderTrait for Provider { + type Error = ClientError; + + async fn get_block_number(&self) -> Result { + self.0.request("eth_blockNumber", None::<()>).await + } +} + +#[async_trait] +pub trait ProviderTrait { + type Error; + + async fn get_block_number(&self) -> Result; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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)); + } +} diff --git a/src/transaction.rs b/src/transaction.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/wallet.rs b/src/wallet.rs new file mode 100644 index 00000000..86e03b9d --- /dev/null +++ b/src/wallet.rs @@ -0,0 +1,23 @@ +use crate::{primitives::Signature, providers::Provider}; + +pub struct Signer<'a> { + provider: Option<&'a Provider>, +} + +impl<'a> Signer<'a> { + pub fn random() -> Self { + Signer { provider: None } + } + + pub fn connect(mut self, provider: &'a Provider) -> Self { + self.provider = Some(provider); + self + } +} + +trait SignerC { + /// Connects to a provider + fn connect<'a>(self, provider: &'a Provider) -> Self; + + fn sign_message(message: &[u8]) -> Signature; +}