use crate::call::RawCallError;
use crate::exn::FaultInfo;
use crate::exn::RawException;
use crate::types::transaction::InvocationFrameOutput::*;
use crate::*;
use native_exec::rv32_stack_usage::Rv32StackUsage;
use native_exec::transaction_handler::TransactionHandler;
use std::panic::UnwindSafe;
use std::vec::Vec;
use thruput_native_exec as native_exec;
use thruput_xdr::auth_claim::{AuthClaim, AuthClaim_source_address_claim};
use thruput_xdr::contract::ContractEntrypointID;
use thruput_xdr::transaction::{
Invocation, InvocationFrameStatusCode, SignedTransaction, Transaction,
Uniquifier,
};
pub fn into_root_call_result<T>(
result: &TransactResult,
) -> Result<T, RawCallError>
where
T: ReadXDROwned + ReceiveAuthHandles,
{
match result.frame().output() {
Return { return_buffer } => {
match result.exec_result() {
&ExecResult::ExecResultOK => {}
r => panic!("Transaction failed: {:?}", r),
}
crate::call::contract_call_decode::<T>(
return_buffer.as_slice(),
[],
)
.map_err(crate::call::widen_uninhabited_exn)
}
Abort { abort_buffer } => {
match RawException::from_abort_buf(abort_buffer.as_slice()) {
Ok(exn) => Err(RawCallError::Exception(exn)),
Err(e) => Err(RawCallError::UnparseableAbort(
FaultString::from_display(e),
)),
}
}
Fault => Err(RawCallError::Fault(
result
.fault_panic()
.map(FaultInfo::from)
.unwrap_or_default(),
)),
other => {
panic!("unsupported InvocationFrameOutput variant: {:?}", other)
}
}
}
#[macro_export]
macro_rules! contract_test {
($name:ident $test_body:tt) => {
#[test]
fn $name() {
let env = $crate::TestEnvironment::no_cost();
env.transact(|tx| {
let addr = <$crate::ContractAddress as $crate::Named>::named(
stringify!($name),
);
tx.call(&addr).run(|| $test_body)
})
.assert_success();
}
};
}
pub trait TestRegisterFn: Fn(&TestEnvironmentHandle) -> MockContract {}
impl<T> TestRegisterFn for T where T: Fn(&TestEnvironmentHandle) -> MockContract
{}
pub fn client_test<R, F, C>(register: R, run_test: F)
where
R: TestRegisterFn,
F: FnOnce(&C) + UnwindSafe,
C: crate::call::ContractClient,
{
static CLIENT_TEST_NONCE: SHA256Hash =
SHA256Hash::new(hash_to_array!("CLIENT_TEST_NONCE"));
let env = TestEnvironment::no_cost();
let test_contract = register(&env);
let contract_hash = *test_contract.as_hash();
env.transact(|tx| {
let caller = <ContractAddress as Named>::named("client_test");
tx.call(&caller).run(move || {
let addr = deploy_contract(&CLIENT_TEST_NONCE, &contract_hash);
let client = C::new(&addr);
run_test(&client);
})
})
.assert_success();
}
mod contract_test_guest {
use crate::SHA256Hash;
use alloc::boxed::Box;
use core::error::Error;
pub trait ContractTest {
fn run(
&self,
contract_hash: &SHA256Hash,
) -> Result<(), Box<dyn Error>>;
}
}
pub use contract_test_guest::*;
#[derive(Copy, Clone, Debug)]
pub struct Stats {
pub gas_consumed: u64,
pub rv32_stack_usage: Option<Rv32StackUsage>,
}
fn guest_test_handler<F>(
env: &TestEnvironmentHandle,
test: F,
) -> TransactionHandler
where
F: Fn() + UnwindSafe + Clone + Send + Sync + 'static,
{
let mut test_contract = env.add_contract();
let entrypoint = ContractEntrypointID::from([0; 8]);
test_contract.register_method(entrypoint, test);
let _ = env.setup_create_contract(&test_contract.as_contract());
let invoke_addr: ContractAddress = [0xFF; 32].into();
env.setup_create_deployment(&invoke_addr, &test_contract.as_contract());
let default_auth_claim: AuthClaim = AuthClaim::kSourceAddressAuthData {
source_address_claim: AuthClaim_source_address_claim {
allowed_keys: 0,
required_weight: 0,
},
};
let stx = SignedTransaction {
signatures: Vec::new().try_into().unwrap(),
tx: Transaction {
uniquifier: Uniquifier {
block_expiry_height: 0.into(),
nonce: 0,
},
auth_claim: default_auth_claim,
invocation: Invocation {
invoked_address: invoke_addr,
entrypoint,
gas_limit: u64::MAX,
calldata: [].try_into().unwrap(),
},
contracts_to_deploy: Vec::new().try_into().unwrap(),
},
pubkey_claim_key: None,
};
TransactionHandler::new(env.clone(), &stx)
}
pub fn test_rv32<P, T>(path: P, test: T) -> Stats
where
P: AsRef<std::path::Path>,
T: ContractTest + Clone + 'static + core::panic::UnwindSafe + Send + Sync,
{
let contract = load_rv32_contract(path);
let env = TestEnvironment::new(None);
TestEnvironment::enable_syscall_logging();
let contract_hash = env.setup_create_contract(&contract);
let mut handler = guest_test_handler(&env, move || -> () {
test.run(&contract_hash)
.unwrap_or_else(|e| panic!("run method failed: {}", e));
});
let res = handler.root_invoke();
if res.output.status_code() != InvocationFrameStatusCode::Return {
panic!("Root invocation panicked with code {:?}", res.output);
}
let gas_consumed = res.gas_consumed;
match handler.try_commit_transaction() {
native_exec::TryCommitResult::Ok => {}
r => panic!("Transaction failed: {:?}", r),
}
Stats {
gas_consumed,
rv32_stack_usage: res.rv32_stack_usage(),
}
}
pub fn load_rv32_contract<P>(path: P) -> Contract
where
P: AsRef<std::path::Path>,
{
use crate::contract::ContractBlob;
let raw_rv32 = std::fs::read(path.as_ref()).unwrap_or_else(|e| {
panic!(
"{}: couldn't load rv32 contract: {}",
path.as_ref().to_str().unwrap_or("?"),
e
)
});
let contract: ContractBlob = ContractBlob {
data: BytesM(raw_rv32),
};
Contract::kRv32 {
rv32_contract: contract,
}
}
pub fn deploy_address(
deployer_address: &ContractAddress,
nonce: &SHA256Hash,
contract_hash: &SHA256Hash,
) -> ContractAddress {
use sha2::{Digest, Sha256};
use thruput_xdr::contract::ContractDeploymentHashPreimage;
let preimage = ContractDeploymentHashPreimage {
deployer_address: *deployer_address,
nonce: *nonce,
contract_hash: *contract_hash,
};
let preimage: Vec<u8> = xdrs::txs::encode(&preimage).unwrap();
let mut hasher = Sha256::new();
hasher.update(preimage.as_slice());
let mut addr = [1u8; 32];
hasher.finalize_into((&mut addr).into());
addr.into()
}
#[cfg(test)]
mod tests {
use super::guest_test_handler;
use super::load_rv32_contract;
use crate::TestEnvironment;
use std::format;
use std::fs;
use std::string::String;
#[test]
fn panic_with_live_transaction_handler_survives_drop() {
let caught =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let env = TestEnvironment::new(None);
let mut handler = guest_test_handler(&env, || {});
let _ = handler.root_invoke();
panic!("original failure message");
}));
let payload = caught.expect_err("the panic must propagate, not abort");
let msg = payload
.downcast_ref::<&'static str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str));
assert_eq!(
msg,
Some("original failure message"),
"the original panic message must survive Drop cleanup"
);
}
#[test]
fn load_rv32_contract_wraps_artifact_bytes() {
let bytes = b"fake-rv32-artifact";
let path = std::env::temp_dir().join(format!(
"thruput-sdk-load-rv32-contract-{}.rv32a",
std::process::id()
));
fs::write(&path, bytes).unwrap();
let contract = load_rv32_contract(&path);
fs::remove_file(&path).unwrap();
match contract {
thruput_xdr::contract::Contract::kRv32 { rv32_contract } => {
assert_eq!(rv32_contract.data.0, bytes);
}
other => panic!("expected rv32 contract, got {other:?}"),
}
}
}