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 + 8).map(|slice| u64::from_le_bytes(slice.try_into().unwrap()))
43}
44
45/// 从指令数据中读取 u32(小端序)- SIMD 优化
46#[inline(always)]
47pub fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
48    data.get(offset..offset + 4).map(|slice| u32::from_le_bytes(slice.try_into().unwrap()))
49}
50
51/// Read a little-endian `i64` from instruction data.
52#[inline(always)]
53pub fn read_i64_le(data: &[u8], offset: usize) -> Option<i64> {
54    data.get(offset..offset + 8).map(|slice| i64::from_le_bytes(slice.try_into().unwrap()))
55}
56
57/// 从指令数据中读取 u16(小端序)- SIMD 优化
58#[inline(always)]
59pub fn read_u16_le(data: &[u8], offset: usize) -> Option<u16> {
60    data.get(offset..offset + 2).map(|slice| u16::from_le_bytes(slice.try_into().unwrap()))
61}
62
63/// 从指令数据中读取 u8
64#[inline(always)]
65pub fn read_u8(data: &[u8], offset: usize) -> Option<u8> {
66    data.get(offset).copied()
67}
68
69/// 从指令数据中读取 i32(小端序)- SIMD 优化
70#[inline(always)]
71pub fn read_i32_le(data: &[u8], offset: usize) -> Option<i32> {
72    data.get(offset..offset + 4).map(|slice| i32::from_le_bytes(slice.try_into().unwrap()))
73}
74
75/// 从指令数据中读取 u128(小端序)- SIMD 优化
76#[inline(always)]
77pub fn read_u128_le(data: &[u8], offset: usize) -> Option<u128> {
78    data.get(offset..offset + 16).map(|slice| u128::from_le_bytes(slice.try_into().unwrap()))
79}
80
81/// 从指令数据中读取布尔值
82#[inline(always)]
83pub fn read_bool(data: &[u8], offset: usize) -> Option<bool> {
84    data.get(offset).map(|&b| b != 0)
85}
86
87/// IDL 自定义类型 `OptionBool`(Anchor:`struct { bool }`)在 **指令参数** 中与 `bool` 相同,Borsh 仅占 **1 字节**。
88/// 勿与 Rust `Option<bool>` 的 Borsh 编码(discriminator + inner,共 2 字节)混淆。
89#[inline(always)]
90pub fn read_option_bool_idl(data: &[u8], offset: usize) -> Option<bool> {
91    match data.get(offset).copied()? {
92        0 => Some(false),
93        1 => Some(true),
94        _ => None,
95    }
96}
97
98/// IDL custom type `OptionU64` is a one-field struct and uses the same 8-byte
99/// little-endian representation as `u64` when present.
100#[inline(always)]
101pub fn read_option_u64_idl(data: &[u8], offset: usize) -> Option<u64> {
102    read_u64_le(data, offset)
103}
104
105/// 从指令数据中读取公钥 - SIMD 优化
106#[inline(always)]
107pub fn read_pubkey(data: &[u8], offset: usize) -> Option<Pubkey> {
108    data.get(offset..offset + 32).and_then(|slice| Pubkey::try_from(slice).ok())
109}
110
111/// 从账户列表中获取账户
112#[inline(always)]
113pub fn get_account(accounts: &[Pubkey], index: usize) -> Option<Pubkey> {
114    accounts.get(index).copied()
115}
116
117/// 计算滑点基点
118pub fn calculate_slippage_bps(amount_in: u64, amount_out_min: u64) -> u16 {
119    if amount_in == 0 {
120        return 0;
121    }
122
123    // 简化的滑点计算
124    let slippage = ((amount_in.saturating_sub(amount_out_min)) * 10000) / amount_in;
125    slippage.min(10000) as u16
126}
127
128/// 计算价格影响基点
129pub fn calculate_price_impact_bps(_amount_in: u64, amount_out: u64, expected_out: u64) -> u16 {
130    if expected_out == 0 {
131        return 0;
132    }
133
134    let impact = ((expected_out.saturating_sub(amount_out)) * 10000) / expected_out;
135    impact.min(10000) as u16
136}
137
138/// Read bytes from instruction data
139pub fn read_bytes(data: &[u8], offset: usize, length: usize) -> Option<&[u8]> {
140    if data.len() < offset + length {
141        return None;
142    }
143    Some(&data[offset..offset + length])
144}
145
146/// `create_v2` instruction payload without the discriminator. All fields after
147/// `is_mayhem_mode` are trailing and optional for backward compatibility.
148/// `mint` / `bonding_curve` / `user` 在账户里,不在 data 中。
149#[inline]
150pub fn parse_create_v2_tail_fields(
151    data_after_discriminator: &[u8],
152) -> Option<(Pubkey, bool, bool, u64, bool)> {
153    let mut offset = 0usize;
154    let (_, l) = read_str_unchecked(data_after_discriminator, offset)?;
155    offset += l;
156    let (_, l) = read_str_unchecked(data_after_discriminator, offset)?;
157    offset += l;
158    let (_, l) = read_str_unchecked(data_after_discriminator, offset)?;
159    offset += l;
160    if data_after_discriminator.len() < offset + 32 + 1 {
161        return None;
162    }
163    let creator = read_pubkey(data_after_discriminator, offset)?;
164    offset += 32;
165    let is_mayhem_mode = read_bool(data_after_discriminator, offset)?;
166    offset += 1;
167    let is_cashback_enabled = if offset < data_after_discriminator.len() {
168        read_option_bool_idl(data_after_discriminator, offset).unwrap_or(false)
169    } else {
170        false
171    };
172    if offset < data_after_discriminator.len() {
173        offset += 1;
174    }
175    let creator_fee_bps = read_option_u64_idl(data_after_discriminator, offset).unwrap_or_default();
176    if offset + 8 <= data_after_discriminator.len() {
177        offset += 8;
178    }
179    let is_holder_reward =
180        read_option_bool_idl(data_after_discriminator, offset).unwrap_or_default();
181    Some((creator, is_mayhem_mode, is_cashback_enabled, creator_fee_bps, is_holder_reward))
182}
183
184/// Read string with 4-byte length prefix (Borsh format)
185/// Returns (string slice, total bytes consumed including length prefix)
186#[inline]
187pub fn read_str_unchecked(data: &[u8], offset: usize) -> Option<(&str, usize)> {
188    if data.len() < offset + 4 {
189        return None;
190    }
191    let len = u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?) as usize;
192    if data.len() < offset + 4 + len {
193        return None;
194    }
195    let string_bytes = &data[offset + 4..offset + 4 + len];
196    let s = std::str::from_utf8(string_bytes).ok()?;
197    Some((s, 4 + len))
198}
199
200/// 从指令数据中读取u64向量(简化版本)
201pub fn read_vec_u64(_data: &[u8], _offset: usize) -> Option<Vec<u64>> {
202    // 简化版本:返回默认的两个元素向量
203    // 实际实现需要根据具体的数据格式来解析
204    Some(vec![0, 0])
205}
206
207/// 快速读取 Pubkey(从字节数组)
208#[inline(always)]
209pub fn read_pubkey_fast(bytes: &[u8]) -> Pubkey {
210    crate::logs::utils::read_pubkey(bytes, 0).unwrap_or_default()
211}
212
213/// 获取指令账户访问器
214/// 返回一个可以通过索引获取 Pubkey 的闭包
215pub fn get_instruction_account_getter<'a>(
216    meta: &'a TransactionStatusMeta,
217    transaction: &'a Option<Transaction>,
218    account_keys: Option<&'a Vec<Vec<u8>>>,
219    // 地址表
220    loaded_writable_addresses: &'a [Vec<u8>],
221    loaded_readonly_addresses: &'a [Vec<u8>],
222    index: &(i32, i32), // (outer_index, inner_index)
223) -> Option<impl Fn(usize) -> Pubkey + 'a> {
224    // 1. 获取指令的账户索引数组
225    let accounts = if index.1 >= 0 {
226        // 内层指令 - 使用二分查找优化 (inner_instructions 按 index 升序排列)
227        let outer_idx = index.0 as u32;
228        meta.inner_instructions
229            .binary_search_by_key(&outer_idx, |i| i.index)
230            .ok()
231            .and_then(|pos| meta.inner_instructions.get(pos))
232            .or_else(|| {
233                // 回退到线性查找(以防数据未排序)
234                meta.inner_instructions.iter().find(|i| i.index == outer_idx)
235            })?
236            .instructions
237            .get(index.1 as usize)?
238            .accounts
239            .as_slice()
240    } else {
241        // 外层指令
242        transaction
243            .as_ref()?
244            .message
245            .as_ref()?
246            .instructions
247            .get(index.0 as usize)?
248            .accounts
249            .as_slice()
250    };
251
252    // 2. 创建高性能的账户查找闭包
253    Some(move |acc_index: usize| -> Pubkey {
254        // 获取账户在交易中的索引
255        let account_index = match accounts.get(acc_index) {
256            Some(&idx) => idx as usize,
257            None => return Pubkey::default(),
258        };
259        // 早期返回优化
260        let Some(keys) = account_keys else {
261            return Pubkey::default();
262        };
263        // 主账户列表
264        if let Some(key_bytes) = keys.get(account_index) {
265            return read_pubkey_fast(key_bytes);
266        }
267        // 可写地址
268        let writable_offset = account_index.saturating_sub(keys.len());
269        if let Some(key_bytes) = loaded_writable_addresses.get(writable_offset) {
270            return read_pubkey_fast(key_bytes);
271        }
272        // 只读地址
273        let readonly_offset = writable_offset.saturating_sub(loaded_writable_addresses.len());
274        if let Some(key_bytes) = loaded_readonly_addresses.get(readonly_offset) {
275            return read_pubkey_fast(key_bytes);
276        }
277        Pubkey::default()
278    })
279}
280
281use crate::core::clock::now_us;
282/// 预构建的 inner_instructions 索引,用于 O(1) 查找
283use std::collections::HashMap;
284
285/// InnerInstructions 索引缓存
286pub struct InnerInstructionsIndex<'a> {
287    /// outer_index -> &InnerInstructions
288    index_map: HashMap<u32, &'a yellowstone_grpc_proto::prelude::InnerInstructions>,
289}
290
291impl<'a> InnerInstructionsIndex<'a> {
292    /// 从 TransactionStatusMeta 构建索引
293    #[inline]
294    pub fn new(meta: &'a TransactionStatusMeta) -> Self {
295        let mut index_map = HashMap::with_capacity(meta.inner_instructions.len());
296        for inner in &meta.inner_instructions {
297            index_map.insert(inner.index, inner);
298        }
299        Self { index_map }
300    }
301
302    /// O(1) 查找 inner_instructions
303    #[inline]
304    pub fn get(
305        &self,
306        outer_index: u32,
307    ) -> Option<&'a yellowstone_grpc_proto::prelude::InnerInstructions> {
308        self.index_map.get(&outer_index).copied()
309    }
310}
311
312/// 使用预构建索引的账户获取器(O(1) 查找)
313pub fn get_instruction_account_getter_indexed<'a>(
314    inner_index: &InnerInstructionsIndex<'a>,
315    transaction: &'a Option<Transaction>,
316    account_keys: Option<&'a Vec<Vec<u8>>>,
317    loaded_writable_addresses: &'a [Vec<u8>],
318    loaded_readonly_addresses: &'a [Vec<u8>],
319    index: &(i32, i32),
320) -> Option<impl Fn(usize) -> Pubkey + 'a> {
321    let accounts = if index.1 >= 0 {
322        // O(1) 查找
323        inner_index.get(index.0 as u32)?.instructions.get(index.1 as usize)?.accounts.as_slice()
324    } else {
325        transaction
326            .as_ref()?
327            .message
328            .as_ref()?
329            .instructions
330            .get(index.0 as usize)?
331            .accounts
332            .as_slice()
333    };
334
335    Some(move |acc_index: usize| -> Pubkey {
336        let account_index = match accounts.get(acc_index) {
337            Some(&idx) => idx as usize,
338            None => return Pubkey::default(),
339        };
340        let Some(keys) = account_keys else {
341            return Pubkey::default();
342        };
343        if let Some(key_bytes) = keys.get(account_index) {
344            return read_pubkey_fast(key_bytes);
345        }
346        let writable_offset = account_index.saturating_sub(keys.len());
347        if let Some(key_bytes) = loaded_writable_addresses.get(writable_offset) {
348            return read_pubkey_fast(key_bytes);
349        }
350        let readonly_offset = writable_offset.saturating_sub(loaded_writable_addresses.len());
351        if let Some(key_bytes) = loaded_readonly_addresses.get(readonly_offset) {
352            return read_pubkey_fast(key_bytes);
353        }
354        Pubkey::default()
355    })
356}
357
358#[cfg(test)]
359mod option_bool_tests {
360    use super::*;
361
362    #[test]
363    fn read_option_bool_idl_strict() {
364        assert_eq!(read_option_bool_idl(&[0], 0), Some(false));
365        assert_eq!(read_option_bool_idl(&[1], 0), Some(true));
366        assert_eq!(read_option_bool_idl(&[2], 0), None);
367    }
368
369    #[test]
370    fn parse_create_v2_tail_matches_anchor_len() {
371        // name "a", "b", "c" + creator (32) + mayhem (1) + OptionBool cashback (1) = 49 bytes payload
372        let mut p = Vec::new();
373        p.extend_from_slice(&(1u32.to_le_bytes()));
374        p.push(b'a');
375        p.extend_from_slice(&(1u32.to_le_bytes()));
376        p.push(b'b');
377        p.extend_from_slice(&(1u32.to_le_bytes()));
378        p.push(b'c');
379        p.extend_from_slice(&[0u8; 32]);
380        p.push(1u8); // mayhem
381        p.push(1u8); // cashback
382        assert_eq!(p.len(), 49);
383        let (creator, mayhem, cb, creator_fee_bps, holder_reward) =
384            parse_create_v2_tail_fields(&p).expect("parse");
385        assert_eq!(creator, Pubkey::default());
386        assert!(mayhem);
387        assert!(cb);
388        assert_eq!(creator_fee_bps, 0);
389        assert!(!holder_reward);
390    }
391
392    #[test]
393    fn parse_create_v2_tail_reads_holder_rewards_fields() {
394        let mut p = Vec::new();
395        for value in ["a", "b", "c"] {
396            p.extend_from_slice(&(value.len() as u32).to_le_bytes());
397            p.extend_from_slice(value.as_bytes());
398        }
399        p.extend_from_slice(&[0u8; 32]);
400        p.push(0); // mayhem
401        p.push(0); // cashback (deprecated)
402        p.extend_from_slice(&250u64.to_le_bytes());
403        p.push(1); // holder rewards
404
405        let (_, mayhem, cashback, creator_fee_bps, holder_reward) =
406            parse_create_v2_tail_fields(&p).expect("parse");
407        assert!(!mayhem);
408        assert!(!cashback);
409        assert_eq!(creator_fee_bps, 250);
410        assert!(holder_reward);
411    }
412}