1use std::collections::{HashMap, HashSet};
25
26use async_trait::async_trait;
27use base64::Engine as _;
28
29use crate::{
30 opaque_market_id, opaque_order_id, ExecutionVerificationContext, ExecutionVerifier,
31 MakerVerificationContext, OrderVerificationContext, OrderVerifier, PlatformMakerControlAction,
32 PlatformMakerCurrentPrepareRequest, PlatformMakerQuickstartOperation,
33 PlatformMakerStrandPrepareRequest, PlatformOrderBatchOperation, PlatformOrderChallengeRequest,
34 PlatformOrderType, PlatformTradeSide, TwapVerificationContext, TwapVerifier,
35};
36
37const WELL_KNOWN_PROGRAMS: [&str; 10] = [
39 "11111111111111111111111111111111", "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", "ComputeBudget111111111111111111111111111111", "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr", "Stake11111111111111111111111111111111111111", "Vote111111111111111111111111111111111111111", "AddressLookupTab1e1111111111111111111111111", "BPFLoaderUpgradeab1e11111111111111111111111", ];
50
51const DELEGATED_ENVELOPE_TAG: u8 = 3;
53const INNER_TAG_BALANCE: u8 = 1;
55const INNER_TAG_CANCEL_ORDER: u8 = 4;
56const INNER_TAG_PLACE_ORDER: u8 = 33;
57const INNER_TAG_MARKET_ACCOUNT: u8 = 34;
58const INNER_TAG_TWAP_CANCEL: u8 = 36;
59const INNER_TAG_TWAP_POST: u8 = 38;
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum TransactionVersion {
64 Legacy,
65 V0,
66}
67
68#[derive(Clone, Debug, Eq, PartialEq)]
70pub struct DecodedInstruction {
71 pub program_id_index: u8,
72 pub account_indexes: Vec<u8>,
73 pub data: Vec<u8>,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct DecodedTransaction {
80 pub version: TransactionVersion,
81 pub signature_count: usize,
82 pub num_required_signatures: u8,
83 pub num_readonly_signed: u8,
84 pub num_readonly_unsigned: u8,
85 pub static_account_keys: Vec<String>,
87 pub recent_blockhash: String,
88 pub instructions: Vec<DecodedInstruction>,
89 pub address_table_lookup_count: usize,
90}
91
92struct Reader<'a> {
93 bytes: &'a [u8],
94 offset: usize,
95}
96
97impl<'a> Reader<'a> {
98 fn new(bytes: &'a [u8]) -> Self {
99 Self { bytes, offset: 0 }
100 }
101
102 fn u8(&mut self) -> Result<u8, String> {
103 let byte = *self
104 .bytes
105 .get(self.offset)
106 .ok_or_else(|| "transaction is truncated".to_owned())?;
107 self.offset += 1;
108 Ok(byte)
109 }
110
111 fn compact_u16(&mut self) -> Result<usize, String> {
112 let mut value = 0usize;
113 let mut shift = 0u32;
114 for _ in 0..3 {
115 let byte = self.u8()?;
116 value |= usize::from(byte & 0x7f) << shift;
117 if byte & 0x80 == 0 {
118 return Ok(value);
119 }
120 shift += 7;
121 }
122 Err("transaction length prefix is invalid".to_owned())
123 }
124
125 fn take(&mut self, length: usize) -> Result<&'a [u8], String> {
126 let end = self
127 .offset
128 .checked_add(length)
129 .filter(|end| *end <= self.bytes.len())
130 .ok_or_else(|| "transaction is truncated".to_owned())?;
131 let out = &self.bytes[self.offset..end];
132 self.offset = end;
133 Ok(out)
134 }
135
136 fn done(&self) -> bool {
137 self.offset == self.bytes.len()
138 }
139}
140
141pub fn decode_transaction(transaction_base64: &str) -> Result<DecodedTransaction, String> {
143 let bytes = base64::engine::general_purpose::STANDARD
144 .decode(transaction_base64.trim())
145 .map_err(|_| "invalid base64 payload".to_owned())?;
146 let mut reader = Reader::new(&bytes);
147 let signature_count = reader.compact_u16()?;
148 reader.take(signature_count.saturating_mul(64))?;
149 let mut first = reader.u8()?;
150 let mut version = TransactionVersion::Legacy;
151 if first & 0x80 != 0 {
152 if first & 0x7f != 0 {
153 return Err("unsupported transaction version".to_owned());
154 }
155 version = TransactionVersion::V0;
156 first = reader.u8()?;
157 }
158 let num_required_signatures = first;
159 let num_readonly_signed = reader.u8()?;
160 let num_readonly_unsigned = reader.u8()?;
161 let key_count = reader.compact_u16()?;
162 let mut static_account_keys = Vec::with_capacity(key_count);
163 for _ in 0..key_count {
164 static_account_keys.push(bs58::encode(reader.take(32)?).into_string());
165 }
166 let recent_blockhash = bs58::encode(reader.take(32)?).into_string();
167 let instruction_count = reader.compact_u16()?;
168 let mut instructions = Vec::with_capacity(instruction_count);
169 for _ in 0..instruction_count {
170 let program_id_index = reader.u8()?;
171 let account_count = reader.compact_u16()?;
172 let account_indexes = reader.take(account_count)?.to_vec();
173 let data_length = reader.compact_u16()?;
174 let data = reader.take(data_length)?.to_vec();
175 instructions.push(DecodedInstruction {
176 program_id_index,
177 account_indexes,
178 data,
179 });
180 }
181 let mut address_table_lookup_count = 0;
182 if version == TransactionVersion::V0 {
183 address_table_lookup_count = reader.compact_u16()?;
184 for _ in 0..address_table_lookup_count {
185 reader.take(32)?;
186 let writable = reader.compact_u16()?;
187 reader.take(writable)?;
188 let readonly = reader.compact_u16()?;
189 reader.take(readonly)?;
190 }
191 }
192 if !reader.done() {
193 return Err("transaction carries trailing bytes".to_owned());
194 }
195 if signature_count != usize::from(num_required_signatures) || num_required_signatures == 0 {
196 return Err("transaction signature layout is invalid".to_owned());
197 }
198 if static_account_keys.len() < usize::from(num_required_signatures) {
199 return Err("transaction signer layout is invalid".to_owned());
200 }
201 Ok(DecodedTransaction {
202 version,
203 signature_count,
204 num_required_signatures,
205 num_readonly_signed,
206 num_readonly_unsigned,
207 static_account_keys,
208 recent_blockhash,
209 instructions,
210 address_table_lookup_count,
211 })
212}
213
214pub fn verify_signed_transaction_message(
217 prepared_transaction_base64: &str,
218 signed_transaction_base64: &str,
219) -> Result<(), String> {
220 let prepared = transaction_message_bytes(prepared_transaction_base64)?;
221 let signed = transaction_message_bytes(signed_transaction_base64)?;
222 if prepared != signed {
223 return Err("signed transaction message changed after verification".to_owned());
224 }
225 Ok(())
226}
227
228fn transaction_message_bytes(transaction_base64: &str) -> Result<Vec<u8>, String> {
229 let bytes = base64::engine::general_purpose::STANDARD
230 .decode(transaction_base64.trim())
231 .map_err(|_| "invalid base64 payload".to_owned())?;
232 let mut reader = Reader::new(&bytes);
233 let signature_count = reader.compact_u16()?;
234 reader.take(signature_count.saturating_mul(64))?;
235 if reader.offset >= bytes.len() {
236 return Err("transaction is truncated".to_owned());
237 }
238 Ok(bytes[reader.offset..].to_vec())
239}
240
241pub fn verify_maker_transaction(context: &MakerVerificationContext<'_>) -> Result<(), String> {
247 let tx = decode_transaction(&context.prepared.transaction_base64)?;
248 if tx.version != TransactionVersion::V0 || tx.address_table_lookup_count != 0 {
249 return Err("maker controls must be native v0 without lookup tables".to_owned());
250 }
251 if tx.num_required_signatures != 1
252 || tx.signature_count != 1
253 || tx.static_account_keys.first().map(String::as_str) != Some(context.maker_wallet)
254 || tx.num_readonly_signed != 0
255 {
256 return Err("maker control must require only the maker wallet".to_owned());
257 }
258 if tx.recent_blockhash != context.prepared.recent_blockhash {
259 return Err("prepared maker blockhash does not match".to_owned());
260 }
261 if tx.instructions.len() != 1 {
262 return Err("maker control must contain exactly one instruction".to_owned());
263 }
264 let instruction = &tx.instructions[0];
265 if instruction.account_indexes.first() != Some(&0) {
266 return Err("maker wallet is not the instruction signer".to_owned());
267 }
268 let program = tx
269 .static_account_keys
270 .get(usize::from(instruction.program_id_index))
271 .ok_or_else(|| "maker instruction program is not static".to_owned())?;
272 if WELL_KNOWN_PROGRAMS.contains(&program.as_str()) {
273 return Err("maker control targets an invalid program".to_owned());
274 }
275 let expected_tag = match context.prepared.action {
276 PlatformMakerControlAction::StrandUpsert => 41,
277 PlatformMakerControlAction::StrandRecenter => 42,
278 PlatformMakerControlAction::StrandCancel => 43,
279 PlatformMakerControlAction::StrandSetEnabled => 44,
280 PlatformMakerControlAction::CurrentUpsert => 47,
281 PlatformMakerControlAction::CurrentCancel => 48,
282 };
283 let data = &instruction.data;
284 if data.first() != Some(&expected_tag) {
285 return Err("maker instruction action changed".to_owned());
286 }
287 match (&context.prepared.action, context.operation) {
288 (
289 PlatformMakerControlAction::StrandUpsert,
290 PlatformMakerQuickstartOperation::Strand(PlatformMakerStrandPrepareRequest::Upsert {
291 enabled,
292 async_only,
293 sync_spread_ticks,
294 mid_price_atoms,
295 max_exposure_base_atoms,
296 bid_offsets_ticks,
297 ask_offsets_ticks,
298 bid_sizes_base_atoms,
299 ask_sizes_base_atoms,
300 valid_until_slot,
301 ..
302 }),
303 ) => {
304 if data.len() != 353 {
305 return Err("Strand upsert has an invalid length".to_owned());
306 }
307 verify_maker_market(&tx, instruction, context.market_id)?;
308 expect_u8(
309 data,
310 1,
311 u8::from(*enabled) | (u8::from(*async_only) << 1),
312 "Strand flags",
313 )?;
314 expect_u16(data, 3, *sync_spread_ticks, "Strand sync spread")?;
315 expect_u64(
316 data,
317 9,
318 atoms(mid_price_atoms, "mid_price_atoms")?,
319 "Strand mid price",
320 )?;
321 expect_u64(
322 data,
323 17,
324 atoms(max_exposure_base_atoms, "max_exposure_base_atoms")?,
325 "Strand exposure",
326 )?;
327 for (index, value) in bid_offsets_ticks.iter().enumerate() {
328 expect_u16(data, 25 + index * 2, *value, "Strand bid offset")?;
329 }
330 for (index, value) in ask_offsets_ticks.iter().enumerate() {
331 expect_u16(data, 57 + index * 2, *value, "Strand ask offset")?;
332 }
333 for (index, value) in bid_sizes_base_atoms.iter().enumerate() {
334 expect_u64(
335 data,
336 89 + index * 8,
337 atoms(value, "bid size")?,
338 "Strand bid size",
339 )?;
340 }
341 for (index, value) in ask_sizes_base_atoms.iter().enumerate() {
342 expect_u64(
343 data,
344 217 + index * 8,
345 atoms(value, "ask size")?,
346 "Strand ask size",
347 )?;
348 }
349 expect_u64(
350 data,
351 345,
352 atoms(valid_until_slot, "valid_until_slot")?,
353 "Strand expiry",
354 )
355 }
356 (
357 PlatformMakerControlAction::StrandRecenter,
358 PlatformMakerQuickstartOperation::Strand(PlatformMakerStrandPrepareRequest::Recenter {
359 new_mid_price_atoms,
360 valid_until_slot,
361 ..
362 }),
363 ) => {
364 if data.len() != 17 {
365 return Err("Strand recenter has an invalid length".to_owned());
366 }
367 expect_u64(
368 data,
369 1,
370 atoms(new_mid_price_atoms, "new_mid_price_atoms")?,
371 "Strand mid price",
372 )?;
373 expect_u64(
374 data,
375 9,
376 atoms(valid_until_slot, "valid_until_slot")?,
377 "Strand expiry",
378 )
379 }
380 (
381 PlatformMakerControlAction::StrandSetEnabled,
382 PlatformMakerQuickstartOperation::Strand(
383 PlatformMakerStrandPrepareRequest::SetEnabled { enabled, .. },
384 ),
385 ) => {
386 if data.len() != 2 {
387 return Err("Strand enable has an invalid length".to_owned());
388 }
389 expect_u8(data, 1, u8::from(*enabled), "Strand enabled state")
390 }
391 (
392 PlatformMakerControlAction::CurrentUpsert,
393 PlatformMakerQuickstartOperation::Current(PlatformMakerCurrentPrepareRequest::Upsert {
394 enabled,
395 async_only,
396 half_spread_bps,
397 band_step_bps,
398 max_conf_bps,
399 max_oracle_dev_bps,
400 max_oracle_age_secs,
401 sync_spread_bps,
402 max_exposure_base_atoms,
403 bid_depth_base_atoms,
404 ask_depth_base_atoms,
405 valid_until_slot,
406 ..
407 }),
408 ) => {
409 if data.len() != 161 {
410 return Err("Current upsert has an invalid length".to_owned());
411 }
412 verify_maker_market(&tx, instruction, context.market_id)?;
413 expect_u8(
414 data,
415 1,
416 u8::from(*enabled) | (u8::from(*async_only) << 1),
417 "Current flags",
418 )?;
419 expect_u16(data, 3, *half_spread_bps, "Current spread")?;
420 expect_u16(data, 5, *band_step_bps, "Current band step")?;
421 expect_u16(data, 7, *max_conf_bps, "Current confidence bound")?;
422 expect_u16(data, 9, *max_oracle_dev_bps, "Current deviation bound")?;
423 expect_u32(data, 11, *max_oracle_age_secs, "Current mark age")?;
424 expect_u16(data, 15, *sync_spread_bps, "Current sync spread")?;
425 expect_u64(
426 data,
427 17,
428 atoms(max_exposure_base_atoms, "max_exposure_base_atoms")?,
429 "Current exposure",
430 )?;
431 for (index, value) in bid_depth_base_atoms.iter().enumerate() {
432 expect_u64(
433 data,
434 25 + index * 8,
435 atoms(value, "bid depth")?,
436 "Current bid depth",
437 )?;
438 }
439 for (index, value) in ask_depth_base_atoms.iter().enumerate() {
440 expect_u64(
441 data,
442 89 + index * 8,
443 atoms(value, "ask depth")?,
444 "Current ask depth",
445 )?;
446 }
447 expect_u64(
448 data,
449 153,
450 atoms(valid_until_slot, "valid_until_slot")?,
451 "Current expiry",
452 )
453 }
454 (
455 PlatformMakerControlAction::StrandCancel,
456 PlatformMakerQuickstartOperation::Strand(PlatformMakerStrandPrepareRequest::Cancel {
457 ..
458 }),
459 )
460 | (
461 PlatformMakerControlAction::CurrentCancel,
462 PlatformMakerQuickstartOperation::Current(PlatformMakerCurrentPrepareRequest::Cancel {
463 ..
464 }),
465 ) => {
466 if data.len() != 1 || instruction.account_indexes.len() != 3 {
467 return Err("maker cancellation has an invalid shape".to_owned());
468 }
469 let receiver = instruction
470 .account_indexes
471 .get(2)
472 .and_then(|index| tx.static_account_keys.get(usize::from(*index)))
473 .map(String::as_str);
474 if receiver != Some(context.maker_wallet) {
475 return Err("maker rent receiver changed".to_owned());
476 }
477 Ok(())
478 }
479 _ => Err("prepared maker action does not match the requested operation".to_owned()),
480 }
481}
482
483fn verify_maker_market(
484 tx: &DecodedTransaction,
485 instruction: &DecodedInstruction,
486 expected_market_id: &str,
487) -> Result<(), String> {
488 let market = instruction
489 .account_indexes
490 .get(1)
491 .and_then(|index| tx.static_account_keys.get(usize::from(*index)))
492 .ok_or_else(|| "maker market account is not static".to_owned())?;
493 if opaque_market_id(market) != expected_market_id {
494 return Err("maker transaction touches another market".to_owned());
495 }
496 Ok(())
497}
498
499fn expect_u8(data: &[u8], offset: usize, expected: u8, field: &str) -> Result<(), String> {
500 if data.get(offset) == Some(&expected) {
501 Ok(())
502 } else {
503 Err(format!("{field} changed"))
504 }
505}
506
507fn expect_u16(data: &[u8], offset: usize, expected: u16, field: &str) -> Result<(), String> {
508 let bytes: [u8; 2] = data
509 .get(offset..offset + 2)
510 .and_then(|slice| slice.try_into().ok())
511 .ok_or_else(|| format!("{field} is missing"))?;
512 if u16::from_le_bytes(bytes) == expected {
513 Ok(())
514 } else {
515 Err(format!("{field} changed"))
516 }
517}
518
519fn expect_u32(data: &[u8], offset: usize, expected: u32, field: &str) -> Result<(), String> {
520 let bytes: [u8; 4] = data
521 .get(offset..offset + 4)
522 .and_then(|slice| slice.try_into().ok())
523 .ok_or_else(|| format!("{field} is missing"))?;
524 if u32::from_le_bytes(bytes) == expected {
525 Ok(())
526 } else {
527 Err(format!("{field} changed"))
528 }
529}
530
531fn expect_u64(data: &[u8], offset: usize, expected: u64, field: &str) -> Result<(), String> {
532 let actual = read_u64(data, offset).map_err(|_| format!("{field} is missing"))?;
533 if actual == expected {
534 Ok(())
535 } else {
536 Err(format!("{field} changed"))
537 }
538}
539
540struct DelegatedInstruction {
541 inner_tag: u8,
542 inner: Vec<u8>,
543 inner_accounts: Vec<Option<String>>,
546}
547
548struct StructuralOptions {
549 allow_address_tables: bool,
550 require_envelope: bool,
551}
552
553fn structural_checks(
556 tx: &DecodedTransaction,
557 session_public_key: &str,
558 owner_wallet: &str,
559 recent_blockhash: &str,
560 options: StructuralOptions,
561) -> Result<Vec<DelegatedInstruction>, String> {
562 if tx.recent_blockhash != recent_blockhash {
563 return Err("prepared transaction blockhash does not match".to_owned());
564 }
565 let keys = &tx.static_account_keys;
566 let required = usize::from(tx.num_required_signatures);
567 let session_index = keys
568 .iter()
569 .position(|key| key == session_public_key)
570 .filter(|index| *index < required)
571 .ok_or_else(|| "the session key is not a required signer".to_owned())?;
572 if session_index == 0 {
573 return Err("the session key must never be the fee payer".to_owned());
574 }
575 if let Some(owner_index) = keys.iter().position(|key| key == owner_wallet) {
576 if owner_index < required {
577 return Err("the owner wallet must not be asked to sign".to_owned());
578 }
579 }
580 if !options.allow_address_tables && tx.address_table_lookup_count != 0 {
581 return Err("order-control transactions carry no lookup tables".to_owned());
582 }
583 let session_index_u8 = u8::try_from(session_index)
584 .map_err(|_| "transaction signer layout is invalid".to_owned())?;
585 let mut envelope_program: Option<&str> = None;
586 let mut inner_program: Option<&str> = None;
587 let mut delegated = Vec::new();
588 for instruction in &tx.instructions {
589 if !instruction.account_indexes.contains(&session_index_u8) {
590 continue;
591 }
592 let program = keys
593 .get(usize::from(instruction.program_id_index))
594 .ok_or_else(|| "instruction program is not static".to_owned())?;
595 if WELL_KNOWN_PROGRAMS.contains(&program.as_str()) {
596 return Err("the session key must not sign a system or token instruction".to_owned());
597 }
598 match envelope_program {
599 None => envelope_program = Some(program),
600 Some(existing) if existing != program => {
601 return Err("the session key signs for more than one program".to_owned());
602 }
603 Some(_) => {}
604 }
605 if instruction.account_indexes.first() != Some(&session_index_u8) {
606 return Err("the session key is not the delegate signer".to_owned());
607 }
608 if !options.require_envelope {
609 continue;
610 }
611 let data = &instruction.data;
612 if data.len() < 14 || data[0] != DELEGATED_ENVELOPE_TAG {
613 return Err("the session key signs a non-delegated instruction".to_owned());
614 }
615 let inner_length = usize::from(data[11]) | (usize::from(data[12]) << 8);
616 let inner_end = 14 + inner_length;
617 if inner_length == 0 || inner_end > data.len() {
618 return Err("delegated instruction is malformed".to_owned());
619 }
620 let inner = data[14..inner_end].to_vec();
621 let inner_program_key = instruction
622 .account_indexes
623 .get(3)
624 .and_then(|index| keys.get(usize::from(*index)))
625 .ok_or_else(|| "delegated instruction target is not static".to_owned())?;
626 match inner_program {
627 None => inner_program = Some(inner_program_key),
628 Some(existing) if existing != inner_program_key => {
629 return Err("delegated instructions target more than one program".to_owned());
630 }
631 Some(_) => {}
632 }
633 delegated.push(DelegatedInstruction {
634 inner_tag: inner[0],
635 inner,
636 inner_accounts: instruction
637 .account_indexes
638 .iter()
639 .skip(6)
640 .map(|index| keys.get(usize::from(*index)).cloned())
641 .collect(),
642 });
643 }
644 if delegated.is_empty() && options.require_envelope {
645 return Err("the transaction carries no delegated instruction".to_owned());
646 }
647 Ok(delegated)
648}
649
650fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, String> {
651 let slice = bytes
652 .get(offset..offset + 8)
653 .ok_or_else(|| "delegated instruction is truncated".to_owned())?;
654 let mut array = [0u8; 8];
655 array.copy_from_slice(slice);
656 Ok(u64::from_le_bytes(array))
657}
658
659const fn side_wire(side: PlatformTradeSide) -> u8 {
660 match side {
661 PlatformTradeSide::Buy => 0,
662 PlatformTradeSide::Sell => 1,
663 }
664}
665
666fn order_type_wire(order_type: PlatformOrderType) -> Result<u8, String> {
667 match order_type {
668 PlatformOrderType::GoodUntilCancelled => Ok(0),
669 PlatformOrderType::PostOnly => Ok(3),
670 PlatformOrderType::ImmediateOrCancel | PlatformOrderType::FillOrKill => {
671 Err("order type is not a resting order".to_owned())
672 }
673 }
674}
675
676fn atoms(value: &str, field: &str) -> Result<u64, String> {
677 if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
678 return Err(format!("{field} must be an unsigned atomic decimal string"));
679 }
680 value
681 .parse::<u64>()
682 .map_err(|_| format!("{field} exceeds u64"))
683}
684
685#[derive(Clone, Debug, Eq, Hash, PartialEq)]
686struct ExpectedPlace {
687 side: u8,
688 order_type: u8,
689 price: u64,
690 size: u64,
691}
692
693impl ExpectedPlace {
694 fn from_request(
695 side: PlatformTradeSide,
696 order_type: PlatformOrderType,
697 limit_price_atoms: &str,
698 size_atoms: &str,
699 ) -> Result<Self, String> {
700 Ok(Self {
701 side: side_wire(side),
702 order_type: order_type_wire(order_type)?,
703 price: atoms(limit_price_atoms, "limit_price_atoms")?,
704 size: atoms(size_atoms, "size_atoms")?,
705 })
706 }
707}
708
709enum ExpectedCancels {
710 Ids(Vec<String>),
712 All,
714}
715
716struct ExpectedOrderIntent {
717 places: Vec<ExpectedPlace>,
718 cancels: ExpectedCancels,
719}
720
721fn expected_order_intent(
722 operation: &PlatformOrderChallengeRequest,
723) -> Result<ExpectedOrderIntent, String> {
724 Ok(match operation {
725 PlatformOrderChallengeRequest::Place {
726 side,
727 order_type,
728 limit_price_atoms,
729 size_atoms,
730 ..
731 } => ExpectedOrderIntent {
732 places: vec![ExpectedPlace::from_request(
733 *side,
734 *order_type,
735 limit_price_atoms,
736 size_atoms,
737 )?],
738 cancels: ExpectedCancels::Ids(Vec::new()),
739 },
740 PlatformOrderChallengeRequest::Cancel { order_id, .. } => ExpectedOrderIntent {
741 places: Vec::new(),
742 cancels: ExpectedCancels::Ids(vec![order_id.clone()]),
743 },
744 PlatformOrderChallengeRequest::CancelAll { .. } => ExpectedOrderIntent {
745 places: Vec::new(),
746 cancels: ExpectedCancels::All,
747 },
748 PlatformOrderChallengeRequest::Replace {
749 order_id,
750 side,
751 order_type,
752 limit_price_atoms,
753 size_atoms,
754 ..
755 } => ExpectedOrderIntent {
756 places: vec![ExpectedPlace::from_request(
757 *side,
758 *order_type,
759 limit_price_atoms,
760 size_atoms,
761 )?],
762 cancels: ExpectedCancels::Ids(vec![order_id.clone()]),
763 },
764 PlatformOrderChallengeRequest::Batch { operations, .. } => {
765 let mut places = Vec::new();
766 let mut cancels = Vec::new();
767 for item in operations {
768 match item {
769 PlatformOrderBatchOperation::Place {
770 side,
771 order_type,
772 limit_price_atoms,
773 size_atoms,
774 ..
775 } => places.push(ExpectedPlace::from_request(
776 *side,
777 *order_type,
778 limit_price_atoms,
779 size_atoms,
780 )?),
781 PlatformOrderBatchOperation::Cancel { order_id } => {
782 cancels.push(order_id.clone());
783 }
784 PlatformOrderBatchOperation::Replace {
785 order_id,
786 side,
787 order_type,
788 limit_price_atoms,
789 size_atoms,
790 ..
791 } => {
792 cancels.push(order_id.clone());
793 places.push(ExpectedPlace::from_request(
794 *side,
795 *order_type,
796 limit_price_atoms,
797 size_atoms,
798 )?);
799 }
800 }
801 }
802 ExpectedOrderIntent {
803 places,
804 cancels: ExpectedCancels::Ids(cancels),
805 }
806 }
807 })
808}
809
810fn same_multiset<T, K, F>(left: &[T], right: &[T], key: F) -> bool
811where
812 K: std::hash::Hash + Eq,
813 F: Fn(&T) -> K,
814{
815 if left.len() != right.len() {
816 return false;
817 }
818 let mut counts: HashMap<K, usize> = HashMap::new();
819 for value in left {
820 *counts.entry(key(value)).or_insert(0) += 1;
821 }
822 for value in right {
823 match counts.get_mut(&key(value)) {
824 Some(remaining) if *remaining > 0 => *remaining -= 1,
825 _ => return false,
826 }
827 }
828 true
829}
830
831fn decode_order_key(order: &str) -> Result<Vec<u8>, String> {
832 bs58::decode(order)
833 .into_vec()
834 .map_err(|_| "order account key is not base58".to_owned())
835}
836
837pub fn verify_order_transaction(context: &OrderVerificationContext<'_>) -> Result<(), String> {
840 let tx = decode_transaction(&context.prepared.transaction_base64)?;
841 let delegated = structural_checks(
842 &tx,
843 context.session_public_key,
844 context.owner_wallet,
845 &context.prepared.recent_blockhash,
846 StructuralOptions {
847 allow_address_tables: false,
848 require_envelope: true,
849 },
850 )?;
851 struct DecodedPlace {
852 place: ExpectedPlace,
853 market: String,
854 order: String,
855 }
856 struct DecodedCancel {
857 market: String,
858 order: String,
859 }
860 let mut places: Vec<DecodedPlace> = Vec::new();
861 let mut cancels: Vec<DecodedCancel> = Vec::new();
862 for instruction in &delegated {
863 match instruction.inner_tag {
864 INNER_TAG_BALANCE | INNER_TAG_MARKET_ACCOUNT => {}
865 INNER_TAG_PLACE_ORDER => {
866 let inner = &instruction.inner;
867 if inner.len() < 30 {
869 return Err("place instruction is truncated".to_owned());
870 }
871 let market = instruction.inner_accounts.get(1).cloned().flatten();
872 let order = instruction.inner_accounts.get(3).cloned().flatten();
873 let (Some(market), Some(order)) = (market, order) else {
874 return Err("place instruction accounts are not static".to_owned());
875 };
876 places.push(DecodedPlace {
877 place: ExpectedPlace {
878 side: inner[1],
879 order_type: inner[2],
880 price: read_u64(inner, 5)?,
881 size: read_u64(inner, 13)?,
882 },
883 market,
884 order,
885 });
886 }
887 INNER_TAG_CANCEL_ORDER => {
888 let market = instruction.inner_accounts.get(1).cloned().flatten();
889 let order = instruction.inner_accounts.get(3).cloned().flatten();
890 let (Some(market), Some(order)) = (market, order) else {
891 return Err("cancel instruction accounts are not static".to_owned());
892 };
893 cancels.push(DecodedCancel { market, order });
894 }
895 other => {
896 return Err(format!(
897 "the transaction delegates an unexpected instruction ({other})"
898 ));
899 }
900 }
901 }
902 let markets: HashSet<&str> = places
904 .iter()
905 .map(|entry| entry.market.as_str())
906 .chain(cancels.iter().map(|entry| entry.market.as_str()))
907 .collect();
908 for market in markets {
909 if opaque_market_id(market) != context.market_id {
910 return Err("the transaction touches another market".to_owned());
911 }
912 }
913 let expected = expected_order_intent(context.operation)?;
914 let decoded_places: Vec<ExpectedPlace> =
915 places.iter().map(|entry| entry.place.clone()).collect();
916 if !same_multiset(&decoded_places, &expected.places, |place| place.clone()) {
917 return Err("the transaction does not place exactly the requested orders".to_owned());
918 }
919 let cancelled_ids = cancels
920 .iter()
921 .map(|entry| {
922 Ok(opaque_order_id(
923 context.market_id,
924 &decode_order_key(&entry.order)?,
925 ))
926 })
927 .collect::<Result<Vec<String>, String>>()?;
928 match &expected.cancels {
929 ExpectedCancels::All => {
930 if cancelled_ids.is_empty() {
931 return Err("cancel_all prepared no cancellation".to_owned());
932 }
933 }
934 ExpectedCancels::Ids(ids) => {
935 if !same_multiset(&cancelled_ids, ids, |id| id.clone()) {
936 return Err(
937 "the transaction does not cancel exactly the requested orders".to_owned(),
938 );
939 }
940 }
941 }
942 let placed_ids = places
944 .iter()
945 .map(|entry| {
946 Ok(opaque_order_id(
947 context.market_id,
948 &decode_order_key(&entry.order)?,
949 ))
950 })
951 .collect::<Result<Vec<String>, String>>()?;
952 let touched: Vec<String> = cancelled_ids.into_iter().chain(placed_ids).collect();
953 if !same_multiset(&touched, &context.prepared.order_ids, |id| id.clone()) {
954 return Err("prepared order IDs do not match the transaction".to_owned());
955 }
956 Ok(())
957}
958
959pub fn verify_twap_transaction(context: &TwapVerificationContext<'_>) -> Result<(), String> {
963 let tx = decode_transaction(&context.prepared.transaction_base64)?;
964 let delegated = structural_checks(
965 &tx,
966 context.session_public_key,
967 context.owner_wallet,
968 &context.prepared.recent_blockhash,
969 StructuralOptions {
970 allow_address_tables: false,
971 require_envelope: true,
972 },
973 )?;
974 for instruction in &delegated {
975 if !matches!(
976 instruction.inner_tag,
977 INNER_TAG_BALANCE
978 | INNER_TAG_MARKET_ACCOUNT
979 | INNER_TAG_TWAP_POST
980 | INNER_TAG_TWAP_CANCEL
981 ) {
982 return Err(format!(
983 "the transaction delegates an unexpected instruction ({})",
984 instruction.inner_tag
985 ));
986 }
987 }
988 Ok(())
989}
990
991pub fn verify_execution_transaction(
995 context: &ExecutionVerificationContext<'_>,
996) -> Result<(), String> {
997 let tx = decode_transaction(&context.prepared.transaction_base64)?;
998 structural_checks(
999 &tx,
1000 context.session_public_key,
1001 context.owner_wallet,
1002 &context.prepared.recent_blockhash,
1003 StructuralOptions {
1004 allow_address_tables: true,
1005 require_envelope: false,
1006 },
1007 )?;
1008 Ok(())
1009}
1010
1011#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1017pub struct DefaultTransactionVerifier;
1018
1019#[async_trait]
1020impl OrderVerifier for DefaultTransactionVerifier {
1021 async fn verify(&self, context: &OrderVerificationContext<'_>) -> Result<(), String> {
1022 verify_order_transaction(context)
1023 }
1024}
1025
1026#[async_trait]
1027impl TwapVerifier for DefaultTransactionVerifier {
1028 async fn verify(&self, context: &TwapVerificationContext<'_>) -> Result<(), String> {
1029 verify_twap_transaction(context)
1030 }
1031}
1032
1033#[async_trait]
1034impl ExecutionVerifier for DefaultTransactionVerifier {
1035 async fn verify(&self, context: &ExecutionVerificationContext<'_>) -> Result<(), String> {
1036 verify_execution_transaction(context)
1037 }
1038}
1039
1040#[cfg(test)]
1043pub(crate) mod test_support {
1044 use super::*;
1045
1046 pub(crate) const OWNER_WALLET: &str = "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL";
1047 pub(crate) const SESSION_PUBLIC_KEY: &str = "9Uu7cLBgfMk233BAjMvTS8XJy6KbZK7oQ7NXuCTi3Fg2";
1048 pub(crate) const FEE_PAYER: [u8; 32] = [1; 32];
1049 pub(crate) const MARKET_PDA: [u8; 32] = [2; 32];
1050 pub(crate) const ORDER_PDA: [u8; 32] = [3; 32];
1051 pub(crate) const RECENT_BLOCKHASH: [u8; 32] = [5; 32];
1052 const VAULT_PROGRAM: [u8; 32] = [11; 32];
1053 const STRATA_PROGRAM: [u8; 32] = [12; 32];
1054 const VAULT_PDA: [u8; 32] = [13; 32];
1055 const DELEGATE_PDA: [u8; 32] = [14; 32];
1056 const USER_ACCOUNT: [u8; 32] = [15; 32];
1057 const RENT_BANK: [u8; 32] = [16; 32];
1058 pub(crate) const PLACE_PRICE: u64 = 150_000_000;
1059 pub(crate) const PLACE_SIZE: u64 = 1_000_000_000;
1060
1061 #[derive(Clone, Copy, Debug, Default)]
1062 pub(crate) struct PlaceTransactionOptions {
1063 pub(crate) side: u8,
1065 pub(crate) session_pays: bool,
1067 pub(crate) extra_system_transfer: bool,
1069 pub(crate) market: Option<[u8; 32]>,
1071 }
1072
1073 pub(crate) fn key(value: &str) -> [u8; 32] {
1074 bs58::decode(value).into_vec().unwrap().try_into().unwrap()
1075 }
1076
1077 pub(crate) fn market_id() -> String {
1078 opaque_market_id(&bs58::encode(MARKET_PDA).into_string())
1079 }
1080
1081 pub(crate) fn order_id() -> String {
1082 opaque_order_id(&market_id(), &ORDER_PDA)
1083 }
1084
1085 pub(crate) fn recent_blockhash() -> String {
1086 bs58::encode(RECENT_BLOCKHASH).into_string()
1087 }
1088
1089 fn compact(mut value: usize) -> Vec<u8> {
1090 let mut out = Vec::new();
1091 loop {
1092 let byte = (value & 0x7f) as u8;
1093 value >>= 7;
1094 if value == 0 {
1095 out.push(byte);
1096 return out;
1097 }
1098 out.push(byte | 0x80);
1099 }
1100 }
1101
1102 fn envelope(inner: &[u8]) -> Vec<u8> {
1103 let mut data = vec![DELEGATED_ENVELOPE_TAG];
1104 data.extend_from_slice(&0u64.to_le_bytes());
1105 data.extend_from_slice(&[0, 0]);
1106 data.extend_from_slice(&(inner.len() as u16).to_le_bytes());
1107 data.push(6);
1108 data.extend_from_slice(inner);
1109 data.extend_from_slice(&[2; 6]);
1110 data
1111 }
1112
1113 fn place_inner(side: u8, order_type: u8, price: u64, size: u64) -> Vec<u8> {
1114 let mut inner = vec![INNER_TAG_PLACE_ORDER, side, order_type, 0, 0];
1115 inner.extend_from_slice(&price.to_le_bytes());
1116 inner.extend_from_slice(&size.to_le_bytes());
1117 inner.extend_from_slice(&0u64.to_le_bytes());
1118 inner.push(255);
1119 inner
1120 }
1121
1122 fn instruction(program: u8, accounts: &[u8], data: &[u8]) -> Vec<u8> {
1123 let mut out = vec![program];
1124 out.extend(compact(accounts.len()));
1125 out.extend_from_slice(accounts);
1126 out.extend(compact(data.len()));
1127 out.extend_from_slice(data);
1128 out
1129 }
1130
1131 pub(crate) fn place_transaction(options: PlaceTransactionOptions) -> String {
1135 let session = key(SESSION_PUBLIC_KEY);
1136 let owner = key(OWNER_WALLET);
1137 let system = key("11111111111111111111111111111111");
1138 let compute_budget = key("ComputeBudget111111111111111111111111111111");
1139 let mut keys: Vec<[u8; 32]> = if options.session_pays {
1140 vec![session, FEE_PAYER]
1141 } else {
1142 vec![FEE_PAYER, session]
1143 };
1144 keys.extend([
1145 VAULT_PDA,
1146 DELEGATE_PDA,
1147 USER_ACCOUNT,
1148 ORDER_PDA,
1149 RENT_BANK,
1150 MARKET_PDA,
1151 owner,
1152 VAULT_PROGRAM,
1153 STRATA_PROGRAM,
1154 system,
1155 compute_budget,
1156 ]);
1157 if let Some(market) = options.market {
1158 keys.push(market);
1159 }
1160 let at = |wanted: [u8; 32]| -> u8 {
1161 keys.iter()
1162 .position(|candidate| *candidate == wanted)
1163 .unwrap() as u8
1164 };
1165 let mut instructions = Vec::new();
1166 instructions.push(instruction(at(compute_budget), &[], &[2, 0, 0, 0, 0]));
1167 let market = options.market.unwrap_or(MARKET_PDA);
1168 let place_accounts = [
1171 at(session),
1172 at(VAULT_PDA),
1173 at(DELEGATE_PDA),
1174 at(STRATA_PROGRAM),
1175 at(owner),
1176 at(FEE_PAYER),
1177 at(VAULT_PDA),
1178 at(market),
1179 at(USER_ACCOUNT),
1180 at(ORDER_PDA),
1181 at(system),
1182 at(RENT_BANK),
1183 ];
1184 let place_data = envelope(&place_inner(options.side, 3, PLACE_PRICE, PLACE_SIZE));
1185 instructions.push(instruction(at(VAULT_PROGRAM), &place_accounts, &place_data));
1186 if options.extra_system_transfer {
1187 let mut data = vec![2, 0, 0, 0];
1188 data.extend_from_slice(&1u64.to_le_bytes());
1189 instructions.push(instruction(
1190 at(system),
1191 &[at(session), at(FEE_PAYER)],
1192 &data,
1193 ));
1194 }
1195 let mut message = vec![0x80, 2, 0, 5];
1196 message.extend(compact(keys.len()));
1197 for key in &keys {
1198 message.extend_from_slice(key);
1199 }
1200 message.extend_from_slice(&RECENT_BLOCKHASH);
1201 message.extend(compact(instructions.len()));
1202 for instruction in &instructions {
1203 message.extend_from_slice(instruction);
1204 }
1205 message.extend(compact(0));
1206 let mut wire = compact(2);
1207 wire.extend_from_slice(&[0u8; 128]);
1208 wire.extend_from_slice(&message);
1209 base64::engine::general_purpose::STANDARD.encode(wire)
1210 }
1211}
1212
1213#[cfg(test)]
1214mod tests {
1215 use super::test_support::*;
1216 use super::*;
1217 use crate::{PlatformOrderAction, PlatformOrderPrepareResponse};
1218
1219 fn prepared(transaction_base64: String) -> PlatformOrderPrepareResponse {
1220 PlatformOrderPrepareResponse {
1221 schema_version: 2,
1222 contract_version: "2.0".to_owned(),
1223 order_control_id: "or_44444444444444444444444444444444".to_owned(),
1224 market_id: market_id(),
1225 action: PlatformOrderAction::Place,
1226 order_ids: vec![order_id()],
1227 transaction_base64,
1228 recent_blockhash: recent_blockhash(),
1229 last_valid_block_height: 400_000_000,
1230 expires_at_ms: 1_786_550_460_000,
1231 }
1232 }
1233
1234 fn operation() -> PlatformOrderChallengeRequest {
1235 PlatformOrderChallengeRequest::Place {
1236 owner_wallet: OWNER_WALLET.to_owned(),
1237 session_public_key: SESSION_PUBLIC_KEY.to_owned(),
1238 account_sequence: None,
1239 client_order_id: "agent-42".to_owned(),
1240 side: PlatformTradeSide::Buy,
1241 order_type: PlatformOrderType::PostOnly,
1242 limit_price_atoms: PLACE_PRICE.to_string(),
1243 size_atoms: PLACE_SIZE.to_string(),
1244 }
1245 }
1246
1247 fn verify(options: PlaceTransactionOptions) -> Result<(), String> {
1248 let prepared = prepared(place_transaction(options));
1249 let operation = operation();
1250 let market_id = market_id();
1251 verify_order_transaction(&OrderVerificationContext {
1252 challenge: None,
1253 operation: &operation,
1254 market_id: &market_id,
1255 prepared: &prepared,
1256 owner_wallet: OWNER_WALLET,
1257 session_public_key: SESSION_PUBLIC_KEY,
1258 })
1259 }
1260
1261 #[test]
1262 fn decodes_a_v0_transaction_with_static_keys_and_instructions() {
1263 let decoded =
1264 decode_transaction(&place_transaction(PlaceTransactionOptions::default())).unwrap();
1265 assert_eq!(decoded.version, TransactionVersion::V0);
1266 assert_eq!(decoded.signature_count, 2);
1267 assert_eq!(decoded.num_required_signatures, 2);
1268 assert_eq!(decoded.num_readonly_signed, 0);
1269 assert_eq!(decoded.num_readonly_unsigned, 5);
1270 assert_eq!(decoded.static_account_keys.len(), 13);
1271 assert_eq!(decoded.static_account_keys[1], SESSION_PUBLIC_KEY);
1272 assert_eq!(decoded.recent_blockhash, recent_blockhash());
1273 assert_eq!(decoded.instructions.len(), 2);
1274 assert_eq!(decoded.instructions[1].account_indexes.len(), 12);
1275 assert_eq!(decoded.instructions[1].data[0], DELEGATED_ENVELOPE_TAG);
1276 assert_eq!(decoded.address_table_lookup_count, 0);
1277 }
1278
1279 #[test]
1280 fn decoder_rejects_truncated_and_trailing_bytes() {
1281 let raw = base64::engine::general_purpose::STANDARD
1282 .decode(place_transaction(PlaceTransactionOptions::default()))
1283 .unwrap();
1284 let truncated = base64::engine::general_purpose::STANDARD.encode(&raw[..raw.len() - 1]);
1285 assert_eq!(
1286 decode_transaction(&truncated).unwrap_err(),
1287 "transaction is truncated"
1288 );
1289 let mut trailing = raw.clone();
1290 trailing.push(0);
1291 assert_eq!(
1292 decode_transaction(&base64::engine::general_purpose::STANDARD.encode(trailing))
1293 .unwrap_err(),
1294 "transaction carries trailing bytes"
1295 );
1296 assert_eq!(
1297 decode_transaction("not base64!").unwrap_err(),
1298 "invalid base64 payload"
1299 );
1300 }
1301
1302 #[test]
1303 fn external_signer_may_change_signatures_but_not_the_message() {
1304 let prepared = place_transaction(PlaceTransactionOptions::default());
1305 let mut signed = base64::engine::general_purpose::STANDARD
1306 .decode(&prepared)
1307 .unwrap();
1308 signed[1] = 9;
1309 let signed = base64::engine::general_purpose::STANDARD.encode(&signed);
1310 verify_signed_transaction_message(&prepared, &signed).unwrap();
1311
1312 let mut changed = base64::engine::general_purpose::STANDARD
1313 .decode(&signed)
1314 .unwrap();
1315 *changed.last_mut().unwrap() ^= 1;
1316 let error = verify_signed_transaction_message(
1317 &prepared,
1318 &base64::engine::general_purpose::STANDARD.encode(changed),
1319 )
1320 .unwrap_err();
1321 assert!(error.contains("message changed"), "{error}");
1322 }
1323
1324 #[test]
1325 fn built_in_verifier_accepts_exactly_the_requested_place() {
1326 verify(PlaceTransactionOptions::default()).unwrap();
1327 }
1328
1329 #[test]
1330 fn built_in_verifier_refuses_a_different_side() {
1331 let error = verify(PlaceTransactionOptions {
1332 side: 1,
1333 ..PlaceTransactionOptions::default()
1334 })
1335 .unwrap_err();
1336 assert!(error.contains("exactly the requested orders"), "{error}");
1337 }
1338
1339 #[test]
1340 fn built_in_verifier_refuses_the_session_as_fee_payer() {
1341 let error = verify(PlaceTransactionOptions {
1342 session_pays: true,
1343 ..PlaceTransactionOptions::default()
1344 })
1345 .unwrap_err();
1346 assert!(error.contains("fee payer"), "{error}");
1347 }
1348
1349 #[test]
1350 fn built_in_verifier_refuses_a_session_signed_system_transfer() {
1351 let error = verify(PlaceTransactionOptions {
1352 extra_system_transfer: true,
1353 ..PlaceTransactionOptions::default()
1354 })
1355 .unwrap_err();
1356 assert!(error.contains("system or token instruction"), "{error}");
1357 }
1358
1359 #[test]
1360 fn built_in_verifier_refuses_another_market() {
1361 let error = verify(PlaceTransactionOptions {
1362 market: Some([7; 32]),
1363 ..PlaceTransactionOptions::default()
1364 })
1365 .unwrap_err();
1366 assert!(error.contains("another market"), "{error}");
1367 }
1368
1369 #[test]
1370 fn built_in_verifier_binds_the_echoed_order_ids_and_blockhash() {
1371 let transaction = place_transaction(PlaceTransactionOptions::default());
1372 let operation = operation();
1373 let market_id = market_id();
1374 let mut wrong_ids = prepared(transaction.clone());
1375 wrong_ids.order_ids = vec!["order_33333333333333333333333333333333".to_owned()];
1376 let error = verify_order_transaction(&OrderVerificationContext {
1377 challenge: None,
1378 operation: &operation,
1379 market_id: &market_id,
1380 prepared: &wrong_ids,
1381 owner_wallet: OWNER_WALLET,
1382 session_public_key: SESSION_PUBLIC_KEY,
1383 })
1384 .unwrap_err();
1385 assert!(error.contains("order IDs do not match"), "{error}");
1386 let mut wrong_blockhash = prepared(transaction);
1387 wrong_blockhash.recent_blockhash = bs58::encode([9u8; 32]).into_string();
1388 let error = verify_order_transaction(&OrderVerificationContext {
1389 challenge: None,
1390 operation: &operation,
1391 market_id: &market_id,
1392 prepared: &wrong_blockhash,
1393 owner_wallet: OWNER_WALLET,
1394 session_public_key: SESSION_PUBLIC_KEY,
1395 })
1396 .unwrap_err();
1397 assert!(error.contains("blockhash"), "{error}");
1398 }
1399
1400 #[test]
1401 fn twap_and_execution_verification_are_structural() {
1402 let transaction = place_transaction(PlaceTransactionOptions::default());
1406 let twap_prepared = crate::PlatformTwapPrepareResponse {
1407 schema_version: 2,
1408 contract_version: "2.0".to_owned(),
1409 twap_control_id: "twctl_44444444444444444444444444444444".to_owned(),
1410 market_id: market_id(),
1411 action: crate::PlatformTwapControlAction::Place,
1412 twap_id: "twap_33333333333333333333333333333333".to_owned(),
1413 transaction_base64: transaction.clone(),
1414 recent_blockhash: recent_blockhash(),
1415 last_valid_block_height: 1,
1416 expires_at_ms: 1,
1417 };
1418 let twap_operation = crate::PlatformTwapChallengeRequest::Place {
1419 owner_wallet: OWNER_WALLET.to_owned(),
1420 session_public_key: SESSION_PUBLIC_KEY.to_owned(),
1421 side: PlatformTradeSide::Buy,
1422 total_size_atoms: "10".to_owned(),
1423 slices_total: 2,
1424 maximum_tolerance_bps: 1,
1425 interval_slots: 25,
1426 limit_price_atoms: "1".to_owned(),
1427 };
1428 let market_id = market_id();
1429 let error = verify_twap_transaction(&TwapVerificationContext {
1430 challenge: None,
1431 operation: &twap_operation,
1432 market_id: &market_id,
1433 prepared: &twap_prepared,
1434 owner_wallet: OWNER_WALLET,
1435 session_public_key: SESSION_PUBLIC_KEY,
1436 })
1437 .unwrap_err();
1438 assert!(error.contains("unexpected instruction (33)"), "{error}");
1439
1440 let quote: crate::QuoteResponse =
1441 serde_json::from_str(strata_public_contract::contract_fixtures::QUOTE).unwrap();
1442 let execution_prepared = crate::ExecutionPrepareResponse {
1443 schema_version: 1,
1444 contract_version: "1.1".to_owned(),
1445 execution_id: "se_0123456789abcdef0123456789abcdef".to_owned(),
1446 quote_id: quote.quote_id.clone(),
1447 market_id: quote.market_id.clone(),
1448 side: quote.side,
1449 amount_in_atoms: quote.amount_in_atoms.clone(),
1450 minimum_output_atoms: quote.minimum_output_atoms.clone(),
1451 transaction_base64: transaction,
1452 recent_blockhash: recent_blockhash(),
1453 last_valid_block_height: 1,
1454 expires_at_ms: 1,
1455 };
1456 verify_execution_transaction(&ExecutionVerificationContext {
1457 quote: "e,
1458 challenge: None,
1459 prepared: &execution_prepared,
1460 owner_wallet: OWNER_WALLET,
1461 session_public_key: SESSION_PUBLIC_KEY,
1462 })
1463 .unwrap();
1464 }
1465}