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 OrderVerificationContext, OrderVerifier, PlatformOrderBatchOperation,
32 PlatformOrderChallengeRequest, PlatformOrderType, PlatformTradeSide, TwapVerificationContext,
33 TwapVerifier,
34};
35
36const WELL_KNOWN_PROGRAMS: [&str; 10] = [
38 "11111111111111111111111111111111", "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", "ComputeBudget111111111111111111111111111111", "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr", "Stake11111111111111111111111111111111111111", "Vote111111111111111111111111111111111111111", "AddressLookupTab1e1111111111111111111111111", "BPFLoaderUpgradeab1e11111111111111111111111", ];
49
50const DELEGATED_ENVELOPE_TAG: u8 = 3;
52const INNER_TAG_BALANCE: u8 = 1;
54const INNER_TAG_CANCEL_ORDER: u8 = 4;
55const INNER_TAG_PLACE_ORDER: u8 = 33;
56const INNER_TAG_MARKET_ACCOUNT: u8 = 34;
57const INNER_TAG_TWAP_CANCEL: u8 = 36;
58const INNER_TAG_TWAP_POST: u8 = 38;
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub enum TransactionVersion {
63 Legacy,
64 V0,
65}
66
67#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct DecodedInstruction {
70 pub program_id_index: u8,
71 pub account_indexes: Vec<u8>,
72 pub data: Vec<u8>,
73}
74
75#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct DecodedTransaction {
79 pub version: TransactionVersion,
80 pub signature_count: usize,
81 pub num_required_signatures: u8,
82 pub num_readonly_signed: u8,
83 pub num_readonly_unsigned: u8,
84 pub static_account_keys: Vec<String>,
86 pub recent_blockhash: String,
87 pub instructions: Vec<DecodedInstruction>,
88 pub address_table_lookup_count: usize,
89}
90
91struct Reader<'a> {
92 bytes: &'a [u8],
93 offset: usize,
94}
95
96impl<'a> Reader<'a> {
97 fn new(bytes: &'a [u8]) -> Self {
98 Self { bytes, offset: 0 }
99 }
100
101 fn u8(&mut self) -> Result<u8, String> {
102 let byte = *self
103 .bytes
104 .get(self.offset)
105 .ok_or_else(|| "transaction is truncated".to_owned())?;
106 self.offset += 1;
107 Ok(byte)
108 }
109
110 fn compact_u16(&mut self) -> Result<usize, String> {
111 let mut value = 0usize;
112 let mut shift = 0u32;
113 for _ in 0..3 {
114 let byte = self.u8()?;
115 value |= usize::from(byte & 0x7f) << shift;
116 if byte & 0x80 == 0 {
117 return Ok(value);
118 }
119 shift += 7;
120 }
121 Err("transaction length prefix is invalid".to_owned())
122 }
123
124 fn take(&mut self, length: usize) -> Result<&'a [u8], String> {
125 let end = self
126 .offset
127 .checked_add(length)
128 .filter(|end| *end <= self.bytes.len())
129 .ok_or_else(|| "transaction is truncated".to_owned())?;
130 let out = &self.bytes[self.offset..end];
131 self.offset = end;
132 Ok(out)
133 }
134
135 fn done(&self) -> bool {
136 self.offset == self.bytes.len()
137 }
138}
139
140pub fn decode_transaction(transaction_base64: &str) -> Result<DecodedTransaction, String> {
142 let bytes = base64::engine::general_purpose::STANDARD
143 .decode(transaction_base64.trim())
144 .map_err(|_| "invalid base64 payload".to_owned())?;
145 let mut reader = Reader::new(&bytes);
146 let signature_count = reader.compact_u16()?;
147 reader.take(signature_count.saturating_mul(64))?;
148 let mut first = reader.u8()?;
149 let mut version = TransactionVersion::Legacy;
150 if first & 0x80 != 0 {
151 if first & 0x7f != 0 {
152 return Err("unsupported transaction version".to_owned());
153 }
154 version = TransactionVersion::V0;
155 first = reader.u8()?;
156 }
157 let num_required_signatures = first;
158 let num_readonly_signed = reader.u8()?;
159 let num_readonly_unsigned = reader.u8()?;
160 let key_count = reader.compact_u16()?;
161 let mut static_account_keys = Vec::with_capacity(key_count);
162 for _ in 0..key_count {
163 static_account_keys.push(bs58::encode(reader.take(32)?).into_string());
164 }
165 let recent_blockhash = bs58::encode(reader.take(32)?).into_string();
166 let instruction_count = reader.compact_u16()?;
167 let mut instructions = Vec::with_capacity(instruction_count);
168 for _ in 0..instruction_count {
169 let program_id_index = reader.u8()?;
170 let account_count = reader.compact_u16()?;
171 let account_indexes = reader.take(account_count)?.to_vec();
172 let data_length = reader.compact_u16()?;
173 let data = reader.take(data_length)?.to_vec();
174 instructions.push(DecodedInstruction {
175 program_id_index,
176 account_indexes,
177 data,
178 });
179 }
180 let mut address_table_lookup_count = 0;
181 if version == TransactionVersion::V0 {
182 address_table_lookup_count = reader.compact_u16()?;
183 for _ in 0..address_table_lookup_count {
184 reader.take(32)?;
185 let writable = reader.compact_u16()?;
186 reader.take(writable)?;
187 let readonly = reader.compact_u16()?;
188 reader.take(readonly)?;
189 }
190 }
191 if !reader.done() {
192 return Err("transaction carries trailing bytes".to_owned());
193 }
194 if signature_count != usize::from(num_required_signatures) || num_required_signatures == 0 {
195 return Err("transaction signature layout is invalid".to_owned());
196 }
197 if static_account_keys.len() < usize::from(num_required_signatures) {
198 return Err("transaction signer layout is invalid".to_owned());
199 }
200 Ok(DecodedTransaction {
201 version,
202 signature_count,
203 num_required_signatures,
204 num_readonly_signed,
205 num_readonly_unsigned,
206 static_account_keys,
207 recent_blockhash,
208 instructions,
209 address_table_lookup_count,
210 })
211}
212
213struct DelegatedInstruction {
214 inner_tag: u8,
215 inner: Vec<u8>,
216 inner_accounts: Vec<Option<String>>,
219}
220
221struct StructuralOptions {
222 allow_address_tables: bool,
223 require_envelope: bool,
224}
225
226fn structural_checks(
229 tx: &DecodedTransaction,
230 session_public_key: &str,
231 owner_wallet: &str,
232 recent_blockhash: &str,
233 options: StructuralOptions,
234) -> Result<Vec<DelegatedInstruction>, String> {
235 if tx.recent_blockhash != recent_blockhash {
236 return Err("prepared transaction blockhash does not match".to_owned());
237 }
238 let keys = &tx.static_account_keys;
239 let required = usize::from(tx.num_required_signatures);
240 let session_index = keys
241 .iter()
242 .position(|key| key == session_public_key)
243 .filter(|index| *index < required)
244 .ok_or_else(|| "the session key is not a required signer".to_owned())?;
245 if session_index == 0 {
246 return Err("the session key must never be the fee payer".to_owned());
247 }
248 if let Some(owner_index) = keys.iter().position(|key| key == owner_wallet) {
249 if owner_index < required {
250 return Err("the owner wallet must not be asked to sign".to_owned());
251 }
252 }
253 if !options.allow_address_tables && tx.address_table_lookup_count != 0 {
254 return Err("order-control transactions carry no lookup tables".to_owned());
255 }
256 let session_index_u8 = u8::try_from(session_index)
257 .map_err(|_| "transaction signer layout is invalid".to_owned())?;
258 let mut envelope_program: Option<&str> = None;
259 let mut inner_program: Option<&str> = None;
260 let mut delegated = Vec::new();
261 for instruction in &tx.instructions {
262 if !instruction.account_indexes.contains(&session_index_u8) {
263 continue;
264 }
265 let program = keys
266 .get(usize::from(instruction.program_id_index))
267 .ok_or_else(|| "instruction program is not static".to_owned())?;
268 if WELL_KNOWN_PROGRAMS.contains(&program.as_str()) {
269 return Err("the session key must not sign a system or token instruction".to_owned());
270 }
271 match envelope_program {
272 None => envelope_program = Some(program),
273 Some(existing) if existing != program => {
274 return Err("the session key signs for more than one program".to_owned());
275 }
276 Some(_) => {}
277 }
278 if instruction.account_indexes.first() != Some(&session_index_u8) {
279 return Err("the session key is not the delegate signer".to_owned());
280 }
281 if !options.require_envelope {
282 continue;
283 }
284 let data = &instruction.data;
285 if data.len() < 14 || data[0] != DELEGATED_ENVELOPE_TAG {
286 return Err("the session key signs a non-delegated instruction".to_owned());
287 }
288 let inner_length = usize::from(data[11]) | (usize::from(data[12]) << 8);
289 let inner_end = 14 + inner_length;
290 if inner_length == 0 || inner_end > data.len() {
291 return Err("delegated instruction is malformed".to_owned());
292 }
293 let inner = data[14..inner_end].to_vec();
294 let inner_program_key = instruction
295 .account_indexes
296 .get(3)
297 .and_then(|index| keys.get(usize::from(*index)))
298 .ok_or_else(|| "delegated instruction target is not static".to_owned())?;
299 match inner_program {
300 None => inner_program = Some(inner_program_key),
301 Some(existing) if existing != inner_program_key => {
302 return Err("delegated instructions target more than one program".to_owned());
303 }
304 Some(_) => {}
305 }
306 delegated.push(DelegatedInstruction {
307 inner_tag: inner[0],
308 inner,
309 inner_accounts: instruction
310 .account_indexes
311 .iter()
312 .skip(6)
313 .map(|index| keys.get(usize::from(*index)).cloned())
314 .collect(),
315 });
316 }
317 if delegated.is_empty() && options.require_envelope {
318 return Err("the transaction carries no delegated instruction".to_owned());
319 }
320 Ok(delegated)
321}
322
323fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, String> {
324 let slice = bytes
325 .get(offset..offset + 8)
326 .ok_or_else(|| "delegated instruction is truncated".to_owned())?;
327 let mut array = [0u8; 8];
328 array.copy_from_slice(slice);
329 Ok(u64::from_le_bytes(array))
330}
331
332const fn side_wire(side: PlatformTradeSide) -> u8 {
333 match side {
334 PlatformTradeSide::Buy => 0,
335 PlatformTradeSide::Sell => 1,
336 }
337}
338
339fn order_type_wire(order_type: PlatformOrderType) -> Result<u8, String> {
340 match order_type {
341 PlatformOrderType::GoodUntilCancelled => Ok(0),
342 PlatformOrderType::PostOnly => Ok(3),
343 PlatformOrderType::ImmediateOrCancel | PlatformOrderType::FillOrKill => {
344 Err("order type is not a resting order".to_owned())
345 }
346 }
347}
348
349fn atoms(value: &str, field: &str) -> Result<u64, String> {
350 if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
351 return Err(format!("{field} must be an unsigned atomic decimal string"));
352 }
353 value
354 .parse::<u64>()
355 .map_err(|_| format!("{field} exceeds u64"))
356}
357
358#[derive(Clone, Debug, Eq, Hash, PartialEq)]
359struct ExpectedPlace {
360 side: u8,
361 order_type: u8,
362 price: u64,
363 size: u64,
364}
365
366impl ExpectedPlace {
367 fn from_request(
368 side: PlatformTradeSide,
369 order_type: PlatformOrderType,
370 limit_price_atoms: &str,
371 size_atoms: &str,
372 ) -> Result<Self, String> {
373 Ok(Self {
374 side: side_wire(side),
375 order_type: order_type_wire(order_type)?,
376 price: atoms(limit_price_atoms, "limit_price_atoms")?,
377 size: atoms(size_atoms, "size_atoms")?,
378 })
379 }
380}
381
382enum ExpectedCancels {
383 Ids(Vec<String>),
385 All,
387}
388
389struct ExpectedOrderIntent {
390 places: Vec<ExpectedPlace>,
391 cancels: ExpectedCancels,
392}
393
394fn expected_order_intent(
395 operation: &PlatformOrderChallengeRequest,
396) -> Result<ExpectedOrderIntent, String> {
397 Ok(match operation {
398 PlatformOrderChallengeRequest::Place {
399 side,
400 order_type,
401 limit_price_atoms,
402 size_atoms,
403 ..
404 } => ExpectedOrderIntent {
405 places: vec![ExpectedPlace::from_request(
406 *side,
407 *order_type,
408 limit_price_atoms,
409 size_atoms,
410 )?],
411 cancels: ExpectedCancels::Ids(Vec::new()),
412 },
413 PlatformOrderChallengeRequest::Cancel { order_id, .. } => ExpectedOrderIntent {
414 places: Vec::new(),
415 cancels: ExpectedCancels::Ids(vec![order_id.clone()]),
416 },
417 PlatformOrderChallengeRequest::CancelAll { .. } => ExpectedOrderIntent {
418 places: Vec::new(),
419 cancels: ExpectedCancels::All,
420 },
421 PlatformOrderChallengeRequest::Replace {
422 order_id,
423 side,
424 order_type,
425 limit_price_atoms,
426 size_atoms,
427 ..
428 } => ExpectedOrderIntent {
429 places: vec![ExpectedPlace::from_request(
430 *side,
431 *order_type,
432 limit_price_atoms,
433 size_atoms,
434 )?],
435 cancels: ExpectedCancels::Ids(vec![order_id.clone()]),
436 },
437 PlatformOrderChallengeRequest::Batch { operations, .. } => {
438 let mut places = Vec::new();
439 let mut cancels = Vec::new();
440 for item in operations {
441 match item {
442 PlatformOrderBatchOperation::Place {
443 side,
444 order_type,
445 limit_price_atoms,
446 size_atoms,
447 ..
448 } => places.push(ExpectedPlace::from_request(
449 *side,
450 *order_type,
451 limit_price_atoms,
452 size_atoms,
453 )?),
454 PlatformOrderBatchOperation::Cancel { order_id } => {
455 cancels.push(order_id.clone());
456 }
457 PlatformOrderBatchOperation::Replace {
458 order_id,
459 side,
460 order_type,
461 limit_price_atoms,
462 size_atoms,
463 ..
464 } => {
465 cancels.push(order_id.clone());
466 places.push(ExpectedPlace::from_request(
467 *side,
468 *order_type,
469 limit_price_atoms,
470 size_atoms,
471 )?);
472 }
473 }
474 }
475 ExpectedOrderIntent {
476 places,
477 cancels: ExpectedCancels::Ids(cancels),
478 }
479 }
480 })
481}
482
483fn same_multiset<T, K, F>(left: &[T], right: &[T], key: F) -> bool
484where
485 K: std::hash::Hash + Eq,
486 F: Fn(&T) -> K,
487{
488 if left.len() != right.len() {
489 return false;
490 }
491 let mut counts: HashMap<K, usize> = HashMap::new();
492 for value in left {
493 *counts.entry(key(value)).or_insert(0) += 1;
494 }
495 for value in right {
496 match counts.get_mut(&key(value)) {
497 Some(remaining) if *remaining > 0 => *remaining -= 1,
498 _ => return false,
499 }
500 }
501 true
502}
503
504fn decode_order_key(order: &str) -> Result<Vec<u8>, String> {
505 bs58::decode(order)
506 .into_vec()
507 .map_err(|_| "order account key is not base58".to_owned())
508}
509
510pub fn verify_order_transaction(context: &OrderVerificationContext<'_>) -> Result<(), String> {
513 let tx = decode_transaction(&context.prepared.transaction_base64)?;
514 let delegated = structural_checks(
515 &tx,
516 context.session_public_key,
517 context.owner_wallet,
518 &context.prepared.recent_blockhash,
519 StructuralOptions {
520 allow_address_tables: false,
521 require_envelope: true,
522 },
523 )?;
524 struct DecodedPlace {
525 place: ExpectedPlace,
526 market: String,
527 order: String,
528 }
529 struct DecodedCancel {
530 market: String,
531 order: String,
532 }
533 let mut places: Vec<DecodedPlace> = Vec::new();
534 let mut cancels: Vec<DecodedCancel> = Vec::new();
535 for instruction in &delegated {
536 match instruction.inner_tag {
537 INNER_TAG_BALANCE | INNER_TAG_MARKET_ACCOUNT => {}
538 INNER_TAG_PLACE_ORDER => {
539 let inner = &instruction.inner;
540 if inner.len() < 30 {
542 return Err("place instruction is truncated".to_owned());
543 }
544 let market = instruction.inner_accounts.get(1).cloned().flatten();
545 let order = instruction.inner_accounts.get(3).cloned().flatten();
546 let (Some(market), Some(order)) = (market, order) else {
547 return Err("place instruction accounts are not static".to_owned());
548 };
549 places.push(DecodedPlace {
550 place: ExpectedPlace {
551 side: inner[1],
552 order_type: inner[2],
553 price: read_u64(inner, 5)?,
554 size: read_u64(inner, 13)?,
555 },
556 market,
557 order,
558 });
559 }
560 INNER_TAG_CANCEL_ORDER => {
561 let market = instruction.inner_accounts.get(1).cloned().flatten();
562 let order = instruction.inner_accounts.get(3).cloned().flatten();
563 let (Some(market), Some(order)) = (market, order) else {
564 return Err("cancel instruction accounts are not static".to_owned());
565 };
566 cancels.push(DecodedCancel { market, order });
567 }
568 other => {
569 return Err(format!(
570 "the transaction delegates an unexpected instruction ({other})"
571 ));
572 }
573 }
574 }
575 let markets: HashSet<&str> = places
577 .iter()
578 .map(|entry| entry.market.as_str())
579 .chain(cancels.iter().map(|entry| entry.market.as_str()))
580 .collect();
581 for market in markets {
582 if opaque_market_id(market) != context.market_id {
583 return Err("the transaction touches another market".to_owned());
584 }
585 }
586 let expected = expected_order_intent(context.operation)?;
587 let decoded_places: Vec<ExpectedPlace> =
588 places.iter().map(|entry| entry.place.clone()).collect();
589 if !same_multiset(&decoded_places, &expected.places, |place| place.clone()) {
590 return Err("the transaction does not place exactly the requested orders".to_owned());
591 }
592 let cancelled_ids = cancels
593 .iter()
594 .map(|entry| {
595 Ok(opaque_order_id(
596 context.market_id,
597 &decode_order_key(&entry.order)?,
598 ))
599 })
600 .collect::<Result<Vec<String>, String>>()?;
601 match &expected.cancels {
602 ExpectedCancels::All => {
603 if cancelled_ids.is_empty() {
604 return Err("cancel_all prepared no cancellation".to_owned());
605 }
606 }
607 ExpectedCancels::Ids(ids) => {
608 if !same_multiset(&cancelled_ids, ids, |id| id.clone()) {
609 return Err(
610 "the transaction does not cancel exactly the requested orders".to_owned(),
611 );
612 }
613 }
614 }
615 let placed_ids = places
617 .iter()
618 .map(|entry| {
619 Ok(opaque_order_id(
620 context.market_id,
621 &decode_order_key(&entry.order)?,
622 ))
623 })
624 .collect::<Result<Vec<String>, String>>()?;
625 let touched: Vec<String> = cancelled_ids.into_iter().chain(placed_ids).collect();
626 if !same_multiset(&touched, &context.prepared.order_ids, |id| id.clone()) {
627 return Err("prepared order IDs do not match the transaction".to_owned());
628 }
629 Ok(())
630}
631
632pub fn verify_twap_transaction(context: &TwapVerificationContext<'_>) -> Result<(), String> {
636 let tx = decode_transaction(&context.prepared.transaction_base64)?;
637 let delegated = structural_checks(
638 &tx,
639 context.session_public_key,
640 context.owner_wallet,
641 &context.prepared.recent_blockhash,
642 StructuralOptions {
643 allow_address_tables: false,
644 require_envelope: true,
645 },
646 )?;
647 for instruction in &delegated {
648 if !matches!(
649 instruction.inner_tag,
650 INNER_TAG_BALANCE
651 | INNER_TAG_MARKET_ACCOUNT
652 | INNER_TAG_TWAP_POST
653 | INNER_TAG_TWAP_CANCEL
654 ) {
655 return Err(format!(
656 "the transaction delegates an unexpected instruction ({})",
657 instruction.inner_tag
658 ));
659 }
660 }
661 Ok(())
662}
663
664pub fn verify_execution_transaction(
668 context: &ExecutionVerificationContext<'_>,
669) -> Result<(), String> {
670 let tx = decode_transaction(&context.prepared.transaction_base64)?;
671 structural_checks(
672 &tx,
673 context.session_public_key,
674 context.owner_wallet,
675 &context.prepared.recent_blockhash,
676 StructuralOptions {
677 allow_address_tables: true,
678 require_envelope: false,
679 },
680 )?;
681 Ok(())
682}
683
684#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
690pub struct DefaultTransactionVerifier;
691
692#[async_trait]
693impl OrderVerifier for DefaultTransactionVerifier {
694 async fn verify(&self, context: &OrderVerificationContext<'_>) -> Result<(), String> {
695 verify_order_transaction(context)
696 }
697}
698
699#[async_trait]
700impl TwapVerifier for DefaultTransactionVerifier {
701 async fn verify(&self, context: &TwapVerificationContext<'_>) -> Result<(), String> {
702 verify_twap_transaction(context)
703 }
704}
705
706#[async_trait]
707impl ExecutionVerifier for DefaultTransactionVerifier {
708 async fn verify(&self, context: &ExecutionVerificationContext<'_>) -> Result<(), String> {
709 verify_execution_transaction(context)
710 }
711}
712
713#[cfg(test)]
716pub(crate) mod test_support {
717 use super::*;
718
719 pub(crate) const OWNER_WALLET: &str = "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL";
720 pub(crate) const SESSION_PUBLIC_KEY: &str = "9Uu7cLBgfMk233BAjMvTS8XJy6KbZK7oQ7NXuCTi3Fg2";
721 pub(crate) const FEE_PAYER: [u8; 32] = [1; 32];
722 pub(crate) const MARKET_PDA: [u8; 32] = [2; 32];
723 pub(crate) const ORDER_PDA: [u8; 32] = [3; 32];
724 pub(crate) const RECENT_BLOCKHASH: [u8; 32] = [5; 32];
725 const VAULT_PROGRAM: [u8; 32] = [11; 32];
726 const STRATA_PROGRAM: [u8; 32] = [12; 32];
727 const VAULT_PDA: [u8; 32] = [13; 32];
728 const DELEGATE_PDA: [u8; 32] = [14; 32];
729 const USER_ACCOUNT: [u8; 32] = [15; 32];
730 const RENT_BANK: [u8; 32] = [16; 32];
731 pub(crate) const PLACE_PRICE: u64 = 150_000_000;
732 pub(crate) const PLACE_SIZE: u64 = 1_000_000_000;
733
734 #[derive(Clone, Copy, Debug, Default)]
735 pub(crate) struct PlaceTransactionOptions {
736 pub(crate) side: u8,
738 pub(crate) session_pays: bool,
740 pub(crate) extra_system_transfer: bool,
742 pub(crate) market: Option<[u8; 32]>,
744 }
745
746 pub(crate) fn key(value: &str) -> [u8; 32] {
747 bs58::decode(value).into_vec().unwrap().try_into().unwrap()
748 }
749
750 pub(crate) fn market_id() -> String {
751 opaque_market_id(&bs58::encode(MARKET_PDA).into_string())
752 }
753
754 pub(crate) fn order_id() -> String {
755 opaque_order_id(&market_id(), &ORDER_PDA)
756 }
757
758 pub(crate) fn recent_blockhash() -> String {
759 bs58::encode(RECENT_BLOCKHASH).into_string()
760 }
761
762 fn compact(mut value: usize) -> Vec<u8> {
763 let mut out = Vec::new();
764 loop {
765 let byte = (value & 0x7f) as u8;
766 value >>= 7;
767 if value == 0 {
768 out.push(byte);
769 return out;
770 }
771 out.push(byte | 0x80);
772 }
773 }
774
775 fn envelope(inner: &[u8]) -> Vec<u8> {
776 let mut data = vec![DELEGATED_ENVELOPE_TAG];
777 data.extend_from_slice(&0u64.to_le_bytes());
778 data.extend_from_slice(&[0, 0]);
779 data.extend_from_slice(&(inner.len() as u16).to_le_bytes());
780 data.push(6);
781 data.extend_from_slice(inner);
782 data.extend_from_slice(&[2; 6]);
783 data
784 }
785
786 fn place_inner(side: u8, order_type: u8, price: u64, size: u64) -> Vec<u8> {
787 let mut inner = vec![INNER_TAG_PLACE_ORDER, side, order_type, 0, 0];
788 inner.extend_from_slice(&price.to_le_bytes());
789 inner.extend_from_slice(&size.to_le_bytes());
790 inner.extend_from_slice(&0u64.to_le_bytes());
791 inner.push(255);
792 inner
793 }
794
795 fn instruction(program: u8, accounts: &[u8], data: &[u8]) -> Vec<u8> {
796 let mut out = vec![program];
797 out.extend(compact(accounts.len()));
798 out.extend_from_slice(accounts);
799 out.extend(compact(data.len()));
800 out.extend_from_slice(data);
801 out
802 }
803
804 pub(crate) fn place_transaction(options: PlaceTransactionOptions) -> String {
808 let session = key(SESSION_PUBLIC_KEY);
809 let owner = key(OWNER_WALLET);
810 let system = key("11111111111111111111111111111111");
811 let compute_budget = key("ComputeBudget111111111111111111111111111111");
812 let mut keys: Vec<[u8; 32]> = if options.session_pays {
813 vec![session, FEE_PAYER]
814 } else {
815 vec![FEE_PAYER, session]
816 };
817 keys.extend([
818 VAULT_PDA,
819 DELEGATE_PDA,
820 USER_ACCOUNT,
821 ORDER_PDA,
822 RENT_BANK,
823 MARKET_PDA,
824 owner,
825 VAULT_PROGRAM,
826 STRATA_PROGRAM,
827 system,
828 compute_budget,
829 ]);
830 if let Some(market) = options.market {
831 keys.push(market);
832 }
833 let at = |wanted: [u8; 32]| -> u8 {
834 keys.iter()
835 .position(|candidate| *candidate == wanted)
836 .unwrap() as u8
837 };
838 let mut instructions = Vec::new();
839 instructions.push(instruction(at(compute_budget), &[], &[2, 0, 0, 0, 0]));
840 let market = options.market.unwrap_or(MARKET_PDA);
841 let place_accounts = [
844 at(session),
845 at(VAULT_PDA),
846 at(DELEGATE_PDA),
847 at(STRATA_PROGRAM),
848 at(owner),
849 at(FEE_PAYER),
850 at(VAULT_PDA),
851 at(market),
852 at(USER_ACCOUNT),
853 at(ORDER_PDA),
854 at(system),
855 at(RENT_BANK),
856 ];
857 let place_data = envelope(&place_inner(options.side, 3, PLACE_PRICE, PLACE_SIZE));
858 instructions.push(instruction(at(VAULT_PROGRAM), &place_accounts, &place_data));
859 if options.extra_system_transfer {
860 let mut data = vec![2, 0, 0, 0];
861 data.extend_from_slice(&1u64.to_le_bytes());
862 instructions.push(instruction(
863 at(system),
864 &[at(session), at(FEE_PAYER)],
865 &data,
866 ));
867 }
868 let mut message = vec![0x80, 2, 0, 5];
869 message.extend(compact(keys.len()));
870 for key in &keys {
871 message.extend_from_slice(key);
872 }
873 message.extend_from_slice(&RECENT_BLOCKHASH);
874 message.extend(compact(instructions.len()));
875 for instruction in &instructions {
876 message.extend_from_slice(instruction);
877 }
878 message.extend(compact(0));
879 let mut wire = compact(2);
880 wire.extend_from_slice(&[0u8; 128]);
881 wire.extend_from_slice(&message);
882 base64::engine::general_purpose::STANDARD.encode(wire)
883 }
884}
885
886#[cfg(test)]
887mod tests {
888 use super::test_support::*;
889 use super::*;
890 use crate::{PlatformOrderAction, PlatformOrderPrepareResponse};
891
892 fn prepared(transaction_base64: String) -> PlatformOrderPrepareResponse {
893 PlatformOrderPrepareResponse {
894 schema_version: 2,
895 contract_version: "2.0".to_owned(),
896 order_control_id: "or_44444444444444444444444444444444".to_owned(),
897 market_id: market_id(),
898 action: PlatformOrderAction::Place,
899 order_ids: vec![order_id()],
900 transaction_base64,
901 recent_blockhash: recent_blockhash(),
902 last_valid_block_height: 400_000_000,
903 expires_at_ms: 1_786_550_460_000,
904 }
905 }
906
907 fn operation() -> PlatformOrderChallengeRequest {
908 PlatformOrderChallengeRequest::Place {
909 owner_wallet: OWNER_WALLET.to_owned(),
910 session_public_key: SESSION_PUBLIC_KEY.to_owned(),
911 account_sequence: None,
912 client_order_id: "agent-42".to_owned(),
913 side: PlatformTradeSide::Buy,
914 order_type: PlatformOrderType::PostOnly,
915 limit_price_atoms: PLACE_PRICE.to_string(),
916 size_atoms: PLACE_SIZE.to_string(),
917 }
918 }
919
920 fn verify(options: PlaceTransactionOptions) -> Result<(), String> {
921 let prepared = prepared(place_transaction(options));
922 let operation = operation();
923 let market_id = market_id();
924 verify_order_transaction(&OrderVerificationContext {
925 challenge: None,
926 operation: &operation,
927 market_id: &market_id,
928 prepared: &prepared,
929 owner_wallet: OWNER_WALLET,
930 session_public_key: SESSION_PUBLIC_KEY,
931 })
932 }
933
934 #[test]
935 fn decodes_a_v0_transaction_with_static_keys_and_instructions() {
936 let decoded =
937 decode_transaction(&place_transaction(PlaceTransactionOptions::default())).unwrap();
938 assert_eq!(decoded.version, TransactionVersion::V0);
939 assert_eq!(decoded.signature_count, 2);
940 assert_eq!(decoded.num_required_signatures, 2);
941 assert_eq!(decoded.num_readonly_signed, 0);
942 assert_eq!(decoded.num_readonly_unsigned, 5);
943 assert_eq!(decoded.static_account_keys.len(), 13);
944 assert_eq!(decoded.static_account_keys[1], SESSION_PUBLIC_KEY);
945 assert_eq!(decoded.recent_blockhash, recent_blockhash());
946 assert_eq!(decoded.instructions.len(), 2);
947 assert_eq!(decoded.instructions[1].account_indexes.len(), 12);
948 assert_eq!(decoded.instructions[1].data[0], DELEGATED_ENVELOPE_TAG);
949 assert_eq!(decoded.address_table_lookup_count, 0);
950 }
951
952 #[test]
953 fn decoder_rejects_truncated_and_trailing_bytes() {
954 let raw = base64::engine::general_purpose::STANDARD
955 .decode(place_transaction(PlaceTransactionOptions::default()))
956 .unwrap();
957 let truncated = base64::engine::general_purpose::STANDARD.encode(&raw[..raw.len() - 1]);
958 assert_eq!(
959 decode_transaction(&truncated).unwrap_err(),
960 "transaction is truncated"
961 );
962 let mut trailing = raw.clone();
963 trailing.push(0);
964 assert_eq!(
965 decode_transaction(&base64::engine::general_purpose::STANDARD.encode(trailing))
966 .unwrap_err(),
967 "transaction carries trailing bytes"
968 );
969 assert_eq!(
970 decode_transaction("not base64!").unwrap_err(),
971 "invalid base64 payload"
972 );
973 }
974
975 #[test]
976 fn built_in_verifier_accepts_exactly_the_requested_place() {
977 verify(PlaceTransactionOptions::default()).unwrap();
978 }
979
980 #[test]
981 fn built_in_verifier_refuses_a_different_side() {
982 let error = verify(PlaceTransactionOptions {
983 side: 1,
984 ..PlaceTransactionOptions::default()
985 })
986 .unwrap_err();
987 assert!(error.contains("exactly the requested orders"), "{error}");
988 }
989
990 #[test]
991 fn built_in_verifier_refuses_the_session_as_fee_payer() {
992 let error = verify(PlaceTransactionOptions {
993 session_pays: true,
994 ..PlaceTransactionOptions::default()
995 })
996 .unwrap_err();
997 assert!(error.contains("fee payer"), "{error}");
998 }
999
1000 #[test]
1001 fn built_in_verifier_refuses_a_session_signed_system_transfer() {
1002 let error = verify(PlaceTransactionOptions {
1003 extra_system_transfer: true,
1004 ..PlaceTransactionOptions::default()
1005 })
1006 .unwrap_err();
1007 assert!(error.contains("system or token instruction"), "{error}");
1008 }
1009
1010 #[test]
1011 fn built_in_verifier_refuses_another_market() {
1012 let error = verify(PlaceTransactionOptions {
1013 market: Some([7; 32]),
1014 ..PlaceTransactionOptions::default()
1015 })
1016 .unwrap_err();
1017 assert!(error.contains("another market"), "{error}");
1018 }
1019
1020 #[test]
1021 fn built_in_verifier_binds_the_echoed_order_ids_and_blockhash() {
1022 let transaction = place_transaction(PlaceTransactionOptions::default());
1023 let operation = operation();
1024 let market_id = market_id();
1025 let mut wrong_ids = prepared(transaction.clone());
1026 wrong_ids.order_ids = vec!["order_33333333333333333333333333333333".to_owned()];
1027 let error = verify_order_transaction(&OrderVerificationContext {
1028 challenge: None,
1029 operation: &operation,
1030 market_id: &market_id,
1031 prepared: &wrong_ids,
1032 owner_wallet: OWNER_WALLET,
1033 session_public_key: SESSION_PUBLIC_KEY,
1034 })
1035 .unwrap_err();
1036 assert!(error.contains("order IDs do not match"), "{error}");
1037 let mut wrong_blockhash = prepared(transaction);
1038 wrong_blockhash.recent_blockhash = bs58::encode([9u8; 32]).into_string();
1039 let error = verify_order_transaction(&OrderVerificationContext {
1040 challenge: None,
1041 operation: &operation,
1042 market_id: &market_id,
1043 prepared: &wrong_blockhash,
1044 owner_wallet: OWNER_WALLET,
1045 session_public_key: SESSION_PUBLIC_KEY,
1046 })
1047 .unwrap_err();
1048 assert!(error.contains("blockhash"), "{error}");
1049 }
1050
1051 #[test]
1052 fn twap_and_execution_verification_are_structural() {
1053 let transaction = place_transaction(PlaceTransactionOptions::default());
1057 let twap_prepared = crate::PlatformTwapPrepareResponse {
1058 schema_version: 2,
1059 contract_version: "2.0".to_owned(),
1060 twap_control_id: "twctl_44444444444444444444444444444444".to_owned(),
1061 market_id: market_id(),
1062 action: crate::PlatformTwapControlAction::Place,
1063 twap_id: "twap_33333333333333333333333333333333".to_owned(),
1064 transaction_base64: transaction.clone(),
1065 recent_blockhash: recent_blockhash(),
1066 last_valid_block_height: 1,
1067 expires_at_ms: 1,
1068 };
1069 let twap_operation = crate::PlatformTwapChallengeRequest::Place {
1070 owner_wallet: OWNER_WALLET.to_owned(),
1071 session_public_key: SESSION_PUBLIC_KEY.to_owned(),
1072 side: PlatformTradeSide::Buy,
1073 total_size_atoms: "10".to_owned(),
1074 slices_total: 2,
1075 maximum_tolerance_bps: 1,
1076 interval_slots: 25,
1077 limit_price_atoms: "1".to_owned(),
1078 };
1079 let market_id = market_id();
1080 let error = verify_twap_transaction(&TwapVerificationContext {
1081 challenge: None,
1082 operation: &twap_operation,
1083 market_id: &market_id,
1084 prepared: &twap_prepared,
1085 owner_wallet: OWNER_WALLET,
1086 session_public_key: SESSION_PUBLIC_KEY,
1087 })
1088 .unwrap_err();
1089 assert!(error.contains("unexpected instruction (33)"), "{error}");
1090
1091 let quote: crate::QuoteResponse =
1092 serde_json::from_str(strata_public_contract::contract_fixtures::QUOTE).unwrap();
1093 let execution_prepared = crate::ExecutionPrepareResponse {
1094 schema_version: 1,
1095 contract_version: "1.1".to_owned(),
1096 execution_id: "se_0123456789abcdef0123456789abcdef".to_owned(),
1097 quote_id: quote.quote_id.clone(),
1098 market_id: quote.market_id.clone(),
1099 side: quote.side,
1100 amount_in_atoms: quote.amount_in_atoms.clone(),
1101 minimum_output_atoms: quote.minimum_output_atoms.clone(),
1102 transaction_base64: transaction,
1103 recent_blockhash: recent_blockhash(),
1104 last_valid_block_height: 1,
1105 expires_at_ms: 1,
1106 };
1107 verify_execution_transaction(&ExecutionVerificationContext {
1108 quote: "e,
1109 challenge: None,
1110 prepared: &execution_prepared,
1111 owner_wallet: OWNER_WALLET,
1112 session_public_key: SESSION_PUBLIC_KEY,
1113 })
1114 .unwrap();
1115 }
1116}