Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
9 changes: 9 additions & 0 deletions .changeset/puny-horses-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@human-protocol/core": major
"@human-protocol/sdk": minor
"@human-protocol/python-sdk": minor
---

Update escrow oracle fee handling so oracle fees are reserved independently from worker payouts.

The escrow contract now reserves oracle fees separately from worker payouts and transfers them on finalization, including when worker submissions are rejected. The SDK adds escrow fund amount accessors so clients and oracles can read the original funded amount and remaining worker payout funds.
2 changes: 1 addition & 1 deletion .github/workflows/ci-dependency-review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Dependency Review
uses: actions/dependency-review-action@v4.9.0
uses: actions/dependency-review-action@v5.0.0
with:
show-openssf-scorecard: false
2 changes: 1 addition & 1 deletion packages/apps/dashboard/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@mui/styled-engine-sc": "7.3.8",
"@mui/system": "^7.3.9",
"@mui/x-data-grid": "^8.7.0",
"@mui/x-date-pickers": "^9.0.4",
"@mui/x-date-pickers": "^9.2.0",
"@tanstack/react-query": "^5.91.3",
"@types/react-router-dom": "^5.3.3",
"@types/recharts": "^1.8.29",
Expand Down
4 changes: 2 additions & 2 deletions packages/apps/dashboard/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
"@nestjs/common": "^11.1.12",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.12",
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/mapped-types": "^2.1.1",
"@nestjs/platform-express": "^11.1.12",
"@nestjs/schedule": "^6.1.1",
"@nestjs/schedule": "^6.1.3",
"@nestjs/swagger": "^11.2.5",
"axios": "^1.3.1",
"cache-manager": "7.2.8",
Expand Down
4 changes: 2 additions & 2 deletions packages/apps/faucet/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
"react-dom": "^19.2.4",
"react-loading-skeleton": "^3.3.1",
"react-router-dom": "^7.13.0",
"serve": "^14.2.4",
"viem": "2.x"
"serve": "^14.2.6",
"viem": "^2.43.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/apps/faucet/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"body-parser": "^1.20.0",
"cors": "^2.8.6",
"express": "^5.2.1",
"express-rate-limit": "^7.3.0",
"express-rate-limit": "^8.5.2",
"node-cache": "^5.1.2",
"web3": "^4.12.1"
},
Expand Down
6 changes: 3 additions & 3 deletions packages/apps/fortune/exchange-oracle/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.0",
"serve": "^14.2.4",
"viem": "2.x",
"wagmi": "^2.14.6"
"serve": "^14.2.6",
"viem": "^2.43.0",
"wagmi": "^3.6.15"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
Typography,
useTheme,
} from '@mui/material';
import { useConnect } from 'wagmi';
import { useConnect, useConnectors } from 'wagmi';
import coinbaseSvg from '../../assets/coinbase.svg';
import metaMaskSvg from '../../assets/metamask.svg';
import walletConnectSvg from '../../assets/walletconnect.svg';
Expand All @@ -25,10 +25,23 @@ export default function WalletModal({
open: boolean;
onClose: () => void;
}) {
const { connect, connectors, error } = useConnect();
const connectors = useConnectors();
const { mutateAsync: connect, error, isPending, variables } = useConnect();

const theme = useTheme();

const handleConnect = async (connector: (typeof connectors)[number]) => {
try {
if (connector.id === 'walletConnect') {
onClose();
}

await connect({ connector });
} catch {
// wagmi exposes the connection error through `error`.
}
};

return (
<Dialog
open={open}
Expand Down Expand Up @@ -86,13 +99,8 @@ export default function WalletModal({
},
}}
key={connector.id}
onClick={() => {
connect({ connector });

if (connector.id === 'walletConnect') {
onClose();
}
}}
disabled={isPending && variables?.connector.id === connector.id}
onClick={() => handleConnect(connector)}
>
<img
src={connector.icon ?? WALLET_ICONS[connector.id]}
Expand Down
2 changes: 1 addition & 1 deletion packages/apps/fortune/exchange-oracle/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@nestjs/core": "^11.1.12",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.12",
"@nestjs/schedule": "^6.1.1",
"@nestjs/schedule": "^6.1.3",
"@nestjs/swagger": "^11.2.5",
"@nestjs/terminus": "^11.1.1",
"@nestjs/typeorm": "^11.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ describe('AssignmentService', () => {
requesterTitle: 'Example Title',
requesterDescription: 'Example Description',
submissionsRequired: 5,
fundAmount: 100,
};

