diff --git a/.env b/.env index 6c9cff76df..fcf4b9f4cd 100644 --- a/.env +++ b/.env @@ -35,7 +35,7 @@ REACT_APP_FORTMATIC_SITE_VERIFICATION="LzjrtdM7hqVJfvvA" # Domain regex (to detect environment) REACT_APP_DOMAIN_REGEX_LOCAL="^(:?localhost:\d{2,5}|(?:127|192)(?:\.[0-9]{1,3}){3})" -REACT_APP_DOMAIN_REGEX_PR="^pr\d+--gpswapui\.review" +REACT_APP_DOMAIN_REGEX_PR="^pr\d+--cowswap\.review" REACT_APP_DOMAIN_REGEX_DEV="^cowswap\.dev" REACT_APP_DOMAIN_REGEX_STAGING="^cowswap\.staging" REACT_APP_DOMAIN_REGEX_PROD="^cowswap\.exchange$" diff --git a/.env.production b/.env.production index 8405471e47..777ebee0ac 100644 --- a/.env.production +++ b/.env.production @@ -36,7 +36,7 @@ REACT_APP_FORTMATIC_KEY_PROD="pk_live_7BD8004CBBF5CDD6" # Domain regex (to detect environment) REACT_APP_DOMAIN_REGEX_LOCAL="^(:?localhost:\d{2,5}|(?:127|192)(?:\.[0-9]{1,3}){3})" -REACT_APP_DOMAIN_REGEX_PR="^pr\d+--gpswapui\.review" +REACT_APP_DOMAIN_REGEX_PR="^pr\d+--cowswap\.review" REACT_APP_DOMAIN_REGEX_DEV="^cowswap\.dev" REACT_APP_DOMAIN_REGEX_STAGING="^cowswap\.staging" REACT_APP_DOMAIN_REGEX_PROD="^cowswap\.exchange$" diff --git a/.github/scripts/prepare_production_deployment.sh b/.github/scripts/prepare_production_deployment.sh index b0dbb6a27f..6ffc868958 100755 --- a/.github/scripts/prepare_production_deployment.sh +++ b/.github/scripts/prepare_production_deployment.sh @@ -10,7 +10,7 @@ if [ -n "$VERSION_TAG" ] && [ -n "$PROD_DEPLOYMENT_HOOK_TOKEN" ] && [ -n "$PROD_ then curl --silent --output /dev/null --write-out "%{http_code}" -X POST \ -F token="$PROD_DEPLOYMENT_HOOK_TOKEN" \ - -F ref=master \ + -F ref=main \ -F "variables[TRIGGER_RELEASE_COMMIT_TAG]=$VERSION_TAG" \ $PROD_DEPLOYMENT_HOOK_URL else diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1fdb07afe..a3bd915954 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,16 @@ name: CI on: - # build on PR creation/updates, also when pushing to master/develop, or create a release + # build on PR creation/updates, also when pushing to main/develop, or create a release pull_request: types: [opened, synchronize] push: - branches: [master, develop] + branches: [main, develop] tags: [v*] env: - REPO_NAME_SLUG: gpswapui + REPO_NAME_SLUG: cowswap PR_NUMBER: ${{ github.event.number }} REACT_APP_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} REACT_APP_PINATA_API_KEY: ${{ secrets.REACT_APP_PINATA_API_KEY }} @@ -232,7 +232,7 @@ jobs: run: aws s3 sync website s3://${{ secrets.AWS_DEV_BUCKET_NAME }} --delete - name: 'Deploy to S3: Staging' - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/main' run: aws s3 sync website s3://${{ secrets.AWS_STAGING_BUCKET_NAME }}/current --delete - name: Get the version diff --git a/.github/workflows/ipfs.yml b/.github/workflows/ipfs.yml index 35c39bba9f..12620641f4 100644 --- a/.github/workflows/ipfs.yml +++ b/.github/workflows/ipfs.yml @@ -5,7 +5,7 @@ on: tags: [v*] env: - REPO_NAME_SLUG: gpswapui + REPO_NAME_SLUG: cowswap REACT_APP_PINATA_API_KEY: ${{ secrets.REACT_APP_PINATA_API_KEY }} REACT_APP_PINATA_SECRET_API_KEY: ${{ secrets.REACT_APP_PINATA_SECRET_API_KEY }} diff --git a/src/custom/abis/MerkleDrop.json b/src/custom/abis/MerkleDrop.json new file mode 100644 index 0000000000..f4efc3aa7b --- /dev/null +++ b/src/custom/abis/MerkleDrop.json @@ -0,0 +1,26 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "merkleProof", + "type": "bytes32[]" + } + ], + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] + diff --git a/src/custom/abis/TokenDistro.json b/src/custom/abis/TokenDistro.json new file mode 100644 index 0000000000..cfdff2c797 --- /dev/null +++ b/src/custom/abis/TokenDistro.json @@ -0,0 +1,33 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balances", + "outputs": [ + { + "internalType": "uint256", + "name": "allocatedTokens", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "claimed", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/src/custom/abis/types/MerkleDrop.d.ts b/src/custom/abis/types/MerkleDrop.d.ts new file mode 100644 index 0000000000..ad939bb677 --- /dev/null +++ b/src/custom/abis/types/MerkleDrop.d.ts @@ -0,0 +1,124 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { + ethers, + EventFilter, + Signer, + BigNumber, + BigNumberish, + PopulatedTransaction, + BaseContract, + ContractTransaction, + Overrides, + CallOverrides, +} from "ethers"; +import { BytesLike } from "@ethersproject/bytes"; +import { Listener, Provider } from "@ethersproject/providers"; +import { FunctionFragment, EventFragment, Result } from "@ethersproject/abi"; +import type { TypedEventFilter, TypedEvent, TypedListener } from "./common"; + +interface MerkleDropInterface extends ethers.utils.Interface { + functions: { + "claim(uint256,uint256,bytes32[])": FunctionFragment; + }; + + encodeFunctionData( + functionFragment: "claim", + values: [BigNumberish, BigNumberish, BytesLike[]] + ): string; + + decodeFunctionResult(functionFragment: "claim", data: BytesLike): Result; + + events: {}; +} + +export class MerkleDrop extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + listeners, EventArgsObject>( + eventFilter?: TypedEventFilter + ): Array>; + off, EventArgsObject>( + eventFilter: TypedEventFilter, + listener: TypedListener + ): this; + on, EventArgsObject>( + eventFilter: TypedEventFilter, + listener: TypedListener + ): this; + once, EventArgsObject>( + eventFilter: TypedEventFilter, + listener: TypedListener + ): this; + removeListener, EventArgsObject>( + eventFilter: TypedEventFilter, + listener: TypedListener + ): this; + removeAllListeners, EventArgsObject>( + eventFilter: TypedEventFilter + ): this; + + listeners(eventName?: string): Array; + off(eventName: string, listener: Listener): this; + on(eventName: string, listener: Listener): this; + once(eventName: string, listener: Listener): this; + removeListener(eventName: string, listener: Listener): this; + removeAllListeners(eventName?: string): this; + + queryFilter, EventArgsObject>( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>>; + + interface: MerkleDropInterface; + + functions: { + claim( + index: BigNumberish, + amount: BigNumberish, + merkleProof: BytesLike[], + overrides?: Overrides & { from?: string | Promise } + ): Promise; + }; + + claim( + index: BigNumberish, + amount: BigNumberish, + merkleProof: BytesLike[], + overrides?: Overrides & { from?: string | Promise } + ): Promise; + + callStatic: { + claim( + index: BigNumberish, + amount: BigNumberish, + merkleProof: BytesLike[], + overrides?: CallOverrides + ): Promise; + }; + + filters: {}; + + estimateGas: { + claim( + index: BigNumberish, + amount: BigNumberish, + merkleProof: BytesLike[], + overrides?: Overrides & { from?: string | Promise } + ): Promise; + }; + + populateTransaction: { + claim( + index: BigNumberish, + amount: BigNumberish, + merkleProof: BytesLike[], + overrides?: Overrides & { from?: string | Promise } + ): Promise; + }; +} diff --git a/src/custom/abis/types/TokenDistro.d.ts b/src/custom/abis/types/TokenDistro.d.ts new file mode 100644 index 0000000000..f5e190b9e5 --- /dev/null +++ b/src/custom/abis/types/TokenDistro.d.ts @@ -0,0 +1,141 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { + ethers, + EventFilter, + Signer, + BigNumber, + BigNumberish, + PopulatedTransaction, + BaseContract, + ContractTransaction, + Overrides, + CallOverrides, +} from "ethers"; +import { BytesLike } from "@ethersproject/bytes"; +import { Listener, Provider } from "@ethersproject/providers"; +import { FunctionFragment, EventFragment, Result } from "@ethersproject/abi"; +import type { TypedEventFilter, TypedEvent, TypedListener } from "./common"; + +interface TokenDistroInterface extends ethers.utils.Interface { + functions: { + "balances(address)": FunctionFragment; + "claim()": FunctionFragment; + }; + + encodeFunctionData(functionFragment: "balances", values: [string]): string; + encodeFunctionData(functionFragment: "claim", values?: undefined): string; + + decodeFunctionResult(functionFragment: "balances", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "claim", data: BytesLike): Result; + + events: {}; +} + +export class TokenDistro extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + listeners, EventArgsObject>( + eventFilter?: TypedEventFilter + ): Array>; + off, EventArgsObject>( + eventFilter: TypedEventFilter, + listener: TypedListener + ): this; + on, EventArgsObject>( + eventFilter: TypedEventFilter, + listener: TypedListener + ): this; + once, EventArgsObject>( + eventFilter: TypedEventFilter, + listener: TypedListener + ): this; + removeListener, EventArgsObject>( + eventFilter: TypedEventFilter, + listener: TypedListener + ): this; + removeAllListeners, EventArgsObject>( + eventFilter: TypedEventFilter + ): this; + + listeners(eventName?: string): Array; + off(eventName: string, listener: Listener): this; + on(eventName: string, listener: Listener): this; + once(eventName: string, listener: Listener): this; + removeListener(eventName: string, listener: Listener): this; + removeAllListeners(eventName?: string): this; + + queryFilter, EventArgsObject>( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>>; + + interface: TokenDistroInterface; + + functions: { + balances( + arg0: string, + overrides?: CallOverrides + ): Promise< + [BigNumber, BigNumber] & { + allocatedTokens: BigNumber; + claimed: BigNumber; + } + >; + + claim( + overrides?: Overrides & { from?: string | Promise } + ): Promise; + }; + + balances( + arg0: string, + overrides?: CallOverrides + ): Promise< + [BigNumber, BigNumber] & { allocatedTokens: BigNumber; claimed: BigNumber } + >; + + claim( + overrides?: Overrides & { from?: string | Promise } + ): Promise; + + callStatic: { + balances( + arg0: string, + overrides?: CallOverrides + ): Promise< + [BigNumber, BigNumber] & { + allocatedTokens: BigNumber; + claimed: BigNumber; + } + >; + + claim(overrides?: CallOverrides): Promise; + }; + + filters: {}; + + estimateGas: { + balances(arg0: string, overrides?: CallOverrides): Promise; + + claim( + overrides?: Overrides & { from?: string | Promise } + ): Promise; + }; + + populateTransaction: { + balances( + arg0: string, + overrides?: CallOverrides + ): Promise; + + claim( + overrides?: Overrides & { from?: string | Promise } + ): Promise; + }; +} diff --git a/src/custom/abis/types/factories/MerkleDrop__factory.ts b/src/custom/abis/types/factories/MerkleDrop__factory.ts new file mode 100644 index 0000000000..7c49900b96 --- /dev/null +++ b/src/custom/abis/types/factories/MerkleDrop__factory.ts @@ -0,0 +1,46 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Signer, utils } from "ethers"; +import { Provider } from "@ethersproject/providers"; +import type { MerkleDrop, MerkleDropInterface } from "../MerkleDrop"; + +const _abi = [ + { + inputs: [ + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "bytes32[]", + name: "merkleProof", + type: "bytes32[]", + }, + ], + name: "claim", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +]; + +export class MerkleDrop__factory { + static readonly abi = _abi; + static createInterface(): MerkleDropInterface { + return new utils.Interface(_abi) as MerkleDropInterface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider + ): MerkleDrop { + return new Contract(address, _abi, signerOrProvider) as MerkleDrop; + } +} diff --git a/src/custom/abis/types/factories/TokenDistro__factory.ts b/src/custom/abis/types/factories/TokenDistro__factory.ts new file mode 100644 index 0000000000..fe7e2ce269 --- /dev/null +++ b/src/custom/abis/types/factories/TokenDistro__factory.ts @@ -0,0 +1,54 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Signer, utils } from "ethers"; +import { Provider } from "@ethersproject/providers"; +import type { TokenDistro, TokenDistroInterface } from "../TokenDistro"; + +const _abi = [ + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "balances", + outputs: [ + { + internalType: "uint256", + name: "allocatedTokens", + type: "uint256", + }, + { + internalType: "uint256", + name: "claimed", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "claim", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +]; + +export class TokenDistro__factory { + static readonly abi = _abi; + static createInterface(): TokenDistroInterface { + return new utils.Interface(_abi) as TokenDistroInterface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider + ): TokenDistro { + return new Contract(address, _abi, signerOrProvider) as TokenDistro; + } +} diff --git a/src/custom/abis/types/index.ts b/src/custom/abis/types/index.ts index 4e0d72bd0e..330d8031be 100644 --- a/src/custom/abis/types/index.ts +++ b/src/custom/abis/types/index.ts @@ -2,8 +2,12 @@ /* tslint:disable */ /* eslint-disable */ export type { GPv2Settlement } from "./GPv2Settlement"; +export type { MerkleDrop } from "./MerkleDrop"; +export type { TokenDistro } from "./TokenDistro"; export type { VCow } from "./VCow"; export { GPv2Settlement__factory } from "./factories/GPv2Settlement__factory"; +export { MerkleDrop__factory } from "./factories/MerkleDrop__factory"; +export { TokenDistro__factory } from "./factories/TokenDistro__factory"; export { VCow__factory } from "./factories/VCow__factory"; export * from "@src/abis/types"; diff --git a/src/custom/components/AddToMetamask/index.tsx b/src/custom/components/AddToMetamask/index.tsx index cf2abdd72a..1a9cfed757 100644 --- a/src/custom/components/AddToMetamask/index.tsx +++ b/src/custom/components/AddToMetamask/index.tsx @@ -10,9 +10,10 @@ import MetaMaskLogo from 'assets/images/metamask.png' export type AddToMetamaskProps = { currency: Currency | undefined + shortLabel?: boolean } -const ButtonCustom = styled.button` +export const ButtonCustom = styled.button` display: flex; flex: 1 1 auto; align-self: center; @@ -59,7 +60,7 @@ const CheckCircleCustom = styled(CheckCircle)` ` export default function AddToMetamask(props: AddToMetamaskProps) { - const { currency } = props + const { currency, shortLabel } = props const theme = useContext(ThemeContext) const { library } = useActiveWeb3React() const { addToken, success } = useAddTokenToMetamask(currency) @@ -72,7 +73,7 @@ export default function AddToMetamask(props: AddToMetamaskProps) { {!success ? ( - Add {currency.symbol} to Metamask + {shortLabel ? 'Add token' : `Add ${currency.symbol} to Metamask`} ) : ( diff --git a/src/custom/components/CowBalanceButton/index.tsx b/src/custom/components/CowBalanceButton/index.tsx new file mode 100644 index 0000000000..d90af3b36f --- /dev/null +++ b/src/custom/components/CowBalanceButton/index.tsx @@ -0,0 +1,64 @@ +import { Trans } from '@lingui/macro' +import styled from 'styled-components/macro' +import CowProtocolLogo from 'components/CowProtocolLogo' +import { useCombinedBalance } from 'state/cowToken/hooks' +import { ChainId } from 'state/lists/actions/actionsMod' +import { formatMax, formatSmartLocaleAware } from 'utils/format' +import { AMOUNT_PRECISION } from 'constants/index' +import { COW } from 'constants/tokens' + +export const Wrapper = styled.div` + ${({ theme }) => theme.card.boxShadow}; + color: ${({ theme }) => theme.text1}; + padding: 0 12px; + font-size: 15px; + font-weight: 500; + height: 38px; + display: flex; + align-items: center; + position: relative; + border-radius: 12px; + pointer-events: auto; + + > b { + margin: 0 0 0 5px; + color: inherit; + font-weight: inherit; + white-space: nowrap; + + ${({ theme }) => theme.mediaWidth.upToMedium` + overflow: hidden; + max-width: 100px; + text-overflow: ellipsis; + `}; + + ${({ theme }) => theme.mediaWidth.upToSmall` + overflow: visible; + max-width: initial; + `}; + } +` + +interface CowBalanceButtonProps { + account?: string | null | undefined + chainId: ChainId | undefined + onClick?: () => void +} + +const COW_DECIMALS = COW[ChainId.MAINNET].decimals + +export default function CowBalanceButton({ onClick }: CowBalanceButtonProps) { + const combinedBalance = useCombinedBalance() + + const formattedBalance = formatSmartLocaleAware(combinedBalance, AMOUNT_PRECISION) + const formattedMaxBalance = formatMax(combinedBalance, COW_DECIMALS) + + return ( + + + + {formattedBalance || 0} + + + ) +} diff --git a/src/custom/components/CowClaimButton/index.tsx b/src/custom/components/CowClaimButton/index.tsx deleted file mode 100644 index c703cc91a3..0000000000 --- a/src/custom/components/CowClaimButton/index.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { Trans } from '@lingui/macro' -import styled, { css } from 'styled-components/macro' -import CowProtocolLogo from 'components/CowProtocolLogo' -import { useTokenBalance } from 'state/wallet/hooks' -import { V_COW } from 'constants/tokens' -import { ChainId } from 'state/lists/actions/actionsMod' -import { formatMax, formatSmartLocaleAware } from 'utils/format' -import { AMOUNT_PRECISION } from 'constants/index' - -export const Wrapper = styled.div<{ isClaimPage?: boolean | null }>` - ${({ theme }) => theme.card.boxShadow}; - color: ${({ theme }) => theme.text1}; - padding: 0 12px; - font-size: 15px; - font-weight: 500; - height: 38px; - display: flex; - align-items: center; - position: relative; - border-radius: 12px; - pointer-events: auto; - - > b { - margin: 0 0 0 5px; - color: inherit; - font-weight: inherit; - white-space: nowrap; - - ${({ theme }) => theme.mediaWidth.upToMedium` - overflow: hidden; - max-width: 100px; - text-overflow: ellipsis; - `}; - - ${({ theme }) => theme.mediaWidth.upToSmall` - overflow: visible; - max-width: initial; - `}; - - &::before, - &::after { - content: ''; - position: absolute; - left: -1px; - top: -1px; - background: ${({ theme }) => - `linear-gradient(45deg, ${theme.primary1}, ${theme.primary2}, ${theme.primary3}, ${theme.bg4}, ${theme.primary1}, ${theme.primary2})`}; - background-size: 800%; - width: calc(100% + 2px); - height: calc(100% + 2px); - z-index: -1; - animation: glow 50s linear infinite; - transition: background-position 0.3s ease-in-out; - border-radius: 12px; - } - - &::after { - filter: blur(8px); - } - - &:hover::before, - &:hover::after { - animation: glow 12s linear infinite; - } - - // Stop glowing effect on claim page - ${({ isClaimPage }) => - isClaimPage && - css` - &::before, - &::after { - content: none; - } - `}; - } - - @keyframes glow { - 0% { - background-position: 0 0; - } - 50% { - background-position: 300% 0; - } - 100% { - background-position: 0 0; - } - } -` - -interface CowClaimButtonProps { - isClaimPage?: boolean | null | undefined - account?: string | null | undefined - chainId: ChainId | undefined - handleOnClickClaim?: () => void -} - -export default function CowClaimButton({ isClaimPage, account, chainId, handleOnClickClaim }: CowClaimButtonProps) { - const vCowToken = chainId ? V_COW[chainId] : undefined - const vCowBalance = useTokenBalance(account || undefined, vCowToken) - - const formattedVCowBalance = formatSmartLocaleAware(vCowBalance, AMOUNT_PRECISION) - const formattedMaxVCowBalance = formatMax(vCowBalance, vCowToken?.decimals) - - return ( - - - - {formattedVCowBalance} vCOW - - - ) -} diff --git a/src/custom/components/Header/index.tsx b/src/custom/components/Header/index.tsx index fc1b3bf402..dac674d7c9 100644 --- a/src/custom/components/Header/index.tsx +++ b/src/custom/components/Header/index.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' import { SupportedChainId as ChainId } from 'constants/chains' // import { ExternalLink } from 'theme' -import { useHistory, useLocation } from 'react-router-dom' +import { useHistory } from 'react-router-dom' import HeaderMod, { Title as TitleMod, @@ -44,7 +44,7 @@ import { import Modal from 'components/Modal' // import ClaimModal from 'components/claim/ClaimModal' import UniBalanceContent from 'components/Header/UniBalanceContent' -import CowClaimButton from 'components/CowClaimButton' +import CowBalanceButton from 'components/CowBalanceButton' export const NETWORK_LABELS: { [chainId in ChainId]?: string } = { [ChainId.RINKEBY]: 'Rinkeby', @@ -221,9 +221,6 @@ const VCowWrapper = styled(UNIWrapper)` ` export default function Header() { - const location = useLocation() - const isClaimPage = location.pathname === '/claim' - const { account, chainId: connectedChainId } = useActiveWeb3React() const chainId = supportedChainId(connectedChainId) @@ -242,7 +239,7 @@ export default function Header() { const isMenuOpen = useModalOpen(ApplicationModal.MENU) const history = useHistory() - const handleOnClickClaim = () => history.push('/claim') + const handleBalanceButtonClick = () => history.push('/profile') // Toggle the 'noScroll' class on body, whenever the orders panel or flyout menu is open. // This removes the inner scrollbar on the page body, to prevent showing double scrollbars. @@ -276,12 +273,7 @@ export default function Header() { - + @@ -303,7 +295,7 @@ export default function Header() { {darkMode ? : } - + {isOrdersPanelOpen && } diff --git a/src/custom/components/Menu/index.tsx b/src/custom/components/Menu/index.tsx index 0bb3af64db..b260b0b031 100644 --- a/src/custom/components/Menu/index.tsx +++ b/src/custom/components/Menu/index.tsx @@ -19,7 +19,7 @@ import { ApplicationModal } from 'state/application/reducer' import { getExplorerAddressLink } from 'utils/explorer' import { useHasOrders } from 'api/gnosisProtocol/hooks' import { useHistory } from 'react-router-dom' -import CowClaimButton, { Wrapper as ClaimButtonWrapper } from 'components/CowClaimButton' +import CowBalanceButton, { Wrapper as BalanceButtonWrapper } from 'components/CowBalanceButton' import twitterImage from 'assets/cow-swap/twitter.svg' import discordImage from 'assets/cow-swap/discord.svg' @@ -58,7 +58,7 @@ const MenuItemResponsive = styled(MenuItemResponsiveBase)` } ` -export const StyledMenu = styled(MenuMod)<{ isClaimPage: boolean }>` +export const StyledMenu = styled(MenuMod)` hr { margin: 15px 0; } @@ -98,7 +98,7 @@ export const StyledMenu = styled(MenuMod)<{ isClaimPage: boolean }>` padding: 0 6px 0 0; } - ${ClaimButtonWrapper} { + ${BalanceButtonWrapper} { margin: 0 0 12px; display: none; @@ -272,30 +272,23 @@ export const CloseMenu = styled.button` interface MenuProps { darkMode: boolean toggleDarkMode: () => void - isClaimPage: boolean } -export function Menu({ darkMode, toggleDarkMode, isClaimPage }: MenuProps) { +export function Menu({ darkMode, toggleDarkMode }: MenuProps) { const { account, chainId } = useActiveWeb3React() const hasOrders = useHasOrders(account) const showOrdersLink = account && hasOrders - /* const showVCOWClaimOption = Boolean(!!account && !!chainId) */ const close = useToggleModal(ApplicationModal.MENU) const history = useHistory() - const handleOnClickClaim = () => { + const handleBalanceButtonClick = () => { close() - history.push('/claim') + history.push('/profile') } return ( - + - + Swap diff --git a/src/custom/components/SearchModal/CurrencySearch/CurrencySearchMod.tsx b/src/custom/components/SearchModal/CurrencySearch/CurrencySearchMod.tsx index 66782c9c5a..382ffceff9 100644 --- a/src/custom/components/SearchModal/CurrencySearch/CurrencySearchMod.tsx +++ b/src/custom/components/SearchModal/CurrencySearch/CurrencySearchMod.tsx @@ -24,7 +24,7 @@ import CommonBases from 'components/SearchModal/CommonBases' import CurrencyList from 'components/SearchModal/CurrencyList' import { filterTokens, useSortedTokensByQuery } from 'components/SearchModal/filtering' import ImportRow from 'components/SearchModal/ImportRow' -import { useTokenComparator } from 'components/SearchModal/sorting' +import { useTokenComparator } from './sorting' import { PaddedColumn, SearchInput, Separator } from 'components/SearchModal/styleds' import useNetworkName from 'hooks/useNetworkName' import { ContentWrapper } from '.' //mod diff --git a/src/custom/components/SearchModal/CurrencySearch/sorting.ts b/src/custom/components/SearchModal/CurrencySearch/sorting.ts new file mode 100644 index 0000000000..eb9452ebf9 --- /dev/null +++ b/src/custom/components/SearchModal/CurrencySearch/sorting.ts @@ -0,0 +1,63 @@ +import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' +import { useMemo } from 'react' + +import { useAllTokenBalances } from 'state/wallet/hooks' + +const PRIORITISED_TOKENS = ['COW', 'GNO'] + +// compare two token amounts with highest one coming first +function balanceComparator(balanceA?: CurrencyAmount, balanceB?: CurrencyAmount) { + if (balanceA && balanceB) { + return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1 + } else if (balanceA && balanceA.greaterThan('0')) { + return -1 + } else if (balanceB && balanceB.greaterThan('0')) { + return 1 + } + return 0 +} + +function getTokenComparator(balances: { + [tokenAddress: string]: CurrencyAmount | undefined +}): (tokenA: Token, tokenB: Token) => number { + return function sortTokens(tokenA: Token, tokenB: Token): number { + // -1 = a is first + // 1 = b is first + + // sort by balances + const balanceA = balances[tokenA.address] + const balanceB = balances[tokenB.address] + + const balanceComp = balanceComparator(balanceA, balanceB) + if (balanceComp !== 0) return balanceComp + + // Mod: modify tokens list by prioritised list + const indexA = PRIORITISED_TOKENS.indexOf(tokenA.symbol || '') + const indexB = PRIORITISED_TOKENS.indexOf(tokenB.symbol || '') + + if (indexA !== -1 && indexB !== -1) { + return indexB < indexA ? 1 : -1 + } else if (indexA !== -1 || indexB !== -1) { + return indexB !== -1 ? 1 : -1 + } + + if (tokenA.symbol && tokenB.symbol) { + // sort by symbol + return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1 + } else { + return tokenA.symbol ? -1 : tokenB.symbol ? -1 : 0 + } + } +} + +export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number { + const balances = useAllTokenBalances() + const comparator = useMemo(() => getTokenComparator(balances ?? {}), [balances]) + return useMemo(() => { + if (inverted) { + return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1 + } else { + return comparator + } + }, [inverted, comparator]) +} diff --git a/src/custom/constants/appDataHash.ts b/src/custom/constants/appDataHash.ts index 2b06f0e453..c2d373a2ab 100644 --- a/src/custom/constants/appDataHash.ts +++ b/src/custom/constants/appDataHash.ts @@ -28,5 +28,5 @@ export function getAppDataHash(): string { default: break } - return 'no' + return DEFAULT_APP_DATA } diff --git a/src/custom/constants/index.ts b/src/custom/constants/index.ts index 63264bca6b..80e528d109 100644 --- a/src/custom/constants/index.ts +++ b/src/custom/constants/index.ts @@ -97,7 +97,7 @@ export const XDAI_LOGO_URI = export const DOCS_LINK = 'https://docs.cow.fi' export const CONTRACTS_CODE_LINK = 'https://github.com/gnosis/gp-v2-contracts' export const CODE_LINK = 'https://github.com/gnosis/gp-swap-ui' -export const DISCORD_LINK = 'https://chat.cowswap.exchange' +export const DISCORD_LINK = 'https://discord.com/invite/cowprotocol' export const DUNE_DASHBOARD_LINK = 'https://duneanalytics.com/gnosis.protocol/Gnosis-Protocol-V2' export const TWITTER_LINK = 'https://twitter.com/mevprotection' export const GPAUDIT_LINK = 'https://github.com/gnosis/gp-v2-contracts/blob/main/audits/GnosisProtocolV2May2021.pdf' diff --git a/src/custom/constants/routing/routingMod.ts b/src/custom/constants/routing/routingMod.ts index 91eff9bc7f..2fd61d5298 100644 --- a/src/custom/constants/routing/routingMod.ts +++ b/src/custom/constants/routing/routingMod.ts @@ -23,6 +23,7 @@ import { // WBTC_ARBITRUM_ONE, // WBTC_OPTIMISM, WETH9_EXTENDED, + COW, } from 'constants/tokens' import { USDC_XDAI, /* USDT_XDAI, */ WBTC_XDAI, WETH_XDAI } from 'utils/xdai/constants' @@ -80,6 +81,7 @@ export const COMMON_BASES: ChainCurrencyList = { [SupportedChainId.MAINNET]: [ // ExtendedEther.onChain(SupportedChainId.MAINNET), DAI, + COW[SupportedChainId.MAINNET], USDC, USDT, WBTC, @@ -92,6 +94,7 @@ export const COMMON_BASES: ChainCurrencyList = { [SupportedChainId.RINKEBY]: [ // ExtendedEther.onChain(SupportedChainId.RINKEBY), WETH9_EXTENDED[SupportedChainId.RINKEBY], + COW[SupportedChainId.RINKEBY], DAI_RINKEBY, USDC_RINKEBY, USDT_RINKEBY, @@ -120,6 +123,7 @@ export const COMMON_BASES: ChainCurrencyList = { [SupportedChainId.XDAI]: [ // ExtendedEther.onChain(SupportedChainId.XDA), USDC_XDAI, + COW[SupportedChainId.XDAI], /*USDT_XDAI,*/ WBTC_XDAI, WETH9_EXTENDED[100], WETH_XDAI, diff --git a/src/custom/hooks/useCowBalanceAndSubsidy.ts b/src/custom/hooks/useCowBalanceAndSubsidy.ts index b44931bab7..34b58bbee4 100644 --- a/src/custom/hooks/useCowBalanceAndSubsidy.ts +++ b/src/custom/hooks/useCowBalanceAndSubsidy.ts @@ -4,7 +4,7 @@ import { JSBI } from '@uniswap/sdk' import { CurrencyAmount } from '@uniswap/sdk-core' import { getDiscountFromBalance } from 'components/CowSubsidyModal/utils' -import { useVCowData } from 'state/claim/hooks' +import { useVCowData } from 'state/cowToken/hooks' import { useTokenBalance } from 'state/wallet/hooks' import { useActiveWeb3React } from '.' diff --git a/src/custom/pages/App/index.tsx b/src/custom/pages/App/index.tsx index da8b798319..aac12b79fc 100644 --- a/src/custom/pages/App/index.tsx +++ b/src/custom/pages/App/index.tsx @@ -2,7 +2,7 @@ import AppMod from './AppMod' import styled from 'styled-components/macro' import { RedirectPathToSwapOnly, RedirectToSwap } from 'pages/Swap/redirects' import { Suspense, lazy } from 'react' -import { Route, Switch } from 'react-router-dom' +import { Redirect, Route, Switch } from 'react-router-dom' import AnySwapAffectedUsers from 'pages/error/AnySwapAffectedUsers' import * as Sentry from '@sentry/react' @@ -17,7 +17,6 @@ const SENTRY_DSN = process.env.REACT_APP_SENTRY_DSN const SENTRY_TRACES_SAMPLE_RATE = process.env.REACT_APP_SENTRY_TRACES_SAMPLE_RATE const Swap = lazy(() => import(/* webpackPrefetch: true, webpackChunkName: "swap" */ 'pages/Swap')) -const Claim = lazy(() => import(/* webpackChunkName: "claim" */ 'pages/Claim')) const PrivacyPolicy = lazy(() => import(/* webpackChunkName: "privacy_policy" */ 'pages/PrivacyPolicy')) const CookiePolicy = lazy(() => import(/* webpackChunkName: "cookie_policy" */ 'pages/CookiePolicy')) const TermsAndConditions = lazy(() => import(/* webpackChunkName: "terms" */ 'pages/TermsAndConditions')) @@ -99,10 +98,10 @@ export default function App() { + - @@ -111,7 +110,6 @@ export default function App() { - diff --git a/src/custom/pages/Profile/index.tsx b/src/custom/pages/Profile/index.tsx index 80fe448ad8..cf70250be9 100644 --- a/src/custom/pages/Profile/index.tsx +++ b/src/custom/pages/Profile/index.tsx @@ -15,6 +15,7 @@ import { ExtLink, CardsWrapper, Card, + CardActions, BannerCard, BalanceDisplay, ConvertWrapper, @@ -26,6 +27,7 @@ import { RefreshCcw } from 'react-feather' import Web3Status from 'components/Web3Status' import useReferralLink from 'hooks/useReferralLink' import useFetchProfile from 'hooks/useFetchProfile' +import { getBlockExplorerUrl } from 'utils' import { formatMax, formatSmartLocaleAware, numberFormatter } from 'utils/format' import { getExplorerAddressLink } from 'utils/explorer' import useTimeAgo from 'hooks/useTimeAgo' @@ -42,28 +44,29 @@ import ArrowIcon from 'assets/cow-swap/arrow.svg' import CowImage from 'assets/cow-swap/cow_v2.svg' import CowProtocolImage from 'assets/cow-swap/cowprotocol.svg' import { useTokenBalance } from 'state/wallet/hooks' -import { useVCowData } from 'state/claim/hooks' -import { AMOUNT_PRECISION } from 'constants/index' +import { useVCowData, useSwapVCowCallback, useSetSwapVCowStatus, useSwapVCowStatus } from 'state/cowToken/hooks' +import { V_COW_CONTRACT_ADDRESS, COW_CONTRACT_ADDRESS, AMOUNT_PRECISION } from 'constants/index' import { COW } from 'constants/tokens' import { useErrorModal } from 'hooks/useErrorMessageAndModal' import { OperationType } from 'components/TransactionConfirmationModal' import useTransactionConfirmationModal from 'hooks/useTransactionConfirmationModal' -import { useClaimDispatchers, useClaimState } from 'state/claim/hooks' -import { SwapVCowStatus } from 'state/claim/actions' -import { useSwapVCowCallback } from 'state/claim/hooks' +import { SwapVCowStatus } from 'state/cowToken/actions' +import AddToMetamask from 'components/AddToMetamask' +import { Link } from 'react-router-dom' +import CopyHelper from 'components/Copy' const COW_DECIMALS = COW[ChainId.MAINNET].decimals export default function Profile() { const referralLink = useReferralLink() - const { account, chainId } = useActiveWeb3React() + const { account, chainId = ChainId.MAINNET, library } = useActiveWeb3React() const { profileData, isLoading, error } = useFetchProfile() const lastUpdated = useTimeAgo(profileData?.lastUpdated) const isTradesTooltipVisible = account && chainId == 1 && !!profileData?.totalTrades const hasOrders = useHasOrders(account) - const { setSwapVCowStatus } = useClaimDispatchers() - const { swapVCowStatus } = useClaimState() + const setSwapVCowStatus = useSetSwapVCowStatus() + const swapVCowStatus = useSwapVCowStatus() // Cow balance const cow = useTokenBalance(account || undefined, chainId ? COW[chainId] : undefined) @@ -159,6 +162,8 @@ export default function Profile() { ) + const currencyCOW = COW[chainId] + return ( @@ -202,6 +207,15 @@ export default function Profile() { )} + + + + View contract ↗ + + +
Copy contract
+
+
)} @@ -213,6 +227,21 @@ export default function Profile() { {cowBalance} COW + + + View contract ↗ + + + {library?.provider?.isMetaMask && } + + {!library?.provider?.isMetaMask && ( + +
Copy contract
+
+ )} + + Buy COW +
@@ -222,7 +251,7 @@ export default function Profile() { {' '} View proposals ↗ - CoW Forum ↗ + CoW forum ↗ @@ -255,7 +284,7 @@ export default function Profile() { )} {hasOrders && ( - + View all orders ↗ )} diff --git a/src/custom/pages/Profile/styled.tsx b/src/custom/pages/Profile/styled.tsx index b9034850f5..43b41966ab 100644 --- a/src/custom/pages/Profile/styled.tsx +++ b/src/custom/pages/Profile/styled.tsx @@ -5,6 +5,8 @@ import { BannerExplainer } from 'pages/Claim/styled' import * as CSS from 'csstype' import { transparentize } from 'polished' import { ExternalLink } from 'theme' +import { ButtonCustom as AddToMetaMask } from 'components/AddToMetamask' +import { CopyIcon as ClickToCopy } from 'components/Copy' export const Container = styled.div` max-width: 910px; @@ -263,6 +265,7 @@ export const Card = styled.div<{ showLoader?: boolean }>` gap: 24px 0; border-radius: 16px; border: 1px solid ${({ theme }) => theme.cardBorder}; + align-items: flex-end; ${({ showLoader, theme }) => showLoader && @@ -429,6 +432,79 @@ export const BannerCard = styled(BannerExplainer)` } ` +export const CardActions = styled.div` + width: 100%; + display: flex; + flex-flow: row wrap; + justify-content: space-between; + align-items: flex-end; + margin: auto 0 0; + + ${({ theme }) => theme.mediaWidth.upToMedium` + justify-content: center; + align-items: center; + flex-flow: column wrap; + gap: 32px 0; + margin: 12px 0; + `}; + + > a, + ${AddToMetaMask}, > ${ClickToCopy} { + font-size: 13px; + height: 100%; + font-weight: 500; + border-radius: 0; + min-height: initial; + margin: 0; + padding: 0; + line-height: 1; + color: ${({ theme }) => theme.text1}; + display: flex; + align-items: center; + text-decoration: underline; + text-decoration-color: transparent; + transition: text-decoration-color 0.2s ease-in-out, color 0.2s ease-in-out; + + ${({ theme }) => theme.mediaWidth.upToMedium` + font-size: 15px; + margin: 0 auto; + `}; + + &:hover { + text-decoration-color: ${({ theme }) => theme.primary1}; + color: ${({ theme }) => theme.primary1}; + } + } + + ${AddToMetaMask} { + border: 0; + min-height: initial; + border-radius: initial; + + &:hover { + background: transparent; + + > div { + text-decoration: underline; + } + } + + > div > img, + > div > svg { + height: 13px; + width: auto; + object-fit: contain; + margin: 0 6px 0 0; + } + } + + > ${ClickToCopy} svg { + height: 13px; + width: auto; + margin: 0 4px 0 0; + } +` + export const BalanceDisplay = styled.div<{ titleSize?: number; altColor?: boolean; hAlign?: string }>` display: flex; flex-flow: row wrap; diff --git a/src/custom/state/cowToken/actions.ts b/src/custom/state/cowToken/actions.ts new file mode 100644 index 0000000000..ea1f01f1d4 --- /dev/null +++ b/src/custom/state/cowToken/actions.ts @@ -0,0 +1,13 @@ +import { createAction } from '@reduxjs/toolkit' + +export enum SwapVCowStatus { + INITIAL = 'INITIAL', + ATTEMPTING = 'ATTEMPTING', + SUBMITTED = 'SUBMITTED', +} + +export type CowTokenActions = { + setSwapVCowStatus: (payload: SwapVCowStatus) => void +} + +export const setSwapVCowStatus = createAction('cowToken/setSwapVCowStatus') diff --git a/src/custom/state/cowToken/hooks.ts b/src/custom/state/cowToken/hooks.ts new file mode 100644 index 0000000000..a57cbca391 --- /dev/null +++ b/src/custom/state/cowToken/hooks.ts @@ -0,0 +1,179 @@ +import { useCallback, useMemo } from 'react' + +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { TransactionResponse } from '@ethersproject/providers' + +import { useVCowContract } from 'hooks/useContract' +import { useActiveWeb3React } from 'hooks/web3' +import { useSingleCallResult, Result } from 'state/multicall/hooks' +import { useTransactionAdder } from 'state/enhancedTransactions/hooks' +import { V_COW, COW } from 'constants/tokens' +import { AppState } from 'state' +import { useAppDispatch, useAppSelector } from 'state/hooks' +import { setSwapVCowStatus, SwapVCowStatus } from './actions' +import { OperationType } from 'components/TransactionConfirmationModal' +import { APPROVE_GAS_LIMIT_DEFAULT } from 'hooks/useApproveCallback/useApproveCallbackMod' +import { useTokenBalance } from 'state/wallet/hooks' + +export type SetSwapVCowStatusCallback = (payload: SwapVCowStatus) => void + +type VCowData = { + isLoading: boolean + total: CurrencyAmount | undefined | null + unvested: CurrencyAmount | undefined | null + vested: CurrencyAmount | undefined | null +} + +interface SwapVCowCallbackParams { + openModal: (message: string, operationType: OperationType) => void + closeModal: () => void +} + +/** + * Hook that parses the result input with BigNumber value to CurrencyAmount + */ +function useParseVCowResult(result: Result | undefined) { + const { chainId } = useActiveWeb3React() + + const vCowToken = chainId ? V_COW[chainId] : undefined + + return useMemo(() => { + if (!chainId || !vCowToken || !result) { + return + } + + return CurrencyAmount.fromRawAmount(vCowToken, result[0].toString()) + }, [chainId, result, vCowToken]) +} + +/** + * Hook that fetches the needed vCow data and returns it in VCowData type + */ +export function useVCowData(): VCowData { + const vCowContract = useVCowContract() + const { account } = useActiveWeb3React() + + const { loading: isVestedLoading, result: vestedResult } = useSingleCallResult(vCowContract, 'swappableBalanceOf', [ + account ?? undefined, + ]) + const { loading: isTotalLoading, result: totalResult } = useSingleCallResult(vCowContract, 'balanceOf', [ + account ?? undefined, + ]) + + const vested = useParseVCowResult(vestedResult) + const total = useParseVCowResult(totalResult) + + const unvested = useMemo(() => { + if (!total || !vested) { + return null + } + + // Check if total < vested, if it is something is probably wrong and we return null + if (total.lessThan(vested)) { + return null + } + + return total.subtract(vested) + }, [total, vested]) + + const isLoading = isVestedLoading || isTotalLoading + + return { isLoading, vested, unvested, total } +} + +/** + * Hook used to swap vCow to Cow token + */ +export function useSwapVCowCallback({ openModal, closeModal }: SwapVCowCallbackParams) { + const { chainId, account } = useActiveWeb3React() + const vCowContract = useVCowContract() + + const addTransaction = useTransactionAdder() + const vCowToken = chainId ? V_COW[chainId] : undefined + + const swapCallback = useCallback(async () => { + if (!account) { + throw new Error('Not connected') + } + if (!chainId) { + throw new Error('No chainId') + } + if (!vCowContract) { + throw new Error('vCOW contract not present') + } + if (!vCowToken) { + throw new Error('vCOW token not present') + } + + const estimatedGas = await vCowContract.estimateGas.swapAll({ from: account }).catch(() => { + // general fallback for tokens who restrict approval amounts + return vCowContract.estimateGas.swapAll().catch((error) => { + console.log( + '[useSwapVCowCallback] Error estimating gas for swapAll. Using default gas limit ' + + APPROVE_GAS_LIMIT_DEFAULT.toString(), + error + ) + return APPROVE_GAS_LIMIT_DEFAULT + }) + }) + + const summary = `Convert vCOW to COW` + openModal(summary, OperationType.CONVERT_VCOW) + + return vCowContract + .swapAll({ from: account, gasLimit: estimatedGas }) + .then((tx: TransactionResponse) => { + addTransaction({ + swapVCow: true, + hash: tx.hash, + summary, + }) + }) + .finally(closeModal) + }, [account, addTransaction, chainId, closeModal, openModal, vCowContract, vCowToken]) + + return { + swapCallback, + } +} + +/** + * Hook that sets the swap vCow->Cow status + */ +export function useSetSwapVCowStatus(): SetSwapVCowStatusCallback { + const dispatch = useAppDispatch() + return useCallback((payload: SwapVCowStatus) => dispatch(setSwapVCowStatus(payload)), [dispatch]) +} + +/** + * Hook that gets swap vCow->Cow status + */ +export function useSwapVCowStatus() { + return useAppSelector((state: AppState) => state.cowToken.swapVCowStatus) +} + +/** + * Hook that returns COW balance + */ +export function useCowBalance() { + const { chainId, account } = useActiveWeb3React() + const cowToken = chainId ? COW[chainId] : undefined + return useTokenBalance(account || undefined, cowToken) +} + +/** + * Hook that returns combined vCOW + COW balance + */ +export function useCombinedBalance() { + const { total: vCowBalance } = useVCowData() + const cowBalance = useCowBalance() + + return useMemo(() => { + if (!vCowBalance || !cowBalance) { + return + } + + const sum = vCowBalance.asFraction.add(cowBalance.asFraction) + return CurrencyAmount.fromRawAmount(cowBalance.currency, sum.quotient) + }, [cowBalance, vCowBalance]) +} diff --git a/src/custom/state/cowToken/middleware.ts b/src/custom/state/cowToken/middleware.ts new file mode 100644 index 0000000000..61921df89a --- /dev/null +++ b/src/custom/state/cowToken/middleware.ts @@ -0,0 +1,44 @@ +import { isAnyOf, Middleware } from '@reduxjs/toolkit' +import { AppState } from 'state' +import { finalizeTransaction } from '../enhancedTransactions/actions' +import { setSwapVCowStatus, SwapVCowStatus } from './actions' +import { getCowSoundSuccess, getCowSoundError } from 'utils/sound' + +const isFinalizeTransaction = isAnyOf(finalizeTransaction) + +// Watch for swapVCow tx being finalized and triggers a change of status +export const cowTokenMiddleware: Middleware, AppState> = (store) => (next) => (action) => { + const result = next(action) + + let cowSound + + if (isFinalizeTransaction(action)) { + const { chainId, hash } = action.payload + const transaction = store.getState().transactions[chainId][hash] + + if (transaction.swapVCow) { + const status = transaction.receipt?.status + + console.debug( + `[stat:swapVCow:middleware] Convert vCOW to COW transaction finalized with status ${status}`, + transaction.hash + ) + + store.dispatch(setSwapVCowStatus(SwapVCowStatus.INITIAL)) + + if (status === 1 && transaction.replacementType !== 'cancel') { + cowSound = getCowSoundSuccess() + } else { + cowSound = getCowSoundError() + } + } + } + + if (cowSound) { + cowSound.play().catch((e) => { + console.error('🐮 [middleware::swapVCow] Moooooo cannot be played', e) + }) + } + + return result +} diff --git a/src/custom/state/cowToken/reducer.ts b/src/custom/state/cowToken/reducer.ts new file mode 100644 index 0000000000..3616f3e684 --- /dev/null +++ b/src/custom/state/cowToken/reducer.ts @@ -0,0 +1,16 @@ +import { createReducer } from '@reduxjs/toolkit' +import { SwapVCowStatus, setSwapVCowStatus } from './actions' + +export type CowTokenState = { + swapVCowStatus: SwapVCowStatus +} + +export const initialState: CowTokenState = { + swapVCowStatus: SwapVCowStatus.INITIAL, +} + +export default createReducer(initialState, (builder) => + builder.addCase(setSwapVCowStatus, (state, { payload }) => { + state.swapVCowStatus = payload + }) +) diff --git a/src/custom/state/index.ts b/src/custom/state/index.ts index c82096b1b1..27a2468722 100644 --- a/src/custom/state/index.ts +++ b/src/custom/state/index.ts @@ -24,9 +24,10 @@ import { updateVersion } from 'state/global/actions' import affiliate from 'state/affiliate/reducer' import enhancedTransactions from 'state/enhancedTransactions/reducer' import claim from 'state/claim/reducer' +import cowToken from 'state/cowToken/reducer' import { popupMiddleware, soundMiddleware } from './orders/middleware' -import { claimMinedMiddleware } from './claim/middleware' +import { cowTokenMiddleware } from 'state/cowToken/middleware' import { DEFAULT_NETWORK_FOR_LISTS } from 'constants/lists' const UNISWAP_REDUCERS = { @@ -54,6 +55,7 @@ const reducers = { affiliate, profile, claim, + cowToken, } const PERSISTED_KEYS: string[] = ['user', 'transactions', 'orders', 'lists', 'gas', 'affiliate', 'profile'] @@ -66,7 +68,7 @@ const store = configureStore({ // .concat(routingApi.middleware) .concat(save({ states: PERSISTED_KEYS, debounce: 1000 })) .concat(popupMiddleware) - .concat(claimMinedMiddleware) + .concat(cowTokenMiddleware) .concat(soundMiddleware), preloadedState: load({ states: PERSISTED_KEYS, disableWarnings: process.env.NODE_ENV === 'test' }), }) diff --git a/src/custom/state/sentry/updater/index.ts b/src/custom/state/sentry/updater/index.ts new file mode 100644 index 0000000000..9ee024e993 --- /dev/null +++ b/src/custom/state/sentry/updater/index.ts @@ -0,0 +1,49 @@ +import { useEffect } from 'react' +import * as Sentry from '@sentry/browser' + +import useIsWindowVisible from 'hooks/useIsWindowVisible' +import { useSwapState } from 'state/swap/hooks' +import { SupportedChainId } from 'constants/chains' +import { useCurrency } from 'hooks/Tokens' +import { useWalletInfo } from 'hooks/useWalletInfo' + +export default function Updater(): null { + const { account, chainId, walletName, isSupportedWallet } = useWalletInfo() + const windowVisible = useIsWindowVisible() + + const { + INPUT: { currencyId: sellTokenAddress }, + OUTPUT: { currencyId: buyTokenAddress }, + } = useSwapState() + + const sellCurrency = useCurrency(sellTokenAddress) + const buyCurrency = useCurrency(buyTokenAddress) + + // create sentry context based on "main" parameters + useEffect(() => { + if (windowVisible) { + Sentry.setContext('user', { + // userAddress: account || 'DISCONNECTED', // TODO: validate with legal + wallet: walletName, + network: chainId ? SupportedChainId[chainId] : chainId, + sellToken: `${sellTokenAddress} <${sellCurrency?.symbol}>`, + buyToken: `${buyTokenAddress} <${buyCurrency?.symbol}>`, + }) + } + }, [ + // user + account, + chainId, + walletName, + isSupportedWallet, + // tokens + sellTokenAddress, + buyTokenAddress, + buyCurrency?.symbol, + sellCurrency?.symbol, + // window visibility check + windowVisible, + ]) + + return null +} diff --git a/src/custom/utils/environments.test.ts b/src/custom/utils/environments.test.ts index ddabae9338..d7d59b4ff0 100644 --- a/src/custom/utils/environments.test.ts +++ b/src/custom/utils/environments.test.ts @@ -79,8 +79,8 @@ describe('Detect environments using host and path', () => { describe('Is PR', () => { const isPr = { ...DEFAULT_ENVIRONMENTS_CHECKS, isPr: true } - it('pr--gpswapui.review.gnosisdev.com', () => { - expect(checkEnvironment('pr1291--gpswapui.review.gnosisdev.com', '')).toEqual(isPr) + it('pr--cowswap.review.gnosisdev.com', () => { + expect(checkEnvironment('pr1291--cowswap.review.gnosisdev.com', '')).toEqual(isPr) }) }) diff --git a/src/custom/utils/index.ts b/src/custom/utils/index.ts index 4240cd8d1e..e5900abef5 100644 --- a/src/custom/utils/index.ts +++ b/src/custom/utils/index.ts @@ -80,6 +80,18 @@ function getBlockscoutUrl(chainId: ChainId, data: string, type: BlockExplorerLin return `https://blockscout.com/${getBlockscoutUrlPrefix(chainId)}/${getBlockscoutUrlSuffix(type, data)}` } +// Get the right block explorer URL by chainId +export function getBlockExplorerUrl(chainId: ChainId, data: string, type: BlockExplorerLinkType): string { + switch (chainId) { + // Check if chain is xDAI to use Blockscout + case ChainId.XDAI: + return getBlockscoutUrl(chainId, data, type) + // Otherwise always use Etherscan for other chains + default: + return getEtherscanUrl(chainId, data, type) + } +} + export function isGpOrder(data: string, type: BlockExplorerLinkType) { return type === 'transaction' && data.length === GP_ORDER_ID_LENGTH } diff --git a/src/custom/utils/price.ts b/src/custom/utils/price.ts index d2a8c53925..8998c0d4fb 100644 --- a/src/custom/utils/price.ts +++ b/src/custom/utils/price.ts @@ -236,11 +236,9 @@ export async function getBestPrice(params: PriceQuoteParams, options?: GetBestPr matcha0xPriceResult, ]) - const { baseToken, quoteToken } = params - const sentryError = new Error() Object.assign(sentryError, priceQuoteError, { - message: `Error querying best price from APIs - baseToken: ${baseToken}, quoteToken: ${quoteToken}`, + message: `Error querying best price from APIs`, name: 'PriceErrorObject', }) diff --git a/src/index.tsx b/src/index.tsx index 5e6091dac3..1338e67706 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -22,6 +22,8 @@ import EnhancedTransactionUpdater from 'state/enhancedTransactions/updater' import UserUpdater from 'state/user/updater' import FeesUpdater from 'state/price/updater' import GasUpdater from 'state/gas/updater' +import SentryUpdater from 'state/sentry/updater' + import { GpOrdersUpdater, CancelledOrdersUpdater, @@ -62,6 +64,7 @@ function Updaters() { + ) }