From e902c8ea371b0567a0d0ab789e5c41c3bde9e22b Mon Sep 17 00:00:00 2001 From: Hrik Bhowal Date: Fri, 3 Mar 2023 01:47:20 -0500 Subject: [PATCH 1/2] using @web3react/core for wallet sign-in --- client/components/AccountStatusDropdown.tsx | 168 + client/components/Address.tsx | 12 +- client/components/AdminUtils.js | 6 +- .../{claim/claim => }/ApyTooltip.tsx | 0 client/components/ConnectButton.tsx | 73 + client/components/Dropdown.tsx | 111 + client/components/Header.tsx | 4 +- client/components/LedgerAccountContent.tsx | 100 + client/components/LedgerDerivationContent.tsx | 379 + client/components/WalletSelectContent.tsx | 237 + client/components/WalletSelectModal.tsx | 47 + client/components/Web3Button.tsx | 238 - client/components/_AccountStatusIndicator.tsx | 100 + client/components/claim/Claim.tsx | 60 - client/components/claim/Education.tsx | 71 - client/components/claim/Eligibility.tsx | 313 - client/components/claim/EligibilityItem.tsx | 72 - client/components/claim/claim/ClaimOgv.tsx | 325 - client/components/claim/claim/ClaimVeOgv.tsx | 287 - .../components/claim/claim/PostClaimModal.tsx | 163 - client/components/claim/education/Ogn.tsx | 90 - client/components/claim/education/Ogv.tsx | 89 - client/components/claim/education/Ousd.tsx | 101 - client/components/claim/education/Quiz.tsx | 226 - client/components/layout.tsx | 2 + .../proposal/EnsureDelegationModal.tsx | 10 +- client/components/proposal/ProposalDetail.tsx | 10 +- .../components/proposal/ProposalHistory.tsx | 6 +- client/components/proposal/Reallocation.tsx | 8 +- client/components/proposal/RegisterToVote.tsx | 10 +- client/components/vote-escrow/LockupForm.tsx | 57 +- .../components/vote-escrow/LockupsTable.tsx | 9 +- client/components/vote-escrow/YourLockups.tsx | 16 +- client/constants/index.ts | 3 + client/hoc/index.ts | 3 + client/hoc/withIsMobile.tsx | 52 + client/hoc/withWalletSelectModal.tsx | 23 + client/hoc/withWeb3Provider.tsx | 35 + client/package.json | 26 + client/pages/_app.tsx | 15 +- client/pages/_document.tsx | 16 + client/pages/claim.tsx | 12 - client/pages/merkle-test.tsx | 3 +- client/pages/proposals/new.tsx | 9 +- client/pages/stake/[id].tsx | 4 +- client/public/arrow-down.png | Bin 0 -> 666 bytes client/public/arrow-up.png | Bin 0 -> 631 bytes client/public/coinbasewallet-icon.svg | 21 + client/public/defiwallet-icon.png | Bin 0 -> 40485 bytes client/public/exodus-icon.svg | 33 + client/public/ledger-icon.svg | 3 + client/public/metamask-icon.svg | 142 + client/public/spinner-green-small.png | Bin 0 -> 1111 bytes client/public/walletconnect-icon.svg | 11 + client/utils/LedgerConnector.ts | 137 + client/utils/account.ts | 60 + client/utils/connectors.tsx | 80 + client/utils/device.ts | 24 + client/utils/image.ts | 5 + client/utils/index.tsx | 3 +- client/utils/shortenAddress.ts | 11 + client/utils/{store.tsx => store.ts} | 49 +- client/utils/useAccountBalances.js | 4 +- client/utils/useClaim.tsx | 32 +- client/utils/useConnectSigner.js | 9 - client/utils/useContracts.tsx | 24 +- client/utils/useGovernance.tsx | 4 +- client/utils/useLockups.tsx | 4 +- client/utils/useOverrideAccount.ts | 20 + client/utils/useShowDelegationModalOption.tsx | 3 +- client/utils/web3.ts | 116 + client/yarn.lock | 19141 +++++++++------- 72 files changed, 12840 insertions(+), 10697 deletions(-) create mode 100644 client/components/AccountStatusDropdown.tsx rename client/components/{claim/claim => }/ApyTooltip.tsx (100%) create mode 100644 client/components/ConnectButton.tsx create mode 100644 client/components/Dropdown.tsx create mode 100644 client/components/LedgerAccountContent.tsx create mode 100644 client/components/LedgerDerivationContent.tsx create mode 100644 client/components/WalletSelectContent.tsx create mode 100644 client/components/WalletSelectModal.tsx delete mode 100644 client/components/Web3Button.tsx create mode 100644 client/components/_AccountStatusIndicator.tsx delete mode 100644 client/components/claim/Claim.tsx delete mode 100644 client/components/claim/Education.tsx delete mode 100644 client/components/claim/Eligibility.tsx delete mode 100644 client/components/claim/EligibilityItem.tsx delete mode 100644 client/components/claim/claim/ClaimOgv.tsx delete mode 100644 client/components/claim/claim/ClaimVeOgv.tsx delete mode 100644 client/components/claim/claim/PostClaimModal.tsx delete mode 100644 client/components/claim/education/Ogn.tsx delete mode 100644 client/components/claim/education/Ogv.tsx delete mode 100644 client/components/claim/education/Ousd.tsx delete mode 100644 client/components/claim/education/Quiz.tsx create mode 100644 client/hoc/index.ts create mode 100644 client/hoc/withIsMobile.tsx create mode 100644 client/hoc/withWalletSelectModal.tsx create mode 100644 client/hoc/withWeb3Provider.tsx create mode 100644 client/public/arrow-down.png create mode 100644 client/public/arrow-up.png create mode 100644 client/public/coinbasewallet-icon.svg create mode 100644 client/public/defiwallet-icon.png create mode 100644 client/public/exodus-icon.svg create mode 100644 client/public/ledger-icon.svg create mode 100644 client/public/metamask-icon.svg create mode 100644 client/public/spinner-green-small.png create mode 100644 client/public/walletconnect-icon.svg create mode 100644 client/utils/LedgerConnector.ts create mode 100644 client/utils/account.ts create mode 100644 client/utils/connectors.tsx create mode 100644 client/utils/device.ts create mode 100644 client/utils/image.ts create mode 100644 client/utils/shortenAddress.ts rename client/utils/{store.tsx => store.ts} (56%) delete mode 100644 client/utils/useConnectSigner.js create mode 100644 client/utils/useOverrideAccount.ts create mode 100644 client/utils/web3.ts diff --git a/client/components/AccountStatusDropdown.tsx b/client/components/AccountStatusDropdown.tsx new file mode 100644 index 00000000..83221ee3 --- /dev/null +++ b/client/components/AccountStatusDropdown.tsx @@ -0,0 +1,168 @@ +import React, { useEffect, useState } from "react"; +import { useWeb3React } from "@web3-react/core"; + +import Dropdown from "components/Dropdown"; +import ConnectButton from "@/components/ConnectButton"; +import { isCorrectNetwork, switchEthereumChain } from "utils/web3"; + +import withWalletSelectModal from "hoc/withWalletSelectModal"; + +import AccountStatusIndicator from "./_AccountStatusIndicator"; +import Link from "./Link"; +import useShowDelegationModalOption from "utils/useShowDelegationModalOption"; +import { useStore } from "utils/store"; +import { providers } from "ethers"; + +interface AccountStatusDropdownProps { + className?: string; + showLogin: () => void; +} + +const AccountStatusDropdown = ({ + className, + showLogin, +}: AccountStatusDropdownProps) => { + const { active, account, chainId, deactivate, library } = useWeb3React(); + const [open, setOpen] = useState(false); + const correctNetwork = isCorrectNetwork(chainId!); + const resetWeb3State = useStore((state) => state.reset); + + const { needToShowDelegation } = useShowDelegationModalOption(); + + const disconnect = () => { + setOpen(false); + deactivate(); + resetWeb3State(); + }; + + return ( + <> + + + {needToShowDelegation && ( + + Vote register + + )} + + } + open={open} + onClose={() => setOpen(false)} + > + { + e.preventDefault(); + if (!active) { + showLogin(); + } else if (active && !correctNetwork) { + // open the dropdown to allow disconnecting, while also requesting an auto switch to mainnet + await switchEthereumChain(); + setOpen(true); + } else { + setOpen(true); + } + }} + > + {/* The button id is used by StakeBoxBig to trigger connect when no wallet connected */} + {!active && ( + + )} + {active && ( + + )} + + + + + ); +}; + +export default withWalletSelectModal(AccountStatusDropdown); diff --git a/client/components/Address.tsx b/client/components/Address.tsx index 0dab6c43..dd0b69c3 100644 --- a/client/components/Address.tsx +++ b/client/components/Address.tsx @@ -11,7 +11,7 @@ export const Address = ({ address: string; noTruncate?: Boolean; }) => { - const { rpcProvider } = useStore(); + const { web3Provider } = useStore(); const [addressDisplay, setAddressDisplay] = useState( noTruncate ? address : truncateEthAddress(address) ); @@ -19,21 +19,21 @@ export const Address = ({ useEffect(() => { const loadEns = async () => { try { - const ens = await rpcProvider.lookupAddress(address); + const ens = await web3Provider.lookupAddress(address); if (ens) { setAddressDisplay(ens); } } catch (error) {} }; - if (rpcProvider) { + if (web3Provider) { loadEns(); } - }, [rpcProvider, address]); + }, [web3Provider, address]); let explorerPrefix; - if (rpcProvider?._network?.chainId === 1) { + if (web3Provider?._network?.chainId === 1) { explorerPrefix = "https://etherscan.io/"; - } else if (rpcProvider?._network?.chainId === 5) { + } else if (web3Provider?._network?.chainId === 5) { explorerPrefix = "https://goerli.etherscan.io/"; } diff --git a/client/components/AdminUtils.js b/client/components/AdminUtils.js index 690ec0f4..b26231b0 100644 --- a/client/components/AdminUtils.js +++ b/client/components/AdminUtils.js @@ -1,9 +1,11 @@ import { useMemo } from "react"; import { useStore } from "utils/store"; import { ZERO_ADDRESS } from "constants/index"; +import { useWeb3React } from "@web3-react/core"; const AdminUtils = () => { const { contracts } = useStore(); + const { library, account } = useWeb3React(); const show = useMemo(() => { if (process.browser) { @@ -33,7 +35,9 @@ const AdminUtils = () => { }; const resetDelegation = async () => { - await contracts.OgvStaking.delegate(ZERO_ADDRESS); + await contracts.OgvStaking.connect(library.getSigner(account)).delegate( + ZERO_ADDRESS + ); }; const buttonClass = "px-2 py-1 my-1 border border-black rounded-md"; diff --git a/client/components/claim/claim/ApyTooltip.tsx b/client/components/ApyTooltip.tsx similarity index 100% rename from client/components/claim/claim/ApyTooltip.tsx rename to client/components/ApyTooltip.tsx diff --git a/client/components/ConnectButton.tsx b/client/components/ConnectButton.tsx new file mode 100644 index 00000000..2d306ec2 --- /dev/null +++ b/client/components/ConnectButton.tsx @@ -0,0 +1,73 @@ +import React, { CSSProperties, useState, useEffect } from "react"; +import classnames from "classnames"; +//@ts-ignore +import { fbt } from "fbt-runtime"; +import { useWeb3React } from "@web3-react/core"; +import withWalletSelectModal from "../hoc/withWalletSelectModal"; +import { walletLogin } from "../utils/account"; +import classNames from "classnames"; + +interface ConnectButtonProps { + id?: string; + style?: CSSProperties; + trackSource?: string; + inPage?: boolean; + showLogin: () => void; +} + +const ConnectButton = ({ + id, + style, + trackSource, + inPage, + showLogin, +}: // trackSource, +ConnectButtonProps) => { + const { activate, active } = useWeb3React(); + const [userAlreadyConnectedWallet, setUserAlreadyConnectedWallet] = + useState(false); + + const defaultClassName = classNames("", { + "btn btn-outline btn-sm border-[#bbc9da] text-white rounded-full text-sm capitalize font-normal hover:bg-white hover:text-secondary": + !inPage, + "btn btn-primary btn-lg rounded-full w-full h-[3.25rem] min-h-[3.25rem]": + inPage, + }); + + useEffect(() => { + if ( + !userAlreadyConnectedWallet && + localStorage.getItem("userConnectedWallet") === "true" + ) { + setUserAlreadyConnectedWallet(true); + } + + if (!userAlreadyConnectedWallet && active) { + localStorage.setItem("userConnectedWallet", "true"); + } + }, [active]); + + return ( + <> + + + ); +}; + +export default withWalletSelectModal(ConnectButton); diff --git a/client/components/Dropdown.tsx b/client/components/Dropdown.tsx new file mode 100644 index 00000000..c41d5212 --- /dev/null +++ b/client/components/Dropdown.tsx @@ -0,0 +1,111 @@ +//@ts-nocheck +import React, { Component, ReactElement } from "react"; + +interface DropdownProps { + className: string; + content: ReactElement; + open: boolean; + onClose: () => void; +} + +class Dropdown extends Component { + constructor(props) { + super(props); + this.onBlur = this.onBlur.bind(this); + this.state = { + open: false, + }; + } + + componentDidMount() { + if (this.props.open) { + this.doOpen(); + } + } + + componentWillUnmount() { + if (this.state.open) { + this.doClose(); + } + } + + componentDidUpdate(prevProps) { + if (prevProps.open === this.props.open) { + return; + } + + if (prevProps.open && !this.props.open) { + // Should close + this.doClose(); + } else if (!prevProps.open && this.props.open) { + // Should open + this.doOpen(); + } + } + + onBlur() { + if (!this.mouseOver) { + this.doClose(); + } + } + + doOpen() { + if (this.state.open) { + return; + } + + document.addEventListener("click", this.onBlur); + + this.setState({ open: true }); + + setTimeout( + () => this.dropdownEl && this.dropdownEl.classList.add("show"), + 10 + ); + } + + doClose() { + if (!this.state.open || this.state.closing) { + return; + } + + document.removeEventListener("click", this.onBlur); + + this.dropdownEl.classList.remove("show"); + if (this.props.onClose) { + if (this.props.animateOnExit) { + this.setState({ closing: true }); + + this.onCloseTimeout = setTimeout(() => { + this.setState({ open: false, closing: false }); + this.props.onClose(); + }, 300); + return; + } + + this.props.onClose(); + } + + this.setState({ open: false }); + } + + render() { + let className = "dropdown"; + if (this.props.className) className += ` ${this.props.className}`; + const El = this.props.el || "div"; + + return ( + (this.dropdownEl = ref)} + className={className} + onMouseOver={() => (this.mouseOver = true)} + onMouseOut={() => (this.mouseOver = false)} + > + {this.props.children} + {this.props.content && this.state.open ? this.props.content : null} + + ); + } +} + +export default Dropdown; diff --git a/client/components/Header.tsx b/client/components/Header.tsx index 207faa28..a2ad5357 100644 --- a/client/components/Header.tsx +++ b/client/components/Header.tsx @@ -1,12 +1,12 @@ import { FunctionComponent, useState } from "react"; import classNames from "classnames"; -import { Web3Button } from "components/Web3Button"; import Wrapper from "components/Wrapper"; import Link from "components/Link"; import Image from "next/image"; import { navItems } from "../constants"; import useClaim from "utils/useClaim"; import { getRewardsApy } from "utils/apy"; +import AccountStatusDropdown from "./AccountStatusDropdown"; interface HeaderProps { hideNav?: boolean; @@ -103,7 +103,7 @@ const Header: FunctionComponent = ({ hideNav }) => { alt="Open Menu" /> - + )} diff --git a/client/components/LedgerAccountContent.tsx b/client/components/LedgerAccountContent.tsx new file mode 100644 index 00000000..5f887f2c --- /dev/null +++ b/client/components/LedgerAccountContent.tsx @@ -0,0 +1,100 @@ +import React from "react"; +import { useWeb3React } from "@web3-react/core"; +import { ledgerConnector } from "utils/connectors"; +import { useStore } from "utils/store"; +import { shortenAddress } from "utils/shortenAddress"; + +interface LedgerAccountContentProps { + addresses: string[]; + addressBalances: { [key: string]: number }; + activePath: string; + next: boolean; +} + +const LedgerAccountContent = ({ + addresses, + addressBalances, + activePath, + next, +}: LedgerAccountContentProps) => { + const { activate } = useWeb3React(); + + const onSelectAddress = async (address: string, i: number) => { + const n = next ? i + 5 : i; + const path = activePath === "44'/60'/0'/0" ? `44'/60'/${n}'/0` : activePath; + await ledgerConnector.setPath(path); + ledgerConnector.setAccount(address); + + await activate( + ledgerConnector, + (err) => { + console.error(err); + }, + // Do not throw the error, handle it in the onError callback above + false + ); + + localStorage.setItem("eagerConnect", "Ledger"); + localStorage.setItem("ledgerAccount", address); + localStorage.setItem( + "ledgerDerivationPath", + ledgerConnector.getBaseDerivationPath()! + ); + + useStore.setState({ + walletSelectModalState: false, + connectorName: "Ledger", + }); + }; + + return ( + <> +
{ + e.stopPropagation(); + }} + className={`flex flex-col`} + > + {addresses.map((address, i) => { + return ( + + ); + })} +
+ + + ); +}; + +export default LedgerAccountContent; diff --git a/client/components/LedgerDerivationContent.tsx b/client/components/LedgerDerivationContent.tsx new file mode 100644 index 00000000..565cc5b8 --- /dev/null +++ b/client/components/LedgerDerivationContent.tsx @@ -0,0 +1,379 @@ +import React, { useState, useEffect } from "react"; +import { fbt } from "fbt-runtime"; +import { ledgerConnector } from "utils/connectors"; +import LedgerAccountContent from "./LedgerAccountContent"; +import { assetRootPath } from "utils/image"; +import { zipObject } from "lodash"; + +const LEDGER_OTHER = "44'/60'/0'/0'"; +const LEDGER_LEGACY_BASE_PATH = "44'/60'/0'"; +const LEDGER_LIVE_BASE_PATH = "44'/60'/0'/0"; + +const LedgerDerivationContent = ({}) => { + const [displayError, setDisplayError] = useState(null); + const [ledgerPath, setLedgerPath] = useState({}); + const [addresses, setAddresses] = useState({}); + const [addressBalances, setAddressBalances] = useState({}); + const [pathTotals, setPathTotals] = useState({}); + const [ready, setReady] = useState(false); + const [preloaded, setPreloaded] = useState(false); + const [activePath, setActivePath] = useState(); + const [next, setNext] = useState({}); + const [nextLoading, setNextLoading] = useState({}); + + const errorMessageMap = (error: Error) => { + if (!error || !error.message) { + return "Unknown error"; + } + if ( + error.message.includes("Ledger device: UNKNOWN_ERROR") || + error.message.includes( + "Failed to sign with Ledger device: U2F DEVICE_INELIGIBLE" + ) + ) { + return "Unlock your Ledger wallet and open the Ethereum application"; /*fbt( + "Unlock your Ledger wallet and open the Ethereum application", + "Unlock ledger" + );*/ + } else if (error.message.includes("MULTIPLE_OPEN_CONNECTIONS_DISALLOWED")) { + return "Unexpected error occurred. Please refresh page and try again." /*fbt( + "Unexpected error occurred. Please refresh page and try again.", + "Unexpected login error" + );*/; + } + return error.message; + }; + + const options = [ + { + display: `Ledger Live`, + path: LEDGER_LIVE_BASE_PATH, + }, + { + display: `Legacy`, + path: LEDGER_LEGACY_BASE_PATH, + }, + { + display: `Ethereum`, + path: LEDGER_OTHER, + }, + ]; + + const loadBalances = async (path) => { + if (!(ledgerConnector.provider && addresses[path])) { + return; + } + const [balances] = await Promise.all([ + Promise.all( + addresses[path].map((a) => + ledgerConnector + .getBalance(a) + .then((r) => (Number(r) / 10 ** 18).toFixed(2)) + ) + ), + ]); + const ethTotal = balances + .map((balance) => Number(balance)) + .reduce((a, b) => a + b, 0); + setAddressBalances({ + ...addressBalances, + [path]: zipObject(addresses[path], balances), + }); + setPathTotals({ ...pathTotals, [path]: ethTotal }); + // preload addresses for each path + if (!ledgerPath[LEDGER_LIVE_BASE_PATH]) { + setLedgerPath({ ...ledgerPath, [LEDGER_LIVE_BASE_PATH]: true }); + onSelectDerivationPath(LEDGER_LIVE_BASE_PATH); + } else if (!ledgerPath[LEDGER_LEGACY_BASE_PATH]) { + setLedgerPath({ ...ledgerPath, [LEDGER_LEGACY_BASE_PATH]: true }); + onSelectDerivationPath(LEDGER_LEGACY_BASE_PATH); + } else if (!ledgerPath[LEDGER_OTHER]) { + setLedgerPath({ ...ledgerPath, [LEDGER_OTHER]: true }); + onSelectDerivationPath(LEDGER_OTHER); + } else if (!activePath) { + // autoselect first path with non-zero ETH balance + if (pathTotals[LEDGER_LIVE_BASE_PATH] > 0) { + setActivePath(LEDGER_LIVE_BASE_PATH); + } else if (pathTotals[LEDGER_LEGACY_BASE_PATH] > 0) { + setActivePath(LEDGER_LEGACY_BASE_PATH); + } else if (ethTotal > 0) { + setActivePath(LEDGER_OTHER); + } else setActivePath(LEDGER_LIVE_BASE_PATH); + } + setPreloaded( + ledgerPath[LEDGER_LIVE_BASE_PATH] && + ledgerPath[LEDGER_LEGACY_BASE_PATH] && + ledgerPath[LEDGER_OTHER] + ); + // indicators for scrolling to next address page within path + setNext({ + [activePath]: nextLoading[activePath] ? !next[activePath] : false, + }); + setNextLoading({ [activePath]: false }); + }; + + useEffect(() => { + if (ready) loadBalances(LEDGER_LIVE_BASE_PATH); + }, [addresses[LEDGER_LIVE_BASE_PATH]]); + + useEffect(() => { + if (ready) loadBalances(LEDGER_LEGACY_BASE_PATH); + }, [addresses[LEDGER_LEGACY_BASE_PATH]]); + + useEffect(() => { + if (ready) loadBalances(LEDGER_OTHER); + }, [addresses[LEDGER_OTHER]]); + + const loadAddresses = async (path, next) => { + if (!ledgerConnector.provider) { + return; + } + + setLedgerPath({ ...ledgerPath, [path]: true }); + if (next) { + setAddresses({ + ...addresses, + [path]: (path === LEDGER_LIVE_BASE_PATH + ? await ledgerConnector.getLedgerLiveAccounts(10) + : await ledgerConnector.getAccounts(10) + ).slice(5), + }); + } else { + setAddresses({ + ...addresses, + [path]: + path === LEDGER_LIVE_BASE_PATH + ? await ledgerConnector.getLedgerLiveAccounts(5) + : await ledgerConnector.getAccounts(5), + }); + } + }; + + useEffect(() => { + onSelectDerivationPath(LEDGER_LIVE_BASE_PATH); + setReady(true); + }, []); + + const onSelectDerivationPath = async (path, next) => { + try { + await ledgerConnector.activate(); + await ledgerConnector.setPath(path); + setDisplayError(null); + } catch (error) { + setDisplayError(errorMessageMap(error as Error)); + return; + } + loadAddresses(path, next); + }; + + return ( + <> +
{ + e.stopPropagation(); + }} + className={`ledger-derivation-content flex flex-col`} + > +

+ Select a Ledger derivation path + {/* {fbt( + "Select a Ledger derivation path", + "Select a Ledger derivation path" + )} */} +

+
+ {options.map((option) => { + return ( + + ); + })} +
+ {displayError && ( +
+ {displayError} +
+ )} +
+ {activePath && preloaded ? ( + <> + {nextLoading[activePath] && ( + + )} + {!nextLoading[activePath] && ( + + )} + {!next[activePath] && !nextLoading[activePath] && ( + + )} + {next[activePath] && !nextLoading[activePath] && ( + + )} + + ) : ( + !displayError && ( + + ) + )} +
+
+ + + ); +}; + +export default LedgerDerivationContent; diff --git a/client/components/WalletSelectContent.tsx b/client/components/WalletSelectContent.tsx new file mode 100644 index 00000000..17b13a89 --- /dev/null +++ b/client/components/WalletSelectContent.tsx @@ -0,0 +1,237 @@ +import React, { useState, useEffect } from "react"; + +//@ts-ignore +import { fbt } from "fbt-runtime"; +import { useWeb3React } from "@web3-react/core"; +import { injectedConnector } from "utils/connectors"; +import { walletConnectConnector } from "utils/connectors"; +import { myEtherWalletConnector } from "utils/connectors"; +import { walletlink, resetWalletConnector } from "utils/connectors"; +import { defiWalletConnector } from "utils/connectors"; +import { AbstractConnector } from "@web3-react/abstract-connector"; +import withIsMobile from "hoc/withIsMobile"; + +import { useStore } from "utils/store"; + +import { assetRootPath } from "utils/image"; +import { WalletConnectConnector } from "@web3-react/walletconnect-connector"; + +const WalletSelectContent = ({ isMobile }: { isMobile: boolean }) => { + const { connector, activate, deactivate, active } = useWeb3React(); + const [error, setError] = useState(null); + const wallets = isMobile + ? [ + "WalletConnect", + "Coinbase Wallet", + "MetaMask", + "MyEtherWallet", + "Ledger", + ] + : [ + "MetaMask", + "Ledger", + "Exodus", + "Coinbase Wallet", + "WalletConnect", + "MyEtherWallet", + "DeFi Wallet", + ]; + + useEffect(() => { + if (active) { + closeWalletSelectModal(); + } + }, [active]); + + const closeWalletSelectModal = () => { + useStore.setState({ walletSelectModalState: false }); + }; + + const errorMessageMap = (error: string | Error) => { + if (error === "ledger-error") { + return "Please use WalletConnect to connect to Ledger Live"; + // fbt( + // "Please use WalletConnect to connect to Ledger Live", + // "No Ledger on mobile" + // ); + } + if ( + error instanceof Error && + error.message.includes( + "No Ethereum provider was found on window.ethereum" + ) + ) { + return "No Ethereum wallet detected"; + // fbt("No Ethereum wallet detected", "No wallet detected"); + } + return (error as Error).message; + }; + + const onConnect = async (name: string) => { + setError(null); + + let connector: AbstractConnector; + if (name === "MetaMask" || name === "Exodus") { + connector = injectedConnector; + localStorage.setItem("eagerConnect", name); + } else if (name === "Ledger") { + // Display window with derivation path select + + useStore.setState({ walletSelectModalState: "LedgerDerivation" }); + + return; + } else if (name === "MyEtherWallet") { + connector = myEtherWalletConnector; + } else if (name === "WalletConnect") { + connector = walletConnectConnector; + } else if (name === "Coinbase Wallet") { + connector = walletlink; + } else if (name === "DeFi Wallet") { + //@ts-ignore, will only be undefined on server side + connector = defiWalletConnector; + } + // fix wallet connect bug: if you click the button and close the modal you wouldn't be able to open it again + if (name === "WalletConnect") { + resetWalletConnector(connector! as WalletConnectConnector); + } + + await activate( + connector!, + (err) => { + setError(err); + }, + // Do not throw the error, handle it in the onError callback above + false + ); + + useStore.setState({ connectorName: name }); + }; + + return ( + <> +
{ + e.stopPropagation(); + }} + className={`wallet-select-content flex flex-col`} + > +

