fix: scope raw instructions by DEX and expose typed identifiers

This commit is contained in:
0xfnzero
2026-10-19 04:15:41 +08:00
parent 8b30d8ecf3
commit b7fa14044d
10 changed files with 207 additions and 44 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "solana-streamer-sdk"
version = "3.0.14"
version = "3.0.15"
edition = "2021"
rust-version = "1.91"
authors = ["William <byteblock6@gmail.com>", "sgxiang <sgxiang@gmail.com>", "wei <1415121722@qq.com>"]
@@ -22,7 +22,7 @@ sdk-perf-stats = ["sol-parser-sdk/perf-stats"]
sdk-ultra-perf = ["sol-parser-sdk/ultra-perf"]
[dependencies]
sol-parser-sdk = { path = "../sol-parser-sdk", version = "=0.7.16", default-features = false }
sol-parser-sdk = { path = "../sol-parser-sdk", version = "=0.7.17", default-features = false }
solana-sdk = "=4.0.1"
solana-client = "=4.2.2"
solana-transaction-status = { version = "=4.2.2", features = ["agave-unstable-api"] }
+13 -8
View File
@@ -1,6 +1,11 @@
<div align="center">
## v3.0.14 — Pump execution replay
## v3.0.15 — DEX-scoped RawInstruction and typed DEX identifiers
Raw instructions now respect DEX subscriptions using programs referenced by each instruction. `RawInstructionDexType` provides typed identifiers with unchanged JSON strings. Unknown router layouts do not establish pool addresses, trade direction, or method versions.
## v3.0.15 — Pump execution replay
Current Anchor TradeEvent CPI decoding is supported alongside the historical encoding. RPC/gRPC instruction budgets and minimum outputs remain in instruction fields. Execution fields `sol_amount`, `quote_amount`, and `token_amount` come from execution events; without logs or event CPIs they remain zero (unavailable, not a proven zero fill).
@@ -8,15 +13,15 @@ Run `cargo run --offline --example rpc_transaction_replay` from this repository
The current Delta sell signature is `4ARos3Yq1SMNE3izDpy9YdKrRgMmD7UCDxvnVrBRaGCJHiifPHSmQWc4DPfjUaEUcxS7puGmEN64K7EZdeXLJaj2` (slot 458542546). Older five-combination multi-hop fixtures are retained as regression tests, not labeled as new mainnet captures.
## v3.0.14 — raw Shred instructions
## v3.0.15 — raw Shred instructions
Protocol narrowing (for example PumpFun with no event filter) preserves default raw unknown instructions end to end. An explicit trade-only event filter still excludes raw instructions; explicitly including RawInstruction selects all outer instructions.
Protocol filters apply to raw DEX associations as well as decoded events.
Default Shred parsing emits `RawInstruction` for unsupported third-party programs, including Rapid/Delta, without inferring direction, amounts, or internal buy methods. Infrastructure programs (System, Token, ATA, ComputeBudget, etc.) are omitted by default. Supported official DEX parsers are unchanged; official Pump/PumpSwap multi-hop instructions retain `PumpRouteIntent`.
The payload contains `program_id`, `instruction_index`, `accounts`, `account_indices`, `instruction_data`, and transaction metadata. Account order and duplicates are preserved. Unresolved ALT addresses are null/None, with original indices retained; supply cached loaded addresses or a resolver. No hot-path RPC is performed. Data is copied verbatim without requiring a known layout.
No filter returns unknown third-party instructions. Explicitly including `RawInstruction` returns every outer instruction, including infrastructure. A `PumpFunTrade`-only filter does not include raw events: subscribe to both types when needed. Streamer forwards `DexEvent::RawInstructionEvent` as Common. A Pump address in accounts does not prove a CPI or a particular buy method. Event count is not fill count.
Without a filter, unknown third-party instructions are returned. Broad trade/swap filters (for example `PumpFunTrade` or `RaydiumCpmmSwap`) also return raw instructions associated with that DEX; direction-specific filters do not infer a buy or sell. Explicit `RawInstruction` selects all outer instructions only when no protocol restriction is set; streamer protocol restrictions always apply. Excluding `RawInstruction` wins. `dex_type` uses the public `RawInstructionDexType` enum (typed string constants in Go); JSON values remain unchanged, such as `"pumpfun"` and `"raydium_cpmm"`. `dexes` contains `{ dex_type, program_id, pool }` for each recognized DEX program targeted or referenced by this instruction, not other instructions in the transaction. Multi-DEX wrappers retain every association. Unknown or unresolved ALT references cannot satisfy a DEX subscription. `pool` is currently null: an unknown wrapper layout cannot establish the pool account. A program reference is not proof of an executed CPI. Raw callbacks are not fills.
Wrapper adapter functions remain explicit compatibility helpers only; default Shred parsing does not call them. The released-version history below describes previous behavior.
@@ -151,24 +156,24 @@ Add the dependency to your `Cargo.toml`:
```toml
# Add to your Cargo.toml
solana-streamer-sdk = { path = "./solana-streamer", version = "3.0.14" }
solana-streamer-sdk = { path = "./solana-streamer", version = "3.0.15" }
```
### Use crates.io
```toml
# Add to your Cargo.toml
solana-streamer-sdk = "3.0.14"
solana-streamer-sdk = "3.0.15"
```
Parser backend features:
```toml
# Default: sol-parser-sdk parse-borsh backend
solana-streamer-sdk = "3.0.14"
solana-streamer-sdk = "3.0.15"
# Zero-copy parser backend for latency-sensitive bots
solana-streamer-sdk = { version = "3.0.14", default-features = false, features = ["sdk-parse-zero-copy"] }
solana-streamer-sdk = { version = "3.0.15", default-features = false, features = ["sdk-parse-zero-copy"] }
```
If both `sdk-parse-borsh` and `sdk-parse-zero-copy` are enabled, `sol-parser-sdk 0.6.1+` uses the zero-copy backend.
+13 -8
View File
@@ -1,6 +1,11 @@
<div align="center">
## v3.0.14 — Pump 执行事件与最新回放示例
## v3.0.15 — RawInstruction 协议过滤与 DEX 枚举
修复原始指令绕过 DEX 订阅范围的问题。按单条指令引用的官方程序识别 DEX,使用 `RawInstructionDexType` 枚举;JSON 字符串保持不变。未知 router 不推断池子地址、买卖方向或方法版本。
## v3.0.15 — Pump 执行事件与最新回放示例
修复官方 Anchor CPI TradeEvent 前缀识别,保留历史编码兼容。RPC/gRPC 的实际 `sol_amount`、`quote_amount`、`token_amount` 只从执行事件合并;预算和最小输出保留在指令参数字段。如果日志及事件 CPI 均缺失,实际金额字段为 0(表示无执行数据,不代表已确认零成交)。非 SOL 报价金额不能当作 SOL。
@@ -8,15 +13,15 @@
新 Delta 卖出样本:`4ARos3Yq1SMNE3izDpy9YdKrRgMmD7UCDxvnVrBRaGCJHiifPHSmQWc4DPfjUaEUcxS7puGmEN64K7EZdeXLJaj2`,slot 458542546。旧的五种双跳组合仅作为回归样本,未冒充本次新抓取交易。
## v3.0.14 — Shred 原始指令透传
## v3.0.15 — Shred 原始指令透传
只限制协议(例如 PumpFun)而不设置事件过滤器时,默认未知指令透传也能到达最终回调。显式只订阅 Trade 仍不包含原始指令;显式订阅 RawInstruction 返回全部外层指令。
协议过滤同时作用于原始指令的 DEX 关联和已解码事件。
默认 Shred 入口将未适配的第三方程序(包括 Rapid/Delta)作为 `RawInstruction` 返回,不再推断买卖方向、金额或内部 buy 版本。System、Token、ATA、ComputeBudget 等基础程序默认不透传;已支持的官方 DEX 指令继续正常解析,官方 Pump/PumpSwap 多跳仍返回 `PumpRouteIntent`。
事件包含 `program_id`、`instruction_index`、`accounts`、`account_indices`、`instruction_data` 和交易 metadata。账户保留原顺序与重复项;未解析 ALT 地址为 null/None,原始索引仍保留。使用缓存 loaded-addresses/resolver 补齐,热路径不请求 RPC。data 是完整原始字节,不要求已知布局。
无过滤器时返回未知第三方指令;显式订阅 `RawInstruction` 时返回所有外层指令(含基础程序)。仅订阅 `PumpFunTrade` 不包含原始事件,需要同时订阅 `RawInstruction`。Streamer 对应 `DexEvent::RawInstructionEvent`,协议为 Common。账户包含 Pump 不证明发生 CPI,更不证明具体 buy 方法;事件数量也不是成交数量。
无过滤器时返回未知第三方指令。宽泛交易/Swap 订阅(如 `PumpFunTrade`、`RaydiumCpmmSwap`)也返回关联该 DEX 的原始指令;买/卖方向订阅不推测方向。显式 `RawInstruction` 仅在未限制协议时返回全部外层指令,streamer 的协议限制始终生效;显式排除 `RawInstruction` 优先。`dex_type` 使用公开枚举 `RawInstructionDexType`(Go 为命名字符串类型与常量);JSON 仍为 `"pumpfun"`、`"raydium_cpmm"` 等原有字符串。`dexes` 数组包含各关联 DEX 的 `{ dex_type, program_id, pool }`,只识别本条指令直接调用或账户中引用的 DEX 程序,不借用同笔其他指令的账户;多 DEX 包装保留所有关联。未知或未解析 ALT 中的程序不能匹配 DEX 订阅。`pool` 当前为 null:未知包装布局无法确定池子账户。program 引用不证明执行了 CPI,原始回调不是成交。
已有 wrapper 适配函数仅作为显式调用的兼容接口保留,不参与默认 Shred 解码。以下已发布版本的历史说明描述旧行为。
@@ -150,24 +155,24 @@ git clone https://github.com/0xfnzero/solana-streamer
```toml
# 添加到您的 Cargo.toml
solana-streamer-sdk = { path = "./solana-streamer", version = "3.0.14" }
solana-streamer-sdk = { path = "./solana-streamer", version = "3.0.15" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
solana-streamer-sdk = "3.0.14"
solana-streamer-sdk = "3.0.15"
```
解析后端 feature:
```toml
# 默认:sol-parser-sdk parse-borsh 后端
solana-streamer-sdk = "3.0.14"
solana-streamer-sdk = "3.0.15"
# 面向低延迟 Bot 的 zero-copy 解析后端
solana-streamer-sdk = { version = "3.0.14", default-features = false, features = ["sdk-parse-zero-copy"] }
solana-streamer-sdk = { version = "3.0.15", default-features = false, features = ["sdk-parse-zero-copy"] }
```
如果同时启用 `sdk-parse-borsh` 和 `sdk-parse-zero-copy`,`sol-parser-sdk 0.6.1+` 会优先使用 zero-copy 后端。
+18
View File
@@ -0,0 +1,18 @@
# v3.0.15 — DEX-scoped raw instructions and typed DEX identifiers
Released 2026-10-19.
RawInstruction callbacks now match the subscribed DEX using only the outer instruction's program and referenced accounts. Unrelated instructions in the same transaction no longer inherit a DEX association. Streamer protocol restrictions apply even when RawInstruction is explicitly selected; legacy sol-shred-sdk PumpFun callbacks only forward PumpFun-associated wrappers.
The dexes array preserves all detected DEX associations for multi-DEX wrappers. Each entry contains dex_type, program_id and a nullable pool. RawInstructionDexType is a public enum in Rust, TypeScript and Python, and a named string type with constants in Go. JSON identifiers remain pumpfun, raydium_cpmm, etc. Unknown router layouts leave pool null and do not imply CPI execution, trade direction, amounts or buy-method versions. Unresolved ALT references cannot match a DEX subscription.
## Compatibility
- Broad trade/swap filters now include associated raw instructions. Excluding RawInstruction takes precedence; direction/method-specific subscriptions do not infer unknown router behavior.
- Without protocol restrictions, explicit RawInstruction still selects all outer instructions.
- Rust callers constructing RawInstructionEvent must populate dexes; RawInstructionDex.dex_type is now an enum. Historical JSON without dexes remains readable with an empty list.
- No third-party router payload decoding or guessed pool-account positions were added.
## Validation
Regression coverage includes instruction-local DEX scope, multi-DEX references, duplicate accounts, unresolved ALT addresses, raw exclusions, streamer protocol gates, enum exports and JSON round trips. Signed replay examples compare PumpFun, Raydium CPMM and all-raw subscriptions across language implementations.
+112 -23
View File
@@ -286,29 +286,6 @@ pub(crate) fn parse_shred_transaction_events(
&mut events,
);
// Protocol narrowing must not suppress protocol-independent default raw
// events. Explicit RawInstruction subscriptions are handled by the main
// parser (all outer instructions); this pass returns unknown programs only.
let wants_default_raw = event_type_filter.is_none_or(|f| {
f.include.is_empty()
&& !f
.exclude
.contains(&crate::streaming::event_parser::common::EventType::RawInstruction)
});
if wants_default_raw
&& sdk_parse_filter
.as_ref()
.is_some_and(|f| !f.should_include(sol_parser_sdk::grpc::EventType::RawInstruction))
{
sol_parser_sdk::shredstream::parse_transaction_unknown_instructions(
tx,
signature,
slot,
tx_index.unwrap_or(0),
recv_us,
&mut events,
);
}
for sdk_event in events.drain(..) {
if let Some(mut event) =
adapt_parser_event(sdk_event, None, recv_us, protocols, event_type_filter)
@@ -524,6 +501,118 @@ mod tests {
None,
|e| events.push(e),
);
assert_eq!(events.len(), 1);
assert!(
matches!(&events[0], DexEvent::RawInstructionEvent(raw) if raw.dexes.iter().any(|d| d.dex_type == sol_parser_sdk::core::events::RawInstructionDexType::PumpFun))
);
}
#[test]
fn raw_callbacks_respect_protocol_even_with_explicit_raw_subscription() {
use sol_parser_sdk::grpc::program_ids::{PUMPFUN_PROGRAM, RAYDIUM_CPMM_PROGRAM};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
message::{Message, VersionedMessage},
signature::Signature,
transaction::VersionedTransaction,
};
let wrapper = Pubkey::new_unique();
let tx = VersionedTransaction {
signatures: vec![Signature::default()],
message: VersionedMessage::Legacy(Message::new(
&[
Instruction {
program_id: wrapper,
accounts: vec![AccountMeta::new_readonly(PUMPFUN_PROGRAM, false)],
data: vec![255],
},
Instruction {
program_id: wrapper,
accounts: vec![AccountMeta::new_readonly(RAYDIUM_CPMM_PROGRAM, false)],
data: vec![254],
},
Instruction { program_id: wrapper, accounts: vec![], data: vec![253] },
Instruction {
program_id: wrapper,
accounts: vec![
AccountMeta::new_readonly(PUMPFUN_PROGRAM, false),
AccountMeta::new_readonly(RAYDIUM_CPMM_PROGRAM, false),
],
data: vec![252],
},
],
Some(&Pubkey::new_unique()),
)),
};
for (protocols, expected) in [
(vec![Protocol::PumpFun], vec![0, 3]),
(vec![Protocol::RaydiumCpmm], vec![1, 3]),
(vec![Protocol::MeteoraDammV2], vec![]),
(vec![], vec![0, 1, 2, 3]),
] {
for filter in
[None, Some(EventTypeFilter::include_only(vec![EventType::RawInstruction]))]
{
let mut events = vec![];
super::parse_shred_transaction_events(
&tx,
tx.signatures[0],
1,
None,
0,
&protocols,
filter.as_ref(),
None,
|e| events.push(e),
);
let indices: Vec<_> = events
.iter()
.map(|e| match e {
DexEvent::RawInstructionEvent(raw) => raw.instruction_index,
_ => panic!("not raw"),
})
.collect();
assert_eq!(indices, expected, "{protocols:?}");
}
}
for (protocol, event_type) in [
(Protocol::PumpFun, EventType::PumpFunBuy),
(Protocol::RaydiumCpmm, EventType::RaydiumCpmmSwapBaseInput),
] {
let mut events = vec![];
let filter = EventTypeFilter::include_only(vec![event_type]);
super::parse_shred_transaction_events(
&tx,
tx.signatures[0],
1,
None,
0,
&[protocol],
Some(&filter),
None,
|e| events.push(e),
);
assert!(
events.is_empty(),
"execution-specific filter must not infer router instruction"
);
}
let filter = EventTypeFilter::include_exclude(
vec![EventType::PumpFunTrade],
vec![EventType::RawInstruction],
);
let mut events = vec![];
super::parse_shred_transaction_events(
&tx,
tx.signatures[0],
1,
None,
0,
&[Protocol::PumpFun],
Some(&filter),
None,
|e| events.push(e),
);
assert!(events.is_empty());
}
@@ -82,6 +82,43 @@ impl EventTypeFilter {
#[inline]
pub fn passes_for_event(&self, ev: &DexEvent) -> bool {
if let DexEvent::RawInstructionEvent(raw) = ev {
// Execution-specific aliases must not become broad raw subscriptions
// merely because the upstream execution parser uses one family enum.
let map = |types: &[EventType], mode| {
let mut out = vec![];
for t in types {
if matches!(
t,
EventType::BonkBuyExactIn
| EventType::BonkBuyExactOut
| EventType::BonkSellExactIn
| EventType::BonkSellExactOut
| EventType::RaydiumCpmmSwapBaseInput
| EventType::RaydiumCpmmSwapBaseOutput
| EventType::RaydiumAmmV4SwapBaseIn
| EventType::RaydiumAmmV4SwapBaseOut
| EventType::RaydiumClmmSwap
| EventType::RaydiumClmmSwapV2
| EventType::MeteoraDammV2Swap
| EventType::MeteoraDammV2Swap2
) {
continue;
}
push_streamer_event_sdk_grpc_types(t, &mut out, mode);
}
out
};
let sdk = SdkGrpcEventTypeFilter {
include_only: if self.include.is_empty() {
None
} else {
Some(map(&self.include, FilterMapMode::Include))
},
exclude_types: Some(map(&self.exclude, FilterMapMode::Exclude)),
};
return sol_parser_sdk::core::events::raw_instruction_matches_filter(&raw.dexes, &sdk);
}
if let DexEvent::PumpRouteIntentEvent(_) = ev {
if self.exclude.contains(&EventType::PumpRouteIntent) {
return false;
@@ -1,5 +1,6 @@
use crate::streaming::event_parser::common::EventMetadata;
use serde::{Deserialize, Serialize};
pub use sol_parser_sdk::core::events::{RawInstructionDex, RawInstructionDexType};
use solana_sdk::pubkey::Pubkey;
/// `sol-parser-sdk` 错误占位(无有效 EventMetadata 字段)
@@ -469,6 +470,8 @@ pub struct MeteoraDlmmClaimFeeEvent {
/// Outer instruction bytes; no claim about internal CPI calls or trade direction.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RawInstructionEvent {
#[serde(default)]
pub dexes: Vec<RawInstructionDex>,
pub metadata: EventMetadata,
pub program_id: Pubkey,
pub instruction_index: usize,
@@ -34,6 +34,7 @@ pub(crate) fn convert_parser_event(
e.program_id,
);
Some(DexEvent::RawInstructionEvent(RawInstructionEvent {
dexes: e.dexes,
metadata,
program_id: e.program_id,
instruction_index: e.instruction_index,
+5 -1
View File
@@ -16,7 +16,6 @@ fn is_protocol_independent_event(ev: &DexEvent) -> bool {
matches!(
ev,
DexEvent::TokenAccountEvent(_)
| DexEvent::RawInstructionEvent(_)
| DexEvent::TokenInfoEvent(_)
| DexEvent::NonceAccountEvent(_)
| DexEvent::BlockMetaEvent(_)
@@ -29,6 +28,11 @@ fn is_protocol_independent_event(ev: &DexEvent) -> bool {
#[allow(clippy::match_like_matches_macro)]
fn protocol_matches_event(p: &Protocol, ev: &DexEvent) -> bool {
if let DexEvent::RawInstructionEvent(raw) = ev {
// LaunchLab's program alone cannot establish StonkFun platform attribution.
return *p != Protocol::StonkFun
&& raw.dexes.iter().any(|dex| p.get_program_id().contains(&dex.program_id));
}
match (p, ev) {
(Protocol::PumpFun, DexEvent::PumpRouteIntentEvent(_)) => true,
(Protocol::PumpSwap, DexEvent::PumpRouteIntentEvent(e)) => {
+3 -2
View File
@@ -65,6 +65,7 @@ mod tests {
let source = PbDexEvent::RawInstruction(Box::new(
sol_parser_sdk::core::events::RawInstructionEvent {
metadata: EventMetadata { slot: 42, ..Default::default() },
dexes: vec![],
program_id: program,
instruction_index: 3,
accounts: vec![Some(account), None, Some(account)],
@@ -72,8 +73,8 @@ mod tests {
instruction_data: vec![255, 0, 1],
},
));
let event =
adapt_parser_event(source.clone(), None, 999, &[Protocol::PumpFun], None).unwrap();
let event = adapt_parser_event(source.clone(), None, 999, &[], None).unwrap();
assert!(adapt_parser_event(source.clone(), None, 999, &[Protocol::PumpFun], None).is_none());
let DexEvent::RawInstructionEvent(ref raw) = event else { panic!() };
assert_eq!(raw.metadata.protocol, ProtocolType::Common);
assert_eq!(raw.metadata.event_type, EventType::RawInstruction);