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::str::FromStr;
@ -9,21 +9,23 @@ async fn main() -> Result<(), failure::Error> {
// create a wallet and connect it to the provider
let client = MainnetWallet::from_str(
"d8ebe1e50cfea1f9961908d9df28e64bb163fee9ee48320361b2eb0a54974269",
"15c42bf2987d5a8a73804a8ea72fb4149f88adf73e98fc3f8a8ce9f24fcb7774",
)?
.connect(&provider);
// 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?;
dbg!(nonce);
// craft the transaction
let tx = UnsignedTransaction {
let tx = TransactionRequest {
from: None,
to: Some("986eE0C8B91A58e490Ee59718Cca41056Cf55f24".parse().unwrap()),
gas: 21000.into(),
gas_price: 100_000.into(),
value: 10000.into(),
input: vec![].into(),
nonce,
gas: Some(21000.into()),
gas_price: Some(100_000.into()),
value: Some(10000.into()),
data: Some(vec![].into()),
nonce: Some(nonce),
};
// send it!

View File

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

View File

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

View File

@ -1,4 +1,8 @@
//! 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;
pub use networks::instantiated::*;
use networks::Network;
@ -9,17 +13,19 @@ pub use wallet::Wallet;
mod 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
///
/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc.
pub trait Signer {
type Error: Error;
/// Signs the hash of the provided message after prefixing it
fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature;
/// 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
fn address(&self) -> Address;

View File

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

View File

@ -4,10 +4,11 @@ use secp256k1::{
};
use std::ops::Deref;
use std::str::FromStr;
use thiserror::Error;
use zeroize::DefaultIsZeroes;
use crate::{
types::{Address, Signature, Transaction, UnsignedTransaction, H256, U256, U64},
types::{Address, Signature, Transaction, TransactionRequest, H256, U256, U64},
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 {
pub fn new<R: Rng>(rng: &mut R) -> Self {
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
/// 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
let hash = tx.hash(chain_id);
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 hash = keccak256(&rlp.0);
Transaction {
Ok(Transaction {
hash: hash.into(),
nonce: tx.nonce,
nonce,
from: self.into(),
to: tx.to,
value: tx.value,
gas_price: tx.gas_price,
gas: tx.gas,
input: tx.input,
value: tx.value.unwrap_or_default(),
gas_price,
gas,
input: tx.data.unwrap_or_default(),
v: signature.v.into(),
r: U256::from_big_endian(signature.r.as_bytes()),
s: U256::from_big_endian(signature.s.as_bytes()),
@ -77,7 +96,7 @@ impl PrivateKey {
block_hash: None,
block_number: None,
transaction_index: None,
}
})
}
fn sign_with_eip155(&self, message: &Message, chain_id: Option<U64>) -> Signature {
@ -193,13 +212,14 @@ mod tests {
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 {
let tx = TransactionRequest {
from: None,
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(),
value: Some(1_000_000_000.into()),
gas: Some(2_000_000.into()),
nonce: Some(0.into()),
gas_price: Some(21_000_000_000u128.into()),
data: None,
};
let chain_id = 1;
@ -207,7 +227,7 @@ mod tests {
.parse()
.unwrap();
let tx = key.sign_transaction(tx, Some(chain_id.into()));
let tx = key.sign_transaction(tx, Some(chain_id.into())).unwrap();
assert_eq!(
tx.hash,

View File

@ -5,12 +5,10 @@ pub use ethereum_types::H256 as TxHash;
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};
pub use transaction::{Transaction, TransactionRequest};
mod keys;
pub use keys::{PrivateKey, PublicKey};
pub use keys::{PrivateKey, PublicKey, TxError};
mod signature;
pub use signature::Signature;

View File

@ -1,23 +1,17 @@
//! 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 rlp::RlpStream;
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)]
pub struct TransactionRequest {
/// 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)
#[serde(skip_serializing_if = "Option::is_none")]
@ -46,40 +40,14 @@ pub struct TransactionRequest {
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 {
impl TransactionRequest {
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);
rlp_opt(rlp, self.nonce);
rlp_opt(rlp, self.gas_price);
rlp_opt(rlp, self.gas);
rlp_opt(rlp, self.to);
rlp_opt(rlp, self.value);
rlp_opt(rlp, self.data.as_ref().map(|d| &d.0[..]));
}
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
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Transaction {
@ -172,7 +148,7 @@ impl Transaction {
rlp.append(&self.nonce);
rlp.append(&self.gas_price);
rlp.append(&self.gas);
rlp_opt(&mut rlp, &self.to);
rlp_opt(&mut rlp, self.to);
rlp.append(&self.value);
rlp.append(&self.input.0);
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)]
mod tests {
use super::*;
#[test]
fn decode_unsigned_transaction() {
let _res: UnsignedTransaction = serde_json::from_str(
let _res: TransactionRequest = serde_json::from_str(
r#"{
"gas":"0xc350",
"gasPrice":"0x4a817c800",