deduplicate tx types

This commit is contained in:
Georgios Konstantopoulos 2020-05-24 20:17:46 +03:00
parent 2b7c4ce7b4
commit 08a25fb848
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
8 changed files with 86 additions and 90 deletions

View File

@ -1,4 +1,4 @@
use ethers::{types::UnsignedTransaction, HttpProvider, MainnetWallet}; use ethers::{types::TransactionRequest, HttpProvider, MainnetWallet};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::str::FromStr; use std::str::FromStr;
@ -9,21 +9,23 @@ async fn main() -> Result<(), failure::Error> {
// create a wallet and connect it to the provider // create a wallet and connect it to the provider
let client = MainnetWallet::from_str( let client = MainnetWallet::from_str(
"d8ebe1e50cfea1f9961908d9df28e64bb163fee9ee48320361b2eb0a54974269", "15c42bf2987d5a8a73804a8ea72fb4149f88adf73e98fc3f8a8ce9f24fcb7774",
)? )?
.connect(&provider); .connect(&provider);
// get the account's nonce (we abuse the Deref to access the provider's functions) // get the account's nonce (we abuse the Deref to access the provider's functions)
let nonce = client.get_transaction_count(client.address(), None).await?; let nonce = client.get_transaction_count(client.address(), None).await?;
dbg!(nonce);
// craft the transaction // craft the transaction
let tx = UnsignedTransaction { let tx = TransactionRequest {
from: None,
to: Some("986eE0C8B91A58e490Ee59718Cca41056Cf55f24".parse().unwrap()), to: Some("986eE0C8B91A58e490Ee59718Cca41056Cf55f24".parse().unwrap()),
gas: 21000.into(), gas: Some(21000.into()),
gas_price: 100_000.into(), gas_price: Some(100_000.into()),
value: 10000.into(), value: Some(10000.into()),
input: vec![].into(), data: Some(vec![].into()),
nonce, nonce: Some(nonce),
}; };
// send it! // send it!

View File

@ -8,11 +8,11 @@ use std::convert::TryFrom;
async fn main() -> Result<(), failure::Error> { async fn main() -> Result<(), failure::Error> {
// connect to the network // connect to the network
let provider = HttpProvider::try_from("http://localhost:8545")?; let provider = HttpProvider::try_from("http://localhost:8545")?;
let from = "4916064D2E9C1b2ccC466EEc3d30B2b08F1C130D".parse()?; let from = "784C1bA9846aB4CE78E9CFa27884E29dd31d593A".parse()?;
// craft the tx // craft the tx
let tx = TransactionRequest { let tx = TransactionRequest {
from, from: Some(from),
to: Some("9A7e5d4bcA656182e66e33340d776D1542143006".parse()?), to: Some("9A7e5d4bcA656182e66e33340d776D1542143006".parse()?),
value: Some(1000u64.into()), value: Some(1000u64.into()),
gas: None, gas: None,

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
providers::{JsonRpcClient, Provider}, providers::{JsonRpcClient, Provider},
signers::Signer, signers::Signer,
types::{Address, Transaction, UnsignedTransaction}, types::{Address, Transaction, TransactionRequest},
}; };
use std::ops::Deref; use std::ops::Deref;
@ -17,10 +17,10 @@ impl<'a, S: Signer, P: JsonRpcClient> Client<'a, S, P> {
/// API /// API
pub async fn sign_and_send_transaction( pub async fn sign_and_send_transaction(
&self, &self,
tx: UnsignedTransaction, tx: TransactionRequest,
) -> Result<Transaction, P::Error> { ) -> Result<Transaction, P::Error> {
// sign the transaction // sign the transaction
let signed_tx = self.signer.sign_transaction(tx.clone()); let signed_tx = self.signer.sign_transaction(tx).unwrap(); // TODO
// broadcast it // broadcast it
self.provider.send_raw_transaction(&signed_tx).await?; self.provider.send_raw_transaction(&signed_tx).await?;

View File

@ -1,4 +1,8 @@
//! Sign and broadcast transactions //! Sign and broadcast transactions
//!
//! Implement the `Signer` trait to add support for new signers, e.g. with Ledger.
//!
//! TODO: We might need a `SignerAsync` trait for HSM use cases?
mod networks; mod networks;
pub use networks::instantiated::*; pub use networks::instantiated::*;
use networks::Network; use networks::Network;
@ -9,17 +13,19 @@ pub use wallet::Wallet;
mod client; mod client;
pub(crate) use client::Client; pub(crate) use client::Client;
use crate::types::{Address, Signature, Transaction, UnsignedTransaction}; use crate::types::{Address, Signature, Transaction, TransactionRequest};
use std::error::Error;
/// Trait for signing transactions and messages /// Trait for signing transactions and messages
/// ///
/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. /// Implement this trait to support different signing modes, e.g. Ledger, hosted etc.
pub trait Signer { pub trait Signer {
type Error: Error;
/// Signs the hash of the provided message after prefixing it /// Signs the hash of the provided message after prefixing it
fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature; fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature;
/// Signs the transaction /// Signs the transaction
fn sign_transaction(&self, message: UnsignedTransaction) -> Transaction; fn sign_transaction(&self, message: TransactionRequest) -> Result<Transaction, Self::Error>;
/// Returns the signer's Ethereum Address /// Returns the signer's Ethereum Address
fn address(&self) -> Address; fn address(&self) -> Address;

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
providers::{JsonRpcClient, Provider}, providers::{JsonRpcClient, Provider},
signers::{Client, Network, Signer}, signers::{Client, Network, Signer},
types::{Address, PrivateKey, PublicKey, Signature, Transaction, UnsignedTransaction}, types::{Address, PrivateKey, PublicKey, Signature, Transaction, TransactionRequest, TxError},
}; };
use rand::Rng; use rand::Rng;
@ -17,11 +17,13 @@ pub struct Wallet<N> {
} }
impl<'a, N: Network> Signer for Wallet<N> { impl<'a, N: Network> Signer for Wallet<N> {
type Error = TxError;
fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature { fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature {
self.private_key.sign(message) self.private_key.sign(message)
} }
fn sign_transaction(&self, tx: UnsignedTransaction) -> Transaction { fn sign_transaction(&self, tx: TransactionRequest) -> Result<Transaction, Self::Error> {
self.private_key.sign_transaction(tx, N::CHAIN_ID) self.private_key.sign_transaction(tx, N::CHAIN_ID)
} }

View File

@ -4,10 +4,11 @@ use secp256k1::{
}; };
use std::ops::Deref; use std::ops::Deref;
use std::str::FromStr; use std::str::FromStr;
use thiserror::Error;
use zeroize::DefaultIsZeroes; use zeroize::DefaultIsZeroes;
use crate::{ use crate::{
types::{Address, Signature, Transaction, UnsignedTransaction, H256, U256, U64}, types::{Address, Signature, Transaction, TransactionRequest, H256, U256, U64},
utils::{hash_message, keccak256}, utils::{hash_message, keccak256},
}; };
@ -24,6 +25,16 @@ impl FromStr for PrivateKey {
} }
} }
#[derive(Clone, Debug, Error)]
pub enum TxError {
#[error("no nonce was specified")]
NonceMissing,
#[error("no gas price was specified")]
GasPriceMissing,
#[error("no gas was specified")]
GasMissing,
}
impl PrivateKey { impl PrivateKey {
pub fn new<R: Rng>(rng: &mut R) -> Self { pub fn new<R: Rng>(rng: &mut R) -> Self {
PrivateKey(SecretKey::new(rng)) PrivateKey(SecretKey::new(rng))
@ -50,7 +61,15 @@ impl PrivateKey {
/// RLP encodes and then signs the stransaction. If no chain_id is provided, then EIP-155 is /// RLP encodes and then signs the stransaction. If no chain_id is provided, then EIP-155 is
/// not used. /// not used.
pub fn sign_transaction(&self, tx: UnsignedTransaction, chain_id: Option<U64>) -> Transaction { pub fn sign_transaction(
&self,
tx: TransactionRequest,
chain_id: Option<U64>,
) -> Result<Transaction, TxError> {
let nonce = tx.nonce.ok_or(TxError::NonceMissing)?;
let gas_price = tx.gas_price.ok_or(TxError::NonceMissing)?;
let gas = tx.gas.ok_or(TxError::NonceMissing)?;
// Hash the transaction's RLP encoding // Hash the transaction's RLP encoding
let hash = tx.hash(chain_id); let hash = tx.hash(chain_id);
let message = Message::from_slice(hash.as_bytes()).expect("hash is non-zero 32-bytes; qed"); let message = Message::from_slice(hash.as_bytes()).expect("hash is non-zero 32-bytes; qed");
@ -60,15 +79,15 @@ impl PrivateKey {
let rlp = tx.rlp_signed(&signature); let rlp = tx.rlp_signed(&signature);
let hash = keccak256(&rlp.0); let hash = keccak256(&rlp.0);
Transaction { Ok(Transaction {
hash: hash.into(), hash: hash.into(),
nonce: tx.nonce, nonce,
from: self.into(), from: self.into(),
to: tx.to, to: tx.to,
value: tx.value, value: tx.value.unwrap_or_default(),
gas_price: tx.gas_price, gas_price,
gas: tx.gas, gas,
input: tx.input, input: tx.data.unwrap_or_default(),
v: signature.v.into(), v: signature.v.into(),
r: U256::from_big_endian(signature.r.as_bytes()), r: U256::from_big_endian(signature.r.as_bytes()),
s: U256::from_big_endian(signature.s.as_bytes()), s: U256::from_big_endian(signature.s.as_bytes()),
@ -77,7 +96,7 @@ impl PrivateKey {
block_hash: None, block_hash: None,
block_number: None, block_number: None,
transaction_index: None, transaction_index: None,
} })
} }
fn sign_with_eip155(&self, message: &Message, chain_id: Option<U64>) -> Signature { fn sign_with_eip155(&self, message: &Message, chain_id: Option<U64>) -> Signature {
@ -193,13 +212,14 @@ mod tests {
fn signs_tx() { fn signs_tx() {
// retrieved test vector from: // retrieved test vector from:
// https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
let tx = UnsignedTransaction { let tx = TransactionRequest {
from: None,
to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse().unwrap()), to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse().unwrap()),
value: 1_000_000_000.into(), value: Some(1_000_000_000.into()),
gas: 2_000_000.into(), gas: Some(2_000_000.into()),
nonce: 0.into(), nonce: Some(0.into()),
gas_price: 21_000_000_000u128.into(), gas_price: Some(21_000_000_000u128.into()),
input: Bytes::new(), data: None,
}; };
let chain_id = 1; let chain_id = 1;
@ -207,7 +227,7 @@ mod tests {
.parse() .parse()
.unwrap(); .unwrap();
let tx = key.sign_transaction(tx, Some(chain_id.into())); let tx = key.sign_transaction(tx, Some(chain_id.into())).unwrap();
assert_eq!( assert_eq!(
tx.hash, tx.hash,

View File

@ -5,12 +5,10 @@ pub use ethereum_types::H256 as TxHash;
pub use ethereum_types::{Address, H256, U256, U64}; pub use ethereum_types::{Address, H256, U256, U64};
mod transaction; mod transaction;
// TODO: Figure out some more intuitive way instead of having 3 similarly named structs pub use transaction::{Transaction, TransactionRequest};
// with the same fields
pub use transaction::{Transaction, TransactionRequest, UnsignedTransaction};
mod keys; mod keys;
pub use keys::{PrivateKey, PublicKey}; pub use keys::{PrivateKey, PublicKey, TxError};
mod signature; mod signature;
pub use signature::Signature; pub use signature::Signature;

View File

@ -1,23 +1,17 @@
//! Transaction types //! 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::{ use crate::{
types::{Address, Bytes, Signature, H256, U256, U64}, types::{Address, Bytes, Signature, H256, U256, U64},
utils::keccak256, utils::keccak256,
}; };
use rlp::RlpStream;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Parameters for sending a transaction to via the eth_sendTransaction API. /// Parameters for sending a transaction
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)] #[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
pub struct TransactionRequest { pub struct TransactionRequest {
/// Sender address or ENS name /// Sender address or ENS name
pub from: Address, #[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<Address>,
/// Recipient address (None for contract creation) /// Recipient address (None for contract creation)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@ -46,40 +40,14 @@ pub struct TransactionRequest {
pub nonce: Option<U256>, pub nonce: Option<U256>,
} }
/// A raw unsigned transaction where all the information is already specified impl TransactionRequest {
#[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) { fn rlp_base(&self, rlp: &mut RlpStream) {
rlp.append(&self.nonce); rlp_opt(rlp, self.nonce);
rlp.append(&self.gas_price); rlp_opt(rlp, self.gas_price);
rlp.append(&self.gas); rlp_opt(rlp, self.gas);
rlp_opt(rlp, &self.to); rlp_opt(rlp, self.to);
rlp.append(&self.value); rlp_opt(rlp, self.value);
rlp.append(&self.input.0); rlp_opt(rlp, self.data.as_ref().map(|d| &d.0[..]));
} }
pub fn hash(&self, chain_id: Option<U64>) -> H256 { pub fn hash(&self, chain_id: Option<U64>) -> H256 {
@ -107,6 +75,14 @@ impl UnsignedTransaction {
} }
} }
fn rlp_opt<T: rlp::Encodable>(rlp: &mut RlpStream, opt: Option<T>) {
if let Some(ref inner) = opt {
rlp.append(inner);
} else {
rlp.append(&"");
}
}
/// Details of a signed transaction /// Details of a signed transaction
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Transaction { pub struct Transaction {
@ -172,7 +148,7 @@ impl Transaction {
rlp.append(&self.nonce); rlp.append(&self.nonce);
rlp.append(&self.gas_price); rlp.append(&self.gas_price);
rlp.append(&self.gas); rlp.append(&self.gas);
rlp_opt(&mut rlp, &self.to); rlp_opt(&mut rlp, self.to);
rlp.append(&self.value); rlp.append(&self.value);
rlp.append(&self.input.0); rlp.append(&self.input.0);
rlp.append(&self.v); rlp.append(&self.v);
@ -183,21 +159,13 @@ impl Transaction {
} }
} }
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn decode_unsigned_transaction() { fn decode_unsigned_transaction() {
let _res: UnsignedTransaction = serde_json::from_str( let _res: TransactionRequest = serde_json::from_str(
r#"{ r#"{
"gas":"0xc350", "gas":"0xc350",
"gasPrice":"0x4a817c800", "gasPrice":"0x4a817c800",