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 2c544bdbd9a..d4b0bc016d4 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 @@ -197,4 +197,41 @@ class TestAptosSigner { "1869b853768f0ba935d67f837a66b172dd39a60ca2315f8d4e0e669bbd35cf2502000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e730107e9c192ff55cffab3963c695cff6dbf9dad6aff2bb5ac19a6415cad26a81860d9086d65655f636f696e074d6565436f696e000220b7c7d12080209e9dc14498c80200706e760363fb31782247e82cf57d1d6e5d6c081027000000000000d0070000000000006400000000000000c2276ada0000000001002062e7a6a486553b56a53e89dfae3f780693e537e5b0a7ed33290780e581ca83694030ebd7e95cb464677f411868e2cbfcb22bc01cc63cded36c459dff45e6d2f1354ae4e090e7dfbb509851c0368b343e0e5ecaf6b08e7c1b94c186530b0f7dee0d" ) } + + @Test + fun AptosFungibleAssetTransfer() { + // Successfully broadcasted https://explorer.aptoslabs.com/txn/0x475fc97bcba87907166a720676e1b2f5320e613fd13014df37dcf17b09ff0e98/balanceChange?network=mainnet + val key = + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec".toHexBytesInByteString() + + val fungibleAssetTransferMessage = Aptos.FungibleAssetTransferMessage.newBuilder() + .setAmount(100000000) + .setTo("0x2d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e52") + .setMetadataAddress("0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12") + .build() + val signingInput = Aptos.SigningInput.newBuilder() + .setChainId(1) + .setSender("0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30") + .setSequenceNumber(74) + .setGasUnitPrice(100) + .setMaxGasAmount(20) + .setExpirationTimestampSecs(1736060099) + .setFungibleAssetTransfer(fungibleAssetTransferMessage) + .setPrivateKey(key) + .build() + + val result = AnySigner.sign(signingInput, CoinType.APTOS, Aptos.SigningOutput.parser()) + assertEquals( + Numeric.cleanHexPrefix(Numeric.toHexString(result.rawTxn.toByteArray())), + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304a00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d657461646174610003202ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12202d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e520800e1f5050000000014000000000000006400000000000000c32c7a670000000001" + ) + assertEquals( + Numeric.cleanHexPrefix(Numeric.toHexString(result.authenticator.signature.toByteArray())), + "2d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c" + ) + assertEquals( + Numeric.cleanHexPrefix(Numeric.toHexString(result.encoded.toByteArray())), + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304a00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d657461646174610003202ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12202d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e520800e1f5050000000014000000000000006400000000000000c32c7a6700000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c402d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c" + ) + } } diff --git a/rust/chains/tw_aptos/src/aptos_move_packages.rs b/rust/chains/tw_aptos/src/aptos_move_packages.rs index e90e6bf2a4b..67b471c2640 100644 --- a/rust/chains/tw_aptos/src/aptos_move_packages.rs +++ b/rust/chains/tw_aptos/src/aptos_move_packages.rs @@ -2,6 +2,8 @@ // // Copyright © 2017 Trust Wallet. +use std::str::FromStr; + use crate::transaction_payload::{EntryFunction, TransactionPayload}; use move_core_types::account_address::AccountAddress; use move_core_types::ident_str; @@ -205,3 +207,32 @@ pub fn managed_coin_register(coin_type: TypeTag) -> TransactionPayload { json!([]), )) } + +pub fn fungible_asset_transfer( + metadata_address: AccountAddress, + to: AccountAddress, + amount: u64, +) -> SigningResult { + Ok(TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("primary_fungible_store").to_owned(), + ), + ident_str!("transfer").to_owned(), + vec![TypeTag::from_str("0x1::fungible_asset::Metadata") + .tw_err(|_| SigningErrorType::Error_internal)?], + vec![ + bcs::encode(&metadata_address)?, + bcs::encode(&to)?, + bcs::encode(&amount)?, + ], + json!([ + metadata_address.to_hex_literal(), + to.to_hex_literal(), + amount.to_string() + ]), + ))) +} diff --git a/rust/chains/tw_aptos/src/transaction_builder.rs b/rust/chains/tw_aptos/src/transaction_builder.rs index 4ccc8d9b1b4..5449f86608d 100644 --- a/rust/chains/tw_aptos/src/transaction_builder.rs +++ b/rust/chains/tw_aptos/src/transaction_builder.rs @@ -5,8 +5,9 @@ use crate::address::from_account_error; use crate::aptos_move_packages::{ aptos_account_create_account, aptos_account_transfer, aptos_account_transfer_coins, - coin_transfer, managed_coin_register, token_transfers_cancel_offer_script, - token_transfers_claim_script, token_transfers_offer_script, + coin_transfer, fungible_asset_transfer, managed_coin_register, + token_transfers_cancel_offer_script, token_transfers_claim_script, + token_transfers_offer_script, }; use crate::constants::{GAS_UNIT_PRICE, MAX_GAS_AMOUNT}; use crate::liquid_staking::{ @@ -147,6 +148,18 @@ impl TransactionFactory { convert_proto_struct_tag_to_type_tag(func)?, ) }, + OneOftransaction_payload::fungible_asset_transfer(fungible_asset_transfer) => factory + .fungible_asset_transfer( + AccountAddress::from_str(&fungible_asset_transfer.metadata_address) + .map_err(from_account_error) + .into_tw() + .context("Invalid metadata address")?, + AccountAddress::from_str(&fungible_asset_transfer.to) + .map_err(from_account_error) + .into_tw() + .context("Invalid destination address")?, + fungible_asset_transfer.amount, + ), OneOftransaction_payload::None => { let is_blind_sign = !input.any_encoded.is_empty(); let v = serde_json::from_str::(&input.any_encoded) @@ -253,6 +266,15 @@ impl TransactionFactory { Ok(self.payload(coin_transfer(coin_type, to, amount)?)) } + pub fn fungible_asset_transfer( + &self, + metadata_address: AccountAddress, + to: AccountAddress, + amount: u64, + ) -> SigningResult { + Ok(self.payload(fungible_asset_transfer(metadata_address, to, amount)?)) + } + pub fn implicitly_create_user_and_coins_transfer( &self, to: AccountAddress, diff --git a/rust/chains/tw_aptos/tests/signer.rs b/rust/chains/tw_aptos/tests/signer.rs index 61ce6f6deca..eb4f41c8232 100644 --- a/rust/chains/tw_aptos/tests/signer.rs +++ b/rust/chains/tw_aptos/tests/signer.rs @@ -29,6 +29,12 @@ pub struct TokenTransfer { tag: TypeTag, } +pub struct FungibleAssetTransfer { + metadata_address: String, + to: String, + amount: u64, +} + pub struct RegisterToken { coin_type: TypeTag, } @@ -41,6 +47,7 @@ pub enum OpsDetails { TokenTransfer(TokenTransfer), ImplicitTokenTransfer(TokenTransfer), NftOps(NftOperation), + FungibleAssetTransfer(FungibleAssetTransfer), } fn setup_proto_transaction<'a>( @@ -135,6 +142,20 @@ fn setup_proto_transaction<'a>( panic!("Unsupported arguments") } }, + "fungible_asset_transfer" => { + if let OpsDetails::FungibleAssetTransfer(fungible_asset_transfer) = ops_details.unwrap() + { + Proto::mod_SigningInput::OneOftransaction_payload::fungible_asset_transfer( + Proto::FungibleAssetTransferMessage { + to: fungible_asset_transfer.to.into(), + amount: fungible_asset_transfer.amount, + metadata_address: fungible_asset_transfer.metadata_address.into(), + }, + ) + } else { + panic!("Unsupported arguments") + } + }, "blind_sign_json" => Proto::mod_SigningInput::OneOftransaction_payload::None, _ => Proto::mod_SigningInput::OneOftransaction_payload::None, }; @@ -321,6 +342,52 @@ fn test_aptos_sign_coin_transfer() { }"#); } +// Successfully broadcasted https://explorer.aptoslabs.com/txn/0x475fc97bcba87907166a720676e1b2f5320e613fd13014df37dcf17b09ff0e98/balanceChange?network=mainnet +#[test] +fn test_aptos_sign_fungible_asset_transfer() { + let input = setup_proto_transaction( + "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", // Sender's address + "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", // Keypair + "fungible_asset_transfer", + 74, // Sequence number + 1, + 20, + 1736060099, + 100, + "", + "", + Some(OpsDetails::FungibleAssetTransfer(FungibleAssetTransfer { + metadata_address: "0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12" + .to_string(), + to: "0x2d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e52".to_string(), + amount: 100000000, + })), + ); + let output = Signer::sign_proto(input); + test_tx_result(output, + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304a00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d657461646174610003202ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12202d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e520800e1f5050000000014000000000000006400000000000000c32c7a670000000001", // Expected raw transaction bytes + "2d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c", // Expected signature + "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304a00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d657461646174610003202ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12202d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e520800e1f5050000000014000000000000006400000000000000c32c7a6700000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c402d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c", // Expected encoded transaction + r#"{ + "expiration_timestamp_secs": "1736060099", + "gas_unit_price": "100", + "max_gas_amount": "20", + "payload": { + "arguments": ["0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12","0x2d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e52", "100000000"], + "function": "0x1::primary_fungible_store::transfer", + "type": "entry_function_payload", + "type_arguments": ["0x1::fungible_asset::Metadata"] + }, + "sender": "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + "sequence_number": "74", + "signature": { + "public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", + "signature": "0x2d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c", + "type": "ed25519_signature" + } + }"#); +} + // Successfully broadcasted https://explorer.aptoslabs.com/txn/0x197d40ea12e2bfc65a0a913b9f4ca3b0b0208fe0c1514d3d55cef3d5bcf25211?network=mainnet #[test] fn test_implicit_aptos_sign_coin_transfer() { diff --git a/src/proto/Aptos.proto b/src/proto/Aptos.proto index f900064aeaa..db2baac9d5d 100644 --- a/src/proto/Aptos.proto +++ b/src/proto/Aptos.proto @@ -48,6 +48,15 @@ message TokenTransferCoinsMessage { StructTag function = 3; } +message FungibleAssetTransferMessage { + // Fungible Asset address (string) + string metadata_address = 1; + // Destination Account address (string) + string to = 2; + // Amount to be transferred (uint64) + uint64 amount = 3; +} + // Necessary fields to process a ManagedTokensRegisterMessage message ManagedTokensRegisterMessage { // token function to register, e.g BTC: 0x43417434fd869edee76cca2a4d2301e528a1551b1d719b75c350c3c97d15b8b9::coins::BTC @@ -165,6 +174,7 @@ message SigningInput { ManagedTokensRegisterMessage register_token = 13; LiquidStaking liquid_staking_message = 14; TokenTransferCoinsMessage token_transfer_coins = 15; + FungibleAssetTransferMessage fungible_asset_transfer = 16; } string abi = 21; diff --git a/swift/Tests/Blockchains/AptosTests.swift b/swift/Tests/Blockchains/AptosTests.swift index 8b5739ed690..7a7cbf4c8f8 100644 --- a/swift/Tests/Blockchains/AptosTests.swift +++ b/swift/Tests/Blockchains/AptosTests.swift @@ -122,4 +122,31 @@ class AptosTests: XCTestCase { XCTAssertEqual(output.authenticator.signature.hexString, expectedSignature) XCTAssertEqual(output.encoded.hexString, expectedSignedTx) } + + func testSignFungibleAssetTransfer() { + // Successfully broadcasted https://explorer.aptoslabs.com/txn/0x475fc97bcba87907166a720676e1b2f5320e613fd13014df37dcf17b09ff0e98/balanceChange?network=mainnet + let privateKeyData = Data(hexString: "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec")! + let fungibleAssetTransferMsg = AptosFungibleAssetTransferMessage.with { + $0.metadataAddress = "0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12" + $0.to = "0x2d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e52" + $0.amount = 100000000 + } + let input = AptosSigningInput.with { + $0.chainID = 1 + $0.sender = "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30" + $0.expirationTimestampSecs = 1736060099 + $0.gasUnitPrice = 100 + $0.maxGasAmount = 20 + $0.sequenceNumber = 74 + $0.fungibleAssetTransfer = fungibleAssetTransferMsg + $0.privateKey = privateKeyData + } + let output: AptosSigningOutput = AnySigner.sign(input: input, coin: .aptos) + let expectedRawTx = "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304a00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d657461646174610003202ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12202d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e520800e1f5050000000014000000000000006400000000000000c32c7a670000000001" + let expectedSignature = "2d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c" + let expectedSignedTx = "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304a00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d657461646174610003202ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12202d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e520800e1f5050000000014000000000000006400000000000000c32c7a6700000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c402d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c" + XCTAssertEqual(output.rawTxn.hexString, expectedRawTx) + XCTAssertEqual(output.authenticator.signature.hexString, expectedSignature) + XCTAssertEqual(output.encoded.hexString, expectedSignedTx) + } }