WIP
This commit is contained in:
parent
1a49a62a81
commit
913b917620
13
Cargo.toml
13
Cargo.toml
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
12
src/lib.rs
12
src/lib.rs
|
@ -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;
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
/// A signature;
|
|
||||||
pub struct Signature([u8; 65]);
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(ð_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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
156
src/wallet.rs
156
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> {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue