This commit is contained in:
Georgios Konstantopoulos 2020-05-23 03:01:20 +03:00
parent 1a49a62a81
commit 913b917620
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
17 changed files with 1249 additions and 47 deletions

View File

@ -5,7 +5,6 @@ authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
solc = { git = "https://github.com/paritytech/rust_solc "}
ethereum-types = "0.9.2" ethereum-types = "0.9.2"
url = "2.1.1" url = "2.1.1"
once_cell = "1.4.0" once_cell = "1.4.0"
@ -14,6 +13,18 @@ reqwest = { version = "0.10.4", features = ["json"] }
serde = { version = "1.0.110", features = ["derive"] } serde = { version = "1.0.110", features = ["derive"] }
serde_json = "1.0.53" serde_json = "1.0.53"
thiserror = "1.0.19" 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] [dev-dependencies]
tokio = { version = "0.2.21", features = ["macros"] } tokio = { version = "0.2.21", features = ["macros"] }
failure = "0.1.8"

35
examples/local_signer.rs Normal file
View File

@ -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::<Mainnet>::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(())
}

View File

@ -1,15 +1,41 @@
use ethers::providers::{Provider, ProviderTrait}; use ethers::providers::{Provider, ProviderTrait};
use ethers::wallet::Signer; use ethers::types::{Address, BlockNumber, TransactionRequest};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::str::FromStr;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() -> Result<(), failure::Error> {
let provider = let provider = Provider::try_from("http://localhost:8545")?;
Provider::try_from("https://mainnet.infura.io/v3/4aebe67796c64b95ab20802677b7bb55")
.unwrap();
let num = provider.get_block_number().await.unwrap(); let from = Address::from_str("4916064D2E9C1b2ccC466EEc3d30B2b08F1C130D")?;
dbg!(num);
// 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(())
} }

View File

