diff --git a/README.md b/README.md index 735677257..b31cd6be4 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,24 @@ You can get one [here](https://polygonscan.io/apis). There is abigen support for Avalanche and the Fuji test network. It is recommended that you set the `SNOWTRACE_API_KEY` environment variable. You can get one [here](https://snowtrace.io/apis). +### Optimism support + +Optimism is supported via the `optimism` feature flag: + +```toml +[dependencies] +ethers = { version = "2.0", features = ["optimism"] } +``` + +Optimism has a new transaction type: [Deposited Transactions](https://github.com/ethereum-optimism/optimism/blob/develop/specs/deposits.md#the-deposited-transaction-type) +with type ID `0x7E`, which requires 3 new fields: + +- `sourceHash`: The hash which uniquely identifies the origin of the deposit +- `mint`: The ETH value to mint on L2. +- `isSystemTx`: True if the tx does not interact with the L2 block gas pool + +**Note:** the `optimism` and `celo` features are mutually exclusive. + ### Celo Support [Celo](https://celo.org) support is turned on via the feature-flag `celo`: @@ -76,6 +94,8 @@ Celo's transactions differ from Ethereum transactions by including 3 new fields: The feature flag enables these additional fields in the transaction request builders and in the transactions which are fetched over JSON-RPC. +**Note:** the `optimism` and `celo` features are mutually exclusive. + ## Features - [x] Ethereum JSON-RPC Client @@ -87,6 +107,7 @@ in the transactions which are fetched over JSON-RPC. - [x] Celo support - [x] Polygon support - [x] Avalanche support +- [x] Optimism support - [x] Websockets / `eth_subscribe` - [x] Hardware Wallet Support - [x] Parity APIs (`tracing`, `parity_blockWithReceipts`) diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index 03cc9d419..533327dc7 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -70,6 +70,7 @@ rand.workspace = true celo = ["legacy"] # celo support extends the transaction format with extra fields legacy = [] macros = ["syn", "cargo_metadata", "once_cell"] +optimism = [] # Deprecated eip712 = [] diff --git a/ethers-core/src/types/transaction/response.rs b/ethers-core/src/types/transaction/response.rs index 5ad0c93cc..595bcdf42 100644 --- a/ethers-core/src/types/transaction/response.rs +++ b/ethers-core/src/types/transaction/response.rs @@ -65,6 +65,22 @@ pub struct Transaction { /// ECDSA signature s pub s: U256, + ///////////////// Optimism-specific transaction fields ////////////// + /// The source-hash that uniquely identifies the origin of the deposit + #[cfg(feature = "optimism")] + #[serde(default, skip_serializing_if = "Option::is_none", rename = "sourceHash")] + pub source_hash: Option, + + /// The ETH value to mint on L2 + #[cfg(feature = "optimism")] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mint: Option, + + /// True if the transaction does not interact with the L2 block gas pool + #[cfg(feature = "optimism")] + #[serde(default, skip_serializing_if = "Option::is_none", rename = "isSystemTx")] + pub is_system_tx: Option, + ///////////////// Celo-specific transaction fields ///////////////// /// The currency fees are paid in (None for native currency) #[cfg(feature = "celo")] @@ -142,7 +158,7 @@ impl Transaction { match self.transaction_type { // EIP-2930 (0x01) - Some(x) if x == U64::from(1) => { + Some(x) if x == U64::from(0x1) => { rlp_opt(&mut rlp, &self.chain_id); rlp.append(&self.nonce); rlp_opt(&mut rlp, &self.gas_price); @@ -158,9 +174,11 @@ impl Transaction { if let Some(chain_id) = self.chain_id { rlp.append(&normalize_v(self.v.as_u64(), U64::from(chain_id.as_u64()))); } + rlp.append(&self.r); + rlp.append(&self.s); } // EIP-1559 (0x02) - Some(x) if x == U64::from(2) => { + Some(x) if x == U64::from(0x2) => { rlp_opt(&mut rlp, &self.chain_id); rlp.append(&self.nonce); rlp_opt(&mut rlp, &self.max_priority_fee_per_gas); @@ -173,6 +191,20 @@ impl Transaction { if let Some(chain_id) = self.chain_id { rlp.append(&normalize_v(self.v.as_u64(), U64::from(chain_id.as_u64()))); } + rlp.append(&self.r); + rlp.append(&self.s); + } + // Optimism Deposited Transaction + #[cfg(feature = "optimism")] + Some(x) if x == U64::from(0x7E) => { + rlp_opt(&mut rlp, &self.source_hash); + rlp.append(&self.from); + rlp_opt(&mut rlp, &self.to); + rlp_opt(&mut rlp, &self.mint); + rlp.append(&self.value); + rlp.append(&self.gas); + rlp_opt(&mut rlp, &self.is_system_tx); + rlp.append(&self.input.as_ref()); } // Legacy (0x00) _ => { @@ -187,27 +219,32 @@ impl Transaction { rlp.append(&self.value); rlp.append(&self.input.as_ref()); rlp.append(&self.v); + rlp.append(&self.r); + rlp.append(&self.s); } } - rlp.append(&self.r); - rlp.append(&self.s); - rlp.finalize_unbounded_list(); let rlp_bytes: Bytes = rlp.out().freeze().into(); let mut encoded = vec![]; match self.transaction_type { - Some(x) if x == U64::from(1) => { + Some(x) if x == U64::from(0x1) => { encoded.extend_from_slice(&[0x1]); encoded.extend_from_slice(rlp_bytes.as_ref()); encoded.into() } - Some(x) if x == U64::from(2) => { + Some(x) if x == U64::from(0x2) => { encoded.extend_from_slice(&[0x2]); encoded.extend_from_slice(rlp_bytes.as_ref()); encoded.into() } + #[cfg(feature = "optimism")] + Some(x) if x == U64::from(0x7E) => { + encoded.extend_from_slice(&[0x7E]); + encoded.extend_from_slice(rlp_bytes.as_ref()); + encoded.into() + } _ => rlp_bytes, } } @@ -1127,4 +1164,42 @@ mod tests { }; Transaction::decode(&Rlp::new(&tx.rlp())).unwrap(); } + + #[test] + #[cfg(feature = "optimism")] + fn test_rlp_encode_deposited_tx() { + let deposited_tx = Transaction { + hash: H256::from_str("0x7fd17d4a368fccdba4291ab121e48c96329b7dc3d027a373643fb23c20a19a3f").unwrap(), + nonce: U256::from(4391989), + block_hash: Some(H256::from_str("0xc2794a16acacd9f7670379ffd12b6968ff98e2a602f57d7d1f880220aa5a4973").unwrap()), + block_number: Some(8453214u64.into()), + transaction_index: Some(0u64.into()), + from: Address::from_str("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001").unwrap(), + to: Some(Address::from_str("0x4200000000000000000000000000000000000015").unwrap()), + value: U256::zero(), + gas_price: Some(U256::zero()), + gas: U256::from(1000000u64), + input: Bytes::from( + hex::decode("015d8eb90000000000000000000000000000000000000000000000000000000000878c1c00000000000000000000000000000000000000000000000000000000644662bc0000000000000000000000000000000000000000000000000000001ee24fba17b7e19cc10812911dfa8a438e0a81a9933f843aa5b528899b8d9e221b649ae0df00000000000000000000000000000000000000000000000000000000000000060000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240").unwrap() + ), + v: U64::zero(), + r: U256::zero(), + s: U256::zero(), + source_hash: Some(H256::from_str("0xa8157ccf61bcdfbcb74a84ec1262e62644dd1e7e3614abcbd8db0c99a60049fc").unwrap()), + mint: Some(0.into()), + is_system_tx: None, + transaction_type: Some(U64::from(126)), + access_list: None, + max_priority_fee_per_gas: None, + max_fee_per_gas: None, + chain_id: None, + other: Default::default() + }; + + let rlp = deposited_tx.rlp(); + + let expected_rlp = Bytes::from(hex::decode("7ef90159a0a8157ccf61bcdfbcb74a84ec1262e62644dd1e7e3614abcbd8db0c99a60049fc94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b90104015d8eb90000000000000000000000000000000000000000000000000000000000878c1c00000000000000000000000000000000000000000000000000000000644662bc0000000000000000000000000000000000000000000000000000001ee24fba17b7e19cc10812911dfa8a438e0a81a9933f843aa5b528899b8d9e221b649ae0df00000000000000000000000000000000000000000000000000000000000000060000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240").unwrap()); + + assert_eq!(rlp, expected_rlp); + } } diff --git a/ethers/Cargo.toml b/ethers/Cargo.toml index 0de802363..1f152f214 100644 --- a/ethers/Cargo.toml +++ b/ethers/Cargo.toml @@ -37,6 +37,8 @@ celo = [ "legacy", ] +optimism = ["ethers-core/optimism"] + rustls = [ "ethers-contract/rustls", "ethers-etherscan/rustls",