From 05d560d3eeea88faf2da6c709a6866abdcff6d72 Mon Sep 17 00:00:00 2001 From: Edson Holanda Teixeira Junior <30915580+EdsonHTJ@users.noreply.github.com> Date: Mon, 22 Jan 2024 09:41:39 -0300 Subject: [PATCH] Add Eth Transaction From Raw (#32) * add eth decode tx * remove idea * add unity test * update format * remove .to_vec() from data * add from json method * update tests --- .gitignore | 3 +- .../kos-sdk/src/chains/ethereum/address.rs | 13 +++ packages/kos-sdk/src/chains/ethereum/mod.rs | 103 ++++++++++++++++++ .../src/chains/ethereum/transaction.rs | 83 +++++++++++++- 4 files changed, 196 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 60fecee..a8c2b32 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ dist .DS_Store demo/.DS_Store -packages/.DS_Store \ No newline at end of file +packages/.DS_Store +.idea/* \ No newline at end of file diff --git a/packages/kos-sdk/src/chains/ethereum/address.rs b/packages/kos-sdk/src/chains/ethereum/address.rs index 97453bb..d7ec3cc 100644 --- a/packages/kos-sdk/src/chains/ethereum/address.rs +++ b/packages/kos-sdk/src/chains/ethereum/address.rs @@ -2,6 +2,7 @@ use kos_crypto::keypair::KeyPair; use kos_types::error::Error; use hex::FromHex; +use rlp::{DecoderError, Rlp}; use std::{fmt, str::FromStr}; use web3::types::Address as Web3Address; @@ -165,6 +166,18 @@ impl AsRef<[u8]> for Address { } } +impl rlp::Decodable for Address { + fn decode(rlp: &Rlp) -> Result { + let mut data: Vec = rlp.as_val()?; + let mut bytes: [u8; ADDRESS_LEN] = [0; ADDRESS_LEN]; + while data.len() < ADDRESS_LEN { + data.push(0); + } + bytes.copy_from_slice(&data[..]); + Ok(Address(bytes)) + } +} + impl TryFrom<&Web3Address> for Address { type Error = Error; diff --git a/packages/kos-sdk/src/chains/ethereum/mod.rs b/packages/kos-sdk/src/chains/ethereum/mod.rs index b8e5717..bbcac47 100644 --- a/packages/kos-sdk/src/chains/ethereum/mod.rs +++ b/packages/kos-sdk/src/chains/ethereum/mod.rs @@ -13,6 +13,7 @@ use kos_types::error::Error; use kos_types::hash::Hash; use kos_types::number::BigNumber; +use rlp::Rlp; use secp256k1::ecdsa::{RecoverableSignature, RecoveryId}; use std::{ops::Div, str::FromStr}; use wasm_bindgen::prelude::*; @@ -377,6 +378,7 @@ impl ETH { }), chain_id: Some(chain_id), nonce, + from: Some(sender), to: Some(receiver), value, data: options.contract_data.unwrap_or_default(), @@ -427,6 +429,50 @@ impl ETH { Ok(true) } + + #[wasm_bindgen(js_name = "txFromRaw")] + pub fn tx_from_raw(raw: &str) -> Result { + let hex_tx = hex::decode(raw)?; + let rlp = Rlp::new(&hex_tx); + + let tx = match transaction::Transaction::decode_legacy(&rlp) { + Ok(tx) => tx, + Err(_) => { + let rlp = Rlp::new(&hex_tx[2..]); + self::transaction::Transaction::decode_eip155(rlp).map_err(|e| { + Error::InvalidTransaction(format!("failed to decode transaction: {}", e)) + })? + } + }; + + let digest = hash_transaction(&tx)?; + Ok(crate::models::Transaction { + chain: chain::Chain::ETH, + sender: "".to_string(), //TODO: implement sender on eth decode + hash: Hash::from_vec(digest)?, + data: Some(TransactionRaw::Ethereum(tx)), + }) + } + + #[wasm_bindgen(js_name = "txFromJson")] + pub fn tx_from_json(raw: &str) -> Result { + // build expected send result + let tx: transaction::Transaction = serde_json::from_str(raw)?; + + let digest = hash_transaction(&tx)?; + + let sender = match tx.from { + Some(addr) => addr.to_string(), + None => "".to_string(), + }; + + Ok(crate::models::Transaction { + chain: chain::Chain::ETH, + sender, + hash: Hash::from_vec(digest)?, + data: Some(TransactionRaw::Ethereum(tx)), + }) + } } #[cfg(test)] @@ -625,4 +671,61 @@ mod tests { assert_eq!(valid, false, "address: {}", addr); } } + + #[test] + fn test_decode_rlp_tx() { + let raw_tx = "af02ed0182012884019716f7850e60f86055827530944cbeee256240c92a9ad920ea6f4d7df6466d2cdc0180c0808080"; + let tx = ETH::tx_from_raw(raw_tx).unwrap(); + + assert_eq!(tx.chain, chain::Chain::ETH); + + let eth_tx = match tx.data { + Some(TransactionRaw::Ethereum(tx)) => tx, + _ => panic!("invalid tx"), + }; + + assert_eq!(eth_tx.chain_id, Some(1)); + assert_eq!(eth_tx.nonce, U256::from_dec_str("296").unwrap()); + assert_eq!( + eth_tx.to.unwrap().to_string(), + "0x4cBeee256240c92A9ad920ea6f4d7Df6466D2Cdc" + ); + assert_eq!(eth_tx.gas, U256::from(30000)); + assert_eq!(eth_tx.value, U256::from_dec_str("1").unwrap()); + assert_eq!(eth_tx.signature, None); + } + + #[test] + fn test_decode_json() { + let json = r#"{ + "from":"0x4cbeee256240c92a9ad920ea6f4d7df6466d2cdc", + "maxPriorityFeePerGas":null,"maxFeePerGas":null, + "gas": "0x00", + "value": "0x00", + "data":"0xa9059cbb000000000000000000000000ac4145fef6c828e8ae017207ad944c988ccb2cf700000000000000000000000000000000000000000000000000000000000f4240", + "to":"0xdac17f958d2ee523a2206206994597c13d831ec7", + "nonce":"0x00"}"#; + let tx = ETH::tx_from_json(json).unwrap(); + + assert_eq!(tx.chain, chain::Chain::ETH); + + let eth_tx = match tx.data { + Some(TransactionRaw::Ethereum(tx)) => tx, + _ => panic!("invalid tx"), + }; + + assert_eq!(eth_tx.chain_id, None); + assert_eq!(eth_tx.nonce, U256::from_dec_str("0").unwrap()); + assert_eq!( + eth_tx.from.unwrap().to_string(), + "0x4cBeee256240c92A9ad920ea6f4d7Df6466D2Cdc" + ); + assert_eq!( + eth_tx.to.unwrap().to_string(), + "0xdAC17F958D2ee523a2206206994597C13D831ec7" + ); + assert_eq!(eth_tx.gas, U256::from(0)); + assert_eq!(eth_tx.value, U256::from(0)); + assert_eq!(eth_tx.signature, None); + } } diff --git a/packages/kos-sdk/src/chains/ethereum/transaction.rs b/packages/kos-sdk/src/chains/ethereum/transaction.rs index f64d906..20ac348 100644 --- a/packages/kos-sdk/src/chains/ethereum/transaction.rs +++ b/packages/kos-sdk/src/chains/ethereum/transaction.rs @@ -1,13 +1,14 @@ use super::address::Address; +use std::str::FromStr; use kos_types::error::Error; -use rlp::RlpStream; +use rlp::{DecoderError, Rlp, RlpStream}; use secp256k1::ecdsa::RecoverableSignature; -use serde::Serialize; +use serde::{Deserialize, Deserializer, Serialize}; use web3::types::U256; -#[derive(Serialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub enum TransactionType { Legacy, EIP1559, @@ -25,19 +26,55 @@ where } } -#[derive(Serialize, Clone, Debug, PartialEq)] +pub fn deserialize_addr<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + if s.is_empty() { + Ok(None) + } else { + Ok(Some( + Address::from_str(&s).map_err(serde::de::Error::custom)?, + )) + } +} + +pub fn deserialize_data<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + if s.is_empty() { + Ok(Vec::new()) + } else { + let s = if s.len() > 2 && (s.starts_with("0x") || s.starts_with("0X")) { + &s[2..] + } else { + &s + }; + + Ok(hex::decode(s).map_err(serde::de::Error::custom)?) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct Transaction { pub transaction_type: Option, pub nonce: U256, + #[serde(deserialize_with = "deserialize_addr")] + pub from: Option
, + #[serde(deserialize_with = "deserialize_addr")] pub to: Option
, pub gas: U256, pub gas_price: Option, pub value: U256, + #[serde(deserialize_with = "deserialize_data")] pub data: Vec, pub chain_id: Option, pub max_fee_per_gas: Option, pub max_priority_fee_per_gas: Option, - #[serde(serialize_with = "signature_serialize")] + #[serde(serialize_with = "signature_serialize", skip_deserializing)] pub signature: Option, } @@ -144,6 +181,40 @@ impl Transaction { } } } + + pub fn decode_legacy(rlp: &Rlp) -> Result { + Ok(Transaction { + transaction_type: Some(TransactionType::Legacy), + nonce: rlp.val_at(0)?, + gas_price: Some(rlp.val_at(1)?), + gas: rlp.val_at(2)?, + from: None, + to: Some(rlp.val_at(3)?), + value: rlp.val_at(4)?, + data: rlp.val_at(5)?, + chain_id: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + signature: None, + }) + } + + pub fn decode_eip155(rlp: Rlp) -> Result { + Ok(Transaction { + transaction_type: Some(TransactionType::EIP1559), + chain_id: Some(rlp.val_at(0)?), + nonce: rlp.val_at(1)?, + max_priority_fee_per_gas: Some(rlp.val_at(2)?), + max_fee_per_gas: Some(rlp.val_at(3)?), + gas: rlp.val_at(4)?, + from: None, + to: Some(rlp.val_at(5)?), // Convert to Option + value: rlp.val_at(6)?, + data: rlp.val_at(7)?, + signature: None, + gas_price: None, + }) + } } #[cfg(test)] @@ -157,6 +228,7 @@ mod tests { let tx = Transaction { transaction_type: Some(TransactionType::Legacy), nonce: U256::from_dec_str("691").unwrap(), + from: None, to: Some(Address::try_from("0x4592D8f8D7B001e72Cb26A73e4Fa1806a51aC79d").unwrap()), gas: U256::from(21000), gas_price: Some(U256::from_dec_str("2000000000").unwrap()), @@ -184,6 +256,7 @@ mod tests { let tx = Transaction { transaction_type: Some(TransactionType::EIP1559), nonce: U256::from_dec_str("241").unwrap(), + from: None, to: Some(Address::try_from("0xe0e5d2B4EDcC473b988b44b4d13c3972cb6694cb").unwrap()), gas: U256::from(21000), gas_price: None,