Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Solana]: Support specifying fee payer for an encoded transaction #4156

Merged
merged 6 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
10gic marked this conversation as resolved.
Show resolved Hide resolved
//
// 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
Loading