use tari_crypto::ristretto::RistrettoSecretKey;
use tari_engine::runtime::{ActionIdent, RuntimeError};
use tari_engine_types::{
commit_result::{ExecuteResult, RejectReason},
limits,
};
use tari_ootle_transaction::{Transaction, args};
use tari_template_lib::types::{Amount, ComponentAddress, ResourceAddress, TemplateAddress};
use tari_template_test_tooling::{
TemplateTest,
support::assert_error::{assert_access_denied_for_action, assert_reject_reason},
};
const CRATE_PATH: &str = env!("CARGO_MANIFEST_DIR");
const TEMPLATE_NAME: &str = "CrossTemplate";
struct CrossTemplateTest {
template_test: TemplateTest,
cross_call_template: TemplateAddress,
state_template: TemplateAddress,
}
impl CrossTemplateTest {
fn secret_key(&self) -> &RistrettoSecretKey {
self.template_test.secret_key()
}
}
struct ComponentInfo {
cross_template_component: ComponentAddress,
state_component: ComponentAddress,
}
fn setup() -> CrossTemplateTest {
let template_test = TemplateTest::new(CRATE_PATH, vec![
"tests/templates/cross_template",
"tests/templates/state",
"tests/templates/faucet",
]);
let cross_call_template = template_test.get_template_address(TEMPLATE_NAME);
let state_template = template_test.get_template_address("State");
CrossTemplateTest {
template_test,
cross_call_template,
state_template,
}
}
fn initialize_composability(test: &mut CrossTemplateTest) -> ComponentInfo {
let res = test.template_test.execute_expect_success(
test.template_test
.transaction()
.call_function(test.cross_call_template, "new", args![test.state_template])
.build_and_seal(test.secret_key()),
vec![],
);
let template_addr = test.template_test.get_template_address(TEMPLATE_NAME);
let state_template_addr = test.template_test.get_template_address("State");
let composability_component = extract_component_address_from_result(&res, &template_addr);
let state_component = extract_component_address_from_result(&res, &state_template_addr);
ComponentInfo {
cross_template_component: composability_component,
state_component,
}
}
fn extract_component_address_from_result(result: &ExecuteResult, template_addr: &TemplateAddress) -> ComponentAddress {
let (substate_addr, _) = result
.expect_success()
.up_iter()
.find(|(address, substate)| {
address.is_component() && substate.substate_value().component().unwrap().template_address() == template_addr
})
.unwrap();
substate_addr.as_component_address().unwrap()
}
fn create_resource_and_fund_account(test: &mut TemplateTest, account: ComponentAddress) -> ResourceAddress {
let faucet_template = test.get_template_address("TestFaucet");
let initial_supply = Amount::from(1_000_000_000_000u64);
test.execute_expect_success(
Transaction::builder_localnet()
.allocate_component_address("faucet_address")
.call_function(faucet_template, "mint_with_opts", args![
initial_supply,
"TT".to_string(),
Workspace("faucet_address")
])
.call_method("faucet_address", "take_free_coins", args![])
.put_last_instruction_output_on_workspace("free_coins")
.call_method(account, "deposit", args![Workspace("free_coins")])
.finish()
.seal(test.secret_key()),
vec![],
);
let resources = test.read_only_state_store().get_all_resources().unwrap();
let (faucet_resource, _) = resources
.into_iter()
.find(|(_, r)| r.token_symbol() == Some("TT"))
.expect("Fungible resource not found");
faucet_resource
}
#[test]
fn it_allows_function_to_function_calls() {
let mut test = setup();
let components = initialize_composability(&mut test);
let value: u32 = test
.template_test
.call_method(components.state_component, "get", args![], vec![]);
assert_eq!(value, 0);
}
#[test]
fn it_allows_function_to_method_calls() {
let mut test = setup();
let components = initialize_composability(&mut test);
let composability_component_0 = components.cross_template_component;
let res = test.template_test.execute_expect_success(
Transaction::builder_localnet()
.allocate_component_address("component_1")
.call_function(test.cross_call_template, "new_from_component", args![
Workspace("component_1"),
composability_component_0
])
.call_method("component_1", "get_state_component_address", args![])
.finish()
.seal(test.secret_key()),
vec![],
);
let inner_component_address = res
.finalize
.execution_results
.last()
.unwrap()
.decode::<ComponentAddress>()
.unwrap();
assert_eq!(inner_component_address, components.state_component);
}
#[test]
fn it_allows_method_to_method_calls() {
let mut test = setup();
let components = initialize_composability(&mut test);
test.template_test.call_method::<()>(
components.cross_template_component,
"increase_inner_state_component",
args![],
vec![],
);
let value: u32 = test
.template_test
.call_method(components.state_component, "get", args![], vec![]);
assert_eq!(value, 1);
}
#[test]
fn it_allows_method_to_function_calls() {
let mut test = setup();
let components = initialize_composability(&mut test);
let initial_state_component = components.state_component;
test.template_test.call_method::<()>(
components.cross_template_component,
"increase_inner_state_component",
args![],
vec![],
);
let value: u32 = test
.template_test
.call_method(initial_state_component, "get", args![], vec![]);
assert_eq!(value, 1);
test.template_test.call_method::<()>(
components.cross_template_component,
"replace_state_component",
args![test.state_template],
vec![],
);
let new_state_component: ComponentAddress = test.template_test.call_method(
components.cross_template_component,
"get_state_component_address",
args![],
vec![],
);
assert_ne!(new_state_component, initial_state_component);
let value: u32 = test
.template_test
.call_method(new_state_component, "get", args![], vec![]);
assert_eq!(value, 0);
}
#[test]
fn it_fails_on_invalid_calls() {
let mut test = setup();
let components = initialize_composability(&mut test);
let (_, _, private_key) = test.template_test.create_empty_account();
let result = test
.template_test
.try_execute(
Transaction::builder_localnet()
.call_method(
components.cross_template_component,
"call_method_that_does_not_exist",
args![],
)
.build_and_seal(&private_key),
vec![],
)
.unwrap();
let reason = result.expect_failure();
assert!(matches!(reason, RejectReason::ExecutionFailure(_)));
}
#[test]
fn it_does_not_propagate_permissions() {
let mut test = setup();
let components = initialize_composability(&mut test);
let (attacker_proof, attacker_key) = test.template_test.get_test_proof_and_secret_key();
let (victim_account, victim_proof, _) = test.template_test.create_empty_account();
let fungible_resource = create_resource_and_fund_account(&mut test.template_test, victim_account);
let result = test
.template_test
.try_execute(
Transaction::builder_localnet()
.call_method(components.cross_template_component, "malicious_withdraw", args![
victim_account,
fungible_resource,
100
])
.build_and_seal(&attacker_key),
vec![attacker_proof, victim_proof],
)
.unwrap();
let reason = result.expect_failure();
assert_access_denied_for_action(reason, ActionIdent::ComponentCallMethod {
component_address: victim_account,
method: "withdraw".to_string(),
});
}
#[test]
fn it_allows_multiple_recursion_levels() {
let mut test = setup();
let mut components = initialize_composability(&mut test);
let composability_0 = components.cross_template_component;
components = initialize_composability(&mut test);
let composability_1 = components.cross_template_component;
test.template_test.call_method::<()>(
composability_1,
"set_nested_composability",
args![composability_0],
vec![],
);
let value: u32 = test
.template_test
.call_method(composability_1, "get_nested_value", args![], vec![]);
assert_eq!(value, 0);
}
#[test]
fn it_fails_when_surpassing_recursion_limit_with_many_nested_components() {
let mut test = setup();
let (_, _, private_key) = test.template_test.create_empty_account();
let max_call_depth = limits::ENGINE_LIMITS.max_call_depth;
let mut components = initialize_composability(&mut test);
let mut last_composability_component = components.cross_template_component;
for _ in 0..max_call_depth {
components = initialize_composability(&mut test);
test.template_test.call_method::<()>(
components.cross_template_component,
"set_nested_composability",
args![last_composability_component],
vec![],
);
last_composability_component = components.cross_template_component;
}
let result = test
.template_test
.try_execute(
Transaction::builder_localnet()
.call_method(last_composability_component, "get_nested_value", args![])
.build_and_seal(&private_key),
vec![],
)
.unwrap();
let reason = result.expect_failure();
assert_reject_reason(reason, RuntimeError::MaxCallDepthExceeded {
max_depth: max_call_depth,
});
}
#[test]
fn it_fails_when_surpassing_recursion_limit() {
let mut test = setup();
let (_, _, private_key) = test.template_test.create_empty_account();
let max_call_depth = limits::ENGINE_LIMITS.max_call_depth;
let components = initialize_composability(&mut test);
test.template_test.execute_expect_success(
Transaction::builder_localnet()
.call_method(components.cross_template_component, "recursion", args![
max_call_depth - 1
])
.build_and_seal(&private_key),
vec![],
);
let reason = test.template_test.execute_expect_failure(
Transaction::builder_localnet()
.call_method(components.cross_template_component, "recursion", args![max_call_depth])
.build_and_seal(&private_key),
vec![],
);
assert_reject_reason(reason, RuntimeError::MaxCallDepthExceeded {
max_depth: max_call_depth,
});
}