diff --git a/docs/registry.md b/docs/registry.md index 1473aa7b5ca..8a109c526d2 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -131,7 +131,7 @@ This list is generated from [./registry.json](../registry.json) | 10004689 | IoTeX EVM | IOTX | | | | 10007000 | NativeZetaChain | ZETA | | | | 10007700 | NativeCanto | CANTO | | | -| 10008217 | Kaia | KAIA | | | +| 10008217 | Kaia | KAIA | | | | 10009000 | Avalanche C-Chain | AVAX | | | | 10009001 | Evmos | EVMOS | | | | 10042170 | Arbitrum Nova | ETH | | | diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 0c16a96d581..93b76802ab9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1724,6 +1724,7 @@ dependencies = [ name = "tw_aptos" version = "0.1.0" dependencies = [ + "anyhow", "move-core-types", "serde", "serde_bytes", diff --git a/rust/chains/tw_aptos/Cargo.toml b/rust/chains/tw_aptos/Cargo.toml index d78b0e3c644..ae46405cccd 100644 --- a/rust/chains/tw_aptos/Cargo.toml +++ b/rust/chains/tw_aptos/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1" serde_json = "1.0" tw_coin_entry = { path = "../../tw_coin_entry" } tw_encoding = { path = "../../tw_encoding" } 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..a49f0356e7c --- /dev/null +++ b/rust/chains/tw_aptos/src/aptos_move_types.rs @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use anyhow::format_err; +use move_core_types::{language_storage::TypeTag, parser::parse_type_tag}; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; +use std::str::FromStr; +use tw_encoding::hex; + +/// Hex encoded bytes to allow for having bytes represented in JSON +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HexEncodedBytes(pub Vec); + +impl HexEncodedBytes { + pub fn json(&self) -> anyhow::Result { + Ok(serde_json::to_value(self)?) + } +} + +impl FromStr for HexEncodedBytes { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let hex_str = if let Some(hex) = s.strip_prefix("0x") { + hex + } else { + s + }; + Ok(Self(hex::decode(hex_str).map_err(|e| { + format_err!( + "decode hex-encoded string({:?}) failed, caused by error: {}", + s, + e + ) + })?)) + } +} + +impl fmt::Display for HexEncodedBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x{}", hex::encode(&self.0, false))?; + Ok(()) + } +} + +impl Serialize for HexEncodedBytes { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for HexEncodedBytes { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = ::deserialize(deserializer)?; + s.parse().map_err(D::Error::custom) + } +} + +impl From> for HexEncodedBytes { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl From for Vec { + fn from(bytes: HexEncodedBytes) -> Self { + bytes.0 + } +} + +/// 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, + /// A Vector of [`MoveType`] + Vector { items: Box }, + /// 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 = anyhow::Error; + + 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::Vector(v) => MoveType::Vector { + items: Box::new(MoveType::from(*v)), + }, + _ => MoveType::Unparsable(tag.to_string()), + } + } +} + +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::Vector(v) => MoveType::Vector { + items: Box::new(MoveType::from(v.as_ref())), + }, + _ => MoveType::Unparsable(tag.to_string()), + } + } +} + +impl TryFrom for TypeTag { + type Error = anyhow::Error; + + fn try_from(tag: MoveType) -> anyhow::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::Vector { items } => TypeTag::Vector(Box::new((*items).try_into()?)), + _ => { + return Err(anyhow::anyhow!( + "Invalid move type for converting into `TypeTag`: {:?}", + &tag + )) + }, + }; + Ok(ret) + } +} 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..ffc44214819 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::try_from((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..2e4c9be5cb1 100644 --- a/rust/chains/tw_aptos/src/transaction_payload.rs +++ b/rust/chains/tw_aptos/src/transaction_payload.rs @@ -2,11 +2,14 @@ // // Copyright © 2017 Trust Wallet. +use crate::aptos_move_types::{HexEncodedBytes, MoveType}; 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::parser::parse_transaction_argument; use move_core_types::transaction_argument::TransactionArgument; +use move_core_types::u256; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::default::Default; @@ -53,26 +56,39 @@ pub struct EntryFunction { json_args: Value, } -impl TryFrom for EntryFunction { +impl TryFrom<(Value, Value)> for EntryFunction { type Error = EntryFunctionError; - fn try_from(value: Value) -> EntryFunctionResult { + fn try_from((value, abi): (Value, 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)?; + let arg_str = arg_str.trim_start_matches('"').trim_end_matches('"'); + + let arg = if let Some(abi_str) = get_abi_str(index) { + let abi_str = abi_str.trim_start_matches('"').trim_end_matches('"'); + parse_argument(arg_str, abi_str) + .map_err(|_| EntryFunctionError::InvalidArguments)? + } else { + parse_transaction_argument(arg_str) + .map_err(|_| EntryFunctionError::InvalidArguments)? + }; serialize_argument(&arg).map_err(EntryFunctionError::from) }) .collect::>>()?; @@ -99,6 +115,41 @@ impl TryFrom for EntryFunction { } } +fn parse_argument(val: &str, abi_str: &str) -> anyhow::Result { + let move_type: MoveType = abi_str.parse::()?; + Ok(match move_type { + MoveType::Bool => TransactionArgument::Bool(val.parse::()?), + MoveType::U8 => TransactionArgument::U8(val.parse::()?), + MoveType::U16 => TransactionArgument::U16(val.parse::()?), + MoveType::U32 => TransactionArgument::U32(val.parse::()?), + MoveType::U64 => TransactionArgument::U64(val.parse::()?), + MoveType::U128 => TransactionArgument::U128(val.parse::()?), + MoveType::U256 => TransactionArgument::U256(val.parse::()?), + MoveType::Address => TransactionArgument::Address(AccountAddress::from_hex_literal(val)?), + MoveType::Vector { items } => parse_vector_argument(val, items)?, + _ => { + anyhow::bail!("unexpected move type {:?} for value {:?}", move_type, val) + }, + }) +} + +fn parse_vector_argument(val: &str, layout: Box) -> anyhow::Result { + let val = serde_json::to_value(val)?; + if matches!(*layout, MoveType::U8) { + Ok(TransactionArgument::U8Vector( + serde_json::from_value::(val)?.into(), + )) + } else if let Value::Array(list) = val { + let vals = list + .into_iter() + .map(|v| serde_json::from_value::(v).map_err(|_| anyhow::anyhow!("expected u8"))) + .collect::>()?; + Ok(TransactionArgument::U8Vector(vals)) + } else { + anyhow::bail!("expected vector<{:?}>, but got: {:?}", layout, val) + } +} + fn serialize_argument(arg: &TransactionArgument) -> EncodingResult { match arg { TransactionArgument::U8(v) => bcs::encode(v), diff --git a/rust/chains/tw_aptos/tests/signer.rs b/rust/chains/tw_aptos/tests/signer.rs index e69c6894839..2f2b8201ba2 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,67 @@ fn test_aptos_blind_sign() { }"#); } +#[test] +fn test_aptos_blind_sign_with_abi() { + let input = setup_proto_transaction( + "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", // Sender's address + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", // Keypair + "blind_sign_json", + 42, // Sequence number + 1, + 100011, + 3664390082, + 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, + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f302a00000000000000029770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da30a636f6e74726f6c6c6572076465706f736974010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e00030d0c4d61696e204163636f756e740880969800000000000100ab860100000000006400000000000000c2276ada0000000001", // Expected raw transaction bytes + "b2f8b30d244d4c4127db00d81bac14586e5e8be4de2d273a03ea6109f4944af9016472b4a75da0d8adc505233a76a0de2e322a63ffbb8bf4a60eb9acab33e20b", // Expected signature + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f302a00000000000000029770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da30a636f6e74726f6c6c6572076465706f736974010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e00030d0c4d61696e204163636f756e740880969800000000000100ab860100000000006400000000000000c2276ada00000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c40b2f8b30d244d4c4127db00d81bac14586e5e8be4de2d273a03ea6109f4944af9016472b4a75da0d8adc505233a76a0de2e322a63ffbb8bf4a60eb9acab33e20b", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "3664390082", + "gas_unit_price": "100", + "max_gas_amount": "100011", + "payload": { + "function": "0x9770fa9c725cbd97eb50b2be5f7416efdfd1f1554beb0750d4dae4c64e860da3::controller::deposit", + "type_arguments": [ + "0x1::aptos_coin::AptosCoin" + ], + "arguments": [ + "0x4d61696e204163636f756e74", + "10000000", + false + ], + "type": "entry_function_payload" + }, + "sender": "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + "sequence_number": "42", + "signature": { + "public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", + "signature": "0xb2f8b30d244d4c4127db00d81bac14586e5e8be4de2d273a03ea6109f4944af9016472b4a75da0d8adc505233a76a0de2e322a63ffbb8bf4a60eb9acab33e20b", + "type": "ed25519_signature" + } + }"#); +} + // Successfully broadcasted: https://explorer.aptoslabs.com/txn/0x25dca849cb4ebacbff223139f7ad5d24c37c225d9506b8b12a925de70429e685/payload #[test] fn test_aptos_blind_sign_staking() { @@ -792,6 +867,7 @@ fn test_aptos_blind_sign_staking() { ], "type": "entry_function_payload" }"#, + "", None, ); let output = Signer::sign_proto(input); @@ -841,6 +917,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..4f9dc63c048 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; } + + optional string abi = 16; } // Information related to the signed transaction