Skip to content

Commit

Permalink
[Solana]: Support specifying fee payer for an encoded transaction (#4156
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
10gic authored Dec 16, 2024
1 parent dd612bb commit b37b780
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
}
12 changes: 10 additions & 2 deletions include/TrustWalletCore/TWSolanaTransaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
31 changes: 31 additions & 0 deletions rust/chains/tw_solana/src/modules/insert_instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]>;

Expand Down
15 changes: 15 additions & 0 deletions rust/chains/tw_solana/src/modules/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<String> {
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(
Expand Down
87 changes: 84 additions & 3 deletions rust/tw_tests/tests/chains/solana/solana_transaction_ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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]
Expand Down Expand Up @@ -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::<Proto::DecodingTransactionOutput>::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::<Proto::PreSigningOutput>::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::<Proto::SigningOutput>::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);
}
23 changes: 23 additions & 0 deletions rust/wallet_core_rs/src/ffi/solana/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}
17 changes: 17 additions & 0 deletions src/interface/TWSolanaTransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<const std::string*>(encodedTx);
auto& feePayerRef = *reinterpret_cast<const std::string*>(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());
}
Loading

0 comments on commit b37b780

Please sign in to comment.