From 9968b98d981d883de681687089183f99136a73a9 Mon Sep 17 00:00:00 2001 From: toniocodo Date: Tue, 3 Oct 2023 11:00:58 +0200 Subject: [PATCH 01/11] feat: activity context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add new provider and components for activities - update use flow to account for activities - use pending states - add different activity cards WIP🚧 --- apps/oeth/src/components/Topnav.tsx | 12 +- apps/oeth/src/main.tsx | 8 +- .../components/ActivityButton.tsx | 38 ++++ .../components/ActivityIcon.tsx | 40 ++++ .../components/ActivityPopover.tsx | 176 ++++++++++++++++++ .../src/components/ActivityProvider/hooks.ts | 86 +++++++++ .../src/components/ActivityProvider/index.ts | 4 + .../src/components/ActivityProvider/state.ts | 15 ++ .../src/components/ActivityProvider/types.ts | 22 +++ libs/oeth/shared/src/components/index.ts | 1 + libs/oeth/swap/src/hooks.ts | 65 ++++--- libs/shared/theme/src/theme.tsx | 10 + 12 files changed, 451 insertions(+), 26 deletions(-) create mode 100644 libs/oeth/shared/src/components/ActivityProvider/components/ActivityButton.tsx create mode 100644 libs/oeth/shared/src/components/ActivityProvider/components/ActivityIcon.tsx create mode 100644 libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx create mode 100644 libs/oeth/shared/src/components/ActivityProvider/hooks.ts create mode 100644 libs/oeth/shared/src/components/ActivityProvider/index.ts create mode 100644 libs/oeth/shared/src/components/ActivityProvider/state.ts create mode 100644 libs/oeth/shared/src/components/ActivityProvider/types.ts diff --git a/apps/oeth/src/components/Topnav.tsx b/apps/oeth/src/components/Topnav.tsx index 703d55a8f..c3f407eb1 100644 --- a/apps/oeth/src/components/Topnav.tsx +++ b/apps/oeth/src/components/Topnav.tsx @@ -10,7 +10,7 @@ import { useMediaQuery, useTheme, } from '@mui/material'; -import { AccountPopover } from '@origin/oeth/shared'; +import { AccountPopover, ActivityButton } from '@origin/oeth/shared'; import { OpenAccountModalButton } from '@origin/shared/providers'; import { useIntl } from 'react-intl'; import { Link, useLocation, useNavigate } from 'react-router-dom'; @@ -198,6 +198,16 @@ export function Topnav(props: BoxProps) { anchor={accountModalAnchor} setAnchor={setAccountModalAnchor} /> + , diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityButton.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityButton.tsx new file mode 100644 index 000000000..a1af13cf5 --- /dev/null +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityButton.tsx @@ -0,0 +1,38 @@ +import { useState } from 'react'; + +import { IconButton } from '@mui/material'; + +import { useGlobalStatus } from '../hooks'; +import { ActivityIcon } from './ActivityIcon'; +import { ActivityPopover } from './ActivityPopover'; + +import type { IconButtonProps } from '@mui/material'; + +export const ActivityButton = ( + props: Omit, +) => { + const [anchorEl, setAnchorEl] = useState(null); + const status = useGlobalStatus(); + + return ( + <> + setAnchorEl(e.currentTarget)} + > + + + + + ); +}; diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityIcon.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityIcon.tsx new file mode 100644 index 000000000..7cb615b01 --- /dev/null +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityIcon.tsx @@ -0,0 +1,40 @@ +import { keyframes } from '@emotion/react'; +import { Box, Fade } from '@mui/material'; + +import type { BoxProps } from '@mui/material'; + +import type { GlobalActivityStatus } from '../types'; + +const spin = keyframes` + to { + transform: rotate(360deg); + } +`; + +const iconPaths: Record = { + idle: '/images/activity.svg', + pending: '/images/pending.svg', + error: '/images/failed.svg', + success: '/images/success.svg', +}; + +type ActivityIconProps = { status: GlobalActivityStatus } & BoxProps<'img'>; + +export const ActivityIcon = ({ status, ...rest }: ActivityIconProps) => { + return ( + + + + ); +}; diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx new file mode 100644 index 000000000..3b427d73e --- /dev/null +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx @@ -0,0 +1,176 @@ +import { + Box, + Divider, + Popover, + Stack, + Typography, + useTheme, +} from '@mui/material'; +import { LinkIcon } from '@origin/shared/components'; +import { isNilOrEmpty } from '@origin/shared/utils'; +import { descend, pipe, prop, sort, take } from 'ramda'; +import { defineMessage, useIntl } from 'react-intl'; +import { formatUnits } from 'viem'; + +import { useActivityState } from '../state'; +import { ActivityIcon } from './ActivityIcon'; + +import type { StackProps } from '@mui/material'; +import type { MessageDescriptor } from 'react-intl'; + +import type { Activity, ActivityType } from '../types'; + +export type AcitivityPopoverProps = { + anchor: HTMLElement | null; + setAnchor: (value: HTMLButtonElement | null) => void; +}; + +export const ActivityPopover = ({ + anchor, + setAnchor, +}: AcitivityPopoverProps) => { + const intl = useIntl(); + const theme = useTheme(); + const [{ activities, maxVisible }] = useActivityState(); + + const handleClose = () => { + setAnchor(null); + }; + + const sortedActivities = pipe( + sort(descend(prop('createdOn'))), + take(maxVisible), + )(activities) as Activity[]; + + return ( + ({ + xs: '90vw', + md: `min(${theme.typography.pxToRem(400)}, 90vw)`, + }), + [theme.breakpoints.down('md')]: { + left: '0 !important', + right: 0, + marginInline: 'auto', + }, + }, + }} + > + + + {intl.formatMessage({ defaultMessage: 'Recent activity' })} + + + }> + {isNilOrEmpty(sortedActivities) ? ( + + ) : ( + sortedActivities.map((a) => ( + + )) + )} + + + + ); +}; + +type ActivityItemProps = { + activity: Activity; +} & StackProps; + +const activityLabel: Record = { + swap: defineMessage({ defaultMessage: 'Swapped' }), + approval: defineMessage({ defaultMessage: 'Approved' }), +}; + +function ActivityItem({ activity, ...rest }: ActivityItemProps) { + const intl = useIntl(); + + return ( + + + + + + {intl.formatMessage(activityLabel[activity.type])} + + {!isNilOrEmpty(activity?.txReceipt?.transactionHash) && ( + + )} + + + + {intl.formatMessage( + { + defaultMessage: + '{amountIn} {symbolIn} for {amountOut} {symbolOut}', + }, + { + amountIn: intl.formatNumber( + +formatUnits(activity.amountIn, activity.tokenIn.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + symbolIn: activity.tokenIn.symbol, + amountOut: intl.formatNumber( + +formatUnits(activity.amountOut, activity.tokenOut.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + symbolOut: activity.tokenOut.symbol, + }, + )} + + + + + + + + + + ); +} + +function EmptyActivity(props: StackProps) { + const intl = useIntl(); + + return ( + + + {intl.formatMessage({ defaultMessage: 'No activity' })} + + + ); +} diff --git a/libs/oeth/shared/src/components/ActivityProvider/hooks.ts b/libs/oeth/shared/src/components/ActivityProvider/hooks.ts new file mode 100644 index 000000000..278b65ead --- /dev/null +++ b/libs/oeth/shared/src/components/ActivityProvider/hooks.ts @@ -0,0 +1,86 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { isNilOrEmpty } from '@origin/shared/utils'; +import { usePrevious } from '@react-hookz/web'; +import { produce } from 'immer'; +import { groupBy, prop, propEq } from 'ramda'; + +import { useActivityState } from './state'; + +import type { Activity, GlobalActivityStatus } from './types'; + +export const usePushActivity = () => { + const [, setState] = useActivityState(); + + return useCallback( + (value: Omit) => { + const activity = { + ...value, + id: Date.now().toString(), + createdOn: Date.now(), + }; + setState( + produce((state) => { + state.activities.unshift(activity); + }), + ); + + return activity; + }, + [setState], + ); +}; + +export const useUpdateActivity = () => { + const [, setState] = useActivityState(); + + return useCallback( + (activity: Partial) => { + setState( + produce((state) => { + const idx = state.activities.findIndex(propEq(activity.id, 'id')); + console.log('update ', idx, state.activities, activity); + if (idx > -1) { + state.activities[idx] = { + ...state.activities[idx], + ...activity, + }; + } + }), + ); + }, + [setState], + ); +}; + +export const useGlobalStatus = () => { + const [{ activities }] = useActivityState(); + const [status, setStatus] = useState('idle'); + const prev = usePrevious(activities); + + useEffect(() => { + const prevGrouped = groupBy(prop('status'), prev ?? []); + const grouped = groupBy(prop('status'), activities ?? []); + + if (isNilOrEmpty(grouped.pending)) { + if (prevGrouped?.success?.length !== grouped?.success?.length) { + setStatus('success'); + setTimeout(() => { + setStatus('idle'); + }, 2000); + } else if (prevGrouped?.error?.length !== grouped?.error?.length) { + setStatus('error'); + setTimeout(() => { + setStatus('idle'); + }, 2000); + } else { + setStatus('idle'); + } + } else { + setStatus('pending'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activities]); + + return status; +}; diff --git a/libs/oeth/shared/src/components/ActivityProvider/index.ts b/libs/oeth/shared/src/components/ActivityProvider/index.ts new file mode 100644 index 000000000..24a15daa0 --- /dev/null +++ b/libs/oeth/shared/src/components/ActivityProvider/index.ts @@ -0,0 +1,4 @@ +export * from './components/ActivityButton'; +export * from './hooks'; +export * from './state'; +export * from './types'; diff --git a/libs/oeth/shared/src/components/ActivityProvider/state.ts b/libs/oeth/shared/src/components/ActivityProvider/state.ts new file mode 100644 index 000000000..62c2a9ef3 --- /dev/null +++ b/libs/oeth/shared/src/components/ActivityProvider/state.ts @@ -0,0 +1,15 @@ +import { useState } from 'react'; + +import { createContainer } from 'react-tracked'; + +import type { Activity } from './types'; + +type ActivityState = { + activities: Activity[]; + maxVisible: number; +}; + +export const { Provider: ActivityProvider, useTracked: useActivityState } = + createContainer(() => + useState({ activities: [], maxVisible: 10 }), + ); diff --git a/libs/oeth/shared/src/components/ActivityProvider/types.ts b/libs/oeth/shared/src/components/ActivityProvider/types.ts new file mode 100644 index 000000000..bbc77fb12 --- /dev/null +++ b/libs/oeth/shared/src/components/ActivityProvider/types.ts @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { Token } from '@origin/shared/contracts'; +import type { TransactionReceipt } from 'viem'; + +export type ActivityType = 'swap' | 'approval'; + +export type ActivityStatus = 'pending' | 'success' | 'error'; + +export type GlobalActivityStatus = 'idle' | ActivityStatus; + +export type Activity = { + id: string; + createdOn: number; + tokenIn: Token; + tokenOut: Token; + amountIn?: bigint; + amountOut?: bigint; + txReceipt?: TransactionReceipt; + type: ActivityType; + status: ActivityStatus; + error?: string; +}; diff --git a/libs/oeth/shared/src/components/index.ts b/libs/oeth/shared/src/components/index.ts index 45b4fb9d0..7d6dc9474 100644 --- a/libs/oeth/shared/src/components/index.ts +++ b/libs/oeth/shared/src/components/index.ts @@ -1,2 +1,3 @@ +export * from './ActivityProvider'; export * from './AccountPopover'; export * from './GasPopover'; diff --git a/libs/oeth/swap/src/hooks.ts b/libs/oeth/swap/src/hooks.ts index dd95fbc14..3cf41aa7b 100644 --- a/libs/oeth/swap/src/hooks.ts +++ b/libs/oeth/swap/src/hooks.ts @@ -1,5 +1,6 @@ import { useCallback, useMemo } from 'react'; +import { usePushActivity, useUpdateActivity } from '@origin/oeth/shared'; import { useCurve, usePushNotification, @@ -179,8 +180,13 @@ export const useHandleApprove = () => { const queryClient = useQueryClient(); const wagmiClient = useWagmiClient(); const pushNotification = usePushNotification(); - const [{ amountIn, selectedSwapRoute, tokenIn, tokenOut }, setSwapState] = - useSwapState(); + const pushActivity = usePushActivity(); + const updateActivity = useUpdateActivity(); + + const [ + { amountIn, amountOut, selectedSwapRoute, tokenIn, tokenOut }, + setSwapState, + ] = useSwapState(); return useCallback(async () => { if (isNilOrEmpty(selectedSwapRoute) || isNilOrEmpty(address)) { @@ -192,33 +198,35 @@ export const useHandleApprove = () => { draft.isApprovalLoading = true; }), ); + const activity = pushActivity({ + tokenIn, + tokenOut, + type: 'approval', + status: 'pending', + amountIn, + amountOut, + }); await swapActions[selectedSwapRoute.action].approve({ tokenIn, tokenOut, amountIn, curve, - onSuccess: () => { + onSuccess: (txReceipt) => { wagmiClient.invalidateQueries({ queryKey: ['swap_balance'], }); queryClient.invalidateQueries({ queryKey: ['swap_allowance'], }); - pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Approval complete' }), - severity: 'success', - }); + updateActivity({ ...activity, status: 'success', txReceipt }); setSwapState( produce((draft) => { draft.isApprovalLoading = false; }), ); }, - onError: () => { - pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Approval failed' }), - severity: 'error', - }); + onError: (error: string) => { + updateActivity({ ...activity, status: 'error', error }); setSwapState( produce((draft) => { draft.isApprovalLoading = false; @@ -227,7 +235,7 @@ export const useHandleApprove = () => { }, onReject: () => { pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Approval cancelled' }), + title: intl.formatMessage({ defaultMessage: 'Approval Cancelled' }), severity: 'info', }); setSwapState( @@ -240,14 +248,17 @@ export const useHandleApprove = () => { }, [ address, amountIn, + amountOut, curve, intl, + pushActivity, pushNotification, queryClient, selectedSwapRoute, setSwapState, tokenIn, tokenOut, + updateActivity, wagmiClient, ]); }; @@ -260,6 +271,8 @@ export const useHandleSwap = () => { const queryClient = useQueryClient(); const wagmiClient = useWagmiClient(); const pushNotification = usePushNotification(); + const pushActivity = usePushActivity(); + const updateActivity = useUpdateActivity(); const [ { amountIn, amountOut, selectedSwapRoute, tokenIn, tokenOut }, setSwapState, @@ -270,6 +283,14 @@ export const useHandleSwap = () => { return; } + const activity = pushActivity({ + tokenIn, + tokenOut, + type: 'swap', + status: 'pending', + amountIn, + amountOut, + }); setSwapState( produce((draft) => { draft.isSwapLoading = true; @@ -283,27 +304,21 @@ export const useHandleSwap = () => { slippage, amountOut, curve, - onSuccess: () => { + onSuccess: (txReceipt) => { wagmiClient.invalidateQueries({ queryKey: ['swap_balance'], }); queryClient.invalidateQueries({ queryKey: ['swap_allowance'], }); - pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Swap complete' }), - severity: 'success', - }); + updateActivity({ ...activity, status: 'success', txReceipt }); }, - onError: () => { - pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Swap failed' }), - severity: 'error', - }); + onError: (error: string) => { + updateActivity({ ...activity, status: 'error', error }); }, onReject: () => { pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Swap cancelled' }), + title: intl.formatMessage({ defaultMessage: 'Swap Cancelled' }), severity: 'info', }); }, @@ -319,6 +334,7 @@ export const useHandleSwap = () => { amountOut, curve, intl, + pushActivity, pushNotification, queryClient, selectedSwapRoute, @@ -326,6 +342,7 @@ export const useHandleSwap = () => { slippage, tokenIn, tokenOut, + updateActivity, wagmiClient, ]); }; diff --git a/libs/shared/theme/src/theme.tsx b/libs/shared/theme/src/theme.tsx index 773463223..036fae1bf 100644 --- a/libs/shared/theme/src/theme.tsx +++ b/libs/shared/theme/src/theme.tsx @@ -364,6 +364,16 @@ export const theme = extendTheme({ }), }, }, + MuiIconButton: { + styleOverrides: { + root: ({ theme }) => ({ + background: theme.palette.background.gradientPaper, + '&:hover': { + background: theme.palette.background.paper, + }, + }), + }, + }, MuiInputBase: { styleOverrides: { root: ({ theme }) => ({ From 46e09d644e29d72fbcd0edb3cd852d5ae795674f Mon Sep 17 00:00:00 2001 From: toniocodo Date: Tue, 3 Oct 2023 21:07:09 +0200 Subject: [PATCH 02/11] feat: add redeem support --- libs/oeth/redeem/src/hooks.tsx | 22 ++++- .../components/ActivityItem.tsx | 87 ++++++++++++++++++ .../components/ActivityPopover.tsx | 92 +------------------ .../src/components/ActivityProvider/types.ts | 2 +- 4 files changed, 109 insertions(+), 94 deletions(-) create mode 100644 libs/oeth/shared/src/components/ActivityProvider/components/ActivityItem.tsx diff --git a/libs/oeth/redeem/src/hooks.tsx b/libs/oeth/redeem/src/hooks.tsx index 58ff0c4e7..4890c9249 100644 --- a/libs/oeth/redeem/src/hooks.tsx +++ b/libs/oeth/redeem/src/hooks.tsx @@ -1,6 +1,7 @@ import { useCallback } from 'react'; -import { contracts } from '@origin/shared/contracts'; +import { usePushActivity, useUpdateActivity } from '@origin/oeth/shared'; +import { contracts, tokens } from '@origin/shared/contracts'; import { BlockExplorerLink, usePushNotification, @@ -40,6 +41,8 @@ export const useHandleRedeem = () => { const intl = useIntl(); const { value: slippage } = useSlippage(); const pushNotification = usePushNotification(); + const pushActivity = usePushActivity(); + const updateActivity = useUpdateActivity(); const { address } = useAccount(); const [{ amountIn, amountOut }, setRedeemState] = useRedeemState(); const wagmiClient = useQueryClient(); @@ -49,6 +52,15 @@ export const useHandleRedeem = () => { return; } + const activity = pushActivity({ + type: 'redeem', + status: 'pending', + tokenIn: tokens.mainnet.OETH, + tokenOut: MIX_TOKEN, + amountIn, + amountOut, + }); + setRedeemState( produce((draft) => { draft.isRedeemLoading = true; @@ -75,6 +87,7 @@ export const useHandleRedeem = () => { console.log('redeem vault done!'); wagmiClient.invalidateQueries({ queryKey: ['redeem_balance'] }); + updateActivity({ ...activity, status: 'success', txReceipt }); pushNotification({ title: intl.formatMessage({ defaultMessage: 'Redeem complete' }), severity: 'success', @@ -88,10 +101,7 @@ export const useHandleRedeem = () => { severity: 'info', }); } else { - pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Redeem vault' }), - severity: 'error', - }); + updateActivity({ ...activity, status: 'error', error: e.short }); } } @@ -105,9 +115,11 @@ export const useHandleRedeem = () => { amountIn, amountOut, intl, + pushActivity, pushNotification, setRedeemState, slippage, + updateActivity, wagmiClient, ]); }; diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityItem.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityItem.tsx new file mode 100644 index 000000000..373d74a15 --- /dev/null +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityItem.tsx @@ -0,0 +1,87 @@ +import { Box, Stack, Typography } from '@mui/material'; +import { LinkIcon } from '@origin/shared/components'; +import { isNilOrEmpty } from '@origin/shared/utils'; +import { defineMessage, useIntl } from 'react-intl'; +import { formatUnits } from 'viem'; + +import { ActivityIcon } from './ActivityIcon'; + +import type { StackProps } from '@mui/material'; +import type { MessageDescriptor } from 'react-intl'; + +import type { Activity, ActivityType } from '../types'; + +type ActivityItemProps = { + activity: Activity; +} & StackProps; + +const activityLabel: Record = { + swap: defineMessage({ defaultMessage: 'Swapped' }), + approval: defineMessage({ defaultMessage: 'Approved' }), + redeem: defineMessage({ defaultMessage: 'Redeemed' }), +}; + +export const ActivityItem = ({ activity, ...rest }: ActivityItemProps) => { + const intl = useIntl(); + + return ( + + + + + + {intl.formatMessage(activityLabel[activity.type])} + + {!isNilOrEmpty(activity?.txReceipt?.transactionHash) && ( + + )} + + + + {intl.formatMessage( + { + defaultMessage: + '{amountIn} {symbolIn} for {amountOut} {symbolOut}', + }, + { + amountIn: intl.formatNumber( + +formatUnits(activity.amountIn, activity.tokenIn.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + symbolIn: activity.tokenIn.symbol, + amountOut: intl.formatNumber( + +formatUnits(activity.amountOut, activity.tokenOut.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + symbolOut: activity.tokenOut.symbol, + }, + )} + + + + + + + + + + ); +}; diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx index 3b427d73e..b19f927c7 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx @@ -1,24 +1,14 @@ -import { - Box, - Divider, - Popover, - Stack, - Typography, - useTheme, -} from '@mui/material'; -import { LinkIcon } from '@origin/shared/components'; +import { Divider, Popover, Stack, Typography, useTheme } from '@mui/material'; import { isNilOrEmpty } from '@origin/shared/utils'; import { descend, pipe, prop, sort, take } from 'ramda'; -import { defineMessage, useIntl } from 'react-intl'; -import { formatUnits } from 'viem'; +import { useIntl } from 'react-intl'; import { useActivityState } from '../state'; -import { ActivityIcon } from './ActivityIcon'; +import { ActivityItem } from './ActivityItem'; import type { StackProps } from '@mui/material'; -import type { MessageDescriptor } from 'react-intl'; -import type { Activity, ActivityType } from '../types'; +import type { Activity } from '../types'; export type AcitivityPopoverProps = { anchor: HTMLElement | null; @@ -89,80 +79,6 @@ export const ActivityPopover = ({ ); }; -type ActivityItemProps = { - activity: Activity; -} & StackProps; - -const activityLabel: Record = { - swap: defineMessage({ defaultMessage: 'Swapped' }), - approval: defineMessage({ defaultMessage: 'Approved' }), -}; - -function ActivityItem({ activity, ...rest }: ActivityItemProps) { - const intl = useIntl(); - - return ( - - - - - - {intl.formatMessage(activityLabel[activity.type])} - - {!isNilOrEmpty(activity?.txReceipt?.transactionHash) && ( - - )} - - - - {intl.formatMessage( - { - defaultMessage: - '{amountIn} {symbolIn} for {amountOut} {symbolOut}', - }, - { - amountIn: intl.formatNumber( - +formatUnits(activity.amountIn, activity.tokenIn.decimals), - { minimumFractionDigits: 4, maximumFractionDigits: 4 }, - ), - symbolIn: activity.tokenIn.symbol, - amountOut: intl.formatNumber( - +formatUnits(activity.amountOut, activity.tokenOut.decimals), - { minimumFractionDigits: 4, maximumFractionDigits: 4 }, - ), - symbolOut: activity.tokenOut.symbol, - }, - )} - - - - - - - - - - ); -} - function EmptyActivity(props: StackProps) { const intl = useIntl(); diff --git a/libs/oeth/shared/src/components/ActivityProvider/types.ts b/libs/oeth/shared/src/components/ActivityProvider/types.ts index bbc77fb12..171386145 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/types.ts +++ b/libs/oeth/shared/src/components/ActivityProvider/types.ts @@ -2,7 +2,7 @@ import type { Token } from '@origin/shared/contracts'; import type { TransactionReceipt } from 'viem'; -export type ActivityType = 'swap' | 'approval'; +export type ActivityType = 'swap' | 'approval' | 'redeem'; export type ActivityStatus = 'pending' | 'success' | 'error'; From 55b4011293ea3449338eea68a8729821cf73ffa3 Mon Sep 17 00:00:00 2001 From: toniocodo Date: Wed, 4 Oct 2023 12:06:22 +0200 Subject: [PATCH 03/11] feat: allow for empty notifications using children --- .../src/notifications/components/NotificationSnack.tsx | 4 ++++ libs/shared/providers/src/notifications/hooks.ts | 2 +- libs/shared/providers/src/notifications/types.ts | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/libs/shared/providers/src/notifications/components/NotificationSnack.tsx b/libs/shared/providers/src/notifications/components/NotificationSnack.tsx index e20102af1..e4f5d4bda 100644 --- a/libs/shared/providers/src/notifications/components/NotificationSnack.tsx +++ b/libs/shared/providers/src/notifications/components/NotificationSnack.tsx @@ -47,8 +47,12 @@ export const NotificationSnack = ({ borderRadius: 1, minWidth: { sm: 300, md: 400, lg: 500, xl: 600 }, maxWidth: { sm: 400, md: 500, lg: 600, xl: 700 }, + '.MuiAlert-message': { + width: 1, + }, ...AlertProps?.sx, }} + {...(!isNilOrEmpty(content) && { icon: false })} onClose={handleCloseClick} > {!isNilOrEmpty(title) && ( diff --git a/libs/shared/providers/src/notifications/hooks.ts b/libs/shared/providers/src/notifications/hooks.ts index ae269fc29..a127e3db4 100644 --- a/libs/shared/providers/src/notifications/hooks.ts +++ b/libs/shared/providers/src/notifications/hooks.ts @@ -10,7 +10,7 @@ import type { ReactNode } from 'react'; type NotificationOptions = { severity?: AlertColor; - title: string; + title?: string; message?: string; content?: ReactNode; visible?: boolean; diff --git a/libs/shared/providers/src/notifications/types.ts b/libs/shared/providers/src/notifications/types.ts index 322fa21e7..1c4e95d31 100644 --- a/libs/shared/providers/src/notifications/types.ts +++ b/libs/shared/providers/src/notifications/types.ts @@ -3,8 +3,8 @@ import type { ReactNode } from 'react'; export type Notification = { id: string; - severity: AlertColor; - title: string; + severity?: AlertColor; + title?: string; message?: string; content?: ReactNode; createdOn: number; From 4262de4a6d72a062913349eac8567fe3624bc512 Mon Sep 17 00:00:00 2001 From: toniocodo Date: Wed, 4 Oct 2023 12:07:20 +0200 Subject: [PATCH 04/11] feat: gather icon components, refactor usage --- .../components/src/Icons/ActivityIcon.tsx | 48 +++++++++++++++++++ .../index.tsx => Icons/LinkIcon.tsx} | 0 libs/shared/components/src/Icons/index.ts | 2 + libs/shared/components/src/index.ts | 2 +- .../src/top-nav/ConnectedButton.tsx | 2 +- .../components/src/top-nav/Transaction.tsx | 2 +- 6 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 libs/shared/components/src/Icons/ActivityIcon.tsx rename libs/shared/components/src/{LinkIcon/index.tsx => Icons/LinkIcon.tsx} (100%) create mode 100644 libs/shared/components/src/Icons/index.ts diff --git a/libs/shared/components/src/Icons/ActivityIcon.tsx b/libs/shared/components/src/Icons/ActivityIcon.tsx new file mode 100644 index 000000000..baaf1133b --- /dev/null +++ b/libs/shared/components/src/Icons/ActivityIcon.tsx @@ -0,0 +1,48 @@ +import { keyframes } from '@emotion/react'; +import { Box, Fade } from '@mui/material'; + +import type { BoxProps } from '@mui/material'; + +export type ActivityIconStatus = 'idle' | 'pending' | 'success' | 'error'; + +const spin = keyframes` + to { + transform: rotate(360deg); + } +`; + +const iconPaths: Record = { + idle: '/images/activity.svg', + pending: '/images/pending.svg', + error: '/images/failed.svg', + success: '/images/success.svg', +}; + +type ActivityIconProps = { + status: ActivityIconStatus; + disablePendingSpin?: boolean; +} & BoxProps<'img'>; + +export const ActivityIcon = ({ + status, + disablePendingSpin, + ...rest +}: ActivityIconProps) => { + return ( + + + + ); +}; diff --git a/libs/shared/components/src/LinkIcon/index.tsx b/libs/shared/components/src/Icons/LinkIcon.tsx similarity index 100% rename from libs/shared/components/src/LinkIcon/index.tsx rename to libs/shared/components/src/Icons/LinkIcon.tsx diff --git a/libs/shared/components/src/Icons/index.ts b/libs/shared/components/src/Icons/index.ts new file mode 100644 index 000000000..a592efbb0 --- /dev/null +++ b/libs/shared/components/src/Icons/index.ts @@ -0,0 +1,2 @@ +export * from './ActivityIcon'; +export * from './LinkIcon'; diff --git a/libs/shared/components/src/index.ts b/libs/shared/components/src/index.ts index 525e4f83b..7158027f7 100644 --- a/libs/shared/components/src/index.ts +++ b/libs/shared/components/src/index.ts @@ -2,7 +2,7 @@ export * from './Cards'; export * from './DataTable'; export * from './InfoTooltip'; export * from './Inputs'; -export * from './LinkIcon'; +export * from './Icons'; export * from './Loader'; export * from './MiddleTruncated'; export * from './Mix'; diff --git a/libs/shared/components/src/top-nav/ConnectedButton.tsx b/libs/shared/components/src/top-nav/ConnectedButton.tsx index 121e778be..523c4b805 100644 --- a/libs/shared/components/src/top-nav/ConnectedButton.tsx +++ b/libs/shared/components/src/top-nav/ConnectedButton.tsx @@ -12,7 +12,7 @@ import { } from '@mui/material'; import { useIntl } from 'react-intl'; -import { LinkIcon } from '../LinkIcon'; +import { LinkIcon } from '../Icons'; import { MiddleTruncated } from '../MiddleTruncated'; import { Icon } from './Icon'; import { styles } from './utils'; diff --git a/libs/shared/components/src/top-nav/Transaction.tsx b/libs/shared/components/src/top-nav/Transaction.tsx index 5bef600f7..437090a1d 100644 --- a/libs/shared/components/src/top-nav/Transaction.tsx +++ b/libs/shared/components/src/top-nav/Transaction.tsx @@ -2,7 +2,7 @@ import { keyframes } from '@emotion/react'; import { Stack, Typography } from '@mui/material'; import { useIntl } from 'react-intl'; -import { LinkIcon } from '../LinkIcon'; +import { LinkIcon } from '../Icons'; import { Icon } from './Icon'; import { messages } from './utils'; From f4f4180659def16d6008f1a1e576a66620f53e48 Mon Sep 17 00:00:00 2001 From: toniocodo Date: Wed, 4 Oct 2023 12:23:43 +0200 Subject: [PATCH 05/11] feat: add dedicated notifications, add control hooks --- .../components/ActivityButton.tsx | 2 +- .../components/ActivityIcon.tsx | 40 ------- .../components/ActivityItem.tsx | 87 --------------- .../components/ActivityPopover.tsx | 19 +++- .../components/ApprovalNotification.tsx | 84 ++++++++++++++ .../components/RedeemNotification.tsx | 104 ++++++++++++++++++ .../components/SwapNotification.tsx | 102 +++++++++++++++++ .../ActivityProvider/components/index.ts | 4 + .../src/components/ActivityProvider/hooks.ts | 23 +++- .../src/components/ActivityProvider/index.ts | 2 +- 10 files changed, 331 insertions(+), 136 deletions(-) delete mode 100644 libs/oeth/shared/src/components/ActivityProvider/components/ActivityIcon.tsx delete mode 100644 libs/oeth/shared/src/components/ActivityProvider/components/ActivityItem.tsx create mode 100644 libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx create mode 100644 libs/oeth/shared/src/components/ActivityProvider/components/RedeemNotification.tsx create mode 100644 libs/oeth/shared/src/components/ActivityProvider/components/SwapNotification.tsx create mode 100644 libs/oeth/shared/src/components/ActivityProvider/components/index.ts diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityButton.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityButton.tsx index a1af13cf5..1b7dcdc57 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityButton.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityButton.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; import { IconButton } from '@mui/material'; +import { ActivityIcon } from '@origin/shared/components'; import { useGlobalStatus } from '../hooks'; -import { ActivityIcon } from './ActivityIcon'; import { ActivityPopover } from './ActivityPopover'; import type { IconButtonProps } from '@mui/material'; diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityIcon.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityIcon.tsx deleted file mode 100644 index 7cb615b01..000000000 --- a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityIcon.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { keyframes } from '@emotion/react'; -import { Box, Fade } from '@mui/material'; - -import type { BoxProps } from '@mui/material'; - -import type { GlobalActivityStatus } from '../types'; - -const spin = keyframes` - to { - transform: rotate(360deg); - } -`; - -const iconPaths: Record = { - idle: '/images/activity.svg', - pending: '/images/pending.svg', - error: '/images/failed.svg', - success: '/images/success.svg', -}; - -type ActivityIconProps = { status: GlobalActivityStatus } & BoxProps<'img'>; - -export const ActivityIcon = ({ status, ...rest }: ActivityIconProps) => { - return ( - - - - ); -}; diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityItem.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityItem.tsx deleted file mode 100644 index 373d74a15..000000000 --- a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityItem.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Box, Stack, Typography } from '@mui/material'; -import { LinkIcon } from '@origin/shared/components'; -import { isNilOrEmpty } from '@origin/shared/utils'; -import { defineMessage, useIntl } from 'react-intl'; -import { formatUnits } from 'viem'; - -import { ActivityIcon } from './ActivityIcon'; - -import type { StackProps } from '@mui/material'; -import type { MessageDescriptor } from 'react-intl'; - -import type { Activity, ActivityType } from '../types'; - -type ActivityItemProps = { - activity: Activity; -} & StackProps; - -const activityLabel: Record = { - swap: defineMessage({ defaultMessage: 'Swapped' }), - approval: defineMessage({ defaultMessage: 'Approved' }), - redeem: defineMessage({ defaultMessage: 'Redeemed' }), -}; - -export const ActivityItem = ({ activity, ...rest }: ActivityItemProps) => { - const intl = useIntl(); - - return ( - - - - - - {intl.formatMessage(activityLabel[activity.type])} - - {!isNilOrEmpty(activity?.txReceipt?.transactionHash) && ( - - )} - - - - {intl.formatMessage( - { - defaultMessage: - '{amountIn} {symbolIn} for {amountOut} {symbolOut}', - }, - { - amountIn: intl.formatNumber( - +formatUnits(activity.amountIn, activity.tokenIn.decimals), - { minimumFractionDigits: 4, maximumFractionDigits: 4 }, - ), - symbolIn: activity.tokenIn.symbol, - amountOut: intl.formatNumber( - +formatUnits(activity.amountOut, activity.tokenOut.decimals), - { minimumFractionDigits: 4, maximumFractionDigits: 4 }, - ), - symbolOut: activity.tokenOut.symbol, - }, - )} - - - - - - - - - - ); -}; diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx index b19f927c7..4ead2ec98 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx @@ -4,7 +4,9 @@ import { descend, pipe, prop, sort, take } from 'ramda'; import { useIntl } from 'react-intl'; import { useActivityState } from '../state'; -import { ActivityItem } from './ActivityItem'; +import { ApprovalNotification } from './ApprovalNotification'; +import { RedeemNotification } from './RedeemNotification'; +import { SwapNotification } from './SwapNotification'; import type { StackProps } from '@mui/material'; @@ -69,9 +71,18 @@ export const ActivityPopover = ({ {isNilOrEmpty(sortedActivities) ? ( ) : ( - sortedActivities.map((a) => ( - - )) + sortedActivities.map( + (a) => + ({ + approval: ( + + ), + redeem: ( + + ), + swap: , + })[a.type], + ) )} diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx new file mode 100644 index 000000000..497f07610 --- /dev/null +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx @@ -0,0 +1,84 @@ +import { Box, Stack, Typography } from '@mui/material'; +import { ActivityIcon, LinkIcon } from '@origin/shared/components'; +import { isNilOrEmpty } from '@origin/shared/utils'; +import { defineMessage, useIntl } from 'react-intl'; +import { formatUnits } from 'viem'; + +import type { StackProps } from '@mui/material'; +import type { Token } from '@origin/shared/contracts'; +import type { MessageDescriptor } from 'react-intl'; +import type { TransactionReceipt } from 'viem'; + +import type { GlobalActivityStatus } from '../types'; + +type ApprovalNotificationProps = { + status: GlobalActivityStatus; + tokenIn: Token; + tokenOut: Token; + amountIn?: bigint; + txReceipt?: TransactionReceipt; + error?: string; +} & StackProps; + +const title: Record = { + pending: defineMessage({ defaultMessage: 'Approving' }), + success: defineMessage({ defaultMessage: 'Approved' }), + error: defineMessage({ defaultMessage: 'Error while approving' }), + idle: defineMessage({ defaultMessage: 'Approve' }), +}; + +export const ApprovalNotification = ({ + status, + tokenIn, + tokenOut, + amountIn, + txReceipt, + error, + ...rest +}: ApprovalNotificationProps) => { + const intl = useIntl(); + + return ( + + + + + {intl.formatMessage(title[status])} + {!isNilOrEmpty(txReceipt?.transactionHash) && ( + + )} + + + {isNilOrEmpty(error) ? ( + + {intl.formatMessage( + { + defaultMessage: '{amountIn} {symbolIn}', + }, + { + amountIn: intl.formatNumber( + +formatUnits(amountIn, tokenIn.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + symbolIn: tokenIn.symbol, + }, + )} + + ) : ( + {error} + )} + + + + + + + ); +}; diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/RedeemNotification.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/RedeemNotification.tsx new file mode 100644 index 000000000..97bb89ae7 --- /dev/null +++ b/libs/oeth/shared/src/components/ActivityProvider/components/RedeemNotification.tsx @@ -0,0 +1,104 @@ +import { Box, Stack, Typography } from '@mui/material'; +import { ActivityIcon, LinkIcon, Mix } from '@origin/shared/components'; +import { isNilOrEmpty } from '@origin/shared/utils'; +import { defineMessage, useIntl } from 'react-intl'; +import { formatUnits } from 'viem'; + +import type { StackProps } from '@mui/material'; +import type { Token } from '@origin/shared/contracts'; +import type { MessageDescriptor } from 'react-intl'; +import type { TransactionReceipt } from 'viem'; + +import type { GlobalActivityStatus } from '../types'; + +type RedeemNotificationProps = { + status: GlobalActivityStatus; + tokenIn: Token; + tokenOut: Token; + amountIn?: bigint; + amountOut?: bigint; + txReceipt?: TransactionReceipt; + error?: string; +} & StackProps; + +const title: Record = { + pending: defineMessage({ defaultMessage: 'Redeeming' }), + success: defineMessage({ defaultMessage: 'Redeemed' }), + error: defineMessage({ defaultMessage: 'Error while redeeming' }), + idle: defineMessage({ defaultMessage: 'Redeem' }), +}; + +export const RedeemNotification = ({ + status, + tokenIn, + tokenOut, + amountIn, + amountOut, + txReceipt, + error, + ...rest +}: RedeemNotificationProps) => { + const intl = useIntl(); + + return ( + + + + + {intl.formatMessage(title[status])} + {!isNilOrEmpty(txReceipt?.transactionHash) && ( + + )} + + + {isNilOrEmpty(error) ? ( + + {intl.formatMessage( + { + defaultMessage: '{amountIn} {symbolIn}', + }, + { + amountIn: intl.formatNumber( + +formatUnits(amountIn, tokenIn.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + symbolIn: tokenIn.symbol, + amountOut: intl.formatNumber( + +formatUnits(amountOut, tokenOut.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + }, + )} + + ) : ( + {error} + )} + + + + + + + + + ); +}; diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/SwapNotification.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/SwapNotification.tsx new file mode 100644 index 000000000..c316b21ab --- /dev/null +++ b/libs/oeth/shared/src/components/ActivityProvider/components/SwapNotification.tsx @@ -0,0 +1,102 @@ +import { Box, Stack, Typography } from '@mui/material'; +import { ActivityIcon, LinkIcon } from '@origin/shared/components'; +import { isNilOrEmpty } from '@origin/shared/utils'; +import { defineMessage, useIntl } from 'react-intl'; +import { formatUnits } from 'viem'; + +import type { StackProps } from '@mui/material'; +import type { Token } from '@origin/shared/contracts'; +import type { MessageDescriptor } from 'react-intl'; +import type { TransactionReceipt } from 'viem'; + +import type { GlobalActivityStatus } from '../types'; + +type SwapNotificationProps = { + status: GlobalActivityStatus; + tokenIn: Token; + tokenOut: Token; + amountIn?: bigint; + amountOut?: bigint; + txReceipt?: TransactionReceipt; + error?: string; +} & StackProps; + +const title: Record = { + pending: defineMessage({ defaultMessage: 'Swapping' }), + success: defineMessage({ defaultMessage: 'Swapped' }), + error: defineMessage({ defaultMessage: 'Error while swapping' }), + idle: defineMessage({ defaultMessage: 'Swap' }), +}; + +export const SwapNotification = ({ + status, + tokenIn, + tokenOut, + amountIn, + amountOut, + txReceipt, + error, + ...rest +}: SwapNotificationProps) => { + const intl = useIntl(); + + return ( + + + + + {intl.formatMessage(title[status])} + {!isNilOrEmpty(txReceipt?.transactionHash) && ( + + )} + + + {isNilOrEmpty(error) ? ( + + {intl.formatMessage( + { + defaultMessage: + '{amountIn} {symbolIn} for {amountOut} {symbolOut}', + }, + { + amountIn: intl.formatNumber( + +formatUnits(amountIn, tokenIn.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + symbolIn: tokenIn.symbol, + amountOut: intl.formatNumber( + +formatUnits(amountOut, tokenOut.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + symbolOut: tokenOut.symbol, + }, + )} + + ) : ( + {error} + )} + + + + + + + + + ); +}; diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/index.ts b/libs/oeth/shared/src/components/ActivityProvider/components/index.ts new file mode 100644 index 000000000..14b1dccb3 --- /dev/null +++ b/libs/oeth/shared/src/components/ActivityProvider/components/index.ts @@ -0,0 +1,4 @@ +export * from './ActivityButton'; +export * from './ApprovalNotification'; +export * from './RedeemNotification'; +export * from './SwapNotification'; diff --git a/libs/oeth/shared/src/components/ActivityProvider/hooks.ts b/libs/oeth/shared/src/components/ActivityProvider/hooks.ts index 278b65ead..a8d5c6178 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/hooks.ts +++ b/libs/oeth/shared/src/components/ActivityProvider/hooks.ts @@ -39,7 +39,6 @@ export const useUpdateActivity = () => { setState( produce((state) => { const idx = state.activities.findIndex(propEq(activity.id, 'id')); - console.log('update ', idx, state.activities, activity); if (idx > -1) { state.activities[idx] = { ...state.activities[idx], @@ -53,6 +52,24 @@ export const useUpdateActivity = () => { ); }; +export const useDeleteActivity = () => { + const [, setState] = useActivityState(); + + return useCallback( + (id: string) => { + setState( + produce((state) => { + const idx = state.activities.findIndex(propEq(id, 'id')); + if (idx > -1) { + state.activities.splice(idx, 1); + } + }), + ); + }, + [setState], + ); +}; + export const useGlobalStatus = () => { const [{ activities }] = useActivityState(); const [status, setStatus] = useState('idle'); @@ -67,12 +84,12 @@ export const useGlobalStatus = () => { setStatus('success'); setTimeout(() => { setStatus('idle'); - }, 2000); + }, 5000); } else if (prevGrouped?.error?.length !== grouped?.error?.length) { setStatus('error'); setTimeout(() => { setStatus('idle'); - }, 2000); + }, 5000); } else { setStatus('idle'); } diff --git a/libs/oeth/shared/src/components/ActivityProvider/index.ts b/libs/oeth/shared/src/components/ActivityProvider/index.ts index 24a15daa0..a260a059b 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/index.ts +++ b/libs/oeth/shared/src/components/ActivityProvider/index.ts @@ -1,4 +1,4 @@ -export * from './components/ActivityButton'; +export * from './components'; export * from './hooks'; export * from './state'; export * from './types'; From 53bd43e4e964cd95becc283cedae8f5540675e0c Mon Sep 17 00:00:00 2001 From: toniocodo Date: Wed, 4 Oct 2023 12:24:10 +0200 Subject: [PATCH 06/11] feat: update operation flow, add notifs and activities --- libs/oeth/redeem/src/hooks.tsx | 80 +++++--- libs/oeth/swap/src/actions/mintVault.ts | 146 +++++--------- libs/oeth/swap/src/actions/swapCurve/index.ts | 106 +++------- libs/oeth/swap/src/actions/swapCurveEth.ts | 70 +++---- libs/oeth/swap/src/actions/swapZapperEth.ts | 108 +++------- .../swap/src/actions/swapZapperSfrxeth.ts | 87 ++------ libs/oeth/swap/src/actions/unwrapWOETH.ts | 36 ++-- libs/oeth/swap/src/actions/wrapOETH.ts | 96 +++------ libs/oeth/swap/src/{hooks.ts => hooks.tsx} | 190 ++++++++++++------ libs/oeth/swap/src/state.ts | 48 +++-- libs/oeth/swap/src/types.ts | 22 +- 11 files changed, 421 insertions(+), 568 deletions(-) rename libs/oeth/swap/src/{hooks.ts => hooks.tsx} (69%) diff --git a/libs/oeth/redeem/src/hooks.tsx b/libs/oeth/redeem/src/hooks.tsx index 4890c9249..99081d424 100644 --- a/libs/oeth/redeem/src/hooks.tsx +++ b/libs/oeth/redeem/src/hooks.tsx @@ -1,12 +1,13 @@ import { useCallback } from 'react'; -import { usePushActivity, useUpdateActivity } from '@origin/oeth/shared'; -import { contracts, tokens } from '@origin/shared/contracts'; import { - BlockExplorerLink, - usePushNotification, - useSlippage, -} from '@origin/shared/providers'; + RedeemNotification, + useDeleteActivity, + usePushActivity, + useUpdateActivity, +} from '@origin/oeth/shared'; +import { contracts, tokens } from '@origin/shared/contracts'; +import { usePushNotification, useSlippage } from '@origin/shared/providers'; import { isNilOrEmpty } from '@origin/shared/utils'; import { prepareWriteContract, @@ -43,6 +44,7 @@ export const useHandleRedeem = () => { const pushNotification = usePushNotification(); const pushActivity = usePushActivity(); const updateActivity = useUpdateActivity(); + const deleteActivity = useDeleteActivity(); const { address } = useAccount(); const [{ amountIn, amountOut }, setRedeemState] = useRedeemState(); const wagmiClient = useQueryClient(); @@ -52,6 +54,14 @@ export const useHandleRedeem = () => { return; } + const minAmountOut = parseUnits( + ( + +formatUnits(amountOut, MIX_TOKEN.decimals) - + +formatUnits(amountOut, MIX_TOKEN.decimals) * slippage + ).toString(), + MIX_TOKEN.decimals, + ); + const activity = pushActivity({ type: 'redeem', status: 'pending', @@ -68,14 +78,6 @@ export const useHandleRedeem = () => { ); try { - const minAmountOut = parseUnits( - ( - +formatUnits(amountOut, MIX_TOKEN.decimals) - - +formatUnits(amountOut, MIX_TOKEN.decimals) * slippage - ).toString(), - MIX_TOKEN.decimals, - ); - const { request } = await prepareWriteContract({ address: contracts.mainnet.OETHVaultCore.address, abi: contracts.mainnet.OETHVaultCore.abi, @@ -83,37 +85,55 @@ export const useHandleRedeem = () => { args: [amountIn, minAmountOut], }); const { hash } = await writeContract(request); + setRedeemState( + produce((draft) => { + draft.isRedeemLoading = false; + }), + ); const txReceipt = await waitForTransaction({ hash }); - - console.log('redeem vault done!'); wagmiClient.invalidateQueries({ queryKey: ['redeem_balance'] }); updateActivity({ ...activity, status: 'success', txReceipt }); pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Redeem complete' }), - severity: 'success', - content: , + content: ( + + ), }); - } catch (e) { - console.error(`redeem vault error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED') { + } catch (error) { + if (error.cause.name === 'UserRejectedRequestError') { + deleteActivity(activity.id); pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Redeem vault' }), + title: intl.formatMessage({ defaultMessage: 'Redeem Cancelled' }), + message: intl.formatMessage({ + defaultMessage: 'User rejected operation', + }), severity: 'info', }); } else { - updateActivity({ ...activity, status: 'error', error: e.short }); + updateActivity({ + ...activity, + status: 'error', + error: error.shortMessage, + }); + pushNotification({ + content: ( + + ), + }); } } - - setRedeemState( - produce((draft) => { - draft.isRedeemLoading = false; - }), - ); }, [ address, amountIn, amountOut, + deleteActivity, intl, pushActivity, pushNotification, diff --git a/libs/oeth/swap/src/actions/mintVault.ts b/libs/oeth/swap/src/actions/mintVault.ts index 331cae331..7b25cd10f 100644 --- a/libs/oeth/swap/src/actions/mintVault.ts +++ b/libs/oeth/swap/src/actions/mintVault.ts @@ -8,7 +8,6 @@ import { prepareWriteContract, readContract, readContracts, - waitForTransaction, writeContract, } from '@wagmi/core'; import { formatUnits, parseUnits } from 'viem'; @@ -84,39 +83,37 @@ const estimateGas: EstimateGas = async ({ return gasEstimate; } catch {} - try { - const [rebaseThreshold, autoAllocateThreshold] = - await queryClient.fetchQuery({ - queryKey: ['vault-info', tokenOut.address], - queryFn: () => - readContracts({ - contracts: [ - { - address: contracts.mainnet.OETHVaultCore.address, - abi: contracts.mainnet.OETHVaultCore.abi, - functionName: 'rebaseThreshold', - }, - { - address: contracts.mainnet.OETHVaultCore.address, - abi: contracts.mainnet.OETHVaultCore.abi, - functionName: 'autoAllocateThreshold', - }, - ], - }), - staleTime: Infinity, - }); - - // TODO check validity - gasEstimate = 220000n; - if (amountIn > autoAllocateThreshold?.result) { - gasEstimate = 2900000n; - } else if (amountIn > rebaseThreshold?.result) { - gasEstimate = 510000n; - } - } catch (e) { - console.error(`mint vault gas estimate error!\n${e.message}`); + const [rebaseThreshold, autoAllocateThreshold] = await queryClient.fetchQuery( + { + queryKey: ['vault-info', tokenOut.address], + queryFn: () => + readContracts({ + contracts: [ + { + address: contracts.mainnet.OETHVaultCore.address, + abi: contracts.mainnet.OETHVaultCore.abi, + functionName: 'rebaseThreshold', + }, + { + address: contracts.mainnet.OETHVaultCore.address, + abi: contracts.mainnet.OETHVaultCore.abi, + functionName: 'autoAllocateThreshold', + }, + ], + }), + staleTime: Infinity, + }, + ); + + gasEstimate = 220000n; + if (amountIn > autoAllocateThreshold?.result) { + gasEstimate = 2900000n; + } else if (amountIn > rebaseThreshold?.result) { + gasEstimate = 510000n; } + console.log(`Mint vault uses fix gas estimate: ${gasEstimate}`); + return gasEstimate; }; @@ -158,7 +155,9 @@ const estimateApprovalGas: EstimateApprovalGas = async ({ args: [contracts.mainnet.OETHVaultCore.address, amountIn], account: address, }); - } catch {} + } catch { + console.log(`Mint vault uses fix approval gas estimate: 0`); + } return approvalEstimate; }; @@ -206,35 +205,16 @@ const estimateRoute: EstimateRoute = async ({ }; }; -const approve: Approve = async ({ - tokenIn, - amountIn, - onSuccess, - onError, - onReject, -}) => { - try { - const { request } = await prepareWriteContract({ - address: tokenIn.address, - abi: erc20ABI, - functionName: 'approve', - args: [contracts.mainnet.OETHVaultCore.address, amountIn], - }); - const { hash } = await writeContract(request); - const txReceipt = await waitForTransaction({ hash }); - - console.log(`mint vault approval done!`); - if (onSuccess) { - await onSuccess(txReceipt); - } - } catch (e) { - console.error(`mint vault approval error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED' && onReject) { - await onReject('Mint vault approval'); - } else if (onError) { - await onError('Mint vault approval'); - } - } +const approve: Approve = async ({ tokenIn, amountIn }) => { + const { request } = await prepareWriteContract({ + address: tokenIn.address, + abi: erc20ABI, + functionName: 'approve', + args: [contracts.mainnet.OETHVaultCore.address, amountIn], + }); + const { hash } = await writeContract(request); + + return hash; }; const swap: Swap = async ({ @@ -243,24 +223,17 @@ const swap: Swap = async ({ amountIn, slippage, amountOut, - onSuccess, - onError, - onReject, }) => { const { address } = getAccount(); if (amountIn === 0n || isNilOrEmpty(address)) { - return; + return null; } const approved = await allowance({ tokenIn, tokenOut }); if (approved < amountIn) { - console.error(`mint vault is not approved`); - if (onError) { - await onError('Mint vault is not approved'); - } - return; + throw new Error(`Mint vault is not approved`); } const minAmountOut = parseUnits( @@ -271,28 +244,15 @@ const swap: Swap = async ({ tokenOut.decimals, ); - try { - const { request } = await prepareWriteContract({ - address: contracts.mainnet.OETHVaultCore.address, - abi: contracts.mainnet.OETHVaultCore.abi, - functionName: 'mint', - args: [tokenIn.address, amountIn, minAmountOut], - }); - const { hash } = await writeContract(request); - const txReceipt = await waitForTransaction({ hash }); - - console.log('mint vault done!'); - if (onSuccess) { - await onSuccess(txReceipt); - } - } catch (e) { - console.error(`mint vault error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED' && onReject) { - await onReject('Mint vault swap'); - } else if (onError) { - await onError('Mint vault swap'); - } - } + const { request } = await prepareWriteContract({ + address: contracts.mainnet.OETHVaultCore.address, + abi: contracts.mainnet.OETHVaultCore.abi, + functionName: 'mint', + args: [tokenIn.address, amountIn, minAmountOut], + }); + const { hash } = await writeContract(request); + + return hash; }; export default { diff --git a/libs/oeth/swap/src/actions/swapCurve/index.ts b/libs/oeth/swap/src/actions/swapCurve/index.ts index 680619264..256f53ba1 100644 --- a/libs/oeth/swap/src/actions/swapCurve/index.ts +++ b/libs/oeth/swap/src/actions/swapCurve/index.ts @@ -5,7 +5,6 @@ import { getPublicClient, prepareWriteContract, readContract, - waitForTransaction, writeContract, } from '@wagmi/core'; import { formatUnits, maxUint256, parseUnits } from 'viem'; @@ -35,7 +34,7 @@ const estimateAmount: EstimateAmount = async ({ const curveConfig = curveRoutes[tokenIn.symbol]?.[tokenOut.symbol]; if (isNilOrEmpty(curveConfig)) { - console.error( + throw new Error( `No curve route found, verify exchange mapping ${tokenIn.symbol} -> ${tokenOut.symbol}`, ); } @@ -78,11 +77,9 @@ const estimateGas: EstimateGas = async ({ const curveConfig = curveRoutes[tokenIn.symbol]?.[tokenOut.symbol]; if (isNilOrEmpty(curveConfig)) { - console.error( + throw new Error( `No curve route found, verify exchange mapping ${tokenIn.symbol} -> ${tokenOut.symbol}`, ); - - return gasEstimate; } try { @@ -100,9 +97,7 @@ const estimateGas: EstimateGas = async ({ ...(isNilOrEmpty(tokenIn.address) && { value: amountIn }), }); } catch (e) { - console.error( - `swap curve exchange multiple gas estimate error, returning fix estimate! \n${e.message}`, - ); + console.log(`Swap curve uses fix gas estimate: 350000`); gasEstimate = 350000n; } @@ -152,7 +147,9 @@ const estimateApprovalGas: EstimateApprovalGas = async ({ args: [curve.CurveRegistryExchange.address, amountIn], account: address, }); - } catch {} + } catch { + console.log(`Swap curve uses fix approval gas estimate: 0`); + } return approvalEstimate; }; @@ -207,36 +204,16 @@ const estimateRoute: EstimateRoute = async ({ }; }; -const approve: Approve = async ({ - tokenIn, - amountIn, - curve, - onSuccess, - onError, - onReject, -}) => { - try { - const { request } = await prepareWriteContract({ - address: tokenIn.address, - abi: erc20ABI, - functionName: 'approve', - args: [curve.CurveRegistryExchange.address, amountIn], - }); - const { hash } = await writeContract(request); - const txReceipt = await waitForTransaction({ hash }); +const approve: Approve = async ({ tokenIn, amountIn, curve }) => { + const { request } = await prepareWriteContract({ + address: tokenIn.address, + abi: erc20ABI, + functionName: 'approve', + args: [curve.CurveRegistryExchange.address, amountIn], + }); + const { hash } = await writeContract(request); - console.log(`swap curve exchange multiple approval done!`); - if (onSuccess) { - await onSuccess(txReceipt); - } - } catch (e) { - console.error(`swap curve exchange multiple approval error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED' && onReject) { - await onReject('Swap Curve approval'); - } else if (onError) { - await onError('Swap Curve approval'); - } - } + return hash; }; const swap: Swap = async ({ @@ -246,24 +223,17 @@ const swap: Swap = async ({ amountOut, slippage, curve, - onSuccess, - onError, - onReject, }) => { const { address } = getAccount(); if (amountIn === 0n || isNilOrEmpty(address)) { - return; + return null; } const approved = await allowance({ tokenIn, tokenOut, curve }); if (approved < amountIn) { - console.error(`swap curve exchange multiple is not approved`); - if (onError) { - await onError('swap curve exchange multiple is not approved'); - } - return; + throw new Error(`Swap curve is not approved`); } const minAmountOut = parseUnits( @@ -277,43 +247,21 @@ const swap: Swap = async ({ const curveConfig = curveRoutes[tokenIn.symbol]?.[tokenOut?.symbol]; if (isNilOrEmpty(curveConfig)) { - console.error( + throw new Error( `No curve route found, verify exchange mapping ${tokenIn.symbol} -> ${tokenOut.symbol}`, ); - if (onError) { - await onError('No curve route found'); - } - return; } - try { - const { request } = await prepareWriteContract({ - address: curve.CurveRegistryExchange.address, - abi: curve.CurveRegistryExchange.abi, - functionName: 'exchange_multiple', - args: [ - curveConfig.routes, - curveConfig.swapParams, - amountIn, - minAmountOut, - ], - ...(isNilOrEmpty(tokenIn.address) && { value: amountIn }), - }); - const { hash } = await writeContract(request); - const txReceipt = await waitForTransaction({ hash }); + const { request } = await prepareWriteContract({ + address: curve.CurveRegistryExchange.address, + abi: curve.CurveRegistryExchange.abi, + functionName: 'exchange_multiple', + args: [curveConfig.routes, curveConfig.swapParams, amountIn, minAmountOut], + ...(isNilOrEmpty(tokenIn.address) && { value: amountIn }), + }); + const { hash } = await writeContract(request); - console.log('swap curve exchange multiple done!'); - if (onSuccess) { - await onSuccess(txReceipt); - } - } catch (e) { - console.error(`swap curve exchange multiple error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED' && onReject) { - await onReject('Swap Curve exchange multiple'); - } else if (onError) { - await onError('Swap Curve exchange multiple'); - } - } + return hash; }; export default { diff --git a/libs/oeth/swap/src/actions/swapCurveEth.ts b/libs/oeth/swap/src/actions/swapCurveEth.ts index c38e8fe64..b69776272 100644 --- a/libs/oeth/swap/src/actions/swapCurveEth.ts +++ b/libs/oeth/swap/src/actions/swapCurveEth.ts @@ -5,7 +5,6 @@ import { getPublicClient, prepareWriteContract, readContract, - waitForTransaction, writeContract, } from '@wagmi/core'; import { formatUnits, isAddressEqual, maxUint256, parseUnits } from 'viem'; @@ -93,9 +92,7 @@ const estimateGas: EstimateGas = async ({ account: address ?? ETH_ADDRESS_CURVE, }); } catch (e) { - console.error( - `swap curve OETHPool gas estimate error, returning fix estimate!\n${e.message}`, - ); + console.log(`Swap curve OETH Pool uses fix gas estimate: 180000`); gasEstimate = 180000n; } @@ -110,6 +107,7 @@ const allowance: Allowance = async () => { const estimateApprovalGas: EstimateApprovalGas = async () => { // ETH doesn't need approval + console.log(`Swap curve OETH Pool uses fix approval gas estimate: 0`); return 0n; }; @@ -163,11 +161,9 @@ const estimateRoute: EstimateRoute = async ({ }; }; -const approve: Approve = async ({ onSuccess }) => { +const approve: Approve = async () => { // ETH doesn't need approval - if (onSuccess) { - await onSuccess(null); - } + return null; }; const swap: Swap = async ({ @@ -177,12 +173,9 @@ const swap: Swap = async ({ amountOut, slippage, curve, - onSuccess, - onError, - onReject, }) => { if (amountIn === 0n) { - return; + return null; } const minAmountOut = parseUnits( @@ -193,42 +186,29 @@ const swap: Swap = async ({ tokenOut.decimals, ); - try { - const { request } = await prepareWriteContract({ - address: contracts.mainnet.curveOethPool.address, - abi: contracts.mainnet.curveOethPool.abi, - functionName: 'exchange', - args: [ - BigInt( - curve.OethPoolUnderlyings.findIndex((t) => - isAddressEqual(t, tokenIn.address ?? ETH_ADDRESS_CURVE), - ), + const { request } = await prepareWriteContract({ + address: contracts.mainnet.curveOethPool.address, + abi: contracts.mainnet.curveOethPool.abi, + functionName: 'exchange', + args: [ + BigInt( + curve.OethPoolUnderlyings.findIndex((t) => + isAddressEqual(t, tokenIn.address ?? ETH_ADDRESS_CURVE), ), - BigInt( - curve.OethPoolUnderlyings.findIndex((t) => - isAddressEqual(t, tokenOut.address ?? ETH_ADDRESS_CURVE), - ), + ), + BigInt( + curve.OethPoolUnderlyings.findIndex((t) => + isAddressEqual(t, tokenOut.address ?? ETH_ADDRESS_CURVE), ), - amountIn, - minAmountOut, - ], - ...(isNilOrEmpty(tokenIn.address) && { value: amountIn }), - }); - const { hash } = await writeContract(request); - const txReceipt = await waitForTransaction({ hash }); + ), + amountIn, + minAmountOut, + ], + ...(isNilOrEmpty(tokenIn.address) && { value: amountIn }), + }); + const { hash } = await writeContract(request); - console.log('swap curve OETHPool done!'); - if (onSuccess) { - await onSuccess(txReceipt); - } - } catch (e) { - console.error(`swap curve OETHPool error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED' && onReject) { - await onReject('Swap Curve exchange'); - } else if (onError) { - await onError('Swap Curve exchange'); - } - } + return hash; }; export default { diff --git a/libs/oeth/swap/src/actions/swapZapperEth.ts b/libs/oeth/swap/src/actions/swapZapperEth.ts index d3799be32..b2499059d 100644 --- a/libs/oeth/swap/src/actions/swapZapperEth.ts +++ b/libs/oeth/swap/src/actions/swapZapperEth.ts @@ -6,7 +6,6 @@ import { getPublicClient, prepareWriteContract, readContract, - waitForTransaction, writeContract, } from '@wagmi/core'; import { formatUnits, maxUint256 } from 'viem'; @@ -26,7 +25,7 @@ const estimateAmount: EstimateAmount = async ({ amountIn }) => { }; const estimateGas: EstimateGas = async ({ amountIn }) => { - let gasEstimate = 200000n; + let gasEstimate = 0n; const { address } = getAccount(); @@ -44,7 +43,10 @@ const estimateGas: EstimateGas = async ({ amountIn }) => { value: amountIn, account: address, }); - } catch {} + } catch { + console.log(`Swap zapper uses fix gas estimate: 200000`); + gasEstimate = 200000n; + } return gasEstimate; }; @@ -97,7 +99,9 @@ const estimateApprovalGas: EstimateApprovalGas = async ({ args: [contracts.mainnet.OETHZapper.address, amountIn], account: address, }); - } catch {} + } catch { + console.log(`Swap zapper uses fix approval gas estimate: 0`); + } return approvalEstimate; }; @@ -140,92 +144,44 @@ const estimateRoute: EstimateRoute = async ({ }; }; -const approve: Approve = async ({ - tokenIn, - tokenOut, - amountIn, - onSuccess, - onError, - onReject, -}) => { - if ( - (isNilOrEmpty(tokenIn.address) || isNilOrEmpty(tokenOut.address)) && - onSuccess - ) { - console.log(`swap eth does not require approval!`); - onSuccess(null); +const approve: Approve = async ({ tokenIn, tokenOut, amountIn }) => { + if (isNilOrEmpty(tokenIn.address) || isNilOrEmpty(tokenOut.address)) { + return null; } - try { - const { request } = await prepareWriteContract({ - address: tokenIn.address, - abi: erc20ABI, - functionName: 'approve', - args: [contracts.mainnet.OETHZapper.address, amountIn], - }); - const { hash } = await writeContract(request); - const txReceipt = await waitForTransaction({ hash }); - - console.log(`swap zapper eth approval done!`); - if (onSuccess) { - await onSuccess(txReceipt); - } - } catch (e) { - console.error(`swap zapper eth approval error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED' && onReject) { - await onReject('Swap Zapper ETH approval'); - } else if (onError) { - await onError('Swap Zapper ETH approval'); - } - } + const { request } = await prepareWriteContract({ + address: tokenIn.address, + abi: erc20ABI, + functionName: 'approve', + args: [contracts.mainnet.OETHZapper.address, amountIn], + }); + const { hash } = await writeContract(request); + + return hash; }; -const swap: Swap = async ({ - tokenIn, - tokenOut, - amountIn, - onSuccess, - onError, - onReject, -}) => { +const swap: Swap = async ({ tokenIn, tokenOut, amountIn }) => { const { address } = getAccount(); if (amountIn === 0n || isNilOrEmpty(address)) { - return; + return null; } const approved = await allowance({ tokenIn, tokenOut }); if (approved < amountIn) { - console.error(`swap zapper eth is not approved`); - if (onError) { - await onError('Swap Zapper Eth is not approved'); - } - return; + throw new Error(`Swap zapper is not approved`); } - try { - const { request } = await prepareWriteContract({ - address: contracts.mainnet.OETHZapper.address, - abi: contracts.mainnet.OETHZapper.abi, - functionName: 'deposit', - value: amountIn, - }); - const { hash } = await writeContract(request); - const txReceipt = await waitForTransaction({ hash }); - - console.log('swap zapper eth done!'); - if (onSuccess) { - await onSuccess(txReceipt); - } - } catch (e) { - console.error(`swap zapper eth error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED' && onReject) { - await onReject('Swap Zapper Eth'); - } else if (onError) { - await onError('Swap Zapper Eth'); - } - } + const { request } = await prepareWriteContract({ + address: contracts.mainnet.OETHZapper.address, + abi: contracts.mainnet.OETHZapper.abi, + functionName: 'deposit', + value: amountIn, + }); + const { hash } = await writeContract(request); + + return hash; }; export default { diff --git a/libs/oeth/swap/src/actions/swapZapperSfrxeth.ts b/libs/oeth/swap/src/actions/swapZapperSfrxeth.ts index 5d855e988..225b7de42 100644 --- a/libs/oeth/swap/src/actions/swapZapperSfrxeth.ts +++ b/libs/oeth/swap/src/actions/swapZapperSfrxeth.ts @@ -7,7 +7,6 @@ import { prepareWriteContract, readContract, readContracts, - waitForTransaction, writeContract, } from '@wagmi/core'; import { formatUnits, maxUint256, parseUnits } from 'viem'; @@ -54,6 +53,7 @@ const estimateAmount: EstimateAmount = async ({ tokenOut, amountIn }) => { }; const estimateGas: EstimateGas = async () => { + console.log(`Swap zapper sfrxETH uses fix gas estimate: 90000`); return 90000n; }; @@ -106,6 +106,7 @@ const estimateApprovalGas: EstimateApprovalGas = async ({ account: address, }); } catch { + console.log(`Swap zapper sfrxETH uses fix approval gas estimate: 64000`); approvalEstimate = 64000n; } @@ -150,44 +151,20 @@ const estimateRoute: EstimateRoute = async ({ }; }; -const approve: Approve = async ({ - tokenIn, - tokenOut, - amountIn, - onSuccess, - onError, - onReject, -}) => { - if ( - (isNilOrEmpty(tokenIn.address) || isNilOrEmpty(tokenOut.address)) && - onSuccess - ) { - console.log(`swap zapper does not require approval!`); - onSuccess(null); +const approve: Approve = async ({ tokenIn, tokenOut, amountIn }) => { + if (isNilOrEmpty(tokenIn.address) || isNilOrEmpty(tokenOut.address)) { + return null; } - try { - const { request } = await prepareWriteContract({ - address: tokenIn.address, - abi: erc20ABI, - functionName: 'approve', - args: [contracts.mainnet.OETHZapper.address, amountIn], - }); - const { hash } = await writeContract(request); - const txReceipt = await waitForTransaction({ hash }); + const { request } = await prepareWriteContract({ + address: tokenIn.address, + abi: erc20ABI, + functionName: 'approve', + args: [contracts.mainnet.OETHZapper.address, amountIn], + }); + const { hash } = await writeContract(request); - console.log(`swap zapper approval done!`); - if (onSuccess) { - await onSuccess(txReceipt); - } - } catch (e) { - console.error(`swap zapper approval error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED' && onReject) { - await onReject('Swap Zapper approval'); - } else if (onError) { - await onError('Swap Zapper approval'); - } - } + return hash; }; const swap: Swap = async ({ @@ -196,9 +173,6 @@ const swap: Swap = async ({ amountIn, slippage, amountOut, - onSuccess, - onError, - onReject, }) => { const { address } = getAccount(); @@ -209,11 +183,7 @@ const swap: Swap = async ({ const approved = await allowance({ tokenIn, tokenOut }); if (approved < amountIn) { - console.error(`swap zapper is not approved`); - if (onError) { - await onError('Swap Zapper is not approved'); - } - return; + throw new Error(`Swap zapper sfrxETH is not approved`); } const minAmountOut = parseUnits( @@ -224,28 +194,15 @@ const swap: Swap = async ({ tokenOut.decimals, ); - try { - const { request } = await prepareWriteContract({ - address: contracts.mainnet.OETHZapper.address, - abi: contracts.mainnet.OETHZapper.abi, - functionName: 'depositSFRXETH', - args: [amountIn, minAmountOut], - }); - const { hash } = await writeContract(request); - const txReceipt = await waitForTransaction({ hash }); + const { request } = await prepareWriteContract({ + address: contracts.mainnet.OETHZapper.address, + abi: contracts.mainnet.OETHZapper.abi, + functionName: 'depositSFRXETH', + args: [amountIn, minAmountOut], + }); + const { hash } = await writeContract(request); - console.log('swap zapper sfrxEth done!'); - if (onSuccess) { - await onSuccess(txReceipt); - } - } catch (e) { - console.error(`swap zapper sfrxEth error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED' && onReject) { - await onReject('Swap Zapper'); - } else if (onError) { - await onError('Swap Zapper'); - } - } + return hash; }; export default { diff --git a/libs/oeth/swap/src/actions/unwrapWOETH.ts b/libs/oeth/swap/src/actions/unwrapWOETH.ts index 13bfdea9c..863efa020 100644 --- a/libs/oeth/swap/src/actions/unwrapWOETH.ts +++ b/libs/oeth/swap/src/actions/unwrapWOETH.ts @@ -5,7 +5,6 @@ import { getPublicClient, prepareWriteContract, readContract, - waitForTransaction, writeContract, } from '@wagmi/core'; import { formatUnits, maxUint256 } from 'viem'; @@ -68,7 +67,9 @@ const estimateGas: EstimateGas = async ({ amountIn }) => { args: [amountIn, whales.mainnet.WOETH, whales.mainnet.WOETH], account: whales.mainnet.WOETH, }); - } catch {} + } catch { + console.log(`Unwrap WOETH uses fix gas estimate: 0`); + } return gasEstimate; }; @@ -80,6 +81,7 @@ const allowance: Allowance = async () => { const estimateApprovalGas: EstimateApprovalGas = async () => { // Unwrap WOETH does not require approval + console.log(`Unwrap WOETH uses fix gas estimate: 0`); return 0n; }; @@ -121,11 +123,9 @@ const estimateRoute: EstimateRoute = async ({ }; }; -const approve: Approve = async ({ onSuccess }) => { +const approve: Approve = async () => { // Unwrap WOETH does not require approval - if (onSuccess) { - await onSuccess(null); - } + return null; }; const swap: Swap = async ({ amountIn }) => { @@ -135,21 +135,15 @@ const swap: Swap = async ({ amountIn }) => { return; } - try { - const { request } = await prepareWriteContract({ - address: contracts.mainnet.WOETH.address, - abi: contracts.mainnet.WOETH.abi, - functionName: 'redeem', - args: [amountIn, address, address], - }); - const { hash } = await writeContract(request); - await waitForTransaction({ hash }); - // TODO trigger notification - console.log('unwrap woeth done!'); - } catch (e) { - // TODO trigger notification - console.log(`unwrap woeth error!\n${e.message}`); - } + const { request } = await prepareWriteContract({ + address: contracts.mainnet.WOETH.address, + abi: contracts.mainnet.WOETH.abi, + functionName: 'redeem', + args: [amountIn, address, address], + }); + const { hash } = await writeContract(request); + + return hash; }; export default { diff --git a/libs/oeth/swap/src/actions/wrapOETH.ts b/libs/oeth/swap/src/actions/wrapOETH.ts index 075f0e696..869e97e46 100644 --- a/libs/oeth/swap/src/actions/wrapOETH.ts +++ b/libs/oeth/swap/src/actions/wrapOETH.ts @@ -6,7 +6,6 @@ import { getPublicClient, prepareWriteContract, readContract, - waitForTransaction, writeContract, } from '@wagmi/core'; import { formatUnits } from 'viem'; @@ -69,7 +68,9 @@ const estimateGas: EstimateGas = async ({ amountIn }) => { args: [amountIn, whales.mainnet.OETH], account: whales.mainnet.OETH, }); - } catch {} + } catch { + console.log(`Wrap OETH uses fix gas estimate: 0`); + } return gasEstimate; }; @@ -112,7 +113,9 @@ const estimateApprovalGas: EstimateApprovalGas = async ({ args: [contracts.mainnet.WOETH.address, amountIn], account: address, }); - } catch {} + } catch { + console.log(`Wrap OETH uses fix approval gas estimate: 0`); + } return approvalEstimate; }; @@ -155,83 +158,40 @@ const estimateRoute: EstimateRoute = async ({ }; }; -const approve: Approve = async ({ - tokenIn, - amountIn, - onSuccess, - onError, - onReject, -}) => { - try { - const { request } = await prepareWriteContract({ - address: tokenIn.address, - abi: erc20ABI, - functionName: 'approve', - args: [contracts.mainnet.WOETH.address, amountIn], - }); - const { hash } = await writeContract(request); - const txReceipt = await waitForTransaction({ hash }); - - console.log(`wrap oeth approval done!`); - if (onSuccess) { - await onSuccess(txReceipt); - } - } catch (e) { - console.error(`wrap oeth approval error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED' && onReject) { - await onReject('Wrap OETH approval'); - } else if (onError) { - await onError('Wrap OETH approval'); - } - } +const approve: Approve = async ({ tokenIn, amountIn }) => { + const { request } = await prepareWriteContract({ + address: tokenIn.address, + abi: erc20ABI, + functionName: 'approve', + args: [contracts.mainnet.WOETH.address, amountIn], + }); + const { hash } = await writeContract(request); + + return hash; }; -const swap: Swap = async ({ - tokenIn, - tokenOut, - amountIn, - onSuccess, - onError, - onReject, -}) => { +const swap: Swap = async ({ tokenIn, tokenOut, amountIn }) => { const { address } = getAccount(); if (amountIn === 0n || isNilOrEmpty(address)) { - return; + return null; } const approved = await allowance({ tokenIn, tokenOut }); if (approved < amountIn) { - console.error(`wrap oeth is not approved`); - if (onError) { - await onError('Wrap OETH is not approved'); - } - return; + throw new Error(`Wrap OETH is not approved`); } - try { - const { request } = await prepareWriteContract({ - address: contracts.mainnet.WOETH.address, - abi: contracts.mainnet.WOETH.abi, - functionName: 'deposit', - args: [amountIn, address], - }); - const { hash } = await writeContract(request); - const txReceipt = await waitForTransaction({ hash }); - - console.log('wrap oeth done!'); - if (onSuccess) { - await onSuccess(txReceipt); - } - } catch (e) { - console.error(`wrap oeth error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED' && onReject) { - await onReject('Wrap OETH'); - } else if (onError) { - await onError('Wrap OETH'); - } - } + const { request } = await prepareWriteContract({ + address: contracts.mainnet.WOETH.address, + abi: contracts.mainnet.WOETH.abi, + functionName: 'deposit', + args: [amountIn, address], + }); + const { hash } = await writeContract(request); + + return hash; }; export default { diff --git a/libs/oeth/swap/src/hooks.ts b/libs/oeth/swap/src/hooks.tsx similarity index 69% rename from libs/oeth/swap/src/hooks.ts rename to libs/oeth/swap/src/hooks.tsx index 3cf41aa7b..24985d31a 100644 --- a/libs/oeth/swap/src/hooks.ts +++ b/libs/oeth/swap/src/hooks.tsx @@ -1,6 +1,12 @@ import { useCallback, useMemo } from 'react'; -import { usePushActivity, useUpdateActivity } from '@origin/oeth/shared'; +import { + ApprovalNotification, + SwapNotification, + useDeleteActivity, + usePushActivity, + useUpdateActivity, +} from '@origin/oeth/shared'; import { useCurve, usePushNotification, @@ -8,6 +14,7 @@ import { } from '@origin/shared/providers'; import { isNilOrEmpty } from '@origin/shared/utils'; import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { waitForTransaction } from '@wagmi/core'; import { produce } from 'immer'; import { useIntl } from 'react-intl'; import { useAccount, useQueryClient as useWagmiClient } from 'wagmi'; @@ -162,12 +169,18 @@ export const useSwapRouteAllowance = (route: SwapRoute) => { route?.tokenOut.symbol, route?.action, ], - queryFn: () => - swapActions[route.action].allowance({ - tokenIn: route.tokenIn, - tokenOut: route.tokenOut, - curve, - }), + queryFn: async () => { + let res = 0n; + try { + res = await swapActions[route.action].allowance({ + tokenIn: route.tokenIn, + tokenOut: route.tokenOut, + curve, + }); + } catch {} + + return res; + }, enabled: !isNilOrEmpty(route), placeholderData: 0n, }); @@ -182,7 +195,7 @@ export const useHandleApprove = () => { const pushNotification = usePushNotification(); const pushActivity = usePushActivity(); const updateActivity = useUpdateActivity(); - + const deleteActivity = useDeleteActivity(); const [ { amountIn, amountOut, selectedSwapRoute, tokenIn, tokenOut }, setSwapState, @@ -206,12 +219,20 @@ export const useHandleApprove = () => { amountIn, amountOut, }); - await swapActions[selectedSwapRoute.action].approve({ - tokenIn, - tokenOut, - amountIn, - curve, - onSuccess: (txReceipt) => { + try { + const hash = await swapActions[selectedSwapRoute.action].approve({ + tokenIn, + tokenOut, + amountIn, + curve, + }); + setSwapState( + produce((draft) => { + draft.isApprovalLoading = false; + }), + ); + if (!isNilOrEmpty(hash)) { + const txReceipt = await waitForTransaction({ hash }); wagmiClient.invalidateQueries({ queryKey: ['swap_balance'], }); @@ -219,37 +240,54 @@ export const useHandleApprove = () => { queryKey: ['swap_allowance'], }); updateActivity({ ...activity, status: 'success', txReceipt }); - setSwapState( - produce((draft) => { - draft.isApprovalLoading = false; - }), - ); - }, - onError: (error: string) => { - updateActivity({ ...activity, status: 'error', error }); - setSwapState( - produce((draft) => { - draft.isApprovalLoading = false; - }), - ); - }, - onReject: () => { + pushNotification({ + content: ( + + ), + }); + } + } catch (error) { + setSwapState( + produce((draft) => { + draft.isApprovalLoading = false; + }), + ); + if (error.cause.name === 'UserRejectedRequestError') { + deleteActivity(activity.id); pushNotification({ title: intl.formatMessage({ defaultMessage: 'Approval Cancelled' }), + message: intl.formatMessage({ + defaultMessage: 'User rejected operation', + }), severity: 'info', }); - setSwapState( - produce((draft) => { - draft.isApprovalLoading = false; - }), - ); - }, - }); + } else { + updateActivity({ + ...activity, + status: 'error', + error: error.shortMessage, + }); + pushNotification({ + content: ( + + ), + }); + } + } }, [ address, amountIn, amountOut, curve, + deleteActivity, intl, pushActivity, pushNotification, @@ -273,6 +311,7 @@ export const useHandleSwap = () => { const pushNotification = usePushNotification(); const pushActivity = usePushActivity(); const updateActivity = useUpdateActivity(); + const deleteActivity = useDeleteActivity(); const [ { amountIn, amountOut, selectedSwapRoute, tokenIn, tokenOut }, setSwapState, @@ -296,43 +335,78 @@ export const useHandleSwap = () => { draft.isSwapLoading = true; }), ); - await swapActions[selectedSwapRoute.action].swap({ - tokenIn, - tokenOut, - amountIn, - estimatedRoute: selectedSwapRoute, - slippage, - amountOut, - curve, - onSuccess: (txReceipt) => { + try { + const hash = await swapActions[selectedSwapRoute.action].swap({ + tokenIn, + tokenOut, + amountIn, + estimatedRoute: selectedSwapRoute, + slippage, + amountOut, + curve, + }); + setSwapState( + produce((draft) => { + draft.isSwapLoading = false; + }), + ); + if (!isNilOrEmpty(hash)) { + const txReceipt = await waitForTransaction({ hash }); wagmiClient.invalidateQueries({ queryKey: ['swap_balance'], }); queryClient.invalidateQueries({ queryKey: ['swap_allowance'], }); + pushNotification({ + content: ( + + ), + }); updateActivity({ ...activity, status: 'success', txReceipt }); - }, - onError: (error: string) => { - updateActivity({ ...activity, status: 'error', error }); - }, - onReject: () => { + } + } catch (error) { + setSwapState( + produce((draft) => { + draft.isSwapLoading = false; + }), + ); + if (error.cause.name === 'UserRejectedRequestError') { + deleteActivity(activity.id); pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Swap Cancelled' }), + title: intl.formatMessage({ defaultMessage: 'Operation Cancelled' }), + message: intl.formatMessage({ + defaultMessage: 'User rejected operation', + }), severity: 'info', }); - }, - }); - setSwapState( - produce((draft) => { - draft.isSwapLoading = false; - }), - ); + } else { + updateActivity({ + ...activity, + status: 'error', + error: error.shortMessage, + }); + pushNotification({ + content: ( + + ), + }); + } + } }, [ address, amountIn, amountOut, curve, + deleteActivity, intl, pushActivity, pushNotification, diff --git a/libs/oeth/swap/src/state.ts b/libs/oeth/swap/src/state.ts index 0e60a175f..dfd1b3893 100644 --- a/libs/oeth/swap/src/state.ts +++ b/libs/oeth/swap/src/state.ts @@ -10,7 +10,7 @@ import { createContainer } from 'react-tracked'; import { swapActions } from './actions'; import { getAvailableRoutes } from './utils'; -import type { SwapState } from './types'; +import type { EstimatedSwapRoute, SwapState } from './types'; export const { Provider: SwapProvider, useTracked: useSwapState } = createContainer(() => { @@ -58,19 +58,39 @@ export const { Provider: SwapProvider, useTracked: useSwapState } = slippage, state.amountIn.toString(), ] as const, - queryFn: async () => - swapActions[route.action].estimateRoute({ - tokenIn: route.tokenIn, - tokenOut: route.tokenOut, - amountIn: state.amountIn, - amountOut: state.amountOut, - route, - slippage, - curve: { - CurveRegistryExchange, - OethPoolUnderlyings, - }, - }), + queryFn: async () => { + let res: EstimatedSwapRoute; + try { + res = await swapActions[route.action].estimateRoute({ + tokenIn: route.tokenIn, + tokenOut: route.tokenOut, + amountIn: state.amountIn, + amountOut: state.amountOut, + route, + slippage, + curve: { + CurveRegistryExchange, + OethPoolUnderlyings, + }, + }); + } catch (error) { + console.error( + `Fail to estimate route ${route.action}\n${error.message}`, + ); + res = { + tokenIn: route.tokenIn, + tokenOut: route.tokenOut, + estimatedAmount: 0n, + action: route.action, + allowanceAmount: 0n, + approvalGas: 0n, + gas: 0n, + rate: 0, + }; + } + + return res; + }, }), ), ); diff --git a/libs/oeth/swap/src/types.ts b/libs/oeth/swap/src/types.ts index 4cf5e9db0..4a9f6076b 100644 --- a/libs/oeth/swap/src/types.ts +++ b/libs/oeth/swap/src/types.ts @@ -1,6 +1,5 @@ import type { Contract, Token } from '@origin/shared/contracts'; import type { HexAddress } from '@origin/shared/utils'; -import type { TransactionReceipt } from 'viem'; export type TokenSource = 'tokenIn' | 'tokenOut'; @@ -25,9 +24,6 @@ type Args = { CurveRegistryExchange: Contract; OethPoolUnderlyings: HexAddress[]; }; - onSuccess?: (txReceipt: TransactionReceipt) => void | Promise; - onError?: (msg: string) => void | Promise; - onReject?: (msg: string) => void | Promise; }; export type EstimateAmount = ( @@ -63,17 +59,8 @@ export type EstimateApprovalGas = ( ) => Promise; export type Approve = ( - args: Pick< - Args, - | 'tokenIn' - | 'tokenOut' - | 'amountIn' - | 'curve' - | 'onSuccess' - | 'onError' - | 'onReject' - >, -) => Promise; + args: Pick, +) => Promise; export type Swap = ( args: Pick< @@ -85,11 +72,8 @@ export type Swap = ( | 'slippage' | 'estimatedRoute' | 'curve' - | 'onSuccess' - | 'onError' - | 'onReject' >, -) => Promise; +) => Promise; export type SwapApi = { estimateAmount: EstimateAmount; From 5cd6fea216b0c68d399ae7f6849a6983c57c360d Mon Sep 17 00:00:00 2001 From: toniocodo Date: Wed, 4 Oct 2023 15:33:23 +0200 Subject: [PATCH 07/11] feat: normalize error, remove Notif unused attributes --- libs/oeth/redeem/src/hooks.tsx | 8 ++-- libs/oeth/redeem/src/state.ts | 14 +++---- .../components/ActivityPopover.tsx | 16 ++++++-- .../components/ApprovalNotification.tsx | 6 +-- .../components/RedeemNotification.tsx | 6 +-- .../components/SwapNotification.tsx | 6 +-- libs/oeth/swap/src/actions/index.ts | 2 + libs/oeth/swap/src/components/BestRoutes.tsx | 4 +- .../swap/src/components/SwapRouteCard.tsx | 39 ++++++++++++------- libs/oeth/swap/src/hooks.tsx | 10 ++--- libs/shared/utils/src/errors.ts | 6 +++ libs/shared/utils/src/index.ts | 1 + 12 files changed, 72 insertions(+), 46 deletions(-) create mode 100644 libs/shared/utils/src/errors.ts diff --git a/libs/oeth/redeem/src/hooks.tsx b/libs/oeth/redeem/src/hooks.tsx index 99081d424..cb125289d 100644 --- a/libs/oeth/redeem/src/hooks.tsx +++ b/libs/oeth/redeem/src/hooks.tsx @@ -8,7 +8,7 @@ import { } from '@origin/oeth/shared'; import { contracts, tokens } from '@origin/shared/contracts'; import { usePushNotification, useSlippage } from '@origin/shared/providers'; -import { isNilOrEmpty } from '@origin/shared/utils'; +import { isNilOrEmpty, isUserRejected } from '@origin/shared/utils'; import { prepareWriteContract, waitForTransaction, @@ -103,7 +103,7 @@ export const useHandleRedeem = () => { ), }); } catch (error) { - if (error.cause.name === 'UserRejectedRequestError') { + if (isUserRejected(error)) { deleteActivity(activity.id); pushNotification({ title: intl.formatMessage({ defaultMessage: 'Redeem Cancelled' }), @@ -116,14 +116,14 @@ export const useHandleRedeem = () => { updateActivity({ ...activity, status: 'error', - error: error.shortMessage, + error: error?.shortMessage ?? error.message, }); pushNotification({ content: ( ), }); diff --git a/libs/oeth/redeem/src/state.ts b/libs/oeth/redeem/src/state.ts index a15ae9d57..f428a44ee 100644 --- a/libs/oeth/redeem/src/state.ts +++ b/libs/oeth/redeem/src/state.ts @@ -77,7 +77,7 @@ export const { Provider: RedeemProvider, useTracked: useRedeemState } = try { splitEstimates = await queryClient.fetchQuery({ queryKey: ['splitEstimates', state.amountIn.toString()], - queryFn: () => + queryFn: async () => readContract({ address: contracts.mainnet.OETHVaultCore.address, abi: contracts.mainnet.OETHVaultCore.abi, @@ -85,8 +85,8 @@ export const { Provider: RedeemProvider, useTracked: useRedeemState } = args: [state.amountIn], }), }); - } catch (e) { - console.error(`redeem vault estimate amount error.\n${e.message}`); + } catch (error) { + console.error(`Fail to estimate redeem operation.\n${error.message}`); setState( produce((draft) => { draft.amountIn = 0n; @@ -99,7 +99,7 @@ export const { Provider: RedeemProvider, useTracked: useRedeemState } = title: intl.formatMessage({ defaultMessage: 'Error while estimating', }), - message: e.shortMessage, + message: error?.shortMessage ?? error.message, severity: 'error', }); @@ -145,10 +145,8 @@ export const { Provider: RedeemProvider, useTracked: useRedeemState } = account: whales.mainnet.OETH, }), }); - } catch (e) { - console.error( - `redeem vault estimate gas error. Using default!\n${e.message}`, - ); + } catch (error) { + console.log(`Redeem uses fix gas estimate: 1500000`); gasEstimate = 1500000n; } diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx index 4ead2ec98..1f1a1e684 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx @@ -75,12 +75,22 @@ export const ActivityPopover = ({ (a) => ({ approval: ( - + ), redeem: ( - + + ), + swap: ( + ), - swap: , })[a.type], ) )} diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx index 497f07610..95debba7d 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx @@ -18,7 +18,7 @@ type ApprovalNotificationProps = { amountIn?: bigint; txReceipt?: TransactionReceipt; error?: string; -} & StackProps; +} & Pick; const title: Record = { pending: defineMessage({ defaultMessage: 'Approving' }), @@ -34,12 +34,12 @@ export const ApprovalNotification = ({ amountIn, txReceipt, error, - ...rest + sx, }: ApprovalNotificationProps) => { const intl = useIntl(); return ( - + diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/RedeemNotification.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/RedeemNotification.tsx index 97bb89ae7..e0024930a 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/RedeemNotification.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/RedeemNotification.tsx @@ -19,7 +19,7 @@ type RedeemNotificationProps = { amountOut?: bigint; txReceipt?: TransactionReceipt; error?: string; -} & StackProps; +} & Pick; const title: Record = { pending: defineMessage({ defaultMessage: 'Redeeming' }), @@ -36,12 +36,12 @@ export const RedeemNotification = ({ amountOut, txReceipt, error, - ...rest + sx, }: RedeemNotificationProps) => { const intl = useIntl(); return ( - + diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/SwapNotification.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/SwapNotification.tsx index c316b21ab..c22b97379 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/SwapNotification.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/SwapNotification.tsx @@ -19,7 +19,7 @@ type SwapNotificationProps = { amountOut?: bigint; txReceipt?: TransactionReceipt; error?: string; -} & StackProps; +} & Pick; const title: Record = { pending: defineMessage({ defaultMessage: 'Swapping' }), @@ -36,12 +36,12 @@ export const SwapNotification = ({ amountOut, txReceipt, error, - ...rest + sx, }: SwapNotificationProps) => { const intl = useIntl(); return ( - + diff --git a/libs/oeth/swap/src/actions/index.ts b/libs/oeth/swap/src/actions/index.ts index 4890e10a5..016b2d6a4 100644 --- a/libs/oeth/swap/src/actions/index.ts +++ b/libs/oeth/swap/src/actions/index.ts @@ -43,9 +43,11 @@ const defaultApi: SwapApi = { }, approve: async () => { console.log('Approve operation not implemented'); + return null; }, swap: async () => { console.log('Route swap operation not implemented'); + return null; }, }; diff --git a/libs/oeth/swap/src/components/BestRoutes.tsx b/libs/oeth/swap/src/components/BestRoutes.tsx index 8de1bc56e..c449b6ed2 100644 --- a/libs/oeth/swap/src/components/BestRoutes.tsx +++ b/libs/oeth/swap/src/components/BestRoutes.tsx @@ -10,8 +10,7 @@ import type { Grid2Props } from '@mui/material'; export type BestRoutesProps = { isLoading: boolean } & Grid2Props; export function BestRoutes(props: Grid2Props) { - const [{ swapRoutes, selectedSwapRoute, isSwapRoutesLoading }] = - useSwapState(); + const [{ swapRoutes, selectedSwapRoute }] = useSwapState(); const handleSelectSwapRoute = useHandleSelectSwapRoute(); return ( @@ -24,7 +23,6 @@ export function BestRoutes(props: Grid2Props) { isBest={index === 0} onSelect={handleSelectSwapRoute} route={route} - isLoading={isSwapRoutesLoading} /> ))} diff --git a/libs/oeth/swap/src/components/SwapRouteCard.tsx b/libs/oeth/swap/src/components/SwapRouteCard.tsx index 2e0bf7f83..f84d8764e 100644 --- a/libs/oeth/swap/src/components/SwapRouteCard.tsx +++ b/libs/oeth/swap/src/components/SwapRouteCard.tsx @@ -21,7 +21,6 @@ import type { EstimatedSwapRoute } from '../types'; export type SwapRouteCardProps = { isSelected: boolean; isBest: boolean; - isLoading: boolean; onSelect: (route: EstimatedSwapRoute) => void; route: EstimatedSwapRoute; } & Omit; @@ -29,19 +28,29 @@ export type SwapRouteCardProps = { export function SwapRouteCard({ isSelected, isBest, - isLoading, onSelect, route, ...rest }: SwapRouteCardProps) { const intl = useIntl(); - const [{ amountIn }] = useSwapState(); + const [{ amountIn, isSwapRoutesLoading }] = useSwapState(); const { data: prices } = usePrices(); - const { data: swapGasPrice, isLoading: swapGasPriceLoading } = useGasPrice( - route.gas, - ); - const { data: approvalGasPrice, isLoading: approvalGasPriceLoading } = - useGasPrice(route.approvalGas, { refetchInterval: 30e3 }); + const { + data: swapGasPrice, + isLoading: swapGasPriceLoading, + isFetching: swapGasPriceFetching, + } = useGasPrice(route.gas, { + refetchInterval: 30e3, + enabled: route.gas > 0n, + }); + const { + data: approvalGasPrice, + isLoading: approvalGasPriceLoading, + isFetching: approvalGasPriceFetching, + } = useGasPrice(route.approvalGas, { + refetchInterval: 30e3, + enabled: route.approvalGas > 0n, + }); const { data: allowance } = useSwapRouteAllowance(route); const estimatedAmount = +formatUnits( @@ -51,7 +60,9 @@ export function SwapRouteCard({ const convertedAmount = (prices?.[route.tokenOut.symbol] ?? 1) * estimatedAmount; const isGasLoading = - isLoading || swapGasPriceLoading || approvalGasPriceLoading; + isSwapRoutesLoading || + (swapGasPriceLoading && swapGasPriceFetching) || + (approvalGasPriceLoading && approvalGasPriceFetching); const gasPrice = swapGasPrice?.gasCostUsd + (allowance < amountIn ? approvalGasPrice?.gasCostUsd : 0); @@ -114,7 +125,7 @@ export function SwapRouteCard({ - {isLoading ? ( + {isSwapRoutesLoading ? ( ) : ( - {isLoading ? ( + {isSwapRoutesLoading ? ( ) : ( formatAmount(route.estimatedAmount, route.tokenOut.decimals) @@ -139,7 +150,7 @@ export function SwapRouteCard({ - {isLoading ? ( + {isSwapRoutesLoading ? ( ) : ( `(${intl.formatNumber(convertedAmount, currencyFormat)})` @@ -152,7 +163,7 @@ export function SwapRouteCard({ fontWeight={500} sx={{ fontSize: 12, marginBlock: { xs: 1.5, md: 1 } }} > - {isLoading ? ( + {isSwapRoutesLoading ? ( ) : ( intl.formatMessage(routeActionLabel[route.action]) @@ -169,7 +180,7 @@ export function SwapRouteCard({ {intl.formatMessage({ defaultMessage: 'Rate:' })} - {isLoading ? ( + {isSwapRoutesLoading ? ( ) : ( `1:${intl.formatNumber(route.rate, quantityFormat)}` diff --git a/libs/oeth/swap/src/hooks.tsx b/libs/oeth/swap/src/hooks.tsx index 24985d31a..f88b608f2 100644 --- a/libs/oeth/swap/src/hooks.tsx +++ b/libs/oeth/swap/src/hooks.tsx @@ -12,7 +12,7 @@ import { usePushNotification, useSlippage, } from '@origin/shared/providers'; -import { isNilOrEmpty } from '@origin/shared/utils'; +import { isNilOrEmpty, isUserRejected } from '@origin/shared/utils'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { waitForTransaction } from '@wagmi/core'; import { produce } from 'immer'; @@ -256,7 +256,7 @@ export const useHandleApprove = () => { draft.isApprovalLoading = false; }), ); - if (error.cause.name === 'UserRejectedRequestError') { + if (isUserRejected(error)) { deleteActivity(activity.id); pushNotification({ title: intl.formatMessage({ defaultMessage: 'Approval Cancelled' }), @@ -269,14 +269,14 @@ export const useHandleApprove = () => { updateActivity({ ...activity, status: 'error', - error: error.shortMessage, + error: error?.shortMessage ?? error.message, }); pushNotification({ content: ( ), }); @@ -375,7 +375,7 @@ export const useHandleSwap = () => { draft.isSwapLoading = false; }), ); - if (error.cause.name === 'UserRejectedRequestError') { + if (isUserRejected(error)) { deleteActivity(activity.id); pushNotification({ title: intl.formatMessage({ defaultMessage: 'Operation Cancelled' }), diff --git a/libs/shared/utils/src/errors.ts b/libs/shared/utils/src/errors.ts new file mode 100644 index 000000000..f99046b61 --- /dev/null +++ b/libs/shared/utils/src/errors.ts @@ -0,0 +1,6 @@ +import { pathEq } from 'ramda'; + +export const isUserRejected = pathEq('UserRejectedRequestError', [ + 'cause', + 'name', +]); diff --git a/libs/shared/utils/src/index.ts b/libs/shared/utils/src/index.ts index aef480533..c571e023d 100644 --- a/libs/shared/utils/src/index.ts +++ b/libs/shared/utils/src/index.ts @@ -1,6 +1,7 @@ export * from './addresses'; export * from './BigInt'; export * from './composeContext'; +export * from './errors'; export * from './formatters'; export * from './isNilOrEmpty'; export * from './types'; From 712c391ac7318f4596bc81bd8f7c78825361036b Mon Sep 17 00:00:00 2001 From: toniocodo Date: Wed, 4 Oct 2023 19:40:05 +0200 Subject: [PATCH 08/11] feat: add clear all activities --- .../components/ActivityPopover.tsx | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx index 1f1a1e684..0439f62c8 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx @@ -1,5 +1,13 @@ -import { Divider, Popover, Stack, Typography, useTheme } from '@mui/material'; +import { + Button, + Divider, + Popover, + Stack, + Typography, + useTheme, +} from '@mui/material'; import { isNilOrEmpty } from '@origin/shared/utils'; +import { produce } from 'immer'; import { descend, pipe, prop, sort, take } from 'ramda'; import { useIntl } from 'react-intl'; @@ -23,12 +31,20 @@ export const ActivityPopover = ({ }: AcitivityPopoverProps) => { const intl = useIntl(); const theme = useTheme(); - const [{ activities, maxVisible }] = useActivityState(); + const [{ activities, maxVisible }, setActivityState] = useActivityState(); const handleClose = () => { setAnchor(null); }; + const handleClearAll = () => { + setActivityState( + produce((state) => { + state.activities = []; + }), + ); + }; + const sortedActivities = pipe( sort(descend(prop('createdOn'))), take(maxVisible), @@ -63,9 +79,19 @@ export const ActivityPopover = ({ }} > - - {intl.formatMessage({ defaultMessage: 'Recent activity' })} - + + + {intl.formatMessage({ defaultMessage: 'Recent activity' })} + + + + }> {isNilOrEmpty(sortedActivities) ? ( From ed8f878bafa2aae612c76aae5e90693a2634b832 Mon Sep 17 00:00:00 2001 From: toniocodo Date: Thu, 5 Oct 2023 00:44:20 +0200 Subject: [PATCH 09/11] feat: add ApyHedaer - extract ApyHeader to oeth/shared - add ApyHeader to RedeemView - eslint ignore generated.ts --- .eslintignore | 1 + .eslintrc.json | 1 - libs/oeth/history/src/queries.generated.ts | 117 ++++-------- libs/oeth/redeem/src/views/RedeemView.tsx | 176 +++++++++--------- libs/oeth/shared/codegen.ts | 2 - .../shared/src/components/ApyHeader/index.tsx | 118 ++++++++++++ .../components/ApyHeader/queries.generated.ts | 43 +++++ .../src/components/ApyHeader}/queries.graphql | 0 libs/oeth/shared/src/components/index.ts | 1 + libs/oeth/swap/src/components/ApyHeader.tsx | 116 ------------ libs/oeth/swap/src/queries.generated.ts | 55 ------ libs/oeth/swap/src/views/SwapView.tsx | 3 +- 12 files changed, 298 insertions(+), 335 deletions(-) create mode 100644 libs/oeth/shared/src/components/ApyHeader/index.tsx create mode 100644 libs/oeth/shared/src/components/ApyHeader/queries.generated.ts rename libs/oeth/{swap/src => shared/src/components/ApyHeader}/queries.graphql (100%) delete mode 100644 libs/oeth/swap/src/components/ApyHeader.tsx delete mode 100644 libs/oeth/swap/src/queries.generated.ts diff --git a/.eslintignore b/.eslintignore index f06235c46..bfe0ccf2b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ node_modules dist +*.generated.ts \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 9559167ba..2aa46d632 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -18,7 +18,6 @@ "@nx/enforce-module-boundaries": [ "error", { - "enforceBuildableLibDependency": true, "allow": [], "depConstraints": [ { diff --git a/libs/oeth/history/src/queries.generated.ts b/libs/oeth/history/src/queries.generated.ts index eb3e0b9ca..cc8d19dcb 100644 --- a/libs/oeth/history/src/queries.generated.ts +++ b/libs/oeth/history/src/queries.generated.ts @@ -1,40 +1,21 @@ -import { graphqlClient } from '@origin/oeth/shared'; -import { useQuery } from '@tanstack/react-query'; +import * as Types from '@origin/oeth/shared'; -import type * as Types from '@origin/oeth/shared'; -import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery, UseQueryOptions } from '@tanstack/react-query'; +import { graphqlClient } from '@origin/oeth/shared'; export type HistoryPageQueryVariables = Types.Exact<{ address: Types.Scalars['String']['input']; offset: Types.Scalars['Int']['input']; filters?: Types.InputMaybe | Types.HistoryType>; }>; -export type HistoryPageQuery = { - __typename?: 'Query'; - addresses: Array<{ - __typename?: 'Address'; - balance: string; - earned: string; - isContract: boolean; - rebasingOption: Types.RebasingOption; - lastUpdated: string; - history: Array<{ - __typename?: 'History'; - type: Types.HistoryType; - value: string; - txHash: string; - timestamp: string; - balance: string; - }>; - }>; -}; -export type HistoryApyQueryVariables = Types.Exact<{ [key: string]: never }>; +export type HistoryPageQuery = { __typename?: 'Query', addresses: Array<{ __typename?: 'Address', balance: string, earned: string, isContract: boolean, rebasingOption: Types.RebasingOption, lastUpdated: string, history: Array<{ __typename?: 'History', type: Types.HistoryType, value: string, txHash: string, timestamp: string, balance: string }> }> }; + +export type HistoryApyQueryVariables = Types.Exact<{ [key: string]: never; }>; + + +export type HistoryApyQuery = { __typename?: 'Query', apies: Array<{ __typename?: 'APY', apy7DayAvg: number, apy30DayAvg: number }> }; -export type HistoryApyQuery = { - __typename?: 'Query'; - apies: Array<{ __typename?: 'APY'; apy7DayAvg: number; apy30DayAvg: number }>; -}; export const HistoryPageDocument = ` query HistoryPage($address: String!, $offset: Int!, $filters: [HistoryType!]) { @@ -59,32 +40,23 @@ export const HistoryPageDocument = ` } } `; -export const useHistoryPageQuery = ( - variables: HistoryPageQueryVariables, - options?: UseQueryOptions, -) => - useQuery( - ['HistoryPage', variables], - graphqlClient( - HistoryPageDocument, - variables, - ), - options, - ); +export const useHistoryPageQuery = < + TData = HistoryPageQuery, + TError = unknown + >( + variables: HistoryPageQueryVariables, + options?: UseQueryOptions + ) => + useQuery( + ['HistoryPage', variables], + graphqlClient(HistoryPageDocument, variables), + options + ); + +useHistoryPageQuery.getKey = (variables: HistoryPageQueryVariables) => ['HistoryPage', variables]; +; -useHistoryPageQuery.getKey = (variables: HistoryPageQueryVariables) => [ - 'HistoryPage', - variables, -]; -useHistoryPageQuery.fetcher = ( - variables: HistoryPageQueryVariables, - options?: RequestInit['headers'], -) => - graphqlClient( - HistoryPageDocument, - variables, - options, - ); +useHistoryPageQuery.fetcher = (variables: HistoryPageQueryVariables, options?: RequestInit['headers']) => graphqlClient(HistoryPageDocument, variables, options); export const HistoryApyDocument = ` query HistoryApy { apies(limit: 1, orderBy: timestamp_DESC) { @@ -93,27 +65,20 @@ export const HistoryApyDocument = ` } } `; -export const useHistoryApyQuery = ( - variables?: HistoryApyQueryVariables, - options?: UseQueryOptions, -) => - useQuery( - variables === undefined ? ['HistoryApy'] : ['HistoryApy', variables], - graphqlClient( - HistoryApyDocument, - variables, - ), - options, - ); +export const useHistoryApyQuery = < + TData = HistoryApyQuery, + TError = unknown + >( + variables?: HistoryApyQueryVariables, + options?: UseQueryOptions + ) => + useQuery( + variables === undefined ? ['HistoryApy'] : ['HistoryApy', variables], + graphqlClient(HistoryApyDocument, variables), + options + ); + +useHistoryApyQuery.getKey = (variables?: HistoryApyQueryVariables) => variables === undefined ? ['HistoryApy'] : ['HistoryApy', variables]; +; -useHistoryApyQuery.getKey = (variables?: HistoryApyQueryVariables) => - variables === undefined ? ['HistoryApy'] : ['HistoryApy', variables]; -useHistoryApyQuery.fetcher = ( - variables?: HistoryApyQueryVariables, - options?: RequestInit['headers'], -) => - graphqlClient( - HistoryApyDocument, - variables, - options, - ); +useHistoryApyQuery.fetcher = (variables?: HistoryApyQueryVariables, options?: RequestInit['headers']) => graphqlClient(HistoryApyDocument, variables, options); \ No newline at end of file diff --git a/libs/oeth/redeem/src/views/RedeemView.tsx b/libs/oeth/redeem/src/views/RedeemView.tsx index 382796cac..26fd8c606 100644 --- a/libs/oeth/redeem/src/views/RedeemView.tsx +++ b/libs/oeth/redeem/src/views/RedeemView.tsx @@ -8,7 +8,7 @@ import { Stack, Typography, } from '@mui/material'; -import { GasPopover } from '@origin/oeth/shared'; +import { ApyHeader, GasPopover } from '@origin/oeth/shared'; import { TokenInput } from '@origin/shared/components'; import { tokens } from '@origin/shared/contracts'; import { @@ -87,54 +87,63 @@ function RedeemViewWrapped() { amountIn === 0n; return ( - - - - {intl.formatMessage({ defaultMessage: 'Redeem' })} - - - - } - /> - - - `linear-gradient(${theme.palette.grey[900]}, ${ - theme.palette.grey[900] - }) padding-box, + <> + + + + + {intl.formatMessage({ defaultMessage: 'Redeem' })} + + theme.spacing(-0.75), + svg: { width: 16, height: 16 }, + }, + }} + /> + + } + /> + + + `linear-gradient(${theme.palette.grey[900]}, ${ + theme.palette.grey[900] + }) padding-box, linear-gradient(90deg, ${alpha( theme.palette.primary.main, 0.4, @@ -142,41 +151,42 @@ function RedeemViewWrapped() { theme.palette.primary.dark, 0.4, )} 100%) border-box;`, - }, - '&:focus-within': { - background: (theme) => - `linear-gradient(${theme.palette.grey[900]}, ${theme.palette.grey[900]}) padding-box, + }, + '&:focus-within': { + background: (theme) => + `linear-gradient(${theme.palette.grey[900]}, ${theme.palette.grey[900]}) padding-box, linear-gradient(90deg, var(--mui-palette-primary-main) 0%, var(--mui-palette-primary-dark) 100%) border-box;`, - }, - }} - /> - - - - - - {isEstimateLoading ? ( - - ) : isRedeemLoading ? ( - intl.formatMessage({ defaultMessage: 'Waiting for signature' }) - ) : ( - redeemButtonLabel - )} - - - + }, + }} + /> + + + + + + {isEstimateLoading ? ( + + ) : isRedeemLoading ? ( + intl.formatMessage({ defaultMessage: 'Waiting for signature' }) + ) : ( + redeemButtonLabel + )} + + + + ); } diff --git a/libs/oeth/shared/codegen.ts b/libs/oeth/shared/codegen.ts index 144a7e360..4b67d5611 100644 --- a/libs/oeth/shared/codegen.ts +++ b/libs/oeth/shared/codegen.ts @@ -6,7 +6,6 @@ const config: CodegenConfig = { schema: process.env.VITE_SUBSQUID_URL, documents: ['**/src/**/*.graphql'], plugins: ['typescript'], - hooks: { afterOneFileWrite: ['prettier --write', 'eslint --fix'] }, config: { scalars: { BigInt: 'string', @@ -22,7 +21,6 @@ const config: CodegenConfig = { extension: '.generated.ts', baseTypesPath: '~@origin/oeth/shared', }, - hooks: { afterOneFileWrite: ['prettier --write', 'eslint --fix'] }, plugins: ['typescript-operations', 'typescript-react-query'], config: { exposeFetcher: true, diff --git a/libs/oeth/shared/src/components/ApyHeader/index.tsx b/libs/oeth/shared/src/components/ApyHeader/index.tsx new file mode 100644 index 000000000..12bd788c9 --- /dev/null +++ b/libs/oeth/shared/src/components/ApyHeader/index.tsx @@ -0,0 +1,118 @@ +import { useState } from 'react'; + +import { + Box, + Button, + Menu, + MenuItem, + Skeleton, + Stack, + Typography, +} from '@mui/material'; +import { defineMessage, useIntl } from 'react-intl'; + +import { useApiesQuery } from './queries.generated'; + +import type { StackProps } from '@mui/material'; + +const trailingOptions = [ + { + label: defineMessage({ defaultMessage: '30 days trailing APY' }), + value: 30, + }, + { label: defineMessage({ defaultMessage: '7 days trailing APY' }), value: 7 }, +]; + +export const ApyHeader = (props: StackProps) => { + const intl = useIntl(); + const [trailing, setTrailing] = useState(trailingOptions[0]); + const [anchorEl, setAnchorEl] = useState(null); + const { data: apy, isLoading: apyLoading } = useApiesQuery( + { + limit: 1, + }, + { + select: (data) => data.apies[0], + }, + ); + + return ( + + {apyLoading ? ( + + ) : ( + + {intl.formatNumber( + trailing.value === 30 ? apy.apy30DayAvg : apy.apy7DayAvg, + { minimumFractionDigits: 2, maximumFractionDigits: 2 }, + )} + % + + )} + + + { + setAnchorEl(null); + }} + MenuListProps={{ dense: true }} + > + {trailingOptions + .filter((t) => t.value !== trailing.value) + .map((t) => ( + { + setTrailing(t); + setAnchorEl(null); + }} + > + {intl.formatMessage(t.label)} + + ))} + + + ); +}; diff --git a/libs/oeth/shared/src/components/ApyHeader/queries.generated.ts b/libs/oeth/shared/src/components/ApyHeader/queries.generated.ts new file mode 100644 index 000000000..caaac857b --- /dev/null +++ b/libs/oeth/shared/src/components/ApyHeader/queries.generated.ts @@ -0,0 +1,43 @@ +import * as Types from '@origin/oeth/shared'; + +import { useQuery, UseQueryOptions } from '@tanstack/react-query'; +import { graphqlClient } from '@origin/oeth/shared'; +export type ApiesQueryVariables = Types.Exact<{ + limit?: Types.InputMaybe; +}>; + + +export type ApiesQuery = { __typename?: 'Query', apies: Array<{ __typename?: 'APY', id: string, timestamp: string, apy7DayAvg: number, apy30DayAvg: number }> }; + + +export const ApiesDocument = ` + query Apies($limit: Int) { + apies( + limit: $limit + orderBy: timestamp_DESC + where: {timestamp_gt: "2023-06-06T12:38:47.000000Z"} + ) { + id + timestamp + apy7DayAvg + apy30DayAvg + } +} + `; +export const useApiesQuery = < + TData = ApiesQuery, + TError = unknown + >( + variables?: ApiesQueryVariables, + options?: UseQueryOptions + ) => + useQuery( + variables === undefined ? ['Apies'] : ['Apies', variables], + graphqlClient(ApiesDocument, variables), + options + ); + +useApiesQuery.getKey = (variables?: ApiesQueryVariables) => variables === undefined ? ['Apies'] : ['Apies', variables]; +; + +useApiesQuery.fetcher = (variables?: ApiesQueryVariables, options?: RequestInit['headers']) => graphqlClient(ApiesDocument, variables, options); \ No newline at end of file diff --git a/libs/oeth/swap/src/queries.graphql b/libs/oeth/shared/src/components/ApyHeader/queries.graphql similarity index 100% rename from libs/oeth/swap/src/queries.graphql rename to libs/oeth/shared/src/components/ApyHeader/queries.graphql diff --git a/libs/oeth/shared/src/components/index.ts b/libs/oeth/shared/src/components/index.ts index 7d6dc9474..5ba8e635d 100644 --- a/libs/oeth/shared/src/components/index.ts +++ b/libs/oeth/shared/src/components/index.ts @@ -1,3 +1,4 @@ export * from './ActivityProvider'; +export * from './ApyHeader'; export * from './AccountPopover'; export * from './GasPopover'; diff --git a/libs/oeth/swap/src/components/ApyHeader.tsx b/libs/oeth/swap/src/components/ApyHeader.tsx deleted file mode 100644 index d2bffbfcc..000000000 --- a/libs/oeth/swap/src/components/ApyHeader.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useState } from 'react'; - -import { - Box, - Button, - Menu, - MenuItem, - Paper, - Skeleton, - Stack, - Typography, -} from '@mui/material'; -import { defineMessage, useIntl } from 'react-intl'; - -import { useApiesQuery } from '../queries.generated'; - -import type { StackProps } from '@mui/material'; - -const trailingOptions = [ - { label: defineMessage({ defaultMessage: '30 days trailing' }), value: 30 }, - { label: defineMessage({ defaultMessage: '7 days trailing' }), value: 7 }, -]; - -export const ApyHeader = (props: StackProps) => { - const intl = useIntl(); - const [trailing, setTrailing] = useState(trailingOptions[0]); - const [anchorEl, setAnchorEl] = useState(null); - const { data: apy, isLoading: apyLoading } = useApiesQuery( - { - limit: 1, - }, - { - select: (data) => data.apies[0], - }, - ); - - return ( - - - {apyLoading ? ( - - ) : ( - - {intl.formatNumber( - trailing.value === 30 ? apy.apy30DayAvg : apy.apy7DayAvg, - { minimumFractionDigits: 2, maximumFractionDigits: 2 }, - )} - % - - )} - - - { - setAnchorEl(null); - }} - MenuListProps={{ dense: true }} - > - {trailingOptions - .filter((t) => t.value !== trailing.value) - .map((t) => ( - { - setTrailing(t); - setAnchorEl(null); - }} - > - {intl.formatMessage(t.label)} - - ))} - - - - ); -}; diff --git a/libs/oeth/swap/src/queries.generated.ts b/libs/oeth/swap/src/queries.generated.ts deleted file mode 100644 index 148c79d8f..000000000 --- a/libs/oeth/swap/src/queries.generated.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { graphqlClient } from '@origin/oeth/shared'; -import { useQuery } from '@tanstack/react-query'; - -import type * as Types from '@origin/oeth/shared'; -import type { UseQueryOptions } from '@tanstack/react-query'; -export type ApiesQueryVariables = Types.Exact<{ - limit?: Types.InputMaybe; -}>; - -export type ApiesQuery = { - __typename?: 'Query'; - apies: Array<{ - __typename?: 'APY'; - id: string; - timestamp: string; - apy7DayAvg: number; - apy30DayAvg: number; - }>; -}; - -export const ApiesDocument = ` - query Apies($limit: Int) { - apies( - limit: $limit - orderBy: timestamp_DESC - where: {timestamp_gt: "2023-06-06T12:38:47.000000Z"} - ) { - id - timestamp - apy7DayAvg - apy30DayAvg - } -} - `; -export const useApiesQuery = ( - variables?: ApiesQueryVariables, - options?: UseQueryOptions, -) => - useQuery( - variables === undefined ? ['Apies'] : ['Apies', variables], - graphqlClient(ApiesDocument, variables), - options, - ); - -useApiesQuery.getKey = (variables?: ApiesQueryVariables) => - variables === undefined ? ['Apies'] : ['Apies', variables]; -useApiesQuery.fetcher = ( - variables?: ApiesQueryVariables, - options?: RequestInit['headers'], -) => - graphqlClient( - ApiesDocument, - variables, - options, - ); diff --git a/libs/oeth/swap/src/views/SwapView.tsx b/libs/oeth/swap/src/views/SwapView.tsx index 285c7df93..5783a9abe 100644 --- a/libs/oeth/swap/src/views/SwapView.tsx +++ b/libs/oeth/swap/src/views/SwapView.tsx @@ -13,7 +13,7 @@ import { Stack, Typography, } from '@mui/material'; -import { GasPopover } from '@origin/oeth/shared'; +import { ApyHeader, GasPopover } from '@origin/oeth/shared'; import { TokenInput } from '@origin/shared/components'; import { ConnectedButton, @@ -24,7 +24,6 @@ import { composeContexts, isNilOrEmpty } from '@origin/shared/utils'; import { useIntl } from 'react-intl'; import { mainnet, useAccount, useBalance, useNetwork } from 'wagmi'; -import { ApyHeader } from '../components/ApyHeader'; import { SwapRoute } from '../components/SwapRoute'; import { TokenSelectModal } from '../components/TokenSelectModal'; import { buttonActionLabel } from '../constants'; From c3786a1843289a25a1609353bff016c4e4df0ed3 Mon Sep 17 00:00:00 2001 From: toniocodo Date: Thu, 5 Oct 2023 15:16:37 +0200 Subject: [PATCH 10/11] feat: extract notification generic component --- libs/oeth/redeem/src/hooks.tsx | 16 ++- .../components/ApprovalNotification.tsx | 71 +++++----- .../components/RedeemNotification.tsx | 122 +++++++++--------- .../components/SwapNotification.tsx | 114 ++++++++-------- .../src/components/ActivityProvider/hooks.ts | 10 +- libs/oeth/swap/src/hooks.tsx | 32 +++-- .../src/Notifications/NotificationSnack.tsx | 42 ++++++ .../components/src/Notifications/index.ts | 1 + libs/shared/components/src/index.ts | 1 + 9 files changed, 236 insertions(+), 173 deletions(-) create mode 100644 libs/shared/components/src/Notifications/NotificationSnack.tsx create mode 100644 libs/shared/components/src/Notifications/index.ts diff --git a/libs/oeth/redeem/src/hooks.tsx b/libs/oeth/redeem/src/hooks.tsx index cb125289d..8721e79a5 100644 --- a/libs/oeth/redeem/src/hooks.tsx +++ b/libs/oeth/redeem/src/hooks.tsx @@ -6,6 +6,7 @@ import { usePushActivity, useUpdateActivity, } from '@origin/oeth/shared'; +import { NotificationSnack } from '@origin/shared/components'; import { contracts, tokens } from '@origin/shared/contracts'; import { usePushNotification, useSlippage } from '@origin/shared/providers'; import { isNilOrEmpty, isUserRejected } from '@origin/shared/utils'; @@ -106,11 +107,16 @@ export const useHandleRedeem = () => { if (isUserRejected(error)) { deleteActivity(activity.id); pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Redeem Cancelled' }), - message: intl.formatMessage({ - defaultMessage: 'User rejected operation', - }), - severity: 'info', + content: ( + + ), }); } else { updateActivity({ diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx index 95debba7d..80b35101f 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx @@ -1,5 +1,5 @@ -import { Box, Stack, Typography } from '@mui/material'; -import { ActivityIcon, LinkIcon } from '@origin/shared/components'; +import { Box, Typography } from '@mui/material'; +import { ActivityIcon, NotificationSnack } from '@origin/shared/components'; import { isNilOrEmpty } from '@origin/shared/utils'; import { defineMessage, useIntl } from 'react-intl'; import { formatUnits } from 'viem'; @@ -39,46 +39,41 @@ export const ApprovalNotification = ({ const intl = useIntl(); return ( - - - - - {intl.formatMessage(title[status])} - {!isNilOrEmpty(txReceipt?.transactionHash) && ( - - )} - - - {isNilOrEmpty(error) ? ( - - {intl.formatMessage( - { - defaultMessage: '{amountIn} {symbolIn}', - }, - { - amountIn: intl.formatNumber( - +formatUnits(amountIn, tokenIn.decimals), - { minimumFractionDigits: 4, maximumFractionDigits: 4 }, - ), - symbolIn: tokenIn.symbol, - }, - )} - - ) : ( - {error} - )} - - - + } + title={intl.formatMessage(title[status])} + href={ + isNilOrEmpty(txReceipt?.transactionHash) + ? null + : `https://etherscan.io/tx/${txReceipt.transactionHash}` + } + subtitle={ + isNilOrEmpty(error) ? ( + + {intl.formatMessage( + { + defaultMessage: '{amountIn} {symbolIn}', + }, + { + amountIn: intl.formatNumber( + +formatUnits(amountIn, tokenIn.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + symbolIn: tokenIn.symbol, + }, + )} + + ) : ( + {error} + ) + } + endIcon={ - - + } + /> ); }; diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/RedeemNotification.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/RedeemNotification.tsx index e0024930a..df96c0add 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/RedeemNotification.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/RedeemNotification.tsx @@ -1,5 +1,9 @@ import { Box, Stack, Typography } from '@mui/material'; -import { ActivityIcon, LinkIcon, Mix } from '@origin/shared/components'; +import { + ActivityIcon, + Mix, + NotificationSnack, +} from '@origin/shared/components'; import { isNilOrEmpty } from '@origin/shared/utils'; import { defineMessage, useIntl } from 'react-intl'; import { formatUnits } from 'viem'; @@ -19,7 +23,7 @@ type RedeemNotificationProps = { amountOut?: bigint; txReceipt?: TransactionReceipt; error?: string; -} & Pick; +} & StackProps; const title: Record = { pending: defineMessage({ defaultMessage: 'Redeeming' }), @@ -36,69 +40,67 @@ export const RedeemNotification = ({ amountOut, txReceipt, error, - sx, + ...rest }: RedeemNotificationProps) => { const intl = useIntl(); return ( - - + } + title={intl.formatMessage(title[status])} + href={ + isNilOrEmpty(txReceipt?.transactionHash) + ? null + : `https://etherscan.io/tx/${txReceipt.transactionHash}` + } + subtitle={ + isNilOrEmpty(error) ? ( + + {intl.formatMessage( + { + defaultMessage: '{amountIn} {symbolIn}', + }, + { + amountIn: intl.formatNumber( + +formatUnits(amountIn, tokenIn.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + symbolIn: tokenIn.symbol, + amountOut: intl.formatNumber( + +formatUnits(amountOut, tokenOut.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + }, + )} + + ) : ( + {error} + ) + } + endIcon={ - - {intl.formatMessage(title[status])} - {!isNilOrEmpty(txReceipt?.transactionHash) && ( - - )} + + + - - {isNilOrEmpty(error) ? ( - - {intl.formatMessage( - { - defaultMessage: '{amountIn} {symbolIn}', - }, - { - amountIn: intl.formatNumber( - +formatUnits(amountIn, tokenIn.decimals), - { minimumFractionDigits: 4, maximumFractionDigits: 4 }, - ), - symbolIn: tokenIn.symbol, - amountOut: intl.formatNumber( - +formatUnits(amountOut, tokenOut.decimals), - { minimumFractionDigits: 4, maximumFractionDigits: 4 }, - ), - }, - )} - - ) : ( - {error} - )} - - - - - - - - + } + /> ); }; diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/SwapNotification.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/SwapNotification.tsx index c22b97379..423bb9aee 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/SwapNotification.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/SwapNotification.tsx @@ -1,5 +1,5 @@ import { Box, Stack, Typography } from '@mui/material'; -import { ActivityIcon, LinkIcon } from '@origin/shared/components'; +import { ActivityIcon, NotificationSnack } from '@origin/shared/components'; import { isNilOrEmpty } from '@origin/shared/utils'; import { defineMessage, useIntl } from 'react-intl'; import { formatUnits } from 'viem'; @@ -19,7 +19,7 @@ type SwapNotificationProps = { amountOut?: bigint; txReceipt?: TransactionReceipt; error?: string; -} & Pick; +} & StackProps; const title: Record = { pending: defineMessage({ defaultMessage: 'Swapping' }), @@ -36,67 +36,65 @@ export const SwapNotification = ({ amountOut, txReceipt, error, - sx, + ...rest }: SwapNotificationProps) => { const intl = useIntl(); return ( - - + } + title={intl.formatMessage(title[status])} + href={ + isNilOrEmpty(txReceipt?.transactionHash) + ? null + : `https://etherscan.io/tx/${txReceipt.transactionHash}` + } + subtitle={ + isNilOrEmpty(error) ? ( + + {intl.formatMessage( + { + defaultMessage: + '{amountIn} {symbolIn} for {amountOut} {symbolOut}', + }, + { + amountIn: intl.formatNumber( + +formatUnits(amountIn, tokenIn.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + symbolIn: tokenIn.symbol, + amountOut: intl.formatNumber( + +formatUnits(amountOut, tokenOut.decimals), + { minimumFractionDigits: 4, maximumFractionDigits: 4 }, + ), + symbolOut: tokenOut.symbol, + }, + )} + + ) : ( + {error} + ) + } + endIcon={ - - {intl.formatMessage(title[status])} - {!isNilOrEmpty(txReceipt?.transactionHash) && ( - - )} + + + - - {isNilOrEmpty(error) ? ( - - {intl.formatMessage( - { - defaultMessage: - '{amountIn} {symbolIn} for {amountOut} {symbolOut}', - }, - { - amountIn: intl.formatNumber( - +formatUnits(amountIn, tokenIn.decimals), - { minimumFractionDigits: 4, maximumFractionDigits: 4 }, - ), - symbolIn: tokenIn.symbol, - amountOut: intl.formatNumber( - +formatUnits(amountOut, tokenOut.decimals), - { minimumFractionDigits: 4, maximumFractionDigits: 4 }, - ), - symbolOut: tokenOut.symbol, - }, - )} - - ) : ( - {error} - )} - - - - - - - - + } + /> ); }; diff --git a/libs/oeth/shared/src/components/ActivityProvider/hooks.ts b/libs/oeth/shared/src/components/ActivityProvider/hooks.ts index a8d5c6178..a8f83ae49 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/hooks.ts +++ b/libs/oeth/shared/src/components/ActivityProvider/hooks.ts @@ -80,12 +80,18 @@ export const useGlobalStatus = () => { const grouped = groupBy(prop('status'), activities ?? []); if (isNilOrEmpty(grouped.pending)) { - if (prevGrouped?.success?.length !== grouped?.success?.length) { + if ( + !isNilOrEmpty(grouped.success) && + prevGrouped?.success?.length !== grouped?.success?.length + ) { setStatus('success'); setTimeout(() => { setStatus('idle'); }, 5000); - } else if (prevGrouped?.error?.length !== grouped?.error?.length) { + } else if ( + !isNilOrEmpty(grouped.error) && + prevGrouped?.error?.length !== grouped?.error?.length + ) { setStatus('error'); setTimeout(() => { setStatus('idle'); diff --git a/libs/oeth/swap/src/hooks.tsx b/libs/oeth/swap/src/hooks.tsx index f88b608f2..e2bdb929e 100644 --- a/libs/oeth/swap/src/hooks.tsx +++ b/libs/oeth/swap/src/hooks.tsx @@ -7,6 +7,7 @@ import { usePushActivity, useUpdateActivity, } from '@origin/oeth/shared'; +import { NotificationSnack } from '@origin/shared/components'; import { useCurve, usePushNotification, @@ -259,11 +260,16 @@ export const useHandleApprove = () => { if (isUserRejected(error)) { deleteActivity(activity.id); pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Approval Cancelled' }), - message: intl.formatMessage({ - defaultMessage: 'User rejected operation', - }), - severity: 'info', + content: ( + + ), }); } else { updateActivity({ @@ -359,6 +365,7 @@ export const useHandleSwap = () => { queryKey: ['swap_allowance'], }); pushNotification({ + hideDuration: null, content: ( { if (isUserRejected(error)) { deleteActivity(activity.id); pushNotification({ - title: intl.formatMessage({ defaultMessage: 'Operation Cancelled' }), - message: intl.formatMessage({ - defaultMessage: 'User rejected operation', - }), - severity: 'info', + content: ( + + ), }); } else { updateActivity({ diff --git a/libs/shared/components/src/Notifications/NotificationSnack.tsx b/libs/shared/components/src/Notifications/NotificationSnack.tsx new file mode 100644 index 000000000..46eda29b1 --- /dev/null +++ b/libs/shared/components/src/Notifications/NotificationSnack.tsx @@ -0,0 +1,42 @@ +import { Stack, Typography } from '@mui/material'; +import { isNilOrEmpty } from '@origin/shared/utils'; + +import { LinkIcon } from '../Icons'; + +import type { StackProps } from '@mui/material'; +import type { ReactNode } from 'react'; + +export type NotificationSnackProps = { + icon?: ReactNode; + title: ReactNode; + href?: string; + subtitle: ReactNode; + endIcon?: ReactNode; +} & Omit; + +export const NotificationSnack = ({ + icon, + title, + href, + subtitle, + endIcon, + ...rest +}: NotificationSnackProps) => { + return ( + + + + {icon} + {title} + {!isNilOrEmpty(href) && } + + + {subtitle} + + + + {endIcon} + + + ); +}; diff --git a/libs/shared/components/src/Notifications/index.ts b/libs/shared/components/src/Notifications/index.ts new file mode 100644 index 000000000..755134b64 --- /dev/null +++ b/libs/shared/components/src/Notifications/index.ts @@ -0,0 +1 @@ +export * from './NotificationSnack'; diff --git a/libs/shared/components/src/index.ts b/libs/shared/components/src/index.ts index 7158027f7..1e8949d57 100644 --- a/libs/shared/components/src/index.ts +++ b/libs/shared/components/src/index.ts @@ -6,4 +6,5 @@ export * from './Icons'; export * from './Loader'; export * from './MiddleTruncated'; export * from './Mix'; +export * from './Notifications'; export * from './top-nav'; From e8f872eb31a44d1d8f55535a1713ce979dd78b1c Mon Sep 17 00:00:00 2001 From: toniocodo Date: Thu, 5 Oct 2023 20:36:47 +0200 Subject: [PATCH 11/11] feat: remove gas input, fix notif logo, fix skeleton radius --- libs/oeth/redeem/src/hooks.tsx | 2 + .../components/ActivityPopover.tsx | 16 +++++--- .../components/ApprovalNotification.tsx | 7 ++-- .../oeth/shared/src/components/GasPopover.tsx | 41 +------------------ libs/oeth/swap/src/hooks.tsx | 4 +- .../src/Notifications/NotificationSnack.tsx | 20 +++++++-- libs/shared/theme/src/theme.tsx | 4 +- 7 files changed, 39 insertions(+), 55 deletions(-) diff --git a/libs/oeth/redeem/src/hooks.tsx b/libs/oeth/redeem/src/hooks.tsx index 8721e79a5..6f822edee 100644 --- a/libs/oeth/redeem/src/hooks.tsx +++ b/libs/oeth/redeem/src/hooks.tsx @@ -1,5 +1,6 @@ import { useCallback } from 'react'; +import { Box } from '@mui/material'; import { RedeemNotification, useDeleteActivity, @@ -109,6 +110,7 @@ export const useHandleRedeem = () => { pushNotification({ content: ( } title={intl.formatMessage({ defaultMessage: 'Operation Cancelled', })} diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx index 0439f62c8..c4229d51d 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ActivityPopover.tsx @@ -83,12 +83,18 @@ export const ActivityPopover = ({ direction="row" alignItems="center" justifyContent="space-between" + sx={{ px: 3, py: 2 }} > - - {intl.formatMessage({ defaultMessage: 'Recent activity' })} + + {intl.formatMessage({ defaultMessage: 'Recent Activity' })} - @@ -132,7 +138,7 @@ function EmptyActivity(props: StackProps) { return ( - {intl.formatMessage({ defaultMessage: 'No activity' })} + {intl.formatMessage({ defaultMessage: 'No Activity' })} ); diff --git a/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx b/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx index 80b35101f..95074e067 100644 --- a/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx +++ b/libs/oeth/shared/src/components/ActivityProvider/components/ApprovalNotification.tsx @@ -14,11 +14,10 @@ import type { GlobalActivityStatus } from '../types'; type ApprovalNotificationProps = { status: GlobalActivityStatus; tokenIn: Token; - tokenOut: Token; amountIn?: bigint; txReceipt?: TransactionReceipt; error?: string; -} & Pick; +} & StackProps; const title: Record = { pending: defineMessage({ defaultMessage: 'Approving' }), @@ -30,16 +29,16 @@ const title: Record = { export const ApprovalNotification = ({ status, tokenIn, - tokenOut, amountIn, txReceipt, error, - sx, + ...rest }: ApprovalNotificationProps) => { const intl = useIntl(); return ( } title={intl.formatMessage(title[status])} href={ diff --git a/libs/oeth/shared/src/components/GasPopover.tsx b/libs/oeth/shared/src/components/GasPopover.tsx index 510e85e97..58d146408 100644 --- a/libs/oeth/shared/src/components/GasPopover.tsx +++ b/libs/oeth/shared/src/components/GasPopover.tsx @@ -7,8 +7,6 @@ import { FormControl, FormHelperText, IconButton, - InputAdornment, - InputBase, InputLabel, Popover, Stack, @@ -16,7 +14,6 @@ import { } from '@mui/material'; import { InfoTooltip, PercentInput } from '@origin/shared/components'; import { useIntl } from 'react-intl'; -import { useFeeData } from 'wagmi'; import type { IconButtonProps } from '@mui/material'; @@ -45,7 +42,6 @@ export function GasPopover({ const theme = useTheme(); const intl = useIntl(); const [anchorEl, setAnchorEl] = useState(null); - const { data: feeData } = useFeeData({ formatUnits: 'gwei' }); return ( <> @@ -87,7 +83,7 @@ export function GasPopover({ }, }} > - + - - - {intl.formatMessage({ defaultMessage: 'Gas Price' })} - - - theme.palette.secondary.main, - backgroundColor: (theme) => - alpha(theme.palette.secondary.main, 0.05), - paddingInlineEnd: 2, - '& .MuiInputBase-input': { - textAlign: 'right', - borderColor: (theme) => theme.palette.secondary.main, - '&::placeholder': { - color: 'text.primary', - opacity: 1, - }, - }, - }} - endAdornment={ - - {intl.formatMessage({ defaultMessage: 'GWEI' })} - - } - /> - - diff --git a/libs/oeth/swap/src/hooks.tsx b/libs/oeth/swap/src/hooks.tsx index e2bdb929e..5b7770817 100644 --- a/libs/oeth/swap/src/hooks.tsx +++ b/libs/oeth/swap/src/hooks.tsx @@ -1,5 +1,6 @@ import { useCallback, useMemo } from 'react'; +import { Box } from '@mui/material'; import { ApprovalNotification, SwapNotification, @@ -262,6 +263,7 @@ export const useHandleApprove = () => { pushNotification({ content: ( } title={intl.formatMessage({ defaultMessage: 'Operation Cancelled', })} @@ -365,7 +367,6 @@ export const useHandleSwap = () => { queryKey: ['swap_allowance'], }); pushNotification({ - hideDuration: null, content: ( { pushNotification({ content: ( } title={intl.formatMessage({ defaultMessage: 'Operation Cancelled', })} diff --git a/libs/shared/components/src/Notifications/NotificationSnack.tsx b/libs/shared/components/src/Notifications/NotificationSnack.tsx index 46eda29b1..7cecf6ab1 100644 --- a/libs/shared/components/src/Notifications/NotificationSnack.tsx +++ b/libs/shared/components/src/Notifications/NotificationSnack.tsx @@ -3,7 +3,7 @@ import { isNilOrEmpty } from '@origin/shared/utils'; import { LinkIcon } from '../Icons'; -import type { StackProps } from '@mui/material'; +import type { StackProps, TypographyProps } from '@mui/material'; import type { ReactNode } from 'react'; export type NotificationSnackProps = { @@ -12,6 +12,8 @@ export type NotificationSnackProps = { href?: string; subtitle: ReactNode; endIcon?: ReactNode; + titleProps?: TypographyProps; + subtitleProps?: TypographyProps; } & Omit; export const NotificationSnack = ({ @@ -20,6 +22,8 @@ export const NotificationSnack = ({ href, subtitle, endIcon, + titleProps, + subtitleProps, ...rest }: NotificationSnackProps) => { return ( @@ -27,11 +31,21 @@ export const NotificationSnack = ({ {icon} - {title} + {typeof title === 'string' ? ( + {title} + ) : ( + title + )} {!isNilOrEmpty(href) && } - {subtitle} + {typeof subtitle === 'string' ? ( + + {subtitle} + + ) : ( + subtitle + )} diff --git a/libs/shared/theme/src/theme.tsx b/libs/shared/theme/src/theme.tsx index 036fae1bf..81f65f3d0 100644 --- a/libs/shared/theme/src/theme.tsx +++ b/libs/shared/theme/src/theme.tsx @@ -164,7 +164,7 @@ export const theme = extendTheme({ ), warning: ( - + ), }, }, @@ -474,7 +474,7 @@ export const theme = extendTheme({ }, styleOverrides: { text: ({ theme }) => ({ - borderRadius: 15, + borderRadius: 10, // backgroundColor: theme.palette.grey[900], }), },