diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e36dd06cd2..c1fd2fd5dc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -248,7 +248,7 @@ Documentation: parallel: matrix: - SERVICE: - - contracts-v1 + - contracts-v1 rules: - !reference [.rules, on_mr ] - !reference [.rules, on_push_to_default_branch ] @@ -354,21 +354,21 @@ Build images: - export LOCAL_RPC_URL=https://$(bash $CI_PROJECT_DIR/ci/argocd/get_rpc_url.sh) - /app/entrypoint.sh $NETWORK $CI_PROJECT_DIR/build.env -#Run E2E App: -# extends: -# - .deploy_anvil -# rules: -# - !reference [.rules, on_mr ] -# - !reference [.rules, on_push_to_default_branch ] -# - !reference [.rules, on_push_to_master_branch ] -# variables: -# ENV_TYPE: "e2e" -# environment: -# name: e2e/$CI_PIPELINE_IID -# url: https://mr-$CI_MERGE_REQUEST_IID-e2e-$CI_PIPELINE_ID-client.octant.wildland.dev -# deployment_tier: development -# on_stop: Destroy E2E App -# auto_stop_in: 6 hours +Run E2E App: + extends: + - .deploy_anvil + rules: + - !reference [.rules, on_mr ] + - !reference [.rules, on_push_to_default_branch ] + - !reference [.rules, on_push_to_master_branch ] + variables: + ENV_TYPE: "e2e" + environment: + name: e2e/$CI_PIPELINE_IID + url: https://mr-$CI_MERGE_REQUEST_IID-e2e-$CI_PIPELINE_ID-client.octant.wildland.dev + deployment_tier: development + on_stop: Destroy E2E App + auto_stop_in: 6 hours Run MR App: extends: @@ -383,16 +383,16 @@ Run MR App: deployment_tier: development on_stop: Destroy MR App -#E2E contracts: -# extends: -# - .deploy_anvil_contracts -# needs: ["Run E2E App"] -# rules: -# - !reference [.rules, on_mr ] -# - !reference [.rules, on_push_to_default_branch ] -# - !reference [.rules, on_push_to_master_branch ] -# variables: -# ENV_TYPE: "e2e" +E2E contracts: + extends: + - .deploy_anvil_contracts + needs: ["Run E2E App"] + rules: + - !reference [.rules, on_mr ] + - !reference [.rules, on_push_to_default_branch ] + - !reference [.rules, on_push_to_master_branch ] + variables: + ENV_TYPE: "e2e" MR contracts: extends: @@ -403,23 +403,23 @@ MR contracts: variables: ENV_TYPE: "mr" -#E2E app deploy: -# extends: -# - .deploy_app -# needs: ["E2E contracts"] -# dependencies: ["E2E contracts"] -# rules: -# - !reference [.rules, on_mr ] -# - !reference [.rules, on_push_to_default_branch ] -# - !reference [.rules, on_push_to_master_branch ] -# variables: -# ENV_TYPE: "e2e" -# NETWORK_NAME: "local" -# NETWORK_ID: "1337" -# SNAPSHOTTER_ENABLED: "true" -# SCHEDULER_ENABLED: "true" -# GLM_CLAIM_ENABLED: "true" -# VAULT_CONFIRM_WITHDRAWALS_ENABLED: "true" +E2E app deploy: + extends: + - .deploy_app + needs: ["E2E contracts"] + dependencies: ["E2E contracts"] + rules: + - !reference [.rules, on_mr ] + - !reference [.rules, on_push_to_default_branch ] + - !reference [.rules, on_push_to_master_branch ] + variables: + ENV_TYPE: "e2e" + NETWORK_NAME: "local" + NETWORK_ID: "1337" + SNAPSHOTTER_ENABLED: "true" + SCHEDULER_ENABLED: "true" + GLM_CLAIM_ENABLED: "true" + VAULT_CONFIRM_WITHDRAWALS_ENABLED: "true" MR app deploy: extends: @@ -525,19 +525,19 @@ Wait for Master: environment: action: stop -#Destroy E2E App: -# extends: -# - .destroy_app -# needs: ["Run E2E App"] -# variables: -# ENV_TYPE: "e2e" -# rules: -# - !reference [.rules, on_mr_manual ] -# - !reference [.rules, on_push_to_default_branch_manual ] -# - !reference [.rules, on_push_to_master_branch_manual ] -# environment: -# name: e2e/$CI_PIPELINE_IID -# deployment_tier: development +Destroy E2E App: + extends: + - .destroy_app + needs: ["Run E2E App"] + variables: + ENV_TYPE: "e2e" + rules: + - !reference [.rules, on_mr_manual ] + - !reference [.rules, on_push_to_default_branch_manual ] + - !reference [.rules, on_push_to_master_branch_manual ] + environment: + name: e2e/$CI_PIPELINE_IID + deployment_tier: development Destroy MR App: extends: @@ -579,61 +579,61 @@ Destroy Master App: name: persistent/master deployment_tier: testing -#E2E Epoch 1: -# stage: application -# needs: ["E2E app deploy"] -# image: !reference [.images, synpress ] -# <<: *env_resolve_init -# rules: -# - !reference [.rules, on_mr] -# - !reference [.rules, on_push_to_default_branch ] -# - !reference [.rules, on_push_to_master_branch ] -# artifacts: -# when: on_failure -# name: cypress -# paths: -# - client/cypress/videos -# - client/cypress/screenshots -# expire_in: 3 days -# cache: -# - key: $CI_COMMIT_REF_SLUG-yarn-client -# policy: pull -# paths: -# - client/.yarn -# - client/node-modules -# - key: $CI_COMMIT_REF_SLUG-yarn-root -# policy: pull -# paths: -# - node_modules -# - .yarn -# script: -# - set -e -# # Setup NVM to use Node version 16 -# - source /usr/share/nvm/init-nvm.sh -# - nvm use 16 -# - npm i -g yarn -# - cd client -# - yarn install --cache-folder .yarn --frozen-lockfile --prefer-offline --no-audit -# # Wait for the E2E app to become ready -# - bash $CI_PROJECT_DIR/ci/argocd/wait_for_app.sh -# - export OCTANT_BASE_URL=https://$(bash $CI_PROJECT_DIR/ci/argocd/get_web_client_url.sh) -# - set +e -# - yarn synpress:run || CY_EXIT_CODE=$? -# - if [[ "$CY_EXIT_CODE" == "0" ]]; then rm -r $CI_PROJECT_DIR/client/cypress/videos $CI_PROJECT_DIR/client/cypress/screenshots; fi -# - set -e -# # Trigger the stop job -# - | -# JOB_ID=$(curl --fail -s -XGET --header "PRIVATE-TOKEN: $CI_JOB_CONTROLLER" https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/jobs | jq '.[] | select(.name == "Destroy E2E App") | .id') -# -# curl -s --fail -X POST \ -# -H "PRIVATE-TOKEN: $CI_JOB_CONTROLLER" \ -# "$CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/$JOB_ID/play" -# - exit $CY_EXIT_CODE -# variables: -# ENV_TYPE: "e2e" -# CYPRESS_DOCKER_RUN: "true" -# CI: "true" -# METAMASK_VERSION: "10.25.0" +E2E Epoch 1: + stage: application + needs: ["E2E app deploy"] + image: !reference [.images, synpress ] + <<: *env_resolve_init + rules: + - !reference [.rules, on_mr] + - !reference [.rules, on_push_to_default_branch ] + - !reference [.rules, on_push_to_master_branch ] + artifacts: + when: on_failure + name: cypress + paths: + - client/cypress/videos + - client/cypress/screenshots + expire_in: 3 days + cache: + - key: $CI_COMMIT_REF_SLUG-yarn-client + policy: pull + paths: + - client/.yarn + - client/node-modules + - key: $CI_COMMIT_REF_SLUG-yarn-root + policy: pull + paths: + - node_modules + - .yarn + script: + - set -e + # Setup NVM to use Node version 16 + - source /usr/share/nvm/init-nvm.sh + - nvm use 16 + - npm i -g yarn + - cd client + - yarn install --cache-folder .yarn --frozen-lockfile --prefer-offline --no-audit + # Wait for the E2E app to become ready + - bash $CI_PROJECT_DIR/ci/argocd/wait_for_app.sh + - export OCTANT_BASE_URL=https://$(bash $CI_PROJECT_DIR/ci/argocd/get_web_client_url.sh) + - set +e + - yarn synpress:run || CY_EXIT_CODE=$? + - if [[ "$CY_EXIT_CODE" == "0" ]]; then rm -r $CI_PROJECT_DIR/client/cypress/videos $CI_PROJECT_DIR/client/cypress/screenshots; fi + - set -e + # Trigger the stop job + - | + JOB_ID=$(curl --fail -s -XGET --header "PRIVATE-TOKEN: $CI_JOB_CONTROLLER" https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/jobs | jq '.[] | select(.name == "Destroy E2E App") | .id') + + curl -s --fail -X POST \ + -H "PRIVATE-TOKEN: $CI_JOB_CONTROLLER" \ + "$CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/$JOB_ID/play" + - exit $CY_EXIT_CODE + variables: + ENV_TYPE: "e2e" + CYPRESS_DOCKER_RUN: "true" + CI: "true" + METAMASK_VERSION: "10.25.0" Deploy Release Candidate app: stage: deploy @@ -657,8 +657,18 @@ Deploy Release Candidate app: cd $GIT_DIR - cat mainnet/octant-image.values.yaml | yq -r ".[0].value.value = \"$IMAGE_TAG\"" | tee mainnet/octant-image.values.yaml - cat testnet/octant-image.values.yaml | yq -r ".[0].value.value = \"$IMAGE_TAG\"" | tee testnet/octant-image.values.yaml + echo '(debug) before update ===' + cat mainnet/octant-image.values.yaml + cat testnet/octant-image.values.yaml + echo '(end debug) ===' + + yq -i -e ".[].value.value = \"$IMAGE_TAG\"" mainnet/octant-image.values.yaml + yq -i -e ".[].value.value = \"$IMAGE_TAG\"" testnet/octant-image.values.yaml + + echo '(debug) after update ===' + cat mainnet/octant-image.values.yaml + cat testnet/octant-image.values.yaml + echo '(end debug) ===' git add mainnet/octant-image.values.yaml git add testnet/octant-image.values.yaml diff --git a/client/cypress/e2e/onboarding.cy.ts b/client/cypress/e2e/onboarding.cy.ts index 06038aa706..7b3e94bdec 100644 --- a/client/cypress/e2e/onboarding.cy.ts +++ b/client/cypress/e2e/onboarding.cy.ts @@ -1,6 +1,6 @@ import { visitWithLoader, navigateWithCheck } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import steps from 'src/hooks/helpers/useOnboardingSteps/steps'; +import { stepsDecisionWindowClosed } from 'src/hooks/helpers/useOnboardingSteps/steps'; import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; @@ -55,9 +55,9 @@ const checkChangeStepsWithArrowKeys = (isTOSAccepted: boolean) => { [ { el: 1, key: 'ArrowRight' }, { el: 2, key: 'ArrowRight' }, - { el: 3, key: 'ArrowRight' }, - { el: 3, key: 'ArrowRight' }, - { el: 2, key: 'ArrowLeft' }, + { el: 2, key: 'ArrowRight' }, + // { el: 3, key: 'ArrowRight' }, + // { el: 2, key: 'ArrowLeft' }, { el: 1, key: 'ArrowLeft' }, { el: 0, key: 'ArrowLeft' }, { el: 0, key: 'ArrowLeft' }, @@ -81,10 +81,10 @@ const checkChangeStepsByClickingEdgeOfTheScreenUpTo25px = (isTOSAccepted: boolea [ { clientX: rightEdgeX - 25, el: 1 }, { clientX: rightEdgeX - 10, el: 2 }, - { clientX: rightEdgeX - 5, el: 3 }, + // { clientX: rightEdgeX - 5, el: 3 }, // rightEdgeX === browser right frame - { clientX: rightEdgeX - 1, el: 3 }, - { clientX: leftEdgeX + 25, el: 2 }, + // { clientX: rightEdgeX - 1, el: 3 }, + // { clientX: leftEdgeX + 25, el: 2 }, { clientX: leftEdgeX + 10, el: 1 }, { clientX: leftEdgeX + 5, el: 0 }, { clientX: leftEdgeX, el: 0 }, @@ -137,15 +137,15 @@ const checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px = (isTOSAcce touchStartClientX: window.innerWidth / 2, }, { - el: 3, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 3, + el: 2, touchMoveClientX: window.innerWidth / 2 - 5, touchStartClientX: window.innerWidth / 2, }, + // { + // el: 3, + // touchMoveClientX: window.innerWidth / 2 - 5, + // touchStartClientX: window.innerWidth / 2, + // }, { el: 2, touchMoveClientX: window.innerWidth / 2 + 5, @@ -231,12 +231,12 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => }); it('user is able to click through entire onboarding flow', () => { - for (let i = 1; i < steps.length - 1; i++) { + for (let i = 1; i < stepsDecisionWindowClosed.length - 1; i++) { checkProgressStepperSlimIsCurrentAndClickNext(i); } cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(steps.length - 1) + .eq(stepsDecisionWindowClosed.length - 1) .click(); cy.get('[data-test=ProposalsView__ProposalsList]').should('be.visible'); }); @@ -318,12 +318,12 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => it('onboarding should have one more step (TOS)', () => { cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]').should( 'have.length', - steps.length + 1, + stepsDecisionWindowClosed.length + 1, ); }); it('user is not able to click through entire onboarding flow', () => { - for (let i = 1; i < steps.length; i++) { + for (let i = 1; i < stepsDecisionWindowClosed.length; i++) { checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); } }); diff --git a/client/cypress/e2e/proposal.cy.ts b/client/cypress/e2e/proposal.cy.ts index fcf3809960..9a39997786 100644 --- a/client/cypress/e2e/proposal.cy.ts +++ b/client/cypress/e2e/proposal.cy.ts @@ -7,24 +7,13 @@ import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; -const getButtonAddToAllocate = (currentEpoch: number, isDesktop: boolean): Chainable => { +const getButtonAddToAllocate = (): Chainable => { const proposalView = cy.get('[data-test=ProposalView__proposal').first(); - switch (currentEpoch) { - case 1: - return proposalView.find('[data-test=ProposalView__proposal__ButtonAddToAllocate--primary]'); - default: - return proposalView.find( - `[data-test=${ - isDesktop - ? 'ProposalView__proposal__ButtonAddToAllocate--secondary' - : 'ProposalView__proposal__ButtonAddToAllocate--primary' - }]`, - ); - } + return proposalView.find('[data-test=ProposalView__proposal__ButtonAddToAllocate]'); }; -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { +Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { describe(`proposal: ${device}`, { viewportHeight, viewportWidth }, () => { let proposalNames: string[] = []; @@ -55,12 +44,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes const proposalView = cy.get('[data-test=ProposalView__proposal').first(); proposalView.get('[data-test=ProposalView__proposal__Img]').should('be.visible'); proposalView.get('[data-test=ProposalView__proposal__name]').should('be.visible'); - - cy.window().then(window => { - // @ts-expect-error missing typing for client window elements. - const currentEpoch = Number(window.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch)); - getButtonAddToAllocate(currentEpoch, isDesktop).should('be.visible'); - }); + getButtonAddToAllocate().should('be.visible'); proposalView.get('[data-test=ProposalView__proposal__Button]').should('be.visible'); proposalView.get('[data-test=ProposalView__proposal__Description]').should('be.visible'); @@ -94,19 +78,11 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes it('entering proposal view allows to add it to allocation and remove, triggering change of the icon, change of the number in navbar', () => { cy.get('[data-test^=ProposalsView__ProposalsListItem').first().click(); - cy.window().then(window => { - // @ts-expect-error missing typing for client window elements. - const currentEpoch = Number(window.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch)); - getButtonAddToAllocate(currentEpoch, isDesktop).click(); - }); + getButtonAddToAllocate().click(); // cy.get('@buttonAddToAllocate').click(); cy.get('[data-test=Navbar__numberOfAllocations]').contains(1); - cy.window().then(window => { - // @ts-expect-error missing typing for client window elements. - const currentEpoch = Number(window.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch)); - getButtonAddToAllocate(currentEpoch, isDesktop).click(); - }); + getButtonAddToAllocate().click(); cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist'); }); diff --git a/client/src/api/errorMessages/index.ts b/client/src/api/errorMessages/index.ts index 3e9473d4e3..e2b6957750 100644 --- a/client/src/api/errorMessages/index.ts +++ b/client/src/api/errorMessages/index.ts @@ -7,7 +7,11 @@ import triggerToast from 'utils/triggerToast'; import { QueryMutationError, QueryMutationErrorConfig, IgnoredQueries } from './types'; -const IGNORED_QUERIES: IgnoredQueries = [ROOTS.cryptoValues, QUERY_KEYS.glmClaimCheck[0]]; +const IGNORED_QUERIES: IgnoredQueries = [ + ROOTS.cryptoValues, + ROOTS.proposalsIpfsResults, + QUERY_KEYS.glmClaimCheck[0], +]; const errors: QueryMutationErrorConfig = { 4001: { diff --git a/client/src/api/errorMessages/types.ts b/client/src/api/errorMessages/types.ts index bedf5c5833..cb3eff7f0b 100644 --- a/client/src/api/errorMessages/types.ts +++ b/client/src/api/errorMessages/types.ts @@ -10,4 +10,8 @@ export type QueryMutationErrorConfig = { [key: string]: QueryMutationError; }; -export type IgnoredQueries = [Root['cryptoValues'], QueryKeys['glmClaimCheck'][0]]; +export type IgnoredQueries = [ + Root['cryptoValues'], + Root['proposalsIpfsResults'], + QueryKeys['glmClaimCheck'][0], +]; diff --git a/client/src/api/queryKeys/index.ts b/client/src/api/queryKeys/index.ts index d201017e20..422aa16986 100644 --- a/client/src/api/queryKeys/index.ts +++ b/client/src/api/queryKeys/index.ts @@ -36,9 +36,14 @@ export const QUERY_KEYS: QueryKeys = { isDecisionWindowOpen: ['isDecisionWindowOpen'], largestLockedAmount: ['largestLockedAmount'], lockedSummaryLatest: ['lockedSummaryLatest'], + lockedSummarySnapshots: ['lockedSummarySnapshots'], matchedProposalRewards: epochNumber => [ROOTS.matchedProposalRewards, epochNumber.toString()], patronMode: userAddress => [ROOTS.patronMode, userAddress], - proposalDonors: proposalAddress => [ROOTS.proposalDonors, proposalAddress], + proposalDonors: (proposalAddress, epochNumber) => [ + ROOTS.proposalDonors, + proposalAddress, + epochNumber.toString(), + ], proposalRewardsThreshold: epochNumber => [ROOTS.proposalRewardsThreshold, epochNumber.toString()], proposalsAllIpfs: ['proposalsAllIpfs'], proposalsCid: ['proposalsCid'], diff --git a/client/src/api/queryKeys/types.ts b/client/src/api/queryKeys/types.ts index b734ccb524..40f50b60a6 100644 --- a/client/src/api/queryKeys/types.ts +++ b/client/src/api/queryKeys/types.ts @@ -38,9 +38,13 @@ export type QueryKeys = { isDecisionWindowOpen: ['isDecisionWindowOpen']; largestLockedAmount: ['largestLockedAmount']; lockedSummaryLatest: ['lockedSummaryLatest']; + lockedSummarySnapshots: ['lockedSummarySnapshots']; matchedProposalRewards: (epochNumber: number) => [Root['matchedProposalRewards'], string]; patronMode: (userAddress: string) => [Root['patronMode'], string]; - proposalDonors: (proposalAddress: string) => [Root['proposalDonors'], string]; + proposalDonors: ( + proposalAddress: string, + epochNumber: number, + ) => [Root['proposalDonors'], string, string]; proposalRewardsThreshold: (epochNumber: number) => [Root['proposalRewardsThreshold'], string]; proposalsAllIpfs: ['proposalsAllIpfs']; proposalsCid: ['proposalsCid']; diff --git a/client/src/components/Metrics/MetricsGrid/MetricsCumulativeGlmLocked/MetricsCumulativeGlmLocked.tsx b/client/src/components/Metrics/MetricsGrid/MetricsCumulativeGlmLocked/MetricsCumulativeGlmLocked.tsx index ab22a1979c..d9b3d65f05 100644 --- a/client/src/components/Metrics/MetricsGrid/MetricsCumulativeGlmLocked/MetricsCumulativeGlmLocked.tsx +++ b/client/src/components/Metrics/MetricsGrid/MetricsCumulativeGlmLocked/MetricsCumulativeGlmLocked.tsx @@ -5,13 +5,13 @@ import { useTranslation } from 'react-i18next'; import AreaChart from 'components/core/AreaChart/AreaChart'; import ChartTimeSlicer from 'components/Metrics/MetricsGrid/common/ChartTimeSlicer/ChartTimeSlicer'; import MetricsGridTile from 'components/Metrics/MetricsGrid/common/MetricsGridTile/MetricsGridTile'; -import useLockedsData from 'hooks/subgraph/useLockedsData'; +import useLockedSummarySnapshots from 'hooks/subgraph/useLockedSummarySnapshots'; import styles from './MetricsCumulativeGlmLocked.module.scss'; const MetricsCumulativeGlmLocked: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'views.metrics' }); - const { data } = useLockedsData(); + const { data } = useLockedSummarySnapshots(); const [dateToFilter, setDateToFilter] = useState(null); diff --git a/client/src/components/core/BoxRounded/Sections/types.ts b/client/src/components/core/BoxRounded/Sections/types.ts index 122738c08c..121f1e0225 100644 --- a/client/src/components/core/BoxRounded/Sections/types.ts +++ b/client/src/components/core/BoxRounded/Sections/types.ts @@ -16,6 +16,7 @@ export interface SectionProps { dataTest?: DoubleValueProps['dataTest']; isDisabled?: DoubleValueProps['isDisabled']; isFetching?: DoubleValueProps['isFetching']; + shouldIgnoreGwei?: DoubleValueProps['shouldIgnoreGwei']; valueCrypto: DoubleValueProps['valueCrypto']; }; hasBottomDivider?: boolean; diff --git a/client/src/components/core/DoubleValue/DoubleValue.tsx b/client/src/components/core/DoubleValue/DoubleValue.tsx index 84ec477cae..5767546773 100644 --- a/client/src/components/core/DoubleValue/DoubleValue.tsx +++ b/client/src/components/core/DoubleValue/DoubleValue.tsx @@ -21,6 +21,7 @@ const DoubleValue: FC = ({ valueString, variant = 'big', isFetching = false, + shouldIgnoreGwei, }) => { const { data: { displayCurrency, isCryptoMainValueDisplay }, @@ -39,6 +40,7 @@ const DoubleValue: FC = ({ displayCurrency: displayCurrency!, error, isCryptoMainValueDisplay, + shouldIgnoreGwei, valueCrypto, valueString, }); diff --git a/client/src/components/core/DoubleValue/types.ts b/client/src/components/core/DoubleValue/types.ts index 3818e7190d..64f817a472 100644 --- a/client/src/components/core/DoubleValue/types.ts +++ b/client/src/components/core/DoubleValue/types.ts @@ -8,11 +8,12 @@ export type DoubleValueVariant = (typeof DOUBLE_VALUE_VARIANTS)[number]; export default interface DoubleValueProps { className?: string; coinPricesServerDowntimeText?: 'Conversion offline' | '...'; - cryptoCurrency?: CryptoCurrency; + cryptoCurrency: CryptoCurrency; dataTest?: string; isDisabled?: boolean; isError?: boolean; isFetching?: boolean; + shouldIgnoreGwei?: boolean; textAlignment?: 'left' | 'right'; valueCrypto?: BigNumber; valueString?: string; diff --git a/client/src/components/core/DoubleValue/utils.ts b/client/src/components/core/DoubleValue/utils.ts index a75b6b0fcb..fe3b6079a2 100644 --- a/client/src/components/core/DoubleValue/utils.ts +++ b/client/src/components/core/DoubleValue/utils.ts @@ -17,6 +17,7 @@ export function getValuesToDisplay({ valueString, isCryptoMainValueDisplay, error, + shouldIgnoreGwei, }: { coinPricesServerDowntimeText?: DoubleValueProps['coinPricesServerDowntimeText']; cryptoCurrency: DoubleValueProps['cryptoCurrency']; @@ -24,6 +25,7 @@ export function getValuesToDisplay({ displayCurrency: NonNullable; error: any; isCryptoMainValueDisplay: SettingsData['isCryptoMainValueDisplay']; + shouldIgnoreGwei?: DoubleValueProps['shouldIgnoreGwei']; valueCrypto?: BigNumber; valueString?: DoubleValueProps['valueString']; }): { @@ -41,6 +43,7 @@ export function getValuesToDisplay({ getValueCryptoToDisplay({ cryptoCurrency, isUsingHairSpace: isCryptoMainValueDisplay, + shouldIgnoreGwei, valueCrypto, }); const valueFiatToDisplay = getValueFiatToDisplay({ diff --git a/client/src/components/core/InputText/InputText.module.scss b/client/src/components/core/InputText/InputText.module.scss index a75fb193aa..f4df4d1bdf 100644 --- a/client/src/components/core/InputText/InputText.module.scss +++ b/client/src/components/core/InputText/InputText.module.scss @@ -36,6 +36,8 @@ width: 100%; height: 100%; border: 0; + // Hides copy/paste popup on safari mobile when an input value is selected + -webkit-user-select: none; &:focus-visible { outline: 0; diff --git a/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx b/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx index bb4f9a8aab..428dfc3a50 100644 --- a/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx +++ b/client/src/components/dedicated/AllocateRewardsBox/AllocateRewardsBox.tsx @@ -8,6 +8,7 @@ import BoxRounded from 'components/core/BoxRounded/BoxRounded'; import Slider from 'components/core/Slider/Slider'; import ModalAllocationValuesEdit from 'components/dedicated/ModalAllocationValuesEdit/ModalAllocationValuesEdit'; import useIndividualReward from 'hooks/queries/useIndividualReward'; +import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; import useAllocationsStore from 'store/allocations/store'; import getValueCryptoToDisplay from 'utils/getValueCryptoToDisplay'; @@ -19,6 +20,7 @@ const AllocateRewardsBox: FC = ({ className, isDisabled keyPrefix: 'components.dedicated.allocationRewardsBox', }); const { data: individualReward } = useIndividualReward(); + const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); const [modalMode, setModalMode] = useState<'closed' | 'donate' | 'withdraw'>('closed'); const { rewardsForProposals, setRewardsForProposals } = useAllocationsStore(state => ({ rewardsForProposals: state.data.rewardsForProposals, @@ -26,6 +28,8 @@ const AllocateRewardsBox: FC = ({ className, isDisabled })); const hasUserIndividualReward = !!individualReward && !individualReward.isZero(); + const isDecisionWindowOpenAndHasIndividualReward = + hasUserIndividualReward && isDecisionWindowOpen; const onSetRewardsForProposals = (index: number) => { if (!individualReward || isDisabled) { @@ -40,19 +44,22 @@ const AllocateRewardsBox: FC = ({ className, isDisabled individualReward?.toHexString(), ]); - const percentRewardsForProposals = !hasUserIndividualReward - ? 50 - : rewardsForProposals.mul(100).div(individualReward).toNumber(); + const percentRewardsForProposals = isDecisionWindowOpenAndHasIndividualReward + ? rewardsForProposals.mul(100).div(individualReward).toNumber() + : 50; const percentWithdraw = 100 - percentRewardsForProposals; - const rewardsForWithdraw = !hasUserIndividualReward - ? BigNumber.from(0) - : individualReward.sub(rewardsForProposals); + const rewardsForProposalsFinal = isDecisionWindowOpenAndHasIndividualReward + ? rewardsForProposals + : BigNumber.from(0); + const rewardsForWithdraw = isDecisionWindowOpenAndHasIndividualReward + ? individualReward?.sub(rewardsForProposals) + : BigNumber.from(0); const sections = [ { header: t('donate', { percentRewardsForProposals }), value: getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', - valueCrypto: rewardsForProposals, + valueCrypto: rewardsForProposalsFinal, }), }, { @@ -69,7 +76,7 @@ const AllocateRewardsBox: FC = ({ className, isDisabled className={cx(styles.root, className)} isVertical subtitle={ - hasUserIndividualReward + isDecisionWindowOpenAndHasIndividualReward ? t('subtitle', { individualReward: getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', diff --git a/client/src/components/dedicated/AllocationItem/AllocationItem.tsx b/client/src/components/dedicated/AllocationItem/AllocationItem.tsx index 348f505351..3d3eba6639 100644 --- a/client/src/components/dedicated/AllocationItem/AllocationItem.tsx +++ b/client/src/components/dedicated/AllocationItem/AllocationItem.tsx @@ -1,12 +1,11 @@ import cx from 'classnames'; -import React, { FC, Fragment } from 'react'; +import React, { FC, Fragment, memo } from 'react'; import { useAccount } from 'wagmi'; import BoxRounded from 'components/core/BoxRounded/BoxRounded'; import Img from 'components/core/Img/Img'; import Svg from 'components/core/Svg/Svg'; import AllocationItemSkeleton from 'components/dedicated/AllocationItem/AllocationItemSkeleton/AllocationItemSkeleton'; -import ProposalLoadingStates from 'components/dedicated/ProposalLoadingStates/ProposalLoadingStates'; import env from 'env'; import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; import useProposalRewardsThreshold from 'hooks/queries/useProposalRewardsThreshold'; @@ -53,12 +52,7 @@ const AllocationItem: FC = ({ className={cx(styles.root, className)} onClick={isConnected && !isDisabled ? () => onSelectItem(address) : undefined} > - {(isLoading || isLoadingError) && ( - - - - )} - + {(isLoading || isLoadingError) && } {!isLoading && !isLoadingError && ( {(isAllocatedTo || isManuallyEdited) && ( @@ -92,4 +86,4 @@ const AllocationItem: FC = ({ ); }; -export default AllocationItem; +export default memo(AllocationItem); diff --git a/client/src/components/dedicated/AllocationItem/types.ts b/client/src/components/dedicated/AllocationItem/types.ts index 3d5ba07f1b..a1c9848f38 100644 --- a/client/src/components/dedicated/AllocationItem/types.ts +++ b/client/src/components/dedicated/AllocationItem/types.ts @@ -7,7 +7,8 @@ export interface AllocationItemWithAllocations extends ProposalIpfsWithRewards { value: BigNumber; } -export default interface AllocationItemProps extends AllocationItemWithAllocations { +export default interface AllocationItemProps + extends Omit { className?: string; isDisabled: boolean; isLocked: boolean; diff --git a/client/src/components/dedicated/AllocationSummary/AllocationSummary.tsx b/client/src/components/dedicated/AllocationSummary/AllocationSummary.tsx index c6ef4ee4b8..04bd319736 100644 --- a/client/src/components/dedicated/AllocationSummary/AllocationSummary.tsx +++ b/client/src/components/dedicated/AllocationSummary/AllocationSummary.tsx @@ -47,6 +47,7 @@ const AllocationSummary: FC = ({ allocationValues }) => doubleValueProps: { cryptoCurrency: 'ethereum', isFetching: isFetchingIndividualReward, + shouldIgnoreGwei: true, valueCrypto: individualReward?.sub(rewardsForProposals), }, label: i18n.t('common.personal'), @@ -58,6 +59,7 @@ const AllocationSummary: FC = ({ allocationValues }) => { doubleValueProps: { cryptoCurrency: 'ethereum', + shouldIgnoreGwei: true, valueCrypto: rewardsForProposals, }, label: t('allocationProjects', { projectsNumber: allocationValuesPositive.length }), diff --git a/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.module.scss b/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.module.scss index 0cb3abb2e4..c31184b4b5 100644 --- a/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.module.scss +++ b/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.module.scss @@ -13,10 +13,21 @@ } } - &.isArchivedProposal.isAllocatedTo { - svg circle { - stroke: $color-octant-grey5; - fill: $color-octant-grey5; + &.isArchivedProposal { + &:hover { + background: transparent; + cursor: default; + } + + svg path { + stroke: $color-octant-grey1; + } + + &.isAllocatedTo { + svg circle { + stroke: $color-octant-grey5; + fill: $color-octant-grey5; + } } } } diff --git a/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.tsx b/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.tsx index 7a792b74d0..f9d7047376 100644 --- a/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.tsx +++ b/client/src/components/dedicated/ButtonAddToAllocate/ButtonAddToAllocate.tsx @@ -38,6 +38,7 @@ const ButtonAddToAllocate: FC = ({ }} dataTest={dataTest} Icon={} + isDisabled={isArchivedProposal} onClick={onClick} variant="iconOnly" /> diff --git a/client/src/components/dedicated/Donors/Donors.tsx b/client/src/components/dedicated/Donors/Donors.tsx index 31733457ea..3fc73eee51 100644 --- a/client/src/components/dedicated/Donors/Donors.tsx +++ b/client/src/components/dedicated/Donors/Donors.tsx @@ -1,6 +1,7 @@ import cx from 'classnames'; import React, { FC, useState, Fragment } from 'react'; import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; import Button from 'components/core/Button/Button'; import DonorsHeader from 'components/dedicated/DonorsHeader/DonorsHeader'; @@ -14,8 +15,12 @@ import styles from './Donors.module.scss'; import DonorsProps from './types'; const Donors: FC = ({ className, dataTest = 'Donors', proposalAddress }) => { + const { epoch } = useParams(); const { t } = useTranslation('translation', { keyPrefix: 'components.dedicated.donors' }); - const { data: proposalDonors, isFetching } = useProposalDonors(proposalAddress); + const { data: proposalDonors, isFetching } = useProposalDonors( + proposalAddress, + parseInt(epoch!, 10), + ); const { data: currentEpoch } = useCurrentEpoch(); const [isFullDonorsListModalOpen, setIsFullDonorsListModalOpen] = useState(false); diff --git a/client/src/components/dedicated/DonorsHeader/DonorsHeader.tsx b/client/src/components/dedicated/DonorsHeader/DonorsHeader.tsx index fe63d2b792..1279683c53 100644 --- a/client/src/components/dedicated/DonorsHeader/DonorsHeader.tsx +++ b/client/src/components/dedicated/DonorsHeader/DonorsHeader.tsx @@ -1,6 +1,7 @@ import cx from 'classnames'; import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; import useProposalDonors from 'hooks/queries/useProposalDonors'; @@ -12,13 +13,16 @@ const DonorsHeader: FC = ({ dataTest = 'DonorsHeader', className, }) => { - const { t } = useTranslation('translation', { keyPrefix: 'components.dedicated.donorsHeader' }); - - const { data: proposalDonors, isFetching } = useProposalDonors(proposalAddress); + const { epoch } = useParams(); + const { i18n } = useTranslation('translation'); + const { data: proposalDonors, isFetching } = useProposalDonors( + proposalAddress, + parseInt(epoch!, 10), + ); return (
- {t('donors')}{' '} + {i18n.t('common.donors')}{' '}
{isFetching ? '--' : proposalDonors?.length}
diff --git a/client/src/components/dedicated/DonorsItem/DonorsItem.tsx b/client/src/components/dedicated/DonorsItem/DonorsItem.tsx index d3eac0f6b5..68273d8462 100644 --- a/client/src/components/dedicated/DonorsItem/DonorsItem.tsx +++ b/client/src/components/dedicated/DonorsItem/DonorsItem.tsx @@ -30,6 +30,7 @@ const DonorsItem: FC = ({ donorAddress, amount, className }) => {isCryptoMainValueDisplay ? getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', + shouldIgnoreGwei: true, valueCrypto: amount, }) : getValueFiatToDisplay({ diff --git a/client/src/components/dedicated/DonorsList/DonorsList.tsx b/client/src/components/dedicated/DonorsList/DonorsList.tsx index a18cea9b42..5c608d1049 100644 --- a/client/src/components/dedicated/DonorsList/DonorsList.tsx +++ b/client/src/components/dedicated/DonorsList/DonorsList.tsx @@ -1,5 +1,6 @@ import cx from 'classnames'; import React, { FC } from 'react'; +import { useParams } from 'react-router-dom'; import DonorsItem from 'components/dedicated/DonorsItem/DonorsItem'; import DonorsItemSkeleton from 'components/dedicated/DonorsItem/DonorsItemSkeleton/DonorsItemSkeleton'; @@ -15,7 +16,11 @@ const DonorsList: FC = ({ proposalAddress, showFullList = false, }) => { - const { data: proposalDonors, isFetching } = useProposalDonors(proposalAddress); + const { epoch } = useParams(); + const { data: proposalDonors, isFetching } = useProposalDonors( + proposalAddress, + parseInt(epoch!, 10), + ); return (
diff --git a/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDateAndTime/utils.ts b/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDateAndTime/utils.ts index ef90b0ad1d..4ac5123945 100644 --- a/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDateAndTime/utils.ts +++ b/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDateAndTime/utils.ts @@ -1,5 +1,6 @@ import format from 'date-fns/format'; export function getHistoryItemDateAndTime(timestamp: string): string { - return format(parseInt(timestamp, 10) / 1000, 'h:mmaaa, dd MMM yyyy'); + // Timestamp from subgraph is in microseconds, needs to be changed to milliseconds. + return format(Math.floor(parseInt(timestamp, 10) / 1000), 'h:mmaaa, dd MMM yyyy'); } diff --git a/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDetailsRest/HistoryItemDetailsRest.tsx b/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDetailsRest/HistoryItemDetailsRest.tsx index b724aea232..49d0f045db 100644 --- a/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDetailsRest/HistoryItemDetailsRest.tsx +++ b/client/src/components/dedicated/History/HistoryItemDetails/HistoryItemDetailsRest/HistoryItemDetailsRest.tsx @@ -33,6 +33,7 @@ const HistoryItemDetailsRest: FC = ({ { doubleValueProps: { cryptoCurrency: type === 'withdrawal' ? 'ethereum' : 'golem', + shouldIgnoreGwei: true, valueCrypto: amount, }, label: t('sections.amount'), @@ -42,6 +43,7 @@ const HistoryItemDetailsRest: FC = ({ cryptoCurrency: 'ethereum', // Gas price is not known for pending transactions. isFetching: isFetchingTransaction || isWaitingForTransactionInitialized, + shouldIgnoreGwei: true, valueCrypto: BigNumber.from(transaction ? transaction.gasPrice : 0), }, label: t('sections.gasPrice'), diff --git a/client/src/components/dedicated/ProjectAllocationDetailRow/ProjectAllocationDetailRow.tsx b/client/src/components/dedicated/ProjectAllocationDetailRow/ProjectAllocationDetailRow.tsx index 3c5d1dfb9a..6025729414 100644 --- a/client/src/components/dedicated/ProjectAllocationDetailRow/ProjectAllocationDetailRow.tsx +++ b/client/src/components/dedicated/ProjectAllocationDetailRow/ProjectAllocationDetailRow.tsx @@ -41,6 +41,7 @@ const ProjectAllocationDetailRow: FC = ({ addre {isCryptoMainValueDisplay ? getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', + shouldIgnoreGwei: true, valueCrypto: amount, }) : getValueFiatToDisplay({ diff --git a/client/src/components/dedicated/ProposalLoadingStates/ProposalLoadingStates.tsx b/client/src/components/dedicated/ProposalLoadingStates/ProposalLoadingStates.tsx deleted file mode 100644 index 03266bbc84..0000000000 --- a/client/src/components/dedicated/ProposalLoadingStates/ProposalLoadingStates.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React, { FC } from 'react'; -import { Trans } from 'react-i18next'; - -import ProposalLoadingStatesProps from './types'; - -const ProposalLoadingStates: FC = ({ - isLoadingError, - isLoading, - children, -}) => { - if (isLoadingError) { - return ; - } - if (isLoading) { - return children; - } - return null; -}; - -export default ProposalLoadingStates; diff --git a/client/src/components/dedicated/ProposalLoadingStates/types.ts b/client/src/components/dedicated/ProposalLoadingStates/types.ts deleted file mode 100644 index 17b7dd86f5..0000000000 --- a/client/src/components/dedicated/ProposalLoadingStates/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ReactNode } from 'react'; - -export default interface ProposalLoadingStatesProps { - children: ReactNode; - isLoading?: boolean; - isLoadingError: boolean; -} diff --git a/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss b/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss index 5dd49606ae..a9b7ad5515 100644 --- a/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss +++ b/client/src/components/dedicated/ProposalRewards/ProposalRewards.module.scss @@ -5,87 +5,66 @@ align-self: stretch; justify-content: space-between; - .separator { - margin: 0 0 1.2rem 0; - } - - .line { + .divider { + margin: 0.3rem 0; height: 0.2rem; width: 100%; content: ''; background: $color-octant-grey3; } - .values { + .sections { + margin-top: 1.2rem; display: flex; justify-content: space-between; - align-items: center; - color: $color-octant-grey5; - .value { - display: flex; - flex-direction: column; + .section { font-weight: $font-weight-bold; - - &:first-child { - align-items: flex-start; - - .number { - color: $color-black; - - &.isArchivedProposal { - color: $color-octant-grey5; - } + color: $color-octant-grey5; + + .label, .value { + &.isFetching { + @include skeleton(); + text-indent: 1000%; // moves text outside of view. + white-space: nowrap; + overflow: hidden; + width: 8.8rem; } } - &:last-child { - align-items: flex-end; - } - - &.isHidden { - visibility: hidden; - } - .label { font-size: $font-size-12; - height: 2.4rem; + height: 1.6rem; + margin-bottom: 0.4rem; display: flex; align-items: center; } - .number { - font-size: $font-size-16; + &.leftSection { + text-align: left; - &.isBelowCutOff { - color: $color-octant-orange; + .label { + justify-content: flex-start; } - &.isArchivedProposal { - color: $color-octant-grey5; + .value { + &.greenValue { + color: $color-octant-green; + } + + &.redValue { + color: $color-octant-orange; + } } } - } - } - .percentage { - position: relative; - color: $color-octant-orange; - margin: 0 0 0 1rem; - padding: 0 0 0 1rem; + &.rightSection { + text-align: right; - &:before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 0.2rem; - height: 100%; - background: $color-octant-grey1; + .label { + justify-content: flex-end; + } + } } } - - .thresholdDataUnavailable { - font-size: $font-size-12; - } } diff --git a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx index c393617d08..f11d50df7b 100644 --- a/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx +++ b/client/src/components/dedicated/ProposalRewards/ProposalRewards.tsx @@ -1,7 +1,7 @@ import cx from 'classnames'; import { BigNumber } from 'ethers'; import { formatUnits } from 'ethers/lib/utils'; -import React, { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import ProgressBar from 'components/core/ProgressBar/ProgressBar'; @@ -17,10 +17,9 @@ import { getProgressPercentage } from './utils'; const ProposalRewards: FC = ({ address, - canFoundedAtHide = true, className, - MiddleElement, epoch, + isProposalView, }) => { const { t, i18n } = useTranslation('translation', { keyPrefix: 'components.dedicated.proposalRewards', @@ -28,33 +27,33 @@ const ProposalRewards: FC = ({ const isArchivedProposal = epoch !== undefined; - const { data: proposalRewardsThreshold } = useProposalRewardsThreshold(epoch); - const { data: matchedProposalRewards } = useMatchedProposalRewards(epoch); - const { data: proposalDonors } = useProposalDonors(address); + const { data: proposalRewardsThreshold, isFetching: isFetchingProposalRewardsThreshold } = + useProposalRewardsThreshold(epoch); + const { data: matchedProposalRewards, isFetching: isFetchingMatchedProposalRewards } = + useMatchedProposalRewards(epoch); + const { data: proposalDonors, isFetching: isFetchingProposalDonors } = useProposalDonors( + address, + epoch, + ); const proposalMatchedProposalRewards = matchedProposalRewards?.find( ({ address: proposalAddress }) => address === proposalAddress, ); - const proposalDonorsRewardsSum = proposalDonors?.reduce( - (prev, curr) => prev.add(formatUnits(curr.amount, 'wei')), - BigNumber.from(0), - ); + const proposalDonorsRewardsSum = isArchivedProposal + ? proposalDonors?.reduce( + (acc, curr) => acc.add(formatUnits(curr.amount, 'wei')), + BigNumber.from(0), + ) + : proposalMatchedProposalRewards?.sum; const isDonationAboveThreshold = useIsDonationAboveThreshold(address, epoch); - const isFundedAtHidden = - proposalDonorsRewardsSum?.isZero() || (canFoundedAtHide && isDonationAboveThreshold); - const totalValueOfAllocationsToDisplay = getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', + shouldIgnoreGwei: true, valueCrypto: proposalMatchedProposalRewards?.sum, }); - const cutOffValueToDisplay = getValueCryptoToDisplay({ - cryptoCurrency: 'ethereum', - valueCrypto: proposalRewardsThreshold, - }); - const proposalDonorsRewardsSumToDisplay = getValueCryptoToDisplay({ cryptoCurrency: 'ethereum', valueCrypto: proposalDonorsRewardsSum, @@ -65,58 +64,108 @@ const ProposalRewards: FC = ({ proposalRewardsThreshold !== undefined && proposalDonorsRewardsSum !== undefined; + const leftSectionLabel = useMemo(() => { + if (isDonationAboveThreshold && !isArchivedProposal) { + return t('currentTotal'); + } + if (isDonationAboveThreshold && isArchivedProposal) { + return t('totalRaised'); + } + return t('totalDonated'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isArchivedProposal, isDonationAboveThreshold]); + + const rightSectionLabel = useMemo(() => { + if (isDonationAboveThreshold && isArchivedProposal && isProposalView) { + return t('fundedIn'); + } + if (isDonationAboveThreshold && isArchivedProposal) { + return i18n.t('common.donors'); + } + if (isArchivedProposal && isProposalView) { + return t('didNotReachThreshold'); + } + if (isArchivedProposal) { + return t('didNotReach'); + } + return t('fundedAt'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isArchivedProposal, isDonationAboveThreshold, isProposalView]); + + const rightSectionValueUseMemoDeps = [ + isArchivedProposal, + isDonationAboveThreshold, + isProposalView, + epoch, + proposalDonors?.length, + proposalRewardsThreshold?.toHexString(), + ]; + + const rightSectionValue = useMemo(() => { + if (isDonationAboveThreshold && isArchivedProposal && isProposalView) { + return t('epoch', { epoch }); + } + if (isDonationAboveThreshold && isArchivedProposal) { + return proposalDonors?.length; + } + return getValueCryptoToDisplay({ + cryptoCurrency: 'ethereum', + valueCrypto: proposalRewardsThreshold, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, rightSectionValueUseMemoDeps); + + const isFetching = + isFetchingProposalRewardsThreshold || + isFetchingMatchedProposalRewards || + isFetchingProposalDonors; return (
-
- {showProgressBar ? ( - - ) : ( -
- )} -
-
- {proposalMatchedProposalRewards !== undefined || proposalDonors !== undefined ? ( -
- - {t(isDonationAboveThreshold ? 'totalRaised' : 'totalDonated')} - - - {isDonationAboveThreshold - ? totalValueOfAllocationsToDisplay - : proposalDonorsRewardsSumToDisplay} - + {showProgressBar ? ( + + ) : ( +
+ )} +
+
+
+ {leftSectionLabel}
- ) : (
- {i18n.t('common.thresholdDataUnavailable')} + {isDonationAboveThreshold + ? totalValueOfAllocationsToDisplay + : proposalDonorsRewardsSumToDisplay}
- )} - {MiddleElement} -
- {t(isArchivedProposal ? 'didNotReach' : 'fundedAt')} - - {cutOffValueToDisplay} -
+ {!(isDonationAboveThreshold && !isArchivedProposal) && ( +
+
+ {rightSectionLabel} +
+
+ {rightSectionValue} +
+
+ )}
); }; - export default ProposalRewards; diff --git a/client/src/components/dedicated/ProposalRewards/types.ts b/client/src/components/dedicated/ProposalRewards/types.ts index 9c5de9daad..eb556ab064 100644 --- a/client/src/components/dedicated/ProposalRewards/types.ts +++ b/client/src/components/dedicated/ProposalRewards/types.ts @@ -1,9 +1,6 @@ -import { ReactNode } from 'react'; - export default interface ProposalRewardsProps { - MiddleElement?: ReactNode; address: string; - canFoundedAtHide?: boolean; className?: string; epoch?: number; + isProposalView?: boolean; } diff --git a/client/src/components/dedicated/ProposalsList/ProposalsList.module.scss b/client/src/components/dedicated/ProposalsList/ProposalsList.module.scss index 1dd4da08d3..ddbb25680b 100644 --- a/client/src/components/dedicated/ProposalsList/ProposalsList.module.scss +++ b/client/src/components/dedicated/ProposalsList/ProposalsList.module.scss @@ -29,10 +29,17 @@ $elementMargin: 1.6rem; border-radius: $border-radius-16; margin: 1.6rem 0; - .epochArchiveEnded { + .epochDurationLabel { color: $color-octant-grey5; font-size: $font-size-12; font-weight: $font-weight-medium; + + &.isFetching { + @include skeleton(); + text-indent: 1000%; // moves text outside of view. + white-space: nowrap; + overflow: hidden; + } } } diff --git a/client/src/components/dedicated/ProposalsList/ProposalsList.tsx b/client/src/components/dedicated/ProposalsList/ProposalsList.tsx index 34aafd92c6..dc8967c49c 100644 --- a/client/src/components/dedicated/ProposalsList/ProposalsList.tsx +++ b/client/src/components/dedicated/ProposalsList/ProposalsList.tsx @@ -1,12 +1,14 @@ -import { formatDistanceToNow } from 'date-fns'; +import cx from 'classnames'; +import { format, isSameYear } from 'date-fns'; import React, { FC, memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import ProposalsListItem from 'components/dedicated/ProposalsList/ProposalsListItem/ProposalsListItem'; import ProposalsListItemSkeleton from 'components/dedicated/ProposalsList/ProposalsListItemSkeleton/ProposalsListItemSkeleton'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; import useProposalsContract from 'hooks/queries/useProposalsContract'; import useProposalsIpfsWithRewards from 'hooks/queries/useProposalsIpfsWithRewards'; -import useEpochsEndTime from 'hooks/subgraph/useEpochsEndTime'; +import useEpochsStartEndTime from 'hooks/subgraph/useEpochsStartEndTime'; import styles from './ProposalsList.module.scss'; import ProposalsListProps from './types'; @@ -20,19 +22,39 @@ const ProposalsList: FC = ({ keyPrefix: 'components.dedicated.proposalsList', }); + const { isDesktop } = useMediaQuery(); + const { data: proposalsAddresses } = useProposalsContract(epoch); - const { data: proposalsWithRewards } = useProposalsIpfsWithRewards(epoch); - const { data: epochsEndTime } = useEpochsEndTime(); + const { data: proposalsWithRewards, isFetching: isFetchingProposalsWithRewards } = + useProposalsIpfsWithRewards(epoch); + const { data: epochsStartEndTime } = useEpochsStartEndTime(); - const epochEndedLabel = useMemo(() => { - if (!epoch || !epochsEndTime) { + const epochDurationLabel = useMemo(() => { + if (!epoch || !epochsStartEndTime) { return ''; } - return formatDistanceToNow(new Date(parseInt(epochsEndTime[epoch - 1].toTs, 10) * 1000), { - addSuffix: true, - }); - }, [epoch, epochsEndTime]); + const epochData = epochsStartEndTime[epoch - 1]; + const epochStartTimestamp = parseInt(epochData.fromTs, 10) * 1000; + const epochEndTimestampPlusDecisionWindowDuration = + (parseInt(epochData.toTs, 10) + parseInt(epochData.decisionWindow, 10)) * 1000; + + const isEpochEndedAtTheSameYear = isSameYear( + epochStartTimestamp, + epochEndTimestampPlusDecisionWindowDuration, + ); + + const epochStartLabel = format( + epochStartTimestamp, + `${isDesktop ? 'dd MMMM' : 'MMM'} ${isEpochEndedAtTheSameYear ? '' : 'yyyy'}`, + ); + const epochEndLabel = format( + epochEndTimestampPlusDecisionWindowDuration, + `${isDesktop ? 'dd MMMM' : 'MMM'} yyyy`, + ); + + return `${epochStartLabel} -> ${epochEndLabel}`; + }, [epoch, epochsStartEndTime, isDesktop]); return (
= ({ )}
{t('epochArchive', { epoch })} - {epochEndedLabel} + + {epochDurationLabel} +
)} - {proposalsWithRewards.length > 0 + {proposalsWithRewards.length > 0 && !isFetchingProposalsWithRewards ? proposalsWithRewards.map((proposalWithRewards, index) => ( = ({ } > {isLoadingError ? ( - - - + ) : (
@@ -81,20 +78,18 @@ const ProposalsListItem: FC = ({ } src={`${ipfsGateway}${profileImageSmall}`} /> - {((isArchivedProposal && isAllocatedTo) || !isArchivedProposal) && ( - onAddRemoveFromAllocate(address)} - /> - )} + onAddRemoveFromAllocate(address)} + />
= ({ classNa
-
+
-
+
-
+
diff --git a/client/src/gql/gql.ts b/client/src/gql/gql.ts index e00604f73d..c622be9dcd 100644 --- a/client/src/gql/gql.ts +++ b/client/src/gql/gql.ts @@ -17,12 +17,14 @@ 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 GetEpochsEndTime($lastEpoch: Int) {\n epoches(first: $lastEpoch) {\n epoch\n toTs\n }\n }\n': - types.GetEpochsEndTimeDocument, + '\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': types.GetLargestLockedAmountDocument, '\n query GetLockedSummaryLatest {\n lockedSummaryLatest(id: "latest") {\n id\n lockedTotal\n lockedRatio\n }\n }\n': types.GetLockedSummaryLatestDocument, + '\n query GetLockedSummarySnapshots($first: Int = 1000, $skip: Int = 0) {\n lockedSummarySnapshots(first: $first, skip: $skip, orderBy: timestamp) {\n lockedTotal\n timestamp\n }\n }\n': + types.GetLockedSummarySnapshotsDocument, '\n query GetLockedsData($first: Int = 100, $skip: Int = 0) {\n lockeds(first: $first, skip: $skip) {\n user\n timestamp\n amount\n }\n }\n': types.GetLockedsDataDocument, }; @@ -57,8 +59,8 @@ export function graphql( * 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 GetEpochsEndTime($lastEpoch: Int) {\n epoches(first: $lastEpoch) {\n epoch\n toTs\n }\n }\n', -): (typeof documents)['\n query GetEpochsEndTime($lastEpoch: Int) {\n epoches(first: $lastEpoch) {\n epoch\n toTs\n }\n }\n']; + source: '\n query GetEpochsStartEndTime($lastEpoch: Int) {\n epoches(first: $lastEpoch) {\n epoch\n toTs\n fromTs\n decisionWindow\n }\n }\n', +): (typeof documents)['\n query GetEpochsStartEndTime($lastEpoch: Int) {\n epoches(first: $lastEpoch) {\n epoch\n toTs\n fromTs\n decisionWindow\n }\n }\n']; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -71,6 +73,12 @@ export function graphql( export function graphql( source: '\n query GetLockedSummaryLatest {\n lockedSummaryLatest(id: "latest") {\n id\n lockedTotal\n lockedRatio\n }\n }\n', ): (typeof documents)['\n query GetLockedSummaryLatest {\n lockedSummaryLatest(id: "latest") {\n id\n lockedTotal\n lockedRatio\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 GetLockedSummarySnapshots($first: Int = 1000, $skip: Int = 0) {\n lockedSummarySnapshots(first: $first, skip: $skip, orderBy: timestamp) {\n lockedTotal\n timestamp\n }\n }\n', +): (typeof documents)['\n query GetLockedSummarySnapshots($first: Int = 1000, $skip: Int = 0) {\n lockedSummarySnapshots(first: $first, skip: $skip, orderBy: timestamp) {\n lockedTotal\n timestamp\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 14b92877a1..fa044e2946 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -410,6 +410,8 @@ export type Query = { lockeds: Array; unlocked?: Maybe; unlockeds: Array; + vaultMerkleRoot?: Maybe; + vaultMerkleRoots: Array; withdrawal?: Maybe; withdrawals: Array; }; @@ -498,6 +500,22 @@ export type QueryUnlockedsArgs = { where?: InputMaybe; }; +export type QueryVaultMerkleRootArgs = { + block?: InputMaybe; + id: Scalars['ID']['input']; + subgraphError?: _SubgraphErrorPolicy_; +}; + +export type QueryVaultMerkleRootsArgs = { + block?: InputMaybe; + first?: InputMaybe; + orderBy?: InputMaybe; + orderDirection?: InputMaybe; + skip?: InputMaybe; + subgraphError?: _SubgraphErrorPolicy_; + where?: InputMaybe; +}; + export type QueryWithdrawalArgs = { block?: InputMaybe; id: Scalars['ID']['input']; @@ -528,6 +546,8 @@ export type Subscription = { lockeds: Array; unlocked?: Maybe; unlockeds: Array; + vaultMerkleRoot?: Maybe; + vaultMerkleRoots: Array; withdrawal?: Maybe; withdrawals: Array; }; @@ -616,6 +636,22 @@ export type SubscriptionUnlockedsArgs = { where?: InputMaybe; }; +export type SubscriptionVaultMerkleRootArgs = { + block?: InputMaybe; + id: Scalars['ID']['input']; + subgraphError?: _SubgraphErrorPolicy_; +}; + +export type SubscriptionVaultMerkleRootsArgs = { + block?: InputMaybe; + first?: InputMaybe; + orderBy?: InputMaybe; + orderDirection?: InputMaybe; + skip?: InputMaybe; + subgraphError?: _SubgraphErrorPolicy_; + where?: InputMaybe; +}; + export type SubscriptionWithdrawalArgs = { block?: InputMaybe; id: Scalars['ID']['input']; @@ -722,6 +758,86 @@ export enum Unlocked_OrderBy { User = 'user', } +export type VaultMerkleRoot = { + __typename?: 'VaultMerkleRoot'; + blockNumber: Scalars['Int']['output']; + epoch: Scalars['Int']['output']; + id: Scalars['Bytes']['output']; + root: Scalars['Bytes']['output']; + timestamp: Scalars['Int']['output']; + transactionHash: Scalars['Bytes']['output']; +}; + +export type VaultMerkleRoot_Filter = { + /** Filter for the block changed event. */ + _change_block?: InputMaybe; + and?: InputMaybe>>; + blockNumber?: InputMaybe; + blockNumber_gt?: InputMaybe; + blockNumber_gte?: InputMaybe; + blockNumber_in?: InputMaybe>; + blockNumber_lt?: InputMaybe; + blockNumber_lte?: InputMaybe; + blockNumber_not?: InputMaybe; + blockNumber_not_in?: InputMaybe>; + epoch?: InputMaybe; + epoch_gt?: InputMaybe; + epoch_gte?: InputMaybe; + epoch_in?: InputMaybe>; + epoch_lt?: InputMaybe; + epoch_lte?: InputMaybe; + epoch_not?: InputMaybe; + epoch_not_in?: InputMaybe>; + id?: InputMaybe; + id_contains?: InputMaybe; + id_gt?: InputMaybe; + id_gte?: InputMaybe; + id_in?: InputMaybe>; + id_lt?: InputMaybe; + id_lte?: InputMaybe; + id_not?: InputMaybe; + id_not_contains?: InputMaybe; + id_not_in?: InputMaybe>; + or?: InputMaybe>>; + root?: InputMaybe; + root_contains?: InputMaybe; + root_gt?: InputMaybe; + root_gte?: InputMaybe; + root_in?: InputMaybe>; + root_lt?: InputMaybe; + root_lte?: InputMaybe; + root_not?: InputMaybe; + root_not_contains?: InputMaybe; + root_not_in?: InputMaybe>; + timestamp?: InputMaybe; + timestamp_gt?: InputMaybe; + timestamp_gte?: InputMaybe; + timestamp_in?: InputMaybe>; + timestamp_lt?: InputMaybe; + timestamp_lte?: InputMaybe; + timestamp_not?: InputMaybe; + timestamp_not_in?: InputMaybe>; + transactionHash?: InputMaybe; + transactionHash_contains?: InputMaybe; + transactionHash_gt?: InputMaybe; + transactionHash_gte?: InputMaybe; + transactionHash_in?: InputMaybe>; + transactionHash_lt?: InputMaybe; + transactionHash_lte?: InputMaybe; + transactionHash_not?: InputMaybe; + transactionHash_not_contains?: InputMaybe; + transactionHash_not_in?: InputMaybe>; +}; + +export enum VaultMerkleRoot_OrderBy { + BlockNumber = 'blockNumber', + Epoch = 'epoch', + Id = 'id', + Root = 'root', + Timestamp = 'timestamp', + TransactionHash = 'transactionHash', +} + export type Withdrawal = { __typename?: 'Withdrawal'; amount: Scalars['BigInt']['output']; @@ -862,13 +978,19 @@ export type GetEpochTimestampHappenedInQuery = { epoches: Array<{ __typename?: 'Epoch'; epoch: number }>; }; -export type GetEpochsEndTimeQueryVariables = Exact<{ +export type GetEpochsStartEndTimeQueryVariables = Exact<{ lastEpoch?: InputMaybe; }>; -export type GetEpochsEndTimeQuery = { +export type GetEpochsStartEndTimeQuery = { __typename?: 'Query'; - epoches: Array<{ __typename?: 'Epoch'; epoch: number; toTs: any }>; + epoches: Array<{ + __typename?: 'Epoch'; + epoch: number; + toTs: any; + fromTs: any; + decisionWindow: any; + }>; }; export type GetLargestLockedAmountQueryVariables = Exact<{ [key: string]: never }>; @@ -890,6 +1012,20 @@ export type GetLockedSummaryLatestQuery = { } | null; }; +export type GetLockedSummarySnapshotsQueryVariables = Exact<{ + first?: InputMaybe; + skip?: InputMaybe; +}>; + +export type GetLockedSummarySnapshotsQuery = { + __typename?: 'Query'; + lockedSummarySnapshots: Array<{ + __typename?: 'LockedSummarySnapshot'; + lockedTotal: any; + timestamp: number; + }>; +}; + export type GetLockedsDataQueryVariables = Exact<{ first?: InputMaybe; skip?: InputMaybe; @@ -986,13 +1122,13 @@ export const GetEpochTimestampHappenedInDocument = { GetEpochTimestampHappenedInQuery, GetEpochTimestampHappenedInQueryVariables >; -export const GetEpochsEndTimeDocument = { +export const GetEpochsStartEndTimeDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'query', - name: { kind: 'Name', value: 'GetEpochsEndTime' }, + name: { kind: 'Name', value: 'GetEpochsStartEndTime' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -1018,6 +1154,8 @@ export const GetEpochsEndTimeDocument = { selections: [ { kind: 'Field', name: { kind: 'Name', value: 'epoch' } }, { kind: 'Field', name: { kind: 'Name', value: 'toTs' } }, + { kind: 'Field', name: { kind: 'Name', value: 'fromTs' } }, + { kind: 'Field', name: { kind: 'Name', value: 'decisionWindow' } }, ], }, }, @@ -1025,7 +1163,7 @@ export const GetEpochsEndTimeDocument = { }, }, ], -} as unknown as DocumentNode; +} as unknown as DocumentNode; export const GetLargestLockedAmountDocument = { kind: 'Document', definitions: [ @@ -1100,6 +1238,66 @@ export const GetLockedSummaryLatestDocument = { }, ], } as unknown as DocumentNode; +export const GetLockedSummarySnapshotsDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'GetLockedSummarySnapshots' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'first' } }, + type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, + defaultValue: { kind: 'IntValue', value: '1000' }, + }, + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'skip' } }, + type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, + defaultValue: { kind: 'IntValue', value: '0' }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'lockedSummarySnapshots' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'first' }, + value: { kind: 'Variable', name: { kind: 'Name', value: 'first' } }, + }, + { + kind: 'Argument', + name: { kind: 'Name', value: 'skip' }, + value: { kind: 'Variable', name: { kind: 'Name', value: 'skip' } }, + }, + { + kind: 'Argument', + name: { kind: 'Name', value: 'orderBy' }, + value: { kind: 'EnumValue', value: 'timestamp' }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'lockedTotal' } }, + { kind: 'Field', name: { kind: 'Name', value: 'timestamp' } }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + GetLockedSummarySnapshotsQuery, + GetLockedSummarySnapshotsQueryVariables +>; export const GetLockedsDataDocument = { kind: 'Document', definitions: [ diff --git a/client/src/hooks/helpers/useOnboardingSteps/index.tsx b/client/src/hooks/helpers/useOnboardingSteps/index.tsx index b71b64e503..98f88b5baa 100644 --- a/client/src/hooks/helpers/useOnboardingSteps/index.tsx +++ b/client/src/hooks/helpers/useOnboardingSteps/index.tsx @@ -37,11 +37,11 @@ const useOnboardingSteps = ( ...(isUserTOSAcceptedInitial === false ? [ { - header: i18n.t('views.onboarding.steps.usingTheApp.header'), + header: i18n.t('views.onboarding.stepsCommon.usingTheApp.header'), image: '/images/onboarding/octant.webp', text: ( -
{i18n.t('views.onboarding.steps.usingTheApp.text')}
+
{i18n.t('views.onboarding.stepsCommon.usingTheApp.text')}
), @@ -51,13 +51,13 @@ const useOnboardingSteps = ( ...(isUserEligibleToClaimGlm && glmClaimCheck?.value ? [ { - header: i18n.t('views.onboarding.steps.claimGlm.header'), + header: i18n.t('views.onboarding.stepsCommon.claimGlm.header'), image: 'images/tip-withdraw.webp', imageClassName: styles.claimGlm, text: ( - data.map(({ address, amount }) => ({ - address, - amount: parseUnits(amount, 'wei'), - })); + data + .map(({ address, amount }) => ({ + address, + amount: parseUnits(amount, 'wei'), + })) + .sort((a, b) => { + if (a.amount.gt(b.amount)) { + return 1; + } + if (a.amount.lt(b.amount)) { + return -1; + } + return 0; + }); export default function useProposalDonors( proposalAddress: string, + epoch?: number, options?: UseQueryOptions, ): UseQueryResult { const queryClient = useQueryClient(); const { data: currentEpoch } = useCurrentEpoch(); + /** + * Socket returns proposal donors for current epoch only. + * When hook is called for other epoch, subscribe should not be used. + */ useSubscription(WebsocketListenEvent.proposalDonors, data => { - queryClient.setQueryData(QUERY_KEYS.proposalDonors(proposalAddress), data); + // eslint-disable-next-line chai-friendly/no-unused-expressions + epoch + ? null + : queryClient.setQueryData( + QUERY_KEYS.proposalDonors(proposalAddress, currentEpoch! - 1), + data, + ); }); return useQuery( - QUERY_KEYS.proposalDonors(proposalAddress), - () => apiGetProposalDonors(proposalAddress, currentEpoch! - 1), + QUERY_KEYS.proposalDonors(proposalAddress, epoch || currentEpoch! - 1), + () => apiGetProposalDonors(proposalAddress, epoch || currentEpoch! - 1), { - enabled: !!currentEpoch && !!proposalAddress && currentEpoch > 1, + enabled: !!proposalAddress && (epoch !== undefined || !!(currentEpoch && currentEpoch > 1)), select: response => mapDataToProposalDonors(response), staleTime: Infinity, ...options, diff --git a/client/src/hooks/queries/useProposalsIpfs.ts b/client/src/hooks/queries/useProposalsIpfs.ts index a38e7953ae..df9cd52cae 100644 --- a/client/src/hooks/queries/useProposalsIpfs.ts +++ b/client/src/hooks/queries/useProposalsIpfs.ts @@ -1,9 +1,12 @@ import { useQueries, UseQueryResult } from '@tanstack/react-query'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { apiGetProposal } from 'api/calls/proposals'; import { QUERY_KEYS } from 'api/queryKeys'; import { ExtendedProposal } from 'types/extended-proposal'; import { BackendProposal } from 'types/gen/backendproposal'; +import triggerToast from 'utils/triggerToast'; import useProposalsCid from './useProposalsCid'; import useProposalsContract from './useProposalsContract'; @@ -13,6 +16,7 @@ export default function useProposalsIpfs(proposalsAddresses?: string[]): { isFetching: boolean; refetch: () => void; } { + const { t } = useTranslation('translation', { keyPrefix: 'api.errorMessage' }); const { data: proposalsCid, isFetching: isFetchingProposalsCid } = useProposalsCid(); const { refetch } = useProposalsContract(); @@ -21,9 +25,21 @@ export default function useProposalsIpfs(proposalsAddresses?: string[]): { enabled: !!address && !!proposalsCid, queryFn: () => apiGetProposal(`${proposalsCid}/${address}`), queryKey: QUERY_KEYS.proposalsIpfsResults(address), + retry: false, })), }); + const isAnyError = proposalsIpfsResults.some(element => element.isError); + useEffect(() => { + if (!isAnyError) { + return; + } + triggerToast({ + message: t('ipfs.message'), + type: 'error', + }); + }, [isAnyError, t]); + const isProposalsIpfsResultsFetching = isFetchingProposalsCid || proposalsIpfsResults.length === 0 || diff --git a/client/src/hooks/subgraph/useEpochsEndTime.ts b/client/src/hooks/subgraph/useEpochsStartEndTime.ts similarity index 55% rename from client/src/hooks/subgraph/useEpochsEndTime.ts rename to client/src/hooks/subgraph/useEpochsStartEndTime.ts index 330334d913..736146f7a1 100644 --- a/client/src/hooks/subgraph/useEpochsEndTime.ts +++ b/client/src/hooks/subgraph/useEpochsStartEndTime.ts @@ -4,34 +4,42 @@ import request from 'graphql-request'; import { QUERY_KEYS } from 'api/queryKeys'; import env from 'env'; import { graphql } from 'gql/gql'; -import { GetEpochsEndTimeQuery } from 'gql/graphql'; +import { GetEpochsStartEndTimeQuery } from 'gql/graphql'; import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; -type EpochsEndTime = { +type EpochsStartEndTime = { epoches: { + // decision window duration + decisionWindow: string; epoch: number; // timestamp + fromTs: string; + // timestamp toTs: string; }[]; }; -const GET_EPOCHS_END_TIME = graphql(` - query GetEpochsEndTime($lastEpoch: Int) { +const GET_EPOCHS_START_END_TIME = graphql(` + query GetEpochsStartEndTime($lastEpoch: Int) { epoches(first: $lastEpoch) { epoch toTs + fromTs + decisionWindow } } `); -export default function useEpochsEndTime(): UseQueryResult { +export default function useEpochsStartEndTime(): UseQueryResult< + EpochsStartEndTime['epoches'] | null +> { const { subgraphAddress } = env; const { data: currentEpoch } = useCurrentEpoch(); - return useQuery( + return useQuery( QUERY_KEYS.epochesEndTime(currentEpoch!), async () => - request(subgraphAddress, GET_EPOCHS_END_TIME, { + request(subgraphAddress, GET_EPOCHS_START_END_TIME, { lastEpoch: currentEpoch, }), { diff --git a/client/src/hooks/subgraph/useLockedSummarySnapshots.ts b/client/src/hooks/subgraph/useLockedSummarySnapshots.ts new file mode 100644 index 0000000000..d42760bcaf --- /dev/null +++ b/client/src/hooks/subgraph/useLockedSummarySnapshots.ts @@ -0,0 +1,78 @@ +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 { GetLockedSummarySnapshotsQuery } from 'gql/graphql'; +import getMetricsChartDataGroupedByDate, { + GroupedGlmAmountByDateItem, +} from 'utils/getMetricsChartDataGroupedByDate'; + +const GET_LOCKED_SUMMARY_SNAPSHOTS = graphql(` + query GetLockedSummarySnapshots($first: Int = 1000, $skip: Int = 0) { + lockedSummarySnapshots(first: $first, skip: $skip, orderBy: timestamp) { + lockedTotal + timestamp + } + } +`); + +type GroupedByDateItem = { + cummulativeGlmAmount: number; + dateTime: number; +}; + +type GroupedByDate = GroupedByDateItem[]; + +type UseLockedSummarySnapshotsResponse = + | { + groupedByDate: GroupedByDate; + } + | undefined; + +export default function useLockedSummarySnapshots(): UseQueryResult { + const { subgraphAddress } = env; + + return useQuery( + QUERY_KEYS.lockedSummarySnapshots, + async () => { + const pageSize = 1000; + const lockedSummarySnapshots: GetLockedSummarySnapshotsQuery['lockedSummarySnapshots'] = []; + + const fetchPage = async (first: number) => { + const data = await request(subgraphAddress, GET_LOCKED_SUMMARY_SNAPSHOTS, { + first, + skip: first - pageSize, + }); + + lockedSummarySnapshots.push(...data.lockedSummarySnapshots); + + if (data.lockedSummarySnapshots.length >= pageSize) { + await fetchPage(first + pageSize); + } + }; + + await fetchPage(pageSize); + + return { lockedSummarySnapshots }; + }, + { + refetchOnMount: false, + select: data => { + if (!data?.lockedSummarySnapshots) { + return undefined; + } + + const groupedByDate = getMetricsChartDataGroupedByDate( + data.lockedSummarySnapshots, + 'lockedSummarySnapshots', + ) as GroupedGlmAmountByDateItem[]; + + return { + groupedByDate, + }; + }, + }, + ); +} diff --git a/client/src/hooks/subgraph/useLockedsData.ts b/client/src/hooks/subgraph/useLockedsData.ts index c20f3610eb..0620fcd22f 100644 --- a/client/src/hooks/subgraph/useLockedsData.ts +++ b/client/src/hooks/subgraph/useLockedsData.ts @@ -1,14 +1,13 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import { getTime, startOfDay } from 'date-fns'; -import { formatUnits, parseUnits } from 'ethers/lib/utils'; import request from 'graphql-request'; -import sortBy from 'lodash/sortBy'; -import uniq from 'lodash/uniq'; import { QUERY_KEYS } from 'api/queryKeys'; import env from 'env'; import { graphql } from 'gql/gql'; import { GetLockedsDataQuery } from 'gql/graphql'; +import getMetricsChartDataGroupedByDate, { + GroupedUsersByDateItem, +} from 'utils/getMetricsChartDataGroupedByDate'; const GET_LOCKEDS_DATA = graphql(` query GetLockedsData($first: Int = 100, $skip: Int = 0) { @@ -21,7 +20,6 @@ const GET_LOCKEDS_DATA = graphql(` `); type GroupedByDateItem = { - cummulativeGlmAmount: number; dateTime: number; users: `0x${string}`[]; }; @@ -68,35 +66,10 @@ export default function useLockedsData(): UseQueryResult return undefined; } - const groupedByDate = sortBy(data.lockeds, l => l.timestamp).reduce( - (acc, curr) => { - // formatting from WEI to GLM (int) - const glmAmount = parseFloat(formatUnits(parseUnits(curr.amount, 'wei'))); - - // grouping by start of day in user's timezone - const dateTime = getTime(startOfDay(curr.timestamp * 1000)); - - const idx = acc.findIndex(v => v.dateTime === dateTime); - if (idx < 0) { - acc.push({ - cummulativeGlmAmount: - acc.length > 0 ? acc[acc.length - 1].cummulativeGlmAmount + glmAmount : glmAmount, - dateTime, - users: - acc.length > 0 ? uniq([...acc[acc.length - 1].users, curr.user]) : [curr.user], - }); - return acc; - } - - // eslint-disable-next-line operator-assignment - acc[idx].users = uniq([...acc[idx].users, curr.user]); - // eslint-disable-next-line operator-assignment - acc[idx].cummulativeGlmAmount = acc[idx].cummulativeGlmAmount + glmAmount; - - return acc; - }, - [], - ); + const groupedByDate = getMetricsChartDataGroupedByDate( + data.lockeds, + 'lockeds', + ) as GroupedUsersByDateItem[]; const totalAddressesWithoutDuplicates = groupedByDate[groupedByDate.length - 1].users.length; diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 2d4ef75be9..dfe0dc0beb 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -17,6 +17,9 @@ "default": { "title": "Sorry, something went wrong there", "message": "Please reload the app and try again" + }, + "ipfs": { + "message": "We seem to be having trouble loading data from IPFS, please hang on or try reloading" } } }, @@ -40,7 +43,8 @@ "thresholdDataUnavailable": "Threshold data unavailable", "noThresholdData": "No threshold data", "valueCantBeEmpty": "Value can't be empty", - "lessThan1m": "less than 1m" + "lessThan1m": "less than 1m", + "donors": "Donors" }, "components": { "settings": { @@ -138,9 +142,6 @@ "noDonationsYet": "No donations yet", "viewAll": "View all" }, - "donorsHeader": { - "donors": "Donors" - }, "glmLock": { "lock": "Lock", "unlock": "Unlock", @@ -218,15 +219,15 @@ "modalWithdrawEth": { "withdrawETH": "Withdraw ETH" }, - "proposalLoadingStates": { - "text": "Loading of a proposal
encountered an error." - }, "proposalRewards": { + "epoch": "Epoch {{epoch}}", "currentTotal": "Current total", "totalRaised": "Total raised", "totalDonated": "Total donated", + "fundedIn": "Funded in", "fundedAt": "Funded at", - "didNotReach": "Did not reach" + "didNotReach": "Did not reach", + "didNotReachThreshold": "Did not reach threshold" }, "proposalsList": { "epochArchive": "Epoch {{epoch}} Archive" @@ -340,7 +341,7 @@ } }, "onboarding": { - "stepsDecisionWindowOpen": { + "stepsCommon": { "usingTheApp": { "header": "Using the app", "text": "Before we can go any further, please read the terms of service and click the checkbox below to agree with them." @@ -353,7 +354,9 @@ "withdrawn": "GLM claimed" }, "signMessage": "Claim {{value}} GLMs" - }, + } + }, + "stepsDecisionWindowOpen": { "welcomeToOctant": { "header": "Welcome to Octant", "text": "To get started, lock some GLM in the <0>Earn view, and take a look at the projects you can donate to in the <0>Projects view.

If you already have GLM locked, you’ll have some rewards to use during E1 Allocation window, which runs from 19 Oct to 2 Nov." @@ -371,21 +374,7 @@ "text": "Once you have rewards, use the slider in the <0>Allocate view to divide your rewards into donations to projects or personal allocation.

Change your choices at any time during the Allocation window, just unlock the slider, make some edits and reconfirm in your wallet." } }, - "stepsDecisionWindowClosed": { - "usingTheApp": { - "header": "Using the app", - "text": "Before we can go any further, please read the terms of service and click the checkbox below to agree with them." - }, - "claimGlm": { - "header": "Claim your GLM", - "text": "As you hold an Epoch Zero Token and you participated in the Snapshot vote, you are eligible to claim {{value}} GLM. Click the button below to withdraw to your wallet.", - "buttonLabel": { - "withdraw": "Withdraw GLM", - "withdrawn": "GLM claimed" - }, - "signMessage": "Claim {{value}} GLMs" - }, "welcomeToOctant": { "header": "Welcome to Octant Epoch 2", "text": "To get started, lock some GLM in the <0>Earn view, and see how the Epoch 1 projects performed in the Epoch 1 archive.

If you made personal allocations in the previous epoch, your ETH will be available to withdraw in the <0>Earn view." diff --git a/client/src/mocks/subgraph/proposals.ts b/client/src/mocks/subgraph/proposals.ts index 7a84fc5a86..59e84c5ec4 100644 --- a/client/src/mocks/subgraph/proposals.ts +++ b/client/src/mocks/subgraph/proposals.ts @@ -2,7 +2,7 @@ import { BigNumber } from 'ethers'; import { ProposalIpfsWithRewards } from 'hooks/queries/useProposalsIpfsWithRewards'; -export const mockedProposalA: ProposalIpfsWithRewards = { +export const mockedProposalATotalValueOfAllocations1: ProposalIpfsWithRewards = { address: 'address', isLoadingError: false, name: 'A', @@ -10,45 +10,45 @@ export const mockedProposalA: ProposalIpfsWithRewards = { totalValueOfAllocations: BigNumber.from(1), }; -export const mockedProposalANoAllocation = { - ...mockedProposalA, +export const mockedProposalATotalValueOfAllocationsUndefined = { + ...mockedProposalATotalValueOfAllocations1, totalValueOfAllocations: undefined, }; -export const mockedProposalAHigherAllocation: ProposalIpfsWithRewards = { - ...mockedProposalA, +export const mockedProposalATotalValueOfAllocations2: ProposalIpfsWithRewards = { + ...mockedProposalATotalValueOfAllocations1, totalValueOfAllocations: BigNumber.from(2), }; -export const mockedProposalB: ProposalIpfsWithRewards = { - ...mockedProposalA, +export const mockedProposalBTotalValueOfAllocations2: ProposalIpfsWithRewards = { + ...mockedProposalATotalValueOfAllocations1, name: 'B', totalValueOfAllocations: BigNumber.from(2), }; -export const mockedProposalBNoAllocation = { - ...mockedProposalB, +export const mockedProposalBTotalValueOfAllocationsUndefined = { + ...mockedProposalBTotalValueOfAllocations2, totalValueOfAllocations: undefined, }; -export const mockedProposalC: ProposalIpfsWithRewards = { - ...mockedProposalA, +export const mockedProposalCTotalValueOfAllocations3: ProposalIpfsWithRewards = { + ...mockedProposalATotalValueOfAllocations1, name: 'C', totalValueOfAllocations: BigNumber.from(3), }; -export const mockedProposalCNoAllocation = { - ...mockedProposalC, +export const mockedProposalCTotalValueOfAllocationsUndefined = { + ...mockedProposalCTotalValueOfAllocations3, totalValueOfAllocations: undefined, }; -export const mockedProposalD: ProposalIpfsWithRewards = { - ...mockedProposalA, +export const mockedProposalDTotalValueOfAllocations4: ProposalIpfsWithRewards = { + ...mockedProposalATotalValueOfAllocations1, name: 'D', totalValueOfAllocations: BigNumber.from(4), }; -export const mockedProposalDNoAllocation = { - ...mockedProposalD, +export const mockedProposalDTotalValueOfAllocationsUndefined = { + ...mockedProposalDTotalValueOfAllocations4, totalValueOfAllocations: undefined, }; diff --git a/client/src/styles/utils/_variables.scss b/client/src/styles/utils/_variables.scss index 08b8270448..8c575be8aa 100644 --- a/client/src/styles/utils/_variables.scss +++ b/client/src/styles/utils/_variables.scss @@ -28,3 +28,4 @@ $layoutMarginHorizontal: 2.4rem; $progressStepperSlimStepPadding: 2.4rem; $modalVariantSmallPaddingMobile: 2.4rem; $modalVariantSmallPaddingDesktop: 5.6rem; +$proposalItemPadding: 2.4rem; diff --git a/client/src/utils/getFormattedEthValue.test.ts b/client/src/utils/getFormattedEthValue.test.ts index e70518f860..c6b0ff36dd 100644 --- a/client/src/utils/getFormattedEthValue.test.ts +++ b/client/src/utils/getFormattedEthValue.test.ts @@ -15,7 +15,17 @@ const testCases = [ { argument: BigNumber.from(10).pow(4).sub(1), expectedValue: '9999 WEI' }, { argument: BigNumber.from(10).pow(4), expectedValue: '10\u200a000 WEI' }, { argument: BigNumber.from(10).pow(5).sub(1), expectedValue: '99\u200a999 WEI' }, + { + argument: BigNumber.from(10).pow(5).sub(1), + expectedValue: '99\u200a999 WEI', + shouldIgnoreGwei: true, + }, { argument: BigNumber.from(10).pow(5), expectedValue: '0 GWEI' }, + { + argument: BigNumber.from(10).pow(5), + expectedValue: '< 0.0001 ETH', + shouldIgnoreGwei: true, + }, { argument: BigNumber.from(10).pow(6).sub(1), expectedValue: '0 GWEI' }, { argument: BigNumber.from(10).pow(6), expectedValue: '0 GWEI' }, { argument: BigNumber.from(10).pow(7).sub(1), expectedValue: '0 GWEI' }, @@ -23,6 +33,11 @@ const testCases = [ { argument: BigNumber.from(10).pow(8).sub(1), expectedValue: '0 GWEI' }, { argument: BigNumber.from(10).pow(8), expectedValue: '0 GWEI' }, { argument: BigNumber.from(10).pow(9).sub(1), expectedValue: '1 GWEI' }, + { + argument: BigNumber.from(10).pow(9).sub(1), + expectedValue: '< 0.0001 ETH', + shouldIgnoreGwei: true, + }, { argument: BigNumber.from(10).pow(9), expectedValue: '1 GWEI' }, { argument: BigNumber.from(10).pow(10).sub(1), expectedValue: '10 GWEI' }, { argument: BigNumber.from(10).pow(10), expectedValue: '10 GWEI' }, @@ -33,6 +48,11 @@ const testCases = [ { argument: BigNumber.from(10).pow(13).sub(1), expectedValue: '10\u200a000 GWEI' }, { argument: BigNumber.from(10).pow(13), expectedValue: '10\u200a000 GWEI' }, { argument: BigNumber.from(10).pow(14).sub(1), expectedValue: '100\u200a000 GWEI' }, + { + argument: BigNumber.from(10).pow(14).sub(1), + expectedValue: '< 0.0001 ETH', + shouldIgnoreGwei: true, + }, { argument: BigNumber.from(10).pow(14), expectedValue: '0.0001 ETH' }, { argument: BigNumber.from(10).pow(15).sub(1), expectedValue: '0.001 ETH' }, { argument: BigNumber.from(10).pow(15), expectedValue: '0.001 ETH' }, @@ -53,18 +73,20 @@ const testCases = [ ]; describe('getFormattedEthValue', () => { - for (const { argument, expectedValue } of testCases) { + for (const { argument, expectedValue, shouldIgnoreGwei } of testCases) { it(`returns ${expectedValue} for an argument ${formatUnits( argument, )} when isUsingHairSpace`, () => { - expect(getFormattedEthValue(argument).fullString).toBe(expectedValue); + expect(getFormattedEthValue(argument, true, shouldIgnoreGwei).fullString).toBe(expectedValue); }); const expectedValueNormalSpace = expectedValue.replace(/\u200a/g, ' '); it(`returns ${expectedValueNormalSpace} for an argument ${formatUnits( argument, )} when !isUsingHairSpace`, () => { - expect(getFormattedEthValue(argument, false).fullString).toBe(expectedValueNormalSpace); + expect(getFormattedEthValue(argument, false, shouldIgnoreGwei).fullString).toBe( + expectedValueNormalSpace, + ); }); } }); diff --git a/client/src/utils/getFormattedEthValue.ts b/client/src/utils/getFormattedEthValue.ts index 4ce4d37db4..406da642ce 100644 --- a/client/src/utils/getFormattedEthValue.ts +++ b/client/src/utils/getFormattedEthValue.ts @@ -11,7 +11,9 @@ const WEI_5 = BigNumber.from(10).pow(5); export default function getFormattedEthValue( value: BigNumber, + // eslint-disable-next-line default-param-last isUsingHairSpace = true, + shouldIgnoreGwei?: boolean, ): FormattedCryptoValue { let returnObject: Omit; @@ -22,6 +24,9 @@ export default function getFormattedEthValue( } else if (value.lt(WEI_5)) { returnObject = { suffix: 'WEI', value: formatUnits(value, 'wei') }; } else if (isInGweiRange) { + if (shouldIgnoreGwei) { + return { fullString: '< 0.0001 ETH', suffix: 'ETH', value: '< 0.0001' }; + } returnObject = { suffix: 'GWEI', value: formatUnits(value, 'gwei') }; } else { returnObject = { suffix: 'ETH', value: formatUnits(value) }; diff --git a/client/src/utils/getMetricsChartDataGroupedByDate.test.ts b/client/src/utils/getMetricsChartDataGroupedByDate.test.ts new file mode 100644 index 0000000000..456b2d8b35 --- /dev/null +++ b/client/src/utils/getMetricsChartDataGroupedByDate.test.ts @@ -0,0 +1,319 @@ +import getMetricsChartDataGroupedByDate from './getMetricsChartDataGroupedByDate'; + +const testCases = [ + { + dataType: 'lockeds', + inputData: [ + { + amount: '100000000000000000000', + timestamp: 1691520096, + user: '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + }, + { + amount: '250000000000000000000', + timestamp: 1692078408, + user: '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + }, + { + amount: '2000000000000000000000', + timestamp: 1691167452, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '100000000000000000000', + timestamp: 1691520072, + user: '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + }, + { + amount: '12000000000000000000', + timestamp: 1693885908, + user: '0x2b50777bf5657cee8d11b41a906a77387b34be84', + }, + { + amount: '100000000000000000000', + timestamp: 1691518800, + user: '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + }, + { + amount: '111000000000000000000', + timestamp: 1691513784, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '30000000000000000000', + timestamp: 1694063148, + user: '0x2b50777bf5657cee8d11b41a906a77387b34be84', + }, + { + amount: '1000000000000000000000', + timestamp: 1691414352, + user: '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + }, + { + amount: '400000000000000000000', + timestamp: 1693885272, + user: '0x2b50777bf5657cee8d11b41a906a77387b34be84', + }, + { + amount: '3000000000000000000000', + timestamp: 1691414196, + user: '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + }, + { + amount: '4000000000000000000000', + timestamp: 1691411496, + user: '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + }, + { + amount: '2000000000000000000000', + timestamp: 1691413836, + user: '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + }, + { + amount: '666000000000000000000', + timestamp: 1691416956, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '5000000000000000000', + timestamp: 1694064276, + user: '0x2b50777bf5657cee8d11b41a906a77387b34be84', + }, + { + amount: '3000000000000000000000', + timestamp: 1691517348, + user: '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + }, + { + amount: '111000000000000000000', + timestamp: 1691513256, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '100000000000000000000000000', + timestamp: 1691334144, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '100000000000000000000', + timestamp: 1691520060, + user: '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + }, + { + amount: '100000000000000000000000000', + timestamp: 1691421540, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '222000000000000000000', + timestamp: 1691514468, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '555000000000000000000', + timestamp: 1692353928, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '1000000000000000000000', + timestamp: 1695213252, + user: '0x26fa7fc3d3801d039ca313e759a55088e19efd1b', + }, + { + amount: '123000000000000000000', + timestamp: 1692622800, + user: '0xfc9527820a76b515a2c66c22e0575501dedd8281', + }, + { + amount: '2000000000000000000000', + timestamp: 1692158796, + user: '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + }, + { + amount: '3000000000000000000000', + timestamp: 1691517276, + user: '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + }, + { + amount: '1000000000000000000000', + timestamp: 1695205176, + user: '0x26fa7fc3d3801d039ca313e759a55088e19efd1b', + }, + { + amount: '100000000000000000000', + timestamp: 1693947888, + user: '0x1078daa844cdf1edb51e5189c8b113b80a6a6957', + }, + ], + outputData: [ + { dateTime: 1691107200000, users: ['0xfc9527820a76b515a2c66c22e0575501dedd8281'] }, + { dateTime: 1691280000000, users: ['0xfc9527820a76b515a2c66c22e0575501dedd8281'] }, + { + dateTime: 1691366400000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + ], + }, + { + dateTime: 1691452800000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + ], + }, + { + dateTime: 1692057600000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + ], + }, + { + dateTime: 1692144000000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + ], + }, + { + dateTime: 1692316800000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + ], + }, + { + dateTime: 1692576000000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + ], + }, + { + dateTime: 1693872000000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + '0x2b50777bf5657cee8d11b41a906a77387b34be84', + '0x1078daa844cdf1edb51e5189c8b113b80a6a6957', + ], + }, + { + dateTime: 1694044800000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + '0x2b50777bf5657cee8d11b41a906a77387b34be84', + '0x1078daa844cdf1edb51e5189c8b113b80a6a6957', + ], + }, + { + dateTime: 1695168000000, + users: [ + '0xfc9527820a76b515a2c66c22e0575501dedd8281', + '0xa0facbd53826095f65cbe48f43ddba293d8fd19b', + '0xe5e11cc5fb894ef5a9d7da768cfb17066b9d35d7', + '0x49d4b4cb01971736e1b3996121d4ddea7733fb3f', + '0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e', + '0x2b50777bf5657cee8d11b41a906a77387b34be84', + '0x1078daa844cdf1edb51e5189c8b113b80a6a6957', + '0x26fa7fc3d3801d039ca313e759a55088e19efd1b', + ], + }, + ], + }, + { + dataType: 'lockedSummarySnapshots', + inputData: [ + { lockedTotal: '2000000000000000000000', timestamp: 1691167452 }, + { lockedTotal: '100002000000000000000000000', timestamp: 1691334144 }, + { lockedTotal: '0', timestamp: 1691334252 }, + { lockedTotal: '4000000000000000000000', timestamp: 1691411496 }, + { lockedTotal: '2000000000000000000000', timestamp: 1691412612 }, + { lockedTotal: '4000000000000000000000', timestamp: 1691413836 }, + { lockedTotal: '1000000000000000000000', timestamp: 1691414040 }, + { lockedTotal: '4000000000000000000000', timestamp: 1691414196 }, + { lockedTotal: '3000000000000000000000', timestamp: 1691414280 }, + { lockedTotal: '4000000000000000000000', timestamp: 1691414352 }, + { lockedTotal: '4666000000000000000000', timestamp: 1691416956 }, + { lockedTotal: '100004666000000000000000000', timestamp: 1691421540 }, + { lockedTotal: '1004666000000000000000000', timestamp: 1691512452 }, + { lockedTotal: '1004777000000000000000000', timestamp: 1691513256 }, + { lockedTotal: '1004888000000000000000000', timestamp: 1691513784 }, + { lockedTotal: '1004666000000000000000000', timestamp: 1691514312 }, + { lockedTotal: '1004888000000000000000000', timestamp: 1691514468 }, + { lockedTotal: '1004777000000000000000000', timestamp: 1691514504 }, + { lockedTotal: '1007777000000000000000000', timestamp: 1691517276 }, + { lockedTotal: '1010777000000000000000000', timestamp: 1691517348 }, + { lockedTotal: '1010877000000000000000000', timestamp: 1691518800 }, + { lockedTotal: '1010977000000000000000000', timestamp: 1691520060 }, + { lockedTotal: '1011077000000000000000000', timestamp: 1691520072 }, + { lockedTotal: '1011177000000000000000000', timestamp: 1691520096 }, + { lockedTotal: '1011427000000000000000000', timestamp: 1692078408 }, + { lockedTotal: '1013427000000000000000000', timestamp: 1692158796 }, + { lockedTotal: '1012761000000000000000000', timestamp: 1692353808 }, + { lockedTotal: '1013316000000000000000000', timestamp: 1692353928 }, + { lockedTotal: '1013439000000000000000000', timestamp: 1692622800 }, + { lockedTotal: '1013839000000000000000000', timestamp: 1693885272 }, + { lockedTotal: '1013851000000000000000000', timestamp: 1693885908 }, + { lockedTotal: '1013951000000000000000000', timestamp: 1693947888 }, + { lockedTotal: '1013981000000000000000000', timestamp: 1694063148 }, + { lockedTotal: '1013986000000000000000000', timestamp: 1694064276 }, + { lockedTotal: '1014986000000000000000000', timestamp: 1695205176 }, + { lockedTotal: '1013986000000000000000000', timestamp: 1695206520 }, + { lockedTotal: '1014986000000000000000000', timestamp: 1695213252 }, + { lockedTotal: '1013986000000000000000000', timestamp: 1695213408 }, + { lockedTotal: '1013539000000000000000000', timestamp: 1696810860 }, + { lockedTotal: '1013289000000000000000000', timestamp: 1696903644 }, + ], + outputData: [ + { cummulativeGlmAmount: 2000, dateTime: 1691107200000 }, + { cummulativeGlmAmount: 0, dateTime: 1691280000000 }, + { cummulativeGlmAmount: 100004666, dateTime: 1691366400000 }, + { cummulativeGlmAmount: 1011177, dateTime: 1691452800000 }, + { cummulativeGlmAmount: 1011427, dateTime: 1692057600000 }, + { cummulativeGlmAmount: 1013427, dateTime: 1692144000000 }, + { cummulativeGlmAmount: 1013316, dateTime: 1692316800000 }, + { cummulativeGlmAmount: 1013439, dateTime: 1692576000000 }, + { cummulativeGlmAmount: 1013951, dateTime: 1693872000000 }, + { cummulativeGlmAmount: 1013986, dateTime: 1694044800000 }, + { cummulativeGlmAmount: 1013986, dateTime: 1695168000000 }, + { cummulativeGlmAmount: 1013539, dateTime: 1696809600000 }, + { cummulativeGlmAmount: 1013289, dateTime: 1696896000000 }, + ], + }, +]; + +describe('getMetricsChartDataGroupedByDate', () => { + for (const { dataType, inputData, outputData } of testCases) { + it(`returns ${JSON.stringify( + outputData, + )} for ${dataType} dataType and inputData: ${JSON.stringify(inputData)}`, () => { + expect( + getMetricsChartDataGroupedByDate( + inputData, + dataType as 'lockedSummarySnapshots' | 'lockeds', + ), + ).toStrictEqual(outputData); + }); + } +}); diff --git a/client/src/utils/getMetricsChartDataGroupedByDate.ts b/client/src/utils/getMetricsChartDataGroupedByDate.ts new file mode 100644 index 0000000000..f03e15b83b --- /dev/null +++ b/client/src/utils/getMetricsChartDataGroupedByDate.ts @@ -0,0 +1,53 @@ +import { getTime, startOfDay } from 'date-fns'; +import { formatUnits, parseUnits } from 'ethers/lib/utils'; +import { sortBy, uniq } from 'lodash'; + +export type GroupedGlmAmountByDateItem = { + cummulativeGlmAmount: number; + dateTime: number; +}; + +export type GroupedUsersByDateItem = { + dateTime: number; + users: `0x${string}`[]; +}; + +type GroupedByDate = GroupedGlmAmountByDateItem[] | GroupedUsersByDateItem[]; + +const getMetricsChartDataGroupedByDate = ( + data: any[], + dataType: 'lockeds' | 'lockedSummarySnapshots', +): GroupedByDate => + sortBy(data, l => l.timestamp).reduce((acc, curr) => { + // grouping by start of day in user's timezone + const dateTime = getTime(startOfDay(curr.timestamp * 1000)); + + const idx = acc.findIndex(v => v.dateTime === dateTime); + if (idx < 0) { + acc.push({ + dateTime, + ...(dataType === 'lockeds' + ? { + users: acc.length > 0 ? uniq([...acc[acc.length - 1].users, curr.user]) : [curr.user], + } + : { + // formatting from WEI to GLM (int) + cummulativeGlmAmount: parseFloat(formatUnits(parseUnits(curr.lockedTotal, 'wei'))), + }), + }); + return acc; + } + + if (dataType === 'lockeds') { + // eslint-disable-next-line operator-assignment + acc[idx].users = uniq([...acc[idx].users, curr.user]); + } else { + // formatting from WEI to GLM (int) + // eslint-disable-next-line operator-assignment + acc[idx].cummulativeGlmAmount = parseFloat(formatUnits(parseUnits(curr.lockedTotal, 'wei'))); + } + + return acc; + }, []); + +export default getMetricsChartDataGroupedByDate; diff --git a/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.test.ts b/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.test.ts index 0d3cae4055..b6f8544b80 100644 --- a/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.test.ts +++ b/client/src/utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical.test.ts @@ -1,47 +1,92 @@ import { - mockedProposalA, - mockedProposalB, - mockedProposalC, - mockedProposalBNoAllocation, - mockedProposalD, - mockedProposalCNoAllocation, - mockedProposalDNoAllocation, - mockedProposalANoAllocation, - mockedProposalAHigherAllocation, + mockedProposalATotalValueOfAllocations1, + mockedProposalBTotalValueOfAllocations2, + mockedProposalBTotalValueOfAllocationsUndefined, + mockedProposalDTotalValueOfAllocationsUndefined, + mockedProposalCTotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocations2, + mockedProposalATotalValueOfAllocationsUndefined, + mockedProposalCTotalValueOfAllocations3, + mockedProposalDTotalValueOfAllocations4, } from 'mocks/subgraph/proposals'; import getSortedElementsByTotalValueOfAllocationsAndAlphabetical from './getSortedElementsByTotalValueOfAllocationsAndAlphabetical'; +const expectedValueRoot = [ + mockedProposalDTotalValueOfAllocations4, + mockedProposalCTotalValueOfAllocations3, + mockedProposalATotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations2, + mockedProposalBTotalValueOfAllocations2, + mockedProposalBTotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations1, + mockedProposalATotalValueOfAllocations1, + mockedProposalATotalValueOfAllocationsUndefined, + mockedProposalBTotalValueOfAllocationsUndefined, + mockedProposalCTotalValueOfAllocationsUndefined, + mockedProposalDTotalValueOfAllocationsUndefined, +]; + +const testCases = [ + { + argument: [ + mockedProposalATotalValueOfAllocations1, + mockedProposalBTotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations2, + mockedProposalCTotalValueOfAllocations3, + mockedProposalBTotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations1, + mockedProposalBTotalValueOfAllocationsUndefined, + mockedProposalDTotalValueOfAllocations4, + mockedProposalCTotalValueOfAllocationsUndefined, + mockedProposalDTotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocations2, + ], + expectedValue: expectedValueRoot, + }, + { + argument: [ + mockedProposalBTotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocations1, + mockedProposalBTotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations2, + mockedProposalCTotalValueOfAllocations3, + mockedProposalBTotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations1, + mockedProposalDTotalValueOfAllocations4, + mockedProposalCTotalValueOfAllocationsUndefined, + mockedProposalDTotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocations2, + ], + expectedValue: expectedValueRoot, + }, + { + argument: [ + mockedProposalBTotalValueOfAllocationsUndefined, + mockedProposalCTotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocations1, + mockedProposalBTotalValueOfAllocations2, + mockedProposalATotalValueOfAllocations2, + mockedProposalCTotalValueOfAllocations3, + mockedProposalBTotalValueOfAllocations2, + mockedProposalDTotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocationsUndefined, + mockedProposalATotalValueOfAllocations1, + mockedProposalDTotalValueOfAllocations4, + mockedProposalATotalValueOfAllocations2, + ], + expectedValue: expectedValueRoot, + }, +]; + describe('getSortedElementsByTotalValueOfAllocationsAndAlphabetical', () => { - it('properly sorts elements', () => { - expect( - getSortedElementsByTotalValueOfAllocationsAndAlphabetical([ - mockedProposalA, - mockedProposalB, - mockedProposalAHigherAllocation, - mockedProposalC, - mockedProposalB, - mockedProposalA, - mockedProposalBNoAllocation, - mockedProposalD, - mockedProposalCNoAllocation, - mockedProposalDNoAllocation, - mockedProposalANoAllocation, - mockedProposalAHigherAllocation, - ]), - ).toEqual([ - mockedProposalD, - mockedProposalC, - mockedProposalAHigherAllocation, - mockedProposalAHigherAllocation, - mockedProposalB, - mockedProposalB, - mockedProposalA, - mockedProposalA, - mockedProposalANoAllocation, - mockedProposalBNoAllocation, - mockedProposalCNoAllocation, - mockedProposalDNoAllocation, - ]); - }); + for (const { argument, expectedValue } of testCases) { + it('properly returns expectedValue', () => { + expect(getSortedElementsByTotalValueOfAllocationsAndAlphabetical(argument)).toEqual( + expectedValue, + ); + }); + } }); diff --git a/client/src/utils/getValueCryptoToDisplay.ts b/client/src/utils/getValueCryptoToDisplay.ts index ea14beb1d9..48e2e5d92a 100644 --- a/client/src/utils/getValueCryptoToDisplay.ts +++ b/client/src/utils/getValueCryptoToDisplay.ts @@ -8,6 +8,7 @@ import getFormattedGlmValue from './getFormattedGlmValue'; export type ValueCryptoToDisplay = { cryptoCurrency?: CryptoCurrency; isUsingHairSpace?: boolean; + shouldIgnoreGwei?: boolean; valueCrypto?: BigNumber; }; @@ -15,8 +16,9 @@ export default function getValueCryptoToDisplay({ cryptoCurrency, isUsingHairSpace = true, valueCrypto = BigNumber.from(0), + shouldIgnoreGwei, }: ValueCryptoToDisplay): string { return cryptoCurrency === 'ethereum' - ? getFormattedEthValue(valueCrypto, isUsingHairSpace).fullString + ? getFormattedEthValue(valueCrypto, isUsingHairSpace, shouldIgnoreGwei).fullString : getFormattedGlmValue(valueCrypto, isUsingHairSpace).fullString; } diff --git a/client/src/utils/getValueFiatToDisplay.test.ts b/client/src/utils/getValueFiatToDisplay.test.ts index a2512ec30c..c7c235dfd6 100644 --- a/client/src/utils/getValueFiatToDisplay.test.ts +++ b/client/src/utils/getValueFiatToDisplay.test.ts @@ -44,14 +44,6 @@ describe('getValueFiatToDisplay', () => { }); it('should return 0.00 when there is a parameter missing', () => { - expect( - // @ts-expect-error error here is caused by lack of typing for defaultProps. - getValueFiatToDisplay({ - ...defaultProps, - cryptoCurrency: undefined, - }), - ).toEqual('$0.00'); - expect( // @ts-expect-error error here is caused by lack of typing for defaultProps. getValueFiatToDisplay({ diff --git a/client/src/utils/getValueFiatToDisplay.ts b/client/src/utils/getValueFiatToDisplay.ts index 34dc3d6d48..245eaa078e 100644 --- a/client/src/utils/getValueFiatToDisplay.ts +++ b/client/src/utils/getValueFiatToDisplay.ts @@ -34,11 +34,17 @@ export default function getValueFiatToDisplay({ * We need to ensure particular cryptoValues[cryptoCurrency][displayCurrency] is already fetched. * Otherwise, Cypress tests failed when changing the displayCurrency * and requesting to see its fiat value immediately. + * + * We need to ensure cryptoValues[cryptoCurrency] is defined too. + * For the reason unknown coin-prices-server sometimes returns data (cryptoValues defined), + * yet cryptoValues[cryptoCurrency] is unknown, resulting in a crash. + * This happens in E2E runs only. */ // if ( !cryptoCurrency || !cryptoValues || + !cryptoValues[cryptoCurrency] || !cryptoValues[cryptoCurrency][displayCurrency] || !valueCrypto ) { diff --git a/client/src/views/AllocationView/AllocationView.tsx b/client/src/views/AllocationView/AllocationView.tsx index 67c056237a..ff9f5c15be 100644 --- a/client/src/views/AllocationView/AllocationView.tsx +++ b/client/src/views/AllocationView/AllocationView.tsx @@ -322,15 +322,15 @@ const AllocationView = (): ReactElement => { const isEpoch1 = currentEpoch === 1; + const showAllocationBottomNavigation = + !isEpoch1 && areAllocationsAvailableOrAlreadyDone && hasUserIndividualReward && !isLocked; + return ( { )} {areAllocationsAvailableOrAlreadyDone && ( - {allocationsWithRewards!.map((allocation, index) => ( + {allocationsWithRewards!.map(allocation => ( ))} diff --git a/client/src/views/ProposalView/ProposalView.module.scss b/client/src/views/ProposalView/ProposalView.module.scss index 1f647dd6dc..a466bdf46e 100644 --- a/client/src/views/ProposalView/ProposalView.module.scss +++ b/client/src/views/ProposalView/ProposalView.module.scss @@ -98,23 +98,10 @@ } .buttonAddToAllocate { - &Primary { - margin-left: 3.2rem; + margin-left: 3.2rem; - @media #{$desktop-up} { - display: none; - margin-left: 2.6rem; - - &.isEpoch1 { - display: initial; - } - } - } - &Secondary { - display: none; - @media #{$desktop-up} { - display: initial; - } + @media #{$desktop-up} { + margin-left: 2.6rem; } } diff --git a/client/src/views/ProposalView/ProposalView.tsx b/client/src/views/ProposalView/ProposalView.tsx index c85f8503a8..0de99086a1 100644 --- a/client/src/views/ProposalView/ProposalView.tsx +++ b/client/src/views/ProposalView/ProposalView.tsx @@ -59,6 +59,9 @@ const ProposalView = (): ReactElement => { const { data: areCurrentEpochsProjectsHiddenOutsideAllocationWindow } = useAreCurrentEpochsProjectsHiddenOutsideAllocationWindow(); + const isArchivedProposal = + epochUrl && currentEpoch ? parseInt(epochUrl!, 10) < currentEpoch : false; + useEffect(() => { if (loadedAddresses.length === 0) { setLoadedAddresses([proposalAddressUrl!]); @@ -241,6 +244,7 @@ const ProposalView = (): ReactElement => { isAllocatedTo: !!userAllocations?.elements.find( ({ address: userAllocationAddress }) => userAllocationAddress === address, ), + isArchivedProposal, onClick: () => onAddRemoveFromAllocate(address), }; return ( @@ -273,11 +277,8 @@ const ProposalView = (): ReactElement => { />
@@ -296,17 +297,9 @@ const ProposalView = (): ReactElement => { {!isEpoch1 ? ( - ) - } + epoch={isArchivedProposal ? parseInt(epochUrl!, 10) : undefined} + isProposalView /> ) : (
diff --git a/payouts-verification/src/Comparison/index.tsx b/payouts-verification/src/Comparison/index.tsx index ba941bd3e3..0e55a9323b 100644 --- a/payouts-verification/src/Comparison/index.tsx +++ b/payouts-verification/src/Comparison/index.tsx @@ -21,7 +21,7 @@ const Comparison: FC = ({ uploadedMerkleTree }) => { axios // @ts-expect-error TS does not understand the way vite imports envs. - .get(`${import.meta.env.VITE_SERVER_ENDPOINT}${epoch}`) + .get(`${import.meta.env.VITE_SERVER_ENDPOINT}rewards/merkle_tree/${epoch}`) .then(({ data }) => { const serverMerkleTreeRoot = data.root; setRootsAreTheSame(serverMerkleTreeRoot === uploadedMerkleTree.root);