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
1 change: 0 additions & 1 deletion packages/apps/human-app/frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ VITE_NAVBAR__LINK__PROTOCOL_URL=https://humanprotocol.org/
VITE_PRIVACY_POLICY_URL=http://local.app/privacy-policy/
VITE_TERMS_OF_SERVICE_URL=http://local.app/terms-and-conditions/

VITE_GOVERNOR_ADDRESS=
VITE_GOVERNANCE_URL=

# Feature flags
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,50 @@ import { useTranslation } from 'react-i18next';
import { env } from '@/shared/env';
import { useColorMode } from '@/shared/contexts/color-mode';
import { useWorkerIdentityVerificationStatus } from '@/modules/worker/profile/hooks';
import { useActiveProposalQuery } from '../hooks/use-active-proposal-query';
import { useProposalQuery } from '../hooks/use-proposal-query';
import { formatCountdown } from '../../../shared/utils/time';
import { type ProposalResponse } from '../services/governance.service';

export type ProposalStatus = 'pending' | 'active';

function getProposalStatus(proposal: ProposalResponse): ProposalStatus {
const now = Date.now();
const { voteStart, voteEnd } = proposal;
if (voteStart <= now && now < voteEnd) return 'active';
return 'pending';
}
export function GovernanceBanner() {
const { t } = useTranslation();
const { data, isLoading, isError } = useActiveProposalQuery();
const { data: proposal, isLoading, isError } = useProposalQuery();
const { isVerificationCompleted } = useWorkerIdentityVerificationStatus();
const { colorPalette } = useColorMode();
const { text, background } = colorPalette.banner;
const [timeRemaining, setTimeRemaining] = useState('00:00:00');

useEffect(() => {
if (!data?.deadline) return;
if (!proposal) return;
const { voteStart, voteEnd } = proposal;

const timer = setInterval(() => {
const now = Math.floor(Date.now() / 1000);
const diff = data.deadline - now;

if (diff <= 0) {
setTimeRemaining('00:00:00');
} else {
const hours = Math.floor(diff / 3600);
const minutes = Math.floor((diff % 3600) / 60);
const seconds = diff % 60;

const hh = hours.toString().padStart(2, '0');
const mm = minutes.toString().padStart(2, '0');
const ss = seconds.toString().padStart(2, '0');

setTimeRemaining(`${hh}:${mm}:${ss}`);
}
const currentStatus = getProposalStatus(proposal);
setTimeRemaining(
formatCountdown(currentStatus === 'pending' ? voteStart : voteEnd)
);
}, 1000);

return () => {
clearInterval(timer);
};
}, [data?.deadline]);
}, [proposal]);

