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