From b37b7809c619a97a826747a73295a07e8f4c9417 Mon Sep 17 00:00:00 2001 From: 10gic Date: Mon, 16 Dec 2024 16:52:31 +0800 Subject: [PATCH] [Solana]: Support specifying fee payer for an encoded transaction (#4156) * Support specifying fee payer for an encoded transaction * Add some comments * Exports solana setFeePayer function to kotlin/swift code * Fix swift test case * Fix swift test case --- .../solana/TestSolanaTransaction.kt | 15 ++++ include/TrustWalletCore/TWSolanaTransaction.h | 12 ++- .../src/modules/insert_instruction.rs | 31 +++++++ rust/chains/tw_solana/src/modules/utils.rs | 15 ++++ .../chains/solana/solana_transaction_ffi.rs | 87 ++++++++++++++++++- .../src/ffi/solana/transaction.rs | 23 +++++ src/interface/TWSolanaTransaction.cpp | 17 ++++ swift/Tests/Blockchains/SolanaTests.swift | 48 ++++++++++ tests/chains/Solana/TWSolanaTransaction.cpp | 54 ++++++++++++ 9 files changed, 297 insertions(+), 5 deletions(-) diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaTransaction.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaTransaction.kt index a7f1ca53348..535970c9957 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaTransaction.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/solana/TestSolanaTransaction.kt @@ -118,4 +118,19 @@ class TestSolanaTransaction { val expectedString = "AVUye82Mv+/aWeU2G+B6Nes365mUU2m8iqcGZn/8kFJvw4wY6AgKGG+vJHaknHlCDwE1yi1SIMVUUtNCOm3kHg8BAAIEODI+iWe7g68B9iwCy8bFkJKvsIEj350oSOpcv4gNnv/st+6qmqipl9lwMK6toB9TiL7LrJVfij+pKwr+pUKxfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAboZ09wD3Y5HNUl7aN8bwpCqowev1hMu7fSgziLswUSgMDAAUCECcAAAICAAEMAgAAAOgDAAAAAAAAAwAJA+gDAAAAAAAA" assertEquals(output.encoded, expectedString) } + + @Test + fun testSetFeePayer() { + val originalTx = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQABA2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQICAAEMAgAAAACcnwYAAAAAAA==" + + // Step 1 - Add fee payer to the transaction. + val updatedTx = SolanaTransaction.setFeePayer(originalTx, "Eg5jqooyG6ySaXKbQUu4Lpvu2SqUPZrNkM4zXs9iUDLJ") + assertEquals(updatedTx, "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAIAAQTLKvCJtWpVdze8Fxjgy/Iyz1sC4U7gqnxmdSM/X2+bV2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQMCAQIMAgAAAACcnwYAAAAAAA==") + + // This case originates from a test case in C++. Here, only the most critical function is verified for correctness, + // while the remaining steps have been omitted. + // Step 2 - Decode transaction into a `RawMessage` Protobuf. + // Step 3 - Obtain preimage hash. + // Step 4 - Compile transaction info. + } } \ No newline at end of file diff --git a/include/TrustWalletCore/TWSolanaTransaction.h b/include/TrustWalletCore/TWSolanaTransaction.h index 871393fc840..420a84ca7b6 100644 --- a/include/TrustWalletCore/TWSolanaTransaction.h +++ b/include/TrustWalletCore/TWSolanaTransaction.h @@ -50,7 +50,7 @@ TWString *_Nullable TWSolanaTransactionGetComputeUnitLimit(TWString *_Nonnull en /// and returns the updated transaction. /// /// \param encodedTx base64 encoded Solana transaction. -/// \price Unit Price as a decimal string. +/// \param price Unit Price as a decimal string. /// \return base64 encoded Solana transaction. Null if an error occurred. TW_EXPORT_STATIC_METHOD TWString *_Nullable TWSolanaTransactionSetComputeUnitPrice(TWString *_Nonnull encodedTx, TWString *_Nonnull price); @@ -59,9 +59,17 @@ TWString *_Nullable TWSolanaTransactionSetComputeUnitPrice(TWString *_Nonnull en /// and returns the updated transaction. /// /// \param encodedTx base64 encoded Solana transaction. -/// \limit Unit Limit as a decimal string. +/// \param limit Unit Limit as a decimal string. /// \return base64 encoded Solana transaction. Null if an error occurred. TW_EXPORT_STATIC_METHOD TWString *_Nullable TWSolanaTransactionSetComputeUnitLimit(TWString *_Nonnull encodedTx, TWString *_Nonnull limit); +/// Adds fee payer to the given transaction and returns the updated transaction. +/// +/// \param encodedTx base64 encoded Solana transaction. +/// \param feePayer fee payer account address. Must be a base58 encoded public key. It must NOT be in the account list yet. +/// \return base64 encoded Solana transaction. Null if an error occurred. +TW_EXPORT_STATIC_METHOD +TWString *_Nullable TWSolanaTransactionSetFeePayer(TWString *_Nonnull encodedTx, TWString *_Nonnull feePayer); + TW_EXTERN_C_END diff --git a/rust/chains/tw_solana/src/modules/insert_instruction.rs b/rust/chains/tw_solana/src/modules/insert_instruction.rs index 22643e349ce..36c1c583de3 100644 --- a/rust/chains/tw_solana/src/modules/insert_instruction.rs +++ b/rust/chains/tw_solana/src/modules/insert_instruction.rs @@ -91,6 +91,37 @@ pub trait InsertInstruction { Ok(account_added_at) } + /// Adds a fee payer account to the message. + /// Note: The fee payer must NOT be in the account list yet. + fn set_fee_payer(&mut self, account: SolanaAddress) -> SigningResult<()> { + if self.account_keys_mut().contains(&account) { + // For security reasons, we don't allow adding a fee payer if it's already in the account list. + // + // If the fee payer is already in the transaction and there is a malicious instruction to + // transfer tokens from the fee payer to another account, The fee payer may have inadvertently + // signed off on such transactions, which is not what they would expect. + // + // Such examples may be difficult to exploit, but we still took precautionary measures to prohibit + // the new fee payer from appearing in the account list of the transaction out of caution + return SigningError::err(SigningErrorType::Error_internal) + .context("Fee payer account is already in the account list"); + } + + // Insert the fee payer account at the beginning of the account list. + self.account_keys_mut().insert(0, account); + self.message_header_mut().num_required_signatures += 1; + + // Update `program id indexes` and `account id indexes` in every instruction as we inserted the account at the beginning of the list. + self.instructions_mut().iter_mut().for_each(|ix| { + ix.program_id_index += 1; // Update `program id indexes` + ix.accounts + .iter_mut() + .for_each(|account_id| *account_id += 1); // Update `account id indexes` + }); + + Ok(()) + } + /// Returns ALT (Address Lookup Tables) if supported by the message version. fn address_table_lookups(&self) -> Option<&[MessageAddressTableLookup]>; diff --git a/rust/chains/tw_solana/src/modules/utils.rs b/rust/chains/tw_solana/src/modules/utils.rs index da19b9d17d8..0d7f61dd8a3 100644 --- a/rust/chains/tw_solana/src/modules/utils.rs +++ b/rust/chains/tw_solana/src/modules/utils.rs @@ -2,6 +2,7 @@ // // Copyright © 2017 Trust Wallet. +use crate::address::SolanaAddress; use crate::defined_addresses::{COMPUTE_BUDGET_ADDRESS, SYSTEM_PROGRAM_ID_ADDRESS}; use crate::modules::insert_instruction::InsertInstruction; use crate::modules::instruction_builder::compute_budget_instruction::{ @@ -158,6 +159,20 @@ impl SolanaTransaction { tx.to_base64().tw_err(|_| SigningErrorType::Error_internal) } + + pub fn set_fee_payer(encoded_tx: &str, fee_payer: SolanaAddress) -> SigningResult { + let tx_bytes = base64::decode(encoded_tx, STANDARD)?; + let mut tx: VersionedTransaction = + bincode::deserialize(&tx_bytes).map_err(|_| SigningErrorType::Error_input_parse)?; + + tx.message.set_fee_payer(fee_payer)?; + + // Set the correct number of zero signatures + let unsigned_tx = VersionedTransaction::unsigned(tx.message); + unsigned_tx + .to_base64() + .tw_err(|_| SigningErrorType::Error_internal) + } } fn try_instruction_as_compute_budget( diff --git a/rust/tw_tests/tests/chains/solana/solana_transaction_ffi.rs b/rust/tw_tests/tests/chains/solana/solana_transaction_ffi.rs index a9a31f80da0..f0bd2c55622 100644 --- a/rust/tw_tests/tests/chains/solana/solana_transaction_ffi.rs +++ b/rust/tw_tests/tests/chains/solana/solana_transaction_ffi.rs @@ -2,11 +2,11 @@ // // Copyright © 2017 Trust Wallet. -use tw_any_coin::test_utils::sign_utils::AnySignerHelper; +use tw_any_coin::test_utils::sign_utils::{AnySignerHelper, CompilerHelper, PreImageHelper}; use tw_any_coin::test_utils::transaction_decode_utils::TransactionDecoderHelper; use tw_coin_registry::coin_type::CoinType; use tw_encoding::base64::STANDARD; -use tw_encoding::hex::DecodeHex; +use tw_encoding::hex::{DecodeHex, ToHex}; use tw_encoding::{base58, base64}; use tw_memory::test_utils::tw_data_helper::TWDataHelper; use tw_memory::test_utils::tw_data_vector_helper::TWDataVectorHelper; @@ -17,7 +17,7 @@ use tw_solana::SOLANA_ALPHABET; use wallet_core_rs::ffi::solana::transaction::{ tw_solana_transaction_get_compute_unit_limit, tw_solana_transaction_get_compute_unit_price, tw_solana_transaction_set_compute_unit_limit, tw_solana_transaction_set_compute_unit_price, - tw_solana_transaction_update_blockhash_and_sign, + tw_solana_transaction_set_fee_payer, tw_solana_transaction_update_blockhash_and_sign, }; #[test] @@ -283,3 +283,84 @@ fn test_solana_transaction_set_priority_fee_transfer_with_address_lookup() { signature: "4vkDYvXnAyauDwgQUT9pjhvArCm1jZZFp6xFiT6SYKDHwabPNyNskzzd8YJZR4UJVXakBtRAFku3axVQoA7Apido", }); } + +#[test] +fn test_solana_transaction_set_fee_payer() { + let encoded_tx_str = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQABA2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQICAAEMAgAAAACcnwYAAAAAAA=="; + let encoded_tx = TWStringHelper::create(encoded_tx_str); + + let fee_payer_str = "Eg5jqooyG6ySaXKbQUu4Lpvu2SqUPZrNkM4zXs9iUDLJ"; + let fee_payer = TWStringHelper::create(fee_payer_str); + + // Step 1 - Add fee payer to the transaction. + let updated_tx = TWStringHelper::wrap(unsafe { + tw_solana_transaction_set_fee_payer(encoded_tx.ptr(), fee_payer.ptr()) + }); + let updated_tx = updated_tx.to_string().unwrap(); + assert_eq!(updated_tx, "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAIAAQTLKvCJtWpVdze8Fxjgy/Iyz1sC4U7gqnxmdSM/X2+bV2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQMCAQIMAgAAAACcnwYAAAAAAA=="); + + // Step 2 - Decode transaction into a `RawMessage` Protobuf. + let tx_data = base64::decode(&updated_tx, STANDARD).unwrap(); + let mut decoder = TransactionDecoderHelper::::default(); + let output = decoder.decode(CoinType::Solana, tx_data); + + assert_eq!(output.error, SigningError::OK); + let decoded_tx = output.transaction.unwrap(); + + let signing_input = Proto::SigningInput { + raw_message: Some(decoded_tx), + tx_encoding: Proto::Encoding::Base64, + ..Proto::SigningInput::default() + }; + + // Step 3 - Obtain preimage hash. + let mut pre_imager = PreImageHelper::::default(); + let preimage_output = pre_imager.pre_image_hashes(CoinType::Solana, &signing_input); + + assert_eq!(preimage_output.error, SigningError::OK); + assert_eq!( + preimage_output.data.to_hex(), + "8002000104cb2af089b56a557737bc1718e0cbf232cf5b02e14ee0aa7c6675233f5f6f9b576b842ab38fbd9341b5d52d4855dc83cfa48f83bf6751edfe1c2f9daaaae6cea64d77772adc14c8915f46cd8f05f7905bcc42119bcdaffe49fd3c7c96d6e7d29c00000000000000000000000000000000000000000000000000000000000000002a3e4116ef5d634aa0e7da38be1c4a97d8ae69ffd9357e74199cb7e1ec9a6c1d01030201020c02000000009c9f060000000000" + ); + + // Step 4 - Compile transaction info. + // Simulate signature, normally obtained from signature server. + let fee_payer_signature = "feb9f15cc345fa156450676100033860edbe80a6f61dab8199e94fdc47678ecfdb95e3bc10ec0a7f863ab8ef5c38edae72db7e5d72855db225fd935fd59b700a".decode_hex().unwrap(); + let fee_payer_public_key = base58::decode(fee_payer_str, SOLANA_ALPHABET).unwrap(); + + let sol_sender_signature = "936cd6d176e701d1f748031925b2f029f6f1ab4b99aec76e24ccf05649ec269569a08ec0bd80f5fee1cb8d13ecd420bf50c5f64ae74c7afa267458cabb4e5804".decode_hex().unwrap(); + let sol_sender_public_key = "6b842ab38fbd9341b5d52d4855dc83cfa48f83bf6751edfe1c2f9daaaae6cea6" + .decode_hex() + .unwrap(); + + let mut compiler = CompilerHelper::::default(); + let output = compiler.compile( + CoinType::Solana, + &signing_input, + vec![fee_payer_signature, sol_sender_signature], + vec![fee_payer_public_key, sol_sender_public_key], + ); + + assert_eq!(output.error, SigningError::OK); + // Successfully broadcasted tx: + // https://explorer.solana.com/tx/66PAVjxFVGP4ctrkXmyNRhp6BdFT7gDe1k356DZzCRaBDTmJZF1ewGsbujWRjDTrt5utnz8oHZw3mg8qBNyct41w?cluster=devnet + assert_eq!(output.encoded, "Av658VzDRfoVZFBnYQADOGDtvoCm9h2rgZnpT9xHZ47P25XjvBDsCn+GOrjvXDjtrnLbfl1yhV2yJf2TX9WbcAqTbNbRducB0fdIAxklsvAp9vGrS5mux24kzPBWSewmlWmgjsC9gPX+4cuNE+zUIL9QxfZK50x6+iZ0WMq7TlgEgAIAAQTLKvCJtWpVdze8Fxjgy/Iyz1sC4U7gqnxmdSM/X2+bV2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQMCAQIMAgAAAACcnwYAAAAAAA=="); + assert_eq!(output.unsigned_tx, "gAIAAQTLKvCJtWpVdze8Fxjgy/Iyz1sC4U7gqnxmdSM/X2+bV2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQMCAQIMAgAAAACcnwYAAAAAAA=="); +} + +#[test] +fn test_solana_transaction_set_fee_payer_already_exists() { + let encoded_tx_str = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQABA2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQICAAEMAgAAAACcnwYAAAAAAA=="; + let encoded_tx = TWStringHelper::create(encoded_tx_str); + + let fee_payer_str = "8EhWjZGEt58UKzeiburZVx6QQF3rbayScpDjPNqCx62q"; + let fee_payer = TWStringHelper::create(fee_payer_str); + + let updated_tx = TWStringHelper::wrap(unsafe { + tw_solana_transaction_set_fee_payer(encoded_tx.ptr(), fee_payer.ptr()) + }); + + // The fee payer is already in the transaction. + // We expect tw_solana_transaction_set_fee_payer to return null. + assert_eq!(updated_tx.to_string(), None); +} diff --git a/rust/wallet_core_rs/src/ffi/solana/transaction.rs b/rust/wallet_core_rs/src/ffi/solana/transaction.rs index 6f97e5803e3..78e6b7ac0ba 100644 --- a/rust/wallet_core_rs/src/ffi/solana/transaction.rs +++ b/rust/wallet_core_rs/src/ffi/solana/transaction.rs @@ -133,3 +133,26 @@ pub unsafe extern "C" fn tw_solana_transaction_set_compute_unit_limit( _ => std::ptr::null_mut(), } } + +/// Adds fee payer to the given transaction, and returns the updated transaction. +/// +/// \param encoded_tx base64 encoded Solana transaction. +/// \param fee_payer fee payer account address. Must be a base58 encoded public key. It must NOT be in the account list yet. +/// \return base64 encoded Solana transaction. Null if an error occurred. +#[no_mangle] +pub unsafe extern "C" fn tw_solana_transaction_set_fee_payer( + encoded_tx: *const TWString, + fee_payer: *const TWString, +) -> *mut TWString { + let encoded_tx = try_or_else!(TWString::from_ptr_as_ref(encoded_tx), std::ptr::null_mut); + let encoded_tx = try_or_else!(encoded_tx.as_str(), std::ptr::null_mut); + + let fee_payer = try_or_else!(TWString::from_ptr_as_ref(fee_payer), std::ptr::null_mut); + let fee_payer = try_or_else!(fee_payer.as_str(), std::ptr::null_mut); + let fee_payer = try_or_else!(fee_payer.parse(), std::ptr::null_mut); + + match SolanaTransaction::set_fee_payer(encoded_tx, fee_payer) { + Ok(updated_tx) => TWString::from(updated_tx).into_ptr(), + _ => std::ptr::null_mut(), + } +} diff --git a/src/interface/TWSolanaTransaction.cpp b/src/interface/TWSolanaTransaction.cpp index f555f6eb5d7..7b06afcf67b 100644 --- a/src/interface/TWSolanaTransaction.cpp +++ b/src/interface/TWSolanaTransaction.cpp @@ -87,3 +87,20 @@ TWString *_Nullable TWSolanaTransactionSetComputeUnitLimit(TWString *_Nonnull en auto updatedTx = updatedTxStr.toStringOrDefault(); return TWStringCreateWithUTF8Bytes(updatedTx.c_str()); } + +TWString *_Nullable TWSolanaTransactionSetFeePayer(TWString *_Nonnull encodedTx, TWString *_Nonnull feePayer) { + auto& encodedTxRef = *reinterpret_cast(encodedTx); + auto& feePayerRef = *reinterpret_cast(feePayer); + + const Rust::TWStringWrapper encodedTxStr = encodedTxRef; + const Rust::TWStringWrapper feePayerStr = feePayerRef; + + Rust::TWStringWrapper updatedTxStr = Rust::tw_solana_transaction_set_fee_payer(encodedTxStr.get(), feePayerStr.get()); + + if (!updatedTxStr) { + return nullptr; + } + + const auto updatedTx = updatedTxStr.toStringOrDefault(); + return TWStringCreateWithUTF8Bytes(updatedTx.c_str()); +} diff --git a/swift/Tests/Blockchains/SolanaTests.swift b/swift/Tests/Blockchains/SolanaTests.swift index bb8f996363c..e84ced805eb 100644 --- a/swift/Tests/Blockchains/SolanaTests.swift +++ b/swift/Tests/Blockchains/SolanaTests.swift @@ -329,6 +329,54 @@ class SolanaTests: XCTestCase { // https://explorer.solana.com/tx/2ho7wZUXbDNz12xGfsXg2kcNMqkBAQjv7YNXNcVcuCmbC4p9FZe9ELeM2gMjq9MKQPpmE3nBW5pbdgwVCfNLr1h8 XCTAssertEqual(output.encoded, "AVUye82Mv+/aWeU2G+B6Nes365mUU2m8iqcGZn/8kFJvw4wY6AgKGG+vJHaknHlCDwE1yi1SIMVUUtNCOm3kHg8BAAIEODI+iWe7g68B9iwCy8bFkJKvsIEj350oSOpcv4gNnv/st+6qmqipl9lwMK6toB9TiL7LrJVfij+pKwr+pUKxfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAboZ09wD3Y5HNUl7aN8bwpCqowev1hMu7fSgziLswUSgMDAAUCECcAAAICAAEMAgAAAOgDAAAAAAAAAwAJA+gDAAAAAAAA") } + + func testSetFeePayer() throws { + // base64 encoded + let originalTx = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQABA2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQICAAEMAgAAAACcnwYAAAAAAA==" + + // Step 1 - Add fee payer to the transaction. + let updatedTx = SolanaTransaction.setFeePayer(encodedTx: originalTx, feePayer: "Eg5jqooyG6ySaXKbQUu4Lpvu2SqUPZrNkM4zXs9iUDLJ")! + + XCTAssertEqual(updatedTx, "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAIAAQTLKvCJtWpVdze8Fxjgy/Iyz1sC4U7gqnxmdSM/X2+bV2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQMCAQIMAgAAAACcnwYAAAAAAA==") + + // Step 2 - Decode transaction into a `RawMessage` Protobuf. + let updatedTxData = Base64.decode(string: updatedTx)! + let decodeOutputData = TransactionDecoder.decode(coinType: .solana, encodedTx: updatedTxData) + let decodeOutput = try SolanaDecodingTransactionOutput(serializedData: decodeOutputData) + + XCTAssertEqual(decodeOutput.error, .ok) + + // Step 3 - Obtain preimage hash. + let signingInput = SolanaSigningInput.with { + $0.rawMessage = decodeOutput.transaction + $0.txEncoding = .base64 + } + let txInputData = try signingInput.serializedData() // Serialize input + let preImageHashes = TransactionCompiler.preImageHashes(coinType: .solana, txInputData: txInputData) + let preSigningOutput: SolanaPreSigningOutput = try SolanaPreSigningOutput(serializedData: preImageHashes) + XCTAssertEqual(preSigningOutput.error, CommonSigningError.ok) + XCTAssertEqual(preSigningOutput.data.hexString, "8002000104cb2af089b56a557737bc1718e0cbf232cf5b02e14ee0aa7c6675233f5f6f9b576b842ab38fbd9341b5d52d4855dc83cfa48f83bf6751edfe1c2f9daaaae6cea64d77772adc14c8915f46cd8f05f7905bcc42119bcdaffe49fd3c7c96d6e7d29c00000000000000000000000000000000000000000000000000000000000000002a3e4116ef5d634aa0e7da38be1c4a97d8ae69ffd9357e74199cb7e1ec9a6c1d01030201020c02000000009c9f060000000000") + + // Step 4 - Compile transaction info. + // Simulate signature, normally obtained from signature server. + let signatureVec = DataVector() + let pubkeyVec = DataVector() + let feePayerSignature = Data(hexString: "feb9f15cc345fa156450676100033860edbe80a6f61dab8199e94fdc47678ecfdb95e3bc10ec0a7f863ab8ef5c38edae72db7e5d72855db225fd935fd59b700a")! + let feePayerPublicKey = Data(hexString: "cb2af089b56a557737bc1718e0cbf232cf5b02e14ee0aa7c6675233f5f6f9b57")! + signatureVec.add(data: feePayerSignature) + pubkeyVec.add(data: feePayerPublicKey) + let solSenderSignature = Data(hexString: "936cd6d176e701d1f748031925b2f029f6f1ab4b99aec76e24ccf05649ec269569a08ec0bd80f5fee1cb8d13ecd420bf50c5f64ae74c7afa267458cabb4e5804")! + let solSenderPublicKey = Data(hexString: "6b842ab38fbd9341b5d52d4855dc83cfa48f83bf6751edfe1c2f9daaaae6cea6")! + signatureVec.add(data: solSenderSignature) + pubkeyVec.add(data: solSenderPublicKey) + + let compileWithSignatures = TransactionCompiler.compileWithSignatures(coinType: .solana, txInputData: txInputData, signatures: signatureVec, publicKeys: pubkeyVec) + let expectedTx = "Av658VzDRfoVZFBnYQADOGDtvoCm9h2rgZnpT9xHZ47P25XjvBDsCn+GOrjvXDjtrnLbfl1yhV2yJf2TX9WbcAqTbNbRducB0fdIAxklsvAp9vGrS5mux24kzPBWSewmlWmgjsC9gPX+4cuNE+zUIL9QxfZK50x6+iZ0WMq7TlgEgAIAAQTLKvCJtWpVdze8Fxjgy/Iyz1sC4U7gqnxmdSM/X2+bV2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQMCAQIMAgAAAACcnwYAAAAAAA==" + let output: SolanaSigningOutput = try SolanaSigningOutput(serializedData: compileWithSignatures) + XCTAssertEqual(output.encoded, expectedTx) + // Successfully broadcasted tx: + // https://explorer.solana.com/tx/66PAVjxFVGP4ctrkXmyNRhp6BdFT7gDe1k356DZzCRaBDTmJZF1ewGsbujWRjDTrt5utnz8oHZw3mg8qBNyct41w?cluster=devnet + } func testSignUserMessage() throws { let privateKey = Data(hexString: "44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d")! diff --git a/tests/chains/Solana/TWSolanaTransaction.cpp b/tests/chains/Solana/TWSolanaTransaction.cpp index 14613f3c344..f562dcfb8b4 100644 --- a/tests/chains/Solana/TWSolanaTransaction.cpp +++ b/tests/chains/Solana/TWSolanaTransaction.cpp @@ -10,6 +10,7 @@ #include "Base64.h" #include "Base58.h" #include "HexCoding.h" +#include "TransactionCompiler.h" #include @@ -137,4 +138,57 @@ TEST(TWSolanaTransaction, SetPriorityFee) { EXPECT_EQ(output.encoded(), "AVUye82Mv+/aWeU2G+B6Nes365mUU2m8iqcGZn/8kFJvw4wY6AgKGG+vJHaknHlCDwE1yi1SIMVUUtNCOm3kHg8BAAIEODI+iWe7g68B9iwCy8bFkJKvsIEj350oSOpcv4gNnv/st+6qmqipl9lwMK6toB9TiL7LrJVfij+pKwr+pUKxfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAboZ09wD3Y5HNUl7aN8bwpCqowev1hMu7fSgziLswUSgMDAAUCECcAAAICAAEMAgAAAOgDAAAAAAAAAwAJA+gDAAAAAAAA"); } + +TEST(TWSolanaTransaction, SetFeePayer) { + const auto coin = TWCoinTypeSolana; + + // base64 encoded + const auto originalTx = STRING("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQABA2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQICAAEMAgAAAACcnwYAAAAAAA=="); + + // Step 1 - Add fee payer to the transaction. + const auto feePayer = STRING("Eg5jqooyG6ySaXKbQUu4Lpvu2SqUPZrNkM4zXs9iUDLJ"); + const auto updatedTx = WRAPS(TWSolanaTransactionSetFeePayer(originalTx.get(), feePayer.get())); + + assertStringsEqual(updatedTx, "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAIAAQTLKvCJtWpVdze8Fxjgy/Iyz1sC4U7gqnxmdSM/X2+bV2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQMCAQIMAgAAAACcnwYAAAAAAA=="); + + // Step 2 - Decode transaction into a `RawMessage` Protobuf. + const std::string updateTxDataB64 {TWStringUTF8Bytes(updatedTx.get()) }; + const auto updatedTxData = Base64::decode(updateTxDataB64); + const auto updatedTxRef = WRAPD(TWDataCreateWithBytes(updatedTxData.data(), updatedTxData.size())); + + const auto decodeOutputData = WRAPD(TWTransactionDecoderDecode(TWCoinTypeSolana, updatedTxRef.get())); + Proto::DecodingTransactionOutput decodeOutput; + decodeOutput.ParseFromArray(TWDataBytes(decodeOutputData.get()), static_cast(TWDataSize(decodeOutputData.get()))); + EXPECT_EQ(decodeOutput.error(), Common::Proto::SigningError::OK); + + // Step 3 - Obtain preimage hash. + Proto::SigningInput input; + *input.mutable_raw_message() = decodeOutput.transaction(); + input.set_tx_encoding(Proto::Encoding::Base64); + auto inputString = input.SerializeAsString(); + auto inputStrData = TW::Data(inputString.begin(), inputString.end()); + + const auto preImageHashesData = TransactionCompiler::preImageHashes(coin, inputStrData); + auto preSigningOutput = Proto::PreSigningOutput(); + preSigningOutput.ParseFromArray(preImageHashesData.data(), (int)preImageHashesData.size()); + ASSERT_EQ(preSigningOutput.signers_size(), 2); + auto preImageHash = preSigningOutput.data(); + EXPECT_EQ(hex(preImageHash), "8002000104cb2af089b56a557737bc1718e0cbf232cf5b02e14ee0aa7c6675233f5f6f9b576b842ab38fbd9341b5d52d4855dc83cfa48f83bf6751edfe1c2f9daaaae6cea64d77772adc14c8915f46cd8f05f7905bcc42119bcdaffe49fd3c7c96d6e7d29c00000000000000000000000000000000000000000000000000000000000000002a3e4116ef5d634aa0e7da38be1c4a97d8ae69ffd9357e74199cb7e1ec9a6c1d01030201020c02000000009c9f060000000000"); + + // Step 4 - Compile transaction info. + // Simulate signature, normally obtained from signature server. + const auto feePayerSignature = parse_hex("feb9f15cc345fa156450676100033860edbe80a6f61dab8199e94fdc47678ecfdb95e3bc10ec0a7f863ab8ef5c38edae72db7e5d72855db225fd935fd59b700a"); + const auto feePayerPublicKey = parse_hex("cb2af089b56a557737bc1718e0cbf232cf5b02e14ee0aa7c6675233f5f6f9b57"); + const auto solSenderSignature = parse_hex("936cd6d176e701d1f748031925b2f029f6f1ab4b99aec76e24ccf05649ec269569a08ec0bd80f5fee1cb8d13ecd420bf50c5f64ae74c7afa267458cabb4e5804"); + const auto solSenderPublicKey = parse_hex("6b842ab38fbd9341b5d52d4855dc83cfa48f83bf6751edfe1c2f9daaaae6cea6"); + + auto outputData = TransactionCompiler::compileWithSignatures(coin, inputStrData, {feePayerSignature, solSenderSignature}, {feePayerPublicKey, solSenderPublicKey}); + const auto expectedTx = "Av658VzDRfoVZFBnYQADOGDtvoCm9h2rgZnpT9xHZ47P25XjvBDsCn+GOrjvXDjtrnLbfl1yhV2yJf2TX9WbcAqTbNbRducB0fdIAxklsvAp9vGrS5mux24kzPBWSewmlWmgjsC9gPX+4cuNE+zUIL9QxfZK50x6+iZ0WMq7TlgEgAIAAQTLKvCJtWpVdze8Fxjgy/Iyz1sC4U7gqnxmdSM/X2+bV2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mTXd3KtwUyJFfRs2PBfeQW8xCEZvNr/5J/Tx8ltbn0pwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo+QRbvXWNKoOfaOL4cSpfYrmn/2TV+dBmct+HsmmwdAQMCAQIMAgAAAACcnwYAAAAAAA=="; + Proto::SigningOutput output; + ASSERT_TRUE(output.ParseFromArray(outputData.data(), (int)outputData.size())); + EXPECT_EQ(output.encoded(), expectedTx); + // Successfully broadcasted tx: + // https://explorer.solana.com/tx/66PAVjxFVGP4ctrkXmyNRhp6BdFT7gDe1k356DZzCRaBDTmJZF1ewGsbujWRjDTrt5utnz8oHZw3mg8qBNyct41w?cluster=devnet +} + } // TW::Solana::tests