feat(shred): tx_index, Migrate without CPI, merge fix, doc limits

- Add tx_index to TransactionWithSlot (entry index), pass to parser
- Emit PumpFun Migrate even when no CPI (shred gets instruction-only)
- Merge: only overwrite Create/CreateV2 with CPI when value non-default
- Rename transaction_index -> tx_index across codebase
- Add docs/SHREDSTREAM_LIMITATIONS.md (ALT/CPI limits, field completeness)
- Comment shred path: static_account_keys only, no inner_instructions

Made-with: Cursor
This commit is contained in:
Wood
2026-03-07 12:35:57 +08:00
parent 04df6db0b0
commit 7270372604
14 changed files with 219 additions and 77 deletions
+3 -3
View File
@@ -301,7 +301,7 @@ pub struct SwapData {
pub struct EventMetadata {
pub signature: Signature,
pub slot: u64,
pub transaction_index: Option<u64>, // 新增:交易在slot中的索引
pub tx_index: Option<u64>, // 新增:交易在slot中的索引
pub block_time: i64,
pub block_time_ms: i64,
pub recv_us: i64,
@@ -327,7 +327,7 @@ impl EventMetadata {
outer_index: i64,
inner_index: Option<i64>,
recv_us: i64,
transaction_index: Option<u64>,
tx_index: Option<u64>,
) -> Self {
Self {
signature,
@@ -342,7 +342,7 @@ impl EventMetadata {
swap_data: None,
outer_index,
inner_index,
transaction_index,
tx_index,
}
}
+14 -28
View File
@@ -39,7 +39,7 @@ impl EventParser {
block_time: Option<Timestamp>,
recv_us: i64,
bot_wallet: Option<Pubkey>,
transaction_index: Option<u64>,
tx_index: Option<u64>,
callback: Arc<dyn Fn(DexEvent) + Send + Sync>,
) -> anyhow::Result<()> {
// 创建适配器回调,将所有权回调转换为引用回调
@@ -93,7 +93,7 @@ impl EventParser {
&accounts,
&inner_instructions,
bot_wallet,
transaction_index,
tx_index,
adapter_callback,
)
.await?;
@@ -119,7 +119,7 @@ impl EventParser {
accounts: &[Pubkey],
inner_instructions: &[InnerInstructions],
bot_wallet: Option<Pubkey>,
transaction_index: Option<u64>,
tx_index: Option<u64>,
callback: Arc<dyn Fn(DexEvent) + Send + Sync>,
) -> anyhow::Result<()> {
// 创建适配器回调,将所有权回调转换为引用回调
@@ -159,7 +159,7 @@ impl EventParser {
index as i64,
None,
bot_wallet,
transaction_index,
tx_index,
inner_instructions,
adapter_callback.clone(),
)?;
@@ -181,7 +181,7 @@ impl EventParser {
index as i64,
Some(inner_index as i64),
bot_wallet,
transaction_index,
tx_index,
Some(&inner_instructions),
adapter_callback.clone(),
)?;
@@ -213,7 +213,7 @@ impl EventParser {
accounts: &[Pubkey],
inner_instructions: &[yellowstone_grpc_proto::prelude::InnerInstructions],
bot_wallet: Option<Pubkey>,
transaction_index: Option<u64>,
tx_index: Option<u64>,
callback: Arc<dyn for<'a> Fn(&'a DexEvent) + Send + Sync>,
) -> anyhow::Result<()> {
// 获取交易的指令和账户
@@ -248,7 +248,7 @@ impl EventParser {
index as i64,
None,
bot_wallet,
transaction_index,
tx_index,
inner_instructions,
callback.clone(),
)?;
@@ -278,7 +278,7 @@ impl EventParser {
inner_instructions.index as i64,
Some(inner_index as i64),
bot_wallet,
transaction_index,
tx_index,
Some(&inner_instructions),
callback.clone(),
)?;
@@ -307,7 +307,7 @@ impl EventParser {
outer_index: i64,
inner_index: Option<i64>,
bot_wallet: Option<Pubkey>,
transaction_index: Option<u64>,
tx_index: Option<u64>,
inner_instructions: Option<&yellowstone_grpc_proto::prelude::InnerInstructions>,
callback: Arc<dyn for<'a> Fn(&'a DexEvent) + Send + Sync>,
) -> anyhow::Result<()> {
@@ -346,7 +346,7 @@ impl EventParser {
outer_index,
inner_index,
recv_us,
transaction_index,
tx_index,
);
if is_cu_program {
@@ -428,14 +428,7 @@ impl EventParser {
}
}
// 特殊处理: PumpFun MIGRATE 指令需要 inner instruction data
if matches!(protocol, Protocol::PumpFun) {
const PUMPFUN_MIGRATE_IX: &[u8] = &[155, 234, 231, 146, 236, 158, 162, 30];
if instruction_discriminator == PUMPFUN_MIGRATE_IX && inner_instruction_event.is_none()
{
return Ok(());
}
}
// PumpFun MIGRATE: 有 CPI 时合并 log;无 CPI 时仍发出仅含指令数据的事件。
// 合并事件
if let Some(inner_instruction_event) = inner_instruction_event {
@@ -471,7 +464,7 @@ impl EventParser {
outer_index: i64,
inner_index: Option<i64>,
bot_wallet: Option<Pubkey>,
transaction_index: Option<u64>,
tx_index: Option<u64>,
inner_instructions: Option<&InnerInstructions>,
callback: Arc<dyn for<'a> Fn(&'a DexEvent) + Send + Sync>,
) -> anyhow::Result<()> {
@@ -511,7 +504,7 @@ impl EventParser {
outer_index,
inner_index,
recv_us,
transaction_index,
tx_index,
);
if is_cu_program {
@@ -611,14 +604,7 @@ impl EventParser {
}
}
// 特殊处理: PumpFun MIGRATE 指令需要 inner instruction data
if matches!(protocol, Protocol::PumpFun) {
const PUMPFUN_MIGRATE_IX: &[u8] = &[155, 234, 231, 146, 236, 158, 162, 30];
if instruction_discriminator == PUMPFUN_MIGRATE_IX && inner_instruction_event.is_none()
{
return Ok(());
}
}
// PumpFun MIGRATE: 有 CPI 时合并 log;无 CPI(如 shred)仍发出仅含指令数据的事件。
// 合并事件
if let Some(inner_instruction_event) = inner_instruction_event {
+60 -20
View File
@@ -36,16 +36,36 @@ pub fn merge(instruction_event: &mut DexEvent, cpi_log_event: DexEvent) {
},
DexEvent::PumpFunCreateTokenEvent(e) => match cpi_log_event {
DexEvent::PumpFunCreateV2TokenEvent(cpie) => {
e.mint = cpie.mint;
e.bonding_curve = cpie.bonding_curve;
e.user = cpie.user;
e.creator = cpie.creator;
e.timestamp = cpie.timestamp;
e.virtual_token_reserves = cpie.virtual_token_reserves;
e.virtual_sol_reserves = cpie.virtual_sol_reserves;
e.real_token_reserves = cpie.real_token_reserves;
e.token_total_supply = cpie.token_total_supply;
e.token_program = cpie.token_program;
if cpie.mint != solana_sdk::pubkey::Pubkey::default() {
e.mint = cpie.mint;
}
if cpie.bonding_curve != solana_sdk::pubkey::Pubkey::default() {
e.bonding_curve = cpie.bonding_curve;
}
if cpie.user != solana_sdk::pubkey::Pubkey::default() {
e.user = cpie.user;
}
if cpie.creator != solana_sdk::pubkey::Pubkey::default() {
e.creator = cpie.creator;
}
if cpie.timestamp != 0 {
e.timestamp = cpie.timestamp;
}
if cpie.virtual_token_reserves != 0 {
e.virtual_token_reserves = cpie.virtual_token_reserves;
}
if cpie.virtual_sol_reserves != 0 {
e.virtual_sol_reserves = cpie.virtual_sol_reserves;
}
if cpie.real_token_reserves != 0 {
e.real_token_reserves = cpie.real_token_reserves;
}
if cpie.token_total_supply != 0 {
e.token_total_supply = cpie.token_total_supply;
}
if cpie.token_program != solana_sdk::pubkey::Pubkey::default() {
e.token_program = cpie.token_program;
}
e.is_mayhem_mode = cpie.is_mayhem_mode;
e.is_cashback_enabled = cpie.is_cashback_enabled;
}
@@ -53,16 +73,36 @@ pub fn merge(instruction_event: &mut DexEvent, cpi_log_event: DexEvent) {
},
DexEvent::PumpFunCreateV2TokenEvent(e) => match cpi_log_event {
DexEvent::PumpFunCreateV2TokenEvent(cpie) => {
e.mint = cpie.mint;
e.bonding_curve = cpie.bonding_curve;
e.user = cpie.user;
e.creator = cpie.creator;
e.timestamp = cpie.timestamp;
e.virtual_token_reserves = cpie.virtual_token_reserves;
e.virtual_sol_reserves = cpie.virtual_sol_reserves;
e.real_token_reserves = cpie.real_token_reserves;
e.token_total_supply = cpie.token_total_supply;
e.token_program = cpie.token_program;
if cpie.mint != solana_sdk::pubkey::Pubkey::default() {
e.mint = cpie.mint;
}
if cpie.bonding_curve != solana_sdk::pubkey::Pubkey::default() {
e.bonding_curve = cpie.bonding_curve;
}
if cpie.user != solana_sdk::pubkey::Pubkey::default() {
e.user = cpie.user;
}
if cpie.creator != solana_sdk::pubkey::Pubkey::default() {
e.creator = cpie.creator;
}
if cpie.timestamp != 0 {
e.timestamp = cpie.timestamp;
}
if cpie.virtual_token_reserves != 0 {
e.virtual_token_reserves = cpie.virtual_token_reserves;
}
if cpie.virtual_sol_reserves != 0 {
e.virtual_sol_reserves = cpie.virtual_sol_reserves;
}
if cpie.real_token_reserves != 0 {
e.real_token_reserves = cpie.real_token_reserves;
}
if cpie.token_total_supply != 0 {
e.token_total_supply = cpie.token_total_supply;
}
if cpie.token_program != solana_sdk::pubkey::Pubkey::default() {
e.token_program = cpie.token_program;
}
e.is_mayhem_mode = cpie.is_mayhem_mode;
e.is_cashback_enabled = cpie.is_cashback_enabled;
}
@@ -192,6 +192,8 @@ fn parse_create_token_instruction(
/// 5: user, 6: system_program, 7: token_program, 8: associated_token_program, 9: mayhem_program_id,
/// 10: global_params, 11: sol_vault, 12: mayhem_state, 13: mayhem_token_vault, 14: event_authority, 15: program.
/// 共 16 个固定账户,不足时返回 None 避免越界。
/// 注意:shredstream 路径仅传入 static_account_keys,若交易使用 Address Lookup Tables
/// 无法解析 loaded_addresses,部分账户会以 default 填充,导致 token_program/global 等错误。
fn parse_create_v2_token_instruction(
data: &[u8],
accounts: &[Pubkey],