use builder pattern for txs
This commit is contained in:
parent
f8ca81ab22
commit
96add97869
|
@ -1,6 +1,5 @@
|
||||||
use ethers::{types::TransactionRequest, HttpProvider, MainnetWallet};
|
use ethers::{types::TransactionRequest, HttpProvider, MainnetWallet};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), failure::Error> {
|
async fn main() -> Result<(), failure::Error> {
|
||||||
|
@ -8,28 +7,17 @@ async fn main() -> Result<(), failure::Error> {
|
||||||
let provider = HttpProvider::try_from("http://localhost:8545")?;
|
let provider = HttpProvider::try_from("http://localhost:8545")?;
|
||||||
|
|
||||||
// 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 = "15c42bf2987d5a8a73804a8ea72fb4149f88adf73e98fc3f8a8ce9f24fcb7774"
|
||||||
"15c42bf2987d5a8a73804a8ea72fb4149f88adf73e98fc3f8a8ce9f24fcb7774",
|
.parse::<MainnetWallet>()?
|
||||||
)?
|
|
||||||
.connect(&provider);
|
.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
|
// craft the transaction
|
||||||
let tx = TransactionRequest {
|
let tx = TransactionRequest::new()
|
||||||
from: None,
|
.send_to_str("986eE0C8B91A58e490Ee59718Cca41056Cf55f24")?
|
||||||
to: Some("986eE0C8B91A58e490Ee59718Cca41056Cf55f24".parse().unwrap()),
|
.value(10000);
|
||||||
gas: Some(21000.into()),
|
|
||||||
gas_price: Some(100_000.into()),
|
|
||||||
value: Some(10000.into()),
|
|
||||||
data: Some(vec![].into()),
|
|
||||||
nonce: Some(nonce),
|
|
||||||
};
|
|
||||||
|
|
||||||
// send it!
|
// send it!
|
||||||
let tx = client.sign_and_send_transaction(tx).await?;
|
let tx = client.sign_and_send_transaction(tx, None).await?;
|
||||||
|
|
||||||
// get the mined tx
|
// get the mined tx
|
||||||
let tx = client.get_transaction(tx.hash).await?;
|
let tx = client.get_transaction(tx.hash).await?;
|
||||||
|
|
|
@ -12,15 +12,10 @@ async fn main() -> Result<(), failure::Error> {
|
||||||
let from = accounts[0];
|
let from = accounts[0];
|
||||||
|
|
||||||
// craft the tx
|
// craft the tx
|
||||||
let tx = TransactionRequest {
|
let tx = TransactionRequest::new()
|
||||||
from: Some(from),
|
.send_to_str("9A7e5d4bcA656182e66e33340d776D1542143006")?
|
||||||
to: Some("9A7e5d4bcA656182e66e33340d776D1542143006".parse()?),
|
.value(1000)
|
||||||
value: Some(1000u64.into()),
|
.from(from); // specify the `from` field so that the client knows which account to use
|
||||||
gas: None,
|
|
||||||
gas_price: None,
|
|
||||||
data: None,
|
|
||||||
nonce: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let balance_before = provider.get_balance(from, None).await?;
|
let balance_before = provider.get_balance(from, None).await?;
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,29 @@ impl<P: JsonRpcClient> Provider<P> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cost related
|
||||||
|
|
||||||
|
/// Gets the current gas price as estimated by the node
|
||||||
|
pub async fn get_gas_price(&self) -> Result<U256, P::Error> {
|
||||||
|
self.0.request("eth_gasPrice", None::<()>).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to estimate the gas for the transaction
|
||||||
|
pub async fn estimate_gas(
|
||||||
|
&self,
|
||||||
|
tx: &TransactionRequest,
|
||||||
|
block: Option<BlockNumber>,
|
||||||
|
) -> Result<U256, P::Error> {
|
||||||
|
let tx = utils::serialize(tx);
|
||||||
|
|
||||||
|
let args = match block {
|
||||||
|
Some(block) => vec![tx, utils::serialize(&block)],
|
||||||
|
None => vec![tx],
|
||||||
|
};
|
||||||
|
|
||||||
|
self.0.request("eth_estimateGas", Some(args)).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets the accounts on the node
|
/// Gets the accounts on the node
|
||||||
pub async fn get_accounts(&self) -> Result<Vec<Address>, P::Error> {
|
pub async fn get_accounts(&self) -> Result<Vec<Address>, P::Error> {
|
||||||
self.0.request("eth_accounts", None::<()>).await
|
self.0.request("eth_accounts", None::<()>).await
|
||||||
|
@ -65,6 +88,8 @@ impl<P: JsonRpcClient> Provider<P> {
|
||||||
self.0.request("eth_getTransactionByHash", Some(hash)).await
|
self.0.request("eth_getTransactionByHash", Some(hash)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// State mutations
|
||||||
|
|
||||||
/// Broadcasts the transaction request via the `eth_sendTransaction` API
|
/// Broadcasts the transaction request via the `eth_sendTransaction` API
|
||||||
pub async fn send_transaction(&self, tx: TransactionRequest) -> Result<TxHash, P::Error> {
|
pub async fn send_transaction(&self, tx: TransactionRequest) -> Result<TxHash, P::Error> {
|
||||||
self.0.request("eth_sendTransaction", Some(tx)).await
|
self.0.request("eth_sendTransaction", Some(tx)).await
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
providers::{JsonRpcClient, Provider},
|
providers::{JsonRpcClient, Provider},
|
||||||
signers::Signer,
|
signers::Signer,
|
||||||
types::{Address, Transaction, TransactionRequest},
|
types::{Address, BlockNumber, Transaction, TransactionRequest},
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
@ -17,8 +17,30 @@ 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: TransactionRequest,
|
mut tx: TransactionRequest,
|
||||||
|
block: Option<BlockNumber>,
|
||||||
) -> Result<Transaction, P::Error> {
|
) -> Result<Transaction, P::Error> {
|
||||||
|
// TODO: Convert to join'ed futures
|
||||||
|
// get the gas price
|
||||||
|
if tx.gas_price.is_none() {
|
||||||
|
tx.gas_price = Some(self.provider.get_gas_price().await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// estimate the gas
|
||||||
|
if tx.gas.is_none() {
|
||||||
|
tx.from = Some(self.address());
|
||||||
|
tx.gas = Some(self.provider.estimate_gas(&tx, block).await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set our nonce
|
||||||
|
if tx.nonce.is_none() {
|
||||||
|
tx.nonce = Some(
|
||||||
|
self.provider
|
||||||
|
.get_transaction_count(self.address(), block)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// sign the transaction
|
// sign the transaction
|
||||||
let signed_tx = self.signer.sign_transaction(tx).unwrap(); // TODO
|
let signed_tx = self.signer.sign_transaction(tx).unwrap(); // TODO
|
||||||
|
|
||||||
|
|
|
@ -59,13 +59,18 @@ impl PrivateKey {
|
||||||
self.sign_with_eip155(&sig_message, None)
|
self.sign_with_eip155(&sig_message, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RLP encodes and then signs the stransaction. If no chain_id is provided, then EIP-155 is
|
/// RLP encodes and then signs the stransaction.
|
||||||
/// not used.
|
///
|
||||||
|
/// If no chain_id is provided, then EIP-155 is not used.
|
||||||
|
///
|
||||||
|
/// This will return an error if called if any of the `nonce`, `gas_price` or `gas`
|
||||||
|
/// fields are not populated.
|
||||||
pub fn sign_transaction(
|
pub fn sign_transaction(
|
||||||
&self,
|
&self,
|
||||||
tx: TransactionRequest,
|
tx: TransactionRequest,
|
||||||
chain_id: Option<U64>,
|
chain_id: Option<U64>,
|
||||||
) -> Result<Transaction, TxError> {
|
) -> Result<Transaction, TxError> {
|
||||||
|
// Calling `
|
||||||
let nonce = tx.nonce.ok_or(TxError::NonceMissing)?;
|
let nonce = tx.nonce.ok_or(TxError::NonceMissing)?;
|
||||||
let gas_price = tx.gas_price.ok_or(TxError::NonceMissing)?;
|
let gas_price = tx.gas_price.ok_or(TxError::NonceMissing)?;
|
||||||
let gas = tx.gas.ok_or(TxError::NonceMissing)?;
|
let gas = tx.gas.ok_or(TxError::NonceMissing)?;
|
||||||
|
|
|
@ -5,9 +5,10 @@ use crate::{
|
||||||
};
|
};
|
||||||
use rlp::RlpStream;
|
use rlp::RlpStream;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// Parameters for sending a transaction
|
/// Parameters for sending a transaction
|
||||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
|
#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)]
|
||||||
pub struct TransactionRequest {
|
pub struct TransactionRequest {
|
||||||
/// Sender address or ENS name
|
/// Sender address or ENS name
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
@ -41,27 +42,89 @@ pub struct TransactionRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TransactionRequest {
|
impl TransactionRequest {
|
||||||
fn rlp_base(&self, rlp: &mut RlpStream) {
|
/// Creates an empty transaction request with all fields left empty
|
||||||
rlp_opt(rlp, self.nonce);
|
pub fn new() -> Self {
|
||||||
rlp_opt(rlp, self.gas_price);
|
Self::default()
|
||||||
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 {
|
/// Convenience function for sending a new payment transaction to the receiver. The
|
||||||
|
/// `gas`, `gas_price` and `nonce` fields are left empty, to be populated
|
||||||
|
pub fn pay<T: Into<Address>, V: Into<U256>>(to: T, value: V) -> Self {
|
||||||
|
TransactionRequest {
|
||||||
|
from: None,
|
||||||
|
to: Some(to.into()),
|
||||||
|
gas: None,
|
||||||
|
gas_price: None,
|
||||||
|
value: Some(value.into()),
|
||||||
|
data: None,
|
||||||
|
nonce: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builder pattern helpers
|
||||||
|
|
||||||
|
/// Sets the `from` field in the transaction to the provided value
|
||||||
|
pub fn from<T: Into<Address>>(mut self, from: T) -> Self {
|
||||||
|
self.from = Some(from.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `to` field in the transaction to the provided value
|
||||||
|
pub fn send_to_str(mut self, to: &str) -> Result<Self, rustc_hex::FromHexError> {
|
||||||
|
self.to = Some(Address::from_str(to)?);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `to` field in the transaction to the provided value
|
||||||
|
pub fn to<T: Into<Address>>(mut self, to: T) -> Self {
|
||||||
|
self.to = Some(to.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `gas` field in the transaction to the provided value
|
||||||
|
pub fn gas<T: Into<U256>>(mut self, gas: T) -> Self {
|
||||||
|
self.gas = Some(gas.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `gas_price` field in the transaction to the provided value
|
||||||
|
pub fn gas_price<T: Into<U256>>(mut self, gas_price: T) -> Self {
|
||||||
|
self.gas_price = Some(gas_price.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `value` field in the transaction to the provided value
|
||||||
|
pub fn value<T: Into<U256>>(mut self, value: T) -> Self {
|
||||||
|
self.value = Some(value.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `data` field in the transaction to the provided value
|
||||||
|
pub fn data<T: Into<Bytes>>(mut self, data: T) -> Self {
|
||||||
|
self.data = Some(data.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `nonce` field in the transaction to the provided value
|
||||||
|
pub fn nonce<T: Into<U256>>(mut self, nonce: T) -> Self {
|
||||||
|
self.nonce = Some(nonce.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hashes the transaction's data with the provided chain id
|
||||||
|
pub fn hash<T: Into<U64>>(&self, chain_id: Option<T>) -> H256 {
|
||||||
let mut rlp = RlpStream::new();
|
let mut rlp = RlpStream::new();
|
||||||
rlp.begin_list(9);
|
rlp.begin_list(9);
|
||||||
self.rlp_base(&mut rlp);
|
self.rlp_base(&mut rlp);
|
||||||
|
|
||||||
rlp.append(&chain_id.unwrap_or_else(U64::zero));
|
rlp.append(&chain_id.map(|c| c.into()).unwrap_or_else(U64::zero));
|
||||||
rlp.append(&0u8);
|
rlp.append(&0u8);
|
||||||
rlp.append(&0u8);
|
rlp.append(&0u8);
|
||||||
|
|
||||||
keccak256(rlp.out().as_ref()).into()
|
keccak256(rlp.out().as_ref()).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Produces the RLP encoding of the transaction with the provided signature
|
||||||
pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
|
pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
|
||||||
let mut rlp = RlpStream::new();
|
let mut rlp = RlpStream::new();
|
||||||
rlp.begin_list(9);
|
rlp.begin_list(9);
|
||||||
|
@ -73,6 +136,15 @@ impl TransactionRequest {
|
||||||
|
|
||||||
rlp.out().into()
|
rlp.out().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rlp_base(&self, rlp: &mut RlpStream) {
|
||||||
|
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[..]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rlp_opt<T: rlp::Encodable>(rlp: &mut RlpStream, opt: Option<T>) {
|
fn rlp_opt<T: rlp::Encodable>(rlp: &mut RlpStream, opt: Option<T>) {
|
||||||
|
|
Loading…
Reference in New Issue