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