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