use crate::errors::ExecutionError;
use crate::secret::{Password, PrivateKey};
use crate::transaction::gas_price::GasPrice;
use crate::transaction::{Account, TransactionBuilder};
use web3::api::Web3;
use web3::types::{
AccessList, Address, Bytes, CallRequest, RawTransaction, SignedTransaction,
TransactionCondition, TransactionParameters, TransactionRequest, H256, U256,
};
use web3::Transport;
impl<T: Transport> TransactionBuilder<T> {
pub async fn build(self) -> Result<Transaction, ExecutionError> {
let options = TransactionOptions {
to: self.to,
gas: self.gas,
gas_price: self.gas_price,
value: self.value,
data: self.data,
nonce: self.nonce,
access_list: self.access_list,
};
let tx = match self.from {
None => Transaction::Request(
build_transaction_request_for_local_signing(
self.web3,
None,
TransactionRequestOptions(options, None),
)
.await?,
),
Some(Account::Local(from, condition)) => Transaction::Request(
build_transaction_request_for_local_signing(
self.web3,
Some(from),
TransactionRequestOptions(options, condition),
)
.await?,
),
Some(Account::Locked(from, password, condition)) => {
build_transaction_signed_with_locked_account(
self.web3,
from,
password,
TransactionRequestOptions(options, condition),
)
.await
.map(|signed| Transaction::Raw {
bytes: signed.raw,
hash: signed.tx.hash,
})?
}
Some(Account::Offline(key, chain_id)) => {
build_offline_signed_transaction(self.web3, key, chain_id, options)
.await
.map(|signed| Transaction::Raw {
bytes: signed.raw_transaction,
hash: signed.transaction_hash,
})?
}
};
Ok(tx)
}
}
#[derive(Clone, Debug, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum Transaction {
Request(TransactionRequest),
Raw {
bytes: Bytes,
hash: H256,
},
}
impl Transaction {
pub fn request(self) -> Option<TransactionRequest> {
match self {
Transaction::Request(tx) => Some(tx),
_ => None,
}
}
pub fn raw(self) -> Option<Bytes> {
match self {
Transaction::Raw { bytes, .. } => Some(bytes),
_ => None,
}
}
}
#[derive(Clone, Debug, Default)]
struct TransactionOptions {
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 access_list: Option<AccessList>,
}
#[derive(Clone, Debug, Default)]
struct TransactionRequestOptions(TransactionOptions, Option<TransactionCondition>);
impl TransactionRequestOptions {
fn build_request(self, from: Address, gas: Option<U256>) -> TransactionRequest {
let resolved_gas_price = self
.0
.gas_price
.map(|gas_price| gas_price.resolve_for_transaction())
.unwrap_or_default();
TransactionRequest {
from,
to: self.0.to,
gas,
gas_price: resolved_gas_price.gas_price,
value: self.0.value,
data: self.0.data,
nonce: self.0.nonce,
condition: self.1,
transaction_type: resolved_gas_price.transaction_type,
access_list: self.0.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,
}
}
}
async fn build_transaction_request_for_local_signing<T: Transport>(
web3: Web3<T>,
from: Option<Address>,
options: TransactionRequestOptions,
) -> Result<TransactionRequest, ExecutionError> {
let from = match from {
Some(address) => address,
None => *web3
.eth()
.accounts()
.await?
.get(0)
.ok_or(ExecutionError::NoLocalAccounts)?,
};
let gas = resolve_gas_limit(&web3, from, &options.0).await?;
let request = options.build_request(from, Some(gas));
Ok(request)
}
async fn build_transaction_signed_with_locked_account<T: Transport>(
web3: Web3<T>,
from: Address,
password: Password,
options: TransactionRequestOptions,
) -> Result<RawTransaction, ExecutionError> {
let gas = resolve_gas_limit(&web3, from, &options.0).await?;
let request = options.build_request(from, Some(gas));
let signed_tx = web3.personal().sign_transaction(request, &password).await?;
Ok(signed_tx)
}
async fn build_offline_signed_transaction<T: Transport>(
web3: Web3<T>,
key: PrivateKey,
chain_id: Option<u64>,
options: TransactionOptions,
) -> Result<SignedTransaction, ExecutionError> {
let gas = resolve_gas_limit(&web3, key.public_address(), &options).await?;
let resolved_gas_price = options
.gas_price
.map(|gas_price| gas_price.resolve_for_transaction())
.unwrap_or_default();
let signed = web3
.accounts()
.sign_transaction(
TransactionParameters {
nonce: options.nonce,
gas_price: resolved_gas_price.gas_price,
gas,
to: options.to,
value: options.value.unwrap_or_default(),
data: options.data.unwrap_or_default(),
chain_id,
transaction_type: resolved_gas_price.transaction_type,
access_list: options.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,
},
&key,
)
.await?;
Ok(signed)
}
async fn resolve_gas_limit<T: Transport>(
web3: &Web3<T>,
from: Address,
options: &TransactionOptions,
) -> Result<U256, ExecutionError> {
let resolved_gas_price = options
.gas_price
.map(|gas_price| gas_price.resolve_for_transaction())
.unwrap_or_default();
match options.gas {
Some(value) => Ok(value),
None => Ok(web3
.eth()
.estimate_gas(
CallRequest {
from: Some(from),
to: options.to,
gas: None,
gas_price: resolved_gas_price.gas_price,
value: options.value,
data: options.data.clone(),
transaction_type: resolved_gas_price.transaction_type,
access_list: options.access_list.clone(),
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?),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::prelude::*;
#[test]
fn tx_build_local() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let from = addr!("0x9876543210987654321098765432109876543210");
transport.add_response(json!("0x9a5"));
let tx = build_transaction_request_for_local_signing(
web3,
Some(from),
TransactionRequestOptions::default(),
)
.immediate()
.expect("failed to build local transaction");
transport.assert_request(
"eth_estimateGas",
&[json!({"from": "0x9876543210987654321098765432109876543210"})],
);
transport.assert_no_more_requests();
assert_eq!(tx.from, from);
}
#[test]
fn tx_build_local_default_account() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let accounts = [
addr!("0x9876543210987654321098765432109876543210"),
addr!("0x1111111111111111111111111111111111111111"),
addr!("0x2222222222222222222222222222222222222222"),
];
transport.add_response(json!(accounts)); transport.add_response(json!("0x9a5")); let tx = build_transaction_request_for_local_signing(
web3,
None,
TransactionRequestOptions::default(),
)
.immediate()
.expect("failed to build local transaction");
transport.assert_request("eth_accounts", &[]);
transport.assert_request(
"eth_estimateGas",
&[json!({"from": "0x9876543210987654321098765432109876543210"})],
);
transport.assert_no_more_requests();
assert_eq!(tx.from, accounts[0]);
assert_eq!(tx.gas_price, None);
}
#[test]
fn tx_build_local_default_account_with_extra_gas_price() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let accounts = [
addr!("0x9876543210987654321098765432109876543210"),
addr!("0x1111111111111111111111111111111111111111"),
addr!("0x2222222222222222222222222222222222222222"),
];
transport.add_response(json!(accounts)); transport.add_response(json!("0x9a5")); let tx = build_transaction_request_for_local_signing(
web3,
None,
TransactionRequestOptions {
0: TransactionOptions {
gas_price: Some(66.0.into()),
..Default::default()
},
..Default::default()
},
)
.immediate()
.expect("failed to build local transaction");
transport.assert_request("eth_accounts", &[]);
transport.assert_request(
"eth_estimateGas",
&[json!({ "from": json!(accounts[0]), "gasPrice": format!("{:#x}", 66), })],
);
transport.assert_no_more_requests();
assert_eq!(tx.from, accounts[0]);
assert_eq!(tx.gas_price, Some(U256::from(0x42)));
assert_eq!(tx.gas, Some(U256::from(0x9a5)));
}
#[test]
fn tx_build_local_with_explicit_gas_price() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let from = addr!("0xffffffffffffffffffffffffffffffffffffffff");
transport.add_response(json!("0x9a5"));
let options = TransactionRequestOptions {
0: TransactionOptions {
gas_price: Some(1337.0.into()),
..Default::default()
},
..Default::default()
};
let tx = build_transaction_request_for_local_signing(web3, Some(from), options)
.immediate()
.expect("failed to build local transaction");
transport.assert_request(
"eth_estimateGas",
&[json!({ "from": json!(from) , "gasPrice": format!("{:#x}", 1337)})],
);
transport.assert_no_more_requests();
assert_eq!(tx.from, from);
assert_eq!(tx.gas_price, Some(1337.into()));
}
#[test]
fn tx_build_local_no_local_accounts() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
transport.add_response(json!([])); let err = build_transaction_request_for_local_signing(
web3,
None,
TransactionRequestOptions::default(),
)
.immediate()
.expect_err("unexpected success building transaction");
transport.assert_request("eth_accounts", &[]);
transport.assert_no_more_requests();
assert!(
matches!(err, ExecutionError::NoLocalAccounts),
"expected no local accounts error but got '{:?}'",
err
);
}
#[test]
fn tx_build_locked() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let from = addr!("0x9876543210987654321098765432109876543210");
let pw = "foobar";
let to = addr!("0x0000000000000000000000000000000000000000");
let signed = bytes!("0x0123456789"); let hash = H256::from_low_u64_be(1);
let gas = json!("0x9a5");
transport.add_response(gas.clone());
transport.add_response(json!({
"raw": signed,
"tx": {
"hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
"nonce": "0x0",
"from": from,
"value": "0x0",
"gas": "0x0",
"gasPrice": "0x0",
"input": "0x",
}
})); let tx = build_transaction_signed_with_locked_account(
web3,
from,
pw.into(),
TransactionRequestOptions(
TransactionOptions {
to: Some(to),
..Default::default()
},
None,
),
)
.immediate()
.expect("failed to build locked transaction");
transport.assert_request(
"eth_estimateGas",
&[json!({ "from": json!(from) , "to": json!(to)})],
);
transport.assert_request(
"personal_signTransaction",
&[
json!({
"from": from,
"to": to,
"gas": gas
}),
json!(pw),
],
);
transport.assert_no_more_requests();
assert_eq!(tx.raw, signed);
assert_eq!(tx.tx.hash, hash);
}
#[test]
fn tx_build_offline() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let key = key!("0x0102030405060708091011121314151617181920212223242526272829303132");
let from: Address = key.public_address();
let to = addr!("0x0000000000000000000000000000000000000000");
let gas = uint!("0x9a5");
let gas_price = uint!("0x1ce");
let nonce = uint!("0x42");
let chain_id = 77777;
transport.add_response(json!(gas));
transport.add_response(json!(nonce));
transport.add_response(json!(format!("{:#x}", chain_id)));
let tx1 = build_offline_signed_transaction(
web3.clone(),
key.clone(),
None,
TransactionOptions {
to: Some(to),
gas_price: Some(gas_price.into()),
..Default::default()
},
)
.immediate()
.expect("failed to build offline transaction");
transport.assert_request(
"eth_estimateGas",
&[json!({
"from": from,
"to": to,
"gasPrice": gas_price,
})],
);
transport.assert_request("eth_getTransactionCount", &[json!(from), json!("latest")]);
transport.assert_request("eth_chainId", &[]);
transport.assert_no_more_requests();
let tx2 = build_offline_signed_transaction(
web3,
key,
Some(chain_id),
TransactionOptions {
to: Some(to),
gas: Some(gas),
gas_price: Some(gas_price.into()),
nonce: Some(nonce),
..Default::default()
},
)
.immediate()
.expect("failed to build offline transaction");
transport.assert_no_more_requests();
assert_eq!(tx1, tx2);
}
}