diff --git a/Cargo.toml b/Cargo.toml index 54c58e8f..75156229 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ 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" @@ -14,6 +13,18 @@ reqwest = { version = "0.10.4", features = ["json"] } serde = { version = "1.0.110", features = ["derive"] } serde_json = "1.0.53" thiserror = "1.0.19" +rustc-hex = "2.1.0" +rand = "0.5.1" # this should be the same rand crate version as the one in secp +secp256k1 = { version = "0.17.2", features = ["recovery", "rand"] } +secrecy = "0.6.0" +zeroize = "1.1.0" +tiny-keccak = "2.0.2" +futures = "0.3.5" + +solc = { git = "https://github.com/paritytech/rust_solc "} +rlp = "0.4.5" [dev-dependencies] tokio = { version = "0.2.21", features = ["macros"] } +failure = "0.1.8" + diff --git a/examples/local_signer.rs b/examples/local_signer.rs new file mode 100644 index 00000000..89dd067f --- /dev/null +++ b/examples/local_signer.rs @@ -0,0 +1,35 @@ +use ethers::providers::{Provider, ProviderTrait}; +use ethers::types::{BlockNumber, UnsignedTransaction}; +use ethers::wallet::{Mainnet, Wallet}; +use std::convert::TryFrom; +use std::str::FromStr; + +#[tokio::main] +async fn main() -> Result<(), failure::Error> { + let provider = Provider::try_from("http://localhost:8545")?; + let signer = Wallet::::from_str( + "d8ebe1e50cfea1f9961908d9df28e64bb163fee9ee48320361b2eb0a54974269", + )? + .connect(&provider); + + let nonce = provider + .get_transaction_count(signer.inner.address, Some(BlockNumber::Latest)) + .await?; + let tx = UnsignedTransaction { + to: Some("986eE0C8B91A58e490Ee59718Cca41056Cf55f24".parse().unwrap()), + gas: 21000.into(), + gas_price: 100000.into(), + value: 10000.into(), + input: vec![].into(), + nonce, + }; + + let tx = signer.send_transaction(tx).await?; + + dbg!(tx.hash); + let tx = provider.get_transaction(tx.hash).await?; + + println!("{}", serde_json::to_string(&tx)?); + + Ok(()) +} diff --git a/examples/transfer_eth.rs b/examples/transfer_eth.rs index 7234899b..86c3eeb4 100644 --- a/examples/transfer_eth.rs +++ b/examples/transfer_eth.rs @@ -1,15 +1,41 @@ use ethers::providers::{Provider, ProviderTrait}; -use ethers::wallet::Signer; +use ethers::types::{Address, BlockNumber, TransactionRequest}; use std::convert::TryFrom; +use std::str::FromStr; #[tokio::main] -async fn main() { - let provider = - Provider::try_from("https://mainnet.infura.io/v3/4aebe67796c64b95ab20802677b7bb55") - .unwrap(); +async fn main() -> Result<(), failure::Error> { + let provider = Provider::try_from("http://localhost:8545")?; - let num = provider.get_block_number().await.unwrap(); - dbg!(num); + let from = Address::from_str("4916064D2E9C1b2ccC466EEc3d30B2b08F1C130D")?; - // let signer = Signer::random().connect(&provider); + let tx_hash = provider + .send_transaction(TransactionRequest { + from, + to: Some(Address::from_str( + "9A7e5d4bcA656182e66e33340d776D1542143006", + )?), + value: Some(1000u64.into()), + gas: None, + gas_price: None, + data: None, + nonce: None, + }) + .await?; + + let tx = provider.get_transaction(tx_hash).await?; + + println!("{}", serde_json::to_string(&tx)?); + + let nonce1 = provider + .get_transaction_count(from, Some(BlockNumber::Latest)) + .await?; + + let nonce2 = provider + .get_transaction_count(from, Some(BlockNumber::Number(0.into()))) + .await?; + + assert!(nonce2 < nonce1); + + Ok(()) } diff --git a/src/jsonrpc.rs b/src/jsonrpc.rs index 92dd84cd..f744bcb7 100644 --- a/src/jsonrpc.rs +++ b/src/jsonrpc.rs @@ -8,14 +8,24 @@ use std::sync::atomic::{AtomicU64, Ordering}; use thiserror::Error; use url::Url; -#[derive(Debug)] /// JSON-RPC 2.0 Client +#[derive(Debug)] pub struct HttpClient { 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(), + } + } +} + impl HttpClient { /// Initializes a new HTTP Client pub fn new(url: impl Into) -> Self { diff --git a/src/lib.rs b/src/lib.rs index b51bfb72..f2b1aedf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,15 +2,17 @@ //! //! 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; +/// Ethereum related datatypes +pub mod types; +/// Re-export solc for convenience +pub use solc; + +/// JSON-RPC client mod jsonrpc; -/// Re-export solc -pub use solc; +mod utils; diff --git a/src/network.rs b/src/network.rs deleted file mode 100644 index 8c4b6595..00000000 --- a/src/network.rs +++ /dev/null @@ -1,18 +0,0 @@ -/// 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 deleted file mode 100644 index 2b00875b..00000000 --- a/src/primitives.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// A signature; -pub struct Signature([u8; 65]); diff --git a/src/providers.rs b/src/providers.rs index 2f0c444d..2a603667 100644 --- a/src/providers.rs +++ b/src/providers.rs @@ -1,10 +1,14 @@ -use crate::jsonrpc::{ClientError, HttpClient}; +use crate::{ + jsonrpc::{ClientError, HttpClient}, + types::{Address, BlockNumber, Bytes, Transaction, TransactionRequest, TxHash, U256}, + utils, +}; use async_trait::async_trait; -use ethereum_types::U256; 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 { @@ -22,24 +26,79 @@ impl TryFrom<&str> for Provider { } #[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); + 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() { @@ -47,4 +106,20 @@ mod tests { 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/transaction.rs b/src/transaction.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/src/types/block.rs b/src/types/block.rs new file mode 100644 index 00000000..c178fc8e --- /dev/null +++ b/src/types/block.rs @@ -0,0 +1,36 @@ +use super::U64; + +/// Block Number +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum BlockNumber { + /// Latest block + Latest, + /// Earliest block (genesis) + Earliest, + /// Pending block (not yet part of the blockchain) + Pending, + /// Block by number from canon chain + Number(U64), +} + +use serde::{Serialize, Serializer}; + +impl> From for BlockNumber { + fn from(num: T) -> Self { + BlockNumber::Number(num.into()) + } +} + +impl Serialize for BlockNumber { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + BlockNumber::Number(ref x) => serializer.serialize_str(&format!("0x{:x}", x)), + BlockNumber::Latest => serializer.serialize_str("latest"), + BlockNumber::Earliest => serializer.serialize_str("earliest"), + BlockNumber::Pending => serializer.serialize_str("pending"), + } + } +} diff --git a/src/types/bytes.rs b/src/types/bytes.rs new file mode 100644 index 00000000..05e1a62f --- /dev/null +++ b/src/types/bytes.rs @@ -0,0 +1,48 @@ +use rustc_hex::{FromHex, ToHex}; +use serde::de::{Error, Unexpected}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// Wrapper type around Vec to deserialize/serialize "0x" prefixed ethereum hex strings +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Bytes( + #[serde( + serialize_with = "serialize_bytes", + deserialize_with = "deserialize_bytes" + )] + pub Vec, +); + +impl Bytes { + /// Returns an empty bytes vector + pub fn new() -> Self { + Bytes(vec![]) + } +} + +impl From> for Bytes { + fn from(src: Vec) -> Self { + Self(src) + } +} + +pub fn serialize_bytes(x: T, s: S) -> Result +where + S: Serializer, + T: AsRef<[u8]>, +{ + s.serialize_str(&format!("0x{}", x.as_ref().to_hex::())) +} + +pub fn deserialize_bytes<'de, D>(d: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = String::deserialize(d)?; + if value.len() >= 2 && &value[0..2] == "0x" { + let bytes = FromHex::from_hex(&value[2..]) + .map_err(|e| Error::custom(format!("Invalid hex: {}", e)))?; + Ok(bytes) + } else { + Err(Error::invalid_value(Unexpected::Str(&value), &"0x prefix")) + } +} diff --git a/src/types/keys.rs b/src/types/keys.rs new file mode 100644 index 00000000..3a3743fb --- /dev/null +++ b/src/types/keys.rs @@ -0,0 +1,240 @@ +use rand::Rng; +use secp256k1::{ + key::ONE_KEY, Error as SecpError, Message, PublicKey as PubKey, Secp256k1, SecretKey, +}; +use std::ops::Deref; +use std::str::FromStr; +use zeroize::DefaultIsZeroes; + +use crate::{ + types::{Address, Signature, Transaction, UnsignedTransaction, H256, U256, U64}, + utils::{hash_message, keccak256}, +}; + +/// A private key on Secp256k1 +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct PrivateKey(pub(super) SecretKey); + +impl FromStr for PrivateKey { + type Err = SecpError; + + fn from_str(src: &str) -> Result { + let sk = SecretKey::from_str(src)?; + Ok(PrivateKey(sk)) + } +} + +impl PrivateKey { + pub fn new(rng: &mut R) -> Self { + PrivateKey(SecretKey::new(rng)) + } + + /// Sign arbitrary string data. + /// + /// The data is UTF-8 encoded and enveloped the same way as with + /// `hash_message`. The returned signed data's signature is in 'Electrum' + /// notation, that is the recovery value `v` is either `27` or `28` (as + /// opposed to the standard notation where `v` is either `0` or `1`). This + /// is important to consider when using this signature with other crates. + pub fn sign(&self, message: S) -> Signature + where + S: AsRef<[u8]>, + { + let message = message.as_ref(); + let message_hash = hash_message(message); + + let sig_message = + Message::from_slice(message_hash.as_bytes()).expect("hash is non-zero 32-bytes; qed"); + self.sign_with_eip155(&sig_message, None) + } + + /// RLP encodes and then signs the stransaction. If no chain_id is provided, then EIP-155 is + /// not used. + pub fn sign_transaction(&self, tx: UnsignedTransaction, chain_id: Option) -> Transaction { + // Hash the transaction's RLP encoding + let hash = tx.hash(chain_id); + let message = Message::from_slice(hash.as_bytes()).expect("hash is non-zero 32-bytes; qed"); + + let signature = self.sign_with_eip155(&message, chain_id); + + let rlp = tx.rlp_signed(&signature); + let hash = keccak256(&rlp.0); + + Transaction { + hash: hash.into(), + nonce: tx.nonce, + from: self.into(), + to: tx.to, + value: tx.value, + gas_price: tx.gas_price, + gas: tx.gas, + input: tx.input, + v: signature.v.into(), + r: U256::from_big_endian(signature.r.as_bytes()), + s: U256::from_big_endian(signature.s.as_bytes()), + + // Leave these empty as they're only used for included transactions + block_hash: None, + block_number: None, + transaction_index: None, + } + } + + fn sign_with_eip155(&self, message: &Message, chain_id: Option) -> Signature { + let (recovery_id, signature) = Secp256k1::signing_only() + .sign_recoverable(message, &self.0) + .serialize_compact(); + + let standard_v = recovery_id.to_i32() as u64; + let v = if let Some(chain_id) = chain_id { + // When signing with a chain ID, add chain replay protection. + standard_v + 35 + chain_id.as_u64() * 2 + } else { + // Otherwise, convert to 'Electrum' notation. + standard_v + 27 + }; + let r = H256::from_slice(&signature[..32]); + let s = H256::from_slice(&signature[32..]); + + // TODO: Check what happens when using the 1337 Geth chain id + Signature { v: v as u8, r, s } + } +} + +impl Default for PrivateKey { + fn default() -> Self { + PrivateKey(ONE_KEY) + } +} + +impl Deref for PrivateKey { + type Target = SecretKey; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DefaultIsZeroes for PrivateKey {} + +/// A public key +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PublicKey(pub(super) PubKey); + +impl FromStr for PublicKey { + type Err = SecpError; + + fn from_str(src: &str) -> Result { + let sk = PubKey::from_str(src)?; + Ok(PublicKey(sk)) + } +} + +impl From for PublicKey { + /// Gets the public address of a private key. + fn from(src: PubKey) -> PublicKey { + PublicKey(src) + } +} + +impl From<&PrivateKey> for PublicKey { + /// Gets the public address of a private key. + fn from(src: &PrivateKey) -> PublicKey { + let secp = Secp256k1::signing_only(); + let public_key = PubKey::from_secret_key(&secp, src); + PublicKey(public_key) + } +} + +/// Gets the address of a public key. +/// +/// The public address is defined as the low 20 bytes of the keccak hash of +/// the public key. Note that the public key returned from the `secp256k1` +/// crate is 65 bytes long, that is because it is prefixed by `0x04` to +/// indicate an uncompressed public key; this first byte is ignored when +/// computing the hash. +impl From<&PublicKey> for Address { + fn from(src: &PublicKey) -> Address { + let public_key = src.0.serialize_uncompressed(); + + debug_assert_eq!(public_key[0], 0x04); + let hash = keccak256(&public_key[1..]); + + Address::from_slice(&hash[12..]) + } +} + +impl From for Address { + fn from(src: PublicKey) -> Address { + Address::from(&src) + } +} + +impl From<&PrivateKey> for Address { + fn from(src: &PrivateKey) -> Address { + let public_key = PublicKey::from(src); + Address::from(&public_key) + } +} + +impl From for Address { + fn from(src: PrivateKey) -> Address { + Address::from(&src) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::Bytes; + use rustc_hex::FromHex; + + #[test] + fn signs_tx() { + // retrieved test vector from: + // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction + let tx = UnsignedTransaction { + to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse().unwrap()), + value: 1_000_000_000.into(), + gas: 2_000_000.into(), + nonce: 0.into(), + gas_price: 21_000_000_000u128.into(), + input: Bytes::new(), + }; + let chain_id = 1; + + let key: PrivateKey = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + .parse() + .unwrap(); + + let tx = key.sign_transaction(tx, Some(chain_id.into())); + + assert_eq!( + tx.hash, + "de8db924885b0803d2edc335f745b2b8750c8848744905684c20b987443a9593" + .parse() + .unwrap() + ); + + let expected_rlp = Bytes("f869808504e3b29200831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a0c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895a0727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68".from_hex().unwrap()); + assert_eq!(tx.rlp(), expected_rlp); + } + + #[test] + fn signs_data() { + // test vector taken from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#sign + + let key: PrivateKey = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + .parse() + .unwrap(); + let sign = key.sign("Some data"); + + assert_eq!( + sign.to_vec(), + "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c" + .from_hex::>() + .unwrap() + ); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 00000000..1ab94fe0 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,67 @@ +//! Various Ethereum Related Datatypes + +// Re-export common ethereum datatypes with more specific names +pub use ethereum_types::{Address, H256, U256, U64}; + +mod transaction; +// TODO: Figure out some more intuitive way instead of having 3 similarly named structs +// with the same fields +pub use transaction::{Transaction, TransactionRequest, UnsignedTransaction}; + +mod keys; +pub use keys::{PrivateKey, PublicKey}; + +pub mod signature; +pub use signature::Signature; + +mod bytes; +pub use bytes::Bytes; + +mod block; +pub use block::BlockNumber; + +use rustc_hex::{FromHex, ToHex}; +use serde::{ + de::{Error, Unexpected}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +/// Wrapper type 0round Vec to deserialize/serialize "0x" prefixed ethereum hex strings +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct TxHash( + #[serde( + serialize_with = "serialize_h256", + deserialize_with = "deserialize_h256" + )] + pub H256, +); + +impl From for TxHash { + fn from(src: H256) -> TxHash { + TxHash(src) + } +} + +pub fn serialize_h256(x: T, s: S) -> Result +where + S: Serializer, + T: AsRef<[u8]>, +{ + s.serialize_str(&format!("0x{}", x.as_ref().to_hex::())) +} + +pub fn deserialize_h256<'de, D>(d: D) -> Result +where + D: Deserializer<'de>, +{ + let value = String::deserialize(d)?; + if value.len() >= 2 && &value[0..2] == "0x" { + let slice: Vec = FromHex::from_hex(&value[2..]) + .map_err(|e| Error::custom(format!("Invalid hex: {}", e)))?; + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(&slice[..32]); + Ok(bytes.into()) + } else { + Err(Error::invalid_value(Unexpected::Str(&value), &"0x prefix")) + } +} diff --git a/src/types/signature.rs b/src/types/signature.rs new file mode 100644 index 00000000..60a5aa48 --- /dev/null +++ b/src/types/signature.rs @@ -0,0 +1,222 @@ +// Code adapted from: https://github.com/tomusdrw/rust-web3/blob/master/src/api/accounts.rs +use crate::{ + types::{Address, PublicKey, H256}, + utils::hash_message, +}; + +use secp256k1::{ + recovery::{RecoverableSignature, RecoveryId}, + Error as Secp256k1Error, Message, Secp256k1, +}; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +use thiserror::Error; + +/// An error involving a signature. +#[derive(Clone, Debug, Error)] +pub enum SignatureError { + /// Internal error inside the recovery + #[error(transparent)] + Secp256k1Error(#[from] Secp256k1Error), + /// Invalid length, secp256k1 signatures are 65 bytes + #[error("invalid signature length, got {0}, expected 65")] + InvalidLength(usize), +} + +/// Recovery message data. +/// +/// The message data can either be a binary message that is first hashed +/// according to EIP-191 and then recovered based on the signature or a +/// precomputed hash. +#[derive(Clone, Debug, PartialEq)] +pub enum RecoveryMessage { + /// Message bytes + Data(Vec), + /// Message hash + Hash(H256), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +/// An ECDSA signature +pub struct Signature { + /// R value + pub r: H256, + /// S Value + pub s: H256, + /// V value in 'Electrum' notation. + pub v: u8, +} + +impl Signature { + /// Recovers the Ethereum address which was used to sign the given message. + /// + /// Recovery signature data uses 'Electrum' notation, this means the `v` + /// value is expected to be either `27` or `28`. + pub fn recover(&self, message: M) -> Result + where + M: Into, + { + let message = message.into(); + let message_hash = match message { + RecoveryMessage::Data(ref message) => hash_message(message), + RecoveryMessage::Hash(hash) => hash, + }; + let signature = self.as_signature()?; + + let message = Message::from_slice(message_hash.as_bytes())?; + let public_key = Secp256k1::verification_only().recover(&message, &signature)?; + + Ok(PublicKey::from(public_key).into()) + } + + /// Retrieves the recovery signature. + fn as_signature(&self) -> Result { + let recovery_id = self.recovery_id()?; + let signature = { + let mut sig = [0u8; 64]; + sig[..32].copy_from_slice(self.r.as_bytes()); + sig[32..].copy_from_slice(self.s.as_bytes()); + sig + }; + + Ok(RecoverableSignature::from_compact(&signature, recovery_id)?) + } + + /// Retrieve the recovery ID. + fn recovery_id(&self) -> Result { + let standard_v = match self.v { + 27 => 0, + 28 => 1, + v if v >= 35 => ((v - 1) % 2) as _, + _ => 4, + }; + + Ok(RecoveryId::from_i32(standard_v)?) + } + + /// Copies and serializes `self` into a new `Vec` with the recovery id included + pub fn to_vec(&self) -> Vec { + self.into() + } +} + +impl<'a> TryFrom<&'a [u8]> for Signature { + type Error = SignatureError; + + /// Parses a raw signature which is expected to be 65 bytes long where + /// the first 32 bytes is the `r` value, the second 32 bytes the `s` value + /// and the final byte is the `v` value in 'Electrum' notation. + fn try_from(raw_signature: &'a [u8]) -> Result { + let bytes = raw_signature.as_ref(); + + if bytes.len() != 65 { + return Err(SignatureError::InvalidLength(bytes.len())); + } + + let v = bytes[64]; + let r = H256::from_slice(&bytes[0..32]); + let s = H256::from_slice(&bytes[32..64]); + + Ok(Signature { r, s, v }) + } +} + +impl From<&Signature> for [u8; 65] { + fn from(src: &Signature) -> [u8; 65] { + let mut sig = [0u8; 65]; + sig[..32].copy_from_slice(src.r.as_bytes()); + sig[32..64].copy_from_slice(src.s.as_bytes()); + sig[64] = src.v; + sig + } +} + +impl From for [u8; 65] { + fn from(src: Signature) -> [u8; 65] { + <[u8; 65]>::from(&src) + } +} + +impl From<&Signature> for Vec { + fn from(src: &Signature) -> Vec { + <[u8; 65]>::from(src).to_vec() + } +} + +impl From for Vec { + fn from(src: Signature) -> Vec { + <[u8; 65]>::from(&src).to_vec() + } +} + +impl From<&[u8]> for RecoveryMessage { + fn from(s: &[u8]) -> Self { + s.to_owned().into() + } +} + +impl From> for RecoveryMessage { + fn from(s: Vec) -> Self { + RecoveryMessage::Data(s) + } +} + +impl From<&str> for RecoveryMessage { + fn from(s: &str) -> Self { + s.as_bytes().to_owned().into() + } +} + +impl From for RecoveryMessage { + fn from(s: String) -> Self { + RecoveryMessage::Data(s.into_bytes()) + } +} + +impl From<[u8; 32]> for RecoveryMessage { + fn from(hash: [u8; 32]) -> Self { + H256(hash).into() + } +} + +impl From for RecoveryMessage { + fn from(hash: H256) -> Self { + RecoveryMessage::Hash(hash) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::PrivateKey; + + #[test] + fn recover_signature_from_message() { + let message = "Some data"; + let hash = hash_message(message); + let key = PrivateKey::new(&mut rand::thread_rng()); + let address = Address::from(key); + + // sign a message + let signature = key.sign(message); + + // ecrecover via the message will hash internally + let recovered = signature.recover(message).unwrap(); + + // if provided with a hash, it will skip hashing + let recovered2 = signature.recover(hash).unwrap(); + + assert_eq!(recovered, address); + assert_eq!(recovered2, address); + } + + #[test] + fn to_vec() { + let message = "Some data"; + let key = PrivateKey::new(&mut rand::thread_rng()); + let signature = key.sign(message); + let serialized = signature.to_vec(); + let de = Signature::try_from(&serialized[..]).unwrap(); + assert_eq!(signature, de); + } +} diff --git a/src/types/transaction.rs b/src/types/transaction.rs new file mode 100644 index 00000000..0e0fe414 --- /dev/null +++ b/src/types/transaction.rs @@ -0,0 +1,258 @@ +//! Transaction types +//! +//! We define 3 transaction types, `TransactionRequest`, `UnsignedTransaction` and `Transaction`. +//! `TransactionRequest` and `UnsignedTransaction` are both unsigned transaction objects. +//! +//! The former gets submitted to the node via an `eth_sendTransaction` call, which populates any missing fields, +//! signs it and broadcasts it. The latter is signed locally by a private key, and the signed +//! transaction is broadcast via `eth_sendRawTransaction`. +use crate::{ + types::{Address, Bytes, Signature, H256, U256, U64}, + utils::keccak256, +}; + +use serde::{Deserialize, Serialize}; + +/// Parameters for sending a transaction to via the eth_sendTransaction API. +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)] +pub struct TransactionRequest { + /// Sender address or ENS name + pub from: Address, + + /// Recipient address (None for contract creation) + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option
, + + /// Supplied gas (None for sensible default) + #[serde(skip_serializing_if = "Option::is_none")] + pub gas: Option, + + /// Gas price (None for sensible default) + #[serde(rename = "gasPrice")] + #[serde(skip_serializing_if = "Option::is_none")] + pub gas_price: Option, + + /// Transfered value (None for no transfer) + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + + /// The compiled code of a contract OR the first 4 bytes of the hash of the + /// invoked method signature and encoded parameters. For details see Ethereum Contract ABI + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + + /// Transaction nonce (None for next available nonce) + #[serde(skip_serializing_if = "Option::is_none")] + pub nonce: Option, +} + +/// A raw unsigned transaction where all the information is already specified +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)] +pub struct UnsignedTransaction { + /// Recipient address (None for contract creation) + pub to: Option
, + + /// Supplied gas + pub gas: U256, + + /// Gas price + #[serde(rename = "gasPrice")] + pub gas_price: U256, + + /// Transfered value + pub value: U256, + + /// The compiled code of a contract OR the first 4 bytes of the hash of the + /// invoked method signature and encoded parameters. For details see Ethereum Contract ABI + pub input: Bytes, + + /// Transaction nonce (None for next available nonce) + pub nonce: U256, +} + +use rlp::RlpStream; + +impl UnsignedTransaction { + fn rlp_base(&self, rlp: &mut RlpStream) { + rlp.append(&self.nonce); + rlp.append(&self.gas_price); + rlp.append(&self.gas); + rlp_opt(rlp, &self.to); + rlp.append(&self.value); + rlp.append(&self.input.0); + } + + pub fn hash(&self, chain_id: Option) -> H256 { + let mut rlp = RlpStream::new(); + rlp.begin_list(9); + self.rlp_base(&mut rlp); + + rlp.append(&chain_id.unwrap_or(U64::zero())); + rlp.append(&0u8); + rlp.append(&0u8); + + keccak256(rlp.out().as_ref()).into() + } + + pub fn rlp_signed(&self, signature: &Signature) -> Bytes { + let mut rlp = RlpStream::new(); + rlp.begin_list(9); + self.rlp_base(&mut rlp); + + rlp.append(&signature.v); + rlp.append(&signature.r); + rlp.append(&signature.s); + + rlp.out().into() + } +} + +/// Details of a signed transaction +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Transaction { + /// The transaction's hash + pub hash: H256, + + /// The transaction's nonce + pub nonce: U256, + + /// Block hash. None when pending. + #[serde(rename = "blockHash")] + #[serde(skip_serializing_if = "Option::is_none")] + pub block_hash: Option, + + /// Block number. None when pending. + #[serde(rename = "blockNumber")] + #[serde(skip_serializing_if = "Option::is_none")] + pub block_number: Option, + + /// Transaction Index. None when pending. + #[serde(rename = "transactionIndex")] + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_index: Option, + + /// Sender + pub from: Address, + + /// Recipient (None when contract creation) + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option
, + + /// Transfered value + pub value: U256, + + /// Gas Price + #[serde(rename = "gasPrice")] + pub gas_price: U256, + + /// Gas amount + pub gas: U256, + + /// Input data + pub input: Bytes, + + /// ECDSA recovery id + pub v: U64, + + /// ECDSA signature r + pub r: U256, + + /// ECDSA signature s + pub s: U256, +} + +impl Transaction { + pub fn hash(&self) -> H256 { + keccak256(&self.rlp().0).into() + } + + pub fn rlp(&self) -> Bytes { + let mut rlp = RlpStream::new(); + rlp.begin_list(9); + rlp.append(&self.nonce); + rlp.append(&self.gas_price); + rlp.append(&self.gas); + rlp_opt(&mut rlp, &self.to); + rlp.append(&self.value); + rlp.append(&self.input.0); + rlp.append(&self.v); + rlp.append(&self.r); + rlp.append(&self.s); + + rlp.out().into() + } +} + +fn rlp_opt(rlp: &mut RlpStream, opt: &Option) { + if let Some(inner) = opt { + rlp.append(inner); + } else { + rlp.append(&""); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decode_unsigned_transaction() { + let _res: UnsignedTransaction = serde_json::from_str( + r#"{ + "gas":"0xc350", + "gasPrice":"0x4a817c800", + "hash":"0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b", + "input":"0x68656c6c6f21", + "nonce":"0x15", + "to":"0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb", + "transactionIndex":"0x41", + "value":"0xf3dbb76162000", + "chain_id": "0x1" + }"#, + ) + .unwrap(); + } + + #[test] + fn decode_transaction_response() { + let _res: Transaction = serde_json::from_str( + r#"{ + "blockHash":"0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2", + "blockNumber":"0x5daf3b", + "from":"0xa7d9ddbe1f17865597fbd27ec712455208b6b76d", + "gas":"0xc350", + "gasPrice":"0x4a817c800", + "hash":"0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b", + "input":"0x68656c6c6f21", + "nonce":"0x15", + "to":"0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb", + "transactionIndex":"0x41", + "value":"0xf3dbb76162000", + "v":"0x25", + "r":"0x1b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5fea", + "s":"0x4ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c" + }"#, + ) + .unwrap(); + + let _res: Transaction = serde_json::from_str( + r#"{ + "hash":"0xdd79ab0f996150aa3c9f135bbb9272cf0dedb830fafcbbf0c06020503565c44f", + "nonce":"0xe", + "blockHash":"0xef3fe1f532c3d8783a6257619bc123e9453aa8d6614e4cdb4cc8b9e1ed861404", + "blockNumber":"0xf", + "transactionIndex":"0x0", + "from":"0x1b67b03cdccfae10a2d80e52d3d026dbe2960ad0", + "to":"0x986ee0c8b91a58e490ee59718cca41056cf55f24", + "value":"0x2710", + "gas":"0x5208", + "gasPrice":"0x186a0", + "input":"0x", + "v":"0x25", + "r":"0x75188beb2f601bb8cf52ef89f92a6ba2bb7edcf8e3ccde90548cc99cbea30b1e", + "s":"0xc0559a540f16d031f3404d5df2bb258084eee56ed1193d8b534bb6affdb3c2c" + }"#, + ) + .unwrap(); + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 00000000..c8e5551d --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,56 @@ +//! Various utilities for manipulating Ethereum related dat +use crate::types::H256; +use tiny_keccak::{Hasher, Keccak}; + +const PREFIX: &str = "\x19Ethereum Signed Message:\n"; + +/// Hash a message according to EIP-191. +/// +/// The data is a UTF-8 encoded string and will enveloped as follows: +/// `"\x19Ethereum Signed Message:\n" + message.length + message` and hashed +/// using keccak256. +pub fn hash_message(message: S) -> H256 +where + S: AsRef<[u8]>, +{ + let message = message.as_ref(); + + let mut eth_message = format!("{}{}", PREFIX, message.len()).into_bytes(); + eth_message.extend_from_slice(message); + + keccak256(ð_message).into() +} + +/// Compute the Keccak-256 hash of input bytes. +// TODO: Add Solidity Keccak256 packing support +pub fn keccak256(bytes: &[u8]) -> [u8; 32] { + let mut output = [0u8; 32]; + let mut hasher = Keccak::v256(); + hasher.update(bytes); + hasher.finalize(&mut output); + output +} + +/// Serialize a type. Panics if the type is returns error during serialization. +pub fn serialize(t: &T) -> serde_json::Value { + serde_json::to_value(t).expect("Types never fail to serialize.") +} + +#[cfg(test)] +mod tests { + use super::*; + + // test vector taken from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#hashmessage + #[test] + fn test_hash_message() { + let hash = hash_message("Hello World"); + + assert_eq!( + hash, + "a1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2" + .parse() + .unwrap() + ); + } +} diff --git a/src/wallet.rs b/src/wallet.rs index 86e03b9d..59062a52 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,23 +1,159 @@ -use crate::{primitives::Signature, providers::Provider}; +use crate::{ + jsonrpc::ClientError, + providers::{Provider, ProviderTrait}, + types::{Address, PrivateKey, PublicKey, Signature, U64}, + types::{Transaction, UnsignedTransaction}, +}; +use rand::Rng; +use std::{marker::PhantomData, str::FromStr}; -pub struct Signer<'a> { - provider: Option<&'a Provider>, +use thiserror::Error; + +/// A keypair +#[derive(Clone, Debug)] +pub struct Wallet { + pub private_key: PrivateKey, + pub public_key: PublicKey, + pub address: Address, + network: PhantomData, } -impl<'a> Signer<'a> { - pub fn random() -> Self { - Signer { provider: None } +pub trait Network { + const CHAIN_ID: Option; + + // TODO: Default providers? +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Mainnet; + +impl Network for Mainnet { + const CHAIN_ID: Option = Some(U64([1])); +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct AnyNet; + +impl Network for AnyNet { + const CHAIN_ID: Option = None; +} + +// No EIP-155 used +pub type AnyWallet = Wallet; + +impl Wallet { + /// Creates a new keypair + pub fn new(rng: &mut R) -> Self { + let private_key = PrivateKey::new(rng); + let public_key = PublicKey::from(&private_key); + let address = Address::from(&private_key); + + Self { + private_key, + public_key, + address, + network: PhantomData, + } } + /// Connects to a provider and returns a signer + pub fn connect<'a>(self, provider: &'a Provider) -> Signer> { + Signer { + inner: self, + provider: Some(provider), + } + } +} + +impl FromStr for Wallet { + type Err = secp256k1::Error; + + fn from_str(src: &str) -> Result { + Ok(PrivateKey::from_str(src)?.into()) + } +} + +impl From for Wallet { + fn from(private_key: PrivateKey) -> Self { + let public_key = PublicKey::from(&private_key); + let address = Address::from(&private_key); + + Self { + private_key, + public_key, + address, + network: PhantomData, + } + } +} + +#[derive(Clone, Debug)] +pub struct Signer<'a, S> { + pub provider: Option<&'a Provider>, + pub inner: S, +} + +#[derive(Error, Debug)] +pub enum SignerError { + #[error(transparent)] + ClientError(#[from] ClientError), + #[error("no provider was found")] + NoProvider, +} + +impl<'a, N: Network> Signer<'a, Wallet> { + /// Generates a random signer with no provider. Should be combined with the + /// `connect` method like `Signer::random(rng).connect(provider)` + pub fn random(rng: &mut R) -> Self { + Signer { + provider: None, + inner: Wallet::new(rng), + } + } + + pub async fn send_transaction( + &self, + tx: UnsignedTransaction, + ) -> Result { + // TODO: Is there any nicer way to do this? + let provider = self.ensure_provider()?; + + let signed_tx = self.sign_transaction(tx.clone()); + + provider.send_raw_transaction(&signed_tx.rlp()).await?; + + Ok(signed_tx) + } +} + +impl<'a, S> Signer<'a, S> { + /// Sets the provider for the signer pub fn connect(mut self, provider: &'a Provider) -> Self { self.provider = Some(provider); self } + + pub fn ensure_provider(&self) -> Result<&Provider, SignerError> { + if let Some(provider) = self.provider { + Ok(provider) + } else { + Err(SignerError::NoProvider) + } + } } trait SignerC { - /// Connects to a provider - fn connect<'a>(self, provider: &'a Provider) -> Self; - - fn sign_message(message: &[u8]) -> Signature; + /// Signs the hash of the provided message after prefixing it + fn sign_message>(&self, message: S) -> Signature; + fn sign_transaction(&self, message: UnsignedTransaction) -> Transaction; +} + +impl<'a, N: Network> SignerC for Signer<'a, Wallet> { + fn sign_message>(&self, message: S) -> Signature { + self.inner.private_key.sign(message) + } + + fn sign_transaction(&self, tx: UnsignedTransaction) -> Transaction { + self.inner.private_key.sign_transaction(tx, N::CHAIN_ID) + } }