//! Transaction types use crate::{ types::{Address, Bloom, Bytes, Log, NameOrAddress, Signature, H256, U256, U64}, utils::keccak256, }; use rlp::RlpStream; use serde::{Deserialize, Serialize}; // Number of tx fields before signing #[cfg(not(feature = "celo"))] const UNSIGNED_TX_FIELDS: usize = 6; // Celo has 3 additional fields #[cfg(feature = "celo")] const UNSIGNED_TX_FIELDS: usize = 9; // Unsigned fields + signature [r s v] const SIGNED_TX_FIELDS: usize = UNSIGNED_TX_FIELDS + 3; /// Parameters for sending a transaction #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct TransactionRequest { /// Sender address or ENS name #[serde(skip_serializing_if = "Option::is_none")] pub from: Option
, /// Recipient address (None for contract creation) #[serde(skip_serializing_if = "Option::is_none")] pub to: Option, /// Supplied gas (None for sensible default) #[serde(skip_serializing_if = "Option::is_none")] pub gas: Option, /// Gas price (None for sensible default) #[serde(rename = "gasPrice")] #[serde(skip_serializing_if = "Option::is_none")] pub gas_price: Option, /// Transfered value (None for no transfer) #[serde(skip_serializing_if = "Option::is_none")] pub value: Option, /// 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 #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, /// Transaction nonce (None for next available nonce) #[serde(skip_serializing_if = "Option::is_none")] pub nonce: Option, ///////////////// Celo-specific transaction fields ///////////////// /// The currency fees are paid in (None for native currency) #[cfg(feature = "celo")] #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] #[serde(skip_serializing_if = "Option::is_none")] pub fee_currency: Option
, /// Gateway fee recipient (None for no gateway fee paid) #[cfg(feature = "celo")] #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] #[serde(skip_serializing_if = "Option::is_none")] pub gateway_fee_recipient: Option
, /// Gateway fee amount (None for no gateway fee paid) #[cfg(feature = "celo")] #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] #[serde(skip_serializing_if = "Option::is_none")] pub gateway_fee: Option, } impl TransactionRequest { /// Creates an empty transaction request with all fields left empty pub fn new() -> Self { Self::default() } /// Convenience function for sending a new payment transaction to the receiver. pub fn pay, V: Into>(to: T, value: V) -> Self { TransactionRequest { to: Some(to.into()), value: Some(value.into()), ..Default::default() } } // Builder pattern helpers /// Sets the `from` field in the transaction to the provided value pub fn from>(mut self, from: T) -> Self { self.from = Some(from.into()); self } /// Sets the `to` field in the transaction to the provided value pub fn to>(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>(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>(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>(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>(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>(mut self, nonce: T) -> Self { self.nonce = Some(nonce.into()); self } /// Hashes the transaction's data with the provided chain id pub fn sighash>(&self, chain_id: Option) -> H256 { keccak256(self.rlp(chain_id).as_ref()).into() } /// Gets the unsigned transaction's RLP encoding pub fn rlp>(&self, chain_id: Option) -> Bytes { let mut rlp = RlpStream::new(); // "If [..] CHAIN_ID is available, then when computing the hash of a // transaction for the purposes of signing, instead of hashing only // six rlp encoded elements (nonce, gasprice, startgas, to, value, data), // you SHOULD hash nine rlp encoded elements // (nonce, gasprice, startgas, to, value, data, chainid, 0, 0)" // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#specification let num_els = if chain_id.is_some() { UNSIGNED_TX_FIELDS + 3 } else { UNSIGNED_TX_FIELDS }; rlp.begin_list(num_els); self.rlp_base(&mut rlp); // Only hash the 3 extra fields when preparing the // data to sign if chain_id is present if let Some(chain_id) = chain_id { rlp.append(&chain_id.into()); rlp.append(&0u8); rlp.append(&0u8); } rlp.out().freeze().into() } /// Produces the RLP encoding of the transaction with the provided signature pub fn rlp_signed(&self, signature: &Signature) -> Bytes { let mut rlp = RlpStream::new(); // construct the RLP body rlp.begin_list(SIGNED_TX_FIELDS); self.rlp_base(&mut rlp); // append the signature rlp.append(&signature.v); rlp.append(&signature.r); rlp.append(&signature.s); rlp.out().freeze().into() } fn rlp_base(&self, rlp: &mut RlpStream) { rlp_opt(rlp, self.nonce); rlp_opt(rlp, self.gas_price); rlp_opt(rlp, self.gas); #[cfg(feature = "celo")] self.inject_celo_metadata(rlp); rlp_opt(rlp, self.to.as_ref()); rlp_opt(rlp, self.value); rlp_opt(rlp, self.data.as_ref().map(|d| d.as_ref())); } } // Separate impl block for the celo-specific fields #[cfg(feature = "celo")] impl TransactionRequest { // modifies the RLP stream with the Celo-specific information fn inject_celo_metadata(&self, rlp: &mut RlpStream) { rlp_opt(rlp, self.fee_currency); rlp_opt(rlp, self.gateway_fee_recipient); rlp_opt(rlp, self.gateway_fee); } /// Sets the `fee_currency` field in the transaction to the provided value #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] pub fn fee_currency>(mut self, fee_currency: T) -> Self { self.fee_currency = Some(fee_currency.into()); self } /// Sets the `gateway_fee` field in the transaction to the provided value #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] pub fn gateway_fee>(mut self, gateway_fee: T) -> Self { self.gateway_fee = Some(gateway_fee.into()); self } /// Sets the `gateway_fee_recipient` field in the transaction to the provided value #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] pub fn gateway_fee_recipient>(mut self, gateway_fee_recipient: T) -> Self { self.gateway_fee_recipient = Some(gateway_fee_recipient.into()); self } } fn rlp_opt(rlp: &mut RlpStream, opt: Option) { if let Some(ref inner) = opt { rlp.append(inner); } else { rlp.append(&""); } } /// Details of a signed transaction #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] pub struct Transaction { /// The transaction's hash pub hash: H256, /// The transaction's nonce pub nonce: U256, /// Block hash. None when pending. #[serde(rename = "blockHash")] #[serde(skip_serializing_if = "Option::is_none")] pub block_hash: Option, /// Block number. None when pending. #[serde(rename = "blockNumber")] #[serde(skip_serializing_if = "Option::is_none")] pub block_number: Option, /// Transaction Index. None when pending. #[serde(rename = "transactionIndex")] #[serde(skip_serializing_if = "Option::is_none")] pub transaction_index: Option, /// Sender pub from: Address, /// Recipient (None when contract creation) #[serde(skip_serializing_if = "Option::is_none")] pub to: Option
, /// Transfered value pub value: U256, /// Gas Price #[serde(rename = "gasPrice")] pub gas_price: U256, /// Gas amount pub gas: U256, /// Input data pub input: Bytes, /// ECDSA recovery id pub v: U64, /// ECDSA signature r pub r: U256, /// ECDSA signature s pub s: U256, ///////////////// Celo-specific transaction fields ///////////////// /// The currency fees are paid in (None for native currency) #[cfg(feature = "celo")] #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] #[serde(skip_serializing_if = "Option::is_none", rename = "feeCurrency")] pub fee_currency: Option
, /// Gateway fee recipient (None for no gateway fee paid) #[cfg(feature = "celo")] #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] #[serde( skip_serializing_if = "Option::is_none", rename = "gatewayFeeRecipient" )] pub gateway_fee_recipient: Option
, /// Gateway fee amount (None for no gateway fee paid) #[cfg(feature = "celo")] #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] #[serde(skip_serializing_if = "Option::is_none", rename = "gatewayFee")] pub gateway_fee: Option, } impl Transaction { // modifies the RLP stream with the Celo-specific information // This is duplicated from TransactionRequest. Is there a good way to get rid // of this code duplication? #[cfg(feature = "celo")] fn inject_celo_metadata(&self, rlp: &mut RlpStream) { rlp_opt(rlp, self.fee_currency); rlp_opt(rlp, self.gateway_fee_recipient); rlp_opt(rlp, self.gateway_fee); } pub fn hash(&self) -> H256 { keccak256(&self.rlp().as_ref()).into() } pub fn rlp(&self) -> Bytes { let mut rlp = RlpStream::new(); rlp.begin_list(SIGNED_TX_FIELDS); rlp.append(&self.nonce); rlp.append(&self.gas_price); rlp.append(&self.gas); #[cfg(feature = "celo")] self.inject_celo_metadata(&mut rlp); rlp_opt(&mut rlp, self.to); rlp.append(&self.value); rlp.append(&self.input.as_ref()); rlp.append(&self.v); rlp.append(&self.r); rlp.append(&self.s); rlp.out().freeze().into() } } /// "Receipt" of an executed transaction: details of its execution. #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct TransactionReceipt { /// Transaction hash. #[serde(rename = "transactionHash")] pub transaction_hash: H256, /// Index within the block. #[serde(rename = "transactionIndex")] pub transaction_index: U64, /// Hash of the block this transaction was included within. #[serde(rename = "blockHash")] pub block_hash: Option, /// Number of the block this transaction was included within. #[serde(rename = "blockNumber")] pub block_number: Option, /// Cumulative gas used within the block after this was executed. #[serde(rename = "cumulativeGasUsed")] pub cumulative_gas_used: U256, /// Gas used by this transaction alone. /// /// Gas used is `None` if the the client is running in light client mode. #[serde(rename = "gasUsed")] pub gas_used: Option, /// Contract address created, or `None` if not a deployment. #[serde(rename = "contractAddress")] pub contract_address: Option
, /// Logs generated within this transaction. pub logs: Vec, /// Status: either 1 (success) or 0 (failure). Only present after activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) pub status: Option, /// State root. Only present before activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) pub root: Option, /// Logs bloom #[serde(rename = "logsBloom")] pub logs_bloom: Bloom, } #[cfg(test)] #[cfg(not(feature = "celo"))] mod tests { use super::*; #[test] fn decode_unsigned_transaction() { let _res: TransactionRequest = serde_json::from_str( r#"{ "gas":"0xc350", "gasPrice":"0x4a817c800", "hash":"0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b", "input":"0x68656c6c6f21", "nonce":"0x15", "to":"0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb", "transactionIndex":"0x41", "value":"0xf3dbb76162000", "chain_id": "0x1" }"#, ) .unwrap(); } #[test] fn decode_transaction_response() { let _res: Transaction = serde_json::from_str( r#"{ "blockHash":"0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2", "blockNumber":"0x5daf3b", "from":"0xa7d9ddbe1f17865597fbd27ec712455208b6b76d", "gas":"0xc350", "gasPrice":"0x4a817c800", "hash":"0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b", "input":"0x68656c6c6f21", "nonce":"0x15", "to":"0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb", "transactionIndex":"0x41", "value":"0xf3dbb76162000", "v":"0x25", "r":"0x1b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5fea", "s":"0x4ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c" }"#, ) .unwrap(); let _res: Transaction = serde_json::from_str( r#"{ "hash":"0xdd79ab0f996150aa3c9f135bbb9272cf0dedb830fafcbbf0c06020503565c44f", "nonce":"0xe", "blockHash":"0xef3fe1f532c3d8783a6257619bc123e9453aa8d6614e4cdb4cc8b9e1ed861404", "blockNumber":"0xf", "transactionIndex":"0x0", "from":"0x1b67b03cdccfae10a2d80e52d3d026dbe2960ad0", "to":"0x986ee0c8b91a58e490ee59718cca41056cf55f24", "value":"0x2710", "gas":"0x5208", "gasPrice":"0x186a0", "input":"0x", "v":"0x25", "r":"0x75188beb2f601bb8cf52ef89f92a6ba2bb7edcf8e3ccde90548cc99cbea30b1e", "s":"0xc0559a540f16d031f3404d5df2bb258084eee56ed1193d8b534bb6affdb3c2c" }"#, ) .unwrap(); } }