1use anyhow::{anyhow, Context, Result};
15use arrow::array::{
16 builder, Array, BinaryArray, BooleanArray, GenericBinaryArray, GenericListArray,
17 GenericStringArray, LargeBinaryArray, LargeStringArray, OffsetSizeTrait, StringArray,
18};
19use arrow::{
20 array::RecordBatch,
21 compute,
22 datatypes::{DataType, Field, Schema},
23};
24use base64::{engine::general_purpose::STANDARD, Engine as _};
25use std::sync::Arc;
26mod deserialize;
27pub use deserialize::{deserialize_data, DynType, DynValue, ParamInput};
28mod arrow_converter;
29use arrow_converter::{to_arrow, to_arrow_dtype};
30
31#[derive(Debug, Clone)]
33pub struct InstructionSignature {
34 pub discriminator: Vec<u8>,
35 pub params: Vec<ParamInput>,
36 pub accounts_names: Vec<String>,
37}
38
39#[derive(Debug, Clone)]
41pub struct LogSignature {
42 pub params: Vec<ParamInput>,
43}
44
45#[cfg(feature = "pyo3")]
46impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for InstructionSignature {
47 type Error = pyo3::PyErr;
48 fn extract(ob: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
49 use pyo3::types::PyAnyMethods;
50 use pyo3::types::PyTypeMethods;
51
52 let discriminator_ob = ob.getattr("discriminator")?;
53
54 let discriminator_ob_type: String = discriminator_ob.get_type().name()?.to_string();
55 let discriminator = match discriminator_ob_type.as_str() {
56 "str" => {
57 let s: &str = discriminator_ob.extract()?;
58 hex_to_bytes(s).context("failed to decode hex")?
59 }
60 "bytes" => discriminator_ob.extract()?,
61 _ => return Err(anyhow!("unknown type: {discriminator_ob_type}").into()),
62 };
63
64 let params = ob.getattr("params")?.extract::<Vec<ParamInput>>()?;
65 let accounts_names = ob.getattr("accounts_names")?.extract::<Vec<String>>()?;
66
67 Ok(InstructionSignature {
68 discriminator,
69 params,
70 accounts_names,
71 })
72 }
73}
74
75#[cfg(feature = "pyo3")]
76fn hex_to_bytes(hex_string: &str) -> Result<Vec<u8>> {
77 let hex_string = hex_string.strip_prefix("0x").unwrap_or(hex_string);
78 let hex_string = if hex_string.len() % 2 == 1 {
79 format!("0{hex_string}")
80 } else {
81 hex_string.to_string()
82 };
83 let out = (0..hex_string.len())
84 .step_by(2)
85 .map(|i| {
86 u8::from_str_radix(&hex_string[i..i + 2], 16)
87 .context("failed to parse hexstring to bytes")
88 })
89 .collect::<Result<Vec<_>, _>>()?;
90
91 Ok(out)
92}
93
94#[cfg(feature = "pyo3")]
95impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for LogSignature {
96 type Error = pyo3::PyErr;
97 fn extract(ob: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
98 use pyo3::types::PyAnyMethods;
99
100 let params = ob.getattr("params")?.extract::<Vec<ParamInput>>()?;
101
102 Ok(LogSignature { params })
103 }
104}
105
106fn unpack_rest_of_accounts<ListI: OffsetSizeTrait, InnerI: OffsetSizeTrait>(
107 num_acc: usize,
108 rest_of_acc: &GenericListArray<ListI>,
109 account_arrays: &mut Vec<BinaryArray>,
110) -> Result<()> {
111 let data_size = rest_of_acc.len() * 32;
112
113 for acc_arr in rest_of_acc.iter().flatten() {
114 if acc_arr.len() < num_acc {
115 return Err(anyhow!(
116 "expected rest_of_accounts to have at least {} addresses but it has {}",
117 num_acc,
118 acc_arr.len()
119 ));
120 }
121 }
122
123 for i in 0..num_acc {
124 let mut builder = builder::BinaryBuilder::with_capacity(rest_of_acc.len(), data_size);
125
126 for acc_arr in rest_of_acc.iter() {
127 let Some(acc_arr) = acc_arr else {
128 builder.append_null();
129 continue;
130 };
131
132 let arr = acc_arr
133 .as_any()
134 .downcast_ref::<GenericBinaryArray<InnerI>>()
135 .context("failed to downcast account array in rest_of_accounts")?;
136 if arr.is_null(i) {
137 builder.append_null();
138 } else {
139 builder.append_value(arr.value(i));
140 }
141 }
142
143 account_arrays.push(builder.finish());
144 }
145
146 Ok(())
147}
148
149pub fn decode_instructions_batch(
154 signature: &InstructionSignature,
155 batch: &RecordBatch,
156 allow_decode_fail: bool,
157 filter_by_discriminator: bool,
158 hstack: bool,
159) -> Result<RecordBatch> {
160 let batch = if filter_by_discriminator {
161 filter_by_discriminator_impl(signature, batch)?
162 } else {
163 batch.clone()
164 };
165 let batch = &batch;
166
167 let mut account_arrays: Vec<BinaryArray> = Vec::with_capacity(20);
168
169 for i in 0..signature.accounts_names.len().min(10) {
170 let col_name = format!("a{i}");
171 let col = batch
172 .column_by_name(&col_name)
173 .with_context(|| format!("account {i} not found but was required"))?;
174
175 if col.data_type() == &DataType::Binary {
176 account_arrays.push(
177 col.as_any()
178 .downcast_ref::<BinaryArray>()
179 .context("failed to downcast Binary account column")?
180 .clone(),
181 );
182 } else if col.data_type() == &DataType::LargeBinary {
183 account_arrays.push(
184 arrow::compute::cast(col, &DataType::Binary)
185 .context("failed to cast LargeBinary account column to Binary")?
186 .as_any()
187 .downcast_ref::<BinaryArray>()
188 .context("failed to downcast casted account column to BinaryArray")?
189 .clone(),
190 );
191 }
192 }
193
194 if signature.accounts_names.len() > 10 {
195 let rest_of_acc = batch
196 .column_by_name("rest_of_accounts")
197 .context("rest_of_accounts column not found in instructions batch")?;
198
199 let num_acc = signature.accounts_names.len() - 10;
200 if rest_of_acc.data_type() == &DataType::new_list(DataType::Binary, true) {
201 unpack_rest_of_accounts::<i32, i32>(
202 num_acc,
203 rest_of_acc
204 .as_any()
205 .downcast_ref()
206 .context("failed to downcast rest_of_accounts to List<Binary>")?,
207 &mut account_arrays,
208 )
209 .context("unpack rest_of_accounts column")?;
210 } else if rest_of_acc.data_type() == &DataType::new_list(DataType::LargeBinary, true) {
211 unpack_rest_of_accounts::<i32, i64>(
212 num_acc,
213 rest_of_acc
214 .as_any()
215 .downcast_ref()
216 .context("failed to downcast rest_of_accounts to List<LargeBinary>")?,
217 &mut account_arrays,
218 )
219 .context("unpack rest_of_accounts column")?;
220 } else if rest_of_acc.data_type() == &DataType::new_large_list(DataType::Binary, true) {
221 unpack_rest_of_accounts::<i64, i32>(
222 num_acc,
223 rest_of_acc
224 .as_any()
225 .downcast_ref()
226 .context("failed to downcast rest_of_accounts to LargeList<Binary>")?,
227 &mut account_arrays,
228 )
229 .context("unpack rest_of_accounts column")?;
230 } else if rest_of_acc.data_type() == &DataType::new_large_list(DataType::LargeBinary, true)
231 {
232 unpack_rest_of_accounts::<i64, i64>(
233 num_acc,
234 rest_of_acc
235 .as_any()
236 .downcast_ref()
237 .context("failed to downcast rest_of_accounts to LargeList<LargeBinary>")?,
238 &mut account_arrays,
239 )
240 .context("unpack rest_of_accounts column")?;
241 }
242 }
243
244 let data_col = batch
245 .column_by_name("data")
246 .context("data column not found in instructions batch")?;
247
248 let decoded = if data_col.data_type() == &DataType::Binary {
249 decode_instructions(
250 signature,
251 &account_arrays,
252 data_col
253 .as_any()
254 .downcast_ref::<BinaryArray>()
255 .context("failed to downcast data column to BinaryArray")?,
256 allow_decode_fail,
257 )
258 } else if data_col.data_type() == &DataType::LargeBinary {
259 decode_instructions(
260 signature,
261 &account_arrays,
262 data_col
263 .as_any()
264 .downcast_ref::<LargeBinaryArray>()
265 .context("failed to downcast data column to LargeBinaryArray")?,
266 allow_decode_fail,
267 )
268 } else {
269 Err(anyhow!(
270 "expected the data column to be Binary or LargeBinary"
271 ))
272 }?;
273
274 if hstack {
275 hstack_impl(&decoded, batch)
276 } else {
277 Ok(decoded)
278 }
279}
280
281pub fn decode_instructions<I: OffsetSizeTrait>(
282 signature: &InstructionSignature,
283 accounts: &[BinaryArray],
284 data: &GenericBinaryArray<I>,
285 allow_decode_fail: bool,
286) -> Result<RecordBatch> {
287 let num_params = signature.params.len();
288
289 let mut decoded_params_vec: Vec<Vec<Option<DynValue>>> =
290 (0..num_params).map(|_| Vec::new()).collect();
291
292 for row_idx in 0..data.len() {
293 if data.is_null(row_idx) {
294 if allow_decode_fail {
295 log::debug!("Instruction data is null in row {row_idx}");
296 for v in &mut decoded_params_vec {
297 v.push(None);
298 }
299 continue;
300 }
301 return Err(anyhow::anyhow!("Instruction data is null in row {row_idx}"));
302 }
303
304 let instruction_data = data.value(row_idx).to_vec();
305 let data_result = match_discriminators(&instruction_data, &signature.discriminator);
306 let data = match data_result {
307 Ok(data) => data,
308 Err(e) if allow_decode_fail => {
309 log::debug!("Error matching discriminators in row {row_idx}: {e:?}");
310 for v in &mut decoded_params_vec {
311 v.push(None);
312 }
313 continue;
314 }
315 Err(e) => {
316 return Err(anyhow::anyhow!(
317 "Error matching discriminators in row {row_idx}: {e:?}"
318 ));
319 }
320 };
321
322 let error_on_remanining = false;
328 let decoded_ix_result = deserialize_data(&data, &signature.params, error_on_remanining);
329 let decoded_ix = match decoded_ix_result {
330 Ok(ix) => ix,
331 Err(e) if allow_decode_fail => {
332 log::debug!("Error deserializing instruction in row {row_idx}: {e:?}");
333 for v in &mut decoded_params_vec {
334 v.push(None);
335 }
336 continue;
337 }
338 Err(e) => {
339 return Err(anyhow::anyhow!(
340 "Error deserializing instruction in row {row_idx}: {e:?}"
341 ));
342 }
343 };
344
345 for (i, value) in decoded_ix.into_iter().enumerate() {
346 decoded_params_vec[i].push(Some(value));
347 }
348 }
349
350 let mut data_arrays: Vec<Arc<dyn Array>> = Vec::with_capacity(decoded_params_vec.len());
351 for (i, v) in decoded_params_vec.iter().enumerate() {
352 let array = to_arrow(&signature.params[i].param_type, v.clone())
353 .context("unable to convert instruction value to a arrow format value")?;
354 data_arrays.push(array);
355 }
356
357 let mut data_fields = Vec::with_capacity(signature.params.len());
358 for param in &signature.params {
359 let field = Field::new(
360 param.name.clone(),
361 to_arrow_dtype(¶m.param_type)
362 .context("unable to convert instruction param type to arrow dtype")?,
363 true,
364 );
365 data_fields.push(field);
366 }
367
368 let acc_names_len = signature.accounts_names.len();
369 let mut accounts_arrays = Vec::new();
370 let mut acc_fields = Vec::new();
371
372 for i in 0..acc_names_len {
373 let arr = accounts
374 .get(i)
375 .context(format!("Account a{i} not found during decoding"))?;
376
377 if arr.data_type() == &DataType::LargeBinary {
378 accounts_arrays.push(
379 arrow::compute::cast(arr, &DataType::Binary)
380 .context("failed to cast LargeBinary account to Binary")?,
381 );
382 } else {
383 accounts_arrays.push(Arc::new(arr.clone()) as Arc<dyn Array>);
384 }
385
386 if signature.accounts_names[i].is_empty() {
387 let field = Field::new(format!("a{i}"), DataType::Binary, true);
388 acc_fields.push(field);
389 } else {
390 let field = Field::new(signature.accounts_names[i].clone(), DataType::Binary, true);
391 acc_fields.push(field);
392 }
393 }
394
395 let decoded_instructions_array = data_arrays
396 .into_iter()
397 .chain(accounts_arrays)
398 .collect::<Vec<_>>();
399 let decoded_instructions_fields = data_fields
400 .into_iter()
401 .chain(acc_fields.clone())
402 .collect::<Vec<_>>();
403
404 let schema = Arc::new(Schema::new(decoded_instructions_fields));
405 let batch = RecordBatch::try_new(schema, decoded_instructions_array)
406 .context("Failed to create record batch from data arrays")?;
407
408 Ok(batch)
409}
410
411pub fn decode_logs_batch(
416 signature: &LogSignature,
417 batch: &RecordBatch,
418 allow_decode_fail: bool,
419 hstack: bool,
420) -> Result<RecordBatch> {
421 let message_col = batch
422 .column_by_name("message")
423 .context("message column not found in logs batch")?;
424
425 let decoded = if message_col.data_type() == &DataType::Utf8 {
426 decode_logs(
427 signature,
428 message_col
429 .as_any()
430 .downcast_ref::<StringArray>()
431 .context("failed to downcast message column to StringArray")?,
432 allow_decode_fail,
433 )
434 } else if message_col.data_type() == &DataType::LargeUtf8 {
435 decode_logs(
436 signature,
437 message_col
438 .as_any()
439 .downcast_ref::<LargeStringArray>()
440 .context("failed to downcast message column to LargeStringArray")?,
441 allow_decode_fail,
442 )
443 } else {
444 Err(anyhow!("expected String or LargeString message column"))
445 }?;
446
447 if hstack {
448 hstack_impl(&decoded, batch)
449 } else {
450 Ok(decoded)
451 }
452}
453
454pub fn decode_logs<I: OffsetSizeTrait>(
460 signature: &LogSignature,
461 data: &GenericStringArray<I>,
462 allow_decode_fail: bool,
463) -> Result<RecordBatch> {
464 let num_params = signature.params.len();
465
466 let mut decoded_params_vec: Vec<Vec<Option<DynValue>>> =
467 (0..num_params).map(|_| Vec::new()).collect();
468
469 for row_idx in 0..data.len() {
470 if data.is_null(row_idx) {
471 if allow_decode_fail {
472 log::debug!("Log data is null in row {row_idx}");
473 for v in &mut decoded_params_vec {
474 v.push(None);
475 }
476 continue;
477 }
478 return Err(anyhow::anyhow!("Log data is null in row {row_idx}"));
479 }
480
481 let log_data = data.value(row_idx);
482 let log_data = STANDARD.decode(log_data);
483 let log_data = match log_data {
484 Ok(log_data) => log_data,
485 Err(e) if allow_decode_fail => {
486 log::debug!("Error base 64 decoding log data in row {row_idx}: {e:?}");
487 for v in &mut decoded_params_vec {
488 v.push(None);
489 }
490 continue;
491 }
492 Err(e) => {
493 return Err(anyhow::anyhow!(
494 "Error base 64 decoding log data in row {row_idx}: {e:?}"
495 ));
496 }
497 };
498
499 let decoded_log_result = deserialize_data(&log_data, &signature.params, false);
500 let decoded_log = match decoded_log_result {
501 Ok(log) => log,
502 Err(e) if allow_decode_fail => {
503 log::debug!("Error deserializing log in row {row_idx}: {e:?}");
504 for v in &mut decoded_params_vec {
505 v.push(None);
506 }
507 continue;
508 }
509 Err(e) => {
510 return Err(anyhow::anyhow!(
511 "Error deserializing log in row {row_idx}: {e:?}"
512 ));
513 }
514 };
515
516 for (i, value) in decoded_log.into_iter().enumerate() {
517 decoded_params_vec[i].push(Some(value));
518 }
519 }
520
521 let mut data_arrays: Vec<Arc<dyn Array>> = Vec::with_capacity(decoded_params_vec.len());
522 for (i, v) in decoded_params_vec.iter().enumerate() {
523 let array = to_arrow(&signature.params[i].param_type, v.clone())
524 .context("unable to convert log value to a arrow format value")?;
525 data_arrays.push(array);
526 }
527
528 let mut data_fields = Vec::with_capacity(signature.params.len());
529 for param in &signature.params {
530 let field = Field::new(
531 param.name.clone(),
532 to_arrow_dtype(¶m.param_type)
533 .context("unable to convert log param type to arrow dtype")?,
534 true,
535 );
536 data_fields.push(field);
537 }
538
539 let schema = Arc::new(Schema::new(data_fields));
540 let batch = RecordBatch::try_new(schema, data_arrays)
541 .context("Failed to create record batch from data arrays")?;
542
543 Ok(batch)
544}
545
546pub fn match_discriminators(instr_data: &[u8], discriminator: &[u8]) -> Result<Vec<u8>> {
547 let discriminator_len = discriminator.len();
548 if instr_data.len() < discriminator_len {
549 return Err(anyhow::anyhow!(
550 "Instruction data is too short to contain discriminator. Expected at least {} bytes, got {} bytes",
551 discriminator_len,
552 instr_data.len()
553 ));
554 }
555 let disc = &instr_data[..discriminator_len].to_vec();
556 let ix_data = &instr_data[discriminator_len..];
557 if !disc.eq(discriminator) {
558 return Err(anyhow::anyhow!(
559 "Instruction data discriminator doesn't match signature discriminator"
560 ));
561 }
562 Ok(ix_data.to_vec())
563}
564
565fn filter_by_discriminator_impl(
566 signature: &InstructionSignature,
567 batch: &RecordBatch,
568) -> Result<RecordBatch> {
569 let Some(data_col) = batch.column_by_name("data") else {
570 return Ok(batch.clone());
571 };
572
573 let discriminator = &signature.discriminator;
574 let mask = build_discriminator_mask(data_col.as_ref(), discriminator)
575 .context("build discriminator filter mask")?;
576
577 let non_matching = mask.iter().filter(|v| !v.unwrap_or(false)).count();
578 if non_matching > 0 {
579 log::debug!("filtering out {non_matching} instructions whose discriminator does not match");
580 }
581
582 compute::filter_record_batch(batch, &mask).context("filter record batch by discriminator")
583}
584
585fn build_discriminator_mask(col: &dyn Array, discriminator: &[u8]) -> Result<BooleanArray> {
586 if col.data_type() == &DataType::Binary {
587 let arr = col
588 .as_any()
589 .downcast_ref::<BinaryArray>()
590 .context("downcast data to BinaryArray")?;
591 Ok(BooleanArray::from(
592 arr.iter()
593 .map(|v| v.map(|b| b.starts_with(discriminator)))
594 .collect::<Vec<_>>(),
595 ))
596 } else if col.data_type() == &DataType::LargeBinary {
597 let arr = col
598 .as_any()
599 .downcast_ref::<LargeBinaryArray>()
600 .context("downcast data to LargeBinaryArray")?;
601 Ok(BooleanArray::from(
602 arr.iter()
603 .map(|v| v.map(|b| b.starts_with(discriminator)))
604 .collect::<Vec<_>>(),
605 ))
606 } else {
607 Err(anyhow!(
608 "unexpected data column type {}. Expected Binary or LargeBinary",
609 col.data_type()
610 ))
611 }
612}
613
614fn hstack_impl(decoded: &RecordBatch, input: &RecordBatch) -> Result<RecordBatch> {
615 let mut fields: Vec<Arc<Field>> = decoded.schema().fields().iter().cloned().collect();
616 let mut arrays: Vec<Arc<dyn Array>> = decoded.columns().to_vec();
617
618 for (i, col) in input.columns().iter().enumerate() {
619 fields.push(input.schema().field(i).clone().into());
620 arrays.push(col.clone());
621 }
622
623 let schema = Schema::new(fields);
624 RecordBatch::try_new(Arc::new(schema), arrays).context("construct hstacked arrow batch")
625}
626
627pub fn instruction_signature_to_arrow_schema(signature: &InstructionSignature) -> Result<Schema> {
630 let mut fields = Vec::new();
631
632 for param in &signature.params {
633 let field = Field::new(
634 param.name.clone(),
635 to_arrow_dtype(¶m.param_type)
636 .context("unable to convert instruction param type to arrow dtype")?,
637 true,
638 );
639 fields.push(field);
640 }
641
642 for account in &signature.accounts_names {
643 let field = Field::new(account.clone(), DataType::Binary, true);
644 fields.push(field);
645 }
646
647 Ok(Schema::new(fields))
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653 use crate::deserialize::{DynType, ParamInput};
654 use std::fs::File;
655
656 #[test]
657 #[ignore]
658 fn test_instructions_with_real_data() {
659 use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
660
661 let builder =
662 ParquetRecordBatchReaderBuilder::try_new(File::open("jup.parquet").unwrap()).unwrap();
663 let mut reader = builder.build().unwrap();
664 let instructions = reader.next().unwrap().unwrap();
665 let ix_signature = InstructionSignature {
666 discriminator: vec![229, 23, 203, 151, 122, 227, 173, 42],
708 params: vec![
709 ParamInput {
710 name: "RoutePlan".to_string(),
711 param_type: DynType::Array(Box::new(DynType::Struct(vec![
712 (
713 "Swap".to_string(),
714 DynType::Enum(vec![
715 ("Saber".to_string(), None),
716 ("SaberAddDecimalsDeposit".to_string(), None),
717 ("SaberAddDecimalsWithdraw".to_string(), None),
718 ("TokenSwap".to_string(), None),
719 ("Sencha".to_string(), None),
720 ("Step".to_string(), None),
721 ("Cropper".to_string(), None),
722 ("Raydium".to_string(), None),
723 (
724 "Crema".to_string(),
725 Some(DynType::Struct(vec![(
726 "a_to_b".to_string(),
727 DynType::Bool,
728 )])),
729 ),
730 ("Lifinity".to_string(), None),
731 ("Mercurial".to_string(), None),
732 ("Cykura".to_string(), None),
733 (
734 "Serum".to_string(),
735 Some(DynType::Struct(vec![(
736 "side".to_string(),
737 DynType::Enum(vec![
738 ("Bid".to_string(), None),
739 ("Ask".to_string(), None),
740 ]),
741 )])),
742 ),
743 ("MarinadeDeposit".to_string(), None),
744 ("MarinadeUnstake".to_string(), None),
745 (
746 "Aldrin".to_string(),
747 Some(DynType::Struct(vec![(
748 "side".to_string(),
749 DynType::Enum(vec![
750 ("Bid".to_string(), None),
751 ("Ask".to_string(), None),
752 ]),
753 )])),
754 ),
755 (
756 "AldrinV2".to_string(),
757 Some(DynType::Struct(vec![(
758 "side".to_string(),
759 DynType::Enum(vec![
760 ("Bid".to_string(), None),
761 ("Ask".to_string(), None),
762 ]),
763 )])),
764 ),
765 (
766 "Whirlpool".to_string(),
767 Some(DynType::Struct(vec![(
768 "a_to_b".to_string(),
769 DynType::Bool,
770 )])),
771 ),
772 (
773 "Invariant".to_string(),
774 Some(DynType::Struct(vec![(
775 "x_to_y".to_string(),
776 DynType::Bool,
777 )])),
778 ),
779 ("Meteora".to_string(), None),
780 ("GooseFX".to_string(), None),
781 (
782 "DeltaFi".to_string(),
783 Some(DynType::Struct(vec![(
784 "stable".to_string(),
785 DynType::Bool,
786 )])),
787 ),
788 ("Balansol".to_string(), None),
789 (
790 "MarcoPolo".to_string(),
791 Some(DynType::Struct(vec![(
792 "x_to_y".to_string(),
793 DynType::Bool,
794 )])),
795 ),
796 (
797 "Dradex".to_string(),
798 Some(DynType::Struct(vec![(
799 "side".to_string(),
800 DynType::Enum(vec![
801 ("Bid".to_string(), None),
802 ("Ask".to_string(), None),
803 ]),
804 )])),
805 ),
806 ("LifinityV2".to_string(), None),
807 ("RaydiumClmm".to_string(), None),
808 (
809 "Openbook".to_string(),
810 Some(DynType::Struct(vec![(
811 "side".to_string(),
812 DynType::Enum(vec![
813 ("Bid".to_string(), None),
814 ("Ask".to_string(), None),
815 ]),
816 )])),
817 ),
818 (
819 "Phoenix".to_string(),
820 Some(DynType::Struct(vec![(
821 "side".to_string(),
822 DynType::Enum(vec![
823 ("Bid".to_string(), None),
824 ("Ask".to_string(), None),
825 ]),
826 )])),
827 ),
828 (
829 "Symmetry".to_string(),
830 Some(DynType::Struct(vec![
831 ("from_token_id".to_string(), DynType::U64),
832 ("to_token_id".to_string(), DynType::U64),
833 ])),
834 ),
835 ("TokenSwapV2".to_string(), None),
836 ("HeliumTreasuryManagementRedeemV0".to_string(), None),
837 ("StakeDexStakeWrappedSol".to_string(), None),
838 (
839 "StakeDexSwapViaStake".to_string(),
840 Some(DynType::Struct(vec![(
841 "bridge_stake_seed".to_string(),
842 DynType::U32,
843 )])),
844 ),
845 ("GooseFXV2".to_string(), None),
846 ("Perps".to_string(), None),
847 ("PerpsAddLiquidity".to_string(), None),
848 ("PerpsRemoveLiquidity".to_string(), None),
849 ("MeteoraDlmm".to_string(), None),
850 (
851 "OpenBookV2".to_string(),
852 Some(DynType::Struct(vec![(
853 "side".to_string(),
854 DynType::Enum(vec![
855 ("Bid".to_string(), None),
856 ("Ask".to_string(), None),
857 ]),
858 )])),
859 ),
860 ("RaydiumClmmV2".to_string(), None),
861 (
862 "StakeDexPrefundWithdrawStakeAndDepositStake".to_string(),
863 Some(DynType::Struct(vec![(
864 "bridge_stake_seed".to_string(),
865 DynType::U32,
866 )])),
867 ),
868 (
869 "Clone".to_string(),
870 Some(DynType::Struct(vec![
871 ("pool_index".to_string(), DynType::U8),
872 ("quantity_is_input".to_string(), DynType::Bool),
873 ("quantity_is_collateral".to_string(), DynType::Bool),
874 ])),
875 ),
876 (
877 "SanctumS".to_string(),
878 Some(DynType::Struct(vec![
879 ("src_lst_value_calc_accs".to_string(), DynType::U8),
880 ("dst_lst_value_calc_accs".to_string(), DynType::U8),
881 ("src_lst_index".to_string(), DynType::U32),
882 ("dst_lst_index".to_string(), DynType::U32),
883 ])),
884 ),
885 (
886 "SanctumSAddLiquidity".to_string(),
887 Some(DynType::Struct(vec![
888 ("lst_value_calc_accs".to_string(), DynType::U8),
889 ("lst_index".to_string(), DynType::U32),
890 ])),
891 ),
892 (
893 "SanctumSRemoveLiquidity".to_string(),
894 Some(DynType::Struct(vec![
895 ("lst_value_calc_accs".to_string(), DynType::U8),
896 ("lst_index".to_string(), DynType::U32),
897 ])),
898 ),
899 ("RaydiumCP".to_string(), None),
900 (
901 "WhirlpoolSwapV2".to_string(),
902 Some(DynType::Struct(vec![
903 ("a_to_b".to_string(), DynType::Bool),
904 (
905 "remaining_accounts_info".to_string(),
906 DynType::Struct(vec![(
907 "slices".to_string(),
908 DynType::Array(Box::new(DynType::Struct(vec![(
909 "remaining_accounts_slice".to_string(),
910 DynType::Struct(vec![
911 ("accounts_type".to_string(), DynType::U8),
912 ("length".to_string(), DynType::U8),
913 ]),
914 )]))),
915 )]),
916 ),
917 ])),
918 ),
919 ("OneIntro".to_string(), None),
920 ("PumpdotfunWrappedBuy".to_string(), None),
921 ("PumpdotfunWrappedSell".to_string(), None),
922 ("PerpsV2".to_string(), None),
923 ("PerpsV2AddLiquidity".to_string(), None),
924 ("PerpsV2RemoveLiquidity".to_string(), None),
925 ("MoonshotWrappedBuy".to_string(), None),
926 ("MoonshotWrappedSell".to_string(), None),
927 ("StabbleStableSwap".to_string(), None),
928 ("StabbleWeightedSwap".to_string(), None),
929 (
930 "Obric".to_string(),
931 Some(DynType::Struct(vec![(
932 "x_to_y".to_string(),
933 DynType::Bool,
934 )])),
935 ),
936 ("FoxBuyFromEstimatedCost".to_string(), None),
937 (
938 "FoxClaimPartial".to_string(),
939 Some(DynType::Struct(vec![(
940 "is_y".to_string(),
941 DynType::Bool,
942 )])),
943 ),
944 (
945 "SolFi".to_string(),
946 Some(DynType::Struct(vec![(
947 "is_quote_to_base".to_string(),
948 DynType::Bool,
949 )])),
950 ),
951 ("SolayerDelegateNoInit".to_string(), None),
952 ("SolayerUndelegateNoInit".to_string(), None),
953 (
954 "TokenMill".to_string(),
955 Some(DynType::Struct(vec![(
956 "side".to_string(),
957 DynType::Enum(vec![
958 ("Bid".to_string(), None),
959 ("Ask".to_string(), None),
960 ]),
961 )])),
962 ),
963 ("DaosFunBuy".to_string(), None),
964 ("DaosFunSell".to_string(), None),
965 ("ZeroFi".to_string(), None),
966 ("StakeDexWithdrawWrappedSol".to_string(), None),
967 ("VirtualsBuy".to_string(), None),
968 ("VirtualsSell".to_string(), None),
969 (
970 "Peren".to_string(),
971 Some(DynType::Struct(vec![
972 ("in_index".to_string(), DynType::U8),
973 ("out_index".to_string(), DynType::U8),
974 ])),
975 ),
976 ("PumpdotfunAmmBuy".to_string(), None),
977 ("PumpdotfunAmmSell".to_string(), None),
978 ("Gamma".to_string(), None),
979 ]),
980 ),
981 ("Percent".to_string(), DynType::U8),
982 ("InputIndex".to_string(), DynType::U8),
983 ("OutputIndex".to_string(), DynType::U8),
984 ]))),
985 },
986 ParamInput {
987 name: "InAmount".to_string(),
988 param_type: DynType::U64,
989 },
990 ParamInput {
991 name: "QuotedOutAmount".to_string(),
992 param_type: DynType::U64,
993 },
994 ParamInput {
995 name: "SlippageBps".to_string(),
996 param_type: DynType::U16,
997 },
998 ParamInput {
999 name: "PlatformFeeBps".to_string(),
1000 param_type: DynType::U8,
1001 },
1002 ],
1003 accounts_names: vec![
1004 "TokenProgram".to_string(),
1005 "UserTransferAuthority".to_string(),
1006 "UserSourceTokenAccount".to_string(),
1007 "UserDestinationTokenAccount".to_string(),
1008 "DestinationTokenAccount".to_string(),
1009 "PlatformFeeAccount".to_string(),
1010 "EventAuthority".to_string(),
1011 "Program".to_string(),
1012 "test8".to_string(),
1013 "test9".to_string(),
1014 ],
1015 };
1016
1017 let result = decode_instructions_batch(&ix_signature, &instructions, true, false, false)
1018 .context("decode failed")
1019 .unwrap();
1020
1021 let mut file = File::create("decoded_instructions.parquet").unwrap();
1023 let mut writer =
1024 parquet::arrow::ArrowWriter::try_new(&mut file, result.schema(), None).unwrap();
1025 writer.write(&result).unwrap();
1026 writer.close().unwrap();
1027 }
1028
1029 #[test]
1030 #[ignore]
1031 fn test_decode_logs_with_real_data() {
1032 use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
1033
1034 let builder =
1035 ParquetRecordBatchReaderBuilder::try_new(File::open("logs.parquet").unwrap()).unwrap();
1036 let mut reader = builder.build().unwrap();
1037 let logs = reader.next().unwrap().unwrap();
1038
1039 let signature = LogSignature {
1040 params: vec![
1041 ParamInput {
1042 name: "whirlpool".to_string(),
1043 param_type: DynType::FixedArray(Box::new(DynType::U8), 32),
1044 },
1045 ParamInput {
1046 name: "a_to_b".to_string(),
1047 param_type: DynType::Bool,
1048 },
1049 ParamInput {
1050 name: "pre_sqrt_price".to_string(),
1051 param_type: DynType::U128,
1052 },
1053 ParamInput {
1054 name: "post_sqrt_price".to_string(),
1055 param_type: DynType::U128,
1056 },
1057 ParamInput {
1058 name: "x".to_string(),
1059 param_type: DynType::U64,
1060 },
1061 ParamInput {
1062 name: "input_amount".to_string(),
1063 param_type: DynType::U64,
1064 },
1065 ParamInput {
1066 name: "output_amount".to_string(),
1067 param_type: DynType::U64,
1068 },
1069 ParamInput {
1070 name: "input_transfer_fee".to_string(),
1071 param_type: DynType::U64,
1072 },
1073 ParamInput {
1074 name: "output_transfer_fee".to_string(),
1075 param_type: DynType::U64,
1076 },
1077 ParamInput {
1078 name: "lp_fee".to_string(),
1079 param_type: DynType::U64,
1080 },
1081 ParamInput {
1082 name: "protocol_fee".to_string(),
1083 param_type: DynType::U64,
1084 },
1085 ],
1086 };
1087
1088 let result = decode_logs_batch(&signature, &logs, true, false)
1089 .context("decode failed")
1090 .unwrap();
1091
1092 let mut file = File::create("decoded_logs.parquet").unwrap();
1094 let mut writer =
1095 parquet::arrow::ArrowWriter::try_new(&mut file, result.schema(), None).unwrap();
1096 writer.write(&result).unwrap();
1097 writer.close().unwrap();
1098 }
1099
1100 #[test]
1101 #[ignore]
1102 fn test_instruction_signature_to_arrow_schema() {
1103 let signature = InstructionSignature {
1105 discriminator: vec![],
1106 params: vec![
1107 ParamInput {
1108 name: "amount".to_string(),
1109 param_type: DynType::U64,
1110 },
1111 ParamInput {
1112 name: "is_valid".to_string(),
1113 param_type: DynType::Bool,
1114 },
1115 ParamInput {
1116 name: "amm".to_string(),
1117 param_type: DynType::FixedArray(Box::new(DynType::U8), 32),
1118 },
1119 ],
1120 accounts_names: vec!["source".to_string(), "destination".to_string()],
1121 };
1122
1123 let schema = instruction_signature_to_arrow_schema(&signature).unwrap();
1125
1126 assert_eq!(schema.fields().len(), 5); let amount_field = schema.field_with_name("amount").unwrap();
1131 assert_eq!(amount_field.name(), "amount");
1132 assert!(amount_field.is_nullable());
1133
1134 let is_valid_field = schema.field_with_name("is_valid").unwrap();
1135 assert_eq!(is_valid_field.name(), "is_valid");
1136 assert!(is_valid_field.is_nullable());
1137
1138 let amm_field = schema.field_with_name("amm").unwrap();
1139 assert_eq!(amm_field.name(), "amm");
1140 assert!(amm_field.is_nullable());
1141
1142 let source_field = schema.field_with_name("source").unwrap();
1144 assert_eq!(source_field.name(), "source");
1145 assert_eq!(source_field.data_type(), &DataType::Binary);
1146 assert!(source_field.is_nullable());
1147
1148 let dest_field = schema.field_with_name("destination").unwrap();
1149 assert_eq!(dest_field.name(), "destination");
1150 assert_eq!(dest_field.data_type(), &DataType::Binary);
1151 assert!(dest_field.is_nullable());
1152 }
1153}