use ootle_byte_type::ToByteType;
use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey};
use tari_engine::runtime::{ActionIdent, RuntimeError};
use tari_ootle_transaction::{Transaction, args};
use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS;
use tari_template_lib::types::{Amount, access_rules::ComponentAccessRules, constants::TARI_TOKEN, rule};
use tari_template_test_tooling::{
TemplateTest,
support::assert_error::{assert_access_denied_for_action, assert_reject_reason},
xtr_faucet_component,
};
const CRATE_PATH: &str = env!("CARGO_MANIFEST_DIR");
#[test]
fn basic_faucet_transfer() {
let mut template_test = TemplateTest::new(CRATE_PATH, Vec::<&str>::new());
let (sender_address, sender_proof, _) = template_test.create_funded_account();
let (receiver_address, _, _) = template_test.create_empty_account();
let result = template_test
.build_and_execute(
Transaction::builder_localnet()
.call_method(sender_address, "withdraw", args![TARI_TOKEN, 100])
.put_last_instruction_output_on_workspace("foo_bucket")
.call_method(receiver_address, "deposit", args![Workspace("foo_bucket")])
.call_method(sender_address, "balance", args![TARI_TOKEN])
.call_method(receiver_address, "balance", args![TARI_TOKEN]),
vec![sender_proof],
)
.unwrap_success();
assert_eq!(
result.finalize.execution_results[3].decode::<Amount>().unwrap(),
Amount::from(999_999_900u64)
);
assert_eq!(
result.finalize.execution_results[4].decode::<Amount>().unwrap(),
Amount::from(100u64)
);
}
#[test]
fn withdraw_from_account_prevented() {
let mut test = TemplateTest::new(CRATE_PATH, Vec::<&str>::new());
let (source_account, _, _) = test.create_funded_account();
let (dest_address, non_owning_token, non_owning_key) = test.create_empty_account();
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.call_method(source_account, "withdraw", args![TARI_TOKEN, 100])
.put_last_instruction_output_on_workspace("stolen_coins")
.call_method(dest_address, "deposit", args![Workspace("stolen_coins")])
.build_and_seal(&non_owning_key),
vec![non_owning_token],
);
assert_access_denied_for_action(reason, ActionIdent::ComponentCallMethod {
component_address: source_account,
method: "withdraw".to_string(),
});
let vaults = test
.read_only_state_store()
.get_vaults_for_account(dest_address)
.unwrap();
assert!(!vaults.contains_key(&TARI_TOKEN));
}
#[test]
fn attempt_to_overwrite_account() {
let mut test = TemplateTest::new_builtin_only();
let (source_account, source_account_proof, source_account_sk) = test.create_funded_account();
let null: Option<()> = None;
let overwriting_tx = test.execute_expect_failure(
Transaction::builder_localnet()
.call_function(
ACCOUNT_TEMPLATE_ADDRESS,
"create",
args![source_account_proof, null, null, null],
)
.build_and_seal(&source_account_sk),
vec![source_account_proof],
);
assert_reject_reason(overwriting_tx, RuntimeError::ComponentAlreadyExists {
address: source_account,
});
let store = test.read_only_state_store();
let account = store.get_account(source_account).unwrap();
let vault = account.get_vault_by_resource(&TARI_TOKEN).unwrap();
let vault = store.get_vault(&vault.vault_id()).expect("no vaults");
assert_eq!(vault.balance(), TemplateTest::FUNDED_ACCOUNT_INITIAL_BALANCE);
}
#[test]
fn create_account_is_idempotent() {
let mut test = TemplateTest::new_builtin_only();
let (source_account, source_account_proof, source_account_sk) = test.create_funded_account();
let source_account_pk = RistrettoPublicKey::from_secret_key(&source_account_sk);
let result = test.execute_expect_success(
Transaction::builder_localnet()
.create_account(source_account_pk.to_byte_type())
.build_and_seal(&source_account_sk),
vec![source_account_proof],
);
assert!(
result
.finalize
.events
.iter()
.all(|e| e.topic() != "std.component.created")
);
let store = test.read_only_state_store();
let account = store.get_account(source_account).unwrap();
let vault = account.get_vault_by_resource(&TARI_TOKEN).unwrap();
let vault = store.get_vault(&vault.vault_id()).expect("no vaults");
assert_eq!(vault.balance(), TemplateTest::FUNDED_ACCOUNT_INITIAL_BALANCE);
}
#[test]
fn create_account_is_idempotent_with_deposit() {
let mut test = TemplateTest::new_builtin_only();
let (source_account, source_account_proof, source_account_sk) = test.create_empty_account();
let source_account_pk = RistrettoPublicKey::from_secret_key(&source_account_sk);
let result = test.execute_expect_success(
Transaction::builder_localnet()
.create_account(source_account_pk.to_byte_type())
.put_last_instruction_output_on_workspace("account")
.call_method(xtr_faucet_component(), "take", args![Workspace("account")])
.build_and_seal(&source_account_sk),
vec![source_account_proof],
);
assert!(
result
.finalize
.events
.iter()
.all(|e| e.topic() != "std.component.created")
);
let store = test.read_only_state_store();
let account = store.get_account(source_account).unwrap();
let vault = account.get_vault_by_resource(&TARI_TOKEN).unwrap();
let vault = store.get_vault(&vault.vault_id()).unwrap();
assert_eq!(vault.balance(), 1_000_000_000u64);
}
#[test]
fn gasless() {
let mut test = TemplateTest::new_builtin_only();
test.enable_fees();
let (fee_account, fee_account_proof, fee_account_sk) = test.create_funded_account();
let (user_account, user_account_proof, user_account_sk) = test.create_funded_account();
let (user2_account, _, _) = test.create_empty_account();
let fee_account_pk = RistrettoPublicKey::from_secret_key(&fee_account_sk);
test.execute_expect_success(
Transaction::builder_localnet()
.pay_fee_from_component(fee_account, 1000u64)
.call_method(user_account, "withdraw", args![TARI_TOKEN, 100])
.put_last_instruction_output_on_workspace("b")
.call_method(user2_account, "deposit", args![Workspace("b")])
.finish()
.add_signer(&fee_account_pk.to_byte_type(), &user_account_sk)
.seal(&fee_account_sk),
vec![fee_account_proof, user_account_proof],
);
let vaults = test
.read_only_state_store()
.get_vaults_for_account(user2_account)
.unwrap();
assert_eq!(vaults.get(&TARI_TOKEN).unwrap().balance(), 100u64);
}
#[test]
fn custom_access_rules() {
let mut test = TemplateTest::new_builtin_only();
let (owner_proof, public_key, secret_key) = test.create_owner_proof();
let access_rules = ComponentAccessRules::new()
.add_method_rule("balance", rule!(allow_all))
.add_method_rule("get_balances", rule!(allow_all))
.add_method_rule("deposit", rule!(allow_all))
.default(rule!(allow_all));
test.execute_expect_success(
Transaction::builder_localnet()
.create_account_custom::<&str>(
public_key.to_byte_type(),
None,
Some(access_rules),
None
)
.put_last_instruction_output_on_workspace("account")
.call_method(xtr_faucet_component(), "take", args![Workspace("account")])
.build_and_seal(&secret_key),
vec![owner_proof],
);
let user_account = *test
.read_only_state_store()
.all_accounts()
.unwrap()
.keys()
.next()
.unwrap();
let (user2_account, user2_account_proof, user2_secret_key) = test.create_funded_account();
test.execute_expect_success(
Transaction::builder_localnet()
.call_method(user_account, "withdraw", args![TARI_TOKEN, 100])
.put_last_instruction_output_on_workspace("b")
.call_method(user2_account, "deposit", args![Workspace("b")])
.build_and_seal(&user2_secret_key),
vec![user2_account_proof],
);
}
#[test]
fn take_from_bucket() {
let mut test = TemplateTest::new(CRATE_PATH, Vec::<&str>::new());
let (alice, _proof, alice_sk) = test.create_funded_account();
let (bob, _proof, _bob_sk) = test.create_empty_account();
test.execute_expect_success(
Transaction::builder_localnet()
.call_method(alice, "withdraw_all", args![TARI_TOKEN])
.put_last_instruction_output_on_workspace("coins")
.take_from_bucket("coins", 100u64, "foo_bucket")
.take_from_bucket("coins", 999_999_900u64, "bar_bucket")
.call_method(alice, "deposit", args![Workspace("foo_bucket")])
.call_method(bob, "deposit", args![Workspace("bar_bucket")])
.add_signer(&test.to_public_key_bytes(), &alice_sk)
.seal(test.secret_key()),
vec![],
);
let alice_acc = test.read_only_state_store().get_account(alice).unwrap();
let bob_acc = test.read_only_state_store().get_account(bob).unwrap();
let alice_balance = test
.read_only_state_store()
.get_vault(&alice_acc.get_vault_by_resource(&TARI_TOKEN).unwrap().vault_id())
.unwrap()
.balance();
let bob_balance = test
.read_only_state_store()
.get_vault(&bob_acc.get_vault_by_resource(&TARI_TOKEN).unwrap().vault_id())
.unwrap()
.balance();
assert_eq!(alice_balance, 100u64);
assert_eq!(bob_balance, 999_999_900u64);
}
#[test]
fn put_into_bucket_merges_same_resource() {
let mut test = TemplateTest::new(CRATE_PATH, Vec::<&str>::new());
let (alice, _proof, alice_sk) = test.create_funded_account();
let (bob, _proof, _bob_sk) = test.create_empty_account();
test.execute_expect_success(
Transaction::builder_localnet()
.call_method(alice, "withdraw", args![TARI_TOKEN, 600u64])
.put_last_instruction_output_on_workspace("first")
.call_method(alice, "withdraw", args![TARI_TOKEN, 400u64])
.put_last_instruction_output_on_workspace("second")
.put_into_bucket("second", "first")
.call_method(bob, "deposit", args![Workspace("first")])
.add_signer(&test.to_public_key_bytes(), &alice_sk)
.seal(test.secret_key()),
vec![],
);
let bob_acc = test.read_only_state_store().get_account(bob).unwrap();
let bob_balance = test
.read_only_state_store()
.get_vault(&bob_acc.get_vault_by_resource(&TARI_TOKEN).unwrap().vault_id())
.unwrap()
.balance();
assert_eq!(bob_balance, 1000u64);
}
#[test]
fn put_into_bucket_rejects_resource_mismatch() {
let mut test = TemplateTest::new(CRATE_PATH, vec!["tests/templates/faucet"]);
let faucet_template = test.get_template_address("TestFaucet");
let (alice, _alice_proof, alice_sk) = test.create_funded_account();
test.execute_expect_success(
Transaction::builder_localnet()
.allocate_component_address("faucet_address")
.call_function(faucet_template, "mint_with_opts", args![
Amount::from(1_000_000u64),
"TT".to_string(),
Workspace("faucet_address")
])
.call_method("faucet_address", "take_free_coins", args![])
.put_last_instruction_output_on_workspace("free_coins")
.call_method(alice, "deposit", args![Workspace("free_coins")])
.build_and_seal(test.secret_key()),
vec![],
);
let resources = test.read_only_state_store().get_all_resources().unwrap();
let (tt_resource, _) = resources
.into_iter()
.find(|(_, r)| r.token_symbol() == Some("TT"))
.expect("TT resource not found");
let reason = test.execute_expect_failure(
Transaction::builder_localnet()
.call_method(alice, "withdraw", args![TARI_TOKEN, 100u64])
.put_last_instruction_output_on_workspace("tari_bucket")
.call_method(alice, "withdraw", args![tt_resource, 100u64])
.put_last_instruction_output_on_workspace("tt_bucket")
.put_into_bucket("tt_bucket", "tari_bucket")
.add_signer(&test.to_public_key_bytes(), &alice_sk)
.seal(test.secret_key()),
vec![],
);
assert!(
format!("{reason}").contains("Resource addresses do not match"),
"expected ResourceAddressMismatch, got: {reason}"
);
}