1use crate::frame::FullTx;
11
12pub const COMPUTE_BUDGET_PROGRAM_ID: [u8; 32] = [
15 3, 6, 70, 111, 229, 33, 23, 50, 255, 236, 173, 186, 114, 195, 155, 231, 188, 140, 229, 187,
16 197, 247, 18, 107, 44, 67, 155, 58, 64, 0, 0, 0,
17];
18
19pub fn fee_payer(tx: &FullTx) -> Option<[u8; 32]> {
21 tx.account_keys.first().copied()
22}
23
24pub fn program_ids(tx: &FullTx) -> Vec<[u8; 32]> {
28 let mut out: Vec<[u8; 32]> = Vec::new();
29 for ix in &tx.instructions {
30 if let Some(k) = tx.account_keys.get(ix.program_id_index as usize) {
31 if !out.contains(k) {
32 out.push(*k);
33 }
34 }
35 }
36 out
37}
38
39pub fn static_writable_accounts(tx: &FullTx) -> Vec<[u8; 32]> {
42 let n = tx.account_keys.len();
43 let nrs = tx.num_required_signatures as usize;
44 let nrsa = tx.num_readonly_signed_accounts as usize;
45 let nrua = tx.num_readonly_unsigned_accounts as usize;
46 let mut out = Vec::new();
47 for i in 0..nrs.saturating_sub(nrsa).min(n) {
48 out.push(tx.account_keys[i]);
49 }
50 let unsigned_end = n.saturating_sub(nrua);
51 for i in nrs.min(n)..unsigned_end {
52 out.push(tx.account_keys[i]);
53 }
54 out
55}
56
57fn compute_budget_ix(tx: &FullTx, discriminator: u8) -> Option<&[u8]> {
58 for ix in &tx.instructions {
59 let is_cb = tx
60 .account_keys
61 .get(ix.program_id_index as usize)
62 .is_some_and(|k| *k == COMPUTE_BUDGET_PROGRAM_ID);
63 if is_cb && ix.data.first() == Some(&discriminator) {
64 return Some(&ix.data[1..]);
65 }
66 }
67 None
68}
69
70pub fn compute_unit_price(tx: &FullTx) -> Option<u64> {
73 let d = compute_budget_ix(tx, 3)?;
74 Some(u64::from_le_bytes(d.get(0..8)?.try_into().ok()?))
75}
76
77pub fn compute_unit_limit(tx: &FullTx) -> Option<u32> {
80 let d = compute_budget_ix(tx, 2)?;
81 Some(u32::from_le_bytes(d.get(0..4)?.try_into().ok()?))
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::frame::{FullAtl, FullInstruction, FullTx};
88
89 fn tx_with(
90 keys: Vec<[u8; 32]>,
91 ixs: Vec<FullInstruction>,
92 nrs: u32,
93 nrsa: u32,
94 nrua: u32,
95 ) -> FullTx {
96 FullTx {
97 slot: 1,
98 versioned: false,
99 num_required_signatures: nrs,
100 num_readonly_signed_accounts: nrsa,
101 num_readonly_unsigned_accounts: nrua,
102 recent_blockhash: [0; 32],
103 signatures: vec![[0u8; 64]],
104 account_keys: keys,
105 instructions: ixs,
106 address_table_lookups: Vec::<FullAtl>::new(),
107 }
108 }
109
110 #[test]
111 fn fee_payer_is_the_first_account_key() {
112 let t = tx_with(vec![[0xA1; 32], [0xB2; 32]], vec![], 1, 0, 0);
113 assert_eq!(fee_payer(&t), Some([0xA1; 32]));
114 let empty = tx_with(vec![], vec![], 0, 0, 0);
115 assert_eq!(fee_payer(&empty), None);
116 }
117
118 #[test]
119 fn program_ids_resolve_and_dedup_in_order() {
120 let t = tx_with(
121 vec![[0xA1; 32], [0xB2; 32], [0xC3; 32]],
122 vec![
123 FullInstruction {
124 program_id_index: 2,
125 accounts: vec![],
126 data: vec![],
127 },
128 FullInstruction {
129 program_id_index: 1,
130 accounts: vec![],
131 data: vec![],
132 },
133 FullInstruction {
134 program_id_index: 2,
135 accounts: vec![],
136 data: vec![],
137 },
138 ],
139 1,
140 0,
141 0,
142 );
143 assert_eq!(program_ids(&t), vec![[0xC3; 32], [0xB2; 32]]);
144 }
145
146 #[test]
147 fn program_id_index_out_of_range_is_skipped_not_panicking() {
148 let t = tx_with(
149 vec![[0xA1; 32]],
150 vec![FullInstruction {
151 program_id_index: 9,
152 accounts: vec![],
153 data: vec![],
154 }],
155 1,
156 0,
157 0,
158 );
159 assert!(program_ids(&t).is_empty());
160 }
161
162 #[test]
163 fn static_writable_follows_the_header_counts() {
164 let t = tx_with(
168 vec![[0u8; 32], [1u8; 32], [2u8; 32], [3u8; 32]],
169 vec![],
170 2,
171 1,
172 1,
173 );
174 assert_eq!(static_writable_accounts(&t), vec![[0u8; 32], [2u8; 32]]);
175 }
176
177 #[test]
178 fn compute_budget_price_and_limit_are_parsed() {
179 let mut price_data = vec![3u8];
181 price_data.extend_from_slice(&7_500u64.to_le_bytes());
182 let mut limit_data = vec![2u8];
183 limit_data.extend_from_slice(&200_000u32.to_le_bytes());
184 let t = tx_with(
185 vec![[0xA1; 32], COMPUTE_BUDGET_PROGRAM_ID],
186 vec![
187 FullInstruction {
188 program_id_index: 1,
189 accounts: vec![],
190 data: price_data,
191 },
192 FullInstruction {
193 program_id_index: 1,
194 accounts: vec![],
195 data: limit_data,
196 },
197 ],
198 1,
199 0,
200 0,
201 );
202 assert_eq!(compute_unit_price(&t), Some(7_500));
203 assert_eq!(compute_unit_limit(&t), Some(200_000));
204 }
205
206 #[test]
207 fn compute_budget_absent_returns_none_not_a_default() {
208 let t = tx_with(vec![[0xA1; 32]], vec![], 1, 0, 0);
210 assert_eq!(compute_unit_price(&t), None);
211 assert_eq!(compute_unit_limit(&t), None);
212 }
213
214 #[test]
215 fn truncated_compute_budget_data_is_ignored() {
216 let t = tx_with(
217 vec![[0xA1; 32], COMPUTE_BUDGET_PROGRAM_ID],
218 vec![FullInstruction {
219 program_id_index: 1,
220 accounts: vec![],
221 data: vec![3, 1, 2],
222 }],
223 1,
224 0,
225 0,
226 );
227 assert_eq!(compute_unit_price(&t), None);
228 }
229
230 #[test]
231 fn compute_budget_program_id_shape() {
232 assert_eq!(COMPUTE_BUDGET_PROGRAM_ID.len(), 32);
233 assert_eq!(COMPUTE_BUDGET_PROGRAM_ID[0], 0x03);
234 }
235}