From 0a08f54ce5661e45467b53aec97febe12648cc35 Mon Sep 17 00:00:00 2001 From: Corantin Noll Date: Mon, 23 May 2022 17:10:53 -0400 Subject: [PATCH 01/12] Hot fix cleanup duplication of fetch balance --- packages/react-app/src/components/quest.tsx | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/packages/react-app/src/components/quest.tsx b/packages/react-app/src/components/quest.tsx index a8bd7ec7..a309b8a5 100644 --- a/packages/react-app/src/components/quest.tsx +++ b/packages/react-app/src/components/quest.tsx @@ -173,31 +173,12 @@ export default function Quest({ }, 500); } } - }, [transaction?.type, transaction?.status]); + }, [transaction?.type, transaction?.status, transaction?.args?.questAddress]); useEffect(() => { setState(questData.state); }, [questData.state]); - useEffect(() => { - // If tx completion impact Quest bounty, update it - if ( - transaction?.status === ENUM_TRANSACTION_STATUS.Confirmed && - transaction.args?.questAddress === questData.address && - (transaction?.type === 'ClaimChallengeResolve' || - transaction?.type === 'ClaimExecute' || - transaction?.type === 'QuestFund' || - transaction?.type === 'QuestReclaimFunds') - ) { - setBounty(undefined); - setTimeout(() => { - if (questData.address && questData.rewardToken) { - fetchBalanceOfQuest(questData.address, questData.rewardToken); - } - }, 500); - } - }, [transaction?.type, transaction?.status, transaction?.args?.questAddress]); - useEffect(() => { if (!questData.rewardToken) { setBounty(undefined); From c228ddb002fae7b07f4fa4d122d146deac3df504 Mon Sep 17 00:00:00 2001 From: KevinMansour <77366744+KevinMansour@users.noreply.github.com> Date: Mon, 23 May 2022 18:32:15 -0400 Subject: [PATCH 02/12] fixed claim button bugg --- packages/react-app/src/components/claim-list.tsx | 9 +-------- packages/react-app/src/components/claim.tsx | 11 ++--------- packages/react-app/src/components/quest.tsx | 7 +++---- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/react-app/src/components/claim-list.tsx b/packages/react-app/src/components/claim-list.tsx index 41caa09c..dc2e5684 100644 --- a/packages/react-app/src/components/claim-list.tsx +++ b/packages/react-app/src/components/claim-list.tsx @@ -43,18 +43,12 @@ const BoxStyled = styled(Box)` type Props = { questData: QuestModel; challengeDeposit: TokenAmountModel; - questTotalBounty?: TokenAmountModel | null; isLoading?: boolean; }; const loadingClaim = { state: ENUM_CLAIM_STATE.None } as ClaimModel; -export default function ClaimList({ - questData, - challengeDeposit, - questTotalBounty, - isLoading = false, -}: Props) { +export default function ClaimList({ questData, challengeDeposit, isLoading = false }: Props) { const [claims, setClaims] = useState([loadingClaim]); const [isLoadingState, setIsLoading] = useState(isLoading); const { transaction } = useTransactionContext(); @@ -139,7 +133,6 @@ export default function ClaimList({ , diff --git a/packages/react-app/src/components/claim.tsx b/packages/react-app/src/components/claim.tsx index a7b9411a..0dfb337d 100644 --- a/packages/react-app/src/components/claim.tsx +++ b/packages/react-app/src/components/claim.tsx @@ -31,18 +31,11 @@ const AddressWrapperStyled = styled.div<{ isSmallScreen: boolean }>` type Props = { claim: ClaimModel; isLoading?: boolean; - questTotalBounty?: TokenAmountModel | null; challengeDeposit: TokenAmountModel; questData: QuestModel; }; -export default function Claim({ - claim, - isLoading, - questTotalBounty, - challengeDeposit, - questData, -}: Props) { +export default function Claim({ claim, isLoading, challengeDeposit, questData }: Props) { const { walletAddress } = useWallet(); const { transaction } = useTransactionContext(); const [state, setState] = useState(claim.state); @@ -61,7 +54,7 @@ export default function Claim({ setActionButton( , ); diff --git a/packages/react-app/src/components/quest.tsx b/packages/react-app/src/components/quest.tsx index a8bd7ec7..d814ee90 100644 --- a/packages/react-app/src/components/quest.tsx +++ b/packages/react-app/src/components/quest.tsx @@ -115,7 +115,7 @@ export default function Quest({ }: Props) { const { walletAddress } = useWallet(); const history = useHistory(); - const [bounty, setBounty] = useState(); + const [bounty, setBounty] = useState(questData?.bounty); const [highlight, setHighlight] = useState(true); const [claimDeposit, setClaimDeposit] = useState(); const [isDepositReleased, setIsDepositReleased] = useState(false); @@ -340,10 +340,9 @@ export default function Quest({ /> {isSummary && fieldsRow} - {!isSummary && challengeDeposit && ( + {!isSummary && challengeDeposit && bounty && ( From ec029703205665db5bd9a7ccd239f89f395aeb97 Mon Sep 17 00:00:00 2001 From: Corantin Noll Date: Wed, 25 May 2022 16:19:32 -0400 Subject: [PATCH 03/12] Fix ipfs warning + refactor claim to refresh data --- .../react-app/src/components/claim-list.tsx | 6 +- packages/react-app/src/components/claim.tsx | 74 ++++++++++++---- .../src/components/modals/challenge-modal.tsx | 69 +++------------ .../components/modals/execute-claim-modal.tsx | 88 +++++++------------ packages/react-app/src/hooks/use-hooks.ts | 37 ++++++++ 5 files changed, 142 insertions(+), 132 deletions(-) create mode 100644 packages/react-app/src/hooks/use-hooks.ts diff --git a/packages/react-app/src/components/claim-list.tsx b/packages/react-app/src/components/claim-list.tsx index dc2e5684..ef1195cd 100644 --- a/packages/react-app/src/components/claim-list.tsx +++ b/packages/react-app/src/components/claim-list.tsx @@ -6,7 +6,7 @@ import { ENUM_CLAIM_STATE, ENUM_TRANSACTION_STATUS } from 'src/constants'; import { TokenAmountModel } from 'src/models/token-amount.model'; import { QuestModel } from 'src/models/quest.model'; import { useEffect, useState } from 'react'; -import { getObjectFromIpfsSafe } from 'src/services/ipfs.service'; +import { getObjectFromIpfs, ipfsTheGraph } from 'src/services/ipfs.service'; import { useTransactionContext } from 'src/contexts/transaction.context'; import { HelpTooltip } from './field-input/help-tooltip'; import * as QuestService from '../services/quest.service'; @@ -83,8 +83,8 @@ export default function ClaimList({ questData, challengeDeposit, isLoading = fal const fetchClaimsUntilNew = (claimsCount?: number) => { if (!claimsCount) { - setClaims([loadingClaim, ...claims]); claimsCount = claims.length; + setClaims([loadingClaim, ...claims]); } setTimeout(async () => { const results = await QuestService.fetchQuestClaims(questData); @@ -113,7 +113,7 @@ export default function ClaimList({ questData, challengeDeposit, isLoading = fal result.map(async (claim) => ({ ...claim, evidence: claim.evidenceIpfsHash - ? await getObjectFromIpfsSafe(claim.evidenceIpfsHash) + ? await getObjectFromIpfs(claim.evidenceIpfsHash, ipfsTheGraph) : 'No evidence', })), ); diff --git a/packages/react-app/src/components/claim.tsx b/packages/react-app/src/components/claim.tsx index 0dfb337d..90a2c3a5 100644 --- a/packages/react-app/src/components/claim.tsx +++ b/packages/react-app/src/components/claim.tsx @@ -1,11 +1,13 @@ -import { useViewport } from '@1hive/1hive-ui'; -import { ReactNode, useEffect, useState } from 'react'; +import { useViewport, Timer } from '@1hive/1hive-ui'; +import { ReactNode, useEffect, useMemo, useState } from 'react'; import { ENUM_CLAIM_STATE, ENUM_DISPUTE_STATES, ENUM_TRANSACTION_STATUS } from 'src/constants'; import { useTransactionContext } from 'src/contexts/transaction.context'; import { useWallet } from 'src/contexts/wallet.context'; +import { useTimeout } from 'src/hooks/use-hooks'; import { ClaimModel } from 'src/models/claim.model'; import { QuestModel } from 'src/models/quest.model'; import { TokenAmountModel } from 'src/models/token-amount.model'; +import { compareCaseInsensitive } from 'src/utils/string.util'; import styled, { css } from 'styled-components'; import { AddressFieldInput } from './field-input/address-field-input'; import AmountFieldInput from './field-input/amount-field-input'; @@ -42,37 +44,75 @@ export default function Claim({ claim, isLoading, challengeDeposit, questData }: const { below } = useViewport(); const [waitForClose, setWaitForClose] = useState(false); const [actionButton, setActionButton] = useState(); + const [isClaimable, setIsClaimable] = useState(false); + + useTimeout(() => { + setIsClaimable(true); + }, Math.max((claim.executionTimeMs ?? 0) - Date.now(), 0)); useEffect(() => { setState(claim.state); }, [claim.state]); + const timer = useMemo( + () => + claim.executionTimeMs && + claim.executionTimeMs - Date.now() > 0 && , + [claim.executionTimeMs], + ); + + const onActionClose = () => { + setWaitForClose(false); + }; + + const executeClaimModal = useMemo( + () => ( + + ), + [claim, questData.bounty, isClaimable], + ); + + const challengeModal = useMemo( + () => ( + + ), + [claim, challengeDeposit], + ); + + const resolveChallengeModal = useMemo( + () => , + [claim], + ); + useEffect(() => { if (waitForClose || !state) return; if (state === ENUM_CLAIM_STATE.Scheduled) { - if (walletAddress === claim.playerAddress) { + if (compareCaseInsensitive(walletAddress, claim.playerAddress) || isClaimable) { setActionButton( - , + <> + {executeClaimModal} + {timer} + , ); } else { setActionButton( - , + <> + {challengeModal} + {timer} + , ); } } else if (state === ENUM_CLAIM_STATE.Challenged) { - setActionButton(); + setActionButton(resolveChallengeModal); } else { setActionButton(undefined); } - }, [state, walletAddress, waitForClose]); + }, [state, walletAddress, waitForClose, isClaimable]); useEffect(() => { // If tx completion impact Claims, update them @@ -108,10 +148,6 @@ export default function Claim({ claim, isLoading, challengeDeposit, questData }: } }, [transaction?.status, transaction?.type, transaction?.[0], claim.container]); - const onActionClose = () => { - setWaitForClose(false); - }; - return (
diff --git a/packages/react-app/src/components/modals/challenge-modal.tsx b/packages/react-app/src/components/modals/challenge-modal.tsx index bcfee623..182ddae4 100644 --- a/packages/react-app/src/components/modals/challenge-modal.tsx +++ b/packages/react-app/src/components/modals/challenge-modal.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-nested-ternary */ -import { Button, useToast, IconFlag, Timer } from '@1hive/1hive-ui'; +import { Button, useToast, IconFlag } from '@1hive/1hive-ui'; import { noop, uniqueId } from 'lodash-es'; import { useState, useRef, useEffect, useMemo } from 'react'; import styled from 'styled-components'; @@ -51,8 +51,7 @@ export default function ChallengeModal({ claim, challengeDeposit, onClose = noop const toast = useToast(); const [loading, setLoading] = useState(false); const [opened, setOpened] = useState(false); - const [challengeTimeout, setChallengedTimeout] = useState(undefined); - const [buttonLabel, setOpenButtonLabel] = useState(); + // const [challengeTimeout, setChallengedTimeout] = useState(undefined); const [isEnoughBalance, setIsEnoughBalance] = useState(false); const [isFeeDepositSameToken, setIsFeeDepositSameToken] = useState(); const [challengeFee, setChallengeFee] = useState(undefined); @@ -70,34 +69,6 @@ export default function ChallengeModal({ claim, challengeDeposit, onClose = noop fetchFee(); }, [walletAddress]); - useEffect(() => { - let handle: number; - const launchSetTimeoutAsync = async (execTimeMs: number) => { - const now = Date.now(); - - if (now > execTimeMs) setChallengedTimeout(true); - else { - setChallengedTimeout(false); - handle = window.setTimeout(() => { - setChallengedTimeout(true); - }, execTimeMs - now); // To ms - } - }; - if (claim.executionTimeMs) launchSetTimeoutAsync(claim.executionTimeMs); - return () => { - if (handle) clearTimeout(handle); - }; - }, [claim.executionTimeMs]); - - useEffect(() => { - if (challengeTimeout !== undefined) { - if (challengeTimeout) - // wait to load - setOpenButtonLabel('Challenge period over'); - else setOpenButtonLabel('Challenge'); - } - }, [claim.state, challengeTimeout]); - useEffect(() => { if (challengeFee) setIsFeeDepositSameToken( @@ -219,19 +190,14 @@ export default function ChallengeModal({ claim, challengeDeposit, onClose = noop title="Challenge quests" openButton={ - {buttonLabel && ( - } - onClick={() => setOpened(true)} - label={buttonLabel} - mode="negative" - title={challengeTimeout ? "This claim can't be challenged anymore" : buttonLabel} - disabled={!buttonLabel || loading || !walletAddress || challengeTimeout} - /> - )} - {!loading && challengeTimeout === false && claim.executionTimeMs && ( - - )} + } + onClick={() => setOpened(true)} + label="Challenge" + mode="negative" + title={"This claim can't be challenged anymore"} + disabled={loading || !walletAddress} + /> } buttons={[ @@ -276,22 +242,13 @@ export default function ChallengeModal({ claim, challengeDeposit, onClose = noop
diff --git a/packages/react-app/src/components/dashboard.tsx b/packages/react-app/src/components/dashboard.tsx index cf30a3e2..0987b78f 100644 --- a/packages/react-app/src/components/dashboard.tsx +++ b/packages/react-app/src/components/dashboard.tsx @@ -47,7 +47,7 @@ const LabelStyled = styled.div` export default function Dashboard() { const theme = useTheme(); const [dashboardModel, setDashboardModel] = useState(); - const { walletAddress } = useWallet(); + const { walletConnected } = useWallet(); useEffect(() => { let isSubscribed = true; @@ -84,7 +84,7 @@ export default function Dashboard() { {dashboardModel?.questCount.toLocaleString()} - {walletAddress && } + {walletConnected && } diff --git a/packages/react-app/src/components/footer.tsx b/packages/react-app/src/components/footer.tsx index feb8d4a8..e5c78663 100644 --- a/packages/react-app/src/components/footer.tsx +++ b/packages/react-app/src/components/footer.tsx @@ -76,7 +76,7 @@ const IconStyled = styled.div` export default function footer() { const theme = useTheme(); const year = new Date().getFullYear(); - const { walletAddress } = useWallet(); + const { walletConnected } = useWallet(); const { networkId } = getNetwork(); return ( @@ -101,7 +101,7 @@ export default function footer() { Quest List - {walletAddress && ( + {walletConnected && ( )} diff --git a/packages/react-app/src/components/header/header-menu.tsx b/packages/react-app/src/components/header/header-menu.tsx index 0f84fbda..1b602ba5 100644 --- a/packages/react-app/src/components/header/header-menu.tsx +++ b/packages/react-app/src/components/header/header-menu.tsx @@ -30,7 +30,7 @@ type Props = { export default function HeaderMenu({ below }: Props) { const theme = useTheme(); - const { walletAddress } = useWallet(); + const { walletConnected } = useWallet(); return ( @@ -53,7 +53,7 @@ export default function HeaderMenu({ below }: Props) { - {walletAddress && ( + {walletConnected && ( )} diff --git a/packages/react-app/src/components/modals/challenge-modal.tsx b/packages/react-app/src/components/modals/challenge-modal.tsx index 66389a03..b49276f5 100644 --- a/packages/react-app/src/components/modals/challenge-modal.tsx +++ b/packages/react-app/src/components/modals/challenge-modal.tsx @@ -68,7 +68,7 @@ export default function ChallengeModal({ claim, challengeDeposit, onClose = noop if (feeAmount && isMountedRef.current) setChallengeFee(feeAmount); }; fetchFee(); - }, [walletAddress]); + }, []); useEffect(() => { if (challengeFee) @@ -188,8 +188,7 @@ export default function ChallengeModal({ claim, challengeDeposit, onClose = noop onClick={() => setOpened(true)} label="Challenge" mode="negative" - title={"Open challenge for this quest's claim"} - disabled={!walletAddress} + title="Open challenge for this quest's claim" /> } @@ -237,8 +236,8 @@ export default function ChallengeModal({ claim, challengeDeposit, onClose = noop mode="negative" type="submit" form="form-challenge" - disabled={!walletAddress || !isEnoughBalance || !isFormValid} - title={!walletAddress ? 'Not ready ...' : !isFormValid ? 'Form not valid' : 'Challenge'} + disabled={!isEnoughBalance || !isFormValid} + title={!isFormValid ? 'Form not valid' : 'Challenge'} className="m-8" />, ]} diff --git a/packages/react-app/src/components/modals/create-quest-modal.tsx b/packages/react-app/src/components/modals/create-quest-modal.tsx index 18f18d9e..ac7bb46e 100644 --- a/packages/react-app/src/components/modals/create-quest-modal.tsx +++ b/packages/react-app/src/components/modals/create-quest-modal.tsx @@ -332,15 +332,13 @@ export default function QuestModal({ form="form-quest" className="m-8" title={ - !walletAddress || !questDeposit?.token + !questDeposit?.token ? 'Not ready ...' : !isFormValid ? 'Form not valid' : 'Schedule claim' } - disabled={ - !walletAddress || !questDeposit?.token || !isEnoughBalance || !isFormValid - } + disabled={!questDeposit?.token || !isEnoughBalance || !isFormValid} /> } diff --git a/packages/react-app/src/components/modals/execute-claim-modal.tsx b/packages/react-app/src/components/modals/execute-claim-modal.tsx index c432abc6..4123dd49 100644 --- a/packages/react-app/src/components/modals/execute-claim-modal.tsx +++ b/packages/react-app/src/components/modals/execute-claim-modal.tsx @@ -11,10 +11,9 @@ import { TokenAmountModel } from 'src/models/token-amount.model'; import { useWallet } from 'src/contexts/wallet.context'; import { computeTransactionErrorMessage } from 'src/utils/errors.util'; import { compareCaseInsensitive } from 'src/utils/string.util'; -import { useIsMountedRef } from 'src/hooks/use-mounted.hook'; import { TransactionModel } from 'src/models/transaction.model'; import * as QuestService from '../../services/quest.service'; -import { AmountFieldInputFormik } from '../field-input/amount-field-input'; +import AmountFieldInput from '../field-input/amount-field-input'; import { Outset } from '../utils/spacer-util'; import ModalBase, { ModalCallback } from './modal-base'; import { AddressFieldInput } from '../field-input/address-field-input'; @@ -55,12 +54,10 @@ export default function ExecuteClaimModal({ claimable, }: Props) { const [opened, setOpened] = useState(false); - const [loading, setLoading] = useState(false); const [amount, setAmount] = useState(); const { setTransaction } = useTransactionContext(); const { walletAddress } = useWallet(); const modalId = useMemo(() => uniqueId('execute-claim-modal'), []); - const isMountedRef = useIsMountedRef(); useEffect(() => { if (questTotalBounty) { @@ -76,7 +73,6 @@ export default function ExecuteClaimModal({ const claimTx = async () => { try { - setLoading(true); const txPayload = { modalId, estimatedDuration: ENUM.ENUM_ESTIMATED_TX_TIME_MS.ClaimExecuting, @@ -109,10 +105,6 @@ export default function ExecuteClaimModal({ message: computeTransactionErrorMessage(e), }, ); - } finally { - if (isMountedRef.current) { - setLoading(false); - } } }; @@ -138,7 +130,6 @@ export default function ExecuteClaimModal({ } disabled={ !questTotalBounty || - !walletAddress || claim.claimedAmount.parsedAmount > questTotalBounty.parsedAmount || !claimable } @@ -150,10 +141,8 @@ export default function ExecuteClaimModal({ onClick={claimTx} icon={} label="Claim" - disabled={loading || !walletAddress || claim.state === ENUM_CLAIM_STATE.Challenged} - title={ - loading || !walletAddress ? 'Not ready ...' : 'Trigger claim operation in the chain' - } + disabled={claim.state === ENUM_CLAIM_STATE.Challenged} + title="Trigger claim operation in the chain" mode="positive" /> } @@ -162,16 +151,10 @@ export default function ExecuteClaimModal({ size="small" > - + {!compareCaseInsensitive(claim.playerAddress, walletAddress) && ( diff --git a/packages/react-app/src/components/modals/fund-modal.tsx b/packages/react-app/src/components/modals/fund-modal.tsx index b4e7bd1b..799a6628 100644 --- a/packages/react-app/src/components/modals/fund-modal.tsx +++ b/packages/react-app/src/components/modals/fund-modal.tsx @@ -108,8 +108,8 @@ export default function FundModal({ quest, onClose = noop }: Props) { form="form-fund" label="Fund" mode="strong" - title={!walletAddress ? 'Not ready ...' : !isFormValid ? 'Form not valid' : 'Fund'} - disabled={!walletAddress || !isEnoughBalance || !isFormValid} + title={!isFormValid ? 'Form not valid' : 'Fund'} + disabled={!isEnoughBalance || !isFormValid} />, ]} onClose={closeModal} diff --git a/packages/react-app/src/components/modals/reclaim-funds-modal.tsx b/packages/react-app/src/components/modals/reclaim-funds-modal.tsx index 429775d3..0c6e245a 100644 --- a/packages/react-app/src/components/modals/reclaim-funds-modal.tsx +++ b/packages/react-app/src/components/modals/reclaim-funds-modal.tsx @@ -139,8 +139,7 @@ export default function ReclaimFundsModal({ icon={} label="Reclaim" mode="strong" - title={!walletAddress ? 'Not ready ...' : 'Reclaim remaining funds and deposit'} - disabled={!walletAddress} + title="Reclaim remaining funds and deposit" /> } onClose={closeModal} diff --git a/packages/react-app/src/components/modals/schedule-claim-modal.tsx b/packages/react-app/src/components/modals/schedule-claim-modal.tsx index 7fd96e69..45ed52c8 100644 --- a/packages/react-app/src/components/modals/schedule-claim-modal.tsx +++ b/packages/react-app/src/components/modals/schedule-claim-modal.tsx @@ -238,13 +238,7 @@ export default function ScheduleClaimModal({ type="submit" form="form-claim" className="m-8" - title={ - !walletAddress - ? 'Not ready ...' - : !isFormValid - ? 'Form not valid' - : 'Schedule claim' - } + title={!isFormValid ? 'Form not valid' : 'Schedule claim'} disabled={!isEnoughBalance} /> diff --git a/packages/react-app/src/components/quest.tsx b/packages/react-app/src/components/quest.tsx index 3dc0a344..b6710140 100644 --- a/packages/react-app/src/components/quest.tsx +++ b/packages/react-app/src/components/quest.tsx @@ -114,7 +114,7 @@ export default function Quest({ isLoading = false, isSummary = false, }: Props) { - const { walletAddress } = useWallet(); + const { walletConnected } = useWallet(); const history = useHistory(); const [bounty, setBounty] = useState(questData?.bounty); const [highlight, setHighlight] = useState(true); @@ -329,7 +329,7 @@ export default function Quest({ isLoading={isLoading} /> )} - {!isSummary && questData.address && walletAddress && ( + {!isSummary && questData.address && walletConnected && ( {questData?.state === ENUM_QUEST_STATE.Active ? ( <> diff --git a/packages/react-app/src/components/sidebar.tsx b/packages/react-app/src/components/sidebar.tsx deleted file mode 100644 index 493ee796..00000000 --- a/packages/react-app/src/components/sidebar.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ENUM_QUEST_VIEW_MODE } from '../constants'; -import { useWallet } from '../contexts/wallet.context'; -import QuestModal from './modals/create-quest-modal'; -import { Filter } from './filter'; -import { Outset } from './utils/spacer-util'; - -export default function Sidebar() { - const { walletAddress } = useWallet(); - return ( - - - {walletAddress && } - - ); -} diff --git a/packages/react-app/src/contexts/wallet.context.tsx b/packages/react-app/src/contexts/wallet.context.tsx index b621ecd1..b67704a8 100644 --- a/packages/react-app/src/contexts/wallet.context.tsx +++ b/packages/react-app/src/contexts/wallet.context.tsx @@ -6,6 +6,7 @@ import { getNetwork } from '../networks'; import { getDefaultProvider, getUseWalletConnectors } from '../utils/web3.utils'; export type WalletContextModel = { + walletConnected: boolean; walletAddress: string; deactivateWallet: Function; activateWallet: Function; @@ -30,6 +31,7 @@ function WalletAugmented({ children }: Props) { const { ethereum } = wallet; const [activationError, setActivationError] = useState<{ name: string; message: string }>(); const [activatingId, setActivating] = useState(); + const [isConnected, setIsConnected] = useState(false); useEffect(() => { const lastWalletConnected = localStorage.getItem('LAST_WALLET_CONNECTOR'); @@ -39,6 +41,7 @@ function WalletAugmented({ children }: Props) { }, []); const ethers = useMemo(() => { + setIsConnected(false); const { chainId, networkId, name } = getNetwork(); if (ethereum) { @@ -63,6 +66,8 @@ function WalletAugmented({ children }: Props) { return getDefaultProvider(); } + setIsConnected(true); + const ensRegistry = undefined; // network?.ensRegistry; return new EthersProviders.Web3Provider(ethereum, { name: networkId, @@ -76,10 +81,11 @@ function WalletAugmented({ children }: Props) { await wallet.connect(id); }; - const handleDeconnect = () => { + const handleDisconnect = () => { setActivating(undefined); wallet.reset(); localStorage.removeItem('LAST_WALLET_CONNECTOR'); + setIsConnected(false); }; const contextValue = useMemo( () => ({ @@ -88,9 +94,10 @@ function WalletAugmented({ children }: Props) { activationError, walletAddress: wallet.account, activateWallet: handleConnect, - deactivateWallet: handleDeconnect, + deactivateWallet: handleDisconnect, activatedId: wallet.connector, activatingId, + walletConnected: isConnected, }), [wallet, ethers], ); From f8c6867bc8ee523fcdee77da624a90f72f320dc1 Mon Sep 17 00:00:00 2001 From: Corantin Date: Sat, 28 May 2022 19:11:49 -0400 Subject: [PATCH 09/12] Reduce calls by adding cache for token-info and balanceOf --- packages/react-app/src/components/quest.tsx | 1 + .../react-app/src/services/cache.service.ts | 90 +++++++++++++++---- .../react-app/src/services/quest.service.ts | 20 +++-- packages/react-app/src/utils/contract.util.ts | 22 +++-- packages/react-app/src/utils/web3.utils.ts | 7 +- 5 files changed, 110 insertions(+), 30 deletions(-) diff --git a/packages/react-app/src/components/quest.tsx b/packages/react-app/src/components/quest.tsx index b6710140..2f6c0ff8 100644 --- a/packages/react-app/src/components/quest.tsx +++ b/packages/react-app/src/components/quest.tsx @@ -196,6 +196,7 @@ export default function Quest({ token, address, depositReleased ? undefined : questData.deposit, + true, ); if (isMountedRef.current) { questData.bounty = result; diff --git a/packages/react-app/src/services/cache.service.ts b/packages/react-app/src/services/cache.service.ts index a7a840a2..c6bd53f1 100644 --- a/packages/react-app/src/services/cache.service.ts +++ b/packages/react-app/src/services/cache.service.ts @@ -1,25 +1,83 @@ -import { BigNumber, ethers } from 'ethers'; +import { BigNumber, Contract, ethers } from 'ethers'; import { TokenModel } from 'src/models/token.model'; import { sleep } from 'src/utils/common.util'; +import { getProviderOrSigner } from 'src/utils/contract.util'; +import { ONE_HOUR_IN_MS } from 'src/utils/date.utils'; +import { Logger } from 'src/utils/logger'; import { fetchRoutePairWithStable } from './uniswap.service'; -const tokenPriceMap = new Map(); +const cacheMap = new Map>(); export async function cacheFetchTokenPrice(token: TokenModel) { - const id = token.token; - if (tokenPriceMap.has(id)) { - const tokenPrice = tokenPriceMap.get(id); - if (tokenPrice) return tokenPrice; - // Token is being fetched - while (tokenPriceMap.get(id) === null) { - // eslint-disable-next-line no-await-in-loop - await sleep(200); + return buildCache( + 'token-price', + token.token, + () => fetchRoutePairWithStable(token).then((x) => ethers.utils.parseEther(x.price)), + ONE_HOUR_IN_MS / 2, // 30 min + ); +} + +export async function cacheFetchBalance( + token: TokenModel, + address: string, + erc20Contract: ethers.Contract, +) { + return buildCache( + 'balance', + token.token + address, + () => erc20Contract.balanceOf(address), + 30 * 1000, // 30 sec + ); +} + +export async function cacheTokenInfo(address: string, erc20Contract: ethers.Contract) { + return buildCache<{ name: string; decimals: number; symbol: string }>( + 'token-info', + address, + async () => ({ + name: await erc20Contract.name(), + decimals: await erc20Contract.decimals(), + symbol: await erc20Contract.symbol(), + }), + ); +} + +async function buildCache( + cacheId: string, + valueId: string, + fetchValue: () => Promise, + cacheDurationMs?: number, // Undefined for permanent cache +): Promise { + let cache = cacheMap.get(cacheId); + if (!cache) { + cache = new Map(); + cacheMap.set(cacheId, cache); + } else { + let cached = cache.get(valueId); + if (cached !== undefined) { + // Token is being fetched + while (cached === null) { + // eslint-disable-next-line no-await-in-loop + await sleep(200); + cached = cache.get(valueId); + } + if (cached !== undefined && (!cached.expirationMs || cached.expirationMs > Date.now())) { + Logger.debug('Using cached version of', { + cacheId, + valueId, + value: cached.value, + cacheDurationMs, + }); + return cached.value; + } } - return tokenPriceMap.get(id)!; } - tokenPriceMap.set(id, null); // Set to null to indicate that it is being fetched - const res = await fetchRoutePairWithStable(token); - const tokenPrice = ethers.utils.parseEther(res.price); - tokenPriceMap.set(id, tokenPrice); - return tokenPrice; + cache.set(valueId, null); // Set to null to indicate that it is being fetched + const value = await fetchValue(); + cache.set(valueId, { + value, + expirationMs: cacheDurationMs ? Date.now() + cacheDurationMs : undefined, + }); + Logger.debug('Building cached version of', { cacheId, valueId, value, cacheDurationMs }); + return value; } diff --git a/packages/react-app/src/services/quest.service.ts b/packages/react-app/src/services/quest.service.ts index 6c82a614..3b310e69 100644 --- a/packages/react-app/src/services/quest.service.ts +++ b/packages/react-app/src/services/quest.service.ts @@ -45,7 +45,7 @@ import { getCelesteContract, } from '../utils/contract.util'; import { getLastBlockTimestamp } from '../utils/date.utils'; -import { cacheFetchTokenPrice } from './cache.service'; +import { cacheFetchBalance, cacheFetchTokenPrice } from './cache.service'; let questList: QuestModel[] = []; @@ -345,10 +345,15 @@ export async function getDashboardInfo(): Promise { const funds = ( await Promise.all( quests.map(async (quest) => - getBalanceOf(quest.questRewardTokenAddress, quest.id, { - amount: BigNumber.from(quest.depositAmount), - token: quest.depositToken, - }), + getBalanceOf( + quest.questRewardTokenAddress, + quest.id, + { + amount: BigNumber.from(quest.depositAmount), + token: quest.depositToken, + }, + true, + ), ), ) ).filter((x) => !!x) as TokenAmountModel[]; @@ -490,6 +495,7 @@ export async function getBalanceOf( token: TokenModel | string, address: string, lockedFunds?: DepositModel, + useCache?: boolean, ): Promise { try { let tokenInfo: TokenModel; @@ -498,7 +504,9 @@ export async function getBalanceOf( if (tokenInfo) { const erc20Contract = getERC20Contract(tokenInfo); if (!erc20Contract) return null; - let balance = (await erc20Contract.balanceOf(address)) as BigNumber; + let balance = useCache + ? await cacheFetchBalance(tokenInfo, address, erc20Contract) + : ((await erc20Contract.balanceOf(address)) as BigNumber); if (lockedFunds && compareCaseInsensitive(lockedFunds.token, tokenInfo.token)) { // Substract deposit from funds if both same token balance = balance.sub(BigNumber.from(lockedFunds.amount)); diff --git a/packages/react-app/src/utils/contract.util.ts b/packages/react-app/src/utils/contract.util.ts index 14061c39..7a2ed4cb 100644 --- a/packages/react-app/src/utils/contract.util.ts +++ b/packages/react-app/src/utils/contract.util.ts @@ -4,12 +4,15 @@ import { getDefaultProvider } from 'src/utils/web3.utils'; import { toChecksumAddress } from 'web3-utils'; import { ContractInstanceError } from 'src/models/contract-error'; import { Logger } from 'src/utils/logger'; +import { cacheTokenInfo } from 'src/services/cache.service'; import ERC20 from '../contracts/ERC20.json'; import UniswapPair from '../contracts/UniswapPair.json'; import contractsJson from '../contracts/hardhat_contracts.json'; import { getNetwork } from '../networks'; +import { sleep } from './common.util'; let contracts: any; +const contractMap = new Map(); // walletAddress is not optional export function getSigner(ethersProvider: any, walletAddress: any) { @@ -37,17 +40,28 @@ function getContract( walletAddress?: string, ): Contract { try { + const id = (contractAddressOverride ?? contractName) + (walletAddress ?? ''); + let contract = contractMap.get(id); + if (contract) { + return contract; + } const network = getNetwork(); if (!contracts) contracts = getContractsJson(network); const askedContract = contracts[contractName]; - const contractAddress = contractAddressOverride ?? askedContract.address; + const contractAddress: string = contractAddressOverride ?? askedContract.address; const contractAbi = askedContract.abi ?? askedContract; const provider = getDefaultProvider(); if (!contractAddress) throw new Error(`${contractName} address was not defined`); if (!contractAbi) throw new Error(`${contractName} ABI was not defined`); - return new Contract(contractAddress, contractAbi, getProviderOrSigner(provider, walletAddress)); + contract = new Contract( + contractAddress, + contractAbi, + getProviderOrSigner(provider, walletAddress), + ); + contractMap.set(id, contract); + return contract; } catch (error) { throw new ContractInstanceError( contractName, @@ -80,9 +94,7 @@ export async function getTokenInfo(tokenAddress: string) { try { const tokenContract = getERC20Contract(tokenAddress); if (tokenContract) { - const symbol = await tokenContract.symbol(); - const decimals = await tokenContract.decimals(); - const name = await tokenContract.name(); + const { symbol, decimals, name } = await cacheTokenInfo(tokenAddress, tokenContract); return { symbol, decimals, diff --git a/packages/react-app/src/utils/web3.utils.ts b/packages/react-app/src/utils/web3.utils.ts index 0bb4c2bc..219a5f22 100644 --- a/packages/react-app/src/utils/web3.utils.ts +++ b/packages/react-app/src/utils/web3.utils.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle */ import { BigNumber, ethers } from 'ethers'; import { noop } from 'lodash-es'; import { TokenAmountModel } from 'src/models/token-amount.model'; @@ -135,11 +136,11 @@ export function fromBigNumber(bigNumber: BigNumber | string, decimals: number = return +ethers.utils.formatUnits(bigNumber, decimals); } -export function getDefaultProvider() { - const { chainId: expectedChainId } = getNetwork(); +export function getDefaultProvider(): ethers.providers.Provider { + const { networkId, chainId: expectedChainId } = getNetwork(); let provider = (window as any).ethereum ?? (window as any).web3?.currentProvider; if (!provider || +provider.chainId !== +expectedChainId) { - provider = new Web3.providers.HttpProvider(getRpcUrl()); + provider = new ethers.providers.StaticJsonRpcProvider(getRpcUrl()); } return provider && new ethers.providers.Web3Provider(provider); From 6cb44bc02a472f69ce64b336647e46aeac807e4d Mon Sep 17 00:00:00 2001 From: Corantin Date: Sat, 28 May 2022 19:53:17 -0400 Subject: [PATCH 10/12] Persistent cache --- .../react-app/src/services/cache.service.ts | 60 ++++++++++++++++--- packages/react-app/src/utils/contract.util.ts | 1 - packages/react-app/src/utils/web3.utils.ts | 4 +- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/packages/react-app/src/services/cache.service.ts b/packages/react-app/src/services/cache.service.ts index c6bd53f1..094c1aa4 100644 --- a/packages/react-app/src/services/cache.service.ts +++ b/packages/react-app/src/services/cache.service.ts @@ -1,20 +1,23 @@ -import { BigNumber, Contract, ethers } from 'ethers'; +import { BigNumber, ethers } from 'ethers'; import { TokenModel } from 'src/models/token.model'; import { sleep } from 'src/utils/common.util'; -import { getProviderOrSigner } from 'src/utils/contract.util'; import { ONE_HOUR_IN_MS } from 'src/utils/date.utils'; import { Logger } from 'src/utils/logger'; import { fetchRoutePairWithStable } from './uniswap.service'; -const cacheMap = new Map>(); +let cacheMap: Map< + string, + Map +> = retrieveCache(); export async function cacheFetchTokenPrice(token: TokenModel) { - return buildCache( + const price = await buildCache( 'token-price', token.token, - () => fetchRoutePairWithStable(token).then((x) => ethers.utils.parseEther(x.price)), + () => fetchRoutePairWithStable(token).then((x) => x.price), ONE_HOUR_IN_MS / 2, // 30 min ); + return ethers.utils.parseEther(price); } export async function cacheFetchBalance( @@ -22,12 +25,13 @@ export async function cacheFetchBalance( address: string, erc20Contract: ethers.Contract, ) { - return buildCache( + const balance = await buildCache( 'balance', token.token + address, - () => erc20Contract.balanceOf(address), + () => erc20Contract.balanceOf(address).then((x: BigNumber) => x.toString()), 30 * 1000, // 30 sec ); + return BigNumber.from(balance.toString()); } export async function cacheTokenInfo(address: string, erc20Contract: ethers.Contract) { @@ -48,6 +52,9 @@ async function buildCache( fetchValue: () => Promise, cacheDurationMs?: number, // Undefined for permanent cache ): Promise { + if (!cacheMap) { + cacheMap = retrieveCache(); + } let cache = cacheMap.get(cacheId); if (!cache) { cache = new Map(); @@ -79,5 +86,44 @@ async function buildCache( expirationMs: cacheDurationMs ? Date.now() + cacheDurationMs : undefined, }); Logger.debug('Building cached version of', { cacheId, valueId, value, cacheDurationMs }); + saveCacheAsync(); // Save cache without waiting for it to be saved + return value; +} + +function replacer(key: string, value: any) { + if (value instanceof Map) { + return { + dataType: 'Map', + value: Array.from(value.entries()), // or with spread: value: [...value] + }; + } return value; } + +function reviver(key: string, value: any) { + if (typeof value === 'object' && value !== null) { + if (value.dataType === 'Map') { + return new Map(value.value); + } + } + return value; +} + +function saveCacheAsync() { + localStorage.setItem('cache', JSON.stringify(cacheMap, replacer)); +} + +function retrieveCache() { + try { + const cacheJson = localStorage.getItem('cache'); + if (cacheJson) { + const map = JSON.parse(cacheJson, reviver) as Map>; + if (map.size > 0) { + return map; + } + } + } catch (error) { + Logger.debug('Error retrieving cache from storage', error); + } + return new Map>(); +} diff --git a/packages/react-app/src/utils/contract.util.ts b/packages/react-app/src/utils/contract.util.ts index 7a2ed4cb..ea0ce6bb 100644 --- a/packages/react-app/src/utils/contract.util.ts +++ b/packages/react-app/src/utils/contract.util.ts @@ -9,7 +9,6 @@ import ERC20 from '../contracts/ERC20.json'; import UniswapPair from '../contracts/UniswapPair.json'; import contractsJson from '../contracts/hardhat_contracts.json'; import { getNetwork } from '../networks'; -import { sleep } from './common.util'; let contracts: any; const contractMap = new Map(); diff --git a/packages/react-app/src/utils/web3.utils.ts b/packages/react-app/src/utils/web3.utils.ts index 219a5f22..f574b3bb 100644 --- a/packages/react-app/src/utils/web3.utils.ts +++ b/packages/react-app/src/utils/web3.utils.ts @@ -140,10 +140,10 @@ export function getDefaultProvider(): ethers.providers.Provider { const { networkId, chainId: expectedChainId } = getNetwork(); let provider = (window as any).ethereum ?? (window as any).web3?.currentProvider; if (!provider || +provider.chainId !== +expectedChainId) { - provider = new ethers.providers.StaticJsonRpcProvider(getRpcUrl()); + provider = new ethers.providers.StaticJsonRpcProvider(getRpcUrl(), networkId); } - return provider && new ethers.providers.Web3Provider(provider); + return provider; } export function getRpcUrl(chainId?: number) { From 3896c84f28e042221150666d930666b6f8758439 Mon Sep 17 00:00:00 2001 From: Corantin Noll Date: Sun, 29 May 2022 21:40:01 -0400 Subject: [PATCH 11/12] Hotfix : Fix use of web3 provider when injected --- packages/react-app/src/utils/web3.utils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/react-app/src/utils/web3.utils.ts b/packages/react-app/src/utils/web3.utils.ts index f574b3bb..1fd7ed7f 100644 --- a/packages/react-app/src/utils/web3.utils.ts +++ b/packages/react-app/src/utils/web3.utils.ts @@ -138,9 +138,12 @@ export function fromBigNumber(bigNumber: BigNumber | string, decimals: number = export function getDefaultProvider(): ethers.providers.Provider { const { networkId, chainId: expectedChainId } = getNetwork(); - let provider = (window as any).ethereum ?? (window as any).web3?.currentProvider; - if (!provider || +provider.chainId !== +expectedChainId) { + const injectedProvider = (window as any).ethereum ?? (window as any).web3?.currentProvider; + let provider; + if (!injectedProvider || +injectedProvider.chainId !== +expectedChainId) { provider = new ethers.providers.StaticJsonRpcProvider(getRpcUrl(), networkId); + } else { + provider = new ethers.providers.Web3Provider(injectedProvider); } return provider; From a85e86dc720e5f3e1268a78b1ff1cce7027d9782 Mon Sep 17 00:00:00 2001 From: Corantin Noll Date: Mon, 30 May 2022 19:50:30 -0400 Subject: [PATCH 12/12] HOTFIX : Fix quests heigth --- packages/react-app/src/components/quest.tsx | 1 + packages/react-app/src/services/uniswap.service.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-app/src/components/quest.tsx b/packages/react-app/src/components/quest.tsx index 2f6c0ff8..1f11b30b 100644 --- a/packages/react-app/src/components/quest.tsx +++ b/packages/react-app/src/components/quest.tsx @@ -36,6 +36,7 @@ import { ConditionalWrapper } from './utils/util'; const ClickableDivStyled = styled.div` text-decoration: none; width: 100%; + height: 100%; `; const CardWrapperStyed = styled.div<{ compact: boolean }>` diff --git a/packages/react-app/src/services/uniswap.service.ts b/packages/react-app/src/services/uniswap.service.ts index 890b1dd6..c6afc4fc 100644 --- a/packages/react-app/src/services/uniswap.service.ts +++ b/packages/react-app/src/services/uniswap.service.ts @@ -46,7 +46,6 @@ export async function fetchPairWithStables(tokenA: TokenModel): Promise