Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ ci-fmt-fix = "fmt --all"
ci-clippy = "lints clippy --all-targets --all-features"
ci-test-compile = "test --no-run --workspace --all-features --no-default-features"
ci-test = "nextest run --all-features --release --workspace --exclude integration-tests --no-fail-fast"
ci-cucumber = "test --release --test cucumber --package integration-tests"
Copy link
Contributor

@martinserts martinserts Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--test cucumbe(r) ?

ci-cucumber = "test --release --test cucumber"
ci-check = "check --release --all-targets --workspace --exclude integration-tests --locked"
ci-lcheck = "lcheck --release --all-targets --workspace --exclude integration-tests --locked"
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ jobs:
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Build
run: cargo build --all-targets --all-features
run: cargo build --release --all-targets --all-features

- name: Run tests
run: cargo test --all-targets --all-features
run: cargo test --release --all-targets --all-features --workspace --exclude integration-tests

- name: Run integration tests
run: cargo test --release --test cucumber

- name: Check unused dependencies
run: cargo machete
3 changes: 2 additions & 1 deletion integration-tests/features/fund_locking.feature
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ Feature: Fund Locking
Given I have a test database with an existing wallet
And the wallet has sufficient balance
When I lock funds with "3" outputs
Then "3" UTXOs should be locked
Then the UTXOs should be marked as locked

Scenario: Lock funds with custom duration
Given I have a test database with an existing wallet
And the wallet has sufficient balance
When I lock funds with duration "7200" seconds
Then the UTXOs should be locked for "7200" seconds

@pie
Scenario: Lock funds with insufficient balance
Given I have a test database with an existing wallet
And the wallet has zero balance
Expand Down
7 changes: 0 additions & 7 deletions integration-tests/features/mining.feature
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,3 @@ Feature: Mining and Blockchain
When I mine 3 blocks on MinerNode
And I mine 2 blocks on MinerNode
Then the chain height should be 5

Scenario: Sync between two nodes
Given I have a seed node SeedNode
And I have a base node RegularNode connected to all seed nodes
When I mine 10 blocks on SeedNode
Then SeedNode should be at height 10
And RegularNode should be at height 10
9 changes: 2 additions & 7 deletions integration-tests/features/transactions.feature
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ Feature: Transaction Creation
Given I have a test database with an existing wallet
And the wallet has sufficient balance
When I create an unsigned transaction with one recipient
Then the transaction file should be created
And the transaction should include the recipient
Then the transaction should include the recipient
And the inputs should be locked

Scenario: Create transaction with multiple recipients
Expand All @@ -18,6 +17,7 @@ Feature: Transaction Creation
Then the transaction should include all recipients
And the total amount should be correct


Scenario: Create transaction with payment ID
Given I have a test database with an existing wallet
And the wallet has sufficient balance
Expand All @@ -31,8 +31,3 @@ Feature: Transaction Creation
Then the transaction creation should fail
And I should see an insufficient balance error

Scenario: Create transaction with custom lock duration
Given I have a test database with an existing wallet
And the wallet has sufficient balance
When I create an unsigned transaction with lock duration "3600" seconds
Then the inputs should be locked for "3600" seconds
2 changes: 1 addition & 1 deletion integration-tests/steps/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ async fn setup_database(world: &mut MinotariWorld) {
}

