1use serde::{Deserialize, Serialize};
9
10pub mod platform;
11
12pub const CONTRACT_MAJOR: u16 = 1;
13pub const CONTRACT_VERSION: &str = "1.1";
14pub const DEFAULT_SLIPPAGE_BPS: u16 = 0;
16
17#[cfg(any(test, feature = "fixtures"))]
22#[doc(hidden)]
23pub mod contract_fixtures {
24 pub const ACTION_GRAPH: &str = include_str!("../fixtures/v1/action-graph.json");
25 pub const CAPABILITIES: &str = include_str!("../fixtures/v1/capabilities.json");
26 pub const EXECUTION_CHALLENGE: &str = include_str!("../fixtures/v1/execution-challenge.json");
27 pub const EXECUTION_PREPARE: &str = include_str!("../fixtures/v1/execution-prepare.json");
28 pub const EXECUTION_SUBMIT: &str = include_str!("../fixtures/v1/execution-submit.json");
29 pub const MARKETS: &str = include_str!("../fixtures/v1/markets.json");
30 pub const QUOTE: &str = include_str!("../fixtures/v1/quote.json");
31}
32
33pub const ACTION_GRAPH_VERSION: &str = "1.0";
34
35#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
36#[serde(rename_all = "snake_case")]
37pub enum ActionNodeKind {
38 Discovery,
39 Read,
40 Prepare,
41 ExternalSignature,
42 Submit,
43 Receipt,
44}
45
46#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
47#[serde(deny_unknown_fields)]
48pub struct ActionAuthorityModel {
49 pub permission_source: String,
51 pub signing_location: String,
53 pub accepts_private_keys: bool,
55}
56
57#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
58#[serde(deny_unknown_fields)]
59pub struct ActionOperation {
60 pub method: String,
61 pub path: String,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub mcp_tool: Option<String>,
64}
65
66#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
67#[serde(deny_unknown_fields)]
68pub struct ActionNode {
69 pub id: String,
70 pub kind: ActionNodeKind,
71 pub summary: String,
72 pub required_capabilities: Vec<String>,
73 pub available: bool,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub operation: Option<ActionOperation>,
77}
78
79#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
80#[serde(deny_unknown_fields)]
81pub struct ActionEdge {
82 pub from: String,
83 pub to: String,
84 pub condition: String,
85}
86
87#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
88#[serde(deny_unknown_fields)]
89pub struct ActionGraph {
90 pub schema_version: u16,
91 pub graph_version: String,
92 pub contract_version: String,
93 pub entry_node: String,
94 pub authority: ActionAuthorityModel,
95 pub nodes: Vec<ActionNode>,
96 pub edges: Vec<ActionEdge>,
97}
98
99#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
100#[serde(rename_all = "snake_case")]
101pub enum QuoteSide {
102 Buy,
103 Sell,
104}
105
106#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
107#[serde(deny_unknown_fields)]
108pub struct QuoteRequest {
109 pub market_id: String,
110 pub side: QuoteSide,
111 pub amount_in_atoms: String,
114 pub slippage_bps: u16,
117}
118
119#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
120#[serde(deny_unknown_fields)]
121pub struct QuoteResponse {
122 pub schema_version: u16,
123 pub contract_version: String,
124 pub quote_id: String,
127 pub server_time_ms: u64,
128 pub expires_at_ms: u64,
129 pub market_id: String,
130 pub side: QuoteSide,
131 pub amount_in_atoms: String,
132 pub amount_in_consumed_atoms: String,
134 pub amount_out_atoms: String,
135 pub minimum_output_atoms: String,
136 pub input_fee_atoms: String,
139 pub output_fee_atoms: String,
141 pub reference_price: String,
144 pub price_impact_pct: String,
145 pub provider: String,
146}
147
148#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
152#[serde(deny_unknown_fields)]
153pub struct ExecutionChallengeRequest {
154 pub quote_id: String,
155 pub owner_wallet: String,
156 pub session_public_key: String,
157 pub account_sequence: String,
160}
161
162#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
163#[serde(deny_unknown_fields)]
164pub struct ExecutionChallengeResponse {
165 pub schema_version: u16,
166 pub contract_version: String,
167 pub challenge_id: String,
168 pub quote_id: String,
169 pub market_id: String,
170 pub side: QuoteSide,
171 pub amount_in_atoms: String,
172 pub minimum_output_atoms: String,
174 pub authorization_payload_base64: String,
176 pub server_time_ms: u64,
177 pub expires_at_ms: u64,
178}
179
180#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
181#[serde(deny_unknown_fields)]
182pub struct ExecutionPrepareRequest {
183 pub challenge_id: String,
184 pub authorization_signature: String,
186}
187
188#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
189#[serde(deny_unknown_fields)]
190pub struct ExecutionPrepareResponse {
191 pub schema_version: u16,
192 pub contract_version: String,
193 pub execution_id: String,
194 pub quote_id: String,
195 pub market_id: String,
196 pub side: QuoteSide,
197 pub amount_in_atoms: String,
198 pub minimum_output_atoms: String,
201 pub transaction_base64: String,
204 pub recent_blockhash: String,
205 pub last_valid_block_height: u64,
206 pub expires_at_ms: u64,
207}
208
209#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
210#[serde(deny_unknown_fields)]
211pub struct ExecutionSubmitRequest {
212 pub execution_id: String,
213 pub signed_transaction_base64: String,
214 pub idempotency_key: String,
217}
218
219#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
220#[serde(deny_unknown_fields)]
221pub struct ExecutionSubmitResponse {
222 pub schema_version: u16,
223 pub contract_version: String,
224 pub execution_id: String,
225 pub signature: String,
226 pub status: ExecutionStatus,
227}
228
229#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
230#[serde(rename_all = "snake_case")]
231pub enum ExecutionStatus {
232 Submitted,
233}
234
235#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
236#[serde(deny_unknown_fields)]
237pub struct Market {
238 pub base: String,
239 pub quote: String,
240 pub market_pda: Option<String>,
241 pub label: String,
242 pub ready: bool,
246 pub base_decimals: u8,
247 pub quote_decimals: u8,
248 pub quote_path: Option<String>,
251}
252
253#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
254#[serde(deny_unknown_fields)]
255pub struct MarketsResponse {
256 pub schema_version: u16,
257 pub contract_version: String,
258 pub markets: Vec<Market>,
259}
260
261impl MarketsResponse {
262 pub fn new(markets: Vec<Market>) -> Self {
263 Self {
264 schema_version: CONTRACT_MAJOR,
265 contract_version: CONTRACT_VERSION.to_owned(),
266 markets,
267 }
268 }
269}
270
271#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
272#[serde(deny_unknown_fields)]
273pub struct ErrorDetail {
274 pub code: String,
275 pub message: String,
276 pub retryable: bool,
277}
278
279#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
280#[serde(deny_unknown_fields)]
281pub struct ErrorResponse {
282 pub schema_version: u16,
283 pub contract_version: String,
284 pub error: ErrorDetail,
285}
286
287impl ErrorResponse {
288 pub fn new(code: impl Into<String>, message: impl Into<String>, retryable: bool) -> Self {
289 Self {
290 schema_version: CONTRACT_MAJOR,
291 contract_version: CONTRACT_VERSION.to_owned(),
292 error: ErrorDetail {
293 code: code.into(),
294 message: message.into(),
295 retryable,
296 },
297 }
298 }
299}
300
301#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
302#[serde(rename_all = "snake_case")]
303pub enum CapabilityStability {
304 Internal,
305 Beta,
306 Stable,
307}
308
309#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
310#[serde(rename_all = "snake_case")]
311pub enum CapabilityRisk {
312 Read,
313 Prepare,
314 Submit,
315 Destructive,
316}
317
318#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
319#[serde(rename_all = "snake_case")]
320pub enum McpExposure {
321 None,
322 Read,
323 Prepare,
324 Submit,
325}
326
327#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
328#[serde(deny_unknown_fields)]
329pub struct CapabilityDescriptor {
330 pub id: String,
331 pub introduced_in: String,
332 pub stability: CapabilityStability,
333 pub required_scope: String,
334 pub risk: CapabilityRisk,
335 pub default_enabled: bool,
336 pub public_sdk: bool,
337 pub mcp_exposure: McpExposure,
338}
339
340#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
341#[serde(deny_unknown_fields)]
342pub struct CapabilityCatalog {
343 pub schema_version: u16,
344 pub contract_version: String,
345 pub capabilities: Vec<CapabilityDescriptor>,
346}
347
348impl CapabilityCatalog {
349 pub fn foundation() -> Self {
350 use CapabilityRisk::{Prepare, Read, Submit};
351 use CapabilityStability::{Beta, Stable};
352 use McpExposure::{
353 None as McpNone, Prepare as McpPrepare, Read as McpRead, Submit as McpSubmit,
354 };
355
356 let capability = |id: &str,
357 introduced_in: &str,
358 stability,
359 scope: &str,
360 risk,
361 default_enabled,
362 public_sdk,
363 mcp_exposure| {
364 CapabilityDescriptor {
365 id: id.to_owned(),
366 introduced_in: introduced_in.to_owned(),
367 stability,
368 required_scope: scope.to_owned(),
369 risk,
370 default_enabled,
371 public_sdk,
372 mcp_exposure,
373 }
374 };
375
376 Self {
377 schema_version: CONTRACT_MAJOR,
378 contract_version: CONTRACT_VERSION.to_owned(),
379 capabilities: vec![
380 capability(
381 "markets.read",
382 "1.0",
383 Stable,
384 "market:read",
385 Read,
386 true,
387 true,
388 McpRead,
389 ),
390 capability(
391 "books.read",
392 "1.1",
393 Beta,
394 "market:read",
395 Read,
396 true,
397 true,
398 McpNone,
399 ),
400 capability(
401 "quotes.read",
402 "1.0",
403 Beta,
404 "market:read",
405 Read,
406 true,
407 true,
408 McpRead,
409 ),
410 capability(
411 "account.read",
412 "1.1",
413 Beta,
414 "account:read",
415 Read,
416 true,
417 true,
418 McpNone,
419 ),
420 capability(
421 "trade.prepare",
422 "1.1",
423 Beta,
424 "trade:prepare",
425 Prepare,
426 true,
427 true,
428 McpPrepare,
429 ),
430 capability(
431 "trade.submit",
432 "1.1",
433 Beta,
434 "trade:submit",
435 Submit,
436 true,
437 true,
438 McpSubmit,
439 ),
440 ],
441 }
442 }
443}
444
445impl ActionGraph {
446 pub fn for_catalog(catalog: &CapabilityCatalog) -> Self {
450 let enabled = |required: &[&str]| {
451 required.iter().all(|id| {
452 catalog.capabilities.iter().any(|capability| {
453 capability.id == *id && capability.default_enabled && capability.public_sdk
454 })
455 })
456 };
457 let operation = |method: &str, path: &str, mcp_tool: Option<&str>| ActionOperation {
458 method: method.to_owned(),
459 path: path.to_owned(),
460 mcp_tool: mcp_tool.map(str::to_owned),
461 };
462 let node = |id: &str,
463 kind,
464 summary: &str,
465 required: &[&str],
466 operation: Option<ActionOperation>| ActionNode {
467 id: id.to_owned(),
468 kind,
469 summary: summary.to_owned(),
470 required_capabilities: required.iter().map(|value| (*value).to_owned()).collect(),
471 available: operation.is_none() || enabled(required),
472 operation,
473 };
474 let edge = |from: &str, to: &str, condition: &str| ActionEdge {
475 from: from.to_owned(),
476 to: to.to_owned(),
477 condition: condition.to_owned(),
478 };
479
480 Self {
481 schema_version: CONTRACT_MAJOR,
482 graph_version: ACTION_GRAPH_VERSION.to_owned(),
483 contract_version: CONTRACT_VERSION.to_owned(),
484 entry_node: "discover_capabilities".to_owned(),
485 authority: ActionAuthorityModel {
486 permission_source: "external_agent_owner".to_owned(),
487 signing_location: "external".to_owned(),
488 accepts_private_keys: false,
489 },
490 nodes: vec![
491 node(
492 "discover_capabilities",
493 ActionNodeKind::Discovery,
494 "Read the live capabilities that currently expose Strata operations.",
495 &[],
496 Some(operation("GET", "/sonar/capabilities", Some("strata_capabilities"))),
497 ),
498 node(
499 "discover_markets",
500 ActionNodeKind::Discovery,
501 "Discover ready markets, token decimals, and public operation paths.",
502 &["markets.read"],
503 Some(operation("GET", "/sonar/markets", Some("strata_markets"))),
504 ),
505 node(
506 "discover_action_graph",
507 ActionNodeKind::Discovery,
508 "Read the executable topology, live node availability, external signing steps, and transition conditions.",
509 &[],
510 Some(operation("GET", "/sonar/action-graph", Some("strata_action_graph"))),
511 ),
512 node(
513 "discover_platform_capabilities",
514 ActionNodeKind::Discovery,
515 "Read the versioned capabilities available through the official SDK.",
516 &[],
517 Some(operation("GET", "/v2/capabilities", None)),
518 ),
519 node(
520 "discover_platform_markets",
521 ActionNodeKind::Discovery,
522 "Discover opaque market IDs and current market status.",
523 &["markets.read"],
524 Some(operation("GET", "/v2/markets", None)),
525 ),
526 node(
527 "read_book",
528 ActionNodeKind::Read,
529 "Read a sequenced Strata book snapshot.",
530 &["books.read"],
531 Some(operation("GET", "/v2/markets/{market_id}/book", None)),
532 ),
533 node(
534 "read_market_status",
535 ActionNodeKind::Read,
536 "Read tick size, minimum order size, and current market status.",
537 &["books.read"],
538 Some(operation("GET", "/v2/markets/{market_id}/status", None)),
539 ),
540 node(
541 "read_best_bid_ask",
542 ActionNodeKind::Read,
543 "Read the current best bid and ask.",
544 &["books.read"],
545 Some(operation("GET", "/v2/markets/{market_id}/bbo", None)),
546 ),
547 node(
548 "read_fees",
549 ActionNodeKind::Read,
550 "Read the market fee schedule.",
551 &["books.read"],
552 Some(operation("GET", "/v2/markets/{market_id}/fees", None)),
553 ),
554 node(
555 "read_trades",
556 ActionNodeKind::Read,
557 "Read recent anonymized trades.",
558 &["books.read"],
559 Some(operation("GET", "/v2/markets/{market_id}/trades", None)),
560 ),
561 node(
562 "stream_market",
563 ActionNodeKind::Read,
564 "Subscribe to book changes, trades, and heartbeats with automatic recovery.",
565 &["books.read"],
566 Some(operation("WEBSOCKET", "/v2/markets/{market_id}/stream", None)),
567 ),
568 node(
569 "authorize_account_read",
570 ActionNodeKind::ExternalSignature,
571 "The agent owner's configured signer authorizes the exact account request or stream challenge.",
572 &[],
573 None,
574 ),
575 node(
576 "read_account",
577 ActionNodeKind::Read,
578 "Read the owner's sanitized open orders and fills for a Strata market.",
579 &["account.read"],
580 Some(operation(
581 "GET",
582 "/v2/markets/{market_id}/account/{wallet_address}",
583 None,
584 )),
585 ),
586 node(
587 "stream_account",
588 ActionNodeKind::Read,
589 "Subscribe to signed, sequenced order and fill state for the owner.",
590 &["account.read"],
591 Some(operation(
592 "WEBSOCKET",
593 "/v2/markets/{market_id}/account/{wallet_address}/stream",
594 None,
595 )),
596 ),
597 node(
598 "request_quote",
599 ActionNodeKind::Read,
600 "Request economics bound to a market, side, exact input atoms, and tolerance.",
601 &["quotes.read"],
602 Some(operation(
603 "POST",
604 "/sonar/markets/{market}/quote",
605 Some("strata_quote"),
606 )),
607 ),
608 node(
609 "request_execution_challenge",
610 ActionNodeKind::Prepare,
611 "Request canonical authorization bytes for an unexpired quote and external signer.",
612 &["trade.prepare"],
613 Some(operation(
614 "POST",
615 "/sonar/markets/{market}/execution/challenge",
616 Some("strata_execution_challenge"),
617 )),
618 ),
619 node(
620 "sign_authorization",
621 ActionNodeKind::ExternalSignature,
622 "The agent owner's configured signer signs the returned authorization bytes externally.",
623 &[],
624 None,
625 ),
626 node(
627 "prepare_execution",
628 ActionNodeKind::Prepare,
629 "Exchange the authorization signature for a quote-bound partially signed transaction.",
630 &["trade.prepare"],
631 Some(operation(
632 "POST",
633 "/sonar/markets/{market}/execution/prepare",
634 Some("strata_execution_prepare"),
635 )),
636 ),
637 node(
638 "sign_transaction",
639 ActionNodeKind::ExternalSignature,
640 "The external signer verifies and fills its signature slot without sending key material to Strata.",
641 &[],
642 None,
643 ),
644 node(
645 "submit_execution",
646 ActionNodeKind::Submit,
647 "Submit the signed transaction with an idempotency key.",
648 &["trade.submit"],
649 Some(operation(
650 "POST",
651 "/sonar/markets/{market}/execution/submit",
652 Some("strata_execution_submit"),
653 )),
654 ),
655 node(
656 "receive_receipt",
657 ActionNodeKind::Receipt,
658 "Receive the execution ID, Solana signature, and submitted status.",
659 &[],
660 None,
661 ),
662 ],
663 edges: vec![
664 edge("discover_capabilities", "discover_action_graph", "the returned contract version is supported"),
665 edge("discover_action_graph", "discover_markets", "markets.read is enabled"),
666 edge("discover_action_graph", "discover_platform_capabilities", "the versioned SDK contract is supported"),
667 edge("discover_platform_capabilities", "discover_platform_markets", "markets.read is enabled"),
668 edge("discover_platform_markets", "read_book", "books.read is enabled and the market is active"),
669 edge("discover_platform_markets", "read_market_status", "books.read is enabled"),
670 edge("discover_platform_markets", "read_best_bid_ask", "books.read is enabled"),
671 edge("discover_platform_markets", "read_fees", "books.read is enabled"),
672 edge("discover_platform_markets", "read_trades", "books.read is enabled"),
673 edge("read_book", "stream_market", "books.read is enabled and the snapshot sequence is accepted"),
674 edge("discover_platform_markets", "authorize_account_read", "account.read is enabled and the owner-configured signer is available"),
675 edge("authorize_account_read", "read_account", "the signature binds the wallet, market, request time, and fill limit"),
676 edge("read_account", "stream_account", "the stream challenge is signed by the same owner-configured signer"),
677 edge("discover_markets", "request_quote", "quotes.read is enabled and the market is ready"),
678 edge("request_quote", "request_execution_challenge", "trade.prepare is enabled and the quote is unexpired"),
679 edge("request_execution_challenge", "sign_authorization", "the challenge bindings match the quote and signer"),
680 edge("sign_authorization", "prepare_execution", "a valid external authorization signature is available"),
681 edge("prepare_execution", "sign_transaction", "the prepared transaction preserves the signed bindings"),
682 edge("sign_transaction", "submit_execution", "trade.submit is enabled and the signed transaction is unmodified"),
683 edge("submit_execution", "receive_receipt", "the execution ID and idempotency key match"),
684 ],
685 }
686 }
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692
693 #[test]
694 fn public_quote_field_set_is_sealed() {
695 let quote: QuoteResponse = serde_json::from_str(contract_fixtures::QUOTE).unwrap();
696 let value = serde_json::to_value(quote).unwrap();
697 let object = value.as_object().unwrap();
698 let mut actual = object.keys().map(String::as_str).collect::<Vec<_>>();
699 actual.sort_unstable();
700 let mut expected = vec![
701 "amount_in_atoms",
702 "amount_in_consumed_atoms",
703 "amount_out_atoms",
704 "contract_version",
705 "expires_at_ms",
706 "input_fee_atoms",
707 "market_id",
708 "minimum_output_atoms",
709 "output_fee_atoms",
710 "price_impact_pct",
711 "provider",
712 "quote_id",
713 "reference_price",
714 "schema_version",
715 "server_time_ms",
716 "side",
717 ];
718 expected.sort_unstable();
719 assert_eq!(actual, expected, "public quote fields must remain sealed");
720 assert_eq!(object["amount_out_atoms"], "1990000");
721 assert_eq!(object["minimum_output_atoms"], "1980050");
722 assert_eq!(object["provider"], "Sonar");
723 }
724
725 #[test]
726 fn reviewed_action_capabilities_are_public_and_typed() {
727 let catalog = CapabilityCatalog::foundation();
728 let prepare = catalog
729 .capabilities
730 .iter()
731 .find(|item| item.id == "trade.prepare")
732 .unwrap();
733 let submit = catalog
734 .capabilities
735 .iter()
736 .find(|item| item.id == "trade.submit")
737 .unwrap();
738 assert!(prepare.default_enabled && prepare.public_sdk);
739 assert_eq!(prepare.risk, CapabilityRisk::Prepare);
740 assert_eq!(prepare.mcp_exposure, McpExposure::Prepare);
741 assert!(submit.default_enabled && submit.public_sdk);
742 assert_eq!(submit.risk, CapabilityRisk::Submit);
743 assert_eq!(submit.mcp_exposure, McpExposure::Submit);
744 }
745
746 #[test]
747 fn shared_v1_fixtures_decode_strictly() {
748 let quote: QuoteResponse = serde_json::from_str(contract_fixtures::QUOTE).unwrap();
749 let markets: MarketsResponse = serde_json::from_str(contract_fixtures::MARKETS).unwrap();
750 let capabilities: CapabilityCatalog =
751 serde_json::from_str(contract_fixtures::CAPABILITIES).unwrap();
752 let action_graph: ActionGraph =
753 serde_json::from_str(contract_fixtures::ACTION_GRAPH).unwrap();
754
755 assert_eq!(quote.contract_version, CONTRACT_VERSION);
756 assert_eq!(markets.contract_version, CONTRACT_VERSION);
757 assert_eq!(capabilities, CapabilityCatalog::foundation());
758 assert_eq!(action_graph, ActionGraph::for_catalog(&capabilities));
759 }
760
761 #[test]
762 fn strict_contract_rejects_unreviewed_quote_fields() {
763 let mut value: serde_json::Value = serde_json::from_str(contract_fixtures::QUOTE).unwrap();
764 value
765 .as_object_mut()
766 .unwrap()
767 .insert("unexpected_field".to_owned(), serde_json::json!("hidden"));
768
769 assert!(serde_json::from_value::<QuoteResponse>(value).is_err());
770 }
771
772 #[test]
773 fn execution_contract_exposes_only_minimum_output_protection() {
774 let challenge: ExecutionChallengeResponse =
775 serde_json::from_str(contract_fixtures::EXECUTION_CHALLENGE).unwrap();
776 let prepared: ExecutionPrepareResponse =
777 serde_json::from_str(contract_fixtures::EXECUTION_PREPARE).unwrap();
778 let submitted: ExecutionSubmitResponse =
779 serde_json::from_str(contract_fixtures::EXECUTION_SUBMIT).unwrap();
780
781 assert_eq!(
782 challenge.minimum_output_atoms,
783 prepared.minimum_output_atoms
784 );
785 assert_eq!(challenge.quote_id, prepared.quote_id);
786 assert_eq!(challenge.market_id, prepared.market_id);
787 assert_eq!(submitted.execution_id, prepared.execution_id);
788
789 for fixture in [
790 contract_fixtures::EXECUTION_CHALLENGE,
791 contract_fixtures::EXECUTION_PREPARE,
792 contract_fixtures::EXECUTION_SUBMIT,
793 ] {
794 let value: serde_json::Value = serde_json::from_str(fixture).unwrap();
795 let keys = value.as_object().unwrap().keys().collect::<Vec<_>>();
796 for forbidden in [
797 "route",
798 "venue",
799 "layer",
800 "plan",
801 "collar",
802 "limit_price",
803 "internal",
804 "l3",
805 "footprint",
806 ] {
807 assert!(
808 keys.iter().all(|key| !key.contains(forbidden)),
809 "execution contract exposed forbidden field containing {forbidden}"
810 );
811 }
812 }
813 }
814}