Skip to main content

tiders_svm_decode/
lib.rs

1//! # tiders-svm-decode
2//!
3//! Decodes Solana (SVM) program data from binary format into Apache Arrow RecordBatches.
4//!
5//! Supports two types of decoding:
6//! - **Instructions** — Decodes Borsh-serialized instruction data using 8-byte discriminators
7//!   and a typed parameter schema ([`InstructionSignature`]).
8//! - **Program logs** — Decodes base64-encoded log event data using a typed parameter
9//!   schema ([`LogSignature`]).
10//!
11//! The type system ([`DynType`] / [`DynValue`]) supports all Borsh primitives (i8–i128,
12//! u8–u128, bool) plus complex types (arrays, structs, enums, options).
13
14use 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/// Defines the schema for decoding a Solana instruction: discriminator bytes, parameter types, and account names.
32#[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/// Defines the schema for decoding a Solana program log event: parameter types to decode from base64 data.
40#[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
149/// Decodes instruction data from an Arrow `RecordBatch` into a new `RecordBatch` of decoded parameters and accounts.
150///
151/// Expects the input batch to contain `data` (binary) and `accounts` (list-of-binary) columns.
152/// Optionally filters rows by discriminator match and horizontally stacks the result with the original batch.
153pub 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        // Don't error on remaining data because this is the behavior implemented by anchor.
323        // Note that borsh does error if there is remaining data after deserialization but anchor
324        // doesn't.
325        //
326        // Might be a good idea to extract this to a parameter to this function as well
327        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(&param.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
411/// Decodes log event data from an Arrow `RecordBatch` into a new `RecordBatch` of decoded parameters.
412///
413/// Expects the input batch to contain a `message` column (Utf8 or LargeUtf8) with base64-encoded event data.
414/// Optionally horizontally stacks the result with the original batch.
415pub 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
454/// Decodes base64-encoded log messages into a `RecordBatch` of typed parameter columns.
455///
456/// Each row's string value is base64-decoded and then Borsh-deserialized according to the
457/// parameter types in `signature`. Rows that fail to decode are filled with nulls when
458/// `allow_decode_fail` is `true`.
459pub 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(&param.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
627/// Converts an [`InstructionSignature`] into an Arrow [`Schema`], mapping each parameter
628/// and account name to the corresponding Arrow field and data type.
629pub 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(&param.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            // // SPL Token Transfer
667            // discriminator: &[3],
668            // params: vec![ParamInput {
669            //     name: "Amount".to_string(),
670            //     param_type: DynType::U64,
671            // }],
672            // accounts: vec![
673            //     "Source".to_string(),
674            //     "Destination".to_string(),
675            //     "Authority".to_string(),
676            // ],
677
678            // // JUP SwapEvent
679            // discriminator: &[
680            //     228, 69, 165, 46, 81, 203, 154, 29, 64, 198, 205, 232, 38, 8, 113, 226,
681            // ],
682            // params: vec![
683            //     ParamInput {
684            //         name: "Amm".to_string(),
685            //         param_type: DynType::Pubkey,
686            //     },
687            //     ParamInput {
688            //         name: "InputMint".to_string(),
689            //         param_type: DynType::Pubkey,
690            //     },
691            //     ParamInput {
692            //         name: "InputAmount".to_string(),
693            //         param_type: DynType::U64,
694            //     },
695            //     ParamInput {
696            //         name: "OutputMint".to_string(),
697            //         param_type: DynType::Pubkey,
698            //     },
699            //     ParamInput {
700            //         name: "OutputAmount".to_string(),
701            //         param_type: DynType::U64,
702            //     },
703            // ],
704            // accounts: vec![],
705
706            // JUP Route
707            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        // Save the filtered instructions to a new parquet file
1022        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        // Save the filtered instructions to a new parquet file
1093        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        // Create a test instruction signature
1104        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        // Convert to schema
1124        let schema = instruction_signature_to_arrow_schema(&signature).unwrap();
1125
1126        // Verify the schema has the correct number of fields
1127        assert_eq!(schema.fields().len(), 5); // 2 params + 2 accounts
1128
1129        // Verify param fields
1130        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        // Verify account fields
1143        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}