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