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