1use crate::call::RawCallError;
14use crate::exn::FaultInfo;
15use crate::exn::RawException;
16use crate::types::transaction::InvocationFrameOutput::*;
17use crate::*;
18use native_exec::rv32_stack_usage::Rv32StackUsage;
19use native_exec::transaction_handler::TransactionHandler;
20use std::panic::UnwindSafe;
21use std::vec::Vec;
22use thruput_native_exec as native_exec;
23use thruput_xdr::auth_claim::{AuthClaim, AuthClaim_source_address_claim};
24use thruput_xdr::contract::ContractEntrypointID;
25use thruput_xdr::transaction::{
26 Invocation, InvocationFrameStatusCode, SignedTransaction, Transaction,
27 Uniquifier,
28};
29
30pub fn into_root_call_result<T>(
38 result: &TransactResult,
39) -> Result<T, RawCallError>
40where
41 T: ReadXDROwned + ReceiveAuthHandles,
42{
43 match result.frame().output() {
44 Return { return_buffer } => {
45 match result.exec_result() {
48 &ExecResult::ExecResultOK => {}
49 r => panic!("Transaction failed: {:?}", r),
50 }
51 crate::call::contract_call_decode::<T>(
54 return_buffer.as_slice(),
55 [],
56 )
57 .map_err(crate::call::widen_uninhabited_exn)
58 }
59 Abort { abort_buffer } => {
62 match RawException::from_abort_buf(abort_buffer.as_slice()) {
63 Ok(exn) => Err(RawCallError::Exception(exn)),
64 Err(e) => Err(RawCallError::UnparseableAbort(
65 FaultString::from_display(e),
66 )),
67 }
68 }
69 Fault => Err(RawCallError::Fault(
76 result
77 .fault_panic()
78 .map(FaultInfo::from)
79 .unwrap_or_default(),
80 )),
81 other => {
82 panic!("unsupported InvocationFrameOutput variant: {:?}", other)
83 }
84 }
85}
86
87#[macro_export]
91macro_rules! contract_test {
92 ($name:ident $test_body:tt) => {
93 #[test]
94 fn $name() {
95 let env = $crate::TestEnvironment::no_cost();
96 env.transact(|tx| {
97 let addr = <$crate::ContractAddress as $crate::Named>::named(
98 stringify!($name),
99 );
100 tx.call(&addr).run(|| $test_body)
101 })
102 .assert_success();
103 }
104 };
105}
106
107pub trait TestRegisterFn: Fn(&TestEnvironmentHandle) -> MockContract {}
108
109impl<T> TestRegisterFn for T where T: Fn(&TestEnvironmentHandle) -> MockContract
110{}
111
112pub fn client_test<R, F, C>(register: R, run_test: F)
113where
114 R: TestRegisterFn,
115 F: FnOnce(&C) + UnwindSafe,
116 C: crate::call::ContractClient,
117{
118 static CLIENT_TEST_NONCE: SHA256Hash =
119 SHA256Hash::new(hash_to_array!("CLIENT_TEST_NONCE"));
120
121 let env = TestEnvironment::no_cost();
122 let test_contract = register(&env);
123 let contract_hash = *test_contract.as_hash();
124
125 env.transact(|tx| {
126 let caller = <ContractAddress as Named>::named("client_test");
127 tx.call(&caller).run(move || {
128 let addr = deploy_contract(&CLIENT_TEST_NONCE, &contract_hash);
130 let client = C::new(&addr);
131 run_test(&client);
132 })
133 })
134 .assert_success();
135}
136
137mod contract_test_guest {
138 use crate::SHA256Hash;
139 use alloc::boxed::Box;
140 use core::error::Error;
141
142 pub trait ContractTest {
143 fn run(
144 &self,
145 contract_hash: &SHA256Hash,
146 ) -> Result<(), Box<dyn Error>>;
147 }
148}
149
150pub use contract_test_guest::*;
151
152#[derive(Copy, Clone, Debug)]
153pub struct Stats {
154 pub gas_consumed: u64,
155 pub rv32_stack_usage: Option<Rv32StackUsage>,
158}
159
160fn guest_test_handler<F>(
164 env: &TestEnvironmentHandle,
165 test: F,
166) -> TransactionHandler
167where
168 F: Fn() + UnwindSafe + Clone + Send + Sync + 'static,
169{
170 let mut test_contract = env.add_contract();
171 let entrypoint = ContractEntrypointID::from([0; 8]);
172 test_contract.register_method(entrypoint, test);
173 let _ = env.setup_create_contract(&test_contract.as_contract());
174
175 let invoke_addr: ContractAddress = [0xFF; 32].into();
176
177 env.setup_create_deployment(&invoke_addr, &test_contract.as_contract());
178
179 let default_auth_claim: AuthClaim = AuthClaim::kSourceAddressAuthData {
180 source_address_claim: AuthClaim_source_address_claim {
181 allowed_keys: 0,
182 required_weight: 0,
183 },
184 };
185
186 let stx = SignedTransaction {
187 signatures: Vec::new().try_into().unwrap(),
188 tx: Transaction {
189 uniquifier: Uniquifier {
190 block_expiry_height: 0.into(),
191 nonce: 0,
192 },
193 auth_claim: default_auth_claim,
194 invocation: Invocation {
195 invoked_address: invoke_addr,
196 entrypoint,
197 gas_limit: u64::MAX,
198 calldata: [].try_into().unwrap(),
199 },
200 contracts_to_deploy: Vec::new().try_into().unwrap(),
201 },
202 pubkey_claim_key: None,
203 };
204
205 TransactionHandler::new(env.clone(), &stx)
206}
207
208pub fn test_rv32<P, T>(path: P, test: T) -> Stats
209where
210 P: AsRef<std::path::Path>,
211 T: ContractTest + Clone + 'static + core::panic::UnwindSafe + Send + Sync,
212{
213 let contract = load_rv32_contract(path);
214
215 let env = TestEnvironment::new(None);
217 TestEnvironment::enable_syscall_logging();
218 let contract_hash = env.setup_create_contract(&contract);
219
220 let mut handler = guest_test_handler(&env, move || -> () {
221 test.run(&contract_hash)
222 .unwrap_or_else(|e| panic!("run method failed: {}", e));
223 });
224
225 let res = handler.root_invoke();
226
227 if res.output.status_code() != InvocationFrameStatusCode::Return {
228 panic!("Root invocation panicked with code {:?}", res.output);
229 }
230
231 let gas_consumed = res.gas_consumed;
232
233 match handler.try_commit_transaction() {
236 native_exec::TryCommitResult::Ok => {}
237 r => panic!("Transaction failed: {:?}", r),
241 }
242
243 Stats {
244 gas_consumed,
245 rv32_stack_usage: res.rv32_stack_usage(),
246 }
247}
248
249pub fn load_rv32_contract<P>(path: P) -> Contract
250where
251 P: AsRef<std::path::Path>,
252{
253 use crate::contract::ContractBlob;
254
255 let raw_rv32 = std::fs::read(path.as_ref()).unwrap_or_else(|e| {
256 panic!(
257 "{}: couldn't load rv32 contract: {}",
258 path.as_ref().to_str().unwrap_or("?"),
259 e
260 )
261 });
262
263 let contract: ContractBlob = ContractBlob {
264 data: BytesM(raw_rv32),
265 };
266
267 Contract::kRv32 {
268 rv32_contract: contract,
269 }
270}
271
272pub fn deploy_address(
273 deployer_address: &ContractAddress,
274 nonce: &SHA256Hash,
275 contract_hash: &SHA256Hash,
276) -> ContractAddress {
277 use sha2::{Digest, Sha256};
278 use thruput_xdr::contract::ContractDeploymentHashPreimage;
279
280 let preimage = ContractDeploymentHashPreimage {
281 deployer_address: *deployer_address,
282 nonce: *nonce,
283 contract_hash: *contract_hash,
284 };
285 let preimage: Vec<u8> = xdrs::txs::encode(&preimage).unwrap();
286
287 let mut hasher = Sha256::new();
288 hasher.update(preimage.as_slice());
289
290 let mut addr = [1u8; 32];
291 hasher.finalize_into((&mut addr).into());
292
293 addr.into()
294}
295
296#[cfg(test)]
297mod tests {
298 use super::guest_test_handler;
299 use super::load_rv32_contract;
300 use crate::TestEnvironment;
301 use std::format;
302 use std::fs;
303 use std::string::String;
304
305 #[test]
306 fn panic_with_live_transaction_handler_survives_drop() {
307 let caught =
322 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
323 let env = TestEnvironment::new(None);
324 let mut handler = guest_test_handler(&env, || {});
325 let _ = handler.root_invoke();
328 panic!("original failure message");
329 }));
330
331 let payload = caught.expect_err("the panic must propagate, not abort");
332 let msg = payload
333 .downcast_ref::<&'static str>()
334 .copied()
335 .or_else(|| payload.downcast_ref::<String>().map(String::as_str));
336 assert_eq!(
337 msg,
338 Some("original failure message"),
339 "the original panic message must survive Drop cleanup"
340 );
341 }
342
343 #[test]
344 fn load_rv32_contract_wraps_artifact_bytes() {
345 let bytes = b"fake-rv32-artifact";
346 let path = std::env::temp_dir().join(format!(
347 "thruput-sdk-load-rv32-contract-{}.rv32a",
348 std::process::id()
349 ));
350 fs::write(&path, bytes).unwrap();
351
352 let contract = load_rv32_contract(&path);
353
354 fs::remove_file(&path).unwrap();
355 match contract {
356 thruput_xdr::contract::Contract::kRv32 { rv32_contract } => {
357 assert_eq!(rv32_contract.data.0, bytes);
358 }
359 other => panic!("expected rv32 contract, got {other:?}"),
360 }
361 }
362}