Skip to main content

thruput_native_exec/
mock_contract.rs

1use crate::test_environment::TestEnvironment;
2
3use crate::invoke_error::invoke_wrapper;
4use sha2::{Digest, Sha256};
5use std::boxed::Box;
6use std::collections::HashMap;
7use std::panic::UnwindSafe;
8use std::rc::Rc;
9use thruput_xdr::contract::Contract;
10use thruput_xdr::contract::ContractEntrypointID;
11use thruput_xdr::types::ContractAddress;
12use thruput_xdr::types::SHA256Hash;
13use xdrs::txs;
14
15/// Backing storage for a mock contract, owned by the [`TestEnvironment`];
16/// interact with it through [`MockContract`] handles.
17pub(crate) struct MockContractInner {
18    // implicitly, this contract is Contract::kMockTest { mock_contract_id : index }
19    index: i32,
20    hash: SHA256Hash, // sha256(Contract::kMocktest(index))
21    funcs: HashMap<
22        ContractEntrypointID,
23        Box<dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync>,
24    >,
25    /// Run instead of a deterministic failure when an invoked method id
26    /// matches no entry in `funcs`. `None` (the default) keeps the
27    /// historical behavior; `thruput_sdk::register_mock_package` installs a
28    /// `LibraryExn::UnknownMethod` throw, so a mock standing in for a real
29    /// contract agrees with the rv32 guest dispatcher.
30    unknown: Option<
31        Box<dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync>,
32    >,
33}
34
35/// Handle to a mock contract registered in a [`TestEnvironment`]: a
36/// contract whose entrypoints are native closures instead of rv32 code.
37pub struct MockContract {
38    contract: *mut MockContractInner,
39    _env: Rc<TestEnvironment>,
40}
41
42fn i32_to_contract(index: i32) -> Contract {
43    Contract::kMockTest {
44        mock_contract_id: index,
45    }
46}
47
48impl MockContract {
49    /// Wrap a raw [`MockContractInner`] owned by `env`.
50    pub(crate) fn new(
51        inner: *mut MockContractInner,
52        env: Rc<TestEnvironment>,
53    ) -> Self {
54        MockContract {
55            contract: inner,
56            _env: env,
57        }
58    }
59
60    /// The `Contract::kMockTest` value identifying this mock.
61    pub fn as_contract(&self) -> Contract {
62        i32_to_contract(unsafe { (*self.contract).index })
63    }
64
65    /// The contract hash (`sha256(Contract::kMockTest(index))`).
66    pub fn as_hash(&self) -> &SHA256Hash {
67        unsafe { &(*self.contract).hash }
68    }
69
70    /// Deploy this prepared mock contract at a fixed address in test setup.
71    ///
72    /// This uses the test environment's setup path, so it should be called
73    /// before executing the transaction under test.
74    pub fn deploy_contract_to(&self, addr: &ContractAddress) {
75        self._env.setup_create_deployment(addr, &self.as_contract());
76    }
77
78    /// Register `func` as the entrypoint for method `id`. The closure runs
79    /// inside the frame's catch site, so panics and aborts out of it map to
80    /// frame outcomes.
81    ///
82    /// Panics if `id` is already registered on this mock: two methods
83    /// resolving to one dispatch ID means the second registration would
84    /// silently shadow the first, so duplicate definitions (same interface
85    /// name, major version, and method name) fail loudly at test setup
86    /// instead. There is deliberately no replace/override variant; on-chain
87    /// dispatch has no override either.
88    ///
89    /// `Send + Sync` on `func` is what keeps a mock body safe to run on the
90    /// thread the coroutine resumed on rather than the one that registered it;
91    /// see [`invoke_wrapper`]. Capturing `Rc`/`RefCell` state to record calls
92    /// is rejected here -- use `Arc`/`Mutex`, or an `AtomicUsize` for a plain
93    /// counter.
94    pub fn register_method<F>(&mut self, id: ContractEntrypointID, func: F)
95    where
96        F: Fn() + UnwindSafe + Clone + Send + Sync + 'static,
97    {
98        // Entry API so the rejected registration never touches the map: a
99        // caller that catches the panic must still observe the ORIGINAL
100        // handler on later dispatches, not the rejected replacement.
101        match unsafe { &mut (*self.contract).funcs }.entry(id) {
102            std::collections::hash_map::Entry::Occupied(_) => panic!(
103                "duplicate entrypoint registration for dispatch ID \
104                 {:016X}: two contract methods share one entrypoint ID \
105                 (same interface name, major version, and method name), so \
106                 calls would silently route to only one of them. Remove or \
107                 rename the duplicate definition.",
108                id.into_u64(),
109            ),
110            std::collections::hash_map::Entry::Vacant(slot) => {
111                slot.insert(invoke_wrapper(func));
112            }
113        }
114    }
115
116    /// Install the handler for method ids this contract does not define,
117    /// replacing any previous one.
118    ///
119    /// The registered closure runs inside the same `invoke_wrapper` catch
120    /// site as a real method, so aborting out of it (e.g. via
121    /// `thruput_sdk::exn::library_throw`) reaches the caller as an ordinary
122    /// exception rather than escaping the frame.
123    pub fn register_unknown_method<F>(&mut self, func: F)
124    where
125        F: Fn() + UnwindSafe + Clone + Send + Sync + 'static,
126    {
127        unsafe { (*self.contract).unknown = Some(invoke_wrapper(func)) };
128    }
129
130    /// Apply a registrar closure (e.g. a generated `__mock` function),
131    /// for chaining.
132    pub fn register<F: FnOnce(&mut Self)>(&mut self, f: F) -> &mut Self {
133        f(self);
134        self
135    }
136}
137
138impl MockContractInner {
139    /// Create the backing storage for mock contract number `n`.
140    pub fn new(n: i32) -> Self {
141        let mut hasher = Sha256::new();
142        let contract = i32_to_contract(n);
143        hasher.update(txs::encode(&contract).unwrap());
144        let result: [u8; 32] = hasher.finalize().into();
145        Self {
146            index: n,
147            hash: result.into(),
148            funcs: HashMap::new(),
149            unknown: None,
150        }
151    }
152
153    /// The registered entrypoint for `method`, if any.
154    pub fn get_func(
155        &self,
156        method: ContractEntrypointID,
157    ) -> Option<&(dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync)>
158    {
159        self.funcs.get(&method).map(|boxed| boxed.as_ref())
160    }
161
162    /// The handler for undefined method ids, if one was registered.
163    pub fn get_unknown_func(
164        &self,
165    ) -> Option<&(dyn Fn() -> thruput_sys::ForeignInvokeResult + Send + Sync)>
166    {
167        self.unknown.as_ref().map(|boxed| boxed.as_ref())
168    }
169}