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}