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::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!
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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?;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue