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}