Skip to main content

thornode_pulse_wire/
derive.rs

1//! Fields a client can compute from a decoded transaction.
2//!
3//! These are deliberately NOT on the wire. `fee_payer` is `account_keys[0]`,
4//! `program_ids` resolve each instruction's `program_id_index`, the static
5//! writable set falls out of the three header counts, and the ComputeBudget
6//! instruction data is already carried verbatim. Shipping them as TLVs would
7//! spend wire bytes and single-threaded hot-path encode time to save a caller
8//! a few lines.
9
10use crate::frame::FullTx;
11
12/// `ComputeBudget111111111111111111111111111111`, derived with a base58 decoder
13/// validated against `transaction::VOTE_PROGRAM_ID`.
14pub 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
19/// The fee payer is always the first account key.
20pub fn fee_payer(tx: &FullTx) -> Option<[u8; 32]> {
21    tx.account_keys.first().copied()
22}
23
24/// Every program the transaction invokes, in first-use order, deduplicated.
25/// Solana forbids an ALT-sourced program id, so this is complete without any
26/// lookup-table resolution.
27pub 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
39/// Writable accounts drawn from the STATIC key array only. ALT-loaded writables
40/// arrive separately in the frame's `loaded_writable` TLV.
41pub 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
70/// Micro-lamports per compute unit from `SetComputeUnitPrice` (discriminator 3).
71/// `None` means the transaction set no price — NOT zero.
72pub 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
77/// Explicit `SetComputeUnitLimit` (discriminator 2) only. `None` means the
78/// transaction set no limit; no implicit per-instruction default is applied.
79pub 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        // 4 keys, 2 signers of which 1 readonly, 1 readonly unsigned.
165        // writable signers   = [0, nrs - nrsa)          = [0,1)  -> key 0
166        // writable unsigned  = [nrs, len - nrua)        = [2,3)  -> key 2
167        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        // discriminator 3 = SetComputeUnitPrice(u64), 2 = SetComputeUnitLimit(u32)
180        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        // An absent value must remain distinguishable from an explicit zero.
209        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}