diff --git a/apps/oeth/src/routes.ts b/apps/oeth/src/routes.ts index dde93b321..ee7103f0c 100644 --- a/apps/oeth/src/routes.ts +++ b/apps/oeth/src/routes.ts @@ -1,4 +1,5 @@ import { HistoryView } from '@origin/oeth/history'; +import { RedeemView } from '@origin/oeth/redeem'; import { SwapView } from '@origin/oeth/swap'; import { defineMessage } from 'react-intl'; @@ -16,6 +17,11 @@ export const routes: RouteObject[] = [ Component: SwapView, handle: { label: defineMessage({ defaultMessage: 'Swap' }) }, }, + { + path: '/redeem', + Component: RedeemView, + handle: { label: defineMessage({ defaultMessage: 'Redeem' }) }, + }, { path: '/history', Component: HistoryView, diff --git a/libs/oeth/redeem/.babelrc b/libs/oeth/redeem/.babelrc new file mode 100644 index 000000000..1ea870ead --- /dev/null +++ b/libs/oeth/redeem/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/oeth/redeem/.eslintrc.json b/libs/oeth/redeem/.eslintrc.json new file mode 100644 index 000000000..a786f2cf3 --- /dev/null +++ b/libs/oeth/redeem/.eslintrc.json @@ -0,0 +1,34 @@ +{ + "extends": [ + "plugin:@nx/react", + "../../../.eslintrc.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} diff --git a/libs/oeth/redeem/README.md b/libs/oeth/redeem/README.md new file mode 100644 index 000000000..23f8464a4 --- /dev/null +++ b/libs/oeth/redeem/README.md @@ -0,0 +1,7 @@ +# redeem + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test redeem` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/oeth/redeem/project.json b/libs/oeth/redeem/project.json new file mode 100644 index 000000000..dad759723 --- /dev/null +++ b/libs/oeth/redeem/project.json @@ -0,0 +1,20 @@ +{ + "name": "oeth-redeem", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/oeth/redeem/src", + "projectType": "library", + "tags": [], + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "outputs": [ + "{options.outputFile}" + ], + "options": { + "lintFilePatterns": [ + "libs/oeth/redeem/**/*.{ts,tsx,js,jsx}" + ] + } + } + } +} diff --git a/libs/oeth/redeem/src/components/Mix.tsx b/libs/oeth/redeem/src/components/Mix.tsx new file mode 100644 index 000000000..fa2fa137e --- /dev/null +++ b/libs/oeth/redeem/src/components/Mix.tsx @@ -0,0 +1,43 @@ +import { Box, Stack } from '@mui/material'; + +import type { SxProps } from '@mui/material'; + +interface Props { + size?: number; + sx?: SxProps; +} + +export function Mix({ size = 2, sx }: Props) { + const imgSrc = [ + '/images/currency/weth-icon-small.png', + '/images/currency/reth-icon-small.png', + '/images/currency/steth-icon-small.svg', + '/images/currency/frxeth-icon-small.svg', + ]; + + return ( + + {imgSrc.map((img, index, arr) => ( + + ))} + + ); +} diff --git a/libs/oeth/redeem/src/components/RedeemInfo.tsx b/libs/oeth/redeem/src/components/RedeemInfo.tsx new file mode 100644 index 000000000..af891b812 --- /dev/null +++ b/libs/oeth/redeem/src/components/RedeemInfo.tsx @@ -0,0 +1,43 @@ +import { Box, Tooltip, Typography } from '@mui/material'; +import { useIntl } from 'react-intl'; + +import type { Theme } from '@mui/material'; + +export function RedeemInfo() { + const intl = useIntl(); + + return ( + + {intl.formatMessage({ + defaultMessage: 'Redeem OETH for the basket of underlying assets', + })} + + } + componentsProps={{ + tooltip: { + sx: { + paddingInline: 2, + paddingBlock: 1.5, + borderRadius: 2, + border: '1px solid', + borderColor: (theme) => theme.palette.grey[500], + boxShadow: (theme: Theme) => theme.shadows[23], + }, + }, + }} + > + theme.typography.pxToRem(12), + height: (theme) => theme.typography.pxToRem(12), + color: (theme) => theme.palette.text.secondary, + }} + > + + ); +} diff --git a/libs/oeth/redeem/src/components/RedeemRoute.tsx b/libs/oeth/redeem/src/components/RedeemRoute.tsx new file mode 100644 index 000000000..a67cd7800 --- /dev/null +++ b/libs/oeth/redeem/src/components/RedeemRoute.tsx @@ -0,0 +1,61 @@ +import { Collapse, Stack, Typography } from '@mui/material'; +import { Card, cardStyles } from '@origin/shared/components'; +import { useIntl } from 'react-intl'; + +import { useRedeemState } from '../state'; +import { RedeemInfo } from './RedeemInfo'; +import { RedeemSplitCard } from './RedeemSplitCard'; +import { RouteCard } from './RouteCard'; + +import type { CardProps } from '@mui/material'; + +export function RedeemRoute(props: Omit) { + const intl = useIntl(); + const [{ amountOut }] = useRedeemState(); + + const hasContent = amountOut > 0n; + + return ( + theme.palette.background.default, + backgroundColor: 'grey.900', + borderRadius: 1, + ...props?.sx, + }} + title={ + + {intl.formatMessage({ defaultMessage: 'Route' })} + + + } + sxCardTitle={{ borderBottom: 'none', paddingBlock: 1, paddingInline: 2 }} + sxCardContent={{ + ...(hasContent + ? cardStyles + : { + p: 0, + paddingBlock: 0, + paddingInline: 0, + '&:last-child': { pb: 0 }, + }), + }} + > + + + + + + + + ); +} diff --git a/libs/oeth/redeem/src/components/RedeemSplitCard.tsx b/libs/oeth/redeem/src/components/RedeemSplitCard.tsx new file mode 100644 index 000000000..ae8343ea9 --- /dev/null +++ b/libs/oeth/redeem/src/components/RedeemSplitCard.tsx @@ -0,0 +1,96 @@ +import { + Box, + Card, + CardHeader, + Skeleton, + Stack, + Typography, +} from '@mui/material'; +import { usePrices } from '@origin/shared/providers'; +import { currencyFormat, formatAmount } from '@origin/shared/utils'; +import { useIntl } from 'react-intl'; +import { formatUnits } from 'viem'; + +import { useRedeemState } from '../state'; +import { Mix } from './Mix'; + +import type { CardProps } from '@origin/shared/components'; + +export const RedeemSplitCard = ( + props: Omit, +) => { + const intl = useIntl(); + const { data: prices, isLoading: isPricesLoading } = usePrices(); + const [{ split, isEstimateLoading }] = useRedeemState(); + + return ( + + theme.spacing(0.5, 0, 1.5, 0), + borderBottom: (theme) => `1px solid ${theme.palette.divider}`, + }} + title={ + + + + {intl.formatMessage({ + defaultMessage: 'Redeem basket of assets', + })} + + + } + /> + + {split?.map((s) => { + const converted = + +formatUnits(s.amount, s.token.decimals) * prices?.[s.token.symbol]; + + return ( + + + + {s.token.symbol} + + + + {isEstimateLoading ? ( + + ) : ( + formatAmount(s.amount, s.token.decimals) + )} + + {isPricesLoading || isEstimateLoading ? ( + + ) : ( + + {intl.formatNumber(converted, currencyFormat)} + + )} + + + ); + })} + + + ); +}; diff --git a/libs/oeth/redeem/src/components/RouteCard.tsx b/libs/oeth/redeem/src/components/RouteCard.tsx new file mode 100644 index 000000000..b9bab0353 --- /dev/null +++ b/libs/oeth/redeem/src/components/RouteCard.tsx @@ -0,0 +1,171 @@ +import { + Box, + Card, + CardHeader, + Skeleton, + Stack, + Typography, +} from '@mui/material'; +import Grid2 from '@mui/material/Unstable_Grid2/Grid2'; +import { tokens } from '@origin/shared/contracts'; +import { usePrices } from '@origin/shared/providers'; +import { + currencyFormat, + formatAmount, + quantityFormat, +} from '@origin/shared/utils'; +import { useIntl } from 'react-intl'; +import { formatUnits } from 'viem'; + +import { MIX_TOKEN } from '../constants'; +import { useRedeemState } from '../state'; + +import type { CardProps } from '@mui/material'; + +export function RouteCard(props: Omit) { + const intl = useIntl(); + const { data: prices } = usePrices(); + const [{ amountOut, gas, rate, isEstimateLoading }] = useRedeemState(); + + const estimatedAmount = +formatUnits(amountOut, MIX_TOKEN.decimals); + const convertedAmount = + (prices?.[tokens.mainnet.WETH.symbol] ?? 1) * estimatedAmount; + const gasAmount = +formatUnits(gas, tokens.mainnet.ETH.decimals); + + return ( + + + + {isEstimateLoading ? ( + + ) : ( + + )} + + + + {isEstimateLoading ? ( + + ) : ( + formatAmount(amountOut, MIX_TOKEN.decimals) + )} + + + + + {isEstimateLoading ? ( + + ) : ( + `(${intl.formatNumber(convertedAmount, currencyFormat)})` + )} + + + theme.shape.borderRadius, + background: (theme) => theme.palette.background.gradient1, + color: 'primary.contrastText', + fontSize: (theme) => theme.typography.pxToRem(12), + top: (theme) => theme.spacing(-3), + right: (theme) => theme.spacing(-1), + paddingInline: 1, + }} + > + {intl.formatMessage({ defaultMessage: 'Best' })} + + + } + > + + + {isEstimateLoading ? ( + + ) : ( + intl.formatMessage({ + defaultMessage: 'Request withdrawal via OETH vault', + }) + )} + + + + + {intl.formatMessage({ defaultMessage: 'Rate:' })} + + + {isEstimateLoading ? ( + + ) : ( + `1:${intl.formatNumber(rate, quantityFormat)}` + )} + + + + + {intl.formatMessage({ defaultMessage: 'Gas:' })} + + + {isEstimateLoading ? ( + + ) : ( + `~${intl.formatNumber(gasAmount, currencyFormat)}` + )} + + + + + {intl.formatMessage({ defaultMessage: 'Wait time:' })} + + + {isEstimateLoading ? ( + + ) : ( + intl.formatMessage({ defaultMessage: '~3 days' }) + )} + + + + + ); +} diff --git a/libs/oeth/redeem/src/constants.ts b/libs/oeth/redeem/src/constants.ts new file mode 100644 index 000000000..f94272e7c --- /dev/null +++ b/libs/oeth/redeem/src/constants.ts @@ -0,0 +1,13 @@ +import { erc20ABI, mainnet } from 'wagmi'; + +import type { Token } from '@origin/shared/contracts'; + +export const MIX_TOKEN: Token = { + address: undefined, + chainId: mainnet.id, + abi: erc20ABI, + decimals: 18, + name: 'Redeem Mix', + symbol: 'MIX_TOKEN', + icon: '/images/backed-graphic.svg', +}; diff --git a/libs/oeth/redeem/src/hooks.tsx b/libs/oeth/redeem/src/hooks.tsx new file mode 100644 index 000000000..1e8b8cd90 --- /dev/null +++ b/libs/oeth/redeem/src/hooks.tsx @@ -0,0 +1,126 @@ +import { useCallback } from 'react'; + +import { contracts } from '@origin/shared/contracts'; +import { + BlockExplorerLink, + usePushNotification, +} from '@origin/shared/providers'; +import { isNilOrEmpty } from '@origin/shared/utils'; +import { + prepareWriteContract, + waitForTransaction, + writeContract, +} from '@wagmi/core'; +import { produce } from 'immer'; +import { useIntl } from 'react-intl'; +import { formatUnits, parseUnits } from 'viem'; +import { useAccount, useQueryClient } from 'wagmi'; + +import { MIX_TOKEN } from './constants'; +import { useRedeemState } from './state'; + +export const useHandleAmountInChange = () => { + const [, setRedeemState] = useRedeemState(); + + return useCallback( + (amount: bigint) => { + setRedeemState( + produce((state) => { + state.amountIn = amount; + state.isEstimateLoading = amount !== 0n; + }), + ); + }, + [setRedeemState], + ); +}; + +export const useHandleSlippageChange = () => { + const [, setRedeemState] = useRedeemState(); + + return useCallback( + (value: number) => { + setRedeemState( + produce((state) => { + state.slippage = value; + }), + ); + }, + [setRedeemState], + ); +}; + +export const useHandleRedeem = () => { + const intl = useIntl(); + const pushNotification = usePushNotification(); + const { address } = useAccount(); + const [{ amountIn, amountOut, slippage }, setRedeemState] = useRedeemState(); + const wagmiClient = useQueryClient(); + + return useCallback(async () => { + if (amountIn === 0n || isNilOrEmpty(address)) { + return; + } + + setRedeemState( + produce((draft) => { + draft.isRedeemLoading = true; + }), + ); + + 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, + functionName: 'redeem', + args: [amountIn, minAmountOut], + }); + const { hash } = await writeContract(request); + const txReceipt = await waitForTransaction({ hash }); + + console.log('redeem vault done!'); + wagmiClient.invalidateQueries({ queryKey: ['redeem_balance'] }); + pushNotification({ + title: intl.formatMessage({ defaultMessage: 'Redeem complete' }), + severity: 'success', + content: , + }); + } catch (e) { + console.error(`redeem vault error!\n${e.message}`); + if (e?.code === 'ACTION_REJECTED') { + pushNotification({ + title: intl.formatMessage({ defaultMessage: 'Redeem vault' }), + severity: 'info', + }); + } else { + pushNotification({ + title: intl.formatMessage({ defaultMessage: 'Redeem vault' }), + severity: 'error', + }); + } + } + + setRedeemState( + produce((draft) => { + draft.isRedeemLoading = false; + }), + ); + }, [ + address, + amountIn, + amountOut, + intl, + pushNotification, + setRedeemState, + slippage, + wagmiClient, + ]); +}; diff --git a/libs/oeth/redeem/src/index.ts b/libs/oeth/redeem/src/index.ts new file mode 100644 index 000000000..2d32501e5 --- /dev/null +++ b/libs/oeth/redeem/src/index.ts @@ -0,0 +1 @@ +export * from './views/RedeemView'; diff --git a/libs/oeth/redeem/src/state.ts b/libs/oeth/redeem/src/state.ts new file mode 100644 index 000000000..fb1bab443 --- /dev/null +++ b/libs/oeth/redeem/src/state.ts @@ -0,0 +1,172 @@ +import { useEffect, useState } from 'react'; + +import { contracts, tokens, whales } from '@origin/shared/contracts'; +import { usePushNotification } from '@origin/shared/providers'; +import { isNilOrEmpty } from '@origin/shared/utils'; +import { useDebouncedEffect } from '@react-hookz/web'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { getAccount, getPublicClient, readContract } from '@wagmi/core'; +import { produce } from 'immer'; +import { useIntl } from 'react-intl'; +import { createContainer } from 'react-tracked'; +import { formatUnits, isAddressEqual, parseUnits } from 'viem'; + +import { MIX_TOKEN } from './constants'; + +import type { RedeemState } from './types'; + +export const { Provider: RedeemProvider, useTracked: useRedeemState } = + createContainer(() => { + const [state, setState] = useState({ + amountIn: 0n, + amountOut: 0n, + split: [], + gas: 0n, + rate: 0, + slippage: 0.01, + isEstimateLoading: false, + isRedeemLoading: false, + }); + const intl = useIntl(); + const queryClient = useQueryClient(); + const pushNotification = usePushNotification(); + + const { data: splitAddresses } = useQuery({ + queryKey: ['assetsDecimals'], + queryFn: async () => { + const assets = await readContract({ + address: contracts.mainnet.OETHVaultCore.address, + abi: contracts.mainnet.OETHVaultCore.abi, + functionName: 'getAllAssets', + }); + + return assets; + }, + staleTime: Infinity, + }); + + useEffect(() => { + if (splitAddresses) { + setState( + produce((draft) => { + draft.split = splitAddresses.map((a) => ({ + amount: 0n, + token: Object.values(tokens.mainnet).find( + (t) => !isNilOrEmpty(t.address) && isAddressEqual(a, t.address), + ), + })); + }), + ); + } + }, [splitAddresses]); + + useDebouncedEffect( + async () => { + if (state.amountIn === 0n) { + setState( + produce((draft) => { + draft.amountOut = 0n; + draft.split.forEach((a) => (a.amount = 0n)); + draft.isEstimateLoading = false; + }), + ); + return; + } + + let splitEstimates; + try { + splitEstimates = await queryClient.fetchQuery({ + queryKey: ['splitEstimates', state.amountIn.toString()], + queryFn: () => + readContract({ + address: contracts.mainnet.OETHVaultCore.address, + abi: contracts.mainnet.OETHVaultCore.abi, + functionName: 'calculateRedeemOutputs', + args: [state.amountIn], + }), + }); + } catch (e) { + console.error(`redeem vault estimate amount error.\n${e.message}`); + setState( + produce((draft) => { + draft.amountIn = 0n; + draft.amountOut = 0n; + draft.split = []; + draft.isEstimateLoading = false; + }), + ); + pushNotification({ + title: intl.formatMessage({ + defaultMessage: 'Error while estimating', + }), + message: e.shortMessage, + severity: 'error', + }); + + return; + } + + const total = splitEstimates.reduce((acc, curr, i) => { + if (state.split[i].token.decimals !== MIX_TOKEN.decimals) { + const exp = MIX_TOKEN.decimals - state.split[i].token.decimals; + + return acc + curr * (10n ^ BigInt(exp)); + } + + return acc + curr; + }, 0n); + + let gasEstimate = 0n; + const publicClient = getPublicClient(); + const { address } = getAccount(); + + const minAmountOut = parseUnits( + ( + +formatUnits(total, MIX_TOKEN.decimals) - + +formatUnits(total, MIX_TOKEN.decimals) * state.slippage + ).toString(), + MIX_TOKEN.decimals, + ); + + try { + gasEstimate = await queryClient.fetchQuery({ + queryKey: [ + 'estimateGasRedeem', + state.amountIn.toString(), + minAmountOut.toString(), + address, + ], + queryFn: () => + publicClient.estimateContractGas({ + address: contracts.mainnet.OETHVaultCore.address, + abi: contracts.mainnet.OETHVaultCore.abi, + functionName: 'redeem', + args: [state.amountIn, minAmountOut], + account: whales.mainnet.OETH, + }), + }); + } catch (e) { + console.error( + `redeem vault estimate gas error. Using default!\n${e.message}`, + ); + gasEstimate = 1500000n; + } + + setState( + produce((draft) => { + draft.amountOut = total; + draft.split.forEach((a, i) => (a.amount = splitEstimates[i])); + draft.gas = gasEstimate; + draft.rate = + +formatUnits(state.amountIn, tokens.mainnet.OETH.decimals) / + +formatUnits(total, MIX_TOKEN.decimals); + draft.isEstimateLoading = false; + }), + ); + }, + [state.amountIn], + state.amountIn === 0n ? 0 : 800, + ); + + return [state, setState]; + }); diff --git a/libs/oeth/redeem/src/types.ts b/libs/oeth/redeem/src/types.ts new file mode 100644 index 000000000..415f58999 --- /dev/null +++ b/libs/oeth/redeem/src/types.ts @@ -0,0 +1,17 @@ +import type { Token } from '@origin/shared/contracts'; + +export type RedeemEstimate = { + token: Token; + amount: bigint; +}; + +export type RedeemState = { + amountIn: bigint; + amountOut: bigint; + split: RedeemEstimate[]; + gas: bigint; + rate: number; + slippage: number; + isEstimateLoading: boolean; + isRedeemLoading: boolean; +}; diff --git a/libs/oeth/redeem/src/views/RedeemView.tsx b/libs/oeth/redeem/src/views/RedeemView.tsx new file mode 100644 index 000000000..c12f47d75 --- /dev/null +++ b/libs/oeth/redeem/src/views/RedeemView.tsx @@ -0,0 +1,216 @@ +import { alpha, Box, CircularProgress, Stack, Typography } from '@mui/material'; +import { GasPopover } from '@origin/oeth/shared'; +import { Card, TokenInput } from '@origin/shared/components'; +import { tokens } from '@origin/shared/contracts'; +import { ConnectedButton, usePrices } from '@origin/shared/providers'; +import { useIntl } from 'react-intl'; +import { useAccount, useBalance } from 'wagmi'; + +import { RedeemRoute } from '../components/RedeemRoute'; +import { + useHandleAmountInChange, + useHandleRedeem, + useHandleSlippageChange, +} from '../hooks'; +import { RedeemProvider, useRedeemState } from '../state'; + +import type { BoxProps } from '@mui/material'; + +const commonStyles = { + paddingBlock: 2.5, + paddingBlockStart: 2.625, + paddingInline: 2, + border: '1px solid', + borderColor: 'divider', + borderRadius: 1, +}; + +const tokenInputStyles = { + border: 'none', + backgroundColor: 'transparent', + borderRadius: 0, + paddingBlock: 0, + paddingInline: 0, + borderImageWidth: 0, + boxSizing: 'border-box', + '& .MuiInputBase-input': { + padding: 0, + lineHeight: '1.875rem', + boxSizing: 'border-box', + fontStyle: 'normal', + fontFamily: 'Sailec, Inter, Helvetica, Arial, sans-serif', + fontSize: '1.5rem', + fontWeight: 700, + height: '1.5rem', + color: 'primary.contrastText', + '&::placeholder': { + color: 'text.secondary', + opacity: 1, + }, + }, +}; + +export const RedeemView = () => ( + + + +); + +function RedeemViewWrapped() { + const intl = useIntl(); + const { address, isConnected } = useAccount(); + const [{ amountIn, slippage, isRedeemLoading, isEstimateLoading }] = + useRedeemState(); + const { data: prices, isLoading: isPricesLoading } = usePrices(); + const { data: balOeth, isLoading: isBalOethLoading } = useBalance({ + address, + token: tokens.mainnet.OETH.address, + watch: true, + scopeKey: 'redeem_balance', + }); + const handleSlippageChange = useHandleSlippageChange(); + const handleAmountInChange = useHandleAmountInChange(); + const handleRedeem = useHandleRedeem(); + + const amountInInputDisabled = isRedeemLoading; + + const redeemButtonLabel = + amountIn === 0n + ? intl.formatMessage({ defaultMessage: 'Enter an amount' }) + : amountIn > balOeth?.value + ? intl.formatMessage({ defaultMessage: 'Insufficient funds' }) + : intl.formatMessage({ defaultMessage: 'Redeem for mix' }); + const redeemButtonLoading = isEstimateLoading || isRedeemLoading; + const redeemButtonDisabled = + isBalOethLoading || + redeemButtonLoading || + amountIn > balOeth?.value || + amountIn === 0n; + + return ( + + + + {intl.formatMessage({ defaultMessage: 'Swap' })} + + + + } + > + + + `linear-gradient(${theme.palette.grey[900]}, ${ + theme.palette.grey[900] + }) padding-box, + linear-gradient(90deg, ${alpha( + theme.palette.primary.main, + 0.4, + )} 0%, ${alpha( + 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, + linear-gradient(90deg, var(--mui-palette-primary-main) 0%, var(--mui-palette-primary-dark) 100%) border-box;`, + }, + }} + /> + + + + + + + {redeemButtonLoading ? ( + + ) : ( + redeemButtonLabel + )} + + + + ); +} + +function ArrowButton(props: BoxProps) { + return ( + theme.palette.background.paper, + strokeWidth: (theme) => theme.typography.pxToRem(2), + stroke: (theme) => theme.palette.grey[700], + backgroundColor: (theme) => theme.palette.divider, + ...props?.sx, + }} + > + + + ); +} diff --git a/libs/oeth/redeem/tsconfig.json b/libs/oeth/redeem/tsconfig.json new file mode 100644 index 000000000..90fcf85c5 --- /dev/null +++ b/libs/oeth/redeem/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": false + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/oeth/redeem/tsconfig.lib.json b/libs/oeth/redeem/tsconfig.lib.json new file mode 100644 index 000000000..d48c074c6 --- /dev/null +++ b/libs/oeth/redeem/tsconfig.lib.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "files": ["../../../libs/shared/theme/src/theme.d.ts"], + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/oeth/swap/src/components/GasPopover.tsx b/libs/oeth/shared/src/components/GasPopover.tsx similarity index 84% rename from libs/oeth/swap/src/components/GasPopover.tsx rename to libs/oeth/shared/src/components/GasPopover.tsx index 3f477a105..d9b471025 100644 --- a/libs/oeth/swap/src/components/GasPopover.tsx +++ b/libs/oeth/shared/src/components/GasPopover.tsx @@ -14,16 +14,14 @@ import { Stack, useTheme, } from '@mui/material'; -import { produce } from 'immer'; +import { PercentInput } from '@origin/shared/components'; import { useIntl } from 'react-intl'; import { useFeeData } from 'wagmi'; -import { useSwapState } from '../state'; - import type { IconButtonProps } from '@mui/material'; -import type { ChangeEvent } from 'react'; -const defaultSlippage = 0.01; +const DEFAULT_SLIPPAGE = 0.01; +const WARNING_THRESHOLD = 0.05; const gridStyles = { display: 'grid', @@ -33,19 +31,22 @@ const gridStyles = { alignItems: 'center', }; -interface Props { +export type GasPopoverProps = { buttonProps?: IconButtonProps; -} + slippage: number; + onSlippageChange: (value: number) => void; +}; -export function GasPopover({ buttonProps }: Props) { +export function GasPopover({ + buttonProps, + slippage, + onSlippageChange, +}: GasPopoverProps) { const theme = useTheme(); const intl = useIntl(); const [anchorEl, setAnchorEl] = useState(null); - const [{ slippage }, setSwapState] = useSwapState(); const { data: feeData } = useFeeData({ formatUnits: 'gwei' }); - const handleSlippageChange = (evt: ChangeEvent) => {}; - return ( <> - theme.palette.secondary.main, @@ -114,15 +115,6 @@ export function GasPopover({ buttonProps }: Props) { }, }, }} - onChange={handleSlippageChange} - endAdornment={ - - {intl.formatMessage({ defaultMessage: '%' })} - - } /> - {slippage > 1 ? ( + {slippage > WARNING_THRESHOLD ? ( = { 'swap-zapper-eth': { ...defaultApi, ...swapZapperEth }, 'swap-zapper-sfrxeth': { ...defaultApi, ...swapZapperSfrxeth }, 'mint-vault': { ...defaultApi, ...mintVault }, - 'redeem-vault': { ...defaultApi, ...redeemVault }, 'wrap-oeth': { ...defaultApi, ...wrapOETH }, 'unwrap-woeth': { ...defaultApi, ...unwrapWOETH }, }; diff --git a/libs/oeth/swap/src/actions/redeemVault.ts b/libs/oeth/swap/src/actions/redeemVault.ts deleted file mode 100644 index 25df7d185..000000000 --- a/libs/oeth/swap/src/actions/redeemVault.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { queryClient } from '@origin/oeth/shared'; -import { contracts } from '@origin/shared/contracts'; -import { isNilOrEmpty } from '@origin/shared/utils'; -import { - erc20ABI, - getAccount, - getPublicClient, - prepareWriteContract, - readContract, - readContracts, - waitForTransaction, - writeContract, -} from '@wagmi/core'; -import { formatUnits, maxUint256, parseUnits } from 'viem'; - -import { MIX_TOKEN } from '../constants'; - -import type { - Allowance, - Approve, - EstimateAmount, - EstimateApprovalGas, - EstimateGas, - EstimateRoute, - Swap, -} from '../types'; - -const estimateAmount: EstimateAmount = async ({ amountIn }) => { - if (amountIn === 0n) { - return 0n; - } - - const assetsDecimals = await queryClient.fetchQuery({ - queryKey: ['assetsDecimals'], - queryFn: async () => { - const assets = await readContract({ - address: contracts.mainnet.OETHVaultCore.address, - abi: contracts.mainnet.OETHVaultCore.abi, - functionName: 'getAllAssets', - }); - - const decimals = await readContracts({ - contracts: assets.map((address) => ({ - address, - abi: erc20ABI, - functionName: 'decimals', - })), - }); - - return decimals.map((r) => r.result); - }, - staleTime: Infinity, - }); - - const split = await readContract({ - address: contracts.mainnet.OETHVaultCore.address, - abi: contracts.mainnet.OETHVaultCore.abi, - functionName: 'calculateRedeemOutputs', - args: [amountIn], - }); - - return split.reduce((acc, curr, i) => { - if (assetsDecimals[i] !== MIX_TOKEN.decimals) { - const exp = MIX_TOKEN.decimals - assetsDecimals[i]; - - return acc + curr * (10n ^ BigInt(exp)); - } - - return acc + curr; - }, 0n); -}; - -const estimateGas: EstimateGas = async ({ - tokenOut, - amountIn, - slippage, - amountOut, -}) => { - let gasEstimate = 0n; - - if (amountIn === 0n) { - return gasEstimate; - } - - const publicClient = getPublicClient(); - const { address } = getAccount(); - - const minAmountOut = parseUnits( - ( - +formatUnits(amountOut, tokenOut.decimals) - - +formatUnits(amountOut, tokenOut.decimals) * slippage - ).toString(), - tokenOut.decimals, - ); - - try { - gasEstimate = await publicClient.estimateContractGas({ - address: contracts.mainnet.OETHVaultCore.address, - abi: contracts.mainnet.OETHVaultCore.abi, - functionName: 'redeem', - args: [amountIn, minAmountOut], - account: address, - }); - } catch {} - - return gasEstimate; -}; - -const allowance: Allowance = async () => { - // Redeem OETH does not require approval - return maxUint256; -}; - -const estimateApprovalGas: EstimateApprovalGas = async () => { - // Redeem OETH does not require approval - return 0n; -}; - -const estimateRoute: EstimateRoute = async ({ - tokenIn, - tokenOut, - amountIn, - route, - slippage, -}) => { - if (amountIn === 0n) { - return { - ...route, - estimatedAmount: 0n, - gas: 0n, - rate: 0, - approvedAmount: 0n, - approvalGas: 0n, - }; - } - - const [estimatedAmount, approvedAmount, approvalGas] = await Promise.all([ - estimateAmount({ - tokenIn, - tokenOut, - amountIn, - }), - allowance({ tokenIn, tokenOut }), - estimateApprovalGas({ amountIn, tokenIn, tokenOut }), - ]); - const gas = await estimateGas({ - tokenIn, - tokenOut, - amountIn, - slippage, - amountOut: estimatedAmount, - }); - - return { - ...route, - estimatedAmount, - gas, - approvalGas, - approvedAmount, - rate: - +formatUnits(amountIn, tokenIn.decimals) / - +formatUnits(estimatedAmount, tokenOut.decimals), - }; -}; - -const approve: Approve = async ({ onSuccess }) => { - // Redeem OETH does not require approval - if (onSuccess) { - await onSuccess(null); - } -}; - -const swap: Swap = async ({ - tokenOut, - amountIn, - slippage, - amountOut, - onSuccess, - onError, - onReject, -}) => { - const { address } = getAccount(); - - if (amountIn === 0n || isNilOrEmpty(address)) { - return; - } - - const minAmountOut = parseUnits( - ( - +formatUnits(amountOut, tokenOut.decimals) - - +formatUnits(amountOut, tokenOut.decimals) * slippage - ).toString(), - tokenOut.decimals, - ); - - try { - const { request } = await prepareWriteContract({ - address: contracts.mainnet.OETHVaultCore.address, - abi: contracts.mainnet.OETHVaultCore.abi, - functionName: 'redeem', - args: [amountIn, minAmountOut], - }); - const { hash } = await writeContract(request); - const txReceipt = await waitForTransaction({ hash }); - - console.log('redeem vault done!'); - if (onSuccess) { - await onSuccess(txReceipt); - } - } catch (e) { - console.error(`redeem vault error!\n${e.message}`); - if (e?.code === 'ACTION_REJECTED' && onReject) { - await onReject('Redeem OETH'); - } else if (onError) { - await onError('Redeem OETH'); - } - } -}; - -export default { - estimateAmount, - estimateGas, - estimateRoute, - allowance, - estimateApprovalGas, - approve, - swap, -}; diff --git a/libs/oeth/swap/src/constants.ts b/libs/oeth/swap/src/constants.ts index 2efdab270..8d7101d4f 100644 --- a/libs/oeth/swap/src/constants.ts +++ b/libs/oeth/swap/src/constants.ts @@ -1,25 +1,12 @@ import { tokens } from '@origin/shared/contracts'; import { defineMessage } from 'react-intl'; -import { erc20ABI, mainnet } from 'wagmi'; -import type { Token } from '@origin/shared/contracts'; import type { MessageDescriptor } from 'react-intl'; import type { SwapAction } from './types'; -export const MIX_TOKEN: Token = { - address: undefined, - chainId: mainnet.id, - abi: erc20ABI, - decimals: 18, - name: 'Redeem Mix', - symbol: 'MIX_TOKEN', - icon: '/images/backed-graphic.svg', -}; - export const routeActionLogos: Record = { 'mint-vault': '/images/protocols/origin.svg', - 'redeem-vault': '/images/protocols/origin.svg', 'swap-curve': '/images/protocols/curve.webp', 'swap-curve-eth': '/images/protocols/curve.webp', 'swap-zapper-eth': '/images/protocols/zapper.svg', @@ -30,7 +17,6 @@ export const routeActionLogos: Record = { export const routeActionLabel: Record = { 'mint-vault': defineMessage({ defaultMessage: 'Mint with Vault' }), - 'redeem-vault': defineMessage({ defaultMessage: 'Redeem with Vault' }), 'swap-curve': defineMessage({ defaultMessage: 'Swap with Curve' }), 'swap-curve-eth': defineMessage({ defaultMessage: 'Swap with CurvePool' }), 'swap-zapper-eth': defineMessage({ defaultMessage: 'Swap with Zapper' }), @@ -102,11 +88,6 @@ export const swapRoutes = [ action: 'swap-zapper-sfrxeth', }, // Redeem - // { - // tokenIn: tokens.mainnet.OETH, - // tokenOut: MIX_TOKEN, - // action: 'redeem-vault', - // }, { tokenIn: tokens.mainnet.OETH, tokenOut: tokens.mainnet.WETH, diff --git a/libs/oeth/swap/src/hooks.ts b/libs/oeth/swap/src/hooks.ts index 62cbcd889..31ffd7b92 100644 --- a/libs/oeth/swap/src/hooks.ts +++ b/libs/oeth/swap/src/hooks.ts @@ -5,6 +5,7 @@ import { isNilOrEmpty } from '@origin/shared/utils'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { produce } from 'immer'; import { useIntl } from 'react-intl'; +import { useAccount, useQueryClient as useWagmiClient } from 'wagmi'; import { swapActions } from './actions'; import { useSwapState } from './state'; @@ -150,7 +151,7 @@ export const useSelectedSwapRouteAllowance = () => { return useQuery({ queryKey: [ - 'allowance', + 'swap_allowance', selectedSwapRoute?.tokenIn.symbol, selectedSwapRoute?.tokenOut.symbol, selectedSwapRoute?.action, @@ -166,16 +167,33 @@ export const useSelectedSwapRouteAllowance = () => { }); }; +export const useHandleSlippageChange = () => { + const [, setSwapState] = useSwapState(); + + return useCallback( + (value: number) => { + setSwapState( + produce((state) => { + state.slippage = value; + }), + ); + }, + [setSwapState], + ); +}; + export const useHandleApprove = () => { const intl = useIntl(); + const { address } = useAccount(); const curve = useCurve(); const queryClient = useQueryClient(); + const wagmiClient = useWagmiClient(); const pushNotification = usePushNotification(); const [{ amountIn, selectedSwapRoute, tokenIn, tokenOut }, setSwapState] = useSwapState(); return useCallback(async () => { - if (isNilOrEmpty(selectedSwapRoute)) { + if (isNilOrEmpty(selectedSwapRoute) || isNilOrEmpty(address)) { return; } @@ -190,13 +208,11 @@ export const useHandleApprove = () => { amountIn, curve, onSuccess: () => { + wagmiClient.invalidateQueries({ + queryKey: ['swap_balance'], + }); queryClient.invalidateQueries({ - queryKey: [ - 'allowance', - selectedSwapRoute?.tokenIn.symbol, - selectedSwapRoute?.tokenOut.symbol, - selectedSwapRoute?.action, - ], + queryKey: ['swap_allowance'], }); pushNotification({ title: intl.formatMessage({ defaultMessage: 'Approval complete' }), @@ -232,6 +248,7 @@ export const useHandleApprove = () => { }, }); }, [ + address, amountIn, curve, intl, @@ -241,13 +258,16 @@ export const useHandleApprove = () => { setSwapState, tokenIn, tokenOut, + wagmiClient, ]); }; export const useHandleSwap = () => { const intl = useIntl(); + const { address } = useAccount(); const curve = useCurve(); const queryClient = useQueryClient(); + const wagmiClient = useWagmiClient(); const pushNotification = usePushNotification(); const [ { amountIn, amountOut, selectedSwapRoute, slippage, tokenIn, tokenOut }, @@ -255,7 +275,7 @@ export const useHandleSwap = () => { ] = useSwapState(); return useCallback(async () => { - if (isNilOrEmpty(selectedSwapRoute)) { + if (isNilOrEmpty(selectedSwapRoute) || isNilOrEmpty(address)) { return; } @@ -273,13 +293,11 @@ export const useHandleSwap = () => { amountOut, curve, onSuccess: () => { + wagmiClient.invalidateQueries({ + queryKey: ['swap_balance'], + }); queryClient.invalidateQueries({ - queryKey: [ - 'allowance', - selectedSwapRoute?.tokenIn.symbol, - selectedSwapRoute?.tokenOut.symbol, - selectedSwapRoute?.action, - ], + queryKey: ['swap_allowance'], }); pushNotification({ title: intl.formatMessage({ defaultMessage: 'Swap complete' }), @@ -315,6 +333,7 @@ export const useHandleSwap = () => { }, }); }, [ + address, amountIn, amountOut, curve, @@ -326,5 +345,6 @@ export const useHandleSwap = () => { slippage, tokenIn, tokenOut, + wagmiClient, ]); }; diff --git a/libs/oeth/swap/src/state.ts b/libs/oeth/swap/src/state.ts index 54b5d3960..9b9e46a3c 100644 --- a/libs/oeth/swap/src/state.ts +++ b/libs/oeth/swap/src/state.ts @@ -1,9 +1,9 @@ import { useState } from 'react'; -import { queryClient } from '@origin/oeth/shared'; import { tokens } from '@origin/shared/contracts'; import { useCurve } from '@origin/shared/providers'; import { useDebouncedEffect } from '@react-hookz/web'; +import { useQueryClient } from '@tanstack/react-query'; import { produce } from 'immer'; import { createContainer } from 'react-tracked'; @@ -27,6 +27,7 @@ export const { Provider: SwapProvider, useTracked: useSwapState } = isApprovalLoading: false, isSwapLoading: false, }); + const queryClient = useQueryClient(); const { CurveRegistryExchange, OethPoolUnderlyings } = useCurve(); useDebouncedEffect( diff --git a/libs/oeth/swap/src/types.ts b/libs/oeth/swap/src/types.ts index 2c479fb78..268eddc33 100644 --- a/libs/oeth/swap/src/types.ts +++ b/libs/oeth/swap/src/types.ts @@ -10,7 +10,6 @@ export type SwapAction = | 'swap-zapper-eth' | 'swap-zapper-sfrxeth' | 'mint-vault' - | 'redeem-vault' | 'wrap-oeth' | 'unwrap-woeth'; diff --git a/libs/oeth/swap/src/views/SwapView.tsx b/libs/oeth/swap/src/views/SwapView.tsx index c3dc173fe..ef1ffd689 100644 --- a/libs/oeth/swap/src/views/SwapView.tsx +++ b/libs/oeth/swap/src/views/SwapView.tsx @@ -8,21 +8,22 @@ import { Collapse, IconButton, Stack, + Typography, } from '@mui/material'; -import { ApyHeader } from '@origin/oeth/shared'; +import { ApyHeader, GasPopover } from '@origin/oeth/shared'; import { Card, TokenInput } from '@origin/shared/components'; import { ConnectedButton, usePrices } from '@origin/shared/providers'; import { isNilOrEmpty } from '@origin/shared/utils'; import { useIntl } from 'react-intl'; import { useAccount, useBalance } from 'wagmi'; -import { GasPopover } from '../components/GasPopover'; import { SwapRoute } from '../components/SwapRoute'; import { TokenSelectModal } from '../components/TokenSelectModal'; import { routeActionLabel } from '../constants'; import { useHandleAmountInChange, useHandleApprove, + useHandleSlippageChange, useHandleSwap, useHandleTokenChange, useHandleTokenFlip, @@ -45,6 +46,31 @@ const commonStyles = { borderRadius: 1, }; +const tokenInputStyles = { + border: 'none', + backgroundColor: 'transparent', + borderRadius: 0, + paddingBlock: 0, + paddingInline: 0, + borderImageWidth: 0, + boxSizing: 'border-box', + '& .MuiInputBase-input': { + padding: 0, + lineHeight: '1.875rem', + boxSizing: 'border-box', + fontStyle: 'normal', + fontFamily: 'Sailec, Inter, Helvetica, Arial, sans-serif', + fontSize: '1.5rem', + fontWeight: 700, + height: '1.5rem', + color: 'primary.contrastText', + '&::placeholder': { + color: 'text.secondary', + opacity: 1, + }, + }, +}; + export const SwapView = () => ( @@ -62,6 +88,7 @@ function SwapViewWrapped() { tokenIn, tokenOut, selectedSwapRoute, + slippage, isSwapLoading, isSwapRoutesLoading, isApprovalLoading, @@ -74,12 +101,15 @@ function SwapViewWrapped() { address, token: tokenIn.address, watch: true, + scopeKey: 'swap_balance', }); const { data: balTokenOut, isLoading: isBalTokenOutLoading } = useBalance({ address, token: tokenOut.address, watch: true, + scopeKey: 'swap_balance', }); + const handleSlippageChange = useHandleSlippageChange(); const handleAmountInChange = useHandleAmountInChange(); const handleTokenChange = useHandleTokenChange(); const handleTokenFlip = useHandleTokenFlip(); @@ -102,7 +132,6 @@ function SwapViewWrapped() { !isNilOrEmpty(selectedSwapRoute) && selectedSwapRoute?.approvedAmount < amountIn && allowance < amountIn; - const swapButtonLabel = amountIn === 0n ? intl.formatMessage({ defaultMessage: 'Enter an amount' }) @@ -111,25 +140,21 @@ function SwapViewWrapped() { : !isNilOrEmpty(selectedSwapRoute) ? intl.formatMessage(routeActionLabel[selectedSwapRoute?.action]) : ''; - + const approveButtonLoading = isSwapRoutesLoading || isApprovalLoading; + const swapButtonLoading = isSwapRoutesLoading || isSwapLoading; const amountInInputDisabled = isSwapLoading || isApprovalLoading; - const approveButtonDisabled = isNilOrEmpty(selectedSwapRoute) || - isApprovalLoading || + approveButtonLoading || amountIn > balTokenIn?.value; - const swapButtonDisabled = needsApproval || isNilOrEmpty(selectedSwapRoute) || isBalTokenInLoading || + swapButtonLoading || amountIn > balTokenIn?.value || amountIn === 0n; - const approveButtonLoading = isSwapRoutesLoading || isApprovalLoading; - - const swapButtonLoading = isSwapRoutesLoading || isSwapLoading; - return ( <> @@ -147,8 +172,12 @@ function SwapViewWrapped() { justifyContent="space-between" alignItems="center" > - {intl.formatMessage({ defaultMessage: 'Swap' })} + + {intl.formatMessage({ defaultMessage: 'Swap' })} + { setTokenSource('tokenOut'); }} tokenPriceUsd={prices?.[tokenOut.symbol]} isPriceLoading={isSwapRoutesLoading || isPriceLoading} - inputProps={{ readOnly: true }} isConnected={isConnected} + inputProps={{ readOnly: true, sx: tokenInputStyles }} sx={{ ...commonStyles, borderStartStartRadius: 0, @@ -229,7 +257,7 @@ function SwapViewWrapped() { backgroundColor: (theme) => alpha(theme.palette.grey[400], 0.2), }} /> - + @@ -269,7 +297,7 @@ function SwapViewWrapped() { ); } -function SwapButton(props: IconButtonProps) { +function ArrowButton(props: IconButtonProps) { return ( diff --git a/libs/oeth/swap/tsconfig.lib.json b/libs/oeth/swap/tsconfig.lib.json index d11ac2d7a..78f9e0f1c 100644 --- a/libs/oeth/swap/tsconfig.lib.json +++ b/libs/oeth/swap/tsconfig.lib.json @@ -20,11 +20,5 @@ "src/**/*.spec.jsx", "src/**/*.test.jsx" ], - "include": [ - "src/**/*.js", - "src/**/*.jsx", - "src/**/*.ts", - "src/**/*.tsx", - "../../shared/providers/src/wagmi/components/TokenSelectModal.tsx" - ] + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] } diff --git a/libs/shared/components/src/Cards/Card.tsx b/libs/shared/components/src/Cards/Card.tsx index 0d7ad9dff..f0e1d308e 100644 --- a/libs/shared/components/src/Cards/Card.tsx +++ b/libs/shared/components/src/Cards/Card.tsx @@ -8,13 +8,13 @@ export const cardStyles = { paddingInline: 2, } as const; -interface Props { +export type CardProps = { title: string | React.ReactNode; children: React.ReactNode; sxCardContent?: SxProps; sxCardTitle?: SxProps; sx?: SxProps; -} +}; export function Card({ title, @@ -22,7 +22,7 @@ export function Card({ sxCardContent, sxCardTitle, sx, -}: Props) { +}: CardProps) { return ( ( ) : ( ); diff --git a/libs/shared/components/src/Inputs/PercentInput.tsx b/libs/shared/components/src/Inputs/PercentInput.tsx new file mode 100644 index 000000000..5d34de566 --- /dev/null +++ b/libs/shared/components/src/Inputs/PercentInput.tsx @@ -0,0 +1,85 @@ +import { forwardRef, useEffect, useState } from 'react'; + +import { InputBase } from '@mui/material'; +import { isNilOrEmpty } from '@origin/shared/utils'; + +import type { InputBaseProps } from '@mui/material'; +import type { ChangeEvent } from 'react'; + +export type PercentInputProps = { + value: number; + precision?: number; + onChange?: (value: number) => void; +} & Omit; + +export const PercentInput = forwardRef( + ({ value, precision = 4, onChange, ...rest }, ref) => { + const [strVal, setStrVal] = useState(formatPercent(value, precision)); + + useEffect(() => { + if (value === 0 && (isNilOrEmpty(strVal) || strVal.endsWith('.'))) { + return; + } + + if (value === 0 && !/0\.0+$/.test(strVal)) { + setStrVal(''); + return; + } + + if ( + isNilOrEmpty(strVal) || + formatPercent(value, precision) !== strVal.replace('.', '') + ) { + setStrVal(formatPercent(value, precision)); + } + }, [precision, strVal, value]); + + const handleChange = (evt: ChangeEvent) => { + if (evt.target.validity.valid) { + const val = + isNilOrEmpty(evt.target.value) || evt.target.value === '.' + ? '0' + : evt.target.value.replace(/\.0+$/, ''); + + try { + const num = Number(val) / 100; + if (num <= 1) { + setStrVal(evt.target.value === '.' ? '0.' : evt.target.value); + if (onChange && num !== value) { + onChange(num); + } + } + } catch {} + } + }; + + return ( + + ); + }, +); + +PercentInput.displayName = 'PercentInput'; + +function formatPercent(value: number, precision: number) { + return Intl.NumberFormat('en', { + useGrouping: false, + maximumFractionDigits: precision, + }).format(value * 100); +} diff --git a/libs/shared/components/src/Inputs/TokenInput.tsx b/libs/shared/components/src/Inputs/TokenInput.tsx index 671c14aaf..87b6a7372 100644 --- a/libs/shared/components/src/Inputs/TokenInput.tsx +++ b/libs/shared/components/src/Inputs/TokenInput.tsx @@ -1,11 +1,21 @@ import { forwardRef } from 'react'; -import { alpha, Box, IconButton, Stack, Typography } from '@mui/material'; -import { formatAmount } from '@origin/shared/utils'; +import { + alpha, + Box, + IconButton, + Skeleton, + Stack, + Typography, +} from '@mui/material'; +import { + currencyFormat, + formatAmount, + isNilOrEmpty, +} from '@origin/shared/utils'; import { useIntl } from 'react-intl'; import { formatUnits } from 'viem'; -import { Loader } from '../Loader'; import { BigIntInput } from './BigIntInput'; import type { StackProps } from '@mui/material'; @@ -13,8 +23,6 @@ import type { Token } from '@origin/shared/contracts'; import type { BigintInputProps } from './BigIntInput'; -const styles = { display: 'flex', justifyContent: 'space-between', gap: 2.5 }; - export type TokenInputProps = { amount: bigint; decimals?: number; @@ -25,9 +33,9 @@ export type TokenInputProps = { isConnected: boolean; balance?: bigint; isBalanceLoading?: boolean; - disableMaxClick?: boolean; token: Token; - onTokenClick: () => void; + onTokenClick?: () => void; + isTokenClickDisabled?: boolean; tokenPriceUsd?: number; isPriceLoading?: boolean; inputProps?: Omit< @@ -48,9 +56,9 @@ export const TokenInput = forwardRef( isConnected, balance = 0n, isBalanceLoading, - disableMaxClick, token, onTokenClick, + isTokenClickDisabled, tokenPriceUsd = 0, isPriceLoading, inputProps, @@ -64,7 +72,9 @@ export const TokenInput = forwardRef( return ( - + ( disabled={isAmountDisabled} isLoading={isAmountLoading} ref={ref} - sx={{ flex: 1 }} /> - - - + - + {isPriceLoading ? ( - - ) : tokenPriceUsd !== undefined ? ( + + ) : !isNilOrEmpty(tokenPriceUsd) ? ( ( lineHeight: '1.5rem', }} > - {intl.formatNumber(amountUsd, { - style: 'currency', - currency: 'usd', - maximumFractionDigits: 4, - })} + {intl.formatNumber(amountUsd, currencyFormat)} ) : null} {isConnected ? ( isBalanceLoading ? ( - + ) : ( - - - } + onClose={handleCloseClick} > {!isNilOrEmpty(title) && ( {title} diff --git a/libs/shared/providers/src/wagmi/components/ChainScanLink.tsx b/libs/shared/providers/src/wagmi/components/BlockExplorerLink.tsx similarity index 85% rename from libs/shared/providers/src/wagmi/components/ChainScanLink.tsx rename to libs/shared/providers/src/wagmi/components/BlockExplorerLink.tsx index 783a6c3c5..1d8977f02 100644 --- a/libs/shared/providers/src/wagmi/components/ChainScanLink.tsx +++ b/libs/shared/providers/src/wagmi/components/BlockExplorerLink.tsx @@ -5,7 +5,7 @@ import { mainnet } from 'wagmi/chains'; import type { LinkProps } from '@mui/material'; -export type ChainScanLinkProps = { +export type BlockExplorerLinkProps = { hash?: string; blockExplorer?: { name: string; @@ -13,15 +13,15 @@ export type ChainScanLinkProps = { }; } & Omit; -export const ChainScanLink = ({ +export const BlockExplorerLink = ({ hash, blockExplorer, ...rest -}: ChainScanLinkProps) => { +}: BlockExplorerLinkProps) => { const intl = useIntl(); const { chain, chains } = useNetwork(); - const base = + const baseUrl = blockExplorer?.url ?? chain?.blockExplorers?.default?.url ?? chains[0].blockExplorers.default.url ?? @@ -35,7 +35,7 @@ export const ChainScanLink = ({ return ( diff --git a/libs/shared/providers/src/wagmi/components/index.ts b/libs/shared/providers/src/wagmi/components/index.ts index 04b724024..fbdc3012b 100644 --- a/libs/shared/providers/src/wagmi/components/index.ts +++ b/libs/shared/providers/src/wagmi/components/index.ts @@ -1,4 +1,4 @@ export * from './AddressLabel'; -export * from './ChainScanLink'; +export * from './BlockExplorerLink'; export * from './ConnectedButton'; export * from './OpenAccountModalButton'; diff --git a/libs/shared/theme/src/theme.tsx b/libs/shared/theme/src/theme.tsx index 469265639..081fa1b90 100644 --- a/libs/shared/theme/src/theme.tsx +++ b/libs/shared/theme/src/theme.tsx @@ -1,3 +1,4 @@ +import { Box } from '@mui/material'; import { alpha, experimental_extendTheme as extendTheme, @@ -89,6 +90,32 @@ export const theme = extendTheme({ '0px 1.7955275774002075px 5.32008171081543px 0px rgba(0, 0, 0, 0.03), 0px 6.030803203582764px 17.869047164916992px 0px rgba(0, 0, 0, 0.04), 0px 27px 80px 0px rgba(0, 0, 0, 0.07)', ], components: { + MuiAlert: { + defaultProps: { + variant: 'standard', + iconMapping: { + error: ( + + ), + info: ( + + ), + success: ( + + ), + warning: ( + + ), + }, + }, + styleOverrides: { + root: ({ theme }) => ({ + backgroundColor: theme.palette.grey['900'], + color: theme.palette.primary.contrastText, + '&&&': { border: 'none' }, + }), + }, + }, MuiButton: { styleOverrides: { root: { @@ -317,9 +344,13 @@ export const theme = extendTheme({ }, }, MuiSkeleton: { + defaultProps: { + animation: 'wave', + }, styleOverrides: { text: ({ theme }) => ({ borderRadius: theme.shape.borderRadius * 22, + backgroundColor: 'grey.900', }), }, }, diff --git a/libs/shared/utils/src/BigDecimal.ts b/libs/shared/utils/src/BigDecimal.ts deleted file mode 100644 index 4d6ef7d19..000000000 --- a/libs/shared/utils/src/BigDecimal.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { formatUnits, parseUnits } from 'viem'; - -const DEFAULT_DECIMALS = 18; - -export class BigDecimal { - value: bigint; - decimals: number; - - constructor(num: bigint, decimals = DEFAULT_DECIMALS) { - this.value = num ? (typeof num === 'bigint' ? num : BigInt(num)) : 0n; - this.decimals = decimals; - } - - static ZERO(): BigDecimal { - return new BigDecimal(0n, DEFAULT_DECIMALS); - } - - static ONE(decimals = DEFAULT_DECIMALS): BigDecimal { - return new BigDecimal(parseUnits('1', decimals), decimals); - } - - static parse( - amountStr: `${number}`, - decimals = DEFAULT_DECIMALS, - ): BigDecimal { - return new BigDecimal(parseUnits(amountStr, decimals), decimals); - } - - static fromSimple( - amountNum: number, - decimals = DEFAULT_DECIMALS, - ): BigDecimal { - return new BigDecimal(BigInt(amountNum), decimals); - } - - get string(): string { - return formatUnits(this.value, this.decimals); - } - - get simple(): number { - return parseFloat(this.string); - } - - get simpleRounded(): number { - return parseFloat(this.simple.toFixed(3).slice(0, -1)); - } - - toJSON(): string { - return JSON.stringify({ - decimals: this.decimals, - value: this.value.toString(), - }); - } - - toFixed(decimalPlaces = 2): number { - return parseFloat(this.simple.toFixed(decimalPlaces + 1).slice(0, -1)); - } - - toPercent(decimalPlaces = 2): number { - return parseFloat((this.simple * 100).toFixed(decimalPlaces)); - } - - format(decimalPlaces = 2): string { - return Intl.NumberFormat('en', { - maximumFractionDigits: decimalPlaces, - }).format(this.simple); - } - - add(other: BigDecimal) { - this.value += other.value; - } - - sub(other: BigDecimal) { - this.value -= other.value; - } - - mul(other: BigDecimal) { - this.value *= other.value; - } - - div(other: BigDecimal) { - this.value /= other.value; - } - - eq(other: BigDecimal): boolean { - return this.value === other.value; - } - - gt(other: BigDecimal): boolean { - return this.value > other.value; - } - - gte(other: BigDecimal): boolean { - return this.value >= other.value; - } - - lt(other: BigDecimal): boolean { - return this.value < other.value; - } - - lte(other: BigDecimal): boolean { - return this.value <= other.value; - } - - isZero(): boolean { - return this.value === 0n; - } -} diff --git a/libs/shared/utils/src/BigInt.ts b/libs/shared/utils/src/BigInt.ts new file mode 100644 index 000000000..d96a687fb --- /dev/null +++ b/libs/shared/utils/src/BigInt.ts @@ -0,0 +1,2 @@ +export const jsonStringifyReplacer = (key, value) => + typeof value === 'bigint' ? value.toString() : value; diff --git a/libs/shared/utils/src/index.ts b/libs/shared/utils/src/index.ts index 6335db81d..aef480533 100644 --- a/libs/shared/utils/src/index.ts +++ b/libs/shared/utils/src/index.ts @@ -1,5 +1,5 @@ export * from './addresses'; -export * from './BigDecimal'; +export * from './BigInt'; export * from './composeContext'; export * from './formatters'; export * from './isNilOrEmpty'; diff --git a/package.json b/package.json index 6f7252cb0..da31588c3 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "wagmi": "^1.4.1" }, "devDependencies": { + "@babel/core": "^7.14.5", "@babel/preset-react": "^7.22.15", "@faker-js/faker": "^8.0.2", "@formatjs/cli": "^6.1.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28f6e9b9b..c994ca695 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,6 +73,9 @@ dependencies: version: 1.4.1(@types/react@18.2.21)(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(viem@1.10.12) devDependencies: + '@babel/core': + specifier: ^7.14.5 + version: 7.22.17 '@babel/preset-react': specifier: ^7.22.15 version: 7.22.15(@babel/core@7.22.17) @@ -338,8 +341,9 @@ packages: resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/highlight': 7.22.10 + '@babel/highlight': 7.22.13 chalk: 2.4.2 + dev: true /@babel/code-frame@7.22.13: resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} @@ -347,36 +351,12 @@ packages: dependencies: '@babel/highlight': 7.22.13 chalk: 2.4.2 - dev: true /@babel/compat-data@7.22.9: resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} engines: {node: '>=6.9.0'} dev: true - /@babel/core@7.22.10: - resolution: {integrity: sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw==} - engines: {node: '>=6.9.0'} - dependencies: - '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.22.10 - '@babel/generator': 7.22.10 - '@babel/helper-compilation-targets': 7.22.10 - '@babel/helper-module-transforms': 7.22.9(@babel/core@7.22.10) - '@babel/helpers': 7.22.10 - '@babel/parser': 7.22.10 - '@babel/template': 7.22.5 - '@babel/traverse': 7.22.10 - '@babel/types': 7.22.10 - convert-source-map: 1.9.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/core@7.22.17: resolution: {integrity: sha512-2EENLmhpwplDux5PSsZnSbnSkB3tZ6QTksgO25xwEL7pIDcNOMhF5v/s6RzwjMZzZzw9Ofc30gHv5ChCC8pifQ==} engines: {node: '>=6.9.0'} @@ -434,17 +414,6 @@ packages: '@babel/types': 7.22.17 dev: true - /@babel/helper-compilation-targets@7.22.10: - resolution: {integrity: sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/compat-data': 7.22.9 - '@babel/helper-validator-option': 7.22.15 - browserslist: 4.21.10 - lru-cache: 5.1.1 - semver: 6.3.1 - dev: true - /@babel/helper-compilation-targets@7.22.15: resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} engines: {node: '>=6.9.0'} @@ -510,15 +479,15 @@ packages: resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.22.5 - '@babel/types': 7.22.10 + '@babel/template': 7.22.15 + '@babel/types': 7.22.17 dev: true /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.22.10 + '@babel/types': 7.22.17 dev: true /@babel/helper-member-expression-to-functions@7.22.15: @@ -533,13 +502,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.22.17 - dev: true - - /@babel/helper-module-imports@7.22.5: - resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.10 /@babel/helper-module-transforms@7.22.17(@babel/core@7.22.17): resolution: {integrity: sha512-XouDDhQESrLHTpnBtCKExJdyY4gJCdrvH2Pyv8r8kovX2U8G0dRUOT45T9XlbLtuu9CLXP15eusnkprhoPV5iQ==} @@ -555,20 +517,6 @@ packages: '@babel/helper-validator-identifier': 7.22.15 dev: true - /@babel/helper-module-transforms@7.22.9(@babel/core@7.22.10): - resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.10 - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-module-imports': 7.22.5 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.5 - dev: true - /@babel/helper-optimise-call-expression@7.22.5: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} @@ -609,7 +557,7 @@ packages: resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.22.10 + '@babel/types': 7.22.17 dev: true /@babel/helper-skip-transparent-expression-wrappers@7.22.5: @@ -623,7 +571,7 @@ packages: resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.22.10 + '@babel/types': 7.22.17 dev: true /@babel/helper-string-parser@7.22.5: @@ -633,11 +581,11 @@ packages: /@babel/helper-validator-identifier@7.22.15: resolution: {integrity: sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-identifier@7.22.5: resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} engines: {node: '>=6.9.0'} + dev: true /@babel/helper-validator-option@7.22.15: resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} @@ -653,17 +601,6 @@ packages: '@babel/types': 7.22.17 dev: true - /@babel/helpers@7.22.10: - resolution: {integrity: sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.22.5 - '@babel/traverse': 7.22.10 - '@babel/types': 7.22.10 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/helpers@7.22.15: resolution: {integrity: sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==} engines: {node: '>=6.9.0'} @@ -675,14 +612,6 @@ packages: - supports-color dev: true - /@babel/highlight@7.22.10: - resolution: {integrity: sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.22.5 - chalk: 2.4.2 - js-tokens: 4.0.0 - /@babel/highlight@7.22.13: resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==} engines: {node: '>=6.9.0'} @@ -690,14 +619,13 @@ packages: '@babel/helper-validator-identifier': 7.22.15 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true /@babel/parser@7.22.10: resolution: {integrity: sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.22.10 + '@babel/types': 7.22.17 dev: true /@babel/parser@7.22.16: @@ -1447,16 +1375,6 @@ packages: '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.22.17) dev: true - /@babel/plugin-transform-react-jsx-self@7.22.5(@babel/core@7.22.10): - resolution: {integrity: sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.10 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-react-jsx-self@7.22.5(@babel/core@7.22.17): resolution: {integrity: sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==} engines: {node: '>=6.9.0'} @@ -1467,16 +1385,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-react-jsx-source@7.22.5(@babel/core@7.22.10): - resolution: {integrity: sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.10 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-react-jsx-source@7.22.5(@babel/core@7.22.17): resolution: {integrity: sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==} engines: {node: '>=6.9.0'} @@ -1828,24 +1736,6 @@ packages: '@babel/types': 7.22.10 dev: true - /@babel/traverse@7.22.10: - resolution: {integrity: sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.22.10 - '@babel/generator': 7.22.10 - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-function-name': 7.22.5 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.22.10 - '@babel/types': 7.22.10 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/traverse@7.22.17: resolution: {integrity: sha512-xK4Uwm0JnAMvxYZxOVecss85WxTEIbTa7bnGyf/+EgCL5Zt3U7htUpEOWv9detPlamGKuRzCqw74xVglDWpPdg==} engines: {node: '>=6.9.0'} @@ -1871,6 +1761,7 @@ packages: '@babel/helper-string-parser': 7.22.5 '@babel/helper-validator-identifier': 7.22.5 to-fast-properties: 2.0.0 + dev: true /@babel/types@7.22.17: resolution: {integrity: sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==} @@ -1879,7 +1770,6 @@ packages: '@babel/helper-string-parser': 7.22.5 '@babel/helper-validator-identifier': 7.22.15 to-fast-properties: 2.0.0 - dev: true /@base2/pretty-print-object@1.0.1: resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} @@ -1939,7 +1829,7 @@ packages: /@emotion/babel-plugin@11.11.0: resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} dependencies: - '@babel/helper-module-imports': 7.22.5 + '@babel/helper-module-imports': 7.22.15 '@babel/runtime': 7.22.10 '@emotion/hash': 0.9.1 '@emotion/memoize': 0.8.1 @@ -3037,7 +2927,7 @@ packages: '@babel/parser': 7.22.16 '@babel/plugin-syntax-import-assertions': 7.22.5(@babel/core@7.22.17) '@babel/traverse': 7.22.17 - '@babel/types': 7.22.10 + '@babel/types': 7.22.17 '@graphql-tools/utils': 10.0.6(graphql@16.8.0) graphql: 16.8.0 tslib: 2.6.2 @@ -6193,13 +6083,13 @@ packages: file-system-cache: 2.3.0 dev: true - /@svgr/babel-plugin-add-jsx-attribute@7.0.0(@babel/core@7.22.10): + /@svgr/babel-plugin-add-jsx-attribute@7.0.0(@babel/core@7.22.17): resolution: {integrity: sha512-khWbXesWIP9v8HuKCl2NU2HNAyqpSQ/vkIl36Nbn4HIwEYSRWL0H7Gs6idJdha2DkpFDWlsqMELvoCE8lfFY6Q==} engines: {node: '>=14'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.10 + '@babel/core': 7.22.17 dev: true /@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.22.17): @@ -6211,13 +6101,13 @@ packages: '@babel/core': 7.22.17 dev: true - /@svgr/babel-plugin-remove-jsx-attribute@7.0.0(@babel/core@7.22.10): + /@svgr/babel-plugin-remove-jsx-attribute@7.0.0(@babel/core@7.22.17): resolution: {integrity: sha512-iiZaIvb3H/c7d3TH2HBeK91uI2rMhZNwnsIrvd7ZwGLkFw6mmunOCoVnjdYua662MqGFxlN9xTq4fv9hgR4VXQ==} engines: {node: '>=14'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.10 + '@babel/core': 7.22.17 dev: true /@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.22.17): @@ -6229,13 +6119,13 @@ packages: '@babel/core': 7.22.17 dev: true - /@svgr/babel-plugin-remove-jsx-empty-expression@7.0.0(@babel/core@7.22.10): + /@svgr/babel-plugin-remove-jsx-empty-expression@7.0.0(@babel/core@7.22.17): resolution: {integrity: sha512-sQQmyo+qegBx8DfFc04PFmIO1FP1MHI1/QEpzcIcclo5OAISsOJPW76ZIs0bDyO/DBSJEa/tDa1W26pVtt0FRw==} engines: {node: '>=14'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.10 + '@babel/core': 7.22.17 dev: true /@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.22.17): @@ -6247,13 +6137,13 @@ packages: '@babel/core': 7.22.17 dev: true - /@svgr/babel-plugin-replace-jsx-attribute-value@7.0.0(@babel/core@7.22.10): + /@svgr/babel-plugin-replace-jsx-attribute-value@7.0.0(@babel/core@7.22.17): resolution: {integrity: sha512-i6MaAqIZXDOJeikJuzocByBf8zO+meLwfQ/qMHIjCcvpnfvWf82PFvredEZElErB5glQFJa2KVKk8N2xV6tRRA==} engines: {node: '>=14'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.10 + '@babel/core': 7.22.17 dev: true /@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.22.17): @@ -6265,13 +6155,13 @@ packages: '@babel/core': 7.22.17 dev: true - /@svgr/babel-plugin-svg-dynamic-title@7.0.0(@babel/core@7.22.10): + /@svgr/babel-plugin-svg-dynamic-title@7.0.0(@babel/core@7.22.17): resolution: {integrity: sha512-BoVSh6ge3SLLpKC0pmmN9DFlqgFy4NxNgdZNLPNJWBUU7TQpDWeBuyVuDW88iXydb5Cv0ReC+ffa5h3VrKfk1w==} engines: {node: '>=14'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.10 + '@babel/core': 7.22.17 dev: true /@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.22.17): @@ -6283,13 +6173,13 @@ packages: '@babel/core': 7.22.17 dev: true - /@svgr/babel-plugin-svg-em-dimensions@7.0.0(@babel/core@7.22.10): + /@svgr/babel-plugin-svg-em-dimensions@7.0.0(@babel/core@7.22.17): resolution: {integrity: sha512-tNDcBa+hYn0gO+GkP/AuNKdVtMufVhU9fdzu+vUQsR18RIJ9RWe7h/pSBY338RO08wArntwbDk5WhQBmhf2PaA==} engines: {node: '>=14'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.10 + '@babel/core': 7.22.17 dev: true /@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.22.17): @@ -6301,13 +6191,13 @@ packages: '@babel/core': 7.22.17 dev: true - /@svgr/babel-plugin-transform-react-native-svg@7.0.0(@babel/core@7.22.10): + /@svgr/babel-plugin-transform-react-native-svg@7.0.0(@babel/core@7.22.17): resolution: {integrity: sha512-qw54u8ljCJYL2KtBOjI5z7Nzg8LnSvQOP5hPKj77H4VQL4+HdKbAT5pnkkZLmHKYwzsIHSYKXxHouD8zZamCFQ==} engines: {node: '>=14'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.10 + '@babel/core': 7.22.17 dev: true /@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.22.17): @@ -6319,13 +6209,13 @@ packages: '@babel/core': 7.22.17 dev: true - /@svgr/babel-plugin-transform-svg-component@7.0.0(@babel/core@7.22.10): + /@svgr/babel-plugin-transform-svg-component@7.0.0(@babel/core@7.22.17): resolution: {integrity: sha512-CcFECkDj98daOg9jE3Bh3uyD9kzevCAnZ+UtzG6+BQG/jOQ2OA3jHnX6iG4G1MCJkUQFnUvEv33NvQfqrb/F3A==} engines: {node: '>=12'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.10 + '@babel/core': 7.22.17 dev: true /@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.22.17): @@ -6337,21 +6227,21 @@ packages: '@babel/core': 7.22.17 dev: true - /@svgr/babel-preset@7.0.0(@babel/core@7.22.10): + /@svgr/babel-preset@7.0.0(@babel/core@7.22.17): resolution: {integrity: sha512-EX/NHeFa30j5UjldQGVQikuuQNHUdGmbh9kEpBKofGUtF0GUPJ4T4rhoYiqDAOmBOxojyot36JIFiDUHUK1ilQ==} engines: {node: '>=14'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.10 - '@svgr/babel-plugin-add-jsx-attribute': 7.0.0(@babel/core@7.22.10) - '@svgr/babel-plugin-remove-jsx-attribute': 7.0.0(@babel/core@7.22.10) - '@svgr/babel-plugin-remove-jsx-empty-expression': 7.0.0(@babel/core@7.22.10) - '@svgr/babel-plugin-replace-jsx-attribute-value': 7.0.0(@babel/core@7.22.10) - '@svgr/babel-plugin-svg-dynamic-title': 7.0.0(@babel/core@7.22.10) - '@svgr/babel-plugin-svg-em-dimensions': 7.0.0(@babel/core@7.22.10) - '@svgr/babel-plugin-transform-react-native-svg': 7.0.0(@babel/core@7.22.10) - '@svgr/babel-plugin-transform-svg-component': 7.0.0(@babel/core@7.22.10) + '@babel/core': 7.22.17 + '@svgr/babel-plugin-add-jsx-attribute': 7.0.0(@babel/core@7.22.17) + '@svgr/babel-plugin-remove-jsx-attribute': 7.0.0(@babel/core@7.22.17) + '@svgr/babel-plugin-remove-jsx-empty-expression': 7.0.0(@babel/core@7.22.17) + '@svgr/babel-plugin-replace-jsx-attribute-value': 7.0.0(@babel/core@7.22.17) + '@svgr/babel-plugin-svg-dynamic-title': 7.0.0(@babel/core@7.22.17) + '@svgr/babel-plugin-svg-em-dimensions': 7.0.0(@babel/core@7.22.17) + '@svgr/babel-plugin-transform-react-native-svg': 7.0.0(@babel/core@7.22.17) + '@svgr/babel-plugin-transform-svg-component': 7.0.0(@babel/core@7.22.17) dev: true /@svgr/babel-preset@8.1.0(@babel/core@7.22.17): @@ -6375,8 +6265,8 @@ packages: resolution: {integrity: sha512-ztAoxkaKhRVloa3XydohgQQCb0/8x9T63yXovpmHzKMkHO6pkjdsIAWKOS4bE95P/2quVh1NtjSKlMRNzSBffw==} engines: {node: '>=14'} dependencies: - '@babel/core': 7.22.10 - '@svgr/babel-preset': 7.0.0(@babel/core@7.22.10) + '@babel/core': 7.22.17 + '@svgr/babel-preset': 7.0.0(@babel/core@7.22.17) camelcase: 6.3.0 cosmiconfig: 8.2.0 transitivePeerDependencies: @@ -6401,7 +6291,7 @@ packages: resolution: {integrity: sha512-42Ej9sDDEmsJKjrfQ1PHmiDiHagh/u9AHO9QWbeNx4KmD9yS5d1XHmXUNINfUcykAU+4431Cn+k6Vn5mWBYimQ==} engines: {node: '>=14'} dependencies: - '@babel/types': 7.22.10 + '@babel/types': 7.22.17 entities: 4.5.0 dev: true @@ -6417,8 +6307,8 @@ packages: resolution: {integrity: sha512-SWlTpPQmBUtLKxXWgpv8syzqIU8XgFRvyhfkam2So8b3BE0OS0HPe5UfmlJ2KIC+a7dpuuYovPR2WAQuSyMoPw==} engines: {node: '>=14'} dependencies: - '@babel/core': 7.22.10 - '@svgr/babel-preset': 7.0.0(@babel/core@7.22.10) + '@babel/core': 7.22.17 + '@svgr/babel-preset': 7.0.0(@babel/core@7.22.17) '@svgr/hast-util-to-babel-ast': 7.0.0 svg-parser: 2.0.4 transitivePeerDependencies: @@ -6545,7 +6435,7 @@ packages: resolution: {integrity: sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==} engines: {node: '>=14'} dependencies: - '@babel/code-frame': 7.22.10 + '@babel/code-frame': 7.22.13 '@babel/runtime': 7.22.10 '@types/aria-query': 5.0.1 aria-query: 5.1.3 @@ -7264,9 +7154,9 @@ packages: peerDependencies: vite: ^4.2.0 dependencies: - '@babel/core': 7.22.10 - '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.22.10) - '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.22.10) + '@babel/core': 7.22.17 + '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.22.17) + '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.22.17) react-refresh: 0.14.0 vite: 4.4.9(@types/node@20.6.0) transitivePeerDependencies: @@ -13102,7 +12992,7 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.22.10 + '@babel/code-frame': 7.22.13 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 diff --git a/tsconfig.base.json b/tsconfig.base.json index 1c53af813..4d831345c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -23,6 +23,9 @@ "@origin/oeth/history": [ "libs/oeth/history/src/index.ts" ], + "@origin/oeth/redeem": [ + "libs/oeth/redeem/src/index.ts" + ], "@origin/oeth/shared": [ "libs/oeth/shared/src/index.ts" ],