Skip to main content

arete_interpreter/
program_sdk.rs

1use crate::ast::{
2    idl_type_snapshot_to_rust_string, AccountResolution, AmountDecimalsSource,
3    IdlArrayElementSnapshot, IdlArrayTypeSnapshot, IdlDefinedInnerSnapshot, IdlDefinedTypeSnapshot,
4    IdlHashMapTypeSnapshot, IdlOptionTypeSnapshot, IdlSnapshot, IdlTypeSnapshot,
5    IdlVecTypeSnapshot, InstructionAccountDef, InstructionAmountHint, InstructionArgDef,
6    InstructionDef, PdaDefinition, PdaProgramDef, PdaSeedDef, SerializableStackSpec,
7    CURRENT_AST_VERSION,
8};
9use arete_idl as idl_parser;
10use std::collections::BTreeMap;
11
12fn sanitize_identifier_segment(name: &str) -> String {
13    let mut sanitized = String::new();
14
15    for ch in name.chars() {
16        if ch.is_ascii_alphanumeric() || ch == '_' {
17            sanitized.push(ch);
18        } else if !sanitized.ends_with('_') {
19            sanitized.push('_');
20        }
21    }
22
23    let sanitized = sanitized.trim_matches('_').to_string();
24    if sanitized.is_empty() {
25        return "value".to_string();
26    }
27    if sanitized
28        .chars()
29        .next()
30        .is_some_and(|ch| ch.is_ascii_digit())
31    {
32        return format!("_{}", sanitized);
33    }
34    sanitized
35}
36
37fn sanitize_identifier(name: &str) -> String {
38    sanitize_identifier_segment(name)
39}
40
41fn sanitize_seed_path(path: &str) -> String {
42    path.split('.')
43        .map(sanitize_identifier_segment)
44        .collect::<Vec<_>>()
45        .join(".")
46}
47
48pub fn build_program_only_stack_spec_from_idl(
49    idl: &idl_parser::IdlSpec,
50    stack_name: &str,
51) -> SerializableStackSpec {
52    let program_spec = build_program_spec_v1_from_idl(idl)
53        .expect("IDL program identity must be valid for ProgramSpecV1");
54    build_program_only_stack_spec_from_program_spec(program_spec, stack_name)
55}
56
57pub fn build_program_spec_v1_from_idl(
58    idl: &idl_parser::IdlSpec,
59) -> Result<arete_hash::ProgramSpecV1, arete_hash::HashError> {
60    arete_hash::build_program_spec_v1_from_idl(idl, None)
61}
62
63pub fn build_program_spec_v1_from_idl_bytes(
64    bytes: &[u8],
65    explicit_program_id: Option<&str>,
66) -> Result<arete_hash::ProgramSpecV1, arete_hash::HashError> {
67    arete_hash::build_program_spec_v1_from_bytes(bytes, explicit_program_id)
68}
69
70pub fn build_oss_program_identity_v1_from_idl(
71    idl: &idl_parser::IdlSpec,
72) -> Result<arete_hash::OssProgramIdentityV1, arete_hash::HashError> {
73    arete_hash::OssProgramIdentityV1::new(build_program_spec_v1_from_idl(idl)?)
74}
75
76pub fn build_oss_program_identity_v1_from_idl_bytes(
77    bytes: &[u8],
78    explicit_program_id: Option<&str>,
79) -> Result<arete_hash::OssProgramIdentityV1, arete_hash::HashError> {
80    arete_hash::build_oss_program_identity_v1_from_bytes(bytes, explicit_program_id)
81}
82
83pub fn build_program_only_stack_spec_from_idl_bytes(
84    bytes: &[u8],
85    explicit_program_id: Option<&str>,
86    stack_name: &str,
87) -> Result<SerializableStackSpec, arete_hash::HashError> {
88    let program_spec = build_program_spec_v1_from_idl_bytes(bytes, explicit_program_id)?;
89    Ok(build_program_only_stack_spec_from_program_spec(
90        program_spec,
91        stack_name,
92    ))
93}
94
95pub fn build_program_only_stack_spec_from_program_spec(
96    program_spec: arete_hash::ProgramSpecV1,
97    stack_name: &str,
98) -> SerializableStackSpec {
99    build_program_only_stack_spec_from_program_spec_ref(&program_spec, stack_name)
100}
101
102pub fn build_program_only_stack_spec_from_identity(
103    identity: &arete_hash::OssProgramIdentityV1,
104    stack_name: &str,
105) -> SerializableStackSpec {
106    build_program_only_stack_spec_from_program_spec_ref(&identity.program_spec, stack_name)
107}
108
109fn build_program_only_stack_spec_from_program_spec_ref(
110    program_spec: &arete_hash::ProgramSpecV1,
111    stack_name: &str,
112) -> SerializableStackSpec {
113    let snapshot = program_spec.idl_snapshot.clone().into_legacy_snapshot();
114    let program_id = Some(program_spec.program_id.clone());
115    let pdas: BTreeMap<String, PdaDefinition> =
116        transcode_program_projection(program_spec.pdas.clone());
117    let instructions: Vec<InstructionDef> =
118        transcode_program_projection(program_spec.instructions.clone());
119
120    let mut grouped_pdas = BTreeMap::new();
121    if !pdas.is_empty() {
122        grouped_pdas.insert(snapshot.name.clone(), pdas);
123    }
124
125    SerializableStackSpec {
126        ast_version: CURRENT_AST_VERSION.to_string(),
127        stack_name: stack_name.to_string(),
128        program_ids: program_id.into_iter().collect(),
129        idls: vec![snapshot],
130        program_specs: vec![program_spec.clone()],
131        entities: vec![],
132        pdas: grouped_pdas,
133        instructions,
134        content_hash: None,
135    }
136    .with_content_hash()
137}
138
139fn transcode_program_projection<T, U>(value: T) -> U
140where
141    T: serde::Serialize,
142    U: serde::de::DeserializeOwned,
143{
144    serde_json::from_value(
145        serde_json::to_value(value).expect("shared ProgramSpec projection must serialize"),
146    )
147    .expect("shared ProgramSpec projection must match the legacy AST adapter")
148}
149
150pub fn convert_idl_to_snapshot(idl: &idl_parser::IdlSpec) -> IdlSnapshot {
151    arete_idl::normalize_idl_snapshot(idl)
152}
153
154pub fn extract_pdas_from_idl(idl: &idl_parser::IdlSpec) -> BTreeMap<String, PdaDefinition> {
155    let mut pdas = BTreeMap::new();
156
157    for pda in &idl.pdas {
158        let pda_name = sanitize_identifier(&pda.name);
159        let pda_def = convert_idl_pda_to_def(&pda_name, &pda.seeds, pda.program.as_ref());
160        pdas.insert(pda_name, pda_def);
161    }
162
163    for instruction in &idl.instructions {
164        for account in instruction.flattened_accounts() {
165            if let Some(pda_info) = &account.pda {
166                let pda_name =
167                    sanitize_identifier(pda_info.name.as_deref().unwrap_or(&account.name));
168                let pda_def =
169                    convert_idl_pda_to_def(&pda_name, &pda_info.seeds, pda_info.program.as_ref());
170                pdas.entry(pda_name).or_insert(pda_def);
171            }
172        }
173    }
174
175    pdas
176}
177
178pub fn extract_instructions_from_idl(
179    idl: &idl_parser::IdlSpec,
180    pdas: &BTreeMap<String, PdaDefinition>,
181) -> Vec<InstructionDef> {
182    let program_id = idl.address.clone().or_else(|| {
183        idl.metadata
184            .as_ref()
185            .and_then(|metadata| metadata.address.clone())
186    });
187
188    let uses_steel = idl.instructions.iter().any(|instruction| {
189        instruction.discriminant.is_some() && instruction.discriminator.is_empty()
190    });
191    let discriminator_size = if uses_steel { 1 } else { 8 };
192
193    idl.instructions
194        .iter()
195        .map(|instruction| {
196            let accounts = instruction
197                .flattened_accounts()
198                .iter()
199                .map(|account| convert_account_to_def(account, pdas))
200                .collect();
201
202            let args = instruction
203                .args
204                .iter()
205                .map(|arg| InstructionArgDef {
206                    name: arg.name.clone(),
207                    arg_type: idl_type_snapshot_to_rust_string(&convert_idl_type(&arg.type_)),
208                    docs: vec![],
209                    amount_hint: arg.amount_hint.as_ref().map(convert_amount_hint),
210                })
211                .collect();
212
213            InstructionDef {
214                name: instruction.name.clone(),
215                discriminator: instruction.get_discriminator(),
216                discriminator_size,
217                accounts,
218                args,
219                errors: Vec::new(),
220                program_id: program_id.clone(),
221                docs: instruction.docs.clone(),
222            }
223        })
224        .collect()
225}
226
227pub fn convert_idl_type(idl_type: &idl_parser::IdlType) -> IdlTypeSnapshot {
228    match idl_type {
229        idl_parser::IdlType::Simple(simple) => IdlTypeSnapshot::Simple(simple.clone()),
230        idl_parser::IdlType::Array(array) => IdlTypeSnapshot::Array(IdlArrayTypeSnapshot {
231            array: array
232                .array
233                .iter()
234                .map(|element| match element {
235                    idl_parser::IdlTypeArrayElement::Nested(ty) => {
236                        IdlArrayElementSnapshot::Type(convert_idl_type(ty))
237                    }
238                    idl_parser::IdlTypeArrayElement::Type(type_name) => {
239                        IdlArrayElementSnapshot::TypeName(type_name.clone())
240                    }
241                    idl_parser::IdlTypeArrayElement::Size(size) => {
242                        IdlArrayElementSnapshot::Size(*size)
243                    }
244                })
245                .collect(),
246        }),
247        idl_parser::IdlType::Option(option) => IdlTypeSnapshot::Option(IdlOptionTypeSnapshot {
248            option: Box::new(convert_idl_type(&option.option)),
249        }),
250        idl_parser::IdlType::Vec(vec_type) => IdlTypeSnapshot::Vec(IdlVecTypeSnapshot {
251            vec: Box::new(convert_idl_type(&vec_type.vec)),
252        }),
253        idl_parser::IdlType::Defined(defined) => IdlTypeSnapshot::Defined(IdlDefinedTypeSnapshot {
254            defined: match &defined.defined {
255                idl_parser::IdlTypeDefinedInner::Named { name } => {
256                    IdlDefinedInnerSnapshot::Named { name: name.clone() }
257                }
258                idl_parser::IdlTypeDefinedInner::Simple(simple) => {
259                    IdlDefinedInnerSnapshot::Simple(simple.clone())
260                }
261            },
262        }),
263        idl_parser::IdlType::HashMap(hash_map) => {
264            IdlTypeSnapshot::HashMap(IdlHashMapTypeSnapshot {
265                hash_map: (
266                    Box::new(convert_idl_type(&hash_map.hash_map.0)),
267                    Box::new(convert_idl_type(&hash_map.hash_map.1)),
268                ),
269            })
270        }
271    }
272}
273
274fn convert_amount_hint(hint: &idl_parser::IdlAmountHint) -> InstructionAmountHint {
275    InstructionAmountHint {
276        decimals_source: match &hint.decimals_source {
277            idl_parser::IdlAmountDecimalsSource::ArgMint { arg_name } => {
278                AmountDecimalsSource::ArgMint {
279                    arg_name: arg_name.clone(),
280                }
281            }
282            idl_parser::IdlAmountDecimalsSource::ArgDecimals { arg_name } => {
283                AmountDecimalsSource::ArgDecimals {
284                    arg_name: arg_name.clone(),
285                }
286            }
287            idl_parser::IdlAmountDecimalsSource::KnownAccount { account_name } => {
288                AmountDecimalsSource::KnownAccount {
289                    account_name: account_name.clone(),
290                }
291            }
292            idl_parser::IdlAmountDecimalsSource::Constant { decimals } => {
293                AmountDecimalsSource::Constant {
294                    decimals: *decimals,
295                }
296            }
297        },
298    }
299}
300
301fn convert_idl_pda_to_def(
302    name: &str,
303    pda_seeds: &[idl_parser::IdlPdaSeed],
304    pda_program: Option<&idl_parser::IdlPdaProgram>,
305) -> PdaDefinition {
306    let seeds = pda_seeds
307        .iter()
308        .map(|seed| match seed {
309            idl_parser::IdlPdaSeed::Const { value } => {
310                if let Ok(string) = String::from_utf8(value.clone()) {
311                    PdaSeedDef::Literal { value: string }
312                } else {
313                    PdaSeedDef::Bytes {
314                        value: value.clone(),
315                    }
316                }
317            }
318            idl_parser::IdlPdaSeed::Account { path, .. } => PdaSeedDef::AccountRef {
319                account_name: sanitize_seed_path(path),
320            },
321            idl_parser::IdlPdaSeed::Arg { path, arg_type } => PdaSeedDef::ArgRef {
322                arg_name: sanitize_seed_path(path),
323                arg_type: arg_type.clone(),
324            },
325        })
326        .collect();
327
328    let (program_id, program) = match pda_program {
329        Some(idl_parser::IdlPdaProgram::Literal { value, .. }) => (Some(value.clone()), None),
330        Some(idl_parser::IdlPdaProgram::Const { value, .. }) => {
331            (Some(bs58::encode(value).into_string()), None)
332        }
333        Some(idl_parser::IdlPdaProgram::Account { path, .. }) => (
334            None,
335            Some(PdaProgramDef::AccountRef {
336                account_name: sanitize_seed_path(path),
337            }),
338        ),
339        None => (None, None),
340    };
341
342    PdaDefinition {
343        name: name.to_string(),
344        seeds,
345        program_id,
346        program,
347    }
348}
349
350fn convert_account_to_def(
351    account: &idl_parser::IdlAccountArg,
352    pdas: &BTreeMap<String, PdaDefinition>,
353) -> InstructionAccountDef {
354    let resolution = if account.is_signer && account.address.is_none() && account.pda.is_none() {
355        AccountResolution::Signer
356    } else if let Some(address) = &account.address {
357        AccountResolution::Known {
358            address: address.clone(),
359        }
360    } else if account.pda.is_some() {
361        let pda_name = sanitize_identifier(
362            account
363                .pda
364                .as_ref()
365                .and_then(|pda| pda.name.as_deref())
366                .unwrap_or(&account.name),
367        );
368        if pdas.contains_key(&pda_name) {
369            AccountResolution::PdaRef {
370                pda_name: pda_name.to_string(),
371            }
372        } else if let Some(pda_info) = &account.pda {
373            let pda_def =
374                convert_idl_pda_to_def(&pda_name, &pda_info.seeds, pda_info.program.as_ref());
375            AccountResolution::PdaInline {
376                seeds: pda_def.seeds,
377                program_id: pda_def.program_id,
378                program: pda_def.program,
379            }
380        } else {
381            AccountResolution::UserProvided
382        }
383    } else if pdas.contains_key(&sanitize_identifier(&account.name)) {
384        AccountResolution::PdaRef {
385            pda_name: sanitize_identifier(&account.name),
386        }
387    } else {
388        AccountResolution::UserProvided
389    };
390
391    InstructionAccountDef {
392        name: sanitize_identifier(&account.name),
393        is_signer: account.is_signer,
394        is_writable: account.is_mut,
395        resolution,
396        is_optional: account.optional,
397        docs: account.docs.clone(),
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn builds_program_only_stack_spec_from_raw_idl() {
407        let idl = arete_idl::parse::parse_idl_content(
408            r#"{
409              "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
410              "version": "0.0.0",
411              "name": "token",
412              "instructions": [
413                {
414                  "name": "InitializeMint2",
415                  "accounts": [
416                    { "name": "mint", "isMut": true, "isSigner": false }
417                  ],
418                  "args": [
419                    { "name": "decimals", "type": "u8" },
420                    { "name": "mintAuthority", "type": "publicKey" }
421                  ],
422                  "discriminant": { "type": "u8", "value": 20 }
423                }
424              ],
425              "accounts": [],
426              "types": [],
427              "events": [],
428              "errors": []
429            }"#,
430        )
431        .expect("IDL should parse");
432
433        let spec = build_program_only_stack_spec_from_idl(&idl, "SplToken");
434        assert_eq!(spec.stack_name, "SplToken");
435        assert!(spec.entities.is_empty());
436        assert_eq!(spec.idls.len(), 1);
437        assert_eq!(spec.instructions.len(), 1);
438        assert_eq!(spec.instructions[0].name, "InitializeMint2");
439        assert_eq!(spec.instructions[0].discriminator, vec![20]);
440        assert_eq!(spec.instructions[0].discriminator_size, 1);
441        assert_eq!(
442            spec.instructions[0].program_id.as_deref(),
443            Some("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
444        );
445        assert_eq!(
446            spec.instructions[0].args[1].arg_type,
447            "solana_pubkey::Pubkey"
448        );
449        assert!(spec.content_hash.is_some());
450    }
451
452    #[test]
453    fn preserves_nested_seed_paths_when_building_program_only_specs() {
454        let idl = arete_idl::parse::parse_idl_content(
455            r#"{
456              "address": "Prog111111111111111111111111111111111111111",
457              "version": "0.0.0",
458              "name": "demo",
459              "instructions": [
460                {
461                  "name": "proposalCreate",
462                  "accounts": [
463                    {
464                      "name": "proposal",
465                      "isMut": true,
466                      "isSigner": false,
467                      "pda": {
468                        "name": "proposal",
469                        "seeds": [
470                          {
471                            "kind": "arg",
472                            "path": "args.transactionIndex",
473                            "type": "u64"
474                          }
475                        ]
476                      }
477                    }
478                  ],
479                  "args": [
480                    {
481                      "name": "args",
482                      "type": {
483                        "defined": {
484                          "name": "ProposalArgs"
485                        }
486                      }
487                    }
488                  ],
489                  "discriminant": { "type": "u8", "value": 3 }
490                }
491              ],
492              "accounts": [],
493              "types": [
494                {
495                  "name": "ProposalArgs",
496                  "type": {
497                    "kind": "struct",
498                    "fields": [
499                      { "name": "transactionIndex", "type": "u64" }
500                    ]
501                  }
502                }
503              ],
504              "events": [],
505              "errors": []
506            }"#,
507        )
508        .expect("IDL should parse");
509
510        let spec = build_program_only_stack_spec_from_idl(&idl, "Demo");
511        let pda = spec
512            .pdas
513            .get("demo")
514            .and_then(|program| program.get("proposal"))
515            .expect("proposal PDA should be present");
516        assert_eq!(
517            pda.seeds,
518            vec![PdaSeedDef::ArgRef {
519                arg_name: "args.transactionIndex".to_string(),
520                arg_type: Some("u64".to_string()),
521            }]
522        );
523    }
524
525    #[test]
526    fn preserves_amount_hints_from_idl_args() {
527        let idl = arete_idl::parse::parse_idl_content(
528            r#"{
529              "address": "Prog111111111111111111111111111111111111111",
530              "version": "0.0.0",
531              "name": "demo",
532              "instructions": [
533                {
534                  "name": "deposit",
535                  "accounts": [],
536                  "args": [
537                    {
538                      "name": "amount",
539                      "type": "u64",
540                      "amountHint": {
541                        "decimalsSource": {
542                          "kind": "argMint",
543                          "argName": "mint"
544                        }
545                      }
546                    },
547                    {
548                      "name": "mint",
549                      "type": "publicKey"
550                    }
551                  ],
552                  "discriminant": { "type": "u8", "value": 7 }
553                }
554              ],
555              "accounts": [],
556              "types": [],
557              "events": [],
558              "errors": []
559            }"#,
560        )
561        .expect("IDL should parse");
562
563        let spec = build_program_only_stack_spec_from_idl(&idl, "Demo");
564        assert_eq!(
565            spec.instructions[0].args[0].amount_hint,
566            Some(InstructionAmountHint {
567                decimals_source: AmountDecimalsSource::ArgMint {
568                    arg_name: "mint".to_string(),
569                },
570            })
571        );
572        assert!(spec.idls[0].instructions[0].args[0].amount_hint.is_some());
573    }
574
575    #[test]
576    fn derives_the_checked_in_program_and_release_identities() {
577        let corpus: serde_json::Value =
578            serde_json::from_str(include_str!("../../test-vectors/hash-v1.json"))
579                .expect("vector corpus");
580        let vector = corpus["idlVectors"]
581            .as_array()
582            .unwrap()
583            .iter()
584            .find(|vector| vector["id"] == "idl-primary")
585            .expect("primary IDL vector");
586        let source = vector["input"]["data"].as_str().unwrap().as_bytes();
587
588        let identity = build_oss_program_identity_v1_from_idl_bytes(source, None)
589            .expect("interpreter identity");
590        let stack_spec = build_program_only_stack_spec_from_identity(&identity, "Demo");
591
592        assert_eq!(
593            identity.program_spec_hash.to_string(),
594            vector["expected"]["programSpecIdentity"]["hashId"]
595        );
596        assert_eq!(
597            identity.release_hash.to_string(),
598            vector["expected"]["ossReleaseIdentity"]["hashId"]
599        );
600        assert_eq!(stack_spec.content_hash.as_deref().unwrap().len(), 64);
601        assert_eq!(stack_spec.program_specs.len(), 1);
602        assert_eq!(
603            stack_spec.program_specs[0].hash().unwrap(),
604            identity.program_spec_hash
605        );
606        assert!(stack_spec
607            .content_hash
608            .as_deref()
609            .unwrap()
610            .bytes()
611            .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()));
612        assert_eq!(
613            stack_spec.content_hash.as_deref(),
614            Some(stack_spec.compute_content_hash().as_str())
615        );
616    }
617}