{"path":"tests/project_mappers.ts","size":13444,"locked":false,"binary":false,"truncated":false,"content":"import * as anchor from \"@coral-xyz/anchor\";\nimport { Program, BN } from \"@coral-xyz/anchor\";\nimport {\n  PublicKey,\n  Keypair,\n  SystemProgram,\n  LAMPORTS_PER_SOL,\n} from \"@solana/web3.js\";\nimport { assert } from \"chai\";\n// Generated after `anchor build` — run build before first test run\nimport { ProjectMappers } from \"../target/types/project_mappers\";\nimport { deriveEscrowPda, deriveVaultPda } from \"../shared/pda\";\nimport { airdrop, createEscrow, expectAnchorError } from \"./helpers\";\n\n// ─── TEST SUITE ──────────────────────────────────────────────────────────────\n\ndescribe(\"project_mappers\", () => {\n  const provider = anchor.AnchorProvider.env();\n  anchor.setProvider(provider);\n\n  const program = anchor.workspace.ProjectMappers as Program<ProjectMappers>;\n  const connection = provider.connection;\n\n  // Two SOL — safely above rent-exempt floor (~0.00089 SOL)\n  const JOB_AMOUNT = new BN(2 * LAMPORTS_PER_SOL);\n\n  // ─── INITIALIZE JOB ────────────────────────────────────────────────────────\n\n  describe(\"initialize_job\", () => {\n    let client: Keypair;\n    let freelancer: Keypair;\n    let oracle: Keypair;\n\n    before(async () => {\n      client = Keypair.generate();\n      freelancer = Keypair.generate();\n      oracle = Keypair.generate();\n      await airdrop(connection, client.publicKey, 10);\n    });\n\n    it(\"creates escrow, locks SOL in vault, stores correct state\", async () => {\n      const jobId = \"init-test-001\";\n      const [escrowPda] = deriveEscrowPda(\n        client.publicKey,\n        jobId,\n        program.programId,\n      );\n      const [vaultPda] = deriveVaultPda(\n        client.publicKey,\n        jobId,\n        program.programId,\n      );\n\n      const clientBalanceBefore = await connection.getBalance(client.publicKey);\n\n      await createEscrow({\n        program,\n        client,\n        freelancer: freelancer.publicKey,\n        oracle: oracle.publicKey,\n        jobId,\n        amount: JOB_AMOUNT,\n      });\n\n      const escrow = await program.account.gigEscrow.fetch(escrowPda);\n      assert.equal(\n        escrow.client.toBase58(),\n        client.publicKey.toBase58(),\n        \"client mismatch\",\n      );\n      assert.equal(\n        escrow.freelancer.toBase58(),\n        freelancer.publicKey.toBase58(),\n        \"freelancer mismatch\",\n      );\n      assert.equal(\n        escrow.oracle.toBase58(),\n        oracle.publicKey.toBase58(),\n        \"oracle mismatch\",\n      );\n      assert.equal(\n        escrow.amount.toString(),\n        JOB_AMOUNT.toString(),\n        \"amount mismatch\",\n      );\n      assert.equal(escrow.jobId, jobId, \"jobId mismatch\");\n      assert.deepEqual(\n        escrow.status,\n        { pending: {} },\n        \"status should be Pending\",\n      );\n\n      const vaultBalance = await connection.getBalance(vaultPda);\n      const clientBalanceAfter = await connection.getBalance(client.publicKey);\n\n      assert.ok(\n        vaultBalance >= JOB_AMOUNT.toNumber(),\n        \"vault must hold at least job amount\",\n      );\n      assert.ok(\n        clientBalanceBefore - clientBalanceAfter >= JOB_AMOUNT.toNumber(),\n        \"client balance must decrease by at least job amount\",\n      );\n    });\n\n    it(\"rejects job_id longer than 32 characters\", async () => {\n      const jobId = \"this-job-id-is-way-too-long-for-the-contract-limit\";\n\n      await expectAnchorError(\n        () =>\n          createEscrow({\n            program,\n            client,\n            freelancer: freelancer.publicKey,\n            oracle: oracle.publicKey,\n            jobId,\n            amount: JOB_AMOUNT,\n          }),\n        \"JobIdTooLong\",\n      );\n    });\n\n    it(\"rejects zero amount\", async () => {\n      const jobId = \"init-zero-amount\";\n\n      await expectAnchorError(\n        () =>\n          createEscrow({\n            program,\n            client,\n            freelancer: freelancer.publicKey,\n            oracle: oracle.publicKey,\n            jobId,\n            amount: new BN(0),\n          }),\n        \"InvalidAmount\",\n      );\n    });\n\n    it(\"rejects amount below rent-exempt floor\", async () => {\n      const jobId = \"init-below-rent\";\n\n      await expectAnchorError(\n        () =>\n          createEscrow({\n            program,\n            client,\n            freelancer: freelancer.publicKey,\n            oracle: oracle.publicKey,\n            jobId,\n            amount: new BN(100), // 100 lamports — far below ~890,880\n          }),\n        \"AmountBelowRentExemption\",\n      );\n    });\n  });\n\n  // ─── RELEASE PAYMENT ───────────────────────────────────────────────────────\n\n  describe(\"release_payment\", () => {\n    let client: Keypair;\n    let freelancer: Keypair;\n    let oracle: Keypair;\n    let escrowPda: PublicKey;\n    let vaultPda: PublicKey;\n\n    const jobId = \"release-test-001\";\n\n    before(async () => {\n      client = Keypair.generate();\n      freelancer = Keypair.generate();\n      oracle = Keypair.generate();\n\n      await airdrop(connection, client.publicKey, 30);\n      await airdrop(connection, oracle.publicKey, 1);\n\n      const pdas = await createEscrow({\n        program,\n        client,\n        freelancer: freelancer.publicKey,\n        oracle: oracle.publicKey,\n        jobId,\n        amount: JOB_AMOUNT,\n      });\n      escrowPda = pdas.escrowPda;\n      vaultPda = pdas.vaultPda;\n    });\n\n    it(\"oracle releases payment to freelancer and closes escrow\", async () => {\n      const freelancerBalanceBefore = await connection.getBalance(\n        freelancer.publicKey,\n      );\n\n      await program.methods\n        .releasePayment()\n        .accounts({\n          authority: oracle.publicKey,\n          freelancer: freelancer.publicKey,\n          client: client.publicKey,\n          escrowAccount: escrowPda,\n          vaultAccount: vaultPda,\n          systemProgram: SystemProgram.programId,\n        })\n        .signers([oracle])\n        .rpc({ commitment: \"confirmed\" });\n\n      const freelancerBalanceAfter = await connection.getBalance(\n        freelancer.publicKey,\n      );\n      assert.ok(\n        freelancerBalanceAfter - freelancerBalanceBefore >=\n          JOB_AMOUNT.toNumber(),\n        \"freelancer must receive at least job amount\",\n      );\n\n      // Escrow account closed — should throw on fetch\n      try {\n        await program.account.gigEscrow.fetch(escrowPda);\n        assert.fail(\"Escrow account should be closed after release\");\n      } catch (err: any) {\n        assert.ok(\n          err.message.includes(\"Account does not exist\") ||\n            err.message.includes(\"could not find account\"),\n        );\n      }\n    });\n\n    it(\"client can manually release payment\", async () => {\n      const clientJobId = \"release-client-002\";\n      const freelancer2 = Keypair.generate();\n\n      const { escrowPda: cEscrowPda, vaultPda: cVaultPda } = await createEscrow(\n        {\n          program,\n          client,\n          freelancer: freelancer2.publicKey,\n          oracle: oracle.publicKey,\n          jobId: clientJobId,\n          amount: JOB_AMOUNT,\n        },\n      );\n\n      const balBefore = await connection.getBalance(freelancer2.publicKey);\n\n      await program.methods\n        .releasePayment()\n        .accounts({\n          authority: client.publicKey, // client signs, not oracle\n          freelancer: freelancer2.publicKey,\n          client: client.publicKey,\n          escrowAccount: cEscrowPda,\n          vaultAccount: cVaultPda,\n          systemProgram: SystemProgram.programId,\n        })\n        .signers([client])\n        .rpc({ commitment: \"confirmed\" });\n\n      const balAfter = await connection.getBalance(freelancer2.publicKey);\n      assert.ok(\n        balAfter - balBefore >= JOB_AMOUNT.toNumber(),\n        \"freelancer2 must receive funds\",\n      );\n    });\n\n    it(\"rejects unauthorized signer\", async () => {\n      const rando = Keypair.generate();\n      const randoId = \"release-unauth-003\";\n      const freelancer3 = Keypair.generate();\n\n      await airdrop(connection, rando.publicKey, 1);\n\n      const { escrowPda: rEscrowPda, vaultPda: rVaultPda } = await createEscrow(\n        {\n          program,\n          client,\n          freelancer: freelancer3.publicKey,\n          oracle: oracle.publicKey,\n          jobId: randoId,\n          amount: JOB_AMOUNT,\n        },\n      );\n\n      await expectAnchorError(\n        () =>\n          program.methods\n            .releasePayment()\n            .accounts({\n              authority: rando.publicKey,\n              freelancer: freelancer3.publicKey,\n              client: client.publicKey,\n              escrowAccount: rEscrowPda,\n              vaultAccount: rVaultPda,\n              systemProgram: SystemProgram.programId,\n            })\n            .signers([rando])\n            .rpc(),\n        \"UnauthorizedExecution\",\n      );\n    });\n\n    it(\"rejects double-spend on completed job\", async () => {\n      const ds2Id = \"release-doublespend-004\";\n      const freelancer4 = Keypair.generate();\n\n      const { escrowPda: dsEscrow, vaultPda: dsVault } = await createEscrow({\n        program,\n        client,\n        freelancer: freelancer4.publicKey,\n        oracle: oracle.publicKey,\n        jobId: ds2Id,\n        amount: JOB_AMOUNT,\n      });\n\n      // First release — succeeds\n      await program.methods\n        .releasePayment()\n        .accounts({\n          authority: oracle.publicKey,\n          freelancer: freelancer4.publicKey,\n          client: client.publicKey,\n          escrowAccount: dsEscrow,\n          vaultAccount: dsVault,\n          systemProgram: SystemProgram.programId,\n        })\n        .signers([oracle])\n        .rpc({ commitment: \"confirmed\" });\n\n      // Second release — must fail (account is closed)\n      await expectAnchorError(\n        () =>\n          program.methods\n            .releasePayment()\n            .accounts({\n              authority: oracle.publicKey,\n              freelancer: freelancer4.publicKey,\n              client: client.publicKey,\n              escrowAccount: dsEscrow,\n              vaultAccount: dsVault,\n              systemProgram: SystemProgram.programId,\n            })\n            .signers([oracle])\n            .rpc(),\n        \"AccountNotInitialized\",\n      );\n    });\n  });\n\n  // ─── CANCEL JOB ────────────────────────────────────────────────────────────\n\n  describe(\"cancel_job\", () => {\n    let client: Keypair;\n    let freelancer: Keypair;\n    let oracle: Keypair;\n    let escrowPda: PublicKey;\n    let vaultPda: PublicKey;\n\n    const jobId = \"cancel-test-001\";\n\n    before(async () => {\n      client = Keypair.generate();\n      freelancer = Keypair.generate();\n      oracle = Keypair.generate();\n\n      await airdrop(connection, client.publicKey, 10);\n      await airdrop(connection, oracle.publicKey, 1);\n\n      const pdas = await createEscrow({\n        program,\n        client,\n        freelancer: freelancer.publicKey,\n        oracle: oracle.publicKey,\n        jobId,\n        amount: JOB_AMOUNT,\n      });\n      escrowPda = pdas.escrowPda;\n      vaultPda = pdas.vaultPda;\n    });\n\n    it(\"oracle refunds client and closes escrow\", async () => {\n      const clientBalanceBefore = await connection.getBalance(client.publicKey);\n\n      await program.methods\n        .cancelJob()\n        .accounts({\n          oracle: oracle.publicKey,\n          client: client.publicKey,\n          escrowAccount: escrowPda,\n          vaultAccount: vaultPda,\n          systemProgram: SystemProgram.programId,\n        })\n        .signers([oracle])\n        .rpc({ commitment: \"confirmed\" });\n\n      const clientBalanceAfter = await connection.getBalance(client.publicKey);\n      assert.ok(\n        clientBalanceAfter > clientBalanceBefore,\n        \"client must receive refund\",\n      );\n\n      // Escrow should be closed\n      try {\n        await program.account.gigEscrow.fetch(escrowPda);\n        assert.fail(\"Escrow account should be closed after cancel\");\n      } catch (err: any) {\n        assert.ok(\n          err.message.includes(\"Account does not exist\") ||\n            err.message.includes(\"could not find account\"),\n        );\n      }\n    });\n\n    it(\"rejects cancel by non-oracle signer\", async () => {\n      const impostor = Keypair.generate();\n      const impostorId = \"cancel-impostor-002\";\n      const freelancer5 = Keypair.generate();\n\n      await airdrop(connection, impostor.publicKey, 1);\n\n      const { escrowPda: iEscrow, vaultPda: iVault } = await createEscrow({\n        program,\n        client,\n        freelancer: freelancer5.publicKey,\n        oracle: oracle.publicKey,\n        jobId: impostorId,\n        amount: JOB_AMOUNT,\n      });\n\n      await expectAnchorError(\n        () =>\n          program.methods\n            .cancelJob()\n            .accounts({\n              oracle: impostor.publicKey, // wrong oracle\n              client: client.publicKey,\n              escrowAccount: iEscrow,\n              vaultAccount: iVault,\n              systemProgram: SystemProgram.programId,\n            })\n            .signers([impostor])\n            .rpc(),\n        \"InvalidOracleAuthority\",\n      );\n    });\n  });\n});\n"}