#[given("I have a test database with an existing wallet")]
async fn database_with_wallet(world: &mut MinotariWorld) {
pub async fn database_with_wallet(world: &mut MinotariWorld) {
world.setup_database();
let db_path = world.database_path.as_ref().expect("Database not set up");
let (cmd, mut args) = world.get_minotari_command();
Expand Down
55 changes: 44 additions & 11 deletions integration-tests/steps/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,34 @@
use cucumber::{given, then, when};
use std::process::Stdio;
use std::time::Duration;
use tari_common::configuration::Network::LocalNet;
use tari_common_types::tari_address::{TariAddress, TariAddressFeatures};
use tokio::time::sleep;

use super::common::MinotariWorld;
use super::common::{MinotariWorld, database_with_wallet};

/// Generate a valid test Tari address from the wallet in world
fn generate_test_address(world: &MinotariWorld) -> String {
let spend_key = world.wallet.get_public_spend_key();
let view_key = world.wallet.get_public_view_key();
let wallet_address = TariAddress::new_dual_address(
view_key,
spend_key,
LocalNet,
TariAddressFeatures::create_one_sided_only(),
None,
)
.unwrap();
wallet_address.to_base58().to_string()
}

// =============================
// Helper Functions
// =============================

/// Start a daemon process with the given configuration
async fn start_daemon_process(world: &mut MinotariWorld, port: u16, scan_interval: Option<u64>) {
world.setup_database();
let (command, mut args) = world.get_minotari_command();

let db_path = world
Expand Down Expand Up @@ -63,6 +81,8 @@ async fn start_daemon_process(world: &mut MinotariWorld, port: u16, scan_interva

#[given("I have a running daemon with an existing wallet")]
async fn running_daemon_with_wallet(world: &mut MinotariWorld) {
// Import a wallet so the daemon has an account to query
database_with_wallet(world).await;
// Start daemon on default port 9000
start_daemon_process(world, 9000, None).await;
}
Expand Down Expand Up @@ -107,7 +127,7 @@ async fn lock_funds_api(world: &mut MinotariWorld, amount: String) {

let amount_num = amount.parse::<u64>().expect("Invalid amount");
let request_body = serde_json::json!({
"amount_microtari": amount_num,
"amount": amount_num,
"idempotency_key": format!("test_lock_{}", chrono::Utc::now().timestamp())
});

Expand All @@ -131,14 +151,13 @@ async fn create_transaction_api(world: &mut MinotariWorld) {
let port = world.api_port.expect("Daemon must be running");
let url = format!("http://127.0.0.1:{}/accounts/default/create_unsigned_transaction", port);

// Create a dummy transaction request
let address = generate_test_address(world);
let request_body = serde_json::json!({
"recipients": [{
"address": "5CKLWUeJH9dZhH8WJnPJc7fV6XHnYNYpFT8YgMHunvFBXj3bZvW8M1TGVvdvP8n4wJV8LF9HxYv7fV8H",
"amount_microtari": 1000000,
"message": "Test transaction"
"address": address,
"amount": 100000,
"payment_id": "test-payment"
}],
"fee_per_gram": 5,
"idempotency_key": format!("test_tx_{}", chrono::Utc::now().timestamp())
});

Expand All @@ -153,6 +172,13 @@ async fn create_transaction_api(world: &mut MinotariWorld) {
let status = response.status();
let body = response.text().await.expect("Failed to read response body");

// Store response in transaction_data for subsequent step assertions
if status.is_success()
&& let Ok(json) = serde_json::from_str::<serde_json::Value>(&body)
{
world.transaction_data.insert("current".to_string(), json);
}

world.last_command_output = Some(body);
world.last_command_exit_code = Some(if status.is_success() { 0 } else { 1 });
}
Expand Down Expand Up @@ -296,8 +322,8 @@ async fn response_has_balance_info(world: &mut MinotariWorld) {
let json: serde_json::Value = serde_json::from_str(output).expect("Response should be valid JSON");

assert!(
json.get("balance_microtari").is_some() || json.get("available_balance_microtari").is_some(),
"Response should include balance information"
json.get("available").is_some() || json.get("total").is_some(),
"Response should include balance information (available or total field)"
);
}

Expand All @@ -312,13 +338,20 @@ async fn api_returns_success(world: &mut MinotariWorld) {

#[then("the API should return the unsigned transaction")]
async fn api_returns_transaction(world: &mut MinotariWorld) {
assert_eq!(
world.last_command_exit_code,
Some(0),
"API should return success for unsigned transaction"
);

let output = world.last_command_output.as_ref().expect("Should have response output");

let json: serde_json::Value = serde_json::from_str(output).expect("Response should be valid JSON");

// PrepareOneSidedTransactionForSigningResult has fields: version, tx_id, info
assert!(
json.get("transaction").is_some() || json.get("unsigned_transaction").is_some(),
"Response should include transaction data"
json.get("tx_id").is_some() || json.get("info").is_some(),
"Response should include transaction data (tx_id or info field)"
);
}

Expand Down
11 changes: 8 additions & 3 deletions integration-tests/steps/fund_locking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@ fn execute_lock_funds(
) {
let db_path = world.database_path.as_ref().expect("Database not set up");
let output_file = world.get_temp_path("locked_funds.json");
world.output_file = Some(output_file.clone());

let (cmd, mut args) = world.get_minotari_command();
args.extend_from_slice(&[
"lock-funds".to_string(),
"--database-path".to_string(),
db_path.to_str().unwrap().to_string(),
"--password".to_string(),
world.test_password.clone(),
"--account-name".to_string(),
"default".to_string(),
"--amount".to_string(),
Expand Down Expand Up @@ -58,7 +57,13 @@ fn execute_lock_funds(
world.last_command_exit_code = Some(output.status.code().unwrap_or(-1));
world.last_command_output = Some(String::from_utf8_lossy(&output.stdout).to_string());
world.last_command_error = Some(String::from_utf8_lossy(&output.stderr).to_string());
world.output_file = Some(output_file);

// Only parse the JSON file if the command succeeded
if output.status.success() && output_file.exists() {
let content = std::fs::read_to_string(&output_file).expect("Failed to read transaction file");
let json: serde_json::Value = serde_json::from_str(&content).expect("Failed to parse transaction JSON");
world.locked_funds.insert("latest".to_string(), json);
}
}

#[when(regex = r#"^I lock funds for amount "([^"]*)" microTari$"#)]
Expand Down
79 changes: 40 additions & 39 deletions integration-tests/steps/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,28 @@ fn execute_create_transaction(world: &mut MinotariWorld, recipients: Vec<String>
.args(&args)
.output()
.expect("Failed to execute create-unsigned-transaction command");

world.last_command_exit_code = output.status.code();
world.last_command_output = Some(String::from_utf8_lossy(&output.stdout).to_string());
world.last_command_error = Some(String::from_utf8_lossy(&output.stderr).to_string());

// Only assert and parse the output file if the command succeeded
if output.status.success() {
let output_file = world.output_file.as_ref().expect("Output file path not set");

assert!(
output_file.exists(),
"Transaction file should exist at {:?}",
output_file
);

// Parse the JSON file
let content = std::fs::read_to_string(output_file).expect("Failed to read transaction file");
let transaction_json: serde_json::Value =
serde_json::from_str(&content).expect("Failed to parse transaction JSON");

// Store for later verification
world.transaction_data.insert("current".to_string(), transaction_json);
}
}

/// Generate a test Tari address (simplified for testing)
Expand Down Expand Up @@ -96,7 +114,6 @@ async fn wallet_has_balance(world: &mut MinotariWorld) {
world.base_nodes.insert("BalanceMiner".to_string(), node);
world.seed_nodes.push("BalanceMiner".to_string());
}

// 2. Mine blocks so the wallet receives coinbase rewards
let spend_key = world.wallet.get_public_spend_key();
let view_key = world.wallet.get_public_view_key();
Expand Down Expand Up @@ -139,7 +156,6 @@ async fn wallet_has_balance(world: &mut MinotariWorld) {
.args(&args)
.output()
.expect("Failed to execute scan command");

assert!(
output.status.success(),
"Scan failed during balance setup: {}",
Expand Down Expand Up @@ -245,31 +261,14 @@ async fn create_transaction_with_lock_duration(world: &mut MinotariWorld, second
// Verification Steps
// =============================

#[then("the transaction file should be created")]
async fn transaction_file_created(world: &mut MinotariWorld) {
let output_file = world.output_file.as_ref().expect("Output file path not set");

assert!(
output_file.exists(),
"Transaction file should exist at {:?}",
output_file
);

// Parse the JSON file
let content = std::fs::read_to_string(output_file).expect("Failed to read transaction file");

let transaction_json: serde_json::Value = serde_json::from_str(&content).expect("Failed to parse transaction JSON");

// Store for later verification
world.transaction_data.insert("current".to_string(), transaction_json);
}

#[then("the transaction should include the recipient")]
async fn transaction_has_recipient(world: &mut MinotariWorld) {
let transaction = world
.transaction_data
.get("current")
.expect("Transaction data not found");
.expect("Transaction data not found")
.get("info")
.expect("Transaction info not found");

// Check that the transaction has outputs/recipients
assert!(
Expand All @@ -283,7 +282,9 @@ async fn inputs_are_locked(world: &mut MinotariWorld) {
let transaction = world
.transaction_data
.get("current")
.expect("Transaction data not found");
.expect("Transaction data not found")
.get("info")
.expect("Transaction info not found");

// Check that the transaction has inputs
assert!(
Expand All @@ -299,11 +300,11 @@ async fn transaction_has_all_recipients(world: &mut MinotariWorld) {
.get("current")
.expect("Transaction data not found");

// Get recipients or outputs array
let recipients = transaction
// Recipients are nested inside the "info" object
let info = transaction.get("info").expect("Transaction should have 'info' field");
let recipients = info
.get("recipients")
.or_else(|| transaction.get("outputs"))
.expect("Transaction should have recipients or outputs");
.expect("Transaction info should have 'recipients' field");

let recipients_array = recipients.as_array().expect("Recipients should be an array");

Expand All @@ -317,16 +318,12 @@ async fn total_amount_correct(world: &mut MinotariWorld) {
.get("current")
.expect("Transaction data not found");

// Check that total amount or value field exists
// Fee information is in the "info" object
let info = transaction.get("info").expect("Transaction should have 'info' field");
assert!(
transaction.get("total_amount").is_some()
|| transaction.get("total_value").is_some()
|| transaction.get("amount").is_some(),
"Transaction should have a total amount field"
info.get("fee").is_some() || info.get("fee_per_gram").is_some(),
"Transaction info should contain fee information"
);

// The total should be 50000 + 30000 + 20000 = 100000 microTari (plus fees)
// We just verify the field exists and is positive
}

#[then("the transaction should include the payment ID")]
Expand All @@ -336,8 +333,10 @@ async fn transaction_has_payment_id(world: &mut MinotariWorld) {
.get("current")
.expect("Transaction data not found");

// Check for payment ID in various possible locations
let has_payment_id = transaction.get("payment_id").is_some()
// Check for payment ID in the transaction info object and at the root level
let info = transaction.get("info");
let has_payment_id = info.and_then(|i| i.get("payment_id")).is_some()
|| transaction.get("payment_id").is_some()
|| transaction.get("memo").is_some()
|| transaction.get("message").is_some();

Expand Down Expand Up @@ -376,7 +375,9 @@ async fn inputs_locked_for_duration(world: &mut MinotariWorld, seconds: String)
let transaction = world
.transaction_data
.get("current")
.expect("Transaction data not found");
.expect("Transaction data not found")
.get("info")
.expect("No info found for transaction");

// Check that lock duration or expiry information is present
let has_lock_info = transaction.get("lock_duration").is_some()
Expand Down
Loading
Loading