@ -8,14 +8,24 @@ use std::sync::atomic::{AtomicU64, Ordering};
use thiserror::Error; use thiserror::Error;
use url::Url; use url::Url;
#[derive(Debug)]
/// JSON-RPC 2.0 Client /// JSON-RPC 2.0 Client
#[derive(Debug)]
pub struct HttpClient { pub struct HttpClient {
id: AtomicU64, id: AtomicU64,
client: Client, client: Client,
url: Url, 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 { impl HttpClient {
/// Initializes a new HTTP Client /// Initializes a new HTTP Client
pub fn new(url: impl Into<Url>) -> Self { pub fn new(url: impl Into<Url>) -> Self {

View File

@ -2,15 +2,17 @@
//! //!
//! ethers-rs is a port of [ethers-js](github.com/ethers-io/ethers.js) in Rust. //! ethers-rs is a port of [ethers-js](github.com/ethers-io/ethers.js) in Rust.
mod network;
pub mod providers; pub mod providers;
pub mod wallet; 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; mod jsonrpc;
/// Re-export solc mod utils;
pub use solc;

View File

@ -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<Address>;
}
#[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<Address> = None;
}

View File

@ -1,2 +0,0 @@
/// A signature;
pub struct Signature([u8; 65]);

View File

@ -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 async_trait::async_trait;
use ethereum_types::U256;
use std::convert::TryFrom; use std::convert::TryFrom;
use url::{ParseError, Url}; use url::{ParseError, Url};
/// An Ethereum JSON-RPC compatible backend /// An Ethereum JSON-RPC compatible backend
#[derive(Clone, Debug)]
pub struct Provider(HttpClient); pub struct Provider(HttpClient);
impl From<HttpClient> for Provider { impl From<HttpClient> for Provider {
@ -22,24 +26,79 @@ impl TryFrom<&str> for Provider {
} }
#[async_trait] #[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 { impl ProviderTrait for Provider {
type Error = ClientError; type Error = ClientError;
async fn get_block_number(&self) -> Result<U256, Self::Error> { async fn get_block_number(&self) -> Result<U256, Self::Error> {
self.0.request("eth_blockNumber", None::<()>).await 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);
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] #[async_trait]
pub trait ProviderTrait { pub trait ProviderTrait {
type Error; type Error;
async fn get_block_number(&self) -> Result<U256, Self::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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use ethereum_types::Address;
use std::str::FromStr;
// TODO: Make a Ganache helper
#[tokio::test] #[tokio::test]
async fn get_balance() { async fn get_balance() {
@ -47,4 +106,20 @@ mod tests {
let num = provider.get_block_number().await.unwrap(); let num = provider.get_block_number().await.unwrap();
assert_eq!(num, U256::from(0)); 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

36
src/types/block.rs Normal file
View File

@ -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<T: Into<U64>> From<T> for BlockNumber {
fn from(num: T) -> Self {
BlockNumber::Number(num.into())
}
}
impl Serialize for BlockNumber {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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"),
}
}
}

48
src/types/bytes.rs Normal file
View File

@ -0,0 +1,48 @@
use rustc_hex::{FromHex, ToHex};
use serde::de::{Error, Unexpected};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// Wrapper type around Vec<u8> 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<u8>,
);
impl Bytes {
/// Returns an empty bytes vector
pub fn new() -> Self {
Bytes(vec![])
}
}
impl From<Vec<u8>> for Bytes {
fn from(src: Vec<u8>) -> Self {
Self(src)
}
}
pub fn serialize_bytes<S, T>(x: T, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: AsRef<[u8]>,
{
s.serialize_str(&format!("0x{}", x.as_ref().to_hex::<String>()))
}
pub fn deserialize_bytes<'de, D>(d: D) -> Result<Vec<u8>, 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"))
}
}

240
src/types/keys.rs Normal file
View File

@ -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<PrivateKey, Self::Err> {
let sk = SecretKey::from_str(src)?;
Ok(PrivateKey(sk))
}
}
impl PrivateKey {
pub fn new<R: Rng>(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<S>(&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<U64>) -> 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<U64>) -> 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<PublicKey, Self::Err> {
let sk = PubKey::from_str(src)?;
Ok(PublicKey(sk))
}
}
impl From<PubKey> 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<PublicKey> 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<PrivateKey> 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::<Vec<u8>>()
.unwrap()
);
}
}

67
src/types/mod.rs Normal file
View File

@ -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<u8> 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<H256> for TxHash {
fn from(src: H256) -> TxHash {
TxHash(src)
}
}
pub fn serialize_h256<S, T>(x: T, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: AsRef<[u8]>,
{
s.serialize_str(&format!("0x{}", x.as_ref().to_hex::<String>()))
}
pub fn deserialize_h256<'de, D>(d: D) -> Result<H256, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(d)?;
if value.len() >= 2 && &value[0..2] == "0x" {
let slice: Vec<u8> = 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"))
}
}

222
src/types/signature.rs Normal file
View File

@ -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<u8>),
/// 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<M>(&self, message: M) -> Result<Address, SignatureError>
where
M: Into<RecoveryMessage>,
{
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<RecoverableSignature, SignatureError> {
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<RecoveryId, SignatureError> {
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<u8> {
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<Self, Self::Error> {
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<Signature> for [u8; 65] {
fn from(src: Signature) -> [u8; 65] {
<[u8; 65]>::from(&src)
}
}
impl From<&Signature> for Vec<u8> {
fn from(src: &Signature) -> Vec<u8> {
<[u8; 65]>::from(src).to_vec()
}
}
impl From<Signature> for Vec<u8> {
fn from(src: Signature) -> Vec<u8> {
<[u8; 65]>::from(&src).to_vec()
}
}
impl From<&[u8]> for RecoveryMessage {
fn from(s: &[u8]) -> Self {
s.to_owned().into()
}
}
impl From<Vec<u8>> for RecoveryMessage {
fn from(s: Vec<u8>) -> Self {
RecoveryMessage::Data(s)
}
}
impl From<&str> for RecoveryMessage {
fn from(s: &str) -> Self {
s.as_bytes().to_owned().into()
}
}
impl From<String> 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<H256> 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);
}
}

258
src/types/transaction.rs Normal file
View File

@ -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<Address>,
/// Supplied gas (None for sensible default)
#[serde(skip_serializing_if = "Option::is_none")]
pub gas: Option<U256>,
/// Gas price (None for sensible default)
#[serde(rename = "gasPrice")]
#[serde(skip_serializing_if = "Option::is_none")]
pub gas_price: Option<U256>,
/// Transfered value (None for no transfer)
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<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
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Bytes>,
/// Transaction nonce (None for next available nonce)
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<U256>,
}
/// 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<Address>,
/// 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<U64>) -> 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<H256>,
/// Block number. None when pending.
#[serde(rename = "blockNumber")]
#[serde(skip_serializing_if = "Option::is_none")]
pub block_number: Option<U64>,
/// Transaction Index. None when pending.
#[serde(rename = "transactionIndex")]
#[serde(skip_serializing_if = "Option::is_none")]
pub transaction_index: Option<U64>,
/// Sender
pub from: Address,
/// Recipient (None when contract creation)
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<Address>,
/// 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<T: rlp::Encodable>(rlp: &mut RlpStream, opt: &Option<T>) {
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();
}
}

