feat(pumpfun): preserve creation ALT keys through streamer bridge

This commit is contained in:
0xfnzero
2026-10-31 03:05:35 +08:00
parent 192fae6160
commit c1428d466c
8 changed files with 56 additions and 1 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ sdk-ultra-perf = ["sol-parser-sdk/ultra-perf"]
[dependencies]
# Unreleased parser API; replace the Git pin with the next registry release before publishing.
sol-parser-sdk = { version = "=0.10.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "0e32287e4056113ff047e2199dc1b5e8289e8f98", default-features = false }
sol-parser-sdk = { version = "=0.10.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "e3e8d6620e2a128c71d5c37cde219a38d438c73e", default-features = false }
solana-sdk = "=4.0.1"
solana-client = "=4.2.2"
solana-transaction-status = { version = "=4.2.2", features = ["agave-unstable-api"] }
+16
View File
@@ -303,6 +303,22 @@ account value is also available from
`PumpFunCreateTokenEvent.transaction_version` reports the transaction message version as `Some(YellowstoneMessageVersion::Legacy)`, `V0` or `V1` on Shred/gRPC/RPC. JSON uses `"legacy"`, `"v0"` and `"v1"`. It remains `None` without transaction context (for example, parsing an isolated event log). This is independent of the Pump instruction name in `ix_name`.
Pump.fun creation events expose `alt_addresses: Option<Vec<Pubkey>>`: only the
`MessageAddressTableLookup.account_key` values for the whole transaction, in
message order, including repeated keys. This is not the list of loaded addresses
or a claim that every table is used by the creation instruction. It is `None`
when there are no lookups or no transaction context. Reading it requires no ALT
resolution or RPC; use it to prioritize cache warming outside the receive task.
Only creation events with nonempty lookups allocate a list.
```rust
if let Some(tables) = &create.alt_addresses {
for address in tables {
println!("ALT table: {address}");
}
}
```
<a id="trade-provenance-and-parsing-boundaries"></a>
## Trade provenance and parsing boundaries
+14
View File
@@ -283,6 +283,20 @@ log-only 的 `CreatePoolEvent` payload 因为 IDL 不包含该字段,会保持
`PumpFunCreateTokenEvent.transaction_version` 在 Shred/gRPC/RPC 路径下表示交易消息版本:`Some(YellowstoneMessageVersion::Legacy)`、`V0` 或 `V1`,JSON 分别为 `"legacy"`、`"v0"`、`"v1"`。缺少交易上下文时(例如单独解析一条事件日志)为 `None`。该字段与 `ix_name` 表示的 Pump 指令版本相互独立。
Pump.fun 创建事件提供 `alt_addresses: Option<Vec<Pubkey>>`,仅包含整笔交易的
`MessageAddressTableLookup.account_key`,保留消息顺序和重复项。它不是表内加载的
账户列表,也不代表每张表都用于创建指令;没有 ALT 或缺少交易上下文时为 `None`。
读取该字段无需展开 ALT,也不调用 RPC;使用者可据此在接收任务之外安排缓存预热。
仅带非空 ALT 列表的创建事件会分配列表。
```rust
if let Some(tables) = &create.alt_addresses {
for address in tables {
println!("ALT table: {address}");
}
}
```
<a id="交易来源与解析边界"></a>
## 交易来源与解析边界
+2
View File
@@ -93,6 +93,7 @@ fn print_trade(e: &PumpFunTradeEvent, now_us: i64) {
fn print_create_legacy(e: &PumpFunCreateTokenEvent, now_us: i64) {
println!("Transaction version: {:?}", e.transaction_version);
println!("ALT table addresses: {:?}", e.alt_addresses);
let latency_us = now_us - e.metadata.recv_us;
println!(
"│ CREATE | sig={:.8}.. | name={} symbol={} | mint={:.8}.. | latency={} μs",
@@ -101,6 +102,7 @@ fn print_create_legacy(e: &PumpFunCreateTokenEvent, now_us: i64) {
}
fn print_create_v2(e: &PumpFunCreateV2TokenEvent, now_us: i64) {
println!("ALT table addresses: {:?}", e.alt_addresses);
let latency_us = now_us - e.metadata.recv_us;
println!(
"│ CREATE_V2 | sig={:.8}.. | name={} symbol={} | mint={:.8}.. | latency={} μs",
@@ -7,6 +7,11 @@ use crate::streaming::event_parser::protocols::pumpfun::types::{BondingCurve, Gl
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpFunCreateTokenEvent {
/// ALT account keys referenced by the whole transaction, in message order.
/// None when there are no lookups or transaction context is unavailable.
#[borsh(skip)]
#[serde(default)]
pub alt_addresses: Option<Vec<Pubkey>>,
/// Transaction message version from Shred/gRPC/RPC context, independent of create/create_v2.
#[borsh(skip)]
#[serde(default)]
@@ -91,6 +96,11 @@ pub struct PumpFunCreateTokenEvent {
/// CreateV2 事件:与 create_v2 指令 16 个账户一致(见 parser 注释)。
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpFunCreateV2TokenEvent {
/// ALT account keys referenced by the whole transaction, in message order.
/// None when there are no lookups or transaction context is unavailable.
#[borsh(skip)]
#[serde(default)]
pub alt_addresses: Option<Vec<Pubkey>>,
#[borsh(skip)]
pub metadata: EventMetadata,
pub name: String,
+2
View File
@@ -339,6 +339,7 @@ mod tests {
let quote_vault = Pubkey::new_unique();
let quote_token_program = Pubkey::new_unique();
let c = PbPumpCreateV2 {
alt_addresses: Some(vec![global, event_authority]),
transaction_version: Some(sol_parser_sdk::YellowstoneMessageVersion::V1),
metadata: EventMetadata {
signature: Signature::default(),
@@ -366,6 +367,7 @@ mod tests {
let ev = convert_parser_event(PbDexEvent::PumpFunCreateV2(c), None, 999).expect("convert");
match ev {
DexEvent::PumpFunCreateTokenEvent(st) => {
assert_eq!(st.alt_addresses, Some(vec![global, event_authority]));
assert_eq!(
st.transaction_version,
Some(sol_parser_sdk::YellowstoneMessageVersion::V1)
@@ -36,6 +36,7 @@ pub(crate) fn pumpfun_create_token_from_parser(
meta: EventMetadata,
) -> PumpFunCreateTokenEvent {
PumpFunCreateTokenEvent {
alt_addresses: c.alt_addresses,
transaction_version: c.transaction_version,
metadata: meta,
name: c.name,
@@ -83,6 +84,7 @@ pub(crate) fn pumpfun_create_token_from_parser_v2(
meta: EventMetadata,
) -> PumpFunCreateTokenEvent {
PumpFunCreateTokenEvent {
alt_addresses: c.alt_addresses,
transaction_version: c.transaction_version,
metadata: meta,
name: c.name,
+9
View File
@@ -27,6 +27,10 @@ async fn reported_alt_creates_and_multi_hop_survive_the_streamer_facade() {
serde_json::from_value(case["wire_rpc"].clone()).unwrap();
let signed = rpc.transaction.transaction.decode().unwrap();
signed.verify_and_hash_message().unwrap();
let expected_alt_addresses =
signed.message.address_table_lookups().filter(|lookups| !lookups.is_empty()).map(
|lookups| lookups.iter().map(|lookup| lookup.account_key).collect::<Vec<_>>(),
);
let captured = Arc::new(Mutex::new(Vec::new()));
let output = captured.clone();
process_shred_transaction(
@@ -45,6 +49,7 @@ async fn reported_alt_creates_and_multi_hop_survive_the_streamer_facade() {
};
let DexEvent::PumpFunTradeEvent(trade) = &events[1] else { panic!("trade missing") };
assert_eq!(create.ix_name, "create_v2");
assert_eq!(create.alt_addresses, expected_alt_addresses);
assert_eq!(create.mint, trade.mint);
assert!(trade.is_created_buy && trade.is_buy);
for event in &*events {
@@ -100,6 +105,10 @@ async fn reported_alt_creates_and_multi_hop_survive_the_streamer_facade() {
..Default::default()
}, &[Protocol::PumpFun], Some(&filter), None);
for executed in [rpc_events, grpc_events] {
let DexEvent::PumpFunCreateTokenEvent(c) = &executed[0] else {
panic!("create event")
};
assert_eq!(c.alt_addresses, expected_alt_addresses);
assert!(
matches!(&executed[0], DexEvent::PumpFunCreateTokenEvent(c) if c.ix_name == "create_v2"),
"{} mode {mode}: {:?}",