thruput_native_exec/frame_builder.rs
1use crate::frame_result::FrameResult;
2use crate::mock_transaction_handler::{
3 MockInvocationFrame, MockInvocationFrameConfig,
4};
5use std::panic::UnwindSafe;
6
7/// What an `execute` closure tells the harness to do with its frame.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum FrameAction {
10 /// Leave the frame open for further `run_continuable` steps.
11 Continue,
12 /// Close the frame, producing its [`FrameResult`].
13 Close,
14}
15
16/// Builder for configuring and executing a single invocation frame.
17pub struct FrameBuilder<'a> {
18 config: MockInvocationFrameConfig<'a>,
19}
20
21impl<'a> FrameBuilder<'a> {
22 pub(crate) fn new(config: MockInvocationFrameConfig<'a>) -> Self {
23 Self { config }
24 }
25
26 /// Set the calldata the invoked frame observes (default: empty).
27 pub fn with_calldata(self, calldata: xdr_common::BytesM) -> Self {
28 Self {
29 config: self.config.with_calldata(calldata),
30 }
31 }
32
33 /// Set the frame's invoke flags, a bitset of
34 /// [`InvokeFlag`](thruput_xdr::invoke_flag::InvokeFlag) values
35 /// (default: none).
36 pub fn with_flags(
37 self,
38 flags: thruput_xdr::invoke_flag::InvokeFlagSet,
39 ) -> Self {
40 Self {
41 config: self.config.with_invoke_flags(flags),
42 }
43 }
44
45 /// Execute the frame with the given closure, then close it.
46 ///
47 /// The closure's return value is XDR-encoded into the frame's return
48 /// buffer (the same convention generated `#[contract]` entrypoints use),
49 /// and can be recovered from the result via `assert_return_as`.
50 /// Any value that encodes to zero bytes (e.g. `()`) skips the explicit
51 /// return and leaves the buffer untouched: an empty buffer is already
52 /// the valid encoding of such types, so decoding them still works.
53 /// Separately, closures that call `raw_Return` themselves never reach
54 /// this encode step at all, since `raw_Return` terminates the
55 /// invocation immediately (it never returns).
56 ///
57 /// Mixing an explicit `raw_Return` with a non-unit closure return is
58 /// unsupported; the explicit buffer wins and the closure's value is
59 /// never encoded.
60 ///
61 /// The return buffer is subject to the protocol's return-size cap
62 /// (1024 bytes); a larger encoded value deterministically reverts the
63 /// invocation, surfacing as an opaque frame failure. Keep test return
64 /// values small — return a digest or summary instead of bulk data.
65 ///
66 /// If you need to open child frames from within the closure, use
67 /// `run_continuable` instead, which provides a `FrameHandle` with
68 /// `child_call()`.
69 pub fn run<T: xdrs::WriteXDR>(
70 self,
71 f: impl FnOnce() -> T + UnwindSafe,
72 ) -> FrameResult {
73 let frame = self.config.build();
74 frame.execute(|_| {
75 let value = f();
76 let encoded = xdrs::cxs::encode(&value)
77 .expect("run(): failed to XDR-encode closure return value");
78 if encoded.is_empty() {
79 // Zero-byte encodings (e.g. `()`): an empty buffer is
80 // already the valid encoding, so close normally with no
81 // explicit return.
82 FrameAction::Close
83 } else {
84 // Sets the return buffer and diverges; the frame closes
85 // via the unwind path, equivalently to FrameAction::Close.
86 thruput_xdr::syscalls::raw_Return(&encoded, false, &[])
87 }
88 });
89 frame.result()
90 }
91
92 /// Execute the frame with a closure that receives a FrameHandle
93 /// and returns a FrameAction. If FrameAction::Continue, returns
94 /// a ContinuableFrame for further chaining.
95 ///
96 /// To return a typed value from a continuable frame, end the closing
97 /// step with an explicit `raw_Return` — it sets the buffer and
98 /// diverges (closing the frame), and `!` coerces to `FrameAction`:
99 ///
100 /// ```rust,ignore
101 /// let result = tx.call(&addr)
102 /// .run_continuable(|frame| {
103 /// // ... child calls, multi-step work ...
104 /// raw_Return(&xdrs::cxs::encode(&value).unwrap(), false, &[])
105 /// })
106 /// .finish();
107 /// let v: T = result.assert_return_as();
108 /// ```
109 pub fn run_continuable(
110 self,
111 f: impl FnOnce(&mut FrameHandle) -> FrameAction + UnwindSafe,
112 ) -> ContinuableFrame<'a> {
113 let frame = self.config.build();
114 frame.execute(|inner_frame| {
115 let mut handle = FrameHandle { frame: inner_frame };
116 f(&mut handle)
117 });
118 ContinuableFrame { frame }
119 }
120}
121
122/// Handle to the current frame, available inside run_continuable closures.
123/// Provides child_call() for opening nested frames.
124pub struct FrameHandle<'a> {
125 frame: &'a mut MockInvocationFrame,
126}
127
128impl<'a> FrameHandle<'a> {
129 /// Open a child frame of the current frame, invoking `addr`. The
130 /// address must differ from every frame already open on the stack (the
131 /// reentrance rule); see [`MockInvocationFrame::open_child_frame`].
132 pub fn child_call(
133 &mut self,
134 addr: &thruput_xdr::types::ContractAddress,
135 ) -> FrameBuilder<'_> {
136 let config = self.frame.open_child_frame(*addr);
137 FrameBuilder::new(config)
138 }
139}
140
141/// A frame that is still open after run_continuable returned FrameAction::Continue.
142/// Supports chaining additional run_continuable() calls or finishing.
143pub struct ContinuableFrame<'a> {
144 frame: &'a mut Box<MockInvocationFrame>,
145}
146
147impl<'a> ContinuableFrame<'a> {
148 /// Execute another step on this frame.
149 pub fn run_continuable(
150 &mut self,
151 f: impl FnOnce(&mut FrameHandle) -> FrameAction + UnwindSafe,
152 ) -> &mut Self {
153 self.frame.execute(|inner_frame| {
154 let mut handle = FrameHandle { frame: inner_frame };
155 f(&mut handle)
156 });
157 self
158 }
159
160 /// Open a child frame from within this continuable frame.
161 pub fn child_call(
162 &mut self,
163 addr: &thruput_xdr::types::ContractAddress,
164 ) -> FrameBuilder<'_> {
165 let config = self.frame.open_child_frame(*addr);
166 FrameBuilder::new(config)
167 }
168
169 /// Extract the frame result. Call this after the frame has been closed
170 /// (i.e., after a run_continuable that returned FrameAction::Close).
171 pub fn finish(&mut self) -> FrameResult {
172 self.frame.result()
173 }
174}