Skip to content

Commit bb49d06

Browse files
feat(solana): Add SolanaTransaction.insertTransferInstruction() (trustwallet#4523)
* feat(solana): Add `transfer_to_fee_payer` instruction * feat(solana): Add `tw_solana_transaction_insert_transfer_instruction` FFI * feat(solana): Add a note comment * feat(solana): Add one more comment
1 parent 5198377 commit bb49d06

File tree

10 files changed

+269
-27
lines changed

10 files changed

+269
-27
lines changed

rust/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/chains/tw_solana/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ tw_hash = { path = "../../tw_hash" }
1515
tw_keypair = { path = "../../tw_keypair" }
1616
tw_memory = { path = "../../tw_memory" }
1717
tw_misc = { path = "../../tw_misc" }
18+
tw_number = { path = "../../tw_number" }
1819
tw_proto = { path = "../../tw_proto" }

rust/chains/tw_solana/src/modules/insert_instruction.rs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,10 @@ pub trait InsertInstruction {
132132
.iter()
133133
.position(|key| *key == account.pubkey)
134134
{
135-
let is_signer =
135+
let existing_is_signer =
136136
existing_index < self.message_header_mut().num_required_signatures as usize;
137137

138-
let is_writable = if is_signer {
138+
let existing_is_writable = if existing_is_signer {
139139
existing_index
140140
< (self.message_header_mut().num_required_signatures
141141
- self.message_header_mut().num_readonly_signed_accounts)
@@ -146,16 +146,30 @@ pub trait InsertInstruction {
146146
- self.message_header_mut().num_readonly_unsigned_accounts as usize)
147147
};
148148

149-
if account.is_signer != is_signer {
150-
return SigningError::err(SigningErrorType::Error_internal).context(
151-
"Account already exists but the `is_signer` attribute does not match",
152-
);
149+
match (existing_is_signer, account.is_signer) {
150+
// If new account requires weaker or the same signing permissions, it's ok.
151+
(true, false) | (false, false) | (true, true) => (),
152+
// If new account requires stronger signing permissions than we have already, then would need to reorder accounts.
153+
// TODO: Implement reordering accounts if needed.
154+
(false, true) => {
155+
return SigningError::err(SigningErrorType::Error_internal).context(
156+
"Account already exists but the `is_signer` attribute does not match",
157+
);
158+
},
153159
}
154-
if account.is_writable != is_writable {
155-
return SigningError::err(SigningErrorType::Error_internal).context(
156-
"Account already exists but the `is_writable` attribute does not match",
157-
);
160+
161+
match (existing_is_writable, account.is_writable) {
162+
// If new account requires weaker or the same writable permissions, it's ok.
163+
(true, false) | (false, false) | (true, true) => (),
164+
// If new account requires stronger writable permissions than we have already, then would need to reorder accounts.
165+
// TODO: Implement reordering accounts if needed.
166+
(false, true) => {
167+
return SigningError::err(SigningErrorType::Error_internal).context(
168+
"Account already exists but the `is_writable` attribute does not match",
169+
);
170+
},
158171
}
172+
159173
// Return the existing index if validation passes
160174
return try_into_u8(existing_index);
161175
}

rust/chains/tw_solana/src/modules/instruction_builder/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ impl InstructionBuilder {
6666
self
6767
}
6868

69+
pub fn maybe_add_instruction(&mut self, instruction: Option<Instruction>) -> &mut Self {
70+
if let Some(instruction) = instruction {
71+
self.instructions.push(instruction);
72+
}
73+
self
74+
}
75+
6976
pub fn add_instructions<I>(&mut self, instructions: I) -> &mut Self
7077
where
7178
I: IntoIterator<Item = Instruction>,

rust/chains/tw_solana/src/modules/message_builder.rs

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ impl<'a> MessageBuilder<'a> {
166166
.maybe_priority_fee_price(self.priority_fee_price())
167167
.maybe_priority_fee_limit(self.priority_fee_limit())
168168
.maybe_memo(transfer.memo.as_ref())
169-
.add_instruction(transfer_ix);
169+
.add_instruction(transfer_ix)
170+
.maybe_add_instruction(self.transfer_to_fee_payer()?);
170171
Ok(builder.output())
171172
}
172173

@@ -202,7 +203,8 @@ impl<'a> MessageBuilder<'a> {
202203
.maybe_advance_nonce(self.nonce_account()?, sender)
203204
.maybe_priority_fee_price(self.priority_fee_price())
204205
.maybe_priority_fee_limit(self.priority_fee_limit())
205-
.add_instructions(deposit_ixs);
206+
.add_instructions(deposit_ixs)
207+
.maybe_add_instruction(self.transfer_to_fee_payer()?);
206208
Ok(builder.output())
207209
}
208210

@@ -222,7 +224,8 @@ impl<'a> MessageBuilder<'a> {
222224
.maybe_advance_nonce(self.nonce_account()?, sender)
223225
.maybe_priority_fee_price(self.priority_fee_price())
224226
.maybe_priority_fee_limit(self.priority_fee_limit())
225-
.add_instruction(deactivate_ix);
227+
.add_instruction(deactivate_ix)
228+
.maybe_add_instruction(self.transfer_to_fee_payer()?);
226229
Ok(builder.output())
227230
}
228231

@@ -246,7 +249,8 @@ impl<'a> MessageBuilder<'a> {
246249
.maybe_advance_nonce(self.nonce_account()?, sender)
247250
.maybe_priority_fee_price(self.priority_fee_price())
248251
.maybe_priority_fee_limit(self.priority_fee_limit())
249-
.add_instructions(deactivate_ixs);
252+
.add_instructions(deactivate_ixs)
253+
.maybe_add_instruction(self.transfer_to_fee_payer()?);
250254
Ok(builder.output())
251255
}
252256

@@ -274,7 +278,8 @@ impl<'a> MessageBuilder<'a> {
274278
.maybe_advance_nonce(self.nonce_account()?, sender)
275279
.maybe_priority_fee_price(self.priority_fee_price())
276280
.maybe_priority_fee_limit(self.priority_fee_limit())
277-
.add_instruction(withdraw_ix);
281+
.add_instruction(withdraw_ix)
282+
.maybe_add_instruction(self.transfer_to_fee_payer()?);
278283
Ok(builder.output())
279284
}
280285

@@ -308,7 +313,8 @@ impl<'a> MessageBuilder<'a> {
308313
.maybe_advance_nonce(self.nonce_account()?, sender)
309314
.maybe_priority_fee_price(self.priority_fee_price())
310315
.maybe_priority_fee_limit(self.priority_fee_limit())
311-
.add_instructions(withdraw_ixs);
316+
.add_instructions(withdraw_ixs)
317+
.maybe_add_instruction(self.transfer_to_fee_payer()?);
312318
Ok(builder.output())
313319
}
314320

@@ -342,7 +348,8 @@ impl<'a> MessageBuilder<'a> {
342348
.maybe_advance_nonce(self.nonce_account()?, funding_account)
343349
.maybe_priority_fee_price(self.priority_fee_price())
344350
.maybe_priority_fee_limit(self.priority_fee_limit())
345-
.add_instruction(instruction);
351+
.add_instruction(instruction)
352+
.maybe_add_instruction(self.transfer_to_fee_payer()?);
346353
Ok(builder.output())
347354
}
348355

@@ -390,7 +397,8 @@ impl<'a> MessageBuilder<'a> {
390397
.maybe_priority_fee_price(self.priority_fee_price())
391398
.maybe_priority_fee_limit(self.priority_fee_limit())
392399
.maybe_memo(token_transfer.memo.as_ref())
393-
.add_instruction(transfer_instruction);
400+
.add_instruction(transfer_instruction)
401+
.maybe_add_instruction(self.transfer_to_fee_payer()?);
394402
Ok(builder.output())
395403
}
396404

@@ -456,7 +464,8 @@ impl<'a> MessageBuilder<'a> {
456464
.add_instruction(create_account_instruction)
457465
// Optional memo. Order: before transfer, as per documentation.
458466
.maybe_memo(create_and_transfer.memo.as_ref())
459-
.add_instruction(transfer_instruction);
467+
.add_instruction(transfer_instruction)
468+
.maybe_add_instruction(self.transfer_to_fee_payer()?);
460469
Ok(builder.output())
461470
}
462471

@@ -489,7 +498,8 @@ impl<'a> MessageBuilder<'a> {
489498
new_nonce_account,
490499
create_nonce.rent,
491500
DEFAULT_CREATE_NONCE_SPACE,
492-
));
501+
))
502+
.maybe_add_instruction(self.transfer_to_fee_payer()?);
493503
Ok(builder.output())
494504
}
495505

@@ -516,7 +526,8 @@ impl<'a> MessageBuilder<'a> {
516526
signer,
517527
recipient,
518528
withdraw_nonce.value,
519-
));
529+
))
530+
.maybe_add_instruction(self.transfer_to_fee_payer()?);
520531
Ok(builder.output())
521532
}
522533

