1use std::{collections::HashSet, str::FromStr};
8
9use serde::{Deserialize, Serialize};
10use serde_with::{DisplayFromStr, serde_as};
11use solana_address::{Address, MAX_SEED_LEN, MAX_SEEDS};
12
13use crate::{Base64DecodeError, BinaryEncoding, EncodedBinary, SwapMode};
14
15pub const AMOUNT_WIDTH: usize = 8;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(try_from = "DirectFillTemplateWire", into = "DirectFillTemplateWire")]
26pub struct DirectFillTemplate {
27 pub label: String,
30 pub mode: SwapMode,
33 pub input_mint: Address,
34 pub output_mint: Address,
35 pub instruction: TemplateInstruction,
36 pub amount_offset: u32,
38 pub taker: TakerSlots,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct TemplateInstruction {
45 pub program_id: Address,
46 pub accounts: Vec<TemplateAccount>,
49 pub data: Vec<u8>,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct TemplateAccount {
54 pub address: Address,
55 pub writable: bool,
56 pub signer: bool,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub struct TakerSlots {
68 pub user: Vec<u8>,
70 #[serde(with = "slot_list")]
73 pub input_token_account: Vec<u8>,
74 #[serde(with = "slot_list")]
77 pub output_token_account: Vec<u8>,
78 #[serde(default, skip_serializing_if = "Vec::is_empty")]
80 pub derived: Vec<DerivedAccount>,
81}
82
83#[serde_as]
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub struct DerivedAccount {
88 #[serde_as(as = "DisplayFromStr")]
89 pub program: Address,
90 pub seeds: Vec<Seed>,
91 #[serde(with = "slot_list")]
92 pub slots: Vec<u8>,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97pub enum Seed {
98 Utf8(String),
99 User,
101}
102
103impl Seed {
104 pub fn bytes<'a>(&'a self, user: &'a Address) -> &'a [u8] {
105 match self {
106 Self::Utf8(text) => text.as_bytes(),
107 Self::User => user.as_ref(),
108 }
109 }
110
111 fn len(&self) -> usize {
112 match self {
113 Self::Utf8(text) => text.len(),
114 Self::User => size_of::<Address>(),
115 }
116 }
117}
118
119mod slot_list {
121 use serde::{Deserialize, Deserializer, Serialize, Serializer};
122
123 #[derive(Deserialize)]
124 #[serde(untagged)]
125 enum OneOrMany {
126 One(u8),
127 Many(Vec<u8>),
128 }
129
130 pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
131 Ok(match OneOrMany::deserialize(d)? {
132 OneOrMany::One(slot) => vec![slot],
133 OneOrMany::Many(slots) => slots,
134 })
135 }
136
137 pub(super) fn serialize<S: Serializer>(slots: &[u8], s: S) -> Result<S::Ok, S::Error> {
138 match slots {
139 [only] => only.serialize(s),
140 many => many.serialize(s),
141 }
142 }
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147struct DirectFillTemplateWire {
148 label: String,
149 mode: SwapMode,
150 input_mint: String,
151 output_mint: String,
152 instruction: TemplateInstructionWire,
153 amount_offset: u32,
154 taker: TakerSlots,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159struct TemplateInstructionWire {
160 program_id: String,
161 accounts: Vec<TemplateAccountWire>,
162 data: EncodedBinary,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167struct TemplateAccountWire {
168 address: String,
169 #[serde(default)]
170 writable: bool,
171 #[serde(default)]
172 signer: bool,
173}
174
175#[derive(Debug, thiserror::Error, PartialEq, Eq)]
177pub enum ParseDirectFillTemplateError {
178 #[error("invalid address {value} in template field {field}")]
179 Address { field: String, value: String },
180 #[error("a template's label must not be blank")]
181 BlankLabel,
182 #[error("a template must trade two different mints")]
183 SameMint,
184 #[error("a template instruction must hold 1..=255 accounts, got {count}")]
186 AccountCount { count: usize },
187 #[error("template instruction data is not valid base64: {0}")]
188 Data(#[from] Base64DecodeError),
189 #[error("amount offset {offset} leaves no room for an amount in {len} bytes of data")]
190 AmountOffset { offset: u32, len: usize },
191 #[error("a template must name at least one user slot")]
192 NoUserSlot,
193 #[error("a template must name at least one slot for each token account")]
194 NoTokenAccountSlot,
195 #[error("{role} slot {slot} is past the instruction's accounts")]
196 SlotOutOfRange { role: &'static str, slot: u8 },
197 #[error("slot {slot} is named by more than one taker role")]
198 DuplicateRoleSlot { slot: u8 },
199 #[error("user slot {slot} must be a signer")]
200 UserNotSigner { slot: u8 },
201 #[error("token account slot {slot} must be writable")]
202 TokenAccountReadOnly { slot: u8 },
203 #[error("a derived account must name 1..={MAX_SEEDS} seeds of at most {MAX_SEED_LEN} bytes")]
204 DerivedSeeds,
205 #[error("a derived account must name at least one slot")]
206 NoDerivedSlot,
207}
208
209impl TryFrom<DirectFillTemplateWire> for DirectFillTemplate {
210 type Error = ParseDirectFillTemplateError;
211
212 fn try_from(wire: DirectFillTemplateWire) -> Result<Self, Self::Error> {
213 let address = |field: String, value: &str| {
214 Address::from_str(value).map_err(|_| ParseDirectFillTemplateError::Address {
215 field,
216 value: value.to_owned(),
217 })
218 };
219 let accounts = wire
220 .instruction
221 .accounts
222 .iter()
223 .enumerate()
224 .map(|(index, account)| {
225 Ok(TemplateAccount {
226 address: address(
227 format!("instruction.accounts[{index}].address"),
228 &account.address,
229 )?,
230 writable: account.writable,
231 signer: account.signer,
232 })
233 })
234 .collect::<Result<Vec<_>, ParseDirectFillTemplateError>>()?;
235 let template = Self {
236 label: wire.label,
237 mode: wire.mode,
238 input_mint: address("inputMint".to_owned(), &wire.input_mint)?,
239 output_mint: address("outputMint".to_owned(), &wire.output_mint)?,
240 instruction: TemplateInstruction {
241 program_id: address(
242 "instruction.programId".to_owned(),
243 &wire.instruction.program_id,
244 )?,
245 accounts,
246 data: wire.instruction.data.decode()?,
247 },
248 amount_offset: wire.amount_offset,
249 taker: wire.taker,
250 };
251 template.validate()?;
252 Ok(template)
253 }
254}
255
256impl DirectFillTemplate {
257 pub fn role_slots(&self) -> impl Iterator<Item = u8> {
260 self.taker
261 .user
262 .iter()
263 .chain(&self.taker.input_token_account)
264 .chain(&self.taker.output_token_account)
265 .copied()
266 }
267
268 pub fn validate(&self) -> Result<(), ParseDirectFillTemplateError> {
271 use ParseDirectFillTemplateError as Error;
272
273 if self.label.trim().is_empty() {
274 return Err(Error::BlankLabel);
275 }
276 if self.input_mint == self.output_mint {
277 return Err(Error::SameMint);
278 }
279 let accounts = &self.instruction.accounts;
280 if !(1..=usize::from(u8::MAX)).contains(&accounts.len()) {
281 return Err(Error::AccountCount {
282 count: accounts.len(),
283 });
284 }
285 let len = self.instruction.data.len();
286 let end = usize::try_from(self.amount_offset)
287 .ok()
288 .and_then(|offset| offset.checked_add(AMOUNT_WIDTH));
289 if end.is_none_or(|end| end > len) {
290 return Err(Error::AmountOffset {
291 offset: self.amount_offset,
292 len,
293 });
294 }
295 if self.taker.user.is_empty() {
296 return Err(Error::NoUserSlot);
297 }
298 if self.taker.input_token_account.is_empty() || self.taker.output_token_account.is_empty() {
299 return Err(Error::NoTokenAccountSlot);
300 }
301 for derived in &self.taker.derived {
302 if !(1..=MAX_SEEDS).contains(&derived.seeds.len())
303 || derived.seeds.iter().any(|seed| seed.len() > MAX_SEED_LEN)
304 {
305 return Err(Error::DerivedSeeds);
306 }
307 if derived.slots.is_empty() {
308 return Err(Error::NoDerivedSlot);
309 }
310 }
311 let mut role_slots = HashSet::new();
312 fn labelled(role: &'static str, slots: &[u8]) -> impl Iterator<Item = (&'static str, u8)> {
313 slots.iter().map(move |&slot| (role, slot))
314 }
315 self.taker
316 .user
317 .iter()
318 .map(|&slot| ("user", slot))
319 .chain(labelled(
320 "inputTokenAccount",
321 &self.taker.input_token_account,
322 ))
323 .chain(labelled(
324 "outputTokenAccount",
325 &self.taker.output_token_account,
326 ))
327 .chain(
328 self.taker
329 .derived
330 .iter()
331 .flat_map(|derived| labelled("derived", &derived.slots)),
332 )
333 .try_for_each(|(role, slot)| {
334 if usize::from(slot) >= accounts.len() {
335 return Err(Error::SlotOutOfRange { role, slot });
336 }
337 role_slots
338 .insert(usize::from(slot))
339 .then_some(())
340 .ok_or(Error::DuplicateRoleSlot { slot })
341 })?;
342 let flags_of = |slot: u8| accounts[usize::from(slot)];
343 if let Some(&slot) = self.taker.user.iter().find(|&&slot| !flags_of(slot).signer) {
344 return Err(Error::UserNotSigner { slot });
345 }
346 if let Some(&slot) = self
347 .taker
348 .input_token_account
349 .iter()
350 .chain(&self.taker.output_token_account)
351 .find(|&&slot| !flags_of(slot).writable)
352 {
353 return Err(Error::TokenAccountReadOnly { slot });
354 }
355 Ok(())
356 }
357}
358
359impl From<DirectFillTemplate> for DirectFillTemplateWire {
360 fn from(template: DirectFillTemplate) -> Self {
361 Self {
362 label: template.label,
363 mode: template.mode,
364 input_mint: template.input_mint.to_string(),
365 output_mint: template.output_mint.to_string(),
366 instruction: TemplateInstructionWire {
367 program_id: template.instruction.program_id.to_string(),
368 accounts: template
369 .instruction
370 .accounts
371 .into_iter()
372 .map(|account| TemplateAccountWire {
373 address: account.address.to_string(),
374 writable: account.writable,
375 signer: account.signer,
376 })
377 .collect(),
378 data: EncodedBinary::from_bytes(&template.instruction.data, BinaryEncoding::Base64),
379 },
380 amount_offset: template.amount_offset,
381 taker: template.taker,
382 }
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use rstest::rstest;
389
390 use super::*;
391
392 fn address(byte: u8) -> Address {
393 Address::from([byte; 32])
394 }
395
396 fn tempest_template_json() -> serde_json::Value {
399 serde_json::json!({
400 "label": "tempest/sol-usdc/sol-to-usdc",
401 "mode": "ExactIn",
402 "inputMint": "So11111111111111111111111111111111111111112",
403 "outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
404 "instruction": {
405 "programId": "T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT",
406 "accounts": [
407 {"address": "11111111111111111111111111111111", "writable": true, "signer": true},
408 {"address": "11111111111111111111111111111111", "writable": true, "signer": true},
409 {"address": "D5YqVMoSxnqeZAKAUUE1Dm3bmjtdxQ5DCF356ozqN9cM"},
410 {"address": "11111111111111111111111111111111", "writable": true},
411 {"address": "11111111111111111111111111111111", "writable": true},
412 {"address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"},
413 {"address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"},
414 {"address": "11111111111111111111111111111111"},
415 {"address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"},
416 {"address": "T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT"},
417 {"address": "T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT"},
418 {"address": "T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT"},
419 {"address": "EW9diL91VgHY5i9qYScz53W3PihQPSnPoMtAKCo1Bs7J", "writable": true},
420 {"address": "3nnVbsCfN1mwUk2XSLCnjzY3bDDTdzpnKjvRmd8nESS2", "writable": true},
421 {"address": "So11111111111111111111111111111111111111112"},
422 {"address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"},
423 {"address": "D5YqVMoSxnqeZAKAUUE1Dm3bmjtdxQ5DCF356ozqN9cM", "writable": true},
424 {"address": "FQmFVQ7i8GCLqwE7EriUA4WQCvyCKxAeY4XUr55ZD6D7", "writable": true},
425 {"address": "EW9diL91VgHY5i9qYScz53W3PihQPSnPoMtAKCo1Bs7J", "writable": true},
426 {"address": "3nnVbsCfN1mwUk2XSLCnjzY3bDDTdzpnKjvRmd8nESS2", "writable": true},
427 {"address": "4kHHmecPiRNJtAQug2cHFChovM4wqdBesWQckrXBWinD", "writable": true},
428 {"address": "6vNWbfi3HwDtoANb13CszPwEdAiyaUyZQ4A5A3hw4pqt", "writable": true},
429 {"address": "So11111111111111111111111111111111111111112"},
430 {"address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"},
431 {"address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"},
432 {"address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"},
433 {"address": "FUQcTWJjXtRxk6XHQBS6gqZ6VCgYeQkGs1H7inC2YKVe"},
434 {"address": "Sysvar1nstructions1111111111111111111111111"},
435 {"address": "2G71FYychcDbuNvMmVcFzU8J9N3giFvgp3a5PS2LmgZm"}
436 ],
437 "data": {"data": "KgAAAAAAAAAAAAAAAAAAAAAAMgACAAAAAQAAAFsAAAEAypo7DQ==", "encoding": "base64"}
438 },
439 "amountOffset": 2,
440 "taker": {"user": [0, 1], "inputTokenAccount": 3, "outputTokenAccount": 4}
441 })
442 }
443
444 #[test]
445 fn a_direct_fill_template_round_trips_through_its_wire_shape() {
446 let json = tempest_template_json();
447
448 let template: DirectFillTemplate = serde_json::from_value(json.clone()).expect("decode");
449
450 assert_eq!(template.label, "tempest/sol-usdc/sol-to-usdc");
451 assert_eq!(template.mode, SwapMode::ExactIn);
452 assert_eq!(
453 template.input_mint.to_string(),
454 "So11111111111111111111111111111111111111112"
455 );
456 assert_eq!(
457 template.output_mint.to_string(),
458 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
459 );
460 assert_eq!(
461 template.instruction.program_id.to_string(),
462 "T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT"
463 );
464 assert_eq!(template.instruction.accounts.len(), 29);
465 assert_eq!(
466 template.instruction.accounts[0],
467 TemplateAccount {
468 address: Address::default(),
469 writable: true,
470 signer: true,
471 }
472 );
473 assert_eq!(
474 template.instruction.accounts[2],
475 TemplateAccount {
476 address: "D5YqVMoSxnqeZAKAUUE1Dm3bmjtdxQ5DCF356ozqN9cM"
477 .parse()
478 .unwrap(),
479 writable: false,
480 signer: false,
481 }
482 );
483 assert_eq!(template.instruction.data.len(), 37);
484 assert_eq!(template.instruction.data[0], 42);
485 assert_eq!(template.instruction.data[2..10], [0; 8]);
486 assert_eq!(template.instruction.data[28], 91);
487 assert_eq!(template.amount_offset, 2);
488 assert_eq!(
489 template.taker,
490 TakerSlots {
491 user: vec![0, 1],
492 input_token_account: vec![3],
493 output_token_account: vec![4],
494 derived: Vec::new(),
495 }
496 );
497
498 let encoded = serde_json::to_value(&template).expect("encode");
499 assert_eq!(encoded["mode"], "ExactIn");
500 assert_eq!(encoded["amountOffset"], 2);
501 assert_eq!(encoded["instruction"]["data"], json["instruction"]["data"]);
502 assert_eq!(encoded["instruction"]["accounts"][2]["signer"], false);
503 assert_eq!(encoded["taker"]["inputTokenAccount"], 3);
504 assert_eq!(
505 serde_json::from_value::<DirectFillTemplate>(encoded).expect("decode again"),
506 template
507 );
508 }
509
510 fn tempest_wire() -> DirectFillTemplateWire {
511 serde_json::from_value(tempest_template_json()).expect("wire shape")
512 }
513
514 fn derived(seeds: Vec<Seed>, slots: Vec<u8>) -> DerivedAccount {
515 DerivedAccount {
516 program: address(7),
517 seeds,
518 slots,
519 }
520 }
521
522 #[test]
523 fn a_derived_account_reads_its_seeds_from_the_wire() {
524 let mut json = tempest_template_json();
525 json["taker"]["derived"] = serde_json::json!([{
526 "program": "3TK9D8aoBFYjYZtKCjciPrVrRStsnvo7KmpcJqDavpaU",
527 "seeds": [{"utf8": "ban"}, "user"],
528 "slots": [26, 28],
529 }]);
530
531 let template: DirectFillTemplate = serde_json::from_value(json.clone()).expect("parses");
532
533 assert_eq!(
534 template.taker.derived,
535 vec![DerivedAccount {
536 program: Address::from_str("3TK9D8aoBFYjYZtKCjciPrVrRStsnvo7KmpcJqDavpaU").unwrap(),
537 seeds: vec![Seed::Utf8("ban".to_owned()), Seed::User],
538 slots: vec![26, 28],
539 }]
540 );
541 assert_eq!(
542 serde_json::to_value(&template).unwrap()["taker"]["derived"],
543 json["taker"]["derived"]
544 );
545 }
546
547 #[rstest]
549 #[case::address(
550 |wire: &mut DirectFillTemplateWire| wire.instruction.program_id = "not-base58".to_owned(),
551 ParseDirectFillTemplateError::Address {
552 field: "instruction.programId".to_owned(),
553 value: "not-base58".to_owned(),
554 }
555 )]
556 #[case::blank_label(
557 |wire: &mut DirectFillTemplateWire| wire.label = " ".to_owned(),
558 ParseDirectFillTemplateError::BlankLabel
559 )]
560 #[case::same_mint(
561 |wire: &mut DirectFillTemplateWire| wire.output_mint = wire.input_mint.clone(),
562 ParseDirectFillTemplateError::SameMint
563 )]
564 #[case::no_accounts(
565 |wire: &mut DirectFillTemplateWire| wire.instruction.accounts.clear(),
566 ParseDirectFillTemplateError::AccountCount { count: 0 }
567 )]
568 #[case::too_many_accounts(
569 |wire: &mut DirectFillTemplateWire| {
570 let filler = wire.instruction.accounts[2].clone();
571 wire.instruction.accounts.resize(256, filler);
572 },
573 ParseDirectFillTemplateError::AccountCount { count: 256 }
574 )]
575 #[case::data(
576 |wire: &mut DirectFillTemplateWire| wire.instruction.data.data = "!!!".to_owned(),
577 ParseDirectFillTemplateError::Data(Base64DecodeError::InvalidByte(0, b'!'))
578 )]
579 #[case::amount_offset(
580 |wire: &mut DirectFillTemplateWire| wire.amount_offset = 30,
581 ParseDirectFillTemplateError::AmountOffset { offset: 30, len: 37 }
582 )]
583 #[case::no_user_slot(
584 |wire: &mut DirectFillTemplateWire| wire.taker.user.clear(),
585 ParseDirectFillTemplateError::NoUserSlot
586 )]
587 #[case::slot_out_of_range(
588 |wire: &mut DirectFillTemplateWire| wire.taker.output_token_account = vec![29],
589 ParseDirectFillTemplateError::SlotOutOfRange { role: "outputTokenAccount", slot: 29 }
590 )]
591 #[case::duplicate_role_slot(
592 |wire: &mut DirectFillTemplateWire| wire.taker.input_token_account = vec![1],
593 ParseDirectFillTemplateError::DuplicateRoleSlot { slot: 1 }
594 )]
595 #[case::user_not_signer(
596 |wire: &mut DirectFillTemplateWire| wire.instruction.accounts[1].signer = false,
597 ParseDirectFillTemplateError::UserNotSigner { slot: 1 }
598 )]
599 #[case::token_account_read_only(
600 |wire: &mut DirectFillTemplateWire| wire.instruction.accounts[4].writable = false,
601 ParseDirectFillTemplateError::TokenAccountReadOnly { slot: 4 }
602 )]
603 #[case::no_derived_seed(
604 |wire: &mut DirectFillTemplateWire| wire.taker.derived = vec![derived(vec![], vec![26])],
605 ParseDirectFillTemplateError::DerivedSeeds
606 )]
607 #[case::derived_seed_too_long(
608 |wire: &mut DirectFillTemplateWire| {
609 wire.taker.derived = vec![derived(vec![Seed::Utf8("x".repeat(33))], vec![26])];
610 },
611 ParseDirectFillTemplateError::DerivedSeeds
612 )]
613 #[case::no_derived_slot(
614 |wire: &mut DirectFillTemplateWire| wire.taker.derived = vec![derived(vec![Seed::User], vec![])],
615 ParseDirectFillTemplateError::NoDerivedSlot
616 )]
617 #[case::derived_slot_out_of_range(
618 |wire: &mut DirectFillTemplateWire| wire.taker.derived = vec![derived(vec![Seed::User], vec![29])],
619 ParseDirectFillTemplateError::SlotOutOfRange { role: "derived", slot: 29 }
620 )]
621 #[case::derived_slot_names_a_role(
622 |wire: &mut DirectFillTemplateWire| wire.taker.derived = vec![derived(vec![Seed::User], vec![3])],
623 ParseDirectFillTemplateError::DuplicateRoleSlot { slot: 3 }
624 )]
625 fn a_malformed_template_is_refused_at_parse(
626 #[case] corrupt: fn(&mut DirectFillTemplateWire),
627 #[case] expected: ParseDirectFillTemplateError,
628 ) {
629 let mut wire = tempest_wire();
630 corrupt(&mut wire);
631
632 assert_eq!(DirectFillTemplate::try_from(wire), Err(expected));
633 }
634
635 #[test]
636 fn a_255_account_template_is_admitted() {
637 let mut wire = tempest_wire();
638 wire.instruction.accounts = (0..u8::MAX)
639 .map(|byte| TemplateAccountWire {
640 address: address(byte).to_string(),
641 writable: true,
642 signer: true,
643 })
644 .collect();
645 wire.taker.user = vec![0];
646
647 let template = DirectFillTemplate::try_from(wire).expect("at the bound");
648
649 assert_eq!(template.role_slots().collect::<Vec<_>>(), vec![0, 3, 4]);
650 }
651}