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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions contracts/job_registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,19 @@ pub struct BidAcceptedEvent {
pub timestamp: u64,
}

/// Event emitted when a deliverable is submitted.
///
/// This event is published to enable off-chain indexing and monitoring
/// of all deliverable submissions on the platform. Includes timestamp for audit trails.
#[contracttype]
#[derive(Clone)]
pub struct DeliverableSubmittedEvent {
pub job_id: u64,
pub freelancer: Address,
pub deliverable_hash: Bytes,
pub timestamp: u64,
}

#[contract]
pub struct JobRegistryContract;

Expand Down Expand Up @@ -258,7 +271,37 @@ impl JobRegistryContract {
Ok(())
}

/// Freelancer submits deliverable IPFS hash.
/// Freelancer submits a deliverable for a job in progress.
///
/// This is the core operation enabling freelancers to submit completed work
/// for jobs they have been assigned to. The deliverable is stored as an IPFS
/// hash to minimize on-chain storage while maintaining decentralized content
/// accessibility. Validation ensures:
/// 1. The freelancer is authenticated via Stellar signature
/// 2. The job exists and is in InProgress status
/// 3. The deliverable hash is not empty (content validation)
/// 4. The caller is the assigned freelancer for the job
///
/// # Arguments
/// * `env` - The Soroban environment
/// * `job_id` - The unique identifier of the job
/// * `freelancer` - The address of the freelancer submitting the deliverable
/// * `hash` - The IPFS CID hash of the deliverable content
///
/// # Returns
/// * `Ok(())` - If the deliverable is successfully submitted
/// * `Err(JobRegistryError::JobNotFound)` - If the job ID does not exist
/// * `Err(JobRegistryError::InvalidInput)` - If the deliverable hash is empty
/// * `Err(JobRegistryError::InvalidState)` - If the job status is not InProgress
/// * `Err(JobRegistryError::Unauthorized)` - If the caller is not the assigned freelancer
///
/// # Security Considerations
/// * Requires freelancer authentication via `require_auth()` to prevent spoofing
/// * Validates job status to prevent premature or invalid submissions
/// * Prevents submission of invalid (empty) deliverable hashes
/// * Ensures only the assigned freelancer can submit deliverables
/// * Emits auditable event with timestamp for off-chain monitoring
/// * Stores deliverable hash persistently for escrow and dispute resolution
pub fn submit_deliverable(
env: Env,
job_id: u64,
Expand Down Expand Up @@ -293,7 +336,12 @@ impl JobRegistryContract {

env.events().publish(
("job_registry", "DeliverableSubmitted"),
(job_id, freelancer, env.ledger().timestamp()),
DeliverableSubmittedEvent {
job_id,
freelancer: freelancer.clone(),
deliverable_hash: hash.clone(),
timestamp: env.ledger().timestamp(),
},
);

Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1121,15 +1121,38 @@
}
],
"data": {
"vec": [
"map": [
{
"u64": 1
"key": {
"symbol": "deliverable_hash"
},
"val": {
"bytes": "516d44656c6976657261626c6548617368"
}
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
"key": {
"symbol": "freelancer"
},
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
}
},
{
"u64": 0
"key": {
"symbol": "job_id"
},
"val": {
"u64": 1
}
},
{
"key": {
"symbol": "timestamp"
},
"val": {
"u64": 0
}
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -856,15 +856,38 @@
}
],
"data": {
"vec": [
"map": [
{
"u64": 1
"key": {
"symbol": "deliverable_hash"
},
"val": {
"bytes": "516d44656c6976657261626c65"
}
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
"key": {
"symbol": "freelancer"
},
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
}
},
{
"u64": 0
"key": {
"symbol": "job_id"
},
"val": {
"u64": 1
}
},
{
"key": {
"symbol": "timestamp"
},
"val": {
"u64": 0
}
}
]
}
Expand Down
29 changes: 29 additions & 0 deletions docs/contracts/job_registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,32 @@ The `JobRegistry` contract manages job postings, bid submissions, bid acceptance
### Notes

This implementation strengthens trustlessness by ensuring bid acceptance can only succeed for bidders who actually participated in the auction.

## `submit_deliverable`

### Purpose

`submit_deliverable` is called by a freelancer to submit their completed work for a job that is in progress. The deliverable is stored as an IPFS hash, enabling decentralized content storage while maintaining on-chain auditability.

### Behavior

- Authenticates the caller with `freelancer.require_auth()`.
- Validates that the deliverable hash is not empty to prevent invalid submissions.
- Verifies the job exists and is currently in the `InProgress` state.
- Confirms the caller is the assigned freelancer for the job.
- Updates the job status to `DeliverableSubmitted`.
- Stores the deliverable hash in persistent storage for later retrieval.
- Emits a `DeliverableSubmitted` event with timestamp for on-chain auditing and off-chain indexing.

### Errors

`submit_deliverable` uses `JobRegistryError` to return structured error information:

- `JobNotFound` (1): job does not exist.
- `InvalidInput` (4): deliverable hash is empty.
- `InvalidState` (5): job is not in `InProgress` status.
- `Unauthorized` (3): caller is not the assigned freelancer for the job.

### Notes

This function is critical for the job completion workflow, enabling freelancers to submit their work while maintaining security through authentication and state validation. The IPFS hash storage minimizes on-chain data while preserving immutability and accessibility.
Loading