Skip to content

Commit a9d845c

Browse files
authored
Release 2025-09-18 (#3555)
2 parents 34853b0 + cd9289f commit a9d845c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1859
-642
lines changed

packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/schemas.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,12 @@ export const dailyHmtSpentResponseSchema = z.object({
1111

1212
export const hcaptchaUserStatsResponseSchema = z.object({
1313
balance: z.object({
14-
available: z.number(),
15-
estimated: z.number(),
1614
recent: z.number(),
1715
total: z.number(),
1816
}),
1917
served: z.number(),
2018
solved: z.number(),
21-
verified: z.number(),
2219
currentDateStats: z.object({
23-
billing_units: z.number(),
24-
bypass: z.number(),
25-
served: z.number(),
2620
solved: z.number(),
2721
}),
28-
currentEarningsStats: z.number(),
2922
});

packages/apps/job-launcher/server/src/common/constants/tokens.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,46 +15,64 @@ export const TOKEN_ADDRESSES: {
1515
address: NETWORKS[ChainId.MAINNET]!.hmtAddress,
1616
decimals: 18,
1717
},
18-
// [EscrowFundToken.USDT]: { address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', decimals: 6 },
19-
// [EscrowFundToken.USDC]: { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606EB48', decimals: 6 },
18+
[EscrowFundToken.USDT]: {
19+
address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
20+
decimals: 6,
21+
},
22+
[EscrowFundToken.USDC]: {
23+
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
24+
decimals: 6,
25+
},
2026
},
2127
[ChainId.SEPOLIA]: {
2228
[EscrowFundToken.HMT]: {
2329
address: NETWORKS[ChainId.SEPOLIA]!.hmtAddress,
2430
decimals: 18,
2531
},
26-
// [EscrowFundToken.USDT]: { address: '0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0', decimals: 6 },
27-
// [EscrowFundToken.USDC]: { address: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', decimals: 6 },
32+
[EscrowFundToken.USDT]: {
33+
address: '0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0',
34+
decimals: 6,
35+
},
36+
[EscrowFundToken.USDC]: {
37+
address: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
38+
decimals: 6,
39+
},
2840
},
2941
[ChainId.BSC_MAINNET]: {
3042
[EscrowFundToken.HMT]: {
3143
address: NETWORKS[ChainId.BSC_MAINNET]!.hmtAddress,
3244
decimals: 18,
3345
},
34-
// [EscrowFundToken.USDT]: { address: '0x55d398326f99059fF775485246999027B3197955', decimals: 18 },
35-
// [EscrowFundToken.USDC]: { address: '0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d', decimals: 18 },
3646
},
3747
[ChainId.POLYGON]: {
3848
[EscrowFundToken.HMT]: {
3949
address: NETWORKS[ChainId.POLYGON]!.hmtAddress,
4050
decimals: 18,
4151
},
42-
// [EscrowFundToken.USDT]: { address: '0x3813e82e6f7098b9583FC0F33a962D02018B6803', decimals: 6 },
43-
// [EscrowFundToken.USDC]: { address: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', decimals: 6 },
52+
[EscrowFundToken.USDT]: {
53+
address: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
54+
decimals: 6,
55+
},
56+
[EscrowFundToken.USDC]: {
57+
address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
58+
decimals: 6,
59+
},
4460
},
4561
[ChainId.POLYGON_AMOY]: {
4662
[EscrowFundToken.HMT]: {
4763
address: NETWORKS[ChainId.POLYGON_AMOY]!.hmtAddress,
4864
decimals: 18,
4965
},
50-
// [EscrowFundToken.USDC]: { address: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582', decimals: 6 },
66+
[EscrowFundToken.USDC]: {
67+
address: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
68+
decimals: 6,
69+
},
5170
},
5271
[ChainId.AURORA_TESTNET]: {
5372
[EscrowFundToken.HMT]: {
5473
address: NETWORKS[ChainId.AURORA_TESTNET]!.hmtAddress,
5574
decimals: 18,
5675
},
57-
// [EscrowFundToken.USDC]: { address: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582', decimals: 6 },
5876
},
5977
[ChainId.LOCALHOST]: {
6078
[EscrowFundToken.HMT]: {

packages/apps/job-launcher/server/src/modules/payment/payment.service.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export class PaymentService {
241241
}
242242

243243
const signer = this.web3Service.getSigner(dto.chainId);
244-
const tokenAddress = transaction.logs[0].address;
244+
const tokenAddress = transaction.logs[0].address.toLowerCase();
245245

246246
const tokenContract: HMToken = HMToken__factory.connect(
247247
tokenAddress,
@@ -262,7 +262,10 @@ export class PaymentService {
262262
const tokenId = (await tokenContract.symbol()).toLowerCase();
263263
const token = TOKEN_ADDRESSES[dto.chainId]?.[tokenId as EscrowFundToken];
264264

265-
if (token?.address !== tokenAddress || !CoingeckoTokenId[tokenId]) {
265+
if (
266+
token?.address?.toLowerCase() !== tokenAddress ||
267+
!CoingeckoTokenId[tokenId]
268+
) {
266269
throw new ConflictError(ErrorPayment.UnsupportedToken);
267270
}
268271

packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/cvat-payouts-calculator.spec.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,14 @@ describe('CvatPayoutsCalculator', () => {
4848
const mockedGetIntermediateResultsUrl = jest
4949
.fn()
5050
.mockImplementation(async () => faker.internet.url());
51+
const mockedGetTokenAddress = jest.fn().mockImplementation(async () => {
52+
return faker.finance.ethereumAddress();
53+
});
5154

5255
beforeAll(() => {
5356
mockedEscrowClient.build.mockResolvedValue({
5457
getIntermediateResultsUrl: mockedGetIntermediateResultsUrl,
58+
getTokenAddress: mockedGetTokenAddress,
5559
} as unknown as EscrowClient);
5660
});
5761

@@ -80,13 +84,91 @@ describe('CvatPayoutsCalculator', () => {
8084
);
8185
});
8286

87+
it('should properly calculate workers bounties trimming the decimals', async () => {
88+
const annotators = [
89+
faker.finance.ethereumAddress(),
90+
faker.finance.ethereumAddress(),
91+
];
92+
93+
const jobsPerAnnotator = faker.number.int({ min: 1, max: 3 });
94+
const tokenDecimals = BigInt(6);
95+
96+
const annotationsMeta: CvatAnnotationMeta = {
97+
jobs: Array.from(
98+
{ length: jobsPerAnnotator * annotators.length },
99+
(_v, index: number) => ({
100+
job_id: index,
101+
final_result_id: faker.number.int(),
102+
}),
103+
),
104+
results: [],
105+
};
106+
for (const job of annotationsMeta.jobs) {
107+
const annotatorIndex = job.job_id % annotators.length;
108+
109+
annotationsMeta.results.push({
110+
id: job.final_result_id,
111+
job_id: job.job_id,
112+
annotator_wallet_address: annotators[annotatorIndex],
113+
annotation_quality: faker.number.float(),
114+
});
115+
}
116+
117+
// imitate weird case: job w/o result
118+
annotationsMeta.jobs.push({
119+
job_id: faker.number.int(),
120+
final_result_id: faker.number.int(),
121+
});
122+
// imitate weird case: result w/o job
123+
annotationsMeta.results.push({
124+
id: faker.number.int(),
125+
job_id: faker.number.int(),
126+
annotator_wallet_address: faker.helpers.arrayElement(annotators),
127+
annotation_quality: faker.number.float(),
128+
});
129+
130+
mockedStorageService.downloadJsonLikeData.mockResolvedValueOnce(
131+
annotationsMeta,
132+
);
133+
mockedWeb3Service.getTokenDecimals.mockResolvedValueOnce(tokenDecimals);
134+
135+
const mockedJobBounty = '0.123456789'; // more decimals than token has
136+
const manifest = {
137+
...generateCvatManifest(),
138+
job_bounty: mockedJobBounty,
139+
};
140+
141+
const payouts = await calculator.calculate({
142+
chainId,
143+
escrowAddress,
144+
manifest,
145+
finalResultsUrl: faker.internet.url(),
146+
});
147+
148+
const trimmedBounty = '0.123456';
149+
150+
const expectedAmountPerAnnotator =
151+
BigInt(jobsPerAnnotator) *
152+
ethers.parseUnits(trimmedBounty, tokenDecimals);
153+
154+
const expectedPayouts = annotators.map((address) => ({
155+
address,
156+
amount: expectedAmountPerAnnotator,
157+
}));
158+
159+
expect(_.sortBy(payouts, 'address')).toEqual(
160+
_.sortBy(expectedPayouts, 'address'),
161+
);
162+
});
163+
83164
it('should properly calculate workers bounties', async () => {
84165
const annotators = [
85166
faker.finance.ethereumAddress(),
86167
faker.finance.ethereumAddress(),
87168
];
88169

89170
const jobsPerAnnotator = faker.number.int({ min: 1, max: 3 });
171+
const tokenDecimals = BigInt(faker.number.int({ min: 6, max: 18 }));
90172

91173
const annotationsMeta: CvatAnnotationMeta = {
92174
jobs: Array.from(
@@ -125,6 +207,7 @@ describe('CvatPayoutsCalculator', () => {
125207
mockedStorageService.downloadJsonLikeData.mockResolvedValueOnce(
126208
annotationsMeta,
127209
);
210+
mockedWeb3Service.getTokenDecimals.mockResolvedValueOnce(tokenDecimals);
128211

129212
const manifest = generateCvatManifest();
130213

@@ -136,7 +219,8 @@ describe('CvatPayoutsCalculator', () => {
136219
});
137220

138221
const expectedAmountPerAnnotator =
139-
BigInt(jobsPerAnnotator) * ethers.parseUnits(manifest.job_bounty, 18);
222+
BigInt(jobsPerAnnotator) *
223+
ethers.parseUnits(manifest.job_bounty, tokenDecimals);
140224

141225
const expectedPayouts = annotators.map((address) => ({
142226
address,

packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/cvat-payouts-calculator.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,15 @@ export class CvatPayoutsCalculator implements EscrowPayoutsCalculator {
4646
throw new Error('Invalid annotation meta');
4747
}
4848

49-
const jobBountyValue = ethers.parseUnits(manifest.job_bounty, 18);
49+
const tokenAddress = await escrowClient.getTokenAddress(escrowAddress);
50+
const tokenDecimals = await this.web3Service.getTokenDecimals(
51+
chainId,
52+
tokenAddress,
53+
);
54+
const jobBountyValue = this.parseJobBounty(
55+
manifest.job_bounty,
56+
Number(tokenDecimals),
57+
);
5058
const workersBounties = new Map<string, bigint>();
5159

5260
for (const job of annotations.jobs) {
@@ -76,4 +84,21 @@ export class CvatPayoutsCalculator implements EscrowPayoutsCalculator {
7684
}),
7785
);
7886
}
87+
88+
private parseJobBounty(jobBounty: string, tokenDecimals: number): bigint {
89+
const parts = jobBounty.split('.');
90+
if (parts.length > 1) {
91+
const decimalsInBounty = parts[1].length;
92+
if (decimalsInBounty > tokenDecimals) {
93+
if (tokenDecimals === 0) {
94+
return ethers.parseUnits(parts[0], tokenDecimals);
95+
}
96+
return ethers.parseUnits(
97+
`${parts[0]}.${parts[1].slice(0, tokenDecimals)}`,
98+
tokenDecimals,
99+
);
100+
}
101+
}
102+
return ethers.parseUnits(jobBounty, tokenDecimals);
103+
}
79104
}

packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/fortune-payouts-calculator.spec.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1+
jest.mock('@human-protocol/sdk');
2+
13
import { faker } from '@faker-js/faker';
24
import { createMock } from '@golevelup/ts-jest';
5+
import { EscrowClient } from '@human-protocol/sdk';
36
import { Test } from '@nestjs/testing';
47
import { ethers } from 'ethers';
58
import _ from 'lodash';
69

710
import { StorageService } from '@/modules/storage';
11+
import { Web3Service } from '@/modules/web3';
812

913
import { generateFortuneManifest, generateFortuneSolution } from '../fixtures';
1014
import { FortunePayoutsCalculator } from './fortune-payouts-calculator';
1115

1216
const mockedStorageService = createMock<StorageService>();
17+
const mockedWeb3Service = createMock<Web3Service>();
18+
const mockedEscrowClient = jest.mocked(EscrowClient);
1319

1420
describe('FortunePayoutsCalculator', () => {
1521
let calculator: FortunePayoutsCalculator;
@@ -22,12 +28,23 @@ describe('FortunePayoutsCalculator', () => {
2228
provide: StorageService,
2329
useValue: mockedStorageService,
2430
},
31+
{
32+
provide: Web3Service,
33+
useValue: mockedWeb3Service,
34+
},
2535
],
2636
}).compile();
2737

2838
calculator = moduleRef.get<FortunePayoutsCalculator>(
2939
FortunePayoutsCalculator,
3040
);
41+
42+
const mockedGetTokenAddress = jest.fn().mockImplementation(async () => {
43+
return faker.finance.ethereumAddress();
44+
});
45+
mockedEscrowClient.build.mockResolvedValue({
46+
getTokenAddress: mockedGetTokenAddress,
47+
} as unknown as EscrowClient);
3148
});
3249

3350
describe('calculate', () => {
@@ -46,6 +63,9 @@ describe('FortunePayoutsCalculator', () => {
4663
const resultsUrl = faker.internet.url();
4764
const manifest = generateFortuneManifest();
4865

66+
const tokenDecimals = BigInt(faker.number.int({ min: 6, max: 18 }));
67+
mockedWeb3Service.getTokenDecimals.mockResolvedValueOnce(tokenDecimals);
68+
4969
const payouts = await calculator.calculate({
5070
chainId: faker.number.int(),
5171
escrowAddress: faker.finance.ethereumAddress(),
@@ -56,8 +76,9 @@ describe('FortunePayoutsCalculator', () => {
5676
const expectedPayouts = validSolutions.map((s) => ({
5777
address: s.workerAddress,
5878
amount:
59-
BigInt(ethers.parseUnits(manifest.fundAmount.toString(), 'ether')) /
60-
BigInt(validSolutions.length),
79+
BigInt(
80+
ethers.parseUnits(manifest.fundAmount.toString(), tokenDecimals),
81+
) / BigInt(validSolutions.length),
6182
}));
6283

6384
expect(_.sortBy(payouts, 'address')).toEqual(

packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/fortune-payouts-calculator.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { EscrowClient } from '@human-protocol/sdk';
12
import { Injectable } from '@nestjs/common';
23
import { ethers } from 'ethers';
34
import type { OverrideProperties } from 'type-fest';
45

56
import { FortuneFinalResult, FortuneManifest } from '@/common/types';
67
import { StorageService } from '@/modules/storage';
8+
import { Web3Service } from '@/modules/web3';
79

810
import {
911
CalclulatePayoutsInput,
@@ -18,10 +20,15 @@ type CalculateFortunePayoutsInput = OverrideProperties<
1820

1921
@Injectable()
2022
export class FortunePayoutsCalculator implements EscrowPayoutsCalculator {
21-
constructor(private readonly storageService: StorageService) {}
23+
constructor(
24+
private readonly storageService: StorageService,
25+
private readonly web3Service: Web3Service,
26+
) {}
2227

2328
async calculate({
2429
manifest,
30+
chainId,
31+
escrowAddress,
2532
finalResultsUrl,
2633
}: CalculateFortunePayoutsInput): Promise<CalculatedPayout[]> {
2734
const finalResults =
@@ -33,8 +40,15 @@ export class FortunePayoutsCalculator implements EscrowPayoutsCalculator {
3340
.filter((result) => !result.error)
3441
.map((item) => item.workerAddress);
3542

43+
const signer = this.web3Service.getSigner(chainId);
44+
const escrowClient = await EscrowClient.build(signer);
45+
const tokenAddress = await escrowClient.getTokenAddress(escrowAddress);
46+
const tokenDecimals = await this.web3Service.getTokenDecimals(
47+
chainId,
48+
tokenAddress,
49+
);
3650
const payoutAmount =
37-
ethers.parseUnits(manifest.fundAmount.toString(), 18) /
51+
ethers.parseUnits(manifest.fundAmount.toString(), tokenDecimals) /
3852
BigInt(recipients.length);
3953

4054
return recipients.map((recipient) => ({

0 commit comments

Comments
 (0)