From 6ac227a257948d5ade56f9ce31218106cf479c63 Mon Sep 17 00:00:00 2001 From: 10gic Date: Fri, 3 Jan 2025 23:44:41 +0800 Subject: [PATCH] [Ethereum]: Support obtaining the function signature from an Ethereum function ABI (#4182) * Support obtaining the function signature from an Ethereum function ABI * Refactor code for improved cohesion and clarity --------- Co-authored-by: Sergei Boiko <127754187+satoshiotomakan@users.noreply.github.com> --- .../blockchains/ethereum/TestEthereumAbi.kt | 27 +++++ codegen-v2/manifest/TWEthereumAbi.yaml | 15 +++ include/TrustWalletCore/TWEthereumAbi.h | 7 ++ rust/tw_evm/src/evm_entry.rs | 24 +++- rust/tw_evm/src/modules/abi_encoder.rs | 21 +++- .../tests/chains/ethereum/ethereum_abi.rs | 107 +++++++++++++++++- rust/wallet_core_rs/src/ffi/ethereum/abi.rs | 25 +++- src/Ethereum/ABI/Function.cpp | 2 +- src/interface/TWEthereumAbi.cpp | 16 +++ .../Tests/Blockchains/EthereumAbiTests.swift | 26 +++++ tests/chains/Ethereum/TWEthereumAbiTests.cpp | 9 ++ 11 files changed, 264 insertions(+), 15 deletions(-) diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestEthereumAbi.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestEthereumAbi.kt index b6ad3654115..bd192a97070 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestEthereumAbi.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestEthereumAbi.kt @@ -209,4 +209,31 @@ class TestEthereumAbiDecoder { assertEquals(decodingOutput.getTokens(0).name, "name") assertEquals(decodingOutput.getTokens(0).stringValue, "deadbeef") } + + @Test + fun testEthereumAbiGetFunctionSignature() { + val abiJson = """ + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } + """.trimIndent() + + val functionSignature = wallet.core.jni.EthereumAbi.getFunctionSignature(abiJson) + assertEquals(functionSignature, "transfer(address,uint256)") + } } diff --git a/codegen-v2/manifest/TWEthereumAbi.yaml b/codegen-v2/manifest/TWEthereumAbi.yaml index fda88c4c4c1..8db92277e5b 100644 --- a/codegen-v2/manifest/TWEthereumAbi.yaml +++ b/codegen-v2/manifest/TWEthereumAbi.yaml @@ -81,3 +81,18 @@ functions: is_constant: true is_nullable: false is_pointer: true +- name: TWEthereumAbiGetFunctionSignature + is_public: true + is_static: true + params: + - name: abi + type: + variant: string + is_constant: true + is_nullable: false + is_pointer: true + return_type: + variant: string + is_constant: true + is_nullable: true + is_pointer: true diff --git a/include/TrustWalletCore/TWEthereumAbi.h b/include/TrustWalletCore/TWEthereumAbi.h index f2d23a4dec5..638acde0367 100644 --- a/include/TrustWalletCore/TWEthereumAbi.h +++ b/include/TrustWalletCore/TWEthereumAbi.h @@ -113,4 +113,11 @@ TWString* _Nullable TWEthereumAbiDecodeCall(TWData* _Nonnull data, TWString* _No TW_EXPORT_STATIC_METHOD TWData* _Nonnull TWEthereumAbiEncodeTyped(TWString* _Nonnull messageJson); +/// Get function signature from Ethereum ABI json +/// +/// \param abi The function ABI json string, for example: {"inputs":[{"internalType":"bool","name":"arg1","type":"bool"}],"name":"fun1","outputs":[],"stateMutability":"nonpayable","type":"function"} +/// \return the function type signature, of the form "baz(int32,uint256)", null if the abi is invalid. +TW_EXPORT_STATIC_METHOD +TWString* _Nullable TWEthereumAbiGetFunctionSignature(TWString* _Nonnull abi); + TW_EXTERN_C_END diff --git a/rust/tw_evm/src/evm_entry.rs b/rust/tw_evm/src/evm_entry.rs index 70a83a374e9..00822c40303 100644 --- a/rust/tw_evm/src/evm_entry.rs +++ b/rust/tw_evm/src/evm_entry.rs @@ -2,6 +2,7 @@ // // Copyright © 2017 Trust Wallet. +use crate::abi::AbiResult; use crate::evm_context::EvmContext; use crate::modules::abi_encoder::AbiEncoder; use crate::modules::rlp_encoder::RlpEncoder; @@ -46,8 +47,14 @@ pub trait EvmEntry { /// Returns the function type signature, of the form "baz(int32,uint256)". #[inline] - fn get_abi_function_signature(input: AbiProto::FunctionGetTypeInput<'_>) -> String { - AbiEncoder::::get_function_signature(input) + fn get_function_signature_from_proto(input: AbiProto::FunctionGetTypeInput<'_>) -> String { + AbiEncoder::::get_function_signature_from_proto(input) + } + + /// Returns the function type signature, of the form "baz(int32,uint256)". + #[inline] + fn get_function_signature_from_abi(abi: &str) -> AbiResult { + AbiEncoder::::get_function_signature_from_abi(abi) } // Encodes function inputs to Eth ABI binary. @@ -71,7 +78,10 @@ pub trait EvmEntryExt { fn decode_abi_params(&self, input: &[u8]) -> ProtoResult; /// Returns the function type signature, of the form "baz(int32,uint256)". - fn get_abi_function_signature(&self, input: &[u8]) -> ProtoResult; + fn get_function_signature_from_proto(&self, input: &[u8]) -> ProtoResult; + + /// Returns the function type signature, of the form "baz(int32,uint256)". + fn get_function_signature_from_abi(&self, abi: &str) -> AbiResult; /// Encodes function inputs to Eth ABI binary. fn encode_abi_function(&self, input: &[u8]) -> ProtoResult; @@ -102,9 +112,13 @@ where serialize(&output) } - fn get_abi_function_signature(&self, input: &[u8]) -> ProtoResult { + fn get_function_signature_from_proto(&self, input: &[u8]) -> ProtoResult { let input = deserialize(input)?; - Ok(::get_abi_function_signature(input)) + Ok(::get_function_signature_from_proto(input)) + } + + fn get_function_signature_from_abi(&self, abi: &str) -> AbiResult { + ::get_function_signature_from_abi(abi) } fn encode_abi_function(&self, input: &[u8]) -> ProtoResult { diff --git a/rust/tw_evm/src/modules/abi_encoder.rs b/rust/tw_evm/src/modules/abi_encoder.rs index 4849bf6a735..1dcde40f915 100644 --- a/rust/tw_evm/src/modules/abi_encoder.rs +++ b/rust/tw_evm/src/modules/abi_encoder.rs @@ -61,8 +61,13 @@ impl AbiEncoder { } #[inline] - pub fn get_function_signature(input: Proto::FunctionGetTypeInput<'_>) -> String { - Self::get_function_signature_impl(input) + pub fn get_function_signature_from_proto(input: Proto::FunctionGetTypeInput<'_>) -> String { + Self::get_function_signature_from_proto_impl(input) + } + + #[inline] + pub fn get_function_signature_from_abi(abi: &str) -> AbiResult { + Self::get_function_signature_from_abi_impl(abi) } #[inline] @@ -174,7 +179,7 @@ impl AbiEncoder { }) } - fn get_function_signature_impl(input: Proto::FunctionGetTypeInput<'_>) -> String { + fn get_function_signature_from_proto_impl(input: Proto::FunctionGetTypeInput<'_>) -> String { let function_inputs = input .inputs .into_iter() @@ -190,6 +195,16 @@ impl AbiEncoder { fun.signature() } + fn get_function_signature_from_abi_impl(function_abi: &str) -> AbiResult { + let mut fun: Function = serde_json::from_str(function_abi) + .tw_err(|_| AbiErrorKind::Error_invalid_abi) + .context("Error deserializing Function ABI as JSON")?; + + // Clear the `outputs` to avoid adding them to the signature. + fun.outputs.clear(); + Ok(fun.signature()) + } + fn encode_contract_call_impl( input: Proto::FunctionEncodingInput<'_>, ) -> AbiResult> { diff --git a/rust/tw_tests/tests/chains/ethereum/ethereum_abi.rs b/rust/tw_tests/tests/chains/ethereum/ethereum_abi.rs index 33802575420..8a15d46a63d 100644 --- a/rust/tw_tests/tests/chains/ethereum/ethereum_abi.rs +++ b/rust/tw_tests/tests/chains/ethereum/ethereum_abi.rs @@ -12,7 +12,7 @@ use tw_proto::{deserialize, serialize}; use wallet_core_rs::ffi::ethereum::abi::{ tw_ethereum_abi_decode_contract_call, tw_ethereum_abi_decode_params, tw_ethereum_abi_decode_value, tw_ethereum_abi_encode_function, - tw_ethereum_abi_function_get_signature, + tw_ethereum_abi_function_get_type, tw_ethereum_abi_get_function_signature, }; use tw_coin_registry::coin_type::CoinType; @@ -117,7 +117,7 @@ fn test_ethereum_abi_decode_params() { } #[test] -fn test_ethereum_abi_function_get_signature() { +fn test_ethereum_abi_function_get_type() { let input = AbiProto::FunctionGetTypeInput { function_name: "baz".into(), inputs: vec![ @@ -132,10 +132,10 @@ fn test_ethereum_abi_function_get_signature() { let input_data = TWDataHelper::create(serialize(&input).unwrap()); let actual = TWStringHelper::wrap(unsafe { - tw_ethereum_abi_function_get_signature(CoinType::Ethereum as u32, input_data.ptr()) + tw_ethereum_abi_function_get_type(CoinType::Ethereum as u32, input_data.ptr()) }) .to_string() - .expect("!tw_ethereum_abi_function_get_signature returned nullptr"); + .expect("!tw_ethereum_abi_function_get_type returned nullptr"); assert_eq!(actual, "baz(uint64,address)"); } @@ -191,3 +191,102 @@ fn test_ethereum_abi_decode_value() { assert!(output.error_message.is_empty()); assert_eq!(output.param_str, "42"); } + +#[test] +fn test_ethereum_abi_get_function_signature() { + let abi = r#"{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}"#; + + let abi_string = TWStringHelper::create(abi); + + let actual = TWStringHelper::wrap(unsafe { + tw_ethereum_abi_get_function_signature(CoinType::Ethereum as u32, abi_string.ptr()) + }) + .to_string() + .expect("!tw_ethereum_abi_get_function_signature returned nullptr"); + + assert_eq!(actual, "transfer(address,uint256)"); +} + +#[test] +fn test_ethereum_get_function_signature_complex() { + // From: https://docs.soliditylang.org/en/latest/abi-spec.html#handling-tuple-types + let abi = r#" +{ + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "a", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "b", + "type": "uint256[]" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "internalType": "struct Test.T[]", + "name": "c", + "type": "tuple[]" + } + ], + "internalType": "struct Test.S", + "name": "", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "internalType": "struct Test.T", + "name": "", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "f", + "outputs": [], + "stateMutability": "pure", + "type": "function" +} +"#; + + let abi_string = TWStringHelper::create(abi); + + let actual = TWStringHelper::wrap(unsafe { + tw_ethereum_abi_get_function_signature(CoinType::Ethereum as u32, abi_string.ptr()) + }) + .to_string() + .expect("!tw_ethereum_abi_get_function_signature returned nullptr"); + + assert_eq!( + actual, + "f((uint256,uint256[],(uint256,uint256)[]),(uint256,uint256),uint256)" + ); +} diff --git a/rust/wallet_core_rs/src/ffi/ethereum/abi.rs b/rust/wallet_core_rs/src/ffi/ethereum/abi.rs index 4c21d659542..124bb391bc3 100644 --- a/rust/wallet_core_rs/src/ffi/ethereum/abi.rs +++ b/rust/wallet_core_rs/src/ffi/ethereum/abi.rs @@ -57,7 +57,7 @@ pub unsafe extern "C" fn tw_ethereum_abi_decode_params( /// \param input The serialized data of `TW.EthereumAbi.Proto.FunctionGetTypeInput`. /// \return function type signature as a Non-null string. #[no_mangle] -pub unsafe extern "C" fn tw_ethereum_abi_function_get_signature( +pub unsafe extern "C" fn tw_ethereum_abi_function_get_type( coin: u32, input: *const TWData, ) -> *mut TWString { @@ -67,11 +67,32 @@ pub unsafe extern "C" fn tw_ethereum_abi_function_get_signature( let evm_dispatcher = try_or_else!(evm_dispatcher(coin), || TWString::new().into_ptr()); evm_dispatcher - .get_abi_function_signature(input_data.as_slice()) + .get_function_signature_from_proto(input_data.as_slice()) .map(|str| TWString::from(str).into_ptr()) .unwrap_or_else(|_| TWString::new().into_ptr()) } +/// Returns the function type signature, of the form "baz(int32,uint256)". +/// +/// \param coin EVM-compatible coin type. +/// \param abi The function ABI json string, for example: {"inputs":[{"internalType":"bool","name":"arg1","type":"bool"}],"name":"fun1","outputs":[],"stateMutability":"nonpayable","type":"function"} +/// \return function type signature, null if the input is invalid. +#[no_mangle] +pub unsafe extern "C" fn tw_ethereum_abi_get_function_signature( + coin: u32, + abi: *const TWString, +) -> *mut TWString { + let coin = try_or_else!(CoinType::try_from(coin), std::ptr::null_mut); + let abi_string = try_or_else!(TWString::from_ptr_as_ref(abi), std::ptr::null_mut); + let abi_str = try_or_else!(abi_string.as_str(), std::ptr::null_mut); + + let evm_dispatcher = try_or_else!(evm_dispatcher(coin), std::ptr::null_mut); + evm_dispatcher + .get_function_signature_from_abi(abi_str) + .map(|str| TWString::from(str).into_ptr()) + .unwrap_or_else(|_| std::ptr::null_mut()) +} + /// Encode function inputs to Eth ABI binary. /// /// \param coin EVM-compatible coin type. diff --git a/src/Ethereum/ABI/Function.cpp b/src/Ethereum/ABI/Function.cpp index be1007516fe..c28c24918bb 100644 --- a/src/Ethereum/ABI/Function.cpp +++ b/src/Ethereum/ABI/Function.cpp @@ -159,7 +159,7 @@ std::string Function::getType() const { *input.mutable_inputs() = inputs.params(); Rust::TWDataWrapper inputData(data(input.SerializeAsString())); - Rust::TWStringWrapper outputPtr = Rust::tw_ethereum_abi_function_get_signature(TWCoinTypeEthereum, inputData.get()); + Rust::TWStringWrapper outputPtr = Rust::tw_ethereum_abi_function_get_type(TWCoinTypeEthereum, inputData.get()); return outputPtr.toStringOrDefault(); } diff --git a/src/interface/TWEthereumAbi.cpp b/src/interface/TWEthereumAbi.cpp index 829eb594a33..59864b6bf41 100644 --- a/src/interface/TWEthereumAbi.cpp +++ b/src/interface/TWEthereumAbi.cpp @@ -86,3 +86,19 @@ TWData* _Nonnull TWEthereumAbiEncodeTyped(TWString* _Nonnull messageJson) { } catch (...) {} // return empty return TWDataCreateWithBytes(data.data(), data.size()); } + +TWString* _Nullable TWEthereumAbiGetFunctionSignature(TWString* _Nonnull abi) { + try { + const Rust::TWStringWrapper abiStr = TWStringUTF8Bytes(abi); + + const Rust::TWStringWrapper outputDataPtr = Rust::tw_ethereum_abi_get_function_signature(TWCoinTypeEthereum, abiStr.get()); + if (!outputDataPtr) { + return nullptr; + } + + return TWStringCreateWithUTF8Bytes(outputDataPtr.c_str()); + } + catch(...) { + return nullptr; + } +} diff --git a/swift/Tests/Blockchains/EthereumAbiTests.swift b/swift/Tests/Blockchains/EthereumAbiTests.swift index 0fbd2e07be2..ce7ada79d07 100644 --- a/swift/Tests/Blockchains/EthereumAbiTests.swift +++ b/swift/Tests/Blockchains/EthereumAbiTests.swift @@ -350,4 +350,30 @@ class EthereumAbiTests: XCTestCase { XCTAssertEqual(decodingOutput.tokens[0].name, "name") XCTAssertEqual(decodingOutput.tokens[0].stringValue, "deadbeef") } + + func testEthereumAbiGetFunctionSignature() throws { + let abiJson = """ + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } + """ + + let functionSignature = EthereumAbi.getFunctionSignature(abi: abiJson) + XCTAssertEqual(functionSignature, "transfer(address,uint256)") + } } diff --git a/tests/chains/Ethereum/TWEthereumAbiTests.cpp b/tests/chains/Ethereum/TWEthereumAbiTests.cpp index 607c2c58193..9bbe53e3df9 100644 --- a/tests/chains/Ethereum/TWEthereumAbiTests.cpp +++ b/tests/chains/Ethereum/TWEthereumAbiTests.cpp @@ -345,4 +345,13 @@ TEST(TWEthereumAbi, encodeTyped) { ); } +TEST(TWEthereumAbi, GetFunctionSignature) { + const auto abiJson = R"|({"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"})|"; + const auto abiJsonStr = WRAPS(TWStringCreateWithUTF8Bytes(abiJson)); + + const auto result = WRAPS(TWEthereumAbiGetFunctionSignature(abiJsonStr.get())); + const auto expected = WRAPS(TWStringCreateWithUTF8Bytes("transfer(address,uint256)")); + EXPECT_TRUE(TWStringEqual(result.get(), expected.get())); +} + } // namespace TW::Ethereum