+ Connect a wallet to get started + {/* {fbt( + "Connect a wallet to get started", + "Connect a wallet to get started" + )} */} +

+ {wallets.map((name) => { + return ( + + ); + })} + {error && ( +
+ {errorMessageMap(error)} +
+ )} +
+ + + ); +}; + +export default withIsMobile(WalletSelectContent); diff --git a/client/components/WalletSelectModal.tsx b/client/components/WalletSelectModal.tsx new file mode 100644 index 00000000..fb06ca8d --- /dev/null +++ b/client/components/WalletSelectModal.tsx @@ -0,0 +1,47 @@ +import React, { useState } from "react"; +import { fbt } from "fbt-runtime"; +import { useStore } from "utils/store"; + +import WalletSelectContent from "components/WalletSelectContent"; +import LedgerDerivationContent from "components/LedgerDerivationContent"; + +const WalletSelectModal = ({}) => { + const { walletSelectModalState: modalState } = useStore(); + + const close = () => { + useStore.setState({ walletSelectModalState: false }); + }; + + return ( + <> + {modalState && ( + // absolute top-0 left-0 bottom-0 right-0 w-full h-full z-[1000] +
{ + e.preventDefault(); + close(); + }} + > + {modalState === "Wallet" && } + {modalState === "LedgerDerivation" && } +
+ )} + + + ); +}; + +export default WalletSelectModal; diff --git a/client/components/Web3Button.tsx b/client/components/Web3Button.tsx deleted file mode 100644 index 24c44c02..00000000 --- a/client/components/Web3Button.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import WalletConnectProvider from "@walletconnect/web3-provider"; -import { MewConnectConnector } from "@myetherwallet/mewconnect-connector"; -import { providers, utils } from "ethers"; -import { useCallback, useEffect, FunctionComponent } from "react"; -import WalletLink from "walletlink"; -import Web3Modal from "web3modal"; -import { truncateEthAddress, useNetworkInfo } from "utils/index"; -import { useStore } from "utils/store"; -import { - RPC_URLS, - mainnetNetworkUrl, - websocketProvider, -} from "constants/index"; -import { toast } from "react-toastify"; -import classNames from "classnames"; -import Link from "components/Link"; -import useShowDelegationModalOption from "utils/useShowDelegationModalOption"; - -const providerOptions = { - walletconnect: { - package: WalletConnectProvider, // required - options: { - rpc: RPC_URLS, - }, - }, - "custom-walletlink": { - display: { - logo: "https://play-lh.googleusercontent.com/PjoJoG27miSglVBXoXrxBSLveV6e3EeBPpNY55aiUUBM9Q1RCETKCOqdOkX2ZydqVf0", - name: "Coinbase", - description: "Connect to Coinbase Wallet (not Coinbase App)", - }, - options: { - appName: "Coinbase", // Your app name - networkUrl: mainnetNetworkUrl, - chainId: 1, - }, - package: WalletLink, - connector: async (_: any, options: any) => { - const { appName, networkUrl, chainId } = options; - const walletLink = new WalletLink({ - appName, - }); - const provider = walletLink.makeWeb3Provider(networkUrl, chainId); - await provider.enable(); - return provider; - }, - }, - "custom-mew": { - display: { - logo: "/myetherwallet-icon.svg", - name: "MyEtherWallet", - description: "Connect to your MyEtherWallet", - }, - package: MewConnectConnector, - options: { - appName: "MyEtherWallet", // Your app name - networkUrl: mainnetNetworkUrl, - chainId: 1, - }, - connector: async () => { - const connector = new MewConnectConnector({ - url: websocketProvider, - }); - await connector.activate(); - const provider = await connector.getProvider(); - provider.enable(); - return provider; - }, - }, -}; - -const networkNameMap = { - 1: "Mainnet", - 5: "Goerli", - 31337: "localhost", -}; - -let web3Modal: any | undefined; -if (typeof window !== "undefined") { - web3Modal = new Web3Modal({ - network: "mainnet", - cacheProvider: true, - providerOptions, - }); - useStore.setState({ web3Modal }); -} - -interface Web3ButtonProps { - inPage?: Boolean; -} - -export const Web3Button: FunctionComponent = ({ inPage }) => { - const { provider, web3Provider, address } = useStore(); - - const resetWeb3State = useStore((state) => state.reset); - - const networkInfo = useNetworkInfo(); - - const { needToShowDelegation } = useShowDelegationModalOption(); - - const connect = useCallback( - async function (isAutoconnect = false) { - if (!isAutoconnect) { - await web3Modal.clearCachedProvider(); - } - let provider; - try { - provider = await web3Modal.connect(); - } catch (e) { - console.warn("Connection error:", e); - return; - } - - provider.on("chainChanged", (chainId) => { - useStore.setState({ chainId: Number(chainId) }); - }); - - const web3Provider = new providers.Web3Provider(provider, "any"); - const signer = web3Provider.getSigner(); - const address = await signer.getAddress(); - const network = await web3Provider.getNetwork(); - - provider.on("accountsChanged", async (accounts) => { - const newAccount: string = - accounts.length === 0 ? undefined : accounts[0]; - let storeUpdate = { - address: utils.getAddress(newAccount), // ensure checksum address to prevent excess state updates - }; - resetWeb3State(); - - if (newAccount !== undefined) { - storeUpdate = { - ...storeUpdate, - provider, - web3Provider, - chainId: network.chainId, - }; - } - useStore.setState(storeUpdate); - }); - - // Add contracts - useStore.setState({ - provider, - web3Provider, - address, - chainId: network.chainId, - }); - }, - [resetWeb3State] - ); - - const disconnect = useCallback( - async function () { - await web3Modal.clearCachedProvider(); - if (provider?.disconnect && typeof provider.disconnect === "function") { - await provider.disconnect(); - } - resetWeb3State(); - }, - [provider, resetWeb3State] - ); - - // Auto connect to the cached provider - useEffect(() => { - if (web3Modal.cachedProvider) { - connect(true); - } - }, [connect]); - - if (web3Provider && !networkInfo.correct) { - return ( - - ); - } - - const defaultClassName = classNames("", { - "btn btn-outline btn-sm border-[#bbc9da] text-white rounded-full text-sm capitalize font-normal hover:bg-white hover:text-secondary": - !inPage, - "btn btn-primary btn-lg rounded-full w-full h-[3.25rem] min-h-[3.25rem]": - inPage, - }); - - return web3Provider ? ( -
- {address && ( - - )} -
- - {needToShowDelegation && ( - - Vote register - - )} -
-
- ) : ( - - ); -}; diff --git a/client/components/_AccountStatusIndicator.tsx b/client/components/_AccountStatusIndicator.tsx new file mode 100644 index 00000000..e6e8311b --- /dev/null +++ b/client/components/_AccountStatusIndicator.tsx @@ -0,0 +1,100 @@ +import React from "react"; +//@ts-ignore +import { fbt } from "fbt-runtime"; +import { shortenAddress } from "utils/web3"; + +import { useOverrideAccount } from "utils/useOverrideAccount"; +import { truncateEthAddress } from "utils"; +import { useStore } from "utils/store"; +import { useWeb3React } from "@web3-react/core"; + +interface AccountStatusIndicatorProps { + account?: string | null; + correctNetwork: boolean; +} + +const AccountStatusIndicator = ({ + account, + correctNetwork, +}: AccountStatusIndicatorProps) => { + const { account: address } = useWeb3React(); + const { overrideAccount } = useOverrideAccount(); + const { web3Provider } = useStore(); + + return ( + <> + {overrideAccount && ( + <> +
+ { +
+ {`${"readonly" /*fbt("readonly", "readonly")*/}: ${shortenAddress( + overrideAccount as string, + true + )}`} +
+ } + + )} + {!correctNetwork && !overrideAccount && ( + <> +
+ {address && ( +
+ Wrong network + {/* {fbt("Wrong network", "Wrong network")} */} +
+ )} + + )} + {correctNetwork && !overrideAccount && ( + <> + {web3Provider && web3Provider._network && ( + + )} + + )} + + + ); +}; + +export default AccountStatusIndicator; diff --git a/client/components/claim/Claim.tsx b/client/components/claim/Claim.tsx deleted file mode 100644 index cfa92976..00000000 --- a/client/components/claim/Claim.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { FunctionComponent, useEffect, useState } from "react"; -import { SectionTitle } from "components/SectionTitle"; -import Wrapper from "components/Wrapper"; -import Card from "components/Card"; -import CardGroup from "components/CardGroup"; -import Button from "components/Button"; -import { useStore } from "utils/store"; -import { Web3Button } from "components/Web3Button"; -import OgvTotalStats from "components/OgvTotalStats"; -import ClaimOgv from "components/claim/claim/ClaimOgv"; -import ClaimVeOgv from "components/claim/claim/ClaimVeOgv"; -import useClaim from "utils/useClaim"; -import useHistoricalLockupToasts from "utils/useHistoricalLockupToasts"; - -interface ClaimProps { - handlePrevStep: () => void; -} - -const Claim: FunctionComponent = ({ handlePrevStep }) => { - const { web3Provider, contracts } = useStore(); - const { hasClaim } = useClaim(); - - useHistoricalLockupToasts(); - - if (!web3Provider) { - return ( - - - Please connect your wallet to claim - - - - ); - } - - if (!hasClaim) { - return ( - - - - Unfortunately, you're not eligible to claim. - -

- Try connecting another wallet. -

-
-
- ); - } - - return ( - - - - - - ); -}; - -export default Claim; diff --git a/client/components/claim/Education.tsx b/client/components/claim/Education.tsx deleted file mode 100644 index da0529ff..00000000 --- a/client/components/claim/Education.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { FunctionComponent, useState } from "react"; -import classNames from "classnames"; -import Card from "components/Card"; -import Ogn from "@/components/claim/education/Ogn"; -import Ousd from "@/components/claim/education/Ousd"; -import Ogv from "@/components/claim/education/Ogv"; -import Sticky from "react-sticky-el"; - -interface EducationProps { - handleNextStep: () => void; -} - -const Education: FunctionComponent = ({ handleNextStep }) => { - const [currentEducationStep, setCurrentEducationStep] = useState(0); - - const handleNextEducationStep = () => { - window && window.scrollTo(0, 0); - setCurrentEducationStep(currentEducationStep + 1); - }; - - const educationSteps = ["OUSD", "OGV", "OGN"]; - - return ( -
- -
-
- -
    - {educationSteps.map((step, index) => { - const isCurrent = index === currentEducationStep; - - const markerClasses = classNames( - "text-sm md:text-base rounded h-6 w-6 flex flex-shrink-0 items-center justify-center", - { - "bg-primary text-white": isCurrent, - "bg-gray-300 text-gray-400": !isCurrent, - } - ); - const textClasses = classNames("text-sm md:text-base", { - "text-gray-500": !isCurrent, - "text-black": isCurrent, - }); - return ( -
  • - {index + 1} - {step} -
  • - ); - })} -
-
-
-
-
-
- {currentEducationStep === 0 && ( - - )} - {currentEducationStep === 1 && ( - - )} - {currentEducationStep === 2 && } -
-
- ); -}; -export default Education; diff --git a/client/components/claim/Eligibility.tsx b/client/components/claim/Eligibility.tsx deleted file mode 100644 index f825e29c..00000000 --- a/client/components/claim/Eligibility.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import { FunctionComponent, useCallback } from "react"; -import Card from "components/Card"; -import { Web3Button } from "components/Web3Button"; -import Button from "components/Button"; -import { useStore } from "utils/store"; -import useClaim from "utils/useClaim"; -import EligibilityItem from "components/claim/EligibilityItem"; -import Icon from "@mdi/react"; -import { mdiWallet, mdiCheckCircle, mdiAlertCircle } from "@mdi/js"; -import { Loading } from "components/Loading"; -import Link from "components/Link"; -import { getRewardsApy } from "utils/apy"; -import { filter } from "lodash"; -import { BigNumber } from "ethers"; - -interface EligibilityProps { - handleNextStep: () => void; -} - -const Eligibility: FunctionComponent = ({ - handleNextStep, -}) => { - const { provider, web3Provider, address, web3Modal } = useStore(); - const claim = useClaim(); - const { loaded, hasClaim } = claim; - - const hasOptionalClaim = claim.optional && claim.optional.isValid; - const hasMandatoryClaim = claim.mandatory && claim.mandatory.isValid; - - const optionalSplits = filter(claim?.optional?.split, (split) => - split.gte(0) - ).length; - const mandatorySplits = filter(claim?.mandatory?.split, (split) => - split.gte(0) - ).length; - - const totalSupplyVeOgv = claim.staking.totalSupplyVeOgvAdjusted || 0; - - // Standard APY figure, assumes 100 OGV locked for max duration - const stakingApy = getRewardsApy( - 100 * 1.8 ** (48 / 12), - 100, - totalSupplyVeOgv - ); - - const claimValid = - (hasClaim && claim.optional && claim.optional.isValid) || - (claim.mandatory && claim.mandatory.isValid); - - const resetWeb3State = useStore((state) => state.reset); - - const handleDisconnect = useCallback( - async function () { - await web3Modal.clearCachedProvider(); - if (provider?.disconnect && typeof provider.disconnect === "function") { - await provider.disconnect(); - } - resetWeb3State(); - }, - [web3Modal, provider, resetWeb3State] - ); - - if (web3Provider && !loaded) { - return ( - -
- -
-
- ); - } - - return ( - <> - -
- {!web3Provider ? ( -
- -

- Connect your wallet to get started -

-

- We will automatically determine eligibility based on your wallet - address. -

-
- -
-
- ) : ( - <> - {hasClaim ? ( -
- {claimValid ? ( -
-
- -

- You are eligible! -

-
-

- Your address: -
- {address} -

-
- -
-
-
-
- ) : ( -
- -

- This address has an invalid claim proof -

-
-

- Your address: -
- {address} -

-
- -
-
-

- OGV stakers earn a {stakingApy.toFixed(2)}% variable - APY -

- - Buy OGV - -
-
-
- )} -
- ) : ( -
- -

- Unfortunately, this address is not eligible -

-
-

- Your address: -
- {address} -

-
- -
-
-

- OGV stakers earn a {stakingApy.toFixed(2)}% variable APY -

- - Buy OGV - -
-
-
- )} - - )} - {hasClaim && claimValid && ( -
- {hasOptionalClaim && ( -
- - - - - - - - - - - - - - {optionalSplits > 1 && ( - - )} - -
- Eligibility Criteria - - Tokens -
-
- )} - {hasMandatoryClaim && ( -
- - - - - - - - - - - {mandatorySplits > 1 && ( - - )} - -
- Eligibility Criteria - - Tokens -
-
- )} -
- )} -
-
- - ); -}; - -export default Eligibility; diff --git a/client/components/claim/EligibilityItem.tsx b/client/components/claim/EligibilityItem.tsx deleted file mode 100644 index ad9e6e85..00000000 --- a/client/components/claim/EligibilityItem.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { FunctionComponent } from "react"; -import { utils, BigNumber } from "ethers"; -import TokenIcon from "components/TokenIcon"; -import CheckIcon from "components/CheckIcon"; -import ReactTooltip from "react-tooltip"; -import { formatCurrency } from "utils/math"; - -interface EligibilityItemProps { - id: string; - itemTitle: string; - tokens?: BigNumber; - showOgvToken: Boolean; - isTotal?: Boolean; -} - -const EligibilityItem: FunctionComponent = ({ - id, - itemTitle, - tokens, - showOgvToken, - isTotal, -}) => { - tokens = tokens || BigNumber.from(0); - const isEligible = tokens.gt(0); - - if (!isEligible) { - return <>; - } - - return ( - <> - - -
- {!isTotal && } - {itemTitle} -
- - -
- - - - {utils.formatUnits(tokens, 18)} - {showOgvToken ? "OGV" : "pre-locked OGV"} - - -
- - - {formatCurrency(utils.formatUnits(tokens, 18))} - - {showOgvToken ? "OGV" : "pre-locked OGV"} - -
-
- - - - ); -}; - -export default EligibilityItem; diff --git a/client/components/claim/claim/ClaimOgv.tsx b/client/components/claim/claim/ClaimOgv.tsx deleted file mode 100644 index 37dd5b8a..00000000 --- a/client/components/claim/claim/ClaimOgv.tsx +++ /dev/null @@ -1,325 +0,0 @@ -import { FunctionComponent, useState } from "react"; -import Card from "components/Card"; -import CardGroup from "components/CardGroup"; -import TokenIcon from "components/TokenIcon"; -import TokenAmount from "components/TokenAmount"; -import CardStat from "components/CardStat"; -import CardDescription from "components/CardDescription"; -import Button from "components/Button"; -import RangeInput from "@/components/RangeInput"; -import useClaim from "utils/useClaim"; -import numeral from "numeraljs"; -import { getRewardsApy } from "utils/apy"; -import { decimal18Bn } from "utils"; -import PostClaimModal from "./PostClaimModal"; -import Icon from "@mdi/react"; -import { mdiArrowRight } from "@mdi/js"; -import { SECONDS_IN_A_MONTH } from "../../../constants/index"; -import ApyToolTip from "components/claim/claim/ApyTooltip"; -import moment from "moment"; -import { useStore } from "utils/store"; - -interface ClaimOgvProps {} - -const ClaimOgv: FunctionComponent = () => { - const claim = useClaim(); - const { totalLockedUpOgv, totalPercentageOfLockedUpOgv } = - useStore().totalBalances; - - const [hideModal, sethideModal] = useState(true); - - const maxLockupDurationInMonths = 12 * 4; - const [lockupDuration, setLockupDuration] = useState( - maxLockupDurationInMonths - ); - const [error, setError] = useState(null); - const totalSupplyVeOgv = claim.staking.totalSupplyVeOgvAdjusted || 0; - if (!claim.loaded || !claim.optional.hasClaim) { - return <>; - } - - const isValidLockup = lockupDuration > 0; - const claimableOgv = claim.optional.isValid - ? numeral(claim.optional.amount.div(decimal18Bn).toString()).value() - : 0; - // as specified here: https://github.com/OriginProtocol/ousd-governance/blob/master/contracts/OgvStaking.sol#L21 - const votingDecayFactor = 1.8; - - const veOgvFromOgvLockup = - claimableOgv * votingDecayFactor ** (lockupDuration / 12); - const ogvLockupRewardApy = getRewardsApy( - veOgvFromOgvLockup, - claimableOgv, - totalSupplyVeOgv - ); - const maxVeOgvFromOgvLockup = claimableOgv * votingDecayFactor ** (48 / 12); - const maxOgvLockupRewardApy = getRewardsApy( - maxVeOgvFromOgvLockup, - claimableOgv, - totalSupplyVeOgv - ); - - let claimButtonText = ""; - if (isValidLockup && claim.optional.state === "ready") { - claimButtonText = "Claim & Stake OGV"; - } else if (claim.optional.state === "ready") { - claimButtonText = "Claim OGV"; - } else if (claim.optional.state === "waiting-for-user") { - claimButtonText = "Please confirm transaction"; - } else if (claim.optional.state === "waiting-for-network") { - claimButtonText = "Waiting to be mined"; - } else if (claim.optional.state === "claimed") { - claimButtonText = "Claimed"; - } - - const showModal = - !hideModal && - (claim.optional.state === "waiting-for-user" || - claim.optional.state === "waiting-for-network" || - claim.optional.state === "claimed"); - - const now = new Date(); - - return ( - <> - -
-
-

Total claimable OGV

- -
-
- - - - - OGV -
-
-
-
-
-
-
-

- Stake your OGV to get maximum rewards and voting power -

-

- Staked OGV is converted to non-transferable veOGV, which - allows you to claim additional OGV staking rewards, OUSD fees, - and participate in governance. -

-
-
- -
- Stake duration - -
-
- {lockupDuration} - Months -
-
-
-
-
- - -
-
- - {isValidLockup ? ogvLockupRewardApy.toFixed(2) : 0} - - % -
-
-
-
-
-
- { - setLockupDuration(e.target.value); - }} - hideLabel - markers={[ - { - label: "0", - value: 0, - }, - { - label: "", - value: 0, - }, - { - label: "1 yr", - value: 12, - }, - { - label: "", - value: 0, - }, - { - label: "2 yrs", - value: 24, - }, - { - label: "", - value: 0, - }, - { - label: "3 yrs", - value: 36, - }, - { - label: "", - value: 0, - }, - { - label: "4 yrs", - value: 48, - }, - ]} - onMarkerClick={(markerValue) => { - if (markerValue) { - setLockupDuration(markerValue); - } - }} - /> -
-
-
-
-
-

