Skip to main content

sol_parser_sdk/grpc/
transaction_meta.rs

1//! Yellowstone [`Transaction`] / [`TransactionStatusMeta`] 通用工具。
2//!
3//! 不依赖 DEX 日志或指令解析,适用于:mentions 订阅后的 SOL/SPL 转账分析、审计、风控等。
4
5use std::collections::HashSet;
6use std::sync::Arc;
7
8use crate::instr::read_pubkey_fast;
9use solana_sdk::pubkey::Pubkey;
10use solana_sdk::signature::Signature;
11use yellowstone_grpc_proto::prelude::{TokenBalance, Transaction, TransactionStatusMeta};
12
13/// 32 字节公钥 → base58 地址字符串。
14#[inline]
15pub fn pubkey_bytes_to_bs58(bytes: &[u8]) -> Option<String> {
16    let a: [u8; 32] = bytes.try_into().ok()?;
17    Some(solana_sdk::pubkey::Pubkey::from(a).to_string())
18}
19
20/// 消息静态 `account_keys` + meta 中 `loaded_writable_addresses` / `loaded_readonly_addresses`,
21/// 顺序与 `pre_balances` / `post_balances` 对齐。
22pub fn collect_account_keys_bs58(
23    tx: &Transaction,
24    meta: &TransactionStatusMeta,
25) -> Option<Vec<String>> {
26    let msg = tx.message.as_ref()?;
27    let mut keys: Vec<String> =
28        msg.account_keys.iter().filter_map(|b| pubkey_bytes_to_bs58(b.as_slice())).collect();
29    for b in &meta.loaded_writable_addresses {
30        keys.push(pubkey_bytes_to_bs58(b)?);
31    }
32    for b in &meta.loaded_readonly_addresses {
33        keys.push(pubkey_bytes_to_bs58(b)?);
34    }
35    Some(keys)
36}
37
38/// 每个账户索引的 lamports 变化(post - pre)。
39#[inline]
40pub fn lamport_balance_deltas(meta: &TransactionStatusMeta) -> Vec<i128> {
41    meta.pre_balances
42        .iter()
43        .zip(meta.post_balances.iter())
44        .map(|(pre, post)| *post as i128 - *pre as i128)
45        .collect()
46}
47
48/// 启发式原生 SOL:对 `watched_bs58` 中出现的账户,若 lamports 净减少 ≥ `min_outflow_lamports`,
49/// 再与其它索引配对,要求对方 delta ≥ `min_outflow_lamports/2`(与常见 mentions 转账监控一致)。
50pub fn heuristic_sol_counterparties_for_watched_keys(
51    account_keys_bs58: &[String],
52    lamport_deltas: &[i128],
53    watched_bs58: &HashSet<&str>,
54    min_outflow_lamports: u64,
55) -> Vec<(String, String)> {
56    let min_l = min_outflow_lamports as i128;
57    let mut pairs = Vec::new();
58    for (i, key) in account_keys_bs58.iter().enumerate() {
59        if !watched_bs58.contains(key.as_str()) {
60            continue;
61        }
62        let d = lamport_deltas.get(i).copied().unwrap_or(0);
63        if d >= -min_l {
64            continue;
65        }
66        for (j, dj) in lamport_deltas.iter().enumerate() {
67            if i == j || *dj <= min_l / 2 {
68                continue;
69            }
70            pairs.push((key.clone(), account_keys_bs58[j].clone()));
71        }
72    }
73    pairs
74}
75
76/// 汇总「监控地址」在一笔交易中的转出对手方(原生 SOL 启发式 + SPL token balance 启发式)。
77///
78/// 返回 `None` 当账户 key 与 balance 数组长度不一致。
79pub fn collect_watch_transfer_counterparty_pairs(
80    tx: &Transaction,
81    meta: &TransactionStatusMeta,
82    watched_bs58: &[String],
83    min_native_outflow_lamports: u64,
84    spl_min_watch_decrease_raw: u64,
85) -> Option<Vec<(String, String)>> {
86    let keys = collect_account_keys_bs58(tx, meta)?;
87    let n = keys.len();
88    if meta.pre_balances.len() != n || meta.post_balances.len() != n {
89        return None;
90    }
91    let deltas = lamport_balance_deltas(meta);
92    let watched_h: HashSet<&str> = watched_bs58.iter().map(|s| s.as_str()).collect();
93
94    let mut pairs = heuristic_sol_counterparties_for_watched_keys(
95        &keys,
96        &deltas,
97        &watched_h,
98        min_native_outflow_lamports,
99    );
100    for w in watched_bs58 {
101        pairs.extend(spl_token_counterparty_by_owner(meta, w, spl_min_watch_decrease_raw));
102    }
103    pairs.sort_by(|a, b| a.1.cmp(&b.1));
104    pairs.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
105    Some(pairs)
106}
107
108/// `TokenBalance.ui_token_amount.amount` 解析为原始整数;失败为 0。
109#[inline]
110pub fn token_balance_raw_amount(t: &TokenBalance) -> u64 {
111    t.ui_token_amount.as_ref().and_then(|u| u.amount.parse().ok()).unwrap_or(0)
112}
113
114/// SPL:对给定 owner(TokenBalance.owner,base58),当其某 mint 上余额净减少 ≥ `min_watch_decrease_raw` 时,
115/// 找出同 mint 下余额增加的其它 owner,返回 `(watch_owner, counterparty_owner)`。
116///
117/// 用于启发式「谁转给谁」配对(非链上 Transfer 事件级精确解析)。
118pub fn spl_token_counterparty_by_owner(
119    meta: &TransactionStatusMeta,
120    watch_owner_bs58: &str,
121    min_watch_decrease_raw: u64,
122) -> Vec<(String, String)> {
123    use std::collections::{HashMap, HashSet};
124
125    let pre = meta.pre_token_balances.as_slice();
126    let post = meta.post_token_balances.as_slice();
127
128    let mut pre_m: HashMap<(String, String), u64> = HashMap::new();
129    for b in pre {
130        if b.owner.is_empty() {
131            continue;
132        }
133        let k = (b.mint.clone(), b.owner.clone());
134        *pre_m.entry(k).or_insert(0) += token_balance_raw_amount(b);
135    }
136    let mut post_m: HashMap<(String, String), u64> = HashMap::new();
137    for b in post {
138        if b.owner.is_empty() {
139            continue;
140        }
141        let k = (b.mint.clone(), b.owner.clone());
142        *post_m.entry(k).or_insert(0) += token_balance_raw_amount(b);
143    }
144
145    let mut mints = HashSet::new();
146    for (m, o) in pre_m.keys() {
147        if o == watch_owner_bs58 {
148            mints.insert(m.clone());
149        }
150    }
151    for (m, o) in post_m.keys() {
152        if o == watch_owner_bs58 {
153            mints.insert(m.clone());
154        }
155    }
156
157    let mut out = Vec::new();
158    let min_l = min_watch_decrease_raw;
159    for mint in mints {
160        let w_pre = pre_m.get(&(mint.clone(), watch_owner_bs58.to_string())).copied().unwrap_or(0);
161        let w_post =
162            post_m.get(&(mint.clone(), watch_owner_bs58.to_string())).copied().unwrap_or(0);
163        let lost = w_pre.saturating_sub(w_post);
164        if lost < min_l.max(1) {
165            continue;
166        }
167        for ((m, owner), po) in &post_m {
168            if m != &mint || owner == watch_owner_bs58 {
169                continue;
170            }
171            let pr = pre_m.get(&(mint.clone(), owner.clone())).copied().unwrap_or(0);
172            if *po > pr {
173                out.push((watch_owner_bs58.to_string(), owner.clone()));
174            }
175        }
176    }
177    out.sort_by(|a, b| a.1.cmp(&b.1));
178    out.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
179    out
180}
181
182/// 仅消息头里的静态 `account_keys`(与 ShredStream `VersionedTransaction::static_account_keys()` 语义对齐;
183/// 不含 ALT 加载地址)。
184#[inline]
185pub fn yellowstone_static_account_keys_arc(tx: &Option<Transaction>) -> Arc<[Pubkey]> {
186    let Some(t) = tx.as_ref() else {
187        return Arc::from(Vec::<Pubkey>::new().into_boxed_slice());
188    };
189    let Some(msg) = t.message.as_ref() else {
190        return Arc::from(Vec::<Pubkey>::new().into_boxed_slice());
191    };
192    let keys: Vec<Pubkey> =
193        msg.account_keys.iter().map(|bytes| read_pubkey_fast(bytes.as_slice())).collect();
194    Arc::from(keys.into_boxed_slice())
195}
196
197/// Yellowstone 交易签名原始字节(64)→ `solana_sdk::signature::Signature`。
198#[inline]
199pub fn try_yellowstone_signature(sig: &[u8]) -> Option<Signature> {
200    if sig.len() != 64 {
201        return None;
202    }
203    let a: [u8; 64] = sig.try_into().ok()?;
204    Some(Signature::from(a))
205}