beforeAll(async () => {
Expand All @@ -128,6 +127,7 @@ describe('AssignmentService', () => {
.mockResolvedValue(null);
jest.spyOn(assignmentRepository, 'countByJobId').mockResolvedValue(0);
jest.spyOn(jobService, 'getManifest').mockResolvedValue(manifest);
jest.spyOn(jobService, 'getRewardAmount').mockResolvedValue(20);
(Escrow__factory.connect as any).mockImplementation(() => ({
duration: jest
.fn()
Expand All @@ -150,13 +150,18 @@ describe('AssignmentService', () => {
workerAddress: workerAddress,
status: AssignmentStatus.ACTIVE,
expiresAt: expect.any(Date),
rewardAmount: manifest.fundAmount / manifest.submissionsRequired,
rewardAmount: 20,
});
expect(jobService.getManifest).toHaveBeenCalledWith(
chainId,
escrowAddress,
MOCK_MANIFEST_URL,
);
expect(jobService.getRewardAmount).toHaveBeenCalledWith(
chainId,
escrowAddress,
manifest.submissionsRequired,
);
});

it('should reassign user who has previously canceled', async () => {
Expand Down Expand Up @@ -371,7 +376,6 @@ describe('AssignmentService', () => {
requesterTitle: 'Example Title',
requesterDescription: 'Example Description',
submissionsRequired: 5,
fundAmount: 100,
};

jest.spyOn(jobService, 'getManifest').mockResolvedValue(manifest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export class AssignmentService {
data.escrowAddress,
jobEntity.manifestUrl,
);
const rewardAmount = await this.jobService.getRewardAmount(
data.chainId,
data.escrowAddress,
manifest.submissionsRequired,
);

// Check if all required qualifications are present
const userQualificationsSet = new Set(jwtUser.qualifications);
Expand Down Expand Up @@ -110,8 +115,7 @@ export class AssignmentService {
newAssignmentEntity.job = jobEntity;
newAssignmentEntity.workerAddress = jwtUser.address;
newAssignmentEntity.status = AssignmentStatus.ACTIVE;
newAssignmentEntity.rewardAmount =
manifest.fundAmount / manifest.submissionsRequired;
newAssignmentEntity.rewardAmount = rewardAmount;
newAssignmentEntity.expiresAt = expirationDate;
return this.assignmentRepository.createUnique(newAssignmentEntity);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export class ManifestDto {
requesterTitle: string;
requesterDescription: string;
submissionsRequired: number;
fundAmount: number;
qualifications?: string[];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Encryption,
EncryptionUtils,
EscrowClient,
EscrowUtils,
OperatorUtils,
} from '@human-protocol/sdk';
import { HttpService } from '@nestjs/axios';
Expand Down Expand Up @@ -381,7 +382,6 @@ describe('JobService', () => {
requesterTitle: 'Example Title',
requesterDescription: 'Example Description',
submissionsRequired: 5,
fundAmount: 100,
};

jest.spyOn(jobService, 'getManifest').mockResolvedValue(manifest);
Expand Down Expand Up @@ -418,6 +418,45 @@ describe('JobService', () => {
);
});

it('should calculate reward amount from net funded amount', async () => {
const manifest: ManifestDto = {
requesterTitle: 'Example Title',
requesterDescription: 'Example Description',
submissionsRequired: 5,
};

jest.spyOn(jobService, 'getManifest').mockResolvedValue(manifest);
jest.spyOn(EscrowUtils, 'getEscrow').mockResolvedValue({
token: MOCK_ADDRESS,
totalFundedAmount: 100000000000000000000n,
recordingOracleFee: 2,
reputationOracleFee: 3,
exchangeOracleFee: 5,
} as any);
jest.spyOn(HMToken__factory, 'connect').mockReturnValue({
decimals: jest.fn().mockResolvedValue(18),
} as any);
jest
.spyOn(jobRepository, 'fetchFiltered')
.mockResolvedValueOnce({ entities: jobs as any, itemCount: 1 });

const result = await jobService.getJobList(
{
chainId,
jobType: JobType.FORTUNE,
fields: [JobFieldName.RewardAmount],
escrowAddress,
status: JobStatus.ACTIVE,
page: 0,
pageSize: 10,
skip: 0,
},
workerAddress,
);

expect(result.results[0].rewardAmount).toBe('18');
});

it('should return an array of jobs without calling the manifest', async () => {
jest.spyOn(jobService, 'getManifest');
jest
Expand Down Expand Up @@ -481,7 +520,6 @@ describe('JobService', () => {
requesterTitle: 'Example Title',
requesterDescription: 'Example Description',
submissionsRequired: 5,
fundAmount: 100,
};

jest
Expand Down Expand Up @@ -537,7 +575,6 @@ describe('JobService', () => {
requesterTitle: 'Example Title',
requesterDescription: 'Example Description',
submissionsRequired: 1,
fundAmount: 100,
};

assignment.status = AssignmentStatus.ACTIVE;
Expand Down Expand Up @@ -569,7 +606,6 @@ describe('JobService', () => {
requesterTitle: 'Example Title',
requesterDescription: 'Example Description',
submissionsRequired: 5,
fundAmount: 100,
};

assignment.status = AssignmentStatus.ACTIVE;
Expand Down Expand Up @@ -605,7 +641,6 @@ describe('JobService', () => {
requesterTitle: 'Example Title',
requesterDescription: 'Example Description',
submissionsRequired: 5,
fundAmount: 100,
};

downloadFileFromUrlMock.mockResolvedValue(JSON.stringify(manifest));
Expand All @@ -626,7 +661,6 @@ describe('JobService', () => {
requesterTitle: 'Example Title',
requesterDescription: 'Example Description',
submissionsRequired: 5,
fundAmount: 100,
};

downloadFileFromUrlMock.mockResolvedValue('encrypted-content');
Expand All @@ -650,7 +684,6 @@ describe('JobService', () => {
requesterTitle: 'Example Title',
requesterDescription: 'Example Description',
submissionsRequired: 5,
fundAmount: 100,
};

downloadFileFromUrlMock.mockResolvedValue(manifest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import {
Encryption,
EncryptionUtils,
EscrowClient,
EscrowUtils,
} from '@human-protocol/sdk';
import { Inject, Injectable } from '@nestjs/common';
import { ethers } from 'ethers';

import { downloadFileFromUrl } from '../../common/utils/storage';
import { PGPConfigService } from '../../common/config/pgp-config.service';
import { ErrorAssignment, ErrorJob } from '../../common/constant/errors';
import { SortDirection } from '../../common/enums/collection';
Expand All @@ -30,6 +31,7 @@ import {
} from '../../common/errors';
import { ISolution } from '../../common/interfaces/job';
import { PageDto } from '../../common/pagination/pagination.dto';
import { downloadFileFromUrl } from '../../common/utils/storage';
import { AssignmentEntity } from '../assignment/assignment.entity';
import { AssignmentRepository } from '../assignment/assignment.repository';
import { StorageService } from '../storage/storage.service';
Expand Down Expand Up @@ -186,7 +188,11 @@ export class JobService {
data.sortField === JobSortField.REWARD_AMOUNT
) {
job.rewardAmount = (
manifest.fundAmount / manifest.submissionsRequired
await this.getRewardAmount(
entity.chainId,
entity.escrowAddress,
manifest.submissionsRequired,
)
).toString();
}
if (data.fields?.includes(JobFieldName.RewardToken)) {
Expand Down Expand Up @@ -411,4 +417,34 @@ export class JobService {

return manifest;
}

public async getRewardAmount(
chainId: number,
escrowAddress: string,
submissionsRequired: number,
): Promise<number> {
const escrow = await EscrowUtils.getEscrow(chainId, escrowAddress);
if (!escrow) {
throw new NotFoundError(ErrorJob.NotFound);
}

const decimals = await HMToken__factory.connect(
escrow.token,
this.web3Service.getSigner(chainId),
).decimals();

const netFundAmount = [
escrow.recordingOracleFee,
escrow.reputationOracleFee,
escrow.exchangeOracleFee,
].reduce(
(amount, fee) =>
amount - (escrow.totalFundedAmount * BigInt(fee ?? 0)) / 100n,
escrow.totalFundedAmount,
);

return (
Number(ethers.formatUnits(netFundAmount, decimals)) / submissionsRequired
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export class Web3Service {

private signers: { [key: number]: Wallet } = {};
readonly signerAddress: string;
readonly currentWeb3Env: string;

constructor(
private readonly web3ConfigService: Web3ConfigService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum ErrorJob {
SolutionAlreadyExists = 'Solution already exists',
AllSolutionsHaveAlreadyBeenSent = 'All solutions have already been sent',
ManifestNotFound = 'Manifest not found',
NotFound = 'Job not found',
}

/**
Expand Down
Loading
Loading