1use serde::{Deserialize, Serialize};
7use solana_sdk::{pubkey, pubkey::Pubkey, transaction::VersionedTransaction};
8use solana_transaction_status::EncodedConfirmedTransactionWithStatusMeta;
9use yellowstone_grpc_proto::prelude::{InnerInstruction, Transaction, TransactionStatusMeta};
10
11use crate::rpc_parser::{convert_rpc_to_grpc, ParseError};
12
13const COMPUTE_BUDGET_PROGRAM_ID: Pubkey = pubkey!("ComputeBudget111111111111111111111111111111");
14const SYSTEM_PROGRAM_ID: Pubkey = pubkey!("11111111111111111111111111111111");
15const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000;
16const MICRO_LAMPORTS_PER_LAMPORT: u128 = 1_000_000;
17const SET_COMPUTE_UNIT_LIMIT_TAG: u8 = 2;
18const SET_COMPUTE_UNIT_PRICE_TAG: u8 = 3;
19const REQUEST_HEAP_FRAME_TAG: u8 = 1;
20const SET_LOADED_ACCOUNTS_DATA_SIZE_LIMIT_TAG: u8 = 4;
21const SYSTEM_TRANSFER_TAG: [u8; 4] = 2u32.to_le_bytes();
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub enum SwqosProvider {
26 Jito,
27 NextBlock,
28 ZeroSlot,
29 Temporal,
30 Bloxroute,
31 Node1,
32 FlashBlock,
33 BlockRazor,
34 Astralane,
35 Stellium,
36 Lightspeed,
37 Soyas,
38 Speedlanding,
39 Helius,
40 Solami,
41 LunarLander,
42 Glaive,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct SwqosTipAccountGroup {
48 pub provider: SwqosProvider,
49 pub accounts: &'static [Pubkey],
50}
51
52pub const JITO_TIP_ACCOUNTS: &[Pubkey] = &[
54 pubkey!("96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5"),
55 pubkey!("HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe"),
56 pubkey!("Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY"),
57 pubkey!("ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49"),
58 pubkey!("DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh"),
59 pubkey!("ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt"),
60 pubkey!("DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL"),
61 pubkey!("3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT"),
62];
63
64pub const HELIUS_TIP_ACCOUNTS: &[Pubkey] = &[
65 pubkey!("4ACfpUFoaSD9bfPdeu6DBt89gB6ENTeHBXCAi87NhDEE"),
66 pubkey!("D2L6yPZ2FmmmTKPgzaMKdhu6EWZcTpLy1Vhx8uvZe7NZ"),
67 pubkey!("9bnz4RShgq1hAnLnZbP8kbgBg1kEmcJBYQq3gQbmnSta"),
68 pubkey!("5VY91ws6B2hMmBFRsXkoAAdsPHBJwRfBht4DXox3xkwn"),
69 pubkey!("2nyhqdwKcJZR2vcqCyrYsaPVdAnFoJjiksCXJ7hfEYgD"),
70 pubkey!("2q5pghRs6arqVjRvT5gfgWfWcHWmw1ZuCzphgd5KfWGJ"),
71 pubkey!("wyvPkWjVZz1M8fHQnMMCDTQDbkManefNNhweYk5WkcF"),
72 pubkey!("3KCKozbAaF75qEU33jtzozcJ29yJuaLJTy2jFdzUY8bT"),
73 pubkey!("4vieeGHPYPG2MmyPRcYjdiDmmhN3ww7hsFNap8pVN3Ey"),
74 pubkey!("4TQLFNWK8AovT1gFvda5jfw2oJeRMKEmw7aH6MGBJ3or"),
75];
76
77pub const NEXTBLOCK_TIP_ACCOUNTS: &[Pubkey] = &[
78 pubkey!("NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE"),
79 pubkey!("NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2"),
80 pubkey!("NeXTBLoCKs9F1y5PJS9CKrFNNLU1keHW71rfh7KgA1X"),
81 pubkey!("NexTBLockJYZ7QD7p2byrUa6df8ndV2WSd8GkbWqfbb"),
82 pubkey!("neXtBLock1LeC67jYd1QdAa32kbVeubsfPNTJC1V5At"),
83 pubkey!("nEXTBLockYgngeRmRrjDV31mGSekVPqZoMGhQEZtPVG"),
84 pubkey!("NEXTbLoCkB51HpLBLojQfpyVAMorm3zzKg7w9NFdqid"),
85 pubkey!("nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc"),
86];
87
88pub const ZEROSLOT_TIP_ACCOUNTS: &[Pubkey] = &[
89 pubkey!("Eb2KpSC8uMt9GmzyAEm5Eb1AAAgTjRaXWFjKyFXHZxF3"),
90 pubkey!("FCjUJZ1qozm1e8romw216qyfQMaaWKxWsuySnumVCCNe"),
91 pubkey!("ENxTEjSQ1YabmUpXAdCgevnHQ9MHdLv8tzFiuiYJqa13"),
92 pubkey!("6rYLG55Q9RpsPGvqdPNJs4z5WTxJVatMB8zV3WJhs5EK"),
93 pubkey!("Cix2bHfqPcKcM233mzxbLk14kSggUUiz2A87fJtGivXr"),
94];
95
96pub const NOZOMI_TIP_ACCOUNTS: &[Pubkey] = &[
97 pubkey!("TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq"),
98 pubkey!("noz3jAjPiHuBPqiSPkkugaJDkJscPuRhYnSpbi8UvC4"),
99 pubkey!("noz3str9KXfpKknefHji8L1mPgimezaiUyCHYMDv1GE"),
100 pubkey!("noz6uoYCDijhu1V7cutCpwxNiSovEwLdRHPwmgCGDNo"),
101 pubkey!("noz9EPNcT7WH6Sou3sr3GGjHQYVkN3DNirpbvDkv9YJ"),
102 pubkey!("nozc5yT15LazbLTFVZzoNZCwjh3yUtW86LoUyqsBu4L"),
103 pubkey!("nozFrhfnNGoyqwVuwPAW4aaGqempx4PU6g6D9CJMv7Z"),
104 pubkey!("nozievPk7HyK1Rqy1MPJwVQ7qQg2QoJGyP71oeDwbsu"),
105 pubkey!("noznbgwYnBLDHu8wcQVCEw6kDrXkPdKkydGJGNXGvL7"),
106 pubkey!("nozNVWs5N8mgzuD3qigrCG2UoKxZttxzZ85pvAQVrbP"),
107 pubkey!("nozpEGbwx4BcGp6pvEdAh1JoC2CQGZdU6HbNP1v2p6P"),
108 pubkey!("nozrhjhkCr3zXT3BiT4WCodYCUFeQvcdUkM7MqhKqge"),
109 pubkey!("nozrwQtWhEdrA6W8dkbt9gnUaMs52PdAv5byipnadq3"),
110 pubkey!("nozUacTVWub3cL4mJmGCYjKZTnE9RbdY5AP46iQgbPJ"),
111 pubkey!("nozWCyTPppJjRuw2fpzDhhWbW355fzosWSzrrMYB1Qk"),
112 pubkey!("nozWNju6dY353eMkMqURqwQEoM3SFgEKC6psLCSfUne"),
113 pubkey!("nozxNBgWohjR75vdspfxR5H9ceC7XXH99xpxhVGt3Bb"),
114];
115
116pub const BLOX_TIP_ACCOUNTS: &[Pubkey] = &[
117 pubkey!("HWEoBxYs7ssKuudEjzjmpfJVX7Dvi7wescFsVx2L5yoY"),
118 pubkey!("95cfoy472fcQHaw4tPGBTKpn6ZQnfEPfBgDQx6gcRmRg"),
119 pubkey!("3UQUKjhMKaY2S6bjcQD6yHB7utcZt5bfarRCmctpRtUd"),
120 pubkey!("FogxVNs6Mm2w9rnGL1vkARSwJxvLE8mujTv3LK8RnUhF"),
121];
122
123pub const NODE1_TIP_ACCOUNTS: &[Pubkey] = &[
124 pubkey!("node1PqAa3BWWzUnTHVbw8NJHC874zn9ngAkXjgWEej"),
125 pubkey!("node1UzzTxAAeBTpfZkQPJXBAqixsbdth11ba1NXLBG"),
126 pubkey!("node1Qm1bV4fwYnCurP8otJ9s5yrkPq7SPZ5uhj3Tsv"),
127 pubkey!("node1PUber6SFmSQgvf2ECmXsHP5o3boRSGhvJyPMX1"),
128 pubkey!("node1AyMbeqiVN6eoQzEAwCA6Pk826hrdqdAHR7cdJ3"),
129 pubkey!("node1YtWCoTwwVYTFLfS19zquRQzYX332hs1HEuRBjC"),
130];
131
132pub const FLASHBLOCK_TIP_ACCOUNTS: &[Pubkey] = &[
133 pubkey!("FLaShB3iXXTWE1vu9wQsChUKq3HFtpMAhb8kAh1pf1wi"),
134 pubkey!("FLashhsorBmM9dLpuq6qATawcpqk1Y2aqaZfkd48iT3W"),
135 pubkey!("FLaSHJNm5dWYzEgnHJWWJP5ccu128Mu61NJLxUf7mUXU"),
136 pubkey!("FLaSHR4Vv7sttd6TyDF4yR1bJyAxRwWKbohDytEMu3wL"),
137 pubkey!("FLASHRzANfcAKDuQ3RXv9hbkBy4WVEKDzoAgxJ56DiE4"),
138 pubkey!("FLasHstqx11M8W56zrSEqkCyhMCCpr6ze6Mjdvqope5s"),
139 pubkey!("FLAShWTjcweNT4NSotpjpxAkwxUr2we3eXQGhpTVzRwy"),
140 pubkey!("FLasHXTqrbNvpWFB6grN47HGZfK6pze9HLNTgbukfPSk"),
141 pubkey!("FLAshyAyBcKb39KPxSzXcepiS8iDYUhDGwJcJDPX4g2B"),
142 pubkey!("FLAsHZTRcf3Dy1APaz6j74ebdMC6Xx4g6i9YxjyrDybR"),
143];
144
145pub const BLOCKRAZOR_TIP_ACCOUNTS: &[Pubkey] = &[
146 pubkey!("FjmZZrFvhnqqb9ThCuMVnENaM3JGVuGWNyCAxRJcFpg9"),
147 pubkey!("6No2i3aawzHsjtThw81iq1EXPJN6rh8eSJCLaYZfKDTG"),
148 pubkey!("A9cWowVAiHe9pJfKAj3TJiN9VpbzMUq6E4kEvf5mUT22"),
149 pubkey!("Gywj98ophM7GmkDdaWs4isqZnDdFCW7B46TXmKfvyqSm"),
150 pubkey!("68Pwb4jS7eZATjDfhmTXgRJjCiZmw1L7Huy4HNpnxJ3o"),
151 pubkey!("4ABhJh5rZPjv63RBJBuyWzBK3g9gWMUQdTZP2kiW31V9"),
152 pubkey!("B2M4NG5eyZp5SBQrSdtemzk5TqVuaWGQnowGaCBt8GyM"),
153 pubkey!("5jA59cXMKQqZAVdtopv8q3yyw9SYfiE3vUCbt7p8MfVf"),
154 pubkey!("5YktoWygr1Bp9wiS1xtMtUki1PeYuuzuCF98tqwYxf61"),
155 pubkey!("295Avbam4qGShBYK7E9H5Ldew4B3WyJGmgmXfiWdeeyV"),
156 pubkey!("EDi4rSy2LZgKJX74mbLTFk4mxoTgT6F7HxxzG2HBAFyK"),
157 pubkey!("BnGKHAC386n4Qmv9xtpBVbRaUTKixjBe3oagkPFKtoy6"),
158 pubkey!("Dd7K2Fp7AtoN8xCghKDRmyqr5U169t48Tw5fEd3wT9mq"),
159 pubkey!("AP6qExwrbRgBAVaehg4b5xHENX815sMabtBzUzVB4v8S"),
160];
161
162pub const ASTRALANE_TIP_ACCOUNTS: &[Pubkey] = &[
163 pubkey!("astrazznxsGUhWShqgNtAdfrzP2G83DzcWVJDxwV9bF"),
164 pubkey!("astra4uejePWneqNaJKuFFA8oonqCE1sqF6b45kDMZm"),
165 pubkey!("astra9xWY93QyfG6yM8zwsKsRodscjQ2uU2HKNL5prk"),
166 pubkey!("astraRVUuTHjpwEVvNBeQEgwYx9w9CFyfxjYoobCZhL"),
167 pubkey!("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"),
168 pubkey!("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"),
169 pubkey!("astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk"),
170 pubkey!("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"),
171 pubkey!("AstrA1ejL4UeXC2SBP4cpeEmtcFPZVLxx3XGKXyCW6to"),
172 pubkey!("AsTra79FET4aCKWspPqeSFvjJNyp96SvAnrmyAxqg5b7"),
173 pubkey!("AstrABAu8CBTyuPXpV4eSCJ5fePEPnxN8NqBaPKQ9fHR"),
174 pubkey!("AsTRADtvb6tTmrsqULQ9Wji9PigDMjhfEMza6zkynEvV"),
175 pubkey!("AsTRAEoyMofR3vUPpf9k68Gsfb6ymTZttEtsAbv8Bk4d"),
176 pubkey!("AStrAJv2RN2hKCHxwUMtqmSxgdcNZbihCwc1mCSnG83W"),
177 pubkey!("Astran35aiQUF57XZsmkWMtNCtXGLzs8upfiqXxth2bz"),
178 pubkey!("AStRAnpi6kFrKypragExgeRoJ1QnKH7pbSjLAKQVWUum"),
179 pubkey!("ASTRaoF93eYt73TYvwtsv6fMWHWbGmMUZfVZPo3CRU9C"),
180];
181
182pub const STELLIUM_TIP_ACCOUNTS: &[Pubkey] = &[
183 pubkey!("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"),
184 pubkey!("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"),
185 pubkey!("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"),
186 pubkey!("ste11p7e2KLYou5bwtt35H7BM6uMdo4pvioGjJXKFcN"),
187 pubkey!("ste11TMV68LMi1BguM4RQujtbNCZvf1sjsASpqgAvSX"),
188];
189
190pub const LIGHTSPEED_TIP_ACCOUNTS: &[Pubkey] = &[
191 pubkey!("53PhM3UTdMQWu5t81wcd35AHGc5xpmHoRjem7GQPvXjA"),
192 pubkey!("9tYF5yPDC1NP8s6diiB3kAX6ZZnva9DM3iDwJkBRarBB"),
193];
194
195pub const SOYAS_TIP_ACCOUNTS: &[Pubkey] = &[
196 pubkey!("soyas4s6L8KWZ8rsSk1mF3d1mQScoTGGAgjk98bF8nP"),
197 pubkey!("soyascXFW5wEEYiwfEmHy2pNwomqzvggJosGVD6TJdY"),
198 pubkey!("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"),
199 pubkey!("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"),
200 pubkey!("soyasi59njacMUPvo3TM5paHjeK8pYSdovXgFi32gRt"),
201 pubkey!("soyasQYhJxv8uZgWDxhg72td6piAf7XTkoyWHtSATEz"),
202 pubkey!("soyastP66xyYC8XADXZjdMM5BAVGD2YRvz8dwtLsqb8"),
203 pubkey!("soyasvdgUJWYcUCzDxpmjUnNjH7KamXLXTzLwFvdVPE"),
204 pubkey!("soyasvxAunisNxaoRxkKGjNir7KmbwYnr37JmefkX9G"),
205 pubkey!("soyas5doVFUwH8s5zK8gEvCL5KR5ogDmf52LsrJEZ9h"),
206];
207
208pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
209 pubkey!("SpEEdz8S1KorkMZqjMUxfxrmWwofmp6ReNP2Nx6CUmq"),
210 pubkey!("SpeeDy3GJM4wcrQmk1itRFWgidvxX4rwjTLMv78wwjE"),
211 pubkey!("SPeEdva37vW8vRtqgYjprQs1g3965icfVN5Rt7SMAyh"),
212 pubkey!("speEdrSEpox5GUfHWcBc7tQjRuSfUin2yvB7qoYvvJh"),
213 pubkey!("SPeEDmkHkN3A2roSZf6aZyEMsmrGqTHKqwP51y2Y4rV"),
214 pubkey!("SpeedLdTJXh2RKpXEaP8JCxkWoUVXhtdPQ1EnxBJMxc"),
215 pubkey!("SpEediGKLbbXndSYTzwmz6Z3NDgHQLDcTDEvGFkSMH9"),
216 pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"),
217];
218
219pub const SOLAMI_TIP_ACCOUNTS: &[Pubkey] = &[
220 pubkey!("15qWd4huAkoxvhDsHMfpUn27TW1YBYMMJJ2jkAkbeam"),
221 pubkey!("9XuGciSwr5wb7dLTQm91JhuBTvj3GG8WjuRDc3obeam"),
222 pubkey!("kiQioJNyFG7pU36ELLsRKXkeT48kFbk3b6rSgrWbeam"),
223 pubkey!("kjmVhW1UzJrW2sU5bY5NtZ79jpvjSStsj37Pzmabeam"),
224 pubkey!("kREnjPWFpt4AHeY5pijPmyXaCrMnbatUQJo7d3Xbeam"),
225 pubkey!("praRZG6N6MdbsT4EFpKgZJWReZGXQhAMFcH68oCbeam"),
226 pubkey!("SqoKQKU5uwBxovq3R7yEBxFwptc4z7vwoghU3M9beam"),
227 pubkey!("sV72TY66T1RfmDSeHPPbwX6wwJ3bBv5hd4ehJ8tbeam"),
228 pubkey!("swf8MyEeLo7gtRUo27UuJj6naCASUrypU7dbteSbeam"),
229 pubkey!("uiuaQsxA47JybQAVN4FTfYuoEDkMiXV1r591Aewbeam"),
230];
231
232pub const LUNARLANDER_TIP_ACCOUNTS: &[Pubkey] = &[
233 pubkey!("moon17L6BgxXRX5uHKudAmqVF96xia9h8ygcmG2sL3F"),
234 pubkey!("moon26Sek222Md7ZydcAGxoKG832DK36CkLrS3PQY4c"),
235 pubkey!("moon7fwyajcVstMoBnVy7UBcTx87SBtNoGGAaH2Cb8V"),
236 pubkey!("moonBtH9HvLHjLqi9ivyrMVKgFUsSfrz9BwQ9khhn1u"),
237 pubkey!("moonCJg8476LNFLptX1qrK8PdRsA1HD1R6XWyu9MB93"),
238 pubkey!("moonF2sz7qwAtdETnrgxNbjonnhGGjd6r4W4UC9284s"),
239 pubkey!("moonKfftMiGSak3cezvhEqvkPSzwrmQxQHXuspC96yj"),
240 pubkey!("moonQBUKBpkifLcTd78bfxxt4PYLwmJ5admLW6cBBs8"),
241 pubkey!("moonXwpKwoVkMegt5Bc776cSW793X1irL5hHV1vJ3JA"),
242 pubkey!("moonZ6u9E2fgk6eWd82621eLPHt9zuJuYECXAYjMY1C"),
243];
244
245pub const GLAIVE_TIP_ACCOUNTS: &[Pubkey] = &[
246 pubkey!("GLaiv4GMRYQmthatDS98uQT4HoucgxWT8NeJz6oSwxeU"),
247 pubkey!("GLaivL5uPrDpvd1wTtvat38KGqb5WLhEdqQfnmNd3oNr"),
248 pubkey!("GLaivinAWh21NaJMhtExtD5G2gZs1xnvaYVZmwqobWZL"),
249 pubkey!("GLaivJSUL71FcocYa8tks5vpVyYzvaDMHtyrzfQF2ABr"),
250 pubkey!("GLaivRU6eDKrta3p3psFAWPEFLzCjeMHGpPUuQqTjtyv"),
251 pubkey!("GLaivq5dU8qHayz9Qf13LjPfVy3SmUhbmickfGiZdmfh"),
252];
253
254pub const SWQOS_TIP_ACCOUNT_GROUPS: &[SwqosTipAccountGroup] = &[
256 SwqosTipAccountGroup { provider: SwqosProvider::Jito, accounts: JITO_TIP_ACCOUNTS },
257 SwqosTipAccountGroup { provider: SwqosProvider::NextBlock, accounts: NEXTBLOCK_TIP_ACCOUNTS },
258 SwqosTipAccountGroup { provider: SwqosProvider::ZeroSlot, accounts: ZEROSLOT_TIP_ACCOUNTS },
259 SwqosTipAccountGroup { provider: SwqosProvider::Temporal, accounts: NOZOMI_TIP_ACCOUNTS },
260 SwqosTipAccountGroup { provider: SwqosProvider::Bloxroute, accounts: BLOX_TIP_ACCOUNTS },
261 SwqosTipAccountGroup { provider: SwqosProvider::Node1, accounts: NODE1_TIP_ACCOUNTS },
262 SwqosTipAccountGroup { provider: SwqosProvider::FlashBlock, accounts: FLASHBLOCK_TIP_ACCOUNTS },
263 SwqosTipAccountGroup { provider: SwqosProvider::BlockRazor, accounts: BLOCKRAZOR_TIP_ACCOUNTS },
264 SwqosTipAccountGroup { provider: SwqosProvider::Astralane, accounts: ASTRALANE_TIP_ACCOUNTS },
265 SwqosTipAccountGroup { provider: SwqosProvider::Stellium, accounts: STELLIUM_TIP_ACCOUNTS },
266 SwqosTipAccountGroup { provider: SwqosProvider::Lightspeed, accounts: LIGHTSPEED_TIP_ACCOUNTS },
267 SwqosTipAccountGroup { provider: SwqosProvider::Soyas, accounts: SOYAS_TIP_ACCOUNTS },
268 SwqosTipAccountGroup {
269 provider: SwqosProvider::Speedlanding,
270 accounts: SPEEDLANDING_TIP_ACCOUNTS,
271 },
272 SwqosTipAccountGroup { provider: SwqosProvider::Helius, accounts: HELIUS_TIP_ACCOUNTS },
273 SwqosTipAccountGroup { provider: SwqosProvider::Solami, accounts: SOLAMI_TIP_ACCOUNTS },
274 SwqosTipAccountGroup {
275 provider: SwqosProvider::LunarLander,
276 accounts: LUNARLANDER_TIP_ACCOUNTS,
277 },
278 SwqosTipAccountGroup { provider: SwqosProvider::Glaive, accounts: GLAIVE_TIP_ACCOUNTS },
279];
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
282pub struct TipPayment {
283 pub provider: SwqosProvider,
284 pub source: Pubkey,
285 pub recipient: Pubkey,
286 pub lamports: u64,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
290pub struct TransactionCost {
291 pub transaction_fee_lamports: Option<u64>,
293 pub total_fee_and_tip_lamports: Option<u64>,
295 pub compute_units_consumed: Option<u64>,
296 pub compute_unit_limit: Option<u32>,
297 pub compute_unit_price_micro_lamports: Option<u64>,
298 pub priority_fee_lamports: Option<u64>,
300 pub tip_payments_confirmed: bool,
302 pub tip_lamports: u64,
303 pub tip_payments: Vec<TipPayment>,
304}
305
306impl TransactionCost {
307 #[inline]
309 pub fn tip_lamports_for(&self, provider: SwqosProvider) -> u64 {
310 self.tip_payments
311 .iter()
312 .filter(|payment| payment.provider == provider)
313 .fold(0u64, |total, payment| total.saturating_add(payment.lamports))
314 }
315}
316
317#[derive(Default)]
318struct ScanState {
319 compute_unit_limit: Option<u32>,
320 compute_unit_price_micro_lamports: Option<u64>,
321 direct_priority_fee_lamports: Option<u64>,
322 is_v1: bool,
323 seen_compute_budget_tags: u8,
324 invalid_compute_budget: bool,
325 tip_lamports: u64,
326 tip_payments: Vec<TipPayment>,
327}
328
329impl ScanState {
330 #[inline]
331 fn with_v1_fields(compute_unit_limit: Option<u32>, priority_fee: Option<u64>) -> Self {
332 Self {
333 compute_unit_limit,
334 direct_priority_fee_lamports: priority_fee,
335 is_v1: true,
336 ..Self::default()
337 }
338 }
339
340 #[inline]
341 fn finish(
342 mut self,
343 transaction_fee_lamports: Option<u64>,
344 compute_units_consumed: Option<u64>,
345 tip_payments_confirmed: bool,
346 ) -> TransactionCost {
347 if self.invalid_compute_budget {
348 self.compute_unit_limit = None;
349 self.compute_unit_price_micro_lamports = None;
350 }
351 let priority_fee_lamports = self.direct_priority_fee_lamports.or_else(|| {
352 self.compute_unit_limit.zip(self.compute_unit_price_micro_lamports).map(
353 |(limit, price)| priority_fee_lamports(limit.min(MAX_COMPUTE_UNIT_LIMIT), price),
354 )
355 });
356 TransactionCost {
357 transaction_fee_lamports,
358 total_fee_and_tip_lamports: transaction_fee_lamports
359 .map(|fee| fee.saturating_add(self.tip_lamports)),
360 compute_units_consumed,
361 compute_unit_limit: self.compute_unit_limit,
362 compute_unit_price_micro_lamports: self.compute_unit_price_micro_lamports,
363 priority_fee_lamports,
364 tip_payments_confirmed,
365 tip_lamports: self.tip_lamports,
366 tip_payments: self.tip_payments,
367 }
368 }
369}
370
371pub fn parse_rpc_transaction_cost(
373 transaction: &EncodedConfirmedTransactionWithStatusMeta,
374) -> Result<TransactionCost, ParseError> {
375 let (meta, transaction) = convert_rpc_to_grpc(transaction)?;
376 parse_yellowstone_transaction_cost(&transaction, &meta)
377 .ok_or_else(|| ParseError::MissingField("transaction.message".to_string()))
378}
379
380pub fn parse_yellowstone_transaction_cost(
384 transaction: &Transaction,
385 meta: &TransactionStatusMeta,
386) -> Option<TransactionCost> {
387 let message = transaction.message.as_ref()?;
388 let get_key = |index: usize| {
389 let static_len = message.account_keys.len();
390 if index < static_len {
391 return pubkey_from_bytes(message.account_keys.get(index)?);
392 }
393 let writable_index = index - static_len;
394 if writable_index < meta.loaded_writable_addresses.len() {
395 return pubkey_from_bytes(meta.loaded_writable_addresses.get(writable_index)?);
396 }
397 let readonly_index = writable_index - meta.loaded_writable_addresses.len();
398 pubkey_from_bytes(meta.loaded_readonly_addresses.get(readonly_index)?)
399 };
400
401 let mut state = message
402 .config
403 .as_ref()
404 .map(|config| ScanState::with_v1_fields(config.compute_unit_limit, config.priority_fee))
405 .unwrap_or_default();
406 for instruction in &message.instructions {
407 scan_instruction(
408 instruction.program_id_index as usize,
409 &instruction.accounts,
410 &instruction.data,
411 &get_key,
412 meta.err.is_none(),
413 &mut state,
414 );
415 }
416 if meta.err.is_none() {
417 for group in &meta.inner_instructions {
418 for instruction in &group.instructions {
419 scan_inner_tip(instruction, &get_key, &mut state);
420 }
421 }
422 }
423
424 Some(state.finish(Some(meta.fee), meta.compute_units_consumed, meta.err.is_none()))
425}
426
427pub fn parse_shred_transaction_cost(transaction: &VersionedTransaction) -> TransactionCost {
432 let message = &transaction.message;
433 let static_keys = message.static_account_keys();
434 let get_key = |index: usize| static_keys.get(index).copied();
435 let mut state = match message {
436 solana_sdk::message::VersionedMessage::V1(message) => ScanState::with_v1_fields(
437 message.config.compute_unit_limit,
438 message.config.priority_fee,
439 ),
440 solana_sdk::message::VersionedMessage::Legacy(_)
441 | solana_sdk::message::VersionedMessage::V0(_) => ScanState::default(),
442 };
443 for instruction in message.instructions() {
444 scan_instruction(
445 instruction.program_id_index as usize,
446 &instruction.accounts,
447 &instruction.data,
448 &get_key,
449 true,
450 &mut state,
451 );
452 }
453 state.finish(None, None, false)
454}
455
456#[inline]
457fn scan_inner_tip<F>(instruction: &InnerInstruction, get_key: &F, state: &mut ScanState)
458where
459 F: Fn(usize) -> Option<Pubkey>,
460{
461 let Some(program_id) = get_key(instruction.program_id_index as usize) else {
462 return;
463 };
464 if program_id == SYSTEM_PROGRAM_ID {
465 scan_system_transfer(&instruction.accounts, &instruction.data, get_key, state);
466 }
467}
468
469#[inline]
470fn scan_instruction<F>(
471 program_id_index: usize,
472 accounts: &[u8],
473 data: &[u8],
474 get_key: &F,
475 scan_tips: bool,
476 state: &mut ScanState,
477) where
478 F: Fn(usize) -> Option<Pubkey>,
479{
480 let Some(program_id) = get_key(program_id_index) else {
481 return;
482 };
483 if program_id == COMPUTE_BUDGET_PROGRAM_ID && !state.is_v1 {
484 scan_compute_budget(data, state);
485 } else if scan_tips && program_id == SYSTEM_PROGRAM_ID {
486 scan_system_transfer(accounts, data, get_key, state);
487 }
488}
489
490#[inline]
491fn scan_compute_budget(data: &[u8], state: &mut ScanState) {
492 let Some(tag) = data.first().copied() else {
493 state.invalid_compute_budget = true;
494 return;
495 };
496 if tag > SET_LOADED_ACCOUNTS_DATA_SIZE_LIMIT_TAG {
497 state.invalid_compute_budget = true;
498 return;
499 }
500 let tag_mask = 1u8 << tag;
501 if state.seen_compute_budget_tags & tag_mask != 0 {
502 state.invalid_compute_budget = true;
503 return;
504 }
505 state.seen_compute_budget_tags |= tag_mask;
506
507 match tag {
508 SET_COMPUTE_UNIT_LIMIT_TAG => {
509 let Some(value) = read_u32_exact(data) else {
510 state.invalid_compute_budget = true;
511 return;
512 };
513 state.compute_unit_limit = Some(value);
514 }
515 SET_COMPUTE_UNIT_PRICE_TAG => {
516 let Some(value) = read_u64_exact(data) else {
517 state.invalid_compute_budget = true;
518 return;
519 };
520 state.compute_unit_price_micro_lamports = Some(value);
521 }
522 REQUEST_HEAP_FRAME_TAG | SET_LOADED_ACCOUNTS_DATA_SIZE_LIMIT_TAG if data.len() == 5 => {}
523 _ => state.invalid_compute_budget = true,
524 }
525}
526
527#[inline]
528fn scan_system_transfer<F>(accounts: &[u8], data: &[u8], get_key: &F, state: &mut ScanState)
529where
530 F: Fn(usize) -> Option<Pubkey>,
531{
532 if data.len() != 12 || data.get(..4) != Some(SYSTEM_TRANSFER_TAG.as_slice()) {
533 return;
534 }
535 let Some(source) = accounts.first().and_then(|index| get_key(*index as usize)) else {
536 return;
537 };
538 let Some(recipient) = accounts.get(1).and_then(|index| get_key(*index as usize)) else {
539 return;
540 };
541 let Some(provider) = find_tip_provider(&recipient) else {
542 return;
543 };
544 let lamports = u64::from_le_bytes(data[4..12].try_into().expect("validated transfer length"));
545 state.tip_lamports = state.tip_lamports.saturating_add(lamports);
546 state.tip_payments.push(TipPayment { provider, source, recipient, lamports });
547}
548
549#[inline]
550fn find_tip_provider(recipient: &Pubkey) -> Option<SwqosProvider> {
551 SWQOS_TIP_ACCOUNT_GROUPS
552 .iter()
553 .find(|group| group.accounts.contains(recipient))
554 .map(|group| group.provider)
555}
556
557#[inline]
558fn priority_fee_lamports(compute_unit_limit: u32, micro_lamports: u64) -> u64 {
559 let fee = (compute_unit_limit as u128).saturating_mul(micro_lamports as u128);
560 fee.saturating_add(MICRO_LAMPORTS_PER_LAMPORT - 1)
561 .checked_div(MICRO_LAMPORTS_PER_LAMPORT)
562 .and_then(|value| u64::try_from(value).ok())
563 .unwrap_or(u64::MAX)
564}
565
566#[inline]
567fn read_u32_exact(data: &[u8]) -> Option<u32> {
568 (data.len() == 5).then(|| u32::from_le_bytes(data[1..5].try_into().unwrap()))
569}
570
571#[inline]
572fn read_u64_exact(data: &[u8]) -> Option<u64> {
573 (data.len() == 9).then(|| u64::from_le_bytes(data[1..9].try_into().unwrap()))
574}
575
576#[inline]
577fn pubkey_from_bytes(bytes: &[u8]) -> Option<Pubkey> {
578 Pubkey::try_from(bytes).ok()
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584 use base64::Engine as _;
585 use solana_client::rpc_response::UiTransactionError;
586 use solana_sdk::{
587 hash::Hash,
588 message::{
589 compiled_instruction::CompiledInstruction as SdkInstruction, v0, v1, MessageHeader,
590 VersionedMessage,
591 },
592 signature::Signature,
593 };
594 use solana_transaction_status::{
595 option_serializer::OptionSerializer, EncodedConfirmedTransactionWithStatusMeta,
596 EncodedTransaction, EncodedTransactionWithStatusMeta, TransactionBinaryEncoding,
597 UiTransactionStatusMeta,
598 };
599 use yellowstone_grpc_proto::prelude::{
600 CompiledInstruction, InnerInstructions, Message, TransactionError, TransactionStatusMeta,
601 };
602
603 fn compute_limit(units: u32) -> Vec<u8> {
604 let mut data = vec![SET_COMPUTE_UNIT_LIMIT_TAG];
605 data.extend_from_slice(&units.to_le_bytes());
606 data
607 }
608
609 fn compute_price(micro_lamports: u64) -> Vec<u8> {
610 let mut data = vec![SET_COMPUTE_UNIT_PRICE_TAG];
611 data.extend_from_slice(µ_lamports.to_le_bytes());
612 data
613 }
614
615 fn transfer(lamports: u64) -> Vec<u8> {
616 let mut data = SYSTEM_TRANSFER_TAG.to_vec();
617 data.extend_from_slice(&lamports.to_le_bytes());
618 data
619 }
620
621 fn grpc_instruction(
622 program_id_index: u32,
623 accounts: Vec<u8>,
624 data: Vec<u8>,
625 ) -> CompiledInstruction {
626 CompiledInstruction { program_id_index, accounts, data }
627 }
628
629 fn v1_transaction() -> VersionedTransaction {
630 let source = Pubkey::new_unique();
631 VersionedTransaction {
632 signatures: vec![Signature::from([7; 64])],
633 message: VersionedMessage::V1(v1::Message {
634 header: MessageHeader {
635 num_required_signatures: 1,
636 num_readonly_signed_accounts: 0,
637 num_readonly_unsigned_accounts: 2,
638 },
639 config: v1::TransactionConfig::empty()
640 .with_compute_unit_limit(234_567)
641 .with_priority_fee(4_321),
642 lifetime_specifier: Hash::new_unique(),
643 account_keys: vec![
644 source,
645 JITO_TIP_ACCOUNTS[0],
646 SYSTEM_PROGRAM_ID,
647 COMPUTE_BUDGET_PROGRAM_ID,
648 ],
649 instructions: vec![
650 SdkInstruction::new_from_raw_parts(3, compute_limit(999_999), vec![]),
651 SdkInstruction::new_from_raw_parts(3, compute_price(999_999), vec![]),
652 SdkInstruction::new_from_raw_parts(2, transfer(42), vec![0, 1]),
653 ],
654 }),
655 }
656 }
657
658 #[test]
659 fn yellowstone_parser_reads_fee_compute_budget_and_known_tip() {
660 let source = Pubkey::new_unique();
661 let ordinary_recipient = Pubkey::new_unique();
662 let keys = [source, COMPUTE_BUDGET_PROGRAM_ID, SYSTEM_PROGRAM_ID, ordinary_recipient];
663 let transaction = Transaction {
664 signatures: vec![vec![0; 64]],
665 message: Some(Message {
666 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
667 instructions: vec![
668 grpc_instruction(1, vec![], compute_limit(300_000)),
669 grpc_instruction(1, vec![], compute_price(12_345)),
670 grpc_instruction(2, vec![0, 3], transfer(99_999)),
671 grpc_instruction(2, vec![0, 4], transfer(137_273)),
672 ],
673 ..Default::default()
674 }),
675 };
676 let meta = TransactionStatusMeta {
677 fee: 8_704,
678 compute_units_consumed: Some(135_026),
679 loaded_writable_addresses: vec![JITO_TIP_ACCOUNTS[0].to_bytes().to_vec()],
680 ..Default::default()
681 };
682
683 let cost =
684 parse_yellowstone_transaction_cost(&transaction, &meta).expect("transaction cost");
685
686 assert_eq!(cost.transaction_fee_lamports, Some(8_704));
687 assert_eq!(cost.compute_units_consumed, Some(135_026));
688 assert_eq!(cost.compute_unit_limit, Some(300_000));
689 assert_eq!(cost.compute_unit_price_micro_lamports, Some(12_345));
690 assert_eq!(cost.priority_fee_lamports, Some(3_704));
691 assert!(cost.tip_payments_confirmed);
692 assert_eq!(cost.tip_lamports, 137_273);
693 assert_eq!(cost.total_fee_and_tip_lamports, Some(145_977));
694 assert_eq!(cost.tip_payments.len(), 1);
695 assert_eq!(cost.tip_payments[0].provider, SwqosProvider::Jito);
696 assert_eq!(cost.tip_payments[0].recipient, JITO_TIP_ACCOUNTS[0]);
697 assert_eq!(cost.tip_lamports_for(SwqosProvider::Jito), 137_273);
698 }
699
700 #[test]
701 fn legacy_priority_fee_uses_runtime_clamp_but_exposes_requested_limit() {
702 let transaction = Transaction {
703 signatures: vec![],
704 message: Some(Message {
705 account_keys: vec![COMPUTE_BUDGET_PROGRAM_ID.to_bytes().to_vec()],
706 instructions: vec![
707 grpc_instruction(0, vec![], compute_limit(2_000_000)),
708 grpc_instruction(0, vec![], compute_price(1_000_000)),
709 ],
710 ..Default::default()
711 }),
712 };
713
714 let cost =
715 parse_yellowstone_transaction_cost(&transaction, &TransactionStatusMeta::default())
716 .expect("transaction cost");
717
718 assert_eq!(cost.compute_unit_limit, Some(2_000_000));
719 assert_eq!(cost.priority_fee_lamports, Some(1_400_000));
720 }
721
722 #[test]
723 fn malformed_duplicate_and_out_of_bounds_instructions_do_not_panic_or_misreport() {
724 let keys = [COMPUTE_BUDGET_PROGRAM_ID, SYSTEM_PROGRAM_ID];
725 let transaction = Transaction {
726 signatures: vec![],
727 message: Some(Message {
728 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
729 instructions: vec![
730 grpc_instruction(0, vec![], compute_limit(100_000)),
731 grpc_instruction(0, vec![], compute_limit(200_000)),
732 grpc_instruction(0, vec![], vec![SET_COMPUTE_UNIT_PRICE_TAG, 1]),
733 grpc_instruction(1, vec![254, 253], transfer(1_000)),
734 grpc_instruction(250, vec![], vec![]),
735 ],
736 ..Default::default()
737 }),
738 };
739
740 let cost =
741 parse_yellowstone_transaction_cost(&transaction, &TransactionStatusMeta::default())
742 .expect("transaction cost");
743
744 assert_eq!(cost.compute_unit_limit, None);
745 assert_eq!(cost.compute_unit_price_micro_lamports, None);
746 assert_eq!(cost.priority_fee_lamports, None);
747 assert_eq!(cost.tip_lamports, 0);
748 }
749
750 #[test]
751 fn yellowstone_parser_sums_outer_and_inner_tip_payments() {
752 let source = Pubkey::new_unique();
753 let keys = [source, SYSTEM_PROGRAM_ID, JITO_TIP_ACCOUNTS[0], GLAIVE_TIP_ACCOUNTS[0]];
754 let transaction = Transaction {
755 signatures: vec![],
756 message: Some(Message {
757 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
758 instructions: vec![grpc_instruction(1, vec![0, 2], transfer(10))],
759 ..Default::default()
760 }),
761 };
762 let meta = TransactionStatusMeta {
763 inner_instructions: vec![InnerInstructions {
764 index: 0,
765 instructions: vec![InnerInstruction {
766 program_id_index: 1,
767 accounts: vec![0, 3],
768 data: transfer(20),
769 stack_height: Some(2),
770 }],
771 }],
772 ..Default::default()
773 };
774
775 let cost =
776 parse_yellowstone_transaction_cost(&transaction, &meta).expect("transaction cost");
777
778 assert_eq!(cost.tip_lamports, 30);
779 assert_eq!(cost.tip_payments.len(), 2);
780 assert_eq!(cost.tip_lamports_for(SwqosProvider::Jito), 10);
781 assert_eq!(cost.tip_lamports_for(SwqosProvider::Glaive), 20);
782 }
783
784 #[test]
785 fn failed_transaction_does_not_report_rolled_back_tips() {
786 let source = Pubkey::new_unique();
787 let keys = [source, SYSTEM_PROGRAM_ID, JITO_TIP_ACCOUNTS[0]];
788 let transaction = Transaction {
789 signatures: vec![],
790 message: Some(Message {
791 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
792 instructions: vec![grpc_instruction(1, vec![0, 2], transfer(500))],
793 ..Default::default()
794 }),
795 };
796 let meta = TransactionStatusMeta {
797 err: Some(TransactionError { err: vec![1] }),
798 fee: 5_000,
799 ..Default::default()
800 };
801
802 let cost =
803 parse_yellowstone_transaction_cost(&transaction, &meta).expect("transaction cost");
804
805 assert_eq!(cost.transaction_fee_lamports, Some(5_000));
806 assert!(!cost.tip_payments_confirmed);
807 assert_eq!(cost.tip_lamports, 0);
808 assert!(cost.tip_payments.is_empty());
809 }
810
811 #[test]
812 fn inner_compute_budget_instruction_does_not_set_transaction_budget() {
813 let keys = [COMPUTE_BUDGET_PROGRAM_ID];
814 let transaction = Transaction {
815 signatures: vec![],
816 message: Some(Message {
817 account_keys: keys.iter().map(|key| key.to_bytes().to_vec()).collect(),
818 ..Default::default()
819 }),
820 };
821 let meta = TransactionStatusMeta {
822 inner_instructions: vec![InnerInstructions {
823 index: 0,
824 instructions: vec![InnerInstruction {
825 program_id_index: 0,
826 accounts: vec![],
827 data: compute_limit(999_999),
828 stack_height: Some(2),
829 }],
830 }],
831 ..Default::default()
832 };
833
834 let cost =
835 parse_yellowstone_transaction_cost(&transaction, &meta).expect("transaction cost");
836
837 assert_eq!(cost.compute_unit_limit, None);
838 }
839
840 #[test]
841 fn shred_parser_exposes_requested_costs_without_status_metadata() {
842 let source = Pubkey::new_unique();
843 let keys = vec![source, COMPUTE_BUDGET_PROGRAM_ID, SYSTEM_PROGRAM_ID, JITO_TIP_ACCOUNTS[0]];
844 let transaction = VersionedTransaction {
845 signatures: vec![Signature::default()],
846 message: VersionedMessage::V0(v0::Message {
847 header: MessageHeader::default(),
848 account_keys: keys,
849 recent_blockhash: Hash::default(),
850 instructions: vec![
851 SdkInstruction::new_from_raw_parts(1, compute_limit(200_000), vec![]),
852 SdkInstruction::new_from_raw_parts(1, compute_price(5_001), vec![]),
853 SdkInstruction::new_from_raw_parts(2, transfer(42), vec![0, 3]),
854 ],
855 address_table_lookups: vec![],
856 }),
857 };
858
859 let cost = parse_shred_transaction_cost(&transaction);
860
861 assert_eq!(cost.transaction_fee_lamports, None);
862 assert_eq!(cost.compute_units_consumed, None);
863 assert!(!cost.tip_payments_confirmed);
864 assert_eq!(cost.priority_fee_lamports, Some(1_001));
865 assert_eq!(cost.tip_lamports, 42);
866 }
867
868 #[test]
869 fn v1_shred_cost_uses_inline_config_and_ignores_compute_budget_instructions() {
870 let transaction = v1_transaction();
871 let bytes = wincode::serialize(&transaction).expect("serialize V1 transaction");
872 let decoded: VersionedTransaction =
873 wincode::deserialize(&bytes).expect("deserialize V1 transaction");
874
875 assert_eq!(decoded, transaction);
876 let cost = parse_shred_transaction_cost(&decoded);
877 assert_eq!(cost.compute_unit_limit, Some(234_567));
878 assert_eq!(cost.compute_unit_price_micro_lamports, None);
879 assert_eq!(cost.priority_fee_lamports, Some(4_321));
880 assert_eq!(cost.tip_lamports, 42);
881 }
882
883 #[test]
884 fn yellowstone_v1_cost_uses_inline_config_and_ignores_compute_budget_instructions() {
885 let transaction = Transaction {
886 signatures: vec![],
887 message: Some(Message {
888 account_keys: vec![COMPUTE_BUDGET_PROGRAM_ID.to_bytes().to_vec()],
889 instructions: vec![
890 grpc_instruction(0, vec![], compute_limit(999_999)),
891 grpc_instruction(0, vec![], compute_price(999_999)),
892 ],
893 config: Some(yellowstone_grpc_proto::prelude::TransactionConfig {
894 priority_fee: Some(4_321),
895 compute_unit_limit: Some(234_567),
896 loaded_accounts_data_size_limit: None,
897 heap_size: None,
898 }),
899 ..Default::default()
900 }),
901 };
902
903 let cost =
904 parse_yellowstone_transaction_cost(&transaction, &TransactionStatusMeta::default())
905 .expect("transaction cost");
906
907 assert_eq!(cost.compute_unit_limit, Some(234_567));
908 assert_eq!(cost.compute_unit_price_micro_lamports, None);
909 assert_eq!(cost.priority_fee_lamports, Some(4_321));
910 }
911
912 #[test]
913 fn rpc_cost_preserves_v1_inline_config_through_base64_conversion() {
914 let bytes = wincode::serialize(&v1_transaction()).expect("serialize V1 transaction");
915 let transaction = EncodedConfirmedTransactionWithStatusMeta {
916 slot: 42,
917 transaction: EncodedTransactionWithStatusMeta {
918 transaction: EncodedTransaction::Binary(
919 base64::engine::general_purpose::STANDARD.encode(bytes),
920 TransactionBinaryEncoding::Base64,
921 ),
922 meta: Some(UiTransactionStatusMeta {
923 err: None,
924 status: Ok(()),
925 fee: 9_321,
926 pre_balances: vec![],
927 post_balances: vec![],
928 inner_instructions: OptionSerializer::None,
929 log_messages: OptionSerializer::None,
930 pre_token_balances: OptionSerializer::None,
931 post_token_balances: OptionSerializer::None,
932 rewards: OptionSerializer::None,
933 loaded_addresses: OptionSerializer::None,
934 return_data: OptionSerializer::None,
935 compute_units_consumed: OptionSerializer::Some(123_456),
936 cost_units: OptionSerializer::None,
937 }),
938 version: None,
939 },
940 block_time: None,
941 transaction_index: None,
942 };
943
944 let cost = parse_rpc_transaction_cost(&transaction).expect("parse V1 RPC cost");
945 assert_eq!(cost.transaction_fee_lamports, Some(9_321));
946 assert_eq!(cost.compute_units_consumed, Some(123_456));
947 assert_eq!(cost.compute_unit_limit, Some(234_567));
948 assert_eq!(cost.compute_unit_price_micro_lamports, None);
949 assert_eq!(cost.priority_fee_lamports, Some(4_321));
950 assert_eq!(cost.tip_lamports, 42);
951 assert_eq!(cost.total_fee_and_tip_lamports, Some(9_363));
952 }
953
954 #[test]
955 fn failed_rpc_transaction_does_not_confirm_rolled_back_tip() {
956 let bytes = wincode::serialize(&v1_transaction()).expect("serialize V1 transaction");
957 let ui_error: UiTransactionError =
958 solana_sdk::transaction::TransactionError::AccountInUse.into();
959 let transaction = EncodedConfirmedTransactionWithStatusMeta {
960 slot: 42,
961 transaction: EncodedTransactionWithStatusMeta {
962 transaction: EncodedTransaction::Binary(
963 base64::engine::general_purpose::STANDARD.encode(bytes),
964 TransactionBinaryEncoding::Base64,
965 ),
966 meta: Some(UiTransactionStatusMeta {
967 err: Some(ui_error.clone()),
968 status: Err(ui_error),
969 fee: 9_321,
970 pre_balances: vec![],
971 post_balances: vec![],
972 inner_instructions: OptionSerializer::None,
973 log_messages: OptionSerializer::None,
974 pre_token_balances: OptionSerializer::None,
975 post_token_balances: OptionSerializer::None,
976 rewards: OptionSerializer::None,
977 loaded_addresses: OptionSerializer::None,
978 return_data: OptionSerializer::None,
979 compute_units_consumed: OptionSerializer::Some(123_456),
980 cost_units: OptionSerializer::None,
981 }),
982 version: None,
983 },
984 block_time: None,
985 transaction_index: None,
986 };
987
988 let (meta, _) = convert_rpc_to_grpc(&transaction).expect("convert failed RPC meta");
989 let decoded_error: solana_sdk::transaction::TransactionError =
990 wincode::deserialize(&meta.err.expect("failed status").err)
991 .expect("deserialize transaction error");
992 assert_eq!(decoded_error, solana_sdk::transaction::TransactionError::AccountInUse);
993
994 let cost = parse_rpc_transaction_cost(&transaction).expect("parse failed RPC cost");
995 assert!(!cost.tip_payments_confirmed);
996 assert_eq!(cost.tip_lamports, 0);
997 assert!(cost.tip_payments.is_empty());
998 assert_eq!(cost.total_fee_and_tip_lamports, Some(9_321));
999 }
1000
1001 #[test]
1002 fn recognizes_every_sol_trade_sdk_provider() {
1003 assert_eq!(SWQOS_TIP_ACCOUNT_GROUPS.len(), 17);
1004 for group in SWQOS_TIP_ACCOUNT_GROUPS {
1005 assert!(!group.accounts.is_empty());
1006 assert_eq!(find_tip_provider(&group.accounts[0]), Some(group.provider));
1007 }
1008 }
1009
1010 #[test]
1011 fn provider_tip_accounts_are_unique() {
1012 for (group_index, group) in SWQOS_TIP_ACCOUNT_GROUPS.iter().enumerate() {
1013 for (account_index, account) in group.accounts.iter().enumerate() {
1014 for candidate_group in &SWQOS_TIP_ACCOUNT_GROUPS[group_index..] {
1015 let start = if candidate_group.provider == group.provider {
1016 account_index + 1
1017 } else {
1018 0
1019 };
1020 assert!(
1021 !candidate_group.accounts[start..].contains(account),
1022 "duplicate tip account {account}"
1023 );
1024 }
1025 }
1026 }
1027 }
1028
1029 #[test]
1030 fn priority_fee_rounds_up_and_saturates() {
1031 assert_eq!(priority_fee_lamports(1, 1), 1);
1032 assert_eq!(priority_fee_lamports(200_000, 5_000), 1_000);
1033 assert_eq!(priority_fee_lamports(u32::MAX, u64::MAX), u64::MAX);
1034 }
1035}