diff --git a/Cargo.toml b/Cargo.toml index 8f5c9f8b..979cc7fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,12 @@ edition = "2018" arrayvec = "0.5.0" ethabi = "9.0.0" ethereum-types = "0.8.0" +ethereum-transaction = "0.2.0" +ethsign = "0.7.3" futures = "0.1.26" jsonrpc-core = "14.0.0" log = "0.4.6" +parity-crypto = "0.4.2" parking_lot = "0.9.0" rustc-hex = "2.0.1" serde = { version = "1.0.90", features = ["derive"] } diff --git a/src/api/accounts.rs b/src/api/accounts.rs new file mode 100644 index 00000000..4252f2ed --- /dev/null +++ b/src/api/accounts.rs @@ -0,0 +1,466 @@ +//! Partial implementation of the `Accounts` namespace. + +use crate::api::{Namespace, Web3}; +use crate::error::Error; +use crate::helpers::CallFuture; +use crate::types::{ + Address, Bytes, Recovery, RecoveryMessage, SignedData, SignedTransaction, TransactionParameters, H256, U256, +}; +use crate::Transport; +use ethereum_transaction::{ + Bytes as EthtxBytes, SignTransaction, SignedTransaction as EthtxSignedTransaction, Transaction, +}; +use ethsign::{Error as EthsignError, SecretKey}; +use futures::future::{self, Either, FutureResult, Join3}; +use futures::{Async, Future, Poll}; +use parity_crypto::Keccak256; +use std::borrow::Cow; +use std::mem; + +/// `Accounts` namespace +#[derive(Debug, Clone)] +pub struct Accounts { + transport: T, +} + +impl Namespace for Accounts { + fn new(transport: T) -> Self + where + Self: Sized, + { + Accounts { transport } + } + + fn transport(&self) -> &T { + &self.transport + } +} + +impl Accounts { + /// Gets the parent `web3` namespace + fn web3(&self) -> Web3 { + Web3::new(self.transport.clone()) + } + + /// Signs an Ethereum transaction with a given private key. + pub fn sign_transaction(&self, tx: TransactionParameters, key: &SecretKey) -> SignTransactionFuture { + SignTransactionFuture::new(self, tx, key) + } + + /// 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(&self, message: S) -> H256 + where + S: AsRef, + { + let message = message.as_ref(); + let eth_message = format!("\x19Ethereum Signed Message:\n{}{}", message.len(), message); + + eth_message.as_bytes().keccak256().into() + } + + /// 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 + /// such as `ethsign`. + pub fn sign(&self, message: S, key: &SecretKey) -> Result + where + S: AsRef, + { + let message = message.as_ref().to_string(); + let message_hash = self.hash_message(&message); + + let signature = key.sign(&message_hash[..]).map_err(EthsignError::from)?; + // convert to 'Electrum' notation + let v = signature.v + 27; + + let signature_bytes = Bytes({ + let mut bytes = Vec::with_capacity(65); + bytes.extend_from_slice(&signature.r[..]); + bytes.extend_from_slice(&signature.s[..]); + bytes.push(v); + bytes + }); + + Ok(SignedData { + message, + message_hash, + v, + r: signature.r.into(), + s: signature.s.into(), + signature: signature_bytes, + }) + } + + /// Recovers the Ethereum address which was used to sign the given data. + /// + /// Recovery signature data uses 'Electrum' notation, this means the `v` + /// value is expected to be either `27` or `28`. + pub fn recover(&self, recovery: R) -> Result + where + R: Into, + { + let recovery = recovery.into(); + let message_hash = match recovery.message { + RecoveryMessage::String(ref message) => self.hash_message(message), + RecoveryMessage::Hash(hash) => hash, + }; + let signature = recovery.as_signature(); + + let public_key = signature.recover(&message_hash[..]).map_err(EthsignError::from)?; + + Ok(public_key.address().into()) + } +} + +type MaybeReady = Either, CallFuture::Out>>; + +type TxParams = Join3, MaybeReady, MaybeReady>; + +/// Future resolving when transaction signing is complete. +/// +/// Transaction signing can perform RPC requests in order to fill missing +/// parameters required for signing `nonce`, `gas_price` and `chain_id`. Note +/// that if all transaction parameters were provided, this future will resolve +/// immediately. +pub struct SignTransactionFuture { + tx: TransactionParameters, + key: SecretKey, + inner: TxParams, +} + +impl SignTransactionFuture { + /// Creates a new SignTransactionFuture with accounts and transaction data. + pub fn new(accounts: &Accounts, tx: TransactionParameters, key: &SecretKey) -> SignTransactionFuture { + macro_rules! maybe { + ($o: expr, $f: expr) => { + match $o.clone() { + Some(value) => Either::A(future::ok(value)), + None => Either::B($f), + } + }; + } + + let from = key.public().address().into(); + let inner = Future::join3( + maybe!(tx.nonce, accounts.web3().eth().transaction_count(from, None)), + maybe!(tx.gas_price, accounts.web3().eth().gas_price()), + // TODO(nlordell): avoid converting chain ID to and from string, + // this will require wrapping the `Net::version()` call to convert + // the result from a string to a u64 + maybe!(tx.chain_id.map(|id| id.to_string()), accounts.web3().net().version()), + ); + + SignTransactionFuture { + tx, + key: key.clone(), + inner, + } + } +} + +impl Future for SignTransactionFuture { + type Item = SignedTransaction; + type Error = Error; + + fn poll(&mut self) -> Poll { + let (nonce, gas_price, chain_id) = try_ready!(self.inner.poll()); + let chain_id = chain_id.parse::().map_err(|e| Error::Decoder(e.to_string()))?; + + let data = mem::replace(&mut self.tx.data, Bytes::default()); + let tx = Transaction { + from: Address::zero(), // not used for signing. + to: self.tx.to, + nonce, + gas: self.tx.gas, + gas_price, + value: self.tx.value, + data: EthtxBytes(data.0), + }; + let signed = sign_transaction(tx, &self.key, chain_id)?; + + Ok(Async::Ready(signed)) + } +} + +/// Sign and return a raw signed transaction. +fn sign_transaction(tx: Transaction, key: &SecretKey, chain_id: u64) -> Result { + let tx = SignTransaction { + transaction: Cow::Owned(tx), + chain_id, + }; + + let hash = tx.hash(); + let sig = key.sign(&hash[..])?; + + let signed_tx = EthtxSignedTransaction::new(tx.transaction, tx.chain_id, sig.v, sig.r, sig.s); + let transaction_hash = signed_tx.hash().into(); + let raw_transaction = Bytes(signed_tx.to_rlp()); + + Ok(SignedTransaction { + message_hash: hash.into(), + v: signed_tx.v, + r: sig.r.into(), + s: sig.s.into(), + raw_transaction, + transaction_hash, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::tests::TestTransport; + use crate::types::Bytes; + use ethsign::SecretKey; + use rustc_hex::FromHex; + use serde_json::json; + + #[test] + fn accounts_sign_transaction() { + // retrieved test vector from: + // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction + + let tx = TransactionParameters { + to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse().unwrap()), + value: 1_000_000_000.into(), + gas: 2_000_000.into(), + ..Default::default() + }; + let secret: H256 = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + .parse() + .unwrap(); + let key = SecretKey::from_raw(&secret[..]).unwrap(); + let nonce = U256::zero(); + let gas_price = U256::from(21_000_000_000u128); + let chain_id = "1"; + let from: Address = key.public().address().into(); + + let mut transport = TestTransport::default(); + transport.add_response(json!(nonce)); + transport.add_response(json!(gas_price)); + transport.add_response(json!(chain_id)); + + let signed = { + let accounts = Accounts::new(&transport); + accounts.sign_transaction(tx, &key).wait() + }; + + transport.assert_request( + "eth_getTransactionCount", + &[json!(from).to_string(), json!("latest").to_string()], + ); + transport.assert_request("eth_gasPrice", &[]); + transport.assert_request("net_version", &[]); + transport.assert_no_more_requests(); + + let expected = SignedTransaction { + message_hash: "88cfbd7e51c7a40540b233cf68b62ad1df3e92462f1c6018d6d67eae0f3b08f5" + .parse() + .unwrap(), + v: 0x25, + r: "c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895" + .parse() + .unwrap(), + s: "727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68" + .parse() + .unwrap(), + raw_transaction: Bytes( + "f869808504e3b29200831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a0c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895a0727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68" + .from_hex() + .unwrap(), + ), + transaction_hash: "de8db924885b0803d2edc335f745b2b8750c8848744905684c20b987443a9593" + .parse() + .unwrap(), + }; + + assert_eq!(signed, Ok(expected)); + } + + #[test] + fn accounts_sign_transaction_with_all_parameters() { + let secret: Vec = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + .from_hex() + .unwrap(); + let key = SecretKey::from_raw(&secret).unwrap(); + + let accounts = Accounts::new(TestTransport::default()); + accounts + .sign_transaction( + TransactionParameters { + nonce: Some(0.into()), + gas_price: Some(1.into()), + chain_id: Some(42), + ..Default::default() + }, + &key, + ) + .wait() + .unwrap(); + + // sign_transaction makes no requests when all parameters are specified + accounts.transport().assert_no_more_requests(); + } + + #[test] + fn accounts_hash_message() { + // test vector taken from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#hashmessage + + let accounts = Accounts::new(TestTransport::default()); + let hash = accounts.hash_message("Hello World"); + + assert_eq!( + hash, + "a1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2" + .parse() + .unwrap() + ); + + // this method does not actually make any requests. + accounts.transport().assert_no_more_requests(); + } + + #[test] + fn accounts_sign() { + // test vector taken from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#sign + + let accounts = Accounts::new(TestTransport::default()); + + let secret: Vec = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + .from_hex() + .unwrap(); + let key = SecretKey::from_raw(&secret).unwrap(); + let signed = accounts.sign("Some data", &key).unwrap(); + + assert_eq!( + signed.message_hash, + "1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655" + .parse() + .unwrap() + ); + assert_eq!( + signed.signature.0, + "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c" + .from_hex::>() + .unwrap() + ); + + // this method does not actually make any requests. + accounts.transport().assert_no_more_requests(); + } + + #[test] + fn accounts_recover() { + // test vector taken from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#recover + + let accounts = Accounts::new(TestTransport::default()); + + let v = 0x1cu64; + let r: H256 = "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd" + .parse() + .unwrap(); + let s: H256 = "6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029" + .parse() + .unwrap(); + + let recovery = Recovery::new("Some data", v, r, s); + assert_eq!( + accounts.recover(recovery).unwrap(), + "2c7536E3605D9C16a7a3D7b1898e529396a65c23".parse().unwrap() + ); + + // this method does not actually make any requests. + accounts.transport().assert_no_more_requests(); + } + + #[test] + fn accounts_recover_signed() { + let secret: Vec = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + .from_hex() + .unwrap(); + let key = SecretKey::from_raw(&secret).unwrap(); + let address: Address = key.public().address().into(); + + let accounts = Accounts::new(TestTransport::default()); + + let signed = accounts.sign("rust-web3 rocks!", &key).unwrap(); + let recovered = accounts.recover(&signed).unwrap(); + assert_eq!(recovered, address); + + let signed = accounts + .sign_transaction( + TransactionParameters { + nonce: Some(0.into()), + gas_price: Some(1.into()), + chain_id: Some(42), + ..Default::default() + }, + &key, + ) + .wait() + .unwrap(); + let recovered = accounts.recover(&signed).unwrap(); + assert_eq!(recovered, address); + + // these methods make no requests + accounts.transport().assert_no_more_requests(); + } + + #[test] + fn sign_ethtx_transaction() { + // retrieved test vector from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#eth-accounts-signtransaction + + let tx = Transaction { + from: Default::default(), // not used for signing + nonce: 0.into(), + gas: 2_000_000.into(), + gas_price: 234_567_897_654_321u64.into(), + to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse().unwrap()), + value: 1_000_000_000.into(), + data: EthtxBytes(Vec::new()), + }; + let key = { + let raw: H256 = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + .parse() + .unwrap(); + SecretKey::from_raw(&raw[..]).expect("valid key") + }; + + let signed = sign_transaction(tx, &key, 1).unwrap(); + + let expected = SignedTransaction { + message_hash: "6893a6ee8df79b0f5d64a180cd1ef35d030f3e296a5361cf04d02ce720d32ec5" + .parse() + .unwrap(), + v: 0x25, + r: "09ebb6ca057a0535d6186462bc0b465b561c94a295bdb0621fc19208ab149a9c" + .parse() + .unwrap(), + s: "440ffd775ce91a833ab410777204d5341a6f9fa91216a6f3ee2c051fea6a0428" + .parse() + .unwrap(), + raw_transaction: Bytes( + "f86a8086d55698372431831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a009ebb6ca057a0535d6186462bc0b465b561c94a295bdb0621fc19208ab149a9ca0440ffd775ce91a833ab410777204d5341a6f9fa91216a6f3ee2c051fea6a0428" + .from_hex() + .unwrap(), + ), + transaction_hash: "d8f64a42b57be0d565f385378db2f6bf324ce14a594afc05de90436e9ce01f60" + .parse() + .unwrap(), + }; + + assert_eq!(signed, expected); + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index cb33cac3..c8ff477d 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,6 @@ //! `Web3` implementation +mod accounts; mod eth; mod eth_filter; mod eth_subscribe; @@ -10,6 +11,7 @@ mod personal; mod traces; mod web3; +pub use self::accounts::{Accounts, SignTransactionFuture}; pub use self::eth::Eth; pub use self::eth_filter::{BaseFilter, CreateFilter, EthFilter, FilterStream}; pub use self::eth_subscribe::{EthSubscribe, SubscriptionId, SubscriptionResult, SubscriptionStream}; @@ -56,6 +58,11 @@ impl Web3 { A::new(self.transport.clone()) } + /// Access methods from `accounts` namespace + pub fn accounts(&self) -> accounts::Accounts { + self.api() + } + /// Access methods from `eth` namespace pub fn eth(&self) -> eth::Eth { self.api() diff --git a/src/error.rs b/src/error.rs index 9edea04e..2a9c5ffe 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,7 @@ //! Web3 Error use crate::rpc::error::Error as RPCError; use derive_more::{Display, From}; +use ethsign::Error as EthsignError; use serde_json::Error as SerdeError; use std::io::Error as IoError; @@ -27,6 +28,10 @@ pub enum Error { /// io error #[display(fmt = "IO error: {}", _0)] Io(IoError), + /// signing error + #[display(fmt = "Signing error: {}", _0)] + #[from(ignore)] + Signing(String), /// web3 internal error #[display(fmt = "Internal Web3 error")] Internal, @@ -36,7 +41,7 @@ impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { use self::Error::*; match *self { - Unreachable | Decoder(_) | InvalidResponse(_) | Transport(_) | Internal => None, + Unreachable | Decoder(_) | InvalidResponse(_) | Transport(_) | Signing(_) | Internal => None, Rpc(ref e) => Some(e), Io(ref e) => Some(e), } @@ -49,6 +54,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: EthsignError) -> Self { + Error::Signing(format!("{:?}", err)) + } +} + impl Clone for Error { fn clone(&self) -> Self { use self::Error::*; @@ -59,6 +70,7 @@ impl Clone for Error { Transport(s) => Transport(s.clone()), Rpc(e) => Rpc(e.clone()), Io(e) => Io(IoError::from(e.kind())), + Signing(e) => Signing(e.clone()), Internal => Internal, } } @@ -69,9 +81,10 @@ impl PartialEq for Error { use self::Error::*; match (self, other) { (Unreachable, Unreachable) | (Internal, Internal) => true, - (Decoder(a), Decoder(b)) | (InvalidResponse(a), InvalidResponse(b)) | (Transport(a), Transport(b)) => { - a == b - } + (Decoder(a), Decoder(b)) + | (InvalidResponse(a), InvalidResponse(b)) + | (Transport(a), Transport(b)) + | (Signing(a), Signing(b)) => a == b, (Rpc(a), Rpc(b)) => a == b, (Io(a), Io(b)) => a.kind() == b.kind(), _ => false, diff --git a/src/helpers.rs b/src/helpers.rs index 448c0639..a439bcb7 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -145,7 +145,7 @@ pub mod tests { assert_eq!(p, params); } - pub fn assert_no_more_requests(&mut self) { + pub fn assert_no_more_requests(&self) { let requests = self.requests.borrow(); assert_eq!( self.asserted, diff --git a/src/types/mod.rs b/src/types/mod.rs index 3df0e2fc..5b4a9bd3 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -4,6 +4,8 @@ mod block; mod bytes; mod log; mod parity_peers; +mod recovery; +mod signed; mod sync_state; mod trace_filtering; mod traces; @@ -19,6 +21,8 @@ pub use self::log::{Filter, FilterBuilder, Log}; pub use self::parity_peers::{ EthProtocolInfo, ParityPeerInfo, ParityPeerType, PeerNetworkInfo, PeerProtocolsInfo, PipProtocolInfo, }; +pub use self::recovery::{Recovery, RecoveryMessage}; +pub use self::signed::{SignedData, SignedTransaction, TransactionParameters}; pub use self::sync_state::{SyncInfo, SyncState}; pub use self::trace_filtering::{ Action, ActionType, Call, CallResult, CallType, Create, CreateResult, Res, Reward, RewardType, Suicide, Trace, diff --git a/src/types/recovery.rs b/src/types/recovery.rs new file mode 100644 index 00000000..1c5b3e8d --- /dev/null +++ b/src/types/recovery.rs @@ -0,0 +1,201 @@ +use crate::types::{SignedData, SignedTransaction, H256}; +use ethereum_transaction::SignedTransaction as EthtxSignedTransaction; +use ethsign::Signature; +use std::error::Error; +use std::fmt::{Display, Formatter, Result as FmtResult}; + +/// Data for recovering the public address of signed data. +/// +/// Note that the signature data is in 'Electrum' notation and may have chain +/// replay protection applied. That means that `v` is expected to be `27`, `28`, +/// or `35 + chain_id * 2` or `36 + chain_id * 2`. +#[derive(Clone, Debug, PartialEq)] +pub struct Recovery { + /// The message to recover + pub message: RecoveryMessage, + /// V value. + pub v: u64, + /// R value. + pub r: H256, + /// S value. + pub s: H256, +} + +impl Recovery { + /// Creates new recovery data from its parts. + pub fn new(message: M, v: u64, r: H256, s: H256) -> Recovery + where + M: Into, + { + Recovery { + message: message.into(), + v, + r, + s, + } + } + + /// Creates new recovery data from a raw signature. + /// + /// This 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. + pub fn from_raw_signature(message: M, raw_signature: B) -> Result + where + M: Into, + B: AsRef<[u8]>, + { + let bytes = raw_signature.as_ref(); + + if bytes.len() != 65 { + return Err(ParseSignatureError); + } + + let v = bytes[64]; + let r = H256::from_slice(&bytes[0..32]); + let s = H256::from_slice(&bytes[32..64]); + + Ok(Recovery::new(message, v as _, r, s)) + } + + /// Retrieves the recovery signature in standard notation. + /// + /// This method returns a `ethsign::Signature` with `v` in standard + /// notation. This means that the `27`, `28` or chain relay protection is + /// removed from the recovery's v value. + pub fn as_signature(&self) -> Signature { + Signature { + v: self.standard_v(), + r: self.r.into(), + s: self.s.into(), + } + } + + /// Retrieve recovery value `v` in standard notation. + pub fn standard_v(&self) -> u8 { + // this is a re-implementation of `ethereum-transaction` as there is no + // good way to call this without building a full `SignedTransaction` + // which isn't needed here + match self.v { + 27 => 0, + 28 => 1, + v if v >= 35 => ((v - 1) % 2) as _, + _ => 4, + } + } +} + +impl<'a> From<&'a SignedData> for Recovery { + fn from(signed: &'a SignedData) -> Self { + Recovery::new(signed.message_hash, signed.v as _, signed.r, signed.s) + } +} + +impl<'a> From<&'a SignedTransaction> for Recovery { + fn from(tx: &'a SignedTransaction) -> Self { + Recovery::new(tx.message_hash, tx.v, tx.r, tx.s) + } +} + +impl<'a> From<&'a EthtxSignedTransaction<'a>> for Recovery { + fn from(tx: &'a EthtxSignedTransaction<'a>) -> Self { + Recovery::new(tx.bare_hash(), tx.v, H256(tx.r.into()), H256(tx.s.into())) + } +} + +/// Recovery message data. +/// +/// The message data can either be a string 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 string + String(String), + /// Message hash + Hash(H256), +} + +impl<'a> From<&'a str> for RecoveryMessage { + fn from(s: &'a str) -> Self { + s.to_owned().into() + } +} + +impl From for RecoveryMessage { + fn from(s: String) -> Self { + RecoveryMessage::String(s) + } +} + +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) + } +} + +/// An error parsing a raw signature. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct ParseSignatureError; + +impl Display for ParseSignatureError { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "error parsing raw signature: wrong number of bytes, expected 65") + } +} + +impl Error for ParseSignatureError {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::Bytes; + use rustc_hex::FromHex; + + #[test] + fn recovery_as_signature() { + let message = "Some data"; + let v = 0x1cu8; + let r: H256 = "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd" + .parse() + .unwrap(); + let s: H256 = "6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029" + .parse() + .unwrap(); + + let signed = SignedData { + message: message.to_string(), + message_hash: "1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655" + .parse() + .unwrap(), + v, + r, + s, + signature: Bytes( + "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c" + .from_hex() + .unwrap() + ), + }; + let expected_signature = Signature { + v: 0x01, + r: r.into(), + s: s.into(), + }; + + assert_eq!(Recovery::from(&signed).as_signature(), expected_signature); + assert_eq!(Recovery::new(message, v as _, r, s).as_signature(), expected_signature); + assert_eq!( + Recovery::from_raw_signature(message, &signed.signature.0) + .unwrap() + .as_signature(), + expected_signature + ); + } +} diff --git a/src/types/signed.rs b/src/types/signed.rs new file mode 100644 index 00000000..2b63e565 --- /dev/null +++ b/src/types/signed.rs @@ -0,0 +1,138 @@ +use crate::types::{Address, Bytes, CallRequest, H256, U256}; +use serde::{Deserialize, Serialize}; + +/// Struct representing signed data returned from `Accounts::sign` method. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub struct SignedData { + /// The original method that was signed. + pub message: String, + /// The keccak256 hash of the signed data. + #[serde(rename = "messageHash")] + pub message_hash: H256, + /// V value. + pub v: u8, + /// R value. + pub r: H256, + /// S value. + pub s: H256, + /// The signature bytes. + pub signature: Bytes, +} + +/// Transaction data for signing. +/// +/// The `Accounts::sign_transaction` method will fill optional fields with sane +/// defaults when they are ommited. Specifically the signing account's current +/// transaction count will be used for the `nonce`, the estimated recommended +/// gas price will be used for `gas_price`, and the current network ID will be +/// used for the `chain_id`. +/// +/// It is worth noting that the chain ID is not equivalent to the network ID. +/// They happen to be the same much of the time but it is recommended to set +/// this for signing transactions. +/// +/// `TransactionParameters` implements `Default` and uses `100_000` as the +/// default `gas` to use for the transaction. This is more than enough for +/// simple transactions sending ETH between accounts but may not be enough when +/// interacting with complex contracts. It is recommended when interacting +/// with contracts to use `Eth::estimate_gas` to estimate the required gas for +/// the transaction. +#[derive(Clone, Debug, PartialEq)] +pub struct TransactionParameters { + /// Transaction nonce (None for account transaction count) + pub nonce: Option, + /// To address + pub to: Option
, + /// Supplied gas + pub gas: U256, + /// Gas price (None for estimated gas price) + pub gas_price: Option, + /// Transfered value + pub value: U256, + /// Data + pub data: Bytes, + /// The chain ID (None for network ID) + pub chain_id: Option, +} + +/// The default fas for transactions. +/// +/// Unfortunatly there is no way to construct `U256`s with const functions for +/// constants so we just build it from it's `u64` words. Note that there is a +/// unit test to verify that it is constructed correctly and holds the expected +/// value of 100_000. +const TRANSACTION_DEFAULT_GAS: U256 = U256([100_000, 0, 0, 0]); + +impl Default for TransactionParameters { + fn default() -> Self { + TransactionParameters { + nonce: None, + to: None, + gas: TRANSACTION_DEFAULT_GAS, + gas_price: None, + value: U256::zero(), + data: Bytes::default(), + chain_id: None, + } + } +} + +impl From for TransactionParameters { + fn from(call: CallRequest) -> Self { + let to = if call.to != Address::zero() { + Some(call.to) + } else { + None + }; + + TransactionParameters { + nonce: None, + to, + gas: call.gas.unwrap_or(TRANSACTION_DEFAULT_GAS), + gas_price: call.gas_price, + value: call.value.unwrap_or_default(), + data: call.data.unwrap_or_default(), + chain_id: None, + } + } +} + +impl Into for TransactionParameters { + fn into(self) -> CallRequest { + CallRequest { + from: None, + to: self.to.unwrap_or_default(), + gas: Some(self.gas), + gas_price: self.gas_price, + value: Some(self.value), + data: Some(self.data), + } + } +} + +/// Data for offline signed transaction +#[derive(Clone, Debug, PartialEq)] +pub struct SignedTransaction { + /// The given message hash + pub message_hash: H256, + /// V value with chain replay protection. + pub v: u64, + /// R value. + pub r: H256, + /// S value. + pub s: H256, + /// The raw signed transaction ready to be sent with `send_raw_transaction` + pub raw_transaction: Bytes, + /// The transaction hash for the RLP encoded transaction. + pub transaction_hash: H256, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_transaction_default_gas() { + assert_eq!(TRANSACTION_DEFAULT_GAS, U256::from(100_000)); + } +}