deduplicate tx types
This commit is contained in:
parent
2b7c4ce7b4
commit
08a25fb848
|
@ -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!
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue