1use std::sync::OnceLock;
12
13use crate::{
14 build_program_spec_v1_from_bytes, AccountResolutionV1, InstructionDefinitionV1, PdaSeedV1,
15 ProgramSpecV1,
16};
17
18use super::BuiltinProgram;
19
20const SYSTEM: &str = include_str!("builtins/system-program-canonical.program-spec.json");
21const SPL_TOKEN: &str = include_str!("builtins/spl-token.program-spec.json");
22const ASSOCIATED_TOKEN: &str = include_str!("builtins/spl-ata.program-spec.json");
23const MEMO: &str = include_str!("builtins/spl-memo.program-spec.json");
24const COMPUTE_BUDGET_IDL: &str = include_str!("builtins/compute-budget.idl.json");
25
26const ALIASES: &[(&str, BuiltinProgram)] = &[
29 ("system-program", BuiltinProgram::System),
30 ("systemProgram", BuiltinProgram::System),
31 ("spl-token", BuiltinProgram::SplToken),
32 ("token", BuiltinProgram::SplToken),
33 ("spl-ata", BuiltinProgram::AssociatedToken),
34 ("splAta", BuiltinProgram::AssociatedToken),
35 ("ata", BuiltinProgram::AssociatedToken),
36 ("associated-token", BuiltinProgram::AssociatedToken),
37 (
38 "spl-associated-token-account",
39 BuiltinProgram::AssociatedToken,
40 ),
41 ("compute-budget", BuiltinProgram::ComputeBudget),
42 ("spl-memo", BuiltinProgram::Memo),
43 ("splMemo", BuiltinProgram::Memo),
44];
45
46fn normalize(name: &str) -> String {
47 name.chars()
48 .filter(|c| !matches!(c, '-' | '_'))
49 .flat_map(char::to_lowercase)
50 .collect()
51}
52
53impl BuiltinProgram {
54 pub const VALUES: [Self; 5] = [
56 Self::System,
57 Self::SplToken,
58 Self::AssociatedToken,
59 Self::ComputeBudget,
60 Self::Memo,
61 ];
62
63 pub const fn name(self) -> &'static str {
65 match self {
66 Self::System => "system",
67 Self::SplToken => "splToken",
68 Self::AssociatedToken => "associatedToken",
69 Self::ComputeBudget => "computeBudget",
70 Self::Memo => "memo",
71 }
72 }
73
74 pub fn from_name(name: &str) -> Option<Self> {
76 Self::VALUES
77 .into_iter()
78 .find(|builtin| builtin.name() == name)
79 }
80
81 pub fn from_alias(name: &str) -> Option<Self> {
85 if Self::from_name(name).is_some() {
86 return None;
87 }
88 let normalized = normalize(name);
89 Self::VALUES
90 .into_iter()
91 .find(|builtin| normalize(builtin.name()) == normalized)
92 .or_else(|| {
93 ALIASES
94 .iter()
95 .find(|(alias, _)| normalize(alias) == normalized)
96 .map(|(_, builtin)| *builtin)
97 })
98 }
99
100 pub const fn registry_package(self) -> Option<&'static str> {
102 match self {
103 Self::System => Some("system-program"),
104 Self::SplToken => Some("spl-token"),
105 Self::AssociatedToken => Some("spl-ata"),
106 Self::ComputeBudget => None,
107 Self::Memo => Some("spl-memo"),
108 }
109 }
110
111 pub const fn program_spec_hash(self) -> &'static str {
113 match self {
114 Self::System => {
115 "arete:h1:program-spec:sha256:a416fd5d908028d96d4e6445d1da3c7a60147e55711640c281d91f6602c7fb17"
116 }
117 Self::SplToken => {
118 "arete:h1:program-spec:sha256:c4660abfd3626b6f8c9161c159ce2ab856905e1455477e526aaceff95c56d544"
119 }
120 Self::AssociatedToken => {
121 "arete:h1:program-spec:sha256:8a2641d19db953d2945a6eb864c4135e0748f5467a5e298d5969972d44441ea4"
122 }
123 Self::ComputeBudget => {
124 "arete:h1:program-spec:sha256:66dcfb8f7fa0f43dcb6a9d4dda212134ad9fab15292ba73665c8cfca84ec1b5b"
125 }
126 Self::Memo => {
127 "arete:h1:program-spec:sha256:63fe09c92de2095d6f56683451058871e12c2f584f4d9ad35a53e6d85daf64a7"
128 }
129 }
130 }
131
132 pub fn program_spec(self) -> &'static ProgramSpecV1 {
134 static SPECS: OnceLock<[ProgramSpecV1; 5]> = OnceLock::new();
135 let specs = SPECS.get_or_init(|| Self::VALUES.map(build_spec));
136 &specs[self as usize]
137 }
138}
139
140fn build_spec(builtin: BuiltinProgram) -> ProgramSpecV1 {
141 let spec = match builtin {
142 BuiltinProgram::System => serde_json::from_str(SYSTEM),
143 BuiltinProgram::SplToken => serde_json::from_str(SPL_TOKEN),
144 BuiltinProgram::AssociatedToken => serde_json::from_str(ASSOCIATED_TOKEN),
145 BuiltinProgram::Memo => serde_json::from_str(MEMO),
146 BuiltinProgram::ComputeBudget => {
147 return build_program_spec_v1_from_bytes(COMPUTE_BUDGET_IDL.as_bytes(), None)
148 .expect("the embedded Compute Budget IDL builds");
149 }
150 };
151 spec.expect("an embedded builtin ProgramSpec parses")
152}
153
154#[doc(hidden)]
157pub const BUILTIN_MODULES_TYPESCRIPT_PATH: &str = "typescript/protocol/src/builtins.generated.ts";
158
159#[doc(hidden)]
164pub fn builtin_modules_typescript() -> String {
165 let q = |text: &str| serde_json::to_string(text).expect("a string serializes");
166 let mut out = String::from(
167 "// Generated by `cargo run -p arete-hash --example generate_builtin_modules`\n\
168 // from the builtin ProgramSpecs in arete-hash. Do not edit.\n\n\
169 /** Each builtin program's id, pinned ProgramSpec hash and instruction shapes. */\n\
170 export const BUILTIN_PROGRAM_MODULES = {\n",
171 );
172 for builtin in BuiltinProgram::VALUES {
173 let spec = builtin.program_spec();
174 out.push_str(&format!(
175 " {}: {{\n builtin: {},\n programId: {},\n programSpecHash: {},\n instructionShapes: {{\n",
176 builtin.name(),
177 q(builtin.name()),
178 q(builtin.program_id()),
179 q(builtin.program_spec_hash()),
180 ));
181 for ix in &spec.instructions {
182 let args = ix.args.iter().map(|arg| q(&arg.name)).collect::<Vec<_>>();
183 out.push_str(&format!(
184 " {}: {{\n args: [{}],\n accounts: {{",
185 q(&ix.name),
186 args.join(", ")
187 ));
188 if ix.accounts.is_empty() {
189 out.push_str("},\n");
190 } else {
191 out.push('\n');
192 for account in &ix.accounts {
193 let derive = match account_derivation(spec, ix, &account.resolution) {
194 Some(names) => format!(
195 "[{}]",
196 names
197 .iter()
198 .map(|name| q(name))
199 .collect::<Vec<_>>()
200 .join(", ")
201 ),
202 None => "null".into(),
203 };
204 out.push_str(&format!(
205 " {}: {{ optional: {}, derive: {} }},\n",
206 q(&account.name),
207 account.is_optional,
208 derive
209 ));
210 }
211 out.push_str(" },\n");
212 }
213 out.push_str(" },\n");
214 }
215 out.push_str(" },\n },\n");
216 }
217 out.push_str("} as const;\n");
218 out
219}
220
221fn account_derivation(
226 spec: &ProgramSpecV1,
227 ix: &InstructionDefinitionV1,
228 resolution: &AccountResolutionV1,
229) -> Option<Vec<String>> {
230 let seeds = |seeds: &[PdaSeedV1], dynamic_program: bool| {
231 if dynamic_program {
232 return None;
233 }
234 let mut accounts = Vec::new();
235 for seed in seeds {
236 match seed {
237 PdaSeedV1::Literal { .. } | PdaSeedV1::Bytes { .. } => {}
238 PdaSeedV1::ArgRef { arg_name, .. } => {
239 if !ix.args.iter().any(|arg| arg.name == *arg_name) {
240 return None;
241 }
242 }
243 PdaSeedV1::AccountRef { account_name } => {
244 if !accounts.contains(account_name) {
245 accounts.push(account_name.clone());
246 }
247 }
248 }
249 }
250 Some(accounts)
251 };
252 match resolution {
253 AccountResolutionV1::Known { .. } => Some(Vec::new()),
254 AccountResolutionV1::PdaRef { pda_name } => spec
255 .pdas
256 .get(pda_name)
257 .and_then(|pda| seeds(&pda.seeds, pda.program.is_some())),
258 AccountResolutionV1::PdaInline {
259 seeds: s, program, ..
260 } => seeds(s, program.is_some()),
261 AccountResolutionV1::Signer | AccountResolutionV1::UserProvided => None,
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn embedded_specs_hash_to_their_pins() {
271 for builtin in BuiltinProgram::VALUES {
272 let spec = builtin.program_spec();
273 assert_eq!(spec.program_id, builtin.program_id(), "{builtin:?}");
274 assert_eq!(
275 spec.hash().unwrap().to_string(),
276 builtin.program_spec_hash(),
277 "{builtin:?}"
278 );
279 }
280 }
281
282 #[test]
283 fn generated_typescript_is_current() {
284 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
285 .join("..")
286 .join(BUILTIN_MODULES_TYPESCRIPT_PATH);
287 let Ok(current) = std::fs::read_to_string(&path) else {
288 return;
290 };
291 assert!(
292 current == builtin_modules_typescript(),
293 "{BUILTIN_MODULES_TYPESCRIPT_PATH} is stale: run `cargo run -p arete-hash --example generate_builtin_modules`"
294 );
295 }
296
297 #[test]
298 fn values_match_all_and_serde() {
299 for (builtin, name) in BuiltinProgram::VALUES.iter().zip(BuiltinProgram::ALL) {
300 assert_eq!(builtin.name(), name);
301 assert_eq!(
302 serde_json::to_value(builtin).unwrap(),
303 serde_json::Value::String(name.into())
304 );
305 }
306 }
307
308 #[test]
309 fn aliases_name_the_canonical_builtin() {
310 assert_eq!(
311 BuiltinProgram::from_alias("spl-ata"),
312 Some(BuiltinProgram::AssociatedToken)
313 );
314 assert_eq!(
315 BuiltinProgram::from_alias("splAta"),
316 Some(BuiltinProgram::AssociatedToken)
317 );
318 assert_eq!(
319 BuiltinProgram::from_alias("associated_token"),
320 Some(BuiltinProgram::AssociatedToken)
321 );
322 assert_eq!(
323 BuiltinProgram::from_alias("spl-token"),
324 Some(BuiltinProgram::SplToken)
325 );
326 assert_eq!(
327 BuiltinProgram::from_alias("compute-budget"),
328 Some(BuiltinProgram::ComputeBudget)
329 );
330 assert_eq!(BuiltinProgram::from_alias("associatedToken"), None);
331 assert_eq!(BuiltinProgram::from_alias("vault"), None);
332 }
333
334 #[test]
335 fn specs_name_the_canonical_builtin_for_an_alias() {
336 let fixture: serde_json::Value = serde_json::from_str(include_str!(
337 "../../../test-vectors/protocol/escrow-resolution.protocol-spec.json"
338 ))
339 .unwrap();
340
341 let mut spec = fixture.clone();
342 spec["programs"]["ata"] = serde_json::json!({ "kind": "builtin", "builtin": "spl-ata" });
343 let error = super::super::validate_protocol_spec_value(&spec).unwrap_err();
344 assert_eq!(error.code, "invalid-value");
345 assert_eq!(error.path, "/programs/ata/builtin");
346 assert_eq!(error.message, "'spl-ata' is the builtin 'associatedToken'");
347
348 let mut spec = fixture;
349 spec["steps"]["open"]["build"]["instructions"][0]["program"] = "splAta".into();
350 let error = super::super::validate_protocol_spec_value(&spec).unwrap_err();
351 assert_eq!(error.code, "unknown-program");
352 assert!(
353 error
354 .message
355 .contains("'splAta' is the builtin 'associatedToken'"),
356 "{}",
357 error.message
358 );
359 }
360
361 #[test]
365 fn token_transfer_owner_signs_when_it_is_not_the_fee_payer() {
366 const PAYER: &str = "Payer11111111111111111111111111111111111111";
367 const OWNER: &str = "Owner11111111111111111111111111111111111111";
368 let spec = BuiltinProgram::SplToken.program_spec();
369 let transfer = spec
370 .instructions
371 .iter()
372 .find(|ix| ix.name == "Transfer")
373 .unwrap();
374 let supplied = |name: &str| match name {
375 "source" => "Source1111111111111111111111111111111111111",
376 "destination" => "Destination11111111111111111111111111111111",
377 "owner" => OWNER,
378 other => panic!("unexpected account '{other}'"),
379 };
380 let metas: Vec<_> = transfer
381 .accounts
382 .iter()
383 .map(|account| {
384 (
385 supplied(&account.name),
386 account.is_signer,
387 account.is_writable,
388 )
389 })
390 .collect();
391 assert_eq!(
392 metas,
393 [
394 ("Source1111111111111111111111111111111111111", false, true),
395 ("Destination11111111111111111111111111111111", false, true),
396 (OWNER, true, false),
397 ]
398 );
399 let mut signers = vec![PAYER];
400 signers.extend(metas.iter().filter(|meta| meta.1).map(|meta| meta.0));
401 assert_eq!(signers, [PAYER, OWNER]);
402 }
403
404 #[test]
409 fn token_authorities_are_signers() {
410 let spec = BuiltinProgram::SplToken.program_spec();
411 let signers: Vec<_> = spec
412 .instructions
413 .iter()
414 .flat_map(|ix| {
415 ix.accounts
416 .iter()
417 .filter(|account| account.is_signer)
418 .map(move |account| (ix.name.as_str(), account.name.as_str()))
419 })
420 .collect();
421 assert_eq!(
422 signers,
423 [
424 ("Transfer", "owner"),
425 ("Approve", "owner"),
426 ("Revoke", "owner"),
427 ("SetAuthority", "authority"),
428 ("MintTo", "authority"),
429 ("Burn", "owner"),
430 ("CloseAccount", "owner"),
431 ("FreezeAccount", "authority"),
432 ("ThawAccount", "authority"),
433 ("TransferChecked", "owner"),
434 ("ApproveChecked", "owner"),
435 ("MintToChecked", "authority"),
436 ("BurnChecked", "owner"),
437 ("UnwrapLamports", "owner"),
438 ]
439 );
440 for ix in &spec.instructions {
441 for account in ix.accounts.iter().filter(|account| account.is_signer) {
442 assert_eq!(
443 account.resolution,
444 AccountResolutionV1::Signer,
445 "{}.{}",
446 ix.name,
447 account.name
448 );
449 }
450 }
451 }
452
453 #[test]
454 fn associated_token_create_idempotent_names_its_accounts() {
455 let spec = BuiltinProgram::AssociatedToken.program_spec();
456 let ix = spec
457 .instructions
458 .iter()
459 .find(|ix| ix.name == "createIdempotent")
460 .unwrap();
461 assert_eq!(ix.discriminator, vec![1]);
462 let names: Vec<_> = ix.accounts.iter().map(|a| a.name.as_str()).collect();
463 assert_eq!(
464 names,
465 [
466 "fundingAddress",
467 "associatedTokenAccountAddress",
468 "walletAddress",
469 "tokenMintAddress",
470 "systemProgram",
471 "tokenProgram"
472 ]
473 );
474 }
475
476 #[test]
477 fn compute_budget_instructions_use_one_byte_discriminators() {
478 let spec = BuiltinProgram::ComputeBudget.program_spec();
479 let found: Vec<_> = spec
480 .instructions
481 .iter()
482 .map(|ix| (ix.name.as_str(), ix.discriminator.clone()))
483 .collect();
484 assert_eq!(
485 found,
486 [
487 ("requestHeapFrame", vec![1]),
488 ("setComputeUnitLimit", vec![2]),
489 ("setComputeUnitPrice", vec![3]),
490 ("setLoadedAccountsDataSizeLimit", vec![4]),
491 ]
492 );
493 }
494}