mod build;
pub mod confirm;
pub mod gas_price;
#[cfg(feature = "aws-kms")]
pub mod kms;
mod send;
pub use self::build::Transaction;
use self::confirm::ConfirmParams;
pub use self::gas_price::GasPrice;
pub use self::send::TransactionResult;
use crate::errors::ExecutionError;
use crate::secret::{Password, PrivateKey};
use web3::api::Web3;
use web3::types::{AccessList, Address, Bytes, CallRequest, TransactionCondition, U256};
use web3::Transport;
#[derive(Clone, Debug)]
pub enum Account {
Local(Address, Option<TransactionCondition>),
Locked(Address, Password, Option<TransactionCondition>),
Offline(PrivateKey, Option<u64>),
#[cfg(feature = "aws-kms")]
Kms(kms::Account, Option<u64>),
}
impl Account {
pub fn address(&self) -> Address {
match self {
Account::Local(address, _) => *address,
Account::Locked(address, _, _) => *address,
Account::Offline(key, _) => key.public_address(),
#[cfg(feature = "aws-kms")]
Account::Kms(kms, _) => kms.public_address(),
}
}
}
#[derive(Clone, Debug)]
pub enum ResolveCondition {
Pending,
Confirmed(ConfirmParams),
}
impl Default for ResolveCondition {
fn default() -> Self {
ResolveCondition::Confirmed(Default::default())
}
}
#[derive(Clone, Debug)]
#[must_use = "transactions do nothing unless you `.build()` or `.send()` them"]
pub struct TransactionBuilder<T: Transport> {
web3: Web3<T>,
pub from: Option<Account>,
pub to: Option<Address>,
pub gas: Option<U256>,
pub gas_price: Option<GasPrice>,
pub value: Option<U256>,
pub data: Option<Bytes>,
pub nonce: Option<U256>,
pub resolve: Option<ResolveCondition>,
pub access_list: Option<AccessList>,
}
impl<T: Transport> TransactionBuilder<T> {
pub fn new(web3: Web3<T>) -> Self {
TransactionBuilder {
web3,
from: None,
to: None,
gas: None,
gas_price: None,
value: None,
data: None,
nonce: None,
resolve: None,
access_list: None,
}
}
pub fn from(mut self, value: Account) -> Self {
self.from = Some(value);
self
}
pub fn to(mut self, value: Address) -> Self {
self.to = Some(value);
self
}
pub fn gas(mut self, value: U256) -> Self {
self.gas = Some(value);
self
}
pub fn gas_price(mut self, value: GasPrice) -> Self {
self.gas_price = Some(value);
self
}
pub fn value(mut self, value: U256) -> Self {
self.value = Some(value);
self
}
pub fn data(mut self, value: Bytes) -> Self {
self.data = Some(value);
self
}
pub fn nonce(mut self, value: U256) -> Self {
self.nonce = Some(value);
self
}
pub fn resolve(mut self, value: ResolveCondition) -> Self {
self.resolve = Some(value);
self
}
pub fn access_list(mut self, value: AccessList) -> Self {
self.access_list = Some(value);
self
}
pub fn confirmations(mut self, value: usize) -> Self {
self.resolve = match self.resolve {
Some(ResolveCondition::Confirmed(params)) => {
Some(ResolveCondition::Confirmed(ConfirmParams {
confirmations: value,
..params
}))
}
_ => Some(ResolveCondition::Confirmed(
ConfirmParams::with_confirmations(value),
)),
};
self
}
pub async fn estimate_gas(self) -> Result<U256, ExecutionError> {
let from = self.from.map(|account| account.address());
let resolved_gas_price = self
.gas_price
.map(|gas_price| gas_price.resolve_for_transaction())
.unwrap_or_default();
self.web3
.eth()
.estimate_gas(
CallRequest {
from,
to: self.to,
gas: None,
gas_price: resolved_gas_price.gas_price,
value: self.value,
data: self.data.clone(),
transaction_type: resolved_gas_price.transaction_type,
access_list: self.access_list,
max_fee_per_gas: resolved_gas_price.max_fee_per_gas,
max_priority_fee_per_gas: resolved_gas_price.max_priority_fee_per_gas,
},
None,
)
.await
.map_err(From::from)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::prelude::*;
use hex_literal::hex;
use web3::types::{AccessListItem, H2048, H256};
#[test]
fn tx_builder_estimate_gas() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let to = addr!("0x0123456789012345678901234567890123456789");
transport.add_response(json!("0x42")); let estimate_gas = TransactionBuilder::new(web3)
.to(to)
.value(42.into())
.estimate_gas()
.immediate()
.expect("success");
assert_eq!(estimate_gas, 0x42.into());
transport.assert_request(
"eth_estimateGas",
&[json!({
"to": to,
"value": "0x2a",
})],
);
transport.assert_no_more_requests();
}
#[test]
fn tx_send_local() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let from = addr!("0x9876543210987654321098765432109876543210");
let to = addr!("0x0123456789012345678901234567890123456789");
let hash = hash!("0x4242424242424242424242424242424242424242424242424242424242424242");
transport.add_response(json!(hash)); let tx = TransactionBuilder::new(web3)
.from(Account::Local(from, Some(TransactionCondition::Block(100))))
.to(to)
.gas(1.into())
.gas_price(2.0.into())
.value(28.into())
.data(Bytes(vec![0x13, 0x37]))
.nonce(42.into())
.access_list(vec![AccessListItem::default()])
.resolve(ResolveCondition::Pending)
.send()
.immediate()
.expect("transaction success");
transport.assert_request(
"eth_sendTransaction",
&[json!({
"from": from,
"to": to,
"gas": "0x1",
"gasPrice": "0x2",
"value": "0x1c",
"data": "0x1337",
"nonce": "0x2a",
"accessList": [{
"address": "0x0000000000000000000000000000000000000000",
"storageKeys": [],
}],
"condition": { "block": 100 },
})],
);
transport.assert_no_more_requests();
assert_eq!(tx.hash(), hash);
}
#[test]
fn tx_send_with_confirmations() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let key = key!("0x0102030405060708091011121314151617181920212223242526272829303132");
let chain_id = 77777;
let tx_hash = H256(hex!(
"248988e44deaff5162c3f998a8b1f510862366a68ef4339dff6ec89e120a6c19"
));
transport.add_response(json!(tx_hash));
transport.add_response(json!("0x1"));
transport.add_response(json!(null));
transport.add_response(json!("0x2"));
transport.add_response(json!("0x3"));
transport.add_response(json!({
"transactionHash": tx_hash,
"transactionIndex": "0x1",
"blockNumber": "0x2",
"blockHash": H256::repeat_byte(3),
"cumulativeGasUsed": "0x1337",
"gasUsed": "0x1337",
"logsBloom": H2048::zero(),
"logs": [],
"status": "0x1",
"effectiveGasPrice": "0x0",
}));
let builder = TransactionBuilder::new(web3)
.from(Account::Offline(key, Some(chain_id)))
.to(Address::zero())
.gas(0x1337.into())
.gas_price(f64::from(0x00ba_b10c).into())
.nonce(0x42.into())
.confirmations(1);
let tx_raw = builder
.clone()
.build()
.wait()
.expect("failed to sign transaction")
.raw()
.expect("offline transactions always build into raw transactions");
let tx_receipt = builder
.send()
.wait()
.expect("send with confirmations failed");
assert_eq!(tx_receipt.hash(), tx_hash);
transport.assert_request("eth_sendRawTransaction", &[json!(tx_raw)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(tx_hash)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(tx_hash)]);
transport.assert_no_more_requests();
}
#[test]
fn tx_failure() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let key = key!("0x0102030405060708091011121314151617181920212223242526272829303132");
let chain_id = 77777;
let tx_hash = H256(hex!(
"248988e44deaff5162c3f998a8b1f510862366a68ef4339dff6ec89e120a6c19"
));
transport.add_response(json!(tx_hash));
transport.add_response(json!("0x1"));
transport.add_response(json!({
"transactionHash": tx_hash,
"transactionIndex": "0x1",
"blockNumber": "0x1",
"blockHash": H256::repeat_byte(1),
"cumulativeGasUsed": "0x1337",
"gasUsed": "0x1337",
"logsBloom": H2048::zero(),
"logs": [],
"effectiveGasPrice": "0x0",
}));
let builder = TransactionBuilder::new(web3)
.from(Account::Offline(key, Some(chain_id)))
.to(Address::zero())
.gas(0x1337.into())
.gas_price(f64::from(0x00ba_b10c).into())
.nonce(0x42.into());
let tx_raw = builder
.clone()
.build()
.immediate()
.expect("failed to sign transaction")
.raw()
.expect("offline transactions always build into raw transactions");
let result = builder.send().immediate();
assert!(
matches!(
&result,
Err(ExecutionError::Failure(ref tx)) if tx.transaction_hash == tx_hash
),
"expected transaction failure with hash {} but got {:?}",
tx_hash,
result
);
transport.assert_request("eth_sendRawTransaction", &[json!(tx_raw)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(tx_hash)]);
transport.assert_no_more_requests();
}
}