diff --git a/src/custom/assets/cow-swap/ammslogo/1inch.png b/src/custom/assets/cow-swap/ammslogo/1inch.png new file mode 100644 index 0000000000..8095b0422a Binary files /dev/null and b/src/custom/assets/cow-swap/ammslogo/1inch.png differ diff --git a/src/custom/assets/cow-swap/ammslogo/baoswap.png b/src/custom/assets/cow-swap/ammslogo/baoswap.png new file mode 100644 index 0000000000..525be48398 Binary files /dev/null and b/src/custom/assets/cow-swap/ammslogo/baoswap.png differ diff --git a/src/custom/assets/cow-swap/ammslogo/honeyswap.png b/src/custom/assets/cow-swap/ammslogo/honeyswap.png new file mode 100644 index 0000000000..9505ec2813 Binary files /dev/null and b/src/custom/assets/cow-swap/ammslogo/honeyswap.png differ diff --git a/src/custom/assets/cow-swap/ammslogo/paraswap.png b/src/custom/assets/cow-swap/ammslogo/paraswap.png new file mode 100644 index 0000000000..b8c0fae2bb Binary files /dev/null and b/src/custom/assets/cow-swap/ammslogo/paraswap.png differ diff --git a/src/custom/assets/cow-swap/ammslogo/sushi.png b/src/custom/assets/cow-swap/ammslogo/sushi.png new file mode 100644 index 0000000000..86051295a7 Binary files /dev/null and b/src/custom/assets/cow-swap/ammslogo/sushi.png differ diff --git a/src/custom/assets/cow-swap/ammslogo/swapr.png b/src/custom/assets/cow-swap/ammslogo/swapr.png new file mode 100644 index 0000000000..9bb9324966 Binary files /dev/null and b/src/custom/assets/cow-swap/ammslogo/swapr.png differ diff --git a/src/custom/assets/cow-swap/ammslogo/uniswap.png b/src/custom/assets/cow-swap/ammslogo/uniswap.png new file mode 100644 index 0000000000..181d44db92 Binary files /dev/null and b/src/custom/assets/cow-swap/ammslogo/uniswap.png differ diff --git a/src/custom/components/AMMsLogo/index.tsx b/src/custom/components/AMMsLogo/index.tsx new file mode 100644 index 0000000000..e84510fe2a --- /dev/null +++ b/src/custom/components/AMMsLogo/index.tsx @@ -0,0 +1,86 @@ +import styled from 'styled-components/macro' +import Sushi from 'assets/cow-swap/ammslogo/sushi.png' +import Paraswap from 'assets/cow-swap/ammslogo/paraswap.png' +import Oneinch from 'assets/cow-swap/ammslogo/1inch.png' +import Uniswap from 'assets/cow-swap/ammslogo/uniswap.png' +import Baoswap from 'assets/cow-swap/ammslogo/baoswap.png' +import Honeyswap from 'assets/cow-swap/ammslogo/honeyswap.png' +import Swapr from 'assets/cow-swap/ammslogo/swapr.png' +import { SupportedChainId } from 'constants/chains' + +export const Wrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 100%; + box-shadow: 0px 0px 10px 2px ${({ theme }) => theme.bg1}; + background-color: ${({ theme }) => theme.white}; + transform-style: preserve-3d; + position: absolute; + top: -4px; + right: 0px; + + img { + position: absolute; + left: 0; + animation: FadeInOut 6s infinite; + } + @keyframes FadeInOut { + 0% { + opacity: 1; + } + 17% { + opacity: 1; + } + 25% { + opacity: 0; + } + 92% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + + img:nth-of-type(1) { + animation-delay: 6s; + } + img:nth-of-type(2) { + animation-delay: 4.5s; + } + img:nth-of-type(3) { + animation-delay: 3s; + } + img:nth-of-type(4) { + animation-delay: 1.5s; + } +` + +type Image = { src: string; alt: string } + +const SushiImage = { src: Sushi, alt: 'AMMs Sushiswap' } +const OneInchImage = { src: Oneinch, alt: 'AMMs 1inch' } +const ParaswapImage = { src: Paraswap, alt: 'AMMs Paraswap' } +const UniswapImage = { src: Uniswap, alt: 'AMMs Uniswap' } +const BaoswapImage = { src: Baoswap, alt: 'AMMs Baoswap' } +const HoneyswapImage = { src: Honeyswap, alt: 'AMMs Honeyswap' } +const SwaprImage = { src: Swapr, alt: 'AMMs Swapr' } + +const LogosPerNetwork: Record> = { + [SupportedChainId.MAINNET]: [SushiImage, OneInchImage, ParaswapImage, UniswapImage], + [SupportedChainId.RINKEBY]: [SushiImage, OneInchImage, ParaswapImage, UniswapImage], + [SupportedChainId.XDAI]: [SushiImage, BaoswapImage, HoneyswapImage, SwaprImage], +} + +export function AMMsLogo({ chainId }: { chainId: SupportedChainId }) { + return ( + + {LogosPerNetwork[chainId].map(({ src, alt }, index) => ( + {alt} + ))} + + ) +} diff --git a/src/custom/components/AccountDetails/Transaction/ActivityDetails.tsx b/src/custom/components/AccountDetails/Transaction/ActivityDetails.tsx index 9d53c0b277..debde2abd2 100644 --- a/src/custom/components/AccountDetails/Transaction/ActivityDetails.tsx +++ b/src/custom/components/AccountDetails/Transaction/ActivityDetails.tsx @@ -8,7 +8,6 @@ import { Summary, SummaryInner, SummaryInnerRow, - TransactionAlertMessage, TransactionInnerDetail, TextAlert, TransactionState as ActivityLink, @@ -21,11 +20,10 @@ import { DEFAULT_PRECISION, V_COW_CONTRACT_ADDRESS } from 'constants/index' import { ActivityDerivedState } from './index' import { GnosisSafeLink } from './StatusDetails' import CurrencyLogo from 'components/CurrencyLogo' -import AttentionIcon from 'assets/cow-swap/attention.svg' import { useToken } from 'hooks/Tokens' -import SVG from 'react-inlinesvg' import { ActivityStatus } from 'hooks/useRecentActivity' import { V_COW, COW } from 'constants/tokens' +import { OrderProgressBar } from './OrderProgressBar' const DEFAULT_ORDER_SUMMARY = { from: '', @@ -34,17 +32,6 @@ const DEFAULT_ORDER_SUMMARY = { validTo: '', } -function unfillableAlert(): JSX.Element { - return ( - <> - - - Limit price out of range: Wait for a matching price or cancel your order. - - - ) -} - function GnosisSafeTxDetails(props: { chainId: number activityDerivedState: ActivityDerivedState @@ -159,8 +146,7 @@ export function ActivityDetails(props: { creationTime?: string | undefined }) { const { activityDerivedState, chainId, activityLinkUrl, disableMouseActions, creationTime } = props - const { id, isOrder, summary, order, enhancedTransaction, isCancelled, isExpired, isUnfillable } = - activityDerivedState + const { id, isOrder, summary, order, enhancedTransaction, isCancelled, isExpired } = activityDerivedState const tokenAddress = enhancedTransaction?.approval?.tokenAddress || (enhancedTransaction?.claim && V_COW_CONTRACT_ADDRESS[chainId]) const singleToken = useToken(tokenAddress) || null @@ -307,10 +293,15 @@ export function ActivityDetails(props: { View details ↗ )} - - {isUnfillable && unfillableAlert()} - + {order && creationTime && validTo && ( + + )} ) diff --git a/src/custom/components/AccountDetails/Transaction/CancelButton.tsx b/src/custom/components/AccountDetails/Transaction/CancelButton.tsx new file mode 100644 index 0000000000..42b3f1d0cf --- /dev/null +++ b/src/custom/components/AccountDetails/Transaction/CancelButton.tsx @@ -0,0 +1,29 @@ +import { useState } from 'react' +import { LinkStyledButton } from 'theme' +import { ActivityDerivedState } from './index' +import { CancellationModal } from './CancelationModal' + +export function CancelButton(props: { chainId: number; activityDerivedState: ActivityDerivedState }) { + const { activityDerivedState, chainId } = props + const { id, summary } = activityDerivedState + + const [showCancelModal, setShowCancelModal] = useState(false) + + const onCancelClick = () => setShowCancelModal(true) + const onDismiss = () => setShowCancelModal(false) + + return ( + <> + Cancel order{' '} + {showCancelModal && ( + + )} + + ) +} diff --git a/src/custom/components/AccountDetails/Transaction/OrderProgressBar/index.tsx b/src/custom/components/AccountDetails/Transaction/OrderProgressBar/index.tsx new file mode 100644 index 0000000000..3f81df6e9d --- /dev/null +++ b/src/custom/components/AccountDetails/Transaction/OrderProgressBar/index.tsx @@ -0,0 +1,223 @@ +import { useEffect, useState } from 'react' +import { useTransition } from 'react-spring' +import { + ProgressBarWrapper, + ProgressBarInnerWrapper, + SuccessProgress, + CowProtocolIcon, + GreenClockIcon, + StatusMsgContainer, + StatusMsg, + OrangeClockIcon, + PendingProgress, + WarningProgress, + WarningLogo, + WarningIcon, + GreenCheckIcon, +} from './styled' +import { AMMsLogo } from 'components/AMMsLogo' +import { EXPECTED_EXECUTION_TIME, getPercentage } from './utils' +import { SupportedChainId } from 'constants/chains' +import { ActivityDerivedState } from '../index' +import { CancelButton } from '../CancelButton' +import loadingCowGif from 'assets/cow-swap/cow-load.gif' + +const REFRESH_INTERVAL_MS = 200 +const COW_STATE_SECONDS = 30 + +type OrderProgressBarProps = { + activityDerivedState: ActivityDerivedState + creationTime: Date + validTo: Date + chainId: SupportedChainId +} + +type ExecutionState = 'cow' | 'amm' | 'confirmed' | 'unfillable' | 'delayed' + +export function OrderProgressBar(props: OrderProgressBarProps) { + const { activityDerivedState, creationTime, validTo, chainId } = props + const { isConfirmed, isCancellable, isUnfillable = false } = activityDerivedState + const { elapsedSeconds, expirationInSeconds, isPending } = useGetProgressBarInfo(props) + const [executionState, setExecutionState] = useState('cow') + const [percentage, setPercentage] = useState(getPercentage(elapsedSeconds, expirationInSeconds, chainId)) + const fadeOutTransition = useTransition(isPending, null, { + from: { opacity: 1 }, + leave: { opacity: 0 }, + trail: 3000, + }) + + useEffect(() => { + if (!isPending) { + return + } + + const id = setInterval(() => { + const percentage = getPercentage(elapsedSeconds, expirationInSeconds, chainId) + setPercentage(percentage) + }, REFRESH_INTERVAL_MS) + + return () => clearInterval(id) + }, [creationTime, validTo, chainId, elapsedSeconds, expirationInSeconds, isPending]) + + useEffect(() => { + if (isConfirmed) { + setPercentage(100) + } + }, [isConfirmed]) + + useEffect(() => { + if (isConfirmed) { + setExecutionState('confirmed') + } else if (isUnfillable) { + setExecutionState('unfillable') + } else if (elapsedSeconds <= COW_STATE_SECONDS) { + setExecutionState('cow') + } else if (elapsedSeconds <= EXPECTED_EXECUTION_TIME[chainId]) { + setExecutionState('amm') + } else { + setExecutionState('delayed') + } + }, [elapsedSeconds, isConfirmed, isUnfillable, chainId]) + + const progressBar = () => { + switch (executionState) { + case 'cow': { + return ( + <> + + + + + + + + Looking for a CoW. + + + ) + } + case 'amm': { + return ( + <> + + + + + + + + Finding best onchain price. + + + ) + } + case 'confirmed': { + return ( + <> + + + + + + + + Transaction confirmed. + + + ) + } + case 'unfillable': { + return ( + <> + + + + + + + + + Your limit price is out of market.{' '} + {isCancellable ? ( + <> + You can wait or + + ) : null} + + + + ) + } + case 'delayed': { + return ( + <> + + + + Loading prices... + + + + + +

The network looks slower than usual. Solvers are adjusting gas fees for you!

+ {isCancellable ? ( +

+ You can wait or +

+ ) : null} +
+
+ + ) + } + default: { + return null + } + } + } + + return ( + <> + {fadeOutTransition.map(({ item, props, key }) => { + return ( + item && ( + + {progressBar()} + + ) + ) + })} + + ) +} + +type ProgressBarInfo = { + elapsedSeconds: number + expirationInSeconds: number + isPending: boolean +} + +function useGetProgressBarInfo({ + creationTime, + validTo, + activityDerivedState, +}: OrderProgressBarProps): ProgressBarInfo { + const { isPending: orderIsPending, isPresignaturePending, order } = activityDerivedState + + if (order?.presignGnosisSafeTx) { + const submissionDate = new Date(order?.presignGnosisSafeTx?.submissionDate) + + return { + elapsedSeconds: (Date.now() - submissionDate.getTime()) / 1000, + expirationInSeconds: (validTo.getTime() - submissionDate.getTime()) / 1000, + isPending: orderIsPending || isPresignaturePending, + } + } + + return { + elapsedSeconds: (Date.now() - creationTime.getTime()) / 1000, + expirationInSeconds: (validTo.getTime() - creationTime.getTime()) / 1000, + isPending: orderIsPending, + } +} diff --git a/src/custom/components/AccountDetails/Transaction/OrderProgressBar/styled.ts b/src/custom/components/AccountDetails/Transaction/OrderProgressBar/styled.ts new file mode 100644 index 0000000000..c019f1fed3 --- /dev/null +++ b/src/custom/components/AccountDetails/Transaction/OrderProgressBar/styled.ts @@ -0,0 +1,157 @@ +import styled from 'styled-components/macro' +import { animated } from 'react-spring' +import { AlertTriangle, CheckCircle, Clock } from 'react-feather' +import CowProtocolLogo from 'assets/cow-swap/cowprotocol.svg' + +export const ProgressBarWrapper = animated(styled.div` + display: flex; + flex-direction: column; + justify-content: center; + width: 575px; + margin: 16px 0 8px 0; + overflow: hidden; + display: flex; + flex-flow: column wrap; + border-radius: 12px; + padding: 20px 20px 4px; + color: ${({ theme }) => theme.text1}; + background-color: ${({ theme }) => theme.bg4}; + + ${({ theme }) => theme.mediaWidth.upToSmall` + margin: 24px auto 12px; + width: 100%; + max-width: 100%; + grid-column: 1 / -1; + `}; +`) + +export const ProgressBarInnerWrapper = styled.div` + background-color: ${({ theme }) => theme.blue4}; + border-radius: 18px; + overflow: visible !important; + position: relative; +` + +export const ProgressBarIndicator = styled.div<{ percentage: number }>` + height: 18px; + background: rgb(233, 214, 37); + transform: translateX(0%); + border-radius: 12px; + transition: all 0.5s; + width: ${({ percentage }) => percentage}%; +` + +export const CowProtocolIcon = styled.div` + position: absolute; + top: -4px; + right: 0px; + height: 24px; + width: 24px; + border-radius: 100%; + border: 1px solid ${({ theme }) => theme.bg4}; + background: url(${CowProtocolLogo}) ${({ theme }) => theme.black} no-repeat center/75%; + box-shadow: 0px 0px 10px 2px ${({ theme }) => theme.bg1}; +` + +export const WarningLogo = styled.div` + position: absolute; + top: -4px; + right: 0px; + height: 26px; + width: 26px; + border-radius: 9px; + border: transparent; + + background: ${({ theme }) => theme.blueShade}; + box-shadow: 0px 0px 10px 2px ${({ theme }) => theme.bg1}; + + img { + margin: 4px 0 0 2px; + width: 22px; + } + + &::after { + filter: blur(10px); + } + + &::before, + &::after { + content: ''; + position: absolute; + left: -2px; + top: -2px; + background: ${({ theme }) => `linear-gradient(45deg, #e57751, #c5daef, #275194, ${theme.bg4}, #c5daef, #1b5a7a)`}; + background-size: 800%; + width: calc(100% + 4px); + height: calc(100% + 4px); + z-index: -1; + animation: steam 7s linear infinite; + border-radius: 9px; + } + + @keyframes steam { + 0% { + background-position: 0 0; + } + 50% { + background-position: 400% 0; + } + 100% { + background-position: 0 0; + } + } +` + +export const WarningProgress = styled(ProgressBarIndicator)` + background: linear-gradient(270deg, #de3f3f 2.17%, #ff784a 106.52%); ; +` + +export const SuccessProgress = styled(ProgressBarIndicator)` + background: linear-gradient(270deg, #27ae5f 16.85%, #b6a82d 106.52%); +` + +export const PendingProgress = styled(ProgressBarIndicator)` + background: linear-gradient(270deg, #b6a82d 16.85%, #ff784a 106.52%); +` + +export const GreenClockIcon = styled(Clock)` + margin: 0 0.5rem 0 0; + color: ${({ theme }) => theme.success}; +` + +export const GreenCheckIcon = styled(CheckCircle)` + margin: 0 0.5rem 0 0; + color: ${({ theme }) => theme.success}; +` + +export const OrangeClockIcon = styled(Clock)` + margin: 0 0.5rem 0 0; + color: ${({ theme }) => theme.yellow1}; +` + +export const WarningIcon = styled(AlertTriangle)` + margin: 0 0.5rem 0 0; + color: ${({ theme }) => theme.attention}; +` + +export const StatusMsgContainer = styled.div` + display: flex; + align-items: center; + margin: 1rem 0; + + ${({ theme }) => theme.mediaWidth.upToSmall` + gap: 0.2rem; + display: flex; + align-items: center; + + svg { + flex-shrink: 0; + } + `}; +` + +export const StatusMsg = styled.p` + font-size: 0.85rem; + color: ${({ theme }) => theme.text1}; + margin: 0; +` diff --git a/src/custom/components/AccountDetails/Transaction/OrderProgressBar/utils.ts b/src/custom/components/AccountDetails/Transaction/OrderProgressBar/utils.ts new file mode 100644 index 0000000000..f01fef7201 --- /dev/null +++ b/src/custom/components/AccountDetails/Transaction/OrderProgressBar/utils.ts @@ -0,0 +1,37 @@ +import { SupportedChainId } from 'constants/chains' + +const EXPECTED_EXECUTION_TIME_PERCENTAGE = 75 + +export const EXPECTED_EXECUTION_TIME: Record = { + [SupportedChainId.MAINNET]: 120, + [SupportedChainId.RINKEBY]: 50, + [SupportedChainId.XDAI]: 50, +} + +const LOG_FUNCTION = Math.log2 + +function getPercentageLogarithmicAux(seconds: number, expirationInSeconds: number) { + return (100 / LOG_FUNCTION(expirationInSeconds + 1)) * Math.log2(seconds + 1) +} + +function getPercentageLogarithmic(seconds: number, expirationInSeconds: number, chainId: SupportedChainId) { + const percentage = + ((100 - EXPECTED_EXECUTION_TIME_PERCENTAGE) / 100) * + getPercentageLogarithmicAux(seconds - EXPECTED_EXECUTION_TIME[chainId], expirationInSeconds) + return EXPECTED_EXECUTION_TIME_PERCENTAGE + percentage +} + +function getPercentageLinear(seconds: number, chainId: SupportedChainId) { + const percentage = (EXPECTED_EXECUTION_TIME_PERCENTAGE * seconds) / EXPECTED_EXECUTION_TIME[chainId] + return percentage +} + +export function getPercentage(seconds: number, expirationInSeconds: number, chainId: SupportedChainId) { + if (seconds >= expirationInSeconds) { + return 100 + } else if (seconds < EXPECTED_EXECUTION_TIME[chainId]) { + return getPercentageLinear(seconds, chainId) + } else { + return getPercentageLogarithmic(seconds, expirationInSeconds, chainId) + } +} diff --git a/src/custom/components/AccountDetails/Transaction/StatusDetails.tsx b/src/custom/components/AccountDetails/Transaction/StatusDetails.tsx index 6d1fb1214b..f421a460f6 100644 --- a/src/custom/components/AccountDetails/Transaction/StatusDetails.tsx +++ b/src/custom/components/AccountDetails/Transaction/StatusDetails.tsx @@ -1,6 +1,5 @@ -import React, { useState } from 'react' import SVG from 'react-inlinesvg' -import { LinkStyledButton, ExternalLink } from 'theme' +import { ExternalLink } from 'theme' import OrderCheckImage from 'assets/cow-swap/order-check.svg' import OrderExpiredImage from 'assets/cow-swap/order-expired.svg' @@ -11,9 +10,9 @@ import OrderOpenImage from 'assets/cow-swap/order-open.svg' import { StatusLabel, StatusLabelWrapper, StatusLabelBelow } from './styled' import { ActivityDerivedState, determinePillColour } from './index' -import { CancellationModal } from './CancelationModal' import { getSafeWebUrl } from 'api/gnosisSafe' import { SafeMultisigTransactionResponse } from '@gnosis.pm/safe-service-client' +import { CancelButton } from './CancelButton' export function GnosisSafeLink(props: { chainId: number @@ -86,10 +85,8 @@ export function StatusDetails(props: { chainId: number; activityDerivedState: Ac const { activityDerivedState, chainId } = props const { - id, status, type, - summary, isPending, isCancelling, isPresignaturePending, @@ -100,11 +97,6 @@ export function StatusDetails(props: { chainId: number; activityDerivedState: Ac isCancellable, } = activityDerivedState - const [showCancelModal, setShowCancelModal] = useState(false) - - const onCancelClick = () => setShowCancelModal(true) - const onDismiss = () => setShowCancelModal(false) - return ( {/* Cancel order */} - Cancel order - {showCancelModal && ( - - )} + )} diff --git a/yarn.lock b/yarn.lock index 21dc140a85..35614e0aee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13795,14 +13795,7 @@ is-color-stop@^1.0.0: rgb-regex "^1.0.1" rgba-regex "^1.0.0" -is-core-module@^2.0.0, is-core-module@^2.2.0, is-core-module@^2.8.1: - version "2.9.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" - integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== - dependencies: - has "^1.0.3" - -is-core-module@^2.5.0: +is-core-module@^2.0.0, is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.8.1: version "2.9.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== @@ -20682,14 +20675,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.2.1, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7: - version "7.3.7" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== - dependencies: - lru-cache "^6.0.0" - -semver@^7.3.4: +semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==