arete-interpreter 0.14.0

AST transformation runtime and VM for Arete streaming pipelines
Documentation
use crate::ast::{
    IdlArrayElementSnapshot, IdlArrayTypeSnapshot, IdlDefinedInnerSnapshot, IdlDefinedTypeSnapshot,
    IdlHashMapTypeSnapshot, IdlOptionTypeSnapshot, IdlSnapshot, IdlTupleTypeSnapshot,
    IdlTypeSnapshot, IdlVecTypeSnapshot, InstructionDef, PdaDefinition, SerializableStackSpec,
    CURRENT_AST_VERSION,
};
use arete_idl as idl_parser;
use std::collections::BTreeMap;

pub fn build_program_only_stack_spec_from_idl(
    idl: &idl_parser::IdlSpec,
    stack_name: &str,
) -> SerializableStackSpec {
    let program_spec = build_program_spec_v1_from_idl(idl)
        .expect("IDL program identity must be valid for ProgramSpecV1");
    build_program_only_stack_spec_from_program_spec(program_spec, stack_name)
}

pub fn build_program_spec_v1_from_idl(
    idl: &idl_parser::IdlSpec,
) -> Result<arete_hash::ProgramSpecV1, arete_hash::HashError> {
    arete_hash::build_program_spec_v1_from_idl(idl, None)
}

pub fn build_program_spec_v1_from_idl_bytes(
    bytes: &[u8],
    explicit_program_id: Option<&str>,
) -> Result<arete_hash::ProgramSpecV1, arete_hash::HashError> {
    arete_hash::build_program_spec_v1_from_bytes(bytes, explicit_program_id)
}

pub fn build_oss_program_identity_v1_from_idl(
    idl: &idl_parser::IdlSpec,
) -> Result<arete_hash::OssProgramIdentityV1, arete_hash::HashError> {
    arete_hash::OssProgramIdentityV1::new(build_program_spec_v1_from_idl(idl)?)
}

pub fn build_oss_program_identity_v1_from_idl_bytes(
    bytes: &[u8],
    explicit_program_id: Option<&str>,
) -> Result<arete_hash::OssProgramIdentityV1, arete_hash::HashError> {
    arete_hash::build_oss_program_identity_v1_from_bytes(bytes, explicit_program_id)
}

pub fn build_program_only_stack_spec_from_idl_bytes(
    bytes: &[u8],
    explicit_program_id: Option<&str>,
    stack_name: &str,
) -> Result<SerializableStackSpec, arete_hash::HashError> {
    let program_spec = build_program_spec_v1_from_idl_bytes(bytes, explicit_program_id)?;
    Ok(build_program_only_stack_spec_from_program_spec(
        program_spec,
        stack_name,
    ))
}

pub fn build_program_only_stack_spec_from_program_spec(
    program_spec: arete_hash::ProgramSpecV1,
    stack_name: &str,
) -> SerializableStackSpec {
    build_program_only_stack_spec_from_program_spec_ref(&program_spec, stack_name)
}

pub fn build_program_only_stack_spec_from_identity(
    identity: &arete_hash::OssProgramIdentityV1,
    stack_name: &str,
) -> SerializableStackSpec {
    build_program_only_stack_spec_from_program_spec_ref(&identity.program_spec, stack_name)
}

fn build_program_only_stack_spec_from_program_spec_ref(
    program_spec: &arete_hash::ProgramSpecV1,
    stack_name: &str,
) -> SerializableStackSpec {
    let snapshot = program_spec.idl_snapshot.clone().into_legacy_snapshot();
    let program_id = Some(program_spec.program_id.clone());
    let pdas: BTreeMap<String, PdaDefinition> =
        transcode_program_projection(program_spec.pdas.clone());
    let instructions: Vec<InstructionDef> =
        transcode_program_projection(program_spec.instructions.clone());

    let mut grouped_pdas = BTreeMap::new();
    if !pdas.is_empty() {
        grouped_pdas.insert(snapshot.name.clone(), pdas);
    }

    SerializableStackSpec {
        ast_version: CURRENT_AST_VERSION.to_string(),
        stack_name: stack_name.to_string(),
        program_ids: program_id.into_iter().collect(),
        idls: vec![snapshot],
        program_specs: vec![program_spec.clone()],
        entities: vec![],
        pdas: grouped_pdas,
        instructions,
        content_hash: None,
    }
    .with_content_hash()
}