if (!isVerificationCompleted || isLoading || isError || !data) {
if (!isVerificationCompleted || isLoading || isError || !proposal) {
return null;
}

const forVotes = parseFloat(data.forVotes) || 0;
const againstVotes = parseFloat(data.againstVotes) || 0;
const abstainVotes = parseFloat(data.abstainVotes) || 0;
const totalVotes = forVotes + againstVotes + abstainVotes;
const status = getProposalStatus(proposal);

const totalVotes =
proposal.forVotes + proposal.againstVotes + proposal.abstainVotes;

return (
<Grid
Expand All @@ -76,22 +75,27 @@ export function GovernanceBanner() {
<Box display="flex" alignItems="center">
<AccessTimeIcon sx={{ mr: 1 }} />
<Typography variant="body2" color={text.secondary}>
{t('governance.timeToReveal', 'Time to reveal vote')}:
{status === 'pending'
? t('governance.timeToStart', 'Voting starts in')
: t('governance.timeToReveal', 'Time to reveal vote')}
:
</Typography>
<Typography variant="body1" ml={1} color={text.primary}>
{timeRemaining}
</Typography>
</Box>
<Typography
variant="body1"
ml={{ xs: 0, md: 8 }}
color={text.primary}
bgcolor={background.secondary}
borderRadius="8px"
padding="4px 8px"
>
{totalVotes} {t('governance.votes', 'votes')}
</Typography>
{status === 'active' && (
<Typography
variant="body1"
ml={{ xs: 0, md: 8 }}
color={text.primary}
bgcolor={background.secondary}
borderRadius="8px"
padding="4px 8px"
>
{totalVotes} {t('governance.votes', 'votes')}
</Typography>
)}
</Grid>

{/* Right side: "More details" link */}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import { fetchProposal } from '../services/governance.service';

export function useProposalQuery() {
return useQuery({
queryKey: ['governanceProposal'],
queryFn: fetchProposal,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { authorizedHumanAppApiClient } from '@/api';

const apiPaths = {
getProposals: '/governance/proposals',
};

export interface ProposalResponse {
proposalId: string;
forVotes: number;
againstVotes: number;
abstainVotes: number;
voteStart: number;
voteEnd: number;
}

export async function fetchProposal(): Promise<ProposalResponse | null> {
const list = await authorizedHumanAppApiClient.get<ProposalResponse[]>(
apiPaths.getProposals
);
if (!Array.isArray(list) || list.length === 0) return null;

const now = Date.now();
const activeProposals = list.filter(
(p) => p.voteStart <= now && now < p.voteEnd
);
if (activeProposals.length > 0)
return activeProposals.sort((a, b) => a.voteEnd - b.voteEnd)[0];

const pendingProposals = list.filter((p) => now < p.voteStart);
if (pendingProposals.length > 0)
return pendingProposals.sort((a, b) => a.voteStart - b.voteStart)[0];

return null;
}
1 change: 0 additions & 1 deletion packages/apps/human-app/frontend/src/shared/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ const envSchema = z.object({
return iconsArray;
}),
VITE_NETWORK: z.enum(['mainnet', 'testnet']),
VITE_GOVERNOR_ADDRESS: z.string(),
VITE_GOVERNANCE_URL: z.string(),
VITE_H_CAPTCHA_ORACLE_ANNOTATION_TOOL: z.string(),
VITE_H_CAPTCHA_ORACLE_ROLE: z.string(),
Expand Down
13 changes: 13 additions & 0 deletions packages/apps/human-app/frontend/src/shared/utils/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function formatCountdown(targetMs: number): string {
const now = Date.now();
const diffMs = targetMs - now;
if (diffMs <= 0) return '00:00:00';
const totalSeconds = Math.floor(diffMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const hh = String(hours).padStart(2, '0');
const mm = String(minutes).padStart(2, '0');
const ss = String(seconds).padStart(2, '0');
return `${hh}:${mm}:${ss}`;
}
3 changes: 3 additions & 0 deletions packages/apps/human-app/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ HCAPTCHA_LABELING_API_KEY=disabled

# Feature flags
FEATURE_FLAG_JOBS_DISCOVERY=true

# Governance
GOVERNOR_ADDRESS=replace_me
3 changes: 2 additions & 1 deletion packages/apps/human-app/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@automapper/classes": "^8.8.1",
"@automapper/core": "^8.8.1",
"@automapper/nestjs": "^8.8.1",
"@human-protocol/core": "workspace:*",
"@human-protocol/logger": "workspace:*",
"@human-protocol/sdk": "workspace:*",
"@nestjs/axios": "^3.1.2",
Expand All @@ -42,7 +43,7 @@
"cache-manager-redis-yet": "^5.1.5",
"class-transformer": "^0.5.1",
"class-validator": "0.14.1",
"ethers": "^6.13.5",
"ethers": "~6.13.5",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/apps/human-app/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import { KycProcedureModule } from './modules/kyc-procedure/kyc-procedure.module
import { NDAController } from './modules/nda/nda.controller';
import { NDAModule } from './modules/nda/nda.module';
import { OracleDiscoveryController } from './modules/oracle-discovery/oracle-discovery.controller';
import { GovernanceModule } from './modules/governance/governance.module';
import { GovernanceController } from './modules/governance/governance.controller';
import { OracleDiscoveryModule } from './modules/oracle-discovery/oracle-discovery.module';
import { PasswordResetModule } from './modules/password-reset/password-reset.module';
import { PrepareSignatureModule } from './modules/prepare-signature/prepare-signature.module';
Expand Down Expand Up @@ -76,6 +78,8 @@ const JOI_BOOLEAN_STRING_SCHEMA = Joi.string().valid('true', 'false');
REDIS_HOST: Joi.string().required(),
REDIS_DB: Joi.number(),
RPC_URL: Joi.string().required(),
GOVERNANCE_RPC_URL: Joi.string(),
GOVERNOR_ADDRESS: Joi.string().required(),
HCAPTCHA_LABELING_STATS_API_URL: Joi.string().required(),
HCAPTCHA_LABELING_VERIFY_API_URL: Joi.string().required(),
HCAPTCHA_LABELING_API_KEY: Joi.string().required(),
Expand Down Expand Up @@ -141,6 +145,7 @@ const JOI_BOOLEAN_STRING_SCHEMA = Joi.string().valid('true', 'false');
UiConfigurationModule,
NDAModule,
AbuseModule,
GovernanceModule,
],
controllers: [
AppController,
Expand All @@ -155,6 +160,7 @@ const JOI_BOOLEAN_STRING_SCHEMA = Joi.string().valid('true', 'false');
TokenRefreshController,
NDAController,
AbuseController,
GovernanceController,
],
exports: [HttpModule],
providers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,21 @@ export class EnvironmentConfigService {
return this.configService.getOrThrow<string>('RPC_URL');
}

/**
* RPC URL for the governance hub (optional). If not provided, falls back to RPC_URL.
*/
get governanceRpcUrl(): string {
return this.configService.get<string>('GOVERNANCE_RPC_URL') || this.rpcUrl;
}

/**
* Governor contract address used for governance queries.
* Required
*/
get governorAddress(): string {
return this.configService.getOrThrow<string>('GOVERNOR_ADDRESS');
Comment thread
dnechay marked this conversation as resolved.
}

/**
* Flag indicating if CORS is enabled.
* Default: false
Expand Down
10 changes: 10 additions & 0 deletions packages/apps/human-app/server/src/common/enums/proposal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export enum ProposalState {
PENDING,
ACTIVE,
CANCELED,
DEFEATED,
SUCCEEDED,
QUEUED,
EXPIRED,
EXECUTED,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Controller, Get, HttpCode, Header } from '@nestjs/common';
import {
ApiBearerAuth,
ApiOkResponse,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { GovernanceService } from './governance.service';
import { ProposalResponse } from './model/governance.model';

@ApiTags('Governance')
@ApiBearerAuth()
@Controller('/governance')
export class GovernanceController {
constructor(private readonly governanceService: GovernanceService) {}

@ApiOperation({ summary: 'Get pending and active governance proposals' })
@ApiOkResponse({ type: ProposalResponse, isArray: true })
@HttpCode(200)
Comment thread
dnechay marked this conversation as resolved.
@Header('Cache-Control', 'private, max-age=60')
@Get('/proposals')
public async getProposals(): Promise<ProposalResponse[]> {
return this.governanceService.getProposals();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { GovernanceService } from './governance.service';

@Module({
providers: [GovernanceService],
exports: [GovernanceService],
})
export class GovernanceModule {}
Loading
Loading