From e339dce15a3b3aef2c7d0e1b0ec2d5981c9f7f41 Mon Sep 17 00:00:00 2001 From: chiscookeke11 Date: Thu, 28 May 2026 13:59:41 +0100 Subject: [PATCH 1/3] setup --- backend/package-lock.json | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 9e5ac2b7..df75af75 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -54,8 +54,7 @@ "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", "devOptional": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.1.1", @@ -799,7 +798,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -1477,7 +1475,8 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "devOptional": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -2185,7 +2184,6 @@ "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -2933,7 +2931,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", @@ -3121,7 +3118,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "7.8.0", "@prisma/dev": "0.24.3", @@ -3423,7 +3419,8 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "devOptional": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/semver": { "version": "7.8.0", @@ -3882,7 +3879,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From 585ffd6ce8343a2fc67f6a9319f44782209f3f0e Mon Sep 17 00:00:00 2001 From: Okeke Chinedu Emmanuel Date: Thu, 28 May 2026 15:05:01 +0100 Subject: [PATCH 2/3] Implement bid acceptance assignment transition --- contracts/job_registry/src/lib.rs | 201 ++- ..._accept_without_matching_bid_panics.1.json | 6 +- .../test/test_duplicate_bid_panics.1.json | 155 +- .../test/test_full_lifecycle.1.json | 157 ++- ...est_get_bids_for_missing_job_panics.1.json | 419 ++++++ ...liverable_without_submission_panics.1.json | 6 +- ...acceptance_panics_with_job_not_open.1.json | 1244 +++++++++++++++++ ...test_mark_disputed_from_in_progress.1.json | 155 +- ...test_mark_disputed_from_open_panics.1.json | 6 +- ...rsized_cid_panics_with_invalid_hash.1.json | 447 ++++++ ...t_job_auto_allocates_sequential_ids.1.json | 12 +- ...ith_explicit_id_updates_next_job_id.1.json | 6 +- ...test_set_escrow_deployer_round_trip.1.json | 452 ++++++ ...cept_bid_panics_with_specific_error.1.json | 1071 ++++++++++++++ 14 files changed, 4218 insertions(+), 119 deletions(-) create mode 100644 contracts/job_registry/test_snapshots/test/test_get_bids_for_missing_job_panics.1.json create mode 100644 contracts/job_registry/test_snapshots/test/test_late_bid_after_acceptance_panics_with_job_not_open.1.json create mode 100644 contracts/job_registry/test_snapshots/test/test_oversized_cid_panics_with_invalid_hash.1.json create mode 100644 contracts/job_registry/test_snapshots/test/test_set_escrow_deployer_round_trip.1.json create mode 100644 contracts/job_registry/test_snapshots/test/test_unauthorized_accept_bid_panics_with_specific_error.1.json diff --git a/contracts/job_registry/src/lib.rs b/contracts/job_registry/src/lib.rs index 7b0e1f36..066a2680 100644 --- a/contracts/job_registry/src/lib.rs +++ b/contracts/job_registry/src/lib.rs @@ -5,7 +5,8 @@ use soroban_sdk::{ Address, Bytes, Env, Vec, }; -const MAX_HASH_LEN: u32 = 96; +const MAX_CID_LEN: u32 = 96; +const MAX_BIDS_PER_JOB: u32 = 1_000; #[contracterror] #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] @@ -25,12 +26,15 @@ pub enum JobRegistryError { InvalidStateTransition = 12, NoDeliverable = 13, Overflow = 14, + BidLimitReached = 15, } #[contracttype] #[derive(Clone, Debug, PartialEq)] pub enum JobStatus { Open, + /// A bid has been accepted and the job is ready for escrow deployment/funding. + Assigned, InProgress, DeliverableSubmitted, Completed, @@ -59,8 +63,12 @@ pub enum DataKey { Admin, NextJobId, Job(u64), - Bids(u64), + BidCount(u64), + BidByIndex(u64, u32), + BidLookup(u64, Address), + Bids(u64), // legacy aggregate key retained so old ledgers decode safely Deliverable(u64), + EscrowDeployer, } #[contract] @@ -154,6 +162,26 @@ impl JobRegistryContract { job_id } + /// Admin configures the off-chain/cross-contract escrow deployer address signaled on acceptance. + pub fn set_escrow_deployer(env: Env, escrow_deployer: Address) { + ensure_initialized(&env); + let admin = read_admin(&env); + admin.require_auth(); + + env.storage() + .instance() + .set(&DataKey::EscrowDeployer, &escrow_deployer); + + log!(&env, "escrow_deployer configured: {}", escrow_deployer); + env.events() + .publish((symbol_short!("escrow"),), escrow_deployer); + } + + pub fn get_escrow_deployer(env: Env) -> Option
{ + ensure_initialized(&env); + env.storage().instance().get(&DataKey::EscrowDeployer) + } + /// Freelancer submits a bid. pub fn submit_bid(env: Env, job_id: u64, freelancer: Address, proposal_hash: Bytes) { ensure_initialized(&env); @@ -171,24 +199,32 @@ impl JobRegistryContract { panic_with_error!(&env, JobRegistryError::JobNotOpen); } - let bids_key = DataKey::Bids(job_id); - let mut bids: Vec = env - .storage() - .persistent() - .get(&bids_key) - .unwrap_or(Vec::new(&env)); + // Bids are persisted as map-like rows keyed by (job_id, freelancer) and + // indexed by (job_id, ordinal). This avoids rewriting a growing vector on + // every bid while preserving deterministic iteration for `get_bids`. + let lookup_key = DataKey::BidLookup(job_id, freelancer.clone()); + if env.storage().persistent().has(&lookup_key) { + panic_with_error!(&env, JobRegistryError::BidAlreadySubmitted); + } - for bid in bids.iter() { - if bid.freelancer == freelancer { - panic_with_error!(&env, JobRegistryError::BidAlreadySubmitted); - } + let count_key = DataKey::BidCount(job_id); + let bid_count: u32 = env.storage().persistent().get(&count_key).unwrap_or(0u32); + if bid_count >= MAX_BIDS_PER_JOB { + panic_with_error!(&env, JobRegistryError::BidLimitReached); } + let next_count = bid_count + .checked_add(1) + .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::Overflow)); - bids.push_back(BidRecord { + let bid = BidRecord { freelancer: freelancer.clone(), proposal_hash, - }); - env.storage().persistent().set(&bids_key, &bids); + }; + env.storage().persistent().set(&lookup_key, &bid); + env.storage() + .persistent() + .set(&DataKey::BidByIndex(job_id, bid_count), &bid); + env.storage().persistent().set(&count_key, &next_count); log!(&env, "submit_bid: id {} freelancer {}", job_id, freelancer); env.events() @@ -214,25 +250,16 @@ impl JobRegistryContract { panic_with_error!(&env, JobRegistryError::Unauthorized); } - let bids: Vec = env - .storage() - .persistent() - .get(&DataKey::Bids(job_id)) - .unwrap_or(Vec::new(&env)); - - let mut found = false; - for bid in bids.iter() { - if bid.freelancer == freelancer { - found = true; - break; - } - } - if !found { + let bid_key = DataKey::BidLookup(job_id, freelancer.clone()); + if !env.storage().persistent().has(&bid_key) { panic_with_error!(&env, JobRegistryError::BidNotFound); } + // Validate every invariant before mutating the job. Acceptance records the + // selected freelancer and moves the registry to Assigned; the configured + // escrow deployer can then provision/fund the escrow without ambiguity. job.freelancer = Some(freelancer.clone()); - job.status = JobStatus::InProgress; + job.status = JobStatus::Assigned; env.storage().persistent().set(&key, &job); log!( @@ -243,7 +270,18 @@ impl JobRegistryContract { freelancer ); env.events() - .publish((symbol_short!("accept"), job_id), freelancer); + .publish((symbol_short!("accept"), job_id), freelancer.clone()); + + if let Some(escrow_deployer) = env + .storage() + .instance() + .get::<_, Address>(&DataKey::EscrowDeployer) + { + env.events().publish( + (symbol_short!("assign"), job_id), + (client, freelancer, escrow_deployer), + ); + } } /// Freelancer submits deliverable IPFS hash. @@ -259,7 +297,7 @@ impl JobRegistryContract { .get(&key) .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::JobNotFound)); - if job.status != JobStatus::InProgress { + if job.status != JobStatus::Assigned && job.status != JobStatus::InProgress { panic_with_error!(&env, JobRegistryError::InvalidStateTransition); } if job.freelancer != Some(freelancer.clone()) { @@ -295,7 +333,10 @@ impl JobRegistryContract { .get(&key) .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::JobNotFound)); - if job.status != JobStatus::InProgress && job.status != JobStatus::DeliverableSubmitted { + if job.status != JobStatus::Assigned + && job.status != JobStatus::InProgress + && job.status != JobStatus::DeliverableSubmitted + { panic_with_error!(&env, JobRegistryError::InvalidStateTransition); } @@ -316,10 +357,29 @@ impl JobRegistryContract { pub fn get_bids(env: Env, job_id: u64) -> Vec { ensure_initialized(&env); - env.storage() + if !env.storage().persistent().has(&DataKey::Job(job_id)) { + panic_with_error!(&env, JobRegistryError::JobNotFound); + } + + let count: u32 = env + .storage() .persistent() - .get(&DataKey::Bids(job_id)) - .unwrap_or(Vec::new(&env)) + .get(&DataKey::BidCount(job_id)) + .unwrap_or(0u32); + let mut bids = Vec::new(&env); + let mut index = 0u32; + while index < count { + let bid: BidRecord = env + .storage() + .persistent() + .get(&DataKey::BidByIndex(job_id, index)) + .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::BidNotFound)); + bids.push_back(bid); + index = index + .checked_add(1) + .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::Overflow)); + } + bids } pub fn get_deliverable(env: Env, job_id: u64) -> Bytes { @@ -365,7 +425,7 @@ fn validate_job_input(env: &Env, job_id: u64, hash: &Bytes, budget: i128) { fn validate_hash(env: &Env, hash: &Bytes) { let len = hash.len(); - if len == 0 || len > MAX_HASH_LEN { + if len == 0 || len > MAX_CID_LEN { panic_with_error!(env, JobRegistryError::InvalidHash); } } @@ -385,10 +445,9 @@ fn post_job_with_id(env: &Env, job_id: u64, client: Address, hash: Bytes, budget }; env.storage().persistent().set(&key, &job); - let bids: Vec = Vec::new(env); env.storage() .persistent() - .set(&DataKey::Bids(job_id), &bids); + .set(&DataKey::BidCount(job_id), &0u32); } #[cfg(test)] @@ -512,7 +571,7 @@ mod test { cc.accept_bid(&1u64, &client, &freelancer); let job = cc.get_job(&1u64); - assert_eq!(job.status, JobStatus::InProgress); + assert_eq!(job.status, JobStatus::Assigned); assert_eq!(job.freelancer, Some(freelancer.clone())); let deliverable = Bytes::from_slice(&env, b"QmDeliverableHash"); @@ -525,6 +584,68 @@ mod test { assert_eq!(d, deliverable); } + #[test] + fn test_set_escrow_deployer_round_trip() { + let (_env, cc, admin, _, freelancer) = setup(); + cc.initialize(&admin); + + cc.set_escrow_deployer(&freelancer); + + assert_eq!(cc.get_escrow_deployer(), Some(freelancer)); + } + + #[test] + #[should_panic(expected = "Error(Contract, #9)")] + fn test_unauthorized_accept_bid_panics_with_specific_error() { + let (env, cc, admin, client, freelancer) = setup(); + let other_client = Address::generate(&env); + cc.initialize(&admin); + + let hash = Bytes::from_slice(&env, b"QmHash"); + cc.post_job(&1u64, &client, &hash, &5000i128); + + let proposal = Bytes::from_slice(&env, b"QmProposal"); + cc.submit_bid(&1u64, &freelancer, &proposal); + cc.accept_bid(&1u64, &other_client, &freelancer); + } + + #[test] + #[should_panic(expected = "Error(Contract, #8)")] + fn test_late_bid_after_acceptance_panics_with_job_not_open() { + let (env, cc, admin, client, freelancer) = setup(); + let late_freelancer = Address::generate(&env); + cc.initialize(&admin); + + let hash = Bytes::from_slice(&env, b"QmHash"); + cc.post_job(&1u64, &client, &hash, &5000i128); + + let proposal = Bytes::from_slice(&env, b"QmProposal"); + cc.submit_bid(&1u64, &freelancer, &proposal); + cc.accept_bid(&1u64, &client, &freelancer); + + let late_proposal = Bytes::from_slice(&env, b"QmLateProposal"); + cc.submit_bid(&1u64, &late_freelancer, &late_proposal); + } + + #[test] + #[should_panic(expected = "Error(Contract, #5)")] + fn test_oversized_cid_panics_with_invalid_hash() { + let (env, cc, admin, client, _) = setup(); + cc.initialize(&admin); + + let oversized = Bytes::from_slice(&env, &[b'a'; 97]); + cc.post_job(&1u64, &client, &oversized, &5000i128); + } + + #[test] + #[should_panic(expected = "Error(Contract, #7)")] + fn test_get_bids_for_missing_job_panics() { + let (_env, cc, admin, _, _) = setup(); + cc.initialize(&admin); + + cc.get_bids(&404u64); + } + #[test] #[should_panic] fn test_duplicate_bid_panics() { diff --git a/contracts/job_registry/test_snapshots/test/test_accept_without_matching_bid_panics.1.json b/contracts/job_registry/test_snapshots/test/test_accept_without_matching_bid_panics.1.json index 931a41d2..ca8b1492 100644 --- a/contracts/job_registry/test_snapshots/test/test_accept_without_matching_bid_panics.1.json +++ b/contracts/job_registry/test_snapshots/test/test_accept_without_matching_bid_panics.1.json @@ -139,7 +139,7 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidCount" }, { "u64": 1 @@ -159,7 +159,7 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidCount" }, { "u64": 1 @@ -168,7 +168,7 @@ }, "durability": "persistent", "val": { - "vec": [] + "u32": 0 } } }, diff --git a/contracts/job_registry/test_snapshots/test/test_duplicate_bid_panics.1.json b/contracts/job_registry/test_snapshots/test/test_duplicate_bid_panics.1.json index 28f0fdc2..54398b2c 100644 --- a/contracts/job_registry/test_snapshots/test/test_duplicate_bid_panics.1.json +++ b/contracts/job_registry/test_snapshots/test/test_duplicate_bid_panics.1.json @@ -197,10 +197,13 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidByIndex" }, { "u64": 1 + }, + { + "u32": 0 } ] }, @@ -217,35 +220,147 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidByIndex" }, { "u64": 1 + }, + { + "u32": 0 } ] }, "durability": "persistent", "val": { + "map": [ + { + "key": { + "symbol": "freelancer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "proposal_hash" + }, + "val": { + "bytes": "516d50726f706f73616c" + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidCount" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { "vec": [ { - "map": [ - { - "key": { - "symbol": "freelancer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - }, - { - "key": { - "symbol": "proposal_hash" - }, - "val": { - "bytes": "516d50726f706f73616c" - } - } - ] + "symbol": "BidCount" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidLookup" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidLookup" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "freelancer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "proposal_hash" + }, + "val": { + "bytes": "516d50726f706f73616c" + } } ] } diff --git a/contracts/job_registry/test_snapshots/test/test_full_lifecycle.1.json b/contracts/job_registry/test_snapshots/test/test_full_lifecycle.1.json index 79660297..4a241ec9 100644 --- a/contracts/job_registry/test_snapshots/test/test_full_lifecycle.1.json +++ b/contracts/job_registry/test_snapshots/test/test_full_lifecycle.1.json @@ -317,10 +317,13 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidByIndex" }, { "u64": 1 + }, + { + "u32": 0 } ] }, @@ -337,35 +340,147 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidByIndex" }, { "u64": 1 + }, + { + "u32": 0 } ] }, "durability": "persistent", "val": { + "map": [ + { + "key": { + "symbol": "freelancer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "proposal_hash" + }, + "val": { + "bytes": "516d50726f706f73616c48617368" + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidCount" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { "vec": [ { - "map": [ - { - "key": { - "symbol": "freelancer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - }, - { - "key": { - "symbol": "proposal_hash" - }, - "val": { - "bytes": "516d50726f706f73616c48617368" - } - } - ] + "symbol": "BidCount" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidLookup" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidLookup" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "freelancer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "proposal_hash" + }, + "val": { + "bytes": "516d50726f706f73616c48617368" + } } ] } @@ -1294,7 +1409,7 @@ "val": { "vec": [ { - "symbol": "InProgress" + "symbol": "Assigned" } ] } diff --git a/contracts/job_registry/test_snapshots/test/test_get_bids_for_missing_job_panics.1.json b/contracts/job_registry/test_snapshots/test/test_get_bids_for_missing_job_panics.1.json new file mode 100644 index 00000000..132eda61 --- /dev/null +++ b/contracts/job_registry/test_snapshots/test/test_get_bids_for_missing_job_panics.1.json @@ -0,0 +1,419 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 21, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextJobId" + } + ] + }, + "val": { + "u64": 1 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "initialize" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "log" + } + ], + "data": { + "vec": [ + { + "string": "JobRegistry initialized with admin: {}" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "init" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "initialize" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "get_bids" + } + ], + "data": { + "u64": 404 + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 7 + } + } + ], + "data": { + "vec": [ + { + "string": "failing with contract error" + }, + { + "u32": 7 + } + ] + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 7 + } + } + ], + "data": { + "string": "escalating error to panic" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 7 + } + } + ], + "data": { + "string": "caught error from function" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 7 + } + } + ], + "data": { + "vec": [ + { + "string": "contract call failed" + }, + { + "symbol": "get_bids" + }, + { + "vec": [ + { + "u64": 404 + } + ] + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 7 + } + } + ], + "data": { + "string": "escalating error to panic" + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file diff --git a/contracts/job_registry/test_snapshots/test/test_get_deliverable_without_submission_panics.1.json b/contracts/job_registry/test_snapshots/test/test_get_deliverable_without_submission_panics.1.json index 393eec55..30d9fadf 100644 --- a/contracts/job_registry/test_snapshots/test/test_get_deliverable_without_submission_panics.1.json +++ b/contracts/job_registry/test_snapshots/test/test_get_deliverable_without_submission_panics.1.json @@ -139,7 +139,7 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidCount" }, { "u64": 1 @@ -159,7 +159,7 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidCount" }, { "u64": 1 @@ -168,7 +168,7 @@ }, "durability": "persistent", "val": { - "vec": [] + "u32": 0 } } }, diff --git a/contracts/job_registry/test_snapshots/test/test_late_bid_after_acceptance_panics_with_job_not_open.1.json b/contracts/job_registry/test_snapshots/test/test_late_bid_after_acceptance_panics_with_job_not_open.1.json new file mode 100644 index 00000000..5941fed7 --- /dev/null +++ b/contracts/job_registry/test_snapshots/test/test_late_bid_after_acceptance_panics_with_job_not_open.1.json @@ -0,0 +1,1244 @@ +{ + "generators": { + "address": 5, + "nonce": 0 + }, + "auth": [ + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "post_job", + "args": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "bytes": "516d48617368" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "submit_bid", + "args": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "bytes": "516d50726f706f73616c" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "accept_bid", + "args": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 21, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidByIndex" + }, + { + "u64": 1 + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidByIndex" + }, + { + "u64": 1 + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "freelancer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "proposal_hash" + }, + "val": { + "bytes": "516d50726f706f73616c" + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidCount" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidCount" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidLookup" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidLookup" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "freelancer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "proposal_hash" + }, + "val": { + "bytes": "516d50726f706f73616c" + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Job" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Job" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "budget_stroops" + }, + "val": { + "i128": { + "hi": 0, + "lo": 5000 + } + } + }, + { + "key": { + "symbol": "client" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "freelancer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "metadata_hash" + }, + "val": { + "bytes": "516d48617368" + } + }, + { + "key": { + "symbol": "status" + }, + "val": { + "vec": [ + { + "symbol": "Assigned" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextJobId" + } + ] + }, + "val": { + "u64": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "initialize" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "log" + } + ], + "data": { + "vec": [ + { + "string": "JobRegistry initialized with admin: {}" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "init" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "initialize" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "post_job" + } + ], + "data": { + "vec": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "bytes": "516d48617368" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "log" + } + ], + "data": { + "vec": [ + { + "string": "post_job: id {} client {} budget {}" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "jobpost" + }, + { + "u64": 1 + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "post_job" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "submit_bid" + } + ], + "data": { + "vec": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "bytes": "516d50726f706f73616c" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "log" + } + ], + "data": { + "vec": [ + { + "string": "submit_bid: id {} freelancer {}" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "bid" + }, + { + "u64": 1 + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "submit_bid" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "accept_bid" + } + ], + "data": { + "vec": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "log" + } + ], + "data": { + "vec": [ + { + "string": "accept_bid: id {} client {} freelancer {}" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "accept" + }, + { + "u64": 1 + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "accept_bid" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "submit_bid" + } + ], + "data": { + "vec": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "bytes": "516d4c61746550726f706f73616c" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 8 + } + } + ], + "data": { + "vec": [ + { + "string": "failing with contract error" + }, + { + "u32": 8 + } + ] + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 8 + } + } + ], + "data": { + "string": "escalating error to panic" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 8 + } + } + ], + "data": { + "string": "caught error from function" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 8 + } + } + ], + "data": { + "vec": [ + { + "string": "contract call failed" + }, + { + "symbol": "submit_bid" + }, + { + "vec": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "bytes": "516d4c61746550726f706f73616c" + } + ] + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 8 + } + } + ], + "data": { + "string": "escalating error to panic" + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file diff --git a/contracts/job_registry/test_snapshots/test/test_mark_disputed_from_in_progress.1.json b/contracts/job_registry/test_snapshots/test/test_mark_disputed_from_in_progress.1.json index 34ff79eb..f77395e7 100644 --- a/contracts/job_registry/test_snapshots/test/test_mark_disputed_from_in_progress.1.json +++ b/contracts/job_registry/test_snapshots/test/test_mark_disputed_from_in_progress.1.json @@ -307,10 +307,13 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidByIndex" }, { "u64": 1 + }, + { + "u32": 0 } ] }, @@ -327,35 +330,147 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidByIndex" }, { "u64": 1 + }, + { + "u32": 0 } ] }, "durability": "persistent", "val": { + "map": [ + { + "key": { + "symbol": "freelancer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "proposal_hash" + }, + "val": { + "bytes": "516d50726f706f73616c" + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidCount" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { "vec": [ { - "map": [ - { - "key": { - "symbol": "freelancer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - }, - { - "key": { - "symbol": "proposal_hash" - }, - "val": { - "bytes": "516d50726f706f73616c" - } - } - ] + "symbol": "BidCount" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidLookup" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidLookup" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "freelancer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "proposal_hash" + }, + "val": { + "bytes": "516d50726f706f73616c" + } } ] } diff --git a/contracts/job_registry/test_snapshots/test/test_mark_disputed_from_open_panics.1.json b/contracts/job_registry/test_snapshots/test/test_mark_disputed_from_open_panics.1.json index e0db2e6b..e0148afb 100644 --- a/contracts/job_registry/test_snapshots/test/test_mark_disputed_from_open_panics.1.json +++ b/contracts/job_registry/test_snapshots/test/test_mark_disputed_from_open_panics.1.json @@ -139,7 +139,7 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidCount" }, { "u64": 1 @@ -159,7 +159,7 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidCount" }, { "u64": 1 @@ -168,7 +168,7 @@ }, "durability": "persistent", "val": { - "vec": [] + "u32": 0 } } }, diff --git a/contracts/job_registry/test_snapshots/test/test_oversized_cid_panics_with_invalid_hash.1.json b/contracts/job_registry/test_snapshots/test/test_oversized_cid_panics_with_invalid_hash.1.json new file mode 100644 index 00000000..f5252812 --- /dev/null +++ b/contracts/job_registry/test_snapshots/test/test_oversized_cid_panics_with_invalid_hash.1.json @@ -0,0 +1,447 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 21, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextJobId" + } + ] + }, + "val": { + "u64": 1 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "initialize" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "log" + } + ], + "data": { + "vec": [ + { + "string": "JobRegistry initialized with admin: {}" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "init" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "initialize" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "post_job" + } + ], + "data": { + "vec": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "bytes": "61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 5 + } + } + ], + "data": { + "vec": [ + { + "string": "failing with contract error" + }, + { + "u32": 5 + } + ] + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 5 + } + } + ], + "data": { + "string": "escalating error to panic" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 5 + } + } + ], + "data": { + "string": "caught error from function" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 5 + } + } + ], + "data": { + "vec": [ + { + "string": "contract call failed" + }, + { + "symbol": "post_job" + }, + { + "vec": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "bytes": "61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 5 + } + } + ], + "data": { + "string": "escalating error to panic" + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file diff --git a/contracts/job_registry/test_snapshots/test/test_post_job_auto_allocates_sequential_ids.1.json b/contracts/job_registry/test_snapshots/test/test_post_job_auto_allocates_sequential_ids.1.json index 502e0787..fa395e09 100644 --- a/contracts/job_registry/test_snapshots/test/test_post_job_auto_allocates_sequential_ids.1.json +++ b/contracts/job_registry/test_snapshots/test/test_post_job_auto_allocates_sequential_ids.1.json @@ -197,7 +197,7 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidCount" }, { "u64": 1 @@ -217,7 +217,7 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidCount" }, { "u64": 1 @@ -226,7 +226,7 @@ }, "durability": "persistent", "val": { - "vec": [] + "u32": 0 } } }, @@ -242,7 +242,7 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidCount" }, { "u64": 2 @@ -262,7 +262,7 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidCount" }, { "u64": 2 @@ -271,7 +271,7 @@ }, "durability": "persistent", "val": { - "vec": [] + "u32": 0 } } }, diff --git a/contracts/job_registry/test_snapshots/test/test_post_job_with_explicit_id_updates_next_job_id.1.json b/contracts/job_registry/test_snapshots/test/test_post_job_with_explicit_id_updates_next_job_id.1.json index f5bfbf53..b6a2827e 100644 --- a/contracts/job_registry/test_snapshots/test/test_post_job_with_explicit_id_updates_next_job_id.1.json +++ b/contracts/job_registry/test_snapshots/test/test_post_job_with_explicit_id_updates_next_job_id.1.json @@ -139,7 +139,7 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidCount" }, { "u64": 42 @@ -159,7 +159,7 @@ "key": { "vec": [ { - "symbol": "Bids" + "symbol": "BidCount" }, { "u64": 42 @@ -168,7 +168,7 @@ }, "durability": "persistent", "val": { - "vec": [] + "u32": 0 } } }, diff --git a/contracts/job_registry/test_snapshots/test/test_set_escrow_deployer_round_trip.1.json b/contracts/job_registry/test_snapshots/test/test_set_escrow_deployer_round_trip.1.json new file mode 100644 index 00000000..b49d897d --- /dev/null +++ b/contracts/job_registry/test_snapshots/test/test_set_escrow_deployer_round_trip.1.json @@ -0,0 +1,452 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "set_escrow_deployer", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 21, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "EscrowDeployer" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextJobId" + } + ] + }, + "val": { + "u64": 1 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "initialize" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "log" + } + ], + "data": { + "vec": [ + { + "string": "JobRegistry initialized with admin: {}" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "init" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "initialize" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "set_escrow_deployer" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "log" + } + ], + "data": { + "vec": [ + { + "string": "escrow_deployer configured: {}" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "escrow" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "set_escrow_deployer" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "get_escrow_deployer" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "get_escrow_deployer" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file diff --git a/contracts/job_registry/test_snapshots/test/test_unauthorized_accept_bid_panics_with_specific_error.1.json b/contracts/job_registry/test_snapshots/test/test_unauthorized_accept_bid_panics_with_specific_error.1.json new file mode 100644 index 00000000..f4767467 --- /dev/null +++ b/contracts/job_registry/test_snapshots/test/test_unauthorized_accept_bid_panics_with_specific_error.1.json @@ -0,0 +1,1071 @@ +{ + "generators": { + "address": 5, + "nonce": 0 + }, + "auth": [ + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "post_job", + "args": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "bytes": "516d48617368" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "submit_bid", + "args": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "bytes": "516d50726f706f73616c" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 21, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidByIndex" + }, + { + "u64": 1 + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidByIndex" + }, + { + "u64": 1 + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "freelancer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "proposal_hash" + }, + "val": { + "bytes": "516d50726f706f73616c" + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidCount" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidCount" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidLookup" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "BidLookup" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "freelancer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "proposal_hash" + }, + "val": { + "bytes": "516d50726f706f73616c" + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Job" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "vec": [ + { + "symbol": "Job" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "budget_stroops" + }, + "val": { + "i128": { + "hi": 0, + "lo": 5000 + } + } + }, + { + "key": { + "symbol": "client" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "freelancer" + }, + "val": "void" + }, + { + "key": { + "symbol": "metadata_hash" + }, + "val": { + "bytes": "516d48617368" + } + }, + { + "key": { + "symbol": "status" + }, + "val": { + "vec": [ + { + "symbol": "Open" + } + ] + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextJobId" + } + ] + }, + "val": { + "u64": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "initialize" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "log" + } + ], + "data": { + "vec": [ + { + "string": "JobRegistry initialized with admin: {}" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "init" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "initialize" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "post_job" + } + ], + "data": { + "vec": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "bytes": "516d48617368" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "log" + } + ], + "data": { + "vec": [ + { + "string": "post_job: id {} client {} budget {}" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "jobpost" + }, + { + "u64": 1 + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "post_job" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "submit_bid" + } + ], + "data": { + "vec": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "bytes": "516d50726f706f73616c" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "log" + } + ], + "data": { + "vec": [ + { + "string": "submit_bid: id {} freelancer {}" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "bid" + }, + { + "u64": 1 + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "submit_bid" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "symbol": "accept_bid" + } + ], + "data": { + "vec": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 9 + } + } + ], + "data": { + "vec": [ + { + "string": "failing with contract error" + }, + { + "u32": 9 + } + ] + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 9 + } + } + ], + "data": { + "string": "escalating error to panic" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 9 + } + } + ], + "data": { + "string": "caught error from function" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 9 + } + } + ], + "data": { + "vec": [ + { + "string": "contract call failed" + }, + { + "symbol": "accept_bid" + }, + { + "vec": [ + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 9 + } + } + ], + "data": { + "string": "escalating error to panic" + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file From bd6a88c300234c721d23f0b841f40afaa506a60c Mon Sep 17 00:00:00 2001 From: Okeke Chinedu Emmanuel Date: Thu, 28 May 2026 15:57:53 +0100 Subject: [PATCH 3/3] Implement dynamic job registry service fees --- contracts/job_registry/src/lib.rs | 1129 +++++++++++++---------------- 1 file changed, 494 insertions(+), 635 deletions(-) diff --git a/contracts/job_registry/src/lib.rs b/contracts/job_registry/src/lib.rs index 7bec419d..326c060d 100644 --- a/contracts/job_registry/src/lib.rs +++ b/contracts/job_registry/src/lib.rs @@ -5,12 +5,21 @@ use soroban_sdk::{ Address, Bytes, Env, Vec, }; -const MAX_HASH_LEN: u32 = 96; - -// Requirement [SC-REG-037]: Contract-wide budget floor and ceiling enforced at input validation. -// MIN prevents dust spam; MAX caps exposure to a realistic large project value. -const MIN_BUDGET_STROOPS: i128 = 100_000; // 0.01 XLM +/// Compact content-addressed metadata/proposals only. IPFS CIDv0/v1 strings are +/// comfortably below this cap, while full briefs and proposals must stay off-chain. +const MAX_CID_LEN: u32 = 96; +const MIN_BUDGET_STROOPS: i128 = 100_000; // 0.01 XLM const MAX_BUDGET_STROOPS: i128 = 100_000_000_000_000; // 10,000,000 XLM +const BASIS_POINTS_DENOMINATOR: i128 = 10_000; +const MAX_CONFIGURABLE_FEE_BPS: u32 = 2_500; // 25%, safely below confiscatory fees. +const MAX_BIDS_PER_JOB: u32 = 1_000; + +const DEFAULT_BASE_FEE_BPS: u32 = 250; +const DEFAULT_BUDGET_STEP_STROOPS: i128 = 10_000_000_000; // 1,000 XLM +const DEFAULT_STEP_FEE_BPS: u32 = 10; +const DEFAULT_MAX_FEE_BPS: u32 = 750; +const DEFAULT_HIGH_VALUE_THRESHOLD_STROOPS: i128 = 250_000_000_000; // 25,000 XLM +const DEFAULT_HIGH_VALUE_DISCOUNT_BPS: u32 = 75; #[contracterror] #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] @@ -20,7 +29,7 @@ pub enum JobRegistryError { NotInitialized = 2, InvalidJobId = 3, InvalidBudget = 4, - InvalidHash = 5, + InvalidCid = 5, JobAlreadyExists = 6, JobNotFound = 7, JobNotOpen = 8, @@ -36,6 +45,9 @@ pub enum JobRegistryError { JobNotExpired = 18, CollateralNotFound = 19, CollateralAlreadyReleased = 20, + InvalidFeeConfig = 21, + BidLimitReached = 22, + BidIndexOutOfBounds = 23, } #[contracttype] @@ -51,22 +63,42 @@ pub enum JobStatus { } #[contracttype] -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] +pub struct FeeConfig { + pub base_fee_bps: u32, + pub budget_step_stroops: i128, + pub step_fee_bps: u32, + pub max_fee_bps: u32, + pub high_value_threshold_stroops: i128, + pub high_value_discount_bps: u32, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct FeeQuote { + pub fee_bps: u32, + pub fee_stroops: i128, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] pub struct JobRecord { pub client: Address, pub freelancer: Option
, - pub metadata_hash: Bytes, + pub metadata_cid: Bytes, pub budget_stroops: i128, + pub service_fee_bps: u32, + pub service_fee_stroops: i128, pub expires_at: u64, pub status: JobStatus, pub bidding_deadline: u64, } #[contracttype] -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] pub struct BidRecord { pub freelancer: Address, - pub proposal_hash: Bytes, + pub proposal_cid: Bytes, pub collateral_stroops: i128, pub collateral_released: bool, } @@ -75,12 +107,13 @@ pub struct BidRecord { pub enum DataKey { Admin, NextJobId, + FeeConfig, + EscrowDeployer, Job(u64), BidCount(u64), - Bid(u64, u32), - BidIndex(u64, Address), + BidByIndex(u64, u32), + BidLookup(u64, Address), Deliverable(u64), - EscrowDeployer, } #[contract] @@ -94,11 +127,12 @@ impl JobRegistryContract { } admin.require_auth(); - env.storage().instance().set(&DataKey::Admin, &admin); env.storage().instance().set(&DataKey::NextJobId, &1u64); - - log!(&env, "initialized"); + env.storage() + .instance() + .set(&DataKey::FeeConfig, &default_fee_config()); + log!(&env, "job registry initialized"); } pub fn is_initialized(env: Env) -> bool { @@ -113,48 +147,63 @@ impl JobRegistryContract { read_next_job_id(&env) } + pub fn get_fee_config(env: Env) -> FeeConfig { + ensure_initialized(&env); + read_fee_config(&env) + } + + pub fn set_fee_config(env: Env, config: FeeConfig) { + ensure_initialized(&env); + let admin = read_admin(&env); + admin.require_auth(); + validate_fee_config(&env, &config); + env.storage().instance().set(&DataKey::FeeConfig, &config); + env.events() + .publish((symbol_short!("feecfg"),), config.max_fee_bps); + } + + pub fn quote_service_fee(env: Env, budget_stroops: i128) -> FeeQuote { + ensure_initialized(&env); + validate_budget(&env, budget_stroops); + compute_service_fee(&env, budget_stroops, &read_fee_config(&env)) + } + pub fn post_job( env: Env, job_id: u64, client: Address, - hash: Bytes, - budget: i128, + metadata_cid: Bytes, + budget_stroops: i128, bidding_deadline: u64, expires_at: u64, ) { ensure_initialized(&env); - validate_job_input( &env, job_id, - &hash, - budget, + &metadata_cid, + budget_stroops, bidding_deadline, expires_at, ); - client.require_auth(); post_job_with_id( &env, job_id, client.clone(), - hash, - budget, + metadata_cid, + budget_stroops, bidding_deadline, expires_at, ); let next_job_id = read_next_job_id(&env); - if job_id >= next_job_id { let updated = job_id .checked_add(1) .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::Overflow)); - - env.storage() - .instance() - .set(&DataKey::NextJobId, &updated); + env.storage().instance().set(&DataKey::NextJobId, &updated); } env.events() @@ -164,32 +213,29 @@ impl JobRegistryContract { pub fn post_job_auto( env: Env, client: Address, - hash: Bytes, - budget: i128, + metadata_cid: Bytes, + budget_stroops: i128, bidding_deadline: u64, expires_at: u64, ) -> u64 { ensure_initialized(&env); - let job_id = read_next_job_id(&env); - validate_job_input( &env, job_id, - &hash, - budget, + &metadata_cid, + budget_stroops, bidding_deadline, expires_at, ); - client.require_auth(); post_job_with_id( &env, job_id, client.clone(), - hash, - budget, + metadata_cid, + budget_stroops, bidding_deadline, expires_at, ); @@ -197,26 +243,9 @@ impl JobRegistryContract { let next = job_id .checked_add(1) .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::Overflow)); - env.storage().instance().set(&DataKey::NextJobId, &next); - - job_id - } - - /// Admin configures the off-chain/cross-contract escrow deployer address signaled on acceptance. - pub fn set_escrow_deployer(env: Env, escrow_deployer: Address) { - ensure_initialized(&env); - let admin = read_admin(&env); - admin.require_auth(); - - env.storage() - .instance() - .set(&DataKey::EscrowDeployer, &escrow_deployer); - - log!(&env, "escrow_deployer configured: {}", escrow_deployer); env.events() - .publish((symbol_short!("jobauto"), job_id), (client, budget)); - + .publish((symbol_short!("jobauto"), job_id), (client, budget_stroops)); job_id } @@ -225,12 +254,9 @@ impl JobRegistryContract { ensure_initialized(&env); let admin = read_admin(&env); admin.require_auth(); - env.storage() .instance() .set(&DataKey::EscrowDeployer, &escrow_deployer); - - log!(&env, "escrow_deployer configured: {}", escrow_deployer); env.events() .publish((symbol_short!("escrow"),), escrow_deployer); } @@ -244,139 +270,85 @@ impl JobRegistryContract { env: Env, job_id: u64, freelancer: Address, - proposal_hash: Bytes, + proposal_cid: Bytes, collateral_stroops: i128, ) { ensure_initialized(&env); - - validate_hash(&env, &proposal_hash); - + validate_cid(&env, &proposal_cid); freelancer.require_auth(); - let key = DataKey::Job(job_id); - - let job: JobRecord = env - .storage() - .persistent() - .get(&key) - .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::JobNotFound)); - + let job = read_job(&env, job_id); if job.status != JobStatus::Open { panic_with_error!(&env, JobRegistryError::JobNotOpen); } - if env.ledger().timestamp() > job.bidding_deadline { panic_with_error!(&env, JobRegistryError::BidWindowClosed); } - if env.ledger().timestamp() >= job.expires_at { panic_with_error!(&env, JobRegistryError::JobExpired); } - if collateral_stroops < 0 { panic_with_error!(&env, JobRegistryError::InvalidBudget); } - let bids_key = DataKey::Bids(job_id); - - let mut bids: Vec = env - .storage() - .persistent() - .get(&bids_key) - .unwrap_or(Vec::new(&env)); - - for bid in bids.iter() { - if bid.freelancer == freelancer { - panic_with_error!(&env, JobRegistryError::BidAlreadySubmitted); - } - // Bids are persisted as map-like rows keyed by (job_id, freelancer) and - // indexed by (job_id, ordinal). This avoids rewriting a growing vector on - // every bid while preserving deterministic iteration for `get_bids`. let lookup_key = DataKey::BidLookup(job_id, freelancer.clone()); if env.storage().persistent().has(&lookup_key) { panic_with_error!(&env, JobRegistryError::BidAlreadySubmitted); } - let count_key = DataKey::BidCount(job_id); - let bid_count: u32 = env.storage().persistent().get(&count_key).unwrap_or(0u32); + let bid_count = read_bid_count(&env, job_id); if bid_count >= MAX_BIDS_PER_JOB { panic_with_error!(&env, JobRegistryError::BidLimitReached); } - - let bid_count = read_bid_count(&env, job_id); let next_count = bid_count .checked_add(1) .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::Overflow)); + let bid = BidRecord { freelancer: freelancer.clone(), - proposal_hash, + proposal_cid, collateral_stroops, collateral_released: false, - }); - - env.storage().persistent().set(&bids_key, &bids); + }; + env.storage() + .persistent() + .set(&DataKey::BidByIndex(job_id, bid_count), &bid); + env.storage().persistent().set(&lookup_key, &bid_count); + env.storage() + .persistent() + .set(&DataKey::BidCount(job_id), &next_count); env.events() .publish((symbol_short!("bid"), job_id), freelancer); } - pub fn accept_bid( - env: Env, - job_id: u64, - client: Address, - freelancer: Address, - ) { + pub fn accept_bid(env: Env, job_id: u64, client: Address, freelancer: Address) { ensure_initialized(&env); - client.require_auth(); let key = DataKey::Job(job_id); - - let mut job: JobRecord = env - .storage() - .persistent() - .get(&key) - .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::JobNotFound)); - + let mut job = read_job(&env, job_id); if job.status != JobStatus::Open { panic_with_error!(&env, JobRegistryError::JobNotOpen); } - if client != job.client { panic_with_error!(&env, JobRegistryError::Unauthorized); } - if env.ledger().timestamp() >= job.expires_at { panic_with_error!(&env, JobRegistryError::JobExpired); } - let bids: Vec = env - .storage() - .persistent() - .get(&DataKey::Bids(job_id)) - .unwrap_or(Vec::new(&env)); - - let mut found = false; - - for bid in bids.iter() { - if bid.freelancer == freelancer { - found = true; - break; - } - } - - if !found { + let lookup_key = DataKey::BidLookup(job_id, freelancer.clone()); + if !env.storage().persistent().has(&lookup_key) { panic_with_error!(&env, JobRegistryError::BidNotFound); } - // Validate every invariant before mutating the job. Acceptance records the - // selected freelancer and moves the registry to Assigned; the configured - // escrow deployer can then provision/fund the escrow without ambiguity. + // All authorization, state, expiry, and bid-existence invariants are + // validated before the only state mutation, yielding an auditable atomic + // transition from Open to Assigned. job.freelancer = Some(freelancer.clone()); job.status = JobStatus::Assigned; - env.storage().persistent().set(&key, &job); - env.events() .publish((symbol_short!("accept"), job_id), freelancer.clone()); @@ -392,168 +364,102 @@ impl JobRegistryContract { } } - pub fn refund_bid_collateral( - env: Env, - job_id: u64, - freelancer: Address, - ) { + pub fn refund_bid_collateral(env: Env, job_id: u64, freelancer: Address) { ensure_initialized(&env); - freelancer.require_auth(); - - release_collateral(&env, job_id, freelancer, false); + release_collateral(&env, job_id, freelancer); } - pub fn slash_bid_collateral( - env: Env, - job_id: u64, - client: Address, - freelancer: Address, - ) { + pub fn slash_bid_collateral(env: Env, job_id: u64, client: Address, freelancer: Address) { ensure_initialized(&env); - client.require_auth(); - - let job: JobRecord = env - .storage() - .persistent() - .get(&DataKey::Job(job_id)) - .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::JobNotFound)); - + let job = read_job(&env, job_id); if client != job.client { panic_with_error!(&env, JobRegistryError::Unauthorized); } - - release_collateral(&env, job_id, freelancer, true); + release_collateral(&env, job_id, freelancer); } - pub fn cancel_expired_job( - env: Env, - job_id: u64, - client: Address, - ) { + pub fn cancel_expired_job(env: Env, job_id: u64, client: Address) { ensure_initialized(&env); - client.require_auth(); let key = DataKey::Job(job_id); - - let mut job: JobRecord = env - .storage() - .persistent() - .get(&key) - .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::JobNotFound)); - + let mut job = read_job(&env, job_id); if job.status != JobStatus::Open { panic_with_error!(&env, JobRegistryError::InvalidStateTransition); } - if client != job.client { panic_with_error!(&env, JobRegistryError::Unauthorized); } - if env.ledger().timestamp() < job.expires_at { panic_with_error!(&env, JobRegistryError::JobNotExpired); } job.status = JobStatus::Expired; - env.storage().persistent().set(&key, &job); - env.events() .publish((symbol_short!("expired"), job_id), client); } - pub fn submit_deliverable( - env: Env, - job_id: u64, - freelancer: Address, - hash: Bytes, - ) { + pub fn submit_deliverable(env: Env, job_id: u64, freelancer: Address, deliverable_cid: Bytes) { ensure_initialized(&env); - - validate_hash(&env, &hash); - + validate_cid(&env, &deliverable_cid); freelancer.require_auth(); let key = DataKey::Job(job_id); - - let mut job: JobRecord = env - .storage() - .persistent() - .get(&key) - .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::JobNotFound)); - + let mut job = read_job(&env, job_id); if job.status != JobStatus::Assigned { panic_with_error!(&env, JobRegistryError::InvalidStateTransition); } - if job.freelancer != Some(freelancer.clone()) { panic_with_error!(&env, JobRegistryError::Unauthorized); } job.status = JobStatus::DeliverableSubmitted; - env.storage().persistent().set(&key, &job); - env.storage() .persistent() - .set(&DataKey::Deliverable(job_id), &hash); - + .set(&DataKey::Deliverable(job_id), &deliverable_cid); env.events() .publish((symbol_short!("deliver"), job_id), freelancer); } pub fn mark_disputed(env: Env, job_id: u64) { ensure_initialized(&env); - let admin = read_admin(&env); - admin.require_auth(); let key = DataKey::Job(job_id); - - let mut job: JobRecord = env - .storage() - .persistent() - .get(&key) - .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::JobNotFound)); - - if job.status != JobStatus::Assigned - && job.status != JobStatus::DeliverableSubmitted - { + let mut job = read_job(&env, job_id); + if job.status != JobStatus::Assigned && job.status != JobStatus::DeliverableSubmitted { panic_with_error!(&env, JobRegistryError::InvalidStateTransition); } - job.status = JobStatus::Disputed; - env.storage().persistent().set(&key, &job); } pub fn get_job(env: Env, job_id: u64) -> JobRecord { ensure_initialized(&env); + read_job(&env, job_id) + } - env.storage() - .persistent() - .get(&DataKey::Job(job_id)) - .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::JobNotFound)) + pub fn get_bid_at(env: Env, job_id: u64, index: u32) -> BidRecord { + ensure_initialized(&env); + let count = read_bid_count(&env, job_id); + if index >= count { + panic_with_error!(&env, JobRegistryError::BidIndexOutOfBounds); + } + read_bid_at(&env, job_id, index) } pub fn get_bids(env: Env, job_id: u64) -> Vec { ensure_initialized(&env); - env.storage() - .persistent() - .get(&DataKey::BidCount(job_id)) - .unwrap_or(0u32); + let count = read_bid_count(&env, job_id); let mut bids = Vec::new(&env); let mut index = 0u32; while index < count { - let bid: BidRecord = env - .storage() - .persistent() - .get(&DataKey::BidByIndex(job_id, index)) - .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::BidNotFound)); - bids.push_back(bid); + bids.push_back(read_bid_at(&env, job_id, index)); index = index .checked_add(1) .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::Overflow)); @@ -561,75 +467,35 @@ impl JobRegistryContract { bids } - // Requirement [SC-REG-039]: Gas-efficient paginated getter avoids loading the full bids vector - // when only a window of records is needed. Callers supply an offset and a limit; the function - // returns at most `limit` entries starting at `offset`, clamping automatically at the end. pub fn get_bids_page(env: Env, job_id: u64, offset: u32, limit: u32) -> Vec { ensure_initialized(&env); - let all_bids: Vec = env - .storage() - .persistent() - .get(&DataKey::Bids(job_id)) - .unwrap_or(Vec::new(&env)); - - let total = all_bids.len(); - let start = offset.min(total); - let end = (start.saturating_add(limit)).min(total); - + let count = read_bid_count(&env, job_id); let mut page = Vec::new(&env); - for i in start..end { - page.push_back(all_bids.get_unchecked(i)); + if offset >= count || limit == 0 { + return page; } - page - } - - // Requirement [SC-REG-039]: Returns only the length of the bids vector without deserialising - // each entry, keeping the read cost proportional to one storage key lookup rather than O(n). - pub fn get_bids_count(env: Env, job_id: u64) -> u32 { - ensure_initialized(&env); - env.storage() - .persistent() - .get::<_, Vec>(&DataKey::Bids(job_id)) - .map(|bids| bids.len()) - .unwrap_or(0) - } - - // Requirement [SC-REG-039]: Gas-efficient paginated getter avoids loading the full bids vector - // when only a window of records is needed. Callers supply an offset and a limit; the function - // returns at most `limit` entries starting at `offset`, clamping automatically at the end. - pub fn get_bids_page(env: Env, job_id: u64, offset: u32, limit: u32) -> Vec { - ensure_initialized(&env); - let all_bids: Vec = env - .storage() - .persistent() - .get(&DataKey::Bids(job_id)) - .unwrap_or(Vec::new(&env)); - let total = all_bids.len(); - let start = offset.min(total); - let end = (start.saturating_add(limit)).min(total); - - let mut page = Vec::new(&env); - for i in start..end { - page.push_back(all_bids.get_unchecked(i)); + let requested_end = offset + .checked_add(limit) + .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::Overflow)); + let end = requested_end.min(count); + let mut index = offset; + while index < end { + page.push_back(read_bid_at(&env, job_id, index)); + index = index + .checked_add(1) + .unwrap_or_else(|| panic_with_error!(&env, JobRegistryError::Overflow)); } page } - // Requirement [SC-REG-039]: Returns only the length of the bids vector without deserialising - // each entry, keeping the read cost proportional to one storage key lookup rather than O(n). pub fn get_bids_count(env: Env, job_id: u64) -> u32 { ensure_initialized(&env); - env.storage() - .persistent() - .get::<_, Vec>(&DataKey::Bids(job_id)) - .map(|bids| bids.len()) - .unwrap_or(0) + read_bid_count(&env, job_id) } pub fn get_deliverable(env: Env, job_id: u64) -> Bytes { ensure_initialized(&env); - env.storage() .persistent() .get(&DataKey::Deliverable(job_id)) @@ -657,48 +523,114 @@ fn read_next_job_id(env: &Env) -> u64 { .unwrap_or_else(|| panic_with_error!(env, JobRegistryError::NotInitialized)) } +fn read_fee_config(env: &Env) -> FeeConfig { + env.storage() + .instance() + .get(&DataKey::FeeConfig) + .unwrap_or(default_fee_config()) +} + +fn default_fee_config() -> FeeConfig { + FeeConfig { + base_fee_bps: DEFAULT_BASE_FEE_BPS, + budget_step_stroops: DEFAULT_BUDGET_STEP_STROOPS, + step_fee_bps: DEFAULT_STEP_FEE_BPS, + max_fee_bps: DEFAULT_MAX_FEE_BPS, + high_value_threshold_stroops: DEFAULT_HIGH_VALUE_THRESHOLD_STROOPS, + high_value_discount_bps: DEFAULT_HIGH_VALUE_DISCOUNT_BPS, + } +} + fn validate_job_input( env: &Env, job_id: u64, - hash: &Bytes, - budget: i128, + metadata_cid: &Bytes, + budget_stroops: i128, bidding_deadline: u64, expires_at: u64, ) { if job_id == 0 { panic_with_error!(env, JobRegistryError::InvalidJobId); } - // Requirement [SC-REG-037]: Verify Budget Bounds against Contract Minimum and Maximum limits. - // Rejects dust amounts and unrealistically large values to prevent storage abuse. - if budget < MIN_BUDGET_STROOPS || budget > MAX_BUDGET_STROOPS { - panic_with_error!(env, JobRegistryError::InvalidBudget); - } - + validate_budget(env, budget_stroops); + validate_cid(env, metadata_cid); if bidding_deadline <= env.ledger().timestamp() { panic_with_error!(env, JobRegistryError::BidWindowClosed); } - if bidding_deadline >= expires_at { panic_with_error!(env, JobRegistryError::InvalidExpiration); } - - validate_hash(env, hash); validate_expiration(env, expires_at); } -fn validate_expiration(env: &Env, expires_at: u64) { - let now = env.ledger().timestamp(); +fn validate_budget(env: &Env, budget_stroops: i128) { + if !(MIN_BUDGET_STROOPS..=MAX_BUDGET_STROOPS).contains(&budget_stroops) { + panic_with_error!(env, JobRegistryError::InvalidBudget); + } +} - if expires_at == 0 || expires_at <= now { +fn validate_expiration(env: &Env, expires_at: u64) { + if expires_at == 0 || expires_at <= env.ledger().timestamp() { panic_with_error!(env, JobRegistryError::InvalidExpiration); } } -fn validate_hash(env: &Env, hash: &Bytes) { - let len = hash.len(); +fn validate_cid(env: &Env, cid: &Bytes) { + let len = cid.len(); + if len == 0 || len > MAX_CID_LEN { + panic_with_error!(env, JobRegistryError::InvalidCid); + } +} + +fn validate_fee_config(env: &Env, config: &FeeConfig) { + if config.max_fee_bps > MAX_CONFIGURABLE_FEE_BPS + || config.base_fee_bps > config.max_fee_bps + || config.budget_step_stroops <= 0 + || config.high_value_threshold_stroops < MIN_BUDGET_STROOPS + || config.high_value_threshold_stroops > MAX_BUDGET_STROOPS + || config.high_value_discount_bps > config.max_fee_bps + { + panic_with_error!(env, JobRegistryError::InvalidFeeConfig); + } - if len == 0 || len > MAX_HASH_LEN { - panic_with_error!(env, JobRegistryError::InvalidHash); + let peak = config + .base_fee_bps + .checked_add(config.step_fee_bps) + .unwrap_or_else(|| panic_with_error!(env, JobRegistryError::Overflow)); + if peak > MAX_CONFIGURABLE_FEE_BPS { + panic_with_error!(env, JobRegistryError::InvalidFeeConfig); + } +} + +fn compute_service_fee(env: &Env, budget_stroops: i128, config: &FeeConfig) -> FeeQuote { + let tier_count = budget_stroops + .checked_div(config.budget_step_stroops) + .unwrap_or_else(|| panic_with_error!(env, JobRegistryError::InvalidFeeConfig)); + let tier_bps_i128 = tier_count + .checked_mul(i128::from(config.step_fee_bps)) + .unwrap_or_else(|| panic_with_error!(env, JobRegistryError::Overflow)); + let tier_bps: u32 = tier_bps_i128 + .try_into() + .unwrap_or_else(|_| panic_with_error!(env, JobRegistryError::Overflow)); + + let mut fee_bps = config + .base_fee_bps + .checked_add(tier_bps) + .unwrap_or_else(|| panic_with_error!(env, JobRegistryError::Overflow)) + .min(config.max_fee_bps); + + if budget_stroops >= config.high_value_threshold_stroops { + fee_bps = fee_bps.saturating_sub(config.high_value_discount_bps); + } + + let fee_stroops = budget_stroops + .checked_mul(i128::from(fee_bps)) + .and_then(|amount| amount.checked_div(BASIS_POINTS_DENOMINATOR)) + .unwrap_or_else(|| panic_with_error!(env, JobRegistryError::Overflow)); + + FeeQuote { + fee_bps, + fee_stroops, } } @@ -719,7 +651,7 @@ fn read_bid_count(env: &Env, job_id: u64) -> u32 { fn read_bid_at(env: &Env, job_id: u64, index: u32) -> BidRecord { env.storage() .persistent() - .get(&DataKey::Bid(job_id, index)) + .get(&DataKey::BidByIndex(job_id, index)) .unwrap_or_else(|| panic_with_error!(env, JobRegistryError::BidIndexOutOfBounds)) } @@ -727,72 +659,55 @@ fn post_job_with_id( env: &Env, job_id: u64, client: Address, - hash: Bytes, - budget: i128, + metadata_cid: Bytes, + budget_stroops: i128, bidding_deadline: u64, expires_at: u64, ) { let key = DataKey::Job(job_id); - if env.storage().persistent().has(&key) { panic_with_error!(env, JobRegistryError::JobAlreadyExists); } + let quote = compute_service_fee(env, budget_stroops, &read_fee_config(env)); let job = JobRecord { client, freelancer: None, - metadata_hash: hash, - budget_stroops: budget, + metadata_cid, + budget_stroops, + service_fee_bps: quote.fee_bps, + service_fee_stroops: quote.fee_stroops, expires_at, status: JobStatus::Open, bidding_deadline, }; env.storage().persistent().set(&key, &job); - - let bids: Vec = Vec::new(env); - env.storage() .persistent() .set(&DataKey::BidCount(job_id), &0u32); } -fn release_collateral(env: &Env, job_id: u64, freelancer: Address, _slash: bool) { - let _job: JobRecord = env +fn release_collateral(env: &Env, job_id: u64, freelancer: Address) { + let _job = read_job(env, job_id); + let lookup_key = DataKey::BidLookup(job_id, freelancer.clone()); + let index: u32 = env .storage() .persistent() - .get(&DataKey::Job(job_id)) - .unwrap_or_else(|| panic_with_error!(env, JobRegistryError::JobNotFound)); + .get(&lookup_key) + .unwrap_or_else(|| panic_with_error!(env, JobRegistryError::CollateralNotFound)); - let bids_key = DataKey::Bids(job_id); - let bids: Vec = env + let bid_key = DataKey::BidByIndex(job_id, index); + let mut bid: BidRecord = env .storage() .persistent() - .get(&bids_key) + .get(&bid_key) .unwrap_or_else(|| panic_with_error!(env, JobRegistryError::CollateralNotFound)); - - let mut updated_bids: Vec = Vec::new(env); - let mut found = false; - - for bid in bids.iter() { - if bid.freelancer == freelancer { - found = true; - if bid.collateral_released { - panic_with_error!(env, JobRegistryError::CollateralAlreadyReleased); - } - let mut updated = bid.clone(); - updated.collateral_released = true; - updated_bids.push_back(updated); - } else { - updated_bids.push_back(bid.clone()); - } - } - - if !found { - panic_with_error!(env, JobRegistryError::CollateralNotFound); + if bid.collateral_released { + panic_with_error!(env, JobRegistryError::CollateralAlreadyReleased); } - - env.storage().persistent().set(&bids_key, &updated_bids); + bid.collateral_released = true; + env.storage().persistent().set(&bid_key, &bid); } #[cfg(test)] @@ -801,6 +716,8 @@ mod test { use soroban_sdk::testutils::{Address as _, Ledger as _}; use soroban_sdk::{Address, Bytes, Env}; + const DEFAULT_COLLATERAL_STROOPS: i128 = 1_000; + fn setup() -> ( Env, JobRegistryContractClient<'static>, @@ -810,11 +727,11 @@ mod test { ) { let env = Env::default(); env.mock_all_auths(); + env.ledger().with_mut(|li| li.timestamp = 1_000); let admin = Address::generate(&env); let client = Address::generate(&env); let freelancer = Address::generate(&env); - let contract_id = env.register_contract(None, JobRegistryContract); let cc = JobRegistryContractClient::new(&env, &contract_id); @@ -829,430 +746,372 @@ mod test { env.ledger().timestamp() + 30 } - const DEFAULT_COLLATERAL_STROOPS: i128 = 1_000; + fn post_default_job(env: &Env, cc: &JobRegistryContractClient<'_>, client: &Address) { + let cid = Bytes::from_slice(env, b"bafyJobCid"); + let deadline = default_bidding_deadline(env); + let expires_at = future_expires_at(env); + cc.post_job( + &1u64, + client, + &cid, + &MIN_BUDGET_STROOPS, + &deadline, + &expires_at, + ); + } #[test] fn test_initialize_bootstraps_storage() { let (_env, cc, admin, _, _) = setup(); - cc.initialize(&admin); - assert!(cc.is_initialized()); assert_eq!(cc.get_admin(), admin); assert_eq!(cc.get_next_job_id(), 1u64); + assert_eq!(cc.get_fee_config(), default_fee_config()); } #[test] - #[should_panic] + #[should_panic(expected = "Error(Contract, #1)")] fn test_double_initialize_panics() { let (_env, cc, admin, _, _) = setup(); - cc.initialize(&admin); cc.initialize(&admin); } #[test] - #[should_panic] + #[should_panic(expected = "Error(Contract, #2)")] fn test_post_job_before_initialize_panics() { let (env, cc, _admin, client, _) = setup(); - let hash = Bytes::from_slice(&env, b"QmHash"); + let cid = Bytes::from_slice(&env, b"bafyJobCid"); let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); + cc.post_job( + &1u64, + &client, + &cid, + &MIN_BUDGET_STROOPS, + &default_bidding_deadline(&env), + &expires_at, + ); } #[test] fn test_post_job_auto_allocates_sequential_ids() { let (env, cc, admin, client, _) = setup(); cc.initialize(&admin); - - let hash1 = Bytes::from_slice(&env, b"QmHash1"); - let hash2 = Bytes::from_slice(&env, b"QmHash2"); - let expires_at1 = future_expires_at(&env); - let expires_at2 = future_expires_at(&env); - - let id1 = cc.post_job_auto(&client, &hash1, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at1); - let id2 = cc.post_job_auto(&client, &hash2, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at2); - + let cid1 = Bytes::from_slice(&env, b"bafyJobCid1"); + let cid2 = Bytes::from_slice(&env, b"bafyJobCid2"); + let id1 = cc.post_job_auto( + &client, + &cid1, + &MIN_BUDGET_STROOPS, + &default_bidding_deadline(&env), + &future_expires_at(&env), + ); + let id2 = cc.post_job_auto( + &client, + &cid2, + &MIN_BUDGET_STROOPS, + &default_bidding_deadline(&env), + &future_expires_at(&env), + ); assert_eq!(id1, 1u64); assert_eq!(id2, 2u64); assert_eq!(cc.get_next_job_id(), 3u64); } #[test] - fn test_post_job_with_explicit_id_updates_next_job_id() { + fn test_dynamic_service_fee_is_stored_on_posting() { let (env, cc, admin, client, _) = setup(); cc.initialize(&admin); + let config = FeeConfig { + base_fee_bps: 100, + budget_step_stroops: 1_000_000, + step_fee_bps: 50, + max_fee_bps: 300, + high_value_threshold_stroops: 10_000_000, + high_value_discount_bps: 25, + }; + cc.set_fee_config(&config); + + let budget = 5_000_000i128; + let quote = cc.quote_service_fee(&budget); + assert_eq!(quote.fee_bps, 300u32); + assert_eq!(quote.fee_stroops, 150_000i128); + + let cid = Bytes::from_slice(&env, b"bafyJobCid"); + cc.post_job( + &1u64, + &client, + &cid, + &budget, + &default_bidding_deadline(&env), + &future_expires_at(&env), + ); + let job = cc.get_job(&1u64); + assert_eq!(job.metadata_cid, cid); + assert_eq!(job.service_fee_bps, quote.fee_bps); + assert_eq!(job.service_fee_stroops, quote.fee_stroops); + } - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&42u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); + #[test] + fn test_high_value_discount_adjusts_fee_downward() { + let (env, cc, admin, client, _) = setup(); + cc.initialize(&admin); + let config = FeeConfig { + base_fee_bps: 200, + budget_step_stroops: 1_000_000, + step_fee_bps: 10, + max_fee_bps: 500, + high_value_threshold_stroops: 2_000_000, + high_value_discount_bps: 75, + }; + cc.set_fee_config(&config); + + let budget = 2_000_000i128; + let quote = cc.quote_service_fee(&budget); + assert_eq!(quote.fee_bps, 145u32); + + let cid = Bytes::from_slice(&env, b"bafyJobCid"); + cc.post_job( + &1u64, + &client, + &cid, + &budget, + &default_bidding_deadline(&env), + &future_expires_at(&env), + ); + assert_eq!(cc.get_job(&1u64).service_fee_bps, 145u32); + } - assert_eq!(cc.get_next_job_id(), 43u64); + #[test] + #[should_panic(expected = "Error(Contract, #21)")] + fn test_invalid_fee_config_panics() { + let (_env, cc, admin, _, _) = setup(); + cc.initialize(&admin); + cc.set_fee_config(&FeeConfig { + base_fee_bps: 251, + budget_step_stroops: 0, + step_fee_bps: 0, + max_fee_bps: 250, + high_value_threshold_stroops: MIN_BUDGET_STROOPS, + high_value_discount_bps: 0, + }); } #[test] - #[should_panic] - fn test_invalid_budget_panics() { + fn test_budget_at_bounds_succeeds() { let (env, cc, admin, client, _) = setup(); cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &0i128, &default_bidding_deadline(&env), &expires_at); + let cid = Bytes::from_slice(&env, b"bafyJobCid"); + cc.post_job( + &1u64, + &client, + &cid, + &MIN_BUDGET_STROOPS, + &default_bidding_deadline(&env), + &future_expires_at(&env), + ); + assert_eq!(cc.get_job(&1u64).budget_stroops, MIN_BUDGET_STROOPS); + + let cid2 = Bytes::from_slice(&env, b"bafyJobCid2"); + cc.post_job( + &2u64, + &client, + &cid2, + &MAX_BUDGET_STROOPS, + &default_bidding_deadline(&env), + &future_expires_at(&env), + ); + assert_eq!(cc.get_job(&2u64).budget_stroops, MAX_BUDGET_STROOPS); } #[test] - #[should_panic] - fn test_empty_hash_panics() { + #[should_panic(expected = "Error(Contract, #4)")] + fn test_budget_below_minimum_panics() { let (env, cc, admin, client, _) = setup(); cc.initialize(&admin); + let cid = Bytes::from_slice(&env, b"bafyJobCid"); + cc.post_job( + &1u64, + &client, + &cid, + &(MIN_BUDGET_STROOPS - 1), + &default_bidding_deadline(&env), + &future_expires_at(&env), + ); + } - let empty = Bytes::from_slice(&env, b""); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &empty, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); + #[test] + #[should_panic(expected = "Error(Contract, #5)")] + fn test_rejects_oversized_metadata_cid() { + let (env, cc, admin, client, _) = setup(); + cc.initialize(&admin); + let oversized = Bytes::from_slice( + &env, + b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ); + cc.post_job( + &1u64, + &client, + &oversized, + &MIN_BUDGET_STROOPS, + &default_bidding_deadline(&env), + &future_expires_at(&env), + ); } #[test] - fn test_full_lifecycle() { + fn test_full_lifecycle_transitions_to_assigned_and_deliverable_submitted() { let (env, cc, admin, client, freelancer) = setup(); cc.initialize(&admin); + post_default_job(&env, &cc, &client); - let hash = Bytes::from_slice(&env, b"QmSomeIPFSHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - let job = cc.get_job(&1u64); - assert_eq!(job.status, JobStatus::Open); - assert_eq!(job.freelancer, None); - - let proposal = Bytes::from_slice(&env, b"QmProposalHash"); + let proposal = Bytes::from_slice(&env, b"bafyProposalCid"); cc.submit_bid(&1u64, &freelancer, &proposal, &DEFAULT_COLLATERAL_STROOPS); - - let bids = cc.get_bids(&1u64); - assert_eq!(bids.len(), 1); + assert_eq!(cc.get_bids_count(&1u64), 1u32); cc.accept_bid(&1u64, &client, &freelancer); - let job = cc.get_job(&1u64); - assert_eq!(job.status, JobStatus::Assigned); - assert_eq!(job.freelancer, Some(freelancer.clone())); + let assigned = cc.get_job(&1u64); + assert_eq!(assigned.status, JobStatus::Assigned); + assert_eq!(assigned.freelancer, Some(freelancer.clone())); - let deliverable = Bytes::from_slice(&env, b"QmDeliverableHash"); + let deliverable = Bytes::from_slice(&env, b"bafyDeliverableCid"); cc.submit_deliverable(&1u64, &freelancer, &deliverable); - - let job = cc.get_job(&1u64); - assert_eq!(job.status, JobStatus::DeliverableSubmitted); - - let d = cc.get_deliverable(&1u64); - assert_eq!(d, deliverable); + assert_eq!(cc.get_job(&1u64).status, JobStatus::DeliverableSubmitted); + assert_eq!(cc.get_deliverable(&1u64), deliverable); } #[test] - #[should_panic] + #[should_panic(expected = "Error(Contract, #10)")] fn test_duplicate_bid_panics() { let (env, cc, admin, client, freelancer) = setup(); cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - let proposal = Bytes::from_slice(&env, b"QmProposal"); + post_default_job(&env, &cc, &client); + let proposal = Bytes::from_slice(&env, b"bafyProposalCid"); cc.submit_bid(&1u64, &freelancer, &proposal, &DEFAULT_COLLATERAL_STROOPS); cc.submit_bid(&1u64, &freelancer, &proposal, &DEFAULT_COLLATERAL_STROOPS); } #[test] - fn test_get_bid_at_reads_indexed_bid_rows() { + fn test_bid_rows_are_map_like_and_paginated() { let (env, cc, admin, client, freelancer) = setup(); let second_freelancer = Address::generate(&env); cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"bafyJobCid"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &5000i128, &expires_at); + post_default_job(&env, &cc, &client); let proposal_one = Bytes::from_slice(&env, b"bafyProposalOne"); let proposal_two = Bytes::from_slice(&env, b"bafyProposalTwo"); - cc.submit_bid(&1u64, &freelancer, &proposal_one); - cc.submit_bid(&1u64, &second_freelancer, &proposal_two); + cc.submit_bid( + &1u64, + &freelancer, + &proposal_one, + &DEFAULT_COLLATERAL_STROOPS, + ); + cc.submit_bid( + &1u64, + &second_freelancer, + &proposal_two, + &DEFAULT_COLLATERAL_STROOPS, + ); let first = cc.get_bid_at(&1u64, &0u32); let second = cc.get_bid_at(&1u64, &1u32); assert_eq!(first.freelancer, freelancer); - assert_eq!(first.proposal_hash, proposal_one); + assert_eq!(first.proposal_cid, proposal_one); assert_eq!(second.freelancer, second_freelancer); - assert_eq!(second.proposal_hash, proposal_two); - - let bids = cc.get_bids(&1u64); - assert_eq!(bids.len(), 2); + assert_eq!(second.proposal_cid, proposal_two); + assert_eq!(cc.get_bids(&1u64).len(), 2u32); + assert_eq!(cc.get_bids_page(&1u64, &1u32, &5u32).len(), 1u32); + assert_eq!(cc.get_bids_page(&1u64, &10u32, &5u32).len(), 0u32); } #[test] - #[should_panic(expected = "Error(Contract, #15)")] + #[should_panic(expected = "Error(Contract, #23)")] fn test_get_bid_at_out_of_bounds_returns_specific_error() { let (env, cc, admin, client, _) = setup(); cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"bafyJobCid"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &5000i128, &expires_at); - + post_default_job(&env, &cc, &client); cc.get_bid_at(&1u64, &0u32); } #[test] - #[should_panic(expected = "Error(Contract, #5)")] - fn test_rejects_oversized_metadata_cid() { - let (env, cc, admin, client, _) = setup(); - cc.initialize(&admin); - - let oversized = Bytes::from_slice( - &env, - b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &oversized, &5000i128, &expires_at); - } - - #[test] - #[should_panic(expected = "Error(Contract, #8)")] - fn test_late_bid_after_assignment_returns_specific_error() { + #[should_panic(expected = "Error(Contract, #9)")] + fn test_only_job_creator_can_accept_proposals() { let (env, cc, admin, client, freelancer) = setup(); - let late_freelancer = Address::generate(&env); + let attacker = Address::generate(&env); cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"bafyJobCid"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &5000i128, &expires_at); - - let proposal = Bytes::from_slice(&env, b"bafyProposal"); - cc.submit_bid(&1u64, &freelancer, &proposal); - cc.accept_bid(&1u64, &client, &freelancer); - - let late_proposal = Bytes::from_slice(&env, b"bafyLateProposal"); - cc.submit_bid(&1u64, &late_freelancer, &late_proposal); + post_default_job(&env, &cc, &client); + let proposal = Bytes::from_slice(&env, b"bafyProposalCid"); + cc.submit_bid(&1u64, &freelancer, &proposal, &DEFAULT_COLLATERAL_STROOPS); + cc.accept_bid(&1u64, &attacker, &freelancer); } #[test] - #[should_panic] + #[should_panic(expected = "Error(Contract, #11)")] fn test_accept_without_matching_bid_panics() { let (env, cc, admin, client, freelancer) = setup(); cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - + post_default_job(&env, &cc, &client); cc.accept_bid(&1u64, &client, &freelancer); } #[test] - fn test_mark_disputed_from_assigned() { + #[should_panic(expected = "Error(Contract, #8)")] + fn test_late_bid_after_acceptance_panics_with_job_not_open() { let (env, cc, admin, client, freelancer) = setup(); + let late_freelancer = Address::generate(&env); cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - let proposal = Bytes::from_slice(&env, b"QmProposal"); + post_default_job(&env, &cc, &client); + let proposal = Bytes::from_slice(&env, b"bafyProposalCid"); cc.submit_bid(&1u64, &freelancer, &proposal, &DEFAULT_COLLATERAL_STROOPS); cc.accept_bid(&1u64, &client, &freelancer); - - cc.mark_disputed(&1u64); - let job = cc.get_job(&1u64); - assert_eq!(job.status, JobStatus::Disputed); - } - - #[test] - #[should_panic] - fn test_mark_disputed_from_open_panics() { - let (env, cc, admin, client, _) = setup(); - cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - cc.mark_disputed(&1u64); + cc.submit_bid( + &1u64, + &late_freelancer, + &proposal, + &DEFAULT_COLLATERAL_STROOPS, + ); } #[test] - #[should_panic] - fn test_submit_bid_after_expiration_panics() { + #[should_panic(expected = "Error(Contract, #15)")] + fn test_late_bid_after_deadline_returns_specific_error() { let (env, cc, admin, client, freelancer) = setup(); cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - env.ledger().set_timestamp(expires_at + 1); - - let proposal = Bytes::from_slice(&env, b"QmProposal"); + post_default_job(&env, &cc, &client); + env.ledger().with_mut(|li| li.timestamp += 31); + let proposal = Bytes::from_slice(&env, b"bafyProposalCid"); cc.submit_bid(&1u64, &freelancer, &proposal, &DEFAULT_COLLATERAL_STROOPS); } #[test] - #[should_panic] - fn test_cancel_expired_job_before_expiration_panics() { - let (env, cc, admin, client, _) = setup(); - cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - cc.cancel_expired_job(&1u64, &client); - } - - // --- SC-REG-037: Budget Bounds Tests --- - - #[test] - fn test_budget_at_minimum_succeeds() { - let (env, cc, admin, client, _) = setup(); - cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - let job = cc.get_job(&1u64); - assert_eq!(job.budget_stroops, MIN_BUDGET_STROOPS); - } - - #[test] - fn test_budget_at_maximum_succeeds() { - let (env, cc, admin, client, _) = setup(); - cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MAX_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - let job = cc.get_job(&1u64); - assert_eq!(job.budget_stroops, MAX_BUDGET_STROOPS); - } - - #[test] - #[should_panic] - fn test_budget_below_minimum_panics() { - let (env, cc, admin, client, _) = setup(); - cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &(MIN_BUDGET_STROOPS - 1), &default_bidding_deadline(&env), &expires_at); - } - - #[test] - #[should_panic] - fn test_budget_above_maximum_panics() { - let (env, cc, admin, client, _) = setup(); - cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &(MAX_BUDGET_STROOPS + 1), &default_bidding_deadline(&env), &expires_at); - } - - #[test] - #[should_panic] - fn test_zero_budget_still_panics() { - let (env, cc, admin, client, _) = setup(); - cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &0i128, &default_bidding_deadline(&env), &expires_at); - } - - // --- SC-REG-039: Paginated Bids Tests --- - - #[test] - fn test_get_bids_count_empty_returns_zero() { - let (env, cc, admin, client, _) = setup(); - cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - assert_eq!(cc.get_bids_count(&1u64), 0u32); - } - - #[test] - fn test_get_bids_count_after_submissions() { - let (env, cc, admin, client, _) = setup(); - cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - for _ in 0..3u32 { - let freelancer = Address::generate(&env); - let proposal = Bytes::from_slice(&env, b"QmProposal"); - cc.submit_bid(&1u64, &freelancer, &proposal, &DEFAULT_COLLATERAL_STROOPS); - } - - assert_eq!(cc.get_bids_count(&1u64), 3u32); - } - - #[test] - fn test_get_bids_page_first_window() { - let (env, cc, admin, client, _) = setup(); + #[should_panic(expected = "Error(Contract, #8)")] + fn test_cannot_accept_bid_twice() { + let (env, cc, admin, client, freelancer) = setup(); cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - for _ in 0..5u32 { - let freelancer = Address::generate(&env); - let proposal = Bytes::from_slice(&env, b"QmProposal"); - cc.submit_bid(&1u64, &freelancer, &proposal, &DEFAULT_COLLATERAL_STROOPS); - } - - let page = cc.get_bids_page(&1u64, &0u32, &3u32); - assert_eq!(page.len(), 3u32); + post_default_job(&env, &cc, &client); + let proposal = Bytes::from_slice(&env, b"bafyProposalCid"); + cc.submit_bid(&1u64, &freelancer, &proposal, &DEFAULT_COLLATERAL_STROOPS); + cc.accept_bid(&1u64, &client, &freelancer); + cc.accept_bid(&1u64, &client, &freelancer); } #[test] - fn test_get_bids_page_second_window() { - let (env, cc, admin, client, _) = setup(); + fn test_set_escrow_deployer_round_trip() { + let (_env, cc, admin, _, freelancer) = setup(); cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - for _ in 0..5u32 { - let freelancer = Address::generate(&env); - let proposal = Bytes::from_slice(&env, b"QmProposal"); - cc.submit_bid(&1u64, &freelancer, &proposal, &DEFAULT_COLLATERAL_STROOPS); - } - - let page = cc.get_bids_page(&1u64, &3u32, &3u32); - assert_eq!(page.len(), 2u32); + cc.set_escrow_deployer(&freelancer); + assert_eq!(cc.get_escrow_deployer(), Some(freelancer)); } #[test] - fn test_get_bids_page_offset_beyond_end_returns_empty() { - let (env, cc, admin, client, _) = setup(); + fn test_collateral_release_updates_indexed_bid() { + let (env, cc, admin, client, freelancer) = setup(); cc.initialize(&admin); - - let hash = Bytes::from_slice(&env, b"QmHash"); - let expires_at = future_expires_at(&env); - cc.post_job(&1u64, &client, &hash, &MIN_BUDGET_STROOPS, &default_bidding_deadline(&env), &expires_at); - - for _ in 0..3u32 { - let freelancer = Address::generate(&env); - let proposal = Bytes::from_slice(&env, b"QmProposal"); - cc.submit_bid(&1u64, &freelancer, &proposal, &DEFAULT_COLLATERAL_STROOPS); - } - - let page = cc.get_bids_page(&1u64, &10u32, &5u32); - assert_eq!(page.len(), 0u32); + post_default_job(&env, &cc, &client); + let proposal = Bytes::from_slice(&env, b"bafyProposalCid"); + cc.submit_bid(&1u64, &freelancer, &proposal, &DEFAULT_COLLATERAL_STROOPS); + cc.refund_bid_collateral(&1u64, &freelancer); + assert!(cc.get_bid_at(&1u64, &0u32).collateral_released); } }