`${element}${profileImageSmall}`)}
/>
-
{name}
+
+ {name}
+
= ({
= ({
isDonationAboveThreshold &&
styles.isDonationAboveThreshold,
)}
+ data-test="AllocationItemRewards"
onClick={onClick}
onMouseLeave={() => setIsSimulateVisible(false)}
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
diff --git a/client/src/components/ui/InputText/InputText.tsx b/client/src/components/ui/InputText/InputText.tsx
index 1a41abf43f..838e2b88e5 100644
--- a/client/src/components/ui/InputText/InputText.tsx
+++ b/client/src/components/ui/InputText/InputText.tsx
@@ -98,6 +98,7 @@ const InputText = forwardRef(
!!error && styles.isError,
suffixClassName,
)}
+ data-test={`${dataTest}__suffix`}
>
{suffix}
diff --git a/client/src/gql/gql.ts b/client/src/gql/gql.ts
index bc1bef80a6..02d68ced80 100644
--- a/client/src/gql/gql.ts
+++ b/client/src/gql/gql.ts
@@ -19,6 +19,7 @@ const documents = {
types.GetBlockNumberDocument,
'\n query GetEpochTimestampHappenedIn($timestamp: BigInt) {\n epoches(where: { fromTs_lte: $timestamp, toTs_gte: $timestamp }) {\n epoch\n }\n }\n':
types.GetEpochTimestampHappenedInDocument,
+ '\n query GetEpoches {\n epoches {\n epoch\n }\n }\n': types.GetEpochesDocument,
'\n query GetEpochsStartEndTime($lastEpoch: Int) {\n epoches(first: $lastEpoch) {\n epoch\n toTs\n fromTs\n decisionWindow\n }\n }\n':
types.GetEpochsStartEndTimeDocument,
'\n query GetLargestLockedAmount {\n lockeds(orderBy: amount, orderDirection: desc, first: 1) {\n amount\n }\n }\n':
@@ -67,6 +68,12 @@ export function graphql(
export function graphql(
source: '\n query GetEpochTimestampHappenedIn($timestamp: BigInt) {\n epoches(where: { fromTs_lte: $timestamp, toTs_gte: $timestamp }) {\n epoch\n }\n }\n',
): (typeof documents)['\n query GetEpochTimestampHappenedIn($timestamp: BigInt) {\n epoches(where: { fromTs_lte: $timestamp, toTs_gte: $timestamp }) {\n epoch\n }\n }\n'];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(
+ source: '\n query GetEpoches {\n epoches {\n epoch\n }\n }\n',
+): (typeof documents)['\n query GetEpoches {\n epoches {\n epoch\n }\n }\n'];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts
index 9ba8409ef6..da404e0813 100644
--- a/client/src/gql/graphql.ts
+++ b/client/src/gql/graphql.ts
@@ -1160,6 +1160,13 @@ export type GetEpochTimestampHappenedInQuery = {
epoches: Array<{ __typename?: 'Epoch'; epoch: number }>;
};
+export type GetEpochesQueryVariables = Exact<{ [key: string]: never }>;
+
+export type GetEpochesQuery = {
+ __typename?: 'Query';
+ epoches: Array<{ __typename?: 'Epoch'; epoch: number }>;
+};
+
export type GetEpochsStartEndTimeQueryVariables = Exact<{
lastEpoch?: InputMaybe
;
}>;
@@ -1350,6 +1357,29 @@ export const GetEpochTimestampHappenedInDocument = {
GetEpochTimestampHappenedInQuery,
GetEpochTimestampHappenedInQueryVariables
>;
+export const GetEpochesDocument = {
+ kind: 'Document',
+ definitions: [
+ {
+ kind: 'OperationDefinition',
+ operation: 'query',
+ name: { kind: 'Name', value: 'GetEpoches' },
+ selectionSet: {
+ kind: 'SelectionSet',
+ selections: [
+ {
+ kind: 'Field',
+ name: { kind: 'Name', value: 'epoches' },
+ selectionSet: {
+ kind: 'SelectionSet',
+ selections: [{ kind: 'Field', name: { kind: 'Name', value: 'epoch' } }],
+ },
+ },
+ ],
+ },
+ },
+ ],
+} as unknown as DocumentNode;
export const GetEpochsStartEndTimeDocument = {
kind: 'Document',
definitions: [
diff --git a/client/src/hooks/helpers/useCypressHelpers.ts b/client/src/hooks/helpers/useCypressHelpers.ts
index 7c5baf95d9..5a5533b063 100644
--- a/client/src/hooks/helpers/useCypressHelpers.ts
+++ b/client/src/hooks/helpers/useCypressHelpers.ts
@@ -1,9 +1,38 @@
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
-import useCypressMoveEpoch from 'hooks/mutations/useCypressMoveEpoch';
+import env from 'env';
+import useCypressMakeSnapshot from 'hooks/mutations/useCypressMakeSnapshot';
+import useCypressMoveToDecisionWindowClosed from 'hooks/mutations/useCypressMoveToDecisionWindowClosed';
+import useCypressMoveToDecisionWindowOpen from 'hooks/mutations/useCypressMoveToDecisionWindowOpen';
+import useCurrentEpoch from 'hooks/queries/useCurrentEpoch';
+import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen';
+import useEpochsIndexedBySubgraph from 'hooks/subgraph/useEpochsIndexedBySubgraph';
-export default function useCypressHelpers(): void {
- const { mutateAsync: mutateAsyncMoveEpoch } = useCypressMoveEpoch();
+export default function useCypressHelpers(): { isFetching: boolean } {
+ const [isRefetchingEpochs, setIsRefetchingEpochs] = useState(false);
+
+ const isHookEnabled = !!window.Cypress || env.network === 'Local';
+
+ const {
+ mutateAsync: mutateAsyncMoveToDecisionWindowOpen,
+ isPending: isPendingMoveToDecisionWindowOpen,
+ } = useCypressMoveToDecisionWindowOpen();
+ const {
+ mutateAsync: mutateAsyncMoveToDecisionWindowClosed,
+ isPending: isPendingMoveToDecisionWindowClosed,
+ } = useCypressMoveToDecisionWindowClosed();
+ const { mutateAsync: mutateAsyncMakeSnapshot, isPending: isPendingMakeSnapshot } =
+ useCypressMakeSnapshot();
+ useIsDecisionWindowOpen({ refetchInterval: isHookEnabled ? 1000 : false });
+ const { data: currentEpoch } = useCurrentEpoch({ refetchInterval: isHookEnabled ? 1000 : false });
+ const { data: epochs } = useEpochsIndexedBySubgraph(isHookEnabled && isRefetchingEpochs);
+
+ const isEpochAlreadyIndexedBySubgraph =
+ epochs !== undefined && currentEpoch !== undefined && epochs.includes(currentEpoch);
+
+ useEffect(() => {
+ setIsRefetchingEpochs(!isEpochAlreadyIndexedBySubgraph);
+ }, [isEpochAlreadyIndexedBySubgraph]);
useEffect(() => {
/**
@@ -18,10 +47,20 @@ export default function useCypressHelpers(): void {
*
* (1) History of commits here: https://github.com/golemfoundation/octant/pull/13.
*/
- if (window.Cypress) {
- // @ts-expect-error Left for debug purposes.
- window.mutateAsyncMoveEpoch = mutateAsyncMoveEpoch;
+ if (isHookEnabled) {
+ window.mutateAsyncMoveToDecisionWindowOpen = mutateAsyncMoveToDecisionWindowOpen;
+ window.mutateAsyncMoveToDecisionWindowClosed = mutateAsyncMoveToDecisionWindowClosed;
+ window.mutateAsyncMakeSnapshot = mutateAsyncMakeSnapshot;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+
+ return {
+ isFetching:
+ isHookEnabled &&
+ (!isEpochAlreadyIndexedBySubgraph ||
+ isPendingMoveToDecisionWindowOpen ||
+ isPendingMoveToDecisionWindowClosed ||
+ isPendingMakeSnapshot),
+ };
}
diff --git a/client/src/hooks/mutations/useCypressMakeSnapshot.ts b/client/src/hooks/mutations/useCypressMakeSnapshot.ts
new file mode 100644
index 0000000000..d910c808a2
--- /dev/null
+++ b/client/src/hooks/mutations/useCypressMakeSnapshot.ts
@@ -0,0 +1,11 @@
+import { useMutation, UseMutationResult } from '@tanstack/react-query';
+
+import { apiPostSnapshotsPending, apiPostSnapshotsFinalized } from 'api/calls/snapshots';
+
+export default function useCypressMakeSnapshot(): UseMutationResult {
+ return useMutation({
+ mutationFn: (type: 'pending' | 'finalized') => type === 'pending'
+ ? apiPostSnapshotsPending()
+ : apiPostSnapshotsFinalized(),
+ })
+}
diff --git a/client/src/hooks/mutations/useCypressMoveToDecisionWindowClosed.ts b/client/src/hooks/mutations/useCypressMoveToDecisionWindowClosed.ts
new file mode 100644
index 0000000000..525a2a4b91
--- /dev/null
+++ b/client/src/hooks/mutations/useCypressMoveToDecisionWindowClosed.ts
@@ -0,0 +1,89 @@
+import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query';
+import { useConfig } from 'wagmi';
+
+import { QUERY_KEYS } from 'api/queryKeys';
+import { readContractEpochs } from 'hooks/contracts/readContracts';
+
+export default function useCypressMoveToDecisionWindowClosed(): UseMutationResult {
+ const queryClient = useQueryClient();
+ const wagmiConfig = useConfig();
+
+ return useMutation({
+ mutationFn: () => {
+ // eslint-disable-next-line no-async-promise-executor
+ return new Promise(async (resolve, reject) => {
+ if (!window.Cypress) {
+ reject(new Error('useCypressMoveToDecisionWindowOpen was called outside Cypress.'));
+ }
+
+ const currentEpochPromise = queryClient.fetchQuery({
+ queryFn: () =>
+ readContractEpochs({
+ functionName: 'getCurrentEpoch',
+ publicClient: wagmiConfig.publicClient,
+ }),
+ queryKey: QUERY_KEYS.currentEpoch,
+ });
+
+ const blockPromise = wagmiConfig.publicClient.getBlock();
+
+ const currentEpochEndPromise = queryClient.fetchQuery({
+ queryFn: () =>
+ readContractEpochs({
+ functionName: 'getCurrentEpochEnd',
+ publicClient: wagmiConfig.publicClient,
+ }),
+ queryKey: QUERY_KEYS.currentEpochEnd,
+ });
+
+ const currentEpochPropsPromise = queryClient.fetchQuery({
+ queryFn: () =>
+ readContractEpochs({
+ functionName: 'getCurrentEpochProps',
+ publicClient: wagmiConfig.publicClient,
+ }),
+ queryKey: QUERY_KEYS.currentEpochProps,
+ });
+
+ const [block, currentEpochEnd, currentEpoch, currentEpochProps] = await Promise.all([
+ blockPromise,
+ currentEpochEndPromise,
+ currentEpochPromise,
+ currentEpochPropsPromise,
+ ]);
+
+ if (
+ [block, currentEpoch, currentEpochEnd, currentEpochProps].some(
+ element => element === undefined,
+ )
+ ) {
+ // eslint-disable-next-line prefer-promise-reject-errors
+ reject(
+ new Error(
+ 'useCypressMoveEpoch fetched undefined block or currentEpoch or currentEpochEnd or currentEpochProps.',
+ ),
+ );
+ }
+
+ const timeToIncrease = Number(currentEpochProps.decisionWindow) + 10; // [s]
+ await wagmiConfig.publicClient.request({
+ method: 'evm_increaseTime' as any,
+ params: [timeToIncrease] as any,
+ });
+ await wagmiConfig.publicClient.request({ method: 'evm_mine' as any, params: [] as any });
+
+ const isDecisionWindowOpenAfter = await queryClient.fetchQuery({
+ queryFn: () =>
+ readContractEpochs({
+ functionName: 'isDecisionWindowOpen',
+ publicClient: wagmiConfig.publicClient,
+ }),
+ queryKey: QUERY_KEYS.isDecisionWindowOpen,
+ });
+
+ // isEpochChanged
+ resolve(isDecisionWindowOpenAfter === false);
+ });
+ },
+ });
+}
diff --git a/client/src/hooks/mutations/useCypressMoveEpoch.ts b/client/src/hooks/mutations/useCypressMoveToDecisionWindowOpen.ts
similarity index 92%
rename from client/src/hooks/mutations/useCypressMoveEpoch.ts
rename to client/src/hooks/mutations/useCypressMoveToDecisionWindowOpen.ts
index d5b1ea9a81..83c1977475 100644
--- a/client/src/hooks/mutations/useCypressMoveEpoch.ts
+++ b/client/src/hooks/mutations/useCypressMoveToDecisionWindowOpen.ts
@@ -4,7 +4,7 @@ import { useConfig } from 'wagmi';
import { QUERY_KEYS } from 'api/queryKeys';
import { readContractEpochs } from 'hooks/contracts/readContracts';
-export default function useCypressMoveEpoch(): UseMutationResult {
+export default function useCypressMoveToDecisionWindowOpen(): UseMutationResult {
const queryClient = useQueryClient();
const wagmiConfig = useConfig();
@@ -13,7 +13,7 @@ export default function useCypressMoveEpoch(): UseMutationResult {
if (!window.Cypress) {
- reject(new Error('useCypressMoveEpoch was called outside Cypress.'));
+ reject(new Error('useCypressMoveToDecisionWindowOpen was called outside Cypress.'));
}
const currentEpochPromise = queryClient.fetchQuery({
diff --git a/client/src/hooks/subgraph/useEpochsIndexedBySubgraph.ts b/client/src/hooks/subgraph/useEpochsIndexedBySubgraph.ts
new file mode 100644
index 0000000000..4834b58fa4
--- /dev/null
+++ b/client/src/hooks/subgraph/useEpochsIndexedBySubgraph.ts
@@ -0,0 +1,27 @@
+import { useQuery, UseQueryResult } from '@tanstack/react-query';
+import request from 'graphql-request';
+
+import { QUERY_KEYS } from 'api/queryKeys';
+import env from 'env';
+import { graphql } from 'gql/gql';
+import { GetEpochesQuery } from 'gql/graphql';
+
+const GET_EPOCHS = graphql(`
+ query GetEpoches {
+ epoches {
+ epoch
+ }
+ }
+`);
+
+export default function useEpochsIndexedBySubgraph(isEnabled?: boolean): UseQueryResult {
+ const { subgraphAddress } = env;
+
+ return useQuery({
+ enabled: isEnabled,
+ queryFn: async () => request(subgraphAddress, GET_EPOCHS),
+ queryKey: QUERY_KEYS.epochsIndexedBySubgraph,
+ refetchInterval: isEnabled ? 2000 : false,
+ select: data => data.epoches.map(({ epoch }) => epoch),
+ });
+}
diff --git a/client/src/routes/RootRoutes/RootRoutes.tsx b/client/src/routes/RootRoutes/RootRoutes.tsx
index bc46797f50..83689c19b9 100644
--- a/client/src/routes/RootRoutes/RootRoutes.tsx
+++ b/client/src/routes/RootRoutes/RootRoutes.tsx
@@ -1,6 +1,7 @@
import React, { Fragment, FC } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
+import env from 'env';
import useIsProjectAdminMode from 'hooks/helpers/useIsProjectAdminMode';
import useCurrentEpoch from 'hooks/queries/useCurrentEpoch';
import useIsPatronMode from 'hooks/queries/useIsPatronMode';
@@ -8,6 +9,7 @@ import getIsPreLaunch from 'utils/getIsPreLaunch';
import AllocationView from 'views/AllocationView/AllocationView';
import EarnView from 'views/EarnView/EarnView';
import MetricsView from 'views/MetricsView/MetricsView';
+import PlaygroundView from 'views/PlaygroundView/PlaygroundView';
import ProjectsView from 'views/ProjectsView/ProjectsView';
import ProjectView from 'views/ProjectView/ProjectView';
import SettingsView from 'views/SettingsView/SettingsView';
@@ -85,6 +87,16 @@ const RootRoutes: FC = props => {
}
path={`${ROOT_ROUTES.earn.relative}/*`}
/>
+ {(window.Cypress || env.network === 'Local') && (
+
+
+
+ }
+ path={`${ROOT_ROUTES.playground.relative}/*`}
+ />
+ )}
Playground
;
+
+export default PlaygroundView;