@@ -533,7 +544,8 @@ impl<'a> MessageBuilder<'a> {
533544
builder
534545
.maybe_advance_nonce(Some(nonce_account), signer)
535546
.maybe_priority_fee_price(self.priority_fee_price())
536-
.maybe_priority_fee_limit(self.priority_fee_limit());
547+
.maybe_priority_fee_limit(self.priority_fee_limit())
548+
.maybe_add_instruction(self.transfer_to_fee_payer()?);
537549
Ok(builder.output())
538550
}
539551

@@ -648,6 +660,24 @@ impl<'a> MessageBuilder<'a> {
648660
.map(|proto| proto.limit)
649661
}
650662

663+
fn transfer_to_fee_payer(&self) -> SigningResult<Option<Instruction>> {
664+
let Some(ref transfer_to_fee_payer) = self.input.transfer_to_fee_payer else {
665+
return Ok(None);
666+
};
667+
668+
let from = self.signer_address()?;
669+
let to = SolanaAddress::from_str(&transfer_to_fee_payer.recipient)
670+
.into_tw()
671+
.context("Invalid 'transfer_to_fee_payer.recipient' address")?;
672+
673+
let references = Self::parse_references(&transfer_to_fee_payer.references)?;
674+
675+
Ok(Some(
676+
SystemInstructionBuilder::transfer(from, to, transfer_to_fee_payer.value)
677+
.with_references(references),
678+
))
679+
}
680+
651681
fn parse_references(refs: &[Cow<'_, str>]) -> SigningResult<Vec<SolanaAddress>> {
652682
refs.iter()
653683
.map(|addr| SolanaAddress::from_str(addr).map_err(SigningError::from))

rust/chains/tw_solana/src/modules/utils.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,25 @@ use crate::modules::insert_instruction::InsertInstruction;
99
use crate::modules::instruction_builder::compute_budget_instruction::{
1010
ComputeBudgetInstruction, ComputeBudgetInstructionBuilder, UnitLimit, UnitPrice,
1111
};
12-
use crate::modules::instruction_builder::system_instruction::SystemInstruction;
12+
use crate::modules::instruction_builder::system_instruction::{
13+
SystemInstruction, SystemInstructionBuilder,
14+
};
1315
use crate::modules::message_decompiler::{InstructionWithoutAccounts, MessageDecompiler};
1416
use crate::modules::proto_builder::ProtoBuilder;
1517
use crate::modules::tx_signer::TxSigner;
1618
use crate::modules::PubkeySignatureMap;
1719
use crate::transaction::versioned::VersionedTransaction;
1820
use crate::SOLANA_ALPHABET;
1921
use std::borrow::Cow;
22+
use std::str::FromStr;
2023
use tw_coin_entry::error::prelude::*;
2124
use tw_coin_entry::signing_output_error;
2225
use tw_encoding::base58;
2326
use tw_encoding::base64::{self, STANDARD};
2427
use tw_hash::H256;
2528
use tw_keypair::{ed25519, KeyPairResult};
2629
use tw_memory::Data;
30+
use tw_number::U256;
2731
use tw_proto::Solana::Proto;
2832

2933
pub struct SolanaTransaction;
@@ -210,6 +214,53 @@ impl SolanaTransaction {
210214
.to_base64()
211215
.tw_err(SigningErrorType::Error_internal)
212216
}
217+
218+
/// Inserts a SOL transfer instruction to the given transaction at the specified position, returning the updated transaction.
219+
/// Please note that compute price and limit instructions should always be the first instructions if they are present in the transaction.
220+
/// If you don't care about the position, use -1.
221+
pub fn insert_transfer_instruction(
222+
encoded_tx: &str,
223+
insert_at: i32,
224+
from: &str,
225+
to: &str,
226+
lamports: &str,
227+
) -> SigningResult<String> {
228+
let tx_bytes = base64::decode(encoded_tx, STANDARD)?;
229+
let from =
230+
SolanaAddress::from_str(from).map_err(|_| SigningErrorType::Error_input_parse)?;
231+
let to = SolanaAddress::from_str(to).map_err(|_| SigningErrorType::Error_input_parse)?;
232+
let lamports = U256::from_str(lamports)
233+
.and_then(u64::try_from)
234+
.map_err(|_| SigningErrorType::Error_input_parse)?;
235+
236+
let mut tx: VersionedTransaction =
237+
bincode::deserialize(&tx_bytes).map_err(|_| SigningErrorType::Error_input_parse)?;
238+
239+
if insert_at >= 0 && insert_at as usize > tx.message.instructions().len() {
240+
return Err(SigningError::from(SigningErrorType::Error_invalid_params));
241+
}
242+
243+
let final_insert_at = if insert_at < 0 {
244+
tx.message.instructions().len() // Append to the end if negative
245+
} else {
246+
insert_at as usize // Use the specified position
247+
};
248+
249+
// Create transfer instruction and insert it at the specified position.
250+
let transfer_ix = SystemInstructionBuilder::transfer(from, to, lamports);
251+
tx.message.insert_instruction(
252+
final_insert_at,
253+
transfer_ix.program_id,
254+
transfer_ix.accounts,
255+
transfer_ix.data,
256+
)?;
257+
258+
// Set the correct number of zero signatures
259+
let unsigned_tx = VersionedTransaction::unsigned(tx.message);
260+
unsigned_tx
261+
.to_base64()
262+
.tw_err(SigningErrorType::Error_internal)
263+
}
213264
}
214265

215266
fn try_instruction_as_compute_budget(

rust/tw_tests/tests/chains/solana/solana_sign.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,3 +1009,42 @@ fn test_solana_sign_sponsored_transfer_token_with_external_fee_payer() {
10091009
// https://solscan.io/tx/3z7beuRPcr6WmTRvCDNgSNXBaEUTAy8EYHN93eUiLXoFoj2VbWuPbvf7nQoZxTHbG6ChQuTJDqwaQnUzK4WxYaQA
10101010
assert_eq!(output.encoded, "gnSfLvpTeWGFvEKGDNAwQpQYczANiAHcpj4ghRgKsJfTXJjqaGYnNG2Ay2JwR5XdRvdkeLjHdht7VctoJkxDcYLRNjWmFcb3khwZqV4oRcU3HCxqnjGbiFmBCTjsupUt4ZzsJs8DS9WGHPgQGfRVQdmq1Zv6Kd4KDR88aT3uLmdNsu1XP5Es5SFAqByGnwAnkthDfNvcmpW9iAsZdf4v7gTsgFZV14ZfsNh66TGzVJLepz689D4jKb19AyvPwBPYYsvpRLxeEaa3zJvsdBBoVkWMZzC2Y8oqxoPXCRXnxzKX9gJSew1P2bgZDN3j3BvFQ19zTYsdugGtRetV94yQgx5xh8Vk9Asbj3YCmEZpFMZborqeanvgK2mWs2rQmbanMY6Fi6FB1xN24YN2B38pK2g3DCYp6nNh1ueacrDakbyrRFCpyKo26yqrkqnbbKZ9roAgvrvm5zhju2GhWU5t5cPc4ADfZbfRWaV2ojETv1a9W838MB7h4N5a97kgkdnuuR5A4fJr5K4jizC2rNeLciDoZQuzoNE3TpnYqxpnJPQWQQB1vGHqdXTTiDc47i7kLm");
10111011
}
1012+
1013+
#[test]
1014+
fn test_solana_sign_sponsored_transfer() {
1015+
const SOL_AMOUNT: u64 = 200_000;
1016+
const TOKEN_AMOUNT: u64 = 10_000;
1017+
const PRIVATE_KEY: &str = "7537978967203bdca1bcde4caa811c2771b36043a303824110cf240b10d9fde8";
1018+
const RECENT_BLOCKHASH: &str = "BTuHSa3pK17vmLR8NFGsn8uJdsYFvQXi5YhrNZKA5ggP";
1019+
1020+
let create_transfer_token = Proto::TokenTransfer {
1021+
token_mint_address: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB".into(),
1022+
recipient_token_address: "DwvTHUygHJ2xdHBHT1HgvHaJfqHkPbS5omrGEEGpLHDc".into(),
1023+
sender_token_address: "DK8JGfS6Acsjh7KCdT7rEZ4ECNXztNmbJMs1mgjdZCXE".into(),
1024+
amount: TOKEN_AMOUNT,
1025+
decimals: 6,
1026+
..Proto::TokenTransfer::default()
1027+
};
1028+
1029+
let transfer_to_fee_payer = Proto::Transfer {
1030+
recipient: "EkBtoCtDihccznHSF3P64kvcTt5xNxQ2jxYMjPXVH3DX".into(),
1031+
value: SOL_AMOUNT,
1032+
..Proto::Transfer::default()
1033+
};
1034+
1035+
let input = Proto::SigningInput {
1036+
private_key: PRIVATE_KEY.decode_hex().unwrap().into(),
1037+
recent_blockhash: RECENT_BLOCKHASH.into(),
1038+
transaction_type: TransactionType::token_transfer_transaction(create_transfer_token),
1039+
transfer_to_fee_payer: Some(transfer_to_fee_payer),
1040+
tx_encoding: Proto::Encoding::Base64,
1041+
..Proto::SigningInput::default()
1042+
};
1043+
1044+
let mut signer = AnySignerHelper::<Proto::SigningOutput>::default();
1045+
let output = signer.sign(CoinType::Solana, input);
1046+
1047+
assert_eq!(output.error, SigningError::OK);
1048+
// https://solscan.io/tx/52s8gt34WfZyJv1cDdadwA3V9PeRwazNqhVKDJ43F9JyxTE7ncqMSmqYAi1u4TsG2AyXNPbswG7krBHqWAhstCtL
1049+
assert_eq!(output.encoded, "Acms/WrZj/mOpUTTBFHLtBFKrMSAJPBBkD8qYK+oqgE0gFT5aoEfw5dlJZZl1edVde325gi0qVPai7ddgoP2WQUBAAMHgKRCBGe2W59ezw1CyHDoV4KZVuJFTtmsN0F25EL3I8228PMELJSiAl2nvEzyY4v/LfSQnxkFNlH4pjU2F3nMpcBeC+e4AaQzF6GCh63HW7KnhG+YuIbF1AvkM6nwOXMjzDg4vU3/xSfZJTqguxQVNsgitkm5dHCm6V6l4NCCDp7OAQ5gr+2yJxe9YxkvVBRaP5ZaM7uC0scCnrLOHiCCZAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACbeRZf0sbtT6rwdYyf6Z9h66lKwNsb48euTgiPa0h+WAIFBAEEAgAKDBAnAAAAAAAABgYCAAMMAgAAAEANAwAAAAAA");
1050+
}

0 commit comments

Comments
 (0)