Skip to main content

simulator_api/
direct_fill.rs

1//! Caller-supplied direct-fill templates: one instruction the server rewrites per probe.
2//!
3//! Split from the crate root because the wire shape, its parse errors and its round-trip
4//! tests are one subject, and `lib.rs` is long enough that adding a ninth unrelated block
5//! to it makes every one of them harder to find.
6
7use 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
15/// The width of the little-endian amount a template's `amountOffset` addresses.
16pub const AMOUNT_WIDTH: usize = 8;
17
18/// One instruction the caller serialized, with the slots the server rewrites per probe and the
19/// offset it writes the amount at. Matches hops of `mode` originals trading `inputMint` →
20/// `outputMint` and is priced against each such hop's realized fill.
21///
22/// Every floor, fee, direction and venue payload is the caller's: the server writes the amount,
23/// rewrites the taker, funds, simulates and reads the fill.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(try_from = "DirectFillTemplateWire", into = "DirectFillTemplateWire")]
26pub struct DirectFillTemplate {
27    /// The template's identity on rows, events, notifications, report groups and stats keys.
28    /// Unique per session, non-blank after trim.
29    pub label: String,
30    /// Which originals match, and whether the hop's input (`ExactIn`) or output (`ExactOut`)
31    /// amount is written.
32    pub mode: SwapMode,
33    pub input_mint: Address,
34    pub output_mint: Address,
35    pub instruction: TemplateInstruction,
36    /// Offset of the [`AMOUNT_WIDTH`]-byte little-endian amount in `instruction.data`.
37    pub amount_offset: u32,
38    pub taker: TakerSlots,
39}
40
41/// The template's single instruction, sent verbatim except the role slots' addresses and the
42/// amount bytes.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct TemplateInstruction {
45    pub program_id: Address,
46    /// Metas in program order. Flags are never touched by the server; duplicate addresses are
47    /// legal; a role slot's address is a placeholder (by convention the zero address).
48    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/// The account slots a probe rewrites, as indices into `instruction.accounts`.
60///
61/// Each role is a list because one instruction can name the same account more than once: a
62/// router's own swap instruction carries the taker's token accounts for itself and again inside
63/// the venue's account run, and rewriting only the first leaves the venue pointed at the
64/// exemplar's accounts. A bare index decodes as a one-element list.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub struct TakerSlots {
68    /// Every slot rewritten to the original's fee payer. Non-empty; each must be a signer.
69    pub user: Vec<u8>,
70    /// Rewritten to the fee payer's associated account for `inputMint` and seeded with the
71    /// trade. Non-empty; each must be writable.
72    #[serde(with = "slot_list")]
73    pub input_token_account: Vec<u8>,
74    /// Rewritten to the fee payer's associated account for `outputMint` and seeded to zero, so
75    /// what lands there is the fill. Non-empty; each must be writable.
76    #[serde(with = "slot_list")]
77    pub output_token_account: Vec<u8>,
78    /// Accounts a venue derives from the taker, re-derived for each probe's taker.
79    #[serde(default, skip_serializing_if = "Vec::is_empty")]
80    pub derived: Vec<DerivedAccount>,
81}
82
83/// A program address derived from `seeds` under `program`, written into every one of `slots`.
84#[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    /// The probe's fee payer, the address `user` slots are rewritten to.
100    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
119/// One slot or a list of them on the wire, always a list in memory.
120mod 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/// Why a supplied template could not be used.
176#[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    /// The per-instruction runtime ceiling; an empty list names no slot to rewrite.
185    #[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    /// The account slots a probe rewrites: every user slot, then the input and output token
258    /// accounts.
259    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    /// The rules `TryFrom<DirectFillTemplateWire>` enforces, for a template built in code:
269    /// [`simulator_trading::template::instantiate`] indexes on them without checking.
270    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    /// A captured Tempest SOL → USDC template: 16 Titan `swap_route_v3` prefix slots, the
397    /// 13-account Tempest run, 37 data bytes.
398    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    /// Each case corrupts exactly one thing about the Tempest template, and the parse names it.
548    #[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}