Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Ethereum]: Support obtaining the function signature from an Ethereum function ABI #4182

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
}
15 changes: 15 additions & 0 deletions codegen-v2/manifest/TWEthereumAbi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions include/TrustWalletCore/TWEthereumAbi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 19 additions & 5 deletions rust/tw_evm/src/evm_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<Self::Context>::get_function_signature(input)
fn get_function_signature_from_proto(input: AbiProto::FunctionGetTypeInput<'_>) -> String {
AbiEncoder::<Self::Context>::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<String> {
AbiEncoder::<Self::Context>::get_function_signature_from_abi(abi)
}

// Encodes function inputs to Eth ABI binary.
Expand All @@ -71,7 +78,10 @@ pub trait EvmEntryExt {
fn decode_abi_params(&self, input: &[u8]) -> ProtoResult<Data>;

/// Returns the function type signature, of the form "baz(int32,uint256)".
fn get_abi_function_signature(&self, input: &[u8]) -> ProtoResult<String>;
fn get_function_signature_from_proto(&self, input: &[u8]) -> ProtoResult<String>;

/// Returns the function type signature, of the form "baz(int32,uint256)".
fn get_function_signature_from_abi(&self, abi: &str) -> AbiResult<String>;

/// Encodes function inputs to Eth ABI binary.
fn encode_abi_function(&self, input: &[u8]) -> ProtoResult<Data>;
Expand Down Expand Up @@ -102,9 +112,13 @@ where
serialize(&output)
}

fn get_abi_function_signature(&self, input: &[u8]) -> ProtoResult<String> {
fn get_function_signature_from_proto(&self, input: &[u8]) -> ProtoResult<String> {
let input = deserialize(input)?;
Ok(<Self as EvmEntry>::get_abi_function_signature(input))
Ok(<Self as EvmEntry>::get_function_signature_from_proto(input))
}

fn get_function_signature_from_abi(&self, abi: &str) -> AbiResult<String> {
<Self as EvmEntry>::get_function_signature_from_abi(abi)
}

fn encode_abi_function(&self, input: &[u8]) -> ProtoResult<Data> {
Expand Down
21 changes: 18 additions & 3 deletions rust/tw_evm/src/modules/abi_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,13 @@ impl<Context: EvmContext> AbiEncoder<Context> {
}

#[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<String> {
Self::get_function_signature_from_abi_impl(abi)
}

#[inline]
Expand Down Expand Up @@ -174,7 +179,7 @@ impl<Context: EvmContext> AbiEncoder<Context> {
})
}

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()
Expand All @@ -190,6 +195,16 @@ impl<Context: EvmContext> AbiEncoder<Context> {
fun.signature()
}

fn get_function_signature_from_abi_impl(function_abi: &str) -> AbiResult<String> {
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<Proto::FunctionEncodingOutput<'static>> {
Expand Down
107 changes: 103 additions & 4 deletions rust/tw_tests/tests/chains/ethereum/ethereum_abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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![
Expand All @@ -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)");
}
Expand Down Expand Up @@ -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())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is tw_ethereum_abi_function_get_signature function already. However, it takes a function name and Params.
I think we need to bring them to a common denominator, in particular:

-// Return the function type signature, of the form "baz(int32,uint256)".
-message FunctionGetTypeInput {
-    // Function signature. Includes function inputs if they are.
-    // Examples:
-    // - `functionName()`
-    // - `functionName()`
-    // - `functionName(bool)`
-    // - `functionName(uint256,bytes32)`
-    string function_name = 1;
+// Return the function type signature, of the form "transfer(address,uint256)".
+message FunctionGetSignatureInput {
+    message FunctionNameParams {
+        // Function name. For example, "baz".
+        string function_name = 1;
+
+        // A set of ABI type parameters.
+        repeated Param inputs = 2;
+    }
 
-    // A set of ABI type parameters.
-    repeated Param inputs = 2;
+    oneof abi {
+        // A pair of function name and parameters.
+        FunctionNameParams function_name_params = 1;
+
+        // A set of ABI parameters in JSON.
+        // Expected to be a JSON array at the entry level.
+        // Example:
+        // ```
+        // {
+        //     "constant": false,
+        //     "inputs": [
+        //         {
+        //             "name": "_to",
+        //             "type": "address"
+        //         },
+        //         {
+        //             "name": "_value",
+        //             "type": "uint256"
+        //         }
+        //     ],
+        //     "name": "transfer",
+        //     "outputs": [],
+        //     "payable": false,
+        //     "stateMutability": "nonpayable",
+        //     "type": "function"
+        // }
+        // ```
+        string abi_json = 2;
+    }
 }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please let me know what do you think about the refactoring? Note that tw_ethereum_abi_function_get_signature is not exposed into Kotlin, Swift and other bindings, it's only used internally from C++, so we can freely change it as no backward compatibility will be broken

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that your refactoring suggestions are valuable for the cleanliness of the code.

However, from the perspective of a library caller, I believe using plain JSON arguments is more convenient. I noticed that the only reason we still need the Protobuf type FunctionGetTypeInput (or the newer FunctionNameParams) is that EthereumAbiFunction.getType() is implemented in C++. Once we have ported all C++ code into Rust, we can totally remove the Protobuf types FunctionGetTypeInput (or the newer FunctionNameParams), as we have already implemented param_from_proto in Rust.

Therefore, I suggest that we introduce a new function rather than enhance the old one.

Copy link
Contributor Author

@10gic 10gic Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make the old and new code appear more cohesive and clear, I can make a few small adjustments to the old code:

  1. Rename tw_ethereum_abi_function_get_signature to tw_ethereum_abi_function_get_type, as this function is only called within EthereumAbiFunction.getType().
  2. Rename EvmEntryExt.get_abi_function_signature to EvmEntryExt.get_function_signature_from_proto.

Please let me know what you think about the adjustments. I would appreciate your feedback.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. It's ok, let's follow the first way then 👍

})
.to_string()
.expect("!tw_ethereum_abi_get_function_signature returned nullptr");

assert_eq!(
actual,
"f((uint256,uint256[],(uint256,uint256)[]),(uint256,uint256),uint256)"
);
}
25 changes: 23 additions & 2 deletions rust/wallet_core_rs/src/ffi/ethereum/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/Ethereum/ABI/Function.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
16 changes: 16 additions & 0 deletions src/interface/TWEthereumAbi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
26 changes: 26 additions & 0 deletions swift/Tests/Blockchains/EthereumAbiTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
}
Loading
Loading