{"path":"programs/project_mappers/src/lib.rs","size":12931,"locked":false,"binary":false,"truncated":false,"content":"use anchor_lang::prelude::*;\nuse anchor_lang::system_program::{transfer, Transfer};\n\ndeclare_id!(\"52yt1gCbPeiKP4JYjUVKmMJSgBMMcUx8xRGqozMKX2Mu\");\n\n/// Minimum settlement window a client may set at job creation.\n/// Prevents griefing via a near-instant timeout that undermines the\n/// freelancer's ability to deliver before a permissionless refund fires.\npub const MIN_DURATION_SECONDS: i64 = 3_600; // 1 hour\n\n/// Maximum settlement window a client may set at job creation.\n/// Prevents indefinite fund lockup caused by an accidental or malicious\n/// multi-year duration value.\npub const MAX_DURATION_SECONDS: i64 = 15_552_000; // 180 days\n\n#[program]\npub mod project_mappers {\n    use super::*;\n\n    pub fn initialize_job(\n        ctx: Context<InitializeJob>,\n        job_id: String,\n        amount: u64,\n        duration_seconds: i64,\n    ) -> Result<()> {\n        require!(job_id.len() <= 32, EscrowError::JobIdTooLong);\n        require!(amount > 0, EscrowError::InvalidAmount);\n        require!(\n            amount >= Rent::get()?.minimum_balance(0),\n            EscrowError::AmountBelowRentExemption\n        );\n        require!(\n            duration_seconds >= MIN_DURATION_SECONDS\n                && duration_seconds <= MAX_DURATION_SECONDS,\n            EscrowError::InvalidDuration\n        );\n\n        let now = Clock::get()?.unix_timestamp;\n        let deadline = now\n            .checked_add(duration_seconds)\n            .ok_or(EscrowError::DurationOverflow)?;\n\n        let escrow = &mut ctx.accounts.escrow_account;\n        escrow.client      = ctx.accounts.client.key();\n        escrow.freelancer  = ctx.accounts.freelancer.key();\n        escrow.oracle      = ctx.accounts.oracle.key();\n        escrow.amount      = amount;\n        escrow.job_id      = job_id;\n        escrow.status      = JobStatus::Pending;\n        escrow.escrow_bump = ctx.bumps.escrow_account;\n        escrow.vault_bump  = ctx.bumps.vault_account;\n        escrow.deadline    = deadline;\n\n        transfer(\n            CpiContext::new(\n                ctx.accounts.system_program.to_account_info(),\n                Transfer {\n                    from: ctx.accounts.client.to_account_info(),\n                    to:   ctx.accounts.vault_account.to_account_info(),\n                },\n            ),\n            amount,\n        )?;\n\n        msg!(\n            \"Escrow initialized. Job: {} | Deadline (unix): {}\",\n            escrow.job_id,\n            escrow.deadline\n        );\n        Ok(())\n    }\n\n    pub fn release_payment(ctx: Context<ReleasePayment>) -> Result<()> {\n        let client_key = ctx.accounts.escrow_account.client;\n        let oracle_key = ctx.accounts.escrow_account.oracle;\n        let vault_bump = ctx.accounts.escrow_account.vault_bump;\n        let job_id     = ctx.accounts.escrow_account.job_id.clone();\n        let status     = ctx.accounts.escrow_account.status;\n\n        require!(status == JobStatus::Pending, EscrowError::JobNotPending);\n        require!(\n            ctx.accounts.authority.key() == client_key\n                || ctx.accounts.authority.key() == oracle_key,\n            EscrowError::UnauthorizedExecution\n        );\n\n        // Drain the vault's live balance rather than the amount recorded at\n        // initialization. Any lamports that arrived after init (dust,\n        // accidental overfunding) settle to the freelancer instead of being\n        // stranded once the escrow account closes.\n        let vault_balance = ctx.accounts.vault_account.lamports();\n\n        transfer(\n            CpiContext::new_with_signer(\n                ctx.accounts.system_program.to_account_info(),\n                Transfer {\n                    from: ctx.accounts.vault_account.to_account_info(),\n                    to:   ctx.accounts.freelancer.to_account_info(),\n                },\n                &[&[b\"vault\", client_key.as_ref(), job_id.as_bytes(), &[vault_bump]]],\n            ),\n            vault_balance,\n        )?;\n\n        ctx.accounts.escrow_account.status = JobStatus::Completed;\n        msg!(\"Payment of {} lamports released.\", vault_balance);\n        Ok(())\n    }\n\n    pub fn cancel_job(ctx: Context<CancelJob>) -> Result<()> {\n        let client_key = ctx.accounts.escrow_account.client;\n        let vault_bump = ctx.accounts.escrow_account.vault_bump;\n        let job_id     = ctx.accounts.escrow_account.job_id.clone();\n        let status     = ctx.accounts.escrow_account.status;\n\n        require!(status == JobStatus::Pending, EscrowError::JobNotPending);\n\n        let vault_balance = ctx.accounts.vault_account.lamports();\n\n        transfer(\n            CpiContext::new_with_signer(\n                ctx.accounts.system_program.to_account_info(),\n                Transfer {\n                    from: ctx.accounts.vault_account.to_account_info(),\n                    to:   ctx.accounts.client.to_account_info(),\n                },\n                &[&[b\"vault\", client_key.as_ref(), job_id.as_bytes(), &[vault_bump]]],\n            ),\n            vault_balance,\n        )?;\n\n        ctx.accounts.escrow_account.status = JobStatus::Cancelled;\n        msg!(\"Job cancelled. {} lamports refunded.\", vault_balance);\n        Ok(())\n    }\n\n    /// Permissionless liveness escape hatch. Anyone may call this once the\n    /// job's deadline has passed — the caller only pays the transaction fee;\n    /// funds always flow to the stored `client`, never to the caller. This\n    /// closes the fund-lock risk that exists if the oracle key is lost,\n    /// offline, or the AI consensus loop escalates a job and no human ever\n    /// resolves it. The vault's full live balance is refunded, matching the\n    /// dust-sweep behavior of `release_payment` and `cancel_job`.\n    pub fn refund_after_timeout(ctx: Context<RefundAfterTimeout>) -> Result<()> {\n        let client_key = ctx.accounts.escrow_account.client;\n        let vault_bump = ctx.accounts.escrow_account.vault_bump;\n        let job_id     = ctx.accounts.escrow_account.job_id.clone();\n        let status     = ctx.accounts.escrow_account.status;\n        let deadline   = ctx.accounts.escrow_account.deadline;\n\n        require!(status == JobStatus::Pending, EscrowError::JobNotPending);\n\n        let now = Clock::get()?.unix_timestamp;\n        require!(now >= deadline, EscrowError::DeadlineNotReached);\n\n        let vault_balance = ctx.accounts.vault_account.lamports();\n\n        transfer(\n            CpiContext::new_with_signer(\n                ctx.accounts.system_program.to_account_info(),\n                Transfer {\n                    from: ctx.accounts.vault_account.to_account_info(),\n                    to:   ctx.accounts.client.to_account_info(),\n                },\n                &[&[b\"vault\", client_key.as_ref(), job_id.as_bytes(), &[vault_bump]]],\n            ),\n            vault_balance,\n        )?;\n\n        ctx.accounts.escrow_account.status = JobStatus::Cancelled;\n        msg!(\n            \"Deadline reached ({} >= {}). {} lamports permissionlessly refunded to client.\",\n            now,\n            deadline,\n            vault_balance\n        );\n        Ok(())\n    }\n}\n\n#[derive(Accounts)]\n#[instruction(job_id: String)]\npub struct InitializeJob<'info> {\n    #[account(mut)]\n    pub client: Signer<'info>,\n    /// CHECK: Stored; enforced at release via has_one.\n    pub freelancer: AccountInfo<'info>,\n    /// CHECK: Stored; enforced at cancel via has_one.\n    pub oracle: AccountInfo<'info>,\n    #[account(\n        init,\n        payer = client,\n        space = GigEscrow::MAXIMUM_SPACE,\n        seeds = [\n            b\"gig-escrow\",\n            client.key().as_ref(),\n            &job_id.as_bytes()[..std::cmp::min(job_id.len(), 32)],\n        ],\n        bump\n    )]\n    pub escrow_account: Account<'info, GigEscrow>,\n    #[account(\n        mut,\n        seeds = [\n            b\"vault\",\n            client.key().as_ref(),\n            &job_id.as_bytes()[..std::cmp::min(job_id.len(), 32)],\n        ],\n        bump\n    )]\n    /// CHECK: Lamport-only system-owned PDA. Bump stored on escrow.\n    pub vault_account: AccountInfo<'info>,\n    pub system_program: Program<'info, System>,\n}\n\n#[derive(Accounts)]\npub struct ReleasePayment<'info> {\n    pub authority: Signer<'info>,\n    #[account(mut)]\n    /// CHECK: Validated by has_one on escrow_account.\n    pub freelancer: AccountInfo<'info>,\n    #[account(mut)]\n    /// CHECK: Validated by has_one. Receives rent on close.\n    pub client: AccountInfo<'info>,\n    #[account(\n        mut,\n        has_one = freelancer @ EscrowError::InvalidFreelancerTarget,\n        has_one = client     @ EscrowError::InvalidClientAuthority,\n        seeds = [b\"gig-escrow\", escrow_account.client.key().as_ref(), escrow_account.job_id.as_bytes()],\n        bump = escrow_account.escrow_bump,\n        close = client\n    )]\n    pub escrow_account: Account<'info, GigEscrow>,\n    #[account(\n        mut,\n        seeds = [b\"vault\", escrow_account.client.key().as_ref(), escrow_account.job_id.as_bytes()],\n        bump = escrow_account.vault_bump\n    )]\n    /// CHECK: Lamport-only PDA vault; verified by seeds + stored vault_bump.\n    pub vault_account: AccountInfo<'info>,\n    pub system_program: Program<'info, System>,\n}\n\n#[derive(Accounts)]\npub struct CancelJob<'info> {\n    pub oracle: Signer<'info>,\n    #[account(mut)]\n    /// CHECK: Receives refund and reclaimed rent on close.\n    pub client: AccountInfo<'info>,\n    #[account(\n        mut,\n        has_one = oracle  @ EscrowError::InvalidOracleAuthority,\n        has_one = client  @ EscrowError::InvalidClientAuthority,\n        seeds = [b\"gig-escrow\", escrow_account.client.key().as_ref(), escrow_account.job_id.as_bytes()],\n        bump = escrow_account.escrow_bump,\n        close = client\n    )]\n    pub escrow_account: Account<'info, GigEscrow>,\n    #[account(\n        mut,\n        seeds = [b\"vault\", escrow_account.client.key().as_ref(), escrow_account.job_id.as_bytes()],\n        bump = escrow_account.vault_bump\n    )]\n    /// CHECK: Lamport-only PDA vault; verified by seeds + stored vault_bump.\n    pub vault_account: AccountInfo<'info>,\n    pub system_program: Program<'info, System>,\n}\n\n#[derive(Accounts)]\npub struct RefundAfterTimeout<'info> {\n    /// CHECK: Permissionless caller — pays the transaction fee only. Funds\n    /// always flow to the stored `client`, never to this signer, so any\n    /// party (the client themselves, or an automated keeper) may trigger\n    /// the timeout refund once the deadline has passed.\n    pub payer: Signer<'info>,\n    #[account(mut)]\n    /// CHECK: Validated by has_one; receives the timeout refund and reclaimed rent.\n    pub client: AccountInfo<'info>,\n    #[account(\n        mut,\n        has_one = client @ EscrowError::InvalidClientAuthority,\n        seeds = [b\"gig-escrow\", escrow_account.client.key().as_ref(), escrow_account.job_id.as_bytes()],\n        bump = escrow_account.escrow_bump,\n        close = client\n    )]\n    pub escrow_account: Account<'info, GigEscrow>,\n    #[account(\n        mut,\n        seeds = [b\"vault\", escrow_account.client.key().as_ref(), escrow_account.job_id.as_bytes()],\n        bump = escrow_account.vault_bump\n    )]\n    /// CHECK: Lamport-only PDA vault; verified by seeds + stored vault_bump.\n    pub vault_account: AccountInfo<'info>,\n    pub system_program: Program<'info, System>,\n}\n\n#[account]\npub struct GigEscrow {\n    pub client:      Pubkey,\n    pub freelancer:  Pubkey,\n    pub oracle:      Pubkey,\n    pub amount:      u64,\n    pub job_id:      String,\n    pub status:      JobStatus,\n    pub escrow_bump: u8,\n    pub vault_bump:  u8,\n    pub deadline:    i64,\n}\n\n#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq)]\npub enum JobStatus {\n    Pending,\n    Completed,\n    Cancelled,\n}\n\nimpl GigEscrow {\n    // 8 (discriminator) + 32*3 (pubkeys) + 8 (amount) + 4+32 (job_id) +\n    // 1 (status) + 1 (escrow_bump) + 1 (vault_bump) + 8 (deadline) = 159\n    pub const MAXIMUM_SPACE: usize = 8 + 32 + 32 + 32 + 8 + 4 + 32 + 1 + 1 + 1 + 8;\n}\n\n#[error_code]\npub enum EscrowError {\n    #[msg(\"Job ID exceeds the 32-character maximum.\")]\n    JobIdTooLong,\n    #[msg(\"Funding amount must be greater than zero lamports.\")]\n    InvalidAmount,\n    #[msg(\"Amount is below the vault rent-exempt minimum.\")]\n    AmountBelowRentExemption,\n    #[msg(\"Job is not in a Pending state.\")]\n    JobNotPending,\n    #[msg(\"Signer is neither the escrow client nor the designated oracle.\")]\n    UnauthorizedExecution,\n    #[msg(\"Target account does not match the assigned freelancer.\")]\n    InvalidFreelancerTarget,\n    #[msg(\"Only the designated oracle can authorize cancellation.\")]\n    InvalidOracleAuthority,\n    #[msg(\"Refund target does not match the original client.\")]\n    InvalidClientAuthority,\n    #[msg(\"The settlement deadline has not been reached yet.\")]\n    DeadlineNotReached,\n    #[msg(\"Duration must be between 1 hour and 180 days.\")]\n    InvalidDuration,\n    #[msg(\"Deadline calculation overflowed the i64 range.\")]\n    DurationOverflow,\n}\n"}