1use arete_idl::{
2 normalize_idl_snapshot_v1, IdlAmountDecimalsSource, IdlAmountHint, IdlErrorSnapshot,
3 IdlSnapshotV1, IdlSpec, IdlType, IdlTypeArrayElement, IdlTypeDefinedInner,
4};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::{BTreeMap, BTreeSet};
8
9use crate::{
10 canonicalize_jcs, hash_jcs, hash_raw_bytes, HashError, HashId, IdlContent, IdlNormalized,
11 IdlPortable, IdlSource, OssGeneratedProgramReleaseV1, ProgramRelease, ProgramSpec,
12};
13
14pub const PROGRAM_SPEC_SCHEMA_V1: &str = "arete.program-spec/v1";
15
16#[derive(Debug, Clone)]
17pub struct IdlHashes {
18 pub source: HashId<IdlSource>,
19 pub content: HashId<IdlContent>,
20 pub portable: HashId<IdlPortable>,
21 pub normalized: HashId<IdlNormalized>,
22}
23
24#[derive(Debug, Clone)]
26pub struct CanonicalIdlDocument {
27 source_bytes: Vec<u8>,
28 content: Value,
29 portable: Value,
30 idl: IdlSpec,
31 program_id: String,
32 snapshot: IdlSnapshotV1,
33 hashes: IdlHashes,
34}
35
36impl CanonicalIdlDocument {
37 pub fn parse(bytes: &[u8], explicit_program_id: Option<&str>) -> Result<Self, HashError> {
38 let mut content = crate::parse_json_bytes_strict(bytes)?;
39 let source_program_ids = collect_program_ids(&content)?;
40 let source_has_program_id = !source_program_ids.is_empty();
41 let program_id = resolve_program_id(source_program_ids, explicit_program_id)?;
42
43 if !source_has_program_id {
44 content
45 .as_object_mut()
46 .ok_or_else(|| HashError::InvalidIdl("IDL root must be an object".to_string()))?
47 .insert("address".to_string(), Value::String(program_id.clone()));
48 }
49
50 let parser_input = serde_json::to_string(&content)
51 .map_err(|error| HashError::Serialization(error.to_string()))?;
52 let mut idl =
53 arete_idl::parse::parse_idl_content(&parser_input).map_err(HashError::InvalidIdl)?;
54 idl.address = Some(program_id.clone());
55
56 let mut snapshot = normalize_idl_snapshot_v1(&idl);
57 snapshot.snapshot.program_id = Some(program_id.clone());
58 let portable = portable_idl_projection(&content)?;
59 let hashes = IdlHashes {
60 source: hash_raw_bytes(bytes)?,
61 content: hash_jcs(&content)?,
62 portable: hash_jcs(&portable)?,
63 normalized: hash_jcs(&snapshot)?,
64 };
65
66 Ok(Self {
67 source_bytes: bytes.to_vec(),
68 content,
69 portable,
70 idl,
71 program_id,
72 snapshot,
73 hashes,
74 })
75 }
76
77 pub fn source_bytes(&self) -> &[u8] {
78 &self.source_bytes
79 }
80
81 pub fn content_projection(&self) -> &Value {
82 &self.content
83 }
84
85 pub fn portable_projection(&self) -> &Value {
86 &self.portable
87 }
88
89 pub fn parsed_idl(&self) -> &IdlSpec {
90 &self.idl
91 }
92
93 pub fn program_id(&self) -> &str {
94 &self.program_id
95 }
96
97 pub fn normalized_snapshot(&self) -> &IdlSnapshotV1 {
98 &self.snapshot
99 }
100
101 pub fn hashes(&self) -> &IdlHashes {
102 &self.hashes
103 }
104
105 pub fn content_payload(&self) -> Result<Vec<u8>, HashError> {
106 canonicalize_jcs(&self.content)
107 }
108
109 pub fn portable_payload(&self) -> Result<Vec<u8>, HashError> {
110 canonicalize_jcs(&self.portable)
111 }
112
113 pub fn normalized_payload(&self) -> Result<Vec<u8>, HashError> {
114 canonicalize_jcs(&self.snapshot)
115 }
116}
117
118pub fn portable_idl_projection(source: &Value) -> Result<Value, HashError> {
119 let mut portable = source.clone();
120 let object = portable
121 .as_object_mut()
122 .ok_or_else(|| HashError::InvalidIdl("IDL root must be an object".to_string()))?;
123 object.remove("address");
124 object.remove("program_id");
125 if let Some(metadata) = object.get_mut("metadata").and_then(Value::as_object_mut) {
126 metadata.remove("address");
127 }
128 if let Some(program) = object.get_mut("program").and_then(Value::as_object_mut) {
129 program.remove("publicKey");
130 }
131 Ok(portable)
132}
133
134fn collect_program_ids(value: &Value) -> Result<Vec<(&'static str, String)>, HashError> {
135 let object = value
136 .as_object()
137 .ok_or_else(|| HashError::InvalidIdl("IDL root must be an object".to_string()))?;
138 let mut values = Vec::new();
139 collect_program_id(&mut values, "address", object.get("address"))?;
140 collect_program_id(&mut values, "program_id", object.get("program_id"))?;
141 collect_nested_program_id(
142 &mut values,
143 "metadata.address",
144 object.get("metadata"),
145 "address",
146 )?;
147 collect_nested_program_id(
148 &mut values,
149 "program.publicKey",
150 object.get("program"),
151 "publicKey",
152 )?;
153 Ok(values)
154}
155
156fn collect_nested_program_id(
157 output: &mut Vec<(&'static str, String)>,
158 location: &'static str,
159 parent: Option<&Value>,
160 key: &str,
161) -> Result<(), HashError> {
162 match parent {
163 None | Some(Value::Null) => Ok(()),
164 Some(Value::Object(object)) => collect_program_id(output, location, object.get(key)),
165 Some(_) => Err(HashError::InvalidProgramIdLocation { location }),
166 }
167}
168
169fn collect_program_id(
170 output: &mut Vec<(&'static str, String)>,
171 location: &'static str,
172 value: Option<&Value>,
173) -> Result<(), HashError> {
174 match value {
175 None | Some(Value::Null) => Ok(()),
176 Some(Value::String(value)) if value.is_empty() => Ok(()),
177 Some(Value::String(value)) => {
178 output.push((location, value.clone()));
179 Ok(())
180 }
181 Some(_) => Err(HashError::InvalidProgramIdLocation { location }),
182 }
183}
184
185fn resolve_program_id(
186 mut values: Vec<(&'static str, String)>,
187 explicit: Option<&str>,
188) -> Result<String, HashError> {
189 if let Some(explicit) = explicit {
190 if explicit.is_empty() {
191 return Err(HashError::MissingProgramId);
192 }
193 values.push(("explicit", explicit.to_string()));
194 }
195 if values.is_empty() {
196 return Err(HashError::MissingProgramId);
197 }
198
199 let distinct: BTreeSet<&str> = values.iter().map(|(_, value)| value.as_str()).collect();
200 if distinct.len() != 1 {
201 let detail = values
202 .iter()
203 .map(|(location, value)| format!("{location}={value}"))
204 .collect::<Vec<_>>()
205 .join(", ");
206 return Err(HashError::ConflictingProgramIds(detail));
207 }
208 Ok(values.remove(0).1)
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct ProgramSpecV1 {
214 pub schema: String,
215 pub program_id: String,
216 pub idl_content_hash: HashId<IdlContent>,
217 pub portable_idl_hash: HashId<IdlPortable>,
218 pub normalized_idl_hash: HashId<IdlNormalized>,
219 pub idl_snapshot: IdlSnapshotV1,
220 pub pdas: BTreeMap<String, PdaDefinitionV1>,
221 pub instructions: Vec<InstructionDefinitionV1>,
222}
223
224impl ProgramSpecV1 {
225 pub fn from_document(document: &CanonicalIdlDocument) -> Self {
226 let pdas = extract_pdas(document.parsed_idl());
227 let instructions =
228 extract_instructions(document.parsed_idl(), &pdas, document.program_id());
229 Self {
230 schema: PROGRAM_SPEC_SCHEMA_V1.to_string(),
231 program_id: document.program_id.clone(),
232 idl_content_hash: document.hashes.content,
233 portable_idl_hash: document.hashes.portable,
234 normalized_idl_hash: document.hashes.normalized,
235 idl_snapshot: document.snapshot.clone(),
236 pdas,
237 instructions,
238 }
239 }
240
241 pub fn hash(&self) -> Result<HashId<ProgramSpec>, HashError> {
242 self.validate()?;
243 hash_jcs(self)
244 }
245
246 pub fn validate(&self) -> Result<(), HashError> {
247 if self.schema != PROGRAM_SPEC_SCHEMA_V1 {
248 return Err(HashError::UnknownVersion(self.schema.clone()));
249 }
250 if self.idl_snapshot.normalization_version != arete_idl::IDL_NORMALIZATION_VERSION {
251 return Err(HashError::UnknownVersion(format!(
252 "IDL normalization version {}",
253 self.idl_snapshot.normalization_version
254 )));
255 }
256 if self.program_id.is_empty() {
257 return Err(HashError::MissingProgramId);
258 }
259 if self.idl_snapshot.snapshot.program_id.as_deref() != Some(self.program_id.as_str()) {
260 return Err(HashError::InvalidProjection {
261 projection: "program spec",
262 reason: "programId must match idlSnapshot.program_id".to_string(),
263 });
264 }
265 for pda in self.pdas.values() {
266 validate_pda_seeds(&pda.seeds)?;
267 }
268 for instruction in &self.instructions {
269 for account in &instruction.accounts {
270 if let AccountResolutionV1::PdaInline { seeds, .. } = &account.resolution {
271 validate_pda_seeds(seeds)?;
272 }
273 }
274 }
275 Ok(())
276 }
277
278 pub fn oss_release(&self) -> Result<OssGeneratedProgramReleaseV1, HashError> {
279 Ok(OssGeneratedProgramReleaseV1::new(
280 self.program_id.clone(),
281 self.hash()?,
282 self.idl_content_hash,
283 self.normalized_idl_hash,
284 ))
285 }
286
287 pub fn oss_release_hash(&self) -> Result<HashId<crate::ProgramRelease>, HashError> {
288 self.oss_release()?.hash()
289 }
290
291 pub fn oss_identity(&self) -> Result<OssProgramIdentityV1, HashError> {
292 OssProgramIdentityV1::new(self.clone())
293 }
294}
295
296#[derive(Debug, Clone)]
297pub struct OssProgramIdentityV1 {
298 pub program_spec: ProgramSpecV1,
299 pub program_spec_hash: HashId<ProgramSpec>,
300 pub release: OssGeneratedProgramReleaseV1,
301 pub release_hash: HashId<ProgramRelease>,
302}
303
304impl OssProgramIdentityV1 {
305 pub fn new(program_spec: ProgramSpecV1) -> Result<Self, HashError> {
306 let program_spec_hash = program_spec.hash()?;
307 let release = OssGeneratedProgramReleaseV1::new(
308 program_spec.program_id.clone(),
309 program_spec_hash,
310 program_spec.idl_content_hash,
311 program_spec.normalized_idl_hash,
312 );
313 let release_hash = release.hash()?;
314 Ok(Self {
315 program_spec,
316 program_spec_hash,
317 release,
318 release_hash,
319 })
320 }
321
322 pub fn from_document(document: &CanonicalIdlDocument) -> Result<Self, HashError> {
323 Self::new(ProgramSpecV1::from_document(document))
324 }
325}
326
327pub fn build_program_spec_v1_from_bytes(
328 bytes: &[u8],
329 explicit_program_id: Option<&str>,
330) -> Result<ProgramSpecV1, HashError> {
331 let document = CanonicalIdlDocument::parse(bytes, explicit_program_id)?;
332 Ok(ProgramSpecV1::from_document(&document))
333}
334
335pub fn build_oss_program_identity_v1_from_bytes(
336 bytes: &[u8],
337 explicit_program_id: Option<&str>,
338) -> Result<OssProgramIdentityV1, HashError> {
339 OssProgramIdentityV1::new(build_program_spec_v1_from_bytes(
340 bytes,
341 explicit_program_id,
342 )?)
343}
344
345pub fn build_program_spec_v1_from_idl(
350 idl: &IdlSpec,
351 explicit_program_id: Option<&str>,
352) -> Result<ProgramSpecV1, HashError> {
353 let bytes =
354 serde_json::to_vec(idl).map_err(|error| HashError::Serialization(error.to_string()))?;
355 build_program_spec_v1_from_bytes(&bytes, explicit_program_id)
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
359pub struct PdaDefinitionV1 {
360 pub name: String,
361 pub seeds: Vec<PdaSeedV1>,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub program_id: Option<String>,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub program: Option<PdaProgramV1>,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
373#[serde(tag = "type", rename_all = "camelCase")]
374pub enum PdaProgramV1 {
375 AccountRef { account_name: String },
376 ArgRef { arg_name: String },
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
380#[serde(tag = "type", rename_all = "camelCase")]
381pub enum PdaSeedV1 {
382 Literal {
383 value: String,
384 },
385 Bytes {
386 value: Vec<u8>,
387 },
388 ArgRef {
389 arg_name: String,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
391 arg_type: Option<String>,
392 },
393 AccountRef {
394 account_name: String,
395 },
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
399#[serde(tag = "category", rename_all = "camelCase")]
400pub enum AccountResolutionV1 {
401 Signer,
402 Known {
403 address: String,
404 },
405 PdaRef {
406 pda_name: String,
407 },
408 PdaInline {
409 seeds: Vec<PdaSeedV1>,
410 #[serde(default, skip_serializing_if = "Option::is_none")]
411 program_id: Option<String>,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
413 program: Option<PdaProgramV1>,
414 },
415 UserProvided,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
419pub struct InstructionAccountV1 {
420 pub name: String,
421 #[serde(default)]
422 pub is_signer: bool,
423 #[serde(default)]
424 pub is_writable: bool,
425 pub resolution: AccountResolutionV1,
426 #[serde(default)]
427 pub is_optional: bool,
428 #[serde(default, skip_serializing_if = "Vec::is_empty")]
429 pub docs: Vec<String>,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
433#[serde(rename_all = "camelCase")]
434pub struct InstructionAmountHintV1 {
435 pub decimals_source: AmountDecimalsSourceV1,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
439#[serde(
440 tag = "kind",
441 rename_all = "camelCase",
442 rename_all_fields = "camelCase"
443)]
444pub enum AmountDecimalsSourceV1 {
445 ArgMint { arg_name: String },
446 ArgDecimals { arg_name: String },
447 KnownAccount { account_name: String },
448 Constant { decimals: u8 },
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
452pub struct InstructionArgumentV1 {
453 pub name: String,
454 #[serde(rename = "type")]
455 pub arg_type: String,
456 #[serde(default, skip_serializing_if = "Vec::is_empty")]
457 pub docs: Vec<String>,
458 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub amount_hint: Option<InstructionAmountHintV1>,
460}
461
462fn default_discriminator_size() -> usize {
463 8
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
467pub struct InstructionDefinitionV1 {
468 pub name: String,
469 pub discriminator: Vec<u8>,
470 #[serde(default = "default_discriminator_size")]
471 pub discriminator_size: usize,
472 pub accounts: Vec<InstructionAccountV1>,
473 pub args: Vec<InstructionArgumentV1>,
474 #[serde(default, skip_serializing_if = "Vec::is_empty")]
475 pub errors: Vec<IdlErrorSnapshot>,
476 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub program_id: Option<String>,
478 #[serde(default, skip_serializing_if = "Vec::is_empty")]
479 pub docs: Vec<String>,
480}
481
482fn extract_pdas(idl: &IdlSpec) -> BTreeMap<String, PdaDefinitionV1> {
483 let mut pdas = BTreeMap::new();
484 let mut named_pdas = BTreeSet::new();
485 for pda in &idl.pdas {
486 let name = sanitize_identifier(&pda.name);
487 named_pdas.insert(name.clone());
488 pdas.insert(
489 name.clone(),
490 convert_pda(&name, &pda.seeds, pda.program.as_ref()),
491 );
492 }
493 let mut conflicting_account_pdas = BTreeSet::new();
494 for instruction in &idl.instructions {
495 for account in instruction.flattened_accounts() {
496 if let Some(pda) = &account.pda {
497 let name = sanitize_identifier(pda.name.as_deref().unwrap_or(&account.name));
498 if named_pdas.contains(&name) || conflicting_account_pdas.contains(&name) {
499 continue;
500 }
501 let candidate = convert_pda(&name, &pda.seeds, pda.program.as_ref());
502 match pdas.get(&name) {
503 None => {
504 pdas.insert(name, candidate);
505 }
506 Some(existing) if existing == &candidate => {}
507 Some(_) => {
508 pdas.remove(&name);
512 conflicting_account_pdas.insert(name);
513 }
514 }
515 }
516 }
517 }
518 pdas
519}
520
521fn extract_instructions(
522 idl: &IdlSpec,
523 pdas: &BTreeMap<String, PdaDefinitionV1>,
524 program_id: &str,
525) -> Vec<InstructionDefinitionV1> {
526 let discriminator_size = idl.instruction_discriminator_size();
527
528 idl.instructions
529 .iter()
530 .map(|instruction| InstructionDefinitionV1 {
531 name: instruction.name.clone(),
532 discriminator: instruction.get_discriminator(),
533 discriminator_size,
534 accounts: instruction
535 .flattened_accounts()
536 .iter()
537 .map(|account| convert_account(account, pdas))
538 .collect(),
539 args: instruction
540 .args
541 .iter()
542 .map(|argument| InstructionArgumentV1 {
543 name: argument.name.clone(),
544 arg_type: idl_type_to_rust_string(&argument.type_),
545 docs: Vec::new(),
546 amount_hint: argument.amount_hint.as_ref().map(convert_amount_hint),
547 })
548 .collect(),
549 errors: Vec::new(),
550 program_id: Some(program_id.to_string()),
551 docs: instruction.docs.clone(),
552 })
553 .collect()
554}
555
556fn convert_pda(
557 name: &str,
558 seeds: &[arete_idl::IdlPdaSeed],
559 program: Option<&arete_idl::IdlPdaProgram>,
560) -> PdaDefinitionV1 {
561 let seeds = seeds
562 .iter()
563 .map(|seed| match seed {
564 arete_idl::IdlPdaSeed::Const { value } => convert_const_pda_seed(value),
565 arete_idl::IdlPdaSeed::Account { path, .. } => PdaSeedV1::AccountRef {
566 account_name: sanitize_seed_path(path),
567 },
568 arete_idl::IdlPdaSeed::Arg { path, arg_type } => PdaSeedV1::ArgRef {
569 arg_name: sanitize_seed_path(path),
570 arg_type: arg_type.clone(),
571 },
572 })
573 .collect();
574 let (program_id, program) = match program {
575 Some(arete_idl::IdlPdaProgram::Literal { value, .. }) => (Some(value.clone()), None),
576 Some(arete_idl::IdlPdaProgram::Const { value, .. }) => {
577 (Some(bs58::encode(value).into_string()), None)
578 }
579 Some(arete_idl::IdlPdaProgram::Account { path, .. }) => (
580 None,
581 Some(PdaProgramV1::AccountRef {
582 account_name: sanitize_seed_path(path),
583 }),
584 ),
585 None => (None, None),
586 };
587 PdaDefinitionV1 {
588 name: name.to_string(),
589 seeds,
590 program_id,
591 program,
592 }
593}
594
595fn convert_const_pda_seed(value: &[u8]) -> PdaSeedV1 {
596 match String::from_utf8(value.to_vec()) {
597 Ok(value) if !value.contains('\0') => PdaSeedV1::Literal { value },
598 _ => PdaSeedV1::Bytes {
599 value: value.to_vec(),
600 },
601 }
602}
603
604fn validate_pda_seeds(seeds: &[PdaSeedV1]) -> Result<(), HashError> {
605 if seeds
606 .iter()
607 .any(|seed| matches!(seed, PdaSeedV1::Literal { value } if value.contains('\0')))
608 {
609 return Err(HashError::InvalidProjection {
610 projection: "program spec",
611 reason: "literal PDA seeds must not contain NUL bytes; use a bytes seed".to_string(),
612 });
613 }
614 Ok(())
615}
616
617fn convert_account(
618 account: &arete_idl::IdlAccountArg,
619 pdas: &BTreeMap<String, PdaDefinitionV1>,
620) -> InstructionAccountV1 {
621 let resolution = if account.is_signer && account.address.is_none() && account.pda.is_none() {
622 AccountResolutionV1::Signer
623 } else if let Some(address) = &account.address {
624 AccountResolutionV1::Known {
625 address: address.clone(),
626 }
627 } else if let Some(pda) = &account.pda {
628 let name = sanitize_identifier(pda.name.as_deref().unwrap_or(&account.name));
629 let converted = convert_pda(&name, &pda.seeds, pda.program.as_ref());
630 if pdas.get(&name) == Some(&converted) {
631 AccountResolutionV1::PdaRef { pda_name: name }
632 } else {
633 AccountResolutionV1::PdaInline {
634 seeds: converted.seeds,
635 program_id: converted.program_id,
636 program: converted.program,
637 }
638 }
639 } else {
640 let name = sanitize_identifier(&account.name);
641 if pdas.contains_key(&name) {
642 AccountResolutionV1::PdaRef { pda_name: name }
643 } else {
644 AccountResolutionV1::UserProvided
645 }
646 };
647 InstructionAccountV1 {
648 name: sanitize_identifier(&account.name),
649 is_signer: account.is_signer,
650 is_writable: account.is_mut,
651 resolution,
652 is_optional: account.optional,
653 docs: account.docs.clone(),
654 }
655}
656
657fn convert_amount_hint(hint: &IdlAmountHint) -> InstructionAmountHintV1 {
658 InstructionAmountHintV1 {
659 decimals_source: match &hint.decimals_source {
660 IdlAmountDecimalsSource::ArgMint { arg_name } => AmountDecimalsSourceV1::ArgMint {
661 arg_name: arg_name.clone(),
662 },
663 IdlAmountDecimalsSource::ArgDecimals { arg_name } => {
664 AmountDecimalsSourceV1::ArgDecimals {
665 arg_name: arg_name.clone(),
666 }
667 }
668 IdlAmountDecimalsSource::KnownAccount { account_name } => {
669 AmountDecimalsSourceV1::KnownAccount {
670 account_name: account_name.clone(),
671 }
672 }
673 IdlAmountDecimalsSource::Constant { decimals } => AmountDecimalsSourceV1::Constant {
674 decimals: *decimals,
675 },
676 },
677 }
678}
679
680fn sanitize_identifier(name: &str) -> String {
681 let mut sanitized = String::new();
682 for character in name.chars() {
683 if character.is_ascii_alphanumeric() || character == '_' {
684 sanitized.push(character);
685 } else if !sanitized.ends_with('_') {
686 sanitized.push('_');
687 }
688 }
689 let sanitized = sanitized.trim_matches('_').to_string();
690 if sanitized.is_empty() {
691 return "value".to_string();
692 }
693 if sanitized
694 .chars()
695 .next()
696 .is_some_and(|character| character.is_ascii_digit())
697 {
698 return format!("_{sanitized}");
699 }
700 sanitized
701}
702
703fn sanitize_seed_path(path: &str) -> String {
704 path.split('.')
705 .map(sanitize_identifier)
706 .collect::<Vec<_>>()
707 .join(".")
708}
709
710fn idl_type_to_rust_string(idl_type: &IdlType) -> String {
711 match idl_type {
712 IdlType::Simple(simple) => match simple.as_str() {
713 "string" => "String".to_string(),
714 "publicKey" | "pubkey" => "solana_pubkey::Pubkey".to_string(),
715 "bytes" => "Vec<u8>".to_string(),
716 other => other.to_string(),
717 },
718 IdlType::Array(array) if array.array.len() == 2 => {
719 match (&array.array[0], &array.array[1]) {
720 (IdlTypeArrayElement::Type(name), IdlTypeArrayElement::Size(size)) => {
721 format!(
722 "[{}; {size}]",
723 idl_type_to_rust_string(&IdlType::Simple(name.clone()))
724 )
725 }
726 (IdlTypeArrayElement::Nested(ty), IdlTypeArrayElement::Size(size)) => {
727 format!("[{}; {size}]", idl_type_to_rust_string(ty))
728 }
729 _ => "Vec<u8>".to_string(),
730 }
731 }
732 IdlType::Array(_) => "Vec<u8>".to_string(),
733 IdlType::Option(option) => format!("Option<{}>", idl_type_to_rust_string(&option.option)),
734 IdlType::Vec(vec_type) => {
736 let inner = idl_type_to_rust_string(&vec_type.vec);
737 if matches!(
738 vec_type.length_prefix,
739 Some(arete_idl::types::IdlLengthPrefix::U64)
740 ) {
741 format!("VecU64Len<{inner}>")
742 } else {
743 format!("Vec<{inner}>")
744 }
745 }
746 IdlType::HashMap(hash_map) => format!(
747 "std::collections::HashMap<{}, {}>",
748 idl_type_to_rust_string(&hash_map.hash_map.0),
749 idl_type_to_rust_string(&hash_map.hash_map.1)
750 ),
751 IdlType::Tuple(tuple) => format!(
752 "({})",
753 tuple
754 .tuple
755 .iter()
756 .map(idl_type_to_rust_string)
757 .collect::<Vec<_>>()
758 .join(", ")
759 ),
760 IdlType::Defined(defined) => match &defined.defined {
761 IdlTypeDefinedInner::Named { name } => name.clone(),
762 IdlTypeDefinedInner::Simple(simple) => simple.clone(),
763 },
764 }
765}
766
767#[cfg(test)]
768mod tests {
769 use super::*;
770
771 #[test]
772 fn preserves_account_selected_pda_programs() {
773 let definition = convert_pda(
774 "metadata",
775 &[arete_idl::IdlPdaSeed::Const {
776 value: b"metadata".to_vec(),
777 }],
778 Some(&arete_idl::IdlPdaProgram::Account {
779 kind: "account".to_string(),
780 path: "metadata_program".to_string(),
781 }),
782 );
783
784 assert_eq!(definition.program_id, None);
785 assert_eq!(
786 definition.program,
787 Some(PdaProgramV1::AccountRef {
788 account_name: "metadata_program".to_string(),
789 })
790 );
791 }
792
793 #[test]
794 fn preserves_nul_const_pda_seeds_as_bytes() {
795 let definition = convert_pda(
796 "binary",
797 &[
798 arete_idl::IdlPdaSeed::Const {
799 value: b"text".to_vec(),
800 },
801 arete_idl::IdlPdaSeed::Const { value: vec![0, 0] },
802 ],
803 None,
804 );
805
806 assert_eq!(
807 definition.seeds,
808 vec![
809 PdaSeedV1::Literal {
810 value: "text".to_string(),
811 },
812 PdaSeedV1::Bytes { value: vec![0, 0] },
813 ]
814 );
815 }
816
817 #[test]
818 fn rejects_nul_in_literal_pda_seeds() {
819 let source = br#"{
820 "address":"11111111111111111111111111111111",
821 "metadata":{"name":"demo","version":"0.1.0","spec":"0.1.0"},
822 "instructions":[
823 {"name":"create","discriminator":[1],"accounts":[
824 {"name":"state","pda":{"seeds":[{"kind":"const","value":[115,116,97,116,101]}]}}
825 ],"args":[]}
826 ],
827 "accounts":[],"types":[],"events":[],"errors":[]
828 }"#;
829 let document = CanonicalIdlDocument::parse(source, None).unwrap();
830 let mut spec = ProgramSpecV1::from_document(&document);
831 spec.pdas.get_mut("state").unwrap().seeds[0] = PdaSeedV1::Literal {
832 value: "\0".to_string(),
833 };
834
835 assert!(matches!(
836 spec.validate(),
837 Err(HashError::InvalidProjection {
838 projection: "program spec",
839 ..
840 })
841 ));
842 }
843
844 #[test]
845 fn keeps_conflicting_account_pdas_inline_per_instruction() {
846 let source = br#"{
847 "address":"11111111111111111111111111111111",
848 "metadata":{"name":"demo","version":"0.1.0","spec":"0.1.0"},
849 "instructions":[
850 {"name":"create","discriminator":[1,0,0,0,0,0,0,0],"accounts":[
851 {"name":"state","pda":{"seeds":[{"kind":"const","value":[99,114,101,97,116,101]}]}}
852 ],"args":[]},
853 {"name":"update","discriminator":[2,0,0,0,0,0,0,0],"accounts":[
854 {"name":"state","pda":{"seeds":[{"kind":"const","value":[117,112,100,97,116,101]}]}}
855 ],"args":[]}
856 ],
857 "accounts":[],"types":[],"events":[],"errors":[]
858 }"#;
859 let document = CanonicalIdlDocument::parse(source, None).unwrap();
860 let spec = ProgramSpecV1::from_document(&document);
861
862 assert!(!spec.pdas.contains_key("state"));
863 for instruction in &spec.instructions {
864 assert!(matches!(
865 &instruction.accounts[0].resolution,
866 AccountResolutionV1::PdaInline { .. }
867 ));
868 }
869 }
870}