Skip to content

Commit 655f89f

Browse files
Support adding instruction in Solana transaction (trustwallet#4371)
Co-authored-by: Sergei Boiko <[email protected]>
1 parent a75b1f1 commit 655f89f

File tree

5 files changed

+354
-4
lines changed

5 files changed

+354
-4
lines changed

rust/chains/tw_solana/src/instruction.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@
55
use crate::address::SolanaAddress;
66
use borsh::BorshSerialize;
77
use serde::{Deserialize, Serialize};
8+
use tw_encoding::base58::as_base58_bitcoin;
89
use tw_memory::Data;
910

11+
#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
12+
#[serde(rename_all = "camelCase")]
1013
pub struct Instruction {
1114
/// Pubkey of the program that executes this instruction.
1215
pub program_id: SolanaAddress,
1316
/// Metadata describing accounts that should be passed to the program.
1417
pub accounts: Vec<AccountMeta>,
1518
/// Opaque data passed to the program for its own interpretation.
16-
pub data: Data,
19+
#[serde(with = "as_base58_bitcoin")]
20+
pub data: Data, // Rpc getTransaction uses base58 encoding, we use the same encoding for consistency
1721
}
1822

1923
impl Instruction {
@@ -72,6 +76,7 @@ impl Instruction {
7276
}
7377

7478
#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
79+
#[serde(rename_all = "camelCase")]
7580
pub struct AccountMeta {
7681
/// An account's public key.
7782
pub pubkey: SolanaAddress,

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

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Copyright © 2017 Trust Wallet.
44

55
use crate::address::SolanaAddress;
6+
use crate::instruction::AccountMeta;
67
use crate::modules::compiled_keys::try_into_u8;
78
use crate::transaction::v0::MessageAddressTableLookup;
89
use crate::transaction::{CompiledInstruction, MessageHeader};
@@ -11,6 +12,59 @@ use tw_coin_entry::error::prelude::*;
1112
use tw_memory::Data;
1213

1314
pub trait InsertInstruction {
15+
/// Pushes an instruction
16+
fn push_instruction(
17+
&mut self,
18+
program_id: SolanaAddress,
19+
accounts: Vec<AccountMeta>,
20+
data: Data,
21+
) -> SigningResult<()> {
22+
let insert_at = self.instructions_mut().len();
23+
self.insert_instruction(insert_at, program_id, accounts, data)
24+
}
25+
26+
/// Inserts an instruction at the given `insert_at` index.
27+
fn insert_instruction(
28+
&mut self,
29+
insert_at: usize,
30+
program_id: SolanaAddress,
31+
accounts: Vec<AccountMeta>,
32+
data: Data,
33+
) -> SigningResult<()> {
34+
if insert_at > self.instructions_mut().len() {
35+
return SigningError::err(SigningErrorType::Error_internal)
36+
.context(format!("Unable to add '{program_id}' instruction at the '{insert_at}' index. Number of existing instructions: {}", self.instructions_mut().len()));
37+
}
38+
39+
// Step 1 - add the `account` in the accounts list.
40+
let accounts: Vec<u8> = accounts
41+
.iter()
42+
.map(|account_meta| self.push_account(account_meta))
43+
.collect::<Result<Vec<u8>, _>>()?;
44+
45+
// Step 2 - find or add the `program_id` in the accounts list.
46+
let program_id_index = match self
47+
.account_keys_mut()
48+
.iter()
49+
.position(|acc| *acc == program_id)
50+
{
51+
Some(pos) => try_into_u8(pos)?,
52+
None => self.push_readonly_unsigned_account(program_id)?,
53+
};
54+
55+
// Step 3 - Create a `CompiledInstruction` based on the `program_id` index and instruction `accounts` and `data`.
56+
let new_compiled_ix = CompiledInstruction {
57+
program_id_index,
58+
accounts,
59+
data,
60+
};
61+
62+
// Step 4 - Insert the created instruction at the given `insert_at` index.
63+
self.instructions_mut().insert(insert_at, new_compiled_ix);
64+
65+
Ok(())
66+
}
67+
1468
/// Pushes a simple instruction that doesn't have accounts.
1569
fn push_simple_instruction(
1670
&mut self,
@@ -56,6 +110,107 @@ pub trait InsertInstruction {
56110
Ok(())
57111
}
58112

113+
/// Pushes an account to the message.
114+
/// If the account already exists, it must match the `is_signer` and `is_writable` attributes
115+
/// Returns the index of the account in the account list.
116+
fn push_account(&mut self, account: &AccountMeta) -> SigningResult<u8> {
117+
// The layout of the account keys is as follows:
118+
// +-------------------------------------+
119+
// | Writable and required signature | \
120+
// +-------------------------------------+ |-> num_required_signatures
121+
// | Readonly and required signature | --> num_readonly_signed_accounts /
122+
// +-------------------------------------+
123+
// | Writable and not required signature |
124+
// +-------------------------------------+
125+
// | Readonly and not required signature | --> num_readonly_unsigned_accounts
126+
// +-------------------------------------+
127+
128+
// Check if the account already exists in `account_keys`,
129+
// if it does, validate `is_signer` and `is_writable` match
130+
if let Some(existing_index) = self
131+
.account_keys_mut()
132+
.iter()
133+
.position(|key| *key == account.pubkey)
134+
{
135+
let is_signer =
136+
existing_index < self.message_header_mut().num_required_signatures as usize;
137+
138+
let is_writable = if is_signer {
139+
existing_index
140+
< (self.message_header_mut().num_required_signatures
141+
- self.message_header_mut().num_readonly_signed_accounts)
142+
as usize
143+
} else {
144+
existing_index
145+
< (self.account_keys_mut().len()
146+
- self.message_header_mut().num_readonly_unsigned_accounts as usize)
147+
};
148+
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+
);
153+
}
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+
);
158+
}
159+
// Return the existing index if validation passes
160+
return try_into_u8(existing_index);
161+
}
162+
163+
// Determine the insertion position based on is_signer and is_writable
164+
let insert_at = match (account.is_signer, account.is_writable) {
165+
(true, true) => {
166+
self.message_header_mut().num_required_signatures += 1;
167+
// The account is added at the end of the writable and signer accounts
168+
(self.message_header_mut().num_required_signatures
169+
- self.message_header_mut().num_readonly_signed_accounts)
170+
as usize
171+
- 1
172+
},
173+
(true, false) => {
174+
self.message_header_mut().num_required_signatures += 1;
175+
self.message_header_mut().num_readonly_signed_accounts += 1;
176+
// The account is added at the end of the read-only and signer accounts
177+
self.message_header_mut().num_required_signatures as usize - 1
178+
},
179+
(false, true) => {
180+
// The account is added at the end of the writable and non-signer accounts
181+
self.account_keys_mut().len()
182+
- self.message_header_mut().num_readonly_unsigned_accounts as usize
183+
},
184+
(false, false) => {
185+
self.message_header_mut().num_readonly_unsigned_accounts += 1;
186+
// The account is added at the end of the list
187+
self.account_keys_mut().len()
188+
},
189+
};
190+
191+
// Insert the account at the determined position
192+
self.account_keys_mut().insert(insert_at, account.pubkey);
193+
194+
let account_added_at = try_into_u8(insert_at)?;
195+
196+
// Update program ID and account indexes if the new account was added before its position
197+
let instructions = self.instructions_mut();
198+
instructions.iter_mut().for_each(|ix| {
199+
// Update program ID index
200+
if ix.program_id_index >= account_added_at {
201+
ix.program_id_index += 1;
202+
}
203+
204+
// Update account indexes
205+
ix.accounts
206+
.iter_mut()
207+
.filter(|ix_account_id| **ix_account_id >= account_added_at)
208+
.for_each(|ix_account_id| *ix_account_id += 1);
209+
});
210+
211+
Ok(account_added_at)
212+
}
213+
59214
fn push_readonly_unsigned_account(&mut self, account: SolanaAddress) -> SigningResult<u8> {
60215
debug_assert!(
61216
!self.account_keys_mut().contains(&account),

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use crate::address::SolanaAddress;
66
use crate::defined_addresses::{COMPUTE_BUDGET_ADDRESS, SYSTEM_PROGRAM_ID_ADDRESS};
7+
use crate::instruction::Instruction;
78
use crate::modules::insert_instruction::InsertInstruction;
89
use crate::modules::instruction_builder::compute_budget_instruction::{
910
ComputeBudgetInstruction, ComputeBudgetInstructionBuilder, UnitLimit, UnitPrice,
@@ -173,6 +174,27 @@ impl SolanaTransaction {
173174
.to_base64()
174175
.tw_err(SigningErrorType::Error_internal)
175176
}
177+
178+
pub fn add_instruction(encoded_tx: &str, instruction: &str) -> SigningResult<String> {
179+
let tx_bytes = base64::decode(encoded_tx, STANDARD)?;
180+
let mut tx: VersionedTransaction =
181+
bincode::deserialize(&tx_bytes).map_err(|_| SigningErrorType::Error_input_parse)?;
182+
183+
let instruction: Instruction =
184+
serde_json::from_str(instruction).map_err(|_| SigningErrorType::Error_input_parse)?;
185+
186+
tx.message.push_instruction(
187+
instruction.program_id,
188+
instruction.accounts,
189+
instruction.data,
190+
)?;
191+
192+
// Set the correct number of zero signatures
193+
let unsigned_tx = VersionedTransaction::unsigned(tx.message);
194+
unsigned_tx
195+
.to_base64()
196+
.tw_err(SigningErrorType::Error_internal)
197+
}
176198
}
177199

178200
fn try_instruction_as_compute_budget(

0 commit comments

Comments
 (0)