sol_parser_sdk/grpc/
transaction_meta.rs1use 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#[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
20pub 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#[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
48pub 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
76pub 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#[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
114pub 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#[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#[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}