Skip to main content

sol_parser_sdk/instr/
utils.rs

1//! 指令解析通用工具函数
2
3use crate::core::events::EventMetadata;
4use solana_sdk::{pubkey::Pubkey, signature::Signature};
5use yellowstone_grpc_proto::prelude::{Transaction, TransactionStatusMeta};
6
7/// 创建事件元数据的通用函数
8pub fn create_metadata(
9    signature: Signature,
10    slot: u64,
11    tx_index: u64,
12    block_time_us: i64,
13    grpc_recv_us: i64,
14) -> EventMetadata {
15    EventMetadata { signature, slot, tx_index, block_time_us, grpc_recv_us, recent_blockhash: None }
16}
17
18/// 创建事件元数据的兼容性函数(用于指令解析)
19#[inline(always)]
20pub fn create_metadata_simple(
21    signature: Signature,
22    slot: u64,
23    tx_index: u64,
24    block_time_us: Option<i64>,
25    _program_id: Pubkey,
26) -> EventMetadata {
27    let current_time = now_us();
28
29    EventMetadata {
30        signature,
31        slot,
32        tx_index,
33        block_time_us: block_time_us.unwrap_or(0),
34        grpc_recv_us: current_time,
35        recent_blockhash: None,
36    }
37}
38
39/// 从指令数据中读取 u64(小端序)- SIMD 优化
40#[inline(always)]
41pub fn read_u64_le(data: &[u8], offset: usize) -> Option<u64> {
42    data.get(offset..offset.checked_add(8)?)
43        .map(|slice| u64::from_le_bytes(slice.try_into().unwrap()))
44}
45
46/// 从指令数据中读取 u32(小端序)- SIMD 优化
47#[inline(always)]
48pub fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
49    data.get(offset..offset.checked_add(4)?)
50        .map(|slice| u32::from_le_bytes(slice.try_into().unwrap()))
51}
52
53/// Read a little-endian `i64` from instruction data.
54#[inline(always)]
55pub fn read_i64_le(data: &[u8], offset: usize) -> Option<i64> {
56    data.get(offset..offset.checked_add(8)?)
57        .map(|slice| i64::from_le_bytes(slice.try_into().unwrap()))
58}
59
60/// 从指令数据中读取 u16(小端序)- SIMD 优化
61#[inline(always)]
62pub fn read_u16_le(data: &[u8], offset: usize) -> Option<u16> {
63    data.get(offset..offset.checked_add(2)?)
64        .map(|slice| u16::from_le_bytes(slice.try_into().unwrap()))
65}
66
67/// 从指令数据中读取 u8
68#[inline(always)]
69pub fn read_u8(data: &[u8], offset: usize) -> Option<u8> {
70    data.get(offset).copied()
71}
72
73/// 从指令数据中读取 i32(小端序)- SIMD 优化
74#[inline(always)]
75pub fn read_i32_le(data: &[u8], offset: usize) -> Option<i32> {
76    data.get(offset..offset.checked_add(4)?)
77        .map(|slice| i32::from_le_bytes(slice.try_into().unwrap()))
78}
79
80/// 从指令数据中读取 u128(小端序)- SIMD 优化
81#[inline(always)]
82pub fn read_u128_le(data: &[u8], offset: usize) -> Option<u128> {
83    data.get(offset..offset.checked_add(16)?)
84        .map(|slice| u128::from_le_bytes(slice.try_into().unwrap()))
85}
86
87/// 从指令数据中读取布尔值
88#[inline(always)]
89pub fn read_bool(data: &[u8], offset: usize) -> Option<bool> {
90    read_option_bool_idl(data, offset)
91}
92
93/// IDL 自定义类型 `OptionBool`(Anchor:`struct { bool }`)在 **指令参数** 中与 `bool` 相同,Borsh 仅占 **1 字节**。
94/// 勿与 Rust `Option<bool>` 的 Borsh 编码(discriminator + inner,共 2 字节)混淆。
95#[inline(always)]
96pub fn read_option_bool_idl(data: &[u8], offset: usize) -> Option<bool> {
97    match data.get(offset).copied()? {
98        0 => Some(false),
99        1 => Some(true),
100        _ => None,
101    }
102}
103
104/// IDL custom type `OptionU64` is a one-field struct and uses the same 8-byte
105/// little-endian representation as `u64` when present.
106#[inline(always)]
107pub fn read_option_u64_idl(data: &[u8], offset: usize) -> Option<u64> {
108    read_u64_le(data, offset)
109}
110
111/// 从指令数据中读取公钥 - SIMD 优化
112#[inline(always)]
113pub fn read_pubkey(data: &[u8], offset: usize) -> Option<Pubkey> {
114    data.get(offset..offset.checked_add(32)?).and_then(|slice| Pubkey::try_from(slice).ok())
115}
116
117/// 从账户列表中获取账户
118#[inline(always)]
119pub fn get_account(accounts: &[Pubkey], index: usize) -> Option<Pubkey> {
120    accounts.get(index).copied()
121}
122
123/// Legacy amount-ratio helper in basis points. Both amounts must share units;
124/// this does not estimate swap slippage between different token mints.
125pub fn calculate_slippage_bps(amount_in: u64, amount_out_min: u64) -> u16 {
126    if amount_in == 0 {
127        return 0;
128    }
129
130    // 简化的滑点计算
131    let slippage =
132        (u128::from(amount_in.saturating_sub(amount_out_min)) * 10000) / u128::from(amount_in);
133    slippage.min(10000) as u16
134}
135
136/// 计算价格影响基点
137pub fn calculate_price_impact_bps(_amount_in: u64, amount_out: u64, expected_out: u64) -> u16 {
138    if expected_out == 0 {
139        return 0;
140    }
141
142    let impact =
143        (u128::from(expected_out.saturating_sub(amount_out)) * 10000) / u128::from(expected_out);
144    impact.min(10000) as u16
145}
146
147/// Read bytes from instruction data
148pub fn read_bytes(data: &[u8], offset: usize, length: usize) -> Option<&[u8]> {
149    data.get(offset..offset.checked_add(length)?)
150}
151
152/// `create_v2` instruction payload without the discriminator. All fields after
153/// `is_mayhem_mode` are trailing and optional for backward compatibility.
154/// `mint` / `bonding_curve` / `user` 在账户里,不在 data 中。
155#[inline]
156pub fn parse_create_v2_tail_fields(
157    data_after_discriminator: &[u8],
158) -> Option<(Pubkey, bool, bool, u64, bool)> {
159    let mut offset = 0usize;
160    let (_, l) = read_str_unchecked(data_after_discriminator, offset)?;
161    offset += l;
162    let (_, l) = read_str_unchecked(data_after_discriminator, offset)?;
163    offset += l;
164    let (_, l) = read_str_unchecked(data_after_discriminator, offset)?;
165    offset += l;
166    if data_after_discriminator.len() < offset + 32 + 1 {
167        return None;
168    }
169    let creator = read_pubkey(data_after_discriminator, offset)?;
170    offset += 32;
171    let is_mayhem_mode = read_option_bool_idl(data_after_discriminator, offset)?;
172    offset += 1;
173    let (is_cashback_enabled, creator_fee_bps, is_holder_reward) =
174        parse_create_v2_optional_tail(&data_after_discriminator[offset..])?;
175    Some((creator, is_mayhem_mode, is_cashback_enabled, creator_fee_bps, is_holder_reward))
176}
177
178/// Legacy tails may end between fields, but never within a field.
179/// Reject incomplete fee bytes instead of interpreting their first byte as a flag.
180#[inline]
181pub(crate) fn parse_create_v2_optional_tail(tail: &[u8]) -> Option<(bool, u64, bool)> {
182    if tail.is_empty() {
183        return Some((false, 0, false));
184    }
185    let cashback = read_option_bool_idl(tail, 0)?;
186    if tail.len() == 1 {
187        return Some((cashback, 0, false));
188    }
189    let fee = read_u64_le(tail, 1)?;
190    let holder_reward = if tail.len() == 9 { false } else { read_option_bool_idl(tail, 9)? };
191    Some((cashback, fee, holder_reward))
192}
193
194/// Read string with 4-byte length prefix (Borsh format)
195/// Returns (string slice, total bytes consumed including length prefix)
196#[inline]
197pub fn read_str_unchecked(data: &[u8], offset: usize) -> Option<(&str, usize)> {
198    let tail = data.get(offset..)?;
199    let len = read_u32_le(tail, 0)? as usize;
200    let bytes = tail.get(4..)?.get(..len)?;
201    Some((std::str::from_utf8(bytes).ok()?, len.checked_add(4)?))
202}
203
204/// Read a Borsh Vec<u64>: u32 element count followed by little-endian values.
205/// Validate the complete byte range before allocating, including untrusted counts.
206pub fn read_vec_u64(data: &[u8], offset: usize) -> Option<Vec<u64>> {
207    let tail = data.get(offset..)?;
208    let count = read_u32_le(tail, 0)? as usize;
209    let bytes = tail.get(4..)?.get(..count.checked_mul(8)?)?;
210    Some(bytes.chunks_exact(8).map(|value| u64::from_le_bytes(value.try_into().unwrap())).collect())
211}
212
213/// 快速读取 Pubkey(从字节数组)
214#[inline(always)]
215pub fn read_pubkey_fast(bytes: &[u8]) -> Pubkey {
216    crate::logs::utils::read_pubkey(bytes, 0).unwrap_or_default()
217}
218
219/// 获取指令账户访问器
220/// 返回一个可以通过索引获取 Pubkey 的闭包
221pub fn get_instruction_account_getter<'a>(
222    meta: &'a TransactionStatusMeta,
223    transaction: &'a Option<Transaction>,
224    account_keys: Option<&'a Vec<Vec<u8>>>,
225    // 地址表
226    loaded_writable_addresses: &'a [Vec<u8>],
227    loaded_readonly_addresses: &'a [Vec<u8>],
228    index: &(i32, i32), // (outer_index, inner_index)
229) -> Option<impl Fn(usize) -> Pubkey + 'a> {
230    // 1. 获取指令的账户索引数组
231    let accounts = if index.1 >= 0 {
232        // 内层指令 - 使用二分查找优化 (inner_instructions 按 index 升序排列)
233        let outer_idx = index.0 as u32;
234        meta.inner_instructions
235            .binary_search_by_key(&outer_idx, |i| i.index)
236            .ok()
237            .and_then(|pos| meta.inner_instructions.get(pos))
238            .or_else(|| {
239                // 回退到线性查找(以防数据未排序)
240                meta.inner_instructions.iter().find(|i| i.index == outer_idx)
241            })?
242            .instructions
243            .get(index.1 as usize)?
244            .accounts
245            .as_slice()
246    } else {
247        // 外层指令
248        transaction
249            .as_ref()?
250            .message
251            .as_ref()?
252            .instructions
253            .get(index.0 as usize)?
254            .accounts
255            .as_slice()
256    };
257
258    // 2. 创建高性能的账户查找闭包
259    Some(move |acc_index: usize| -> Pubkey {
260        // 获取账户在交易中的索引
261        let account_index = match accounts.get(acc_index) {
262            Some(&idx) => idx as usize,
263            None => return Pubkey::default(),
264        };
265        // 早期返回优化
266        let Some(keys) = account_keys else {
267            return Pubkey::default();
268        };
269        // 主账户列表
270        if let Some(key_bytes) = keys.get(account_index) {
271            return Pubkey::try_from(key_bytes.as_slice()).unwrap_or_default();
272        }
273        // 可写地址
274        let writable_offset = account_index.saturating_sub(keys.len());
275        if let Some(key_bytes) = loaded_writable_addresses.get(writable_offset) {
276            return Pubkey::try_from(key_bytes.as_slice()).unwrap_or_default();
277        }
278        // 只读地址
279        let readonly_offset = writable_offset.saturating_sub(loaded_writable_addresses.len());
280        if let Some(key_bytes) = loaded_readonly_addresses.get(readonly_offset) {
281            return Pubkey::try_from(key_bytes.as_slice()).unwrap_or_default();
282        }
283        Pubkey::default()
284    })
285}
286
287use crate::core::clock::now_us;
288/// 预构建的 inner_instructions 索引,用于 O(1) 查找
289use std::collections::HashMap;
290
291/// InnerInstructions 索引缓存
292pub struct InnerInstructionsIndex<'a> {
293    /// outer_index -> &InnerInstructions
294    index_map: HashMap<u32, &'a yellowstone_grpc_proto::prelude::InnerInstructions>,
295}
296
297impl<'a> InnerInstructionsIndex<'a> {
298    /// 从 TransactionStatusMeta 构建索引
299    #[inline]
300    pub fn new(meta: &'a TransactionStatusMeta) -> Self {
301        let mut index_map = HashMap::with_capacity(meta.inner_instructions.len());
302        for inner in &meta.inner_instructions {
303            index_map.insert(inner.index, inner);
304        }
305        Self { index_map }
306    }
307
308    /// O(1) 查找 inner_instructions
309    #[inline]
310    pub fn get(
311        &self,
312        outer_index: u32,
313    ) -> Option<&'a yellowstone_grpc_proto::prelude::InnerInstructions> {
314        self.index_map.get(&outer_index).copied()
315    }
316}
317
318/// 使用预构建索引的账户获取器(O(1) 查找)
319pub fn get_instruction_account_getter_indexed<'a>(
320    inner_index: &InnerInstructionsIndex<'a>,
321    transaction: &'a Option<Transaction>,
322    account_keys: Option<&'a Vec<Vec<u8>>>,
323    loaded_writable_addresses: &'a [Vec<u8>],
324    loaded_readonly_addresses: &'a [Vec<u8>],
325    index: &(i32, i32),
326) -> Option<impl Fn(usize) -> Pubkey + 'a> {
327    let accounts = if index.1 >= 0 {
328        // O(1) 查找
329        inner_index.get(index.0 as u32)?.instructions.get(index.1 as usize)?.accounts.as_slice()
330    } else {
331        transaction
332            .as_ref()?
333            .message
334            .as_ref()?
335            .instructions
336            .get(index.0 as usize)?
337            .accounts
338            .as_slice()
339    };
340
341    Some(move |acc_index: usize| -> Pubkey {
342        let account_index = match accounts.get(acc_index) {
343            Some(&idx) => idx as usize,
344            None => return Pubkey::default(),
345        };
346        let Some(keys) = account_keys else {
347            return Pubkey::default();
348        };
349        if let Some(key_bytes) = keys.get(account_index) {
350            return Pubkey::try_from(key_bytes.as_slice()).unwrap_or_default();
351        }
352        let writable_offset = account_index.saturating_sub(keys.len());
353        if let Some(key_bytes) = loaded_writable_addresses.get(writable_offset) {
354            return Pubkey::try_from(key_bytes.as_slice()).unwrap_or_default();
355        }
356        let readonly_offset = writable_offset.saturating_sub(loaded_writable_addresses.len());
357        if let Some(key_bytes) = loaded_readonly_addresses.get(readonly_offset) {
358            return Pubkey::try_from(key_bytes.as_slice()).unwrap_or_default();
359        }
360        Pubkey::default()
361    })
362}
363
364#[cfg(test)]
365mod option_bool_tests {
366    use super::*;
367
368    #[test]
369    fn read_option_bool_idl_strict() {
370        assert_eq!(read_option_bool_idl(&[0], 0), Some(false));
371        assert_eq!(read_option_bool_idl(&[1], 0), Some(true));
372        assert_eq!(read_option_bool_idl(&[2], 0), None);
373    }
374
375    #[test]
376    fn parse_create_v2_tail_matches_anchor_len() {
377        // name "a", "b", "c" + creator (32) + mayhem (1) + OptionBool cashback (1) = 49 bytes payload
378        let mut p = Vec::new();
379        p.extend_from_slice(&(1u32.to_le_bytes()));
380        p.push(b'a');
381        p.extend_from_slice(&(1u32.to_le_bytes()));
382        p.push(b'b');
383        p.extend_from_slice(&(1u32.to_le_bytes()));
384        p.push(b'c');
385        p.extend_from_slice(&[0u8; 32]);
386        p.push(1u8); // mayhem
387        p.push(1u8); // cashback
388        assert_eq!(p.len(), 49);
389        let (creator, mayhem, cb, creator_fee_bps, holder_reward) =
390            parse_create_v2_tail_fields(&p).expect("parse");
391        assert_eq!(creator, Pubkey::default());
392        assert!(mayhem);
393        assert!(cb);
394        assert_eq!(creator_fee_bps, 0);
395        assert!(!holder_reward);
396    }
397
398    #[test]
399    fn parse_create_v2_tail_reads_holder_rewards_fields() {
400        let mut p = Vec::new();
401        for value in ["a", "b", "c"] {
402            p.extend_from_slice(&(value.len() as u32).to_le_bytes());
403            p.extend_from_slice(value.as_bytes());
404        }
405        p.extend_from_slice(&[0u8; 32]);
406        p.push(0); // mayhem
407        p.push(0); // cashback (deprecated)
408        p.extend_from_slice(&250u64.to_le_bytes());
409        p.push(1); // holder rewards
410
411        let (_, mayhem, cashback, creator_fee_bps, holder_reward) =
412            parse_create_v2_tail_fields(&p).expect("parse");
413        assert!(!mayhem);
414        assert!(!cashback);
415        assert_eq!(creator_fee_bps, 250);
416        assert!(holder_reward);
417    }
418}
419
420#[cfg(test)]
421mod review_tail_regressions {
422    use super::*;
423    #[test]
424    fn optional_create_tail_rejects_partial_fee_and_invalid_boolean() {
425        for n in 1..8 {
426            let mut tail = vec![0];
427            tail.extend(std::iter::repeat_n(1, n));
428            assert!(parse_create_v2_optional_tail(&tail).is_none());
429        }
430        assert_eq!(parse_create_v2_optional_tail(&[]), Some((false, 0, false)));
431        assert_eq!(parse_create_v2_optional_tail(&[1]), Some((true, 0, false)));
432        let mut tail = vec![0];
433        tail.extend_from_slice(&1u64.to_le_bytes());
434        assert_eq!(parse_create_v2_optional_tail(&tail), Some((false, 1, false)));
435        tail.push(2);
436        assert!(parse_create_v2_optional_tail(&tail).is_none());
437        tail[9] = 1;
438        assert_eq!(parse_create_v2_optional_tail(&tail), Some((false, 1, true)));
439        tail[0] = 2;
440        assert!(parse_create_v2_optional_tail(&tail).is_none());
441    }
442}
443
444#[cfg(test)]
445mod review_numeric_bounds_tests {
446    use super::*;
447
448    #[test]
449    fn oversized_offsets_and_lengths_return_none_without_panicking() {
450        let data = [0u8; 64];
451        for offset in [65, usize::MAX - 32, usize::MAX - 1, usize::MAX] {
452            assert_eq!(read_u16_le(&data, offset), None);
453            assert_eq!(read_u32_le(&data, offset), None);
454            assert_eq!(read_i32_le(&data, offset), None);
455            assert_eq!(read_u64_le(&data, offset), None);
456            assert_eq!(read_i64_le(&data, offset), None);
457            assert_eq!(read_u128_le(&data, offset), None);
458            assert_eq!(read_pubkey(&data, offset), None);
459            assert_eq!(read_str_unchecked(&data, offset), None);
460            assert_eq!(read_bytes(&data, offset, 8), None);
461            assert_eq!(read_vec_u64(&data, offset), None);
462        }
463        assert_eq!(read_bytes(&data, 1, usize::MAX), None);
464        assert_eq!(read_str_unchecked(&u32::MAX.to_le_bytes(), 0), None);
465        assert_eq!(read_str_unchecked(&[1, 0, 0, 0, 255], 0), None);
466        assert_eq!(read_str_unchecked(&[1, 0, 0, 0, b'x'], 0), Some(("x", 5)));
467    }
468
469    #[test]
470    fn borsh_vector_reads_values_and_rejects_truncation_before_allocation() {
471        let values = [0u64, 7, u64::MAX];
472        let mut data = vec![99, 99];
473        data.extend_from_slice(&3u32.to_le_bytes());
474        for value in values {
475            data.extend_from_slice(&value.to_le_bytes());
476        }
477        assert_eq!(read_vec_u64(&data, 2), Some(values.to_vec()));
478        for end in 0..data.len() {
479            assert_eq!(read_vec_u64(&data[..end], 2), None);
480        }
481        assert_eq!(read_vec_u64(&0u32.to_le_bytes(), 0), Some(Vec::new()));
482        assert_eq!(read_vec_u64(&u32::MAX.to_le_bytes(), 0), None);
483    }
484
485    #[test]
486    fn bps_ratio_is_bounded_and_correct_for_full_u64_range() {
487        for denominator in [0, 1, 10_000, u64::MAX / 2, u64::MAX] {
488            for amount in [0, 1, 10_000, u64::MAX / 2, u64::MAX] {
489                let expected = if denominator == 0 {
490                    0
491                } else {
492                    (u128::from(denominator.saturating_sub(amount)) * 10_000
493                        / u128::from(denominator)) as u16
494                };
495                assert_eq!(calculate_slippage_bps(denominator, amount), expected);
496                assert_eq!(calculate_price_impact_bps(0, amount, denominator), expected);
497            }
498        }
499    }
500}
501
502#[cfg(test)]
503mod transaction_account_key_length_regressions {
504    use super::*;
505    use yellowstone_grpc_proto::prelude::{CompiledInstruction, Message};
506
507    #[test]
508    fn both_getters_keep_malformed_static_and_alt_keys_unresolved() {
509        let expected = Pubkey::new_unique();
510        for source in 0..3 {
511            for length in [0, 31, 32, 33, 64] {
512                let mut bytes = expected.to_bytes().to_vec();
513                bytes.resize(length, 0);
514                let keys =
515                    vec![if source == 0 { bytes.clone() } else { expected.to_bytes().to_vec() }];
516                let writable =
517                    vec![if source == 1 { bytes.clone() } else { expected.to_bytes().to_vec() }];
518                let readonly = vec![if source == 2 { bytes } else { expected.to_bytes().to_vec() }];
519                let tx = Some(Transaction {
520                    message: Some(Message {
521                        account_keys: keys.clone(),
522                        instructions: vec![CompiledInstruction {
523                            accounts: vec![0, 1, 2],
524                            ..Default::default()
525                        }],
526                        ..Default::default()
527                    }),
528                    ..Default::default()
529                });
530                let meta = TransactionStatusMeta::default();
531                let index = InnerInstructionsIndex::new(&meta);
532                let position = (0, -1);
533                let get = get_instruction_account_getter(
534                    &meta,
535                    &tx,
536                    Some(&keys),
537                    &writable,
538                    &readonly,
539                    &position,
540                )
541                .unwrap();
542                let indexed = get_instruction_account_getter_indexed(
543                    &index,
544                    &tx,
545                    Some(&keys),
546                    &writable,
547                    &readonly,
548                    &position,
549                )
550                .unwrap();
551                let actual = if length == 32 { expected } else { Pubkey::default() };
552                assert_eq!(get(source), actual);
553                assert_eq!(indexed(source), actual);
554                assert_eq!(get(3), Pubkey::default());
555            }
556        }
557    }
558}
559
560#[cfg(test)]
561mod review_canonical_bool_regressions {
562    use super::*;
563    #[test]
564    fn bool_readers_accept_only_borsh_zero_and_one() {
565        for byte in 0..=u8::MAX {
566            let expected = match byte { 0 => Some(false), 1 => Some(true), _ => None };
567            assert_eq!(read_bool(&[byte],0),expected);
568            assert_eq!(read_option_bool_idl(&[byte],0),expected);
569        }
570        assert_eq!(read_bool(&[],0),None);
571        assert_eq!(read_bool(&[0],usize::MAX),None);
572    }
573}