This implementation ensures trustless verification by validating all blockchain transactions on the backend before updating the database.
1. Frontend: Recruiter signs transaction → Creates PDA & stakes SOL
2. Frontend: Gets transaction signature from blockchain
3. Frontend: Sends transaction signature to backend
4. Backend: Verifies transaction exists on blockchain
5. Backend: Verifies PDA was created by recruiter's wallet
6. Backend: Verifies full amount is staked in escrow
7. Backend: Only then updates database (job status, project, milestones)
8. Frontend: Receives confirmation and shows success
-
/http-backend/src/utils/solana-verification.ts- Blockchain verification utilities
- PDA derivation
- Escrow account parsing
- Transaction signature verification
-
/http-backend/src/routes/escrow.ts- API endpoints for escrow operations
/api/escrow/verify-funding- Verify job funding/api/escrow/verify-approval- Verify milestone approval/api/escrow/verify-claim- Verify milestone claim/api/escrow/status/:jobId- Get escrow status
-
/frontend/src/hooks/useEscrowWithVerification.tsx- Enhanced React hook with backend verification
- Two-step process: blockchain → backend verification
- Automatic error handling and notifications
-
/frontend/src/lib/api-helpers.ts- API client functions to call backend verification endpoints
// In JobApplicants.tsx or JobDetail.tsx
import { useEscrowWithVerification } from "@/hooks/useEscrowWithVerification";
function SelectFreelancerButton({ job, application }) {
const { fundJobEscrow, isLoading } = useEscrowWithVerification();
const handleSelect = async () => {
const result = await fundJobEscrow(
job.id, // Job ID (UUID)
application.freelancer.walletAddress, // Freelancer's Solana wallet
[1.5, 2.0, 1.5], // 3 milestones in SOL
(data) => {
// Success! Database is now updated
console.log("Job funded and verified:", data);
navigate(`/project/${job.id}`);
}
);
};
return (
<Button onClick={handleSelect} disabled={isLoading}>
{isLoading ? "Processing..." : "Select & Fund Job"}
</Button>
);
}// 1. Frontend calls fundJobEscrow()
// 2. Creates escrow on Solana blockchain
// 3. Gets transaction signature
// 4. Calls backend: POST /api/escrow/verify-funding
// 5. Backend verifies:
// - Transaction exists on blockchain ✓
// - PDA created by recruiter's wallet ✓
// - Full amount staked ✓
// - Freelancer wallet matches ✓
// 6. Backend updates:
// - Job status → "in_progress"
// - Creates Project record
// - Creates Milestone records
// - Creates Staking record
// - Creates Transaction record
// - Sends notification to freelancer
// 7. Frontend receives success responseEndpoint: POST /api/escrow/verify-funding
Request:
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"recruiterWallet": "CMvVjcRz1CfmbLJ2RRUsDBYXh4bRcWttpkNY7FREHLUK",
"freelancerWallet": "8YWqVpPKuXRp1mZZLxdTYJqvFWLYm5Lsh3YRgJwYT5JL",
"txSignature": "5J7x...",
"expectedTotal": 5.0,
"milestoneAmounts": [1.5, 2.0, 1.5]
}Backend Verification Steps:
// 1. Verify user is the recruiter
if (job.recruiterId !== userId) {
return error("Unauthorized");
}
// 2. Verify transaction signature exists on blockchain
const txStatus = await connection.getSignatureStatus(txSignature);
if (!txStatus || txStatus.err) {
return error("Transaction not found or failed");
}
// 3. Calculate expected PDA address
const [escrowPDA] = deriveEscrowPDA(recruiterPubkey, jobId);
// 4. Verify escrow account exists
const accountInfo = await connection.getAccountInfo(escrowPDA);
if (!accountInfo) {
return error("Escrow account not found");
}
// 5. Verify account is owned by our program
if (accountInfo.owner !== PROGRAM_ID) {
return error("Invalid escrow account");
}
// 6. Parse escrow account data
const escrowData = parseEscrowAccount(accountInfo.data);
// 7. Verify recruiter matches
if (escrowData.recruiter !== recruiterWallet) {
return error("Recruiter mismatch");
}
// 8. Verify freelancer matches
if (escrowData.freelancer !== freelancerWallet) {
return error("Freelancer mismatch");
}
// 9. Verify job ID matches
if (escrowData.jobId !== jobId) {
return error("Job ID mismatch");
}
// 10. Verify balance is sufficient
const balance = await connection.getBalance(escrowPDA);
const totalMilestones = milestoneAmounts.reduce((a, b) => a + b);
if (balance < totalMilestones) {
return error("Insufficient balance");
}
// ✅ All checks passed - update databaseResponse (Success):
{
"success": true,
"message": "Escrow verified and job activated",
"data": {
"job": { "id": "...", "status": "in_progress" },
"project": { "id": "...", "status": "active" },
"escrowAddress": "9xQeW...",
"balance": 5.0,
"milestones": [...]
}
}Endpoint: POST /api/escrow/verify-approval
Request:
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"milestoneIndex": 0,
"txSignature": "3K8y..."
}Backend Actions:
- Verifies transaction on blockchain
- Fetches escrow account data
- Checks
milestonesApproved[0] === true - Updates milestone status in database to "approved"
- Notifies freelancer
Endpoint: POST /api/escrow/verify-claim
Request:
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"milestoneIndex": 0,
"txSignature": "7R9m..."
}Backend Actions:
- Verifies transaction on blockchain
- Fetches escrow account data
- Checks
milestonesClaimed[0] === true - Updates milestone status to "completed"
- Marks payment as released
- Creates transaction record
- Updates staking total released
- Notifies recruiter
cd http-backend
npm install @coral-xyz/anchor @solana/web3.jsAdd to .env:
# Solana Configuration
SOLANA_RPC_URL=https://api.devnet.solana.com
# For production:
# SOLANA_RPC_URL=https://api.mainnet-beta.solana.comcd http-backend
npm run devThe escrow verification endpoints will be available at:
http://localhost:3001/api/escrow/verify-fundinghttp://localhost:3001/api/escrow/verify-approvalhttp://localhost:3001/api/escrow/verify-claimhttp://localhost:3001/api/escrow/status/:jobId
// Replace useEscrow with useEscrowWithVerification
import { useEscrowWithVerification } from "@/hooks/useEscrowWithVerification";
function YourComponent() {
const {
fundJobEscrow, // Includes backend verification
approveMilestonePayment, // Includes backend verification
claimMilestonePayment, // Includes backend verification
isLoading,
} = useEscrowWithVerification();
// Use exactly the same as before - verification is automatic!
}Old useEscrow:
- Only creates blockchain transaction
- You manually call backend API to update database
- Risk: Database could be out of sync with blockchain
New useEscrowWithVerification:
- Creates blockchain transaction
- Automatically verifies on backend
- Backend updates database only after verification
- Guaranteed: Database always matches blockchain state
Ensures the escrow account is owned by the correct Solana program:
if (accountInfo.owner !== PROGRAM_ID) {
return error("Invalid escrow");
}Ensures the recruiter who funded the escrow is the correct user:
if (escrowData.recruiter !== recruiterWallet) {
return error("Unauthorized");
}Ensures the full amount is staked:
const balance = await connection.getBalance(escrowPDA);
if (balance < expectedTotal) {
return error("Insufficient balance");
}Ensures the transaction actually succeeded on blockchain:
const status = await connection.getSignatureStatus(signature);
if (!status || status.err) {
return error("Transaction failed");
}"Escrow account not found"
- The PDA doesn't exist on blockchain
- Transaction may have failed
- Wrong network (devnet vs mainnet)
"Insufficient balance"
- Recruiter didn't stake enough SOL
- Transaction partially failed
- Network issue during funding
"Recruiter mismatch"
- Different wallet funded the escrow
- Potential security issue
"Transaction not found"
- Invalid transaction signature
- Wrong network
- Transaction not yet confirmed
After successful verification, the backend updates:
UPDATE jobs SET
status = 'in_progress',
selected_freelancer_id = '<freelancer_id>'
WHERE id = '<job_id>';INSERT INTO projects (job_id, recruiter_id, freelancer_id, status)
VALUES ('<job_id>', '<recruiter_id>', '<freelancer_id>', 'active');INSERT INTO milestones (project_id, stage_id, payment_amount, status)
VALUES (...);INSERT INTO staking (
project_id, recruiter_id, wallet_address,
total_staked, transaction_signature
)
VALUES (...);INSERT INTO transactions (
from_user_id, to_user_id, project_id,
type, amount, wallet_signature, status
)
VALUES (...);- Fund Job:
# Frontend calls blockchain
POST /api/escrow/verify-funding
# Check database
SELECT * FROM jobs WHERE id = '<job_id>';
# Should show status = 'in_progress'
SELECT * FROM projects WHERE job_id = '<job_id>';
# Should have a new project
SELECT * FROM milestones WHERE project_id = '<project_id>';
# Should have 3 milestones- Approve Milestone:
POST /api/escrow/verify-approval
SELECT * FROM milestones WHERE id = '<milestone_id>';
# Should show status = 'approved'- Claim Milestone:
POST /api/escrow/verify-claim
SELECT * FROM milestones WHERE id = '<milestone_id>';
# Should show payment_released = true, status = 'completed'
SELECT * FROM transactions WHERE milestone_id = '<milestone_id>';
# Should have transaction record✅ Trustless - Backend verifies blockchain state
✅ Atomic - Database updates only after blockchain confirmation
✅ Audit Trail - All transaction signatures stored
✅ Prevention - Can't fake escrow funding
✅ Integrity - Database always matches blockchain
✅ Seamless - Automatic verification in background
✅ Fast - Parallel blockchain & backend processing
✅ Reliable - Rollback on verification failure
✅ Transparent - Clear error messages
This implementation ensures your platform maintains data integrity while providing a smooth user experience!