diff --git a/.env b/.env index ab2a0d016..154aa821b 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -VITE_WALLET_CONNECT_PROJECT_ID= \ No newline at end of file +VITE_WALLET_CONNECT_PROJECT_ID= +VITE_INFURA_ID= diff --git a/.eslintrc.json b/.eslintrc.json index 4ead2a719..4dcc7dca2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,6 +28,7 @@ } ], "react/react-in-jsx-scope": "off", + "no-empty": ["error", { "allowEmptyCatch": true }], // Unused imports rules "unused-imports/no-unused-imports": "error", "unused-imports/no-unused-vars": [ diff --git a/apps/oeth/src/App.tsx b/apps/oeth/src/App.tsx index 999231de7..be90f4dbb 100644 --- a/apps/oeth/src/App.tsx +++ b/apps/oeth/src/App.tsx @@ -2,12 +2,17 @@ import { Container, Stack } from '@mui/material'; import { HistoryView } from '@origin/oeth/history'; import { SwapView } from '@origin/oeth/swap'; import { WrapView } from '@origin/oeth/wrap'; +import { usePrices } from '@origin/shared/providers'; import { Route, Routes } from 'react-router-dom'; import { ApyHeader } from './components/ApyHeader'; import { Topnav } from './components/Topnav'; export function App() { + const { data } = usePrices(); + + console.log('data', data); + return ( diff --git a/apps/oeth/src/clients/wagmi.ts b/apps/oeth/src/clients/wagmi.ts index 54f81723f..5c807f52c 100644 --- a/apps/oeth/src/clients/wagmi.ts +++ b/apps/oeth/src/clients/wagmi.ts @@ -13,15 +13,16 @@ import { } from '@rainbow-me/rainbowkit/wallets'; import { configureChains, createConfig } from 'wagmi'; import { goerli, localhost, mainnet } from 'wagmi/chains'; +import { infuraProvider } from 'wagmi/providers/infura'; import { publicProvider } from 'wagmi/providers/public'; -const VITE_WALLET_CONNECT_PROJECT_ID = import.meta.env[ - 'VITE_WALLET_CONNECT_PROJECT_ID' -]; +const VITE_WALLET_CONNECT_PROJECT_ID = import.meta.env + .VITE_WALLET_CONNECT_PROJECT_ID; +const VITE_INFURA_ID = import.meta.env.VITE_INFURA_ID; export const { chains, publicClient, webSocketPublicClient } = configureChains( [mainnet, goerli, localhost], - [publicProvider()], + [infuraProvider({ apiKey: VITE_INFURA_ID }), publicProvider()], ); const connectors = connectorsForWallets([ diff --git a/apps/oeth/src/components/ApyHeader.tsx b/apps/oeth/src/components/ApyHeader.tsx index 6d1aac157..d273c9119 100644 --- a/apps/oeth/src/components/ApyHeader.tsx +++ b/apps/oeth/src/components/ApyHeader.tsx @@ -11,7 +11,9 @@ import { Typography, } from '@mui/material'; import { Icon } from '@origin/shared/components'; +import { tokens } from '@origin/shared/contracts'; import { useIntl } from 'react-intl'; +import { useAccount, useBalance } from 'wagmi'; const days = [7, 30]; @@ -19,10 +21,16 @@ export function ApyHeader() { const intl = useIntl(); const [selectedPeriod, setSelectedPeriod] = useState(30); const [anchorEl, setAnchorEl] = React.useState(null); + const { address } = useAccount(); + const { data: oethBalance } = useBalance({ + address, + token: tokens.mainnet.OUSD.address, + watch: true, + }); - function handleClose() { + const handleClose = () => { setAnchorEl(null); - } + }; return ( <> @@ -138,7 +146,9 @@ export function ApyHeader() { + +interface ImportMetaEnv { + readonly VITE_WALLET_CONNECT_PROJECT_ID: string; + readonly VITE_INFURA_ID: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/apps/oeth/vite.config.ts b/apps/oeth/vite.config.ts index aff56abc6..e1b3e9e4e 100644 --- a/apps/oeth/vite.config.ts +++ b/apps/oeth/vite.config.ts @@ -50,7 +50,7 @@ export default defineConfig({ viteStaticCopy({ targets: [ { - src: path.resolve(__dirname, '../../libs/shared/assets/files/*'), + src: path.resolve(__dirname, '../../libs/shared/assets/files/**/*'), dest: './images', }, ], diff --git a/libs/oeth/swap/src/components/GasPopover.tsx b/libs/oeth/swap/src/components/GasPopover.tsx index 2af852e2e..fe0e3f26e 100644 --- a/libs/oeth/swap/src/components/GasPopover.tsx +++ b/libs/oeth/swap/src/components/GasPopover.tsx @@ -1,10 +1,9 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { alpha, Box, Button, - debounce, FormControl, FormHelperText, IconButton, @@ -15,12 +14,16 @@ import { Stack, useTheme, } from '@mui/material'; -import { isNumber } from 'lodash'; +import { produce } from 'immer'; import { useIntl } from 'react-intl'; +import { useFeeData } from 'wagmi'; + +import { useSwapState } from '../state'; import type { Theme } from '@mui/material'; +import type { ChangeEvent } from 'react'; -const defaultPriceTolerance = 0.01; +const defaultSlippage = 0.01; const gridStyles = { display: 'grid', @@ -30,20 +33,14 @@ const gridStyles = { alignItems: 'center', }; -interface Props { - gasPrice: number; - onPriceToleranceChange: (value: number) => void; -} - -export function GasPopover({ gasPrice, onPriceToleranceChange }: Props) { +export function GasPopover() { const theme = useTheme(); const intl = useIntl(); const [anchorEl, setAnchorEl] = useState(null); - const [priceTolerance, setPriceTolerance] = useState(defaultPriceTolerance); + const [{ slippage }, setSwapState] = useSwapState(); + const { data: feeData } = useFeeData({ formatUnits: 'gwei' }); - useEffect(() => { - onPriceToleranceChange(priceTolerance); - }, [priceTolerance, onPriceToleranceChange]); + const handleSlippageChange = (evt: ChangeEvent) => {}; return ( <> @@ -89,13 +86,13 @@ export function GasPopover({ gasPrice, onPriceToleranceChange }: Props) { > - - {intl.formatMessage({ defaultMessage: 'Price tolerance' })} + + {intl.formatMessage({ defaultMessage: 'Slippage' })} { - if (isNumber(parseFloat(e.target.value))) { - setPriceTolerance(Number(e.target.value)); - } - }, 300)} + onChange={handleSlippageChange} endAdornment={ setPriceTolerance(defaultPriceTolerance)} + disabled={slippage === defaultSlippage} + onClick={() => { + setSwapState( + produce((draft) => { + draft.slippage = defaultSlippage; + }), + ); + }} > {intl.formatMessage({ defaultMessage: 'Auto' })} - {priceTolerance > 1 ? ( + {slippage > 1 ? ( { + const [state, setState] = useState({ + amountIn: 0n, + tokenIn: tokens.mainnet.ETH as Token, + amountOut: 0n, + tokenOut: tokens.mainnet.OETH as Token, + slippage: 0.01, + }); + + useDebouncedEffect( + () => { + setState( + produce((draft) => { + draft.amountOut = draft.amountIn; + }), + ); + }, + [state.amountIn], + 1e3, + ); + + return [state, setState]; + }); diff --git a/libs/oeth/swap/src/views/SwapView.tsx b/libs/oeth/swap/src/views/SwapView.tsx index 392759617..5c6840291 100644 --- a/libs/oeth/swap/src/views/SwapView.tsx +++ b/libs/oeth/swap/src/views/SwapView.tsx @@ -1,81 +1,66 @@ import { useState } from 'react'; -import { Stack } from '@mui/material'; -import { - ActionButton, - DropdownIcon, - SwapCard, - TokenListModal, -} from '@origin/shared/components'; -import random from 'lodash/random'; +import { Box, IconButton, Stack } from '@mui/material'; +import { Card, TokenInput } from '@origin/shared/components'; +import { TokenSelectModal, usePrices } from '@origin/shared/providers'; +import { isNilOrEmpty } from '@origin/shared/utils'; +import { produce } from 'immer'; import { useIntl } from 'react-intl'; +import { useAccount, useBalance } from 'wagmi'; import { GasPopover } from '../components/GasPopover'; -import { SwapRoute } from '../components/SwapRoute'; +import { swapTokens } from '../constants'; +import { SwapProvider, useSwapState } from '../state'; -import type { Option } from '@origin/shared/components'; +import type { IconButtonProps } from '@mui/material'; +import type { Token } from '@origin/shared/contracts'; -export function SwapView() { +function SwapViewWrapped() { const intl = useIntl(); - const [isSelectionModalOpen, setSelectionModal] = useState(false); - const [values, setValues] = useState<{ - baseToken: Omit; - exchangeCurrency: Omit; - }>({ - baseToken: { - abbreviation: 'OETH', - imgSrc: 'https://app.oeth.com/images/currency/oeth-icon-small.svg', - quantity: 0, - value: 0, - }, - exchangeCurrency: { - abbreviation: 'ETH', - imgSrc: 'https://app.oeth.com/images/currency/eth-icon-small.svg', - quantity: 0, - value: 0, - }, + const { address, isConnected } = useAccount(); + const [tokenModal, setTokenModal] = useState<'tokenIn' | 'tokenOut' | null>( + null, + ); + const [{ amountIn, amountOut, tokenIn, tokenOut }, setSwapState] = + useSwapState(); + const { data: prices, isLoading: isPriceLoading } = usePrices(); + const { data: balTokenIn, isLoading: isBalTokenInLoading } = useBalance({ + address, + token: tokenIn.address, + }); + const { data: balTokenOut, isLoading: isBalTokenOutLoading } = useBalance({ + address, + token: tokenOut.address, }); - function handleCloseSelectionModal() { - setSelectionModal(false); - } + const handleCloseSelectionModal = () => { + setTokenModal(null); + }; - function handleValueChange(value: string) { - const number = parseInt(value) || 0; - setValues((prev) => ({ - baseToken: { - ...prev.baseToken, - quantity: number, - value: number * random(18500, 19000, true), - }, - exchangeCurrency: { - ...prev.exchangeCurrency, - quantity: number * random(number - 0.5, number, true), - value: number * random(18500, 19000, true), - }, - })); - } + const handleSelectToken = (value: Token) => { + setSwapState( + produce((draft) => { + draft[tokenModal] = value; + }), + ); + }; - function swapTokens() { - setValues((prev) => ({ - baseToken: { - ...prev.exchangeCurrency, - quantity: prev.baseToken.quantity, - value: prev.baseToken.quantity * random(18500, 19000), - }, - exchangeCurrency: { - ...prev.baseToken, - quantity: - prev.baseToken.quantity * - random(prev.baseToken.quantity - 0.5, prev.baseToken.quantity, true), - value: prev.baseToken.quantity * random(18500, 19000, true), - }, - })); - } + const handleExchangeTokens = () => { + setSwapState( + produce((draft) => { + draft.amountIn = 0n; + draft.amountOut = 0n; + const oldTokenOut = draft.tokenOut; + draft.tokenOut = draft.tokenIn; + draft.tokenIn = oldTokenOut; + }), + ); + }; return ( <> - {intl.formatMessage({ defaultMessage: 'Swap' })} - null} - /> + } - onSwap={swapTokens} - onValueChange={handleValueChange} - baseTokenIcon={values.baseToken.imgSrc} - baseTokenName={values.baseToken.abbreviation as string} - baseTokenValue={values.baseToken.value} - exchangeTokenName={values.exchangeCurrency.abbreviation as string} - exchangeTokenIcon={values.exchangeCurrency.imgSrc} - exchangeTokenQuantity={values.exchangeCurrency.quantity} - exchangeTokenValue={values.exchangeCurrency.value} - exchangeTokenNode={ - setSelectionModal(true)} /> - } > - - console.log('test')}> - {intl.formatMessage({ defaultMessage: 'Swap' })} - - - - setValues((prev) => ({ - ...prev, - exchangeCurrency: { - value: prev.baseToken.quantity * random(18500, 19000, true), - quantity: random( - prev.baseToken.quantity - 0.5, - prev.baseToken.quantity, - ), - abbreviation: option.name, - imgSrc: option.imgSrc, - }, - })) + + { + setSwapState( + produce((draft) => { + draft.amountIn = val; + }), + ); + }} + balance={balTokenIn?.value} + isBalanceLoading={isBalTokenInLoading} + token={tokenIn} + onTokenClick={() => { + setTokenModal('tokenIn'); + }} + tokenPriceUsd={prices?.[tokenIn.symbol]} + isPriceLoading={isPriceLoading} + isConnected={isConnected} + /> + { + setTokenModal('tokenOut'); + }} + tokenPriceUsd={prices?.[tokenOut.symbol]} + isPriceLoading={isPriceLoading} + inputProps={{ readOnly: true }} + isConnected={isConnected} + /> + + + + ); } + +export const SwapView = () => ( + + + +); + +function SwapButton(props: IconButtonProps) { + return ( + theme.palette.background.paper, + strokeWidth: (theme) => theme.typography.pxToRem(2), + stroke: (theme) => theme.palette.grey[700], + transform: { xs: 'translateY(-20%)', md: 'translateY(-8%)' }, + backgroundColor: (theme) => theme.palette.divider, + '& img': { + transition: (theme) => theme.transitions.create('transform'), + }, + '&:hover': { + backgroundColor: (theme) => theme.palette.background.default, + '& img': { + transform: 'rotate(-180deg)', + }, + }, + ...props?.sx, + }} + > + + + ); +} diff --git a/libs/oeth/swap/tsconfig.lib.json b/libs/oeth/swap/tsconfig.lib.json index c96232320..4772a6cac 100644 --- a/libs/oeth/swap/tsconfig.lib.json +++ b/libs/oeth/swap/tsconfig.lib.json @@ -8,6 +8,22 @@ "../../../node_modules/@nx/react/typings/cssmodule.d.ts", "../../../node_modules/@nx/react/typings/image.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"] + "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", + "../../shared/providers/src/wagmi/components/TokenSelectModal.tsx" + ] } diff --git a/libs/shared/assets/files/tokens/DAI.svg b/libs/shared/assets/files/tokens/DAI.svg new file mode 100644 index 000000000..55f30e4ad --- /dev/null +++ b/libs/shared/assets/files/tokens/DAI.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/libs/shared/assets/files/tokens/ETH.svg b/libs/shared/assets/files/tokens/ETH.svg new file mode 100644 index 000000000..c330fd730 --- /dev/null +++ b/libs/shared/assets/files/tokens/ETH.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/libs/shared/assets/files/tokens/OETH.svg b/libs/shared/assets/files/tokens/OETH.svg new file mode 100644 index 000000000..e12abe4ae --- /dev/null +++ b/libs/shared/assets/files/tokens/OETH.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/shared/assets/files/tokens/OUSD.svg b/libs/shared/assets/files/tokens/OUSD.svg new file mode 100644 index 000000000..412c5703c --- /dev/null +++ b/libs/shared/assets/files/tokens/OUSD.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/shared/assets/files/tokens/USDC.svg b/libs/shared/assets/files/tokens/USDC.svg new file mode 100644 index 000000000..9303ee78e --- /dev/null +++ b/libs/shared/assets/files/tokens/USDC.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/libs/shared/assets/files/tokens/USDT.svg b/libs/shared/assets/files/tokens/USDT.svg new file mode 100644 index 000000000..c7088804d --- /dev/null +++ b/libs/shared/assets/files/tokens/USDT.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/libs/shared/assets/files/tokens/WETH.svg b/libs/shared/assets/files/tokens/WETH.svg new file mode 100644 index 000000000..54781a0fc --- /dev/null +++ b/libs/shared/assets/files/tokens/WETH.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/libs/shared/assets/files/tokens/WOETH.svg b/libs/shared/assets/files/tokens/WOETH.svg new file mode 100644 index 000000000..856f1950d --- /dev/null +++ b/libs/shared/assets/files/tokens/WOETH.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/shared/assets/files/tokens/WOUSD.svg b/libs/shared/assets/files/tokens/WOUSD.svg new file mode 100644 index 000000000..6d665bf22 --- /dev/null +++ b/libs/shared/assets/files/tokens/WOUSD.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/shared/assets/files/tokens/frxETH.svg b/libs/shared/assets/files/tokens/frxETH.svg new file mode 100644 index 000000000..88b0ff85f --- /dev/null +++ b/libs/shared/assets/files/tokens/frxETH.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/libs/shared/assets/files/tokens/rETH.svg b/libs/shared/assets/files/tokens/rETH.svg new file mode 100644 index 000000000..6e86e83b2 --- /dev/null +++ b/libs/shared/assets/files/tokens/rETH.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/libs/shared/assets/files/tokens/sfrxETH.svg b/libs/shared/assets/files/tokens/sfrxETH.svg new file mode 100644 index 000000000..289b9156d --- /dev/null +++ b/libs/shared/assets/files/tokens/sfrxETH.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/libs/shared/assets/files/tokens/stETH.svg b/libs/shared/assets/files/tokens/stETH.svg new file mode 100644 index 000000000..76e013643 --- /dev/null +++ b/libs/shared/assets/files/tokens/stETH.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/libs/shared/components/src/Inputs/BigintInput.tsx b/libs/shared/components/src/Inputs/BigintInput.tsx new file mode 100644 index 000000000..bc9972830 --- /dev/null +++ b/libs/shared/components/src/Inputs/BigintInput.tsx @@ -0,0 +1,106 @@ +import { forwardRef, useEffect, useState } from 'react'; + +import { InputBase } from '@mui/material'; +import { isNilOrEmpty } from '@origin/shared/utils'; +import { formatUnits, parseUnits } from 'viem'; + +import type { InputBaseProps } from '@mui/material'; +import type { ChangeEvent } from 'react'; + +export type BigintInputProps = { + value: bigint; + decimals?: number; + onChange?: (value: bigint) => void; + isLoading?: boolean; + isError?: boolean; +} & Omit; + +export const BigintInput = forwardRef( + ({ value, decimals = 18, isLoading, isError, onChange, ...rest }, ref) => { + const [strVal, setStrVal] = useState(formatUnits(value, decimals)); + + useEffect(() => { + if (value === 0n && (isNilOrEmpty(strVal) || strVal === '0.')) { + return; + } + + if (value === 0n && !/0\.0+$/.test(strVal)) { + setStrVal(''); + return; + } + + if ( + isNilOrEmpty(strVal) || + strVal === '0.' || + value !== parseUnits(strVal, decimals) + ) { + setStrVal(formatUnits(value, decimals)); + } + }, [value, decimals, strVal]); + + 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 = parseUnits(val, decimals); + setStrVal(evt.target.value === '.' ? '0.' : evt.target.value); + if (onChange && num !== value) { + onChange(num); + } + } catch {} + } + }; + + return ( + + ); + }, +); + +BigintInput.displayName = 'BigintInput'; diff --git a/libs/shared/components/src/Inputs/TokenInput.tsx b/libs/shared/components/src/Inputs/TokenInput.tsx new file mode 100644 index 000000000..743616544 --- /dev/null +++ b/libs/shared/components/src/Inputs/TokenInput.tsx @@ -0,0 +1,172 @@ +import { forwardRef } from 'react'; + +import { alpha, Box, Button, Skeleton, Stack, Typography } from '@mui/material'; +import { useIntl } from 'react-intl'; +import { formatUnits } from 'viem'; + +import { BigintInput } from './BigintInput'; + +import type { StackProps } from '@mui/material'; +import type { Token } from '@origin/shared/contracts'; + +import type { BigintInputProps } from './BigintInput'; + +export type TokenInputProps = { + amount: bigint; + decimals?: number; + onAmountChange?: (value: bigint) => void; + isAmountLoading?: boolean; + isAmountDisabled?: boolean; + isAmountError?: boolean; + isConnected: boolean; + balance?: bigint; + isBalanceLoading?: boolean; + disableMaxClick?: boolean; + token: Token; + onTokenClick: () => void; + tokenPriceUsd?: number; + isPriceLoading?: boolean; + inputProps?: Omit< + BigintInputProps, + 'value' | 'decimals' | 'onChange' | 'isLoading' | 'isError' + >; +} & StackProps; + +export const TokenInput = forwardRef( + ( + { + amount, + decimals = 18, + onAmountChange, + isAmountLoading, + isAmountDisabled, + isAmountError, + isConnected, + balance = 0n, + isBalanceLoading, + disableMaxClick, + token, + onTokenClick, + tokenPriceUsd = 0, + isPriceLoading, + inputProps, + ...rest + }, + ref, + ) => { + const intl = useIntl(); + + const handleMaxClick = () => { + if (onAmountChange) { + onAmountChange(balance); + } + }; + + const bal = +formatUnits(balance, decimals); + const amountUsd = +formatUnits(amount, decimals) * tokenPriceUsd; + + return ( + + + + {isPriceLoading ? ( + + ) : tokenPriceUsd > 0 ? ( + + {intl.formatNumber(amountUsd, { + style: 'currency', + currency: 'usd', + maximumFractionDigits: 4, + })} + + ) : null} + + + {isConnected && ( + + {isBalanceLoading ? ( + + ) : ( + <> + + {intl.formatNumber(bal, { maximumFractionDigits: 4 })}  + {token.symbol} + + {!disableMaxClick && ( + + )} + + )} + + )} + + + + ); + }, +); +TokenInput.displayName = 'TokenInput'; + +type TokenButtonProps = { token: Token; isDisabled?: boolean } & StackProps; + +function TokenButton({ token, isDisabled, ...rest }: TokenButtonProps) { + return ( + alpha(theme.palette.common.white, 0.1), + fontStyle: 'normal', + cursor: 'pointer', + fontWeight: 500, + gap: 1, + ':hover': { + background: (theme) => + `linear-gradient(#3B3C3E, #3B3C3E) padding-box, linear-gradient(90deg, ${alpha( + theme.palette.primary.main, + 0.4, + )} 0%, ${alpha(theme.palette.primary.dark, 0.4)} 100%) border-box;`, + }, + ...rest?.sx, + }} + {...rest} + > + + + {token.symbol} + + {!isDisabled && } + + ); +} diff --git a/libs/shared/components/src/Inputs/index.ts b/libs/shared/components/src/Inputs/index.ts new file mode 100644 index 000000000..332bccc92 --- /dev/null +++ b/libs/shared/components/src/Inputs/index.ts @@ -0,0 +1,2 @@ +export * from './BigintInput'; +export * from './TokenInput'; diff --git a/libs/shared/components/src/MiddleTruncated/index.tsx b/libs/shared/components/src/MiddleTruncated/index.tsx index 90ef9583f..4a7429f16 100644 --- a/libs/shared/components/src/MiddleTruncated/index.tsx +++ b/libs/shared/components/src/MiddleTruncated/index.tsx @@ -15,13 +15,6 @@ const truncate: SxProps = { textOverflow: 'ellipsis', }; -const Text = (props: TypographyProps) => ( - -); - export const MiddleTruncated = ({ children, textProps, @@ -36,9 +29,9 @@ export const MiddleTruncated = ({ {...rest} sx={{ display: 'flex', flexWrap: 'nowrap', minWidth: 0, ...rest?.sx }} > - + {children} - + ); } @@ -54,11 +47,11 @@ export const MiddleTruncated = ({ {...rest} sx={{ display: 'flex', flexWrap: 'nowrap', minWidth: 0, ...rest?.sx }} > - + {partStart} - - {breakspace &&  } - {partEnd} + + {breakspace &&  } + {partEnd} ); }; diff --git a/libs/shared/components/src/index.ts b/libs/shared/components/src/index.ts index a94a68bb3..2d02ac512 100644 --- a/libs/shared/components/src/index.ts +++ b/libs/shared/components/src/index.ts @@ -1,4 +1,5 @@ export * from './Cards'; +export * from './Inputs'; export * from './LinkIcon'; export * from './Loader'; export * from './MiddleTruncated'; diff --git a/libs/shared/contracts/.babelrc b/libs/shared/contracts/.babelrc new file mode 100644 index 000000000..1ea870ead --- /dev/null +++ b/libs/shared/contracts/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/shared/contracts/.eslintrc.json b/libs/shared/contracts/.eslintrc.json new file mode 100644 index 000000000..a786f2cf3 --- /dev/null +++ b/libs/shared/contracts/.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/shared/contracts/README.md b/libs/shared/contracts/README.md new file mode 100644 index 000000000..474d2492c --- /dev/null +++ b/libs/shared/contracts/README.md @@ -0,0 +1,7 @@ +# shared-contracts + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared-contracts` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared/contracts/project.json b/libs/shared/contracts/project.json new file mode 100644 index 000000000..39cb67ed6 --- /dev/null +++ b/libs/shared/contracts/project.json @@ -0,0 +1,20 @@ +{ + "name": "shared-contracts", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shared/contracts/src", + "projectType": "library", + "tags": [], + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "outputs": [ + "{options.outputFile}" + ], + "options": { + "lintFilePatterns": [ + "libs/shared/contracts/**/*.{ts,tsx,js,jsx}" + ] + } + } + } +} diff --git a/libs/shared/contracts/src/index.ts b/libs/shared/contracts/src/index.ts new file mode 100644 index 000000000..1b22cc5bd --- /dev/null +++ b/libs/shared/contracts/src/index.ts @@ -0,0 +1,2 @@ +export * from './tokens'; +export * from './types'; diff --git a/libs/shared/contracts/src/tokens.ts b/libs/shared/contracts/src/tokens.ts new file mode 100644 index 000000000..f82880af6 --- /dev/null +++ b/libs/shared/contracts/src/tokens.ts @@ -0,0 +1,138 @@ +import { erc20ABI } from 'wagmi'; +import { mainnet } from 'wagmi/chains'; + +export const tokens = { + mainnet: { + ETH: { + address: undefined, + chainId: mainnet.id, + abi: erc20ABI, + name: 'ETH', + icon: '/images/tokens/ETH.svg', + decimals: 18, + symbol: 'ETH', + }, + WETH: { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + chainId: mainnet.id, + abi: erc20ABI, + name: 'Wrapped Ether', + icon: '/images/tokens/WETH.svg', + decimals: 18, + symbol: 'WETH', + }, + // Native stablecoins + DAI: { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + chainId: mainnet.id, + abi: erc20ABI, + name: 'Dai Stablecoin', + icon: '/images/tokens/DAI.svg', + decimals: 18, + symbol: 'DAI', + }, + USDC: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + chainId: mainnet.id, + abi: erc20ABI, + name: 'USD Coin', + icon: '/images/tokens/USDC.svg', + decimals: 6, + symbol: 'USDC', + }, + USDT: { + address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + chainId: mainnet.id, + abi: erc20ABI, + name: 'Tether USD', + icon: '/images/tokens/USDT.svg', + decimals: 6, + symbol: 'USDT', + }, + TUSD: { + address: '0x0000000000085d4780B73119b644AE5ecd22b376', + chainId: mainnet.id, + abi: erc20ABI, + name: 'TrueUSD', + icon: '/images/tokens/TUSD.svg', + decimals: 18, + symbol: 'TUSD', + }, + // Origin + OETH: { + address: '0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3', + chainId: mainnet.id, + abi: erc20ABI, + name: 'Origin Ether', + icon: '/images/tokens/OETH.svg', + decimals: 18, + symbol: 'OETH', + }, + WOETH: { + address: '0xDcEe70654261AF21C44c093C300eD3Bb97b78192', + chainId: mainnet.id, + abi: erc20ABI, + name: 'Wrapped Origin Ether', + icon: '/images/tokens/WOETH.svg', + decimals: 18, + symbol: 'WOETH', + }, + OUSD: { + address: '0x2A8e1E676Ec238d8A992307B495b45B3fEAa5e86', + chainId: mainnet.id, + abi: erc20ABI, + name: 'Origin Dollar', + icon: '/images/tokens/OUSD.svg', + decimals: 18, + symbol: 'OUSD', + }, + WOUSD: { + address: '0xD2af830E8CBdFed6CC11Bab697bB25496ed6FA62', + chainId: mainnet.id, + abi: erc20ABI, + name: 'WrappedOrigin Dollar', + icon: '/images/tokens/WOUSD.svg', + decimals: 18, + symbol: 'WOUSD', + }, + // 1-inch LP + stETH: { + address: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', + chainId: mainnet.id, + abi: erc20ABI, + name: 'Liquid Staked Ether 2.0', + icon: '/images/tokens/stETH.svg', + decimals: 18, + symbol: 'stETH', + }, + // rocket pool + rETH: { + address: '0xae78736Cd615f374D3085123A210448E74Fc6393', + chainId: mainnet.id, + abi: erc20ABI, + name: 'Rocket Pool ETH', + icon: '/images/tokens/rETH.svg', + decimals: 18, + symbol: 'rETH', + }, + // Frax + frxETH: { + address: '0x5e8422345238f34275888049021821e8e08caa1f', + chainId: mainnet.id, + abi: erc20ABI, + name: 'Frax Ether', + icon: '/images/tokens/frxETH.svg', + decimals: 18, + symbol: 'frxETH', + }, + sfrxETH: { + address: '0xac3E018457B222d93114458476f3E3416Abbe38F', + chainId: mainnet.id, + abi: erc20ABI, + name: 'Staked Frax Ether', + icon: ' /images/tokens/sfrxETH.svg', + decimals: 18, + symbol: 'sfrxETH', + }, + }, +} as const; diff --git a/libs/shared/contracts/src/types.ts b/libs/shared/contracts/src/types.ts new file mode 100644 index 000000000..77949a852 --- /dev/null +++ b/libs/shared/contracts/src/types.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { HexAddress } from '@origin/shared/utils'; + +export type Contract = { + address: undefined | HexAddress; + chainId: number; + abi: any; + name?: string; + icon?: string; +}; + +export type Token = { symbol: string; decimals: number } & Contract; diff --git a/libs/shared/contracts/tsconfig.json b/libs/shared/contracts/tsconfig.json new file mode 100644 index 000000000..90fcf85c5 --- /dev/null +++ b/libs/shared/contracts/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/shared/contracts/tsconfig.lib.json b/libs/shared/contracts/tsconfig.lib.json new file mode 100644 index 000000000..c96232320 --- /dev/null +++ b/libs/shared/contracts/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../node_modules/@nx/react/typings/image.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/shared/providers/src/index.ts b/libs/shared/providers/src/index.ts index ce6a723cd..14734c906 100644 --- a/libs/shared/providers/src/index.ts +++ b/libs/shared/providers/src/index.ts @@ -1 +1,2 @@ +export * from './prices'; export * from './wagmi'; diff --git a/libs/shared/providers/src/prices/constants.ts b/libs/shared/providers/src/prices/constants.ts new file mode 100644 index 000000000..8727d6fea --- /dev/null +++ b/libs/shared/providers/src/prices/constants.ts @@ -0,0 +1,18 @@ +import type { SupportedToken } from './types'; + +export const coingeckoApiEndpoint = 'https://api.coingecko.com/api/v3'; + +export const coingeckoTokenIds: Record = { + ETH: 'ethereum', + WETH: 'weth', + DAI: 'dai', + USDC: 'usd-coin', + USDT: 'tether', + TUSD: 'true-usd', + OETH: 'origin-ether', + OUSD: 'origin-dollar', + stETH: 'staked-ether', + rETH: 'rocket-pool-eth', + frxETH: 'frax-ether', + sfrxETH: 'staked-frax-ether', +}; diff --git a/libs/shared/providers/src/prices/hooks.ts b/libs/shared/providers/src/prices/hooks.ts new file mode 100644 index 000000000..77ed146a9 --- /dev/null +++ b/libs/shared/providers/src/prices/hooks.ts @@ -0,0 +1,44 @@ +import { isNilOrEmpty } from '@origin/shared/utils'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; + +import { coingeckoApiEndpoint, coingeckoTokenIds } from './constants'; + +import type { UseQueryOptions } from '@tanstack/react-query'; + +import type { SupportedToken } from './types'; + +export const usePrices = ( + tokens?: SupportedToken[], + options?: UseQueryOptions< + Record, + Error, + Record, + ['usePrices', SupportedToken[]] + >, +) => { + return useQuery({ + queryKey: ['usePrices', tokens] as const, + queryFn: async ({ queryKey }) => { + let tokens = queryKey[1]; + if (isNilOrEmpty(tokens)) { + tokens = Object.keys(coingeckoTokenIds) as SupportedToken[]; + } + + const res = await axios.get( + `${coingeckoApiEndpoint}/simple/price?ids=${tokens + .map((t) => coingeckoTokenIds[t]) + .join('%2C')}&vs_currencies=usd`, + ); + + return tokens.reduce( + (acc, curr) => ({ + ...acc, + [curr]: res?.data?.[coingeckoTokenIds[curr]]?.usd ?? 0, + }), + {}, + ); + }, + ...options, + }); +}; diff --git a/libs/shared/providers/src/prices/index.ts b/libs/shared/providers/src/prices/index.ts new file mode 100644 index 000000000..4cc90d02b --- /dev/null +++ b/libs/shared/providers/src/prices/index.ts @@ -0,0 +1 @@ +export * from './hooks'; diff --git a/libs/shared/providers/src/prices/types.ts b/libs/shared/providers/src/prices/types.ts new file mode 100644 index 000000000..9b87b7eec --- /dev/null +++ b/libs/shared/providers/src/prices/types.ts @@ -0,0 +1,13 @@ +export type SupportedToken = + | 'ETH' + | 'WETH' + | 'DAI' + | 'USDC' + | 'USDT' + | 'TUSD' + | 'OETH' + | 'OUSD' + | 'stETH' + | 'rETH' + | 'frxETH' + | 'sfrxETH'; diff --git a/libs/shared/providers/src/wagmi/components/TokenSelectModal.tsx b/libs/shared/providers/src/wagmi/components/TokenSelectModal.tsx new file mode 100644 index 000000000..ade44affa --- /dev/null +++ b/libs/shared/providers/src/wagmi/components/TokenSelectModal.tsx @@ -0,0 +1,151 @@ +import { + Box, + Dialog, + MenuItem, + MenuList, + Skeleton, + Stack, + Typography, +} from '@mui/material'; +import { useIntl } from 'react-intl'; +import { useAccount, useBalance } from 'wagmi'; + +import { usePrices } from '../../prices'; + +import type { DialogProps, MenuItemProps } from '@mui/material'; +import type { Token } from '@origin/shared/contracts'; + +export type TokenSelectModalProps = { + tokens: Token[]; + onSelectToken: (value: Token) => void; + selectedTokenSymbol?: string; +} & DialogProps; + +export const TokenSelectModal = ({ + tokens, + onSelectToken, + selectedTokenSymbol, + onClose, + ...rest +}: TokenSelectModalProps) => { + return ( + theme.palette.background.paper, + borderRadius: 2, + border: '1px solid', + borderColor: (theme) => theme.palette.grey[800], + backgroundImage: 'none', + margin: 0, + minWidth: 'min(90vw, 33rem)', + }, + }} + {...rest} + onClose={onClose} + > + + {tokens.map((token) => ( + { + onSelectToken(token); + onClose({}, 'backdropClick'); + }} + selected={selectedTokenSymbol === token.symbol} + /> + ))} + + + ); +}; + +type TokenListItemProps = { + token: Token; + selected: boolean; +} & MenuItemProps; + +function TokenListItem({ token, selected, ...rest }: TokenListItemProps) { + const intl = useIntl(); + const { address } = useAccount(); + const { data: balance, isLoading: isBalanceLoading } = useBalance({ + address, + token: token.address, + }); + const { data: prices } = usePrices(); + + const bal = parseFloat(balance?.formatted ?? '0'); + const balUsd = bal * (prices?.[token.symbol] ?? 0); + + return ( + theme.palette.background.paper, + borderRadius: 1, + '&:hover': { + background: (theme) => theme.palette.grey[700], + }, + ...rest?.sx, + }} + > + + + + {token?.name} + span:not(:last-child):after': { + content: '", "', + }, + }} + > + {token.symbol} + + + + + + + {isBalanceLoading ? ( + + ) : ( + intl.formatNumber(bal, { + minimumFractionDigits: 0, + maximumFractionDigits: 4, + }) + )} + + + {intl.formatNumber(balUsd, { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + })} + + + + ); +} diff --git a/libs/shared/providers/src/wagmi/index.ts b/libs/shared/providers/src/wagmi/index.ts index ba85f62be..cce378ad8 100644 --- a/libs/shared/providers/src/wagmi/index.ts +++ b/libs/shared/providers/src/wagmi/index.ts @@ -1,2 +1,3 @@ export * from './components/AddressLabel'; export * from './components/OpenAccountModalButton'; +export * from './components/TokenSelectModal'; diff --git a/libs/shared/utils/src/BigDecimal.ts b/libs/shared/utils/src/BigDecimal.ts index d4f54d374..4d6ef7d19 100644 --- a/libs/shared/utils/src/BigDecimal.ts +++ b/libs/shared/utils/src/BigDecimal.ts @@ -3,11 +3,11 @@ import { formatUnits, parseUnits } from 'viem'; const DEFAULT_DECIMALS = 18; export class BigDecimal { - exact: bigint; + value: bigint; decimals: number; constructor(num: bigint, decimals = DEFAULT_DECIMALS) { - this.exact = num ? (typeof num === 'bigint' ? num : BigInt(num)) : 0n; + this.value = num ? (typeof num === 'bigint' ? num : BigInt(num)) : 0n; this.decimals = decimals; } @@ -34,7 +34,7 @@ export class BigDecimal { } get string(): string { - return formatUnits(this.exact, this.decimals); + return formatUnits(this.value, this.decimals); } get simple(): number { @@ -48,7 +48,7 @@ export class BigDecimal { toJSON(): string { return JSON.stringify({ decimals: this.decimals, - exact: this.exact.toString(), + value: this.value.toString(), }); } @@ -67,42 +67,42 @@ export class BigDecimal { } add(other: BigDecimal) { - this.exact += other.exact; + this.value += other.value; } sub(other: BigDecimal) { - this.exact -= other.exact; + this.value -= other.value; } mul(other: BigDecimal) { - this.exact *= other.exact; + this.value *= other.value; } div(other: BigDecimal) { - this.exact /= other.exact; + this.value /= other.value; } eq(other: BigDecimal): boolean { - return this.exact === other.exact; + return this.value === other.value; } gt(other: BigDecimal): boolean { - return this.exact > other.exact; + return this.value > other.value; } gte(other: BigDecimal): boolean { - return this.exact >= other.exact; + return this.value >= other.value; } lt(other: BigDecimal): boolean { - return this.exact < other.exact; + return this.value < other.value; } lte(other: BigDecimal): boolean { - return this.exact <= other.exact; + return this.value <= other.value; } - eq0(): boolean { - return this.exact === 0n; + isZero(): boolean { + return this.value === 0n; } } diff --git a/package.json b/package.json index 490ef11ea..fff80f395 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@react-hookz/web": "^23.1.0", "@tanstack/react-query": "^4.32.6", "@tanstack/react-table": "^8.9.3", + "axios": "^1.4.0", "immer": "^10.0.2", "lodash": "^4.17.21", "react": "18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62cffcff8..d61b0f81b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ dependencies: '@tanstack/react-table': specifier: ^8.9.3 version: 8.9.3(react-dom@18.2.0)(react@18.2.0) + axios: + specifier: ^1.4.0 + version: 1.4.0 immer: specifier: ^10.0.2 version: 10.0.2 @@ -7577,7 +7580,6 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: true /atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} @@ -7613,7 +7615,6 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: true /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} @@ -8326,7 +8327,6 @@ packages: engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 - dev: true /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -8823,7 +8823,6 @@ packages: /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - dev: true /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} @@ -10098,7 +10097,6 @@ packages: peerDependenciesMeta: debug: optional: true - dev: true /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -10154,7 +10152,6 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: true /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -11876,14 +11873,12 @@ packages: /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - dev: true /mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} dependencies: mime-db: 1.52.0 - dev: true /mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} @@ -12781,7 +12776,6 @@ packages: /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - dev: true /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} diff --git a/tsconfig.base.json b/tsconfig.base.json index dc286fbad..77386f6e5 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -38,6 +38,9 @@ "@origin/shared/components": [ "libs/shared/components/src/index.ts" ], + "@origin/shared/contracts": [ + "libs/shared/contracts/src/index.ts" + ], "@origin/shared/data-access": [ "libs/shared/data-access/src/index.ts" ],