A decentralized job board platform built on the Sui blockchain, enabling employers to post jobs and candidates to apply on-chain.
- Job Posting: Employers can create job postings with title, description, and optional salary
- Application Submission: Candidates can submit applications with cover messages
- Hiring: Employers can review applications and hire candidates
- Shared State: Global JobBoard tracks all posted jobs
applications: vector<Application>- Stores multiple applications per jobjob_ids: vector<ID>- Tracks all job IDs in the JobBoard- Vector operations:
push_back,length,borrowfor iteration
salary: Option<u64>- Optional salary field (employer can choose to disclose or not)hired_candidate: Option<address>- Tracks hired candidate (None when position is open, Some when filled)- Option methods:
is_some(),is_none(),some(),none()
JobBoard- Shared object accessible by all users, tracks platform-wide stateJob- Shared object for each job posting, allowing candidates to apply
JobPosted- Emitted when employer creates a jobApplicationSubmitted- Emitted when candidate appliesCandidateHired- Emitted when employer hires a candidateJobBoardCreated- Emitted when the platform initializes
EmployerCap- Capability object proving job ownership- Only the employer with the matching capability can hire for their job
- Prevents unauthorized users from modifying job postings
- Dual verification: checks both capability job_id and sender address
// Global shared object tracking all jobs
public struct JobBoard has key {
id: UID,
job_count: u64,
}
// Individual job posting
public struct Job has key, store {
id: UID,
employer: address,
title: String,
description: String,
salary: Option<u64>, // Optional<T> usage
applications: vector<Application>, // Vector usage
hired_candidate: Option<address>, // Optional<T> usage
is_active: bool,
}
// Candidate application
public struct Application has store, copy, drop {
candidate: address,
cover_message: String,
timestamp: u64,
}
// Employer capability for access control
public struct EmployerCap has key, store {
id: UID,
job_id: ID,
}- Creates a new job posting
- Issues an
EmployerCapto the employer for access control - Adds job to the global JobBoard
- Emits
JobPostedevent
- Allows candidates to submit applications
- Stores application in the job's vector
- Requires job to be active and unfilled
- Emits
ApplicationSubmittedevent
- Employer hires a candidate (requires
EmployerCap) - Access control: verifies employer owns the job via capability
- Updates
hired_candidatefromNonetoSome(address) - Marks job as inactive
- Emits
CandidateHiredevent
get_job_info- Returns job detailsget_application_count- Returns number of applicationsget_total_jobs- Returns total jobs on platformis_job_filled- Checks if position is filled
The project includes comprehensive unit tests covering:
-
Happy Paths
- Posting jobs with and without salary
- Multiple candidates applying to jobs
- Successful hiring flow
- Multiple job postings
-
Access Control
- Unauthorized users cannot hire (capability check)
-
Business Logic
- Cannot apply to filled positions
- Cannot hire non-applicants
- Position status updates correctly
sui move testAll 8 tests pass successfully:
test_post_job_with_salarytest_post_job_without_salarytest_apply_to_jobtest_hire_candidatetest_hire_without_permission(expected failure)test_apply_to_filled_job(expected failure)test_hire_non_applicant(expected failure)test_multiple_jobs
sui move buildsui client publish --gas-budget 100000000sui client call --package <PACKAGE_ID> --module minihub --function post_job \
--args <JOB_BOARD_ID> "Senior Move Developer" "Build blockchain apps" "some(150000)" \
--gas-budget 10000000sui client call --package <PACKAGE_ID> --module minihub --function apply_to_job \
--args <JOB_ID> "I have 5 years of blockchain experience" <CLOCK_ID> \
--gas-budget 10000000sui client call --package <PACKAGE_ID> --module minihub --function hire_candidate \
--args <JOB_ID> <EMPLOYER_CAP_ID> <CANDIDATE_ADDRESS> \
--gas-budget 10000000- Capability-Based Access Control: Only employers with the matching
EmployerCapcan hire for their jobs - Double Verification: Both capability ID and sender address are verified
- State Validation: Checks prevent hiring for filled positions or non-existent applications
- Immutable History: All applications are stored on-chain permanently
ENotAuthorized (1): Caller doesn't have permission to perform actionEJobAlreadyFilled (2): Cannot apply/hire for already filled positionEInvalidApplication (3): Trying to hire candidate who didn't apply
MIT
MiniHub Move kontratı ile TypeScript/React üzerinden etkileşim için optimize edilmiş SDK'yı kullanabilirsiniz. Tüm fonksiyonlar, veri yapıları ve event'ler Move kontratı ile birebir uyumludur.
npm install @mysten/sui/sdk/minihub.ts dosyasını projenize ekleyin.
import { SuiClient } from '@mysten/sui/client';
import { createMiniHubSDK, DEFAULT_CLOCK_ID } from './sdk/minihub';
const client = new SuiClient({ url: 'https://fullnode.testnet.sui.io' });
const sdk = createMiniHubSDK(client, {
packageId: '<PACKAGE_ID>',
jobBoardId: '<JOB_BOARD_ID>',
userRegistryId: '<USER_REGISTRY_ID>',
employerRegistryId: '<EMPLOYER_REGISTRY_ID>',
clockId: DEFAULT_CLOCK_ID,
});const tx = sdk.createPostJobTransaction({
employerProfileId: '<EMPLOYER_PROFILE_ID>',
title: 'Senior Move Developer',
description: 'Build blockchain apps',
salary: 150000,
deadline: Date.now() + 7 * 24 * 60 * 60 * 1000, // 1 hafta sonrası
});
// client.signAndExecuteTransactionBlock({ transactionBlock: tx, ... })const tx = sdk.createApplyToJobTransaction({
jobId: '<JOB_ID>',
userProfileId: '<USER_PROFILE_ID>',
coverMessage: 'I have 5 years of blockchain experience',
cvUrl: 'https://mycv.com/cv.pdf',
});const tx = sdk.createHireCandidateTransaction({
jobId: '<JOB_ID>',
employerCapId: '<EMPLOYER_CAP_ID>',
candidateAddress: '<CANDIDATE_ADDRESS>',
candidateIndex: 0,
});const tx = sdk.createUserProfileTransaction({
name: 'Berkay',
bio: 'Blockchain developer',
avatarUrl: 'https://avatar.com/me.png',
skills: ['Move', 'TypeScript'],
experienceYears: 5,
portfolioUrl: 'https://portfolio.com',
});const tx = sdk.createEmployerProfileTransaction({
companyName: 'Sui Labs',
description: 'Web3 company',
logoUrl: 'https://logo.com/logo.png',
website: 'https://suilabs.com',
industry: 'Blockchain',
employeeCount: 50,
foundedYear: 2022,
});const jobs = await sdk.getAllJobs();const job = await sdk.getJob('<JOB_ID>');const count = await sdk.getJob('<JOB_ID>').then(j => j?.applicationCount);const profile = await sdk.getUserProfile('<USER_PROFILE_ID>');const employer = await sdk.getEmployerProfile('<EMPLOYER_PROFILE_ID>');const applications = await sdk.getJobApplications('<JOB_ID>');const jobPostedEvents = await sdk.getJobPostedEvents();
const applicationEvents = await sdk.getApplicationSubmittedEvents();
const hiredEvents = await sdk.getCandidateHiredEvents();import { ErrorCode, ERROR_MESSAGES } from './sdk/minihub';
try {
// işlem
} catch (e: any) {
if (e.code && ERROR_MESSAGES[e.code]) {
alert(ERROR_MESSAGES[e.code]);
}
}import { useEffect, useState } from 'react';
function JobList() {
const [jobs, setJobs] = useState<Job[]>([]);
useEffect(() => {
sdk.getAllJobs().then(setJobs);
}, []);
return (
<ul>
{jobs.map(job => (
<li key={job.id}>{job.title} - {sdk.formatSalary(job.salary)}</li>
))}
</ul>
);
}