56
src/utils.rs Normal file
View File

@ -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<S>(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(&eth_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: serde::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()
);
}
}

View File

@ -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> { use thiserror::Error;
provider: Option<&'a Provider>,
/// A keypair
#[derive(Clone, Debug)]
pub struct Wallet<N> {
pub private_key: PrivateKey,
pub public_key: PublicKey,
pub address: Address,
network: PhantomData<N>,
} }
impl<'a> Signer<'a> { pub trait Network {
pub fn random() -> Self { const CHAIN_ID: Option<U64>;
Signer { provider: None }
// TODO: Default providers?
} }
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Mainnet;
impl Network for Mainnet {
const CHAIN_ID: Option<U64> = Some(U64([1]));
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct AnyNet;
impl Network for AnyNet {
const CHAIN_ID: Option<U64> = None;
}
// No EIP-155 used
pub type AnyWallet = Wallet<AnyNet>;
impl<N: Network> Wallet<N> {
/// Creates a new keypair
pub fn new<R: Rng>(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<Wallet<N>> {
Signer {
inner: self,
provider: Some(provider),
}
}
}
impl<N: Network> FromStr for Wallet<N> {
type Err = secp256k1::Error;
fn from_str(src: &str) -> Result<Self, Self::Err> {
Ok(PrivateKey::from_str(src)?.into())
}
}
impl<N: Network> From<PrivateKey> for Wallet<N> {
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<N>> {
/// Generates a random signer with no provider. Should be combined with the
/// `connect` method like `Signer::random(rng).connect(provider)`
pub fn random<R: Rng>(rng: &mut R) -> Self {
Signer {
provider: None,
inner: Wallet::new(rng),
}
}
pub async fn send_transaction(
&self,
tx: UnsignedTransaction,
) -> Result<Transaction, SignerError> {
// 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 { pub fn connect(mut self, provider: &'a Provider) -> Self {
self.provider = Some(provider); self.provider = Some(provider);
self self
} }
pub fn ensure_provider(&self) -> Result<&Provider, SignerError> {
if let Some(provider) = self.provider {
Ok(provider)
} else {
Err(SignerError::NoProvider)
}
}
} }
trait SignerC { trait SignerC {
/// Connects to a provider /// Signs the hash of the provided message after prefixing it
fn connect<'a>(self, provider: &'a Provider) -> Self; fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature;
fn sign_transaction(&self, message: UnsignedTransaction) -> Transaction;
fn sign_message(message: &[u8]) -> Signature; }
impl<'a, N: Network> SignerC for Signer<'a, Wallet<N>> {
fn sign_message<S: AsRef<[u8]>>(&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)
}
} }