fn transcode_program_projection<T, U>(value: T) -> U
where
    T: serde::Serialize,
    U: serde::de::DeserializeOwned,
{
    serde_json::from_value(
        serde_json::to_value(value).expect("shared ProgramSpec projection must serialize"),
    )
    .expect("shared ProgramSpec projection must match the legacy AST adapter")
}

pub fn convert_idl_to_snapshot(idl: &idl_parser::IdlSpec) -> IdlSnapshot {
    arete_idl::normalize_idl_snapshot(idl)
}

pub fn convert_idl_type(idl_type: &idl_parser::IdlType) -> IdlTypeSnapshot {
    match idl_type {
        idl_parser::IdlType::Simple(simple) => IdlTypeSnapshot::Simple(simple.clone()),
        idl_parser::IdlType::Array(array) => IdlTypeSnapshot::Array(IdlArrayTypeSnapshot {
            array: array
                .array
                .iter()
                .map(|element| match element {
                    idl_parser::IdlTypeArrayElement::Nested(ty) => {
                        IdlArrayElementSnapshot::Type(convert_idl_type(ty))
                    }
                    idl_parser::IdlTypeArrayElement::Type(type_name) => {
                        IdlArrayElementSnapshot::TypeName(type_name.clone())
                    }
                    idl_parser::IdlTypeArrayElement::Size(size) => {
                        IdlArrayElementSnapshot::Size(*size)
                    }
                })
                .collect(),
        }),
        idl_parser::IdlType::Option(option) => IdlTypeSnapshot::Option(IdlOptionTypeSnapshot {
            option: Box::new(convert_idl_type(&option.option)),
        }),
        idl_parser::IdlType::Vec(vec_type) => IdlTypeSnapshot::Vec(IdlVecTypeSnapshot {
            vec: Box::new(convert_idl_type(&vec_type.vec)),
            length_prefix: vec_type.length_prefix,
        }),
        idl_parser::IdlType::Defined(defined) => IdlTypeSnapshot::Defined(IdlDefinedTypeSnapshot {
            defined: match &defined.defined {
                idl_parser::IdlTypeDefinedInner::Named { name } => {
                    IdlDefinedInnerSnapshot::Named { name: name.clone() }
                }
                idl_parser::IdlTypeDefinedInner::Simple(simple) => {
                    IdlDefinedInnerSnapshot::Simple(simple.clone())
                }
            },
        }),
        idl_parser::IdlType::HashMap(hash_map) => {
            IdlTypeSnapshot::HashMap(IdlHashMapTypeSnapshot {
                hash_map: (
                    Box::new(convert_idl_type(&hash_map.hash_map.0)),
                    Box::new(convert_idl_type(&hash_map.hash_map.1)),
                ),
            })
        }
        idl_parser::IdlType::Tuple(tuple) => IdlTypeSnapshot::Tuple(IdlTupleTypeSnapshot {
            tuple: tuple.tuple.iter().map(convert_idl_type).collect(),
        }),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ast::{AmountDecimalsSource, InstructionAmountHint, PdaSeedDef};

    #[test]
    fn builds_program_only_stack_spec_from_raw_idl() {
        let idl = arete_idl::parse::parse_idl_content(
            r#"{
              "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
              "version": "0.0.0",
              "name": "token",
              "instructions": [
                {
                  "name": "InitializeMint2",
                  "accounts": [
                    { "name": "mint", "isMut": true, "isSigner": false }
                  ],
                  "args": [
                    { "name": "decimals", "type": "u8" },
                    { "name": "mintAuthority", "type": "publicKey" }
                  ],
                  "discriminant": { "type": "u8", "value": 20 }
                }
              ],
              "accounts": [],
              "types": [],
              "events": [],
              "errors": []
            }"#,
        )
        .expect("IDL should parse");

        let spec = build_program_only_stack_spec_from_idl(&idl, "SplToken");
        assert_eq!(spec.stack_name, "SplToken");
        assert!(spec.entities.is_empty());
        assert_eq!(spec.idls.len(), 1);
        assert_eq!(spec.instructions.len(), 1);
        assert_eq!(spec.instructions[0].name, "InitializeMint2");
        assert_eq!(spec.instructions[0].discriminator, vec![20]);
        assert_eq!(spec.instructions[0].discriminator_size, 1);
        assert_eq!(
            spec.instructions[0].program_id.as_deref(),
            Some("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
        );
        assert_eq!(
            spec.instructions[0].args[1].arg_type,
            "solana_pubkey::Pubkey"
        );
        assert!(spec.content_hash.is_some());
    }

    #[test]
    #[test]
    fn preserves_nested_seed_paths_when_building_program_only_specs() {
        let idl = arete_idl::parse::parse_idl_content(
            r#"{
              "address": "Prog111111111111111111111111111111111111111",
              "version": "0.0.0",
              "name": "demo",
              "instructions": [
                {
                  "name": "proposalCreate",
                  "accounts": [
                    {
                      "name": "proposal",
                      "isMut": true,
                      "isSigner": false,
                      "pda": {
                        "name": "proposal",
                        "seeds": [
                          {
                            "kind": "arg",
                            "path": "args.transactionIndex",
                            "type": "u64"
                          }
                        ]
                      }
                    }
                  ],
                  "args": [
                    {
                      "name": "args",
                      "type": {
                        "defined": {
                          "name": "ProposalArgs"
                        }
                      }
                    }
                  ],
                  "discriminant": { "type": "u8", "value": 3 }
                }
              ],
              "accounts": [],
              "types": [
                {
                  "name": "ProposalArgs",
                  "type": {
                    "kind": "struct",
                    "fields": [
                      { "name": "transactionIndex", "type": "u64" }
                    ]
                  }
                }
              ],
              "events": [],
              "errors": []
            }"#,
        )
        .expect("IDL should parse");

        let spec = build_program_only_stack_spec_from_idl(&idl, "Demo");
        let pda = spec
            .pdas
            .get("demo")
            .and_then(|program| program.get("proposal"))
            .expect("proposal PDA should be present");
        assert_eq!(
            pda.seeds,
            vec![PdaSeedDef::ArgRef {
                arg_name: "args.transactionIndex".to_string(),
                arg_type: Some("u64".to_string()),
            }]
        );
    }

    #[test]
    fn preserves_amount_hints_from_idl_args() {
        let idl = arete_idl::parse::parse_idl_content(
            r#"{
              "address": "Prog111111111111111111111111111111111111111",
              "version": "0.0.0",
              "name": "demo",
              "instructions": [
                {
                  "name": "deposit",
                  "accounts": [],
                  "args": [
                    {
                      "name": "amount",
                      "type": "u64",
                      "amountHint": {
                        "decimalsSource": {
                          "kind": "argMint",
                          "argName": "mint"
                        }
                      }
                    },
                    {
                      "name": "mint",
                      "type": "publicKey"
                    }
                  ],
                  "discriminant": { "type": "u8", "value": 7 }
                }
              ],
              "accounts": [],
              "types": [],
              "events": [],
              "errors": []
            }"#,
        )
        .expect("IDL should parse");

        let spec = build_program_only_stack_spec_from_idl(&idl, "Demo");
        assert_eq!(
            spec.instructions[0].args[0].amount_hint,
            Some(InstructionAmountHint {
                decimals_source: AmountDecimalsSource::ArgMint {
                    arg_name: "mint".to_string(),
                },
            })
        );
        assert!(spec.idls[0].instructions[0].args[0].amount_hint.is_some());
    }

    #[test]
    fn derives_the_checked_in_program_and_release_identities() {
        let corpus: serde_json::Value =
            serde_json::from_str(include_str!("../../test-vectors/hash-v1.json"))
                .expect("vector corpus");
        let vector = corpus["idlVectors"]
            .as_array()
            .unwrap()
            .iter()
            .find(|vector| vector["id"] == "idl-primary")
            .expect("primary IDL vector");
        let source = vector["input"]["data"].as_str().unwrap().as_bytes();

        let identity = build_oss_program_identity_v1_from_idl_bytes(source, None)
            .expect("interpreter identity");
        let stack_spec = build_program_only_stack_spec_from_identity(&identity, "Demo");

        assert_eq!(
            identity.program_spec_hash.to_string(),
            vector["expected"]["programSpecIdentity"]["hashId"]
        );
        assert_eq!(
            identity.release_hash.to_string(),
            vector["expected"]["ossReleaseIdentity"]["hashId"]
        );
        assert_eq!(stack_spec.content_hash.as_deref().unwrap().len(), 64);
        assert_eq!(stack_spec.program_specs.len(), 1);
        assert_eq!(
            stack_spec.program_specs[0].hash().unwrap(),
            identity.program_spec_hash
        );
        assert!(stack_spec
            .content_hash
            .as_deref()
            .unwrap()
            .bytes()
            .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()));
        assert_eq!(
            stack_spec.content_hash.as_deref(),
            Some(stack_spec.compute_content_hash().as_str())
        );
    }
}