diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosSigner.kt index e536821658b..2c544bdbd9a 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosSigner.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosSigner.kt @@ -69,6 +69,59 @@ class TestAptosSigner { ) } + @Test + fun AptosTransactionBlindSigningWithABI() { + // Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x1ee2aa55382bf6b5a9f7a7f2b2066e16979489c6b2868704a2cf2c482f12b5ca/payload?network=mainnet + val key = + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec".toHexBytesInByteString() + + val payloadJson = """ + { + "function": "0x9770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da3::controller::deposit", + "type_arguments": [ + "0x1::aptos_coin::AptosCoin" + ], + "arguments": [ + "0x4d61696e204163636f756e74", + "10000000", + false + ], + "type": "entry_function_payload" + } + """.trimIndent() + val signingInput = Aptos.SigningInput.newBuilder() + .setChainId(1) + .setExpirationTimestampSecs(1735902711) + .setGasUnitPrice(100) + .setMaxGasAmount(50000) + .setSender("0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30") + .setSequenceNumber(69) + .setAnyEncoded(payloadJson) + .setPrivateKey(key) + .setAbi(""" + [ + "vector", + "u64", + "bool" + ] + """.trimIndent()) + .build() + + val result = AnySigner.sign(signingInput, CoinType.APTOS, Aptos.SigningOutput.parser()) + assertEquals( + Numeric.cleanHexPrefix(Numeric.toHexString(result.rawTxn.toByteArray())), + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304500000000000000029770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da30a636f6e74726f6c6c6572076465706f736974010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e00030d0c4d61696e204163636f756e74088096980000000000010050c30000000000006400000000000000f7c577670000000001" + ) + assertEquals( + Numeric.cleanHexPrefix(Numeric.toHexString(result.authenticator.signature.toByteArray())), + "13dcf1636abd31996729ded4d3bf56e9c7869a7188df4f185cbcce42f0dc74b6e1b54d31703ee3babbea2ef72b3338b8c2866cec68cbd761ccc7f80910124304" + ) + assertEquals( + Numeric.cleanHexPrefix(Numeric.toHexString(result.encoded.toByteArray())), + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304500000000000000029770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da30a636f6e74726f6c6c6572076465706f736974010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e00030d0c4d61696e204163636f756e74088096980000000000010050c30000000000006400000000000000f7c5776700000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c4013dcf1636abd31996729ded4d3bf56e9c7869a7188df4f185cbcce42f0dc74b6e1b54d31703ee3babbea2ef72b3338b8c2866cec68cbd761ccc7f80910124304" + ) + } + @Test fun AptosTransactionSigning() { // Successfully broadcasted https://explorer.aptoslabs.com/txn/0xb4d62afd3862116e060dd6ad9848ccb50c2bc177799819f1d29c059ae2042467?network=devnet diff --git a/rust/chains/tw_aptos/src/aptos_move_types.rs b/rust/chains/tw_aptos/src/aptos_move_types.rs new file mode 100644 index 00000000000..f364b7f6e42 --- /dev/null +++ b/rust/chains/tw_aptos/src/aptos_move_types.rs @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use move_core_types::{ + account_address::AccountAddress, + identifier::Identifier, + language_storage::{StructTag, TypeTag}, + parser::parse_type_tag, +}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use tw_encoding::EncodingError; + +/// The address of an account +/// +/// This is represented in a string as a 64 character hex string, sometimes +/// shortened by stripping leading 0s, and adding a 0x. +#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Address(AccountAddress); + +impl From for Address { + fn from(address: AccountAddress) -> Self { + Self(address) + } +} + +impl From
for AccountAddress { + fn from(address: Address) -> Self { + address.0 + } +} + +impl From<&Address> for AccountAddress { + fn from(address: &Address) -> Self { + address.0 + } +} + +/// A wrapper of a Move identifier +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] +pub struct IdentifierWrapper(pub Identifier); + +impl From for Identifier { + fn from(value: IdentifierWrapper) -> Identifier { + value.0 + } +} + +impl From for IdentifierWrapper { + fn from(value: Identifier) -> IdentifierWrapper { + Self(value) + } +} + +impl From<&Identifier> for IdentifierWrapper { + fn from(value: &Identifier) -> IdentifierWrapper { + Self(value.clone()) + } +} + +/// A Move struct tag for referencing an onchain struct type +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MoveStructTag { + pub address: Address, + pub module: IdentifierWrapper, + pub name: IdentifierWrapper, + /// Generic type parameters associated with the struct + pub generic_type_params: Vec, +} + +impl From for MoveStructTag { + fn from(tag: StructTag) -> Self { + Self { + address: tag.address.into(), + module: tag.module.into(), + name: tag.name.into(), + generic_type_params: tag.type_params.into_iter().map(MoveType::from).collect(), + } + } +} + +impl From<&StructTag> for MoveStructTag { + fn from(tag: &StructTag) -> Self { + Self { + address: tag.address.into(), + module: IdentifierWrapper::from(&tag.module), + name: IdentifierWrapper::from(&tag.name), + generic_type_params: tag.type_params.iter().map(MoveType::from).collect(), + } + } +} + +impl TryFrom for StructTag { + type Error = EncodingError; + + fn try_from(tag: MoveStructTag) -> Result { + Ok(Self { + address: tag.address.into(), + module: tag.module.into(), + name: tag.name.into(), + type_params: tag + .generic_type_params + .into_iter() + .map(|p| p.try_into()) + .collect::, Self::Error>>()?, + }) + } +} + +/// An enum of Move's possible types on-chain +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MoveType { + /// A bool type + Bool, + /// An 8-bit unsigned int + U8, + /// A 16-bit unsigned int + U16, + /// A 32-bit unsigned int + U32, + /// A 64-bit unsigned int + U64, + /// A 128-bit unsigned int + U128, + /// A 256-bit unsigned int + U256, + /// A 32-byte account address + Address, + /// An account signer + Signer, + /// A Vector of [`MoveType`] + Vector { items: Box }, + /// A struct of [`MoveStructTag`] + Struct(MoveStructTag), + /// A generic type param with index + GenericTypeParam { index: u16 }, + /// A reference + Reference { mutable: bool, to: Box }, + /// A move type that couldn't be parsed + /// + /// This prevents the parser from just throwing an error because one field + /// was unparsable, and gives the value in it. + Unparsable(String), +} + +impl FromStr for MoveType { + type Err = EncodingError; + + // Taken from: https://github.com/aptos-labs/aptos-core/blob/aaa3514c8ee4e5d38b89d916eadff7286a42e040/api/types/src/move_types.rs#L612-L639 + fn from_str(mut s: &str) -> Result { + let mut is_ref = false; + let mut is_mut = false; + if s.starts_with('&') { + s = &s[1..]; + is_ref = true; + } + if is_ref && s.starts_with("mut ") { + s = &s[4..]; + is_mut = true; + } + // Previously this would just crap out, but this meant the API could + // return a serialized version of an object and not be able to + // deserialize it using that same object. + let inner = match parse_type_tag(s) { + Ok(inner) => inner.into(), + Err(_e) => MoveType::Unparsable(s.to_string()), + }; + if is_ref { + Ok(MoveType::Reference { + mutable: is_mut, + to: Box::new(inner), + }) + } else { + Ok(inner) + } + } +} + +impl From for MoveType { + fn from(tag: TypeTag) -> Self { + match tag { + TypeTag::Bool => MoveType::Bool, + TypeTag::U8 => MoveType::U8, + TypeTag::U16 => MoveType::U16, + TypeTag::U32 => MoveType::U32, + TypeTag::U64 => MoveType::U64, + TypeTag::U256 => MoveType::U256, + TypeTag::U128 => MoveType::U128, + TypeTag::Address => MoveType::Address, + TypeTag::Signer => MoveType::Signer, + TypeTag::Vector(v) => MoveType::Vector { + items: Box::new(MoveType::from(*v)), + }, + TypeTag::Struct(v) => MoveType::Struct((*v).into()), + } + } +} + +impl From<&TypeTag> for MoveType { + fn from(tag: &TypeTag) -> Self { + match tag { + TypeTag::Bool => MoveType::Bool, + TypeTag::U8 => MoveType::U8, + TypeTag::U16 => MoveType::U16, + TypeTag::U32 => MoveType::U32, + TypeTag::U64 => MoveType::U64, + TypeTag::U128 => MoveType::U128, + TypeTag::U256 => MoveType::U256, + TypeTag::Address => MoveType::Address, + TypeTag::Signer => MoveType::Signer, + TypeTag::Vector(v) => MoveType::Vector { + items: Box::new(MoveType::from(v.as_ref())), + }, + TypeTag::Struct(v) => MoveType::Struct((&**v).into()), + } + } +} + +impl TryFrom for TypeTag { + type Error = EncodingError; + + fn try_from(tag: MoveType) -> Result { + let ret = match tag { + MoveType::Bool => TypeTag::Bool, + MoveType::U8 => TypeTag::U8, + MoveType::U16 => TypeTag::U16, + MoveType::U32 => TypeTag::U32, + MoveType::U64 => TypeTag::U64, + MoveType::U128 => TypeTag::U128, + MoveType::U256 => TypeTag::U256, + MoveType::Address => TypeTag::Address, + MoveType::Signer => TypeTag::Signer, + MoveType::Vector { items } => TypeTag::Vector(Box::new((*items).try_into()?)), + MoveType::Struct(v) => TypeTag::Struct(Box::new(v.try_into()?)), + MoveType::GenericTypeParam { index: _ } => TypeTag::Address, // Dummy type, allows for Object + _ => { + return Err(EncodingError::InvalidInput); + }, + }; + Ok(ret) + } +} diff --git a/rust/chains/tw_aptos/src/constants.rs b/rust/chains/tw_aptos/src/constants.rs index 714c1a2f782..2f2996ccceb 100644 --- a/rust/chains/tw_aptos/src/constants.rs +++ b/rust/chains/tw_aptos/src/constants.rs @@ -2,6 +2,11 @@ // // Copyright © 2017 Trust Wallet. +use move_core_types::{ident_str, identifier::IdentStr}; + pub const GAS_UNIT_PRICE: u64 = 100; pub const MAX_GAS_AMOUNT: u64 = 100_000_000; pub const APTOS_SALT: &[u8] = b"APTOS::RawTransaction"; + +pub const OBJECT_MODULE: &IdentStr = ident_str!("object"); +pub const OBJECT_STRUCT: &IdentStr = ident_str!("Object"); diff --git a/rust/chains/tw_aptos/src/lib.rs b/rust/chains/tw_aptos/src/lib.rs index 5388e03f4e7..1407bdd73c0 100644 --- a/rust/chains/tw_aptos/src/lib.rs +++ b/rust/chains/tw_aptos/src/lib.rs @@ -4,6 +4,7 @@ pub mod address; pub mod aptos_move_packages; +pub mod aptos_move_types; pub mod constants; pub mod entry; mod serde_helper; diff --git a/rust/chains/tw_aptos/src/transaction_builder.rs b/rust/chains/tw_aptos/src/transaction_builder.rs index deae84c8008..4ccc8d9b1b4 100644 --- a/rust/chains/tw_aptos/src/transaction_builder.rs +++ b/rust/chains/tw_aptos/src/transaction_builder.rs @@ -152,8 +152,10 @@ impl TransactionFactory { let v = serde_json::from_str::(&input.any_encoded) .into_tw() .context("Error decoding 'SigningInput::any_encoded' as JSON")?; + let abi = + serde_json::from_str::(&input.abi).unwrap_or(serde_json::json!([])); if is_blind_sign { - let entry_function = EntryFunction::try_from(v)?; + let entry_function = EntryFunction::parse_with_abi(v, abi)?; Ok(factory.payload(TransactionPayload::EntryFunction(entry_function))) } else { SigningError::err(SigningErrorType::Error_input_parse) diff --git a/rust/chains/tw_aptos/src/transaction_payload.rs b/rust/chains/tw_aptos/src/transaction_payload.rs index d0105bc949d..6b1c78e4cd3 100644 --- a/rust/chains/tw_aptos/src/transaction_payload.rs +++ b/rust/chains/tw_aptos/src/transaction_payload.rs @@ -2,16 +2,22 @@ // // Copyright © 2017 Trust Wallet. +use crate::aptos_move_types::MoveType; +use crate::constants::{OBJECT_MODULE, OBJECT_STRUCT}; use crate::serde_helper::vec_bytes; +use move_core_types::account_address::AccountAddress; use move_core_types::identifier::Identifier; -use move_core_types::language_storage::{ModuleId, StructTag, TypeTag}; +use move_core_types::language_storage::{ModuleId, StructTag, TypeTag, CORE_CODE_ADDRESS}; use move_core_types::parser::parse_transaction_argument; use move_core_types::transaction_argument::TransactionArgument; +use move_core_types::u256; +use move_core_types::value::{MoveStruct, MoveStructLayout, MoveTypeLayout, MoveValue}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::default::Default; use std::str::FromStr; use tw_coin_entry::error::prelude::*; +use tw_encoding::hex::DecodeHex; use tw_encoding::{bcs, EncodingError, EncodingResult}; use tw_memory::Data; use tw_proto::Aptos; @@ -57,23 +63,41 @@ impl TryFrom for EntryFunction { type Error = EntryFunctionError; fn try_from(value: Value) -> EntryFunctionResult { + Self::parse_with_abi(value, json!([])) + } +} + +impl EntryFunction { + pub fn parse_with_abi(value: Value, abi: Value) -> EntryFunctionResult { let function_str = value["function"] .as_str() .ok_or(EntryFunctionError::MissingFunctionName)?; let tag = StructTag::from_str(function_str) .map_err(|_| EntryFunctionError::InvalidFunctionName)?; + let abi = abi + .as_array() + .ok_or(EntryFunctionError::MissingTypeArguments)?; + let get_abi_str = + |index: usize| -> Option { abi.get(index)?.as_str().map(|s| s.to_string()) }; + let args = value["arguments"] .as_array() .ok_or(EntryFunctionError::MissingArguments)? .iter() - .map(|element| { + .enumerate() + .map(|(index, element)| { let arg_str = element.to_string(); - let arg = parse_transaction_argument( - arg_str.trim_start_matches('"').trim_end_matches('"'), - ) - .map_err(|_| EntryFunctionError::InvalidArguments)?; - serialize_argument(&arg).map_err(EntryFunctionError::from) + let arg_str = arg_str.trim_start_matches('"').trim_end_matches('"'); + + if let Some(abi_str) = get_abi_str(index) { + let arg = convert_to_move_value(&abi_str, element.clone())?; + bcs::encode(&arg).map_err(EntryFunctionError::from) + } else { + let arg = parse_transaction_argument(arg_str) + .map_err(|_| EntryFunctionError::InvalidArguments)?; + serialize_argument(&arg).map_err(EntryFunctionError::from) + } }) .collect::>>()?; @@ -99,6 +123,215 @@ impl TryFrom for EntryFunction { } } +fn convert_to_move_value(abi_str: &str, element: Value) -> EntryFunctionResult { + let move_type: MoveType = abi_str + .parse() + .map_err(|_| EntryFunctionError::InvalidTypeArguments)?; + let type_tag: TypeTag = move_type + .try_into() + .map_err(|_| EntryFunctionError::InvalidTypeArguments)?; + // Taken from: https://github.com/aptos-labs/aptos-core/blob/aaa3514c8ee4e5d38b89d916eadff7286a42e040/api/types/src/convert.rs#L845-L872 + let layout = match type_tag { + TypeTag::Struct(ref boxed_struct) => { + // The current framework can't handle generics, so we handle this here + if boxed_struct.address == AccountAddress::ONE + && boxed_struct.module.as_ident_str() == OBJECT_MODULE + && boxed_struct.name.as_ident_str() == OBJECT_STRUCT + { + // Objects are just laid out as an address + MoveTypeLayout::Address + } else { + // For all other structs, use their set layout + build_type_layout(&type_tag)? + } + }, + _ => build_type_layout(&type_tag)?, + }; + parse_argument(&layout, element).map_err(|_| EntryFunctionError::InvalidArguments) +} + +fn build_type_layout(t: &TypeTag) -> EncodingResult { + use TypeTag::*; + Ok(match t { + Bool => MoveTypeLayout::Bool, + U8 => MoveTypeLayout::U8, + U64 => MoveTypeLayout::U64, + U128 => MoveTypeLayout::U128, + Address => MoveTypeLayout::Address, + Vector(elem_t) => MoveTypeLayout::Vector(Box::new(build_type_layout(elem_t)?)), + Struct(s) => MoveTypeLayout::Struct(build_struct_layout(s)?), + U16 => MoveTypeLayout::U16, + U32 => MoveTypeLayout::U32, + U256 => MoveTypeLayout::U256, + Signer => Err(EncodingError::InvalidInput)?, + }) +} + +fn build_struct_layout(s: &StructTag) -> EncodingResult { + let type_arguments = s + .type_params + .iter() + .map(build_type_layout) + .collect::>>()?; + if type_arguments.is_empty() { + Ok(MoveStructLayout::WithTypes { + type_: s.clone(), + fields: vec![], + }) + } else { + Ok(MoveStructLayout::Runtime(type_arguments)) + } +} + +fn parse_argument(layout: &MoveTypeLayout, val: Value) -> EncodingResult { + let val_str = val + .to_string() + .trim_start_matches('"') + .trim_end_matches('"') + .to_string(); + Ok(match layout { + MoveTypeLayout::Bool => MoveValue::Bool( + val_str + .parse::() + .map_err(|_| EncodingError::InvalidInput)?, + ), + MoveTypeLayout::U8 => MoveValue::U8( + val_str + .parse::() + .map_err(|_| EncodingError::InvalidInput)?, + ), + MoveTypeLayout::U16 => MoveValue::U16( + val_str + .parse::() + .map_err(|_| EncodingError::InvalidInput)?, + ), + MoveTypeLayout::U32 => MoveValue::U32( + val_str + .parse::() + .map_err(|_| EncodingError::InvalidInput)?, + ), + MoveTypeLayout::U64 => MoveValue::U64( + val_str + .parse::() + .map_err(|_| EncodingError::InvalidInput)?, + ), + MoveTypeLayout::U128 => MoveValue::U128( + val_str + .parse::() + .map_err(|_| EncodingError::InvalidInput)?, + ), + MoveTypeLayout::U256 => MoveValue::U256( + val_str + .parse::() + .map_err(|_| EncodingError::InvalidInput)?, + ), + MoveTypeLayout::Address => MoveValue::Address( + val_str + .parse::() + .map_err(|_| EncodingError::InvalidInput)?, + ), + MoveTypeLayout::Vector(item_layout) => parse_vector_argument(item_layout.as_ref(), val)?, + MoveTypeLayout::Struct(struct_layout) => parse_struct_argument(struct_layout, val)?, + // Some values, e.g., signer or ones with custom serialization + // (native), are not stored to storage and so we do not expect + // to see them here. + MoveTypeLayout::Signer => { + return Err(EncodingError::InvalidInput); + }, + }) +} + +fn parse_vector_argument(layout: &MoveTypeLayout, val: Value) -> EncodingResult { + if matches!(layout, MoveTypeLayout::U8) { + Ok(MoveValue::Vector( + val.as_str() + .ok_or(EncodingError::InvalidInput)? + .decode_hex() + .map_err(|_| EncodingError::InvalidInput)? + .into_iter() + .map(MoveValue::U8) + .collect::>(), + )) + } else { + let val = trim_if_needed(val)?; + if let Value::Array(list) = val { + let vals = list + .into_iter() + .map(|v| parse_argument(layout, v).map_err(|_| EncodingError::InvalidInput)) + .collect::>()?; + Ok(MoveValue::Vector(vals)) + } else { + Err(EncodingError::InvalidInput) + } + } +} + +// Inspired from: https://github.com/aptos-labs/aptos-core/blob/aaa3514c8ee4e5d38b89d916eadff7286a42e040/api/types/src/convert.rs#L924 +// However, we expect struct with strings and unnamed fields while the original code expects struct with named fields. +// This is because the original code uses a module resolver internally to obtain the struct types and we don't have that here. +// In order to be able to accept that as an API, we need to change the code to accept struct with unnamed fields. +fn parse_struct_argument(layout: &MoveStructLayout, val: Value) -> EncodingResult { + let field_layouts = match layout { + MoveStructLayout::Runtime(fields) => fields, + MoveStructLayout::WithTypes { type_, .. } => { + if is_utf8_string(type_) { + let string = val.as_str().ok_or(EncodingError::InvalidInput)?; + return Ok(new_vm_utf8_string(string)); + } else { + return Err(EncodingError::InvalidInput); + } + }, + _ => return Err(EncodingError::InvalidInput), + }; + let val = trim_if_needed(val)?; + let field_values = if let Value::Array(fields) = val { + fields + } else { + return Err(EncodingError::InvalidInput); + }; + let fields = field_layouts + .iter() + .zip(field_values.into_iter()) + .map(|(field_layout, value)| { + let move_value = parse_argument(field_layout, value)?; + Ok(move_value) + }) + .collect::>()?; + + Ok(MoveValue::Struct(MoveStruct::Runtime(fields))) +} + +fn trim_if_needed(val: Value) -> EncodingResult { + if val.is_string() { + let val_str = val.as_str().ok_or(EncodingError::InvalidInput)?; + let val_str = val_str + .trim_start_matches('"') + .trim_end_matches('"') + .to_string(); + let val: Value = serde_json::from_str(&val_str).map_err(|_| EncodingError::InvalidInput)?; + Ok(val) + } else { + Ok(val) + } +} + +fn is_utf8_string(st: &StructTag) -> bool { + st.address == CORE_CODE_ADDRESS + && st.name.to_string() == "String" + && st.module.to_string() == "string" +} + +fn new_vm_utf8_string(string: &str) -> MoveValue { + let byte_vector = MoveValue::Vector( + string + .as_bytes() + .iter() + .map(|byte| MoveValue::U8(*byte)) + .collect(), + ); + MoveValue::Struct(MoveStruct::Runtime(vec![byte_vector])) +} + fn serialize_argument(arg: &TransactionArgument) -> EncodingResult { match arg { TransactionArgument::U8(v) => bcs::encode(v), @@ -273,4 +506,141 @@ mod tests { let serialized = bcs::encode(&tp).unwrap(); assert_eq!(hex::encode(serialized, false), expected_serialized); } + + #[test] + fn test_payload_with_vector_of_u8() { + let payload_value: Value = json!({ + "type":"entry_function_payload", + "function":"0x9770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da3::controller::deposit", + "type_arguments":["0x1::aptos_coin::AptosCoin"], + "arguments":[ + "0x010302" + ] + }); + let abi = r#"[ + "vector" + ]"#; + let abi_value: Value = serde_json::from_str(abi).unwrap(); + let v = EntryFunction::parse_with_abi(payload_value.clone(), abi_value).unwrap(); + let v = bcs::decode::>(&v.args[0]).unwrap(); + assert_eq!(v, vec![1u8, 3u8, 2u8]); + } + + #[test] + fn test_payload_with_vector_of_u64() { + let payload_value: Value = json!({ + "type":"entry_function_payload", + "function":"0x9770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da3::controller::deposit", + "type_arguments":["0x1::aptos_coin::AptosCoin"], + "arguments":[ + "[\"1\", \"2\", \"3\"]" + ] + }); + let abi = r#"[ + "vector" + ]"#; + let abi_value: Value = serde_json::from_str(abi).unwrap(); + let v = EntryFunction::parse_with_abi(payload_value.clone(), abi_value).unwrap(); + let v = bcs::decode::>(&v.args[0]).unwrap(); + assert_eq!(v, vec![1u64, 2u64, 3u64]); + } + + #[test] + fn test_payload_with_vector_of_vector() { + let payload_value: Value = json!({ + "type":"entry_function_payload", + "function":"0x9770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da3::controller::deposit", + "type_arguments":["0x1::aptos_coin::AptosCoin"], + "arguments":[ + "[\"0x4d61696e204163636f756e74\",\"0x6112\"]" + ] + }); + let abi = r#"[ + "vector>" + ]"#; + let abi_value: Value = serde_json::from_str(abi).unwrap(); + let v = EntryFunction::parse_with_abi(payload_value.clone(), abi_value).unwrap(); + let v = bcs::decode::>>(&v.args[0]).unwrap(); + assert_eq!( + hex::encode(v[0].clone(), true), + "0x4d61696e204163636f756e74" + ); + assert_eq!(hex::encode(v[1].clone(), true), "0x6112"); + } + + #[test] + fn test_payload_with_struct_string() { + let payload_value: Value = json!({ + "type":"entry_function_payload", + "function":"0x9770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da3::controller::deposit", + "type_arguments":["0x1::aptos_coin::AptosCoin"], + "arguments":[ + "123" + ] + }); + let abi = r#"[ + "0x1::string::String" + ]"#; + let abi_value: Value = serde_json::from_str(abi).unwrap(); + let v = EntryFunction::parse_with_abi(payload_value.clone(), abi_value).unwrap(); + let v = bcs::decode::(&v.args[0]).unwrap(); + assert_eq!(v, "123"); + } + + #[test] + fn test_payload_with_struct() { + let payload_value: Value = json!({ + "type":"entry_function_payload", + "function":"0x9770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da3::controller::deposit", + "type_arguments":["0x1::aptos_coin::AptosCoin"], + "arguments":[ + "[10]" + ] + }); + let abi = r#"[ + "0x1::coin::Coin" + ]"#; + let abi_value: Value = serde_json::from_str(abi).unwrap(); + let v = EntryFunction::parse_with_abi(payload_value.clone(), abi_value).unwrap(); + let v = bcs::decode::(&v.args[0]).unwrap(); + assert_eq!(v, 10u64); + } + + fn assert_value_conversion(abi_str: &str, v: V, expected: MoveValue) { + let vm_value = convert_to_move_value(abi_str, json!(v)).unwrap(); + assert_eq!(vm_value, expected); + } + + #[test] + fn test_value_conversion() { + assert_value_conversion("u8", 1i32, MoveValue::U8(1)); + assert_value_conversion("u64", "1", MoveValue::U64(1)); + assert_value_conversion("u128", "1", MoveValue::U128(1)); + assert_value_conversion("bool", true, MoveValue::Bool(true)); + + let address = AccountAddress::from_hex_literal("0x1").unwrap(); + assert_value_conversion("address", "0x1", MoveValue::Address(address)); + + assert_value_conversion("0x1::string::String", "hello", new_vm_utf8_string("hello")); + + assert_value_conversion( + "vector", + "0x0102", + MoveValue::Vector(vec![MoveValue::U8(1), MoveValue::U8(2)]), + ); + assert_value_conversion( + "vector", + ["1", "2"], + MoveValue::Vector(vec![MoveValue::U64(1), MoveValue::U64(2)]), + ); + + assert_value_conversion( + "0x1::guid::ID", // As we do not have access to the module resolver, the types of the struct should be provided as params + ["1", "0x1"], + MoveValue::Struct(MoveStruct::Runtime(vec![ + MoveValue::U64(1), + MoveValue::Address(address), + ])), + ); + } } diff --git a/rust/chains/tw_aptos/tests/signer.rs b/rust/chains/tw_aptos/tests/signer.rs index e69c6894839..61ce6f6deca 100644 --- a/rust/chains/tw_aptos/tests/signer.rs +++ b/rust/chains/tw_aptos/tests/signer.rs @@ -53,6 +53,7 @@ fn setup_proto_transaction<'a>( timestamp: u64, gas_unit_price: u64, any_encoded: &'a str, + abi: &'a str, ops_details: Option, ) -> SigningInput<'a> { let private = hex::decode(keypair_str).unwrap(); @@ -148,6 +149,7 @@ fn setup_proto_transaction<'a>( private_key: private.into(), any_encoded: any_encoded.into(), transaction_payload: payload, + abi: abi.into(), }; input @@ -194,6 +196,7 @@ fn test_aptos_sign_transaction_transfer() { 3664390082, 100, "", + "", Some(OpsDetails::Transfer(Transfer { to: "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30".to_string(), amount: 1000, @@ -237,6 +240,7 @@ fn test_aptos_sign_create_account() { 3664390082, 100, "", + "", Some(OpsDetails::AccountCreation(AccountCreation { to: "0x3aa1672641a4e17b3d913b4c0301e805755a80b12756fc729c5878f12344d30e".to_string(), })), @@ -279,6 +283,7 @@ fn test_aptos_sign_coin_transfer() { 3664390082, 100, "", + "", Some(OpsDetails::TokenTransfer(TokenTransfer { transfer: Transfer { to: "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30" @@ -328,6 +333,7 @@ fn test_implicit_aptos_sign_coin_transfer() { 3664390082, 100, "", + "", Some(OpsDetails::ImplicitTokenTransfer(TokenTransfer { transfer: Transfer { to: "0xb7c7d12080209e9dc14498c80200706e760363fb31782247e82cf57d1d6e5d6c".to_string(), amount: 10000 }, tag: TypeTag::from_str("0xe9c192ff55cffab3963c695cff6dbf9dad6aff2bb5ac19a6415cad26a81860d9::mee_coin::MeeCoin").unwrap() })), ); let output = Signer::sign_proto(input); @@ -368,6 +374,7 @@ fn test_aptos_nft_offer() { 3664390082, 100, "", + "", Some(OpsDetails::NftOps(NftOperation::Offer(Offer { receiver: AccountAddress::from_str( "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", @@ -424,6 +431,7 @@ fn test_aptos_cancel_nft_offer() { 3664390082, 100, "", + "", Some(OpsDetails::NftOps(NftOperation::Cancel(Offer { receiver: AccountAddress::from_str( "0x783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee", @@ -480,6 +488,7 @@ fn test_aptos_nft_claim() { 3664390082, 100, "", + "", Some(OpsDetails::NftOps(NftOperation::Claim(Claim { sender: AccountAddress::from_str( "0x783135e8b00430253a22ba041d860c373d7a1501ccf7ac2d1ad37a8ed2775aee", @@ -534,6 +543,7 @@ fn test_aptos_register_token() { 3664390082, 100, "", + "", Some(OpsDetails::RegisterToken(RegisterToken { coin_type: TypeTag::from_str("0xe4497a32bf4a9fd5601b27661aa0b933a923191bf403bd08669ab2468d43b379::move_coin::MoveCoin").unwrap() })), ); let output = Signer::sign_proto(input); @@ -574,6 +584,7 @@ fn test_aptos_tortuga_stake() { 1670240203, 100, "", + "", Some(OpsDetails::LiquidStakingOps(LiquidStakingOperation::Stake( Stake { amount: 100000000, @@ -624,6 +635,7 @@ fn test_aptos_tortuga_unstake() { 1670304949, 120, "", + "", Some(OpsDetails::LiquidStakingOps( LiquidStakingOperation::Unstake(Unstake { amount: 99178100, @@ -674,6 +686,7 @@ fn test_aptos_tortuga_claim() { 1682066783, 148, "", + "", Some(OpsDetails::LiquidStakingOps(LiquidStakingOperation::Claim( liquid_staking::Claim { idx: 0, @@ -737,6 +750,7 @@ fn test_aptos_blind_sign() { ], "type": "entry_function_payload" }"#, + "", None, ); let output = Signer::sign_proto(input); @@ -772,6 +786,68 @@ fn test_aptos_blind_sign() { }"#); } +// Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x1ee2aa55382bf6b5a9f7a7f2b2066e16979489c6b2868704a2cf2c482f12b5ca/payload?network=mainnet +#[test] +fn test_aptos_blind_sign_with_abi() { + let input = setup_proto_transaction( + "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", // Sender's address + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", // Keypair + "blind_sign_json", + 69, // Sequence number + 1, + 50000, + 1735902711, + 100, + r#"{ + "function": "0x9770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da3::controller::deposit", + "type_arguments": [ + "0x1::aptos_coin::AptosCoin" + ], + "arguments": [ + "0x4d61696e204163636f756e74", + "10000000", + false + ], + "type": "entry_function_payload" + }"#, + r#"[ + "vector", + "u64", + "bool" + ]"#, + None, + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304500000000000000029770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da30a636f6e74726f6c6c6572076465706f736974010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e00030d0c4d61696e204163636f756e74088096980000000000010050c30000000000006400000000000000f7c577670000000001", // Expected raw transaction bytes + "13dcf1636abd31996729ded4d3bf56e9c7869a7188df4f185cbcce42f0dc74b6e1b54d31703ee3babbea2ef72b3338b8c2866cec68cbd761ccc7f80910124304", // Expected signature + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304500000000000000029770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da30a636f6e74726f6c6c6572076465706f736974010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e00030d0c4d61696e204163636f756e74088096980000000000010050c30000000000006400000000000000f7c5776700000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c4013dcf1636abd31996729ded4d3bf56e9c7869a7188df4f185cbcce42f0dc74b6e1b54d31703ee3babbea2ef72b3338b8c2866cec68cbd761ccc7f80910124304", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "1735902711", + "gas_unit_price": "100", + "max_gas_amount": "50000", + "payload": { + "function": "0x9770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da3::controller::deposit", + "type_arguments": [ + "0x1::aptos_coin::AptosCoin" + ], + "arguments": [ + "0x4d61696e204163636f756e74", + "10000000", + false + ], + "type": "entry_function_payload" + }, + "sender": "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + "sequence_number": "69", + "signature": { + "public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", + "signature": "0x13dcf1636abd31996729ded4d3bf56e9c7869a7188df4f185cbcce42f0dc74b6e1b54d31703ee3babbea2ef72b3338b8c2866cec68cbd761ccc7f80910124304", + "type": "ed25519_signature" + } + }"#); +} + // Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x25dca849cb4ebacbff223139f7ad5d24c37c225d9506b8b12a925de70429e685/payload #[test] fn test_aptos_blind_sign_staking() { @@ -792,6 +868,7 @@ fn test_aptos_blind_sign_staking() { ], "type": "entry_function_payload" }"#, + "", None, ); let output = Signer::sign_proto(input); @@ -841,6 +918,7 @@ fn test_aptos_blind_sign_unstaking() { ], "type": "entry_function_payload" }"#, + "", None, ); let output = Signer::sign_proto(input); diff --git a/src/proto/Aptos.proto b/src/proto/Aptos.proto index 6323e2e16d1..f900064aeaa 100644 --- a/src/proto/Aptos.proto +++ b/src/proto/Aptos.proto @@ -166,6 +166,8 @@ message SigningInput { LiquidStaking liquid_staking_message = 14; TokenTransferCoinsMessage token_transfer_coins = 15; } + + string abi = 21; } // Information related to the signed transaction diff --git a/swift/Tests/Blockchains/AptosTests.swift b/swift/Tests/Blockchains/AptosTests.swift index 335b02aad87..8b5739ed690 100644 --- a/swift/Tests/Blockchains/AptosTests.swift +++ b/swift/Tests/Blockchains/AptosTests.swift @@ -53,6 +53,49 @@ class AptosTests: XCTestCase { XCTAssertEqual(output.authenticator.signature.hexString, expectedSignature) XCTAssertEqual(output.encoded.hexString, expectedSignedTx) } + + func testBlindSignWithABI() { + // Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x1ee2aa55382bf6b5a9f7a7f2b2066e16979489c6b2868704a2cf2c482f12b5ca/payload?network=mainnet + let payloadJson = """ + { + "function": "0x9770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da3::controller::deposit", + "type_arguments": [ + "0x1::aptos_coin::AptosCoin" + ], + "arguments": [ + "0x4d61696e204163636f756e74", + "10000000", + false + ], + "type": "entry_function_payload" + } + """ + let privateKeyData = Data(hexString: "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec")! + let input = AptosSigningInput.with { + $0.chainID = 1 + $0.anyEncoded = payloadJson + $0.expirationTimestampSecs = 1735902711 + $0.gasUnitPrice = 100 + $0.maxGasAmount = 50000 + $0.sequenceNumber = 69 + $0.sender = "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30" + $0.privateKey = privateKeyData + $0.abi = """ + [ + "vector", + "u64", + "bool" + ] + """ + } + let output: AptosSigningOutput = AnySigner.sign(input: input, coin: .aptos) + let expectedRawTx = "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304500000000000000029770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da30a636f6e74726f6c6c6572076465706f736974010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e00030d0c4d61696e204163636f756e74088096980000000000010050c30000000000006400000000000000f7c577670000000001" + let expectedSignature = "13dcf1636abd31996729ded4d3bf56e9c7869a7188df4f185cbcce42f0dc74b6e1b54d31703ee3babbea2ef72b3338b8c2866cec68cbd761ccc7f80910124304" + let expectedSignedTx = "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304500000000000000029770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da30a636f6e74726f6c6c6572076465706f736974010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e00030d0c4d61696e204163636f756e74088096980000000000010050c30000000000006400000000000000f7c5776700000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c4013dcf1636abd31996729ded4d3bf56e9c7869a7188df4f185cbcce42f0dc74b6e1b54d31703ee3babbea2ef72b3338b8c2866cec68cbd761ccc7f80910124304" + XCTAssertEqual(output.rawTxn.hexString, expectedRawTx) + XCTAssertEqual(output.authenticator.signature.hexString, expectedSignature) + XCTAssertEqual(output.encoded.hexString, expectedSignedTx) + } func testSign() { // Successfully broadcasted https://explorer.aptoslabs.com/txn/0xb4d62afd3862116e060dd6ad9848ccb50c2bc177799819f1d29c059ae2042467?network=devnet diff --git a/tests/chains/Aptos/TWAnySignerTests.cpp b/tests/chains/Aptos/TWAnySignerTests.cpp index dce9c2f3d72..b0ad67d6d8c 100644 --- a/tests/chains/Aptos/TWAnySignerTests.cpp +++ b/tests/chains/Aptos/TWAnySignerTests.cpp @@ -57,4 +57,41 @@ TEST(TWAnySignerAptos, TxSign) { assertJSONEqual(expectedJson, parsedJson); } +TEST(TWAnySignerAptos, TxSignWithABI) { + // Successfully broadcasted https://explorer.aptoslabs.com/txn/0x1ee2aa55382bf6b5a9f7a7f2b2066e16979489c6b2868704a2cf2c482f12b5ca/payload?network=mainnet + Proto::SigningInput input; + input.set_sender("0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30"); + input.set_sequence_number(69); + input.set_max_gas_amount(50000); + input.set_gas_unit_price(100); + input.set_expiration_timestamp_secs(1735902711); + input.set_chain_id(1); + input.set_any_encoded(R"( + { + "function": "0x9770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da3::controller::deposit", + "type_arguments": [ + "0x1::aptos_coin::AptosCoin" + ], + "arguments": [ + "0x4d61696e204163636f756e74", + "10000000", + false + ], + "type": "entry_function_payload" + } + )"); + input.set_abi(R"([ + "vector", + "u64", + "bool" + ])"); + auto privateKey = PrivateKey(parse_hex("5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec")); + input.set_private_key(privateKey.bytes.data(), privateKey.bytes.size()); + Proto::SigningOutput output; + ANY_SIGN(input, TWCoinTypeAptos); + ASSERT_EQ(hex(output.raw_txn()), "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304500000000000000029770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da30a636f6e74726f6c6c6572076465706f736974010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e00030d0c4d61696e204163636f756e74088096980000000000010050c30000000000006400000000000000f7c577670000000001"); + ASSERT_EQ(hex(output.authenticator().signature()), "13dcf1636abd31996729ded4d3bf56e9c7869a7188df4f185cbcce42f0dc74b6e1b54d31703ee3babbea2ef72b3338b8c2866cec68cbd761ccc7f80910124304"); + ASSERT_EQ(hex(output.encoded()), "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304500000000000000029770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da30a636f6e74726f6c6c6572076465706f736974010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e00030d0c4d61696e204163636f756e74088096980000000000010050c30000000000006400000000000000f7c5776700000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c4013dcf1636abd31996729ded4d3bf56e9c7869a7188df4f185cbcce42f0dc74b6e1b54d31703ee3babbea2ef72b3338b8c2866cec68cbd761ccc7f80910124304"); +} + } // namespace TW::Aptos::tests