Your claim summary

- {!isValidLockup ? ( -
- You are claiming - -
-
- - - - - OGV -
-
-
-
- ) : ( - -
- You are staking - -
-
- - - - - OGV -
-
- Unlocks{" "} - {moment( - now.getTime() + - lockupDuration * SECONDS_IN_A_MONTH * 1000 - ).format("MMM D, YYYY")} -
-
-
-
-
- Today you get - -
-
- - - - - veOGV -
-
-
-
-
-
-
- -
- - )} -
- {!isValidLockup && ( -
- If you don't stake your OGV, you'll miss out on the{" "} - {maxOgvLockupRewardApy.toFixed(2)}% variable APY and maximized - voting power.{" "} - OGV ( - {totalPercentageOfLockedUpOgv.toFixed(2)}% of the total supply) - has already been staked by other users. -
- )} - {error && ( -
- {error} -
- )} -
- -
-
- - - - ); -}; - -export default ClaimOgv; diff --git a/client/components/claim/claim/ClaimVeOgv.tsx b/client/components/claim/claim/ClaimVeOgv.tsx deleted file mode 100644 index 1dfe20e0..00000000 --- a/client/components/claim/claim/ClaimVeOgv.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import { FunctionComponent, useState } from "react"; -import moment from "moment"; -import Card from "components/Card"; -import CardGroup from "components/CardGroup"; -import TokenIcon from "components/TokenIcon"; -import TokenAmount from "components/TokenAmount"; -import CardStat from "components/CardStat"; -import CardDescription from "components/CardDescription"; -import Button from "components/Button"; -import useClaim from "utils/useClaim"; -import { decimal18Bn } from "utils"; -import numeral from "numeraljs"; -import { getRewardsApy } from "utils/apy"; -import Icon from "@mdi/react"; -import { mdiArrowRight } from "@mdi/js"; -import PostClaimModal from "./PostClaimModal"; -import ApyToolTip from "components/claim/claim/ApyTooltip"; -import { SECONDS_IN_A_MONTH } from "constants/index"; - -interface ClaimVeOgvProps {} - -const ClaimVeOgv: FunctionComponent = () => { - const claim = useClaim(); - const [error, setError] = useState(null); - const [hideModal, sethideModal] = useState(true); - - if (!claim.loaded || !claim.mandatory.hasClaim) { - return <>; - } - const totalSupplyVeOgv = claim.staking.totalSupplyVeOgvAdjusted || 0; - const claimableVeOgv = claim.mandatory.isValid - ? numeral(claim.mandatory.amount.div(decimal18Bn).toString()).value() - : 0; - - // as specified here: https://github.com/OriginProtocol/ousd-governance/blob/master/contracts/OgvStaking.sol#L21 - const votingDecayFactor = 1.8; - - const now = new Date(); - const veOgvFromVeOgvClaim = - (claimableVeOgv / 4) * votingDecayFactor ** (12 / 12) + - (claimableVeOgv / 4) * votingDecayFactor ** (24 / 12) + - (claimableVeOgv / 4) * votingDecayFactor ** (36 / 12) + - (claimableVeOgv / 4) * votingDecayFactor ** (48 / 12); - - const veOgvLockupRewardApy = getRewardsApy( - veOgvFromVeOgvClaim, - claimableVeOgv, - totalSupplyVeOgv - ); - - const fourYearsFromNow = new Date( - now.getTime() + 48 * SECONDS_IN_A_MONTH * 1000 - ); - const threeYearsFromNow = new Date( - now.getTime() + 36 * SECONDS_IN_A_MONTH * 1000 - ); - const twoYearsFromNow = new Date( - now.getTime() + 24 * SECONDS_IN_A_MONTH * 1000 - ); - const oneYearFromNow = new Date( - now.getTime() + 12 * SECONDS_IN_A_MONTH * 1000 - ); - - let claimButtonText = ""; - if (claim.mandatory.state === "ready") { - claimButtonText = "Claim & Stake OGV"; - } else if (claim.mandatory.state === "waiting-for-user") { - claimButtonText = "Please confirm transaction"; - } else if (claim.mandatory.state === "waiting-for-network") { - claimButtonText = "Waiting to be mined"; - } else if (claim.mandatory.state === "claimed") { - claimButtonText = "Claimed"; - } - - const showModal = - !hideModal && - (claim.mandatory.state === "waiting-for-user" || - claim.mandatory.state === "waiting-for-network" || - claim.mandatory.state === "claimed"); - - return ( - <> - -
-
-

Total claimable staked OGV

- - -
-
- - - - - OGV -
-
-
-
-
-

- Your OGV will be staked automatically for rewards and voting - power -

-

- Staked OGV is converted to non-transferrable veOGV, which allows - you to claim additional OGV staking rewards, OUSD fees, and - participate in governance. -

-
- -
- Staking periods - -
-
-
- 12 - Months -
-
-
-
- 24 - Months -
-
-
-
- 36 - Months -
-
-
-
- 48 - Months -
-
-
-
-
-
- - -
-
- - {veOgvLockupRewardApy.toFixed(2)} - - % -
-
-
-
-
-
-

Your claim summary

- -
- You are staking - -
-
-
- - - - - OGV -
-
- Unlocks {moment(oneYearFromNow).format("MMM D, YYYY")} -
-
-
-
- - - - - OGV -
-
- Unlocks{" "} - {moment(twoYearsFromNow).format("MMM D, YYYY")} -
-
-
-
- - - - - OGV -
-
- Unlocks{" "} - {moment(threeYearsFromNow).format("MMM D, YYYY")} -
-
-
-
- - - - - OGV -
-
- Unlocks{" "} - {moment(fourYearsFromNow).format("MMM D, YYYY")} -
-
-
-
-
-
- Today you get - -
-
- - - - - veOGV -
-
-
-
-
-
-
- -
- -
- {error && ( -
- {error} -
- )} -
- -
-
-
- - - - ); -}; - -export default ClaimVeOgv; diff --git a/client/components/claim/claim/PostClaimModal.tsx b/client/components/claim/claim/PostClaimModal.tsx deleted file mode 100644 index c8d27493..00000000 --- a/client/components/claim/claim/PostClaimModal.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { FunctionComponent, Dispatch, SetStateAction } from "react"; -import Modal from "components/Modal"; -import { Loading } from "components/Loading"; -import Link from "components/Link"; -import CardGroup from "components/CardGroup"; -import Card from "components/Card"; -import TokenIcon from "components/TokenIcon"; -import CardStat from "components/CardStat"; -import TokenAmount from "components/TokenAmount"; -import CardDescription from "components/CardDescription"; -import Icon from "@mdi/react"; -import { mdiWallet, mdiLaunch, mdiArrowDown } from "@mdi/js"; -import Image from "next/image"; - -interface PostClaimModalProps { - show: Boolean; - claim: Object; - handleClose: Dispatch>; - didLock?: Boolean; - veOgv?: Number; -} - -const PostClaimModalProps: FunctionComponent = ({ - show, - claim, - handleClose, - didLock, - veOgv, -}) => { - const { state, amount, receipt } = claim; - - return ( - -
- {"waiting-for-user" === state && ( -
- -
-

Confirm in wallet

-

- Approve the transaction to claim {didLock && "and stake "}your - tokens -

-
-

- Please check your wallet and approve the transaction -

-
- )} - {"waiting-for-network" === state && ( -
- -
-

Processing transaction

-

- {didLock ? ( - <> - Locking OGV and claiming{" "} - veOGV... - - ) : ( - <> - Claiming OGV... - - )} -

-
- - View explorer - - -
- )} - {"claimed" === state && ( -
-
-

Success!

-
- -
- - {didLock ? "You have locked" : "You have claimed"} - - -
-
- - - - - OGV -
-
-
-
- {didLock && ( -
- You have claimed - -
-
- - - - - veOGV -
-
-
-
- )} -
- - View explorer - - -
-

Next step...

-
-
-
- -
-

- ...earn ETH when you stake OGN on Origin - Story. -

- - Origin Story - - - Earn ETH rewards - - -
-
-
- )} -
- - ); -}; - -export default PostClaimModalProps; diff --git a/client/components/claim/education/Ogn.tsx b/client/components/claim/education/Ogn.tsx deleted file mode 100644 index 4e523cc4..00000000 --- a/client/components/claim/education/Ogn.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { FunctionComponent, Dispatch, SetStateAction } from "react"; -import TokenIcon from "components/TokenIcon"; -import Video from "components/Video"; -import Card from "components/Card"; -import CardGroup from "components/CardGroup"; -import Quiz from "components/claim/education/Quiz"; - -interface OgnProps { - handleNextStep: () => void; -} - -const questions = [ - { - question: - "Which of the following is NOT a product offering of Origin Story?", - answers: [ - "NFT minting", - "Branded, collection-specific marketplace on a creator's own domain", - "Token-gated access to exclusive experiences", - "NFT borrowing and lending", - ], - correctAnswer: "NFT borrowing and lending", - }, - { - question: - "Which of the following is the value accrual token for Origin Story?", - answers: ["OUSD", "OGV", "OGN", "DOGE"], - correctAnswer: "OGN", - }, - { - question: "Which of the following will OGN stakers be entitled to?", - answers: [ - "Fees generated by Origin Story primary sales (NFT mints)", - "Fees generated by Origin Story secondary royalties", - "Fees generated by Origin Story secondary marketplace fees", - "100% of the fees generated by Origin Story", - ], - correctAnswer: "100% of the fees generated by Origin Story", - }, -]; - -const Ogn: FunctionComponent = ({ handleNextStep }) => ( - - -
-
- -

Origin Token (OGN)

-
-
-
- - - -
-); - -export default Ogn; diff --git a/client/components/claim/education/Ogv.tsx b/client/components/claim/education/Ogv.tsx deleted file mode 100644 index d121e37d..00000000 --- a/client/components/claim/education/Ogv.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { FunctionComponent, Dispatch, SetStateAction } from "react"; -import TokenIcon from "components/TokenIcon"; -import Video from "components/Video"; -import Card from "components/Card"; -import CardGroup from "components/CardGroup"; -import Quiz from "components/claim/education/Quiz"; - -interface OgvProps { - onComplete?: Dispatch>; -} - -const questions = [ - { - question: "Which of the following is the governance token for OUSD?", - answers: ["OGN", "OGV", "GOV", "ODG"], - correctAnswer: "OGV", - }, - { - question: "Which is the best reason to stake OGV and convert it to veOGV?", - answers: [ - "To keep it from being burned", - "veOGV is freely transferable", - "To hedge against Bitcoin", - "veOGV holders earn fees from OUSD's yield and staking rewards", - ], - correctAnswer: - "veOGV holders earn fees from OUSD's yield and staking rewards", - }, - { - question: "How can you earn more as OUSD adoption increases?", - answers: [ - "Wait to claim OGV after OUSD succeeds", - "Stake OGV now to get rewards, OUSD fees and voting power", - ], - correctAnswer: "Stake OGV now to get rewards, OUSD fees and voting power", - }, -]; - -const Ogv: FunctionComponent = ({ onComplete }) => ( - - -
-
- -

- Origin Dollar Governance (OGV) -

-
-
-
- - - -
-); - -export default Ogv; diff --git a/client/components/claim/education/Ousd.tsx b/client/components/claim/education/Ousd.tsx deleted file mode 100644 index 6697ab92..00000000 --- a/client/components/claim/education/Ousd.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { FunctionComponent, Dispatch, SetStateAction } from "react"; -import TokenIcon from "components/TokenIcon"; -import Video from "components/Video"; -import Quiz from "components/claim/education/Quiz"; -import CardGroup from "components/CardGroup"; -import Card from "components/Card"; - -interface OusdProps { - onComplete?: () => void; -} - -const questions = [ - { - question: "Which best describes OUSD?", - answers: [ - "An algorithmic stablecoin", - "A governance token", - "A fully backed stablecoin that generates yield", - "A staking token", - ], - correctAnswer: "A fully backed stablecoin that generates yield", - }, - { - question: "How do you earn yield with OUSD?", - answers: [ - "You get OGV for locking it up", - "The balance grows automatically in your wallet", - "You stake it in the Origin Vault", - "The price goes up over time", - ], - correctAnswer: "The balance grows automatically in your wallet", - }, - { - question: "Who manages OUSD's funds?", - answers: [ - "Origin Protocol's team of Internet pioneers", - "Every token holder chooses a strategy", - "An elite group of ex-Wall Street traders", - "A transparent set of automated smart contracts", - ], - correctAnswer: "A transparent set of automated smart contracts", - }, -]; - -const Ousd: FunctionComponent = ({ onComplete }) => ( - - -
-
- -

- Origin Dollar (OUSD) -

-
-
-
- - - -
-); - -export default Ousd; diff --git a/client/components/claim/education/Quiz.tsx b/client/components/claim/education/Quiz.tsx deleted file mode 100644 index cf0722c5..00000000 --- a/client/components/claim/education/Quiz.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { FunctionComponent, useState, Dispatch, SetStateAction } from "react"; -import classNames from "classnames"; -import Button from "components/Button"; -import CheckIconWhite from "components/CheckIconWhite"; -import CrossIconWhite from "components/CrossIconWhite"; -import { shuffle } from "lodash"; - -interface QuizQuestion { - question: string; - answers: Array; - correctAnswer: string; - canAdvance?: Boolean; -} - -interface QuizProps { - questions: Array; - onComplete?: () => void | Dispatch>; - onCompleteMessage?: string; - lastQuiz?: Boolean; - handleNextStep?: () => void; -} - -const Quiz: FunctionComponent = ({ - questions, - onComplete, - onCompleteMessage, - lastQuiz, - handleNextStep, -}) => { - const [currentQuestion, setCurrentQuestion] = useState(0); - const [currentAnswers, setCurrentAnswers] = useState( - shuffle(questions[currentQuestion].answers) - ); - const [currentAnswer, setCurrentAnswer] = useState(""); - const [status, setStatus] = useState({ - type: "", - message: "", - note: "", - }); - const [canProgress, setCanProgress] = useState(false); - - const quizComplete = currentQuestion === questions.length - 1; - - const handleSubmitAnswer = () => { - if (currentAnswer === questions[currentQuestion].correctAnswer) { - setStatus({ - type: "success", - message: "That's right!", - note: "", - }); - setCanProgress(true); - - if (onComplete && quizComplete && lastQuiz) { - onComplete(true); - } - } else { - setStatus({ - type: "error", - message: "Sorry, that's incorrect", - note: "", - }); - setCanProgress(false); - - if (onComplete && quizComplete && lastQuiz) { - onComplete(false); - } - } - }; - - const handleResetQuestion = () => { - setStatus({ - type: "", - message: "", - note: "", - }); - setCurrentAnswer(""); - setCanProgress(false); - - if (currentQuestion < questions.length) { - setCurrentQuestion(currentQuestion); - setCurrentAnswers(shuffle(questions[currentQuestion].answers)); - } - - return; - }; - - const handleNextQuestion = () => { - setStatus({ - type: "", - message: "", - note: "", - }); - setCurrentAnswer(""); - setCanProgress(false); - - if (currentQuestion < questions.length) { - setCurrentQuestion(currentQuestion + 1); - setCurrentAnswers(shuffle(questions[currentQuestion + 1].answers)); - } - - return; - }; - - const letterMap = ["A", "B", "C", "D"]; - - return ( -
- - Question {currentQuestion + 1} of {questions.length} - -
- {questions.map((q, index) => { - const { question, correctAnswer } = q; - - if (index !== currentQuestion) return null; - - return ( -
-

{question}

-
    - {currentAnswers.map((answer, index) => { - const isCurrent = currentAnswer === answer; - const isCorrect = currentAnswer === correctAnswer; - - const discClasses = classNames( - "flex-shrink-0 bg-gray-500 text-white font-bold h-8 w-8 p-2 flex items-center justify-center rounded-full", - { - "bg-green-500": - isCurrent && isCorrect && status?.type === "success", - "bg-orange-500": - isCurrent && !isCorrect && status?.type === "error", - "bg-gray-500": !isCurrent, - } - ); - - const answerClasses = classNames( - "text-left border rounded p-3 flex items-center space-x-3 w-full", - { - "bg-green-200 border-green-400": - isCorrect && status?.type === "success", - "bg-orange-200 border-orange-400": - !isCorrect && status?.type === "error", - "border-gray-400 bg-gray-300": - isCurrent && status?.type === "", - "border-gray-300 hover:bg-gray-100 disabled:bg-white": - !isCurrent, - } - ); - - return ( -
  • - -
  • - ); - })} -
-
- ); - })} - {status?.type && ( -
- {status.message &&

{status.message}

} - {status.note && ( -

{status.note}

- )} - {canProgress && quizComplete && ( -

- {onCompleteMessage - ? onCompleteMessage - : "It's time to claim the airdrop"} -

- )} -
- )} - {!canProgress && currentAnswer && status?.type === "" && ( - - )} - {!canProgress && status?.type === "error" && ( - - )} - {canProgress && !quizComplete && ( - - )} - {onComplete && quizComplete && canProgress && !lastQuiz && ( - - )} - {quizComplete && canProgress && lastQuiz && handleNextStep && ( - - )} -
-
- ); -}; - -export default Quiz; diff --git a/client/components/layout.tsx b/client/components/layout.tsx index 8a4221ad..6d589e6b 100644 --- a/client/components/layout.tsx +++ b/client/components/layout.tsx @@ -2,6 +2,7 @@ import { FunctionComponent, ReactNode } from "react"; import Header from "components/Header"; import Footer from "components/Footer"; import AdminUtils from "components/AdminUtils"; +import WalletSelectModal from "./WalletSelectModal"; interface LayoutProps { children: ReactNode; @@ -10,6 +11,7 @@ interface LayoutProps { const Layout: FunctionComponent = ({ children, hideNav }) => (
+
diff --git a/client/components/proposal/EnsureDelegationModal.tsx b/client/components/proposal/EnsureDelegationModal.tsx index d0c90c4a..4d6e708b 100644 --- a/client/components/proposal/EnsureDelegationModal.tsx +++ b/client/components/proposal/EnsureDelegationModal.tsx @@ -2,10 +2,12 @@ import { useStore } from "utils/store"; import { useState } from "react"; import useAccountBalances from "utils/useAccountBalances"; import Modal from "components/Modal"; +import { useWeb3React } from "@web3-react/core"; export const EnsureDelegationModal = () => { - const { contracts, ensureDelegationModalOpened, address, rpcProvider } = + const { contracts, ensureDelegationModalOpened, address, web3Provider } = useStore(); + const { account, library } = useWeb3React(); /* * 0 - initial prompt to initiate contract delegation call * 1 - confirmation stage @@ -31,10 +33,12 @@ export const EnsureDelegationModal = () => { const handleRegistration = async () => { setRegisterStatus("waiting-for-user"); // self delegate - const transaction = await contracts.OgvStaking.delegate(address); + const transaction = await contracts.OgvStaking.connect( + library.getSigner(account) + ).delegate(address); setRegisterStatus("waiting-for-network"); // wait for self delegation to be mined before continuing - await rpcProvider.waitForTransaction(transaction.hash); + await web3Provider.waitForTransaction(transaction.hash); setStage(1); setRegisterStatus("ready"); /* diff --git a/client/components/proposal/ProposalDetail.tsx b/client/components/proposal/ProposalDetail.tsx index 59e37095..daf5268c 100644 --- a/client/components/proposal/ProposalDetail.tsx +++ b/client/components/proposal/ProposalDetail.tsx @@ -19,6 +19,7 @@ import { StateTag } from "./StateTag"; import { ProposalHistory } from "./ProposalHistory"; import moment from "moment"; import RegisterToVote from "components/proposal/RegisterToVote"; +import { useWeb3React } from "@web3-react/core"; export const ProposalDetail = ({ id, @@ -35,7 +36,8 @@ export const ProposalDetail = ({ voters: Array; transactions: Array; }) => { - const { address, contracts, pendingTransactions, rpcProvider } = useStore(); + const { contracts, pendingTransactions, web3Provider } = useStore(); + const { account: address } = useWeb3React(); const [proposalActions, setProposalActions] = useState(null); const { showModalIfApplicable } = useShowDelegationModalOption(); const [proposal, setProposal] = useState(null); @@ -52,13 +54,13 @@ export const ProposalDetail = ({ useEffect(() => { const getBlockNumber = async () => { - const blockNumber = await rpcProvider.getBlockNumber(); + const blockNumber = await web3Provider.getBlockNumber(); setBlockNumber(parseInt(blockNumber)); }; - if (rpcProvider) { + if (web3Provider) { getBlockNumber(); } - }, [rpcProvider]); + }, [web3Provider]); useEffect(() => { const loadVoters = async () => { diff --git a/client/components/proposal/ProposalHistory.tsx b/client/components/proposal/ProposalHistory.tsx index ed98d753..91f10973 100644 --- a/client/components/proposal/ProposalHistory.tsx +++ b/client/components/proposal/ProposalHistory.tsx @@ -15,12 +15,12 @@ interface ProposalHistoryProps { const ProposalHistory: FunctionComponent = ({ transactions, }) => { - const { rpcProvider } = useStore(); + const { web3Provider } = useStore(); let explorerPrefix; - if (rpcProvider?._network?.chainId === 1) { + if (web3Provider?._network?.chainId === 1) { explorerPrefix = "https://etherscan.io/"; - } else if (rpcProvider?._network?.chainId === 5) { + } else if (web3Provider?._network?.chainId === 5) { explorerPrefix = "https://goerli.etherscan.io/"; } diff --git a/client/components/proposal/Reallocation.tsx b/client/components/proposal/Reallocation.tsx index 95909d5a..18b356ed 100644 --- a/client/components/proposal/Reallocation.tsx +++ b/client/components/proposal/Reallocation.tsx @@ -7,6 +7,7 @@ import useShowDelegationModalOption from "utils/useShowDelegationModalOption"; import { EnsureDelegationModal } from "components/proposal/EnsureDelegationModal"; import { useRouter } from "next/router"; import { SubmitProposalButton } from "components/proposal/SubmitProposalButton"; +import { useWeb3React } from "@web3-react/core"; interface ReallocationProps { proposalDetails: string; @@ -17,6 +18,7 @@ const Reallocation: FunctionComponent = ({ }) => { const router = useRouter(); const { contracts, pendingTransactions } = useStore(); + const { account, library } = useWeb3React(); const { showModalIfApplicable } = useShowDelegationModalOption(); const [fromStrategy, setFromStrategy] = useState(""); const [toStrategy, setToStrategy] = useState(""); @@ -162,9 +164,9 @@ const Reallocation: FunctionComponent = ({ return; } - const transaction = await contracts.Governance[ - "propose(address[],uint256[],string[],bytes[],string)" - ]( + const transaction = await contracts.Governance.connect( + library.getSigner(account) + )["propose(address[],uint256[],string[],bytes[],string)"]( proposal.targets, proposal.values, proposal.signatures, diff --git a/client/components/proposal/RegisterToVote.tsx b/client/components/proposal/RegisterToVote.tsx index 63af084b..68bfabd2 100644 --- a/client/components/proposal/RegisterToVote.tsx +++ b/client/components/proposal/RegisterToVote.tsx @@ -6,6 +6,7 @@ import { toast } from "react-toastify"; import Card from "components/Card"; import Link from "components/Link"; import classNames from "classnames"; +import { useWeb3React } from "@web3-react/core"; interface RegisterToVoteProps { withCard?: Boolean; @@ -21,8 +22,11 @@ const RegisterToVote: FunctionComponent = ({ showNoVeOgvMessage, whiteRegisterCta, }) => { - const { contracts, address, balances, pendingTransactions } = useStore(); + const { contracts, balances, pendingTransactions } = useStore(); + const { account: address } = useWeb3React(); const { veOgv } = balances; + const { library, account } = useWeb3React(); + const [registerStatus, setRegisterStatus] = useState("ready"); const { needToShowDelegation } = useShowDelegationModalOption(); @@ -38,7 +42,9 @@ const RegisterToVote: FunctionComponent = ({ const handleRegistration = async () => { setRegisterStatus("waiting-for-user"); try { - const transaction = await contracts.OgvStaking.delegate(address); + const transaction = await contracts.OgvStaking.connect( + library.getSigner(account) + ).delegate(address); setRegisterStatus("waiting-for-network"); useStore.setState({ diff --git a/client/components/vote-escrow/LockupForm.tsx b/client/components/vote-escrow/LockupForm.tsx index 4ac94724..da9b58ce 100644 --- a/client/components/vote-escrow/LockupForm.tsx +++ b/client/components/vote-escrow/LockupForm.tsx @@ -1,6 +1,6 @@ import { FunctionComponent } from "react"; import { ethers } from "ethers"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { useRouter } from "next/router"; import { useStore } from "utils/store"; import Card from "components/Card"; @@ -19,10 +19,11 @@ import CardGroup from "components/CardGroup"; import moment from "moment"; import { mdiArrowRight, mdiAlertCircle } from "@mdi/js"; import Icon from "@mdi/react"; -import ApyToolTip from "components/claim/claim/ApyTooltip"; +import ApyToolTip from "@/components/ApyTooltip"; import { getRewardsApy } from "utils/apy"; import numeral from "numeraljs"; import { decimal18Bn } from "utils"; +import { useWeb3React } from "@web3-react/core"; interface LockupFormProps { existingLockup?: Object; @@ -118,13 +119,14 @@ const maxLockupDurationInMonths = 12 * 4; const LockupForm: FunctionComponent = ({ existingLockup }) => { const { contracts, - rpcProvider, + web3Provider, pendingTransactions, balances, allowances, blockTimestamp, totalBalances, } = useStore(); + const { library, account } = useWeb3React(); const router = useRouter(); const { totalSupplyVeOgvAdjusted } = totalBalances; @@ -234,11 +236,11 @@ const LockupForm: FunctionComponent = ({ existingLockup }) => { let transaction; try { - transaction = await contracts.OriginDollarGovernance.approve( - contracts.OgvStaking.address, - ethers.constants.MaxUint256, - { gasLimit: 144300 } - ); + transaction = await contracts.OriginDollarGovernance.connect( + library.getSigner(account) + ).approve(contracts.OgvStaking.address, ethers.constants.MaxUint256, { + gasLimit: 144300, + }); } catch (e) { setTransactionError("Error approving!"); setApprovalStatus("ready"); @@ -249,7 +251,7 @@ const LockupForm: FunctionComponent = ({ existingLockup }) => { let receipt; try { - receipt = await rpcProvider.waitForTransaction(transaction.hash); + receipt = await web3Provider.waitForTransaction(transaction.hash); } catch (e) { setTransactionError("Error approving!"); setApprovalStatus("ready"); @@ -297,15 +299,15 @@ const LockupForm: FunctionComponent = ({ existingLockup }) => { // When the user has delegation set already, it'll be ~200k. const maxGasNeeded = 350000 * 1.2; // Adds a buffer - const gasEstimate = await contracts.OgvStaking.estimateGas[ - "stake(uint256,uint256)" - ](amountToStake, duration); + const gasEstimate = await contracts.OgvStaking.connect( + library.getSigner(account) + ).estimateGas["stake(uint256,uint256)"](amountToStake, duration); - transaction = await contracts.OgvStaking["stake(uint256,uint256)"]( - amountToStake, - duration, - { gasLimit: Math.max(Math.ceil(gasEstimate * 1.25), maxGasNeeded) } - ); + transaction = await contracts.OgvStaking.connect( + library.getSigner(account) + )["stake(uint256,uint256)"](amountToStake, duration, { + gasLimit: Math.max(Math.ceil(gasEstimate * 1.25), maxGasNeeded), + }); } catch (e) { setTransactionError("Error locking up!"); setLockupStatus("ready"); @@ -316,7 +318,7 @@ const LockupForm: FunctionComponent = ({ existingLockup }) => { let receipt; try { - receipt = await rpcProvider.waitForTransaction(transaction.hash); + receipt = await web3Provider.waitForTransaction(transaction.hash); } catch (e) { setTransactionError("Error locking up!"); setLockupStatus("ready"); @@ -363,15 +365,18 @@ const LockupForm: FunctionComponent = ({ existingLockup }) => { // When the user has delegation set already, it'll be ~200k. const maxGasNeeded = 300000 * 1.2; // Adds a buffer - const gasEstimate = await contracts.OgvStaking.estimateGas[ - "extend(uint256,uint256)" - ](existingLockup.lockupId, duration); - - transaction = await contracts.OgvStaking["extend(uint256,uint256)"]( + const gasEstimate = await contracts.OgvStaking.connect( + library.getSigner(account) + ).estimateGas["extend(uint256,uint256)"]( existingLockup.lockupId, - duration, - { gasLimit: Math.max(Math.ceil(gasEstimate * 1.25), maxGasNeeded) } + duration ); + + transaction = await contracts.OgvStaking.connect( + library.getSigner(account) + )["extend(uint256,uint256)"](existingLockup.lockupId, duration, { + gasLimit: Math.max(Math.ceil(gasEstimate * 1.25), maxGasNeeded), + }); } catch (e) { setTransactionError("Error extending lockup!"); setLockupStatus("ready"); @@ -382,7 +387,7 @@ const LockupForm: FunctionComponent = ({ existingLockup }) => { let receipt; try { - receipt = await rpcProvider.waitForTransaction(transaction.hash); + receipt = await web3Provider.waitForTransaction(transaction.hash); } catch (e) { setTransactionError("Error extending lockup!"); setLockupStatus("ready"); diff --git a/client/components/vote-escrow/LockupsTable.tsx b/client/components/vote-escrow/LockupsTable.tsx index d8facbdb..91c91b2b 100644 --- a/client/components/vote-escrow/LockupsTable.tsx +++ b/client/components/vote-escrow/LockupsTable.tsx @@ -15,10 +15,12 @@ import Modal from "components/Modal"; import { truncateEthAddress } from "utils"; import EtherscanIcon from "components/EtherscanIcon"; import ExternalLinkIcon from "../ExternalLinkIcon"; +import { useWeb3React } from "@web3-react/core"; const LockupsTable: FunctionComponent = () => { const { lockups, pendingTransactions, contracts, blockTimestamp, chainId } = useStore(); + const { library, account } = useWeb3React(); const { reloadTotalBalances } = useTotalBalances(); const { reloadAccountBalances } = useAccountBalances(); @@ -36,10 +38,9 @@ const LockupsTable: FunctionComponent = () => { } const handleUnlock = async (lockupId) => { - const transaction = await contracts.OgvStaking["unstake(uint256)"]( - lockupId, - { gasLimit: 210000 } - ); + const transaction = await contracts.OgvStaking.connect( + library.getSigner(account) + )["unstake(uint256)"](lockupId, { gasLimit: 210000 }); useStore.setState({ pendingTransactions: [ diff --git a/client/components/vote-escrow/YourLockups.tsx b/client/components/vote-escrow/YourLockups.tsx index 4eac50eb..31240429 100644 --- a/client/components/vote-escrow/YourLockups.tsx +++ b/client/components/vote-escrow/YourLockups.tsx @@ -12,7 +12,8 @@ import { getRewardsApy } from "utils/apy"; import Image from "next/image"; import DisabledButtonToolTip from "../DisabledButtonTooltip"; import LockupsTable from "./LockupsTable"; -import { Web3Button } from "components/Web3Button"; +import ConnectButton from "../ConnectButton"; +import { useWeb3React } from "@web3-react/core"; interface YourLockupsProps {} @@ -25,6 +26,7 @@ const YourLockups: FunctionComponent = () => { totalBalances, web3Provider, } = useStore(); + const { library, account } = useWeb3React(); const { ogv, accruedRewards } = balances; const { totalPercentageOfLockedUpOgv } = totalBalances; const { reloadAccountBalances } = useAccountBalances(); @@ -62,7 +64,9 @@ const YourLockups: FunctionComponent = () => { let transaction; try { - transaction = await contracts.OgvStaking["collectRewards()"]({ + transaction = await contracts.OgvStaking.connect( + library.getSigner(account) + )["collectRewards()"]({ gasLimit: 140000, }); } catch (e) { @@ -74,9 +78,7 @@ const YourLockups: FunctionComponent = () => { let receipt; try { - receipt = await contracts.rpcProvider.waitForTransaction( - transaction.hash - ); + receipt = await web3Provider?.waitForTransaction(transaction.hash); } catch (e) { setCollectRewardsStatus("ready"); throw e; @@ -212,7 +214,7 @@ const YourLockups: FunctionComponent = () => { - {web3Provider ? ( + {web3Provider && account ? ( = () => { Buy OGV ) : ( - + )} )} diff --git a/client/constants/index.ts b/client/constants/index.ts index 90e18488..af00b80f 100644 --- a/client/constants/index.ts +++ b/client/constants/index.ts @@ -6,6 +6,9 @@ import { governanceEnabled } from "utils"; export const mainnetNetworkUrl = process.env.WEB3_PROVIDER; export const goerliNetworkUrl = process.env.WEB3_PROVIDER; +export const isDevelopment = process.env.NODE_ENV === "development"; +export const isProduction = process.env.NODE_ENV === "production"; + export const websocketProvider = process.env.WEB3_PROVIDER?.replace( "http", "ws" diff --git a/client/hoc/index.ts b/client/hoc/index.ts new file mode 100644 index 00000000..a10ef0f3 --- /dev/null +++ b/client/hoc/index.ts @@ -0,0 +1,3 @@ +export { default as withIsMobile } from "./withIsMobile"; +export { default as withWeb3Provider } from "./withWeb3Provider"; +export { default as withWalletSelectModal } from "./withWalletSelectModal"; diff --git a/client/hoc/withIsMobile.tsx b/client/hoc/withIsMobile.tsx new file mode 100644 index 00000000..bcec6fb3 --- /dev/null +++ b/client/hoc/withIsMobile.tsx @@ -0,0 +1,52 @@ +import { NextPageContext } from "next"; +import React, { useEffect, useState } from "react"; + +const withIsMobile = (WrappedComponent: any) => { + const Wrapper = (props: any) => { + const [isMobile, setIsMobile] = useState( + process.browser ? window.innerWidth < 768 : false + ); + + const onResize = () => { + if (window.innerWidth < 768 && !isMobile) { + setIsMobile(true); + } else if (window.innerWidth >= 768 && isMobile) { + setIsMobile(false); + } + }; + + useEffect(onResize, []); + useEffect(() => { + window.addEventListener("resize", onResize); + + const unsubscribe = () => { + window.removeEventListener("resize", onResize); + }; + return unsubscribe; + }, []); + + return ( + + ); + }; + + if (WrappedComponent.getInitialProps) { + Wrapper.getInitialProps = async (ctx: NextPageContext) => { + const componentProps = await WrappedComponent.getInitialProps(ctx); + return componentProps; + }; + } + + return Wrapper; +}; + +export default withIsMobile; diff --git a/client/hoc/withWalletSelectModal.tsx b/client/hoc/withWalletSelectModal.tsx new file mode 100644 index 00000000..3bead51a --- /dev/null +++ b/client/hoc/withWalletSelectModal.tsx @@ -0,0 +1,23 @@ +import { NextPageContext } from "next"; +import React from "react"; +import { useStore } from "utils/store"; + +const withWalletSelectModal = (WrappedComponent: any) => { + const Wrapper = (props: any) => { + const showLogin = () => { + useStore.setState({ walletSelectModalState: "Wallet" }); + }; + return ; + }; + + if (WrappedComponent.getInitialProps) { + Wrapper.getInitialProps = async (ctx: NextPageContext) => { + const componentProps = await WrappedComponent.getInitialProps(ctx); + return componentProps; + }; + } + + return Wrapper; +}; + +export default withWalletSelectModal; diff --git a/client/hoc/withWeb3Provider.tsx b/client/hoc/withWeb3Provider.tsx new file mode 100644 index 00000000..ded8c744 --- /dev/null +++ b/client/hoc/withWeb3Provider.tsx @@ -0,0 +1,35 @@ +import { useEffect, useState } from "react"; +import { + Web3Provider, + ExternalProvider, + JsonRpcFetchFunc, +} from "@ethersproject/providers"; +import { Web3ReactProvider } from "@web3-react/core"; +import { NextPageContext } from "next"; + +const withWeb3Provider = (WrappedComponent: any) => { + function getLibrary(provider: ExternalProvider | JsonRpcFetchFunc) { + const library = new Web3Provider(provider); + library.pollingInterval = 12000; + return library; + } + + const Wrapper = (props: any) => { + return ( + + + + ); + }; + + if (WrappedComponent.getInitialProps) { + Wrapper.getInitialProps = async (ctx: NextPageContext) => { + const componentProps = await WrappedComponent.getInitialProps(ctx); + return componentProps; + }; + } + + return Wrapper; +}; + +export default withWeb3Provider; diff --git a/client/package.json b/client/package.json index fc257e5d..e7a454b3 100644 --- a/client/package.json +++ b/client/package.json @@ -19,6 +19,11 @@ "prettier:fix": "prettier --write '{components,constants,lib,pages,utils}/**/*.{js,jsx,ts,tsx}'" }, "dependencies": { + "@0x/subproviders": "^7.0.1", + "@fbtjs/default-collection-transform": "^1.0.0", + "@gnosis.pm/safe-apps-web3-react": "^1.5.3", + "@ledgerhq/hw-app-eth": "^6.32.0", + "@ledgerhq/hw-transport-webusb": "^6.27.12", "@mdi/js": "^6.9.96", "@mdi/react": "^1.6.0", "@myetherwallet/mewconnect-connector": "^0.1.8", @@ -31,16 +36,28 @@ "@types/react-sticky": "^6.0.4", "@types/react-sticky-el": "^1.0.3", "@walletconnect/web3-provider": "^1.7.1", + "@web3-react/core": "^6.1.9", + "@web3-react/injected-connector": "^6.0.7", + "@web3-react/ledger-connector": "^6.1.9", + "@web3-react/walletconnect-connector": "^6.2.13", + "@web3-react/walletlink-connector": "^6.2.14", "autoprefixer": "^10.4.2", + "babel-plugin-fbt": "^1.0.0", + "babel-plugin-fbt-runtime": "^1.0.0", + "bootstrap": "4.5.2", "chart.js": "^3.7.1", "classnames": "^2.3.1", "daisyui": "^2.0.9", "dayjs": "^1.11.4", + "deficonnect": "^1.6.14-dev.2", "dotenv": "^16.0.0", "eslint": "8.9.0", "eslint-config-next": "12.1.0", "ethereum-events": "https://github.com/AleG94/ethereum-events#develop", "ethers": "^5.5.4", + "fbjs": "^3.0.4", + "fbt": "^1.0.0", + "fbt-runtime": "^0.9.4", "keccak256": "^1.0.6", "lodash": "^4.17.21", "merkletreejs": "^0.2.31", @@ -73,5 +90,14 @@ "web3modal": "^1.9.5", "winston": "^3.6.0", "zustand": "^3.7.0" + }, + "babel": { + "presets": [ + "next/babel" + ], + "plugins": [ + "babel-plugin-fbt", + "babel-plugin-fbt-runtime" + ] } } diff --git a/client/pages/_app.tsx b/client/pages/_app.tsx index 5342b45c..c22dfd84 100644 --- a/client/pages/_app.tsx +++ b/client/pages/_app.tsx @@ -11,9 +11,14 @@ import Layout from "../components/layout"; import { claimOpenTimestampPassed } from "utils"; import Script from "next/script"; import { GTM_ID, pageview } from "../lib/gtm"; +import { withWeb3Provider } from "hoc"; +import { useWeb3React } from "@web3-react/core"; +import { useStore } from "utils/store"; -export default function App({ Component, pageProps }) { +export function App({ Component, pageProps }) { const router = useRouter(); + const { account } = useWeb3React(); + const resetWeb3State = useStore((state) => state.reset); useEffect(() => { router.events.on("routeChangeComplete", pageview); @@ -22,6 +27,12 @@ export default function App({ Component, pageProps }) { }; }, [router.events]); + // If somebody locks their wallet, "account" from within the Web3React state + // will be undefined... we want to use that re-render to reset the web3 store. + useEffect(() => { + if (!account) resetWeb3State(); + }, [account]); + useContracts(); useTotalBalances(); useAccountBalances(); @@ -48,3 +59,5 @@ export default function App({ Component, pageProps }) { ); } + +export default withWeb3Provider(App); diff --git a/client/pages/_document.tsx b/client/pages/_document.tsx index 4b24679b..8dc8d7da 100644 --- a/client/pages/_document.tsx +++ b/client/pages/_document.tsx @@ -14,6 +14,22 @@ class OUSDGovernanceDocument extends Document { /> + {/* jQuery is required for bootstrap javascript */} +