diff --git a/escrow/SPECIFICATION.md b/escrow/SPECIFICATION.md index b79ec0b..e1ef3ab 100644 --- a/escrow/SPECIFICATION.md +++ b/escrow/SPECIFICATION.md @@ -69,14 +69,18 @@ pub enum EscrowStatus { ### EscrowData (Struct) ```rust pub struct EscrowData { - pub employer: ManagedAddress, // Who deposited the funds - pub receiver: ManagedAddress, // Who receives on release (agent) - pub token_id: EgldOrEsdtTokenIdentifier, // EGLD or ESDT token - pub token_nonce: u64, // SFT/NFT nonce (0 for fungible) - pub amount: BigUint, // Locked amount - pub poa_hash: ManagedBuffer, // Proof-of-Agreement hash - pub deadline: u64, // Unix timestamp (seconds) - pub status: EscrowStatus, // Current state + /// Who deposited the funds + pub employer: ManagedAddress, + /// Who receives on release (agent) + pub receiver: ManagedAddress, + /// Payment details: token, nonce, amount + pub payment: Payment, + /// Proof-of-Agreement hash + pub poa_hash: ManagedBuffer, + /// Unix timestamp (seconds) of the block for the escrow deadline + pub deadline: TimestampSeconds, + /// Current state of the escrow + pub status: EscrowStatus, } ``` @@ -200,7 +204,7 @@ The `release` function reads job data directly from the Validation Registry's st ### 5.4 Zero-Allocation Compliance The contract uses **only** `Managed*` types: -- `ManagedBuffer`, `ManagedAddress`, `BigUint`, `EgldOrEsdtTokenIdentifier` +- `ManagedBuffer`, `ManagedAddress`, `BigUint`, `TokenId` - No `String`, `Vec`, `Box`, `HashMap`, `format!`, or `alloc` anywhere - `#![no_std]` at the crate root diff --git a/escrow/multiversx.json b/escrow/multiversx.json new file mode 100644 index 0000000..7365539 --- /dev/null +++ b/escrow/multiversx.json @@ -0,0 +1,3 @@ +{ + "language": "rust" +} \ No newline at end of file diff --git a/escrow/src/errors.rs b/escrow/src/errors.rs index 4d8c5f8..10d3964 100644 --- a/escrow/src/errors.rs +++ b/escrow/src/errors.rs @@ -5,4 +5,3 @@ pub const ERR_JOB_NOT_VERIFIED: &str = "Job must be verified before release"; pub const ERR_DEADLINE_NOT_PASSED: &str = "Deadline has not passed yet"; pub const ERR_DEADLINE_IN_PAST: &str = "Deadline must be in the future"; pub const ERR_ALREADY_SETTLED: &str = "Escrow already settled"; -pub const ERR_ZERO_DEPOSIT: &str = "Deposit amount must be greater than zero"; diff --git a/escrow/src/events.rs b/escrow/src/events.rs index cea1cb6..5b35498 100644 --- a/escrow/src/events.rs +++ b/escrow/src/events.rs @@ -2,27 +2,27 @@ multiversx_sc::imports!(); #[multiversx_sc::module] pub trait EventsModule { - #[event("escrow_deposited")] + #[event("escrowDeposited")] fn escrow_deposited_event( &self, #[indexed] job_id: &ManagedBuffer, #[indexed] employer: &ManagedAddress, - amount: BigUint, + amount: &NonZeroBigUint, ); - #[event("escrow_released")] + #[event("escrowReleased")] fn escrow_released_event( &self, #[indexed] job_id: &ManagedBuffer, #[indexed] receiver: &ManagedAddress, - amount: BigUint, + amount: &NonZeroBigUint, ); - #[event("escrow_refunded")] + #[event("escrowRefunded")] fn escrow_refunded_event( &self, #[indexed] job_id: &ManagedBuffer, #[indexed] employer: &ManagedAddress, - amount: BigUint, + amount: &NonZeroBigUint, ); } diff --git a/escrow/src/lib.rs b/escrow/src/lib.rs index ad17fba..d558cf5 100644 --- a/escrow/src/lib.rs +++ b/escrow/src/lib.rs @@ -45,8 +45,7 @@ pub trait EscrowContract: poa_hash: ManagedBuffer, deadline: TimestampSeconds, ) { - let payment = self.call_value().egld_or_single_esdt(); - require!(payment.amount > 0u64, ERR_ZERO_DEPOSIT); + let payment = self.call_value().single(); let current_timestamp = self.blockchain().get_block_timestamp_seconds(); require!(deadline > current_timestamp, ERR_DEADLINE_IN_PAST); @@ -59,9 +58,7 @@ pub trait EscrowContract: let escrow = EscrowData { employer: caller.clone(), receiver, - token_id: payment.token_identifier.clone(), - token_nonce: payment.token_nonce, - amount: payment.amount.clone(), + payment: payment.clone(), poa_hash, deadline, status: EscrowStatus::Active, @@ -70,7 +67,7 @@ pub trait EscrowContract: // Effects: store escrow escrow_mapper.set(&escrow); - self.escrow_deposited_event(&job_id, &caller, payment.amount); + self.escrow_deposited_event(&job_id, &caller, &payment.amount); } /// Release escrowed funds to the receiver. @@ -99,19 +96,15 @@ pub trait EscrowContract: // Effects: mark as released BEFORE interactions escrow.status = EscrowStatus::Released; - let receiver = escrow.receiver.clone(); - let amount = escrow.amount.clone(); - let token_id = escrow.token_id.clone(); - let token_nonce = escrow.token_nonce; escrow_mapper.set(&escrow); // Interactions: transfer funds to receiver self.tx() - .to(&receiver) - .egld_or_single_esdt(&token_id, token_nonce, &amount) + .to(&escrow.receiver) + .payment(&escrow.payment) .transfer(); - self.escrow_released_event(&job_id, &receiver, amount); + self.escrow_released_event(&job_id, &escrow.receiver, &escrow.payment.amount); } /// Refund escrowed funds to the employer if the deadline has passed. @@ -129,18 +122,14 @@ pub trait EscrowContract: // Effects: mark as refunded BEFORE interactions escrow.status = EscrowStatus::Refunded; - let employer = escrow.employer.clone(); - let amount = escrow.amount.clone(); - let token_id = escrow.token_id.clone(); - let token_nonce = escrow.token_nonce; escrow_mapper.set(&escrow); // Interactions: transfer funds back to employer self.tx() - .to(&employer) - .egld_or_single_esdt(&token_id, token_nonce, &amount) + .to(&escrow.employer) + .payment(&escrow.payment) .transfer(); - self.escrow_refunded_event(&job_id, &employer, amount); + self.escrow_refunded_event(&job_id, &escrow.employer, &escrow.payment.amount); } } diff --git a/escrow/src/storage.rs b/escrow/src/storage.rs index c5ddc08..7c01af9 100644 --- a/escrow/src/storage.rs +++ b/escrow/src/storage.rs @@ -14,13 +14,17 @@ pub enum EscrowStatus { #[type_abi] #[derive(TopEncode, TopDecode, NestedEncode, NestedDecode, PartialEq, Debug)] pub struct EscrowData { + /// Who deposited the funds pub employer: ManagedAddress, + /// Who receives on release (agent) pub receiver: ManagedAddress, - pub token_id: EgldOrEsdtTokenIdentifier, - pub token_nonce: u64, - pub amount: BigUint, + /// Payment details: token, nonce, amount + pub payment: Payment, + /// Proof-of-Agreement hash pub poa_hash: ManagedBuffer, + /// Unix timestamp (seconds) of the block for the escrow deadline pub deadline: TimestampSeconds, + /// Current state of the escrow pub status: EscrowStatus, } diff --git a/identity-registry/src/views.rs b/identity-registry/src/views.rs index ba0b5eb..c041834 100644 --- a/identity-registry/src/views.rs +++ b/identity-registry/src/views.rs @@ -30,14 +30,10 @@ pub trait ViewsModule: crate::storage::StorageModule { &self, nonce: u64, service_id: u32, - ) -> OptionalValue> { + ) -> OptionalValue> { let mapper = self.agent_service_config(nonce); if let Some(payment) = mapper.get(&service_id) { - OptionalValue::Some(EgldOrEsdtTokenPayment::new( - EgldOrEsdtTokenIdentifier::from(payment.token_identifier), - payment.token_nonce, - payment.amount.into_big_uint(), - )) + OptionalValue::Some(payment) } else { OptionalValue::None } diff --git a/tests/src/interact.rs b/tests/src/interact.rs index 8741882..2a676ac 100644 --- a/tests/src/interact.rs +++ b/tests/src/interact.rs @@ -379,7 +379,6 @@ impl CsInteract { // ── Reputation Registry ── - pub async fn give_feedback_simple( &mut self, from: &Address, @@ -507,7 +506,6 @@ impl CsInteract { .await; } - pub async fn submit_proof_expect_err( &mut self, from: &Address, diff --git a/tests/src/setup.rs b/tests/src/setup.rs index e612cdb..6b66ce9 100644 --- a/tests/src/setup.rs +++ b/tests/src/setup.rs @@ -574,7 +574,6 @@ impl AgentTestState { // ── Reputation Registry ── - pub fn give_feedback_simple( &mut self, from: &multiversx_sc::types::TestAddress, @@ -1104,7 +1103,6 @@ impl AgentTestState { .run(); } - pub fn append_response_expect_err( &mut self, from: &multiversx_sc::types::TestAddress, diff --git a/tests/tests/cs_tests.rs b/tests/tests/cs_tests.rs index 692b7ec..e2808b5 100644 --- a/tests/tests/cs_tests.rs +++ b/tests/tests/cs_tests.rs @@ -3,384 +3,391 @@ // // These tests require a running MultiversX chain simulator on http://localhost:8085 -#[cfg(feature = "chain-simulator-tests")] -mod cs { - use mx_8004_tests::interact::CsInteract; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_deploy_all_cs() { - let _ = env_logger::try_init(); - let interact = CsInteract::new().await; - - assert!( - !interact.identity_addr.to_bech32_string().is_empty(), - "Identity address should be non-empty" - ); - assert!( - !interact.validation_addr.to_bech32_string().is_empty(), - "Validation address should be non-empty" - ); - assert!( - !interact.reputation_addr.to_bech32_string().is_empty(), - "Reputation address should be non-empty" - ); - assert!( - !interact.agent_token_id.is_empty(), - "Agent token ID should be set" - ); - println!( - "All contracts deployed successfully. Token: {}", - interact.agent_token_id - ); - } - - #[tokio::test] - #[serial] - async fn test_register_agent_cs() { - let _ = env_logger::try_init(); - let mut interact = CsInteract::new().await; - - let bob = interact.agent_owner.clone(); - interact - .register_agent( - &bob, - b"TestAgent", - b"https://agent.example.com", - b"pubkey123", - ) - .await; - - let token_id = interact.query_agent_token_id().await; - assert!( - !token_id.is_empty(), - "Token ID should exist after registration" - ); - println!("Agent registered and confirmed with token: {token_id}"); - } - - #[tokio::test] - #[serial] - async fn test_job_lifecycle_cs() { - let _ = env_logger::try_init(); - let mut interact = CsInteract::new().await; - - let bob = interact.agent_owner.clone(); - interact - .register_agent( - &bob, - b"TestAgent", - b"https://agent.example.com", - b"pubkey123", - ) - .await; - - let agent_nonce = 1u64; - let carol = interact.client.clone(); - let worker = interact.worker.clone(); - - interact.init_job(&carol, b"job-001", agent_nonce).await; - interact - .submit_proof(&worker, b"job-001", b"proof-data-hash") - .await; - - // Use validation_request + validation_response flow - let validator = interact.owner.clone(); - interact - .validation_request(&bob, b"job-001", &validator, b"req-uri", b"req-hash") - .await; - interact - .validation_response( - &validator, - b"req-hash", - 90, - b"resp-uri", - b"resp-hash", - b"quality", - ) - .await; - - let verified = interact.query_is_job_verified(b"job-001").await; - assert!(verified, "Job should be verified"); - println!("Full job lifecycle (init -> proof -> validate) passed"); - } - - /// Full lifecycle including reputation feedback. - /// - /// This test is ignored by default because the reputation-registry uses - /// `storage_mapper_from_address` (ManagedStorageReadFromAddress VM hook) - /// to read job data from the validation-registry. This VM hook only works - /// when both contracts are deployed in the same shard. On the chain simulator - /// with 3 shards, contracts deployed at different nonces land in different - /// shards non-deterministically, causing "Job not found" errors. - /// - /// The full lifecycle (including feedback) is covered by the 32 scenario tests. - /// - /// To run: cargo test -p mx-8004-tests --features chain-simulator-tests -- --ignored --test-threads=1 - #[tokio::test] - #[serial] - #[ignore = "Requires same-shard deployment; storage_mapper_from_address is shard-local"] - async fn test_full_lifecycle_with_feedback_cs() { - let _ = env_logger::try_init(); - let mut interact = CsInteract::new().await; - - let bob = interact.agent_owner.clone(); - interact - .register_agent( - &bob, - b"TestAgent", - b"https://agent.example.com", - b"pubkey123", - ) - .await; - - let agent_nonce = 1u64; - let carol = interact.client.clone(); - let worker = interact.worker.clone(); - - // Job lifecycle - interact.init_job(&carol, b"job-001", agent_nonce).await; - interact - .submit_proof(&worker, b"job-001", b"proof-data-hash") - .await; - - // Validation flow - let validator = interact.owner.clone(); - interact - .validation_request(&bob, b"job-001", &validator, b"req-uri", b"req-hash") - .await; - interact - .validation_response( - &validator, - b"req-hash", - 90, - b"resp-uri", - b"resp-hash", - b"quality", - ) - .await; - - // Reputation (cross-contract storage reads from validation) - interact - .give_feedback_simple(&carol, b"job-001", agent_nonce, 85) - .await; - interact - .append_response(&bob, b"job-001", b"https://response.example.com/result") - .await; - - let score = interact.query_reputation_score(agent_nonce).await; - assert!( - score > multiversx_sc_scenario::imports::RustBigUint::from(0u64), - "Score should be > 0" - ); - - let total = interact.query_total_jobs(agent_nonce).await; - assert_eq!(total, 1, "Should have 1 completed job"); - - let has_feedback = interact.query_has_given_feedback(b"job-001").await; - assert!(has_feedback, "Feedback should be recorded"); - - println!("Full lifecycle with feedback passed!"); - } - - // ── Error-path tests ── - - #[tokio::test] - #[serial] - async fn test_duplicate_agent_registration_cs() { - let _ = env_logger::try_init(); - let mut interact = CsInteract::new().await; - - let bob = interact.agent_owner.clone(); - interact - .register_agent( - &bob, - b"TestAgent", - b"https://agent.example.com", - b"pubkey123", - ) - .await; - - // Second registration from same address should fail - interact - .register_agent_expect_err( - &bob, - b"TestAgent2", - b"https://agent2.example.com", - b"pubkey456", - 4, - "Agent already registered for this address", - ) - .await; - - println!("Duplicate agent registration correctly rejected"); - } - - #[tokio::test] - #[serial] - async fn test_issue_token_already_issued_cs() { - let _ = env_logger::try_init(); - let mut interact = CsInteract::new().await; - - // Token is already issued during CsInteract::new(), second issuance should fail - interact - .issue_token_expect_err(4, "Token already issued") - .await; - - println!("Duplicate token issuance correctly rejected"); - } - - #[tokio::test] - #[serial] - async fn test_validation_request_not_owner_cs() { - let _ = env_logger::try_init(); - let mut interact = CsInteract::new().await; - - let bob = interact.agent_owner.clone(); - interact - .register_agent( - &bob, - b"TestAgent", - b"https://agent.example.com", - b"pubkey123", - ) - .await; - - let carol = interact.client.clone(); - interact.init_job(&carol, b"job-001", 1u64).await; - - let worker = interact.worker.clone(); - interact - .submit_proof(&worker, b"job-001", b"proof-data") - .await; - - // Non-owner (carol) tries to make a validation request — should fail - interact - .validation_request_expect_err( - &carol, - b"job-001", - b"req-uri", - b"req-hash", - 4, - "Only the agent owner can request validation", - ) - .await; - - println!("Non-owner validation request correctly rejected"); - } - - #[tokio::test] - #[serial] - async fn test_submit_proof_nonexistent_job_cs() { - let _ = env_logger::try_init(); - let mut interact = CsInteract::new().await; - - let worker = interact.worker.clone(); - interact - .submit_proof_expect_err( - &worker, - b"nonexistent-job", - b"proof-data", - 4, - "Job not found", - ) - .await; - - println!("Proof for nonexistent job correctly rejected"); - } - - #[tokio::test] - #[serial] - async fn test_init_job_duplicate_cs() { - let _ = env_logger::try_init(); - let mut interact = CsInteract::new().await; - - let bob = interact.agent_owner.clone(); - interact - .register_agent( - &bob, - b"TestAgent", - b"https://agent.example.com", - b"pubkey123", - ) - .await; - - let carol = interact.client.clone(); - interact.init_job(&carol, b"job-001", 1u64).await; - - // Same job_id again should fail - interact - .init_job_expect_err(&carol, b"job-001", 1u64, 4, "Job already initialized") - .await; - - println!("Duplicate job init correctly rejected"); - } - - /// Test: register agent with a free service (price=0, service_id=1), - /// then init_job with that service_id but NO payment → should succeed. - #[tokio::test] - #[serial] - async fn test_init_job_free_service_cs() { - let _ = env_logger::try_init(); - let mut interact = CsInteract::new().await; - - let bob = interact.agent_owner.clone(); - // Register agent with service_id=1, price=0, EGLD, nonce=0 - interact - .register_agent_with_meta( - &bob, - b"FreeBot", - b"https://free.example.com", - b"pubkey123", - &[], - &[(1, 0, b"EGLD-000000", 0)], // free service - ) - .await; - - let carol = interact.client.clone(); - // Init job with service_id=1 (free) and no payment → should succeed - interact - .init_job_with_free_service(&carol, b"free-job-001", 1u64, 1) - .await; - - println!("Free service job init succeeded without payment"); - } - - /// Test: register agent with a paid service (1 EGLD, service_id=1), - /// then init_job with that service_id but NO payment → ERR_INSUFFICIENT_PAYMENT. - #[tokio::test] - #[serial] - async fn test_init_job_no_payment_for_paid_service_cs() { - let _ = env_logger::try_init(); - let mut interact = CsInteract::new().await; - - let bob = interact.agent_owner.clone(); - // Register agent with service_id=1, price=1 EGLD - interact - .register_agent_with_meta( - &bob, - b"PaidBot", - b"https://paid.example.com", - b"pubkey123", - &[], - &[(1, 1_000_000_000_000_000_000, b"EGLD-000000", 0)], // 1 EGLD - ) - .await; - - let carol = interact.client.clone(); - // Init job with service_id=1 but NO payment → should fail - interact - .init_job_with_free_service_expect_err( - &carol, - b"no-pay-job-001", - 1u64, - 1, - 4, - "Insufficient payment", - ) - .await; - - println!("No-payment for paid service correctly rejected"); - } +use mx_8004_tests::interact::CsInteract; +use serial_test::serial; + +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +#[serial] +async fn test_deploy_all_cs() { + let _ = env_logger::try_init(); + let interact = CsInteract::new().await; + + assert!( + !interact.identity_addr.to_bech32_string().is_empty(), + "Identity address should be non-empty" + ); + assert!( + !interact.validation_addr.to_bech32_string().is_empty(), + "Validation address should be non-empty" + ); + assert!( + !interact.reputation_addr.to_bech32_string().is_empty(), + "Reputation address should be non-empty" + ); + assert!( + !interact.agent_token_id.is_empty(), + "Agent token ID should be set" + ); + println!( + "All contracts deployed successfully. Token: {}", + interact.agent_token_id + ); +} + +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +#[serial] +async fn test_register_agent_cs() { + let _ = env_logger::try_init(); + let mut interact = CsInteract::new().await; + + let bob = interact.agent_owner.clone(); + interact + .register_agent( + &bob, + b"TestAgent", + b"https://agent.example.com", + b"pubkey123", + ) + .await; + + let token_id = interact.query_agent_token_id().await; + assert!( + !token_id.is_empty(), + "Token ID should exist after registration" + ); + println!("Agent registered and confirmed with token: {token_id}"); +} + +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +#[serial] +async fn test_job_lifecycle_cs() { + let _ = env_logger::try_init(); + let mut interact = CsInteract::new().await; + + let bob = interact.agent_owner.clone(); + interact + .register_agent( + &bob, + b"TestAgent", + b"https://agent.example.com", + b"pubkey123", + ) + .await; + + let agent_nonce = 1u64; + let carol = interact.client.clone(); + let worker = interact.worker.clone(); + + interact.init_job(&carol, b"job-001", agent_nonce).await; + interact + .submit_proof(&worker, b"job-001", b"proof-data-hash") + .await; + + // Use validation_request + validation_response flow + let validator = interact.owner.clone(); + interact + .validation_request(&bob, b"job-001", &validator, b"req-uri", b"req-hash") + .await; + interact + .validation_response( + &validator, + b"req-hash", + 90, + b"resp-uri", + b"resp-hash", + b"quality", + ) + .await; + + let verified = interact.query_is_job_verified(b"job-001").await; + assert!(verified, "Job should be verified"); + println!("Full job lifecycle (init -> proof -> validate) passed"); +} + +/// Full lifecycle including reputation feedback. +/// +/// This test is ignored by default because the reputation-registry uses +/// `storage_mapper_from_address` (ManagedStorageReadFromAddress VM hook) +/// to read job data from the validation-registry. This VM hook only works +/// when both contracts are deployed in the same shard. On the chain simulator +/// with 3 shards, contracts deployed at different nonces land in different +/// shards non-deterministically, causing "Job not found" errors. +/// +/// The full lifecycle (including feedback) is covered by the 32 scenario tests. +/// +/// To run: cargo test -p mx-8004-tests --features chain-simulator-tests -- --ignored --test-threads=1 +#[tokio::test] +#[serial] +#[ignore = "Requires same-shard deployment; storage_mapper_from_address is shard-local"] +async fn test_full_lifecycle_with_feedback_cs() { + let _ = env_logger::try_init(); + let mut interact = CsInteract::new().await; + + let bob = interact.agent_owner.clone(); + interact + .register_agent( + &bob, + b"TestAgent", + b"https://agent.example.com", + b"pubkey123", + ) + .await; + + let agent_nonce = 1u64; + let carol = interact.client.clone(); + let worker = interact.worker.clone(); + + // Job lifecycle + interact.init_job(&carol, b"job-001", agent_nonce).await; + interact + .submit_proof(&worker, b"job-001", b"proof-data-hash") + .await; + + // Validation flow + let validator = interact.owner.clone(); + interact + .validation_request(&bob, b"job-001", &validator, b"req-uri", b"req-hash") + .await; + interact + .validation_response( + &validator, + b"req-hash", + 90, + b"resp-uri", + b"resp-hash", + b"quality", + ) + .await; + + // Reputation (cross-contract storage reads from validation) + interact + .give_feedback_simple(&carol, b"job-001", agent_nonce, 85) + .await; + interact + .append_response(&bob, b"job-001", b"https://response.example.com/result") + .await; + + let score = interact.query_reputation_score(agent_nonce).await; + assert!( + score > multiversx_sc_scenario::imports::RustBigUint::from(0u64), + "Score should be > 0" + ); + + let total = interact.query_total_jobs(agent_nonce).await; + assert_eq!(total, 1, "Should have 1 completed job"); + + let has_feedback = interact.query_has_given_feedback(b"job-001").await; + assert!(has_feedback, "Feedback should be recorded"); + + println!("Full lifecycle with feedback passed!"); +} + +// ── Error-path tests ── + +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +#[serial] +async fn test_duplicate_agent_registration_cs() { + let _ = env_logger::try_init(); + let mut interact = CsInteract::new().await; + + let bob = interact.agent_owner.clone(); + interact + .register_agent( + &bob, + b"TestAgent", + b"https://agent.example.com", + b"pubkey123", + ) + .await; + + // Second registration from same address should fail + interact + .register_agent_expect_err( + &bob, + b"TestAgent2", + b"https://agent2.example.com", + b"pubkey456", + 4, + "Agent already registered for this address", + ) + .await; + + println!("Duplicate agent registration correctly rejected"); +} + +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +#[serial] +async fn test_issue_token_already_issued_cs() { + let _ = env_logger::try_init(); + let mut interact = CsInteract::new().await; + + // Token is already issued during CsInteract::new(), second issuance should fail + interact + .issue_token_expect_err(4, "Token already issued") + .await; + + println!("Duplicate token issuance correctly rejected"); +} + +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +#[serial] +async fn test_validation_request_not_owner_cs() { + let _ = env_logger::try_init(); + let mut interact = CsInteract::new().await; + + let bob = interact.agent_owner.clone(); + interact + .register_agent( + &bob, + b"TestAgent", + b"https://agent.example.com", + b"pubkey123", + ) + .await; + + let carol = interact.client.clone(); + interact.init_job(&carol, b"job-001", 1u64).await; + + let worker = interact.worker.clone(); + interact + .submit_proof(&worker, b"job-001", b"proof-data") + .await; + + // Non-owner (carol) tries to make a validation request — should fail + interact + .validation_request_expect_err( + &carol, + b"job-001", + b"req-uri", + b"req-hash", + 4, + "Only the agent owner can request validation", + ) + .await; + + println!("Non-owner validation request correctly rejected"); +} + +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +#[serial] +async fn test_submit_proof_nonexistent_job_cs() { + let _ = env_logger::try_init(); + let mut interact = CsInteract::new().await; + + let worker = interact.worker.clone(); + interact + .submit_proof_expect_err( + &worker, + b"nonexistent-job", + b"proof-data", + 4, + "Job not found", + ) + .await; + + println!("Proof for nonexistent job correctly rejected"); +} + +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +#[serial] +async fn test_init_job_duplicate_cs() { + let _ = env_logger::try_init(); + let mut interact = CsInteract::new().await; + + let bob = interact.agent_owner.clone(); + interact + .register_agent( + &bob, + b"TestAgent", + b"https://agent.example.com", + b"pubkey123", + ) + .await; + + let carol = interact.client.clone(); + interact.init_job(&carol, b"job-001", 1u64).await; + + // Same job_id again should fail + interact + .init_job_expect_err(&carol, b"job-001", 1u64, 4, "Job already initialized") + .await; + + println!("Duplicate job init correctly rejected"); +} + +/// Test: register agent with a free service (price=0, service_id=1), +/// then init_job with that service_id but NO payment → should succeed. +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +#[serial] +async fn test_init_job_free_service_cs() { + let _ = env_logger::try_init(); + let mut interact = CsInteract::new().await; + + let bob = interact.agent_owner.clone(); + // Register agent with service_id=1, price=0, EGLD, nonce=0 + interact + .register_agent_with_meta( + &bob, + b"FreeBot", + b"https://free.example.com", + b"pubkey123", + &[], + &[(1, 0, b"EGLD-000000", 0)], // free service + ) + .await; + + let carol = interact.client.clone(); + // Init job with service_id=1 (free) and no payment → should succeed + interact + .init_job_with_free_service(&carol, b"free-job-001", 1u64, 1) + .await; + + println!("Free service job init succeeded without payment"); +} + +/// Test: register agent with a paid service (1 EGLD, service_id=1), +/// then init_job with that service_id but NO payment → ERR_INSUFFICIENT_PAYMENT. +#[tokio::test] +#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)] +#[serial] +async fn test_init_job_no_payment_for_paid_service_cs() { + let _ = env_logger::try_init(); + let mut interact = CsInteract::new().await; + + let bob = interact.agent_owner.clone(); + // Register agent with service_id=1, price=1 EGLD + interact + .register_agent_with_meta( + &bob, + b"PaidBot", + b"https://paid.example.com", + b"pubkey123", + &[], + &[(1, 1_000_000_000_000_000_000, b"EGLD-000000", 0)], // 1 EGLD + ) + .await; + + let carol = interact.client.clone(); + // Init job with service_id=1 but NO payment → should fail + interact + .init_job_with_free_service_expect_err( + &carol, + b"no-pay-job-001", + 1u64, + 1, + 4, + "Insufficient payment", + ) + .await; + + println!("No-payment for paid service correctly rejected"); } diff --git a/tests/tests/escrow_tests.rs b/tests/tests/escrow_tests.rs index 3ce9e38..2f84d34 100644 --- a/tests/tests/escrow_tests.rs +++ b/tests/tests/escrow_tests.rs @@ -1,5 +1,5 @@ use escrow::storage::EscrowStatus; -use multiversx_sc::types::{BigUint, ManagedAddress, ManagedBuffer}; +use multiversx_sc::imports::*; use multiversx_sc_scenario::api::StaticApi; use mx_8004_tests::{constants::*, setup::EscrowTestState}; @@ -34,7 +34,7 @@ fn test_deposit_egld() { let escrow = state.query_escrow(b"job_egld_1"); assert_eq!(escrow.employer, EMPLOYER.to_managed_address()); assert_eq!(escrow.receiver, AGENT_OWNER.to_managed_address()); - assert_eq!(escrow.amount, BigUint::::from(500_000u64)); + assert_eq!(escrow.payment.amount, 500_000u64); assert_eq!( escrow.poa_hash, ManagedBuffer::::from(b"poa_hash_123") @@ -64,7 +64,7 @@ fn test_deposit_esdt() { let escrow = state.query_escrow(b"job_esdt_1"); assert_eq!(escrow.employer, EMPLOYER.to_managed_address()); assert_eq!(escrow.receiver, AGENT_OWNER.to_managed_address()); - assert_eq!(escrow.amount, BigUint::::from(1_000u64)); + assert_eq!(escrow.payment.amount, 1_000u64); assert_eq!(escrow.status, EscrowStatus::Active); } @@ -83,7 +83,7 @@ fn test_deposit_zero_amount() { b"poa_hash", 1_000_000, 0, - "Deposit amount must be greater than zero", + "incorrect number of transfers", ); } @@ -585,7 +585,7 @@ fn test_full_lifecycle_egld() { // 4. Verify escrow data let escrow = state.query_escrow(b"lifecycle_egld"); assert_eq!(escrow.status, EscrowStatus::Active); - assert_eq!(escrow.amount, BigUint::::from(1_000_000u64)); + assert_eq!(escrow.payment.amount, 1_000_000u64); // 5. Release state.release(&EMPLOYER, b"lifecycle_egld"); @@ -647,7 +647,7 @@ fn test_full_lifecycle_esdt() { // 4. Verify escrow let escrow = state.query_escrow(b"lifecycle_esdt"); assert_eq!(escrow.status, EscrowStatus::Active); - assert_eq!(escrow.amount, BigUint::::from(500u64)); + assert_eq!(escrow.payment.amount, 500u64); // 5. Release state.release(&EMPLOYER, b"lifecycle_esdt");