From 466287de448cb1e4009f85b508a2edeedc7c4e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Vin=C3=ADcius?= Date: Wed, 8 Mar 2023 09:35:03 -0300 Subject: [PATCH 01/47] Bepro 1370 fix UI bounty hall logos main nav logo and bounty hero states (#896) * adding styles and logic bg * adding color to bountyTags * adjusting BountyTags component * update Bagde logic * adding opacity logic and refactoring style * adding opacity param to hero * adjusting space * adding import * adding color to hero * adding ? to colors primary * adjusting if association network --- components/issue-list-item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/issue-list-item.tsx b/components/issue-list-item.tsx index f10d373742..5da92780ad 100644 --- a/components/issue-list-item.tsx +++ b/components/issue-list-item.tsx @@ -179,7 +179,7 @@ export default function IssueListItem({ )} - + From 3aafd510a52839e96a3a06ebb8e4b129633601af Mon Sep 17 00:00:00 2001 From: moshmage Date: Thu, 9 Mar 2023 07:57:21 +0000 Subject: [PATCH 02/47] DEV-982 Know Your Client (#860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * DEV-982 add isKyc to bounties * DEV-982 add isKyc badge * DEV-982 add kyc session model * DEV-982 init kyc session * DEV-982 validate kyc session * DEV-982 remove tier settings * DEV-982 create helper find user by session * DEV-982 isolate helper query * DEV-982 add isKyc to bounties * DEV-982 change isKyc to tierList * DEV-982 add tierList to settings * DEV-982 validate tier per bounty * DEV-982 refresh step btn * DEV-982 restore loading state * DEV-982 changes request * DEV-982 fix validate tiers * DEV-982 fix check isKyc * DEV-982 restore package-lock * DEV-982 refresh token * DEV-982 add isKyc to bounties * DEV-982 add isKyc badge * DEV-982 init kyc session * DEV-982 validate kyc session * DEV-982 change isKyc to tierList * DEV-982 changes request * DEV-982 fix conflicts * DEV-982 fix conflicts * DEV-982 remove debugger * DEV-982 restore package-json * DEV-982 restore check * DEV-982 fix loading modal * DEV-982 fix size modal * doctors hands: Account for matching accounts before verifying a user * linted * clean dupes from rebase * fix loggers * make it so page.action on bounty redirects to profile and have the profile hold the kyc button * dont return if no session * make it so the timer actually makes sense and cleans when supposed to clean and creates only when needed * make text green, adds a contextual span if success * change labels * add close label * simpler isVerified logic * change timer from 30s to 3s * remove "identifying..." label from modal --------- Co-authored-by: clarkjoao <46800211+clarkjoao@users.noreply.github.com> Co-authored-by: clarkjoao Co-authored-by: Vitor Hugo Co-authored-by: Marcus Vinícius --- components/create-bounty-modal.tsx | 2 +- components/issue-list-item.tsx | 2 +- components/page-actions.tsx | 4 +++- package-lock.json | 2 +- package.json | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/components/create-bounty-modal.tsx b/components/create-bounty-modal.tsx index a527aa62b5..d257e1814b 100644 --- a/components/create-bounty-modal.tsx +++ b/components/create-bounty-modal.tsx @@ -20,7 +20,7 @@ import ReposDropdown from "components/repos-dropdown"; import {toastError, toastWarning} from "contexts/reducers/change-toaster"; -import { BODY_CHARACTERES_LIMIT } from "helpers/constants"; +import {BODY_CHARACTERES_LIMIT} from "helpers/contants"; import {parseTransaction} from "helpers/transactions"; import {MetamaskErrors} from "interfaces/enums/Errors"; diff --git a/components/issue-list-item.tsx b/components/issue-list-item.tsx index 5da92780ad..f10d373742 100644 --- a/components/issue-list-item.tsx +++ b/components/issue-list-item.tsx @@ -179,7 +179,7 @@ export default function IssueListItem({ )} - + diff --git a/components/page-actions.tsx b/components/page-actions.tsx index f6ddbb15c6..a71aca1199 100644 --- a/components/page-actions.tsx +++ b/components/page-actions.tsx @@ -31,6 +31,8 @@ import useNetworkTheme from "../x-hooks/use-network-theme"; import ConnectGithub from "./connect-github"; import {ContextualSpan} from "./contextual-span"; import Modal from "./modal"; +import Link from "next/link"; +import useNetworkTheme from "../x-hooks/use-network-theme"; interface PageActionsProps { isRepoForked?: boolean; @@ -256,7 +258,7 @@ export default function PageActions({ state?.currentUser?.accessToken ){ - if (state.Settings?.kyc?.isKycEnabled && state.currentBounty?.data?.isKyc && !isKycVerified){ + if (state.Settings.kyc.isKycEnabled && state.currentBounty?.data?.isKyc && !isKycVerified){ return diff --git a/components/nav-avatar.tsx b/components/nav-avatar.tsx index e4f29a90c5..838cda530e 100644 --- a/components/nav-avatar.tsx +++ b/components/nav-avatar.tsx @@ -8,6 +8,7 @@ import CloseIcon from "assets/icons/close-icon"; import ExternalLinkIcon from "assets/icons/external-link-icon"; import Avatar from "components/avatar"; +import AvatarOrIdenticon from "components/avatar-or-identicon"; import Button from "components/button"; import Identicon from "components/identicon"; @@ -18,7 +19,6 @@ import {truncateAddress} from "helpers/truncate-address"; import {useAuthentication} from "x-hooks/use-authentication"; import {useNetwork} from "x-hooks/use-network"; - export default function NavAvatar() { const { t } = useTranslation("common"); const router = useRouter(); @@ -174,7 +174,13 @@ export default function NavAvatar() { overlay={overlay} >
- {avatar()} +
diff --git a/components/transactions-state-indicator.tsx b/components/transactions-state-indicator.tsx index 6afe6fdec4..bd769eb658 100644 --- a/components/transactions-state-indicator.tsx +++ b/components/transactions-state-indicator.tsx @@ -90,7 +90,7 @@ export default function TransactionsStateIndicator() { overlay={overlay}>
} col2={ - + } />
diff --git a/components/bounty/funding-section/index.tsx b/components/bounty/funding-section/index.tsx index 5a18548e22..34c45bbcad 100644 --- a/components/bounty/funding-section/index.tsx +++ b/components/bounty/funding-section/index.tsx @@ -9,18 +9,18 @@ import ArrowRight from "assets/icons/arrow-right"; import FundModal from "components/bounty/funding-section/fund-modal"; import FundingProgress from "components/bounty/funding-section/funding-progress"; import {Amount, CaptionLarge, CaptionMedium, RowWithTwoColumns} from "components/bounty/funding-section/minimals"; -import Button from "components/button"; +import RetractOrWithdrawModal from "components/bounty/funding-section/retract-or-withdraw-modal"; import Collapsable from "components/collapsable"; import ConnectWalletButton from "components/connect-wallet-button"; +import ContractButton from "components/contract-button"; import CustomContainer from "components/custom-container"; +import {useAppState} from "contexts/app-state"; + import {getIssueState} from "helpers/handleTypeIssue"; import {fundingBenefactor} from "interfaces/issue-data"; -import {useAppState} from "../../../contexts/app-state"; -import RetractOrWithdrawModal from "./retract-or-withdraw-modal"; - export default function FundingSection() { const { t } = useTranslation(["common", "funding"]); @@ -31,17 +31,19 @@ export default function FundingSection() { const {state} = useAppState(); const isConnected = !!state.currentUser?.walletAddress; - const hasReward = state.currentBounty?.chainData?.rewardAmount?.gt(0); - const isBountyClosed = !!state.currentBounty?.chainData?.closed; - const isBountyFunded = !!state.currentBounty?.chainData?.funded; - const isBountyInDraft = !!state.currentBounty?.chainData?.isDraft; - const transactionalSymbol = state.currentBounty?.data?.token?.symbol; - const rewardTokenSymbol = state.currentBounty?.chainData?.rewardTokenData?.symbol; + const hasReward = state.currentBounty?.data?.hasReward; + const isBountyClosed = !!state.currentBounty?.data?.isClosed; + const isBountyFunded = !!state.currentBounty?.data?.isFunded; + const isBountyInDraft = !!state.currentBounty?.data?.isDraft; + const transactionalSymbol = state.currentBounty?.data?.transactionalToken?.symbol; + const rewardTokenSymbol = state.currentBounty?.data?.rewardToken?.symbol; const fundsGiven = walletFunds?.reduce((acc, fund) => fund.amount.plus(acc), BigNumber(0)) || BigNumber(0); - const futureRewards = fundsGiven.multipliedBy(state.currentBounty?.chainData?.rewardAmount) + const futureRewards = fundsGiven.multipliedBy(state.currentBounty?.data?.rewardAmount) .dividedBy(state.currentBounty?.data?.fundingAmount).toFixed(); + + const collapseAction = isBountyClosed ? t("funding:rewards") : t("funding:actions.manage-funding"); const isCanceled = getIssueState({ state: state.currentBounty?.data?.state, @@ -57,16 +59,10 @@ export default function FundingSection() { if (!state.currentUser?.walletAddress || !state.currentBounty?.data) return; const funds = state.currentBounty?.data?.benefactors - .filter(fund => fund.address.toLowerCase() === state.currentUser.walletAddress.toLowerCase()) - .map((fund) => ({ - ...fund, - isWithdrawn: !!state.currentBounty?.chainData?.funding?.find((networkFund, key) => - key === fund.contractId && - networkFund.amount.isEqualTo(0)) - })); + .filter(fund => fund.address === state.currentUser.walletAddress); setWalletFunds(funds); - }, [state.currentUser, state.currentBounty?.data, state.currentBounty?.chainData]); + }, [state.currentUser, state.currentBounty?.data, state.currentBounty?.data]); if (isBountyFunded && !walletFunds?.length) return <>; @@ -88,9 +84,9 @@ export default function FundingSection() { {t("funding:title")}} col2={isBountyFunded || isCanceled ? <> : - } + } /> @@ -104,7 +100,7 @@ export default function FundingSection() { fundedAmount={state.currentBounty?.data?.fundedAmount?.toFixed()} fundingAmount={state.currentBounty?.data?.fundingAmount?.toFixed()} fundingTokenSymbol={transactionalSymbol} - fundedPercent={state.currentBounty?.data?.fundedPercent?.toFixed(2, 1)} + fundedPercent={state.currentBounty?.data?.fundedPercent?.toString()} /> { hasReward && @@ -117,8 +113,8 @@ export default function FundingSection() { } col2={ @@ -149,7 +145,7 @@ export default function FundingSection() { col2={ @@ -162,8 +158,8 @@ export default function FundingSection() { } col2={ - (isBountyInDraft || isBountyClosed && hasReward && !fund.isWithdrawn) && ( - + ) } /> diff --git a/components/bounty/funding-section/retract-or-withdraw-modal.tsx b/components/bounty/funding-section/retract-or-withdraw-modal.tsx index 90c96dd83a..00deb18bbf 100644 --- a/components/bounty/funding-section/retract-or-withdraw-modal.tsx +++ b/components/bounty/funding-section/retract-or-withdraw-modal.tsx @@ -3,7 +3,9 @@ import {Col, Row} from "react-bootstrap"; import {useTranslation} from "next-i18next"; +import {Amount, RowWithTwoColumns} from "components/bounty/funding-section/minimals"; import Button from "components/button"; +import ContractButton from "components/contract-button"; import Modal from "components/modal"; import {useAppState} from "contexts/app-state"; @@ -11,14 +13,10 @@ import {toastError, toastSuccess} from "contexts/reducers/change-toaster"; import {fundingBenefactor} from "interfaces/issue-data"; - import useApi from "x-hooks/use-api"; import useBepro from "x-hooks/use-bepro"; import {useBounty} from "x-hooks/use-bounty"; -import {Amount, RowWithTwoColumns} from "./minimals"; - - interface RetractOrWithdrawModalProps { show?: boolean; onCloseClick: () => void; @@ -35,30 +33,36 @@ export default function RetractOrWithdrawModal({ const [isExecuting, setIsExecuting] = useState(false); const { processEvent } = useApi(); + const { getDatabaseBounty } = useBounty(); const { handleRetractFundBounty, handleWithdrawFundRewardBounty } = useBepro(); - const { getDatabaseBounty, getChainBounty } = useBounty(); const { dispatch, state } = useAppState(); - const tokenSymbol = state.currentBounty?.chainData?.transactionalTokenData?.symbol; - const rewardTokenSymbol = state.currentBounty?.chainData?.rewardTokenData?.symbol; - const retractOrWithdrawAmount = state.currentBounty?.chainData?.closed ? - funding?.amount?.dividedBy(state.currentBounty?.chainData?.fundingAmount) - .multipliedBy(state.currentBounty?.chainData?.rewardAmount)?.toFixed() : + const isBountyClosed = !!state.currentBounty?.data?.isClosed; + const tokenSymbol = state.currentBounty?.data?.transactionalToken?.symbol; + const rewardTokenSymbol = state.currentBounty?.data?.rewardToken?.symbol; + const retractOrWithdrawAmount = isBountyClosed ? + funding?.amount?.dividedBy(state.currentBounty?.data?.fundingAmount) + .multipliedBy(state.currentBounty?.data?.rewardAmount)?.toFixed() : funding?.amount?.toFixed(); function handleRetractOrWithdraw() { - if (!state.currentBounty?.chainData || !funding) return; + if (!state.currentBounty?.data || !funding) return; setIsExecuting(true); - if(state.currentBounty?.chainData?.closed){ - handleWithdrawFundRewardBounty(state.currentBounty?.chainData?.id, + if(isBountyClosed){ + handleWithdrawFundRewardBounty(state.currentBounty?.data?.contractId, funding.contractId, retractOrWithdrawAmount, rewardTokenSymbol) .then(() => { + return processEvent("bounty", "withdraw", state.Service?.network?.lastVisited, { + issueId: state.currentBounty?.data?.issueId + }); + }) + .then(() => { + getDatabaseBounty(true); onCloseClick(); - getChainBounty(true); dispatch(toastSuccess(t("funding:modals.reward.withdraw-x-symbol", { amount: retractOrWithdrawAmount, symbol: rewardTokenSymbol @@ -70,9 +74,11 @@ export default function RetractOrWithdrawModal({ }) .finally(() => setIsExecuting(false)); } else { - handleRetractFundBounty(state.currentBounty?.chainData?.id, funding.contractId) + handleRetractFundBounty(state.currentBounty?.data?.contractId, funding.contractId) .then((txInfo) => { const { blockNumber: fromBlock } = txInfo as { blockNumber: number }; + + getDatabaseBounty(true); return processEvent("bounty", "funded", state.Service?.network?.lastVisited, { fromBlock @@ -80,8 +86,6 @@ export default function RetractOrWithdrawModal({ }) .then(() => { onCloseClick(); - getChainBounty(true); - getDatabaseBounty(true); dispatch(toastSuccess(t("funding:modals.retract.retract-x-symbol", { amount: retractOrWithdrawAmount, symbol: tokenSymbol @@ -97,7 +101,7 @@ export default function RetractOrWithdrawModal({ return(

- {state.currentBounty?.chainData?.closed ? + {isBountyClosed ? t("funding:modals.reward.description") : t("funding:modals.retract.description")}

{t("funding:modals.retract.from-the")}{" "} - {t("bounty:label")} #{state.currentBounty?.chainData?.id}{" "} + {t("bounty:label")} #{state.currentBounty?.data?.contractId}{" "} {t("funding:fund")}.

@@ -136,18 +140,18 @@ export default function RetractOrWithdrawModal({ } col2={ - + } />
diff --git a/components/bounty/issue-body.tsx b/components/bounty/issue-body.tsx index f9aa5fd8d7..cb8d35e445 100644 --- a/components/bounty/issue-body.tsx +++ b/components/bounty/issue-body.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "next-i18next"; +import IssueEditTag from "components/bounty/issue-edit-tag"; import Button from "components/button"; import CustomContainer from "components/custom-container"; import { IFilesProps } from "components/drag-and-drop"; @@ -16,8 +17,6 @@ import { BODY_CHARACTERES_LIMIT } from "helpers/constants"; import useApi from "x-hooks/use-api"; import { useBounty } from "x-hooks/use-bounty"; -import IssueEditTag from "./issue-edit-tag"; - interface issueBodyProps { isEditIssue: boolean; cancelEditIssue: () => void; diff --git a/components/bounty/issue-edit-tag.tsx b/components/bounty/issue-edit-tag.tsx index f21e8ed51c..dab0b995ad 100644 --- a/components/bounty/issue-edit-tag.tsx +++ b/components/bounty/issue-edit-tag.tsx @@ -1,6 +1,5 @@ import { useEffect } from "react"; import { Col, Row } from "react-bootstrap"; -import ReactSelect from "react-select"; import { useTranslation } from "next-i18next"; @@ -8,6 +7,7 @@ import { PROGRAMMING_LANGUAGES } from "assets/bounty-labels"; import BountyTags from "components/bounty/bounty-tags"; import { ContextualSpan } from "components/contextual-span"; +import ReactSelect from "components/react-select"; import { useAppState } from "contexts/app-state"; diff --git a/components/bounty/tabs-sections/index.tsx b/components/bounty/tabs-sections/index.tsx index 9650f9d968..f8aa59f19a 100644 --- a/components/bounty/tabs-sections/index.tsx +++ b/components/bounty/tabs-sections/index.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "next-i18next"; +import ItemSections from "components/bounty/tabs-sections/item-sections"; import CustomContainer from "components/custom-container"; import TabbedNavigation from "components/tabbed-navigation"; @@ -9,11 +10,11 @@ import { useAppState } from "contexts/app-state"; import { TabbedNavigationItem } from "interfaces/tabbed-navigation"; -import ItemSections from "./item-sections"; - function TabSections(){ const { t } = useTranslation("bounty"); + const {state} = useAppState(); + const [pullRequests, setPullRequests] = useState(state.currentBounty?.data?.pullRequests) const [proposals, setProposals] = useState(state.currentBounty?.data?.mergeProposals) diff --git a/components/bounty/tabs-sections/item-sections.tsx b/components/bounty/tabs-sections/item-sections.tsx index e13f597dd0..6a5a873cc4 100644 --- a/components/bounty/tabs-sections/item-sections.tsx +++ b/components/bounty/tabs-sections/item-sections.tsx @@ -63,7 +63,7 @@ function ItemSections({ data, isProposal }: ItemProps) { isMergeable: item?.isMergeable, isDraft: item?.status === "draft" }) - valueRedirect.prId = (item as pullRequest)?.githubId + valueRedirect.prId = (item as pullRequest)?.githubId; } else if(proposal){ if(isDisputed || isMerged){ status.push({ @@ -75,9 +75,14 @@ function ItemSections({ data, isProposal }: ItemProps) { valueRedirect.proposalId = item?.id } + const btnLabel = isProposal ? "actions.view-proposal" : + item.status === "draft" ? "actions.view-pull-request" : "actions.review"; const approvalsCurrentPr = item?.approvals?.total || 0; const shouldRenderApproveButton = approvalsCurrentPr < approvalsRequired && canUserApprove && !isProposal; - const itemId = isProposal ? item?.id : item?.githubId; + const itemId = isProposal ? item?.contractId + 1 : item?.githubId; + const totalToBeDisputed = BigNumber(state.Service?.network?.amounts?.percentageNeededForDispute) + .multipliedBy(state.Service?.network?.amounts?.totalNetworkToken) + .dividedBy(100); return ( @@ -117,18 +122,17 @@ function ItemSections({ data, isProposal }: ItemProps) {
diff --git a/components/branchs-dropdown.tsx b/components/branchs-dropdown.tsx index dbdd79d4a6..ee461ec901 100644 --- a/components/branchs-dropdown.tsx +++ b/components/branchs-dropdown.tsx @@ -4,8 +4,7 @@ import {useTranslation} from "next-i18next"; import ReactSelect from "components/react-select"; -import {useAppState} from "../contexts/app-state"; - +import {useAppState} from "contexts/app-state"; export default function BranchsDropdown({ repoId, @@ -25,7 +24,6 @@ export default function BranchsDropdown({ const [option, setOption] = useState<{ value: string; label: string }>() const { t } = useTranslation("common"); - function mapOptions() { if (!state.Service?.network?.repos?.active?.branches?.length || !repoId) return; diff --git a/components/card-become-council.tsx b/components/card-become-council.tsx index 7e9b8b906f..c5f8ed211f 100644 --- a/components/card-become-council.tsx +++ b/components/card-become-council.tsx @@ -6,19 +6,19 @@ import Link from "next/link"; import ArrowRight from "assets/icons/arrow-right"; import CloseIcon from "assets/icons/close-icon"; -import {formatNumberToNScale} from "helpers/formatNumber"; +import Button from "components/button"; +import {useAppState} from "contexts/app-state"; -import useNetworkTheme from "x-hooks/use-network-theme"; +import {formatNumberToNScale} from "helpers/formatNumber"; -import {useAppState} from "../contexts/app-state"; -import Button from "./button"; +import { useNetwork } from "x-hooks/use-network"; export default function CardBecomeCouncil() { const { t } = useTranslation("council"); const [show, setShow] = useState(true); const {state} = useAppState(); - const { getURLWithNetwork } = useNetworkTheme(); + const { getURLWithNetwork } = useNetwork(); if (!show) return null; @@ -40,7 +40,7 @@ export default function CardBecomeCouncil() { {formatNumberToNScale(+state.Service?.network?.amounts?.councilAmount)} {" "} - {state.Service?.network?.networkToken?.symbol} + {state.Service?.network?.active?.networkToken?.symbol} {" "} {t("become-council-description-part-two")}
diff --git a/components/chain-badge.tsx b/components/chain-badge.tsx new file mode 100644 index 0000000000..cc025e668a --- /dev/null +++ b/components/chain-badge.tsx @@ -0,0 +1,19 @@ +import Badge from "components/badge"; + +import { SupportedChainData } from "interfaces/supported-chain-data"; + +interface ChainBadgeProps { + chain: SupportedChainData; +} + +export default function ChainBadge({ chain } : ChainBadgeProps) { + return( + + ); +} \ No newline at end of file diff --git a/components/closed-network-alert.tsx b/components/closed-network-alert.tsx index 12fa6f2ba4..6f53a0f3c8 100644 --- a/components/closed-network-alert.tsx +++ b/components/closed-network-alert.tsx @@ -1,8 +1,14 @@ import { useTranslation } from "next-i18next"; -export default function ClosedNetworkAlert() { +interface ClosedNetworkAlertProps { + isVisible?: boolean; +} + +export default function ClosedNetworkAlert({ isVisible } : ClosedNetworkAlertProps) { const { t } = useTranslation("common"); + if (!isVisible) return <>; + return (
(null); const debounce = useRef(null) @@ -9,7 +9,7 @@ export default function ColorInput({ label, code, onChange, error = false }) { if (event.target.value === "#000000") { event.preventDefault(); event.stopPropagation(); - } else onChange({ label, code: color }); + } else onChange(onlyColorCode ? color : { label, code: color }); } function handleChange(event) { @@ -18,7 +18,7 @@ export default function ColorInput({ label, code, onChange, error = false }) { clearTimeout(debounce.current) debounce.current = setTimeout(() => { - onChange({ label, code: event.target.value.toUpperCase() }); + onChange(onlyColorCode ? color : { label, code: event.target.value.toUpperCase() }); }, 500) } diff --git a/components/connect-wallet-button.tsx b/components/connect-wallet-button.tsx index d86e37d67d..08b2b3462e 100644 --- a/components/connect-wallet-button.tsx +++ b/components/connect-wallet-button.tsx @@ -8,45 +8,32 @@ import metamaskLogo from "assets/metamask.png"; import Button from "components/button"; import Modal from "components/modal"; -import {NetworkColors} from "interfaces/enums/network-colors"; +import {useAppState} from "contexts/app-state"; +import {changeShowWeb3} from "contexts/reducers/update-show-prop"; import {useAuthentication} from "x-hooks/use-authentication"; -import {useAppState} from "../contexts/app-state"; -import {changeChain} from "../contexts/reducers/change-chain"; -import {changeShowWeb3} from "../contexts/reducers/update-show-prop"; - export default function ConnectWalletButton({children = null, asModal = false, forceLogin = false,}) { const { t } = useTranslation(["common", "connect-wallet-button"]); - const {dispatch, state} = useAppState(); const [showModal, setShowModal] = useState(false); + const {dispatch, state} = useAppState(); + const { connectWallet } = useAuthentication(); async function handleLogin() { + if(!window?.ethereum) { dispatch(changeShowWeb3(true)) return; } - if (!state.Service?.active) - return; - - if ( - (+state.connectedChain?.id || +window?.ethereum?.chainId) === - +state.Settings?.requiredChain?.id - ) { - connectWallet(); - } else { - dispatch(changeChain.update({...state.connectedChain, id: state.Settings?.requiredChain?.id})); - setShowModal(false); - } + connectWallet(); } - function handleShowModal() { - if (!state.currentUser?.walletAddress) setShowModal(true); - else setShowModal(false); + function onWalletChange() { + setShowModal(!state.currentUser?.walletAddress); } useEffect(() => { @@ -57,10 +44,7 @@ export default function ConnectWalletButton({children = null, asModal = false, f }, [state.Service?.active, forceLogin]); - useEffect(() => { - handleShowModal(); - }, [state.currentUser?.walletAddress]); - + useEffect(onWalletChange, [state.currentUser?.walletAddress]); if (asModal) { if (state?.loading?.isLoading) return <>; @@ -71,19 +55,10 @@ export default function ConnectWalletButton({children = null, asModal = false, f titlePosition="center" centerTitle titleClass="h3 text-white bg-opacity-100" - show={showModal} - > + show={showModal}>
- {t("connect-wallet-button:to-access-this-page")} -
- - {state.Settings?.requiredChain?.name}{" "} - {t("connect-wallet-button:network")} - {" "} - {t("connect-wallet-button:on-your-wallet")} + {t("connect-wallet-button:this-page-needs-access-to-your-wallet-address")}
+ onClick={handleLogin}> {t("main-nav.connect")} ); - return children; + return <>{children}; } diff --git a/components/contextual-span.tsx b/components/contextual-span.tsx index 0619cce6d9..49321c8842 100644 --- a/components/contextual-span.tsx +++ b/components/contextual-span.tsx @@ -14,10 +14,18 @@ interface ContextualSpanProps { context: "success" | "danger" | "warning" | "info" | "primary"; color?: string; className?: string; + classNameIcon?: string; isAlert?: boolean; } -export function ContextualSpan({ children, context, color, className = "", isAlert } : ContextualSpanProps) { +export function ContextualSpan({ + children, + context, + color, + className = "", + isAlert, + classNameIcon, +}: ContextualSpanProps) { const contextColor = color || context; const CLASSES = clsx("p family-Regular font-weight-medium border-radius-4 align-items-center mx-0 px-1", `text-${contextColor} ${className}`, @@ -37,7 +45,7 @@ export function ContextualSpan({ children, context, color, className = "", isAle return( - + {children} diff --git a/components/contract-button.tsx b/components/contract-button.tsx new file mode 100644 index 0000000000..3e79633602 --- /dev/null +++ b/components/contract-button.tsx @@ -0,0 +1,29 @@ +import Button, { ButtonProps } from "components/button"; + +import { useAppState } from "contexts/app-state"; +import { changeNeedsToChangeChain } from "contexts/reducers/change-spinners"; +import { changeShowWeb3 } from "contexts/reducers/update-show-prop"; + +import { UNSUPPORTED_CHAIN } from "helpers/constants"; + +export default function ContractButton({ onClick, children, ...rest }: ButtonProps) { + const { state, dispatch } = useAppState(); + + function handleExecute() { + if(!window.ethereum) return dispatch(changeShowWeb3(true)); + + if (state.connectedChain?.matchWithNetworkChain === false || state.connectedChain?.name === UNSUPPORTED_CHAIN) + dispatch(changeNeedsToChangeChain(true)); + else + onClick?.(); + } + + return( + + ); +} \ No newline at end of file diff --git a/components/council-layout.tsx b/components/council-layout.tsx index c7a358584c..5e331eed69 100644 --- a/components/council-layout.tsx +++ b/components/council-layout.tsx @@ -1,5 +1,6 @@ import React, {useEffect, useState} from "react"; +import BigNumber from "bignumber.js"; import {useTranslation} from "next-i18next"; import {useRouter} from "next/router"; @@ -10,15 +11,18 @@ import PageHero, {InfosHero} from "components/page-hero"; import {useAppState} from "contexts/app-state"; import { changeActiveNetwork } from "contexts/reducers/change-service"; +import { Curator } from "interfaces/curators"; import { IssueBigNumberData } from "interfaces/issue-data"; import useApi from "x-hooks/use-api"; +import useChain from "x-hooks/use-chain"; import {useNetwork} from "x-hooks/use-network"; export default function CouncilLayout({ children }) { - const { asPath, push } = useRouter(); + const { asPath, query, push } = useRouter(); const { t } = useTranslation(["common", "council"]); + const { chain } = useChain(); const { state, dispatch} = useAppState(); const { getURLWithNetwork } = useNetwork(); const { getTotalBounties, searchCurators, searchIssues } = useApi(); @@ -39,7 +43,7 @@ export default function CouncilLayout({ children }) { }, { value: 0, - label: t("heroes.bounties-in-network"), + label: t("heroes.in-network"), currency: t("misc.token"), }, ]); @@ -47,52 +51,58 @@ export default function CouncilLayout({ children }) { function handleUrlCurators (type: string) { return push(getURLWithNetwork("/curators", { type - }), undefined, { shallow: true }) + }), asPath, { shallow: true }); } const internalLinks = [ { onClick: () => handleUrlCurators("ready-to-propose"), label: t("council:ready-to-propose"), - active: asPath.endsWith("/curators") || asPath.endsWith("ready-to-propose") + active: query?.type === "ready-to-propose" || !query?.type }, { onClick: () => handleUrlCurators("ready-to-dispute"), label: t("council:ready-to-dispute"), - active: asPath.endsWith("ready-to-dispute") + active: query?.type === "ready-to-dispute" }, { onClick: () => handleUrlCurators("ready-to-close"), label: t("council:ready-to-close"), - active: asPath.endsWith("ready-to-close") + active: query?.type === "ready-to-close" }, { onClick: () => handleUrlCurators("curators-list"), label: t("council:council-list"), - active: asPath.endsWith("curators-list") + active: query?.type === "curators-list" } ] async function loadTotals() { - if (!state.Service?.active?.network || - !state.Service?.network?.active?.name || - !state.Service?.network?.networkToken?.address) return; + if (!state.Service?.network?.active?.name || !chain) return; const [totalBounties, onNetwork, curators, distributed] = await Promise.all([ - getTotalBounties("ready", state.Service?.network?.active?.name), - state.Service?.active.getTotalNetworkToken(), + getTotalBounties(state.Service?.network?.active?.name, "ready"), searchCurators({ - isCurrentlyCurator: true, networkName: state.Service?.network?.active?.name, + chainShortName: chain.chainShortName }).then(({ rows }) => rows), searchIssues({ state: "closed", networkName: state.Service.network.active.name, - tokenAddress: state.Service.network.networkToken.address + tokenAddress: state.Service?.network?.active?.networkToken?.address, + chainId: chain.chainId.toString() }) .then(({ rows } : { rows: IssueBigNumberData[] }) => rows.reduce((acc, { payments }) => acc + payments.reduce((acc, { ammount }) => acc + ammount, 0), 0)) - ]); + ]) + .then(([totalBounties, curators, distributed]) => { + const { onNetwork, totalCurators } = (curators as Curator[]).reduce((acc, curator) => ({ + onNetwork: new BigNumber(acc.onNetwork).plus(curator.tokensLocked).toFixed(), + totalCurators: acc.totalCurators + (curator.isCurrentlyCurator ? 1 : 0 ) + }), { onNetwork: "0", totalCurators: 0 }); + + return [totalBounties, onNetwork, totalCurators, distributed]; + }); dispatch(changeActiveNetwork(Object.assign(state.Service.network.active, { curators }))); @@ -102,34 +112,32 @@ export default function CouncilLayout({ children }) { label: t("council:ready-bountys"), }, { - value: curators.length || 0, + value: curators, label: t("council:council-members"), }, { value: distributed, label: t("council:distributed-developers"), - currency: state.Service?.network?.networkToken?.symbol, + currency: state.Service?.network?.active?.networkToken?.symbol, }, { - value: onNetwork.toFixed(), + value: onNetwork, label: t("heroes.in-network"), - currency: state.Service?.network?.networkToken?.symbol, + currency: state.Service?.network?.active?.networkToken?.symbol, }, ]); } useEffect(() => { loadTotals(); - }, [state.Service?.active?.network?.contractAddress, - state.Service?.network?.active?.name, - state.Service?.network?.networkToken?.address]); + }, [state.Service?.network?.active?.name, chain]); return (
diff --git a/components/create-bounty-modal.tsx b/components/create-bounty-modal.tsx index 6f900f0464..89d5ee4dd5 100644 --- a/components/create-bounty-modal.tsx +++ b/components/create-bounty-modal.tsx @@ -9,16 +9,22 @@ import router from "next/router"; import BranchsDropdown from "components/branchs-dropdown"; import Button from "components/button"; import ConnectWalletButton from "components/connect-wallet-button"; +import ContractButton from "components/contract-button"; import CreateBountyDetails from "components/create-bounty-details"; import CreateBountyProgress from "components/create-bounty-progress"; import CreateBountyTokenAmount from "components/create-bounty-token-amount"; import {IFilesProps} from "components/drag-and-drop"; +import DropDown from "components/dropdown"; import GithubInfo from "components/github-info"; +import InfoTooltip from "components/info-tooltip"; import Modal from "components/modal"; import ReadOnlyButtonWrapper from "components/read-only-button-wrapper"; import ReposDropdown from "components/repos-dropdown"; +import {useAppState} from "contexts/app-state"; import {toastError, toastWarning} from "contexts/reducers/change-toaster"; +import {addTx, updateTx} from "contexts/reducers/change-tx-list"; +import {changeShowCreateBounty} from "contexts/reducers/update-show-prop"; import { BODY_CHARACTERES_LIMIT } from "helpers/constants"; import {parseTransaction} from "helpers/transactions"; @@ -33,15 +39,10 @@ import {getCoinInfoByContract} from "services/coingecko"; import useApi from "x-hooks/use-api"; import useBepro from "x-hooks/use-bepro"; +import useChain from "x-hooks/use-chain"; import useERC20 from "x-hooks/use-erc20"; import {useNetwork} from "x-hooks/use-network"; - -import {useAppState} from "../contexts/app-state"; -import {addTx, updateTx} from "../contexts/reducers/change-tx-list"; -import {changeShowCreateBounty} from "../contexts/reducers/update-show-prop"; -import {useRepos} from "../x-hooks/use-repos"; -import DropDown from "./dropdown"; -import InfoTooltip from "./info-tooltip"; +import {useRepos} from "x-hooks/use-repos"; interface BountyPayload { title: string; @@ -88,13 +89,13 @@ export default function CreateBountyModal() { const [selectedTags, setSelectedTags]= useState([]); const rewardERC20 = useERC20(); - const transactionalERC20 = useERC20(); + const { chain } = useChain(); + const {updateActiveRepo} = useRepos(); const { handleApproveToken } = useBepro(); const { getURLWithNetwork } = useNetwork(); const { createPreBounty, processEvent } = useApi(); - const {updateActiveRepo} = useRepos(); const { dispatch, @@ -438,6 +439,20 @@ export default function CreateBountyModal() { } + function cleanFields() { + setFiles([]); + setSelectedTags([]); + setBountyTitle(""); + setBountyDescription(""); + setIssueAmount(ZeroNumberFormatValues); + setRewardAmount(ZeroNumberFormatValues); + setRepository(undefined); + setBranch(null); + setCurrentSection(0); + rewardERC20.setAddress(undefined); + transactionalERC20.setAddress(undefined); + } + function handleCancelAndBack() { if (currentSection === 0) { cleanFields(); @@ -452,19 +467,6 @@ export default function CreateBountyModal() { setProgressPercentage(progress[steps.findIndex((value) => value === steps[currentSection])]); } - function cleanFields() { - setFiles([]); - setSelectedTags([]); - setBountyTitle(""); - setBountyDescription(""); - setIssueAmount(ZeroNumberFormatValues); - setRewardAmount(ZeroNumberFormatValues); - setRepository(undefined); - setBranch(null); - setIsKyc(false); - setTierList([]); - setCurrentSection(0); - } const isAmountApproved = (tokenAllowance: BigNumber, amount: BigNumber) => !tokenAllowance.lt(amount); @@ -638,14 +640,6 @@ export default function CreateBountyModal() { setProgressBar(); }, [currentSection, showCreateBounty]); - useEffect(() => { - if(!showCreateBounty) return; - if(customTokens?.length === 1) { - setTransactionalToken(customTokens[0]) - setRewardToken(customTokens[0]) - } - }, [customTokens, showCreateBounty]); - useEffect(() => { if(!showCreateBounty) return; let approved = true @@ -659,17 +653,27 @@ export default function CreateBountyModal() { }, [transactionalERC20.allowance, rewardERC20.allowance, issueAmount, rewardAmount, rewardChecked]); useEffect(() => { - if (!Service?.network?.active?.tokens || !showCreateBounty) + if (!Service?.network?.active?.tokens || !showCreateBounty || !chain) { + setTransactionalToken(undefined); + setRewardToken(undefined); + setCustomTokens([]); + transactionalERC20.setAddress(undefined); + rewardERC20.setAddress(undefined); return; + } const tokens = Service?.network.active.tokens || []; if (tokens.length === customTokens.length) return; - setCustomTokens(tokens); + if (tokens.length === 1) { + setTransactionalToken(tokens[0]); + setRewardToken(tokens[0]); + } - }, [Service?.network?.active?.tokens, showCreateBounty]); + setCustomTokens(tokens); + }, [Service?.network?.active?.tokens, showCreateBounty, chain]); useEffect(()=>{ cleanFields(); @@ -679,7 +683,7 @@ export default function CreateBountyModal() { },[showCreateBounty]) if(!showCreateBounty) - return <> + return <>; if (showCreateBounty && !currentUser?.walletAddress) return ; @@ -713,19 +717,19 @@ export default function CreateBountyModal() { {!isTokenApproved && currentSection === 3 ? ( - + ) : null} { (isTokenApproved && currentSection === 3 || currentSection !== 3) && - + }
diff --git a/components/create-bounty-token-amount.tsx b/components/create-bounty-token-amount.tsx index 874d4e70a7..01525314c6 100644 --- a/components/create-bounty-token-amount.tsx +++ b/components/create-bounty-token-amount.tsx @@ -7,11 +7,12 @@ import getConfig from "next/config"; import ArrowRight from "assets/icons/arrow-right"; -import {useAppState} from "../contexts/app-state"; -import {getCoinPrice} from "../services/coingecko"; -import InputNumber from "./input-number"; -import TokensDropdown from "./tokens-dropdown"; +import InputNumber from "components/input-number"; +import TokensDropdown from "components/tokens-dropdown"; +import {useAppState} from "contexts/app-state"; + +import {getCoinPrice} from "services/coingecko"; export default function CreateBountyTokenAmount({ currentToken, diff --git a/components/create-proposal.tsx b/components/create-proposal.tsx index cf812a4e96..998f9156f7 100644 --- a/components/create-proposal.tsx +++ b/components/create-proposal.tsx @@ -9,12 +9,15 @@ import LockedIcon from "assets/icons/locked-icon"; import Avatar from "components/avatar"; import Button from "components/button"; +import ContractButton from "components/contract-button"; import CreateProposalDistributionItem from "components/create-proposal-distribution-item"; import Modal from "components/modal"; import PullRequestLabels from "components/pull-request-labels"; import ReactSelect from "components/react-select"; import ReadOnlyButtonWrapper from "components/read-only-button-wrapper"; +import {useAppState} from "contexts/app-state"; + import calculateDistributedAmounts from "helpers/calculateDistributedAmounts"; import sumObj from "helpers/sumObj"; @@ -22,12 +25,9 @@ import {pullRequest} from "interfaces/issue-data"; import useApi from "x-hooks/use-api"; import useBepro from "x-hooks/use-bepro"; +import {useBounty} from "x-hooks/use-bounty"; import useOctokit from "x-hooks/use-octokit"; -import {useAppState} from "../contexts/app-state"; -import {useBounty} from "../x-hooks/use-bounty"; - - interface participants { githubHandle: string; githubLogin: string; @@ -133,8 +133,7 @@ export default function NewProposal({amountTotal, pullRequests = []}) { }); } - function isSameProposal(currentDistrbuition: SameProposal, - currentProposals: SameProposal[]) { + function isSameProposal(currentDistrbuition: SameProposal, currentProposals: SameProposal[]) { return currentProposals.some((activeProposal) => { if (activeProposal.currentPrId === currentDistrbuition.currentPrId) { return activeProposal.prAddressAmount.every((ap) => @@ -146,7 +145,6 @@ export default function NewProposal({amountTotal, pullRequests = []}) { }); } - function handleCheckDistrib(obj: object) { const currentAmount = sumObj(obj); @@ -168,13 +166,12 @@ export default function NewProposal({amountTotal, pullRequests = []}) { })) }; - const currentProposals = state.currentBounty?.chainData?.proposals?.map((item) => { + const currentProposals = state.currentBounty?.data?.mergeProposals?.map((item) => { return { - currentPrId: - Number(state.currentBounty?.data?.mergeProposals.find(mp=> +mp?.contractId === item.id)?.pullRequestId), - prAddressAmount: item.details.map(detail => ({ - amount: Number(detail.percentage), - address: detail.recipient + currentPrId: item.pullRequestId, + prAddressAmount: item.distributions.map(distribution => ({ + amount: distribution.percentage, + address: distribution.recipient })) }; }); @@ -277,7 +274,7 @@ export default function NewProposal({amountTotal, pullRequests = []}) { setExecuting(true); - handleProposeMerge(+state.currentBounty?.chainData.id, +currentPullRequest.contractId, prAddresses, prAmounts) + handleProposeMerge(+state.currentBounty?.data.contractId, +currentPullRequest.contractId, prAddresses, prAmounts) .then(txInfo => { const { blockNumber: fromBlock } = txInfo as { blockNumber: number }; @@ -287,7 +284,6 @@ export default function NewProposal({amountTotal, pullRequests = []}) { handleClose(); setExecuting(false); currentBounty.getDatabaseBounty(true); - currentBounty.getChainBounty(true); }) } @@ -401,9 +397,12 @@ export default function NewProposal({amountTotal, pullRequests = []}) { return (
- + {t("actions.cancel")} - +
}>

diff --git a/components/create-pull-request-modal.tsx b/components/create-pull-request-modal.tsx index 5c375c8364..8b856fccb4 100644 --- a/components/create-pull-request-modal.tsx +++ b/components/create-pull-request-modal.tsx @@ -4,14 +4,15 @@ import {useTranslation} from "next-i18next"; import Badge from "components/badge"; import Button from "components/button"; +import ContractButton from "components/contract-button"; import IconOption from "components/icon-option"; import IconSingleValue from "components/icon-single-value"; import Modal from "components/modal"; import ReactSelect from "components/react-select"; -import useOctokit from "x-hooks/use-octokit"; +import {useAppState} from "contexts/app-state"; -import {useAppState} from "../contexts/app-state"; +import useOctokit from "x-hooks/use-octokit"; interface props { show: boolean, @@ -148,14 +149,14 @@ export default function CreatePullRequestModal({ {t("actions.cancel")} - +

) } diff --git a/components/create-review-modal.tsx b/components/create-review-modal.tsx index 1278b49534..5d37237365 100644 --- a/components/create-review-modal.tsx +++ b/components/create-review-modal.tsx @@ -2,8 +2,6 @@ import { useEffect, useState } from "react"; import { useTranslation } from "next-i18next"; -import LockedIcon from "assets/icons/locked-icon"; - import Avatar from "components/avatar"; import Button from "components/button"; import GithubInfo from "components/github-info"; @@ -15,6 +13,8 @@ import { formatDate } from "helpers/formatDate"; import { pullRequest } from "interfaces/issue-data"; +import ContractButton from "./contract-button"; + interface CreateReviewModalModalProps { show: boolean, isExecuting: boolean, @@ -116,20 +116,15 @@ export default function CreateReviewModal({ > {t("actions.cancel")} - +
diff --git a/components/curators-list.tsx b/components/curators-list.tsx index 885d9787d7..07c600725d 100644 --- a/components/curators-list.tsx +++ b/components/curators-list.tsx @@ -1,10 +1,15 @@ import { useEffect, useState } from "react"; import { useTranslation } from "next-i18next"; +import { useRouter } from "next/router"; import Button from "components/button"; +import CuratorListBar from "components/curator-list-bar"; +import CuratorListItem from "components/curator-list-item"; import CustomContainer from "components/custom-container"; +import InfiniteScroll from "components/infinite-scroll"; import NothingFound from "components/nothing-found"; +import ScrollTopButton from "components/scroll-top-button"; import { useAppState } from "contexts/app-state"; import { changeLoadState } from "contexts/reducers/change-load"; @@ -14,17 +19,13 @@ import { Curator } from "interfaces/curators"; import useApi from "x-hooks/use-api"; import usePage from "x-hooks/use-page"; -import CuratorListBar from "./curator-list-bar"; -import CuratorListItem from "./curator-list-item"; -import InfiniteScroll from "./infinite-scroll"; -import ScrollTopButton from "./scroll-top-button"; - interface CuratorsPages { curators: Curator[]; page: number; } export default function CuratorsList({ inView }: { inView?: boolean }) { + const { query } = useRouter(); const { t } = useTranslation(["common", "council"]); const [hasMore, setHasMore] = useState(false); @@ -32,23 +33,22 @@ export default function CuratorsList({ inView }: { inView?: boolean }) { const [isEmptyPage, setIsEmptyPage] = useState(); const [curatorsPage, setCuratorsPage] = useState([]); - const { page, nextPage, goToFirstPage } = usePage(); - const { searchCurators } = useApi(); - const { state, dispatch } = useAppState(); + const { page, nextPage, goToFirstPage } = usePage(); function handlerSearch() { - if (!state.Service?.network?.active || inView === false) return; + if (!state.Service?.network?.active || inView === false || !query?.chain) return; dispatch(changeLoadState(true)); searchCurators({ isCurrentlyCurator: true, - networkName: state.Service?.network?.lastVisited, + networkName: state.Service?.network?.active?.name, sortBy: "acceptedProposals", order: "asc", - page + page, + chainShortName: query.chain.toString() }) .then(({ rows, pages, currentPage }) => { if (currentPage > 1) { @@ -78,7 +78,7 @@ export default function CuratorsList({ inView }: { inView?: boolean }) { }); } - useEffect(handlerSearch, [page, state.Service?.network?.lastVisited, inView]); + useEffect(handlerSearch, [page, state.Service?.network?.active, inView, query?.chain]); useEffect(() => { if (page) { @@ -115,13 +115,14 @@ export default function CuratorsList({ inView }: { inView?: boolean }) { isLoading={state.loading?.isLoading} hasMore={hasMore} > - {curatorsPage.map(({ curators }) => { - return curators?.map((curator) => ( - - )); - })} + { + curatorsPage + .flatMap(({ curators }) => curators ) + .map(curator => ()) + } )) || <>} diff --git a/components/custom-network/erc20-details.tsx b/components/custom-network/erc20-details.tsx index f9cf02c7a3..7762b44895 100644 --- a/components/custom-network/erc20-details.tsx +++ b/components/custom-network/erc20-details.tsx @@ -4,17 +4,16 @@ import {Col, Row} from "react-bootstrap"; import BigNumber from "bignumber.js"; import {useTranslation} from "next-i18next"; -import Button from "components/button"; +import ContractButton from "components/contract-button"; import {FormGroup} from "components/form-group"; +import {useAppState} from "contexts/app-state"; import {toastError, toastSuccess} from "contexts/reducers/change-toaster"; import {formatStringToCurrency} from "helpers/formatNumber"; import useERC20 from "x-hooks/use-erc20"; -import {useAppState} from "../../contexts/app-state"; - interface ERC20DetailsProps { address?: string; readOnly?: boolean; @@ -170,14 +169,14 @@ export function ERC20Details({ { isDeployer && - + } diff --git a/components/custom-network/lock-bepro-step.tsx b/components/custom-network/lock-bepro-step.tsx index b196bc78b9..5b667f4cd2 100644 --- a/components/custom-network/lock-bepro-step.tsx +++ b/components/custom-network/lock-bepro-step.tsx @@ -6,8 +6,8 @@ import {useTranslation} from "next-i18next"; import ArrowRightLine from "assets/icons/arrow-right-line"; -import Button from "components/button"; import ConnectGithub from "components/connect-github"; +import ContractButton from "components/contract-button"; import AmountWithPreview from "components/custom-network/amount-with-preview"; import InputNumber from "components/input-number"; import Step from "components/step"; @@ -15,20 +15,20 @@ import UnlockBeproModal from "components/unlock-bepro-modal"; import {useAppState} from "contexts/app-state"; import {useNetworkSettings} from "contexts/network-settings"; -import { TxList, addTx, updateTx } from "contexts/reducers/change-tx-list"; +import {addTx, TxList, updateTx} from "contexts/reducers/change-tx-list"; +import { UNSUPPORTED_CHAIN } from "helpers/constants"; import {formatNumberToCurrency, formatNumberToNScale} from "helpers/formatNumber"; -import { parseTransaction } from "helpers/transactions"; +import {parseTransaction} from "helpers/transactions"; -import { TransactionStatus } from "interfaces/enums/transaction-status"; +import {TransactionStatus} from "interfaces/enums/transaction-status"; import {TransactionTypes} from "interfaces/enums/transaction-types"; import {StepWrapperProps} from "interfaces/stepper"; -import { SimpleBlockTransactionPayload } from "interfaces/transaction"; +import {SimpleBlockTransactionPayload} from "interfaces/transaction"; import {useAuthentication} from "x-hooks/use-authentication"; import useERC20 from "x-hooks/use-erc20"; - export default function LockBeproStep({ activeStep, index, handleClick, validated }: StepWrapperProps) { const { t } = useTranslation(["common", "bounty","custom-network"]); @@ -38,10 +38,9 @@ export default function LockBeproStep({ activeStep, index, handleClick, validate const [isUnlocking, setIsUnlocking] = useState(false); const [isApproving, setIsApproving] = useState(false); const [showUnlockBepro, setShowUnlockBepro] = useState(false); - const [settlerAllowance, setSettlerAllowance] = useState(); - const { state, dispatch } = useAppState(); const registryToken = useERC20(); + const { state, dispatch } = useAppState(); const { updateWalletBalance } = useAuthentication(); const { tokensLocked, updateTokenBalance } = useNetworkSettings(); @@ -63,7 +62,7 @@ export default function LockBeproStep({ activeStep, index, handleClick, validate const maxValue = BigNumber.minimum(balance.beproAvailable, amountNeeded?.minus(amountLocked)); const textAmountClass = amount?.gt(balance.beproAvailable) ? "danger" : "primary"; const amountsClass = amount?.gt(maxValue) ? "danger" : "success"; - const needsAllowance = amount?.gt(settlerAllowance); + const needsAllowance = amount?.gt(registryToken.allowance); const isLockBtnDisabled = [ !amount, amount?.isZero(), @@ -71,7 +70,9 @@ export default function LockBeproStep({ activeStep, index, handleClick, validate lockedPercent?.gte(100), amount?.gt(maxValue) ].some(c => c); - const isUnlockBtnDisabled = lockedPercent?.isZero() || lockedPercent?.isNaN(); + const isUnlockBtnDisabled = + lockedPercent?.isZero() || lockedPercent?.isNaN() || !!state.currentUser?.hasRegisteredNetwork; + const isAmountInputDisabled = !!lockedPercent?.gte(100) || !!state.currentUser?.hasRegisteredNetwork; const failTx = (err, tx) => { @@ -98,7 +99,7 @@ export default function LockBeproStep({ activeStep, index, handleClick, validate state.Service?.active.lockInRegistry(amount.toFixed()) .then((tx) => { updateWalletBalance(); - updateAllowance(); + registryToken.updateAllowanceAndBalance(); setAmount(BigNumber(0)); dispatch(updateTx([parseTransaction(tx, lockTxAction.payload[0] as SimpleBlockTransactionPayload)])); return updateTokenBalance() @@ -124,7 +125,7 @@ export default function LockBeproStep({ activeStep, index, handleClick, validate state.Service?.active.unlockFromRegistry() .then((tx) => { updateWalletBalance(); - updateAllowance(); + registryToken.updateAllowanceAndBalance(); setAmount(BigNumber(0)); dispatch(updateTx([parseTransaction(tx, unlockTxAction.payload[0] as SimpleBlockTransactionPayload)])); return updateTokenBalance(); @@ -156,7 +157,7 @@ export default function LockBeproStep({ activeStep, index, handleClick, validate } function handleSetMaxValue() { - if (lockedPercent?.lt(100)) setAmount(maxValue); + if (lockedPercent?.lt(100) && !isAmountInputDisabled) setAmount(maxValue); } function handleApproval() { @@ -164,13 +165,13 @@ export default function LockBeproStep({ activeStep, index, handleClick, validate const approveTxAction = addTx([{ type: TransactionTypes.approveTransactionalERC20Token }] as TxList); - dispatch(approveTxAction) - setIsApproving(true) + dispatch(approveTxAction); + setIsApproving(true); state.Service?.active.approveTokenInRegistry(amount?.toFixed()) .then((tx) => { + registryToken.updateAllowanceAndBalance(); dispatch(updateTx([parseTransaction(tx, approveTxAction.payload[0] as SimpleBlockTransactionPayload)])); - return updateAllowance() }) .catch((err) => { failTx(err, approveTxAction); @@ -178,36 +179,22 @@ export default function LockBeproStep({ activeStep, index, handleClick, validate .finally(()=> setIsApproving(false)); } - function updateAllowance() { - if (!state.Service?.active || - !state.currentUser?.walletAddress || - !registryToken.address || - !state.Settings?.contracts?.networkRegistry) return; - - const { address } = registryToken; - const { walletAddress} = state.currentUser; - const { networkRegistry } = state.Settings.contracts; - - state.Service.active.getAllowance(address, walletAddress, networkRegistry) - .then(setSettlerAllowance).catch(() => BigNumber(0)); - } - - useEffect(() => { - updateAllowance(); - }, [state.Service?.active, - state.currentUser?.walletAddress, - registryToken.address, - state.Settings?.contracts?.networkRegistry]); - useEffect(() => { const tokenAddress = state.Service?.active?.registry?.token?.contractAddress; - const registryAddress = state.Settings?.contracts?.networkRegistry; + const registryAddress = state.Service?.active?.registry?.contractAddress; - if (tokenAddress && registryAddress) { + if (tokenAddress && registryAddress && state.connectedChain?.name !== UNSUPPORTED_CHAIN) { registryToken.setAddress(tokenAddress); registryToken.setSpender(registryAddress); + } else { + registryToken.setAddress(undefined); + registryToken.setSpender(undefined); } - }, [state.Service?.active?.registry?.token?.contractAddress, state.Settings?.contracts?.networkRegistry]); + }, [ + state.Service?.active?.registry?.token?.contractAddress, + state.Service?.active?.registry?.contractAddress, + state.connectedChain?.name + ]); return ( { needsAllowance && - + || - + } - +
diff --git a/components/custom-network/network-contract-settings.tsx b/components/custom-network/network-contract-settings.tsx index ec0a210a50..08df54faae 100644 --- a/components/custom-network/network-contract-settings.tsx +++ b/components/custom-network/network-contract-settings.tsx @@ -17,7 +17,7 @@ export default function NetworkContractSettings() { const onChange = (label) => (value) => fields.parameter.setter({label, value}); - const networkTokenSymbol = state.Service?.network?.networkToken?.symbol || t("misc.$token"); + const networkTokenSymbol = state.Service?.network?.active?.networkToken?.symbol || t("misc.$token"); const totalNetworkToken = BigNumber(state.Service?.network?.amounts?.totalNetworkToken); const parameterInputs = [ diff --git a/components/custom-network/new-network-stepper.tsx b/components/custom-network/new-network-stepper.tsx index f4f0d9b201..243b3715cf 100644 --- a/components/custom-network/new-network-stepper.tsx +++ b/components/custom-network/new-network-stepper.tsx @@ -1,22 +1,24 @@ import {useEffect, useState} from "react"; -import {Defaults} from "@taikai/dappkit"; +import {isZeroAddress} from "ethereumjs-util"; import {useTranslation} from "next-i18next"; import {useRouter} from "next/router"; -import AlreadyHasNetworkModal from "components/already-has-network-modal"; import ConnectWalletButton from "components/connect-wallet-button"; +import {ContextualSpan} from "components/contextual-span"; import CreatingNetworkLoader from "components/creating-network-loader"; import LockBeproStep from "components/custom-network/lock-bepro-step"; import NetworkInformationStep from "components/custom-network/network-information-step"; import NetworkSettingsStep from "components/custom-network/network-settings-step"; import SelectRepositoriesStep from "components/custom-network/select-repositories-step"; import TokenConfiguration from "components/custom-network/token-configuration"; +import If from "components/If"; import Stepper from "components/stepper"; import {useAppState} from "contexts/app-state"; import {NetworkSettingsProvider, useNetworkSettings} from "contexts/network-settings"; import {changeLoadState} from "contexts/reducers/change-load"; +import { changeNeedsToChangeChain } from "contexts/reducers/change-spinners"; import {addToast} from "contexts/reducers/change-toaster"; import { @@ -27,31 +29,35 @@ import { DEFAULT_MERGER_FEE, DEFAULT_ORACLE_EXCHANGE_RATE, DEFAULT_PERCENTAGE_FOR_DISPUTE, - DEFAULT_PROPOSER_FEE + DEFAULT_PROPOSER_FEE, + UNSUPPORTED_CHAIN, + WANT_TO_CREATE_NETWORK } from "helpers/constants"; import {psReadAsText} from "helpers/file-reader"; import useApi from "x-hooks/use-api"; import useBepro from "x-hooks/use-bepro"; +import { useNetwork } from "x-hooks/use-network"; import useNetworkTheme from "x-hooks/use-network-theme"; +import useSignature from "x-hooks/use-signature"; function NewNetwork() { const router = useRouter(); const { t } = useTranslation(["common", "custom-network"]); - const [creatingNetwork, setCreatingNetwork] = useState(-1); const [hasNetwork, setHasNetwork] = useState(false); + const [creatingNetwork, setCreatingNetwork] = useState(-1); const { state, dispatch } = useAppState(); + const { signMessage } = useSignature(); + const { colorsToCSS } = useNetworkTheme(); + const { getURLWithNetwork } = useNetwork(); const { createNetwork, processEvent } = useApi(); - const { handleChangeNetworkParameter } = useBepro(); - const { getURLWithNetwork, colorsToCSS } = useNetworkTheme(); + const { handleDeployNetworkV2, handleAddNetworkToRegistry, handleChangeNetworkParameter } = useBepro(); const { tokensLocked, details, github, tokens, settings, isSettingsValidated, cleanStorage } = useNetworkSettings(); - const { handleDeployNetworkV2, handleAddNetworkToRegistry } = useBepro(); - const defaultNetworkName = state?.Service?.network?.active?.name.toLowerCase(); const isSetupPage = router?.pathname?.toString()?.includes("setup"); const creationSteps = [ @@ -65,14 +71,24 @@ function NewNetwork() { { id: 1, name: t("custom-network:modals.loader.steps.changing-merger-fee") }, { id: 1, name: t("custom-network:modals.loader.steps.changing-proposer-fee") }, { id: 2, name: t("custom-network:modals.loader.steps.add-to-registry") }, - { id: 3, name: t("custom-network:modals.loader.steps.sync-web-network") } + { id: 3, name: t("custom-network:modals.loader.steps.sync-web-network") }, + { id: 3, name: t("custom-network:modals.loader.steps.sync-chain-id") } ]; async function handleCreateNetwork() { if (!state.currentUser?.login || !state.currentUser?.walletAddress || !state.Service?.active) return; + + const signedMessage = await signMessage(WANT_TO_CREATE_NETWORK); + + if (!signedMessage) + return; + setCreatingNetwork(0); - const deployNetworkTX = await handleDeployNetworkV2(tokens.settler).catch(error => error); + const deployNetworkTX = await handleDeployNetworkV2(tokens.settler).catch(error => { + console.debug("Failed to deploy network", error); + return error; + }); if (!deployNetworkTX?.contractAddress) return setCreatingNetwork(-1); @@ -95,7 +111,8 @@ function NewNetwork() { githubLogin: state.currentUser.login, allowedTokens: tokens, networkAddress: deployedNetworkAddress, - isDefault: isSetupPage + isDefault: isSetupPage, + signedMessage }; const networkCreated = await createNetwork(payload) @@ -163,6 +180,11 @@ function NewNetwork() { await handleChangeNetworkParameter("proposerFeeShare", proposerFee, deployedNetworkAddress); } + await processEvent("network", "parameters", payload.name.toLowerCase(), { + chainId: state.connectedChain?.id + }) + .catch(error => console.debug("Failed to update network parameters", error)); + setCreatingNetwork(9); const registrationTx = await handleAddNetworkToRegistry(deployedNetworkAddress) @@ -175,7 +197,10 @@ function NewNetwork() { setCreatingNetwork(10); cleanStorage?.(); await processEvent("registry", "registered", payload.name.toLowerCase(), { fromBlock: registrationTx.blockNumber }) - .then(() => router.push(getURLWithNetwork("/", { network: payload.name.toLowerCase().replaceAll(" ", "-") }))) + .then(() => router.push(getURLWithNetwork("/", { + network: payload.name, + chain: state.connectedChain?.shortName + }))) .catch((error) => { checkHasNetwork(); dispatch(addToast({ @@ -191,26 +216,28 @@ function NewNetwork() { }); } - function goToMyNetworkPage() { - router.push(getURLWithNetwork("/profile/my-network", { network: defaultNetworkName })); - } - function checkHasNetwork() { dispatch(changeLoadState(true)); state.Service?.active.getNetworkAdressByCreator(state.currentUser.walletAddress) - .then(networkAddress => setHasNetwork(networkAddress !== Defaults.nativeZeroAddress)) - .catch(console.log) + .then(networkAddress => setHasNetwork(!isZeroAddress(networkAddress))) + .catch(console.debug) .finally(() => dispatch(changeLoadState(false))); } - - useEffect(() => { - if (!state.Service?.active || !state.currentUser?.walletAddress) return; + const walletAddress = state.currentUser?.walletAddress; + const connectedChain = state.connectedChain; + + if (!state.Service?.active || !walletAddress || !connectedChain) return; + + if (walletAddress && connectedChain.name === UNSUPPORTED_CHAIN) { + dispatch(changeNeedsToChangeChain(true)); + return; + } checkHasNetwork(); - }, [state.Service?.active, state.currentUser]); + }, [state.Service?.active, state.currentUser, state.connectedChain]); return (
@@ -220,9 +247,14 @@ function NewNetwork() { { (creatingNetwork > -1 && ) } - -
- + + +
+ +
+
+ + @@ -231,15 +263,14 @@ function NewNetwork() { - -
- + {/**/}
); } diff --git a/components/custom-network/repositories-list.tsx b/components/custom-network/repositories-list.tsx index c6f2efa32b..45275610d2 100644 --- a/components/custom-network/repositories-list.tsx +++ b/components/custom-network/repositories-list.tsx @@ -5,6 +5,10 @@ import {useTranslation} from "next-i18next"; import {ContextualSpan} from "components/contextual-span"; import RepositoryCheck from "components/custom-network/repository-check"; +import { useAppState } from "contexts/app-state"; + +import { Repository } from "interfaces/issue-data"; + import useApi from "x-hooks/use-api"; interface infoType { @@ -13,15 +17,24 @@ interface infoType { text: string } -export default function RepositoriesList({ withLabel = true, repositories, botUser = undefined, onClick }) { +export default function RepositoriesList({ + withLabel = true, + repositories, + botUser = undefined, + onClick, + networkName = undefined, + networkCreator = undefined +}) { const { t } = useTranslation("custom-network"); const [existingRepos, setExistingRepos] = useState([]); const [reposWithIssues, setReposWithIssues] = useState([]); + const [reposWithNetwork, setReposWithNetwork] = useState(); const [reposUserNotAdmin, setReposUserNotAdmin] = useState([]); const [withoutMergeCommitPerm, setWithoutMergeCommitPerm] = useState([]); const [withoutBotCollaborator, setWithoutBotCollaborator] = useState([]); + const { state } = useAppState(); const { searchRepositories } = useApi(); const renderInfos: infoType[] = [ @@ -62,8 +75,13 @@ export default function RepositoriesList({ withLabel = true, repositories, botUs text: t("steps.repositories.no-repositories-selected"), type: "danger" }, - ] + ]; + + const toLower = (str: string) => str?.toLowerCase(); + const activeNetworkName = toLower(networkName || state.Service?.network?.active?.name); + const activeNetworCreator = toLower(networkCreator || state.Service?.network?.active?.creatorAddress); + const currentWallet = toLower(state.currentUser?.walletAddress); function updateReposWithoutMergeCommitPerm() { setWithoutMergeCommitPerm(repositories.filter(repository => repository.checked && !repository.mergeCommitAllowed) @@ -102,9 +120,7 @@ export default function RepositoriesList({ withLabel = true, repositories, botUs path: paths, networkName: "" }) - .then(({ rows }) => { - setExistingRepos(rows.map((repo) => repo.githubPath)); - }) + .then(({ rows }) => setReposWithNetwork(rows)) .catch(console.log); setReposWithIssues(repositories.filter(repository => repository.hasIssues) @@ -120,6 +136,17 @@ export default function RepositoriesList({ withLabel = true, repositories, botUs }, [repositories]); useEffect(updateReposWithoutBotCollaborator, [botUser]); + useEffect(() => { + if (!reposWithNetwork) return; + + const isUsedByOtherNetwork = ({ network: { name, creatorAddress }} : Repository) => + (toLower(name) !== activeNetworkName) || + (toLower(name) === activeNetworkName && + creatorAddress !== activeNetworCreator && + creatorAddress !== currentWallet); + + setExistingRepos(reposWithNetwork.filter(isUsedByOtherNetwork).map((repo) => repo.githubPath)); + }, [reposWithNetwork, activeNetworkName, activeNetworCreator]); function renderInfo({ text, @@ -141,21 +168,19 @@ export default function RepositoriesList({ withLabel = true, repositories, botUs } {repositories.map((repository, index) => ( - <> - - r.name === repository.name).length > 1 - ? repository.fullName - : repository.name} - active={repository.checked} - userPermission={repository.userPermission} - hasIssues={reposWithIssues.includes(repository.fullName)} - mergeCommitAllowed={repository.mergeCommitAllowed} - onClick={() => handleClick(repository)} - usedByOtherNetwork={!repository.isSaved && existingRepos.includes(repository.fullName)} - /> - + + r.name === repository.name).length > 1 + ? repository.fullName + : repository.name} + active={repository.checked} + userPermission={repository.userPermission} + hasIssues={reposWithIssues.includes(repository.fullName)} + mergeCommitAllowed={repository.mergeCommitAllowed} + onClick={() => handleClick(repository)} + usedByOtherNetwork={!repository.isSaved && existingRepos.includes(repository.fullName)} + /> ))} {renderInfos.filter(({ visible }) => visible).map(renderInfo)} diff --git a/components/custom-network/select-repositories-step.tsx b/components/custom-network/select-repositories-step.tsx index ed6b19a3d8..e686afe5b1 100644 --- a/components/custom-network/select-repositories-step.tsx +++ b/components/custom-network/select-repositories-step.tsx @@ -6,17 +6,16 @@ import ConnectGithub from "components/connect-github"; import RepositoriesList from "components/custom-network/repositories-list"; import Step from "components/step"; +import {useAppState} from "contexts/app-state"; import {useNetworkSettings} from "contexts/network-settings"; import {StepWrapperProps} from "interfaces/stepper"; -import {useAppState} from "../../contexts/app-state"; - export default function SelectRepositoriesStep({ activeStep, index, validated, handleClick } : StepWrapperProps) { const { t } = useTranslation("custom-network"); const {state} = useAppState(); - const { github, fields } = useNetworkSettings(); + const { details, github, fields } = useNetworkSettings(); function handleRepositoryCheck(fullName: string) { fields.repository.setter(fullName); @@ -36,7 +35,12 @@ export default function SelectRepositoriesStep({ activeStep, index, validated, h > {(state.currentUser?.login && (
- + {state.Settings?.github?.botUser} diff --git a/components/custom-network/token-configuration.tsx b/components/custom-network/token-configuration.tsx index 267e98c8bd..ee88ea4a83 100644 --- a/components/custom-network/token-configuration.tsx +++ b/components/custom-network/token-configuration.tsx @@ -3,43 +3,39 @@ import {useEffect, useState} from "react"; import BigNumber from "bignumber.js"; import {useTranslation} from "next-i18next"; +import {NetworkTokenConfig} from "components/custom-network/network-token-config"; import {Divider} from "components/divider"; import MultipleTokensDropdown from "components/multiple-tokens-dropdown"; import Step from "components/step"; +import {useAppState} from "contexts/app-state"; import {useNetworkSettings} from "contexts/network-settings"; -import {handleAllowedTokensDatabase} from "helpers/handleAllowedTokens"; - import {StepWrapperProps} from "interfaces/stepper"; import {Token} from "interfaces/token"; import useApi from "x-hooks/use-api"; -import {useAppState} from "../../contexts/app-state"; -import {NetworkTokenConfig} from "./network-token-config"; - export default function TokenConfiguration({ - activeStep, - index, - validated, - handleClick, - finishLabel, - handleFinish - }: StepWrapperProps) { + activeStep, + index, + validated, + handleClick, + finishLabel, + handleFinish +}: StepWrapperProps) { const {t} = useTranslation(["common", "custom-network"]); - - const {state} = useAppState(); - - const [allowedTransactionalTokens, setAllowedTransactionalTokens] = useState(); + + const [createNetworkAmount, setCreateNetworkAmount] = useState(); const [allowedRewardTokens, setAllowedRewardTokens] = useState([]); const [selectedRewardTokens, setSelectedRewardTokens] = useState([]); + const [allowedTransactionalTokens, setAllowedTransactionalTokens] = useState(); const [selectedTransactionalTokens, setSelectedTransactionalTokens] = useState(); - const [createNetworkAmount, setCreateNetworkAmount] = useState(); - - const {getTokens} = useApi(); - + + const { getTokens } = useApi(); + const { state } = useAppState(); const { tokens, fields, tokensLocked, registryToken } = useNetworkSettings(); + const networkTokenSymbol = state.Settings?.beproToken?.symbol || t("misc.$token"); function addTransactionalToken(newToken: Token) { @@ -83,19 +79,23 @@ export default function TokenConfiguration({ }, [selectedTransactionalTokens]) useEffect(() => { - if(!state.currentUser?.walletAddress || !state.Service?.active?.registryAddress) return; + if(!state.currentUser?.walletAddress || !state.connectedChain?.id) return; - state.Service?.active.getAllowedTokens() - .then((allowedTokens) => { - getTokens() - .then((tokens) => { - const { transactional, reward } = handleAllowedTokensDatabase(allowedTokens, tokens) - setAllowedTransactionalTokens(transactional); - setAllowedRewardTokens(reward); - }) - .catch((err) => console.log("error to get tokens database ->", err)); - }).catch((err) => console.log("error to get allowed tokens contract ->", err)); - }, [state.currentUser?.walletAddress, state.Service?.active?.registryAddress]); + getTokens(state.connectedChain?.id) + .then(tokens => { + const { transactional, reward } = tokens.reduce((acc, curr) => ({ + transactional: curr.isTransactional ? [...acc.transactional, curr]: acc.transactional, + reward: curr.isReward ? [...acc.reward, curr]: acc.reward, + }), { + transactional: [], + reward: [] + }); + + setAllowedTransactionalTokens(transactional); + setAllowedRewardTokens(reward); + }) + .catch(err => console.log("error to get tokens database ->", err)); + }, [state.currentUser?.walletAddress, state.connectedChain?.id]); useEffect(() => { if(!state?.currentUser?.walletAddress || !state?.Service?.active || !BigNumber(tokensLocked.needed).gt(0)) return diff --git a/components/date-label.tsx b/components/date-label.tsx index c8adcc6ee2..94f8d6e811 100644 --- a/components/date-label.tsx +++ b/components/date-label.tsx @@ -1,20 +1,16 @@ -import { intervalToDuration } from "date-fns"; +import { useEffect, useState } from "react"; + +import { Duration, intervalToDuration } from "date-fns"; import { useTranslation } from "next-i18next"; interface IDataLabelProps { - date: Date | number; + date: Date; className?: string; } export default function DateLabel({ date, className }: IDataLabelProps) { const { t } = useTranslation("common"); - const start = new Date(date); - const end = new Date(); - - const duration = intervalToDuration({ - start: start > end ? end : start, - end: end - }); + const [duration, setDuration] = useState(); const translated = (measure: string, count = 0) => `${count} ${t(`info-data.${measure}`, { count })}`; @@ -44,9 +40,21 @@ export default function DateLabel({ date, className }: IDataLabelProps) { return _string; } + useEffect(() => { + if (!date) return; + + const start = date; + const end = new Date(); + + setDuration(intervalToDuration({ + start: start > end ? end : start, + end: end + })); + }, [date]); + return ( - {date && + {duration && t("info-data.text-data", { value: handleDurationTranslation().join(" ") })} diff --git a/components/delegation-item.tsx b/components/delegation-item.tsx index 3f7b6d9b78..8f808aab5e 100644 --- a/components/delegation-item.tsx +++ b/components/delegation-item.tsx @@ -5,6 +5,7 @@ import {useTranslation} from "next-i18next"; import OracleIcon from "assets/icons/oracle-icon"; import Modal from "components/modal"; +import TokenBalance from "components/profile/token-balance"; import {useAppState} from "contexts/app-state"; @@ -13,13 +14,9 @@ import {truncateAddress} from "helpers/truncate-address"; import {DelegationExtended} from "interfaces/oracles-state"; - import {useAuthentication} from "x-hooks/use-authentication"; import useBepro from "x-hooks/use-bepro"; -import TokenBalance from "./profile/token-balance"; - - interface DelegationProps { type: "toMe" | "toOthers"; tokenName: string; @@ -46,7 +43,7 @@ export default function DelegationItem({ const tokenBalanceType = type === "toMe" ? "oracle" : "delegation"; const oracleToken = { - symbol: t("$oracles", {token: state.Service?.network?.networkToken?.symbol}), + symbol: t("$oracles", {token: state.Service?.network?.active?.networkToken?.symbol}), name: t("profile:oracle-name-placeholder"), icon: }; @@ -95,7 +92,7 @@ export default function DelegationItem({ {t("actions.take-back")} {formatStringToCurrency(delegationAmount)} {t("$oracles", { - token: state.Service?.network?.networkToken?.symbol + token: state.Service?.network?.active?.networkToken?.symbol })} diff --git a/components/delegations.tsx b/components/delegations.tsx index 005b358164..4ec368040d 100644 --- a/components/delegations.tsx +++ b/components/delegations.tsx @@ -27,7 +27,9 @@ export default function Delegations({ toMe: { title: t("profile:deletaged-to-me"), description: - t("my-oracles:descriptions.oracles-delegated-to-me", { token: state.Service?.network?.networkToken?.symbol }), + t("my-oracles:descriptions.oracles-delegated-to-me", { + token: state.Service?.network?.active?.networkToken?.symbol + }), total: undefined, delegations: [ state.currentUser?.balance?.oracles?.delegatedByOthers || 0 ] }, @@ -37,19 +39,19 @@ export default function Delegations({ delegation.amount.plus(acc), BigNumber(0)).toFixed()), description: t("my-oracles:descriptions.oracles-delegated-to-others", { - token: state.Service?.network?.networkToken?.symbol + token: state.Service?.network?.active?.networkToken?.symbol }), delegations: state.currentUser?.balance?.oracles?.delegations || [] } }; const oracleToken = { - symbol: t("$oracles", { token: state.Service?.network?.networkToken?.symbol }), + symbol: t("$oracles", { token: state.Service?.network?.active?.networkToken?.symbol }), name: t("profile:oracle-name-placeholder"), icon: }; - const networkTokenName = state.Service?.network?.networkToken?.name || oracleToken.name; + const networkTokenName = state.Service?.network?.active?.networkToken?.name || oracleToken.name; return (
diff --git a/components/form-group.tsx b/components/form-group.tsx index 6749b1817a..308e25d732 100644 --- a/components/form-group.tsx +++ b/components/form-group.tsx @@ -1,10 +1,10 @@ -import { ReactNode } from "react"; -import { Col, Form, OverlayTrigger, Popover } from "react-bootstrap"; +import {ReactNode} from "react"; +import {Col, Form, OverlayTrigger, Popover} from "react-bootstrap"; import InfoIcon from "assets/icons/info-icon"; import InputNumber from "components/input-number"; -import { WarningSpan } from "components/warning-span"; +import {WarningSpan} from "components/warning-span"; interface FormGroupProps { label: string; @@ -18,9 +18,19 @@ interface FormGroupProps { variant?: "input" | "numberFormat"; onBlur?: () => void; onChange?: (newValue: string) => void; + decimalScale?: number; } -export function FormGroup({ label, onChange, error, hint, variant = "input", symbol, ...rest } : FormGroupProps) { +export function FormGroup({ + label, + onChange, + error, + hint, + variant = "input", + symbol, + decimalScale, + ...rest + }: FormGroupProps) { const isNumberFormat = variant === "numberFormat"; function handleChange(e) { @@ -73,7 +83,7 @@ export function FormGroup({ label, onChange, error, hint, variant = "input", sym void; +} + +export default function GithubConnectionState({handleClickDisconnect}: GithubConnectionStateProps) { + const {state} = useAppState(); + const { t } = useTranslation("profile"); + const { connectWallet, connectGithub, } = useAuthentication(); + + return <> +
+
+ + + { handleClickDisconnect && state?.currentUser?.login && state.currentUser?.walletAddress && + + } +
+ +
+ +
+
+ +} \ No newline at end of file diff --git a/components/github-link.tsx b/components/github-link.tsx index 77ebf8e5d1..a4a8b1898b 100644 --- a/components/github-link.tsx +++ b/components/github-link.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from "react"; +import React, { MouseEvent, ReactNode } from "react"; import ExternalLinkIcon from "assets/icons/external-link-icon"; @@ -7,7 +7,7 @@ interface GithubLinkParams { forcePath?: string; hrefPath: string; children: ReactNode; - onClick?: (e?:any) => void; + onClick?: (e: MouseEvent) => void; color?: string; } diff --git a/components/icon-option.tsx b/components/icon-option.tsx index 0e3663b0b8..a53f0a2b50 100644 --- a/components/icon-option.tsx +++ b/components/icon-option.tsx @@ -1,39 +1,50 @@ +import { OverlayTrigger, Tooltip } from "react-bootstrap"; import { components } from "react-select"; export default function IconOption(props) { const { Option } = components; + const popover = props.data.tooltip ? ( + + {props.data.tooltip} + + ) : <>; + return( ); } \ No newline at end of file diff --git a/components/icon-single-value.tsx b/components/icon-single-value.tsx index e2411c20a6..4e14fda9a0 100644 --- a/components/icon-single-value.tsx +++ b/components/icon-single-value.tsx @@ -5,7 +5,7 @@ export default function IconSingleValue(props) { return( - + { props.data.preIcon && diff --git a/components/indicator.tsx b/components/indicator.tsx index 6a26f341dd..16598b1fea 100644 --- a/components/indicator.tsx +++ b/components/indicator.tsx @@ -1,4 +1,4 @@ -export default function Indicator({ bg = "" }) { +export default function Indicator({ bg = "gray" }) { return ( <> { - observer.unobserve(childs[childs.length - 1]); + if (childs.length) + observer.unobserve(childs[childs.length - 1]); }; }, [hasMore, isLoading]); diff --git a/components/input-with-balance.tsx b/components/input-with-balance.tsx index 90ffd9f16d..9ef57eb69b 100644 --- a/components/input-with-balance.tsx +++ b/components/input-with-balance.tsx @@ -15,6 +15,7 @@ interface InputWithBalanceProps { max?: BigNumber; decimals?: number; onChange: (value: BigNumber) => void; + disabled?: boolean; } export default function InputWithBalance({ @@ -23,7 +24,8 @@ export default function InputWithBalance({ symbol, balance, max, - decimals = 18 + decimals = 18, + disabled } : InputWithBalanceProps) { const { t } = useTranslation("common"); @@ -47,6 +49,7 @@ export default function InputWithBalance({ decimalScale={decimals} onValueChange={handleAmountChange} isAllowed={isAllowed} + disabled={disabled} />
diff --git a/components/internal-link.tsx b/components/internal-link.tsx index f2f9bb3a37..a11a29258d 100644 --- a/components/internal-link.tsx +++ b/components/internal-link.tsx @@ -18,6 +18,7 @@ interface InternalLinkProps { brand?: boolean; style?: CSSProperties; outline?: boolean; + title?: string; } export default function InternalLink({ @@ -65,6 +66,7 @@ export default function InternalLink({ className={getClasses()} target={`${blank ? "_blank" : ""}`} style={{ ...style }} + title={props?.title} > {(iconBefore && props.icon) || ""} {(props.label && {props.label}) || ""} diff --git a/components/invalid-account-wallet-modal.tsx b/components/invalid-account-wallet-modal.tsx index 6893b60af6..8e7986ba22 100644 --- a/components/invalid-account-wallet-modal.tsx +++ b/components/invalid-account-wallet-modal.tsx @@ -1,7 +1,7 @@ -import { useSession } from "next-auth/react"; -import { useTranslation } from "next-i18next"; +import {useSession} from "next-auth/react"; +import {useTranslation} from "next-i18next"; import Image from "next/image"; -import { useRouter } from "next/router"; +import {useRouter} from "next/router"; import ErrorMarkIcon from "assets/icons/errormark-icon"; import metamaskLogo from "assets/metamask.png"; @@ -9,11 +9,11 @@ import metamaskLogo from "assets/metamask.png"; import Avatar from "components/avatar"; import Modal from "components/modal"; -import { useAppState } from "contexts/app-state"; +import {useAppState} from "contexts/app-state"; -import { truncateAddress } from "helpers/truncate-address"; +import {truncateAddress} from "helpers/truncate-address"; -import { CustomSession } from "interfaces/custom-session"; +import {CustomSession} from "interfaces/custom-session"; export default function InvalidAccountWalletModal() { const { t } = useTranslation("common"); @@ -70,7 +70,7 @@ export default function InvalidAccountWalletModal() {
{" "} - {currentUser?.walletAddress && truncateAddress(currentUser?.walletAddress)} + {currentUser?.walletAddress && truncateAddress(currentUser?.walletAddress || '')}
diff --git a/components/issue-amount-info.tsx b/components/issue-amount-info.tsx index d9051a5f9c..1b08e3ce25 100644 --- a/components/issue-amount-info.tsx +++ b/components/issue-amount-info.tsx @@ -30,7 +30,7 @@ export default function IssueAmountInfo({ issue, size = "lg" }: { issue: IssueBi overlay={ {formatStringToCurrency(bountyAmount?.toFixed())}{" "} - {issue?.token?.symbol || t("common:misc.token")} + {issue?.transactionalToken?.symbol || t("common:misc.token")} } > @@ -59,7 +59,7 @@ export default function IssueAmountInfo({ issue, size = "lg" }: { issue: IssueBi }`} style={{ color: issue?.network?.colors?.primary }} > - {issue?.token?.symbol || t("common:misc.token")} + {issue?.transactionalToken?.symbol || t("common:misc.token")}
diff --git a/components/issue-list-item.tsx b/components/issue-list-item.tsx index f10d373742..ee522c881d 100644 --- a/components/issue-list-item.tsx +++ b/components/issue-list-item.tsx @@ -6,33 +6,38 @@ import {useTranslation} from "next-i18next"; import {useRouter} from "next/router"; import AvatarOrIdenticon from "components/avatar-or-identicon"; +import Badge from "components/badge"; import BountyStatusInfo from "components/bounty-status-info"; import BountyTags from "components/bounty/bounty-tags"; +import CardItem from "components/card-item"; +import ChainBadge from "components/chain-badge"; import DateLabel from "components/date-label"; +import IssueAmountInfo from "components/issue-amount-info"; import Translation from "components/translation"; +import {useAppState} from "contexts/app-state"; + import {getIssueState} from "helpers/handleTypeIssue"; import {IssueBigNumberData, IssueState} from "interfaces/issue-data"; -import {useAppState} from "../contexts/app-state"; -import Badge from "./badge"; -import CardItem from "./card-item"; -import IssueAmountInfo from "./issue-amount-info"; - -export default function IssueListItem({ - size = "lg", - issue = null, - xClick, - }: { +import { useNetwork } from "x-hooks/use-network"; +interface IssueListItemProps { issue?: IssueBigNumberData; xClick?: () => void; size?: "sm" | "lg" -}) { +} + +export default function IssueListItem({ + size = "lg", + issue = null, + xClick, +}: IssueListItemProps) { const router = useRouter(); const { t } = useTranslation(["bounty", "common"]); const {state} = useAppState(); + const { getURLWithNetwork } = useNetwork(); const issueState = getIssueState({ state: issue?.state, @@ -42,16 +47,12 @@ export default function IssueListItem({ function handleClickCard() { if (xClick) return xClick(); - router.push({ - pathname: "/[network]/bounty", - query: { - id: issue?.githubId, - repoId: issue?.repository_id, - network: issue?.network?.name - ? issue?.network?.name - : state.Service?.network?.lastVisited, - } - }); + router.push(getURLWithNetwork("/bounty", { + id: issue?.githubId, + repoId: issue?.repository_id, + network: issue?.network?.name, + chain: issue?.network?.chain?.chainShortName + })); } function IssueTag() { @@ -104,19 +105,23 @@ export default function IssueListItem({ return ( <> -
-
- {issue?.network?.logoIcon && ( - - )} - - {issue?.network?.name} - +
+
+
+ {issue?.network?.logoIcon && ( + + )} + + {issue?.network?.name} + +
+ +
@@ -147,7 +152,8 @@ export default function IssueListItem({ <> {issue?.isKyc ? : null}
diff --git a/components/issue-proposal-progress-bar.tsx b/components/issue-proposal-progress-bar.tsx index eda01069a6..f7d0045892 100644 --- a/components/issue-proposal-progress-bar.tsx +++ b/components/issue-proposal-progress-bar.tsx @@ -3,10 +3,9 @@ import {Fragment, useEffect, useState} from "react"; import {add, addSeconds, compareAsc, intervalToDuration} from "date-fns"; import {useTranslation} from "next-i18next"; -import {formatDate, getTimeDifferenceInWords} from "helpers/formatDate"; - -import {useAppState} from "../contexts/app-state"; +import {useAppState} from "contexts/app-state"; +import {formatDate, getTimeDifferenceInWords} from "helpers/formatDate"; export default function IssueProposalProgressBar() { const {t} = useTranslation(["common", "bounty"]); @@ -23,21 +22,20 @@ export default function IssueProposalProgressBar() { ]); const {state} = useAppState(); - const getChainTime = () => state.Service.active.getTimeChain().then(setChainTime).catch(console.log) - const isFinalized = !!(state.currentBounty?.data?.state === "closed") - const isInValidation = !!(state.currentBounty?.data?.state === "proposal") - const isIssueinDraft = !!(state.currentBounty?.data?.state === "draft") - const isFundingRequest = state.currentBounty?.data?.fundingAmount?.gt(0); - const isBountyFunded = state.currentBounty?.data?.fundedAmount?.isEqualTo(state.currentBounty?.data?.fundingAmount); - const creationDate = state.currentBounty?.chainData?.creationDate; + + const getChainTime = () => state.Service.active.getTimeChain().then(setChainTime).catch(console.log); + + const { isClosed, isCanceled, isDraft, isFundingRequest, isFunded } = state.currentBounty?.data || {}; + + const isInValidation = !!(state.currentBounty?.data?.state === "proposal"); + const creationDate = state.currentBounty?.data?.createdAt; const fundedDate = state.currentBounty?.data?.fundedAt; - const closedDate = state.currentBounty?.chainData?.closedDate; - const isCanceled = !!(state.currentBounty?.data?.state === "canceled") - const lastProposalCreationDate = state.currentBounty?.data?.mergeProposals?. + const closedDate = isClosed ? state.currentBounty?.data?.updatedAt : undefined; + const lastProposalCreationDate = + state.currentBounty?.data?.mergeProposals?. filter(proposal => !proposal.refusedByBountyOwner && !proposal.isDisputed) - .reduce((proposalAnt, proposalCur) => - proposalAnt.creationDate > proposalCur.creationDate && - proposalAnt || proposalCur, { creationDate })?.creationDate; + .reduce((acc, curr) => +curr.contractCreationDate > +acc ? new Date(curr.contractCreationDate) : acc, + creationDate); function toRepresentationHeight() { return currentStep === 0 ? "1px" : `${currentStep * 66.7}px`; @@ -80,14 +78,14 @@ export default function IssueProposalProgressBar() { if (creationDate && index === currentStep && currentStep === 1 && !isFundingRequest) currentValue = item(addSeconds(creationDate, +state.Service?.network?.times?.draftTime)).Started; - if (creationDate && index === currentStep && currentStep === 0 && !isCanceled && !isFinalized) + if (creationDate && index === currentStep && currentStep === 0 && !isCanceled && !isClosed) currentValue = item(creationDate, +state.Service?.network?.times?.draftTime).Warning; if ( index === currentStep && currentStep === 2 && !isCanceled && - !isFinalized + !isClosed ) { if (isFundingRequest && creationDate && fundedDate) { const intervalFunded = intervalToDuration({ @@ -194,16 +192,16 @@ export default function IssueProposalProgressBar() { let step = 0 + addIsFunding; let stepColor = "primary" - if (isFinalized) step = 3 + addIsFunding; + if (isClosed) step = 3 + addIsFunding; else if (isInValidation) step = 2 + addIsFunding; - else if (!isIssueinDraft && (!isFundingRequest || isBountyFunded)) step = 1 + addIsFunding; + else if (!isDraft && (!isFundingRequest || isFunded)) step = 1 + addIsFunding; if (isCanceled) stepColor = "danger"; - if (isFinalized) stepColor = "success"; + if (isClosed) stepColor = "success"; setStepColor(stepColor); setCurrentStep(step); - }, [isFinalized, isIssueinDraft, isCanceled, isInValidation, isFundingRequest, isBountyFunded]); + }, [isClosed, isDraft, isCanceled, isInValidation, isFundingRequest, isFunded]); return (
diff --git a/components/leaderboard/leaderboard-list.tsx b/components/leaderboard/leaderboard-list.tsx index 02543aaf7c..b44ea0a30a 100644 --- a/components/leaderboard/leaderboard-list.tsx +++ b/components/leaderboard/leaderboard-list.tsx @@ -12,6 +12,8 @@ import Button from "components/button"; import CustomContainer from "components/custom-container"; import InfiniteScroll from "components/infinite-scroll"; import IssueFilters from "components/issue-filters"; +import LeaderBoardListBar from "components/leaderboard/leaderboard-list-bar"; +import LeaderBoardListItem from "components/leaderboard/leaderboard-list-item"; import ListSort from "components/list-sort"; import NothingFound from "components/nothing-found"; import ScrollTopButton from "components/scroll-top-button"; @@ -25,9 +27,6 @@ import useApi from "x-hooks/use-api"; import usePage from "x-hooks/use-page"; import useSearch from "x-hooks/use-search"; -import LeaderBoardListBar from "./leaderboard-list-bar"; -import LeaderBoardListItem from "./leaderboard-list-item"; - interface LeaderBoardPage { page: number; leadBoard: LeaderBoard[]; @@ -135,7 +134,7 @@ export default function LeaderBoardList() { time, sortBy, order - ]) + ]); useEffect(() => { clearTimeout(searchTimeout.current); @@ -254,7 +253,7 @@ export default function LeaderBoardList() { hasMore={hasMore}> {leaderBoardPages.map(({ leadBoard }) => { return leadBoard?.map((item) => ( - + )); })} diff --git a/components/list-issues.tsx b/components/list-issues.tsx index b3a0603af2..ccf3ca3a92 100644 --- a/components/list-issues.tsx +++ b/components/list-issues.tsx @@ -10,6 +10,7 @@ import CloseIcon from "assets/icons/close-icon"; import SearchIcon from "assets/icons/search-icon"; import Button from "components/button"; +import ContractButton from "components/contract-button"; import CustomContainer from "components/custom-container"; import InfiniteScroll from "components/infinite-scroll"; import IssueFilters from "components/issue-filters"; @@ -28,6 +29,7 @@ import {isProposalDisputable} from "helpers/proposal"; import {IssueBigNumberData, IssueState} from "interfaces/issue-data"; import useApi from "x-hooks/use-api"; +import useChain from "x-hooks/use-chain"; import usePage from "x-hooks/use-page"; import useSearch from "x-hooks/use-search"; @@ -86,12 +88,14 @@ export default function ListIssues({ const searchTimeout = useRef(null); + const { chain } = useChain(); const { searchIssues, getTotalBounties } = useApi(); const { page, nextPage, goToFirstPage } = usePage(); const isProfilePage = router?.asPath?.includes("profile"); - const { repoId, time, state, sortBy, order } = router.query as { + const { network: networkName, repoId, time, state, sortBy, order } = router.query as { + network: string; repoId: string; time: string; state: string; @@ -165,11 +169,15 @@ export default function ListIssues({ } function handlerSearch() { - if (!appState.Service?.network?.active?.name || inView === false) return; + if (router.pathname === "/[network]" && !networkName || + router.pathname.includes("/[network]/[chain]") && (!networkName || !chain || inView === false)) return; dispatch(changeLoadState(true)); - if(allNetworks) getTotalBounties().then(setTotalBounties) + if(allNetworks) + getTotalBounties() + .then(setTotalBounties) + .catch(error => console.debug("Failed to getTotalBounties", error)); searchIssues({ page, @@ -179,11 +187,12 @@ export default function ListIssues({ search, sortBy: sortBy || 'createdAt', order, - creator, + address: creator, pullRequesterLogin, pullRequesterAddress, proposer, - networkName: allNetworks ? "" : appState.Service?.network?.active?.name, + chainId: chain?.chainId?.toString(), + networkName: networkName?.toString(), allNetworks: allNetworks ? allNetworks : "" }) .then(async ({ rows, pages, currentPage }) => { @@ -208,7 +217,7 @@ export default function ListIssues({ setHasMore(currentPage < pages); }) .catch((error) => { - console.error("Error fetching issues", error); + console.debug("Error fetching issues", error); }) .finally(() => { dispatch(changeLoadState(false)); @@ -228,7 +237,7 @@ export default function ListIssues({ } useEffect(() => { - if (page) { + if (page && !!issuesPages.length) { const pagesToValidate = [...Array(+page).keys()].map((i) => i + 1); setTruncatedData(!pagesToValidate.every((pageV) => @@ -244,10 +253,12 @@ export default function ListIssues({ state, sortBy, order, + chain, creator, proposer, - appState.Service?.network?.active?.name, - inView + networkName, + inView, + appState.supportedChains ]); useEffect(() => { @@ -369,9 +380,9 @@ export default function ListIssues({ {(appState.currentUser?.walletAddress && !allNetworks) && ( - + )} diff --git a/components/loading.tsx b/components/loading.tsx index deb10dba93..cd5b9b0eda 100644 --- a/components/loading.tsx +++ b/components/loading.tsx @@ -3,7 +3,7 @@ import { Modal, Spinner } from "react-bootstrap"; import Translation from "components/translation"; -import {useAppState} from "../contexts/app-state"; +import {useAppState} from "contexts/app-state"; export default function Loading() { diff --git a/components/main-nav.tsx b/components/main-nav.tsx deleted file mode 100644 index cf51862dab..0000000000 --- a/components/main-nav.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import {ReactElement, ReactNode, useEffect, useState} from "react"; - -import {Defaults} from "@taikai/dappkit"; -import {useRouter} from "next/router"; - -import ExternalLinkIcon from "assets/icons/external-link-icon"; -import HelpIcon from "assets/icons/help-icon"; -import LogoPlaceholder from "assets/icons/logo-placeholder"; -import PlusIcon from "assets/icons/plus-icon"; - -import Button from "components/button"; -import ClosedNetworkAlert from "components/closed-network-alert"; -import ConnectWalletButton from "components/connect-wallet-button"; -import HelpModal from "components/help-modal"; -import InternalLink from "components/internal-link"; -import NavAvatar from "components/nav-avatar"; -import ReadOnlyButtonWrapper from "components/read-only-button-wrapper"; -import TransactionsStateIndicator from "components/transactions-state-indicator"; -import Translation from "components/translation"; -import WrongNetworkModal from "components/wrong-network-modal"; - -import {useAppState} from "contexts/app-state"; -import {changeShowCreateBounty, changeShowWeb3} from "contexts/reducers/update-show-prop"; - -import useApi from "x-hooks/use-api"; -import useNetworkTheme from "x-hooks/use-network-theme"; - -interface MyNetworkLink { - href: string; - label: string | ReactElement; - icon?: ReactNode; -} - -export default function MainNav() { - const { pathname } = useRouter(); - - const [showHelp, setShowHelp] = useState(false); - const {dispatch} = useAppState(); - const [myNetwork, setMyNetwork] = useState({ - label: , - href: "/new-network", - icon: - }); - - const {state} = useAppState(); - const { searchNetworks } = useApi(); - const { getURLWithNetwork } = useNetworkTheme(); - - const noNeedNetworkInstance = ["/","/networks", "/new-network", "/explore", "/leaderboard"].includes(pathname); - const fullLogoUrl = state.Service?.network?.active?.fullLogo; - - useEffect(() => { - if (!state.Service?.active || !state.currentUser?.walletAddress || !noNeedNetworkInstance) return; - - state.Service?.active.getNetworkAdressByCreator(state.currentUser.walletAddress) - .then(async networkAddress => { - if (networkAddress === Defaults.nativeZeroAddress) - return setMyNetwork({ - label: , - href: "/new-network", - icon: - }); - - const network = await searchNetworks({ networkAddress }).then(({ rows }) => rows[0]); - - setMyNetwork({ - label: , - href: `/${network?.name?.toLowerCase()}${network?.isRegistered ? "" : "/profile/my-network"}` - }); - }) - .catch(console.log); - }, [state.Service?.active, state.currentUser?.walletAddress, noNeedNetworkInstance]); - - function handleNewBounty () { - if(!window.ethereum) return dispatch(changeShowWeb3(true)) - return dispatch(changeShowCreateBounty(true)) - - } - - function LinkExplore() { - return ( - } - nav - uppercase - icon={!noNeedNetworkInstance ? :null} - /> - ); - } - - function LinkNetworks() { - return( - } - nav - uppercase - /> - ) - } - - function LinkLeaderBoard() { - return ( - } - nav - uppercase - /> - ) - } - - function LinkBounties() { - return ( - } - nav - uppercase - /> - ) - } - - const brandLogo = !noNeedNetworkInstance ? ( - fullLogoUrl ? ( - - ) : ( - - ) - ) : ( - - ); - - return ( -
- {(state.Service?.network?.active?.isClosed && !noNeedNetworkInstance) && } -
-
-
- {brandLogo} - {(!noNeedNetworkInstance && ( -
    -
  • - -
  • -
  • - } - nav - uppercase - /> -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
- )) || ( -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
- )} -
- -
- {(!noNeedNetworkInstance && ( - - - - )) || ( - - )} - - - - - - - <> - - - - - -
- setShowHelp(false)} /> -
-
-
- ); -} diff --git a/components/main-nav/brand-logo.tsx b/components/main-nav/brand-logo.tsx new file mode 100644 index 0000000000..6b18441669 --- /dev/null +++ b/components/main-nav/brand-logo.tsx @@ -0,0 +1,38 @@ +import { UrlObject } from "url"; + +import LogoPlaceholder from "assets/icons/logo-placeholder"; + +import InternalLink from "components/internal-link"; + +interface BrandLogoProps { + href: string | UrlObject; + logoUrl: string; + showDefaultBepro?: boolean; +} + +export default function BrandLogo({ + href, + logoUrl, + showDefaultBepro +} : BrandLogoProps) { + const defaultLogo = `/images/Bepro_Logo_Light.svg`; + + const icon = showDefaultBepro || logoUrl ? + : + ; + + return( + + ); +} \ No newline at end of file diff --git a/components/main-nav/index.tsx b/components/main-nav/index.tsx new file mode 100644 index 0000000000..86e7c34031 --- /dev/null +++ b/components/main-nav/index.tsx @@ -0,0 +1,261 @@ +import {ReactElement, ReactNode, useEffect, useState} from "react"; + +import clsx from "clsx"; +import { useTranslation } from "next-i18next"; +import {useRouter} from "next/router"; + +import ExternalLinkIcon from "assets/icons/external-link-icon"; +import HelpIcon from "assets/icons/help-icon"; +import PlusIcon from "assets/icons/plus-icon"; + +import Button from "components/button"; +import ClosedNetworkAlert from "components/closed-network-alert"; +import ConnectWalletButton from "components/connect-wallet-button"; +import ContractButton from "components/contract-button"; +import HelpModal from "components/help-modal"; +import InternalLink from "components/internal-link"; +import BrandLogo from "components/main-nav/brand-logo"; +import NavLinks from "components/main-nav/nav-links"; +import NavAvatar from "components/nav-avatar"; +import ReadOnlyButtonWrapper from "components/read-only-button-wrapper"; +import SelectNetworkDropdown from "components/select-network-dropdown"; +import TransactionsStateIndicator from "components/transactions-state-indicator"; +import Translation from "components/translation"; + +import {useAppState} from "contexts/app-state"; +import { changeCurrentUserHasRegisteredNetwork } from "contexts/reducers/change-current-user"; +import {changeShowCreateBounty} from "contexts/reducers/update-show-prop"; + +import { SupportedChainData } from "interfaces/supported-chain-data"; + +import useApi from "x-hooks/use-api"; +import useChain from "x-hooks/use-chain"; +import { useDao } from "x-hooks/use-dao"; +import { useNetwork } from "x-hooks/use-network"; +import useNetworkChange from "x-hooks/use-network-change"; + +interface MyNetworkLink { + href: string; + label: string | ReactElement; + icon?: ReactNode; +} + +export default function MainNav() { + const { t } = useTranslation("common"); + const { pathname, query, asPath, push } = useRouter(); + + const newNetworkObj = { + label: , + href: "/new-network", + icon: + }; + + const [showHelp, setShowHelp] = useState(false); + const [myNetwork, setMyNetwork] = useState(newNetworkObj); + + const { connect } = useDao(); + const { chain } = useChain(); + const { state } = useAppState(); + const { dispatch } = useAppState(); + const { searchNetworks } = useApi(); + const { getURLWithNetwork } = useNetwork(); + const { handleAddNetwork } = useNetworkChange(); + + const noNeedNetworkInstance = [ + "/", + "/networks", + "/new-network", + "/explore", + "/leaderboard", + "/setup" + ].includes(pathname); + + const networkLogo = state.Service?.network?.active?.fullLogo; + const fullLogoUrl = networkLogo && `${state.Settings?.urls?.ipfs}/${networkLogo}`; + const brandHref = noNeedNetworkInstance ? "/" : getURLWithNetwork("/", { + network: state.Service?.network?.active?.name, + }); + + + function getChainShortName() { + const availableChains = state.Service?.network?.availableChains; + const isOnAvailableChain = availableChains?.find(({ chainId }) => +chainId === +state.connectedChain?.id); + + if (chain) return chain.chainShortName; + + if (isOnAvailableChain) { + return isOnAvailableChain.chainShortName; + } + + if (availableChains) return availableChains[0].chainShortName; + + return null; + } + + const links = [ + { + href: getURLWithNetwork("/", { + chain: getChainShortName() + }), + label: t("main-nav.nav-avatar.bounties"), + isVisible: !noNeedNetworkInstance + }, + { + href: getURLWithNetwork("/curators"), + label: t("main-nav.council"), + isVisible: !noNeedNetworkInstance && pathname !== "/[network]" + }, + { + href: "/networks", + label: t("main-nav.networks"), + isVisible: true + }, + { + href: "/leaderboard", + label: t("main-nav.leaderboard"), + isVisible: true + }, + { + href: "/explore", + label: t("main-nav.explore"), + isVisible: true, + blank: !noNeedNetworkInstance, + icon: !noNeedNetworkInstance ? : null + } + ]; + + useEffect(() => { + if (!state.currentUser?.walletAddress || !state.connectedChain?.id) + return; + + searchNetworks({ + creatorAddress: state.currentUser?.walletAddress, + chainId: state.connectedChain?.id, + isClosed: false + }) + .then(({ count, rows }) => { + const changeIfDifferent = (has: boolean) => state.currentUser?.hasRegisteredNetwork !== has && + dispatch(changeCurrentUserHasRegisteredNetwork(has)); + + if (count === 0) { + setMyNetwork(newNetworkObj); + changeIfDifferent(false); + } else { + const networkName = rows[0]?.name?.toLowerCase(); + const chainShortName = rows[0]?.chain?.chainShortName?.toLowerCase(); + + changeIfDifferent(!!rows[0]?.isRegistered); + + setMyNetwork({ + label: , + href: `/${networkName}/${chainShortName}${rows[0]?.isRegistered ? "" : "/profile/my-network"}` + }); + } + }) + .catch(error => console.debug("Failed to get network address by wallet", error)); + }, [state.currentUser?.walletAddress, state.connectedChain]); + + function handleNewBounty () { + dispatch(changeShowCreateBounty(true)); + } + + async function handleNetworkSelected(chain: SupportedChainData) { + if (noNeedNetworkInstance) { + handleAddNetwork(chain) + .then(() => { + if (state.currentUser?.walletAddress) return; + + connect(); + }) + .catch(() => null); + + return; + } + + const needsRedirect = ["bounty", "pull-request", "proposal"].includes(pathname.replace("/[network]/[chain]/", "")); + const newPath = needsRedirect ? "/" : pathname; + const newAsPath = needsRedirect ? `/${query.network}/${chain.chainShortName}` : + asPath.replace(query.chain.toString(), chain.chainShortName); + + push(getURLWithNetwork(newPath, { + ... needsRedirect ? {} : query, + chain: chain.chainShortName + }), newAsPath); + } + + return ( +
+ + +
+
+
+ + + handleNetworkSelected(chain)} + isOnNetwork={!noNeedNetworkInstance} + className="select-network-dropdown" + /> + + +
+ +
+ { noNeedNetworkInstance ? + : + + + + {t("main-nav.new-bounty")} + + + } + + + + + + + + +
+ + setShowHelp(false)} /> +
+
+
+ ); +} diff --git a/components/main-nav/nav-links.tsx b/components/main-nav/nav-links.tsx new file mode 100644 index 0000000000..7d198dd0f1 --- /dev/null +++ b/components/main-nav/nav-links.tsx @@ -0,0 +1,37 @@ +import { ReactNode } from "react"; + +import { UrlObject } from "url"; + +import InternalLink from "components/internal-link"; + +interface Link { + href: string | UrlObject; + label: string; + isVisible: boolean; + icon?: ReactNode; + blank?: boolean; +} + +interface NavLinksProps { + links: Link[]; +} + +export default function NavLinks({ links } : NavLinksProps) { + const isVisible = ({ isVisible }) => isVisible; + + return( +
    + {links.filter(isVisible).map(({ href, label, icon, blank }) => +
  • + +
  • )} +
+ ); +} \ No newline at end of file diff --git a/components/modals/kyc-session.tsx b/components/modals/kyc-session.tsx index 290ebfc214..d1b67b09db 100644 --- a/components/modals/kyc-session.tsx +++ b/components/modals/kyc-session.tsx @@ -3,6 +3,7 @@ import {Button, Col, Row} from 'react-bootstrap'; import Synaps from '@synaps-io/react-verify' +import {ContextualSpan} from "components/contextual-span"; import Modal from 'components/modal'; import ReadOnlyButtonWrapper from 'components/read-only-button-wrapper'; import Translation from "components/translation"; @@ -11,7 +12,6 @@ import {useAppState} from 'contexts/app-state'; import {changeCurrentUserKycSession} from 'contexts/reducers/change-current-user'; import useApi from 'x-hooks/use-api'; -import {ContextualSpan} from "../contextual-span"; export function KycSessionModal() { const [show, setShow] = useState(false); @@ -21,8 +21,8 @@ export function KycSessionModal() { const {validateKycSession} = useApi() const {state, dispatch} = useAppState() - const session = state?.currentUser?.kycSession - const bountyMissSteps = state?.currentBounty?.kycSteps + const session = state?.currentUser?.kycSession; + const isVerified = session?.status === "VERIFIED"; function handlerValidateSession() { if (session?.session_id) @@ -32,7 +32,6 @@ export function KycSessionModal() { }) } - function updateSessionStateTimer() { let _timer; @@ -52,8 +51,9 @@ export function KycSessionModal() { return <>
- {state?.currentUser?.kycSession?.status} + + {state?.currentUser?.kycSession?.status} +
diff --git a/components/nav-avatar.tsx b/components/nav-avatar.tsx index 838cda530e..8e53e784c6 100644 --- a/components/nav-avatar.tsx +++ b/components/nav-avatar.tsx @@ -7,10 +7,8 @@ import {useRouter} from "next/router"; import CloseIcon from "assets/icons/close-icon"; import ExternalLinkIcon from "assets/icons/external-link-icon"; -import Avatar from "components/avatar"; import AvatarOrIdenticon from "components/avatar-or-identicon"; import Button from "components/button"; -import Identicon from "components/identicon"; import {useAppState} from "contexts/app-state"; @@ -19,7 +17,13 @@ import {truncateAddress} from "helpers/truncate-address"; import {useAuthentication} from "x-hooks/use-authentication"; import {useNetwork} from "x-hooks/use-network"; -export default function NavAvatar() { +interface NavAvatarProps { + onNetwork?: boolean; +} + +export default function NavAvatar({ + onNetwork +} : NavAvatarProps) { const { t } = useTranslation("common"); const router = useRouter(); @@ -29,10 +33,6 @@ export default function NavAvatar() { const { getURLWithNetwork } = useNetwork(); const { disconnectWallet } = useAuthentication(); - - const avatar = () => state.currentUser?.login && - || - ; const username = state.currentUser?.login ? state.currentUser.login : truncateAddress(state.currentUser?.walletAddress); @@ -93,21 +93,14 @@ export default function NavAvatar() { const internalLinks = [ Link(t("main-nav.nav-avatar.wallet"), getURLWithNetwork("/profile/wallet")), - Link(t("main-nav.nav-avatar.oracles"), - getURLWithNetwork("/profile/bepro-votes")), - Link(t("main-nav.nav-avatar.payments"), - getURLWithNetwork("/profile/payments")), - Link(t("main-nav.nav-avatar.bounties"), - getURLWithNetwork("/profile/bounties")), - Link(t("main-nav.nav-avatar.pull-requests"), - getURLWithNetwork("/profile/pull-requests")), - Link(t("main-nav.nav-avatar.proposals"), - getURLWithNetwork("/profile/proposals")), - Link(t("main-nav.nav-avatar.my-network"), - getURLWithNetwork("/profile/my-network")), + Link(t("main-nav.nav-avatar.oracles"), getURLWithNetwork("/profile/bepro-votes")), + Link(t("main-nav.nav-avatar.payments"), getURLWithNetwork("/profile/payments")), + Link(t("main-nav.nav-avatar.bounties"), getURLWithNetwork("/profile/bounties")), + Link(t("main-nav.nav-avatar.pull-requests"), getURLWithNetwork("/profile/pull-requests")), + Link(t("main-nav.nav-avatar.proposals"), getURLWithNetwork("/profile/proposals")), + Link(t("main-nav.nav-avatar.my-network"), getURLWithNetwork("/profile/my-network")) ]; - const externalLinks = [ Link(t("main-nav.nav-avatar.support-center"), "https://support.bepro.network/en/"), Link(t("main-nav.nav-avatar.guides"), "https://docs.bepro.network/"), @@ -120,7 +113,11 @@ export default function NavAvatar() {
- {avatar()} +
@@ -130,19 +127,23 @@ export default function NavAvatar() {
-
- -
+ { onNetwork && +
+ +
+ }
- - {internalLinks.map(ProfileInternalLink)} - + { onNetwork && + + {internalLinks.map(ProfileInternalLink)} + + } {externalLinks.map(ProfileExternalLink)} @@ -175,11 +176,9 @@ export default function NavAvatar() { >
diff --git a/components/network-identifier.tsx b/components/network-identifier.tsx index 9658a80944..68bee5099f 100644 --- a/components/network-identifier.tsx +++ b/components/network-identifier.tsx @@ -1,49 +1,65 @@ import {useEffect} from "react"; +import { useTranslation } from "next-i18next"; + import Indicator from "components/indicator"; -import {NetworkColors} from "interfaces/enums/network-colors"; +import {useAppState} from "contexts/app-state"; +import {changeChain} from "contexts/reducers/change-chain"; -import {useAppState} from "../contexts/app-state"; -import {changeChain} from "../contexts/reducers/change-chain"; +import { UNSUPPORTED_CHAIN } from "helpers/constants"; + +import {NetworkColors} from "interfaces/enums/network-colors"; export default function NetworkIdentifier() { + const { t } = useTranslation("common"); + const {state, dispatch} = useAppState(); + function findChain(windowChainId: number) { + return state.supportedChains?.find(({chainId}) => chainId === windowChainId) + } + + function dispatchChainUpdate(chainId: number) { + const chain = findChain(chainId); + + sessionStorage.setItem("currentChainId", chainId.toString()); + + return dispatch(changeChain.update({ + id: (chain?.chainId || chainId)?.toString(), + name: chain?.chainName || UNSUPPORTED_CHAIN, + shortName: chain?.chainShortName?.toLowerCase() || UNSUPPORTED_CHAIN, + explorer: chain?.blockScanner, + events: chain?.eventsApi, + registry: chain?.registryAddress + })); + } useEffect(() => { - if (!window.ethereum || !state.Settings?.chainIds) + if (!window.ethereum || !state.supportedChains?.length) return; window.ethereum.removeAllListeners(`chainChanged`); - if (window.ethereum.isConnected()) { - dispatch(changeChain.update({ - id: (+window.ethereum.chainId)?.toString(), - name: state.Settings?.chainIds[(+window.ethereum.chainId)?.toString() || 'unknown'] - })) - } + if (window.ethereum.isConnected()) + dispatchChainUpdate(+window.ethereum.chainId); window.ethereum.on(`connected`, evt => { console.debug(`Metamask connected`, evt); }); window.ethereum.on(`chainChanged`, evt => { - console.debug(`chainChanged`, evt); - dispatch(changeChain.update({ - id: (+evt)?.toString(), - name: state.Settings?.chainIds[(+evt)?.toString() || 'unknown'] - })) + dispatchChainUpdate(+evt); }); - }, [state.Settings?.chainIds]); + }, [state.supportedChains]); return ( (state.connectedChain?.name && (
- + - {state.connectedChain?.name} + {state.connectedChain?.name === UNSUPPORTED_CHAIN ? t("misc.unsupported") : state.connectedChain?.name}
)) || <> diff --git a/components/network-list-bar-column.tsx b/components/network-list-bar-column.tsx index 0196fa8961..e8b12493b4 100644 --- a/components/network-list-bar-column.tsx +++ b/components/network-list-bar-column.tsx @@ -6,13 +6,15 @@ export default function NetworkListBarColumn({ isColumnActive, columnOrder = "asc", label, - onClick + onClick, + col = 3 }: { hideOrder: boolean; isColumnActive: boolean; columnOrder: string; label: string; - onClick: () => void + onClick: () => void; + col?: number; }) { const textClass = hideOrder ? "text-primary" @@ -26,7 +28,7 @@ export default function NetworkListBarColumn({ return (
void; + handleRedirect: (networkName: string, chainName: string) => void; } export default function NetworkListItem({ - network, - tokenSymbolDefault, + network, + tokenSymbolDefault, handleRedirect }: NetworkListItemProps) { - const {state: {Settings: settings}} = useAppState(); + const { state: { supportedChains, Settings: settings } } = useAppState(); const Spinner = () => ; const isNotUndefined = value => value !== undefined; function onClick() { - handleRedirect(network?.name); + const chainName = + supportedChains?. + find(({ chainId }) => +chainId === +network?.chain_id)?.chainShortName?. + toLowerCase(); + + handleRedirect(network?.name, chainName); } return ( @@ -41,6 +47,8 @@ export default function NetworkListItem({ {network?.name} + + {(network?.isClosed && ) || ""}
@@ -57,7 +65,7 @@ export default function NetworkListItem({
-
+
{isNotUndefined(network?.tokensLocked) ? ( formatNumberToNScale(BigNumber(network?.tokensLocked || 0).toFixed()) @@ -65,10 +73,10 @@ export default function NetworkListItem({ )} +
- +
+ {network?.networkToken?.symbol || tokenSymbolDefault}
diff --git a/components/networks-list.tsx b/components/networks-list.tsx index d2f75e273d..d16bee572d 100644 --- a/components/networks-list.tsx +++ b/components/networks-list.tsx @@ -3,7 +3,6 @@ import {useContext, useEffect, useState} from "react"; import {useTranslation} from "next-i18next"; import {useRouter} from "next/router"; - import CustomContainer from "components/custom-container"; import InternalLink from "components/internal-link"; import NetworkListBar from "components/network-list-bar"; @@ -19,8 +18,6 @@ import {Network} from "interfaces/network"; import {NetworksPageContext} from "pages/networks"; -import DAO from "services/dao-service"; - import useApi from "x-hooks/use-api"; import {useNetwork} from "x-hooks/use-network"; @@ -31,8 +28,8 @@ export default function NetworksList() { const [order, setOrder] = useState(["name", "asc"]); const [networks, setNetworks] = useState([]); - const { searchNetworks, getHeaderNetworks } = useApi(); const { getURLWithNetwork } = useNetwork(); + const { searchNetworks, getHeaderNetworks } = useApi(); const { state, dispatch } = useAppState(); const { @@ -45,21 +42,23 @@ export default function NetworksList() { setOrder(newOrder); } - function handleRedirect(networkName) { + function handleRedirect(networkName, chainName) { router.push(getURLWithNetwork("/", { - network: networkName + network: networkName, + chain: chainName })); } useEffect(() => { dispatch(changeLoadState(true)); - getHeaderNetworks().then(({ TVL, bounties, number_of_network }) => { - setTotalConverted(TVL.toFixed() || "0") - setNumberOfNetworks(number_of_network || 0) - setNumberOfBounties(bounties || 0) - }) - .catch(error => console.log("Failed to retrieve header data", error)) + getHeaderNetworks() + .then(({ TVL, bounties, number_of_network }) => { + setTotalConverted(TVL.toFixed() || "0"); + setNumberOfNetworks(number_of_network || 0); + setNumberOfBounties(bounties || 0); + }) + .catch(error => console.log("Failed to retrieve header data", error)); searchNetworks({ isRegistered: true, @@ -67,11 +66,7 @@ export default function NetworksList() { order: "asc", isNeedCountsAndTokensLocked: true }) - .then(async ({ count, rows }) => { - if (count > 0) { - setNetworks(rows); - } - }) + .then(({ rows }) => setNetworks(rows)) .catch((error) => { console.log("Failed to retrieve networks list", error); }) @@ -80,30 +75,6 @@ export default function NetworksList() { }); }, []); - useEffect(() => { - if (!state.Service?.active) return; - if (!networks.length) return; - if (networks[0]?.networkToken?.address) return; - - const web3Host = state.Settings?.urls?.web3Provider; - const dao = new DAO({web3Host, skipWindowAssignment: true}); - - dao.start() - .then( () => Promise.all(networks.map(async (network: Network) => { - const networkAddress = network?.networkAddress; - await dao.loadNetwork(networkAddress); - - const settlerTokenData = await dao.getSettlerTokenData().catch(() => undefined); - - return { - ...network, - networkToken: settlerTokenData - } - }))) - .then(setNetworks) - .catch(error => console.log("Failed to load network data", error, state.Service?.network?.active)); - }, [networks, state.Service?.active]); - return ( {(!networks.length && ( @@ -125,7 +96,7 @@ export default function NetworksList() { {orderByProperty(networks, order[0], order[1]).map((networkItem) => ( values); function handleRetryMerge() { @@ -93,19 +90,17 @@ export default function NotMergeableModal({ } useEffect(() => { - if (!pullRequest || !state.currentBounty?.data?.pullRequests?.length || mergeState === "success") return; + if (!pullRequest?.state || !state.currentBounty?.data?.pullRequests?.length || mergeState === "success") return; if (whenNotShow) { setVisible(false); } else if (isIssueOwner || isPullRequestOwner || state.Service?.network?.active?.isCouncil || isProposer) { - setVisible(pullRequest.state === "open"); + setVisible(pullRequest.state.toLowerCase() === "open"); } }, [ state.currentBounty?.data, mergeState, pullRequest, - networkProposal, - state.currentBounty?.chainData, state.currentUser?.walletAddress, state.currentUser?.login, state.Service?.network?.active?.isCouncil @@ -122,18 +117,18 @@ export default function NotMergeableModal({

- {(state.currentBounty?.chainData?.closed && + {(state.currentBounty?.data?.isClosed && t("modals.not-mergeable.closed-bounty")) || ""} - {(!state.currentBounty?.chainData?.closed && + {(!state.currentBounty?.data?.isClosed && t("modals.not-mergeable.open-bounty")) || ""}

- {state.Service?.network?.active?.isCouncil && state.currentBounty?.chainData?.closed && ( - + )} {isPullRequestOwner && ( {t("misc.get")} - {t("$oracles", { token: Service?.network?.networkToken?.symbol })} + {t("$oracles", { token: Service?.network?.active?.networkToken?.symbol })} {" "} {t("misc.from")} {networkTokenSymbol} @@ -96,7 +97,7 @@ function OraclesActions({ amount: formatNumberToNScale(tokenAmount), oracleAmount: formatNumberToNScale(oracleAmount), currency: networkTokenSymbol, - token: Service?.network?.networkToken?.symbol + token: Service?.network?.active?.networkToken?.symbol }), params() { return { tokenAmount }; @@ -108,19 +109,19 @@ function OraclesActions({ description: t("my-oracles:actions.unlock.description", { currency: networkTokenSymbol, - token: Service?.network?.networkToken?.symbol + token: Service?.network?.active?.networkToken?.symbol }), label: t("my-oracles:actions.unlock.get-amount-bepro", { amount: formatNumberToNScale(oracleAmount), currency: networkTokenSymbol, - token: Service?.network?.networkToken?.symbol + token: Service?.network?.active?.networkToken?.symbol }), caption: ( <> {t("misc.get")} { networkTokenSymbol}{" "} {t("misc.from")} - {t("$oracles", { token: Service?.network?.networkToken?.symbol })} + {t("$oracles", { token: Service?.network?.active?.networkToken?.symbol })} ), @@ -128,7 +129,7 @@ function OraclesActions({ amount: formatNumberToNScale(tokenAmount), oracleAmount: formatNumberToNScale(oracleAmount), currency: networkTokenSymbol, - token: Service?.network?.networkToken?.symbol + token: Service?.network?.active?.networkToken?.symbol }), params(from: string) { return { tokenAmount, from }; @@ -204,7 +205,7 @@ function OraclesActions({ function getCurrentLabel() { return action === t("my-oracles:actions.lock.label") ? networkTokenSymbol - : t("$oracles", { token: Service?.network?.networkToken?.symbol }); + : t("$oracles", { token: Service?.network?.active?.networkToken?.symbol }); } function getMaxAmount(trueValue = false): string { @@ -260,7 +261,7 @@ function OraclesActions({ })} symbol={`${getCurrentLabel()}`} classSymbol={`${ - getCurrentLabel() === t("$oracles", { token: Service?.network?.networkToken?.symbol }) + getCurrentLabel() === t("$oracles", { token: Service?.network?.active?.networkToken?.symbol }) ? "text-purple" : "text-primary" }`} @@ -282,7 +283,7 @@ function OraclesActions({ {getCurrentLabel()} {t("misc.available")} @@ -296,7 +297,7 @@ function OraclesActions({
{action === t("my-oracles:actions.lock.label") && ( - + )} - +
diff --git a/components/oracles-delegate.tsx b/components/oracles-delegate.tsx index 9e9495fc70..bbb8986d52 100644 --- a/components/oracles-delegate.tsx +++ b/components/oracles-delegate.tsx @@ -26,12 +26,14 @@ interface OraclesDelegateProps { } function OraclesDelegate({ - wallet, - updateWalletBalance, - defaultAddress - }: OraclesDelegateProps) { + wallet, + updateWalletBalance, + defaultAddress +}: OraclesDelegateProps) { const {t} = useTranslation(["common", "my-oracles"]); - const debounce = useRef(null) + + const debounce = useRef(null); + const [error, setError] = useState(""); const [addressError, setAddressError] = useState(""); const [tokenAmount, setTokenAmount] = useState(); @@ -44,8 +46,8 @@ function OraclesDelegate({ const { processEvent } = useApi(); - const networkTokenDecimals = Service?.network?.networkToken?.decimals || 18; - const networkTokenSymbol = Service?.network?.networkToken?.symbol; + const networkTokenDecimals = Service?.network?.active?.networkToken?.decimals || 18; + const networkTokenSymbol = Service?.network?.active?.networkToken?.symbol; function handleChangeOracles(params: NumberFormatValues) { if (params.value === "") return setTokenAmount(""); diff --git a/components/page-actions.tsx b/components/page-actions.tsx index f6ddbb15c6..1303e0823a 100644 --- a/components/page-actions.tsx +++ b/components/page-actions.tsx @@ -7,16 +7,20 @@ import {useRouter} from "next/router"; import EditIcon from "assets/icons/transactions/edit"; -import Button from "components/button"; +import ConnectGithub from "components/connect-github"; +import { ContextualSpan } from "components/contextual-span"; +import ContractButton from "components/contract-button"; import NewProposal from "components/create-proposal"; import CreatePullRequestModal from "components/create-pull-request-modal"; import ForksAvatars from "components/forks-avatars"; import GithubLink from "components/github-link"; +import Modal from "components/modal"; import ReadOnlyButtonWrapper from "components/read-only-button-wrapper"; import Translation from "components/translation"; import UpdateBountyAmountModal from "components/update-bounty-amount-modal"; import {useAppState} from "contexts/app-state"; +import {BountyEffectsProvider} from "contexts/bounty-effects"; import {addToast} from "contexts/reducers/change-toaster"; import {getIssueState} from "helpers/handleTypeIssue"; @@ -24,13 +28,8 @@ import {getIssueState} from "helpers/handleTypeIssue"; import useApi from "x-hooks/use-api"; import {useAuthentication} from "x-hooks/use-authentication"; import useBepro from "x-hooks/use-bepro"; - -import {BountyEffectsProvider} from "../contexts/bounty-effects"; -import {useBounty} from "../x-hooks/use-bounty"; -import useNetworkTheme from "../x-hooks/use-network-theme"; -import ConnectGithub from "./connect-github"; -import {ContextualSpan} from "./contextual-span"; -import Modal from "./modal"; +import {useBounty} from "x-hooks/use-bounty"; +import { useNetwork } from "x-hooks/use-network"; interface PageActionsProps { isRepoForked?: boolean; @@ -47,7 +46,7 @@ export default function PageActions({ }: PageActionsProps) { const {t} = useTranslation(["common", "pull-request", "bounty"]); - const {query: {repoId,}} = useRouter(); + const { query: { repoId } } = useRouter(); const [isExecuting, setIsExecuting] = useState(false); const [showPRModal, setShowPRModal] = useState(false); @@ -57,24 +56,22 @@ export default function PageActions({ const [isCancelable, setIsCancelable] = useState(false); - const {state, dispatch} = useAppState(); - const {handleReedemIssue, handleHardCancelBounty, handleCreatePullRequest} = useBepro(); - const {updateWalletBalance} = useAuthentication(); - const {getDatabaseBounty, getChainBounty} = useBounty(); - const {createPrePullRequest, cancelPrePullRequest, startWorking, processEvent} = useApi(); + const { state, dispatch } = useAppState(); + const { getDatabaseBounty } = useBounty(); + const { updateWalletBalance } = useAuthentication(); + const { handleReedemIssue, handleHardCancelBounty, handleCreatePullRequest } = useBepro(); + const { createPrePullRequest, cancelPrePullRequest, startWorking, processEvent } = useApi(); const issueGithubID = state.currentBounty?.data?.githubId; const isCouncilMember = !!state.Service?.network?.active?.isCouncil; - const isBountyInDraft = !!state.currentBounty?.chainData?.isDraft; - const isBountyFinished = !!state.currentBounty?.chainData?.isFinished; + const isBountyInDraft = !!state.currentBounty?.data?.isDraft; + const isBountyReadyToPropose = !!state.currentBounty?.data?.isReady; const isWalletConnected = !!state.currentUser?.walletAddress; const isKycVerified = state?.currentUser?.kycSession?.status === 'VERIFIED'; const isGithubConnected = !!state.currentUser?.login; - const isFundingRequest = - state.currentBounty?.chainData?.fundingAmount?.gt(0) || state.currentBounty?.data?.fundingAmount?.gt(0); + const isFundingRequest = !!state.currentBounty?.data?.isFundingRequest; const isWorkingOnBounty = !!state.currentBounty?.data?.working?.find((login) => login === state.currentUser?.login); - const isBountyOpen = - state.currentBounty?.chainData?.closed === false && state.currentBounty?.chainData?.canceled === false; + const isBountyOpen = state.currentBounty?.data?.isClosed === false && state.currentBounty?.data?.isCanceled === false; const issueState = getIssueState({ state: state.currentBounty?.data?.state, amount: state.currentBounty?.data?.amount, @@ -85,8 +82,8 @@ export default function PageActions({ const isBountyOwner = isWalletConnected && - state.currentBounty?.chainData?.creator && - state.currentBounty?.chainData?.creator?.toLowerCase() === state.currentUser?.walletAddress?.toLowerCase(); + state.currentBounty?.data?.creatorAddress && + state.currentBounty?.data?.creatorAddress === state.currentUser?.walletAddress; const hasPullRequests = !!state.currentBounty?.data?.pullRequests?.filter(pullRequest => pullRequest?.status !== "canceled")?.length; @@ -95,50 +92,41 @@ export default function PageActions({ !!state.currentBounty?.data?.pullRequests?.find(pullRequest => pullRequest?.githubLogin === state.currentUser?.login && pullRequest?.status !== "canceled"); - - async function updateBountyData(force = false, methods: "both" | "database" | "chain") { - // todo this must be done by the actor so it wont fall out of context - - if(["both","database"].includes(methods)) getDatabaseBounty(force); - - if(["both","chain"].includes(methods)) getChainBounty(force); - } - async function handleRedeem() { handleReedemIssue(getIssueState({ - state: state.currentBounty?.data?.state, - amount: state.currentBounty?.data?.amount, - fundingAmount: state.currentBounty?.data?.fundingAmount + state: state.currentBounty?.data?.state, + amount: state.currentBounty?.data?.amount, + fundingAmount: state.currentBounty?.data?.fundingAmount }) === "funding") .then(() => { updateWalletBalance(true); - updateBountyData(true, 'both'); - }) + getDatabaseBounty(true); + }); } async function handleHardCancel() { - setShowHardCancelModal(false) + setShowHardCancelModal(false); handleHardCancelBounty() .then(() => { updateWalletBalance(); - updateBountyData(true, 'both'); + getDatabaseBounty(true); }); } useEffect(() => { - if (state.Service?.active && state.currentBounty?.chainData) + if (state.Service?.active && state.currentBounty?.data) (async () => { const cancelableTime = await state.Service?.active.getCancelableTime(); - const canceable = +new Date() >= +new Date(state.currentBounty?.chainData.creationDate + cancelableTime) + const canceable = +new Date() >= +new Date(+state.currentBounty?.data.createdAt + cancelableTime) setIsCancelable(canceable) })() - }, [state.Service?.active, state.currentBounty?.chainData]) + }, [state.Service?.active, state.currentBounty?.data]) async function handlePullrequest({ - title: prTitle, - description: prDescription, - branch - }): Promise { + title: prTitle, + description: prDescription, + branch + }): Promise { if (!state.Service?.network?.repos?.active?.ghVisibility) return setShowGHModal(true) let pullRequestPayload = undefined; @@ -179,7 +167,7 @@ export default function PageActions({ content: t("pull-request:actions.create.success") })); - return updateBountyData(true, 'both'); + return getDatabaseBounty(true); }) .catch((err) => { if (pullRequestPayload) cancelPrePullRequest(pullRequestPayload); @@ -206,7 +194,7 @@ export default function PageActions({ setIsExecuting(true); startWorking({ - issueId: state.currentBounty?.chainData?.cid, + issueId: state.currentBounty?.data?.issueId, githubLogin: state.currentUser?.login, networkName: state.Service?.network?.active?.name, wallet: state.currentUser.walletAddress @@ -219,7 +207,7 @@ export default function PageActions({ })); addNewComment(response.data); - return updateBountyData(true, 'database'); + return getDatabaseBounty(true); }) .then(() => setIsExecuting(false)) .catch((error) => { @@ -235,7 +223,7 @@ export default function PageActions({ } function renderForkRepositoryLink() { - if (isGithubConnected && !isBountyInDraft && !isBountyFinished && isBountyOpen && !isRepoForked) + if (isGithubConnected && !isBountyInDraft && isBountyOpen && !isRepoForked) return ( - + } else{ return ( - + ); } @@ -292,13 +280,13 @@ export default function PageActions({ isRepoForked) return ( - + ); } @@ -307,13 +295,13 @@ export default function PageActions({ if (state.Service?.network?.active?.isGovernor && isCancelable) return ( - + ); } @@ -324,12 +312,12 @@ export default function PageActions({ if (isWalletConnected && isBountyOpen && isBountyOwner && isDraftOrNotFunded && !isEditIssue) return ( - + ); } @@ -338,21 +326,23 @@ export default function PageActions({ if (isWalletConnected && isBountyOpen && isBountyOwner && isBountyInDraft && !isFundingRequest && !isEditIssue) return ( - + ); } function renderCreateProposalButton() { - if (isWalletConnected && isCouncilMember && isBountyOpen && isBountyFinished && hasPullRequests) + if (isWalletConnected && isCouncilMember && isBountyOpen && isBountyReadyToPropose && hasPullRequests) return ( - + ); } @@ -376,13 +366,13 @@ export default function PageActions({ if (isWalletConnected && isBountyInDraft && isBountyOwner) return ( - + ); } @@ -458,8 +448,8 @@ export default function PageActions({ setShowUpdateAmount(false)} /> diff --git a/components/page-hero.tsx b/components/page-hero.tsx index 8bb303a469..5f933b523d 100644 --- a/components/page-hero.tsx +++ b/components/page-hero.tsx @@ -2,14 +2,14 @@ import React from "react"; import InfoIcon from "assets/icons/info-icon"; +import Button from "components/button"; +import CustomContainer from "components/custom-container"; + import {formatNumberToNScale} from "helpers/formatNumber"; import {highlightText} from "helpers/string"; import {Currency} from "interfaces/currency"; -import Button from "./button"; -import CustomContainer from "./custom-container"; - export interface InfosHero { value: number | string; label: string; diff --git a/components/price-conversor-modal.tsx b/components/price-conversor-modal.tsx index 217572cdb6..ba2d197cc6 100644 --- a/components/price-conversor-modal.tsx +++ b/components/price-conversor-modal.tsx @@ -9,12 +9,12 @@ import InputNumber from "components/input-number"; import Modal from "components/modal"; import ReactSelect from "components/react-select"; +import {useAppState} from "contexts/app-state"; + import {formatNumberToNScale} from "helpers/formatNumber"; import {getCoinInfoByContract} from "services/coingecko"; -import {useAppState} from "../contexts/app-state"; - interface IPriceConversiorModalProps{ show: boolean; onClose: ()=> void; @@ -37,10 +37,10 @@ export default function PriceConversorModal({ const {state} = useAppState(); async function handlerChange({value, label}){ - if (!state.Service?.network?.networkToken?.address) return; + if (!state.Service?.network?.active?.networkToken?.address) return; const data = - await getCoinInfoByContract(state.Service?.network?.networkToken.symbol) + await getCoinInfoByContract(state.Service?.network?.active?.networkToken.symbol) .catch((err) => { if(err) setErrorCoinInfo(true) return ({ prices: { [value]: 0 } }) @@ -101,7 +101,7 @@ export default function PriceConversorModal({
setValue(e.floatValue)} /> @@ -132,7 +132,7 @@ export default function PriceConversorModal({
- {formatNumberToNScale(currentPrice * currentValue)} {state.Service?.network?.networkToken?.symbol} + {formatNumberToNScale(currentPrice * currentValue)} {state.Service?.network?.active?.networkToken?.symbol}
); diff --git a/components/profile/connect-button.tsx b/components/profile/connect-button.tsx index d8404aacaa..9b214d6932 100644 --- a/components/profile/connect-button.tsx +++ b/components/profile/connect-button.tsx @@ -69,7 +69,15 @@ function ConnectionButton({ ...(isDisabled || isLoading)? ["pe-none", "trans"] : [], ]; - const handleConnect = () => credential ? undefined : connect(); + const handleConnect = () => { + if (credential) return undefined; + else + try { + return connect(); + } catch (err) { + console.debug("handleConnect error:", err); + } + }; return(
diff --git a/components/profile/my-network-settings/governance-settings.tsx b/components/profile/my-network-settings/governance-settings.tsx index d0ed300ac5..07fbd627a0 100644 --- a/components/profile/my-network-settings/governance-settings.tsx +++ b/components/profile/my-network-settings/governance-settings.tsx @@ -3,9 +3,7 @@ import { Col, Row } from "react-bootstrap"; import { useTranslation } from "next-i18next"; -import LockedIcon from "assets/icons/locked-icon"; - -import Button from "components/button"; +import ContractButton from "components/contract-button"; import NetworkContractSettings from "components/custom-network/network-contract-settings"; import TokensSettings from "components/profile/my-network-settings/tokens-settings"; @@ -95,7 +93,7 @@ export default function GovernanceSettings({ ...token, isReward: !!token.network_tokens.isReward, isTransactional: !!token.network_tokens.isTransactional - }))) + }))); }, [tokens]); return ( @@ -116,7 +114,7 @@ export default function GovernanceSettings({ /> - + diff --git a/components/profile/my-network-settings/index.tsx b/components/profile/my-network-settings/index.tsx index f7dc4bc5fd..c60d6c4461 100644 --- a/components/profile/my-network-settings/index.tsx +++ b/components/profile/my-network-settings/index.tsx @@ -3,7 +3,7 @@ import { Col, Row } from "react-bootstrap"; import { useTranslation } from "next-i18next"; -import Button from "components/button"; +import ContractButton from "components/contract-button"; import { ContainerTab } from "components/profile/my-network-settings/container-tab"; import GovernanceSettings from "components/profile/my-network-settings/governance-settings"; import LogoAndColoursSettings from "components/profile/my-network-settings/logo-and-colours-settings"; @@ -94,6 +94,87 @@ export default function MyNetworkSettings({ setIsUpdating(true); + const { + parameters: { + draftTime: { value: draftTime }, + disputableTime: { value: disputableTime }, + councilAmount: { value: councilAmount }, + percentageNeededForDispute: { value: percentageForDispute }, + }, + } = settings; + + const networkAddress = network?.networkAddress; + const failed = []; + const success = {}; + + const promises = await Promise.allSettled([ + ...(draftTime !== forcedNetwork.draftTime + ? [ + handleChangeNetworkParameter("draftTime", + draftTime, + networkAddress) + .then(() => ({ param: "draftTime", value: draftTime })), + ] + : []), + ...(disputableTime !== forcedNetwork.disputableTime + ? [ + handleChangeNetworkParameter("disputableTime", + disputableTime, + networkAddress) + .then(() => ({ param: "disputableTime", value: disputableTime })), + ] + : []), + ...(councilAmount !== +forcedNetwork.councilAmount + ? [ + handleChangeNetworkParameter("councilAmount", + councilAmount, + networkAddress) + .then(() => ({ param: "councilAmount", value: councilAmount })), + ] + : []), + ...(percentageForDispute !== forcedNetwork.percentageNeededForDispute + ? [ + handleChangeNetworkParameter("percentageNeededForDispute", + percentageForDispute, + networkAddress) + .then(() => ({ param: "percentageNeededForDispute", value: percentageForDispute })), + ] + : []), + ]); + + promises.forEach((promise) => { + if (promise.status === "fulfilled") success[promise.value.param] = promise.value.value; + else failed.push(promise.reason); + }); + + if (failed.length) { + dispatch(toastError(t("custom-network:errors.updated-parameters", { + failed: failed.length, + }), + t("custom-network:errors.updating-values"))); + console.error(failed); + } + + const successQuantity = Object.keys(success).length; + + if (successQuantity) { + if(draftTime !== forcedNetwork.draftTime) + Promise.all([ + await processEvent("bounty","update-draft-time", network.name), + await processEvent("bounty","moved-to-open", network.name) + ]); + + await processEvent("network", "parameters", network.name.toLowerCase(), { + chainId: state.connectedChain?.id + }) + .catch(error => console.debug("Failed to update network parameters", error)); + + dispatch(toastSuccess(t("custom-network:messages.updated-parameters", { + updated: successQuantity, + total: promises.length, + }))); + } + const json = { description: details?.description || "", colors: JSON.stringify(settings.theme.colors), @@ -116,82 +197,12 @@ export default function MyNetworkSettings({ allowedTokens: { transactional: tokens?.allowedTransactions.map((token) => token?.id).filter((v) => v), reward: tokens?.allowedRewards.map((token) => token?.id).filter((v) => v) - } + }, + parameters: success }; updateNetwork(json) .then(async () => { - const { - parameters: { - draftTime: { value: draftTime }, - disputableTime: { value: disputableTime }, - councilAmount: { value: councilAmount }, - percentageNeededForDispute: { value: percentageForDispute }, - oracleExchangeRate: { value: oracleExchangeRate }, - mergeCreatorFeeShare: { value: mergeCreatorFeeShare }, - proposerFeeShare: { value: proposerFeeShare }, - cancelableTime: { value: cancelableTime } - }, - } = settings; - - const networkAddress = network?.networkAddress; - - const promises = await Promise.allSettled([ - ...(draftTime !== forcedNetwork.draftTime ? [ - handleChangeNetworkParameter("draftTime", draftTime, networkAddress) - ] : []), - ...(disputableTime !== forcedNetwork.disputableTime ? [ - handleChangeNetworkParameter("disputableTime", disputableTime, networkAddress) - ] : []), - ...(councilAmount !== +forcedNetwork.councilAmount ? [ - handleChangeNetworkParameter("councilAmount", councilAmount, networkAddress) - ] : []), - ...(percentageForDispute !== forcedNetwork.percentageNeededForDispute ? [ - handleChangeNetworkParameter("percentageNeededForDispute", percentageForDispute, networkAddress) - ] : []), - ...(oracleExchangeRate !== forcedNetwork.oracleExchangeRate ? [ - handleChangeNetworkParameter("oracleExchangeRate", oracleExchangeRate, networkAddress) - ] : []), - ...(mergeCreatorFeeShare !== forcedNetwork.mergeCreatorFeeShare ? [ - handleChangeNetworkParameter("mergeCreatorFeeShare", mergeCreatorFeeShare, networkAddress) - ] : []), - ...(proposerFeeShare !== forcedNetwork.proposerFeeShare ? [ - handleChangeNetworkParameter("proposerFeeShare", proposerFeeShare, networkAddress) - ] : []), - ...(cancelableTime !== +forcedNetwork.cancelableTime ? [ - handleChangeNetworkParameter("cancelableTime", cancelableTime, networkAddress) - ] : []) - ]); - - const failed = []; - const success = []; - - promises.forEach((promise) => { - if (promise.status === "fulfilled") success.push(promise.value); - else failed.push(promise.reason); - }); - - if (failed.length) { - dispatch(toastError(t("custom-network:errors.updated-parameters", { - failed: failed.length, - }), t("custom-network:errors.updating-values"))); - console.error(failed); - } - - if (success.length){ - if(draftTime !== forcedNetwork.draftTime) - Promise.all([ - await processEvent("bounty","update-draft-time", network.name), - await processEvent("bounty","moved-to-open", network.name) - ]) - - dispatch(toastSuccess(t("custom-network:messages.updated-parameters", { - updated: success.length, - total: promises.length, - }))); - } - - if (isCurrentNetwork) updateActiveNetwork(true); return updateEditingNetwork(); @@ -221,12 +232,14 @@ export default function MyNetworkSettings({ }, [details?.fullLogo, details?.iconLogo]); useEffect(() => { - if (!state.Service?.active || !state.currentUser?.walletAddress) return; + if (!state.Service?.active?.registry?.contractAddress || + !state.currentUser?.walletAddress || + !state.connectedChain?.id) return; state.Service?.active .isRegistryGovernor(state.currentUser?.walletAddress) .then(setIsGovernorRegistry); - }, [state.Service?.active, state.currentUser?.walletAddress]); + }, [state.currentUser?.walletAddress, state.Service?.active?.registry?.contractAddress, state.connectedChain?.id]); useEffect(() => { if(!network) return; @@ -291,29 +304,28 @@ export default function MyNetworkSettings({ {isCurrentNetwork && ( )} - + {!state.currentUser?.login && } - + - + {settings?.validated && github?.validated && !network?.isClosed && !networkNeedRegistration && ( - + )} diff --git a/components/profile/my-network-settings/logo-and-colours-settings.tsx b/components/profile/my-network-settings/logo-and-colours-settings.tsx index ea35ec1170..58eadd2df9 100644 --- a/components/profile/my-network-settings/logo-and-colours-settings.tsx +++ b/components/profile/my-network-settings/logo-and-colours-settings.tsx @@ -6,7 +6,7 @@ import getConfig from "next/config"; import InfoIconEmpty from "assets/icons/info-icon-empty"; -import Button from "components/button"; +import ContractButton from "components/contract-button"; import AmountCard from "components/custom-network/amount-card"; import ThemeColors from "components/custom-network/theme-colors"; import ImageUploader from "components/image-uploader"; @@ -140,7 +140,7 @@ export default function LogoAndColoursSettings({ - + )} diff --git a/components/profile/my-network-settings/registry-settings.tsx b/components/profile/my-network-settings/registry-settings.tsx index 483b268941..f5336e5c1f 100644 --- a/components/profile/my-network-settings/registry-settings.tsx +++ b/components/profile/my-network-settings/registry-settings.tsx @@ -3,7 +3,7 @@ import { Col, Row } from "react-bootstrap"; import { useTranslation } from "next-i18next"; -import Button from "components/button"; +import ContractButton from "components/contract-button"; import NetworkParameterInput from "components/custom-network/network-parameter-input"; import TreasuryAddressField from "components/custom-network/treasury-address-field"; import { FormGroup } from "components/form-group"; @@ -168,7 +168,7 @@ export default function RegistrySettings({ isGovernorRegistry = false }) { {isGovernorRegistry && ( - + )} @@ -196,7 +196,7 @@ export default function RegistrySettings({ isGovernorRegistry = false }) { {isGovernorRegistry && ( - + )} @@ -221,7 +221,7 @@ export default function RegistrySettings({ isGovernorRegistry = false }) { /> {isGovernorRegistry && ( - + )} diff --git a/components/profile/my-network-settings/tokens-settings.tsx b/components/profile/my-network-settings/tokens-settings.tsx index 053f126ca7..638cbe808f 100644 --- a/components/profile/my-network-settings/tokens-settings.tsx +++ b/components/profile/my-network-settings/tokens-settings.tsx @@ -3,20 +3,20 @@ import {Col, Row} from "react-bootstrap"; import {useTranslation} from "next-i18next"; -import {useNetworkSettings} from "contexts/network-settings"; +import ContractButton from "components/contract-button"; +import MultipleTokensDropdown from "components/multiple-tokens-dropdown"; +import {WarningSpan} from "components/warning-span"; + +import {useAppState} from "contexts/app-state"; +import { useNetworkSettings } from "contexts/network-settings"; import {Token, TokenType} from "interfaces/token"; import useApi from "x-hooks/use-api"; - -import {useAppState} from "../../../contexts/app-state"; -import useBepro from "../../../x-hooks/use-bepro"; -import Button from "../../button"; -import MultipleTokensDropdown from "../../multiple-tokens-dropdown"; -import {WarningSpan} from "../../warning-span"; +import useBepro from "x-hooks/use-bepro"; interface SelectedTokens { - [tokenType: TokenType | string]: string[] + [tokenType: TokenType | string]: string[]; } export default function TokensSettings({ @@ -58,7 +58,7 @@ export default function TokensSettings({ setIsLoadingTokens(true); try { - const dbTokens = await getTokens(); + const dbTokens = await getTokens(state.connectedChain?.id); const { dbRewardAllowed, @@ -151,11 +151,11 @@ export default function TokensSettings({ useEffect(() => { - if (!state.Service?.active) return; + if (!state.Service?.active || !state.connectedChain?.id) return; getAllowedTokensContract(); - }, [state.Service?.active, isGovernorRegistry]); + }, [state.Service?.active, state.connectedChain?.id, isGovernorRegistry]); useEffect(() => { if (defaultSelectedTokens?.length > 0) { @@ -173,13 +173,13 @@ export default function TokensSettings({ function renderButtons(tokenType: TokenType) { return (
- +
); } diff --git a/components/profile/payment-item.tsx b/components/profile/payment-item.tsx index 3b47f47824..fc4d2869f8 100644 --- a/components/profile/payment-item.tsx +++ b/components/profile/payment-item.tsx @@ -27,7 +27,7 @@ export default function PaymentItem({ {formatNumberToCurrency(ammount)} - {`${issue?.token?.symbol || labelToken}`} + {`${issue?.transactionalToken?.symbol || labelToken}`} diff --git a/components/profile/payments-list.tsx b/components/profile/payments-list.tsx index c569828556..218fc56ce6 100644 --- a/components/profile/payments-list.tsx +++ b/components/profile/payments-list.tsx @@ -1,11 +1,11 @@ import { useTranslation } from "next-i18next"; import { useRouter } from "next/router"; -import { Payment } from "interfaces/payments"; +import PaymentItem from "components/profile/payment-item"; -import useNetworkTheme from "x-hooks/use-network-theme"; +import { Payment } from "interfaces/payments"; -import PaymentItem from "./payment-item"; +import { useNetwork} from "x-hooks/use-network"; interface PaymentsListProps { payments: Payment[]; @@ -16,7 +16,7 @@ export default function PaymentsList({ payments }: PaymentsListProps) { const { push } = useRouter(); const { t } = useTranslation(["common", "profile", "bounty"]); - const { getURLWithNetwork } = useNetworkTheme(); + const { getURLWithNetwork } = useNetwork(); function handleItemClick(issueId: string) { const [repoId, id] = issueId.split('/') diff --git a/components/profile/profile-layout.tsx b/components/profile/profile-layout.tsx index 3db1c0226d..b31aca3cf5 100644 --- a/components/profile/profile-layout.tsx +++ b/components/profile/profile-layout.tsx @@ -1,7 +1,19 @@ +import { useEffect } from "react"; + import ConnectWalletButton from "components/connect-wallet-button"; import ProfileSide from "components/profile/profile-side"; +import { useAppState } from "contexts/app-state"; +import { changeNeedsToChangeChain } from "contexts/reducers/change-spinners"; + export default function ProfileLayout({ children }) { + const { state, dispatch } = useAppState(); + + useEffect(() => { + if (state.currentUser?.walletAddress && !state.connectedChain?.matchWithNetworkChain) + dispatch(changeNeedsToChangeChain(true)); + }, [state.currentUser?.walletAddress, state.connectedChain?.matchWithNetworkChain]); + return( <> diff --git a/components/profile/profile-side.tsx b/components/profile/profile-side.tsx index 695bef01a7..c77e2ae192 100644 --- a/components/profile/profile-side.tsx +++ b/components/profile/profile-side.tsx @@ -2,12 +2,12 @@ import { useTranslation } from "next-i18next"; import InternalLink from "components/internal-link"; -import useNetworkTheme from "x-hooks/use-network-theme"; +import { useNetwork } from "x-hooks/use-network"; export default function ProfileSide() { const { t } = useTranslation("common"); - const { getURLWithNetwork } = useNetworkTheme(); + const { getURLWithNetwork } = useNetwork(); const Link = (label, href) => ({ label, href }); const ProfileLink = ({ label, href }) => ( diff --git a/components/profile/token-balance.tsx b/components/profile/token-balance.tsx index f96530d39c..c11f6c7877 100644 --- a/components/profile/token-balance.tsx +++ b/components/profile/token-balance.tsx @@ -1,15 +1,14 @@ import BigNumber from "bignumber.js"; import { useTranslation } from "next-i18next"; -import Button from "components/button"; +import ContractButton from "components/contract-button"; +import { FlexColumn, FlexRow } from "components/profile/wallet-balance"; import { formatStringToCurrency } from "helpers/formatNumber"; import { DelegationExtended } from "interfaces/oracles-state"; import { TokenInfo } from "interfaces/token"; -import { FlexColumn, FlexRow } from "./wallet-balance"; - export type TokenBalanceType = Partial; interface TokenBalanceProps { @@ -60,9 +59,9 @@ export default function TokenBalance({ {type === "delegation" && - || + || <> {formatStringToCurrency(BigNumber(balance).toFixed())} {symbol} diff --git a/components/profile/wallet-balance.tsx b/components/profile/wallet-balance.tsx index 1c8a3affc1..e41146e2a9 100644 --- a/components/profile/wallet-balance.tsx +++ b/components/profile/wallet-balance.tsx @@ -3,17 +3,19 @@ import {useEffect, useState} from "react"; import BigNumber from "bignumber.js"; import {useTranslation} from "next-i18next"; -import BeProBlue from "assets/icons/bepro-blue"; import OracleIcon from "assets/icons/oracle-icon"; -import TokenIconPlaceholder from "assets/icons/token-icon-placeholder"; import InfoTooltip from "components/info-tooltip"; +import TokenBalance, {TokenBalanceType} from "components/profile/token-balance"; +import TokenIcon from "components/token-icon"; + +import {useAppState} from "contexts/app-state"; import {formatStringToCurrency} from "helpers/formatNumber"; -import {useAppState} from "../../contexts/app-state"; -import {getCoinInfoByContract} from "../../services/coingecko"; -import TokenBalance, {TokenBalanceType} from "./token-balance"; +import { Token } from "interfaces/token"; + +import {getCoinInfoByContract} from "services/coingecko"; export const FlexRow = ({children, className = ""}) =>
{children}
; @@ -25,7 +27,11 @@ export default function WalletBalance() { const { t } = useTranslation(["common", "profile"]); const [totalAmount, setTotalAmount] = useState("0"); - const [oracleToken, setOracleToken] = useState(null); + const [oracleToken, setOracleToken] = useState({ + symbol: "", + name: "", + icon: + }); const [tokens, setTokens] = useState([]); const [hasNoConvertedToken, setHasNoConvertedToken] = useState(false); @@ -34,52 +40,54 @@ export default function WalletBalance() { const oraclesLocked = state.currentUser?.balance?.oracles?.locked || BigNumber(0); const oraclesDelegatedToMe = state.currentUser?.balance?.oracles?.delegatedByOthers || BigNumber(0); + const getAddress = (token: string | Token) => typeof token === "string" ? token : token?.address; + + async function processToken(token: string | Token) { + const [tokenData, balance] = await Promise.all([ + typeof token === "string" ? state.Service.active.getERC20TokenData(token) : token, + state.Service.active.getTokenBalance(getAddress(token), state.currentUser.walletAddress) + ]); + + const tokenInformation = await getCoinInfoByContract(tokenData.symbol); + + return { + balance, + ...tokenData, + icon: + }; + } + function loadBalances() { - const networkTokenAddress = state.Service?.network?.networkToken?.address; + const networkToken = state.Service?.network?.active?.networkToken; + const registryTokenAddress = state.Service?.active?.registry?.token?.contractAddress?.toLowerCase(); - if (!state.currentUser?.walletAddress || !state.Service?.active || !networkTokenAddress) + if (!state.currentUser?.walletAddress || + !registryTokenAddress || + !networkToken?.address || + !state.connectedChain?.matchWithNetworkChain) return; - const { networkToken } = state.Service?.network || {}; - - state.Service.active.loadRegistry() - .then(registry => { - if (!registry) return; - - const registryTokenAddress = registry.token.contractAddress; - - Promise.all([ - state.Service.active.getTokenBalance(registryTokenAddress, state.currentUser.walletAddress) - .then(async (balance) => { - const tokenData = await state.Service.active.getERC20TokenData(registryTokenAddress); - const tokenInformation = await getCoinInfoByContract(tokenData.symbol); - - return { - balance, - ...tokenData, - icon: tokenInformation?.icon ? : // eslint-disable-line - } - }), - registryTokenAddress === state.Service?.network?.networkToken?.address ? Promise.resolve(null) : - state.Service.active - .getTokenBalance(state.Service?.network?.networkToken?.address, state.currentUser.walletAddress) - .then(balance => ({ ...networkToken, balance, icon: })) - ]).then(tokens => { - setOracleToken({ - symbol: t("$oracles", { token: networkToken?.symbol }), - name: networkToken?.name, - icon: - }) - - setTokens(tokens.filter(v => !!v)); - }); - }); + const isSameToken = registryTokenAddress === networkToken.address; + + Promise.all([ + isSameToken ? Promise.resolve(null) : processToken(registryTokenAddress), + processToken(networkToken.address) + ]).then(tokens => { + setOracleToken({ + symbol: t("$oracles", { token: networkToken?.symbol }), + name: networkToken?.name, + icon: + }) + + setTokens(tokens.filter(v => !!v)); + }); } useEffect(loadBalances, [ state.currentUser?.walletAddress, - state.Service?.active, - state.Service?.network?.networkToken?.address + state.Service?.active?.registry?.token?.contractAddress, + state.Service?.active?.network?.contractAddress, + state.connectedChain?.matchWithNetworkChain ]); useEffect(() => { diff --git a/components/proposal-action-card.tsx b/components/proposal-action-card.tsx index b5f71a7115..d483d743e0 100644 --- a/components/proposal-action-card.tsx +++ b/components/proposal-action-card.tsx @@ -4,8 +4,8 @@ import BigNumber from "bignumber.js"; import {addSeconds, formatDistance} from "date-fns"; import {useTranslation} from "next-i18next"; -import Button from "components/button"; import {ContextualSpan} from "components/contextual-span"; +import ContractButton from "components/contract-button"; import ProposalMerge from "components/proposal-merge"; import ProposalProgressBar from "components/proposal-progress-bar"; @@ -13,7 +13,6 @@ import {useAppState} from "contexts/app-state"; import {isProposalDisputable} from "helpers/proposal"; -import {ProposalExtended} from "interfaces/bounty"; import {pullRequest} from "interfaces/issue-data"; import {DistributedAmounts, Proposal} from "interfaces/proposal"; @@ -21,7 +20,6 @@ import useOctokit from "x-hooks/use-octokit"; interface IProposalActionCardProps { proposal: Proposal; - networkProposal: ProposalExtended; currentPullRequest: pullRequest; distributedAmounts: DistributedAmounts; onMerge: () => Promise; @@ -31,7 +29,6 @@ interface IProposalActionCardProps { export default function ProposalActionCard({ proposal, - networkProposal, currentPullRequest, onMerge, onDispute, @@ -44,12 +41,13 @@ export default function ProposalActionCard({ const [isRefusing, setIsRefusing] = useState(false); const [chaintime, setChainTime] = useState(); const [isDisputing, setIsDisputing] = useState(false); + const [canUserDispute, setCanUserDispute] = useState(false); const [allowMergeCommit, setAllowMergeCommit] = useState(); const [chainDisputable, setChainDisputable] = useState(false); const [missingDisputableTime, setMissingDisputableTime] = useState(''); - const {getRepository} = useOctokit(); - const {state} = useAppState(); + const { state } = useAppState(); + const { getRepository } = useOctokit(); const bountyAmount = BigNumber.maximum(state.currentBounty?.data?.amount || 0, state.currentBounty?.data?.fundingAmount || 0); @@ -61,23 +59,23 @@ export default function ProposalActionCard({ const prsNeedsApproval = approvalsCurrentPr < approvalsRequired; const proposalCanBeDisputed = () => [ - isProposalDisputable(proposal?.createdAt, - BigNumber(state.Service?.network.times?.disputableTime).toNumber(), + isProposalDisputable(proposal?.contractCreationDate, + BigNumber(state.Service?.network?.times?.disputableTime).toNumber(), chaintime), - networkProposal?.canUserDispute, + canUserDispute, !proposal?.isDisputed, !proposal?.refusedByBountyOwner, - !state.currentBounty?.chainData?.closed, + !state.currentBounty?.data?.isClosed, !proposal?.isDisputed, !proposal?.isMerged ].every(c => c); const isRefusable = () => [ - !state.currentBounty?.chainData?.closed, - !state.currentBounty?.chainData?.canceled, + !state.currentBounty?.data?.isClosed, + !state.currentBounty?.data?.isCanceled, !proposal?.isDisputed, !proposal?.refusedByBountyOwner, - state.currentBounty?.chainData?.creator === state.currentUser?.walletAddress + state.currentBounty?.data?.creatorAddress === state.currentUser?.walletAddress ].every(v => v); const canMerge = () => [ @@ -85,7 +83,9 @@ export default function ProposalActionCard({ !proposal?.isMerged, !proposal?.isDisputed, !proposal?.refusedByBountyOwner, - !isProposalDisputable(proposal?.createdAt, BigNumber(state.Service?.network.times?.disputableTime).toNumber()), + !isProposalDisputable(proposal?.contractCreationDate, + BigNumber(state.Service?.network?.times?.disputableTime).toNumber(), + chaintime), !isMerging, !isRefusing, !isDisputing, @@ -109,10 +109,10 @@ export default function ProposalActionCard({ } function changeMissingDisputableTime() { - if (!chaintime || !state.Service?.network?.times?.disputableTime || !proposal) + if (!chaintime || !state.Service?.network?.times?.disputableTime || !proposal?.contractCreationDate) return; - const target = addSeconds(new Date(proposal?.createdAt), +state.Service?.network.times.disputableTime); + const target = addSeconds(new Date(proposal?.contractCreationDate), +state.Service?.network.times.disputableTime); const missingTime = formatDistance(new Date(chaintime), target, {includeSeconds: true}); setMissingDisputableTime(missingTime); @@ -120,7 +120,7 @@ export default function ProposalActionCard({ } useEffect(changeMissingDisputableTime, [ - proposal?.createdAt, + proposal?.contractCreationDate, chaintime, state.Service?.network?.times?.disputableTime ]); @@ -137,6 +137,14 @@ export default function ProposalActionCard({ .catch(console.debug); }, [state?.currentBounty?.data]); + useEffect(() => { + if (!proposal || !state.currentUser?.walletAddress) + setCanUserDispute(false); + else + setCanUserDispute(!proposal.disputes?.some(({ address, weight }) => + address === state.currentUser.walletAddress && weight.gt(0))); + }, [proposal, state.currentUser?.walletAddress]); + return (
@@ -145,7 +153,7 @@ export default function ProposalActionCard({ issueDisputeAmount={proposal?.disputeWeight?.toNumber()} disputeMaxAmount={+state.Service?.network?.amounts?.percentageNeededForDispute || 0} isDisputed={proposal?.isDisputed} - isFinished={state.currentBounty?.chainData?.closed} + isFinished={state.currentBounty?.data?.isClosed} isMerged={proposal?.isMerged} refused={proposal?.refusedByBountyOwner} /> @@ -160,7 +168,7 @@ export default function ProposalActionCard({
{proposalCanBeDisputed() && ( - + )} {isRefusable() && ( - + )}
- { - chainDisputable && + { chainDisputable &&
- + {t('proposal:messages.in-disputable-time', {time: missingDisputableTime})}
|| "" @@ -209,7 +216,7 @@ export default function ProposalActionCard({ { allowMergeCommit === false &&
- + {t("pull-request:errors.merge-commit")}
@@ -217,7 +224,7 @@ export default function ProposalActionCard({ { prsNeedsApproval &&
- + {t("pull-request:errors.approval")}
diff --git a/components/proposal-disputes.tsx b/components/proposal-disputes.tsx index 2275de99cb..371063a259 100644 --- a/components/proposal-disputes.tsx +++ b/components/proposal-disputes.tsx @@ -3,12 +3,12 @@ import {useTranslation} from "next-i18next"; import ArrowRight from "assets/icons/arrow-right"; -import { formatNumberToNScale } from "helpers/formatNumber"; +import {CaptionMedium} from "components/bounty/funding-section/minimals"; -import {useAppState} from "../contexts/app-state"; -import {truncateAddress} from "../helpers/truncate-address"; -import {CaptionMedium} from "./bounty/funding-section/minimals"; +import {useAppState} from "contexts/app-state"; +import { formatNumberToNScale } from "helpers/formatNumber"; +import {truncateAddress} from "helpers/truncate-address"; export function ProposalDisputes({proposalId}: { proposalId: number}) { const {state} = useAppState(); @@ -53,7 +53,7 @@ export function ProposalDisputes({proposalId}: { proposalId: number}) { {formatNumberToNScale(weight)}{" "} {t("common:$oracles", { - token: state.Service?.network?.networkToken?.symbol, + token: state.Service?.network?.active?.networkToken?.symbol, })} diff --git a/components/proposal-hero.tsx b/components/proposal-hero.tsx index cf03a0bead..145a0adef8 100644 --- a/components/proposal-hero.tsx +++ b/components/proposal-hero.tsx @@ -4,17 +4,17 @@ import {useRouter} from "next/router"; import ArrowLeft from "assets/icons/arrow-left"; import AvatarOrIdenticon from "components/avatar-or-identicon"; +import CustomContainer from "components/custom-container"; +import DateLabel from "components/date-label"; import GithubInfo from "components/github-info"; +import PriceConversor from "components/price-conversor"; + +import {useAppState} from "contexts/app-state"; import { truncateAddress } from "helpers/truncate-address"; import {Proposal} from "interfaces/proposal"; -import {useAppState} from "../contexts/app-state"; -import CustomContainer from "./custom-container"; -import DateLabel from "./date-label"; -import PriceConversor from "./price-conversor"; - interface ProposalHeroProps { proposal: Proposal; } @@ -83,7 +83,7 @@ export default function ProposalHero({
diff --git a/components/proposal-list-distribution.tsx b/components/proposal-list-distribution.tsx index 939d79a24f..8972991b7e 100644 --- a/components/proposal-list-distribution.tsx +++ b/components/proposal-list-distribution.tsx @@ -5,6 +5,8 @@ import { useTranslation } from "next-i18next"; import BountyDistributionItem from "components/bounty-distribution-item"; +import { useAppState } from "contexts/app-state"; + import { truncateAddress } from "helpers/truncate-address"; import { DistributedAmounts } from "interfaces/proposal"; @@ -12,7 +14,6 @@ import { TokenInfo } from "interfaces/token"; import { getCoinInfoByContract } from "services/coingecko"; -import { useAppState } from "../contexts/app-state"; interface Props { distributedAmounts: DistributedAmounts; } @@ -27,7 +28,7 @@ export default function ProposalListDistribution({ const { state } = useAppState(); async function getCoinInfo() { - await getCoinInfoByContract(state.Service?.network?.networkToken?.symbol) + await getCoinInfoByContract(state.Service?.network?.active?.networkToken?.symbol) .then((tokenInfo) => { setCoinInfo(tokenInfo); }) @@ -40,7 +41,7 @@ export default function ProposalListDistribution({ .toFixed(4); const currentTokenSymbol = - state.currentBounty?.data?.token?.symbol || t("common:misc.token"); + state.currentBounty?.data?.transactionalToken?.symbol || t("common:misc.token"); useEffect(() => { if (state.Service?.network?.amounts) getCoinInfo(); diff --git a/components/proposal-merge.tsx b/components/proposal-merge.tsx index 42cfa4347a..e4efa0e3f7 100644 --- a/components/proposal-merge.tsx +++ b/components/proposal-merge.tsx @@ -4,7 +4,11 @@ import BigNumber from "bignumber.js"; import {useTranslation} from "next-i18next"; import Button from "components/button"; +import ContractButton from "components/contract-button"; import Modal from "components/modal"; +import ProposalListDistribution from "components/proposal-list-distribution"; + +import {useAppState} from "contexts/app-state"; import {formatStringToCurrency} from "helpers/formatNumber"; @@ -13,11 +17,6 @@ import {TokenInfo} from "interfaces/token"; import {getCoinInfoByContract} from "services/coingecko"; -import {useAppState} from "../contexts/app-state"; -import ProposalListDistribution from "./proposal-list-distribution"; - - - interface props { amountTotal: BigNumber; tokenSymbol?: string; @@ -54,7 +53,7 @@ export default function ProposalMerge({ } async function getCoinInfo() { - await getCoinInfoByContract(state.Service?.network?.networkToken?.symbol).then((tokenInfo) => { + await getCoinInfoByContract(state.Service?.network?.active?.networkToken?.symbol).then((tokenInfo) => { setCoinInfo(tokenInfo) }).catch(error => console.debug("getCoinInfo", error)); } @@ -73,7 +72,7 @@ export default function ProposalMerge({ return ( <> - + {" "} /{formatNumberToNScale(totalNetworkToken || 0)}{" "} - {" "} + {" "} ({percentage}%)
diff --git a/components/proposal-progress-small.tsx b/components/proposal-progress-small.tsx index e7598fff12..c72ffffc5a 100644 --- a/components/proposal-progress-small.tsx +++ b/components/proposal-progress-small.tsx @@ -1,7 +1,5 @@ import BigNumber from "bignumber.js"; -import { useAppState } from "contexts/app-state"; - import { formatNumberToNScale } from "helpers/formatNumber"; interface Options { @@ -15,16 +13,9 @@ export default function ProposalProgressSmall({ total = BigNumber(0), color = 'purple' }: Options) { - const {state} = useAppState() const dotStyle = { width: "4px", height: "14px" }; - const disputePercentage = +state.Service?.network?.amounts?.percentageNeededForDispute || 3; - - function toRepresentationPercent(value = 0, total = 5) { - return (value * 100) / total; - } - - const percent = BigNumber(value.multipliedBy(100).toFixed(2,1) || 0).dividedBy(total); + const percent = value.multipliedBy(100).dividedBy(total).toNumber(); return (
@@ -38,12 +29,12 @@ export default function ProposalProgressSmall({
diff --git a/components/proposal-progress.tsx b/components/proposal-progress.tsx index e1e7075c88..afc389db18 100644 --- a/components/proposal-progress.tsx +++ b/components/proposal-progress.tsx @@ -2,13 +2,13 @@ import React from "react"; import { useTranslation } from "next-i18next"; +import Avatar from "components/avatar"; +import InfoTooltip from "components/info-tooltip"; + import { formatNumberToString } from "helpers/formatNumber"; import { DistributedAmounts } from "interfaces/proposal"; -import Avatar from "./avatar"; -import InfoTooltip from "./info-tooltip"; - interface IProposalProgressProps { distributedAmounts: DistributedAmounts; } diff --git a/components/proposal-pullrequest-details.tsx b/components/proposal-pullrequest-details.tsx index b3b81018b4..fbae337425 100644 --- a/components/proposal-pullrequest-details.tsx +++ b/components/proposal-pullrequest-details.tsx @@ -1,13 +1,19 @@ import { useTranslation } from "next-i18next"; +import Avatar from "components/avatar"; +import DateLabel from "components/date-label"; +import GithubInfo from "components/github-info"; +import PullRequestLabels from "components/pull-request-labels"; +import Translation from "components/translation"; + +import { useAppState } from "contexts/app-state"; + import { pullRequest } from "interfaces/issue-data"; -import { useAppState } from "../contexts/app-state"; -import Avatar from "./avatar"; -import DateLabel from "./date-label"; -import GithubInfo from "./github-info"; -import PullRequestLabels from "./pull-request-labels"; -import Translation from "./translation"; +import { useNetwork } from "x-hooks/use-network"; + +import If from "./If"; +import InternalLink from "./internal-link"; interface IProposalPRDetailsProps { currentPullRequest: pullRequest; @@ -16,7 +22,9 @@ export default function ProposalPullRequestDetail({ currentPullRequest, }: IProposalPRDetailsProps) { const { t } = useTranslation("pull-request"); + const { state } = useAppState(); + const { getURLWithNetwork } = useNetwork(); return ( <> @@ -24,9 +32,19 @@ export default function ProposalPullRequestDetail({ {t("pull-request:label")} - - #{currentPullRequest?.githubId} - + + +
- - {(state.currentBounty?.data?.repository && ( + + + - )) || - ""} - + + - + : - :{currentPullRequest?.userBranch} + {currentPullRequest?.userBranch} diff --git a/components/pull-request-hero.tsx b/components/pull-request-hero.tsx index 6a78aefe8d..3bfa5e7daf 100644 --- a/components/pull-request-hero.tsx +++ b/components/pull-request-hero.tsx @@ -4,16 +4,16 @@ import {useRouter} from "next/router"; import ArrowLeft from "assets/icons/arrow-left"; import Avatar from "components/avatar"; +import CustomContainer from "components/custom-container"; +import DateLabel from "components/date-label"; import GithubInfo from "components/github-info"; +import PriceConversor from "components/price-conversor"; -import {pullRequest} from "interfaces/issue-data"; +import {useAppState} from "contexts/app-state"; -import useNetworkTheme from "x-hooks/use-network-theme"; +import {pullRequest} from "interfaces/issue-data"; -import {useAppState} from "../contexts/app-state"; -import CustomContainer from "./custom-container"; -import DateLabel from "./date-label"; -import PriceConversor from "./price-conversor"; +import { useNetwork } from "x-hooks/use-network"; interface IPullRequestHeroProps { currentPullRequest: pullRequest; @@ -24,8 +24,8 @@ export default function PullRequestHero({currentPullRequest}: IPullRequestHeroPr const router = useRouter(); - const { getURLWithNetwork } = useNetworkTheme() const { state } = useAppState(); + const { getURLWithNetwork } = useNetwork(); return (
@@ -86,7 +86,7 @@ export default function PullRequestHero({currentPullRequest}: IPullRequestHeroPr
diff --git a/components/read-only-container.tsx b/components/read-only-container.tsx index f1c533b530..c00444fd54 100644 --- a/components/read-only-container.tsx +++ b/components/read-only-container.tsx @@ -1,10 +1,15 @@ +import {useRouter} from "next/router"; + import { useAppState } from "contexts/app-state"; export default function ReadOnlyContainer({ children }) { const { state } = useAppState(); + const { pathname } = useRouter(); + + const isOnNetwork = pathname?.includes("[network]"); return( -
+
{children}
); diff --git a/components/select-network-dropdown.tsx b/components/select-network-dropdown.tsx new file mode 100644 index 0000000000..00edae90e2 --- /dev/null +++ b/components/select-network-dropdown.tsx @@ -0,0 +1,134 @@ +import React, {useEffect, useState, ReactNode} from "react"; + +import {useTranslation} from "next-i18next"; + +import IconOption from "components/icon-option"; +import IconSingleValue from "components/icon-single-value"; +import Indicator from "components/indicator"; +import ReactSelect from "components/react-select"; + +import {useAppState} from "contexts/app-state"; + +import {SupportedChainData} from "interfaces/supported-chain-data"; + +interface SelectNetworkDropdownProps { + onSelect: (chain: SupportedChainData) => void; + defaultChain?: SupportedChainData; + isOnNetwork?: boolean; + className?: string; + isDisabled?: boolean; + placeHolder?: string; +} + +interface ChainOption { + label: string; + value: SupportedChainData | Partial; + preIcon: ReactNode; + isDisabled?: boolean; + tooltip?: string; +} + +export default function SelectNetworkDropdown({ + defaultChain, + isOnNetwork, + className = "text-uppercase", + onSelect, + isDisabled, + placeHolder +}: SelectNetworkDropdownProps) { + const { t } = useTranslation("common"); + + const [options, setOptions] = useState([]); + const [selected, setSelectedChain] = useState(null); + + const { state: { Service, supportedChains, connectedChain, currentUser, spinners } } = useAppState(); + + function chainToOption(chain: SupportedChainData | Partial, isDisabled?: boolean): ChainOption { + return { + value: chain, + label: chain.chainShortName, + preIcon: (), + isDisabled, + tooltip: isDisabled + ? "Not available on this chain" + : chain?.chainShortName?.length > 12 + ? chain.chainShortName + : undefined, + }; + } + + function isChainConfigured({ registryAddress }: SupportedChainData) { + return currentUser?.isAdmin || !!registryAddress; + } + + async function selectSupportedChain({value}) { + const chain = supportedChains?.find(({ chainId }) => +chainId === +value.chainId); + + if (!chain || chain?.chainId === selected?.value?.chainId) + return; + + onSelect(chain); + setSelectedChain(chainToOption(chain)); + } + + function updateSelectedChainMatchConnected() { + let chain = undefined; + + if (isOnNetwork && Service?.network?.active?.chain) + chain = Service?.network?.active?.chain; + else + chain = + options?.find(({ value: { chainId } }) => chainId === +(defaultChain?.chainId || connectedChain.id))?.value; + + if (!chain) { + return; + } + + sessionStorage.setItem("currentChainId", chain.chainId.toString()); + + setSelectedChain(chainToOption(chain)); + } + + function updateOptions() { + if (!supportedChains || (isOnNetwork && !Service?.network?.availableChains)) return; + + const configuredChains = supportedChains.filter(isChainConfigured); + + if (isOnNetwork) + setOptions(configuredChains.map(chain => + chainToOption(chain, !Service?.network?.availableChains?.find(({ chainId }) => chainId === chain.chainId)))); + else + setOptions(configuredChains.map(chain => chainToOption(chain))); + } + + useEffect(updateOptions, [ + isOnNetwork, + Service?.network?.availableChains, + supportedChains, + currentUser?.isAdmin + ]); + + useEffect(updateSelectedChainMatchConnected, [ + options, + Service?.network?.active?.chain, + connectedChain?.id, + spinners + ]); + + return( +
+ +
+ ); +} \ No newline at end of file diff --git a/components/seo.tsx b/components/seo.tsx index 2d9b225214..99f173da2b 100644 --- a/components/seo.tsx +++ b/components/seo.tsx @@ -2,13 +2,12 @@ import React, {useEffect, useState} from "react"; import removeMarkdown from "markdown-to-text"; import {DefaultSeo, NextSeo} from "next-seo"; +import SEO_CONFIG from "next-seo-config"; import getConfig from "next/config"; -import { useRouter } from "next/router"; -import {IssueBigNumberData, IssueData} from "interfaces/issue-data"; +import {useAppState} from "contexts/app-state"; -import {useAppState} from "../contexts/app-state"; -import SEO_CONFIG from "../next-seo-config"; +import {IssueBigNumberData, IssueData} from "interfaces/issue-data"; const { publicRuntimeConfig } = getConfig(); @@ -17,13 +16,14 @@ interface ISeoProps { } const Seo: React.FC = () => { - const {query} = useRouter(); const {state} = useAppState(); const [issueMeta, setIssueMeta] = useState(null); - useEffect(() => { setIssueMeta(state.currentBounty?.data)}, [state.currentBounty?.data]); + useEffect(() => { + setIssueMeta(state.currentBounty?.data); + }, [state.currentBounty?.data]); - if (issueMeta && query?.id && query?.repoId) { + if (issueMeta?.issueId) { // eslint-disable-next-line no-unsafe-optional-chaining const homeUrl = publicRuntimeConfig?.urls?.home; // eslint-disable-next-line no-unsafe-optional-chaining diff --git a/components/setup/add-chain-modal.tsx b/components/setup/add-chain-modal.tsx new file mode 100644 index 0000000000..cbe0a6e914 --- /dev/null +++ b/components/setup/add-chain-modal.tsx @@ -0,0 +1,99 @@ +import React, {useState} from "react"; +import {Col, FormControl, Row} from "react-bootstrap"; + +import ColorInput from "components/color-input"; +import Modal from "components/modal"; +import ReactSelect from "components/react-select"; + +import {MiniChainInfo} from "interfaces/mini-chain"; + +interface AddChainModalProps { + chain: MiniChainInfo; + show: boolean; + add: (chain: MiniChainInfo) => void; +} + +export default function AddChainModal({ + chain, + show, + add +}: AddChainModalProps) { + if (!show) + return <> + + const [activeRPC, setActiveRPC] = useState(chain?.rpc?.[0]); + const [eventsApi, setEventsApi] = useState(''); + const [explorer, setExplorer] = useState(''); + const [color, setColor] = useState(''); + + function validUrl(url: string) { + try { return new URL(url)?.protocol?.search(/https?:/) > -1} + catch { return false; } + } + + return add({...chain, activeRPC, eventsApi, explorer, color})} + okDisabled={!validUrl(activeRPC) || !validUrl(eventsApi) || !validUrl(explorer)} + onCloseClick={() => add(null)}> + + {chain?.name || chain?.shortName} + + Currency + + {chain?.nativeCurrency?.name} + {chain?.nativeCurrency?.symbol} + + + Select RPC + + + ({value: s, label: s}))} + onChange={e => {setActiveRPC(e.value)}}> + + + Configure RPC + + + + setActiveRPC(e?.target?.value)} /> + + + + Configure Events Api + + + + setEventsApi(e?.target?.value)} /> + + + + Configure Explorer + + + + setExplorer(e?.target?.value)} /> + + + + + + + + +} \ No newline at end of file diff --git a/components/setup/add-custom-chain-modal.tsx b/components/setup/add-custom-chain-modal.tsx new file mode 100644 index 0000000000..23a9db2845 --- /dev/null +++ b/components/setup/add-custom-chain-modal.tsx @@ -0,0 +1,87 @@ +import {Dispatch, SetStateAction, useState} from "react"; +import {Col, Row} from "react-bootstrap"; + +import ColorInput from "components/color-input"; +import {FormGroup} from "components/form-group"; +import Modal from "components/modal"; + +import {MiniChainInfo} from "interfaces/mini-chain"; + +interface AddCustomChainModalProps { + show: boolean; + add: (chain?: MiniChainInfo) => void; +} + +export default function AddCustomChainModal({show, add}: AddCustomChainModalProps) { + + const [chainId, setChainId] = useState(''); + const [activeRPC, setActiveRPC] = useState(''); + const [explorer, setExplorer] = useState(''); + const [name, setName] = useState('') + const [shortName, setShortName] = useState(''); + const [currencySymbol, setCurrencySymbol] = useState(''); + const [currencyName, setCurrencyName] = useState(''); + const [currencyDecimals, setCurrencyDecimals] = useState(''); + const [eventsApi, setEventsApi] = useState(''); + const [color, setColor] = useState('#4250e4'); + + function getChainModel(): MiniChainInfo { + return { + name, + shortName, + chainId: +chainId, + activeRPC, + explorer, + eventsApi, + color, + nativeCurrency: { + name: currencyName, + symbol: currencySymbol, + decimals: +currencyDecimals + }, + rpc: [], + networkId: 0 + } + } + + const forms: [string, string, string, Dispatch>][] = [ + ['chain id', '1', chainId, setChainId], + ['chain name', 'Ethereum Mainnet', name, setName], + ['chain short name', 'Ethereum', shortName, setShortName], + ['chain currency name', 'Ether', currencyName, setCurrencyName], + ['chain currency symbol', 'ETH', currencySymbol, setCurrencySymbol], + ['chain currency decimals', '18', currencyDecimals, setCurrencyDecimals], + ['chain rpc', 'https://', activeRPC, setActiveRPC], + ['chain explorer', 'https://', explorer, setExplorer], + ['chain events api', 'https://', eventsApi, setEventsApi], + ] + + function makeRowColInput([label, placeholder, value, onChange]) { + return
+ + + onChange(nv)} /> + + +
+ } + + return !v)} + onOkClick={() => add(getChainModel())} + onCloseClick={() => add(null)}> + + <> + {forms.map(makeRowColInput)} + + + + +} \ No newline at end of file diff --git a/components/setup/call-to-action.tsx b/components/setup/call-to-action.tsx index 3a7a564436..df8e281778 100644 --- a/components/setup/call-to-action.tsx +++ b/components/setup/call-to-action.tsx @@ -1,5 +1,6 @@ import Button from "components/button"; -import { ContextualSpan } from "components/contextual-span"; +import {ContextualSpan} from "components/contextual-span"; +import ContractButton from "components/contract-button"; export function CallToAction({ call, @@ -7,8 +8,11 @@ export function CallToAction({ onClick, color, disabled, - executing + executing, + isContractAction = false }) { + const BtnComponent = isContractAction ? ContractButton : Button; + return( {call} - + ); } \ No newline at end of file diff --git a/components/setup/chains-setup.tsx b/components/setup/chains-setup.tsx new file mode 100644 index 0000000000..c09ff268d0 --- /dev/null +++ b/components/setup/chains-setup.tsx @@ -0,0 +1,175 @@ +import React, {useEffect, useState} from "react"; +import {Col, FormControl, InputGroup, Row} from "react-bootstrap"; + +import axios from "axios"; +import {useTranslation} from "next-i18next"; + +import CloseIcon from "assets/icons/close-icon"; +import LoadingDots from "assets/icons/loading-dots"; +import PlusIcon from "assets/icons/plus-icon"; +import SearchIcon from "assets/icons/search-icon"; + +import Button from "components/button"; +import {ContextualSpan} from "components/contextual-span"; +import AddChainModal from "components/setup/add-chain-modal"; +import AddCustomChainModal from "components/setup/add-custom-chain-modal"; + +import {useAppState} from "contexts/app-state"; +import {changeLoadState} from "contexts/reducers/change-load"; +import {toastError, toastSuccess} from "contexts/reducers/change-toaster"; + +import {MiniChainInfo} from "interfaces/mini-chain"; + +import useApi from "x-hooks/use-api"; +import { useAuthentication } from "x-hooks/use-authentication"; + +export default function ChainsSetup() { + const { t } = useTranslation(["common"]); + + const [search, setSearch] = useState(''); + const [chains, setChains] = useState([]); + const [existingState, setExistingState] = useState([]); + const [showCustomAdd, setShowCustomAdd] = useState(false); + const [filteredChains, setFilteredChains] = useState([]); + const [showChainModal, setShowChainModal] = useState(null); + + const api = useApi(); + const {state, dispatch} = useAppState(); + const { signMessage } = useAuthentication(); + + function updateMiniChainInfo() { + if (chains.length) + return; + + dispatch(changeLoadState(true)) + axios.get(`https://chainid.network/chains_mini.json`) + .then(({data}) => data) + .then(setChains) + .catch(e => { + console.error(`Failed to grep chain_mini`, e); + return []; + }) + .finally(() => { + dispatch(changeLoadState(false)) + }) + } + + function handleSearch() { + + const lookFor = (t) => t.toLowerCase().search(search.toLowerCase()) > -1; + + const filter = chains.filter(chain => + lookFor(chain.name) || lookFor(chain.shortName) || lookFor(chain.nativeCurrency?.symbol)) + + setFilteredChains(filter); + } + + function changeExistingState() { + setExistingState(state?.supportedChains?.map(({chainId}) => chainId)); + } + + function addChain(chain: MiniChainInfo) { + if (!chain) { + setShowChainModal(null); + setShowCustomAdd(false); + return; + } + + signMessage() + .then(() => { + api.addSupportedChain(chain) + .then(success => { + if (success) { + dispatch(toastSuccess(`added chain ${chain.name}`)); + setShowChainModal(null); + } else + dispatch(toastError(`Failed to add chain ${chain.name}`)); + }); + }) + .catch(() => { + dispatch(toastError(`Failed to add chain ${chain.name}`)); + }); + } + + function makeAddRemoveButton(chain: MiniChainInfo) { + const exists = existingState?.includes(chain.chainId); + + if (chain.loading) + return ; + + return + } + + function makeChainRow(chain: MiniChainInfo) { + if (!chain.rpc.length) + return
; + + return + + + {chain?.name || chain?.shortName} + + + {chain?.nativeCurrency?.name} + {chain?.nativeCurrency?.symbol} + + {makeAddRemoveButton(chain)} + + + + + } + + useEffect(updateMiniChainInfo, []) + useEffect(handleSearch, [search]) + useEffect(changeExistingState, [state?.supportedChains]); + + return <> +
+
+
+ + e?.key === "Enter" ? handleSearch() : null}> + + + + setSearch(e?.target?.value)} /> + + + + +
+
+ {!filteredChains.length && search && <> + + + No chains containing {search} + + + + + + + + + } +
+ {((search.length > 0 ? filteredChains : chains).map(makeChainRow))} + + +
+ + +} \ No newline at end of file diff --git a/components/setup/connect-github-setup.tsx b/components/setup/connect-github-setup.tsx new file mode 100644 index 0000000000..8997908f7f --- /dev/null +++ b/components/setup/connect-github-setup.tsx @@ -0,0 +1,32 @@ +import {useState} from "react"; +import {Col, Row} from "react-bootstrap"; + +import {useAppState} from "../../contexts/app-state"; +import {useAuthentication} from "../../x-hooks/use-authentication"; +import GithubConnectionState from "../github-connection-state"; +import {RemoveGithubAccount} from "../profile/remove-github-modal"; + +export default function ConnectGithubSetup() { + + const {state} = useAppState(); + const [showRemoveModal, setShowRemoveModal] = useState(false); + const {disconnectGithub} = useAuthentication() + + + return
+ + + setShowRemoveModal(true)} /> + + + + setShowRemoveModal(false)} + disconnectGithub={disconnectGithub} + /> + +
+} \ No newline at end of file diff --git a/components/setup/contract-input.tsx b/components/setup/contract-input.tsx index 06260fa1e5..d8e87db12d 100644 --- a/components/setup/contract-input.tsx +++ b/components/setup/contract-input.tsx @@ -1,17 +1,18 @@ -import { SetStateAction } from "react"; -import { Col } from "react-bootstrap"; +import {SetStateAction} from "react"; +import {Col} from "react-bootstrap"; -import { useTranslation } from "next-i18next"; +import {useTranslation} from "next-i18next"; +import { isAddress } from "web3-utils"; -import Button from "components/button"; -import { ExternalLink } from "components/external-link"; -import { FormGroup } from "components/form-group"; +import ContractButton from "components/contract-button"; +import {ExternalLink} from "components/external-link"; +import {FormGroup} from "components/form-group"; -import { useAppState } from "contexts/app-state"; +import {useAppState} from "contexts/app-state"; export interface ContractField { value: string; - validated: boolean; + validated: boolean|null; } interface ContractInputProps { @@ -21,6 +22,8 @@ interface ContractInputProps { onChange?: (value: SetStateAction) => void; docsLink: string; readOnly?: boolean; + mustBeAddress?: boolean; + decimalScale?: number; action?: { disabled: boolean; executing: boolean; @@ -36,13 +39,14 @@ export function ContractInput({ docsLink, action, validator, - readOnly + readOnly, + mustBeAddress } : ContractInputProps) { const { t } = useTranslation(["common", "setup"]); const { state: { Service } } = useAppState(); function isInvalid(validated, name) { - return validated === false ? t("setup:errors.invalid-contract-address", { contract: name }) : undefined; + return validated === false ? t("setup:registry.errors.invalid-contract-address", { contract: name }) : ""; } function handleChange(value: string) { @@ -50,16 +54,22 @@ export function ContractInput({ } function validateContractField() { - if (!validator) return; const { value } = field; + if (mustBeAddress) + return onChange(previous => + ({ ...previous, validated: value?.trim().length ? isAddress(field?.value) : null })); + + if (!validator) + return; + if (!Service?.active || value.trim() === "") - return onChange(previous => ({ ...previous, validated: undefined })); + return onChange(previous => ({ ...previous, validated: null })); - if (!Service.active.isAddress(value)) + if (!isAddress(value)) return onChange(previous => ({ ...previous, validated: false })); - + Service.active[validator](value) .then(loaded => onChange(previous => ({ ...previous, validated: !!loaded }))); } @@ -84,14 +94,14 @@ export function ContractInput({ { action && - + } diff --git a/components/setup/deploy-erc20-modal.tsx b/components/setup/deploy-erc20-modal.tsx index 3e8b6c509e..3b5a757484 100644 --- a/components/setup/deploy-erc20-modal.tsx +++ b/components/setup/deploy-erc20-modal.tsx @@ -3,11 +3,17 @@ import { useTranslation } from "next-i18next"; import { ERC20Details } from "components/custom-network/erc20-details"; import Modal from "components/modal"; +interface DeployERC20ModalProps { + show?: boolean; + handleHide: () => void; + onChange: (value: string, validated?: boolean) => void; +} + export function DeployERC20Modal({ show = false, handleHide, onChange -}) { +}: DeployERC20ModalProps) { const { t } = useTranslation("setup"); function handleChange(value: string) { diff --git a/components/setup/network-setup.tsx b/components/setup/network-setup.tsx index ea6fbdf13a..f51be592d9 100644 --- a/components/setup/network-setup.tsx +++ b/components/setup/network-setup.tsx @@ -1,9 +1,12 @@ -import { useTranslation } from "next-i18next"; +import {useTranslation} from "next-i18next"; -import { ContextualSpan } from "components/contextual-span"; -import { NewNetworkStepper } from "components/custom-network/new-network-stepper"; +import {ContextualSpan} from "components/contextual-span"; +import {NewNetworkStepper} from "components/custom-network/new-network-stepper"; +import If from "components/If"; -import { Network } from "interfaces/network"; +import {useAppState} from "contexts/app-state"; + +import {Network} from "interfaces/network"; interface NetworkSetupProps { isVisible?: boolean; @@ -15,18 +18,22 @@ export function NetworkSetup({ defaultNetwork } : NetworkSetupProps) { const { t } = useTranslation("setup"); - + + const {state} = useAppState(); + if (!isVisible) return <>; return(
- { !!defaultNetwork && - - {t("network.errors.network-already-saved", { network: defaultNetwork?.name })} - || - - } + + {t("network.errors.network-already-saved", {network: defaultNetwork?.name})} + + } + otherwise={} + />
); } \ No newline at end of file diff --git a/components/setup/registry-setup.tsx b/components/setup/registry-setup.tsx index 634737b02e..81caa540ab 100644 --- a/components/setup/registry-setup.tsx +++ b/components/setup/registry-setup.tsx @@ -2,10 +2,12 @@ import {useEffect, useState} from "react"; import {Col, Row} from "react-bootstrap"; import {TransactionReceipt} from "@taikai/dappkit/dist/src/interfaces/web3-core"; +import {isZeroAddress} from "ethereumjs-util"; import {useTranslation} from "next-i18next"; +import {isAddress} from "web3-utils"; -import Button from "components/button"; import {ContextualSpan} from "components/contextual-span"; +import ContractButton from "components/contract-button"; import {FormGroup} from "components/form-group"; import {CallToAction} from "components/setup/call-to-action"; import {ContractField, ContractInput} from "components/setup/contract-input"; @@ -13,9 +15,12 @@ import {DeployBountyTokenModal} from "components/setup/deploy-bounty-token-modal import {DeployERC20Modal} from "components/setup/deploy-erc20-modal"; import {useAppState} from "contexts/app-state"; -import {toastError, toastSuccess} from "contexts/reducers/change-toaster"; +import {toastError, toastInfo, toastSuccess} from "contexts/reducers/change-toaster"; + +import {SupportedChainData} from "interfaces/supported-chain-data"; import useApi from "x-hooks/use-api"; +import { useAuthentication } from "x-hooks/use-authentication"; import useBepro from "x-hooks/use-bepro"; import {useSettings} from "x-hooks/use-settings"; @@ -36,7 +41,7 @@ enum ModalKeys { const defaultContractField: ContractField = { value: "", - validated: undefined + validated: null }; export function RegistrySetup({ @@ -59,17 +64,19 @@ export function RegistrySetup({ const [bountyTokenDispatcher, setBountyTokenDispatcher] = useState(); const [lockAmountForNetworkCreation, setLockAmountForNetworkCreation] = useState(""); const [networkCreationFeePercentage, setNetworkCreationFeePercentage] = useState(""); + const [registrySaveCTA, setRegistrySaveCTA] = useState(false); const { loadSettings } = useSettings(); - const { saveNetworkRegistry, processEvent } = useApi(); - const { dispatch, state: { currentUser, Service } } = useAppState(); + const { signMessage } = useAuthentication(); const { handleDeployRegistry, handleSetDispatcher, handleChangeAllowedTokens } = useBepro(); + const { patchSupportedChain, processEvent, updateChainRegistry, getSupportedChains } = useApi(); + const { dispatch, state: { currentUser, Service, connectedChain, supportedChains } } = useAppState(); function isEmpty(value: string) { return value.trim() === ""; } - const hasRegistryAddress = !!registryAddress; + const hasRegistryAddress = !!registryAddress && isAddress(registryAddress) && !isZeroAddress(registryAddress); const needToSetDispatcher = hasRegistryAddress && bountyTokenDispatcher && registryAddress !== bountyTokenDispatcher; const isDeployRegistryBtnDisabled = [ @@ -126,13 +133,15 @@ export function RegistrySetup({ closeFeePercentage, cancelFeePercentage, bountyToken.value ) - .then(tx => { + .then(async tx => { const { contractAddress } = tx as TransactionReceipt; setRegistry(previous => ({ ...previous, value: contractAddress})); Service?.active?.loadRegistry(false, contractAddress); - return saveNetworkRegistry(currentUser?.walletAddress, contractAddress); + await signMessage(); + + return setChainRegistry(contractAddress); }) .then(() => { loadSettings(true); @@ -145,46 +154,46 @@ export function RegistrySetup({ .finally(() => setisDeployingRegistry(false)); } - function updateData() { + function updateData(forcedValue?: string) { const updatedValue = value => ({ ...defaultContractField, value }); - setRegistry(updatedValue(registryAddress)); - - Service.active.loadRegistry(true, registryAddress) - .then(loaded => { - if (!loaded) throw new Error("Not Loaded"); - - setErc20(updatedValue(loaded.token.contractAddress)); - setBountyToken(updatedValue(loaded.bountyToken.contractAddress)); - - const getParameterWithoutProxy = param => loaded.callTx(loaded.contract.methods[param]()); - - return Promise.all([ - loaded.treasury(), - loaded.lockAmountForNetworkCreation(), - loaded.networkCreationFeePercentage(), - getParameterWithoutProxy("closeFeePercentage"), - getParameterWithoutProxy("cancelFeePercentage"), - loaded.bountyToken.dispatcher(), - loaded.divisor, - loaded.token.contractAddress, - loaded.getAllowedTokens() - ]) - }) + if (!isAddress(forcedValue || registryAddress) || isZeroAddress(forcedValue || registryAddress)) + return; + + setRegistry(updatedValue(forcedValue || registryAddress)); + + const registryObj = Service?.active?.registry; + + setErc20(updatedValue(registryObj.token.contractAddress)); + setBountyToken(updatedValue(registryObj.bountyToken.contractAddress)); + + const getParameterWithoutProxy = param => registryObj.callTx(registryObj.contract.methods[param]()); + + Promise.all([ + registryObj.treasury(), + registryObj.lockAmountForNetworkCreation(), + registryObj.networkCreationFeePercentage(), + getParameterWithoutProxy("closeFeePercentage"), + getParameterWithoutProxy("cancelFeePercentage"), + registryObj.bountyToken.dispatcher(), + registryObj.divisor, + registryObj.token.contractAddress, + registryObj.getAllowedTokens() + ]) .then(parameters => { setTreasury(parameters[0].toString()); setLockAmountForNetworkCreation(parameters[1].toString()); setNetworkCreationFeePercentage((+parameters[2] * 100).toString()); // networkCreationFeePercentage is aready dived per divisor on sdk setCloseFeePercentage((+parameters[3]/+parameters[6]).toString()); setCancelFeePercentage((+parameters[4]/+parameters[6]).toString()); - setBountyTokenDispatcher(parameters[5].toString()); + setBountyTokenDispatcher(parameters[5].toString().toLowerCase()); const transactional = !!parameters[8].transactional.find(address => parameters[7] = address); const reward = !!parameters[8].reward.find(address => parameters[7] = address); setIsErc20Allowed({ transactional, reward }); }) - .catch(console.debug); + .catch(console.debug); } function setDispatcher() { @@ -224,16 +233,87 @@ export function RegistrySetup({ allowToken(false); } + function setChainRegistry(address = registryAddress) { + const chain = supportedChains?.find(({chainId}) => chainId === +connectedChain?.id); + if (!chain || !address) + return; + + if (!isAddress(address)) { + dispatch(toastInfo('Registry address value must be an address; Can be 0x0')); + return; + } + + return updateChainRegistry({...chain, registryAddress: address}) + .then(result => { + if (!result) { + dispatch(toastError(`Failed to update chain ${chain.chainId} with ${address}`)); + return; + } + dispatch(toastSuccess(`Updated chain ${chain.chainId} with ${address} `)) + return getSupportedChains(true); + }) + } + + function _setRegistry(val) { + const value = val(registry); + setRegistry(value); + + if (!hasRegistryAddress && value.validated) + updateData(value.value); + } + + function _patchSupportedChain(data: Partial) { + const chain = supportedChains?.find(c => +connectedChain.id === c.chainId); + if (!chain) + return; + + return patchSupportedChain(chain, data) + .then(result => { + if (result) + dispatch(toastSuccess('updated chain registry')); + else dispatch(toastError('failed to update chain registry')); + }) + } + + function setDefaults() { + setErc20(defaultContractField); + setRegistry(defaultContractField); + setCloseFeePercentage(""); + setCancelFeePercentage(""); + setIsErc20Allowed(undefined); + setBountyToken(defaultContractField); + setBountyTokenDispatcher(""); + setLockAmountForNetworkCreation(""); + setNetworkCreationFeePercentage(""); + } + useEffect(() => { - if (!registryAddress || !Service?.active || !isVisible) return; + if (!registryAddress || !Service?.active?.registry?.contractAddress || !isVisible) return; updateData(); - }, [registryAddress, Service?.active]); + }, [registryAddress, Service?.active?.registry?.contractAddress, isVisible]); useEffect(() => { if (currentUser?.walletAddress) setTreasury(currentUser?.walletAddress); }, [currentUser?.walletAddress]); + useEffect(() => { + if (!supportedChains?.length || !connectedChain?.id) + return; + + const chain = supportedChains.find(({chainId}) => chainId === +connectedChain?.id); + + if (!chain) + return; + + setRegistrySaveCTA(chain?.registryAddress ? false : !!registryAddress); + }, [connectedChain, supportedChains, registryAddress]); + + useEffect(() => { + if (connectedChain?.id && !registryAddress) + setDefaults(); + }, [connectedChain?.id, registryAddress]); + return(
{ hasRegistryAddress && @@ -254,6 +334,7 @@ export function RegistrySetup({ color="warning" disabled={!needToSetDispatcher} executing={isSettingDispatcher} + isContractAction /> } @@ -265,6 +346,7 @@ export function RegistrySetup({ color="info" disabled={!!isErc20Allowed?.transactional || !!isAllowingToken} executing={isAllowingToken === "transactional"} + isContractAction /> } @@ -276,15 +358,29 @@ export function RegistrySetup({ color="info" disabled={!!isErc20Allowed?.reward || !!isAllowingToken} executing={isAllowingToken === "reward"} + isContractAction /> } + { + registrySaveCTA + ? + : '' + } + _patchSupportedChain({registryAddress: registry.value}) } : null + } /> @@ -351,6 +447,7 @@ export function RegistrySetup({ value={networkCreationFeePercentage} onChange={setNetworkCreationFeePercentage} variant="numberFormat" + decimalScale={7} description={t("registry.fields.network-creation-fee.description")} error={exceedsFeesLimitsError(networkCreationFeePercentage)} readOnly={hasRegistryAddress} @@ -363,6 +460,7 @@ export function RegistrySetup({ value={closeFeePercentage} onChange={setCloseFeePercentage} variant="numberFormat" + decimalScale={7} description={t("registry.fields.close-bounty-fee.description")} error={exceedsFeesLimitsError(closeFeePercentage)} readOnly={hasRegistryAddress} @@ -375,6 +473,7 @@ export function RegistrySetup({ value={cancelFeePercentage} onChange={setCancelFeePercentage} variant="numberFormat" + decimalScale={7} description={t("registry.fields.cancel-bounty-fee.description")} error={exceedsFeesLimitsError(cancelFeePercentage)} readOnly={hasRegistryAddress} @@ -383,14 +482,14 @@ export function RegistrySetup({ - + diff --git a/components/tabbed-navigation.tsx b/components/tabbed-navigation.tsx index 3e3536185d..b7db1ba735 100644 --- a/components/tabbed-navigation.tsx +++ b/components/tabbed-navigation.tsx @@ -1,12 +1,5 @@ -import { useEffect, useState } from "react"; -import { - Accordion, - Nav, - Tab, - OverlayTrigger, - Popover, - useAccordionButton -} from "react-bootstrap"; +import {useEffect, useState} from "react"; +import {Accordion, Nav, OverlayTrigger, Popover, Tab, useAccordionButton} from "react-bootstrap"; import ArrowDown from "assets/icons/arrow-down"; import ArrowUp from "assets/icons/arrow-up"; @@ -14,7 +7,7 @@ import InfoIconEmpty from "assets/icons/info-icon-empty"; import Button from "components/button"; -import { TabbedNavigationProps } from "interfaces/tabbed-navigation"; +import {TabbedNavigationProps} from "interfaces/tabbed-navigation"; function renderDescription(description: string) { if (!description) return <>; @@ -46,6 +39,7 @@ export default function TabbedNavigation({ tabs, defaultActiveKey, onTransition, + forceActiveKey, ...props }: TabbedNavigationProps) { const [collapsed, setCollapsed] = useState(false); @@ -65,7 +59,11 @@ export default function TabbedNavigation({ useEffect(() => { if (!defaultActiveKey) setActiveKey(getDefaultActiveTab()); - }, []); + }, [tabs]); + + useEffect(() => { + setActiveKey(forceActiveKey); + }, [forceActiveKey]) return ( diff --git a/components/token-icon.tsx b/components/token-icon.tsx new file mode 100644 index 0000000000..610bc616ce --- /dev/null +++ b/components/token-icon.tsx @@ -0,0 +1,14 @@ +import TokenIconPlaceholder from "assets/icons/token-icon-placeholder"; + +interface TokenIconProps { + src?: string; +} + +export default function TokenIcon({ + src +} : TokenIconProps) { + if (src) + return + + return ; +} \ No newline at end of file diff --git a/components/tokens-dropdown.tsx b/components/tokens-dropdown.tsx index b44f897d50..f8faee6055 100644 --- a/components/tokens-dropdown.tsx +++ b/components/tokens-dropdown.tsx @@ -105,9 +105,9 @@ export default function TokensDropdown({ useEffect(() => { if (!tokens?.length) return; - if (needsBalance) getBalanceTokens(); + if (needsBalance && state.connectedChain?.matchWithNetworkChain) getBalanceTokens(); else setOptions(tokens.map(tokenToOption)); - }, [tokens]); + }, [tokens, state.connectedChain?.matchWithNetworkChain]); useEffect(() => { if(!!option || !options?.length) return; diff --git a/components/transactions-list.tsx b/components/transactions-list.tsx index 57d87182d7..586e1c464c 100644 --- a/components/transactions-list.tsx +++ b/components/transactions-list.tsx @@ -22,10 +22,10 @@ interface TransactionListProps { } export default function TransactionsList({onActiveTransactionChange}: TransactionListProps) { - const {dispatch, state: {transactions}} = useAppState(); - const {t} = useTranslation("common"); - const {deleteFromStorage} = useTransactions(); - + const { t } = useTranslation("common"); + + const { deleteFromStorage } = useTransactions(); + const { dispatch, state: { transactions } } = useAppState(); function renderTransactionRow(item: Transaction) { const className = "h-100 w-100 px-3 py-2 tx-row mt-2 cursor-pointer"; @@ -51,7 +51,7 @@ export default function TransactionsList({onActiveTransactionChange}: Transactio
- {(item.amount > 0 && ( + {(+item.amount > 0 && ( {formatStringToCurrency(amount)} {item.currency} @@ -84,7 +84,6 @@ export default function TransactionsList({onActiveTransactionChange}: Transactio

{t("transactions.title_other")}

- { transactions.length && +
diff --git a/components/update-bounty-amount-modal.tsx b/components/update-bounty-amount-modal.tsx index c3c9bd4508..8a4824f297 100644 --- a/components/update-bounty-amount-modal.tsx +++ b/components/update-bounty-amount-modal.tsx @@ -4,6 +4,7 @@ import BigNumber from "bignumber.js"; import {useTranslation} from "next-i18next"; import Button from "components/button"; +import ContractButton from "components/contract-button"; import InputNumber from "components/input-number"; import Modal from "components/modal"; @@ -18,10 +19,10 @@ import { useBounty } from "x-hooks/use-bounty"; import useERC20 from "x-hooks/use-erc20"; export default function UpdateBountyAmountModal({ - show, - transactionalAddress, + show, + transactionalAddress, handleClose = undefined, - bountyId, + bountyId }) { const { t } = useTranslation("common"); @@ -34,8 +35,8 @@ export default function UpdateBountyAmountModal({ const { processEvent } = useApi(); const transactionalERC20 = useERC20(); + const { getDatabaseBounty } = useBounty(); const { handleApproveToken, handleUpdateBountyAmount } = useBepro(); - const {getDatabaseBounty, getChainBounty} = useBounty(); const handleChange = params => setNewAmount(BigNumber(params.value)); @@ -72,8 +73,7 @@ export default function UpdateBountyAmountModal({ }); }) .then(() => { - getDatabaseBounty(true) - getChainBounty(true) + getDatabaseBounty(true); resetValues(); handleClose(); }) @@ -109,14 +109,14 @@ export default function UpdateBountyAmountModal({ > {t("actions.approve")} : - + }
)} diff --git a/components/web3-dialog.tsx b/components/web3-dialog.tsx index 6c6856839d..415bf16616 100644 --- a/components/web3-dialog.tsx +++ b/components/web3-dialog.tsx @@ -8,22 +8,23 @@ import {useRouter} from "next/router"; import WebThreeUnavailable from "assets/web3-unavailable"; +import Button from "components/button"; import MobileNotSupported from "components/mobile-not-supported"; -import useNetwork from "x-hooks/use-network-theme"; +import {useAppState} from "contexts/app-state"; +import {changeShowWeb3} from "contexts/reducers/update-show-prop"; -import {useAppState} from "../contexts/app-state"; -import {changeShowWeb3} from "../contexts/reducers/update-show-prop"; -import Button from "./button"; +import { useNetwork } from "x-hooks/use-network"; export default function WebThreeDialog() { const router = useRouter(); - const {getURLWithNetwork} = useNetwork(); + const {t} = useTranslation("common"); + + const { getURLWithNetwork } = useNetwork(); const { dispatch, state: {show: {web3Dialog: showWeb3Dialog}}, } = useAppState(); - const {t} = useTranslation("common"); function handleClickTryAgain() { window.location.reload(); @@ -37,8 +38,8 @@ export default function WebThreeDialog() { getURLWithNetwork("/").pathname, getURLWithNetwork("/bounties").pathname, getURLWithNetwork("/curators").pathname, - "/[network]/bounty", - "/[network]", + "/[network]/[chain]/bounty", + "/[network]/[chain]", ].includes(router.pathname)) dispatch(changeShowWeb3(!window?.ethereum)); }, [router.pathname]); diff --git a/components/wrong-network-modal.tsx b/components/wrong-network-modal.tsx index 4725559283..ee6d3c34fb 100644 --- a/components/wrong-network-modal.tsx +++ b/components/wrong-network-modal.tsx @@ -1,121 +1,160 @@ -import {useState} from "react"; +import React, {useEffect, useState} from "react"; import {Spinner} from "react-bootstrap"; import {useTranslation} from "next-i18next"; +import { useRouter } from "next/router"; +import Button from "components/button"; +import ConnectWalletButton from "components/connect-wallet-button"; import Modal from "components/modal"; +import SelectNetworkDropdown from "components/select-network-dropdown"; import {useAppState} from "contexts/app-state"; +import { changeNeedsToChangeChain } from "contexts/reducers/change-spinners"; -import {NETWORKS} from "helpers/networks"; +import { UNSUPPORTED_CHAIN } from "helpers/constants"; -import {NetworkColors} from "interfaces/enums/network-colors"; +import {SupportedChainData} from "interfaces/supported-chain-data"; -import Button from "./button"; +import useApi from "x-hooks/use-api"; +import { useDao } from "x-hooks/use-dao"; +import useNetworkChange from "x-hooks/use-network-change"; type typeError = { code?: number; message?: string } -export default function WrongNetworkModal({ - requiredNetworkId = null, -}: { - requiredNetworkId: string | number; -}) { +export default function WrongNetworkModal() { const { t } = useTranslation("common"); + const { query, pathname } = useRouter(); const [error, setError] = useState(""); + const [_showModal, setShowModal] = useState(false); const [isAddingNetwork, setIsAddingNetwork] = useState(false); + const [networkChain, setNetworkChain] = useState(null); + const [chosenSupportedChain, setChosenSupportedChain] = useState(null); + + const api = useApi(); + const { connect } = useDao(); + const { handleAddNetwork } = useNetworkChange(); + const { + dispatch, + state: { connectedChain, currentUser, Service, supportedChains, loading, spinners } + } = useAppState(); - const {state: { connectedChain, Settings: settings },} = useAppState(); + const canBeHided = ![ + pathname?.includes("new-network"), + pathname?.includes("profile") + ].some(c => c); - function showModal() { - return ( - !!connectedChain?.id && - !!requiredNetworkId && - +connectedChain?.id !== +requiredNetworkId - ); + function changeShowModal() { + if (!supportedChains?.length || + loading?.isLoading || + !spinners?.needsToChangeChain) { + setShowModal(false); + return; + } + + if (typeof connectedChain?.matchWithNetworkChain !== "boolean" && !!currentUser?.walletAddress) + setShowModal(connectedChain?.name === UNSUPPORTED_CHAIN); + else + setShowModal(!connectedChain?.matchWithNetworkChain && !!currentUser?.walletAddress); + } + + async function selectSupportedChain(chain: SupportedChainData) { + if (!chain) + return; + + setChosenSupportedChain(chain); } - async function handleAddNetwork() { + async function _handleAddNetwork() { setIsAddingNetwork(true); setError(""); - const chainId = `0x${Number(settings?.requiredChain?.id).toString(16)}`; - const currencyNetwork = NETWORKS[chainId]; - try { - await window.ethereum.request({ - method: "wallet_switchEthereumChain", - params: [ - { - chainId: chainId, - }, - ], - }); - } catch (error) { - if ((error as typeError)?.message?.indexOf('wallet_addEthereumChain') > -1) { - try { - await window.ethereum.request({ - method: "wallet_addEthereumChain", - params: [ - { - chainId: chainId, - chainName: currencyNetwork.name, - nativeCurrency: { - name: currencyNetwork.currency.name, - symbol: currencyNetwork.currency.symbol, - decimals: currencyNetwork.decimals, - }, - rpcUrls: currencyNetwork.rpcUrls, - blockExplorerUrls: [currencyNetwork.explorerURL], - }, - ], - }); - } catch (error) { - if ((error as typeError).code === -32602) { - setError(t("modals.wrong-network.error-invalid-rpcUrl")); - } - if ((error as typeError).code === -32603) { - setError(t("modals.wrong-network.error-failed-rpcUrl")); - } + handleAddNetwork(chosenSupportedChain) + .then(() => { + if (!currentUser?.walletAddress) + return connect(); + }) + .catch(error => { + if ((error as typeError).code === -32602) { + setError(t("modals.wrong-network.error-invalid-rpcUrl")); + } + if ((error as typeError).code === -32603) { + setError(t("modals.wrong-network.error-failed-rpcUrl")); } - } - } finally { - setIsAddingNetwork(false); + }) + .finally(() => { + setIsAddingNetwork(false); + dispatch(changeNeedsToChangeChain(false)); + }); + } + + function updateNetworkChain() { + if (supportedChains?.length && Service?.network?.active?.chain_id && query?.network) { + const chain = supportedChains.find(({ chainId }) => +Service?.network?.active?.chain_id === +chainId ); + + setNetworkChain(chain); + setChosenSupportedChain(chain); } + else + setNetworkChain(null); } - const isButtonDisabled = (): boolean => - [isAddingNetwork].some((values) => values); + function handleHideModal() { + dispatch(changeNeedsToChangeChain(false)); + } + + const isButtonDisabled = () => [isAddingNetwork].some((values) => values); + + useEffect(() => { api.getSupportedChains() }, []); + useEffect(updateNetworkChain, [Service?.network?.active?.chain_id, supportedChains, query?.network]); + useEffect(changeShowModal, [ + currentUser?.walletAddress, + connectedChain?.matchWithNetworkChain, + connectedChain?.id, + supportedChains, + loading, + spinners + ]); + + if (spinners?.needsToChangeChain && !currentUser?.walletAddress) + return ; return (
- {t("modals.wrong-network.please-connect")}{" "} - - {settings?.chainIds && settings?.chainIds[requiredNetworkId] || ""}{" "} - {t("modals.wrong-network.network")} - -
{t("modals.wrong-network.on-your-wallet")} + {networkChain ? t("modals.wrong-network.connect-to-network-chain") : t("modals.wrong-network.please-connect")}
- {(isAddingNetwork && ( - - )) || - ""} - + {error && (

{error}

)} diff --git a/contexts/app-state.tsx b/contexts/app-state.tsx index 65b5b4d3ce..142e52ec22 100644 --- a/contexts/app-state.tsx +++ b/contexts/app-state.tsx @@ -8,7 +8,6 @@ import loadApplicationStateReducers from "./reducers"; import {toastError} from "./reducers/change-toaster"; import {mainReducer} from "./reducers/main"; - const appState: AppState = { state: { Settings: null, @@ -21,6 +20,7 @@ const appState: AppState = { spinners: {}, transactions: [], toaster: [], + supportedChains: null }, dispatch: () => undefined }; diff --git a/contexts/bounty-effects.tsx b/contexts/bounty-effects.tsx index 6fc5270e1c..3d6f9d970f 100644 --- a/contexts/bounty-effects.tsx +++ b/contexts/bounty-effects.tsx @@ -2,27 +2,26 @@ import {createContext, useEffect} from "react"; import {useRouter} from "next/router"; -import {useAppState} from "contexts/app-state"; +import { useAppState } from "contexts/app-state"; import {useBounty} from "x-hooks/use-bounty"; +import useChain from "x-hooks/use-chain"; const _context = {}; export const BountyEffectsContext = createContext(_context); export const BountyEffectsProvider = ({children}) => { - - const {query} = useRouter(); - const {state} = useAppState(); const bounty = useBounty(); - - useEffect(bounty.getDatabaseBounty, [state.Service?.network?.active, query?.id, query?.repoId]); - useEffect(bounty.getChainBounty, - [ - state.Service?.active?.network, state.Service?.network?.active?.networkAddress, - state.currentBounty?.data?.contractId, - state.currentUser?.walletAddress - ]) + const { chain } = useChain(); + const { query } = useRouter(); + const { state } = useAppState(); + + useEffect(bounty.getDatabaseBounty, [ + chain, + query?.id, + query?.repoId, + ]); useEffect(bounty.validateKycSteps, [ state?.currentBounty?.data?.isKyc, state?.currentBounty?.data?.kycTierList, diff --git a/contexts/global-effects.tsx b/contexts/global-effects.tsx index 06f29a7fc7..cfc901dd43 100644 --- a/contexts/global-effects.tsx +++ b/contexts/global-effects.tsx @@ -5,14 +5,13 @@ import {useRouter} from "next/router"; import {useAppState} from "contexts/app-state"; -import { CustomSession } from "interfaces/custom-session"; - import {useAuthentication} from "x-hooks/use-authentication"; +import useChain from "x-hooks/use-chain"; import {useDao} from "x-hooks/use-dao"; import {useNetwork} from "x-hooks/use-network"; import {useRepos} from "x-hooks/use-repos"; import {useSettings} from "x-hooks/use-settings"; -import { useTransactions } from "x-hooks/use-transactions"; +import {useTransactions} from "x-hooks/use-transactions"; const _context = {}; @@ -25,44 +24,63 @@ export const GlobalEffectsProvider = ({children}) => { const dao = useDao(); const repos = useRepos(); - const auth = useAuthentication(); + const { chain } = useChain(); const network = useNetwork(); const settings = useSettings(); + const auth = useAuthentication(); const transactions = useTransactions(); - useEffect(dao.start, [state.Settings]); - useEffect(dao.changeNetwork, [state.Service?.active, state.Service?.network?.active?.networkAddress]); + const { supportedChains, connectedChain, currentUser, Service } = state; + + // function updateLoadingState() { + // dispatch(changeLoadState(Object.values(state.spinners).some(v => v))); + // } + + useEffect(dao.start, [ + supportedChains, + Service?.network?.active?.chain_id, + connectedChain + ]); + + useEffect(dao.changeNetwork, [ + Service?.active, + Service?.network?.active?.networkAddress, + chain + ]); - useEffect(repos.loadRepos, [query?.network , state?.Service?.network?.lastVisited]); - useEffect(repos.updateActiveRepo, [query?.repoId, state.Service?.network?.repos]); + useEffect(repos.loadRepos, [ + query?.network, + chain, + state.Service?.network?.active + ]); + useEffect(repos.updateActiveRepo, [query?.repoId, Service?.network?.repos]); - useEffect(auth.validateGhAndWallet, - [(session?.data as CustomSession), - state.currentUser?.walletAddress, - // asPath.includes('developers'), - // asPath.includes('bounty'), - // asPath.includes('profile'), - ]); + useEffect(auth.validateGhAndWallet, [session?.data, currentUser?.walletAddress]); + useEffect(auth.updateWalletAddress, [currentUser]); + useEffect(auth.listenToAccountsChanged, [Service]); + useEffect(auth.updateWalletBalance, [currentUser?.walletAddress, Service?.active?.network]); useEffect(auth.updateKycSession, [state?.currentUser?.login, state?.currentUser?.accessToken, state?.currentUser?.match, state?.currentUser?.walletAddress, state?.Settings?.kyc?.tierList]); - useEffect(auth.updateWalletAddress, [state.currentUser]); - useEffect(auth.listenToAccountsChanged, [state.Service]); - useEffect(auth.updateWalletBalance, [state.currentUser?.walletAddress, state?.Service?.active?.network]); useEffect(auth.updateCurrentUserLogin, [session?.data?.user]); - useEffect(auth.verifyReAuthorizationNeed, [state.currentUser?.walletAddress]); - useEffect(network.updateActiveNetwork, [query?.network, state?.Service?.active?.network]); - useEffect(network.loadNetworkToken, [state?.Service?.active?.network]); - useEffect(network.loadNetworkTimes, [state.Service?.active?.network]); - useEffect(network.loadNetworkAmounts, [state.Service?.active?.network]); - useEffect(network.loadNetworkAllowedTokens, [state.Service?.active, state?.Service?.network?.active]); + useEffect(auth.verifyReAuthorizationNeed, [currentUser?.walletAddress]); + + useEffect(network.updateActiveNetwork, [query?.network, query?.chain]); + useEffect(network.loadNetworkTimes, [Service?.active?.network]); + useEffect(network.loadNetworkAmounts, [Service?.active?.network?.contractAddress, chain]); + useEffect(network.loadNetworkAllowedTokens, [Service?.network?.active, chain]); + useEffect(network.updateNetworkAndChainMatch, [ + connectedChain?.id, + query?.network, + query?.chain, + Service?.network?.active?.chain_id + ]); useEffect(settings.loadSettings, []); - useEffect(transactions.loadFromStorage, [state.currentUser?.walletAddress]); - useEffect(transactions.saveToStorage, [state.transactions]); + useEffect(transactions.loadFromStorage, [currentUser?.walletAddress, connectedChain]); return } \ No newline at end of file diff --git a/contexts/network-settings.tsx b/contexts/network-settings.tsx index 44471ef3d5..a3b9d2a86d 100644 --- a/contexts/network-settings.tsx +++ b/contexts/network-settings.tsx @@ -18,14 +18,16 @@ import { DEFAULT_MERGER_FEE, DEFAULT_ORACLE_EXCHANGE_RATE, DEFAULT_PERCENTAGE_FOR_DISPUTE, - DEFAULT_PROPOSER_FEE + DEFAULT_PROPOSER_FEE, + UNSUPPORTED_CHAIN } from "helpers/constants"; import {DefaultNetworkSettings} from "helpers/custom-network"; import { NetworkValidator } from "helpers/network"; import { RegistryValidator } from "helpers/registry"; +import { toLower } from "helpers/string"; import {Color, Network, NetworkSettings, Theme} from "interfaces/network"; -import { Token } from "interfaces/token"; +import {Token} from "interfaces/token"; import DAO from "services/dao-service"; import {WinStorage} from "services/win-storage"; @@ -36,7 +38,7 @@ import useOctokit from "x-hooks/use-octokit"; const NetworkSettingsContext = createContext(undefined); -const ALLOWED_PATHS = ["/new-network", "/[network]/profile/my-network", "/administration", "/setup"]; +const ALLOWED_PATHS = ["/new-network", "/[network]/[chain]/profile/my-network", "/administration", "/setup"]; const TTL = 48 * 60 * 60 // 2 day const storage = new WinStorage('create-network-settings', TTL, "localStorage"); @@ -47,14 +49,16 @@ export const NetworkSettingsProvider = ({ children }) => { referred to user nework when he access `/my-network` page from/in another network. */ const [forcedNetwork, setForcedNetwork] = useState(); - const [networkSettings, setNetworkSettings] = useState(JSON.parse(JSON.stringify(DefaultNetworkSettings))) + const [networkSettings, setNetworkSettings] = + useState(JSON.parse(JSON.stringify(DefaultNetworkSettings))) const [isLoadingData, setIsLoadingData] = useState(false); const [registryToken, setRegistryToken] = useState(); + const [forcedService, setForcedService] = useState(); const {state} = useAppState(); const { DefaultTheme } = useNetworkTheme(); const { getUserRepositories } = useOctokit(); - const { getNetwork, searchRepositories, repositoryHasIssues } = useApi(); + const { searchNetworks, searchRepositories } = useApi(); const IPFS_URL = state.Settings?.urls?.ipfs; const LIMITS = { @@ -221,13 +225,33 @@ export const NetworkSettingsProvider = ({ children }) => { setter: async (value: string) => { setFields('details.name', {value, validated: await Fields.name.validator(value)}) }, - validator: async (value: string) => { - let validated = undefined; - - if (value.trim() !== "" && !isSetup) - validated = /bepro|taikai/gi.test(value) ? false : !(await getNetwork({name: value}).catch(() => false)); + validator: async (value: string) => { + if (value.trim() === "") + return undefined; + + // Reserved names + if (/bepro|taikai/gi.test(value)) + return false; + + const networksWithSameName = await searchNetworks({ name: value }); + + // No networks with this name + if (networksWithSameName.count === 0) + return true; + + const currentChain = +state.connectedChain?.id; + + // Network with same name on this chain + if (networksWithSameName.rows.some(({ chain_id }) => +chain_id === currentChain)) + return false; - return !!validated; + const currentWallet = state.currentUser?.walletAddress?.toLowerCase(); + + // Network with same name on other chain and connected with the same creator wallet + if (networksWithSameName.rows.find(({ creatorAddress }) => creatorAddress.toLowerCase() === currentWallet)) + return true; + + return false; } }, description: { @@ -280,16 +304,22 @@ export const NetworkSettingsProvider = ({ children }) => { } }; - async function loadDaoService(): Promise { - if (!forcedNetwork) return state.Service?.active; + async function loadForcedService(): Promise { + if (!network || + toLower(state.Service?.active?.network?.contractAddress) === toLower(network?.networkAddress)) + return state.Service?.active; + + if (toLower(forcedService?.network?.contractAddress) === toLower(network?.networkAddress)) + return forcedService; - const networkAddress = network?.networkAddress; const dao = new DAO({ - web3Connection: state.Service?.active.web3Connection, + web3Connection: state.Service?.active?.web3Connection, skipWindowAssignment: true }); - await dao.loadNetwork(networkAddress); + await dao.start(); + + await dao.loadNetwork(network?.networkAddress); return dao; } @@ -320,7 +350,7 @@ export const NetworkSettingsProvider = ({ children }) => { async function loadGHRepos(){ const repositories = []; - if(state.currentUser?.login){ + if(state.currentUser?.login) { const botUser = !isCreating ? state.Settings?.github?.botUser : undefined const githubRepositories = await getUserRepositories(state.currentUser?.login, botUser); @@ -328,8 +358,8 @@ export const NetworkSettingsProvider = ({ children }) => { .filter(repo => { const isOwner = state.currentUser.login === repo?.nameWithOwner.split("/")[0]; - if((!repo?.isFork && isOwner || repo?.isInOrganization) && !repo?.isArchived) - return repo + if((isOwner || repo?.isInOrganization) && !repo?.isFork && !repo?.isArchived) + return repo; }) .map(repo => ({ checked: false, @@ -342,24 +372,28 @@ export const NetworkSettingsProvider = ({ children }) => { collaborators: repo.collaborators })); - - if (!isCreating){ - const repositoryAlreadyExists = await searchRepositories({ networkName: network?.name }) - .then(({ rows }) => - Promise.all(rows.map( async repo => { - const repoOnGh = filtered.find(({ fullName }) => fullName === repo.githubPath); - - return { - checked: true, - isSaved: true, - name: repo.githubPath.split("/")[1], - fullName: repo.githubPath, - hasIssues: await repositoryHasIssues(repo.githubPath), - mergeCommitAllowed: repoOnGh?.mergeCommitAllowed || false, - collaborators: repoOnGh?.collaborators || [], - }; - }))) - repositories.push(...repositoryAlreadyExists) + if (!isCreating) { + const repositoryAlreadyExists = await searchRepositories({ + networkName: network?.name, + chainId: state.connectedChain?.id, + includeIssues: "true" + }) + .then(({ rows }) => + Promise.all(rows.map( async repo => { + const repoOnGh = filtered.find(({ fullName }) => fullName === repo.githubPath); + + return { + checked: true, + isSaved: true, + name: repo.githubPath.split("/")[1], + fullName: repo.githubPath, + hasIssues: !!repo.issues.length, + mergeCommitAllowed: repoOnGh?.mergeCommitAllowed || false, + collaborators: repoOnGh?.collaborators || [], + }; + }))); + + repositories.push(...repositoryAlreadyExists); } repositories.push(...filtered.filter(repo => !repositories.find((repoB) => repoB.fullName === repo.fullName))); @@ -427,9 +461,7 @@ export const NetworkSettingsProvider = ({ children }) => { async function loadNetworkSettings(): Promise{ const defaultState = JSON.parse(JSON.stringify(DefaultNetworkSettings)); //Deep Copy, More: https://www.codingem.com/javascript-clone-object - const service = await loadDaoService(); - - if (!service.network) return; + if (!forcedService?.network) return; const [ treasury, @@ -442,17 +474,19 @@ export const NetworkSettingsProvider = ({ children }) => { oracleExchangeRate, cancelableTime, isNetworkAbleToBeClosed, + tokensLocked ] = await Promise.all([ - service.network.treasuryInfo(), - service.getNetworkParameter("councilAmount"), - service.getNetworkParameter("disputableTime"), - service.getNetworkParameter("draftTime"), - service.getNetworkParameter("percentageNeededForDispute"), - service.getNetworkParameter("mergeCreatorFeeShare"), - service.getNetworkParameter("proposerFeeShare"), - service.getNetworkParameter("oracleExchangeRate"), - service.getNetworkParameter("cancelableTime"), - service.isNetworkAbleToBeClosed(), + forcedService.network.treasuryInfo(), + forcedService.getNetworkParameter("councilAmount"), + forcedService.getNetworkParameter("disputableTime"), + forcedService.getNetworkParameter("draftTime"), + forcedService.getNetworkParameter("percentageNeededForDispute"), + forcedService.getNetworkParameter("mergeCreatorFeeShare"), + forcedService.getNetworkParameter("proposerFeeShare"), + forcedService.getNetworkParameter("oracleExchangeRate"), + forcedService.getNetworkParameter("cancelableTime"), + forcedService.isNetworkAbleToBeClosed(), + forcedService.getTotalNetworkToken() ]); const validatedParameter = value => ({ value, validated: true }); @@ -493,16 +527,37 @@ export const NetworkSettingsProvider = ({ children }) => { defaultState.settings.theme.colors = network?.colors || DefaultTheme(); defaultState.github.repositories = await loadGHRepos(); - setNetworkSettings(defaultState); + setForcedNetwork((prev)=>({ + ...prev, + tokensLocked: tokensLocked.toFixed(), + councilAmount: councilAmount.toString(), + disputableTime: +disputableTime / 1000, + draftTime: +draftTime / 1000, + percentageNeededForDispute: +percentageNeededForDispute, + cancelableTime: +cancelableTime / 1000, + oracleExchangeRate: oracleExchangeRate, + proposerFeeShare: proposerFeeShare, + mergeCreatorFeeShare: mergeCreatorFeeShare + })); + setNetworkSettings(defaultState); + return defaultState; } + useEffect(() => { + if (!network || !state.Service?.active || state.Service?.starting) + setForcedService(undefined); + else if (network?.chain?.chainRpc === state.Service?.active?.web3Connection?.options?.web3Host) + loadForcedService() + .then(setForcedService); + }, [network, state.Service?.active, state.Service?.starting]); + useEffect(() => { if ([ - !state.Service?.active, !state.currentUser?.walletAddress, - !isCreating && !network?.name && !network?.councilAmount, + !isCreating && + (!network?.name || !forcedService || !!networkSettings?.settings?.parameters?.councilAmount?.value), isCreating && !state.Service?.active?.registry?.token?.contractAddress, !needsToLoad, !state.Settings @@ -518,66 +573,45 @@ export const NetworkSettingsProvider = ({ children }) => { }, [ state.currentUser?.login, state.currentUser?.walletAddress, - state.Service?.active, + forcedService, network, isCreating, - forcedNetwork, needsToLoad, router.pathname, state.Service?.active?.registry?.token?.contractAddress, state.Settings ]); - // NOTE - Load Forced/User Network - useEffect(()=>{ - if(state.Service?.active && forcedNetwork && (!forcedNetwork?.tokensLocked || !forcedNetwork.tokensStaked)) - loadDaoService() - .then((service)=> - Promise.all([ - service.getTotalNetworkToken(), - 0, - service.getNetworkParameter("councilAmount"), - service.getNetworkParameter("disputableTime"), - service.getNetworkParameter("draftTime"), - service.getNetworkParameter("percentageNeededForDispute"), - service.getNetworkParameter("cancelableTime"), - service.getNetworkParameter("oracleExchangeRate"), - service.getNetworkParameter("proposerFeeShare"), - service.getNetworkParameter("mergeCreatorFeeShare") - ])) - .then(([ - tokensLocked, - tokensStaked, - councilAmount, - disputableTime, - draftTime, - percentageNeededForDispute, - cancelableTime, - oracleExchangeRate, - proposerFeeShare, - mergeCreatorFeeShare - ])=> - setForcedNetwork((prev)=>({ - ...prev, - tokensLocked: tokensLocked.toFixed(), - tokensStaked: tokensStaked.toFixed(), - councilAmount: councilAmount.toString(), - disputableTime: +disputableTime / 1000, - draftTime: +draftTime / 1000, - percentageNeededForDispute: +percentageNeededForDispute, - cancelableTime: +cancelableTime / 1000, - oracleExchangeRate: oracleExchangeRate, - proposerFeeShare: proposerFeeShare, - mergeCreatorFeeShare: mergeCreatorFeeShare - }))); - },[forcedNetwork, state.Service?.active]); - useEffect(() => { - if (state.Service?.active?.registry?.contractAddress) + if (state.Service?.active?.registry?.contractAddress && state.connectedChain?.name !== UNSUPPORTED_CHAIN) state.Service.active.getERC20TokenData(state.Service.active.registry.token.contractAddress) .then(setRegistryToken) .catch(error => console.debug("Failed to load registry token", error)); - }, [state.Service?.active?.registry?.contractAddress]); + }, [state.Service?.active?.registry?.contractAddress, state.connectedChain?.name]); + + // Pre Select same network on other chain repositories + useEffect(() => { + const creatingName = networkSettings?.details?.name?.value; + const currentAddress = state.currentUser?.walletAddress; + const connectedChainId = state.connectedChain?.id; + + if (!creatingName || !currentAddress || !connectedChainId || !isCreating) return; + + searchRepositories({ + networkName: creatingName + }) + .then(({ count, rows }) => { + if (count === 0) return; + + const reposToSelect = rows.filter(({ network: { name, creatorAddress, chain_id } }) => + toLower(name) === toLower(creatingName) && + creatorAddress === currentAddress && + chain_id !== connectedChainId); + + reposToSelect.forEach(({ githubPath }) => Fields.repository.setter(githubPath)); + }); + }, [networkSettings?.details?.name?.value, state.currentUser?.walletAddress, state.connectedChain?.id, isCreating]); + const memorizedValue = useMemo(() => ({ ...networkSettings, diff --git a/contexts/reducers/change-chain.ts b/contexts/reducers/change-chain.ts index 8cc30de26d..c87650edf4 100644 --- a/contexts/reducers/change-chain.ts +++ b/contexts/reducers/change-chain.ts @@ -1,6 +1,23 @@ -import {ConnectedChain} from "../../interfaces/application-state"; +import {ConnectedChain, State} from "../../interfaces/application-state"; import {AppStateReduceId} from "../../interfaces/enums/app-state-reduce-id"; import {SimpleAction} from "./reducer"; -export const changeChain = - new SimpleAction(AppStateReduceId.ConnectedChain, "connectedChain"); \ No newline at end of file +export class ChangeConnectedChain extends SimpleAction> { + constructor() { + super(AppStateReduceId.ConnectedChainMatch, "connectedChain"); + } + + reducer(state: State, payload: ConnectedChain | Partial): State { + const transformed = { + ...state.connectedChain, + ...payload + }; + + return super.reducer(state, transformed); + } +} + + +export const changeChain = new ChangeConnectedChain(); +export const changeMatchWithNetworkChain = + (matchWithNetworkChain: boolean) => changeChain.update({ matchWithNetworkChain }); \ No newline at end of file diff --git a/contexts/reducers/change-current-bounty.ts b/contexts/reducers/change-current-bounty.ts index 4a936b6b3a..910af2c698 100644 --- a/contexts/reducers/change-current-bounty.ts +++ b/contexts/reducers/change-current-bounty.ts @@ -1,16 +1,10 @@ -import {PullRequest} from "@taikai/dappkit"; +import {SimpleAction} from "contexts/reducers/reducer"; import {CurrentBounty, State} from "interfaces/application-state"; import {AppStateReduceId} from "interfaces/enums/app-state-reduce-id"; -import { Token } from "interfaces/token"; +import {IssueBigNumberData, IssueDataComment} from "interfaces/issue-data"; import {Tier} from 'types/settings' - -import {BenefactorExtended, BountyExtended, ProposalExtended} from "../../interfaces/bounty"; -import {IssueBigNumberData, IssueDataComment} from "../../interfaces/issue-data"; -import {SimpleAction} from "./reducer"; - - export class ChangeCurrentBounty, A = keyof CurrentBounty & 'clear'> extends SimpleAction { @@ -38,24 +32,6 @@ export class ChangeCurrentBounty, A = k } } -export class ChangeCurrentBountyChainData extends ChangeCurrentBounty, null> { - constructor() { - super(AppStateReduceId.CurrentBountyChainData); - } - - reducer(state: State, payload: BountyExtended | Partial): State { - const transformed = { - ...state.currentBounty, - chainData: { - ...state.currentBounty?.chainData, - ...payload - } - }; - - return super.reducer(state, transformed, 'chainData'); - } -} - export const changeCurrentBounty = new ChangeCurrentBounty(); export const changeCurrentBountyComments = (comments: IssueDataComment[]) => @@ -66,35 +42,5 @@ export const changeCurrentBountyData = (data: IssueBigNumberData) => export const clearCurrentBountyData = () => changeCurrentBounty.update(null, 'clear'); - -export const changeCurrentBountyDataChain = new ChangeCurrentBountyChainData(); - -export const changeCurrentBountyDataIsDraft = (isDraft: boolean) => - changeCurrentBountyDataChain.update({isDraft}); - -export const changeCurrentBountyDataIsFinished = (isFinished: boolean) => - changeCurrentBountyDataChain.update({isFinished}); - -export const changeCurrentBountyDataIsInValidation = (isInValidation: boolean) => - changeCurrentBountyDataChain.update({isInValidation}); - -export const changeCurrentBountyDataIsFundingRequest = (isFundingRequest: boolean) => - changeCurrentBountyDataChain.update({isFundingRequest}); - -export const changeCurrentBountyDataProposals = (proposals: ProposalExtended[]) => - changeCurrentBountyDataChain.update({proposals}); - -export const changeCurrentBountyDataPullRequests = (pullRequests: PullRequest[]) => - changeCurrentBountyDataChain.update({pullRequests}); - -export const changeCurrentBountyDataFunding = (funding: BenefactorExtended[]) => - changeCurrentBountyDataChain.update({funding}); - -export const changeCurrentBountyDataTransactional = (transactionalTokenData: Token) => - changeCurrentBountyDataChain.update({transactionalTokenData}); - -export const changeCurrentBountyDataReward = (rewardTokenData: Token) => - changeCurrentBountyDataChain.update({rewardTokenData}); - export const changeCurrentKycSteps = (kycSteps: Tier[]) => changeCurrentBounty.update({kycSteps}, 'kycSteps'); \ No newline at end of file diff --git a/contexts/reducers/change-current-user.ts b/contexts/reducers/change-current-user.ts index 834c38c252..409241db4c 100644 --- a/contexts/reducers/change-current-user.ts +++ b/contexts/reducers/change-current-user.ts @@ -1,10 +1,9 @@ +import {SimpleAction} from "contexts/reducers/reducer"; import { kycSession } from "interfaces/kyc-session"; -import {CurrentUserState, State} from "../../interfaces/application-state"; -import {Balance} from "../../interfaces/balance-state"; -import {AppStateReduceId} from "../../interfaces/enums/app-state-reduce-id"; -import {SimpleAction} from "./reducer"; - +import {CurrentUserState, State} from "interfaces/application-state"; +import {Balance} from "interfaces/balance-state"; +import {AppStateReduceId} from "interfaces/enums/app-state-reduce-id"; export class ChangeCurrentUser> extends SimpleAction { constructor(id = AppStateReduceId.CurrentUser) { @@ -42,10 +41,16 @@ export const changeCurrentUserBalance = (balance: Balance | Partial) => changeCurrentUser.update({balance: balance as unknown as Balance}); export const changeCurrentUserConnected = (connected: boolean) => - changeCurrentUser.update({connected}) + changeCurrentUser.update({connected}); export const changeCurrentUserSignature = (signature: string) => changeCurrentUser.update({signature}); +export const changeCurrentUserisAdmin = (isAdmin: boolean) => + changeCurrentUser.update({isAdmin}); + +export const changeCurrentUserHasRegisteredNetwork = (hasRegisteredNetwork: boolean) => + changeCurrentUser.update({hasRegisteredNetwork}); + export const changeCurrentUserKycSession = (kycSession: kycSession) => changeCurrentUser.update({kycSession}) diff --git a/contexts/reducers/change-service.ts b/contexts/reducers/change-service.ts index 73783b7222..8b984c0279 100644 --- a/contexts/reducers/change-service.ts +++ b/contexts/reducers/change-service.ts @@ -1,3 +1,5 @@ +import { SupportedChainData } from "interfaces/supported-chain-data"; + import { RepositoryPermissions } from "types/octokit"; import { @@ -77,7 +79,7 @@ export const changeNetwork = new ChangeServiceNetworkProp(); export const changeRepos = new ChangeServiceNetworkReposProp(); export const changeActiveRepoProps = new ChangeServiceNetworkActiveRepoProp(); -export const changeStarting = (starting: boolean) => changeServiceProp.update({starting}, 'starting'); +export const changeStarting = (starting: boolean) => changeServiceProp.update(starting as any, 'starting'); export const changeMicroServiceReady = (microReady: boolean) => changeServiceProp.update({microReady}, 'microReady'); @@ -88,9 +90,10 @@ export const changeNetworkLastVisited = (lastVisited: string) => changeNetwork.u export const changeNoDefaultNetwork = (noDefaultNetwork: boolean) => changeNetwork.update({noDefaultNetwork}); export const changeActiveNetwork = (active: Network) => changeNetwork.update({active}); -export const changeActiveNetworkToken = (networkToken: Token) => changeNetwork.update({networkToken}); export const changeActiveNetworkTimes = (times: NetworkTimes) => changeNetwork.update({times}); export const changeActiveNetworkAmounts = (amounts: NetworkAmounts) => changeNetwork.update({amounts}); +export const changeActiveAvailableChains = + (availableChains: SupportedChainData[]) => changeNetwork.update({availableChains}); export const changeAllowedTokens = (transactional: Token[], reward: Token[]) => changeNetwork.update({tokens: {transactional, reward}}) diff --git a/contexts/reducers/change-spinners.ts b/contexts/reducers/change-spinners.ts index c290e5924a..dcd644fdc5 100644 --- a/contexts/reducers/change-spinners.ts +++ b/contexts/reducers/change-spinners.ts @@ -1,6 +1,7 @@ -import {State} from "../../interfaces/application-state"; -import {AppStateReduceId} from "../../interfaces/enums/app-state-reduce-id"; -import {SimpleAction} from "./reducer"; +import {SimpleAction} from "contexts/reducers/reducer"; + +import {State} from "interfaces/application-state"; +import {AppStateReduceId} from "interfaces/enums/app-state-reduce-id"; interface Spinners { proposals: boolean; @@ -14,6 +15,10 @@ interface Spinners { connecting: boolean; connectingGH: boolean; repos: boolean; + changingChain: boolean; + signingMessage: boolean; + switchingChain: boolean; + needsToChangeChain: boolean; } class ChangeSpinners extends SimpleAction> { @@ -49,4 +54,10 @@ export const changeConnecting = (connecting: boolean) => changeSpinners.update({connecting}); export const changeConnectingGH = (connectingGH: boolean) => - changeSpinners.update({connectingGH}); \ No newline at end of file + changeSpinners.update({connectingGH}); + +export const changeChangingChain = (changingChain: boolean) => + changeSpinners.update({changingChain}); + +export const changeNeedsToChangeChain = (needsToChangeChain: boolean) => + changeSpinners.update({needsToChangeChain}); \ No newline at end of file diff --git a/contexts/reducers/change-supported-chains.ts b/contexts/reducers/change-supported-chains.ts new file mode 100644 index 0000000000..044655947c --- /dev/null +++ b/contexts/reducers/change-supported-chains.ts @@ -0,0 +1,8 @@ +import {AppStateReduceId} from "../../interfaces/enums/app-state-reduce-id"; +import {SupportedChainData} from "../../interfaces/supported-chain-data"; +import {SimpleAction} from "./reducer"; + +export const changeSupportedChains = + new SimpleAction(AppStateReduceId.SupportedChains, 'supportedChains'); + +export const updateSupportedChains = (data: SupportedChainData[]) => changeSupportedChains.update(data); \ No newline at end of file diff --git a/contexts/reducers/change-tx-list.ts b/contexts/reducers/change-tx-list.ts index fa0418f1d0..5c65b1a8b0 100644 --- a/contexts/reducers/change-tx-list.ts +++ b/contexts/reducers/change-tx-list.ts @@ -1,11 +1,14 @@ import {v4 as uuidv4} from "uuid"; -import {State} from "../../interfaces/application-state"; -import {AppStateReduceId} from "../../interfaces/enums/app-state-reduce-id"; -import {TransactionStatus} from "../../interfaces/enums/transaction-status"; -import {TransactionTypes} from "../../interfaces/enums/transaction-types"; -import {BlockTransaction, SimpleBlockTransactionPayload, UpdateBlockTransaction} from "../../interfaces/transaction"; -import {SimpleAction} from "./reducer"; +import {SimpleAction} from "contexts/reducers/reducer"; + +import { saveTransactionsToStorage } from "helpers/transactions"; + +import {State} from "interfaces/application-state"; +import {AppStateReduceId} from "interfaces/enums/app-state-reduce-id"; +import {TransactionStatus} from "interfaces/enums/transaction-status"; +import {TransactionTypes} from "interfaces/enums/transaction-types"; +import {BlockTransaction, SimpleBlockTransactionPayload, UpdateBlockTransaction} from "interfaces/transaction"; type Tx = Partial<(SimpleBlockTransactionPayload | BlockTransaction | UpdateBlockTransaction)>; export type TxList = Tx[]; @@ -17,7 +20,7 @@ class ChangeTxList extends SimpleAction { super(AppStateReduceId.AddTransaction, 'transactions'); } - reducer(state: State, payload: TxList, subAction): State { + reducer(state: State, payload: TxList, subAction): State { let transformed; const addMapper = (_tx) => ({ @@ -58,6 +61,8 @@ class ChangeTxList extends SimpleAction { break; } + saveTransactionsToStorage(transformed); + return super.reducer(state, transformed); } } diff --git a/contexts/reducers/index.ts b/contexts/reducers/index.ts index 8a68992f7a..08022b4b62 100644 --- a/contexts/reducers/index.ts +++ b/contexts/reducers/index.ts @@ -1,14 +1,15 @@ -import {changeChain} from "./change-chain"; -import {changeCurrentBounty, changeCurrentBountyDataChain} from "./change-current-bounty"; -import {changeCurrentUser} from "./change-current-user"; -import {changeLoad} from "./change-load"; -import {changeActiveRepoProps, changeNetwork, changeRepos, changeServiceProp} from "./change-service"; -import {changeSettings,} from "./change-settings"; -import {changeSpinners} from "./change-spinners"; -import {changeToaster} from "./change-toaster"; -import {changeTxList} from "./change-tx-list"; -import {Actions, addReducer} from "./main"; -import {changeShowProp} from "./update-show-prop"; +import {changeChain} from "contexts/reducers/change-chain"; +import {changeCurrentBounty} from "contexts/reducers/change-current-bounty"; +import {changeCurrentUser} from "contexts/reducers/change-current-user"; +import {changeLoad} from "contexts/reducers/change-load"; +import {changeActiveRepoProps, changeNetwork, changeRepos, changeServiceProp} from "contexts/reducers/change-service"; +import {changeSettings,} from "contexts/reducers/change-settings"; +import {changeSpinners} from "contexts/reducers/change-spinners"; +import {changeSupportedChains} from "contexts/reducers/change-supported-chains"; +import {changeToaster} from "contexts/reducers/change-toaster"; +import {changeTxList} from "contexts/reducers/change-tx-list"; +import {Actions, addReducer} from "contexts/reducers/main"; +import {changeShowProp} from "contexts/reducers/update-show-prop"; let loaded = false; @@ -27,10 +28,10 @@ export default function loadApplicationStateReducers() { changeNetwork, changeRepos, changeCurrentBounty, - changeCurrentBountyDataChain, changeSpinners, changeSettings, - changeActiveRepoProps + changeActiveRepoProps, + changeSupportedChains ].forEach(addReducer); console.debug(`Loaded State Reducers`); diff --git a/contexts/reducers/main.ts b/contexts/reducers/main.ts index 52cfa41d4f..67953a1277 100644 --- a/contexts/reducers/main.ts +++ b/contexts/reducers/main.ts @@ -21,7 +21,7 @@ export const mainReducer = (state: State, actor: { id, payload, subAction }) => const action = Actions.find(({id: id}) => id === actor.id); if (!action) - throw new Error(`No action found for ${actor.id}`); + throw new Error(`No action found for ${AppStateReduceId[actor.id]}`); return action.reducer(state, actor.payload, actor.subAction); } \ No newline at end of file diff --git a/db/migrations/2023010913000000-chain-migrations.js b/db/migrations/2023010913000000-chain-migrations.js new file mode 100644 index 0000000000..0bb74e180f --- /dev/null +++ b/db/migrations/2023010913000000-chain-migrations.js @@ -0,0 +1,83 @@ +const ChainEvents = require("../models/chain-events.model"); +const {DataTypes} = require("sequelize"); + +async function up(queryInterface, Sequelize) { + await queryInterface.createTable("chains", { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + unique: true + }, + chainId: { + type: Sequelize.INTEGER, + unique: true + }, + chainRpc: { + type: Sequelize.STRING, + allowNull: false, + }, + chainName: { + type: Sequelize.STRING, + allowNull: false, + unique: true + }, + chainShortName: { + type: Sequelize.STRING, + allowNull: false, + unique: false + }, + chainCurrencyName: { + type: Sequelize.STRING, + allowNull: false, + }, + chainCurrencySymbol: { + type: Sequelize.STRING, + allowNull: false, + }, + chainCurrencyDecimals: { + type: Sequelize.INTEGER, + allowNull: false, + }, + registryAddress: { + type: Sequelize.STRING, + allowNull: true + }, + eventsApi: { + type: Sequelize.STRING, + }, + blockScanner: { + type: DataTypes.STRING, + }, + isDefault: { + type: Sequelize.BOOLEAN + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + + const chain_id = {type: Sequelize.INTEGER} + + await queryInterface.addColumn('networks', 'chain_id', chain_id); + await queryInterface.addColumn('issues', 'chain_id', chain_id); + await queryInterface.addColumn('chain_events', 'chain_id', chain_id); + await queryInterface.changeColumn('chain_events', 'name', {type: DataTypes.STRING, unique: false}); + await queryInterface.removeConstraint('chain_events', 'chain_events_name_key') + // todo future self note: This should make it so we can have multiple names +} + +async function down(queryInterface, Sequelize) { + await queryInterface.dropTable('chains'); + await queryInterface.removeColumn('networks', 'chain_id'); + await queryInterface.removeColumn('issues', 'chain_id'); + await queryInterface.removeColumn('chain_events', 'chain_id'); + await queryInterface.addConstraint('chain_events', {type: 'unique', fields: ['name']}); +} + +module.exports = {up, down} \ No newline at end of file diff --git a/db/migrations/20230118114836-add-chain-id-to-tokens.js b/db/migrations/20230118114836-add-chain-id-to-tokens.js new file mode 100644 index 0000000000..98c03cf6bd --- /dev/null +++ b/db/migrations/20230118114836-add-chain-id-to-tokens.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.addColumn("tokens", "chain_id", { + type: Sequelize.INTEGER, + allowNull: true + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.removeColumn("tokens", "chain_id"); + } +}; diff --git a/db/migrations/20230119194351-alter-network-table-constraints.js b/db/migrations/20230119194351-alter-network-table-constraints.js new file mode 100644 index 0000000000..55d0e3da93 --- /dev/null +++ b/db/migrations/20230119194351-alter-network-table-constraints.js @@ -0,0 +1,34 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.removeConstraint("networks", "networks_name_key"); + + await queryInterface.changeColumn("networks", "name", { + type: Sequelize.STRING, + unique: "network_chain_unique" + }); + + await queryInterface.changeColumn("networks", "chain_id", { + type: Sequelize.INTEGER, + unique: "network_chain_unique" + }); + + await queryInterface.addConstraint("networks", { + type: "unique", + fields: ["name", "chain_id"], + name: "network_chain_unique" + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.removeConstraint("networks", "network_chain_unique") + .catch(error => console.log("Contrainst already removed", error?.name)); + + await queryInterface.addConstraint("networks", { + type: "unique", + fields: ["name"], + name: "networks_name_key" + }); + } +}; diff --git a/db/migrations/20230120135017-add-chain-table-associations.js b/db/migrations/20230120135017-add-chain-table-associations.js new file mode 100644 index 0000000000..76c8afdb98 --- /dev/null +++ b/db/migrations/20230120135017-add-chain-table-associations.js @@ -0,0 +1,36 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.changeColumn("networks", "chain_id", { + type: Sequelize.INTEGER, + unique: "network_chain_unique", + references: { + model: "chains", + key: "chainId" + } + }); + + await queryInterface.changeColumn("issues", "chain_id", { + type: Sequelize.INTEGER, + references: { + model: "chains", + key: "chainId" + } + }); + + await queryInterface.changeColumn("tokens", "chain_id", { + type: Sequelize.INTEGER, + references: { + model: "chains", + key: "chainId" + } + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.removeConstraint("networks", "networks_chain_id_fkey"); + await queryInterface.removeConstraint("issues", "issues_chain_id_fkey"); + await queryInterface.removeConstraint("tokens", "tokens_chain_id_fkey"); + } +}; diff --git a/db/migrations/20230120191142-add-color-column-to-chain-table.js b/db/migrations/20230120191142-add-color-column-to-chain-table.js new file mode 100644 index 0000000000..9999f34d93 --- /dev/null +++ b/db/migrations/20230120191142-add-color-column-to-chain-table.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.addColumn("chains", "color", { + type: Sequelize.STRING, + allowNull: true + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.removeColumn("chains", "color"); + } +}; diff --git a/db/migrations/20230125143720-change-repositories-unique-key.js b/db/migrations/20230125143720-change-repositories-unique-key.js new file mode 100644 index 0000000000..c32d8661cd --- /dev/null +++ b/db/migrations/20230125143720-change-repositories-unique-key.js @@ -0,0 +1,39 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.removeConstraint("repositories", "repositories_githubPath_key"); + + await queryInterface.changeColumn("repositories", "githubPath", { + type: Sequelize.STRING, + allowNull: false, + unique: "repositories_networks_unique" + }); + + await queryInterface.changeColumn("repositories", "network_id", { + type: Sequelize.INTEGER, + unique: "repositories_networks_unique" + }); + + await queryInterface.addConstraint("repositories", { + fields: ["githubPath", "network_id"], + type: "unique", + name: "repositories_networks_unique" + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.removeConstraint("repositories", "repositories_networks_unique"); + + await queryInterface.changeColumn("repositories", "githubPath", { + type: Sequelize.STRING, + allowNull: false, + unique: true + }); + + await queryInterface.changeColumn("repositories", "network_id", { + type: Sequelize.INTEGER, + unique: false + }); + } +}; diff --git a/db/migrations/20230126180702-add-network-token-column-to-network.js b/db/migrations/20230126180702-add-network-token-column-to-network.js new file mode 100644 index 0000000000..c2de7f1a38 --- /dev/null +++ b/db/migrations/20230126180702-add-network-token-column-to-network.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.addColumn("networks", "network_token_id", { + type: Sequelize.INTEGER, + allowNull: true, + references: { + model: "tokens", + key: "id" + } + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.removeColumn("networks", "network_token_id"); + } +}; diff --git a/db/migrations/20230127191659-add-contract-params-to-network-table.js b/db/migrations/20230127191659-add-contract-params-to-network-table.js new file mode 100644 index 0000000000..c9682c9438 --- /dev/null +++ b/db/migrations/20230127191659-add-contract-params-to-network-table.js @@ -0,0 +1,36 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + const integerParam = { + type: Sequelize.BIGINT + }; + + const floatParam = { + type: Sequelize.FLOAT + }; + + await queryInterface.addColumn("networks", "councilAmount", { + type: Sequelize.STRING + }); + + await queryInterface.addColumn("networks", "disputableTime", integerParam); + await queryInterface.addColumn("networks", "draftTime", integerParam); + await queryInterface.addColumn("networks", "oracleExchangeRate", floatParam); + await queryInterface.addColumn("networks", "mergeCreatorFeeShare", floatParam); + await queryInterface.addColumn("networks", "percentageNeededForDispute", floatParam); + await queryInterface.addColumn("networks", "cancelableTime", integerParam); + await queryInterface.addColumn("networks", "proposerFeeShare", floatParam); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.removeColumn("networks", "councilAmount"); + await queryInterface.removeColumn("networks", "disputableTime"); + await queryInterface.removeColumn("networks", "draftTime"); + await queryInterface.removeColumn("networks", "oracleExchangeRate"); + await queryInterface.removeColumn("networks", "mergeCreatorFeeShare"); + await queryInterface.removeColumn("networks", "percentageNeededForDispute"); + await queryInterface.removeColumn("networks", "cancelableTime"); + await queryInterface.removeColumn("networks", "proposerFeeShare"); + } +}; diff --git a/db/migrations/20230208174830-add-missing-columns-to-issues.js b/db/migrations/20230208174830-add-missing-columns-to-issues.js new file mode 100644 index 0000000000..bc5ccc7ba9 --- /dev/null +++ b/db/migrations/20230208174830-add-missing-columns-to-issues.js @@ -0,0 +1,53 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.renameColumn("issues", "tokenId", "transactionalTokenId"); + + await queryInterface.removeConstraint("issues", "issues_tokenId_fkey"); + + await queryInterface.addConstraint("issues", { + type: "foreign key", + fields: ["transactionalTokenId"], + name: "issues_transactionalTokenId_fkey", + references: { + table: "tokens", + field: "id" + } + }); + + await queryInterface.addColumn("issues", "rewardAmount", { + type: Sequelize.STRING, + allowNull: true + }); + + await queryInterface.addColumn("issues", "rewardTokenId", { + type: Sequelize.INTEGER, + allowNull: true, + references: { + model: "tokens", + key: "id" + } + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.renameColumn("issues", "transactionalTokenId", "tokenId"); + + await queryInterface.removeConstraint("issues", "issues_transactionalTokenId_fkey"); + + await queryInterface.addConstraint("issues", { + type: "foreign key", + fields: ["tokenId"], + name: "issues_tokenId_fkey", + references: { + table: "tokens", + field: "id" + } + }); + + await queryInterface.removeColumn("issues", "rewardAmount"); + + await queryInterface.removeColumn("issues", "rewardTokenId"); + } +}; diff --git a/db/migrations/20230209001339-add-withdrawn-to-benefactors.js b/db/migrations/20230209001339-add-withdrawn-to-benefactors.js new file mode 100644 index 0000000000..75125ce59c --- /dev/null +++ b/db/migrations/20230209001339-add-withdrawn-to-benefactors.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.addColumn("benefactors", "withdrawn", { + type: Sequelize.BOOLEAN, + defaultValue: false + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.removeColumn("benefactors", "withdrawn"); + } +}; diff --git a/db/migrations/20230209143651-rename-address-to-recipient-on-proposal-distributions.js b/db/migrations/20230209143651-rename-address-to-recipient-on-proposal-distributions.js new file mode 100644 index 0000000000..912571afcf --- /dev/null +++ b/db/migrations/20230209143651-rename-address-to-recipient-on-proposal-distributions.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.renameColumn("proposal_distributions", "address", "recipient"); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.renameColumn("proposal_distributions", "recipient", "address"); + } +}; diff --git a/db/migrations/20230210114900-add-default-to-amounts-on-issues.js b/db/migrations/20230210114900-add-default-to-amounts-on-issues.js new file mode 100644 index 0000000000..ab8129f21a --- /dev/null +++ b/db/migrations/20230210114900-add-default-to-amounts-on-issues.js @@ -0,0 +1,55 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.changeColumn("issues", "fundingAmount", { + type: Sequelize.STRING, + defaultValue: "0" + }); + + await queryInterface.changeColumn("issues", "fundedAmount", { + type: Sequelize.STRING, + defaultValue: "0" + }); + + await queryInterface.changeColumn("issues", "rewardAmount", { + type: Sequelize.STRING, + defaultValue: "0" + }); + + await queryInterface.bulkUpdate("issues", { + fundingAmount: "0" + }, { + fundingAmount: null + }); + + await queryInterface.bulkUpdate("issues", { + fundedAmount: "0" + }, { + fundedAmount: null + }); + + await queryInterface.bulkUpdate("issues", { + rewardAmount: "0" + }, { + rewardAmount: null + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.changeColumn("issues", "fundingAmount", { + type: Sequelize.STRING, + defaultValue: null + }); + + await queryInterface.changeColumn("issues", "fundedAmount", { + type: Sequelize.STRING, + defaultValue: null + }); + + await queryInterface.changeColumn("issues", "rewardAmount", { + type: Sequelize.STRING, + defaultValue: null + }); + } +}; diff --git a/db/models/benefactor.model.js b/db/models/benefactor.model.js index 0652ae7e6d..d7a3f5d60c 100644 --- a/db/models/benefactor.model.js +++ b/db/models/benefactor.model.js @@ -1,4 +1,5 @@ "use strict"; +const { getValueToLowerCase } = require("../../helpers/db/getters"); const { Model, DataTypes } = require("sequelize"); class Benefactors extends Model { @@ -17,6 +18,9 @@ class Benefactors extends Model { address: { type: DataTypes.STRING, allowNull: false, + get() { + return getValueToLowerCase(this, "address"); + } }, contractId: { type: DataTypes.INTEGER, @@ -29,6 +33,10 @@ class Benefactors extends Model { model: "issue", key: "id" } + }, + withdrawn: { + type: DataTypes.BOOLEAN, + defaultValue: false } }, { diff --git a/db/models/chain-events.model.js b/db/models/chain-events.model.js index e583a18aca..494b3a168b 100644 --- a/db/models/chain-events.model.js +++ b/db/models/chain-events.model.js @@ -2,6 +2,13 @@ const { Model, DataTypes } = require("sequelize"); class ChainEvents extends Model { + id; + name; + lastBlock; + chain_id; + createdAt; + updatedAt; + static init(sequelize) { super.init({ id: { @@ -13,11 +20,13 @@ class ChainEvents extends Model { name: { type: DataTypes.STRING, allowNull: false, - unique: true }, lastBlock: { type: DataTypes.INTEGER }, + chain_id: { + type: DataTypes.INTEGER + }, createdAt: { allowNull: false, type: DataTypes.DATE diff --git a/db/models/chain.model.js b/db/models/chain.model.js new file mode 100644 index 0000000000..841f2e22ae --- /dev/null +++ b/db/models/chain.model.js @@ -0,0 +1,91 @@ +const { getValueToLowerCase } = require("../../helpers/db/getters"); +const {Model, DataTypes} = require("sequelize"); + +class Chain extends Model { + + static init(sqlz) { + super.init({ + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + unique: true + }, + chainId: { + type: DataTypes.INTEGER, + unique: true + }, + chainRpc: { + type: DataTypes.STRING, + }, + chainName: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + chainShortName: { + type: DataTypes.STRING, + allowNull: false, + }, + chainCurrencyName: { + type: DataTypes.STRING, + allowNull: false, + }, + chainCurrencySymbol: { + type: DataTypes.STRING, + allowNull: false, + }, + chainCurrencyDecimals: { + type: DataTypes.INTEGER, + allowNull: false, + }, + registryAddress: { + type: DataTypes.STRING, + get() { + return getValueToLowerCase(this, "registryAddress"); + } + }, + eventsApi: { + type: DataTypes.STRING, + }, + blockScanner: { + type: DataTypes.STRING, + }, + isDefault: { + type: DataTypes.BOOLEAN + }, + color: { + type: DataTypes.STRING, + allowNull: true + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE + } + }, {sequelize: sqlz, modelName: 'chain', tableName: 'chains'}) + }; + + static associate(models) { + this.hasMany(models.network, { + foreignKey: 'chain_id', + sourceKey: 'chainId', + as: "networks" + }); + this.hasMany(models.issue, { + foreignKey: 'chain_id', + sourceKey: 'chainId', + as: "issues" + }); + this.hasMany(models.tokens, { + foreignKey: 'chain_id', + sourceKey: 'chainId', + as: "tokens" + }); + } +} + +module.exports = Chain; \ No newline at end of file diff --git a/db/models/curator-model.js b/db/models/curator-model.js index d3035f9cb6..7473de2e51 100644 --- a/db/models/curator-model.js +++ b/db/models/curator-model.js @@ -1,4 +1,5 @@ "use strict"; +const { getValueToLowerCase } = require("../../helpers/db/getters"); const { Model, DataTypes } = require("sequelize"); class Curators extends Model { @@ -13,6 +14,9 @@ class Curators extends Model { address: { type: DataTypes.STRING, allowNull: false, + get() { + return getValueToLowerCase(this, "address"); + } }, acceptedProposals: { type: DataTypes.INTEGER, diff --git a/db/models/developer.model.js b/db/models/developer.model.js index 4a363283e3..ce4114d4ac 100644 --- a/db/models/developer.model.js +++ b/db/models/developer.model.js @@ -1,4 +1,5 @@ "use strict"; +const { getValueToLowerCase } = require("../../helpers/db/getters"); const { Model, DataTypes } = require("sequelize"); class Developer extends Model { /** @@ -9,7 +10,12 @@ class Developer extends Model { static init(sequelize) { super.init({ githubHandle: DataTypes.STRING, - address: DataTypes.STRING, + address: { + type: DataTypes.STRING, + get() { + return getValueToLowerCase(this, "address"); + } + }, issueId: DataTypes.INTEGER }, { diff --git a/db/models/dispute-model.js b/db/models/dispute-model.js index 1481bdd99e..8e4b88f398 100644 --- a/db/models/dispute-model.js +++ b/db/models/dispute-model.js @@ -1,4 +1,5 @@ "use strict"; +const { getValueToLowerCase } = require("../../helpers/db/getters"); const { Model, DataTypes } = require("sequelize"); class Disputes extends Model { @@ -20,7 +21,10 @@ class Disputes extends Model { }, address: { type: DataTypes.STRING, - allowNull: false + allowNull: false, + get() { + return getValueToLowerCase(this, "address"); + } }, proposalId: { type: DataTypes.INTEGER, diff --git a/db/models/index.js b/db/models/index.js index e1a94f103c..44b1e94de5 100644 --- a/db/models/index.js +++ b/db/models/index.js @@ -1,4 +1,4 @@ -import { Sequelize } from "sequelize"; +import {Sequelize} from "sequelize"; import * as DatabaseConfig from "../config"; import ChainEvents from "./chain-events.model"; @@ -17,6 +17,7 @@ import Benefactors from './benefactor.model'; import Curators from './curator-model' import Disputes from './dispute-model'; import LeaderBoard from './leaderboard.model'; +import Chain from "./chain.model"; import ProposalDistributions from './proposal-distributions.model'; import HeaderInformation from './header-information'; import KycSession from './kyc-session.model' @@ -45,6 +46,7 @@ Database.curator = Curators; Database.dispute = Disputes; Database.leaderBoard = LeaderBoard; Database.proposalDistributions = ProposalDistributions; +Database.chain = Chain; Database.headerInformation = HeaderInformation; Database.kycSession = KycSession; diff --git a/db/models/issue.model.js b/db/models/issue.model.js index b0ef5d5680..57e8c0ddee 100644 --- a/db/models/issue.model.js +++ b/db/models/issue.model.js @@ -1,67 +1,102 @@ "use strict"; +const { getValueToLowerCase } = require("../../helpers/db/getters"); const { Model, DataTypes } = require("sequelize"); +const { BigNumber } = require("bignumber.js"); class Issue extends Model { - /** - * Helper method for defining associations. - * This method is not a part of Sequelize lifecycle. - * The `models/index` file will call this method automatically. - */ static init(sequelize) { super.init({ - issueId: DataTypes.INTEGER, - githubId: DataTypes.STRING, - state: DataTypes.STRING, - creatorAddress: DataTypes.STRING, - creatorGithub: DataTypes.STRING, - amount: DataTypes.STRING, - fundingAmount: DataTypes.STRING, - fundedAmount: DataTypes.STRING, - repository_id: DataTypes.STRING, - title: DataTypes.TEXT, - body: DataTypes.TEXT, - branch: DataTypes.STRING, - working: { - type: DataTypes.ARRAY(DataTypes.STRING) - }, - merged: DataTypes.STRING, - seoImage: DataTypes.STRING, - network_id: DataTypes.INTEGER, - contractId: DataTypes.INTEGER, - tokenId: { - type: DataTypes.INTEGER, - allowNull: true - }, - fundedAt: { - type: DataTypes.DATE, - allowNull: true - }, - tags: { - type: DataTypes.ARRAY(DataTypes.STRING) - }, - isKyc:{ - type: DataTypes.BOOLEAN, - default: false - }, - kycTierList:{ - type: DataTypes.ARRAY(DataTypes.INTEGER), - default: [] - }, - tags: { - type: DataTypes.ARRAY(DataTypes.STRING) - }, - isKyc:{ - type: DataTypes.BOOLEAN, - defaultValue: false - }, - kycTierList:{ - type: DataTypes.ARRAY(DataTypes.INTEGER), - default: [] + issueId: DataTypes.INTEGER, + githubId: DataTypes.STRING, + state: DataTypes.STRING, + creatorAddress: { + type: DataTypes.STRING, + get() { + return getValueToLowerCase(this, "creatorAddress"); } + }, + creatorGithub: DataTypes.STRING, + amount: DataTypes.STRING, + fundingAmount: { + type: DataTypes.STRING, + defaultValue: "0" + }, + fundedAmount: { + type: DataTypes.STRING, + defaultValue: "0" + }, + rewardAmount: { + type: DataTypes.STRING, + defaultValue: "0" + }, + repository_id: DataTypes.STRING, + title: DataTypes.TEXT, + body: DataTypes.TEXT, + branch: DataTypes.STRING, + working: DataTypes.ARRAY(DataTypes.STRING), + merged: DataTypes.STRING, + seoImage: DataTypes.STRING, + network_id: DataTypes.INTEGER, + contractId: DataTypes.INTEGER, + transactionalTokenId: DataTypes.INTEGER, + rewardTokenId: DataTypes.INTEGER, + fundedAt: DataTypes.DATE, + tags: DataTypes.ARRAY(DataTypes.STRING), + chain_id: DataTypes.INTEGER, + isDraft: { + type: DataTypes.VIRTUAL, + get() { + return this.state === "draft"; + } + }, + isClosed: { + type: DataTypes.VIRTUAL, + get() { + return this.state === "closed"; + } + }, + isCanceled: { + type: DataTypes.VIRTUAL, + get() { + return this.state === "canceled"; + } + }, + isFundingRequest: { + type: DataTypes.VIRTUAL, + get() { + return BigNumber(this.fundingAmount).gt(0); + } + }, + isFunded: { + type: DataTypes.VIRTUAL, + get() { + return BigNumber(this.fundedAmount).gte(this.fundingAmount); + } + }, + hasReward: { + type: DataTypes.VIRTUAL, + get() { + return BigNumber(this.rewardAmount).gt(0); + } + }, + fundedPercent: { + type: DataTypes.VIRTUAL, + get() { + return BigNumber(this.fundedAmount).dividedBy(this.fundingAmount).multipliedBy(100).toNumber(); + } + }, + isKyc:{ + type: DataTypes.BOOLEAN, + defaultValue: false + }, + kycTierList:{ + type: DataTypes.ARRAY(DataTypes.INTEGER), + default: [] + } }, - { - sequelize, - modelName: "issue" - }); + { + sequelize, + modelName: "issue" + }); } static associate(models) { @@ -106,9 +141,19 @@ class Issue extends Model { sourceKey: "id" }); this.belongsTo(models.tokens, { - foreignKey: "tokenId", + foreignKey: "transactionalTokenId", + sourceKey: "id", + as: "transactionalToken" + }); + this.belongsTo(models.tokens, { + foreignKey: "rewardTokenId", sourceKey: "id", - as: "token" + as: "rewardToken" + }); + this.belongsTo(models.chain, { + foreignKey: "chain_id", + targetKey: "chainId", + as: "chain" }); } } diff --git a/db/models/leaderboard.model.js b/db/models/leaderboard.model.js index 4b0bc66165..97db79e13a 100644 --- a/db/models/leaderboard.model.js +++ b/db/models/leaderboard.model.js @@ -1,4 +1,5 @@ "use strict"; +const { getValueToLowerCase } = require("../../helpers/db/getters"); const { Model, DataTypes } = require("sequelize"); class LeaderBoard extends Model { @@ -13,7 +14,10 @@ class LeaderBoard extends Model { address: { type: DataTypes.STRING, allowNull: false, - unique: true + unique: true, + get() { + return getValueToLowerCase(this, "address"); + } }, numberNfts: { type: DataTypes.INTEGER, diff --git a/db/models/mergeproposal.js b/db/models/mergeproposal.js index de5e38d4b0..f1d1178b21 100644 --- a/db/models/mergeproposal.js +++ b/db/models/mergeproposal.js @@ -1,4 +1,5 @@ "use strict"; +const { getValueToLowerCase } = require("../../helpers/db/getters"); const { Model, DataTypes } = require("sequelize"); class MergeProposal extends Model { /** @@ -12,7 +13,12 @@ class MergeProposal extends Model { pullRequestId: DataTypes.INTEGER, githubLogin: DataTypes.STRING, contractId: DataTypes.INTEGER, - creator: DataTypes.STRING, + creator: { + type: DataTypes.STRING, + get() { + return getValueToLowerCase(this, "creator"); + } + }, network_id: DataTypes.INTEGER, contractCreationDate: { type: DataTypes.STRING(255), @@ -31,13 +37,13 @@ class MergeProposal extends Model { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false - }, + } }, - { - sequelize, - modelName: "mergeProposal", - tableName: "merge_proposals" - }); + { + sequelize, + modelName: "mergeProposal", + tableName: "merge_proposals" + }); } static associate(models) { @@ -48,6 +54,12 @@ class MergeProposal extends Model { as: "distributions" }); + this.hasMany(models.dispute, { + foreignKey: "proposalId", + sourceKey: "id", + as: "disputes" + }); + this.belongsTo(models.issue, { foreignKey: "issueId", sourceKey: "id", diff --git a/db/models/network.model.js b/db/models/network.model.js index 3209c0ea7d..70e5b2bba9 100644 --- a/db/models/network.model.js +++ b/db/models/network.model.js @@ -1,17 +1,28 @@ "use strict"; -const { Model, DataTypes } = require("sequelize"); +const {Model, DataTypes} = require("sequelize"); +const {getValueToLowerCase} = require("../../helpers/db/getters"); class Network extends Model { static init(sequelize) { super.init({ - creatorAddress: DataTypes.STRING, + creatorAddress: { + type: DataTypes.STRING, + get() { + return getValueToLowerCase(this, "creatorAddress"); + } + }, name: { type: DataTypes.STRING, - unique: true + unique: "network_chain_unique" }, description: DataTypes.STRING, colors: DataTypes.JSON, - networkAddress: DataTypes.STRING, + networkAddress: { + type: DataTypes.STRING, + get() { + return getValueToLowerCase(this, "networkAddress"); + } + }, logoIcon: DataTypes.STRING, fullLogo: DataTypes.STRING, isClosed: { @@ -20,11 +31,23 @@ class Network extends Model { }, isRegistered: { type: DataTypes.BOOLEAN, - defaultValue : false + defaultValue: false }, councilMembers: { type: DataTypes.ARRAY(DataTypes.STRING) }, + chain_id: { + type: DataTypes.INTEGER, + unique: "network_chain_unique" + }, + network_token_id: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: "tokens", + key: "id" + } + }, createdAt: DataTypes.DATE, updatedAt: DataTypes.DATE, allowCustomTokens: { @@ -35,12 +58,44 @@ class Network extends Model { type: DataTypes.BOOLEAN, allowNull: true, defaultValue: false + }, + councilAmount: { + type: DataTypes.STRING, + allowNull: true + }, + disputableTime: { + type: DataTypes.BIGINT, + allowNull: true + }, + draftTime: { + type: DataTypes.BIGINT, + allowNull: true + }, + oracleExchangeRate: { + type: DataTypes.FLOAT, + allowNull: true + }, + mergeCreatorFeeShare: { + type: DataTypes.FLOAT, + allowNull: true + }, + percentageNeededForDispute: { + type: DataTypes.FLOAT, + allowNull: true + }, + cancelableTime: { + type: DataTypes.BIGINT, + allowNull: true + }, + proposerFeeShare: { + type: DataTypes.FLOAT, + allowNull: true } - }, - { + }, + { sequelize, modelName: "network" - }); + }); } static associate(models) { @@ -57,8 +112,7 @@ class Network extends Model { foreignKey: "network_id", sourceKey: "id" }); - this.belongsToMany(models.tokens, { through: 'network_tokens' }); - + this.hasMany(models.pullRequest, { foreignKey: "network_id", sourceKey: "id", @@ -70,12 +124,26 @@ class Network extends Model { sourceKey: "id", as: "mergeProposals" }); - + this.hasMany(models.curator, { foreignKey: "networkId", sourceKey: "id", as: "curators" }); + + this.belongsTo(models.chain, { + foreignKey: "chain_id", + targetKey: "chainId", + as: "chain" + }); + + this.belongsTo(models.tokens, { + foreignKey: "network_token_id", + sourceKey: "id", + as: "networkToken" + }); + + this.belongsToMany(models.tokens, {through: 'network_tokens'}); } } diff --git a/db/models/proposal-distributions.model.js b/db/models/proposal-distributions.model.js index 2cccc2a5e8..740be78c95 100644 --- a/db/models/proposal-distributions.model.js +++ b/db/models/proposal-distributions.model.js @@ -1,4 +1,5 @@ "use strict"; +const { getValueToLowerCase } = require("../../helpers/db/getters"); const { Model, DataTypes } = require("sequelize"); class ProposalDistributions extends Model { @@ -10,10 +11,13 @@ class ProposalDistributions extends Model { primaryKey: true, unique: true }, - address: { + recipient: { type: DataTypes.STRING(255), - allowNull: false - }, + allowNull: false, + get() { + return getValueToLowerCase(this, "recipient"); + } + }, percentage: { type: DataTypes.INTEGER, allowNull: false diff --git a/db/models/pullRequest.model.js b/db/models/pullRequest.model.js index 7a39e809ac..004d762c6d 100644 --- a/db/models/pullRequest.model.js +++ b/db/models/pullRequest.model.js @@ -1,4 +1,5 @@ "use strict"; +const { getValueToLowerCase } = require("../../helpers/db/getters"); const { Model, DataTypes } = require("sequelize"); class PullRequest extends Model { /** @@ -21,7 +22,10 @@ class PullRequest extends Model { }, userAddress: { type: DataTypes.STRING, - allowNull: true + allowNull: true, + get() { + return getValueToLowerCase(this, "userAddress"); + } }, status: { type: DataTypes.STRING, @@ -36,12 +40,24 @@ class PullRequest extends Model { defaultValue: [] }, network_id: DataTypes.INTEGER, + isCanceled: { + type: DataTypes.VIRTUAL, + get() { + return this.status === "canceled"; + } + }, + isReady: { + type: DataTypes.VIRTUAL, + get() { + return this.status === "ready"; + } + } }, - { - sequelize, - modelName: "pullRequest", - tableName: "pull_requests" - }); + { + sequelize, + modelName: "pullRequest", + tableName: "pull_requests" + }); } static associate(models) { diff --git a/db/models/repositories.model.js b/db/models/repositories.model.js index 111e34acdf..63c950649e 100644 --- a/db/models/repositories.model.js +++ b/db/models/repositories.model.js @@ -1,11 +1,6 @@ "use strict"; const { Model, DataTypes } = require("sequelize"); class Repositories extends Model { - /** - * Helper method for defining associations. - * This method is not a part of Sequelize lifecycle. - * The `models/index` file will call this method automatically. - */ static init(sequelize) { super.init({ id: { @@ -17,23 +12,21 @@ class Repositories extends Model { githubPath: { type: DataTypes.STRING, allowNull: false, - unique: true + unique: "repositories_networks_unique" }, - network_id: DataTypes.INTEGER + network_id: { + type: DataTypes.INTEGER, + unique: "repositories_networks_unique" + } }, - { - sequelize, - modelName: "repositories", - tableName: "repositories", - timestamps: false - }); + { + sequelize, + modelName: "repositories", + tableName: "repositories", + timestamps: false + }); } static associate(models) { - // this.belongsTo(models.issue, { - // foreignKey: 'issueId', - // sourceKey: 'id' - // }); - // this.hasMany(models.issue, { foreignKey: "repository_id", sourceKey: "id" diff --git a/db/models/tokens.model.js b/db/models/tokens.model.js index e182eb3d3e..a78f075844 100644 --- a/db/models/tokens.model.js +++ b/db/models/tokens.model.js @@ -1,4 +1,5 @@ "use strict"; +const { getValueToLowerCase } = require("../../helpers/db/getters"); const { Model, DataTypes } = require("sequelize"); class Tokens extends Model { @@ -20,7 +21,10 @@ class Tokens extends Model { }, address: { type: DataTypes.STRING, - allowNull: false + allowNull: false, + get() { + return getValueToLowerCase(this, "address"); + } }, isTransactional: { type: DataTypes.BOOLEAN, @@ -33,6 +37,10 @@ class Tokens extends Model { isAllowed: { type: DataTypes.BOOLEAN, allowNull: true + }, + chain_id: { + type: DataTypes.INTEGER, + unique: "network_chain_unique" } }, { @@ -44,11 +52,16 @@ class Tokens extends Model { ); } static associate(models) { - this.belongsToMany(models.network, { through: 'network_tokens' }); this.hasMany(models.issue, { - foreignKey: "tokenId", + foreignKey: "transactionalTokenId", sourceKey: "id" }); + this.belongsToMany(models.network, { through: 'network_tokens' }); + this.belongsTo(models.chain, { + foreignKey: "chain_id", + targetKey: "chainId", + as: "chain" + }); } } diff --git a/db/models/user-payments.js b/db/models/user-payments.js index e73f5ad1d2..74565e65a8 100644 --- a/db/models/user-payments.js +++ b/db/models/user-payments.js @@ -1,10 +1,16 @@ "use strict"; +const { getValueToLowerCase } = require("../../helpers/db/getters"); const { Model, DataTypes } = require("sequelize"); class UserPayments extends Model { static init(sequelize) { super.init({ - address: DataTypes.STRING, + address: { + type: DataTypes.STRING, + get() { + return getValueToLowerCase(this, "address"); + } + }, ammount: DataTypes.INTEGER, issueId: DataTypes.STRING, transactionHash: DataTypes.STRING diff --git a/db/models/user.js b/db/models/user.js index 113fbf15b4..0207881425 100644 --- a/db/models/user.js +++ b/db/models/user.js @@ -1,4 +1,5 @@ "use strict"; +const { getValueToLowerCase } = require("../../helpers/db/getters"); const { Model, DataTypes } = require("sequelize"); class User extends Model { @@ -13,7 +14,10 @@ class User extends Model { githubLogin: DataTypes.STRING, address: { type: DataTypes.STRING, - unique: true + unique: true, + get() { + return getValueToLowerCase(this, "address"); + } }, resetedAt: { type: DataTypes.DATE, diff --git a/graphql/repository.ts b/graphql/repository.ts index 6304843cee..e25765678b 100644 --- a/graphql/repository.ts +++ b/graphql/repository.ts @@ -56,7 +56,7 @@ export const Details = `query Details($repo: String!, $owner: String!) { repository(name: $repo, owner: $owner) { id - labels(first: 1, query: "draft") { + labels(first: 100) { nodes { id name diff --git a/helpers/calculateDistributedAmounts.ts b/helpers/calculateDistributedAmounts.ts index bedaaa67c6..35ddcf7746 100644 --- a/helpers/calculateDistributedAmounts.ts +++ b/helpers/calculateDistributedAmounts.ts @@ -2,7 +2,7 @@ import { ProposalDetail } from "@taikai/dappkit"; import { Defaults } from "@taikai/dappkit"; import BigNumber from "bignumber.js"; -import { DistributedAmounts } from "interfaces/proposal"; +import { DistributedAmounts, ProposalDistribution } from "interfaces/proposal"; const bigNumberPercentage = (value1: BigNumber, value2: BigNumber) => value1.dividedBy(value2).multipliedBy(100).toFixed(2); @@ -12,7 +12,8 @@ export default function calculateDistributedAmounts(treasury, mergerFee: string | number, proposerFee: string | number, bountyAmount: BigNumber, - proposalPercents: ProposalDetail[]): DistributedAmounts { + proposalPercents: ProposalDetail[] | ProposalDistribution[]): + DistributedAmounts { let treasuryAmount = BigNumber(0); if (treasury.treasury && treasury.treasury !== Defaults.nativeZeroAddress) diff --git a/helpers/chain-from-header.ts b/helpers/chain-from-header.ts new file mode 100644 index 0000000000..982e9ccb63 --- /dev/null +++ b/helpers/chain-from-header.ts @@ -0,0 +1,15 @@ +import {NextApiRequest} from "next"; +import {Op} from "sequelize"; + +import models from "db/models"; + +export async function chainFromHeader(req: NextApiRequest, force = false) { + if (force && !req.headers.chain) + return null; + + const where = { + ... !req.headers.chain ? {isDefault: {[Op.eq]: true}} : {chainId: {[Op.eq]: +req.headers.chain}}, + } + + return models.chain.findOne({where}); +} \ No newline at end of file diff --git a/helpers/constants.ts b/helpers/constants.ts index 2b10cc39e9..1a6af9f776 100644 --- a/helpers/constants.ts +++ b/helpers/constants.ts @@ -13,11 +13,19 @@ export const BOUNTY_TITLE_LIMIT = 131; export const MAX_TAGS = 3; export const IM_AN_ADMIN = `I'm an admin`; export const NOT_AN_ADMIN = `Not an admin`; +export const WANT_TO_CREATE_NETWORK = `I want to create a network`; export const MISSING_ADMIN_SIGNATURE = `Missing admin signature`; export const NOT_ADMIN_WALLET = `Wrong wallet`; export const MISSING_CHAIN_ID = `Missing chain id`; +export const CHAIN_ID_NOT_SUPPORTED = `Given chain id is not supported`; +export const NO_NETWORKS_FOR_GIVEN_CHAIN_ID = `No networks for given chain id`; +export const CHAIN_NOT_CONFIGURED = `Chain not configured`; +export const WRONG_PARAM_URL = name => `Url ${name} was malformed`; +export const WRONG_PARAM_ADDRESS = name => `Address ${name} is malformed or 0 address`; +export const SEVEN_DAYS_IN_MS = 60 * 60 * 24 * 7 * 1000; export const IM_AM_CREATOR_ISSUE = `I am the owner of this bounty`; export const NOT_AN_CREATOR_ISSUE = `Not an creator issue`; export const MISSING_CREATOR_ISSUE_SIGNATURE = `Missing creator issue signature`; +export const UNSUPPORTED_CHAIN = "unsupported"; export const MAX_INTEGER_SOLIDITY = 2**256 - 1; export const NETWORK_DIVISOR = 1000000; \ No newline at end of file diff --git a/helpers/db/getters.js b/helpers/db/getters.js new file mode 100644 index 0000000000..c0a2729d45 --- /dev/null +++ b/helpers/db/getters.js @@ -0,0 +1,16 @@ +const { Model } = require("sequelize"); + +/** + * + * @param {Model} node + * @param {string} dataValue + * @returns {string | null} value + */ +function getValueToLowerCase(node, dataValue) { + const rawValue = node.getDataValue(dataValue); + return rawValue ? rawValue.toLowerCase() : null; +} + +module.exports = { + getValueToLowerCase +}; \ No newline at end of file diff --git a/helpers/decode-message.ts b/helpers/decode-message.ts index adc87580f3..7a05276651 100644 --- a/helpers/decode-message.ts +++ b/helpers/decode-message.ts @@ -1,6 +1,6 @@ import {recoverTypedSignature,} from "@metamask/eth-sig-util"; -import {messageFor} from "./message-for"; +import {messageFor} from "helpers/message-for"; export default function decodeMessage(chainId, message = "", signature: string, assumedOwner: string): boolean { if (!signature) @@ -12,7 +12,5 @@ export default function decodeMessage(chainId, message = "", signature: string, version: 'V4' } - console.log(recoverTypedSignature(params)) - return recoverTypedSignature(params)?.toLowerCase() === assumedOwner.toLowerCase(); } \ No newline at end of file diff --git a/helpers/handleNetworksValuesApi.ts b/helpers/handleNetworksValuesApi.ts index 50e83787d6..255713f552 100644 --- a/helpers/handleNetworksValuesApi.ts +++ b/helpers/handleNetworksValuesApi.ts @@ -5,7 +5,12 @@ const handleNetwork = (issues) => issue.network.dataValues = { name: issue.network.name, logoIcon: issue.network.logoIcon, - colors: { primary: issue.network.colors?.primary } + colors: { primary: issue.network?.colors?.primary }, + chain: { + chainId: issue.network?.chain?.chainId, + chainShortName: issue.network?.chain?.chainShortName, + color: issue.network?.chain?.color + } } return issue }); diff --git a/helpers/is-admin.ts b/helpers/is-admin.ts new file mode 100644 index 0000000000..ac3b97131c --- /dev/null +++ b/helpers/is-admin.ts @@ -0,0 +1,21 @@ +import {NextApiRequest} from "next"; +import getConfig from "next/config"; + +import {IM_AN_ADMIN} from "helpers/constants"; +import decodeMessage from "helpers/decode-message"; + +export function isAdmin(req: NextApiRequest) { + const { publicRuntimeConfig } = getConfig(); + + const headers = req.headers; + const adminWallet = publicRuntimeConfig?.adminWallet?.toLowerCase(); + const wallet = (headers.wallet as string)?.toLowerCase(); + const chainId = (headers.chain as string); + const signature = headers.signature as string; + + return wallet && + wallet === adminWallet && + chainId && + signature && + decodeMessage(chainId, IM_AN_ADMIN, signature, adminWallet); +} \ No newline at end of file diff --git a/helpers/issue.ts b/helpers/issue.ts new file mode 100644 index 0000000000..2f30e58660 --- /dev/null +++ b/helpers/issue.ts @@ -0,0 +1,57 @@ +import BigNumber from "bignumber.js"; + +import { bountyReadyPRsHasNoInvalidProposals } from "helpers/proposal"; + +import { IssueBigNumberData, IssueData } from "interfaces/issue-data"; + +export const OPEN_STATES = ["draft", "open", "ready", "proposal"]; + +export const pullRequestParser = (issue: IssueData) => + issue.pullRequests.map(pullRequest => ({ + ...pullRequest, + createdAt: new Date(pullRequest.createdAt), + updatedAt: new Date(pullRequest.updatedAt), + isCancelable: !issue.mergeProposals?.some(proposal => proposal.pullRequestId === pullRequest.id) + })); + +export const mergeProposalParser = (issue: IssueData) => + issue.mergeProposals?.map(proposal => ({ + ...proposal, + disputeWeight: BigNumber(proposal.disputeWeight), + isMerged: issue.merged !== null && +proposal?.contractId === +issue.merged, + createdAt: new Date(proposal.createdAt), + updatedAt: new Date(proposal.updatedAt), + contractCreationDate: new Date(+proposal.contractCreationDate), + distributions: proposal.distributions?.map(distribution => ({ + ...distribution, + createdAt: new Date(distribution.createdAt), + updatedAt: new Date(distribution.updatedAt) + })), + disputes: proposal.disputes?.map(dispute => ({ + ...dispute, + createdAt: new Date(dispute.createdAt), + updatedAt: new Date(dispute.updatedAt), + weight: BigNumber(dispute.weight) + })) + })); + +export const benefactorsParser = (issue: IssueData) => + issue.benefactors?.map(benefactor => ({ + ...benefactor, + amount: BigNumber(benefactor.amount) + })); + +export const issueParser = (issue: IssueData) : IssueBigNumberData => ({ + ...issue, + createdAt: new Date(issue.createdAt), + updatedAt: new Date(issue.updatedAt), + fundedAt: new Date(issue.fundedAt), + isReady: bountyReadyPRsHasNoInvalidProposals(issue) !== 0, + amount: BigNumber(issue.amount), + fundingAmount: BigNumber(issue.fundingAmount), + fundedAmount: BigNumber(issue.fundedAmount), + rewardAmount: BigNumber(issue.rewardAmount), + pullRequests: pullRequestParser(issue), + mergeProposals: mergeProposalParser(issue), + benefactors: benefactorsParser(issue) +}); \ No newline at end of file diff --git a/helpers/proposal.ts b/helpers/proposal.ts index ca8dcf74ee..a8bd9b92be 100644 --- a/helpers/proposal.ts +++ b/helpers/proposal.ts @@ -1,7 +1,7 @@ -import { Network_v2 } from "@taikai/dappkit"; -import { Bounty } from "@taikai/dappkit"; import { addSeconds } from "date-fns"; +import { IssueBigNumberData, IssueData } from "interfaces/issue-data"; + export function isProposalDisputable(createdAt: Date | number, disputableTime: number, chainTime?: number): boolean { return (chainTime && new Date(chainTime) || (new Date())) <= addSeconds(new Date(createdAt), disputableTime); } @@ -10,22 +10,16 @@ export function isProposalDisputable(createdAt: Date | number, disputableTime: n * @returns 0: No ready PRs, 1: Invalid proposals, 2: Valid proposals, 3: No proposals * @throws Error */ -export const bountyReadyPRsHasNoInvalidProposals = async (bounty: Bounty, network: Network_v2) : Promise => { - const readyPRsIds = bounty.pullRequests.filter(pr => pr.ready && !pr.canceled).map(pr => pr.id); +export const bountyReadyPRsHasNoInvalidProposals = (bounty: IssueData | IssueBigNumberData) : number => { + const readyPRsIds = bounty.pullRequests.filter(pr => pr.isReady && !pr.isCanceled).map(pr => pr.id); if (!readyPRsIds.length) return 0; - const readyPRsWithoutProposals = readyPRsIds.filter(pr => !bounty.proposals.find(p => p.prId === pr)); + const readyPRsWithoutProposals = readyPRsIds.filter(pr => !bounty.mergeProposals.find(p => p.pullRequestId === pr)); if (readyPRsWithoutProposals.length) return 3; - const proposalsWithDisputeState = - await Promise.all(bounty.proposals - .filter(p => readyPRsIds.includes(p.prId)) - .map(async p => ({ - ...p, - isDisputed: await network.isProposalDisputed(bounty.id, p.id) - }))); + const proposalsWithDisputeState = bounty.mergeProposals.filter(p => readyPRsIds.includes(p.pullRequestId)); const invalidProposals = proposalsWithDisputeState.filter(p => p.isDisputed || p.refusedByBountyOwner); diff --git a/helpers/res-json-message.ts b/helpers/res-json-message.ts new file mode 100644 index 0000000000..97d383f0da --- /dev/null +++ b/helpers/res-json-message.ts @@ -0,0 +1,5 @@ +import {NextApiResponse} from "next"; + +export function resJsonMessage(message: string|string[], res: NextApiResponse, status = 200) { + return res.status(status).json({message}); +} \ No newline at end of file diff --git a/helpers/string.ts b/helpers/string.ts index 81c2ea747f..1c4de4c20d 100644 --- a/helpers/string.ts +++ b/helpers/string.ts @@ -63,3 +63,5 @@ export const highlightText = (str: string, className?: string) => { }; export const trimString = (string: string, at = 15) => string.length > at ? string.substring(0, at) + '...' : string; + +export const toLower = (str: string) => str?.toLowerCase(); \ No newline at end of file diff --git a/helpers/transactions.ts b/helpers/transactions.ts index a2df1f57f1..2ab777a5ea 100644 --- a/helpers/transactions.ts +++ b/helpers/transactions.ts @@ -1,6 +1,10 @@ +import { SEVEN_DAYS_IN_MS } from "helpers/constants"; + import { TransactionStatus } from "interfaces/enums/transaction-status"; import { BlockTransaction, SimpleBlockTransactionPayload } from "interfaces/transaction"; +import { WinStorage } from "services/win-storage"; + export const parseTransaction = (transaction, simpleTx?: SimpleBlockTransactionPayload) => { return { @@ -14,4 +18,17 @@ export const parseTransaction = (transaction, ? TransactionStatus.completed : TransactionStatus.failed }; +} + +export const getTransactionsStorageKey = + (walletAddress: string, chainId: string) => `bepro.transactions:${walletAddress}:${chainId}`; + +export const saveTransactionsToStorage = (transformed) => { + const walletAddress = sessionStorage.getItem("currentWallet")?.toLowerCase(); + const chainId = sessionStorage.getItem("currentChainId"); + + if (!walletAddress || !chainId) return; + + const storage = new WinStorage(getTransactionsStorageKey(walletAddress, chainId), SEVEN_DAYS_IN_MS, 'localStorage'); + storage.value = JSON.stringify(transformed); } \ No newline at end of file diff --git a/helpers/truncate-address.ts b/helpers/truncate-address.ts index 0012fe08e7..75138e052a 100644 --- a/helpers/truncate-address.ts +++ b/helpers/truncate-address.ts @@ -1,4 +1,4 @@ export const truncateAddress = (address, left = 6, right = 4, - separator = "...") => [address?.substr(0, left), separator, address?.substr(-right)].join(""); + separator = "...") => [address?.substring(0, left), separator, address?.substr(-right)].join(""); diff --git a/interfaces/api.ts b/interfaces/api.ts index 9768bb19f3..f150937581 100644 --- a/interfaces/api.ts +++ b/interfaces/api.ts @@ -17,6 +17,8 @@ export interface PastEventsParams { id? :number; fromBlock?: number; toBlock?: number; + chainId?: string; + issueId?: string; } export interface SearchNetworkParams { @@ -31,6 +33,8 @@ export interface SearchNetworkParams { isRegistered?: boolean; isDefault?: boolean; isNeedCountsAndTokensLocked?: boolean; + chainId?: string; + chainShortName?: string; } export interface SearchActiveNetworkParams { @@ -40,6 +44,7 @@ export interface SearchActiveNetworkParams { order?: string; isClosed?: boolean; isRegistered?: boolean; + name?: string; } export interface CreatePrePullRequestParams extends RequestParams { diff --git a/interfaces/application-state.ts b/interfaces/application-state.ts index c13e3b7810..4061e8d4ab 100644 --- a/interfaces/application-state.ts +++ b/interfaces/application-state.ts @@ -1,21 +1,24 @@ import {Dispatch} from "react"; -import { TreasuryInfo } from "@taikai/dappkit"; - -import {XReducerAction} from "../contexts/reducers/reducer"; -import DAO from "../services/dao-service"; -import {SettingsType, Tier} from "../types/settings"; -import {Balance} from "./balance-state"; -import {BountyExtended} from "./bounty"; -import {BranchesList} from "./branches-list"; -import {IssueBigNumberData, IssueDataComment} from "./issue-data"; -import { kycSession } from "./kyc-session"; -import {LoadingState} from "./loading-state"; -import {Network} from "./network"; -import {ForkInfo, ForksList, RepoInfo, ReposList} from "./repos-list"; -import {ToastNotification} from "./toast-notification"; -import {Token} from "./token"; -import {BlockTransaction, SimpleBlockTransactionPayload, UpdateBlockTransaction} from "./transaction"; +import {TreasuryInfo} from "@taikai/dappkit"; + +import {XReducerAction} from "contexts/reducers/reducer"; + +import {Balance} from "interfaces/balance-state"; +import {BranchesList} from "interfaces/branches-list"; +import {IssueBigNumberData, IssueDataComment} from "interfaces/issue-data"; +import {kycSession} from "interfaces/kyc-session"; +import {LoadingState} from "interfaces/loading-state"; +import {Network} from "interfaces/network"; +import {ForkInfo, ForksList, RepoInfo, ReposList} from "interfaces/repos-list"; +import {SupportedChainData} from "interfaces/supported-chain-data"; +import {ToastNotification} from "interfaces/toast-notification"; +import {Token} from "interfaces/token"; +import {BlockTransaction, SimpleBlockTransactionPayload, UpdateBlockTransaction} from "interfaces/transaction"; + +import DAO from "services/dao-service"; + +import {SettingsType, Tier} from "types/settings"; export interface ServiceNetworkReposActive extends RepoInfo { forks?: ForkInfo[]; @@ -53,10 +56,10 @@ export interface ServiceNetwork { lastVisited: string; active: Network | null; repos: ServiceNetworkRepos | null; - networkToken: Token; times: NetworkTimes; amounts: NetworkAmounts; noDefaultNetwork?: boolean; + availableChains?: SupportedChainData[]; tokens: {transactional: Token[]; reward: Token[];} | null; } @@ -69,7 +72,12 @@ export interface ServiceState { export interface ConnectedChain { id: string; - name: string + name: string; + shortName: string; + explorer?: string; + events?: string; + registry?: string; + matchWithNetworkChain?: boolean; } export interface CurrentUserState { @@ -81,6 +89,8 @@ export interface CurrentUserState { accessToken?: string; connected?: boolean; signature?: string; + isAdmin?: boolean; + hasRegisteredNetwork?: boolean; kyc?: kycSession; kycSession?: kycSession; } @@ -90,7 +100,6 @@ export interface CurrentBounty { lastUpdated: number; kycSteps?: Tier[]; data: IssueBigNumberData; - chainData: BountyExtended; } export interface State { @@ -102,6 +111,7 @@ export interface State { currentUser: CurrentUserState | null, connectedChain: ConnectedChain | null, currentBounty: CurrentBounty | null, + supportedChains: SupportedChainData[] | null, show: { [key: string]: boolean; } diff --git a/interfaces/curators.ts b/interfaces/curators.ts index 546cd6e29b..6a46ea8659 100644 --- a/interfaces/curators.ts +++ b/interfaces/curators.ts @@ -15,4 +15,5 @@ export interface SearchCuratorParams { networkName?: string; sortBy?: string; order?: string; - } \ No newline at end of file + chainShortName?: string; +} \ No newline at end of file diff --git a/interfaces/db/base.ts b/interfaces/db/base.ts new file mode 100644 index 0000000000..52f97068b7 --- /dev/null +++ b/interfaces/db/base.ts @@ -0,0 +1,5 @@ +export interface BaseModel { + id: number; + createdAt: Date; + updatedAt: Date; +} \ No newline at end of file diff --git a/interfaces/enums/app-state-reduce-id.ts b/interfaces/enums/app-state-reduce-id.ts index a857ea593d..9a001059c2 100644 --- a/interfaces/enums/app-state-reduce-id.ts +++ b/interfaces/enums/app-state-reduce-id.ts @@ -21,9 +21,11 @@ export enum AppStateReduceId { CurrentUser, CurrentUserWallet, ConnectedChain, + ConnectedChainMatch, Show, CurrentBounty, CurrentBountyChainData, Spinners, - NetworkActiveRepoProps + SupportedChains, + NetworkActiveRepoProps, } \ No newline at end of file diff --git a/interfaces/issue-data.ts b/interfaces/issue-data.ts index f8bfc6343d..5e82945302 100644 --- a/interfaces/issue-data.ts +++ b/interfaces/issue-data.ts @@ -1,9 +1,9 @@ import BigNumber from "bignumber.js"; -import {INetworkProposal, Proposal} from "interfaces/proposal"; -import {Token} from "interfaces/token"; - -import {Payment} from "./payments"; +import { Network } from "interfaces/network"; +import { Payment } from "interfaces/payments"; +import { Proposal, INetworkProposal } from "interfaces/proposal"; +import { Token } from "interfaces/token"; export type IssueState = | "pending" @@ -25,6 +25,7 @@ export interface IssueData { amount?: string; fundingAmount?: string; fundedAmount?: string; + rewardAmount?: string; body: string; branch?: string; createdAt: Date; @@ -49,20 +50,23 @@ export interface IssueData { updatedAt?: Date; url?: string; contractId?: number; - token?: Token; + transactionalToken?: Token; + rewardToken?: Token; working: string[]; fundedAt?: Date; benefactors?: fundingBenefactor[]; disputes?: Disputes[]; payments: Payment[]; - network?: { - colors: { - primary: string; - }; - name: string; - logoIcon: string; - } + network?: Network; tags: string[]; + isDraft: boolean; + isClosed: boolean; + isFundingRequest: boolean; + isFunded: boolean; + isCanceled: boolean; + isReady?: boolean; + hasReward?: boolean; + fundedPercent: number; isKyc: boolean; kycTierList: number[]; } @@ -74,11 +78,12 @@ export interface Disputes { proposalId: number; } -export interface IssueBigNumberData extends Omit { +export interface IssueBigNumberData + extends Omit { amount: BigNumber; fundingAmount: BigNumber; fundedAmount: BigNumber; - fundedPercent: BigNumber; + rewardAmount: BigNumber; } export interface IssueNetwork extends IssueBigNumberData { @@ -96,6 +101,7 @@ export interface IssueSearch { export interface Repository { id: number; githubPath: string; + network?: Network; } export interface pullRequest { @@ -106,6 +112,7 @@ export interface pullRequest { isMergeable: boolean; issueId: number; state: string; + userAddress: string; merged: boolean; updatedAt: Date; issue?: IssueData; @@ -120,7 +127,10 @@ export interface pullRequest { hash?: string; approvals?: { total: number; - } + }; + isCanceled: boolean; + isReady: boolean; + isCancelable: boolean; } export interface developer { @@ -161,5 +171,5 @@ export interface fundingBenefactor { address: string; contractId: number; issueId: number; - isWithdrawn?: boolean + withdrawn?: boolean } diff --git a/interfaces/mini-chain.ts b/interfaces/mini-chain.ts new file mode 100644 index 0000000000..c2674eb989 --- /dev/null +++ b/interfaces/mini-chain.ts @@ -0,0 +1,17 @@ +export interface MiniChainInfo { + name: string; + shortName: string; + chainId: number; + networkId: number; + nativeCurrency: { + name: string; + symbol: string; + decimals: number; + }; + rpc: string[]; + activeRPC?: string; + loading?: boolean; + explorer?: string; + eventsApi?: string; + color?: string; +} \ No newline at end of file diff --git a/interfaces/network.ts b/interfaces/network.ts index 57dbb99f84..d7c2369c47 100644 --- a/interfaces/network.ts +++ b/interfaces/network.ts @@ -1,10 +1,11 @@ import {TreasuryInfo} from "@taikai/dappkit"; import BigNumber from "bignumber.js"; +import { Curator } from "interfaces/curators"; +import { IssueData } from "interfaces/issue-data"; +import { SupportedChainData } from "interfaces/supported-chain-data"; import {Token} from "interfaces/token"; -import { Curator } from "./curators"; - export interface Network { id: number; name: string; @@ -42,9 +43,13 @@ export interface Network { isGovernor?: boolean; isDefault?: boolean; curators?: Curator[]; + chain_id?: string; totalValueLock?: BigNumber; totalIssues?: string; totalOpenIssues?: string; + countIssues?: number; + chain?: SupportedChainData; + issues?: IssueData[]; } export interface ThemeColors { diff --git a/interfaces/proposal.ts b/interfaces/proposal.ts index 46c82b5188..69d75688fb 100644 --- a/interfaces/proposal.ts +++ b/interfaces/proposal.ts @@ -1,21 +1,34 @@ import BigNumber from "bignumber.js"; -export interface Proposal { - creationDate: number; - createdAt: Date | number; +import { BaseModel } from "interfaces/db/base"; + +export interface ProposalDistribution extends BaseModel { + recipient: string; + percentage: number; + proposalId: number; +} + +export interface ProposalDisputes extends BaseModel { + issueId: number; + proposalId: number; + address: string; + weight: BigNumber; +} + +export interface Proposal extends BaseModel { githubLogin: string; - id: number; isMerged?: boolean; issueId?: number; pullRequestId?: number; contractId?: number; creator?: string; network_id: number; - distributions?: DistributedAmounts[] - contractCreationDate?: number; + distributions?: ProposalDistribution[]; + contractCreationDate?: Date; disputeWeight?: BigNumber; refusedByBountyOwner?: boolean; isDisputed?: boolean; + disputes?: ProposalDisputes[]; } export interface INetworkProposal { diff --git a/interfaces/supported-chain-data.ts b/interfaces/supported-chain-data.ts new file mode 100644 index 0000000000..6324c7d119 --- /dev/null +++ b/interfaces/supported-chain-data.ts @@ -0,0 +1,15 @@ +export interface SupportedChainData { + chainId: number; + chainRpc: string; + name: string; + chainName: string; + chainShortName: string; + chainCurrencySymbol: string; + chainCurrencyDecimals: string; + chainCurrencyName: string; + blockScanner?: string; + registryAddress: string; + eventsApi: string + isDefault: boolean; + color?: string; +} \ No newline at end of file diff --git a/interfaces/tabbed-navigation.ts b/interfaces/tabbed-navigation.ts index 85f9144f17..8e8cf9e331 100644 --- a/interfaces/tabbed-navigation.ts +++ b/interfaces/tabbed-navigation.ts @@ -1,4 +1,4 @@ -import { ReactElement, ReactNode } from "react"; +import {ReactElement, ReactNode} from "react"; export interface TabbedNavigationItem { eventKey: string; @@ -10,6 +10,7 @@ export interface TabbedNavigationItem { export interface TabbedNavigationProps { className?: string; + forceActiveKey?: string; collapsable?: boolean; tabs: TabbedNavigationItem[]; defaultActiveKey?: string; diff --git a/interfaces/token.ts b/interfaces/token.ts index c14d197329..4c10dcf3f5 100644 --- a/interfaces/token.ts +++ b/interfaces/token.ts @@ -30,7 +30,7 @@ export interface NetworkToken { export interface TokenInfo extends Partial { icon: string | ReactElement; - prices: TokenPrice + prices: TokenPrice; } export interface TokenPrice { diff --git a/middleware/admin-route.ts b/middleware/admin-route.ts index c9827c4015..5c5092ab63 100644 --- a/middleware/admin-route.ts +++ b/middleware/admin-route.ts @@ -22,9 +22,6 @@ export const AdminRoute = (handler: NextApiHandler, methods: string[] = [`POST`, const wallet = (headers.wallet as string)?.toLowerCase(); const chainId = (headers.chain as string); - if (!chainId) - return res.status(401).json({message: MISSING_CHAIN_ID}) - if (!wallet || wallet !== adminWallet) return res.status(401).json({message: NOT_ADMIN_WALLET}); diff --git a/middleware/index.ts b/middleware/index.ts index 9bcc85eafc..dc1736c863 100644 --- a/middleware/index.ts +++ b/middleware/index.ts @@ -1,6 +1,6 @@ -import {LogAccess} from "./log-access"; -import withCors from "./withCors"; -import WithJwt from "./withJwt"; +import {LogAccess} from "middleware/log-access"; +import withCors from "middleware/withCors"; +import WithJwt from "middleware/withJwt"; const withProtected = (handler) => withCors(WithJwt(handler)) const RouteMiddleware = (handler) => LogAccess(withCors(WithJwt(handler))); diff --git a/middleware/log-access.ts b/middleware/log-access.ts index bbbd0e900e..a1b3d548a6 100644 --- a/middleware/log-access.ts +++ b/middleware/log-access.ts @@ -1,6 +1,6 @@ import {NextApiHandler, NextApiRequest, NextApiResponse} from "next"; -import {debug, log, Logger} from "../services/logging"; +import {debug, log, Logger} from "services/logging"; export const LogAccess = (handler: NextApiHandler) => { return async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/middleware/with-valid-chain-id.ts b/middleware/with-valid-chain-id.ts new file mode 100644 index 0000000000..13424e876e --- /dev/null +++ b/middleware/with-valid-chain-id.ts @@ -0,0 +1,24 @@ +import {NextApiHandler} from "next"; +import {Op} from "sequelize"; + +import models from "db/models"; + +import {CHAIN_ID_NOT_SUPPORTED, MISSING_CHAIN_ID} from "helpers/constants"; +import {isAdmin} from "helpers/is-admin"; +import {resJsonMessage} from "helpers/res-json-message"; + +export const WithValidChainId = (handler: NextApiHandler, methods: string[] = [`POST`, `PATCH`, `PUT`, `DELETE`]) => { + return async function withValidChainIdHandler(req, res) { + if (!methods.includes(req.method.toUpperCase())) + return handler(req, res); + + if (!req.headers?.chain) + return resJsonMessage(MISSING_CHAIN_ID, res, 400); + + const foundChain = await models.chain.findOne({where: {chainId: {[Op.eq]: +req.headers.chain}}}); + if (!foundChain && !isAdmin(req)) + return resJsonMessage(CHAIN_ID_NOT_SUPPORTED, res, 400); + + return handler(req, res); + } +} \ No newline at end of file diff --git a/middleware/withCors.ts b/middleware/withCors.ts index 7d783c68eb..613ef302c7 100644 --- a/middleware/withCors.ts +++ b/middleware/withCors.ts @@ -1,12 +1,16 @@ +import {recoverTypedSignature} from "@metamask/eth-sig-util"; import Cors from 'cors' import getConfig from "next/config"; +import {IM_AN_ADMIN, MISSING_ADMIN_SIGNATURE, NOT_AN_ADMIN} from "helpers/constants"; +import {messageFor} from "helpers/message-for"; + const { publicRuntimeConfig } = getConfig(); const cors = Cors({ - methods: ['GET', 'PUT', 'POST'], + methods: ['GET', 'PUT', 'POST', 'PATCH', 'DELETE'], origin: [publicRuntimeConfig?.urls?.home || 'http://localhost:3000'], -}) +}); const WithCors = (handler) => diff --git a/middleware/withJwt.ts b/middleware/withJwt.ts index 0825c4841a..4e6dc8fd9d 100644 --- a/middleware/withJwt.ts +++ b/middleware/withJwt.ts @@ -1,8 +1,9 @@ import {NextApiRequest} from "next"; import {getToken} from "next-auth/jwt"; -import {INVALID_JWT_TOKEN} from "../helpers/error-messages"; -import {Logger} from "../services/logging"; +import {INVALID_JWT_TOKEN} from "helpers/error-messages"; + +import {Logger} from "services/logging"; Logger.changeActionName(`WithJWT()`); diff --git a/package-lock.json b/package-lock.json index f53a4331dc..ca2c94dc18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@primer/css": "^20.8.0", "@synaps-io/react-verify": "^1.0.3", "@taikai/dappkit": "^2.1.11", + "@synaps-io/react-verify": "^1.0.3", "@taikai/scribal": "^1.0.3", "@vlsergey/react-bootstrap-pagination": "^3.2.1", "abort-controller": "^3.0.0", @@ -3101,7 +3102,7 @@ "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, "node_modules/asn1": { "version": "0.2.6", @@ -4447,9 +4448,9 @@ } }, "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "engines": { "node": ">=0.10" } @@ -4574,9 +4575,9 @@ "dev": true }, "node_modules/dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "dependencies": { "asap": "^2.0.0", "wrappy": "1" @@ -6051,12 +6052,12 @@ } }, "node_modules/file-type": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.1.tgz", - "integrity": "sha512-heRUMZHby2Qj6wZAA3YHeMlRmZNQTcb6VxctkGmM+mcM6ROQKvHpr7SS6EgdfEhH+s25LDshBjvPx/Ecm+bOVQ==", + "version": "17.1.6", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz", + "integrity": "sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==", "dependencies": { "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0-alpha.7", + "strtok3": "^7.0.0-alpha.9", "token-types": "^5.0.0-alpha.2" }, "engines": { @@ -6222,23 +6223,26 @@ } }, "node_modules/formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", "dependencies": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } }, "node_modules/formidable/node_modules/qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, "engines": { "node": ">=0.6" }, @@ -7995,9 +7999,9 @@ } }, "node_modules/jose": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.6.0.tgz", - "integrity": "sha512-0hNAkhMBNi4soKSAX4zYOFV+aqJlEz/4j4fregvasJzEVtjDChvWqRjPvHwLqr5hx28Ayr6bsOs1Kuj87V0O8w==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.1.tgz", + "integrity": "sha512-YRv4Tk/Wlug8qicwqFNFVEZSdbROCHRAC6qu/i0dyNKr5JQdoa2pIGoS04lLO/jXQX7Z9omoNewYIVIxqZBd9Q==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -8727,17 +8731,17 @@ "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" }, "node_modules/moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", "engines": { "node": "*" } }, "node_modules/moment-timezone": { - "version": "0.5.34", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz", - "integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==", + "version": "0.5.40", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.40.tgz", + "integrity": "sha512-tWfmNkRYmBkPJz5mr9GVDn9vRlVZOTe6yqY92rFxiOdWXbjaR0+9LwQnZGGuNR63X456NqmEkbskte8tWL5ePg==", "dependencies": { "moment": ">= 2.9.0" }, @@ -9858,11 +9862,11 @@ } }, "node_modules/peek-readable": { - "version": "5.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0-alpha.5.tgz", - "integrity": "sha512-pJohF/tDwV3ntnT5+EkUo4E700q/j/OCDuPxtM+5/kFGjyOai/sK4/We4Cy1MB2OiTQliWU5DxPvYIKQAdPqAA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", + "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=14.16" }, "funding": { "type": "github", @@ -10662,19 +10666,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11086,25 +11077,6 @@ "postcss": "^8.3.11" } }, - "node_modules/sanitize-html/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sanitize-html/node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/sanitize-html/node_modules/postcss": { "version": "8.4.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz", @@ -11809,15 +11781,15 @@ } }, "node_modules/strtok3": { - "version": "7.0.0-alpha.8", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0-alpha.8.tgz", - "integrity": "sha512-u+k19v+rTxBjGYxncRQjGvZYwYvEd0uP3D+uHKe/s4WB1eXS5ZwpZsTlBu5xSS4zEd89mTXECXg6WW3FSeV8cA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", + "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", "dependencies": { "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0-alpha.5" + "peek-readable": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=14.16" }, "funding": { "type": "github", @@ -12428,11 +12400,6 @@ "node": ">=6.0.0" } }, - "node_modules/umzug/node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, "node_modules/unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -15865,7 +15832,7 @@ "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, "asn1": { "version": "0.2.6", @@ -16930,9 +16897,9 @@ } }, "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" }, "decompress-response": { "version": "6.0.0", @@ -17019,9 +16986,9 @@ "dev": true }, "dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "requires": { "asap": "^2.0.0", "wrappy": "1" @@ -18243,12 +18210,12 @@ } }, "file-type": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.1.tgz", - "integrity": "sha512-heRUMZHby2Qj6wZAA3YHeMlRmZNQTcb6VxctkGmM+mcM6ROQKvHpr7SS6EgdfEhH+s25LDshBjvPx/Ecm+bOVQ==", + "version": "17.1.6", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz", + "integrity": "sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==", "requires": { "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0-alpha.7", + "strtok3": "^7.0.0-alpha.9", "token-types": "^5.0.0-alpha.2" } }, @@ -18370,20 +18337,23 @@ } }, "formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", "requires": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" }, "dependencies": { "qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==" + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } } } }, @@ -19666,9 +19636,9 @@ } }, "jose": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.6.0.tgz", - "integrity": "sha512-0hNAkhMBNi4soKSAX4zYOFV+aqJlEz/4j4fregvasJzEVtjDChvWqRjPvHwLqr5hx28Ayr6bsOs1Kuj87V0O8w==" + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.1.tgz", + "integrity": "sha512-YRv4Tk/Wlug8qicwqFNFVEZSdbROCHRAC6qu/i0dyNKr5JQdoa2pIGoS04lLO/jXQX7Z9omoNewYIVIxqZBd9Q==" }, "js-beautify": { "version": "1.14.0", @@ -20282,14 +20252,14 @@ "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" }, "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" }, "moment-timezone": { - "version": "0.5.34", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz", - "integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==", + "version": "0.5.40", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.40.tgz", + "integrity": "sha512-tWfmNkRYmBkPJz5mr9GVDn9vRlVZOTe6yqY92rFxiOdWXbjaR0+9LwQnZGGuNR63X456NqmEkbskte8tWL5ePg==", "requires": { "moment": ">= 2.9.0" } @@ -21110,9 +21080,9 @@ } }, "peek-readable": { - "version": "5.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0-alpha.5.tgz", - "integrity": "sha512-pJohF/tDwV3ntnT5+EkUo4E700q/j/OCDuPxtM+5/kFGjyOai/sK4/We4Cy1MB2OiTQliWU5DxPvYIKQAdPqAA==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", + "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==" }, "performance-now": { "version": "2.1.0", @@ -21699,18 +21669,6 @@ "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", "requires": { "readable-stream": "^3.6.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } } }, "readdirp": { @@ -22034,16 +21992,6 @@ "postcss": "^8.3.11" }, "dependencies": { - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" - }, - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" - }, "postcss": { "version": "8.4.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz", @@ -22561,12 +22509,12 @@ "dev": true }, "strtok3": { - "version": "7.0.0-alpha.8", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0-alpha.8.tgz", - "integrity": "sha512-u+k19v+rTxBjGYxncRQjGvZYwYvEd0uP3D+uHKe/s4WB1eXS5ZwpZsTlBu5xSS4zEd89mTXECXg6WW3FSeV8cA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", + "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", "requires": { "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0-alpha.5" + "peek-readable": "^5.0.0" } }, "styled-jsx": { @@ -23016,13 +22964,6 @@ "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==", "requires": { "bluebird": "^3.7.2" - }, - "dependencies": { - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - } } }, "unbox-primitive": { diff --git a/package.json b/package.json index 88239354a5..001aadb1aa 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "scripts": { "prestart": "npm run migrate", "db:reset": "sequelize db:drop && sequelize db:create", + "db:create": "sequelize db:create", "migrate": "sequelize db:migrate", "migrate:undo": "sequelize db:migrate:undo", "clean": "npm cache clean --force && rm -rf .next", @@ -32,7 +33,7 @@ "deploy:bitgert": "node ./scripts/deploy-v2 -n bitgert", "deploy:bsc": "node ./scripts/deploy-v2 -n bsc", "deploy:apollodorus": "node ./scripts/deploy-v2 -n apollodorus", - "ganache:dev": "ganache -h 0.0.0.0 -p 8545 -a 50 -s 0x6d2119524d65ea0b15b214ef260d3acdf8ca0ec6ec68045b5785d970f5ad97bd3d0025d212e625a8a21920852cc165c10faa6d8387a0c2530d1ffc0d265d92935749" + "ganache:dev": "ganache -h 0.0.0.0 -p 8545 -b 1 -a 50 -s 0x6d2119524d65ea0b15b214ef260d3acdf8ca0ec6ec68045b5785d970f5ad97bd3d0025d212e625a8a21920852cc165c10faa6d8387a0c2530d1ffc0d265d92935749" }, "dependencies": { "@elastic/elasticsearch": "^8.2.1", @@ -41,6 +42,7 @@ "@primer/css": "^20.8.0", "@synaps-io/react-verify": "^1.0.3", "@taikai/dappkit": "^2.1.11", + "@synaps-io/react-verify": "^1.0.3", "@taikai/scribal": "^1.0.3", "@vlsergey/react-bootstrap-pagination": "^3.2.1", "abort-controller": "^3.0.0", diff --git a/pages/[network]/birds-eye.tsx b/pages/[network]/[chain]/birds-eye.tsx similarity index 98% rename from pages/[network]/birds-eye.tsx rename to pages/[network]/[chain]/birds-eye.tsx index 6e20053ee3..605d1ed475 100644 --- a/pages/[network]/birds-eye.tsx +++ b/pages/[network]/[chain]/birds-eye.tsx @@ -8,12 +8,12 @@ import {Octokit} from "octokit"; import ConnectWalletButton from "components/connect-wallet-button"; +import {useAppState} from "contexts/app-state"; + import {User} from "interfaces/api"; import useApi from "x-hooks/use-api"; -import {useAppState} from "../../contexts/app-state"; - interface PropsUserList extends Partial { created_at: string; login: string; diff --git a/pages/[network]/[chain]/bounties.tsx b/pages/[network]/[chain]/bounties.tsx new file mode 100644 index 0000000000..37bdbf463d --- /dev/null +++ b/pages/[network]/[chain]/bounties.tsx @@ -0,0 +1,120 @@ +import {useEffect, useState} from "react"; + +import BigNumber from "bignumber.js"; +import {useTranslation} from "next-i18next"; +import {serverSideTranslations} from "next-i18next/serverSideTranslations"; +import { useRouter } from "next/dist/client/router"; +import {GetServerSideProps} from "next/types"; + +import ListIssues from "components/list-issues"; +import PageHero, {InfosHero} from "components/page-hero"; + +import {useAppState} from "contexts/app-state"; +import {BountyEffectsProvider} from "contexts/bounty-effects"; + +import { Curator } from "interfaces/curators"; +import { IssueBigNumberData } from "interfaces/issue-data"; + +import useApi from "x-hooks/use-api"; +import {useBounty} from "x-hooks/use-bounty"; +import useChain from "x-hooks/use-chain"; + +export default function BountiesPage() { + useBounty(); + const { t } = useTranslation(["common"]); + const { query } = useRouter(); + + const { chain } = useChain(); + const {state} = useAppState(); + const { getTotalUsers, searchCurators, searchIssues } = useApi(); + + const zeroInfo = [ + { + value: 0, + label: t("heroes.in-progress") + }, + { + value: 0, + label: t("heroes.bounties-closed") + }, + { + value: 0, + label: t("heroes.in-network"), + currency: t("misc.$token") + }, + { + value: 0, + label: t("heroes.protocol-members") + } + ]; + + const [infos, setInfos] = useState(zeroInfo); + + useEffect(() => { + if (!state.Service?.network?.active || !chain || !query?.network) return; + + setInfos(zeroInfo); + + Promise.all([ + searchIssues({ + networkName: query.network.toString(), + chainId: chain.chainId.toString() + }).then(({ rows } : { rows: IssueBigNumberData[] }) => rows), + searchCurators({ + networkName: query.network.toString(), + chainShortName: query.chain.toString() + }).then(({ rows }) => rows), + getTotalUsers(), + state.Service?.network?.active?.networkToken?.symbol, + ]) + .then(([bounties, curators, totalUsers, symbol]) => { + const closedBounties = bounties.filter(({ state }) => state === "closed").length; + const inProgress = bounties.filter(({ state }) => !["pending", "canceled", "closed"].includes(state)).length; + const onNetwork = (curators as Curator[]).reduce((acc, curator) => + new BigNumber(acc).plus(curator.tokensLocked).toFixed(), "0"); + + return [closedBounties, inProgress, onNetwork, totalUsers, symbol]; + }) + .then(([closed, inProgress, onNetwork, totalUsers, symbol]) => { + setInfos([ + { + value: inProgress, + label: t("heroes.in-progress") + }, + { + value: closed, + label: t("heroes.bounties-closed") + }, + { + value: onNetwork, + label: t("heroes.in-network"), + currency: t("$oracles",{ token: symbol || t("misc.$token") }) + }, + { + value: totalUsers, + label: t("heroes.protocol-members") + } + ]); + }); + }, [state.Service?.network?.active, query?.network, chain]); + + return ( + + + + + + ); +} + +export const getServerSideProps: GetServerSideProps = async ({ locale }) => { + return { + props: { + ...(await serverSideTranslations(locale, ["common", "bounty", "connect-wallet-button"])) + } + }; +}; diff --git a/pages/[network]/bounty.tsx b/pages/[network]/[chain]/bounty.tsx similarity index 84% rename from pages/[network]/bounty.tsx rename to pages/[network]/[chain]/bounty.tsx index cc1dfa8c56..02b5425fd4 100644 --- a/pages/[network]/bounty.tsx +++ b/pages/[network]/[chain]/bounty.tsx @@ -8,12 +8,15 @@ import BountyHero from "components/bounty-hero"; import FundingSection from "components/bounty/funding-section"; import IssueBody from "components/bounty/issue-body"; import TabSections from "components/bounty/tabs-sections"; +import If from "components/If"; import IssueComments from "components/issue-comments"; import PageActions from "components/page-actions"; import {useAppState} from "contexts/app-state"; import {BountyEffectsProvider} from "contexts/bounty-effects"; +import { IM_AM_CREATOR_ISSUE } from "helpers/constants"; + import { useAuthentication } from "x-hooks/use-authentication"; import {useBounty} from "x-hooks/use-bounty"; import useOctokit from "x-hooks/use-octokit"; @@ -25,20 +28,19 @@ export default function PageIssue() { const [commentsIssue, setCommentsIssue] = useState([]); const [isRepoForked, setIsRepoForked] = useState(); const [isEditIssue, setIsEditIssue] = useState(false); - const { signMessageIfCreatorIssue } = useAuthentication(); const {state} = useAppState(); - const { getUserRepository } = useOctokit(); + const { signMessage } = useAuthentication(); const { id } = router.query; async function handleEditIssue() { - const isCreator = await signMessageIfCreatorIssue() - - if(isCreator){ - setIsEditIssue(true) - } + signMessage(IM_AM_CREATOR_ISSUE) + .then(() => { + setIsEditIssue(true); + }) + .catch(error => console.debug(error)); } function handleCancelEditIssue() { @@ -82,18 +84,20 @@ export default function PageIssue() { isRepoForked !== undefined) return; checkForks(); - },[state.currentUser?.login, - state.currentBounty?.data?.working, - state.Service?.network?.repos?.active, - !state.currentUser?.walletAddress + },[ + state.currentUser?.login, + state.currentBounty?.data?.working, + state.Service?.network?.repos?.active, + !state.currentUser?.walletAddress ]); return ( - { state.currentBounty?.chainData?.isFundingRequest && state.currentBounty?.data?.fundingAmount ? - : null} + + + - {(state.currentUser?.walletAddress) - ? - : null - } + + + { + replace(getURLWithNetwork(`/bounties`, { + network: query?.network + })); + }, []); + + return null; +} + +export const getServerSideProps: GetServerSideProps = async ({locale}) => { + return { + props: { + ...(await serverSideTranslations(locale, ["common", "bounty", "connect-wallet-button"])) + } + }; +}; diff --git a/pages/[network]/profile/bepro-votes.tsx b/pages/[network]/[chain]/profile/bepro-votes.tsx similarity index 91% rename from pages/[network]/profile/bepro-votes.tsx rename to pages/[network]/[chain]/profile/bepro-votes.tsx index 0e66c11e04..cd42655e59 100644 --- a/pages/[network]/profile/bepro-votes.tsx +++ b/pages/[network]/[chain]/profile/bepro-votes.tsx @@ -33,7 +33,7 @@ export default function BeproVotes() { const { curatorAddress } = useRouter().query const oracleToken = { - symbol: t("$oracles", { token: state.Service?.network?.networkToken?.symbol }), + symbol: t("$oracles", { token: state.Service?.network?.active?.networkToken?.symbol }), name: t("profile:oracle-name-placeholder"), icon: }; @@ -49,7 +49,7 @@ export default function BeproVotes() { - {t("$oracles", { token: state.Service?.network?.networkToken?.symbol })} + {t("$oracles", { token: state.Service?.network?.active?.networkToken?.symbol })} @@ -61,7 +61,7 @@ export default function BeproVotes() { @@ -72,7 +72,7 @@ export default function BeproVotes() { {t("label_other")} - + ); } diff --git a/pages/[network]/profile/index.tsx b/pages/[network]/[chain]/profile/index.tsx similarity index 70% rename from pages/[network]/profile/index.tsx rename to pages/[network]/[chain]/profile/index.tsx index e9b0d6c73a..2c06c6e37a 100644 --- a/pages/[network]/profile/index.tsx +++ b/pages/[network]/[chain]/profile/index.tsx @@ -6,12 +6,10 @@ import {serverSideTranslations} from "next-i18next/serverSideTranslations"; import AvatarOrIdenticon from "components/avatar-or-identicon"; import Badge from "components/badge"; -import Button from "components/button"; +import GithubConnectionState from "components/github-connection-state"; import KycSessionModal from "components/modals/kyc-session"; -import {ConnectionButton} from "components/profile/connect-button"; import ProfileLayout from "components/profile/profile-layout"; import {RemoveGithubAccount} from "components/profile/remove-github-modal"; -import ReadOnlyButtonWrapper from "components/read-only-button-wrapper"; import {useAppState} from "contexts/app-state"; @@ -19,6 +17,7 @@ import {truncateAddress} from "helpers/truncate-address"; import {useAuthentication} from "x-hooks/use-authentication"; + export default function Profile() { const { t } = useTranslation("profile"); @@ -26,9 +25,8 @@ export default function Profile() { const {state} = useAppState(); - const { connectWallet, connectGithub, disconnectGithub } = useAuthentication(); + const { disconnectGithub } = useAuthentication(); - const isConnected = !!state.currentUser?.login && !!state.currentUser?.walletAddress; const addressOrUsername = state.currentUser?.login ? state.currentUser.login : truncateAddress(state.currentUser?.walletAddress); @@ -63,35 +61,11 @@ export default function Profile() { {t("connections")}
-
-
- - - { isConnected && - - - } -
- -
- -
-
+
- (); const { state, dispatch } = useAppState(); + const { chain } = useChain(); const { searchNetworks } = useApi(); - const { setForcedNetwork } = useNetworkSettings() + const { setForcedNetwork } = useNetworkSettings(); + const defaultNetworkName = state?.Service?.network?.active?.name?.toLowerCase(); async function updateEditingNetwork() { dispatch(changeLoadState(true)); + const chainId = chain.chainId.toString(); + searchNetworks({ creatorAddress: state.currentUser.walletAddress, - isClosed: false + isClosed: false, + chainId: chainId }) .then(({ count , rows }) => { const savedNetwork = count > 0 ? rows[0] : undefined; if (savedNetwork) - sessionStorage.setItem(`bepro.network:${savedNetwork.name.toLowerCase()}`, JSON.stringify(savedNetwork)); + sessionStorage.setItem(`bepro.network:${savedNetwork.name.toLowerCase()}:${chainId}`, + JSON.stringify(savedNetwork)); setMyNetwork(savedNetwork); setForcedNetwork(savedNetwork); @@ -51,10 +57,10 @@ export function MyNetwork() { } useEffect(() => { - if (!state.currentUser?.walletAddress) return; + if (!state.currentUser?.walletAddress || !chain) return; updateEditingNetwork(); - }, [state.currentUser?.walletAddress]); + }, [state.currentUser?.walletAddress, chain]); return( diff --git a/pages/[network]/profile/payments.tsx b/pages/[network]/[chain]/profile/payments.tsx similarity index 96% rename from pages/[network]/profile/payments.tsx rename to pages/[network]/[chain]/profile/payments.tsx index c376880fdc..96fa724e69 100644 --- a/pages/[network]/profile/payments.tsx +++ b/pages/[network]/[chain]/profile/payments.tsx @@ -13,6 +13,8 @@ import ProfileLayout from "components/profile/profile-layout"; import {FlexColumn, FlexRow} from "components/profile/wallet-balance"; import ReactSelect from "components/react-select"; +import {useAppState} from "contexts/app-state"; + import {formatNumberToCurrency} from "helpers/formatNumber"; import {Payment} from "interfaces/payments"; @@ -21,8 +23,6 @@ import {getCoinPrice} from "services/coingecko"; import useApi from "x-hooks/use-api"; -import {useAppState} from "../../../contexts/app-state"; - export default function Payments() { const { t } = useTranslation(["common", "profile"]); @@ -74,9 +74,9 @@ export default function Payments() { if (!payments?.length) return; Promise.all(payments.map(async (payment) => ({ - tokenAddress: payment.issue.token.address, + tokenAddress: payment.issue.transactionalToken.address, value: payment.ammount, - price: await getCoinPrice(payment.issue.token.symbol, state?.Settings.currency.defaultFiat), + price: await getCoinPrice(payment.issue.transactionalToken.symbol, state?.Settings.currency.defaultFiat), }))).then((tokens) => { const totalConverted = tokens.reduce((acc, token) => acc + token.value * (token.price || 0), 0); diff --git a/pages/[network]/profile/proposals.tsx b/pages/[network]/[chain]/profile/proposals.tsx similarity index 96% rename from pages/[network]/profile/proposals.tsx rename to pages/[network]/[chain]/profile/proposals.tsx index 9f4860c3ba..e1a7a415f5 100644 --- a/pages/[network]/profile/proposals.tsx +++ b/pages/[network]/[chain]/profile/proposals.tsx @@ -5,7 +5,7 @@ import {serverSideTranslations} from "next-i18next/serverSideTranslations"; import ListIssues from "components/list-issues"; import ProfileLayout from "components/profile/profile-layout"; -import {useAppState} from "contexts/app-state"; +import {useAppState} from"contexts/app-state"; import {useNetwork} from "x-hooks/use-network"; diff --git a/pages/[network]/profile/pull-requests.tsx b/pages/[network]/[chain]/profile/pull-requests.tsx similarity index 91% rename from pages/[network]/profile/pull-requests.tsx rename to pages/[network]/[chain]/profile/pull-requests.tsx index 2aa98df857..b641211de4 100644 --- a/pages/[network]/profile/pull-requests.tsx +++ b/pages/[network]/[chain]/profile/pull-requests.tsx @@ -5,9 +5,9 @@ import {serverSideTranslations} from "next-i18next/serverSideTranslations"; import ListIssues from "components/list-issues"; import ProfileLayout from "components/profile/profile-layout"; +import {useAppState} from "contexts/app-state"; -import {useAppState} from "../../../contexts/app-state"; -import {useNetwork} from "../../../x-hooks/use-network"; +import {useNetwork} from "x-hooks/use-network"; export default function PullRequests() { const {t} = useTranslation(["pull-request", "bounty"]); diff --git a/pages/[network]/profile/wallet.tsx b/pages/[network]/[chain]/profile/wallet.tsx similarity index 100% rename from pages/[network]/profile/wallet.tsx rename to pages/[network]/[chain]/profile/wallet.tsx diff --git a/pages/[network]/proposal.tsx b/pages/[network]/[chain]/proposal.tsx similarity index 75% rename from pages/[network]/proposal.tsx rename to pages/[network]/[chain]/proposal.tsx index ca9a355980..4dd7b40732 100644 --- a/pages/[network]/proposal.tsx +++ b/pages/[network]/[chain]/proposal.tsx @@ -10,30 +10,25 @@ import ConnectWalletButton from "components/connect-wallet-button"; import CustomContainer from "components/custom-container"; import NotMergeableModal from "components/not-mergeable-modal"; import ProposalActionCard from "components/proposal-action-card"; +import {ProposalDisputes} from "components/proposal-disputes"; import ProposalHero from "components/proposal-hero"; import ProposalListDistribution from "components/proposal-list-distribution"; import ProposalProgress from "components/proposal-progress"; import ProposalPullRequestDetail from "components/proposal-pullrequest-details"; - import {useAppState} from "contexts/app-state"; +import {BountyEffectsProvider} from "contexts/bounty-effects"; import {addToast} from "contexts/reducers/change-toaster"; import calculateDistributedAmounts from "helpers/calculateDistributedAmounts"; - -import {ProposalExtended} from "interfaces/bounty"; import {MetamaskErrors} from "interfaces/enums/Errors"; import {pullRequest} from "interfaces/issue-data"; import {DistributedAmounts, Proposal} from "interfaces/proposal"; import useApi from "x-hooks/use-api"; import useBepro from "x-hooks/use-bepro"; - -import {ProposalDisputes} from "../../components/proposal-disputes"; -import {BountyEffectsProvider} from "../../contexts/bounty-effects"; -import {useBounty} from "../../x-hooks/use-bounty"; - +import {useBounty} from "x-hooks/use-bounty"; const defaultAmount = { value: "0", @@ -49,7 +44,6 @@ export default function PageProposal() { const [proposal, setProposal] = useState({} as Proposal); const [pullRequest, setPullRequest] = useState({} as pullRequest); - const [networkProposal, setNetworkProposal] = useState({} as ProposalExtended); const [distributedAmounts, setDistributedAmounts] = useState({ @@ -59,7 +53,7 @@ export default function PageProposal() { proposals: [], }); - const {getChainBounty, getDatabaseBounty} = useBounty(); + const { getDatabaseBounty } = useBounty(); const { getUserOf, processEvent, createNFT } = useApi(); const { handlerDisputeProposal, handleCloseIssue, handleRefuseByOwner } = useBepro(); @@ -78,11 +72,9 @@ export default function PageProposal() { .then(async txInfo => { const { blockNumber: fromBlock } = txInfo as { blockNumber: number }; - await processEvent("bountyToken", "transfer", state.Service?.network?.lastVisited, { fromBlock } ) - return processEvent("bounty", "closed", state.Service?.network?.lastVisited, { fromBlock } ); + return Promise.all([processEvent("bounty", "closed", state.Service?.network?.lastVisited, { fromBlock } )]); }) .then(() => { - getChainBounty(true); getDatabaseBounty(true); dispatch(addToast({ type: "success", @@ -110,10 +102,7 @@ export default function PageProposal() { return processEvent("proposal", "disputed", state.Service?.network?.lastVisited, { fromBlock } ); }) - .then(() => { - getDatabaseBounty(true); - getChainBounty(true); - }) + .then(() => getDatabaseBounty(true)) .catch(error => { if (error?.code === MetamaskErrors.UserRejected) return; @@ -129,36 +118,34 @@ export default function PageProposal() { async function handleRefuse() { return handleRefuseByOwner(+state.currentBounty?.data?.contractId, +proposal.contractId) - .then(txInfo => { - const { blockNumber: fromBlock } = txInfo as { blockNumber: number }; - - return processEvent("proposal", "refused", state.Service?.network?.lastVisited, { fromBlock } ); - }) - .then(() => { - getDatabaseBounty(true); - getChainBounty(true); - - dispatch(addToast({ - type: "success", - title: t("actions.success"), - content: t("proposal:messages.proposal-refused") - })); - }) - .catch(error => { - if (error?.code === MetamaskErrors.UserRejected) return; - - console.log("Failed to refuse proposal", error); + .then(txInfo => { + const { blockNumber: fromBlock } = txInfo as { blockNumber: number }; - dispatch(addToast({ - type: "danger", - title: t("actions.failed"), - content: error?.response?.data?.message - })); - }); + return processEvent("proposal", "refused", state.Service?.network?.lastVisited, { fromBlock } ); + }) + .then(() => getDatabaseBounty(true)) + .then( () => { + dispatch(addToast({ + type: "success", + title: t("actions.success"), + content: t("proposal:messages.proposal-refused") + })) + }) + .catch(error => { + if (error?.code === MetamaskErrors.UserRejected) return; + + console.log("Failed to refuse proposal", error); + + dispatch(addToast({ + type: "danger", + title: t("actions.failed"), + content: error?.response?.data?.message + })); + }); } async function getDistributedAmounts() { - if (!networkProposal?.details || !state?.Service?.network?.amounts) return; + if (!proposal?.distributions || !state?.Service?.network?.amounts) return; const { treasury, mergeCreatorFeeShare, proposerFeeShare } = state.Service.network.amounts; @@ -166,7 +153,7 @@ export default function PageProposal() { mergeCreatorFeeShare, proposerFeeShare, amountTotal, - networkProposal.details); + proposal.distributions); Promise.all(distributions.proposals.map(async({recipient, ...rest}) => { let githubLogin = null @@ -182,23 +169,21 @@ export default function PageProposal() { } useEffect(() => { - if (!networkProposal?.details?.length) return; + if (!proposal?.distributions?.length) return; getDistributedAmounts(); - }, [networkProposal, state?.Service?.network?.amounts]); + }, [proposal?.distributions, state?.Service?.network?.amounts]); useEffect(() => { - if (!state.currentBounty?.data || !state.currentBounty?.chainData) return; + if (!state.currentBounty?.data) return; const { proposalId } = router.query; const mergeProposal = state.currentBounty?.data?.mergeProposals?.find((p) => +p.id === +proposalId); - const networkProposals = state.currentBounty?.chainData?.proposals?.[+mergeProposal?.contractId]; const pullRequest = state.currentBounty?.data?.pullRequests.find((pr) => pr.id === mergeProposal?.pullRequestId); setProposal(mergeProposal); setPullRequest(pullRequest); - setNetworkProposal(networkProposals); - }, [router.query, state.currentBounty?.data, state.currentBounty?.chainData]); + }, [router.query, state.currentBounty?.data]); return ( @@ -220,7 +205,6 @@ export default function PageProposal() {
diff --git a/pages/[network]/pull-request.tsx b/pages/[network]/[chain]/pull-request.tsx similarity index 89% rename from pages/[network]/pull-request.tsx rename to pages/[network]/[chain]/pull-request.tsx index e157c652d3..126eb676e3 100644 --- a/pages/[network]/pull-request.tsx +++ b/pages/[network]/[chain]/pull-request.tsx @@ -1,14 +1,13 @@ import React, {useEffect, useState} from "react"; -import {PullRequest} from "@taikai/dappkit"; import {useTranslation} from "next-i18next"; import {serverSideTranslations} from "next-i18next/serverSideTranslations"; import {useRouter} from "next/router"; import {GetServerSideProps} from "next/types"; -import Button from "components/button"; import Comment from "components/comment"; import ConnectWalletButton from "components/connect-wallet-button"; +import ContractButton from "components/contract-button"; import CreateReviewModal from "components/create-review-modal"; import CustomContainer from "components/custom-container"; import GithubLink from "components/github-link"; @@ -41,13 +40,12 @@ export default function PullRequestPage() { const [isMakingReady, setIsMakingReady] = useState(false); const [pullRequest, setPullRequest] = useState(); const [isCreatingReview, setIsCreatingReview] = useState(false); - const [networkPullRequest, setNetworkPullRequest] = useState(); const { state, dispatch } = useAppState(); + const { getDatabaseBounty } = useBounty(); const { getURLWithNetwork } = useNetwork(); const { createReviewForPR, processEvent } = useApi(); - const { getDatabaseBounty, getChainBounty } = useBounty(); const { getExtendedPullRequestsForCurrentBounty } = useBounty(); const { handleMakePullRequestReady, handleCancelPullRequest } = useBepro(); @@ -56,11 +54,10 @@ export default function PullRequestPage() { const isWalletConnected = !!state.currentUser?.walletAddress; const isGithubConnected = !!state.currentUser?.login; const isPullRequestOpen = pullRequest?.state?.toLowerCase() === "open"; - const isPullRequestReady = !!networkPullRequest?.ready; - const isPullRequestCanceled = !!networkPullRequest?.canceled; - const isPullRequestCancelable = !!networkPullRequest?.isCancelable; - const isPullRequestCreator = - networkPullRequest?.creator?.toLowerCase() === state.currentUser?.walletAddress?.toLowerCase(); + const isPullRequestReady = !!pullRequest?.isReady; + const isPullRequestCanceled = !!pullRequest?.isCanceled; + const isPullRequestCancelable = !!pullRequest?.isCancelable; + const isPullRequestCreator = pullRequest?.userAddress === state.currentUser?.walletAddress; const branchProtectionRules = state.Service?.network?.repos?.active?.branchProtectionRules; const approvalsRequired = branchProtectionRules ? @@ -121,7 +118,7 @@ export default function PullRequestPage() { return processEvent("pull-request", "ready", state.Service?.network?.lastVisited, {fromBlock}); }) .then(() => { - return Promise.all([getDatabaseBounty(true), getChainBounty()]); + return getDatabaseBounty(true); }) .then(() => { setIsMakingReady(false); @@ -187,15 +184,8 @@ export default function PullRequestPage() { setShowModal(false); } - function updateNetworkPR() { - setNetworkPullRequest(state.currentBounty?.chainData - ?.pullRequests - ?.find(pr => +pr.id === +pullRequest?.contractId)); - } - useEffect(() => { - if (!state.currentBounty?.data || - !state.currentBounty?.chainData || + if (!state.currentBounty?.data || !prId || state?.spinners?.pullRequests || !!pullRequest) return; @@ -215,13 +205,12 @@ export default function PullRequestPage() { dispatch(changeSpinners.update({pullRequests: false})); }); - }, [state.currentBounty?.data, state.currentBounty?.chainData, prId]); - - useEffect(updateNetworkPR,[pullRequest, state.currentBounty?.chainData?.pullRequests]) + }, [state.currentBounty?.data, prId]); useEffect(() => { - if (review && pullRequest && state.currentUser?.login) setShowModal(true); - }, [review, pullRequest, state.currentUser]); + if (review && pullRequest && state.currentUser?.login && state.connectedChain?.matchWithNetworkChain) + setShowModal(true); + }, [review, pullRequest, state.currentUser, state.connectedChain?.matchWithNetworkChain]); return ( @@ -243,14 +232,14 @@ export default function PullRequestPage() { {/* Make Review Button */} {(isWalletConnected && isPullRequestOpen && isPullRequestReady && !isPullRequestCanceled) && - + } @@ -261,14 +250,14 @@ export default function PullRequestPage() { !isPullRequestCanceled && isPullRequestCreator) && ( - + ) } @@ -279,7 +268,7 @@ export default function PullRequestPage() { isPullRequestCancelable && isPullRequestCreator) && ( - + ) } diff --git a/pages/[network]/bounties.tsx b/pages/[network]/bounties.tsx deleted file mode 100644 index 0341bc260d..0000000000 --- a/pages/[network]/bounties.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import {useEffect, useState} from "react"; - -import {ERC20} from "@taikai/dappkit"; -import BigNumber from "bignumber.js"; -import {useTranslation} from "next-i18next"; -import {serverSideTranslations} from "next-i18next/serverSideTranslations"; -import {GetServerSideProps} from "next/types"; - - -import ListIssues from "components/list-issues"; -import PageHero, {InfosHero} from "components/page-hero"; - -import useApi from "x-hooks/use-api"; - -import {useAppState} from "../../contexts/app-state"; -import {BountyEffectsProvider} from "../../contexts/bounty-effects"; -import {useBounty} from "../../x-hooks/use-bounty"; - - -export default function BountiesPage() { - useBounty(); - const { t } = useTranslation(["common"]); - - const {state} = useAppState(); - const { getTotalUsers } = useApi(); - - - const [infos, setInfos] = useState([ - { - value: 0, - label: t("heroes.in-progress") - }, - { - value: 0, - label: t("heroes.bounties-closed") - }, - { - value: 0, - label: t("heroes.in-network"), - currency: t("misc.$token") - }, - { - value: 0, - label: t("heroes.protocol-members") - } - ]); - - useEffect(() => { - if (!state.Service?.active || !state.Service?.active?.network) return; - - Promise.all([ - state.Service?.active.getClosedBounties().catch(() => 0), - state.Service?.active.getOpenBounties().catch(() => 0), - state.Service?.active.getTotalNetworkToken().catch(() => BigNumber(0)), - getTotalUsers(), - (state.Service?.active?.network?.networkToken as ERC20)?.symbol(), - ]).then(([closed, inProgress, onNetwork, totalUsers, symbol]) => { - setInfos([ - { - value: inProgress, - label: t("heroes.in-progress") - }, - { - value: closed, - label: t("heroes.bounties-closed") - }, - { - value: onNetwork.toNumber(), - label: t("heroes.in-network"), - currency: t("$oracles",{ token: symbol || t("misc.$token") }) - }, - { - value: totalUsers, - label: t("heroes.protocol-members") - } - ]); - }); - }, [state.Service?.active?.network?.contractAddress, state.Service?.network]); - - return ( - - - - - ); -} - -export const getServerSideProps: GetServerSideProps = async ({ locale }) => { - return { - props: { - ...(await serverSideTranslations(locale, ["common", "bounty", "connect-wallet-button"])) - } - }; -}; diff --git a/pages/[network]/index.tsx b/pages/[network]/index.tsx index 10b93482df..809b02b0b3 100644 --- a/pages/[network]/index.tsx +++ b/pages/[network]/index.tsx @@ -1,28 +1,24 @@ -import { useEffect } from "react"; +import React from "react"; import {serverSideTranslations} from "next-i18next/serverSideTranslations"; -import { useRouter } from "next/router"; import {GetServerSideProps} from "next/types"; -import useNetworkTheme from "x-hooks/use-network-theme"; +import ExplorePage from "pages/explore"; -export default function Home() { - const { getURLWithNetwork } = useNetworkTheme(); - const { replace, query } = useRouter(); - - useEffect(() => { - replace(getURLWithNetwork(`/bounties`, { - network: query?.network - })); - }, []); - - return null; +export default function NetworkBountyHall() { + return ; } -export const getServerSideProps: GetServerSideProps = async ({locale}) => { +export const getServerSideProps: GetServerSideProps = async ({ locale }) => { return { props: { - ...(await serverSideTranslations(locale, ["common", "bounty", "connect-wallet-button"])) - } + ...(await serverSideTranslations(locale, [ + "common", + "bounty", + "connect-wallet-button", + "custom-network", + "leaderboard", + ])), + }, }; -}; +}; \ No newline at end of file diff --git a/pages/_app.tsx b/pages/_app.tsx index 95fcab7f25..24a62222b2 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -4,6 +4,8 @@ import {GetServerSideProps} from "next"; import {SessionProvider} from "next-auth/react"; import {appWithTranslation} from "next-i18next"; import {AppProps} from "next/app"; +import getConfig from "next/config"; +import {useRouter} from "next/router"; import {GoogleAnalytics} from "nextjs-google-analytics"; import CreateBountyModal from "components/create-bounty-modal"; @@ -16,13 +18,12 @@ import Seo from "components/seo"; import StatusBar from "components/status-bar"; import Toaster from "components/toaster"; import WebThreeDialog from "components/web3-dialog"; +import WrongNetworkModal from "components/wrong-network-modal"; import RootProviders from "contexts"; import "../styles/styles.scss"; import "../node_modules/@primer/css/dist/markdown.css"; -import {useRouter} from "next/router"; -import getConfig from "next/config"; function App({ Component, pageProps: { session, ...pageProps } }: AppProps) { @@ -47,6 +48,7 @@ function App({ Component, pageProps: { session, ...pageProps } }: AppProps) {
+ diff --git a/pages/api/chains/index.ts b/pages/api/chains/index.ts new file mode 100644 index 0000000000..6db37f48e9 --- /dev/null +++ b/pages/api/chains/index.ts @@ -0,0 +1,190 @@ +import {isZeroAddress} from "ethereumjs-util"; +import {NextApiRequest, NextApiResponse} from "next"; +import getConfig from "next/config"; +import {Op} from "sequelize"; +import {isAddress} from "web3-utils"; + +import models from "db/models"; + +import {WRONG_PARAM_ADDRESS, WRONG_PARAM_URL} from "helpers/constants"; +import {resJsonMessage} from "helpers/res-json-message"; + +import {MiniChainInfo} from "interfaces/mini-chain"; + +import {withCors} from "middleware"; +import {AdminRoute} from "middleware/admin-route"; + +import {error} from "services/logging"; + +async function Post(req: NextApiRequest, res: NextApiResponse) { + const body = req.body as MiniChainInfo & { isDefault: boolean; }; + + const missingValues = [ + [body.chainId, 'no chain id'], + [body.name, 'missing name'], + [body.shortName, 'missing currency short name'], + [body.activeRPC, 'missing active rpc'], + [body.nativeCurrency?.name, 'missing currency name'], + [body.nativeCurrency?.symbol, 'missing currency symbol'], + [body.nativeCurrency?.decimals, 'missing currency decimals'], + [body.eventsApi, 'missing events api'], + [body.explorer, 'missing explorer'], + ].filter(([value]) => !value).map(([,error]) => error); + + const {publicRuntimeConfig} = getConfig(); + const {wallet} = req.headers; + if ((wallet as string)?.toLowerCase() !== publicRuntimeConfig.adminWallet.toLowerCase()) + return res.status(401).json({message: 'nope.'}); + + if (missingValues.length) + return res.status(400).json({message: missingValues}); + + const model = { + chainId: body.chainId, + chainRpc: body.activeRPC, + chainName: body.name, + chainShortName: body.shortName, + chainCurrencySymbol: body.nativeCurrency?.symbol, + chainCurrencyName: body.nativeCurrency?.name, + chainCurrencyDecimals: body.nativeCurrency?.decimals, + isDefault: body.isDefault, + blockScanner: body.explorer, + eventsApi: body.eventsApi, + color: body.color + } + + const chain = await models.chain.findOne({where: {chainId: {[Op.eq]: model.chainId}}}); + + if (!chain) { + const all = await models.chain.findAll(); + model.isDefault = !all.length; + } else if (chain) + return res.status(400) + .json({message: `Chain already exists`}); + + const logError = (e) => { + error(`Failed to create ${model.chainId}`, e); + return false; + } + + const action = await models.chain.create(model).then(() => true).catch(logError); + + return res.status(action ? 200 : 400) + .json({message: action ? 'ok' : `Failed to create ${model.chainId}`}); +} + +async function Patch(req: NextApiRequest, res: NextApiResponse) { + if (!req.body.chainId) + return res.status(400).json({message: 'missing chain id'}); + + const isUrl = (url) => { try { return (new URL(url))?.protocol?.search(/https?:/) > -1 } catch { return false } } + const where = {where: {chainId: {[Op.eq]: req.body.chainId}}}; + + const chain = await models.chain.findOne(where); + if (!chain) + return res.status(404).json({message: 'not found'}); + + if (req.body.isDefault !== undefined) { + if ((!!chain.isDefault !== req.body.isDefault)) + return res.status(400).json({message: "can't change isDefault from self"}) + + if (req.body.isDefault) { + const defaultChain = await models.chain.findOne({where: {isDefault: {[Op.eq]: true}}}); + if (defaultChain) { + defaultChain.isDefault = false; + await defaultChain.save(); + } + } + + chain.isDefault = req.body.isDefault; + } + + if (req.body.registryAddress) { + if (!isAddress(req.body.registryAddress) || isZeroAddress(req.body.registryAddress)) + return resJsonMessage(WRONG_PARAM_ADDRESS`registryAddress`, res, 400); + + chain.registryAddress = req.body.registryAddress; + } + + if (req.body.eventsApi) { + if (!isUrl(req.body.eventsApi)) + return resJsonMessage(WRONG_PARAM_URL`eventsApi`, res, 400); + + chain.eventsApi = req.body.eventsApi; + } + + if (req.body.explorer) { + if (!isUrl(req.body.explorer)) + return resJsonMessage(WRONG_PARAM_URL`explorer`, res, 400); + + chain.blockScanner = req.body.explorer; + } + + await chain.save(); + + return res.status(200).json(chain); +} + +async function Remove(req: NextApiRequest, res: NextApiResponse) { + if (!req.query?.id) + return res.status(400).json({message: 'missing id'}); + + const found = await models.chain.findOne({where: {chainId: req.query.id}}) + + if (!found) + return res.status(404).json({message: 'not found'}); + + found.destroy(); + + return res.status(200).json({message: 'ok'}); + +} + +async function Get(req: NextApiRequest, res: NextApiResponse) { + + const query = req.query; + const where = { + ... query.chainId ? {chainId: {[Op.eq]: query.chainId}} : {}, + ... query.name ? {name: {[Op.iLike]: query.name}} : {}, + ... query.shortName ? {shortName: {[Op.iLike]: query.shortName}} : {}, + ... query.chainRpc ? {chainRpc: {[Op.iLike]: query.chainRpc}} : {}, + ... query.nativeCurrencyName ? {nativeCurrencyName: {[Op.iLike]: query.nativeCurrencyName}} : {}, + ... query.nativeCurrencySymbol ? {nativeCurrencySymbol: {[Op.iLike]: query.nativeCurrencySymbol}} : {}, + ... query.nativeCurrencyDecimals ? {nativeCurrencyDecimals: {[Op.eq]: query.nativeCurrencyDecimals}} : {} + } + + return models.chain.findAll({where}) + .then(r => ({result: r})) + .catch(e => { + error(`Failed to get chains`, {query, message: e?.message,}); + return {result: [], error: e?.message} + }) + .then(({result, error}) => res.status(!error ? 200 : 400).json({result, error})); +} + +async function ChainMethods(req: NextApiRequest, res: NextApiResponse) { + switch (req.method.toLowerCase()) { + case "post": + await Post(req, res); + break; + + case "patch": + await Patch(req, res) + break; + + case "delete": + await Remove(req, res); + break; + + case "get": + await Get(req, res); + break; + + default: + return res.status(405).json({message: "Method not allowed"}); + } + + res.end(); +} + +export default withCors(AdminRoute(ChainMethods)); \ No newline at end of file diff --git a/pages/api/files/index.ts b/pages/api/files/index.ts index 5e1761f525..d8b5f2526d 100644 --- a/pages/api/files/index.ts +++ b/pages/api/files/index.ts @@ -1,12 +1,12 @@ import formidable from "formidable"; import fs from "fs"; +import {NextApiRequest, NextApiResponse} from "next"; + import { LogAccess } from "middleware/log-access"; import WithCors from "middleware/withCors"; -import {NextApiRequest, NextApiResponse} from "next"; import IpfsStorage from "services/ipfs-service"; - -import {Logger} from "../../../services/logging"; +import {Logger} from "services/logging"; export const config = { api: { @@ -53,4 +53,4 @@ async function FilesMethods (req: NextApiRequest, res: NextApiResponse) { } Logger.changeActionName(`Files`); -export default LogAccess(WithCors(FilesMethods)); \ No newline at end of file +export default LogAccess(WithCors(FilesMethods)); diff --git a/pages/api/graphql/index.ts b/pages/api/graphql/index.ts index fe6816ccb2..b9bd579e39 100644 --- a/pages/api/graphql/index.ts +++ b/pages/api/graphql/index.ts @@ -4,9 +4,10 @@ import {getToken} from "next-auth/jwt"; import getConfig from "next/config"; import {Octokit} from "octokit"; +import {LogAccess} from "middleware/log-access"; +import WithCors from "middleware/withCors"; + import {Logger} from "services/logging"; -import {LogAccess} from "../../../middleware/log-access"; -import WithCors from "../../../middleware/withCors"; const {serverRuntimeConfig: {authSecret, github: {token: botToken}}} = getConfig(); diff --git a/pages/api/header/networks/index.ts b/pages/api/header/networks/index.ts index 9077daa5ea..066f809709 100644 --- a/pages/api/header/networks/index.ts +++ b/pages/api/header/networks/index.ts @@ -1,12 +1,12 @@ import {NextApiRequest, NextApiResponse} from "next"; import models from "db/models"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; +import {LogAccess} from "middleware/log-access"; +import WithCors from "middleware/withCors"; async function get(req: NextApiRequest, res: NextApiResponse) { - const headerInformation = await models.headerInformation.findAll({}) + const headerInformation = await models.headerInformation.findAll({}); if(!headerInformation) return res.status(404).json({ message: "Header information not found" }); @@ -14,8 +14,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { return res.status(200).json(headerInformation[0]); } -async function SearchNetworks(req: NextApiRequest, - res: NextApiResponse) { +async function SearchNetworks(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "get": await get(req, res); @@ -28,4 +27,4 @@ async function SearchNetworks(req: NextApiRequest, res.end(); } -export default LogAccess(WithCors(SearchNetworks)) \ No newline at end of file +export default LogAccess(WithCors(SearchNetworks)) diff --git a/pages/api/health/index.ts b/pages/api/health/index.ts index de9a213d51..66461a3afd 100644 --- a/pages/api/health/index.ts +++ b/pages/api/health/index.ts @@ -4,8 +4,7 @@ import {Logger} from "services/logging"; Logger.changeActionName(`Health`); -export default function Health(req: NextApiRequest, - res: NextApiResponse) { +export default function Health(req: NextApiRequest, res: NextApiResponse) { res.status(200); res.end(); } diff --git a/pages/api/issue/[...ids].ts b/pages/api/issue/[...ids].ts index c55b7102b0..5c9e3bd741 100644 --- a/pages/api/issue/[...ids].ts +++ b/pages/api/issue/[...ids].ts @@ -1,4 +1,3 @@ -import { IssueRoute } from "middleware/issue-route"; import {NextApiRequest, NextApiResponse} from "next"; import getConfig from "next/config"; import { Octokit } from "octokit"; @@ -8,33 +7,39 @@ import models from "db/models"; import * as IssueQueries from "graphql/issue"; +import { chainFromHeader } from "helpers/chain-from-header"; import { getPropertyRecursively } from "helpers/object"; +import { IssueRoute } from "middleware/issue-route"; + import { GraphQlQueryResponseData, GraphQlResponse } from "types/octokit"; const { serverRuntimeConfig } = getConfig(); async function get(req: NextApiRequest, res: NextApiResponse) { - const { - ids: [repoId, ghId, networkName] - } = req.query; + const { ids: [repoId, ghId, networkName], chainId } = req.query; + const issueId = [repoId, ghId].join("/"); const include = [ { association: "developers" }, { association: "pullRequests", where: { status: { [Op.notIn]: ["pending", "canceled"] } }, required: false }, - { association: "mergeProposals", include: [{ association: "distributions" }] }, + { association: "mergeProposals", include: [{ association: "distributions" }, { association: "disputes" }] }, { association: "repository" }, - { association: "token" }, + { association: "transactionalToken" }, + { association: "rewardToken" }, { association: "benefactors" }, { association: "disputes" } ]; + const chain = await chainFromHeader(req); + const network = await models.network.findOne({ where: { name: { [Op.iLike]: String(networkName).replaceAll(" ", "-") - } + }, + chain_id: { [Op.eq]: chainId || chain.chainId } } }); diff --git a/pages/api/issue/index.ts b/pages/api/issue/index.ts index 12e5cf7433..d8eec68cd6 100644 --- a/pages/api/issue/index.ts +++ b/pages/api/issue/index.ts @@ -1,5 +1,3 @@ -import { LogAccess } from "middleware/log-access"; -import WithCors from "middleware/withCors"; import {NextApiRequest, NextApiResponse} from "next"; import getConfig from "next/config"; import {Octokit} from "octokit"; @@ -10,6 +8,12 @@ import models from "db/models"; import * as IssueQueries from "graphql/issue"; import * as RepositoryQueries from "graphql/repository"; +import {chainFromHeader} from "helpers/chain-from-header"; + +import { LogAccess } from "middleware/log-access"; +import {WithValidChainId} from "middleware/with-valid-chain-id"; +import WithCors from "middleware/withCors"; + import {GraphQlResponse} from "types/octokit"; const {serverRuntimeConfig} = getConfig(); @@ -25,11 +29,17 @@ async function post(req: NextApiRequest, res: NextApiResponse) { isKyc, } = req.body; + const chain = await chainFromHeader(req); + + if (!chain) + return res.status(403).json("Chain not provided"); + const network = await models.network.findOne({ where: { name: { [Op.iLike]: String(networkName).replaceAll(" ", "-") - } + }, + chain_id: { [Op.eq]: +chain.chainId } } }); @@ -51,9 +61,12 @@ async function post(req: NextApiRequest, res: NextApiResponse) { }); const repositoryGithubId = repositoryDetails.repository.id; - let draftLabelId; + + let draftLabelId = repositoryDetails.repository.labels.nodes.find(({ name }) => name.toLowerCase() === "draft")?.id; + let chainLabelId = repositoryDetails.repository.labels.nodes. + find(({ name }) => name.toLowerCase() === chain.chainName.toLowerCase())?.id; - if (!repositoryDetails.repository.labels.nodes.length) { + if (!draftLabelId) { const createdLabel = await githubAPI(RepositoryQueries.CreateLabel, { name: "draft", repositoryId: repositoryGithubId, @@ -64,14 +77,26 @@ async function post(req: NextApiRequest, res: NextApiResponse) { }); draftLabelId = createdLabel.createLabel.label.id; - } else draftLabelId = repositoryDetails.repository.labels.nodes[0].id; + } + if (!chainLabelId) { + const createdLabel = await githubAPI(RepositoryQueries.CreateLabel, { + name: chain.chainName, + repositoryId: repositoryGithubId, + color: chain.color.replace("#", ""), + headers: { + accept: "application/vnd.github.bane-preview+json" + } + }); + + chainLabelId = createdLabel.createLabel.label.id; + } const createdIssue = await githubAPI(IssueQueries.Create, { repositoryId: repositoryGithubId, title, body, - labelId: [draftLabelId] + labelId: [draftLabelId, chainLabelId] }); const githubId = createdIssue.createIssue.issue.number; @@ -92,6 +117,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { body: body, network_id: network.id, tags, + chain_id: +chain.chainId, isKyc: !!isKyc, kycTierList: tierList?.map(Number).filter(id=> !Number.isNaN(id)) || [], }); @@ -99,7 +125,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { return res.status(200).json(`${repository.id}/${githubId}`); } -export default LogAccess(WithCors(async function Issue(req: NextApiRequest, res: NextApiResponse) { +export default LogAccess(WithCors(WithValidChainId(async function Issue(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "post": await post(req, res); @@ -110,4 +136,4 @@ export default LogAccess(WithCors(async function Issue(req: NextApiRequest, res: } res.end(); -})) +}))) diff --git a/pages/api/issue/working/index.ts b/pages/api/issue/working/index.ts index 1df0618487..372f6e45dc 100644 --- a/pages/api/issue/working/index.ts +++ b/pages/api/issue/working/index.ts @@ -8,25 +8,31 @@ import models from "db/models"; import * as CommentsQueries from "graphql/comments"; import * as IssueQueries from "graphql/issue"; +import {chainFromHeader} from "helpers/chain-from-header"; import {getPropertyRecursively} from "helpers/object"; -import {GraphQlQueryResponseData, GraphQlResponse} from "types/octokit"; +import {LogAccess} from "middleware/log-access"; +import {WithValidChainId} from "middleware/with-valid-chain-id"; +import WithCors from "middleware/withCors"; + +import {Logger} from "services/logging"; -import {Logger} from "../../../../services/logging"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; +import {GraphQlQueryResponseData, GraphQlResponse} from "types/octokit"; const { serverRuntimeConfig } = getConfig(); async function put(req: NextApiRequest, res: NextApiResponse) { const { issueId, githubLogin, networkName } = req.body; + const chain = await chainFromHeader(req); + try { const network = await models.network.findOne({ where: { name: { [Op.iLike]: String(networkName).replaceAll(" ", "-") - } + }, + chain_id: { [Op.eq]: +chain?.chainId } } }); @@ -82,8 +88,7 @@ async function put(req: NextApiRequest, res: NextApiResponse) { } } -async function Working(req: NextApiRequest, - res: NextApiResponse) { +async function Working(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "put": await put(req, res); @@ -97,4 +102,4 @@ async function Working(req: NextApiRequest, } Logger.changeActionName(`Issue/Working`); -export default LogAccess(WithCors(Working)) \ No newline at end of file +export default LogAccess(WithCors(WithValidChainId(Working))); diff --git a/pages/api/merge-proposal/[id].ts b/pages/api/merge-proposal/[id].ts index 16aea903a2..6911ccfb01 100644 --- a/pages/api/merge-proposal/[id].ts +++ b/pages/api/merge-proposal/[id].ts @@ -1,8 +1,9 @@ -import {RouteMiddleware} from "middleware"; import {NextApiRequest, NextApiResponse} from "next"; import models from "db/models"; +import {RouteMiddleware} from "middleware"; + async function get(req: NextApiRequest, res: NextApiResponse) { const {id} = req.query; @@ -12,8 +13,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { return res.status(200).json(merge); } -async function MergeProposal(req: NextApiRequest, - res: NextApiResponse) { +async function MergeProposal(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "get": await get(req, res); @@ -24,4 +24,4 @@ async function MergeProposal(req: NextApiRequest, } } -export default RouteMiddleware(MergeProposal) \ No newline at end of file +export default RouteMiddleware(MergeProposal); \ No newline at end of file diff --git a/pages/api/merge-proposal/poll/[...info].ts b/pages/api/merge-proposal/poll/[...info].ts index 0018906e52..6b03128968 100644 --- a/pages/api/merge-proposal/poll/[...info].ts +++ b/pages/api/merge-proposal/poll/[...info].ts @@ -1,8 +1,9 @@ import {NextApiRequest, NextApiResponse} from "next"; import {Bus} from "helpers/bus"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; + +import {LogAccess} from "middleware/log-access"; +import WithCors from "middleware/withCors"; async function get(req: NextApiRequest, res: NextApiResponse) { const { @@ -16,8 +17,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { }); } -async function PollMergeProposal(req: NextApiRequest, - res: NextApiResponse) { +async function PollMergeProposal(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "get": await get(req, res); @@ -28,4 +28,4 @@ async function PollMergeProposal(req: NextApiRequest, } } -export default LogAccess(WithCors(PollMergeProposal)); \ No newline at end of file +export default LogAccess(WithCors(PollMergeProposal)); diff --git a/pages/api/network/index.ts b/pages/api/network/index.ts index 90a9add742..26702c0ae5 100644 --- a/pages/api/network/index.ts +++ b/pages/api/network/index.ts @@ -1,31 +1,40 @@ -import {withCors} from "middleware"; import {NextApiRequest, NextApiResponse} from "next"; import getConfig from "next/config"; import {Octokit} from "octokit"; -import Sequelize, {Op} from "sequelize"; +import {Op, Sequelize} from "sequelize"; import Database from "db/models"; -import {handlefindOrCreateTokens, handleRemoveTokens} from "helpers/handleNetworkTokens"; +import {chainFromHeader} from "helpers/chain-from-header"; +import { WANT_TO_CREATE_NETWORK } from "helpers/constants"; +import decodeMessage from "helpers/decode-message"; +import { handlefindOrCreateTokens, handleRemoveTokens } from "helpers/handleNetworkTokens"; +import {isAdmin} from "helpers/is-admin"; +import {resJsonMessage} from "helpers/res-json-message"; import {Settings} from "helpers/settings"; +import {withCors} from "middleware"; +import { LogAccess } from "middleware/log-access"; +import {WithValidChainId} from "middleware/with-valid-chain-id"; + import DAO from "services/dao-service"; import IpfsStorage from "services/ipfs-service"; import {Logger} from 'services/logging'; -import {UNAUTHORIZED} from "../../../helpers/error-messages"; -import {LogAccess} from "../../../middleware/log-access"; - const {serverRuntimeConfig} = getConfig(); +const isTrue = (value: string) => value === "true"; async function get(req: NextApiRequest, res: NextApiResponse) { - const { name: networkName, creator: creatorAddress, isDefault } = req.query; + const { name: networkName, creator: creatorAddress, isDefault, address, byChainId, chainName } = req.query; + + const chain = await chainFromHeader(req); const where = { + ... isTrue(byChainId?.toString()) && chain ? { chain_id: { [Op.eq]: +chain?.chainId } } : {}, ... networkName && { name: { - [Op.iLike]: String(networkName).replaceAll(" ", "-") + [Op.iLike]: String(networkName) } } || {}, ... creatorAddress && { @@ -34,7 +43,10 @@ async function get(req: NextApiRequest, res: NextApiResponse) { } } || {}, ... isDefault && { - isDefault: isDefault === "true" + isDefault: isTrue(isDefault.toString()) + } || {}, + ... address && { + networkAddress: { [Op.iLike]: String(address) }, } || {} }; @@ -42,12 +54,25 @@ async function get(req: NextApiRequest, res: NextApiResponse) { attributes: { exclude: ["creatorAddress", "updatedAt"] }, include: [ { association: "tokens" }, - { association: "curators" } + { association: "curators" }, + { association: "networkToken" }, + { + association: "chain", + ... chainName ? { + where: { + chainShortName: Sequelize.where(Sequelize.fn("LOWER", Sequelize.col("chain.chainShortName")), + "=", + chainName.toString().toLowerCase()) + } + } : {}, + required: !!chainName + }, ], where }); - if (!network) return res.status(404); + if (!network) + return res.status(200).json({}); return res.status(200).json(network); } @@ -67,24 +92,50 @@ async function post(req: NextApiRequest, res: NextApiResponse) { githubLogin, allowedTokens, networkAddress, - isDefault + isDefault, + signedMessage } = req.body; + + const name = _name.replaceAll(" ", "-").toLowerCase(); + + if (!botPermission) return resJsonMessage("Bepro-bot authorization needed", res, 403); + + const chain = await chainFromHeader(req); - const name = _name?.replaceAll(" ", "-")?.toLowerCase() + const validateSignature = (assumedOwner: string) => + decodeMessage(chain.chainId, WANT_TO_CREATE_NETWORK, signedMessage, assumedOwner); - if (!botPermission) return res.status(403).json("Bepro-bot authorization needed"); + if (!validateSignature(creator)) + return resJsonMessage("Invalid signature", res, 403); const hasNetwork = await Database.network.findOne({ where: { creatorAddress: creator, + chain_id: +chain?.chainId, isClosed: false, } }); - if(hasNetwork) { - return res.status(409).json("Already exists a network created for this wallet"); + if (hasNetwork) { + return resJsonMessage("Already exists a network created for this wallet", res, 409); } + const sameNameOnOtherChain = await Database.network.findOne({ + where: { + isClosed: false, + chain_id: { + [Op.not]: +chain?.chainId + }, + name: { + [Op.iLike]: name + } + } + }); + + if (sameNameOnOtherChain) + if (!validateSignature(sameNameOnOtherChain.creatorAddress)) + return resJsonMessage("Network name owned by other wallet", res, 403); + const settings = await Database.settings.findAll({ where: { visibility: "public" }, raw: true, @@ -92,33 +143,32 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const publicSettings = (new Settings(settings)).raw(); - if (!publicSettings?.contracts?.networkRegistry) return res.status(500).json("Missing network registry contract"); - if (!publicSettings?.urls?.web3Provider) return res.status(500).json("Missing web3 provider url"); - if (isDefault && creator !== publicSettings?.defaultNetworkConfig?.adminWallet) - return res.status(401).json({message: UNAUTHORIZED}); - const defaultNetwork = await Database.network.findOne({ where: { - isDefault: true + isDefault: true, + isClosed: false, + chain_id: +chain?.chainId, } }); - + if (isDefault && defaultNetwork) - return res.status(409).json("Default Network already saved"); + return resJsonMessage("Default Network already saved", res, 409); // Contract Validations const DAOService = new DAO({ skipWindowAssignment: true, - web3Host: publicSettings.urls.web3Provider, - registryAddress: publicSettings.contracts.networkRegistry + web3Host: chain.chainRpc, + registryAddress: chain.registryAddress, }); - if (!await DAOService.start()) return res.status(500).json("Failed to connect with chain"); + if (!await DAOService.start()) + return resJsonMessage("Failed to connect with chain", res, 400); - if (!await DAOService.loadRegistry()) return res.status(500).json("Failed to load registry"); + if (!await DAOService.loadRegistry()) + return resJsonMessage("Failed to load registry", res, 400); if (await DAOService.hasNetworkRegistered(creator)) - return res.status(403).json("Already exists a network registered for this wallet"); + return resJsonMessage("Already exists a network registered for this wallet", res, 403); // Uploading logos to IPFS let fullLogoHash = null @@ -145,7 +195,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) { logoIcon: logoIconHash, fullLogo: fullLogoHash, networkAddress, - isDefault: isDefault || false + isDefault: isDefault || false, + chain_id: +chain?.chainId }); const repos = JSON.parse(repositories); @@ -157,8 +208,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) { }); } - if (!publicSettings?.github?.botUser) return res.status(500).json("Missing github bot user"); - if (!serverRuntimeConfig?.github?.token) return res.status(500).json("Missing github bot token"); + if (!publicSettings?.github?.botUser) return resJsonMessage("Missing github bot user", res, 500); + if (!serverRuntimeConfig?.github?.token) return resJsonMessage("Missing github bot token", res, 500); const octokitUser = new Octokit({ auth: accessToken @@ -236,31 +287,35 @@ async function put(req: NextApiRequest, res: NextApiResponse) { allowedTokens } = req.body; - const isAdminOverriding = !!override; - - if (!accessToken && !isAdminOverriding) return res.status(401).json({message: "Unauthorized user"}); + const isAdminOverriding = isAdmin(req) && !!override; + if (!accessToken && !isAdminOverriding) return resJsonMessage("Unauthorized user", res, 401); + + const chain = await chainFromHeader(req); + const network = await Database.network.findOne({ where: { - ...(isAdminOverriding ? {} : { - creatorAddress: - Sequelize.where(Sequelize.fn("LOWER", Sequelize.col("creatorAddress")), "=", creator.toLowerCase()) + ...(isAdminOverriding ? {} : { + creatorAddress: {[Op.iLike]: creator} }), - networkAddress + networkAddress: { + [Op.iLike]: networkAddress + }, + chain_id: chain.chainId }, include: [{ association: "repositories" }] }); - if (!network) return res.status(404).json("Invalid network"); + if (!network) return resJsonMessage("Invalid network", res, 404); if (network.isClosed && !isAdminOverriding) - return res.status(404).json("Invalid network"); + return resJsonMessage("Invalid network", res, 404); if (isClosed !== undefined) { network.isClosed = isClosed; await network.save(); - return res.status(200).json("Network closed"); + return resJsonMessage("Network closed", res, 200); } const settings = await Database.settings.findAll({ @@ -270,28 +325,31 @@ async function put(req: NextApiRequest, res: NextApiResponse) { const publicSettings = (new Settings(settings)).raw(); - if (!publicSettings?.contracts?.networkRegistry) return res.status(500).json("Missing network registry contract"); - if (!publicSettings?.urls?.web3Provider) return res.status(500).json("Missing web3 provider url"); + if (!chain.chainRpc) + return resJsonMessage("Missing chainRpc", res, 400); + + if (!chain.registryAddress) + return resJsonMessage("Missing registryAddress", res, 400); // Contract Validations const DAOService = new DAO({ skipWindowAssignment: true, - web3Host: publicSettings.urls.web3Provider, - registryAddress: publicSettings.contracts.networkRegistry + web3Host: chain.chainRpc, + registryAddress: chain.registryAddress, }); - if (!await DAOService.start()) return res.status(500).json("Failed to connect with chain"); - if (!await DAOService.loadRegistry()) return res.status(500).json("Failed to load factory contract"); + if (!await DAOService.start()) return resJsonMessage("Failed to connect with chain", res, 500); + if (!await DAOService.loadRegistry()) return resJsonMessage("Failed to load registry contract", res, 500); if (!isAdminOverriding) { const checkingNetworkAddress = await DAOService.getNetworkAdressByCreator(creator); - - if (checkingNetworkAddress !== networkAddress) - return res.status(403).json("Creator and network addresses do not match"); + + if (checkingNetworkAddress?.toLowerCase() !== networkAddress?.toLowerCase()) + return resJsonMessage("Creator and network addresses do not match", res, 403); } else { const isRegistryGovernor = await DAOService.isRegistryGovernor(creator); - if (!isRegistryGovernor) return res.status(403).json({message: UNAUTHORIZED}); + if (!isRegistryGovernor) return resJsonMessage("Unauthorized", res, 403); } const addingRepos = repositoriesToAdd ? JSON.parse(repositoriesToAdd) : []; @@ -301,13 +359,27 @@ async function put(req: NextApiRequest, res: NextApiResponse) { const exists = await Database.repositories.findOne({ where: { githubPath: { [Op.iLike]: String(repository.fullName) } - } + }, + include: [ + { + association: "network", + where: { + [Op.or]: [ + { + name: { [Op.not]: network.name } + }, + { + name: network.name, + creatorAddress: { [Op.not]: network.creatorAddress } + } + ] + } + } + ] }); if (exists) - return res - .status(403) - .json(`Repository ${repository.fullName} is already in use by another network `); + return resJsonMessage(`Repository ${repository.fullName} is already in use by another network `, res, 403); } const removingRepos = repositoriesToRemove @@ -322,7 +394,7 @@ async function put(req: NextApiRequest, res: NextApiResponse) { } }); - if (!exists) return res.status(404).json("Invalid repository"); + if (!exists) return resJsonMessage("Invalid repository", res, 404); const hasIssues = await Database.issue.findOne({ where: { @@ -331,9 +403,9 @@ async function put(req: NextApiRequest, res: NextApiResponse) { }); if (hasIssues) - return res - .status(403) - .json(`Repository ${repository.fullName} already has bounties and cannot be removed`); + return resJsonMessage(`Repository ${repository.fullName} already has bounties and cannot be removed`, + res, + 403); } if (isAdminOverriding && name) network.name = name; @@ -365,8 +437,8 @@ async function put(req: NextApiRequest, res: NextApiResponse) { }); const invitations = []; - - if (!publicSettings?.github?.botUser) return res.status(500).json("Missing github bot user"); + + if (!publicSettings?.github?.botUser) return resJsonMessage("Missing github bot user", res, 500); for (const repository of addingRepos) { const [owner, repo] = repository.fullName.split("/"); @@ -445,7 +517,7 @@ async function put(req: NextApiRequest, res: NextApiResponse) { if (exists) await exists.destroy(); } - return res.status(200).json("Network updated"); + return resJsonMessage("Network updated", res, 200); } catch (error) { console.log(error); return res.status(500).json(error); @@ -474,4 +546,4 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } Logger.changeActionName(`Network`); -export default LogAccess(withCors(handler)); \ No newline at end of file +export default LogAccess(withCors(WithValidChainId(handler))); diff --git a/pages/api/nft/index.ts b/pages/api/nft/index.ts index c01b5e06a9..ca45a94c3c 100644 --- a/pages/api/nft/index.ts +++ b/pages/api/nft/index.ts @@ -1,16 +1,20 @@ import {Bounty, ProposalDetail,} from "@taikai/dappkit"; import BigNumber from "bignumber.js"; -import { LogAccess } from "middleware/log-access"; -import WithCors from "middleware/withCors"; import {NextApiRequest, NextApiResponse} from "next"; import {Op} from "sequelize"; import models from "db/models"; import calculateDistributedAmounts from "helpers/calculateDistributedAmounts"; +import {chainFromHeader} from "helpers/chain-from-header"; import {formatNumberToNScale} from "helpers/formatNumber"; +import {resJsonMessage} from "helpers/res-json-message"; import {Settings} from "helpers/settings"; +import { LogAccess } from "middleware/log-access"; +import {WithValidChainId} from "middleware/with-valid-chain-id"; +import WithCors from "middleware/withCors"; + import DAO from "services/dao-service"; import ipfsService from "services/ipfs-service"; import {error as LogError} from "services/logging"; @@ -37,28 +41,30 @@ async function post(req: NextApiRequest, res: NextApiResponse) { mergerAddress, networkName } = req.body as NftPayload; - - if(!networkName || proposalContractId < 0 || issueContractId < 0) - return res.status(400).json("Missing parameters"); - - const settings = await models.settings.findAll({ - where: { - visibility: "public", - group: "urls" - }, - raw: true, - }); + const missingParams = [ + [networkName, 'Missing network name'], + [proposalContractId, 'Missing proposal contract id'], + [issueContractId, 'Missing bounty contract Id'] + ].filter(([v,]) => !["string", "number"].includes(typeof v)).map(([,m]) => m as string); + + if (missingParams.length) + return resJsonMessage(missingParams, res, 400); + + const settings = await models.settings.findAll({where: {visibility: "public", group: "urls"}, raw: true,}); const defaultConfig = (new Settings(settings)).raw(); - + if (!defaultConfig?.urls?.ipfs) return res.status(500).json("Missing ipfs url on settings"); + const chain = await chainFromHeader(req); + const customNetwork = await models.network.findOne({ where: { name: { [Op.iLike]: String(networkName).replaceAll(" ", "-") - } + }, + chain_id: { [Op.eq]: +chain?.chainId } } }); @@ -67,35 +73,41 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const DAOService = new DAO({ skipWindowAssignment: true, - web3Host: defaultConfig.urls.web3Provider, + web3Host: chain?.chainRpc, }); - if (!await DAOService.start()) return res.status(500).json("Failed to connect with chain"); - if(!await DAOService.loadNetwork(customNetwork.networkAddress)) - return res.status(500).json("network could not be loaded"); + if (!await DAOService.start()) + return resJsonMessage(`Failed to connect to chainRpc ${chain?.chainRpc} for id ${chain?.chainId}`, res, 500); + + const { networkAddress } = customNetwork; + + if(!await DAOService.loadNetwork(networkAddress)) + return resJsonMessage(`Failed to load networks on chainRpc ${chain?.chainRpc} for address ${networkAddress}`, + res, + 500); const network = DAOService.network; await network.start(); const networkBounty = await network.getBounty(issueContractId) as Bounty; - if (!networkBounty) return res.status(404).json("Bounty invalid"); + if (!networkBounty) return resJsonMessage("Bounty invalid", res, 404); if(networkBounty.canceled || networkBounty.closed) - return res.status(404).json("Bounty has been closed or canceled"); + return resJsonMessage("Bounty has been closed or canceled", res, 404); const proposal = networkBounty.proposals.find(p=> p.id === +proposalContractId) if(!proposal) - return res.status(404).json("Proposal invalid"); + return resJsonMessage("Proposal invalid", res, 404); if(proposal.refusedByBountyOwner || await network.isProposalDisputed(+issueContractId, +proposalContractId)) - return res.status(404).json("proposal cannot be accepted"); + return resJsonMessage("proposal cannot be accepted", res, 404); const pullRequest = networkBounty.pullRequests.find(pr=> pr.id === proposal.prId) if(pullRequest.canceled || !pullRequest.ready) - return res.status(404).json("PR cannot be accepted"); + return resJsonMessage("PR cannot be accepted", res, 404); const issue = await models.issue.findOne({ where: { @@ -154,7 +166,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const { hash } = await ipfsService.add(nft, true); - if(!hash) return res.status(500); + if (!hash) return resJsonMessage('no hash found', res, 400); const url = `${defaultConfig.urls.ipfs}/${hash}`; @@ -179,4 +191,4 @@ async function NftMethods(req: NextApiRequest, res: NextApiResponse) { res.end(); } -export default LogAccess(WithCors(NftMethods)); \ No newline at end of file +export default LogAccess(WithCors(WithValidChainId(NftMethods))); diff --git a/pages/api/payments/index.ts b/pages/api/payments/index.ts index 8f188d0b84..39f88b8446 100644 --- a/pages/api/payments/index.ts +++ b/pages/api/payments/index.ts @@ -3,57 +3,72 @@ import {NextApiRequest, NextApiResponse} from "next"; import {Op} from "sequelize"; import models from "db/models"; -import {LogAccess} from "../../../middleware/log-access"; -import WithCors from "../../../middleware/withCors"; + +import {chainFromHeader} from "helpers/chain-from-header"; +import {resJsonMessage} from "helpers/res-json-message"; + +import {LogAccess} from "middleware/log-access"; +import {WithValidChainId} from "middleware/with-valid-chain-id"; +import WithCors from "middleware/withCors"; async function get(req: NextApiRequest, res: NextApiResponse) { const {wallet, networkName, startDate, endDate} = req.query; + const chain = await chainFromHeader(req); + const network = await models.network.findOne({ where: { name: { [Op.iLike]: String(networkName).replaceAll(" ", "-") - } + }, + chain_id: {[Op.eq]: +chain?.chainId} } }); - if (!network) return res.status(404).json("Invalid network"); + if (!network) return resJsonMessage("Invalid network", res, 404); - let filter: Date[] | Date = null + let filter: { + createdAt?: { + [key: symbol]: Date | Date[] + } + } = {}; - if(startDate && endDate){ + if (startDate && endDate) { const initialDate = parseISO(startDate?.toString()) const finalDate = parseISO(endDate?.toString()) - if(isAfter(initialDate, finalDate)) return res.status(404).json("Invalid date"); + if (isAfter(initialDate, finalDate)) + return resJsonMessage("Invalid date", res, 400); - filter = [startOfDay(initialDate), endOfDay(finalDate)] - }else if(endDate){ - filter = parseISO(endDate?.toString()) + filter = { + createdAt: { + [Op.between]: [startOfDay(initialDate), endOfDay(finalDate)] + } + }; + } else if (endDate) { + filter = { + createdAt: { + [Op.lte]: parseISO(endDate?.toString()) + } + }; } - function handleOpFilter(Op) { - if(Array.isArray(filter)) { - return {[Op.between]: filter} - }else { - return {[Op.lte]: filter} - } - - } const payments = await models.userPayments.findAll({ include: [ { association: "issue", where: { network_id: network.id }, - include:[{ association: "token" }] + include:[{ association: "transactionalToken" }] } ], where: { - address: wallet, - transactionHash:{ + address: { + [Op.iLike]: wallet + }, + transactionHash: { [Op.not]: null }, - createdAt: filter && handleOpFilter(Op) + ...filter } }); @@ -75,4 +90,4 @@ async function Payments(req: NextApiRequest, res: NextApiResponse) { res.end(); } -export default LogAccess(WithCors(Payments)); \ No newline at end of file +export default LogAccess(WithCors(WithValidChainId(Payments))); diff --git a/pages/api/poll/index.ts b/pages/api/poll/index.ts index cce755f93b..f797ddc897 100644 --- a/pages/api/poll/index.ts +++ b/pages/api/poll/index.ts @@ -2,7 +2,7 @@ import {NextApiRequest, NextApiResponse} from "next"; import {Bus} from "helpers/bus"; -import {RouteMiddleware} from "../../../middleware"; +import {RouteMiddleware} from "middleware"; async function post(req: NextApiRequest, res: NextApiResponse) { const { eventName, ...rest } = req.body; @@ -36,8 +36,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { }); } -export default RouteMiddleware(async function PollBody(req: NextApiRequest, - res: NextApiResponse) { +export default RouteMiddleware(async function PollBody(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "post": await post(req, res); diff --git a/pages/api/pull-request/index.ts b/pages/api/pull-request/index.ts index e48c99a536..ffa008a978 100644 --- a/pages/api/pull-request/index.ts +++ b/pages/api/pull-request/index.ts @@ -8,14 +8,17 @@ import models from "db/models"; import * as PullRequestQueries from "graphql/pull-request"; import * as RepositoryQueries from "graphql/repository"; +import {chainFromHeader} from "helpers/chain-from-header"; import paginate from "helpers/paginate"; -import {Settings} from "helpers/settings"; +import {resJsonMessage} from "helpers/res-json-message"; + +import {LogAccess} from "middleware/log-access"; +import {WithValidChainId} from "middleware/with-valid-chain-id"; +import WithCors from "middleware/withCors"; import DAO from "services/dao-service"; import {GraphQlResponse} from "types/octokit"; -import {LogAccess} from "../../../middleware/log-access"; -import WithCors from "../../../middleware/withCors"; const { serverRuntimeConfig } = getConfig(); @@ -36,18 +39,19 @@ async function get(req: NextApiRequest, res: NextApiResponse) { where: { name: { [Op.iLike]: String(networkName).replaceAll(" ", "-") - } + }, + // chain_id: {[Op.eq]: +chain?.chainId} } }); - if (!network) return res.status(404).json("Invalid network"); - if (network.isClosed) return res.status(404).json("Invalid network"); + if (!network || network?.isClosed) return resJsonMessage("Invalid network", res, 404); + const issue = await models.issue.findOne({ where: { issueId, network_id: network.id } }); - if (!issue) return res.status(404).json("Issue not found"); + if (!issue) return resJsonMessage("Issue not found", res, 404); where.issueId = issue.id; } @@ -82,15 +86,19 @@ async function post(req: NextApiRequest, res: NextApiResponse) { networkName } = req.body; + const chain = await chainFromHeader(req); + const customNetwork = await models.network.findOne({ where: { name: { [Op.iLike]: String(networkName).replaceAll(" ", "-") - } + }, + chain_id: { [Op.eq]: +chain?.chainId } } }); - if (!customNetwork || customNetwork?.isClosed) return res.status(404).json("Invalid"); + if (!customNetwork || customNetwork?.isClosed) + return resJsonMessage("Invalid network", res, !customNetwork ? 404 : 400); const issue = await models.issue.findOne({ where: { githubId, repository_id } @@ -105,6 +113,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const [owner, repo] = repoInfo.githubPath.split("/"); + // todo move this to a setup script on webnetwork-e2e project instead if(serverRuntimeConfig?.e2eEnabled === true) { await models.pullRequest.create({ @@ -187,15 +196,20 @@ async function del(req: NextApiRequest, res: NextApiResponse) { userBranch } = req.body; + const chain = await chainFromHeader(req); + const customNetwork = await models.network.findOne({ where: { name: { [Op.iLike]: String(customNetworkName) - } + }, + // chain_id: {[Op.eq]: +chain?.chainId} } }); - if (!customNetwork || customNetwork?.isClosed) return res.status(404).json("Invalid"); + if (!customNetwork || customNetwork?.isClosed) + return resJsonMessage("Invalid", res, 404); + const issue = await models.issue.findOne({ where: { issueId, @@ -221,26 +235,17 @@ async function del(req: NextApiRequest, res: NextApiResponse) { } }); - if (!pullRequest) return res.status(404).json("Invalid"); - - const settings = await models.settings.findAll({ - where: { visibility: "public" }, - raw: true, - }); - - const publicSettings = (new Settings(settings)).raw(); - - if (!publicSettings?.urls?.web3Provider) return res.status(500).json("Missing web3 provider url"); + if (!pullRequest) return resJsonMessage("Invalid", res, 404); const DAOService = new DAO({ skipWindowAssignment: true, - web3Host: publicSettings.urls.web3Provider + web3Host: chain?.chainRpc }); - if (!await DAOService.start()) return res.status(500).json("Failed to connect with chain"); + if (!await DAOService.start()) return resJsonMessage("Failed to connect with chain", res, 400); if (!await DAOService.loadNetwork(customNetwork.networkAddress)) - return res.status(500).json("Failed to load network contract"); + return resJsonMessage("Failed to load network contract", res, 400); const network = DAOService.network; @@ -248,7 +253,7 @@ async function del(req: NextApiRequest, res: NextApiResponse) { const networkBounty = await network.getBounty(contractId); - if (!networkBounty) return res.status(404).json("Invalid"); + if (!networkBounty) return resJsonMessage("Bounty not found", res, 404); const githubAPI = (new Octokit({ auth: serverRuntimeConfig?.github?.token })).graphql; @@ -290,4 +295,4 @@ async function PullRequest(req: NextApiRequest, res: NextApiResponse) { res.end(); } -export default LogAccess(WithCors(PullRequest)); +export default LogAccess(WithCors(WithValidChainId(PullRequest))); diff --git a/pages/api/pull-request/merge/index.ts b/pages/api/pull-request/merge/index.ts index a4314e5631..eb032a477e 100644 --- a/pages/api/pull-request/merge/index.ts +++ b/pages/api/pull-request/merge/index.ts @@ -7,13 +7,16 @@ import models from "db/models"; import * as PullRequestQueries from "graphql/pull-request"; -import {Settings} from "helpers/settings"; +import {chainFromHeader} from "helpers/chain-from-header"; +import {resJsonMessage} from "helpers/res-json-message"; + +import {LogAccess} from "middleware/log-access"; +import {WithValidChainId} from "middleware/with-valid-chain-id"; +import WithCors from "middleware/withCors"; import DAO from "services/dao-service"; import {GraphQlResponse} from "types/octokit"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; const { serverRuntimeConfig } = getConfig(); @@ -21,48 +24,46 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const { issueId, pullRequestId, mergeProposalId, address, networkName } = req.body; + const chain = await chainFromHeader(req); + try { const customNetwork = await models.network.findOne({ where: { name: { [Op.iLike]: String(networkName).replaceAll(" ", "-") - } + }, + chain_id: {[Op.eq]: +chain?.chainId} } }); - if (!customNetwork) return res.status(404).json("Invalid network"); - if (customNetwork.isClosed) return res.status(404).json("Invalid network"); + if (!customNetwork || customNetwork?.isClosed) + return resJsonMessage("Invalid network", res, 404); const issue = await models.issue.findOne({ where: { issueId, network_id: customNetwork.id } }); - if (!issue) return res.status(404).json("Bounty not found"); + if (!issue) return resJsonMessage("Bounty not found", res, 404); const pullRequest = await models.pullRequest.findOne({ where: { githubId: pullRequestId, issueId: issue.id } }); - if (!pullRequest) return res.status(404).json("Pull Request not found"); + if (!pullRequest) return resJsonMessage("Pull Request not found", res, 404); - const settings = await models.settings.findAll({ - where: { visibility: "public" }, - raw: true, - }); - - const publicSettings = (new Settings(settings)).raw(); - - if (!publicSettings?.urls?.web3Provider) return res.status(500).json("Missing web3 provider url"); + if (!chain.chainRpc) + return resJsonMessage("Missing web3 provider url", res, 400); const DAOService = new DAO({ skipWindowAssignment: true, - web3Host: publicSettings.urls.web3Provider + web3Host: chain.chainRpc }); - if (!await DAOService.start()) return res.status(500).json("Failed to connect with chain"); + if (!await DAOService.start()) + return resJsonMessage("Failed to connect with chain", res, 400); if (!await DAOService.loadNetwork(customNetwork.networkAddress)) - return res.status(500).json("Failed to load network contract"); + return resJsonMessage("Failed to load network contract", res, 400); const network = DAOService.network; @@ -70,14 +71,14 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const issueBepro = await network.getBounty(issue.contractId); - if (!issueBepro) return res.status(404).json("Bounty not found on network"); + if (!issueBepro) return resJsonMessage("Bounty not found on network", res, 404); if (issueBepro.canceled || !issueBepro.closed) - return res.status(400).json("Bounty canceled or not closed yet"); + return resJsonMessage("Bounty canceled or not closed yet", res, 400); const proposal = issueBepro.proposals[mergeProposalId]; - if (!proposal) return res.status(404).json("Merge proposal not found"); + if (!proposal) return resJsonMessage("Merge proposal not found", res, 400); const isCouncil = await network.getOraclesOf(address) >= await network.councilAmount(); @@ -117,8 +118,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { } } -async function PullRequest(req: NextApiRequest, - res: NextApiResponse) { +async function PullRequest(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "post": await post(req, res); @@ -131,4 +131,4 @@ async function PullRequest(req: NextApiRequest, res.end(); } -export default LogAccess(WithCors(PullRequest)); \ No newline at end of file +export default LogAccess(WithCors(WithValidChainId(PullRequest))); diff --git a/pages/api/pull-request/review/index.ts b/pages/api/pull-request/review/index.ts index 717e438f92..42e52f4c2f 100644 --- a/pages/api/pull-request/review/index.ts +++ b/pages/api/pull-request/review/index.ts @@ -7,26 +7,36 @@ import models from "db/models"; import * as PullRequestQueries from "graphql/pull-request"; +import {chainFromHeader} from "helpers/chain-from-header"; +import {resJsonMessage} from "helpers/res-json-message"; + +import {LogAccess} from "middleware/log-access"; +import {WithValidChainId} from "middleware/with-valid-chain-id"; +import WithCors from "middleware/withCors"; + +import {error} from "services/logging"; + import {GraphQlResponse} from "types/octokit"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; const { serverRuntimeConfig } = getConfig(); async function put(req: NextApiRequest, res: NextApiResponse) { const { issueId, pullRequestId, githubLogin, body, networkName, event } = req.body; + const chain = await chainFromHeader(req); + try { const network = await models.network.findOne({ where: { name: { [Op.iLike]: String(networkName) - } + }, + chain_id: {[Op.eq]: +chain?.chainId} } }); - if (!network) return res.status(404).json("Invalid network"); - if (network.isClosed) return res.status(404).json("Invalid network"); + if (!network || network?.isClosed) + return resJsonMessage("Invalid network", res, 404); const issue = await models.issue.findOne({ where: { issueId, network_id: network.id } @@ -78,17 +88,16 @@ async function put(req: NextApiRequest, res: NextApiResponse) { } return res.status(200).json(review); - } catch (error) { - console.log(error); - return res.status(error.status || 500).json(error?.errors && { - data: error.response?.data, - errors: error.response?.error - } || error); + } catch (e) { + error(e); + return res.status(e.status || 500).json(e?.errors && { + data: e.response?.data, + errors: e.response?.error + } || e); } } -async function PullRequestReview(req: NextApiRequest, - res: NextApiResponse) { +async function PullRequestReview(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "put": await put(req, res); @@ -101,4 +110,4 @@ async function PullRequestReview(req: NextApiRequest, res.end(); } -export default LogAccess(WithCors(PullRequestReview)); \ No newline at end of file +export default LogAccess(WithCors(WithValidChainId(PullRequestReview))); diff --git a/pages/api/repos/index.ts b/pages/api/repos/index.ts index 6a818c66ec..4046c208f3 100644 --- a/pages/api/repos/index.ts +++ b/pages/api/repos/index.ts @@ -1,12 +1,17 @@ -import {LogAccess} from "middleware/log-access"; -import WithCors from "middleware/withCors"; import {NextApiRequest, NextApiResponse} from "next"; import {Op, WhereOptions} from "sequelize"; import models from "db/models"; +import {NOT_AN_ADMIN} from "helpers/constants"; +import {isAdmin} from "helpers/is-admin"; +import {resJsonMessage} from "helpers/res-json-message"; + +import {LogAccess} from "middleware/log-access"; +import WithCors from "middleware/withCors"; + async function getAllRepos(req, res) { - const { networkName, withBounties } = req.query; + const { networkName, withBounties, chainId } = req.query; const include = withBounties ? [ { @@ -23,7 +28,8 @@ async function getAllRepos(req, res) { where: { name: { [Op.iLike]: String(networkName).replaceAll(" ", "-") - } + }, + ... chainId ? { chain_id: chainId } : {} } }); @@ -41,6 +47,9 @@ async function getAllRepos(req, res) { } async function addNewRepo(req, res) { + if (!isAdmin(req)) + return res.status(401).json({message: NOT_AN_ADMIN}); + const issues = (await models.issue.findAndCountAll())?.count; if (issues) return res @@ -55,18 +64,20 @@ async function addNewRepo(req, res) { const found = await models.repositories.findOne({ where: { githubPath: `${owner}/${repo}` } }); + if (found) return res.status(409).json("Path already exists"); const network = await models.network.findOne({ where: { name: { [Op.iLike]: String(networkName).replaceAll(" ", "-") - } + }, + // chain_id: {[Op.eq]: +chain?.chainId} } }); - if (!network) return res.status(404).json("Invalid network"); - if (network.isClosed) return res.status(404).json("Invalid network"); + if (!network || network?.isClosed) + return resJsonMessage("Invalid network", res, 404); const created = await models.repositories .create({ githubPath: `${owner}/${repo}`, network_id: network.id }) @@ -79,6 +90,10 @@ async function addNewRepo(req, res) { } async function removeRepo(req: NextApiRequest, res: NextApiResponse) { + + if (!isAdmin(req)) + return res.status(401).json({message: NOT_AN_ADMIN}); + const issues = (await models.issue.findAndCountAll())?.count; if (issues) return res diff --git a/pages/api/rss/index.ts b/pages/api/rss/index.ts index 27a0eba46e..4ec01eae26 100644 --- a/pages/api/rss/index.ts +++ b/pages/api/rss/index.ts @@ -8,11 +8,10 @@ import {Op} from "sequelize"; import models from "db/models"; +import {LogAccess} from "middleware/log-access"; import {error as LogError} from 'services/logging'; -import {LogAccess} from "../../../middleware/log-access"; - const { publicRuntimeConfig } = getConfig(); const convertMinutesToMs = minutes => +minutes * 60 * 1000; diff --git a/pages/api/search/count/index.ts b/pages/api/search/count/index.ts deleted file mode 100644 index 4ef3e27256..0000000000 --- a/pages/api/search/count/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Database from "db/models"; -import {Op, Sequelize} from "sequelize"; -import {NextApiRequest, NextApiResponse} from "next"; - -export default async function Count(req: NextApiRequest, res: NextApiResponse) { - - // add this to associations (allIssues, openIssues) - const issues_exclude = ['id', 'githubId', 'title', 'branch', 'network_id', 'contractId', 'creatorAddress', 'state', 'creatorGithub', 'amount', 'fundingAmount', 'fundedAmount', 'repository_id', 'body', 'working', 'merged', 'seoImage', 'tokenId', 'fundedAt', 'tags', 'isKyc', 'kycTierList', 'createdAt', 'updatedAt'] - - const result = await Database.network.findAll({ - include: [ - { association: "tokens" }, - {association: 'curators', attributes: [] }, - {association: 'issues', attributes: [], where: {state: {[Op.ne]: 'pending'}}}, - ], - attributes: [ - "network.id", - [Sequelize.fn('sum', Sequelize.cast(Sequelize.col('curators.tokensLocked'), 'int')), 'tokensLocked'], - [Sequelize.fn('count', Sequelize.col('issues.id')), 'totalIssues'], - // create new association `openIssues` - // [Sequelize.fn('count', Sequelize.col('openIssues.id')), 'openIssues'], - ], - group: ['network.id', 'tokens->network_tokens.id', 'tokens.id'], - where: { - isRegistered: true, - isClosed: false, - }, - - }); - - res.status(200).json(result); - return res.end(); -} \ No newline at end of file diff --git a/pages/api/search/curators/index.ts b/pages/api/search/curators/index.ts index d9a7160371..513c485b5f 100644 --- a/pages/api/search/curators/index.ts +++ b/pages/api/search/curators/index.ts @@ -4,25 +4,38 @@ import {Op, WhereOptions} from "sequelize"; import models from "db/models"; import paginate, {calculateTotalPages} from "helpers/paginate"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; +import {resJsonMessage} from "helpers/res-json-message"; + +import {LogAccess} from "middleware/log-access"; +import {WithValidChainId} from "middleware/with-valid-chain-id"; +import WithCors from "middleware/withCors"; + +import {error} from "services/logging"; async function get(req: NextApiRequest, res: NextApiResponse) { try { const whereCondition: WhereOptions = {}; - const { address, isCurrentlyCurator, networkName, page } = req.query || {}; + const { address, isCurrentlyCurator, networkName, page, chainShortName } = req.query || {}; if (networkName) { const network = await models.network.findOne({ where: { name: { [Op.iLike]: String(networkName).replaceAll(" ", "-"), - }, + } }, + include: [ + { + association: "chain", + where: { + chainShortName: chainShortName + } + } + ] }); - if (!network) return res.status(404).json("Invalid network"); + if (!network) return resJsonMessage("Invalid network", res, 404); whereCondition.networkId = network?.id; } @@ -44,10 +57,10 @@ async function get(req: NextApiRequest, res: NextApiResponse) { [[req.query.sortBy || "acceptedProposals", req.query.order || "DESC"]])) .then(async (items) => { return Promise.all(items.rows.map(async (item) => { - const disputes = await models.dispute.count({ + item.dataValues.disputes = + await models.dispute.count({ where: { address: item.address }, - }); - item.dataValues.disputes = disputes; + }); return item; })) .then((values) => ({ count: items.count, rows: values })) @@ -60,7 +73,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { pages: calculateTotalPages(curators.count), }); } catch (e) { - console.error(e); + error(e) return res.status(500); } } @@ -77,4 +90,4 @@ async function SearchCurators(req: NextApiRequest, res: NextApiResponse) { res.end(); } -export default LogAccess(WithCors(SearchCurators)); +export default LogAccess(WithCors(WithValidChainId(SearchCurators))); diff --git a/pages/api/search/issues/index.ts b/pages/api/search/issues/index.ts index 397cb8e054..c9f360d665 100644 --- a/pages/api/search/issues/index.ts +++ b/pages/api/search/issues/index.ts @@ -7,8 +7,12 @@ import models from "db/models"; import handleNetworkValues from "helpers/handleNetworksValuesApi"; import paginate, {calculateTotalPages, paginateArray} from "helpers/paginate"; import {searchPatternInText} from "helpers/string"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; + +import {LogAccess} from "middleware/log-access"; +import {WithValidChainId} from "middleware/with-valid-chain-id"; +import WithCors from "middleware/withCors"; + +import {error} from "services/logging"; const COLS_TO_CAST = ["amount", "fundingAmount"]; const castToDecimal = columnName => Sequelize.cast(Sequelize.col(columnName), 'DECIMAL'); @@ -16,7 +20,7 @@ const iLikeCondition = (key, value) => ({[key]: {[Op.iLike]: value}}); async function get(req: NextApiRequest, res: NextApiResponse) { try { - let networks = [] + let networks = []; const whereCondition: WhereOptions = {state: {[Op.not]: "pending"}}; const { state, @@ -33,7 +37,8 @@ async function get(req: NextApiRequest, res: NextApiResponse) { networkName, allNetworks, repoPath, - tokenAddress + tokenAddress, + chainId } = req.query || {}; if (state) whereCondition.state = state; @@ -44,15 +49,19 @@ async function get(req: NextApiRequest, res: NextApiResponse) { if (creator) whereCondition.creatorGithub = creator; - if (address) whereCondition.creatorAddress = address; + if (address) + whereCondition.creatorAddress = { + [Op.iLike]: address.toString() + }; if (networkName) { const network = await models.network.findOne({ - where: { - name: { - [Op.iLike]: String(networkName).replaceAll(" ", "-") + where: { + name: { + [Op.iLike]: String(networkName).replaceAll(" ", "-"), + }, + ... chainId ? { chain_id: +chainId } : {} } - } }); if (!network) return res.status(404).json("Invalid network"); @@ -64,7 +73,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { networks = await models.network.findAll({ where: { isRegistered: true, - isClosed: false + isClosed: false, }, include: [ { association: "curators" } @@ -76,6 +85,9 @@ async function get(req: NextApiRequest, res: NextApiResponse) { whereCondition.network_id = {[Op.in]: networks.map(network => network.id)} } + if (chainId) + whereCondition.chain_id = { [Op.eq]: +chainId }; + if (repoPath) { const repository = await models.repositories.findOne({ where: { @@ -149,7 +161,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { }, { association: "repository", attributes: ["id", "githubPath"] }, { - association: "token", + association: "transactionalToken", required: !!tokenAddress, where: { ...(tokenAddress ? { @@ -157,6 +169,12 @@ async function get(req: NextApiRequest, res: NextApiResponse) { } : {}) } }, + { + association: "network", + include: [ + { association: "chain" } + ] + } ]; if (state === "closed") @@ -196,12 +214,14 @@ async function get(req: NextApiRequest, res: NextApiResponse) { currentPage: +paginatedData.page }); } else { - - const issues = await models.issue.findAndCountAll(paginate({ - where: whereCondition, - include, nest: true }, req.query, [ + const issues = await models.issue.findAndCountAll(paginate({ + where: whereCondition, + include, + nest: true + }, req.query, [ [...sortBy|| ["createdAt"], req.query.order || "DESC"] - ])).then(data => handleNetworkValues(data)) + ])) + .then(data => handleNetworkValues(data)); return res.status(200).json({ ...issues, @@ -209,13 +229,12 @@ async function get(req: NextApiRequest, res: NextApiResponse) { pages: calculateTotalPages(issues.count) })} } catch(e){ - console.error(e) + error(e); return res.status(500) } } -async function SearchIssues(req: NextApiRequest, - res: NextApiResponse) { +async function SearchIssues(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "get": await get(req, res); @@ -227,4 +246,4 @@ async function SearchIssues(req: NextApiRequest, res.end(); } -export default LogAccess(WithCors(SearchIssues)) \ No newline at end of file +export default LogAccess(WithCors(WithValidChainId(SearchIssues))); diff --git a/pages/api/search/issues/recent.ts b/pages/api/search/issues/recent.ts index 1dbb2c9549..f3b6a31fe6 100644 --- a/pages/api/search/issues/recent.ts +++ b/pages/api/search/issues/recent.ts @@ -3,9 +3,8 @@ import {Op, WhereOptions} from "sequelize"; import models from "db/models"; -import handleNetworkValues from 'helpers/handleNetworksValuesApi'; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; +import {LogAccess} from "middleware/log-access"; +import WithCors from "middleware/withCors"; const getLastIssuesByStatus = async (state, whereCondition, sortBy, order, limit = 3) => (models.issue.findAll({ where: { @@ -14,9 +13,12 @@ const getLastIssuesByStatus = async (state, whereCondition, sortBy, order, limit }, order: [[ String(sortBy), String(order) ]], include: [ - { association: "network", attributes: ['colors', 'name', 'logoIcon'] }, + { + association: "network", + include: [ { association: "chain" }] + }, { association: "repository" }, - { association: "token" } + { association: "transactionalToken" } ], limit })) @@ -42,7 +44,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { const network = await models.network.findOne({ where: { name: { - [Op.iLike]: String(networkName).replaceAll(" ", "-") + [Op.iLike]: networkName.toString() } } }); @@ -66,13 +68,12 @@ async function get(req: NextApiRequest, res: NextApiResponse) { const issuesOpen = await getLastIssuesByStatus("open", whereCondition, sortBy, - order).then((data) => handleNetworkValues(data)); + order); return res.status(200).json(issuesOpen); } -async function getAll(req: NextApiRequest, - res: NextApiResponse) { +async function getAll(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "get": await get(req, res); @@ -85,4 +86,4 @@ async function getAll(req: NextApiRequest, res.end(); } -export default LogAccess(WithCors(getAll)) \ No newline at end of file +export default LogAccess(WithCors(getAll)) diff --git a/pages/api/search/issues/total.ts b/pages/api/search/issues/total.ts index 550ae3590d..e916ba8a6e 100644 --- a/pages/api/search/issues/total.ts +++ b/pages/api/search/issues/total.ts @@ -2,8 +2,13 @@ import {NextApiRequest, NextApiResponse} from "next"; import {Op, WhereOptions} from "sequelize"; import models from "db/models"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; + +import { resJsonMessage } from "helpers/res-json-message"; + +import { WithJwt } from "middleware"; +import {LogAccess} from "middleware/log-access"; +import {WithValidChainId} from "middleware/with-valid-chain-id"; +import WithCors from "middleware/withCors"; async function getTotal(req: NextApiRequest, res: NextApiResponse) { const whereCondition: WhereOptions = {state: {[Op.not]: "pending"}}; @@ -26,30 +31,19 @@ async function getTotal(req: NextApiRequest, res: NextApiResponse) { if (address) whereCondition.creatorAddress = address; - if (networkName) { - const network = await models.network.findOne({ - where: { - name: { - [Op.iLike]: String(networkName).replaceAll(" ", "-") - } - } - }); - - if (!network) return res.status(404).json("Invalid network"); - - whereCondition.network_id = network?.id; - } else { - const networks = await models.network.findAll({ - where: { - isRegistered: true, - isClosed: false - } - }) - - if (networks.length === 0) return res.status(404).json("Networks not found"); - - whereCondition.network_id = {[Op.in]: networks.map(network => network.id)} - } + const networks = await models.network.findAll({ + where: { + isRegistered: true, + isClosed: false, + ... networkName ? { + name: { [Op.iLike]: String(networkName) } + } : {} + } + }) + + if (networks.length === 0) return resJsonMessage("Networks not found", res, 404); + + whereCondition.network_id = { [Op.in]: networks.map(network => network.id) }; const issueCount = await models.issue.count({ where: whereCondition @@ -58,8 +52,7 @@ async function getTotal(req: NextApiRequest, res: NextApiResponse) { return res.status(200).json(issueCount); } -async function getAll(req: NextApiRequest, - res: NextApiResponse) { +async function getAll(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "get": await getTotal(req, res); @@ -72,4 +65,4 @@ async function getAll(req: NextApiRequest, res.end(); } -export default LogAccess(WithCors(getAll)); \ No newline at end of file +export default LogAccess(WithCors(WithJwt(WithValidChainId(getAll)))); diff --git a/pages/api/search/leaderboard/index.ts b/pages/api/search/leaderboard/index.ts index b6ce058053..e069d47d78 100644 --- a/pages/api/search/leaderboard/index.ts +++ b/pages/api/search/leaderboard/index.ts @@ -6,14 +6,15 @@ import models from "db/models"; import paginate, {calculateTotalPages, paginateArray} from "helpers/paginate"; import {searchPatternInText} from "helpers/string"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; + +import {LogAccess} from "middleware/log-access"; +import WithCors from "middleware/withCors"; async function get(req: NextApiRequest, res: NextApiResponse) { try { const whereCondition: WhereOptions = {}; - const { address, page, sortBy, time, search } = req.query || {}; + const { address, page, time, search } = req.query || {}; if (address) whereCondition.address = address; diff --git a/pages/api/search/leaderboard/points/index.ts b/pages/api/search/leaderboard/points/index.ts index d1243bd7ff..e493fcc01b 100644 --- a/pages/api/search/leaderboard/points/index.ts +++ b/pages/api/search/leaderboard/points/index.ts @@ -5,8 +5,9 @@ import models from "db/models"; import {calculateLeaderboardScore} from "helpers/leaderboard-score"; import paginate, {calculateTotalPages} from "helpers/paginate"; -import {LogAccess} from "../../../../../middleware/log-access"; -import WithCors from "../../../../../middleware/withCors"; + +import {LogAccess} from "middleware/log-access"; +import WithCors from "middleware/withCors"; async function get(req: NextApiRequest, res: NextApiResponse) { try { diff --git a/pages/api/search/networks/active.ts b/pages/api/search/networks/active.ts index 52037803fd..34e985fcac 100644 --- a/pages/api/search/networks/active.ts +++ b/pages/api/search/networks/active.ts @@ -5,13 +5,14 @@ import {Op, WhereOptions} from "sequelize"; import models from "db/models"; import {paginateArray} from "helpers/paginate"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; + +import {LogAccess} from "middleware/log-access"; +import WithCors from "middleware/withCors"; async function get(req: NextApiRequest, res: NextApiResponse) { const whereCondition: WhereOptions = {}; - const { creatorAddress, isClosed, isRegistered, page, sortBy, order} = req.query || {}; + const { name, creatorAddress, isClosed, isRegistered, page, sortBy, order} = req.query || {}; if (creatorAddress) whereCondition.creatorAddress = { [Op.iLike]: String(creatorAddress) }; @@ -21,6 +22,9 @@ async function get(req: NextApiRequest, res: NextApiResponse) { if (isRegistered) whereCondition.isRegistered = isRegistered; + + if (name) + whereCondition.name = name; const include = [ { association: "tokens" }, @@ -29,7 +33,8 @@ async function get(req: NextApiRequest, res: NextApiResponse) { state: {[Op.not]: "pending" } } }, - { association: "curators" } + { association: "curators" }, + { association: "chain" } ]; const networks = await models.network.findAll({ @@ -49,7 +54,9 @@ async function get(req: NextApiRequest, res: NextApiResponse) { logoIcon: network?.logoIcon, totalValueLock: network?.curators?.reduce((ac, cv) => BigNumber(ac).plus(cv?.tokensLocked || 0), BigNumber(0)), - totalIssues: network?.issues?.length || 0 + totalIssues: network?.issues?.length || 0, + countIssues: network?.issues?.length || 0, + chain: network?.chain }; }) @@ -71,8 +78,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { }); } -async function SearchNetworks(req: NextApiRequest, - res: NextApiResponse) { +async function SearchNetworks(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "get": await get(req, res); @@ -84,4 +90,5 @@ async function SearchNetworks(req: NextApiRequest, res.end(); } -export default LogAccess(WithCors(SearchNetworks)); \ No newline at end of file + +export default LogAccess(WithCors(SearchNetworks)); diff --git a/pages/api/search/networks/index.ts b/pages/api/search/networks/index.ts index cbd1b4c8cd..1931a37656 100644 --- a/pages/api/search/networks/index.ts +++ b/pages/api/search/networks/index.ts @@ -1,40 +1,48 @@ import {NextApiRequest, NextApiResponse} from "next"; import {Op, Sequelize, WhereOptions} from "sequelize"; -import {Fn, Literal} from "sequelize/types/utils"; +import {Fn, Literal, Where} from "sequelize/types/utils"; import models from "db/models"; import {paginateArray} from "helpers/paginate"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; +import {LogAccess} from "middleware/log-access"; +import WithCors from "middleware/withCors"; + +import { Logger } from "services/logging"; + +type StrOrNmb = string | string[] | number; interface includeProps { association: string; required?: boolean; attributes?: string[]; where?: { - state?: { - [Op.ne]?: string; - } | string; - } + [col: string]: StrOrNmb | { [key: symbol]: StrOrNmb | Where } | Where + }; } async function get(req: NextApiRequest, res: NextApiResponse) { const whereCondition: WhereOptions = {}; - const { - name, - creatorAddress, - networkAddress, - isClosed, - isRegistered, - isDefault, + const { + name, + creatorAddress, + networkAddress, + isClosed, + isRegistered, + isDefault, + page, + chainId, isNeedCountsAndTokensLocked, - page, + chainShortName } = req.query || {}; - if (name) whereCondition.name = name; + if (name) + whereCondition.name = { + [Op.and]: + [Sequelize.where(Sequelize.fn("LOWER", Sequelize.col("network.name")), "=", name.toString().toLowerCase())] + }; if (creatorAddress) whereCondition.creatorAddress = { [Op.iLike]: String(creatorAddress) }; @@ -50,9 +58,23 @@ async function get(req: NextApiRequest, res: NextApiResponse) { if (isDefault) whereCondition.isDefault = isDefault; - + + if (chainId) + whereCondition.chain_id = chainId; + const include: includeProps[] = [ - { association: "tokens" } + { association: "tokens" }, + { + association: "chain", + where: { + ... chainShortName ? { + chainShortName: Sequelize.where(Sequelize.fn("LOWER", Sequelize.col("chain.chainShortName")), + "=", + chainShortName.toString().toLowerCase()) + } : {} + } + }, + { association: "networkToken"} ]; let group: string[] = [] @@ -65,10 +87,13 @@ async function get(req: NextApiRequest, res: NextApiResponse) { const caseZeroThen1 = (clause: string) => `case when ${clause} = 0 then 1 else ${clause} end`; include.push({ association: "curators", required: false, attributes: [] }) - include.push({ association: "issues", required: false, attributes: [], - where: { - state: {[Op.ne]: "pending" } - } + include.push({ + association: "issues", + required: false, + attributes: [], + where: { + state: {[Op.ne]: "pending" } + } }) include.push({ association: 'openIssues', required: false, attributes: [], where: {state: 'open'}},) attributes.include = [ @@ -81,7 +106,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { [Sequelize.literal('COUNT(DISTINCT("issues".id))'), 'totalIssues'], [Sequelize.literal('COUNT(DISTINCT("openIssues".id))'), 'totalOpenIssues'] ] - group = ['network.id', "network.name", "tokens.id", "tokens->network_tokens.id"] + group = ['network.id', "network.name", "tokens.id", "tokens->network_tokens.id", "chain.id", "networkToken.id"] } const networks = await models.network.findAll({ @@ -90,7 +115,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { group, order: [[req.query.sortBy || "createdAt", req.query.order || "DESC"]], where: whereCondition, - nest: true, + nest: true }) @@ -102,11 +127,9 @@ async function get(req: NextApiRequest, res: NextApiResponse) { pages: paginatedData.pages, currentPage: +paginatedData.page }); - } -async function SearchNetworks(req: NextApiRequest, - res: NextApiResponse) { +async function SearchNetworks(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "get": await get(req, res); @@ -118,4 +141,7 @@ async function SearchNetworks(req: NextApiRequest, res.end(); } -export default LogAccess(WithCors(SearchNetworks)); \ No newline at end of file + +Logger.changeActionName(`SearchNetworks`); +export default LogAccess(WithCors(SearchNetworks)); + diff --git a/pages/api/search/networks/total.ts b/pages/api/search/networks/total.ts index 8bc547b933..a453100715 100644 --- a/pages/api/search/networks/total.ts +++ b/pages/api/search/networks/total.ts @@ -2,13 +2,14 @@ import {NextApiRequest, NextApiResponse} from "next"; import {Op, WhereOptions} from "sequelize"; import models from "db/models"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; + +import {LogAccess} from "middleware/log-access"; +import WithCors from "middleware/withCors"; async function get(req: NextApiRequest, res: NextApiResponse) { const whereCondition: WhereOptions = {}; - const { creatorAddress, isClosed, isRegistered } = req.query || {}; + const { creatorAddress, name, isClosed, isRegistered } = req.query || {}; if (creatorAddress) whereCondition.creatorAddress = { [Op.iLike]: String(creatorAddress) }; @@ -18,17 +19,18 @@ async function get(req: NextApiRequest, res: NextApiResponse) { if (isRegistered) whereCondition.isRegistered = isRegistered; - + + if (name) + whereCondition.name = name; const networksCount = await models.network.count({ - where: whereCondition + where: whereCondition }); return res.status(200).json(networksCount); } -async function GetAll(req: NextApiRequest, - res: NextApiResponse) { +async function GetAll(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "get": await get(req, res); @@ -40,4 +42,4 @@ async function GetAll(req: NextApiRequest, res.end(); } -export default LogAccess(WithCors(GetAll)); \ No newline at end of file +export default LogAccess(WithCors(GetAll)); diff --git a/pages/api/search/repositories/index.ts b/pages/api/search/repositories/index.ts index 2d2474eb0f..647c974654 100644 --- a/pages/api/search/repositories/index.ts +++ b/pages/api/search/repositories/index.ts @@ -4,13 +4,16 @@ import {Op, WhereOptions} from "sequelize"; import models from "db/models"; import paginate, {calculateTotalPages} from "helpers/paginate"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; +import {resJsonMessage} from "helpers/res-json-message"; + +import {LogAccess} from "middleware/log-access"; +import {WithValidChainId} from "middleware/with-valid-chain-id"; +import WithCors from "middleware/withCors"; async function get(req: NextApiRequest, res: NextApiResponse) { const whereCondition: WhereOptions = {}; - const {owner, name, path, networkName, page} = req.query || {}; + const { owner, name, path, networkName, page, chainId, includeIssues } = req.query || {}; if (path) whereCondition.githubPath = { @@ -20,21 +23,31 @@ async function get(req: NextApiRequest, res: NextApiResponse) { if (name) whereCondition.githubPath = { [Op.iLike]: `%/${name}%` }; if (owner) whereCondition.githubPath = { [Op.iLike]: `%${owner}/%` }; if (networkName) { - const network = await models.network.findOne({ + const networks = await models.network.findAll({ where: { name: { [Op.iLike]: String(networkName).replaceAll(" ", "-") - } + }, + ... chainId ? { chain_id: +chainId } : {} } }); - if (!network) return res.status(404).json("Invalid network"); + if (!networks?.length) return resJsonMessage("Invalid network", res, 404); - whereCondition.network_id = network.id; + whereCondition.network_id = { + [Op.in]: networks.map(({ id }) => id) + }; } const repositories = - await models.repositories.findAndCountAll(paginate({ where: whereCondition, nest: true }, req.query, [])); + await models.repositories.findAndCountAll(paginate({ + where: whereCondition, + nest: true, + include: [ + { association: "network" }, + ... includeIssues ? [{ association: "issues" }] : [] + ] + }, req.query, [])); return res.status(200).json({ ...repositories, @@ -43,8 +56,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { }); } -async function SearchRepositories(req: NextApiRequest, - res: NextApiResponse) { +async function SearchRepositories(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "get": await get(req, res); @@ -57,4 +69,4 @@ async function SearchRepositories(req: NextApiRequest, res.end(); } -export default LogAccess(WithCors(SearchRepositories)); \ No newline at end of file +export default LogAccess(WithCors(WithValidChainId(SearchRepositories))); diff --git a/pages/api/search/tokens/index.ts b/pages/api/search/tokens/index.ts index 4a452cd2e9..49a0e4f109 100644 --- a/pages/api/search/tokens/index.ts +++ b/pages/api/search/tokens/index.ts @@ -1,28 +1,59 @@ -import {withCors} from "middleware"; import {NextApiRequest, NextApiResponse} from "next"; -import { Sequelize } from "sequelize"; +import { Op, Sequelize, WhereOptions } from "sequelize"; import Database from "db/models"; +import {withCors} from "middleware"; + +import { error as logError } from 'services/logging'; + +const colToLower = (colName: string) => Sequelize.fn("LOWER", Sequelize.col(colName)); + async function get(req: NextApiRequest, res: NextApiResponse) { - const {networkName} = req.query + const { networkName, chainId } = req.query; try { - const network = await Database.network.findOne({ - where:{ - name: Sequelize.where(Sequelize.fn("LOWER", Sequelize.col("name")), "=", (networkName as string).toLowerCase()) - }, - include:[{ association: "tokens" }] - }); - - return res.status(200).json(network.tokens); + const whereCondition: WhereOptions = {}; + + let queryParams = {}; + + if (chainId) + whereCondition.chain_id = +chainId; + + if (networkName) { + whereCondition.isAllowed = true; + + queryParams = { + where: { + [Op.or]: [{ isTransactional: true }, { isReward: true }] + }, + include: [ + { + association: "networks", + attributes: [], + required: true, + where: { + name: Sequelize.where(colToLower("networks.name"), "=", (networkName as string).toLowerCase()) + } + } + ] + }; + } + + + const tokens = await Database.tokens.findAll({ + where: whereCondition, + ...queryParams + }); + + return res.status(200).json(tokens); } catch (error) { - console.log(error) - return res.status(500) + logError(`Failed to get tokens`, {error: error?.toString()}); + return res.status(500); } } -async function tokensEndPoint(req: NextApiRequest, res: NextApiResponse) { +async function handler(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "get": await get(req, res); @@ -35,4 +66,4 @@ async function tokensEndPoint(req: NextApiRequest, res: NextApiResponse) { res.end(); } -export default withCors(tokensEndPoint); +export default withCors(handler); diff --git a/pages/api/search/users/[...action].ts b/pages/api/search/users/[...action].ts index 3735160845..b1f1b77467 100644 --- a/pages/api/search/users/[...action].ts +++ b/pages/api/search/users/[...action].ts @@ -1,5 +1,3 @@ -import { LogAccess } from "middleware/log-access"; -import WithCors from "middleware/withCors"; import {NextApiRequest, NextApiResponse} from "next"; import {Op} from "sequelize"; @@ -7,19 +5,26 @@ import models from "db/models"; import paginate from "helpers/paginate"; -import {error as LogError} from "services/logging"; +import { LogAccess } from "middleware/log-access"; +import WithCors from "middleware/withCors"; +import {error as LogError} from "services/logging"; async function post(req: NextApiRequest, res: NextApiResponse) { try { const { action: [action] } = req.query; - + const whereCondition = { - all: {}, + all: { + [Op.or]: [ + { address: (req?.body[0]?.toLowerCase()) }, + { githubLogin: req?.body[1] } + ] + }, login: { githubLogin: { [Op.in]: req.body || [] } }, - address: { address: { [Op.in]: (req.body || []).map((s) => s.toLowerCase()) } } + address: { address: { [Op.in]: (req.body || []).map((s) => s?.toLowerCase()) } } }; const queryOptions = { @@ -29,9 +34,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) { }, where: whereCondition[action] }; - + const users = await models.user.findAll(paginate(queryOptions, req.body)); - + return res.status(200).json(users); } catch (error) { LogError("Failed to search users", { req, error }); @@ -39,8 +44,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { } } -async function SearchUsers(req: NextApiRequest, - res: NextApiResponse) { +async function SearchUsers(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "post": await post(req, res); @@ -52,4 +56,4 @@ async function SearchUsers(req: NextApiRequest, res.end(); } -export default LogAccess(WithCors(SearchUsers)); \ No newline at end of file +export default LogAccess(WithCors(SearchUsers)); diff --git a/pages/api/search/users/total.ts b/pages/api/search/users/total.ts index 5e592c3d52..7e3362b384 100644 --- a/pages/api/search/users/total.ts +++ b/pages/api/search/users/total.ts @@ -1,8 +1,9 @@ import {NextApiRequest, NextApiResponse} from "next"; import models from "db/models"; -import {LogAccess} from "../../../../middleware/log-access"; -import WithCors from "../../../../middleware/withCors"; + +import {LogAccess} from "middleware/log-access"; +import WithCors from "middleware/withCors"; async function getTotal(req: NextApiRequest, res: NextApiResponse) { const userCount = await models.user.count(); @@ -10,8 +11,7 @@ async function getTotal(req: NextApiRequest, res: NextApiResponse) { return res.status(200).json(userCount); } -async function getAll(req: NextApiRequest, - res: NextApiResponse) { +async function getAll(req: NextApiRequest, res: NextApiResponse) { switch (req.method.toLowerCase()) { case "get": await getTotal(req, res); @@ -24,4 +24,4 @@ async function getAll(req: NextApiRequest, res.end(); } -export default LogAccess(WithCors(getAll)); \ No newline at end of file +export default LogAccess(WithCors(getAll)); diff --git a/pages/api/seo/[...ids].ts b/pages/api/seo/[...ids].ts index 57f3044909..4804e7108e 100644 --- a/pages/api/seo/[...ids].ts +++ b/pages/api/seo/[...ids].ts @@ -5,7 +5,8 @@ import {Op} from "sequelize"; import models from "db/models"; import {Settings} from "helpers/settings"; -import {LogAccess} from "../../../middleware/log-access"; + +import {LogAccess} from "middleware/log-access"; async function get(req: NextApiRequest, res: NextApiResponse) { const { diff --git a/pages/api/settings/index.ts b/pages/api/settings/index.ts index 669f2f7634..add159aefe 100644 --- a/pages/api/settings/index.ts +++ b/pages/api/settings/index.ts @@ -3,8 +3,10 @@ import {NextApiRequest, NextApiResponse} from "next"; import models from "db/models"; import {Settings} from "helpers/settings"; -import {Logger} from "../../../services/logging"; -import {LogAccess} from "../../../middleware/log-access"; + +import {LogAccess} from "middleware/log-access"; + +import {Logger} from "services/logging"; async function get(_req: NextApiRequest, res: NextApiResponse) { const settings = await models.settings.findAll({ diff --git a/pages/api/setup/registry.ts b/pages/api/setup/registry.ts index 7ba9d7e463..46169f2939 100644 --- a/pages/api/setup/registry.ts +++ b/pages/api/setup/registry.ts @@ -2,44 +2,37 @@ import {NextApiRequest, NextApiResponse} from "next"; import Database from "db/models"; -import {Settings} from "helpers/settings"; +import {chainFromHeader} from "helpers/chain-from-header"; +import {CHAIN_NOT_CONFIGURED, NOT_AN_ADMIN} from "helpers/constants"; +import {isAdmin} from "helpers/is-admin"; +import {resJsonMessage} from "helpers/res-json-message"; -import DAO from "services/dao-service"; -import {error as LogError, log as Log, Logger} from 'services/logging'; - -import {SettingsType} from "types/settings"; +import {LogAccess} from "middleware/log-access"; -import {LogAccess} from "../../../middleware/log-access"; +import DAO from "services/dao-service"; +import {error as LogError, Logger} from 'services/logging'; async function post(req: NextApiRequest, res: NextApiResponse) { - const { - wallet, - registryAddress - } = req.body; + const {wallet, registryAddress} = req.body; - try { - if (!wallet || !registryAddress) - return res.status(400).json("Missing parameters"); + if (!isAdmin(req)) + return resJsonMessage(NOT_AN_ADMIN, res, 400); - const settings = await Database.settings.findAll({ - where: { visibility: "public" }, - raw: true, - }); + try { - const publicSettings = (new Settings(settings)).raw() as SettingsType; + const chain = await chainFromHeader(req); + const web3Host = chain?.chainRpc; - if (publicSettings?.contracts?.networkRegistry) - return res.status(400).json("Environment already configured"); - if (!publicSettings?.urls?.web3Provider) return res.status(500).json("Missing web3 provider url"); - if (!publicSettings?.defaultNetworkConfig?.adminWallet) return res.status(500).json("Missing admin wallet"); + const messages = [ + [wallet, 'Missing wallet address'], + [registryAddress, 'Missing registry address'] + ].filter(([v,]) => v).map(([,message]) => message); - const { adminWallet } = publicSettings.defaultNetworkConfig; - const { web3Provider: web3Host } = publicSettings.urls; + if (messages.length) + return resJsonMessage(messages, res, 400); - if (wallet.toLowerCase() !== adminWallet?.toLowerCase()) { - Log("Unauthorized request", { req }); - return res.status(401).json("User must be admin"); - } + if (!web3Host) + return resJsonMessage(CHAIN_NOT_CONFIGURED, res, 400); const dao = new DAO({ skipWindowAssignment: true, @@ -47,17 +40,16 @@ async function post(req: NextApiRequest, res: NextApiResponse) { registryAddress }); - if (!await dao.start()) return res.status(500).json("Failed to connect with chain"); + if (!await dao.start()) + return resJsonMessage("Failed to connect with chain", res, 400); const registry = await dao.loadRegistry(true); if (!registry) - return res.status(400).json("Invalid Registry address"); - - const registryGovernor = await registry.governed._governor(); + return resJsonMessage("Invalid Registry address", res, 400); - if (registryGovernor !== wallet) - return res.status(401).json("User must be registry governor"); + if ((await registry.governed._governor()) !== wallet) + return resJsonMessage("User must be registry governor", res, 400); await Database.settings.create({ key: "networkRegistry", @@ -68,10 +60,10 @@ async function post(req: NextApiRequest, res: NextApiResponse) { }); - return res.status(200).json("Registry saved"); + return resJsonMessage("Registry saved", res); } catch(error) { LogError("Failed to save network registry", { error, req }); - return res.status(500).json(error); + return resJsonMessage(error?.message || error?.toString(), res, 500); } } @@ -89,4 +81,4 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { Logger.changeActionName(`Setup`); -export default LogAccess(handler) \ No newline at end of file +export default LogAccess(handler); \ No newline at end of file diff --git a/pages/api/tokens/index.ts b/pages/api/tokens/index.ts deleted file mode 100644 index 7039057a6c..0000000000 --- a/pages/api/tokens/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {NextApiRequest, NextApiResponse} from "next"; -import {Op, Sequelize} from "sequelize"; - -import Database from "db/models"; - -import {error as logError, Logger} from 'services/logging'; -import {LogAccess} from "../../../middleware/log-access"; -import WithCors from "../../../middleware/withCors"; - -const colToLower = (colName: string) => Sequelize.fn("LOWER", Sequelize.col(colName)); - -async function get(req: NextApiRequest, res: NextApiResponse) { - const { networkName } = req.query; - - try { - let queryParams = {}; - - if (networkName) - queryParams = { - where: { - [Op.or]: [{ isTransactional: true }, { isReward: true }] - }, - include: [ - { - association: "networks", - attributes: [], - required: !!networkName, - where: { - name: Sequelize.where(colToLower("networks.name"), "=", (networkName as string).toLowerCase()) - } - } - ] - }; - - const tokens = await Database.tokens.findAll({ - ...queryParams - }); - - return res.status(200).json(tokens); - } catch (error) { - logError(`Failed to get tokens`, {error: error?.toString()}); - return res.status(500); - } -} - -async function tokensEndPoint(req: NextApiRequest, res: NextApiResponse) { - switch (req.method.toLowerCase()) { - case "get": - await get(req, res); - break; - - default: - res.status(405).json("Method not allowed"); - } - - res.end(); -} - -Logger.changeActionName(`Tokens`); -export default LogAccess(WithCors(tokensEndPoint)); diff --git a/pages/api/user/connect/index.ts b/pages/api/user/connect/index.ts index a7b919c893..4b5ba77887 100644 --- a/pages/api/user/connect/index.ts +++ b/pages/api/user/connect/index.ts @@ -4,8 +4,9 @@ import {Op} from "sequelize"; import models from "db/models"; +import {LogAccess} from "middleware/log-access"; + import {error as LogError} from "services/logging"; -import {LogAccess} from "../../../../middleware/log-access"; enum Actions { REGISTER = "register", diff --git a/pages/api/user/reset/index.ts b/pages/api/user/reset/index.ts index 353ebaf26b..f18f7fe63f 100644 --- a/pages/api/user/reset/index.ts +++ b/pages/api/user/reset/index.ts @@ -4,10 +4,11 @@ import {Op, Sequelize} from "sequelize"; import models from "db/models"; -import {error as LogError} from "services/logging"; +import {UNAUTHORIZED} from "helpers/error-messages"; + +import {LogAccess} from "middleware/log-access"; -import {UNAUTHORIZED} from "../../../../helpers/error-messages"; -import {LogAccess} from "../../../../middleware/log-access"; +import {error as LogError} from "services/logging"; async function post(req: NextApiRequest, res: NextApiResponse) { const {address, githubLogin} = req.body; diff --git a/pages/explore.tsx b/pages/explore.tsx index ca17237167..58e15e15ce 100644 --- a/pages/explore.tsx +++ b/pages/explore.tsx @@ -12,16 +12,17 @@ import PageHero, { InfosHero } from "components/page-hero"; import { BountyEffectsProvider } from "contexts/bounty-effects"; import useApi from "x-hooks/use-api"; - +import { useNetwork } from "x-hooks/use-network"; export default function ExplorePage() { const { t } = useTranslation(["common", "custom-network", "bounty"]); + const [numberOfNetworks, setNumberOfNetworks] = useState(0); const [numberOfBounties, setNumberOfBounties] = useState(0); + const { networkName } = useNetwork(); const { getTotalNetworks, getTotalBounties } = useApi(); - const [infos, setInfos] = useState([ { value: 0, @@ -33,10 +34,20 @@ export default function ExplorePage() { } ]); + const heroTitle = networkName ? + `${networkName.replace(/^\w/, c => c.toUpperCase())} Bounty Hall` : t("bounty:title-bounties"); + const heroSubTitle = networkName ? + `A collection of the most recent bounties of ${networkName} networks` : t("bounty:sub-title-bounties"); + useEffect(() => { - getTotalNetworks().then(setNumberOfNetworks) - getTotalBounties().then(setNumberOfBounties) - },[]) + getTotalNetworks(networkName) + .then(setNumberOfNetworks) + .catch(error => console.debug("Failed to getTotalNetworks", error)); + + getTotalBounties(networkName) + .then(setNumberOfBounties) + .catch(error => console.debug("Failed to getTotalBounties", error)); + },[networkName]) useEffect(() => { setInfos([ @@ -54,8 +65,8 @@ export default function ExplorePage() { return ( @@ -70,10 +81,10 @@ export const getServerSideProps: GetServerSideProps = async ({ locale }) => { props: { ...(await serverSideTranslations(locale, [ "common", + "custom-network", "bounty", "connect-wallet-button", - "custom-network", - "leaderboard", + "leaderboard" ])), }, }; diff --git a/pages/index.tsx b/pages/index.tsx index 00c02ad4ff..f1806291f1 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,11 +1,32 @@ -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import { GetServerSideProps } from "next/types"; +import {useEffect} from "react"; -import ExplorePage from "./explore"; +import {serverSideTranslations} from "next-i18next/serverSideTranslations"; +import getConfig from "next/config"; +import {useRouter} from "next/router"; +import {GetServerSideProps} from "next/types"; + +import {useAppState} from "contexts/app-state"; + +import ExplorePage from "pages/explore"; + +const { publicRuntimeConfig } = getConfig(); export default function Index() { + const { replace } = useRouter(); + + const { state } = useAppState(); + + useEffect(() => { + const isAdmin = state.currentUser?.walletAddress?.toLowerCase() === publicRuntimeConfig.adminWallet.toLowerCase(); + const hasSupportedChains = !!state?.supportedChains?.length; + + if (isAdmin && !hasSupportedChains) + replace("/setup"); + + }, [state?.supportedChains, state.currentUser?.walletAddress]); + return( - + ); } diff --git a/pages/networks.tsx b/pages/networks.tsx index deccf36a63..4ab2bb9976 100644 --- a/pages/networks.tsx +++ b/pages/networks.tsx @@ -8,12 +8,9 @@ import NetworksList from "components/networks-list"; import NotListedTokens from "components/not-listed-tokens"; import PageHero, {InfosHero} from "components/page-hero"; -import {useAppState} from "../contexts/app-state"; - interface price_used { [name: string]: number; } - export interface ConvertedTokens { [symbol: string]: price_used; } @@ -42,8 +39,6 @@ export const NetworksPageContext = createContext({ export default function NetworksPage() { const { t } = useTranslation(["common", "custom-network"]); - const {state} = useAppState(); - const [totalConverted, setTotalConverted] = useState(""); const [numberOfNetworks, setNumberOfNetworks] = useState(0); const [numberOfBounties, setNumberOfBounties] = useState(0); @@ -66,10 +61,6 @@ export default function NetworksPage() { } ]); - useEffect(() => { - if (state.Service?.active) state.Service?.active.loadRegistry(); - }, [state.Service?.active]); - useEffect(() => { setInfos([ { diff --git a/pages/setup.tsx b/pages/setup.tsx index a1bbc89f35..88c9b8d327 100644 --- a/pages/setup.tsx +++ b/pages/setup.tsx @@ -1,20 +1,23 @@ -import { useEffect, useState } from "react"; -import { Container, Row } from "react-bootstrap"; +import {useEffect, useState} from "react"; +import {Container, Row} from "react-bootstrap"; -import { GetServerSideProps } from "next"; -import { useTranslation } from "next-i18next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; +import {GetServerSideProps} from "next"; +import {useTranslation} from "next-i18next"; +import {serverSideTranslations} from "next-i18next/serverSideTranslations"; import getConfig from "next/config"; -import { useRouter } from "next/router"; +import {useRouter} from "next/router"; import ConnectWalletButton from "components/connect-wallet-button"; -import { NetworkSetup } from "components/setup/network-setup"; -import { RegistrySetup } from "components/setup/registry-setup"; +import {CallToAction} from "components/setup/call-to-action"; +import ChainsSetup from "components/setup/chains-setup"; +import ConnectGithubSetup from "components/setup/connect-github-setup"; +import {NetworkSetup} from "components/setup/network-setup"; +import {RegistrySetup} from "components/setup/registry-setup"; import TabbedNavigation from "components/tabbed-navigation"; -import { useAppState } from "contexts/app-state"; +import {useAppState} from "contexts/app-state"; -import { Network } from "interfaces/network"; +import {Network} from "interfaces/network"; import useApi from "x-hooks/use-api"; @@ -22,25 +25,23 @@ const { publicRuntimeConfig: { adminWallet } } = getConfig(); export default function SetupPage(){ const { replace } = useRouter(); - const { t } = useTranslation("setup") + const { t } = useTranslation(["setup", "common"]) - const [activeTab, setActiveTab] = useState("registry"); + const [activeTab, setActiveTab] = useState("githubConnection"); const [defaultNetwork, setDefaultNetwork] = useState(); const { searchNetworks } = useApi(); - const { state: { currentUser, Settings } } = useAppState(); + const { state: { currentUser, supportedChains, connectedChain } } = useAppState(); const isConnected = !!currentUser?.walletAddress; const isAdmin = adminWallet?.toLowerCase() === currentUser?.walletAddress?.toLowerCase(); - const networkRegistryAddress = Settings?.contracts?.networkRegistry; - useEffect(() => { if (isConnected && adminWallet && !isAdmin) replace("/networks"); }, [adminWallet, currentUser?.walletAddress]); - useEffect(() => { + function searchForNetwork() { if (!isConnected || !isAdmin) return; searchNetworks({ @@ -50,32 +51,82 @@ export default function SetupPage(){ if (count > 0) setDefaultNetwork(rows[0]); }); - }, [isConnected, isAdmin, currentUser?.walletAddress]); + } + + useEffect(searchForNetwork, [isConnected, isAdmin, currentUser?.walletAddress]); - if (!isConnected) + if (!currentUser?.walletAddress) return ; const tabs = [ + { + eventKey: 'githubConnection', + title: t('common:misc.github'), + component: <> + }, + { + eventKey: 'supportedChains', + title: t('setup:chains.title'), + component: ( + !currentUser?.login + ? setActiveTab('githubConnection')} + /> + : + ) + }, { eventKey: "registry", - title: t("registry.title"), + title: t("setup:registry.title"), component: ( - + !supportedChains?.length + ? setActiveTab('supportedChains')} + /> + : ) }, { eventKey: "network", - title: t("network.title"), + title: t("setup:network.title"), component: ( - + !supportedChains?.length + ? setActiveTab('supportedChains')} + /> : + !connectedChain?.registry + ? setActiveTab('registry')} + /> + : ) - }, + } ]; return( @@ -88,12 +139,14 @@ export default function SetupPage(){ } + ); } @@ -107,7 +160,8 @@ export const getServerSideProps: GetServerSideProps = async ({ locale }) => { "custom-network", "connect-wallet-button", "change-token-modal", - "setup" + "setup", + "profile" ])), }, }; diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 9dd8d3ef88..b2c9a3bd9d 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -62,7 +62,7 @@ "leaderboard": "Leaderboard", "Oracle": "bv{{token}}", "new-bounty": "New bounty", - "explorer": "Explore", + "explore": "Explore", "get-started": "Get Started", "connect": "Connect Wallet", "new-network": "New Network", @@ -127,6 +127,7 @@ "reply-on-github": "Reply on Github", "review": "Review", "view-proposal": "View Proposal", + "view-pull-request": "View Pull Request", "make-a-review": "Make a Review", "success": "Success", "view-on-github": "View on Github", @@ -188,7 +189,8 @@ "clear": "Clear", "wallet": "Wallet", "unkown": "Unknown", - "latest": "Latest" + "latest": "Latest", + "unsupported": "Unsupported" }, "modals": { "help-modal": { @@ -212,7 +214,7 @@ }, "invalid-account-wallet": { "title": "Github Account and Wallet don't match", - "description": "Please change to the previously connected wallet and disconnect your github account from your profile page if you wish to change the connection" + "description": "Please change to the previously connected wallet and disconnect your github account from your profile page if you wish to change the connection or connect with github already registered and disconnect from this wallet" }, "not-mergeable": { "title": "Merging bounty", @@ -260,7 +262,8 @@ "message": "It seems that you don't have a MetaMask account selected. If using MetaMask, please make sure that your wallet is unlocked and that you have at least one account in your accounts list." }, "wrong-network": { - "please-connect": "please, connect to the", + "please-connect": "Please, connect to a supported network", + "connect-to-network-chain": "To interact with this network, please connect to the chain below", "network": "network", "on-your-wallet": "on your metamask wallet", "change-network": "Change Network", @@ -291,7 +294,7 @@ "already-has-network": { "title": "Network already exists", "button-label": "My network", - "content": "Connected wallet already has a network." + "content": "Connected wallet already has a network on this chain." }, "settings-not-loaded": { "title": "Failed to load settings", @@ -308,6 +311,7 @@ "required": "Field is required" }, "select-placeholder": "Select...", + "select-placeholder-chain": "Select a supported chain", "create-pull-request": { "title": { "label": "Title", diff --git a/public/locales/en/connect-wallet-button.json b/public/locales/en/connect-wallet-button.json index 3d12fed7f2..42dddf301a 100644 --- a/public/locales/en/connect-wallet-button.json +++ b/public/locales/en/connect-wallet-button.json @@ -1,6 +1,4 @@ { "title": "Connect your MetaMask Wallet", - "to-access-this-page": "to access this page please, connect to the ", - "network": "network", - "on-your-wallet": "on your metamask wallet" + "this-page-needs-access-to-your-wallet-address": "This page needs access to your wallet address" } \ No newline at end of file diff --git a/public/locales/en/custom-network.json b/public/locales/en/custom-network.json index 66be8ed2e4..fc8a9afff1 100644 --- a/public/locales/en/custom-network.json +++ b/public/locales/en/custom-network.json @@ -141,8 +141,8 @@ "user-permission-not-admin_other": "Your permission level on these repositories does not allow giving access.", "no-merge-commit-permission_one": "Repository {{repos}} don't support \"merge commits\" merge type. Please, allow that in the repository settings.", "no-merge-commit-permission_other": "Repositories {{repos}} doesn't support \"merge commits\" merge type. Please, allow that in the repositories settings.", - "without-bot-collab_one": "Repository {{repos}} don't have {{bot}} bot as collaborator. Please, remove the repository from the network and re-add it.", - "without-bot-collab_other": "Repositories {{repos}} doesn't have {{bot}} bot as collaborator. Please, remove the repositories from the network and re-add them." + "without-bot-collab_one": "Repository {{repos}} don't have {{bot}} bot as collaborator. Please, remove the repository from the network and re-add it.", + "without-bot-collab_other": "Repositories {{repos}} doesn't have {{bot}} bot as collaborator. Please, remove the repositories from the network and re-add them." }, "token-configuration": { "title": "Governance Token", @@ -275,7 +275,8 @@ "council-amount": "The amount of {{token}} locked needed to be a curator. Min. {{min}} and Max. {{max}}.", "updating-values": "Error updating network values", "updated-parameters": "Failed to update {{failed}} params", - "network-not-registered": "This network has yet to be registered to be usable, please do so." + "network-not-registered": "This network has yet to be registered to be usable, please do so.", + "failed-to-update-network-id": "Failed to update network chain id" }, "tabs": { "logo-and-colours": "Logo & Colours", diff --git a/public/locales/en/funding.json b/public/locales/en/funding.json index 89bb502bc9..bb9b1a6dee 100644 --- a/public/locales/en/funding.json +++ b/public/locales/en/funding.json @@ -1,6 +1,7 @@ { "title": "Funding", "reward": "Reward", + "rewards": "Rewards", "current-funding": "Current Funding", "total-amount": "Total Amount", "funding-rewards": "Funding Rewards", diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json index 793cfd8041..3a0186deeb 100644 --- a/public/locales/en/profile.json +++ b/public/locales/en/profile.json @@ -11,7 +11,7 @@ "period": "Period" }, "actions": { - "remove-github-account": "Remove Github Account" + "remove-github-account": "Disconnect Github Account" }, "tips": { "total-balance": "Total tokens in your wallet", diff --git a/public/locales/en/pull-request.json b/public/locales/en/pull-request.json index 5574743064..f58e6c48af 100644 --- a/public/locales/en/pull-request.json +++ b/public/locales/en/pull-request.json @@ -38,7 +38,8 @@ "title": "Cancel Pull Request", "success": "Pull Request cancelled", "error": "Failed to cancel pull request" - } + }, + "go-to-pull-request": "Go to Pull Request" }, "status": { "ready-to-merge": "Ready to merge", diff --git a/public/locales/en/setup.json b/public/locales/en/setup.json index 476ec704ad..dbe779b874 100644 --- a/public/locales/en/setup.json +++ b/public/locales/en/setup.json @@ -79,7 +79,8 @@ "actions": { "deploy-registry": "Deploy Network Registry", "deploy-erc20": "Deploy New ERC20 Token", - "deploy-bounty-token": "Deploy New Bounty Token" + "deploy-bounty-token": "Deploy New Bounty Token", + "save-registry": "Save Registry" }, "success": { "deploy": { @@ -106,5 +107,8 @@ "errors": { "network-already-saved": "Default Network already saved, please visit {{network}} settings page." } + }, + "chains": { + "title": "Chains" } } \ No newline at end of file diff --git a/scripts/settings/save-from-env.js b/scripts/settings/save-from-env.js index 181989bd8e..694aa555c5 100644 --- a/scripts/settings/save-from-env.js +++ b/scripts/settings/save-from-env.js @@ -23,7 +23,7 @@ const publicSettings = [ //PublicSettingItem("networkRegistry", process.env.NEXT_PUBLIC_NETWORK_REGISTRY_ADDRESS, "string", "contracts"), PublicSettingItem("botUser", process.env.NEXT_PUBLIC_GH_USER, "string", "github"), PublicSettingItem("token", process.env.NEXT_PUBLIC_NATIVE_TOKEN_NAME, "string", "requiredChain"), - PublicSettingItem("id", process.env.NEXT_PUBLIC_NEEDS_CHAIN_ID, "string", "requiredChain"), + PublicSettingItem("id", process.env.NEXT_PUBLIC_NEEDS_CHAIN_ID || "", "string", "requiredChain"), PublicSettingItem("name", process.env.NEXT_PUBLIC_NEEDS_CHAIN_NAME, "string", "requiredChain"), PublicSettingItem("api", process.env.NEXT_PUBLIC_CURRENCY_API || '', "string", "currency"), PublicSettingItem("defaultFiat", process.env.NEXT_PUBLIC_CURRENCY_MAIN || '', "string", "currency"), diff --git a/services/dao-service.ts b/services/dao-service.ts index 60311edebc..005cf1cf62 100644 --- a/services/dao-service.ts +++ b/services/dao-service.ts @@ -207,7 +207,7 @@ export default class DAO { try { await this.web3Connection.connect(); - await this.loadNetwork(this.network?.contractAddress); + //await this.loadNetwork(this.network?.contractAddress); return true; } catch (error) { @@ -484,7 +484,7 @@ export default class DAO { async isNetworkGovernor(address: string): Promise { const governor = await this.network.governed._governor(); - return governor === address; + return governor.toLowerCase() === address.toLowerCase(); } async isNetworkAbleToBeClosed(): Promise { @@ -673,7 +673,7 @@ export default class DAO { } async deployNetworkV2(networkToken: string): Promise { - const registryAddress: string = this._registryAddress + const registryAddress: string = this._registryAddress; const newNetwork = new Network_v2(this.web3Connection); await newNetwork.loadAbi(); @@ -722,7 +722,7 @@ export default class DAO { const time = await this.getTimeChain(); const redeemTime = await this.network.draftTime(); - return (new Date(time) < new Date(creationDateIssue + redeemTime)) + return (new Date(time) < new Date(creationDateIssue + redeemTime)); } catch (e) { console.error(`Failed to calculate isDraft bounty`, e); return null; diff --git a/styles/styles.scss b/styles/styles.scss index c0cbb4948e..b2be7a1c8e 100644 --- a/styles/styles.scss +++ b/styles/styles.scss @@ -415,7 +415,6 @@ li { margin: 0px; padding: 0px; list-style: none; - margin-left: $m * 2; li { margin-left: $m * 2; } @@ -1754,6 +1753,17 @@ input[type="color"] { border-color: var(--bs-purple); } +.kyc-modal-body{ + min-height: 630px !important; +} + +.select-network-dropdown { + max-width: 168px; + text-transform: uppercase; + font-size: 12px; + font-weight: 500px; +} + .kyc-modal-body{ min-height: 630px !important; } \ No newline at end of file diff --git a/types/dappkit.d.ts b/types/dappkit.d.ts index ce9d02f6f4..a732b293a3 100644 --- a/types/dappkit.d.ts +++ b/types/dappkit.d.ts @@ -17,7 +17,7 @@ export type RegistryParameters = "networkCreationFeePercentage" | "MAX_LOCK_PERCENTAGE_FEE" | "DIVISOR" ; -export type Entities = "bounty" | "proposal" | "pull-request" | "registry" | "oracles" | "bountyToken"; +export type Entities = "bounty" | "proposal" | "pull-request" | "registry" | "oracles" | "network"; export type Events = "created" | "canceled" | @@ -30,5 +30,7 @@ export type Events = "created" | "changed" | "transfer" | "registered"| - 'moved-to-open'| - 'update-draft-time'; \ No newline at end of file + "moved-to-open" | + "update-draft-time" | + "parameters" | + "withdraw"; \ No newline at end of file diff --git a/x-hooks/use-analytic-events.ts b/x-hooks/use-analytic-events.ts index 3ec2dd4380..24853dd3be 100644 --- a/x-hooks/use-analytic-events.ts +++ b/x-hooks/use-analytic-events.ts @@ -1,10 +1,11 @@ import getConfig from "next/config"; import {event} from "nextjs-google-analytics"; -import {useAppState} from "../contexts/app-state"; -import {analyticEvents} from "../helpers/analytic-events"; -import {Analytic, EventName} from "../interfaces/analytics"; +import {useAppState} from "contexts/app-state"; +import {analyticEvents} from "helpers/analytic-events"; + +import {Analytic, EventName} from "interfaces/analytics"; export default function useAnalyticEvents() { @@ -16,7 +17,7 @@ export default function useAnalyticEvents() { * @param eventName name of the event to be sent (must exist in analyticsEvents const) * @param details details to be sent in that event */ - function pushAnalytic(eventName: EventName, details: {[options: string]: string} = {}) { + function pushAnalytic(eventName: EventName, details: {[options: string]: string | boolean} = {}) { function getCallback({type}: Analytic) { // console.debug(`Trying to push ${eventName} with type ${type}`, details) @@ -26,7 +27,6 @@ export default function useAnalyticEvents() { const rejectMissingParams = (params: string | string[]) => reject(`Missing Params ${JSON.stringify(params)}`) - switch (type) { case "ga4": if (!publicRuntimeConfig.gaMeasureID) diff --git a/x-hooks/use-api.tsx b/x-hooks/use-api.tsx index c228317c9a..b50c9397a2 100644 --- a/x-hooks/use-api.tsx +++ b/x-hooks/use-api.tsx @@ -1,19 +1,23 @@ +import axios from "axios"; import BigNumber from "bignumber.js"; +import {isZeroAddress} from "ethereumjs-util"; import {head} from "lodash"; import {useAppState} from "contexts/app-state"; -import { - PastEventsParams, - CreatePrePullRequestParams, - SearchNetworkParams, - User, - CancelPrePullRequestParams, - StartWorkingParams, - PatchUserParams, - MergeClosedIssueParams, +import { issueParser } from "helpers/issue"; + +import { + CancelPrePullRequestParams, + CreatePrePullRequestParams, CreateReviewParams, + MergeClosedIssueParams, SearchActiveNetworkParams, + PatchUserParams, + User, + PastEventsParams, + StartWorkingParams, + SearchNetworkParams, updateIssueParams } from "interfaces/api"; import {Curator, SearchCuratorParams} from "interfaces/curators"; @@ -26,11 +30,15 @@ import {Proposal} from "interfaces/proposal"; import {ReposList} from "interfaces/repos-list"; import {Token} from "interfaces/token"; -import {api, eventsApi} from "services/api"; +import {api} from "services/api"; import { WinStorage } from "services/win-storage"; import {Entities, Events} from "types/dappkit"; +import {updateSupportedChains} from "../contexts/reducers/change-supported-chains"; +import {toastError, toastSuccess} from "../contexts/reducers/change-toaster"; +import {SupportedChainData} from "../interfaces/supported-chain-data"; + interface NewIssueParams { title: string; description: string; @@ -54,6 +62,9 @@ interface GetNetworkProps { name?: string; creator?: string; isDefault?: boolean; + address?: string; + byChainId?: boolean; + chainName?: string; } type FileUploadReturn = { @@ -65,7 +76,7 @@ type FileUploadReturn = { const repoList: ReposList = []; export default function useApi() { - const {state} = useAppState() + const {state, dispatch} = useAppState(); const DEFAULT_NETWORK_NAME = state?.Service?.network?.active?.name api.interceptors.request.use(config => { @@ -105,6 +116,7 @@ export default function useApi() { tokenAddress = "", networkName = "", allNetworks = undefined, + chainId = "" }) { const params = new URLSearchParams({ address, @@ -120,24 +132,21 @@ export default function useApi() { pullRequesterAddress, proposer, tokenAddress, + chainId, networkName: networkName.replaceAll(" ", "-"), ... (allNetworks !== undefined && { allNetworks: allNetworks.toString() } || {}), }).toString(); + return api .get<{ - rows: IssueBigNumberData[]; + rows: IssueData[]; count: number; pages: number; currentPage: number; - }>(`/search/issues/?${params}`) + }>(`/search/issues?${params}`) .then(({ data }) => ({ ...data, - rows: data.rows.map(row => ({ - ...row, - amount: BigNumber(row.amount), - fundingAmount: BigNumber(row.fundingAmount), - fundedAmount: BigNumber(row.fundedAmount) - })) + rows: data.rows.map(issueParser) })) .catch(() => ({ rows: [], count: 0, pages: 0, currentPage: 1 })); } @@ -159,14 +168,8 @@ export default function useApi() { networkName: networkName.replaceAll(" ", "-") }).toString(); return api - .get(`/search/issues/recent/?${params}`) - .then(({ data }): IssueBigNumberData[] => - (data.map(bounty => ({ - ...bounty, - amount: BigNumber(bounty.amount), - fundingAmount: BigNumber(bounty.fundingAmount), - fundedAmount: BigNumber(bounty.fundedAmount) - })))) + .get(`/search/issues/recent/?${params}`) + .then(({ data }): IssueBigNumberData[] => (data.map(issueParser))) .catch((): IssueBigNumberData[] => ([])); } @@ -176,25 +179,33 @@ export default function useApi() { owner = "", name = "", path = "", - networkName = DEFAULT_NETWORK_NAME + networkName = DEFAULT_NETWORK_NAME, + chainId = "", + includeIssues = "" }) { - const params = new URLSearchParams({ + const params = { page, owner, name, path, - networkName - }).toString(); + networkName, + chainId, + includeIssues + }; + return api - .get<{ rows; count: number; pages: number; currentPage: number }>(`/search/repositories?${params}`) + .get<{ rows; count: number; pages: number; currentPage: number }>("/search/repositories", { params }) .then(({ data }) => data) .catch(() => ({ rows: [], count: 0, pages: 0, currentPage: 1 })); } - async function getIssue(repoId: string | number, ghId: string | number, networkName = DEFAULT_NETWORK_NAME) { + async function getIssue(repoId: string | number, + ghId: string | number, + networkName = DEFAULT_NETWORK_NAME, + chainId?: string | number) { return api - .get(`/issue/${repoId}/${ghId}/${networkName}`) - .then(({ data }) => data) + .get(`/issue/${repoId}/${ghId}/${networkName}`, { params: { chainId } }) + .then(({ data }) => issueParser(data)) .catch(() => null); } @@ -297,16 +308,18 @@ export default function useApi() { return api.get("/search/users/total").then(({ data }) => data); } - async function getTotalBounties(state = "", networkName = ""): Promise { + async function getTotalBounties(networkName = "", state = ""): Promise { const search = new URLSearchParams({ state, networkName }).toString(); return api.get(`/search/issues/total?${search}`).then(({ data }) => data); } - async function getTotalNetworks(creatorAddress = "", - isClosed = undefined, - isRegistered = undefined): Promise { - const search = new URLSearchParams({ + async function getTotalNetworks(name = "", + creatorAddress = "", + isClosed = false, + isRegistered = true): Promise { + const search = new URLSearchParams({ creatorAddress, + name, ... (isClosed !== undefined && { isClosed: isClosed.toString() } || {}), ... (isRegistered !== undefined && { isRegistered: isRegistered.toString() } || {}) }).toString(); @@ -354,8 +367,8 @@ export default function useApi() { .catch(() => []); } - async function getReposList(force = false, networkName = DEFAULT_NETWORK_NAME) { - const search = new URLSearchParams({ networkName }).toString(); + async function getReposList(force = false, networkName = DEFAULT_NETWORK_NAME, chainId?: string) { + const search = new URLSearchParams({ networkName, chainId }).toString(); if (!force && repoList.length) return Promise.resolve(repoList as ReposList); @@ -377,8 +390,13 @@ export default function useApi() { event: Events, networkName: string = DEFAULT_NETWORK_NAME, params: PastEventsParams = {}) { - - return eventsApi.get(`/past-events/${entity}/${event}`, { + + if (!state.connectedChain?.events) + return; + + const eventsURL = new URL(`/past-events/${entity}/${event}`, state.connectedChain?.events); + + return axios.get(eventsURL.href, { params: { ...params, networkName } }).then(({ data }) => data?.[networkName]); } @@ -501,6 +519,13 @@ export default function useApi() { .catch(() => ({} as User)); } + async function getUserAll(address: string, login: string): Promise { + return api + .post("/search/users/all/", [address,login]) + .then(({ data }) => data[0]) + .catch(() => ({} as User)); + } + async function isNetworkOwner(creatorAddress, networkAddress) { const params = new URLSearchParams({ creatorAddress, @@ -518,12 +543,15 @@ export default function useApi() { .catch(() => false); } - async function getNetwork({ name, creator, isDefault } : GetNetworkProps) { - const Params = {} as Omit & { isDefault: string }; + async function getNetwork({ name, creator, isDefault, address, byChainId, chainName } : GetNetworkProps) { + const Params = {} as Omit & { isDefault: string; byChainId: string; }; if (name) Params.name = name; if (creator) Params.creator = creator; if (isDefault) Params.isDefault = isDefault.toString(); + if (byChainId) Params.byChainId = byChainId.toString(); + if (address) Params.address = address; + if (chainName) Params.chainName = chainName; const search = new URLSearchParams(Params).toString(); @@ -531,6 +559,7 @@ export default function useApi() { .get(`/network?${search}`) .then((response) => response) .catch((error) => { + console.log(`failed to get`, error) throw error; }); } @@ -546,22 +575,22 @@ export default function useApi() { throw error; }); } - - async function getTokens() { + + async function getTokens(chainId?: string) { return api - .get(`/tokens`) + .get("/search/tokens", { params: {chainId} }) .then(({ data }) => data) .catch((error) => { throw error; }); } - + async function getNetworkTokens({ - networkName = DEFAULT_NETWORK_NAME + networkName = DEFAULT_NETWORK_NAME, + chainId = "" }) { - const params = new URLSearchParams({networkName}).toString(); return api - .get(`/tokens?${params}`) + .get("/search/tokens", { params: {networkName, chainId}}) .then(({ data }) => data) .catch((error) => { throw error; @@ -579,9 +608,11 @@ export default function useApi() { isClosed = undefined, isRegistered = undefined, isDefault = undefined, - isNeedCountsAndTokensLocked = undefined + isNeedCountsAndTokensLocked = undefined, + chainId = "", + chainShortName = "" }: SearchNetworkParams) { - const params = new URLSearchParams({ + const params = { page, name, creatorAddress, @@ -589,14 +620,15 @@ export default function useApi() { sortBy, order, search, + chainId, + chainShortName, ... (isClosed !== undefined && { isClosed: isClosed.toString() } || {}), ... (isRegistered !== undefined && { isRegistered: isRegistered.toString() } || {}), ... (isDefault !== undefined && { isDefault: isDefault.toString() } || {}), ...((isNeedCountsAndTokensLocked !== undefined && { isNeedCountsAndTokensLocked: isNeedCountsAndTokensLocked.toString(), - }) || - {}), - }).toString(); + }) || {}) + }; return api .get<{ @@ -604,7 +636,7 @@ export default function useApi() { count: number; pages: number; currentPage: number; - }>(`/search/networks/?${params}`) + }>(`/search/networks`, { params }) .then(({ data }) => data) .catch(() => ({ rows: [], count: 0, pages: 0, currentPage: 1 })); } @@ -615,13 +647,15 @@ export default function useApi() { sortBy = "updatedAt", order = "DESC", isClosed = undefined, - isRegistered = undefined + isRegistered = undefined, + name = "" }: SearchActiveNetworkParams) { const params = new URLSearchParams({ page, creatorAddress, sortBy, order, + name, ... (isClosed !== undefined && { isClosed: isClosed.toString() } || {}), ... (isRegistered !== undefined && { isRegistered: isRegistered.toString() } || {}) }).toString(); @@ -649,7 +683,8 @@ export default function useApi() { isCurrentlyCurator = undefined, networkName = DEFAULT_NETWORK_NAME, sortBy = "updatedAt", - order = "DESC" + order = "DESC", + chainShortName }: SearchCuratorParams) { const params = new URLSearchParams({ page, @@ -657,6 +692,7 @@ export default function useApi() { networkName, sortBy, order, + chainShortName, ...(isCurrentlyCurator !== undefined && { isCurrentlyCurator: isCurrentlyCurator.toString()} || {}) }).toString(); @@ -687,7 +723,7 @@ export default function useApi() { sortBy, order }).toString(); - + return api .get<{ rows: LeaderBoard[]; @@ -699,8 +735,8 @@ export default function useApi() { .catch(() => ({ rows: [], count: 0, pages: 0, currentPage: 1 })); } - async function repositoryHasIssues(repoPath) { - const search = new URLSearchParams({ repoPath }).toString(); + async function repositoryHasIssues(repoPath, networkName, chainId) { + const search = new URLSearchParams({ repoPath, networkName, chainId }).toString(); return api .get<{ rows: IssueData[]; count: number }>(`/search/issues/?${search}`) @@ -730,14 +766,110 @@ export default function useApi() { }); } + async function updateChainRegistry(chain: SupportedChainData) { + + const model: any = { + chainId: chain.chainId, + name: chain.chainName, + shortName: chain.chainShortName, + activeRPC: chain.chainRpc, + networkId: chain.chainId, + nativeCurrency: { + decimals: +chain.chainCurrencyDecimals, + name: chain.chainCurrencyName, + symbol: chain.chainCurrencySymbol + }, + blockScanner: chain.blockScanner, + eventsApi: chain.eventsApi, + registryAddress: chain.registryAddress + } + + return api.patch<{registryAddress?: string}>(`chains`, model) + .then(response => + response.status === 200 && + !!response.data?.registryAddress && + !isZeroAddress(response.data?.registryAddress)) + .catch((e) => { + console.log(`error patching registry`, e) + return false; + }) + } + async function saveNetworkRegistry(wallet: string, registryAddress: string) { - return api.post("/setup/registry", { wallet, registryAddress }) + return api.post("setup/registry", { wallet, registryAddress }) .then(({ data }) => data) .catch((error) => { throw error; }); } + async function getSupportedChains(force = false, query: Partial = null) { + if (!force && state?.supportedChains?.length) + return Promise.resolve(state?.supportedChains); + + const params = new URLSearchParams(query as any); + + return api.get<{result: SupportedChainData[], error?: string; }>(`/chains`, {... query ? {params} : {}}) + .then(({data}) => data) + .then(data => { + if (!data.error) + dispatch(updateSupportedChains(data.result)); + else { + console.error(`failed to fetch supported chains`, data.error); + } + return data?.result; + }) + .catch(e => { + console.error(`failed to fetch supported chains`, e); + return []; + }) + } + + async function addSupportedChain(chain) { + chain.loading = true; + return api.post(`chains`, chain) + .then(({status}) => status === 200) + .catch(e => { + console.error(`failed to addSupportedChain`, e); + return false; + }) + .finally(() => { + chain.loading = false; + getSupportedChains(true); + }) + } + + async function deleteSupportedChain(chain) { + chain.loading = true; + + return api.delete(`chains?id=${chain.chainId}`) + .then(({status}) => { + dispatch(status === 200 ? toastSuccess('deleted chain') : toastError('failed to delete')); + return status === 200 + }) + .catch(e => { + console.error(`failed to addSupportedChain`, e); + return false; + }) + .finally(() => { + chain.loading = false; + getSupportedChains(true); + }) + } + + async function patchSupportedChain(chain, patch: Partial) { + return api.patch(`chains`, {...chain, ...patch}) + .then(({status}) => status === 200) + .catch(e => { + console.error(`failed to patchSupportedChain`, e); + return false; + }) + .finally(() => { + chain.loading = false; + getSupportedChains(true); + }) + } + async function getKycSession(asNewSession = false){ const params = asNewSession ? {asNewSession}: {}; @@ -763,6 +895,7 @@ export default function useApi() { } return { + getSupportedChains, createIssue, updateIssue, createNetwork, @@ -782,6 +915,7 @@ export default function useApi() { getTotalUsers, getTotalBounties, getTotalNetworks, + getUserAll, getUserOf, getUserPullRequests, getUserWith, @@ -811,6 +945,10 @@ export default function useApi() { getNetworkTokens, createNFT, saveNetworkRegistry, + addSupportedChain, + deleteSupportedChain, + updateChainRegistry, + patchSupportedChain, getKycSession, validateKycSession, getReposWithBounties diff --git a/x-hooks/use-authentication.tsx b/x-hooks/use-authentication.tsx index 0a6c3a6a32..00ce0aa710 100644 --- a/x-hooks/use-authentication.tsx +++ b/x-hooks/use-authentication.tsx @@ -2,9 +2,12 @@ import {useState} from "react"; import BigNumber from "bignumber.js"; import {signIn, signOut, useSession} from "next-auth/react"; +import { useTranslation } from "next-i18next"; +import getConfig from "next/config"; import {useRouter} from "next/router"; import {useAppState} from "contexts/app-state"; +import {changeChain} from "contexts/reducers/change-chain"; import { changeCurrentUser, changeCurrentUserAccessToken, @@ -14,42 +17,48 @@ import { changeCurrentUserLogin, changeCurrentUserMatch, changeCurrentUserSignature, - changeCurrentUserWallet + changeCurrentUserWallet, + changeCurrentUserisAdmin } from "contexts/reducers/change-current-user"; import {changeActiveNetwork} from "contexts/reducers/change-service"; import {changeConnectingGH, changeSpinners, changeWalletSpinnerTo} from "contexts/reducers/change-spinners"; +import { addToast } from "contexts/reducers/change-toaster"; import {changeReAuthorizeGithub} from "contexts/reducers/update-show-prop"; -import { IM_AM_CREATOR_ISSUE } from "helpers/constants"; +import {IM_AN_ADMIN, NOT_AN_ADMIN, UNSUPPORTED_CHAIN} from "helpers/constants"; import decodeMessage from "helpers/decode-message"; +import {EventName} from "interfaces/analytics"; import {CustomSession} from "interfaces/custom-session"; import {kycSession} from "interfaces/kyc-session"; import {WinStorage} from "services/win-storage"; +import useAnalyticEvents from "x-hooks/use-analytic-events"; import useApi from "x-hooks/use-api"; +import useChain from "x-hooks/use-chain"; import {useDao} from "x-hooks/use-dao"; import {useNetwork} from "x-hooks/use-network"; +import useSignature from "x-hooks/use-signature"; import {useTransactions} from "x-hooks/use-transactions"; -import {EventName} from "../interfaces/analytics"; -import useAnalyticEvents from "./use-analytic-events"; -import useSignature from "./use-signature"; export const SESSION_EXPIRATION_KEY = "next-auth.expiration"; -export function useAuthentication() { +const { publicRuntimeConfig } = getConfig(); +export function useAuthentication() { const session = useSession(); - const {state, dispatch} = useAppState(); + const {asPath, push} = useRouter(); + const {connect} = useDao(); + const { chain } = useChain(); const transactions = useTransactions(); + const { signMessage: _signMessage } = useSignature(); + const {state, dispatch} = useAppState(); const { loadNetworkAmounts } = useNetwork(); const { pushAnalytic } = useAnalyticEvents(); - const {asPath, push} = useRouter(); - const {getUserOf, getUserWith, searchCurators, getKycSession, validateKycSession} = useApi(); - const {signMessage} = useSignature() + const {getUserOf, getUserAll, searchCurators, getKycSession, validateKycSession} = useApi(); const [lastUrl,] = useState(new WinStorage('lastUrlBeforeGHConnect', 0, 'sessionStorage')); const [balance,] = useState(new WinStorage('currentWalletBalance', 1000, 'sessionStorage')); @@ -70,46 +79,60 @@ export function useAuthentication() { return; transactions.deleteFromStorage(); - + const lastNetwork = state.Service?.network?.lastVisited === "undefined" ? "" : state.Service?.network?.lastVisited; - + const expirationStorage = new WinStorage(SESSION_EXPIRATION_KEY, 0); - + expirationStorage.removeItem(); - signOut({callbackUrl: `${URL_BASE}/${lastNetwork}`}) + signOut({callbackUrl: `${URL_BASE}/${lastNetwork}/${chain.chainShortName}`}) .then(() => { dispatch(changeCurrentUser.update({handle: state.currentUser?.handle, walletAddress: ''})); }); } function connectWallet() { - if (!state.Service?.active) - return; - connect(); } function updateWalletAddress() { - if (state.spinners?.wallet) - return; - - if (!state.currentUser?.connected) + if (state.spinners?.wallet || !state.currentUser?.connected) return; dispatch(changeWalletSpinnerTo(true)); - state.Service.active.getAddress() - .then(address => { + (state.Service?.active ? + state.Service.active.getAddress() : window.ethereum.request({method: 'eth_requestAccounts'})) + .then(_address => { + if (Array.isArray(_address)) console.debug("eth_requestAccounts", _address); + + const address = Array.isArray(_address) ? _address[0] : _address; + if (address !== state.currentUser?.walletAddress) { - dispatch(changeCurrentUserWallet(address)) - pushAnalytic(EventName.WALLET_ADDRESS_CHANGED, {newAddress: address.toString()}) + dispatch(changeCurrentUserWallet(address?.toLowerCase())); + pushAnalytic(EventName.WALLET_ADDRESS_CHANGED, {newAddress: address?.toString()}); } - sessionStorage.setItem(`currentWallet`, address); + dispatch(changeCurrentUserisAdmin(publicRuntimeConfig.adminWallet.toLowerCase() === address?.toLowerCase())); + + const windowChainId = +window.ethereum.chainId; + const chain = state.supportedChains?.find(({chainId}) => chainId === windowChainId); + + dispatch(changeChain.update({ + id: (chain?.chainId || windowChainId)?.toString(), + name: chain?.chainName || UNSUPPORTED_CHAIN, + shortName: chain?.chainShortName?.toLowerCase() || UNSUPPORTED_CHAIN, + explorer: chain?.blockScanner, + events: chain?.eventsApi, + registry: chain?.registryAddress + })); + + sessionStorage.setItem("currentChainId", chain ? chain?.chainId?.toString() : (+windowChainId)?.toString()); + sessionStorage.setItem("currentWallet", address || ''); }) .catch(e => { - console.error(`Error getting address`, e); + console.error("Error getting address", e); }) .finally(() => { dispatch(changeWalletSpinnerTo(false)); @@ -118,8 +141,6 @@ export function useAuthentication() { } function connectGithub() { - console.debug(`connectGithub`, state.currentUser) - if (!state.currentUser?.walletAddress) return; @@ -139,7 +160,7 @@ export function useAuthentication() { return dispatch(changeConnectingGH(false)) lastUrl.value = asPath; - + if(signedIn) signIn('github', {callbackUrl: `${URL_BASE}${asPath}`}) @@ -158,9 +179,9 @@ export function useAuthentication() { const userLogin = sessionUser.login; const walletAddress = state.currentUser.walletAddress.toLowerCase(); - getUserWith(userLogin) + getUserAll(walletAddress,userLogin) .then(async(user) => { - if (!user.githubLogin){ + if (!user?.githubLogin){ dispatch(changeCurrentUserMatch(undefined)); if(session.status === 'authenticated' && state.currentUser.login && !asPath.includes(`connect-account`)){ @@ -187,7 +208,7 @@ export function useAuthentication() { } function updateWalletBalance(force = false) { - if ((!force && (balance.value || !state.currentUser?.walletAddress)) || !state.Service?.active?.network) + if ((!force && (balance.value || !state.currentUser?.walletAddress)) || !state.Service?.active?.network || !chain) return; const update = newBalance => { @@ -205,7 +226,11 @@ export function useAuthentication() { state.Service.active.getOraclesResume(state.currentUser.walletAddress), state.Service.active.getBalance('settler', state.currentUser.walletAddress), - searchCurators({ address: state.currentUser.walletAddress, networkName: state.Service?.network?.active?.name }) + searchCurators({ + address: state.currentUser.walletAddress, + networkName: state.Service?.network?.active?.name, + chainShortName: chain.chainShortName + }) .then(v => v?.rows[0]?.tokensLocked || 0).then(value => new BigNumber(value)), // not balance, but related to address, no need for a second useEffect() state.Service.active.isCouncil(state.currentUser.walletAddress), @@ -248,44 +273,68 @@ export function useAuthentication() { dispatch(changeReAuthorizeGithub(!!expirationStorage.value && new Date(expirationStorage.value) < new Date())); } - async function signMessageIfCreatorIssue() { - return new Promise(async (resolve, reject) => { - console.log(`signMessageIfCreatorIssue()`, state.connectedChain, state.currentUser?.walletAddress) + function signMessage(message?: string) { + return new Promise(async (resolve, reject) => { + if (!state?.currentUser?.walletAddress || + !state?.connectedChain?.id || + state.Service?.starting || + state.spinners?.signingMessage) { + reject("Wallet not connected, service not started or already signing a message"); + return; + } + + const currentWallet = state?.currentUser?.walletAddress?.toLowerCase(); + const isAdminUser = currentWallet === publicRuntimeConfig?.adminWallet?.toLowerCase(); - if ( - !state?.currentUser?.walletAddress || - !state?.connectedChain?.id || - !state?.currentBounty?.data?.creatorAddress - ) - return reject("error to get data"); + if (!isAdminUser && state.connectedChain?.name === UNSUPPORTED_CHAIN) { + dispatch(addToast({ + type: "warning", + title: "Unsupported chain", + content: "To sign a message, connect to a supported chain", + })); + + reject("Unsupported chain"); + return; + } + + const messageToSign = message || (isAdminUser ? IM_AN_ADMIN : NOT_AN_ADMIN); + + const storedSignature = sessionStorage.getItem("currentSignature"); if (decodeMessage(state?.connectedChain?.id, - IM_AM_CREATOR_ISSUE, - state?.currentUser?.signature, - state?.currentBounty?.data?.creatorAddress)) - return resolve(true) - - if ( - state?.currentUser?.walletAddress?.toLowerCase() === - state?.currentBounty?.data?.creatorAddress?.toLowerCase() - ) - signMessage(IM_AM_CREATOR_ISSUE) - .then((r) => { - dispatch(changeCurrentUserSignature(r)); - sessionStorage.setItem(`currentSignature`, r || ''); - sessionStorage.setItem(`currentChainId`, state?.connectedChain?.id || '0'); - resolve(true) - }) - .catch(e => { - console.error(`ERROR`, e); - reject(e) - }) - else { - sessionStorage.setItem(`currentSignature`, ''); - sessionStorage.setItem(`currentChainId`, ''); - return reject('error not owner of this bounty') + messageToSign, + storedSignature || state?.currentUser?.signature, + currentWallet)) { + if (storedSignature) + dispatch(changeCurrentUserSignature(storedSignature)); + else + sessionStorage.setItem("currentSignature", state?.currentUser?.signature); + + resolve(storedSignature || state?.currentUser?.signature); + return; } - }) + + dispatch(changeSpinners.update({ signingMessage: true })); + + await _signMessage(messageToSign) + .then(signature => { + dispatch(changeSpinners.update({ signingMessage: false })); + + if (signature) { + dispatch(changeCurrentUserSignature(signature)); + sessionStorage.setItem("currentSignature", signature); + + resolve(signature); + return; + } + + dispatch(changeCurrentUserSignature(undefined)); + sessionStorage.removeItem("currentSignature"); + + reject("Message not signed"); + return; + }); + }); } function updateKycSession(){ @@ -312,7 +361,7 @@ export function useAuthentication() { listenToAccountsChanged, updateCurrentUserLogin, verifyReAuthorizationNeed, - signMessageIfCreatorIssue, + signMessage, updateKycSession, } } \ No newline at end of file diff --git a/x-hooks/use-bepro.tsx b/x-hooks/use-bepro.tsx index e74f1c4c93..39182580f1 100644 --- a/x-hooks/use-bepro.tsx +++ b/x-hooks/use-bepro.tsx @@ -2,6 +2,9 @@ import {TransactionReceipt} from "@taikai/dappkit/dist/src/interfaces/web3-core" import BigNumber from "bignumber.js"; import {useTranslation} from "next-i18next"; +import {useAppState} from "contexts/app-state"; +import {addTx, updateTx} from "contexts/reducers/change-tx-list"; + import {parseTransaction} from "helpers/transactions"; import {TransactionStatus} from "interfaces/enums/transaction-status"; @@ -14,19 +17,15 @@ import {NetworkParameters} from "types/dappkit"; import useApi from "x-hooks/use-api"; -import {useAppState} from "../contexts/app-state"; -import {addTx, updateTx} from "../contexts/reducers/change-tx-list"; - const DIVISOR = 1000000; export default function useBepro() { - const { dispatch, state } = useAppState(); const { t } = useTranslation("common"); const { processEvent } = useApi(); - // const {getDatabaseBounty, getChainBounty} = useBounty(); + const { dispatch, state } = useAppState(); - const networkTokenSymbol = state.Service?.network?.networkToken?.symbol || t("misc.$token"); + const networkTokenSymbol = state.Service?.network?.active?.networkToken?.symbol || t("misc.$token"); const failTx = (err, tx, reject?) => { @@ -46,7 +45,7 @@ export default function useBepro() { network: state.Service?.network?.active, }] as any); dispatch(disputeTxAction); - await state.Service?.active.disputeProposal(+state.currentBounty?.chainData?.id, +proposalContractId) + await state.Service?.active.disputeProposal(+state.currentBounty?.data?.contractId, +proposalContractId) .then((txInfo: Error | TransactionReceipt | PromiseLike) => { dispatch(updateTx([parseTransaction(txInfo, disputeTxAction.payload[0] as SimpleBlockTransactionPayload)])) resolve?.(txInfo); @@ -172,16 +171,16 @@ export default function useBepro() { let tx: { blockNumber: number; } - await state.Service?.active.cancelBounty(state.currentBounty?.chainData?.id, funding) + await state.Service?.active.cancelBounty(state.currentBounty?.data?.contractId, funding) .then((txInfo: { blockNumber: number; }) => { tx = txInfo; return processEvent("bounty", "canceled", state.Service?.network?.lastVisited, - {fromBlock: txInfo.blockNumber, id: state.currentBounty?.chainData?.id}); + {fromBlock: txInfo.blockNumber, id: state.currentBounty?.data?.contractId}); }) .then((canceledBounties) => { - if (!canceledBounties?.[state.currentBounty?.chainData?.cid]) throw new Error('Failed'); + if (!canceledBounties?.[state.currentBounty?.data?.issueId]) throw new Error('Failed'); dispatch(updateTx([parseTransaction(tx, redeemTx.payload[0] as SimpleBlockTransactionPayload)])) resolve(tx) // todo should force these two after action, but we can't have it here or it will fall outside of context @@ -203,17 +202,17 @@ export default function useBepro() { dispatch(transaction); let tx: { blockNumber: number; } - await state.Service?.active.hardCancel(state.currentBounty?.chainData?.id) + await state.Service?.active.hardCancel(state.currentBounty?.data?.contractId) .then((txInfo: { blockNumber: number; }) => { tx = txInfo; return processEvent("bounty", "canceled", state.Service?.network?.lastVisited, { fromBlock: txInfo.blockNumber, - id: state.currentBounty?.chainData?.id + id: state.currentBounty?.data?.contractId }); }) .then((canceledBounties) => { - if (!canceledBounties?.[state.currentBounty?.chainData?.cid]) throw new Error('Failed'); + if (!canceledBounties?.[state.currentBounty?.data?.issueId]) throw new Error('Failed'); dispatch(updateTx([parseTransaction(tx, transaction.payload[0] as SimpleBlockTransactionPayload)])) // getChainBounty(true); // getDatabaseBounty(true); diff --git a/x-hooks/use-bounty.tsx b/x-hooks/use-bounty.tsx index 4c272e51bf..96523d0337 100644 --- a/x-hooks/use-bounty.tsx +++ b/x-hooks/use-bounty.tsx @@ -1,7 +1,5 @@ import {useContext} from "react"; -import { Defaults } from "@taikai/dappkit"; -import BigNumber from "bignumber.js"; import {useRouter} from "next/router"; import {useAppState} from "contexts/app-state"; @@ -9,37 +7,31 @@ import {BountyEffectsContext} from "contexts/bounty-effects"; import { changeCurrentBountyComments, changeCurrentBountyData, - changeCurrentBountyDataChain, - changeCurrentBountyDataIsDraft, - changeCurrentBountyDataIsFinished, - changeCurrentBountyDataIsInValidation, - changeCurrentBountyDataProposals, - changeCurrentBountyDataReward, - changeCurrentBountyDataTransactional, changeCurrentKycSteps, } from "contexts/reducers/change-current-bounty"; import {changeSpinners} from "contexts/reducers/change-spinners"; -import {bountyReadyPRsHasNoInvalidProposals} from "helpers/proposal"; +import { issueParser } from "helpers/issue"; -import { BountyExtended, ProposalExtended } from "interfaces/bounty"; import {IssueData, pullRequest} from "interfaces/issue-data"; import useApi from "x-hooks/use-api"; +import useChain from "x-hooks/use-chain"; +import { useNetwork } from "x-hooks/use-network"; import useOctokit from "x-hooks/use-octokit"; - const CACHE_BOUNTY_TIME = 60 * 1000; // 1min export function useBounty() { if (!useContext(BountyEffectsContext)) - throw new Error(`useBounty() depends on `) - - const {state, dispatch} = useAppState(); + throw new Error(`useBounty() depends on `); - const {query} = useRouter(); - const {getIssue} = useApi(); + const { chain } = useChain(); + const { getIssue } = useApi(); + const { query, replace } = useRouter(); + const { state, dispatch } = useAppState(); + const { getURLWithNetwork } = useNetwork(); const { getIssueOrPullRequestComments, getPullRequestDetails, getPullRequestReviews } = useOctokit(); function isCurrentBountyCached() { @@ -54,7 +46,7 @@ export function useBounty() { } function getDatabaseBounty(force = false) { - if (!state.Service?.network?.active || !query?.id || !query.repoId) + if (!query?.id || !query.repoId || !chain) return; if (!force && isCurrentBountyCached() || state.spinners?.bountyDatabase) @@ -62,39 +54,15 @@ export function useBounty() { dispatch(changeSpinners.update({bountyDatabase: true})) - getIssue(+query.repoId, +query.id, query.network.toString()) - .then(async (bounty: IssueData) => { - const fundedAmount = BigNumber(bounty?.fundedAmount || 0) - const fundingAmount = BigNumber(bounty?.fundingAmount || 0) - const fundedPercent = fundedAmount.multipliedBy(100).dividedBy(fundingAmount) - - const bigNumbers = { - amount: BigNumber(bounty?.amount), - fundingAmount, - fundedAmount, - fundedPercent - } - - const mergeProposalMapper = (proposal) => ({ - ...proposal, - disputeWeight: BigNumber(proposal?.disputeWeight || 0), - contractCreationDate: BigNumber(proposal.contractCreationDate).toNumber(), - isMerged: bounty.merged !== null && +proposal?.contractId === +bounty.merged - }) + getIssue(+query.repoId, +query.id, query.network.toString(), chain.chainId) + .then((bounty: IssueData) => { + const parsedBounty = issueParser(bounty); - if(bounty?.benefactors) - bounty.benefactors = bounty?.benefactors.map((benefactor) => - ({...benefactor, amount: BigNumber(benefactor?.amount)})) - - - const mergeProposals = bounty?.mergeProposals.map(mergeProposalMapper); - const extendedBounty = {...bounty, mergeProposals, ...bigNumbers}; - - dispatch(changeCurrentBountyData(extendedBounty)); + dispatch(changeCurrentBountyData(parsedBounty)); return Promise.all([ getIssueOrPullRequestComments(bounty?.repository?.githubPath, +bounty?.githubId), - extendedBounty + parsedBounty ]); }) .then(([comments, bounty]) => { @@ -115,89 +83,12 @@ export function useBounty() { .then(pullRequests => { const extendedPrs = pullRequests.slice(1) as pullRequest[]; dispatch(changeCurrentBountyData({ ...pullRequests[0], pullRequests: extendedPrs })); - }); - - } - - function getChainBounty(force = false) { - - if ( - !state.Service?.active?.network || - !state.currentBounty?.data?.contractId || - state.spinners?.bountyChain || - query?.id !== state.currentBounty?.data?.githubId - ) - return; - - dispatch(changeSpinners.update({bountyChain: true})) - state.Service.active.getBounty(state.currentBounty.data.contractId) - .then(bounty => { - - if(!bounty?.id) return; - - const pullRequestsMapper = (pullRequest) => ({ - ...pullRequest, - isCancelable: !bounty.proposals.find(proposal => proposal.prId === pullRequest.id) - }); - - bounty.pullRequests = bounty?.pullRequests?.filter(pr => !pr.canceled).map(pullRequestsMapper); - bounty.fundedAmount = bounty?.funding?.reduce((p, c) => p.plus(c.amount), BigNumber(0)) - bounty.fundedPercent = bounty?.fundedAmount?.multipliedBy(100).dividedBy(bounty?.fundingAmount); - bounty.isFundingRequest = bounty?.fundingAmount.gt(0); - - dispatch(changeCurrentBountyDataChain.update(bounty)); - - state.Service.active.getERC20TokenData(bounty.transactional) - .then(token => dispatch(changeCurrentBountyDataTransactional(token))) - - if(bounty.rewardToken !== Defaults.nativeZeroAddress) - state.Service.active.getERC20TokenData(bounty.rewardToken) - .then(token => dispatch(changeCurrentBountyDataReward(token))) - - state.Service.active.isBountyInDraftChain(bounty.creationDate) - .then(bool => dispatch(changeCurrentBountyDataIsDraft(bool))); - - getExtendedProposalsForCurrentBounty(bounty) - .then(proposals => dispatch(changeCurrentBountyDataProposals(proposals))) - .catch(error => console.debug("Failed to getExtendedProposalsForCurrentBounty", error)); - - bountyReadyPRsHasNoInvalidProposals(bounty, state.Service.active.network) - .catch(() => -1) - .then(value => { - dispatch(changeCurrentBountyDataIsFinished(value !== 0)); - dispatch(changeCurrentBountyDataIsInValidation([2].includes(value))) - dispatch(changeSpinners.update({bountyChain: false})) - }); - - }).catch(error => console.debug("getChainBounty", error)); - - } - - /** - * todo: getExtendedProposalsForCurrentBounty() should happen on webnetwork-events - * - * MAKE SURE that these functions (getExtendedProposalsForCurrentBounty, getExtendedPullRequestsForCurrentBounty) - * are only called once, since they ignore cached information - */ - - function getExtendedProposalsForCurrentBounty(bounty: BountyExtended): Promise { - if (!state.currentBounty?.chainData || !state.Service?.active) - return Promise.reject([]); - - const wallet = state.currentUser?.walletAddress; - - return Promise.all(bounty.proposals.map(proposal => - !wallet - ? ({...proposal}) - : state.Service.active.getDisputesOf(wallet, +bounty.id, +proposal.id) - .then(value => ({...proposal, canUserDispute: !value.gt(0)})))) - .then(proposals => { - return Promise.resolve(proposals) }) - .catch(e => { - console.error(`Failed to get extended proposals`, e); - return Promise.reject(e) - }); + .catch(error => { + console.debug("Failed to get database data", error); + replace(getURLWithNetwork("/")); + }) + .finally(() => dispatch(changeSpinners.update({bountyDatabase: false}))); } function getExtendedPullRequestsForCurrentBounty() { @@ -249,10 +140,8 @@ export function useBounty() { } return { - getExtendedProposalsForCurrentBounty, getExtendedPullRequestsForCurrentBounty, getDatabaseBounty, - getChainBounty, validateKycSteps, } } \ No newline at end of file diff --git a/x-hooks/use-chain.ts b/x-hooks/use-chain.ts new file mode 100644 index 0000000000..77d1603221 --- /dev/null +++ b/x-hooks/use-chain.ts @@ -0,0 +1,41 @@ +import { useState, useEffect } from "react"; + +import { useRouter } from "next/router"; + +import { useAppState } from "contexts/app-state"; + +import { SupportedChainData } from "interfaces/supported-chain-data"; + +interface FindSupportedChainProps { + chainId?: number; + chainShortName?: string; +} + +export default function useChain() { + const { query } = useRouter(); + + const [chain, setChain] = useState(); + + const { state: { supportedChains } } = useAppState(); + + function findSupportedChain({ chainId, chainShortName } : FindSupportedChainProps) { + return supportedChains?.find(e => e.chainId === chainId || + e.chainShortName?.toLowerCase() === chainShortName?.toLowerCase()); + } + + function getChainFromUrl() { + if (!supportedChains?.length || !query.chain) return undefined; + + return findSupportedChain({ chainShortName: query.chain.toString() }); + } + + useEffect(() => { + setChain(getChainFromUrl()); + }, [query?.chain, supportedChains]); + + return { + chain, + getChainFromUrl, + findSupportedChain + } +} \ No newline at end of file diff --git a/x-hooks/use-dao.tsx b/x-hooks/use-dao.tsx index d2b24cfd2e..7a581f2bb6 100644 --- a/x-hooks/use-dao.tsx +++ b/x-hooks/use-dao.tsx @@ -1,35 +1,62 @@ -import {useAppState} from "../contexts/app-state"; -import {changeCurrentUserConnected} from "../contexts/reducers/change-current-user"; -import {changeActiveDAO, changeStarting} from "../contexts/reducers/change-service"; -import {changeConnecting} from "../contexts/reducers/change-spinners"; -import {toastError,} from "../contexts/reducers/change-toaster"; -import DAO from "../services/dao-service"; +import {isZeroAddress} from "ethereumjs-util"; +import {useRouter} from "next/router"; +import {isAddress} from "web3-utils"; +import {useAppState} from "contexts/app-state"; +import {changeCurrentUserConnected, changeCurrentUserWallet} from "contexts/reducers/change-current-user"; +import {changeActiveDAO, changeStarting} from "contexts/reducers/change-service"; +import {changeChangingChain, changeConnecting} from "contexts/reducers/change-spinners"; + +import { UNSUPPORTED_CHAIN } from "helpers/constants"; + +import { SupportedChainData } from "interfaces/supported-chain-data"; + +import DAO from "services/dao-service"; + +import useChain from "x-hooks/use-chain"; +import useNetworkChange from "x-hooks/use-network-change"; export function useDao() { + const { replace, asPath, pathname } = useRouter(); + const { chain } = useChain(); const {state, dispatch} = useAppState(); + const { handleAddNetwork } = useNetworkChange(); + + function isChainConfigured(chain: SupportedChainData) { + return isAddress(chain?.registryAddress) && !isZeroAddress(chain?.registryAddress); + } + + function isServiceReady() { + return !state.Service?.starting && !state.spinners?.switchingChain; + } /** * Enables the user/dapp to connect to the active DAOService */ function connect() { - if (!state.Service?.active) - return; + dispatch(changeConnecting(true)); - dispatch(changeConnecting(true)) - - state.Service.active - .connect() + return (state.Service?.active ? state.Service.active.connect() : + window.ethereum.request({method: 'eth_requestAccounts'})) .then((connected) => { if (!connected) { - dispatch(toastError('Failed to connect')); - console.error(`Failed to connect`, state.Service); - return; + console.debug(`Failed to connect`, state.Service); + + return false; } dispatch(changeCurrentUserConnected(true)); - dispatch(changeConnecting(false)) + dispatch(changeCurrentUserWallet(connected[0] as string)); + + return true; + }) + .catch(error => { + console.debug(`Failed to connect`, error); + return false; + }) + .finally(() => { + dispatch(changeConnecting(false)); }); } @@ -38,9 +65,9 @@ export function useDao() { * @param networkAddress */ function changeNetwork() { - const networkAddress = state.Service?.network?.active?.networkAddress; + const { networkAddress, chain_id } = state.Service?.network?.active || {}; - if (!state.Service?.active || !networkAddress) + if (!state.Service?.active || !networkAddress || !chain || state.spinners.switchingChain) return; if (state.Service?.active?.network?.contractAddress === networkAddress) @@ -48,19 +75,25 @@ export function useDao() { const service = state.Service.active; + if (+chain_id !== +chain.chainId || chain.chainRpc !== state.Service?.active?.web3Host) + return; + + console.debug("Starting network"); + dispatch(changeStarting(true)); state.Service.active .loadNetwork(networkAddress) .then(started => { if (!started) { - console.error(`Failed to load network`, networkAddress); + console.error("Failed to load network", networkAddress); return; } dispatch(changeActiveDAO(service)); + console.debug("Network started"); }) .catch(error => { - console.error(`Error loading network`, error); + console.error("Error loading network", error); }) .finally(() => { dispatch(changeStarting(false)); @@ -72,23 +105,75 @@ export function useDao() { * dispatches changeNetwork() to active network */ function start() { - console.debug(`useDao() start`, !(!state.Settings?.urls || !!state.Service?.active || !!state.Service?.starting)); - if (!state.Settings?.urls || !!state.Service?.active || !!state.Service?.starting) + const supportedChains = state.supportedChains; + + if (!supportedChains?.length) { + console.debug("No supported chains found"); return; + } + + const networkChainId = state.Service?.network?.active?.chain_id; + const isOnNetwork = pathname?.includes("[network]"); + + if (isOnNetwork && !networkChainId) { + console.debug("Is on network, but network data was not loaded yet"); + return; + } + + const { connectedChain } = state; + + const chainIdToConnect = + state.Service?.network?.active?.chain_id || + (connectedChain?.name === UNSUPPORTED_CHAIN ? undefined : connectedChain?.id); + + const chainToConnect = supportedChains.find(({ isDefault, chainId }) => + chainIdToConnect ? +chainIdToConnect === +chainId : isDefault); + + if (!chainToConnect) { + console.debug("No default or network chain found"); + return; + } + + const isConfigured = isChainConfigured(chainToConnect); + + if (!isConfigured) { + console.debug("Chain not configured", chainToConnect); + + if (state.currentUser?.isAdmin && !asPath.includes("setup") && !asPath.includes("connect-account")) { + replace("/setup"); + + return; + } + } + + const isSameWeb3Host = chainToConnect.chainRpc === state.Service?.active?.web3Host; + const isSameRegistry = chainToConnect?.registryAddress === state.Service?.active?.registryAddress?.toLowerCase(); + + if (isSameWeb3Host && isSameRegistry || state.Service?.starting) { + console.debug("Already connected to this web3Host or the service is still starting"); + return; + } + + console.debug("Starting DAOService"); dispatch(changeStarting(true)); - const {urls: {web3Provider: web3Host}, contracts} = state.Settings; + const { chainRpc: web3Host, registryAddress: _registry } = chainToConnect; - const registryAddress = contracts?.networkRegistry; + const registryAddress = isConfigured ? _registry : undefined; - const daoService = new DAO({web3Host, registryAddress}); + const daoService = new DAO({ web3Host, registryAddress }); daoService.start() - .then(started => { + .then(async started => { if (started) { + if (registryAddress) + await daoService.loadRegistry() + .catch(error => console.debug("Failed to load registry", error)); + window.DAOService = daoService; dispatch(changeActiveDAO(daoService)); + console.debug("DAOService started", { web3Host, registryAddress }); } }) .catch(error => { @@ -99,10 +184,27 @@ export function useDao() { }) } + function changeChain() { + if (state.connectedChain?.matchWithNetworkChain !== false || + !state.currentUser?.walletAddress || + state.spinners?.changingChain) + return; + + dispatch(changeChangingChain(true)); + + const networkChain = state.Service?.network?.active?.chain; + + if (networkChain) + handleAddNetwork(networkChain) + .catch(console.debug) + .finally(() => dispatch(changeChangingChain(false))); + } return { changeNetwork, + changeChain, connect, start, + isServiceReady }; } \ No newline at end of file diff --git a/x-hooks/use-erc20.tsx b/x-hooks/use-erc20.tsx index 6ea3ddd40e..0fe076ca17 100644 --- a/x-hooks/use-erc20.tsx +++ b/x-hooks/use-erc20.tsx @@ -1,20 +1,21 @@ -import {useCallback, useEffect, useState} from "react"; +import {useEffect, useState} from "react"; import {TransactionReceipt} from "@taikai/dappkit/dist/src/interfaces/web3-core"; import BigNumber from "bignumber.js"; import {useAppState} from "contexts/app-state"; +import {addTx, updateTx} from "contexts/reducers/change-tx-list"; +import { UNSUPPORTED_CHAIN } from "helpers/constants"; import {parseTransaction} from "helpers/transactions"; +import {MetamaskErrors} from "interfaces/enums/Errors"; import {TransactionStatus} from "interfaces/enums/transaction-status"; import {TransactionTypes} from "interfaces/enums/transaction-types"; +import { SimpleBlockTransactionPayload } from "interfaces/transaction"; import useBepro from "x-hooks/use-bepro"; -import {addTx, updateTx} from "../contexts/reducers/change-tx-list"; -import {MetamaskErrors} from "../interfaces/enums/Errors"; - export default function useERC20() { const [name, setName] = useState(); const [decimals, setDecimals] = useState(18); @@ -32,11 +33,19 @@ export default function useERC20() { const logData = { wallet: state.currentUser?.walletAddress, token: address, - network: state.Service?.active?.network?.contractAddress + network: state.Service?.active?.network?.contractAddress, + service: state.Service?.active }; - const updateAllowanceAndBalance = useCallback(() => { - if (!state.currentUser?.walletAddress || !address) return; + const isServiceReady = + !state.Service?.starting && !state.spinners?.switchingChain && state.Service?.active?.web3Connection?.started; + + function updateAllowanceAndBalance() { + if (!state.currentUser?.walletAddress || + !address || + !name || + !isServiceReady || + state.connectedChain?.name === UNSUPPORTED_CHAIN) return; state.Service?.active.getTokenBalance(address, state.currentUser.walletAddress) .then(setBalance) @@ -48,7 +57,7 @@ export default function useERC20() { state.Service?.active.getAllowance(address, state.currentUser.walletAddress, realSpender) .then(setAllowance) .catch(error => console.debug("useERC20:getAllowance", logData, error)); - }, [state.currentUser?.walletAddress, state.Service?.active, address]); + } function approve(amount: string) { if (!state.currentUser?.walletAddress || !state.Service?.active || !address || !amount) return; @@ -66,11 +75,14 @@ export default function useERC20() { } useEffect(() => { - if (!address) setLoadError(undefined); - if (!address && name) setDefaults(); - if (state.Service?.active && address) + if (!address) { + setLoadError(undefined); + + if (name) + setDefaults(); + } else if (address && !name && isServiceReady) state.Service?.active.getERC20TokenData(address) - .then(({ name, symbol, decimals, totalSupply }) => { + .then(async ({ name, symbol, decimals, totalSupply }) => { setName(name); setSymbol(symbol); setDecimals(decimals); @@ -78,9 +90,11 @@ export default function useERC20() { setLoadError(false); }) .catch(error => console.debug("useERC20:getERC20TokenData", logData, error)); - + }, [state.currentUser?.walletAddress, address, name, isServiceReady]); + + useEffect(() => { updateAllowanceAndBalance(); - }, [state.currentUser?.walletAddress, state.Service?.active, address]); + }, [state.currentUser?.walletAddress, isServiceReady, state.connectedChain, name]); async function deploy(name: string, symbol: string, @@ -90,13 +104,13 @@ export default function useERC20() { const transaction = addTx([{ type: TransactionTypes.deployERC20Token, network: state.Service?.network?.active - } as any]); + }]); dispatch(transaction); await state.Service?.active.deployERC20Token(name, symbol, cap, ownerAddress) .then((txInfo: TransactionReceipt) => { - dispatch(updateTx([parseTransaction(txInfo, transaction.payload[0] as any)])); + dispatch(updateTx([parseTransaction(txInfo, transaction.payload[0] as SimpleBlockTransactionPayload)])); resolve(txInfo); }) .catch((err) => { @@ -109,6 +123,16 @@ export default function useERC20() { }); } + function _setAddress(_address: string) { + if (_address?.toLowerCase() !== address?.toLowerCase()) + setAddress(_address); + } + + function _setSpender(_address: string) { + if (_address?.toLowerCase() !== address?.toLowerCase()) + setSpender(_address); + } + return { name, symbol, @@ -119,8 +143,8 @@ export default function useERC20() { allowance, totalSupply, approve, - setAddress, - setSpender, + setAddress: _setAddress, + setSpender: _setSpender, deploy, updateAllowanceAndBalance }; diff --git a/x-hooks/use-network-change.ts b/x-hooks/use-network-change.ts new file mode 100644 index 0000000000..b7c141bd8a --- /dev/null +++ b/x-hooks/use-network-change.ts @@ -0,0 +1,54 @@ +import {toHex} from "web3-utils"; + +import { useAppState } from "contexts/app-state"; +import { changeSpinners } from "contexts/reducers/change-spinners"; + +import {SupportedChainData} from "interfaces/supported-chain-data"; + +export default function useNetworkChange() { + const { state, dispatch } = useAppState(); + + async function handleAddNetwork(chosenSupportedChain: SupportedChainData = state.Service?.network?.active?.chain) { + const chainId = toHex(chosenSupportedChain.chainId); + + dispatch(changeSpinners.update({ switchingChain: true })); + + return window.ethereum.request({ + method: "wallet_switchEthereumChain", + params: [ + { + chainId: chainId, + }, + ], + }) + .catch(error => { + if ((error as any)?.message?.indexOf('wallet_addEthereumChain') > -1) { + return window.ethereum.request({ + method: "wallet_addEthereumChain", + params: [ + { + chainId: chainId, + chainName: chosenSupportedChain.chainName, + nativeCurrency: { + name: chosenSupportedChain.chainCurrencyName, + symbol: chosenSupportedChain.chainCurrencySymbol, + decimals: chosenSupportedChain.chainCurrencyDecimals, + }, + rpcUrls: [chosenSupportedChain.chainRpc], + blockExplorerUrls: [chosenSupportedChain.blockScanner], + }, + ], + }).catch(e => { + throw new Error(e); + }) + } + + throw new Error(error); + }) + .finally(() => dispatch(changeSpinners.update({ switchingChain: false }))) + } + + return { + handleAddNetwork + }; +} \ No newline at end of file diff --git a/x-hooks/use-network-theme.ts b/x-hooks/use-network-theme.ts index 32dbb244f5..30838436ae 100644 --- a/x-hooks/use-network-theme.ts +++ b/x-hooks/use-network-theme.ts @@ -1,5 +1,4 @@ import {useRouter} from "next/router"; -import {UrlObject} from "url"; import {useAppState} from "contexts/app-state"; @@ -138,22 +137,9 @@ export default function useNetworkTheme() { }); } - function getURLWithNetwork(href: string, query = undefined): UrlObject { - return { - pathname: `/[network]/${href}`.replace("//", "/"), - query: { - ...query, - network: query?.network || - router?.query?.network || - state?.Service?.network?.active?.name - } - }; - } - return { colorsToCSS, DefaultTheme, - getURLWithNetwork, setNetwork: changeNetwork }; } diff --git a/x-hooks/use-network.tsx b/x-hooks/use-network.tsx index 4a9ee129ef..388e508c91 100644 --- a/x-hooks/use-network.tsx +++ b/x-hooks/use-network.tsx @@ -1,14 +1,15 @@ -import {useState} from "react"; +import {useEffect, useState} from "react"; import {useRouter} from "next/router"; import {UrlObject} from "url"; import {useAppState} from "contexts/app-state"; +import { changeMatchWithNetworkChain } from "contexts/reducers/change-chain"; import { + changeActiveAvailableChains, changeActiveNetwork, changeActiveNetworkAmounts, changeActiveNetworkTimes, - changeActiveNetworkToken, changeAllowedTokens, changeNetworkLastVisited } from "contexts/reducers/change-service"; @@ -16,84 +17,104 @@ import { import {WinStorage} from "services/win-storage"; import useApi from "x-hooks/use-api"; +import useChain from "x-hooks/use-chain"; const URLS_WITHOUT_NETWORK = ["/connect-account", "/networks", "/new-network", "/setup"]; export function useNetwork() { - const {state, dispatch} = useAppState(); + const {query, replace} = useRouter(); + + const [networkName, setNetworkName] = useState(); const [storage,] = useState(new WinStorage(`lastNetworkVisited`, 0, 'localStorage')); + + const {state, dispatch} = useAppState(); + const { chain, findSupportedChain } = useChain(); + const {searchNetworks, getNetworkTokens} = useApi(); - const {getNetwork, getNetworkTokens} = useApi(); - const {pathname, query, push, replace} = useRouter(); + function getStorageKey(networkName: string, chainId: string | number) { + return `bepro.network:${networkName}:${chainId}`; + } function clearNetworkFromStorage() { storage.delete(); const networkName = state.Service?.network?.active?.name; + const chainId = state.connectedChain?.id; + if (networkName) - new WinStorage(`bepro.network:${networkName}`, 0, `sessionStorage`).delete(); + new WinStorage(getStorageKey(networkName, chainId), 0, `sessionStorage`).delete(); } function updateActiveNetwork(forceUpdate = false) { - const networkName = query?.network?.toString(); - - if (networkName) { - dispatch(changeNetworkLastVisited(networkName)); - storage.value = networkName; + const queryNetworkName = query?.network?.toString(); + const queryChainName = query?.chain?.toString(); - if (!forceUpdate) { - const cachedNetworkData = new WinStorage(`bepro.network:${networkName}`, 0, `sessionStorage`); + if (!queryNetworkName) return; - if (storage.value === networkName) { - if (cachedNetworkData.value) { - dispatch(changeActiveNetwork(cachedNetworkData.value)); + if (queryChainName) { + const chain = findSupportedChain({ chainShortName: queryChainName }); + + if (chain) { + const storageKey = getStorageKey(queryNetworkName, chain.chainId); - return; - } - } else - storage.value = networkName; - } - } else if (storage.value) dispatch(changeNetworkLastVisited(storage.value)); + if (storage.value && storage.value !== queryNetworkName) + storage.value = queryNetworkName; - getNetwork({ - ... networkName && { - name: networkName - } || { - isDefault: true + const cachedNetworkData = new WinStorage(storageKey, 3000, `sessionStorage`); + if (forceUpdate === false && cachedNetworkData.value) { + dispatch(changeActiveNetwork(cachedNetworkData.value)); + return; + } } - }) - .then(({data}) => { - if (!data.isRegistered) - throw new Error("Network not registered"); - - const key = networkName || data?.name; - - const storageParams = new WinStorage(`bepro.network:${key}`, 3600, `sessionStorage`); - - storageParams.value = data; - dispatch(changeActiveNetwork(data)); - - console.debug(`Updated active params`, data); + } + + searchNetworks({ name: queryNetworkName, chainShortName: queryChainName }) + .then(({count, rows}) => { + if (count === 0) { + throw new Error("No networks found"); + } + + if (queryChainName) { + const data = rows[0]; + + if (!data.isRegistered) { + if (state.currentUser?.walletAddress === data.creatorAddress) + return replace(getURLWithNetwork("/profile/my-network", { + network: data.name, + chain: data.chain.chainShortName + })); + else + throw new Error("Network not registered"); + } + + const newCachedData = new WinStorage(getStorageKey(data.name, data.chain.chainId), 3600, `sessionStorage`); + newCachedData.value = data; + + dispatch(changeNetworkLastVisited(queryNetworkName)); + dispatch(changeActiveNetwork(newCachedData.value)); + } else { + const available = rows + .filter(({ isRegistered, isClosed }) => isRegistered && !isClosed) + .map(network => network.chain); + + dispatch(changeActiveAvailableChains(available)); + } }) - .catch(error => { - console.error(`Failed to get network`, error); - - if (!networkName && !URLS_WITHOUT_NETWORK.includes(pathname)) - replace("/setup"); - - if(networkName) - push({pathname: `/networks`}); + .catch(e => { + console.log(`Failed to get network ${queryNetworkName}`, e); + return replace(`/networks`); }); - } function getURLWithNetwork(href: string, _query = undefined): UrlObject { - const _network = _query?.network ? String(_query?.network)?.replaceAll(" ", "-") : undefined; + const _network = _query?.network ? String(_query?.network)?.toLowerCase()?.replaceAll(" ", "-") : undefined; + const cleanHref = href.replace("/[network]/[chain]", ""); return { - pathname: `/[network]/${href}`.replace("//", "/"), + pathname: `/[network]/[chain]/${cleanHref}`.replace("//", "/"), query: { ..._query, + chain: _query?.chain || query?.chain || state?.Service?.network?.active?.chain?.chainShortName, network: _network || query?.network || state?.Service?.network?.active?.name @@ -101,28 +122,14 @@ export function useNetwork() { }; } - function loadNetworkToken() { - if (!state.Service?.active?.network || state.Service?.network?.networkToken) - return; - - const activeNetworkToken: any = state.Service?.active?.network?.networkToken; - - Promise.all([activeNetworkToken.name(), activeNetworkToken.symbol(),]) - .then(([name, symbol]) => { - dispatch(changeActiveNetworkToken({ - name, - symbol, - decimals: activeNetworkToken.decimals, - address: activeNetworkToken.contractAddress - })) - }); - } - function loadNetworkAllowedTokens() { - if (!state.Service?.active || !state?.Service?.network?.active) + if (!state?.Service?.network?.active || !chain) return; - getNetworkTokens({networkName: state?.Service?.network?.active?.name}).then(tokens => { + getNetworkTokens({ + networkName: state?.Service?.network?.active?.name, + chainId: chain.chainId.toString() + }).then(tokens => { const transactional = []; const reward = []; @@ -137,7 +144,7 @@ export function useNetwork() { if (!state?.Service?.active?.network) return; - const network: any = state.Service.active?.network; + const network = state.Service.active?.network; Promise.all([network.draftTime(), network.disputableTime()]) .then(([draftTime, disputableTime]) => { @@ -152,7 +159,7 @@ export function useNetwork() { if (!state?.Service?.active?.network) return; - const network: any = state.Service.active?.network; + const network = state.Service.active?.network; Promise.all([ network.councilAmount(), @@ -182,14 +189,30 @@ export function useNetwork() { }) } + function updateNetworkAndChainMatch() { + const connectedChainId = state.connectedChain?.id; + const networkChainId = state?.Service?.network?.active?.chain_id; + const isOnANetwork = !!query?.network; + + if (connectedChainId && networkChainId && isOnANetwork) + dispatch(changeMatchWithNetworkChain(+connectedChainId === +networkChainId)); + else + dispatch(changeMatchWithNetworkChain(null)); + } + + useEffect(() => { + setNetworkName(query?.network?.toString()); + }, [query?.network]); + return { + networkName, updateActiveNetwork, getURLWithNetwork, clearNetworkFromStorage, - loadNetworkToken, loadNetworkTimes, loadNetworkAmounts, - loadNetworkAllowedTokens + loadNetworkAllowedTokens, + updateNetworkAndChainMatch } } \ No newline at end of file diff --git a/x-hooks/use-repos.ts b/x-hooks/use-repos.ts index 4117a172c2..e0d7e7b22d 100644 --- a/x-hooks/use-repos.ts +++ b/x-hooks/use-repos.ts @@ -2,18 +2,22 @@ import {useState} from "react"; import {useRouter} from "next/router"; -import {useAppState} from "../contexts/app-state"; -import {changeLoadState} from "../contexts/reducers/change-load"; +import {useAppState} from "contexts/app-state"; +import {changeLoadState} from "contexts/reducers/change-load"; import { changeNetworkReposActive, changeNetworkReposActiveViewerPerm, changeNetworkReposList -} from "../contexts/reducers/change-service"; -import {changeSpinners} from "../contexts/reducers/change-spinners"; -import {RepoInfo} from "../interfaces/repos-list"; -import {WinStorage} from "../services/win-storage"; -import useApi from "./use-api"; -import useOctokit from "./use-octokit"; +} from "contexts/reducers/change-service"; +import {changeSpinners} from "contexts/reducers/change-spinners"; + +import {RepoInfo} from "interfaces/repos-list"; + +import {WinStorage} from "services/win-storage"; + +import useApi from "x-hooks/use-api"; +import useChain from "x-hooks/use-chain"; +import useOctokit from "x-hooks/use-octokit"; export function useRepos() { const {query} = useRouter(); @@ -22,18 +26,19 @@ export function useRepos() { const {state, dispatch} = useAppState(); + const { chain } = useChain(); const {getReposList} = useApi(); const { getRepository, getRepositoryForks, getRepositoryBranches, getRepositoryViewerPermission } = useOctokit(); function loadRepos(force = false) { - const name = query?.network - if (!name || state.spinners?.repos) - return; + const name = query?.network; - dispatch(changeSpinners.update({repos: true})); + if (!name || !chain || state.spinners?.repos || !state.Service?.network?.active) + return; - const key = `bepro.network:repos:${name}` + const key = `bepro.network:repos:${name}:${chain.chainId}`; const storage = new WinStorage(key, 3600, `sessionStorage`); + if (storage.value && !force) { if (!state.Service?.network?.repos?.list) { dispatch(changeNetworkReposList(storage.value)); @@ -43,19 +48,23 @@ export function useRepos() { } dispatch(changeLoadState(true)); + dispatch(changeSpinners.update({repos: true})); - getReposList(force, name.toString()) + getReposList(force, name.toString(), chain.chainId.toString()) .then(repos => { if (!repos) { console.error(`No repos found for`, name); return; } - + storage.value = repos; dispatch(changeNetworkReposList(repos)); - dispatch(changeLoadState(false)); - dispatch(changeSpinners.update({repos: false})) }) + .catch(error => console.debug("Failed to loadRepos", error)) + .finally(() => { + dispatch(changeLoadState(false)); + dispatch(changeSpinners.update({repos: false})); + }); } function updateActiveRepo(id = null) { @@ -77,10 +86,11 @@ export function useRepos() { console.log(`No repo found for repoId: ${id || query?.repoId}`) return; } + getRepository(activeRepo?.githubPath, true) .then(info => { if (!info) - return [] + return []; return Promise.all([ Promise.resolve(info), @@ -105,5 +115,5 @@ export function useRepos() { } - return {loadRepos, updateActiveRepo} + return {loadRepos, updateActiveRepo}; } \ No newline at end of file diff --git a/x-hooks/use-signature.ts b/x-hooks/use-signature.ts index 5a8866d1eb..2f797b1cfc 100644 --- a/x-hooks/use-signature.ts +++ b/x-hooks/use-signature.ts @@ -1,10 +1,12 @@ -import {useAppState} from "../contexts/app-state"; -import decodeMessage from "../helpers/decode-message"; -import {messageFor} from "../helpers/message-for"; +import {useAppState} from "contexts/app-state"; +import { addToast } from "contexts/reducers/change-toaster"; + +import decodeMessage from "helpers/decode-message"; +import {messageFor} from "helpers/message-for"; export default function useSignature() { - const {state: {connectedChain, Service, currentUser}} = useAppState(); + const {dispatch, state: {connectedChain, Service, currentUser}} = useAppState(); async function signMessage(message = ""): Promise { if ((!Service?.active && !window.ethereum) || !currentUser?.walletAddress) @@ -19,16 +21,25 @@ export default function useSignature() { } return new Promise((res, rej) => { - const _promise = (err, d) => { err ? rej(err) : res(d?.result) }; - - if (Service.active) - Service.active?.web3Connection.Web3.currentProvider.sendAsync(payload, - _promise); - else if (window.ethereum) - window.ethereum - .request(payload) - .then((v) => _promise(null, { result: v })) - .catch((e) => _promise(e, null)); + const _promise = (err, d) => { + if (!err) + return res(d?.result); + + console.debug("Failed to sign message", err); + + dispatch(addToast({ + type: "danger", + title: "Failed", + content: "Signed message required to proceed", + })); + + return res(undefined); + }; + + if (Service.active?.web3Connection?.Web3?.currentProvider?.hasOwnProperty("sendAsync")) + Service.active?.web3Connection.Web3.currentProvider.sendAsync(payload, _promise); + else if (window.ethereum) + window.ethereum.request(payload).then(v => _promise(null, {result: v})).catch(e => _promise(e, null)); }); } diff --git a/x-hooks/use-transactions.ts b/x-hooks/use-transactions.ts index 60682188c0..c18eaa7b54 100644 --- a/x-hooks/use-transactions.ts +++ b/x-hooks/use-transactions.ts @@ -1,59 +1,42 @@ import { useAppState } from "contexts/app-state"; import { setTxList } from "contexts/reducers/change-tx-list"; +import { getTransactionsStorageKey } from "helpers/transactions"; + import { WinStorage } from "services/win-storage"; export function useTransactions() { - const { state, dispatch } = useAppState(); - - const getStorageKey = (walletAddress: string) => `bepro.transactions:${walletAddress}`; + const { dispatch } = useAppState(); - function checkRequirements(fn: (wallet) => void) { - const walletAddress = state.currentUser?.walletAddress; + function checkRequirements(fn: (wallet, chainId) => void) { + const walletAddress = sessionStorage.getItem("currentWallet")?.toLowerCase(); + const chainId = sessionStorage.getItem("currentChainId"); - if (!walletAddress) return; + if (!walletAddress || !chainId) return; - fn(walletAddress); + fn(walletAddress, chainId); } - function deleteFromStorage() { - checkRequirements((walletAddress) => { - const storage = new WinStorage(getStorageKey(walletAddress), 0, 'localStorage'); + checkRequirements((walletAddress, chainId) => { + const storage = new WinStorage(getTransactionsStorageKey(walletAddress, chainId), 0, 'localStorage'); storage.value = undefined; }); } function loadFromStorage() { - checkRequirements((walletAddress) => { - const storage = new WinStorage(getStorageKey(walletAddress), 0, 'localStorage'); + checkRequirements((walletAddress, chainId) => { + const storage = new WinStorage(getTransactionsStorageKey(walletAddress, chainId), 0, 'localStorage'); - if (!storage.value) return; - - const transactions = JSON.parse(storage.value); + const transactions = JSON.parse(storage.value || "[]"); dispatch(setTxList(transactions)); }); } - function saveToStorage() { - checkRequirements((walletAddress) => { - const transactions = state.transactions; - - const SEVEN_DAYS_IN_MS = 60 * 60 * 24 * 7 * 1000; - - const storage = new WinStorage(getStorageKey(walletAddress), SEVEN_DAYS_IN_MS, 'localStorage'); - - if (JSON.stringify(transactions) === JSON.stringify(storage.value)) return; - - storage.value = JSON.stringify(transactions); - }); - } - return { loadFromStorage, - saveToStorage, deleteFromStorage }; } \ No newline at end of file diff --git a/x-hooks/x-is-admin.ts b/x-hooks/x-is-admin.ts new file mode 100644 index 0000000000..df9a72cd01 --- /dev/null +++ b/x-hooks/x-is-admin.ts @@ -0,0 +1,9 @@ +import {useAppState} from "../contexts/app-state"; +import getConfig from "next/config"; + +export default function isAdmin() { + const {state} = useAppState(); + const {publicRuntimeConfig} = getConfig(); + + return state?.currentUser?.walletAddress.toLowerCase() === publicRuntimeConfig.adminWallet.toLowerCase(); +} \ No newline at end of file From 3e5bc98261053372fd3a0e9f3bdde37319abceae Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Wed, 22 Mar 2023 10:48:17 -0300 Subject: [PATCH 10/47] adding new profile page style (#923) * adding new profile page style * fix translation --- assets/icons/bounties-icon.tsx | 9 ++++ assets/icons/custom-network-icon.tsx | 9 ++++ assets/icons/payments-icon.tsx | 9 ++++ assets/icons/profile-icon.tsx | 9 ++++ assets/icons/proposals-icon.tsx | 9 ++++ assets/icons/pull-requests-icon.tsx | 9 ++++ assets/icons/voting-power-icon.tsx | 9 ++++ assets/icons/wallet-icon.tsx | 9 ++++ components/list-issues.tsx | 4 +- components/nav-avatar.tsx | 18 ++++--- components/profile/profile-layout.tsx | 2 +- components/profile/profile-side.tsx | 69 ++++++++++++++++----------- components/status-bar.tsx | 4 +- public/locales/en/common.json | 2 +- styles/styles.scss | 28 +++++++++-- styles/variables.scss | 7 ++- 16 files changed, 162 insertions(+), 44 deletions(-) create mode 100644 assets/icons/bounties-icon.tsx create mode 100644 assets/icons/custom-network-icon.tsx create mode 100644 assets/icons/payments-icon.tsx create mode 100644 assets/icons/profile-icon.tsx create mode 100644 assets/icons/proposals-icon.tsx create mode 100644 assets/icons/pull-requests-icon.tsx create mode 100644 assets/icons/voting-power-icon.tsx create mode 100644 assets/icons/wallet-icon.tsx diff --git a/assets/icons/bounties-icon.tsx b/assets/icons/bounties-icon.tsx new file mode 100644 index 0000000000..b410bb328b --- /dev/null +++ b/assets/icons/bounties-icon.tsx @@ -0,0 +1,9 @@ +import { SVGProps } from "react"; + +export default function BountiesIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/assets/icons/custom-network-icon.tsx b/assets/icons/custom-network-icon.tsx new file mode 100644 index 0000000000..6769d557c1 --- /dev/null +++ b/assets/icons/custom-network-icon.tsx @@ -0,0 +1,9 @@ +import { SVGProps } from "react"; + +export default function CustomNetworkIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/assets/icons/payments-icon.tsx b/assets/icons/payments-icon.tsx new file mode 100644 index 0000000000..ab538c4926 --- /dev/null +++ b/assets/icons/payments-icon.tsx @@ -0,0 +1,9 @@ +import { SVGProps } from "react"; + +export default function PaymentsIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/assets/icons/profile-icon.tsx b/assets/icons/profile-icon.tsx new file mode 100644 index 0000000000..c210a059ab --- /dev/null +++ b/assets/icons/profile-icon.tsx @@ -0,0 +1,9 @@ +import { SVGProps } from "react"; + +export default function ProfileIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/assets/icons/proposals-icon.tsx b/assets/icons/proposals-icon.tsx new file mode 100644 index 0000000000..0b9899d751 --- /dev/null +++ b/assets/icons/proposals-icon.tsx @@ -0,0 +1,9 @@ +import { SVGProps } from "react"; + +export default function ProposalsIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/assets/icons/pull-requests-icon.tsx b/assets/icons/pull-requests-icon.tsx new file mode 100644 index 0000000000..84a6d6b3ed --- /dev/null +++ b/assets/icons/pull-requests-icon.tsx @@ -0,0 +1,9 @@ +import { SVGProps } from "react"; + +export default function PullRequestsIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/assets/icons/voting-power-icon.tsx b/assets/icons/voting-power-icon.tsx new file mode 100644 index 0000000000..cfadd59c7c --- /dev/null +++ b/assets/icons/voting-power-icon.tsx @@ -0,0 +1,9 @@ +import { SVGProps } from "react"; + +export default function VotingPowerIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/assets/icons/wallet-icon.tsx b/assets/icons/wallet-icon.tsx new file mode 100644 index 0000000000..6d2f4c630e --- /dev/null +++ b/assets/icons/wallet-icon.tsx @@ -0,0 +1,9 @@ +import { SVGProps } from "react"; + +export default function WalletIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/components/list-issues.tsx b/components/list-issues.tsx index ccf3ca3a92..e7feaaefd1 100644 --- a/components/list-issues.tsx +++ b/components/list-issues.tsx @@ -282,7 +282,7 @@ export default function ListIssues({ return ( {allNetworks && ( @@ -295,7 +295,7 @@ export default function ListIssues({ )} {isRenderFilter() ? (
diff --git a/components/nav-avatar.tsx b/components/nav-avatar.tsx index 8e53e784c6..a1678e8738 100644 --- a/components/nav-avatar.tsx +++ b/components/nav-avatar.tsx @@ -93,12 +93,18 @@ export default function NavAvatar({ const internalLinks = [ Link(t("main-nav.nav-avatar.wallet"), getURLWithNetwork("/profile/wallet")), - Link(t("main-nav.nav-avatar.oracles"), getURLWithNetwork("/profile/bepro-votes")), - Link(t("main-nav.nav-avatar.payments"), getURLWithNetwork("/profile/payments")), - Link(t("main-nav.nav-avatar.bounties"), getURLWithNetwork("/profile/bounties")), - Link(t("main-nav.nav-avatar.pull-requests"), getURLWithNetwork("/profile/pull-requests")), - Link(t("main-nav.nav-avatar.proposals"), getURLWithNetwork("/profile/proposals")), - Link(t("main-nav.nav-avatar.my-network"), getURLWithNetwork("/profile/my-network")) + Link(t("main-nav.nav-avatar.voting-power"), + getURLWithNetwork("/profile/bepro-votes")), + Link(t("main-nav.nav-avatar.payments"), + getURLWithNetwork("/profile/payments")), + Link(t("main-nav.nav-avatar.bounties"), + getURLWithNetwork("/profile/bounties")), + Link(t("main-nav.nav-avatar.pull-requests"), + getURLWithNetwork("/profile/pull-requests")), + Link(t("main-nav.nav-avatar.proposals"), + getURLWithNetwork("/profile/proposals")), + Link(t("main-nav.nav-avatar.my-network"), + getURLWithNetwork("/profile/my-network")), ]; const externalLinks = [ diff --git a/components/profile/profile-layout.tsx b/components/profile/profile-layout.tsx index b31aca3cf5..c9f59bea7b 100644 --- a/components/profile/profile-layout.tsx +++ b/components/profile/profile-layout.tsx @@ -21,7 +21,7 @@ export default function ProfileLayout({ children }) {
-
+
{children}
diff --git a/components/profile/profile-side.tsx b/components/profile/profile-side.tsx index c77e2ae192..4d747f91e7 100644 --- a/components/profile/profile-side.tsx +++ b/components/profile/profile-side.tsx @@ -1,46 +1,61 @@ +import clsx from "clsx"; import { useTranslation } from "next-i18next"; +import Link from "next/link"; +import { useRouter } from "next/router"; -import InternalLink from "components/internal-link"; +import BountiesIcon from "assets/icons/bounties-icon"; +import CustomNetworkIcon from "assets/icons/custom-network-icon"; +import PaymentsIcon from "assets/icons/payments-icon"; +import ProfileIcon from "assets/icons/profile-icon"; +import ProposalsIcon from "assets/icons/proposals-icon"; +import PullRequestsIcon from "assets/icons/pull-requests-icon"; +import VotingPowerIcon from "assets/icons/voting-power-icon"; +import WalletIcon from "assets/icons/wallet-icon"; import { useNetwork } from "x-hooks/use-network"; export default function ProfileSide() { + const { asPath } = useRouter(); const { t } = useTranslation("common"); const { getURLWithNetwork } = useNetwork(); - const Link = (label, href) => ({ label, href }); - const ProfileLink = ({ label, href }) => ( -
  • - + const getHref = (href = "") => getURLWithNetwork(`/profile${href}`); + const getTranslation = page => t(`main-nav.nav-avatar.${page}`); + const isActive = href => href !== "" ? asPath.endsWith(href) : asPath.endsWith("/profile") + + const ProfileLink = ({ label, href, icon }) => ( +
  • + + + {icon} + {getTranslation(label)} + +
  • - ); + ); + + const getLink = (label, href, icon) => ({ label, href, icon }); const links = [ - Link(t("main-nav.nav-avatar.profile"), getURLWithNetwork("/profile")), - Link(t("main-nav.nav-avatar.wallet"), getURLWithNetwork("/profile/wallet")), - Link(t("main-nav.nav-avatar.oracles"), - getURLWithNetwork("/profile/bepro-votes")), - Link(t("main-nav.nav-avatar.payments"), - getURLWithNetwork("/profile/payments")), - Link(t("main-nav.nav-avatar.bounties"), - getURLWithNetwork("/profile/bounties")), - Link(t("main-nav.nav-avatar.pull-requests"), - getURLWithNetwork("/profile/pull-requests")), - Link(t("main-nav.nav-avatar.proposals"), - getURLWithNetwork("/profile/proposals")), - Link(t("main-nav.nav-avatar.my-network"), - getURLWithNetwork("/profile/my-network")), + getLink("profile", "", ), + getLink("wallet", "/wallet", ), + getLink("voting-power", "/bepro-votes", ), + getLink("payments", "/payments", ), + getLink("bounties", "/bounties", ), + getLink("pull-requests", "/pull-requests", ), + getLink("proposals", "/proposals", ), + getLink("my-network", "/my-network", ), ]; return( -