diff --git a/packages/frontend/src/features/core/hooks/useEpoch/useEpoch.test.ts b/packages/frontend/src/features/core/hooks/useEpoch/useEpoch.test.ts index bdecd6460..bf7e8ac9b 100644 --- a/packages/frontend/src/features/core/hooks/useEpoch/useEpoch.test.ts +++ b/packages/frontend/src/features/core/hooks/useEpoch/useEpoch.test.ts @@ -1,7 +1,8 @@ -import { renderHook, waitFor } from '@testing-library/react' import { wrapper } from '@/utils/test-helpers/wrapper' +import { renderHook, waitFor } from '@testing-library/react' import { useEpoch } from './useEpoch' +// Mock useUserState jest.mock('@/features/core/hooks/useUserState/useUserState', () => ({ useUserState: () => ({ userState: { @@ -13,10 +14,22 @@ jest.mock('@/features/core/hooks/useUserState/useUserState', () => ({ }), })) +// Mock useRelayConfig +jest.mock('@/features/core/hooks/useRelayConfig/useRelayConfig', () => ({ + useRelayConfig: () => ({ + data: { + EPOCH_LENGTH: 300, // 300 seconds + }, + isPending: false, + isSuccess: true, + }), +})) + describe('useEpoch', () => { it('should get epoch time information', async () => { const { result } = renderHook(useEpoch, { wrapper }) + await waitFor(() => expect(result.current.isPending).toBe(false)) await waitFor(() => expect(result.current.currentEpoch).toBe(2)) await waitFor(() => expect(result.current.remainingTime).toBe(120000)) await waitFor(() => expect(result.current.epochLength).toBe(300000)) diff --git a/packages/frontend/src/features/core/hooks/useEpoch/useEpoch.ts b/packages/frontend/src/features/core/hooks/useEpoch/useEpoch.ts index 35931afe9..ebeb36de4 100644 --- a/packages/frontend/src/features/core/hooks/useEpoch/useEpoch.ts +++ b/packages/frontend/src/features/core/hooks/useEpoch/useEpoch.ts @@ -1,14 +1,18 @@ import { QueryKeys } from '@/constants/queryKeys' -import { useUserState } from '@/features/core' +import { useRelayConfig, useUserState } from '@/features/core' import { useQuery } from '@tanstack/react-query' import isNull from 'lodash/isNull' import isUndefined from 'lodash/isUndefined' import { useEffect, useMemo } from 'react' -const epochLength = 300000 // 300000 ms - export function useEpoch() { const { isPending: isUserStatePending, userState } = useUserState() + const { data: config, isPending: isConfigPending } = useRelayConfig() + + // Convert EPOCH_LENGTH from seconds to milliseconds + const epochLength = useMemo(() => { + return config?.EPOCH_LENGTH ? config.EPOCH_LENGTH * 1000 : undefined + }, [config]) const { isPending: isCurrentEpochPending, @@ -23,6 +27,7 @@ export function useEpoch() { return userState.sync.calcCurrentEpoch() }, staleTime: epochLength, + enabled: !!epochLength, }) const { @@ -39,21 +44,27 @@ export function useEpoch() { return time * 1000 }, staleTime: epochLength, + enabled: !!epochLength, }) const isPending = - isUserStatePending || isCurrentEpochPending || isRemainingTimePending + isUserStatePending || + isCurrentEpochPending || + isRemainingTimePending || + isConfigPending || + !epochLength const epochStartTime = useMemo(() => { if ( isUndefined(currentEpoch) || isNull(currentEpoch) || - !remainingTime + !remainingTime || + !epochLength ) { return 0 } return Date.now() - (epochLength - remainingTime) - }, [currentEpoch, remainingTime]) + }, [currentEpoch, remainingTime, epochLength]) const epochEndTime = useMemo(() => { if ( @@ -70,7 +81,8 @@ export function useEpoch() { if ( isUndefined(currentEpoch) || isNull(currentEpoch) || - !remainingTime + !remainingTime || + !epochLength ) { return } @@ -85,7 +97,13 @@ export function useEpoch() { return () => { clearTimeout(timer) } - }, [currentEpoch, refetchCurrentEpoch, refetchRemainingTime, remainingTime]) + }, [ + currentEpoch, + refetchCurrentEpoch, + refetchRemainingTime, + remainingTime, + epochLength, + ]) return { isPending, diff --git a/packages/frontend/src/features/post/hooks/useCreatePost/useCreatePost.test.ts b/packages/frontend/src/features/post/hooks/useCreatePost/useCreatePost.test.ts index b1a325337..b5466dd78 100644 --- a/packages/frontend/src/features/post/hooks/useCreatePost/useCreatePost.test.ts +++ b/packages/frontend/src/features/post/hooks/useCreatePost/useCreatePost.test.ts @@ -5,6 +5,16 @@ import { act, renderHook } from '@testing-library/react' import nock from 'nock' import { useCreatePost } from './useCreatePost' +jest.mock('@/features/core/hooks/useRelayConfig/useRelayConfig', () => ({ + useRelayConfig: () => ({ + data: { + EPOCH_LENGTH: 300, + }, + isPending: false, + isSuccess: true, + }), +})) + jest.mock('@/features/core/hooks/useWeb3Provider/useWeb3Provider', () => ({ useWeb3Provider: () => ({ getGuaranteedProvider: () => ({ diff --git a/packages/frontend/src/features/post/hooks/useVotes/useVotes.test.ts b/packages/frontend/src/features/post/hooks/useVotes/useVotes.test.ts index 84a6a5738..4d9e069eb 100644 --- a/packages/frontend/src/features/post/hooks/useVotes/useVotes.test.ts +++ b/packages/frontend/src/features/post/hooks/useVotes/useVotes.test.ts @@ -5,6 +5,16 @@ import { act, renderHook } from '@testing-library/react' import nock from 'nock' import { useVotes } from './useVotes' +jest.mock('@/features/core/hooks/useRelayConfig/useRelayConfig', () => ({ + useRelayConfig: () => ({ + data: { + EPOCH_LENGTH: 300, + }, + isPending: false, + isSuccess: true, + }), +})) + jest.mock('@/features/core/hooks/useUserState/useUserState', () => ({ useUserState: () => ({ userState: { diff --git a/packages/frontend/src/features/shared/hooks/useDatePicker.ts b/packages/frontend/src/features/shared/hooks/useDatePicker.ts index 37c55a84e..e6fb23cb8 100644 --- a/packages/frontend/src/features/shared/hooks/useDatePicker.ts +++ b/packages/frontend/src/features/shared/hooks/useDatePicker.ts @@ -1,6 +1,6 @@ -import { useUserState } from '@/features/core' +import { useRelayConfig, useUserState } from '@/features/core' import dayjs from 'dayjs' -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { EpochDateService, FromToEpoch, @@ -9,14 +9,20 @@ import { export function useDatePicker() { const { userState } = useUserState() + const { data: config } = useRelayConfig() const [startDate, setStartDate] = useState(undefined) const [endDate, setEndDate] = useState(undefined) const [fromToEpoch, setFromToEpoch] = useState( new InvalidFromToEpoch(), ) + // Calculate epochLength in milliseconds + const epochLength = useMemo(() => { + return config?.EPOCH_LENGTH ? config.EPOCH_LENGTH * 1000 : undefined + }, [config]) + const updateFromToEpoch = useCallback(async () => { - if (!userState) { + if (!userState || !epochLength) { setFromToEpoch(new InvalidFromToEpoch()) return } @@ -25,9 +31,10 @@ export function useDatePicker() { startDate, endDate, userState.sync, + epochLength, ), ) - }, [startDate, endDate, userState]) + }, [startDate, endDate, userState, epochLength]) const onChange = (dates: [Date | null, Date | null]) => { const [start, end] = dates diff --git a/packages/frontend/src/features/shared/services/EpochDateService.test.ts b/packages/frontend/src/features/shared/services/EpochDateService.test.ts index d386a733c..252eb3ae0 100644 --- a/packages/frontend/src/features/shared/services/EpochDateService.test.ts +++ b/packages/frontend/src/features/shared/services/EpochDateService.test.ts @@ -15,6 +15,7 @@ describe('EpochDateService', () => { it('should execute properly', () => { const synchronizer = new MockSynchronizer() as unknown as Synchronizer + const epochLength = 300000 // 5 minutes in milliseconds // service start time: 1720656000000 = 1721088000000 - 0 - 300000 * 1440 // service start time: 2024-07-11T00:00:00.000Z @@ -23,25 +24,45 @@ describe('EpochDateService', () => { // at service start & now const date0 = new Date('2024-07-11T00:00:00Z') expect( - EpochDateService.calcEpochByDate(mockNow, date0, synchronizer), + EpochDateService.calcEpochByDate( + mockNow, + date0, + synchronizer, + epochLength, + ), ).toBe(0) // between service start & now const date1 = new Date('2024-07-11T00:15:00Z') expect( - EpochDateService.calcEpochByDate(mockNow, date1, synchronizer), + EpochDateService.calcEpochByDate( + mockNow, + date1, + synchronizer, + epochLength, + ), ).toBe(3) // 724 * 300000 + 1720655850000 // before service start const date2 = new Date('2024-07-10T23:59:00Z') expect( - EpochDateService.calcEpochByDate(mockNow, date2, synchronizer), + EpochDateService.calcEpochByDate( + mockNow, + date2, + synchronizer, + epochLength, + ), ).toBe(0) // future const date3 = new Date('2024-07-17T00:00:00Z') expect( - EpochDateService.calcEpochByDate(mockNow, date3, synchronizer), + EpochDateService.calcEpochByDate( + mockNow, + date3, + synchronizer, + epochLength, + ), ).toBe(1440 + 12 * 24) }) }) diff --git a/packages/frontend/src/features/shared/services/EpochDateService.ts b/packages/frontend/src/features/shared/services/EpochDateService.ts index 7ca377719..2f4b4fe9c 100644 --- a/packages/frontend/src/features/shared/services/EpochDateService.ts +++ b/packages/frontend/src/features/shared/services/EpochDateService.ts @@ -9,21 +9,32 @@ export class EpochDateService { start: Date | undefined, end: Date | undefined, synchoronizer: Synchronizer, + epochLength: number, ) { if (!!start && !!end && EpochDateService.isValidDateRange(start, end)) { const [from, to] = EpochDateService.calcEpochsByDates( [start, end], synchoronizer, + epochLength, ) return new ValidFromToEpoch(from, to) } return new InvalidFromToEpoch() } - static calcEpochsByDates(dates: Date[], synchoronizer: Synchronizer) { + static calcEpochsByDates( + dates: Date[], + synchoronizer: Synchronizer, + epochLength: number, + ) { const now = Date.now() return dates.map((date) => - EpochDateService.calcEpochByDate(now, date, synchoronizer), + EpochDateService.calcEpochByDate( + now, + date, + synchoronizer, + epochLength, + ), ) } @@ -31,8 +42,8 @@ export class EpochDateService { now: number, date: Date, synchoronizer: Synchronizer, + epochLength: number, ) { - const epochLength = 300000 const currentEpoch = synchoronizer.calcCurrentEpoch() const remainingTime = synchoronizer.calcEpochRemainingTime() * 1000 const currentEpochStartTime = now - (epochLength - remainingTime) diff --git a/packages/frontend/src/types/api.ts b/packages/frontend/src/types/api.ts index 4b2311dcf..a332b5ce7 100644 --- a/packages/frontend/src/types/api.ts +++ b/packages/frontend/src/types/api.ts @@ -17,6 +17,7 @@ export interface FetchRelayConfigResponse { UNIREP_ADDRESS: string APP_ADDRESS: string ETH_PROVIDER_URL: string + EPOCH_LENGTH: number } export type FetchPostsResponse = RelayRawPost[] diff --git a/packages/frontend/src/utils/test-helpers/buildMockAPIs.ts b/packages/frontend/src/utils/test-helpers/buildMockAPIs.ts index ee620fb5c..4e13181f1 100644 --- a/packages/frontend/src/utils/test-helpers/buildMockAPIs.ts +++ b/packages/frontend/src/utils/test-helpers/buildMockAPIs.ts @@ -6,6 +6,7 @@ export function buildMockConfigAPI() { UNIREP_ADDRESS: '0x83cB6AF63eAfEc7998cC601eC3f56d064892b386', APP_ADDRESS: '0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1', ETH_PROVIDER_URL: 'http://127.0.0.1:8545', + EPOCH_LENGTH: 300, } const expectation = nock(SERVER).get('/api/config').reply(200, response) diff --git a/packages/relay/src/routes/appConfigRoute.ts b/packages/relay/src/routes/appConfigRoute.ts index 02b1929ab..bb0781ed2 100644 --- a/packages/relay/src/routes/appConfigRoute.ts +++ b/packages/relay/src/routes/appConfigRoute.ts @@ -1,12 +1,24 @@ +import { DB } from 'anondb/node' import { Express } from 'express' -import { UNIREP_ADDRESS, APP_ADDRESS, ETH_PROVIDER_URL } from '../config' +import { APP_ADDRESS, ETH_PROVIDER_URL, UNIREP_ADDRESS } from '../config' +import { UnirepSocialSynchronizer } from '../services/singletons/UnirepSocialSynchronizer' + +export default ( + app: Express, + _: DB, + synchronizer: UnirepSocialSynchronizer +) => { + app.get('/api/config', async (_, res) => { + const epochLength = + await synchronizer.unirepContract.attesterEpochLength( + BigInt(APP_ADDRESS).toString() + ) -export default (app: Express) => { - app.get('/api/config', (_, res) => res.json({ UNIREP_ADDRESS, APP_ADDRESS, ETH_PROVIDER_URL, + EPOCH_LENGTH: epochLength, }) - ) + }) } diff --git a/packages/relay/test/config.test.ts b/packages/relay/test/config.test.ts new file mode 100644 index 000000000..8bb2c9df8 --- /dev/null +++ b/packages/relay/test/config.test.ts @@ -0,0 +1,77 @@ +import { Unirep, UnirepApp } from '@unirep-app/contracts/typechain-types' +import { DB } from 'anondb' +import { expect } from 'chai' +import { ethers } from 'hardhat' +import { ETH_PROVIDER_URL } from '../src/config' +import { UnirepSocialSynchronizer } from '../src/services/singletons/UnirepSocialSynchronizer' +import { deployContracts, startServer, stopServer } from './environment' + +describe('GET /api/config', function () { + let snapshot: any + let express: ChaiHttp.Agent + let unirep: Unirep + let app: UnirepApp + let db: DB + let prover: any + let provider: any + let sync: UnirepSocialSynchronizer + + before(async function () { + snapshot = await ethers.provider.send('evm_snapshot', []) + // deploy contracts + const { unirep: _unirep, app: _app } = await deployContracts(100000) + // start server + const { + db: _db, + prover: _prover, + provider: _provider, + synchronizer, + chaiServer, + } = await startServer(_unirep, _app) + express = chaiServer + unirep = _unirep + db = _db + app = _app + prover = _prover + provider = _provider + sync = synchronizer + }) + + after(async function () { + await stopServer('config', snapshot, sync, express) + }) + + it('should return the correct configuration', async function () { + const res = await express + .get('/api/config') + .set('content-type', 'application/json') + + expect(res).to.have.status(200) + expect(res.body).to.have.property('UNIREP_ADDRESS') + expect(res.body).to.have.property('APP_ADDRESS') + expect(res.body).to.have.property('ETH_PROVIDER_URL') + expect(res.body).to.have.property('EPOCH_LENGTH') + + expect(res.body.UNIREP_ADDRESS).to.equal(unirep.address) + expect(res.body.APP_ADDRESS).to.equal(app.address) + expect(res.body.ETH_PROVIDER_URL).to.equal(ETH_PROVIDER_URL) + + const expectedEpochLength = + await sync.unirepContract.attesterEpochLength( + BigInt(app.address).toString() + ) + expect(res.body.EPOCH_LENGTH).to.equal(expectedEpochLength) + }) + + it('should return the correct data types', async function () { + const res = await express + .get('/api/config') + .set('content-type', 'application/json') + + expect(res).to.have.status(200) + expect(res.body.UNIREP_ADDRESS).to.be.a('string') + expect(res.body.APP_ADDRESS).to.be.a('string') + expect(res.body.ETH_PROVIDER_URL).to.be.a('string') + expect(res.body.EPOCH_LENGTH).to.be.a('number') + }) +})