diff --git a/chains/ethereum/server/src/lib.rs b/chains/ethereum/server/src/lib.rs index a5970a74..8d2b546c 100644 --- a/chains/ethereum/server/src/lib.rs +++ b/chains/ethereum/server/src/lib.rs @@ -1,9 +1,12 @@ use crate::eth_types::GENESIS_BLOCK_INDEX; -use crate::utils::{get_block, get_transaction, populate_transactions, EthDetokenizer}; -use anyhow::{anyhow, bail, Context, Result}; -use ethers::abi::Abi; -use ethers::contract as ethers_contract; +use crate::utils::{ + get_block, get_transaction, parse_method, populate_transactions, EthDetokenizer, +}; +use anyhow::{bail, Context, Result}; use ethers::prelude::*; +use ethers::utils::keccak256; +use ethers::utils::rlp::Encodable; +use proof::verify_proof; use rosetta_config_ethereum::{EthereumMetadata, EthereumMetadataParams}; use rosetta_server::crypto::address::Address; use rosetta_server::crypto::PublicKey; @@ -12,11 +15,12 @@ use rosetta_server::types::{ TransactionIdentifier, }; use rosetta_server::{BlockchainClient, BlockchainConfig}; -use serde_json::Value; +use serde_json::{json, Value}; use std::str::FromStr; use std::sync::Arc; mod eth_types; +mod proof; mod utils; pub struct EthereumClient { @@ -33,7 +37,10 @@ impl BlockchainClient for EthereumClient { async fn new(network: &str, addr: &str) -> Result { let config = rosetta_config_ethereum::config(network)?; let client = Arc::new(Provider::::try_from(format!("http://{addr}"))?); - let genesis = client.get_block(0).await?.unwrap(); + let genesis = client + .get_block(0) + .await? + .context("Failed to get genesis block")?; let genesis_block = BlockIdentifier { index: 0, hash: hex::encode(genesis.hash.as_ref().unwrap()), @@ -113,8 +120,7 @@ impl BlockchainClient for EthereumClient { let from: H160 = public_key .to_address(self.config().address_format) .address() - .parse() - .unwrap(); + .parse()?; let to = H160::from_slice(&options.destination); let chain_id = self.client.get_chainid().await?; let nonce = self.client.get_transaction_count(from, None).await?; @@ -137,12 +143,13 @@ impl BlockchainClient for EthereumClient { async fn submit(&self, transaction: &[u8]) -> Result> { let tx = transaction.to_vec().into(); + Ok(self .client .send_raw_transaction(Bytes(tx)) .await? .await? - .unwrap() + .context("Failed to get transaction receipt")? .transaction_hash .0 .to_vec()) @@ -202,42 +209,41 @@ impl BlockchainClient for EthereumClient { match call_type.to_lowercase().as_str() { "call" => { //process constant call - let abi_str = params["abi"].as_str().context("ABI not found")?; - - let abi: Abi = serde_json::from_str(abi_str).map_err(|err| anyhow!(err))?; - let contract_address = H160::from_str( params["contract_address"] .as_str() .context("contact address not found")?, - ) - .map_err(|err| anyhow!(err))?; + )?; + + let function = parse_method(&method)?; + + let bytes: Vec = function.encode_input(&[])?; + + let tx = Eip1559TransactionRequest { + to: Some(contract_address.into()), + data: Some(bytes.into()), + ..Default::default() + }; + + let tx = &tx.into(); + let received_data = self.client.call(tx, None).await?; - let contract = - ethers_contract::Contract::new(contract_address, abi, self.client.clone()); + let data: EthDetokenizer = decode_function_data(&function, received_data, false)?; - let value: EthDetokenizer = contract - .method(&method, ()) - .map_err(|err| anyhow!(err))? - .call() - .await - .map_err(|err| anyhow!(err))?; + let result: Value = serde_json::from_str(&data.json)?; - let result: Value = serde_json::from_str(&value.json)?; return Ok(result); } "storage" => { //process storage call let from = H160::from_str( - params["address"] + params["contract_address"] .as_str() .context("address field not found")?, - ) - .map_err(|err| anyhow!(err))?; + )?; let location = - H256::from_str(params["position"].as_str().context("position not found")?) - .map_err(|err| anyhow!(err))?; + H256::from_str(params["position"].as_str().context("position not found")?)?; let block_num = params["block_number"] .as_u64() @@ -249,6 +255,47 @@ impl BlockchainClient for EthereumClient { .await?; return Ok(Value::String(format!("{storage_check:#?}",))); } + "storage_proof" => { + let from = H160::from_str( + params["contract_address"] + .as_str() + .context("address field not found")?, + )?; + + let location = + H256::from_str(params["position"].as_str().context("position not found")?)?; + + let block_num = params["block_number"] + .as_u64() + .map(|block_num| BlockId::Number(block_num.into())); + + let proof_data = self + .client + .get_proof(from, vec![location], block_num) + .await?; + + //process verfiicatin of proof + let storage_hash = proof_data.storage_hash; + let storage_proof = proof_data.storage_proof.first().context("No proof found")?; + + let key = &storage_proof.key; + let key_hash = keccak256(key); + let encoded_val = storage_proof.value.rlp_bytes().to_vec(); + + let is_valid = verify_proof( + &storage_proof.proof, + storage_hash.as_bytes(), + &key_hash.to_vec(), + &encoded_val, + ); + + let result = serde_json::to_value(&proof_data)?; + + return Ok(json!({ + "proof": result, + "isValid": is_valid + })); + } _ => { bail!("request type not supported") } @@ -289,4 +336,16 @@ mod tests { let config = rosetta_config_ethereum::config("dev")?; rosetta_server::tests::construction(config).await } + + #[tokio::test] + async fn test_find_transaction() -> Result<()> { + let config = rosetta_config_ethereum::config("dev")?; + rosetta_server::tests::find_transaction(config).await + } + + #[tokio::test] + async fn test_list_transactions() -> Result<()> { + let config = rosetta_config_ethereum::config("dev")?; + rosetta_server::tests::list_transactions(config).await + } } diff --git a/chains/ethereum/server/src/proof.rs b/chains/ethereum/server/src/proof.rs new file mode 100644 index 00000000..d111a540 --- /dev/null +++ b/chains/ethereum/server/src/proof.rs @@ -0,0 +1,194 @@ +use ethers::types::{Bytes, EIP1186ProofResponse}; +use ethers::utils::keccak256; +use ethers::utils::rlp::{decode_list, RlpStream}; + +pub fn verify_proof(proof: &Vec, root: &[u8], path: &Vec, value: &Vec) -> bool { + let mut expected_hash = root.to_vec(); + let mut path_offset = 0; + + for (i, node) in proof.iter().enumerate() { + if expected_hash != keccak256(node).to_vec() { + return false; + } + + let node_list: Vec> = decode_list(node); + + if node_list.len() == 17 { + if i == proof.len() - 1 { + // exclusion proof + let nibble = get_nibble(path, path_offset); + let node = &node_list[nibble as usize]; + + if node.is_empty() && is_empty_value(value) { + return true; + } + } else { + let nibble = get_nibble(path, path_offset); + expected_hash = node_list[nibble as usize].clone(); + + path_offset += 1; + } + } else if node_list.len() == 2 { + if i == proof.len() - 1 { + // exclusion proof + if !paths_match(&node_list[0], skip_length(&node_list[0]), path, path_offset) + && is_empty_value(value) + { + return true; + } + + // inclusion proof + if &node_list[1] == value { + return paths_match( + &node_list[0], + skip_length(&node_list[0]), + path, + path_offset, + ); + } + } else { + let node_path = &node_list[0]; + let prefix_length = shared_prefix_length(path, path_offset, node_path); + if prefix_length < node_path.len() * 2 - skip_length(node_path) { + // The proof shows a divergent path, but we're not + // at the end of the proof, so something's wrong. + return false; + } + path_offset += prefix_length; + expected_hash = node_list[1].clone(); + } + } else { + return false; + } + } + + false +} + +fn paths_match(p1: &Vec, s1: usize, p2: &Vec, s2: usize) -> bool { + let len1 = p1.len() * 2 - s1; + let len2 = p2.len() * 2 - s2; + + if len1 != len2 { + return false; + } + + for offset in 0..len1 { + let n1 = get_nibble(p1, s1 + offset); + let n2 = get_nibble(p2, s2 + offset); + + if n1 != n2 { + return false; + } + } + + true +} + +#[allow(dead_code)] +fn get_rest_path(p: &Vec, s: usize) -> String { + let mut ret = String::new(); + for i in s..p.len() * 2 { + let n = get_nibble(p, i); + ret += &format!("{n:01x}"); + } + ret +} + +fn is_empty_value(value: &Vec) -> bool { + let mut stream = RlpStream::new(); + stream.begin_list(4); + stream.append_empty_data(); + stream.append_empty_data(); + let empty_storage_hash = "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"; + stream.append(&hex::decode(empty_storage_hash).unwrap()); + let empty_code_hash = "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; + stream.append(&hex::decode(empty_code_hash).unwrap()); + let empty_account = stream.out(); + + let is_empty_slot = value.len() == 1 && value[0] == 0x80; + let is_empty_account = value == &empty_account; + is_empty_slot || is_empty_account +} + +fn shared_prefix_length(path: &Vec, path_offset: usize, node_path: &Vec) -> usize { + let skip_length = skip_length(node_path); + + let len = std::cmp::min( + node_path.len() * 2 - skip_length, + path.len() * 2 - path_offset, + ); + let mut prefix_len = 0; + + for i in 0..len { + let path_nibble = get_nibble(path, i + path_offset); + let node_path_nibble = get_nibble(node_path, i + skip_length); + + if path_nibble == node_path_nibble { + prefix_len += 1; + } else { + break; + } + } + + prefix_len +} + +fn skip_length(node: &Vec) -> usize { + if node.is_empty() { + return 0; + } + + let nibble = get_nibble(node, 0); + match nibble { + 0 => 2, + 1 => 1, + 2 => 2, + 3 => 1, + _ => 0, + } +} + +fn get_nibble(path: &[u8], offset: usize) -> u8 { + let byte = path[offset / 2]; + if offset % 2 == 0 { + byte >> 4 + } else { + byte & 0xF + } +} + +pub fn _encode_account(proof: &EIP1186ProofResponse) -> Vec { + let mut stream = RlpStream::new_list(4); + stream.append(&proof.nonce); + stream.append(&proof.balance); + stream.append(&proof.storage_hash); + stream.append(&proof.code_hash); + let encoded = stream.out(); + encoded.to_vec() +} + +#[cfg(test)] +mod tests { + use crate::proof::shared_prefix_length; + + #[tokio::test] + async fn test_shared_prefix_length() { + // We compare the path starting from the 6th nibble i.e. the 6 in 0x6f + let path: Vec = vec![0x12, 0x13, 0x14, 0x6f, 0x6c, 0x64, 0x21]; + let path_offset = 6; + // Our node path matches only the first 5 nibbles of the path + let node_path: Vec = vec![0x6f, 0x6c, 0x63, 0x21]; + let shared_len = shared_prefix_length(&path, path_offset, &node_path); + assert_eq!(shared_len, 5); + + // Now we compare the path starting from the 5th nibble i.e. the 4 in 0x14 + let path: Vec = vec![0x12, 0x13, 0x14, 0x6f, 0x6c, 0x64, 0x21]; + let path_offset = 5; + // Our node path matches only the first 7 nibbles of the path + // Note the first nibble is 1, so we skip 1 nibble + let node_path: Vec = vec![0x14, 0x6f, 0x6c, 0x64, 0x11]; + let shared_len = shared_prefix_length(&path, path_offset, &node_path); + assert_eq!(shared_len, 7); + } +} diff --git a/chains/ethereum/server/src/utils.rs b/chains/ethereum/server/src/utils.rs index 834f32b0..8f3555e1 100644 --- a/chains/ethereum/server/src/utils.rs +++ b/chains/ethereum/server/src/utils.rs @@ -7,8 +7,9 @@ use crate::eth_types::{ }; use anyhow::{anyhow, bail, Context, Result}; use ethers::{ - abi::{Detokenize, InvalidOutputType, Token}, + abi::{Abi, Detokenize, Function, HumanReadableParser, InvalidOutputType, Token}, prelude::*, + utils::to_checksum, }; use ethers::{ providers::{Http, Middleware, Provider}, @@ -19,8 +20,7 @@ use rosetta_server::types::{ AccountIdentifier, Amount, BlockIdentifier, Currency, Operation, OperationIdentifier, PartialBlockIdentifier, TransactionIdentifier, }; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; +use serde_json::json; use std::collections::HashMap; use std::str::FromStr; @@ -33,7 +33,7 @@ pub async fn get_block( client: &Provider, ) -> Result<(Block, Vec, Vec>)> { let bl_id = if let Some(hash) = block.hash.as_ref() { - let h256 = H256::from_str(hash).map_err(|err| anyhow!(err))?; + let h256 = H256::from_str(hash)?; BlockId::Hash(h256) } else if let Some(index) = block.index { let ehters_u64 = U64::from(index); @@ -45,8 +45,7 @@ pub async fn get_block( let block_eth = client .get_block_with_txs(bl_id) - .await - .map_err(|err| anyhow!(err))? + .await? .context("Block not found")?; let block_number = block_eth @@ -76,7 +75,7 @@ pub async fn get_block( traces.extend(block_traces.0); } - let mut loaded_transaction_vec = vec![]; + let mut loaded_transactions = vec![]; for (idx, transaction) in block_transactions.iter().enumerate() { let tx_receipt = &receipts[idx]; @@ -96,15 +95,15 @@ pub async fn get_block( }; if !add_traces { - loaded_transaction_vec.push(loaded_tx); + loaded_transactions.push(loaded_tx); continue; } loaded_tx.trace = Some(traces[idx].result.clone()); - loaded_transaction_vec.push(loaded_tx); + loaded_transactions.push(loaded_tx); } - Ok((block_eth, loaded_transaction_vec, uncles)) + Ok((block_eth, loaded_transactions, uncles)) } pub async fn get_transaction( @@ -113,11 +112,10 @@ pub async fn get_transaction( client: &Provider, currency: &Currency, ) -> Result { - let tx_hash = H256::from_str(hash).map_err(|err| anyhow!(err))?; + let tx_hash = H256::from_str(hash)?; let transaction = client .get_transaction(tx_hash) - .await - .map_err(|err| anyhow!(err))? + .await? .context("Unable to get transaction")?; let ehters_u64 = U64::from(block_identifier.index); @@ -126,8 +124,7 @@ pub async fn get_transaction( let block = client .get_block(block_num) - .await - .map_err(|err| anyhow!(err))? + .await? .context("Block not found")?; let block_hash = block.hash.context("Block hash not found")?; @@ -135,8 +132,7 @@ pub async fn get_transaction( let tx_receipt = client .get_transaction_receipt(tx_hash) - .await - .map_err(|err| anyhow!(err))? + .await? .context("Transaction receipt not found")?; if tx_receipt @@ -216,7 +212,7 @@ pub async fn populate_transaction( let transaction = rosetta_types::Transaction { transaction_identifier: TransactionIdentifier { - hash: format!("{:?}", tx.transaction.hash), + hash: hex::encode(tx.transaction.hash), }, operations, related_transactions: None, @@ -235,17 +231,16 @@ pub async fn get_uncles( uncles: &[H256], client: &Provider, ) -> Result>> { - let mut uncles_vec = vec![]; + let mut uncles_data = vec![]; for (idx, _) in uncles.iter().enumerate() { let index = U64::from(idx); let uncle_response = client .get_uncle(block_index, index) - .await - .map_err(|err| anyhow!("{err}"))? + .await? .context("Uncle block now found")?; - uncles_vec.push(uncle_response); + uncles_data.push(uncle_response); } - Ok(uncles_vec) + Ok(uncles_data) } pub async fn get_block_receipts( @@ -258,8 +253,7 @@ pub async fn get_block_receipts( let tx_hash = tx.hash; let receipt = client .get_transaction_receipt(tx_hash) - .await - .map_err(|err| anyhow!("{err}"))? + .await? .context("Transaction receipt not found")?; if receipt @@ -288,8 +282,7 @@ pub async fn get_block_traces( let traces: ResultGethExecTraces = client .request("debug_traceBlockByHash", [hash_serialize, cfg]) - .await - .map_err(|err| anyhow!(err))?; + .await?; Ok(traces) } @@ -304,8 +297,7 @@ async fn get_transaction_trace(hash: &H256, client: &Provider) -> Result Result tx.fee_amount }; - let mut operations_vec = vec![]; + let mut operations = vec![]; let first_op = Operation { operation_identifier: OperationIdentifier { @@ -328,7 +320,7 @@ pub fn get_fee_operations(tx: &LoadedTransaction, currency: &Currency) -> Result r#type: FEE_OP_TYPE.into(), status: Some(SUCCESS_STATUS.into()), account: Some(AccountIdentifier { - address: format!("{:?}", tx.from), + address: to_checksum(&tx.from, None), sub_account: None, metadata: None, }), @@ -353,7 +345,7 @@ pub fn get_fee_operations(tx: &LoadedTransaction, currency: &Currency) -> Result r#type: FEE_OP_TYPE.into(), status: Some(SUCCESS_STATUS.into()), account: Some(AccountIdentifier { - address: format!("{:?}", tx.miner), + address: to_checksum(&tx.miner, None), sub_account: None, metadata: None, }), @@ -366,8 +358,8 @@ pub fn get_fee_operations(tx: &LoadedTransaction, currency: &Currency) -> Result metadata: None, }; - operations_vec.push(first_op); - operations_vec.push(second_op); + operations.push(first_op); + operations.push(second_op); if let Some(fee_burned) = tx.fee_burned { let burned_operation = Operation { @@ -379,7 +371,7 @@ pub fn get_fee_operations(tx: &LoadedTransaction, currency: &Currency) -> Result r#type: FEE_OP_TYPE.into(), status: Some(SUCCESS_STATUS.into()), account: Some(AccountIdentifier { - address: format!("{:?}", tx.from), + address: to_checksum(&tx.from, None), sub_account: None, metadata: None, }), @@ -392,11 +384,9 @@ pub fn get_fee_operations(tx: &LoadedTransaction, currency: &Currency) -> Result metadata: None, }; - operations_vec.push(burned_operation); - Ok(operations_vec) - } else { - Ok(operations_vec) + operations.push(burned_operation); } + Ok(operations) } pub fn get_traces_operations( @@ -429,8 +419,8 @@ pub fn get_traces_operations( should_add = false; } - let from = format!("{:?}", trace.from); - let to = format!("{:?}", trace.to); + let from = to_checksum(&trace.from, None); + let to = to_checksum(&trace.to, None); if should_add { let mut from_operation = Operation { @@ -541,7 +531,7 @@ pub fn get_traces_operations( r#type: DESTRUCT_OP_TYPE.into(), status: Some(SUCCESS_STATUS.into()), account: Some(AccountIdentifier { - address: k.clone(), + address: to_checksum(&H160::from_str(k)?, None), sub_account: None, metadata: None, }), @@ -608,7 +598,7 @@ pub fn get_mining_rewards( r#type: MINING_REWARD_OP_TYPE.into(), status: Some(SUCCESS_STATUS.into()), account: Some(AccountIdentifier { - address: format!("{miner:?}"), + address: to_checksum(miner, None), sub_account: None, metadata: None, }), @@ -638,7 +628,7 @@ pub fn get_mining_rewards( r#type: UNCLE_REWARD_OP_TYPE.into(), status: Some(SUCCESS_STATUS.into()), account: Some(AccountIdentifier { - address: format!("{uncle_miner:?}"), + address: to_checksum(&uncle_miner, None), sub_account: None, metadata: None, }), @@ -718,9 +708,7 @@ fn effective_gas_price(tx: &Transaction, base_fee: Option) -> Result .transaction_type .context("transaction type is not available")?; let tx_gas_price = tx.gas_price.context("gas price is not available")?; - let tx_max_priority_fee_per_gas = tx - .max_priority_fee_per_gas - .context("max priority fee per gas is not available")?; + let tx_max_priority_fee_per_gas = tx.max_priority_fee_per_gas.unwrap_or_default(); if tx_transaction_type.as_u64() != 2 { return Ok(tx_gas_price); @@ -729,51 +717,32 @@ fn effective_gas_price(tx: &Transaction, base_fee: Option) -> Result Ok(base_fee + tx_max_priority_fee_per_gas) } -#[derive(Serialize)] -#[doc(hidden)] -pub(crate) struct GethLoggerConfig { - /// enable memory capture - #[serde(rename = "EnableMemory")] - enable_memory: bool, - /// disable stack capture - #[serde(rename = "DisableStack")] - disable_stack: bool, - /// disable storage capture - #[serde(rename = "DisableStorage")] - disable_storage: bool, - /// enable return data capture - #[serde(rename = "EnableReturnData")] - enable_return_data: bool, -} - -impl Default for GethLoggerConfig { - fn default() -> Self { - Self { - enable_memory: false, - disable_stack: false, - disable_storage: false, - enable_return_data: true, - } +pub fn parse_method(method: &str) -> Result { + let parse_result = HumanReadableParser::parse_function(method); + if parse_result.is_ok() { + parse_result.map_err(|e| anyhow!(e)) + } else { + let json_parse: Result = + if !(method.starts_with('[') && method.ends_with(']')) { + let abi_str = format!("[{method}]"); + serde_json::from_str(&abi_str) + } else { + serde_json::from_str(method) + }; + let abi: Abi = json_parse?; + let (_, functions): (&String, &Vec) = abi + .functions + .iter() + .next() + .context("No functions found in abi")?; + let function: Function = functions + .get(0) + .context("Abi function list is empty")? + .clone(); + Ok(function) } } -#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] -pub struct GethExecTrace { - /// Used gas - pub gas: Gas, - /// True when the transaction has failed. - pub failed: bool, - /// Return value of execution which is a hex encoded byte array - #[serde(rename = "returnValue")] - pub return_value: String, - /// Vector of geth execution steps of the trace. - #[serde(rename = "structLogs")] - pub struct_logs: Vec, -} - -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] -pub struct Gas(pub u64); - pub struct LoadedTransaction { pub transaction: Transaction, pub from: H160, diff --git a/chains/ethereum/tx/Cargo.toml b/chains/ethereum/tx/Cargo.toml index a3e2b1f7..463211c9 100644 --- a/chains/ethereum/tx/Cargo.toml +++ b/chains/ethereum/tx/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [dependencies] anyhow = "1.0.69" ethers-core = "1.0.2" +ethers = "1.0.2" rosetta-config-ethereum = { version = "0.1.0", path = "../config" } rosetta-core = { version = "0.1.0", path = "../../../rosetta-core" } +serde_json = "1.0.70" sha3 = "0.10.6" diff --git a/chains/ethereum/tx/src/lib.rs b/chains/ethereum/tx/src/lib.rs index d43842d9..3a86d466 100644 --- a/chains/ethereum/tx/src/lib.rs +++ b/chains/ethereum/tx/src/lib.rs @@ -1,9 +1,12 @@ -use anyhow::Result; +use anyhow::{anyhow, Context, Result}; +use ethers::abi::token::{LenientTokenizer, Tokenizer}; +use ethers::abi::{Abi, Function, HumanReadableParser, Param, Token}; use ethers_core::types::{Eip1559TransactionRequest, Signature, H160, U256}; use rosetta_config_ethereum::{EthereumMetadata, EthereumMetadataParams}; use rosetta_core::crypto::address::Address; use rosetta_core::crypto::SecretKey; use rosetta_core::{BlockchainConfig, TransactionBuilder}; +use serde_json::Value; use sha3::{Digest, Keccak256}; #[derive(Default)] @@ -23,6 +26,27 @@ impl TransactionBuilder for EthereumTransactionBuilder { }) } + fn method_call(&self, address: &Address, params: &Value) -> Result { + let destination: H160 = address.address().parse()?; + + let method_str = params["method_signature"] + .as_str() + .context("Method signature not found")?; + let function_params = params["params"].as_array().context("Params not found")?; + + let function = parse_method(method_str)?; + + let tokens = tokenize_params(function_params, &function.inputs); + + let bytes: Vec = function.encode_input(&tokens).map(Into::into)?; + + Ok(EthereumMetadataParams { + destination: destination.0.to_vec(), + amount: [0, 0, 0, 0], + data: bytes, + }) + } + fn create_and_sign( &self, config: &BlockchainConfig, @@ -65,3 +89,32 @@ impl TransactionBuilder for EthereumTransactionBuilder { tx } } + +fn parse_method(method: &str) -> Result { + let parse_result = HumanReadableParser::parse_function(method); + if parse_result.is_ok() { + parse_result.map_err(|e| anyhow!(e)) + } else { + let json_parse: Result = + if !(method.starts_with('[') && method.ends_with(']')) { + let abi_str = format!("[{method}]"); + serde_json::from_str(&abi_str) + } else { + serde_json::from_str(method) + }; + let abi: Abi = json_parse.unwrap(); + let (_, functions): (&String, &Vec) = abi.functions.iter().next().unwrap(); + let function: Function = functions.get(0).unwrap().clone(); + Ok(function) + } +} + +fn tokenize_params(values: &[Value], inputs: &[Param]) -> Vec { + let value_strings: Vec = values.iter().map(|v| v.as_str().unwrap().into()).collect(); + + inputs + .iter() + .zip(value_strings.iter()) + .map(|(param, arg)| LenientTokenizer::tokenize(¶m.kind, arg).unwrap()) + .collect() +} diff --git a/chains/polkadot/server/src/block.rs b/chains/polkadot/server/src/block.rs index e2c69fca..a38299c9 100644 --- a/chains/polkadot/server/src/block.rs +++ b/chains/polkadot/server/src/block.rs @@ -38,7 +38,7 @@ pub async fn get_transaction>( let op_neg_amount: Option = event_parsed_data.amount.as_ref().map(|amount| Amount { - value: format!("-{}", amount), + value: format!("-{amount}"), currency: config.currency(), metadata: None, }); @@ -99,7 +99,7 @@ fn get_operation_data( let pallet_name = event.pallet_name(); let event_name = event.variant_name(); - let call_type = format!("{}.{}", pallet_name, event_name); + let call_type = format!("{pallet_name}.{event_name}"); let event_fields = event.field_values()?; let parsed_data = match event_fields { diff --git a/chains/polkadot/tx/src/lib.rs b/chains/polkadot/tx/src/lib.rs index cea1562b..67559ece 100644 --- a/chains/polkadot/tx/src/lib.rs +++ b/chains/polkadot/tx/src/lib.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use parity_scale_codec::{Compact, Decode, Encode}; use rosetta_config_polkadot::{PolkadotMetadata, PolkadotMetadataParams}; use rosetta_core::crypto::address::Address; @@ -98,6 +98,14 @@ impl TransactionBuilder for PolkadotTransactionBuilder { }) } + fn method_call( + &self, + _address: &Address, + _params: &serde_json::Value, + ) -> Result { + bail!("Not Implemented") + } + fn create_and_sign( &self, _config: &BlockchainConfig, diff --git a/rosetta-client/src/wallet.rs b/rosetta-client/src/wallet.rs index 0473d735..5f51d37e 100644 --- a/rosetta-client/src/wallet.rs +++ b/rosetta-client/src/wallet.rs @@ -12,6 +12,7 @@ use crate::types::{ use crate::{BlockchainConfig, Client, TransactionBuilder}; use anyhow::Result; use futures::{Future, Stream}; +use serde_json::Value; use std::pin::Pin; use std::task::{Context, Poll}; @@ -36,6 +37,17 @@ impl GenericTransactionBuilder { }) } + pub fn method_call( + &self, + method: &Address, + params: &serde_json::Value, + ) -> Result { + Ok(match self { + Self::Ethereum(tx) => serde_json::to_value(tx.method_call(method, params)?)?, + Self::Polkadot(tx) => serde_json::to_value(tx.method_call(method, params)?)?, + }) + } + pub fn create_and_sign( &self, config: &BlockchainConfig, @@ -173,6 +185,23 @@ impl Wallet { self.submit(&transaction).await } + pub async fn method_call( + &self, + account: &AccountIdentifier, + params: Value, + ) -> Result { + let address = Address::new(self.config.address_format, account.address.clone()); + let metadata_params = self.tx.method_call(&address, ¶ms)?; + let metadata = self.metadata(metadata_params.clone()).await?; + let transaction = self.tx.create_and_sign( + &self.config, + metadata_params, + metadata, + self.secret_key.secret_key(), + ); + self.submit(&transaction).await + } + pub async fn faucet(&self, faucet_parameter: u128) -> Result { let req = AccountFaucetRequest { network_identifier: self.config.network(), diff --git a/rosetta-core/src/lib.rs b/rosetta-core/src/lib.rs index 3cbeffd7..73eb99f1 100644 --- a/rosetta-core/src/lib.rs +++ b/rosetta-core/src/lib.rs @@ -122,6 +122,8 @@ pub trait TransactionBuilder: Default + Sized { fn transfer(&self, address: &Address, amount: u128) -> Result; + fn method_call(&self, address: &Address, values: &Value) -> Result; + fn create_and_sign( &self, config: &BlockchainConfig, diff --git a/rosetta-server/src/indexer.rs b/rosetta-server/src/indexer.rs index 21c02b4a..c9ee783a 100644 --- a/rosetta-server/src/indexer.rs +++ b/rosetta-server/src/indexer.rs @@ -204,7 +204,7 @@ impl Indexer { pub async fn sync(&self) -> Result<()> { let synced_height = self.transaction_table.height()?; let current_height = self.client.current_block().await?.index; - for block_index in (synced_height + 1)..current_height { + for block_index in (synced_height + 1)..current_height + 1 { let block = self.block_by_index(block_index).await?; for (transaction_index, transaction) in block.transactions.iter().enumerate() { let tx = TransactionRef::new(block_index, transaction_index as _); diff --git a/rosetta-server/src/lib.rs b/rosetta-server/src/lib.rs index 49900efb..6662efc1 100644 --- a/rosetta-server/src/lib.rs +++ b/rosetta-server/src/lib.rs @@ -113,7 +113,7 @@ fn ok(t: &T) -> tide::Result { fn err(err: &anyhow::Error) -> tide::Result { let error = crate::types::Error { code: 500, - message: format!("{}", err), + message: format!("{err}"), description: None, retriable: false, details: None, @@ -537,6 +537,7 @@ pub mod tests { alice.transfer(bob.account(), value).await?; alice.transfer(bob.account(), value).await?; + tokio::time::sleep(Duration::from_secs(11)).await; let mut stream = bob.transactions(1); let mut count = 0; while let Some(res) = stream.next().await { diff --git a/rosetta-wallet/src/main.rs b/rosetta-wallet/src/main.rs index 7bea0f25..0a34e586 100644 --- a/rosetta-wallet/src/main.rs +++ b/rosetta-wallet/src/main.rs @@ -3,6 +3,7 @@ use clap::Parser; use futures::stream::StreamExt; use rosetta_client::types::{AccountIdentifier, BlockTransaction, TransactionIdentifier}; use std::path::PathBuf; +use surf::http::convert::json; #[derive(Parser)] pub struct Opts { @@ -27,6 +28,7 @@ pub enum Command { Transfer(TransferOpts), Transaction(TransactionOpts), Transactions, + MethodCall(MethodCallOpts), } #[derive(Parser)] @@ -45,6 +47,14 @@ pub struct TransactionOpts { pub transaction: String, } +#[derive(Parser)] +pub struct MethodCallOpts { + pub address: String, + pub method_signature: String, + #[clap(value_delimiter = ' ')] + pub params: Vec, +} + #[async_std::main] async fn main() -> Result<()> { tracing_subscriber::fmt::init(); @@ -138,6 +148,24 @@ async fn main() -> Result<()> { println!("No transactions found"); } } + Command::MethodCall(MethodCallOpts { + address, + method_signature, + params, + }) => { + let acc_identifier = AccountIdentifier { + address, + sub_account: None, + metadata: None, + }; + let params = json!({ + "method_signature": method_signature, + "params": params + }); + + let tx = wallet.method_call(&acc_identifier, params).await?; + println!("Transaction hash: {:?}", tx.hash); + } } Ok(()) }