diff --git a/.eslintrc.json b/.eslintrc.json
index 9ca2e830c..35ea19bb3 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,10 +1,17 @@
{
"root": true,
"ignorePatterns": ["**/*"],
- "plugins": ["@nx"],
+ "plugins": [
+ "@nx",
+ "formatjs",
+ "unused-imports",
+ "simple-import-sort",
+ "prettier"
+ ],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+ "extends": ["plugin:react/recommended", "plugin:prettier/recommended"],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
@@ -18,7 +25,41 @@
}
]
}
- ]
+ ],
+ "react/react-in-jsx-scope": "off",
+ // Unused imports rules
+ "unused-imports/no-unused-imports": "error",
+ "unused-imports/no-unused-vars": "warn",
+
+ // Import ordering rules
+ "simple-import-sort/imports": [
+ "warn",
+ {
+ "groups": [
+ // Side effect imports
+ ["^\\u0000"],
+ // React Package(s) comes first as seperate group
+ ["^react(-dom(/client)?)?$"],
+ // All other imports
+ ["^@?\\w"],
+ ["^((?!\\u0000$)|/.*|$)"],
+ ["^\\."],
+ // Type imports: keep these last!
+ ["^@?\\w.*\\u0000$"],
+ ["^.*\\u0000$"],
+ ["^\\..*\\u0000$"]
+ ]
+ }
+ ],
+
+ // import types rules
+ "@typescript-eslint/consistent-type-imports": "error",
+
+ // FormatJS rules
+ "formatjs/enforce-default-message": ["error", "literal"],
+ "formatjs/no-id": "error",
+ "formatjs/no-multiple-whitespaces": "error",
+ "formatjs/no-offset": "error"
}
},
{
diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml
new file mode 100644
index 000000000..6b4e13ece
--- /dev/null
+++ b/.github/workflows/chromatic.yml
@@ -0,0 +1,25 @@
+name: ⚡ Chromatic Check
+
+on: push
+
+jobs:
+ publish-chromatic:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - uses: pnpm/action-setup@v2
+ with:
+ version: 8
+ run_install: true
+
+ - name: Build storybook
+ run: pnpm build-storybook
+
+ - name: Publish to Chromatic
+ uses: chromaui/action@v1
+ with:
+ projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
+ storybookBuildDir: 'dist/storybook/shared-storybook'
diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml
deleted file mode 100644
index 70815d899..000000000
--- a/.github/workflows/storybook.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-name: Build and Deploy Storybook
-on:
- push
-jobs:
- deploy-storybook:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout 🛎️
- uses: actions/checkout@v3
- - name: Install pnpm
- uses: pnpm/action-setup@v2
- with:
- version: latest
- - name: Init 🖥
- uses: actions/setup-node@v3
- with:
- node-version: 18
- cache: 'pnpm'
- - name: Install dependencies
- run: pnpm install
- - name: Build Storybook
- run: pnpm build-storybook
- - name: Deploy 🚀
- uses: peaceiris/actions-gh-pages@v3
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- publish_dir: ./dist/storybook/shared-storybook
diff --git a/.prettierrc b/.prettierrc
index 544138be4..ec1ea7556 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,3 +1,5 @@
{
- "singleQuote": true
+ "singleQuote": true,
+ "trailingComma": "all",
+ "tabWidth": 2
}
diff --git a/apps/oeth/index.html b/apps/oeth/index.html
index d55247934..aaf7aa420 100644
--- a/apps/oeth/index.html
+++ b/apps/oeth/index.html
@@ -1,4 +1,4 @@
-
+
@@ -7,7 +7,14 @@
-
+
+
+
+
+
diff --git a/apps/oeth/src/components/App.tsx b/apps/oeth/src/components/App.tsx
new file mode 100644
index 000000000..45621181b
--- /dev/null
+++ b/apps/oeth/src/components/App.tsx
@@ -0,0 +1,17 @@
+import { HistoryView, SwapView, WrapView } from '@origin/defi/oeth';
+import { Route, Routes } from 'react-router-dom';
+
+import { Layout } from './Layout';
+
+export function App() {
+ return (
+
+ }>
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
diff --git a/apps/oeth/src/components/Layout.tsx b/apps/oeth/src/components/Layout.tsx
new file mode 100644
index 000000000..b404853c0
--- /dev/null
+++ b/apps/oeth/src/components/Layout.tsx
@@ -0,0 +1,39 @@
+import { Container } from '@mui/material';
+import { TopNav } from '@origin/shared/components';
+import { useIntl } from 'react-intl';
+import { Outlet } from 'react-router-dom';
+
+export function Layout() {
+ const intl = useIntl();
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/apps/oeth/src/components/index.tsx b/apps/oeth/src/components/index.tsx
new file mode 100644
index 000000000..9342f378e
--- /dev/null
+++ b/apps/oeth/src/components/index.tsx
@@ -0,0 +1 @@
+export * from './App';
\ No newline at end of file
diff --git a/apps/oeth/src/index.tsx b/apps/oeth/src/index.tsx
deleted file mode 100644
index 0fb71c82c..000000000
--- a/apps/oeth/src/index.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-// placeholder component until we actually start migration
-export function OUSDRoot() {
- return ousd
-}
\ No newline at end of file
diff --git a/apps/oeth/src/main.tsx b/apps/oeth/src/main.tsx
index 1898e9b7f..5e65c7a48 100644
--- a/apps/oeth/src/main.tsx
+++ b/apps/oeth/src/main.tsx
@@ -1,7 +1,6 @@
import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
-import { OethRoot } from './views/root';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from '@origin/shared/data-access';
import { theme } from '@origin/shared/theme';
@@ -11,6 +10,7 @@ import {
} from '@mui/material';
import { IntlProvider } from 'react-intl';
import { en } from './lang';
+import { App } from './components';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
@@ -22,7 +22,7 @@ root.render(
-
+
diff --git a/apps/oeth/src/views/root/index.tsx b/apps/oeth/src/views/root/index.tsx
deleted file mode 100644
index 517bdaba2..000000000
--- a/apps/oeth/src/views/root/index.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-// placeholder component until we actually start migration
-import { DefiOeth } from '@origin/defi/oeth';
-import { useIntl } from 'react-intl';
-
-export function OethRoot() {
- const intl = useIntl();
- return (
- <>
- {intl.formatMessage({ defaultMessage: 'test OEth' })}
-
- >
- );
-}
diff --git a/apps/oeth/tsconfig.app.json b/apps/oeth/tsconfig.app.json
index e4322b551..eb2d36377 100644
--- a/apps/oeth/tsconfig.app.json
+++ b/apps/oeth/tsconfig.app.json
@@ -18,5 +18,5 @@
"src/**/*.spec.jsx",
"src/**/*.test.jsx"
],
- "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
+ "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx", "../../libs/defi/oeth/src/components/TopNav.tsx"]
}
diff --git a/apps/oeth/vite.config.ts b/apps/oeth/vite.config.ts
index 39f6c9cba..89ae276f5 100644
--- a/apps/oeth/vite.config.ts
+++ b/apps/oeth/vite.config.ts
@@ -1,6 +1,10 @@
///
-import { defineConfig } from 'vite';
+///
import react from '@vitejs/plugin-react';
+import path from 'path';
+import { defineConfig } from 'vite';
+import { viteStaticCopy } from 'vite-plugin-static-copy';
+import svgr from 'vite-plugin-svgr';
import viteTsConfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
@@ -17,6 +21,7 @@ export default defineConfig({
},
plugins: [
+ svgr(),
react({
babel: {
plugins: [
@@ -33,6 +38,14 @@ export default defineConfig({
viteTsConfigPaths({
root: '../../',
}),
+ viteStaticCopy({
+ targets: [
+ {
+ src: path.resolve(__dirname, '../../libs/shared/assets/files/*'),
+ dest: './images',
+ },
+ ],
+ }),
],
// Uncomment this if you are using workers.
diff --git a/apps/ousd/index.html b/apps/ousd/index.html
index 53b2040b3..01bc9d957 100644
--- a/apps/ousd/index.html
+++ b/apps/ousd/index.html
@@ -1,4 +1,4 @@
-
+
@@ -7,7 +7,6 @@
-
diff --git a/libs/defi/oeth/src/components/History/HistoryButton.stories.tsx b/libs/defi/oeth/src/components/History/HistoryButton.stories.tsx
new file mode 100644
index 000000000..6943e2112
--- /dev/null
+++ b/libs/defi/oeth/src/components/History/HistoryButton.stories.tsx
@@ -0,0 +1,30 @@
+import { HistoryFilterButton } from './HistoryButton';
+
+import type { Meta, StoryObj } from '@storybook/react';
+
+const meta: Meta = {
+ component: HistoryFilterButton,
+ title: 'History/History filter button',
+ args: {
+ circle: false,
+ children: 'Test',
+ },
+ render: (args) => ,
+};
+
+export default meta;
+
+export const Primary: StoryObj = {};
+
+export const WithCircle: StoryObj = {
+ args: {
+ circle: true,
+ },
+};
+
+export const Selected: StoryObj = {
+ args: {
+ circle: true,
+ selected: true,
+ },
+};
diff --git a/libs/defi/oeth/src/components/History/HistoryButton.tsx b/libs/defi/oeth/src/components/History/HistoryButton.tsx
new file mode 100644
index 000000000..9c5f4b672
--- /dev/null
+++ b/libs/defi/oeth/src/components/History/HistoryButton.tsx
@@ -0,0 +1,69 @@
+import { alpha, Box, Button } from '@mui/material';
+
+import type { ButtonProps, SxProps } from '@mui/material';
+import type { Theme } from '@origin/shared/theme';
+
+interface Props extends ButtonProps {
+ circle?: boolean;
+ selected?: boolean;
+}
+
+export function HistoryFilterButton({
+ children,
+ circle = false,
+ onClick,
+ selected = false,
+ sx,
+ ...rest
+}: Props) {
+ return (
+
+ );
+}
+
+function Circle({ sx }: { sx: SxProps }) {
+ return (
+ theme.palette.background.default,
+ height: '0.5rem',
+ width: '0.5rem',
+ borderRadius: '100%',
+ ...sx,
+ }}
+ />
+ );
+}
diff --git a/libs/defi/oeth/src/components/History/HistoryCard.tsx b/libs/defi/oeth/src/components/History/HistoryCard.tsx
new file mode 100644
index 000000000..c13d944c7
--- /dev/null
+++ b/libs/defi/oeth/src/components/History/HistoryCard.tsx
@@ -0,0 +1,83 @@
+import { useState } from 'react';
+
+import { Box, Button, Stack, Typography } from '@mui/material';
+import { Card } from '@origin/shared/components';
+import { useIntl } from 'react-intl';
+
+import { HistoryFilterButton } from './HistoryButton';
+import { HistoryTable } from './HistoryTable';
+
+import type { ColumnFilter } from '@tanstack/react-table';
+
+export function HistoryCard() {
+ const [isConnected, setConnectionState] = useState(false);
+ const intl = useIntl();
+ const [filter, setFilter] = useState({
+ id: 'type',
+ value: [],
+ });
+
+ function filterRows(value: string) {
+ setFilter((prev) => {
+ if ((prev.value as string[]).includes(value)) {
+ return {
+ ...prev,
+ value: [...(prev.value as string[]).filter((val) => val !== value)],
+ };
+ } else {
+ return {
+ ...prev,
+ value: [...(prev.value as string[]), value],
+ };
+ }
+ });
+ }
+ return (
+
+ History
+
+ {[
+ intl.formatMessage({ defaultMessage: 'Received' }),
+ intl.formatMessage({ defaultMessage: 'Sent' }),
+ intl.formatMessage({ defaultMessage: 'Swap' }),
+ intl.formatMessage({ defaultMessage: 'Yield' }),
+ ].map((label) => (
+ filterRows(label.toLowerCase())}
+ >
+ {label}
+
+ ))}
+
+
+ {intl.formatMessage({ defaultMessage: 'Export CSV' })}
+
+
+ }
+ >
+ {isConnected ? (
+
+ ) : (
+
+
+ {intl.formatMessage({
+ defaultMessage: 'Connect your wallet to see your history',
+ })}
+
+
+
+ )}
+
+ );
+}
diff --git a/libs/defi/oeth/src/components/History/HistoryTable.stories.tsx b/libs/defi/oeth/src/components/History/HistoryTable.stories.tsx
new file mode 100644
index 000000000..dc19da82b
--- /dev/null
+++ b/libs/defi/oeth/src/components/History/HistoryTable.stories.tsx
@@ -0,0 +1,116 @@
+import { useState } from 'react';
+
+import { faker } from '@faker-js/faker';
+import { Container, Stack, Typography } from '@mui/material';
+import { within } from '@storybook/testing-library';
+
+import { HistoryFilterButton } from './HistoryButton';
+import { HistoryTable } from './HistoryTable';
+
+import type { Meta, StoryObj } from '@storybook/react';
+import type { ColumnFilter } from '@tanstack/react-table';
+
+import type { HistoryRow } from './HistoryTable';
+
+const rows = faker.helpers.multiple(
+ () => ({
+ date: faker.date.recent({ days: 35 }),
+ balance: faker.number.float({
+ min: 10000,
+ max: 10000000000,
+ precision: 10,
+ }),
+ change: faker.number.float({ min: -10, max: 25 }),
+ type: faker.helpers.arrayElement(['swap', 'received', 'sent', 'yield']),
+ link: faker.internet.url(),
+ }),
+ { count: 150 },
+);
+
+const WithFilters = () => {
+ const [filter, setFilter] = useState({ id: 'type', value: [] });
+ return (
+
+
+ History
+
+ {['Received', 'Sent', 'Swap', 'Yield'].map((label) => (
+
+ setFilter((prev) => {
+ const filter = label.toLowerCase();
+ if ((prev.value as string[]).includes(filter)) {
+ return {
+ ...prev,
+ value: [
+ ...(prev.value as string[]).filter(
+ (val) => val !== filter,
+ ),
+ ],
+ };
+ } else {
+ return {
+ ...prev,
+ value: [...(prev.value as string[]), filter],
+ };
+ }
+ })
+ }
+ circle
+ >
+ {label}
+
+ ))}
+
+
+
+
+ );
+};
+
+const meta: Meta = {
+ component: HistoryTable,
+ title: 'History/History table',
+ args: {
+ isLoading: false,
+ rows,
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+export default meta;
+
+export const Primary = {
+ args: {},
+};
+
+export const Filter: StoryObj = {
+ render: () => (
+
+
+
+ ),
+};
+
+export const SelectedFilters: StoryObj = {
+ render: () => (
+
+
+
+ ),
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await canvas.getByText('Received').click();
+ await canvas.getByText('Yield').click();
+ },
+};
diff --git a/libs/defi/oeth/src/components/History/HistoryTable.tsx b/libs/defi/oeth/src/components/History/HistoryTable.tsx
new file mode 100644
index 000000000..2401bb7dc
--- /dev/null
+++ b/libs/defi/oeth/src/components/History/HistoryTable.tsx
@@ -0,0 +1,166 @@
+import { useEffect, useMemo, useState } from 'react';
+
+import {
+ Box,
+ Pagination,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableRow,
+} from '@mui/material';
+import { LinkIcon, quantityFormat } from '@origin/shared/components';
+import {
+ createColumnHelper,
+ flexRender,
+ getCoreRowModel,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ useReactTable,
+} from '@tanstack/react-table';
+import { useIntl } from 'react-intl';
+
+import type { ColumnFilter, ColumnFiltersState } from '@tanstack/react-table';
+
+type Filter = 'swap' | 'yield' | 'received' | 'sent';
+
+export interface HistoryRow {
+ date: Date;
+ type: Filter;
+ change: number;
+ balance: number;
+ link: string;
+}
+
+interface Props {
+ rows: HistoryRow[];
+ isLoading: boolean;
+ filter: ColumnFilter;
+}
+
+const columnHelper = createColumnHelper();
+
+export function HistoryTable({ rows, filter }: Props) {
+ const intl = useIntl();
+ const [columnFilters, setColumnFilters] = useState([]);
+ const columns = useMemo(
+ () => [
+ columnHelper.accessor('date', {
+ cell: (info) => intl.formatDate(info.getValue()),
+ header: intl.formatMessage({ defaultMessage: 'Date' }),
+ }),
+ columnHelper.accessor('type', {
+ id: 'type',
+ cell: (info) => info.getValue(),
+ header: intl.formatMessage({ defaultMessage: 'Type' }),
+ enableColumnFilter: true,
+ filterFn: (row, _, value) => {
+ if (!value.value.length) return true;
+ return value.value.includes(row.original.type);
+ },
+ }),
+ columnHelper.accessor('change', {
+ cell: (info) => intl.formatNumber(info.getValue(), quantityFormat),
+ header: intl.formatMessage({ defaultMessage: 'Change' }),
+ }),
+ columnHelper.accessor('balance', {
+ cell: (info) => (
+
+
+ {intl.formatNumber(info.getValue(), quantityFormat)}
+
+
+
+
+ ),
+ header: intl.formatMessage({ defaultMessage: 'OETH Balance' }),
+ }),
+ ],
+ [intl],
+ );
+
+ const table = useReactTable({
+ data: rows,
+ columns,
+ state: {
+ pagination: {
+ pageSize: 20,
+ pageIndex: 0,
+ },
+ columnFilters,
+ },
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ onColumnFiltersChange: setColumnFilters,
+ // add when we do server side pagination
+ // manualPagination: true,
+ pageCount: rows.length / 3,
+ // add when we do server side pagination
+ // onPaginationChange: setPagination
+ });
+
+ useEffect(() => {
+ table.getColumn('type')?.setFilterValue(filter);
+ }, [filter, table]);
+ return (
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ ))}
+
+
+ table.setPageIndex(page)}
+ />
+
+ );
+}
diff --git a/libs/defi/oeth/src/components/History/index.tsx b/libs/defi/oeth/src/components/History/index.tsx
new file mode 100644
index 000000000..27e462d84
--- /dev/null
+++ b/libs/defi/oeth/src/components/History/index.tsx
@@ -0,0 +1 @@
+export * from './HistoryCard';
diff --git a/libs/defi/oeth/src/components/Swap/BestRoutes.tsx b/libs/defi/oeth/src/components/Swap/BestRoutes.tsx
new file mode 100644
index 000000000..2484e3889
--- /dev/null
+++ b/libs/defi/oeth/src/components/Swap/BestRoutes.tsx
@@ -0,0 +1,33 @@
+import { Box } from '@mui/material';
+
+import { SwapRouteCard } from './SwapRouteCard';
+
+import type { Route } from './SwapRoute';
+
+interface Props {
+ routes: Route[];
+ selected: number;
+ onSelect: (index: number) => void;
+}
+
+export function BestRoutes({ routes, selected, onSelect }: Props) {
+ return (
+
+ {routes.slice(0, 2).map((route, index) => (
+ onSelect(index)}
+ route={route}
+ />
+ ))}
+
+ );
+}
diff --git a/libs/defi/oeth/src/components/Swap/GasPopover.stories.tsx b/libs/defi/oeth/src/components/Swap/GasPopover.stories.tsx
new file mode 100644
index 000000000..13b9340f1
--- /dev/null
+++ b/libs/defi/oeth/src/components/Swap/GasPopover.stories.tsx
@@ -0,0 +1,36 @@
+import { screen, userEvent, within } from '@storybook/testing-library';
+
+import { GasPopover } from './GasPopover';
+
+import type { Meta, StoryObj } from '@storybook/react';
+
+const meta: Meta = {
+ component: GasPopover,
+ title: 'Swap/GasPopover',
+ args: {
+ gasPrice: 21,
+ onPriceToleranceChange: (val) => null,
+ },
+};
+
+export default meta;
+
+export const Default: StoryObj = {};
+
+export const Expanded: StoryObj = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await canvas.getByTestId('gas-popover-button').click();
+ },
+};
+
+export const HighTolerance: StoryObj = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ await canvas.getByTestId('gas-popover-button').click();
+ const input = await screen.findByLabelText('Price tolerance');
+ await userEvent.clear(input);
+ await userEvent.type(input, '1.2');
+ },
+};
diff --git a/libs/defi/oeth/src/components/Swap/GasPopover.tsx b/libs/defi/oeth/src/components/Swap/GasPopover.tsx
new file mode 100644
index 000000000..6df2f071f
--- /dev/null
+++ b/libs/defi/oeth/src/components/Swap/GasPopover.tsx
@@ -0,0 +1,199 @@
+import { useEffect, useState } from 'react';
+
+import {
+ alpha,
+ Box,
+ Button,
+ debounce,
+ FormControl,
+ FormHelperText,
+ IconButton,
+ InputAdornment,
+ InputBase,
+ InputLabel,
+ Popover,
+ Stack,
+ useTheme,
+} from '@mui/material';
+import { isNumber } from 'lodash';
+import { useIntl } from 'react-intl';
+
+import type { Theme } from '@mui/material';
+
+const defaultPriceTolerance = 0.01;
+
+const gridStyles = {
+ display: 'grid',
+ gridTemplateColumns: (theme: Theme) => `1.5fr 1fr`,
+ gap: 1,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+};
+
+interface Props {
+ gasPrice: number;
+ onPriceToleranceChange: (value: number) => void;
+}
+
+export function GasPopover({ gasPrice, onPriceToleranceChange }: Props) {
+ const theme = useTheme();
+ const intl = useIntl();
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [priceTolerance, setPriceTolerance] = useState(defaultPriceTolerance);
+
+ useEffect(() => {
+ onPriceToleranceChange(priceTolerance);
+ }, [priceTolerance, onPriceToleranceChange]);
+ return (
+ <>
+ setAnchorEl(e.currentTarget)}
+ data-testid="gas-popover-button"
+ >
+
+
+ setAnchorEl(null)}
+ anchorOrigin={{
+ vertical: 'bottom',
+ horizontal: 'center',
+ }}
+ transformOrigin={{
+ vertical: 'top',
+ horizontal: 'right',
+ }}
+ sx={{
+ '& .MuiPaper-root.MuiPopover-paper': {
+ padding: 3,
+ boxSizing: 'border-box',
+ maxWidth: {
+ xs: '90vw',
+ md: '16.5rem',
+ },
+ width: '100%',
+ border: '1px solid',
+ borderColor: 'grey.700',
+ [theme.breakpoints.down('md')]: {
+ left: '0 !important',
+ right: 0,
+ marginInline: 'auto',
+ },
+ },
+ }}
+ >
+
+
+
+ {intl.formatMessage({ defaultMessage: 'Price tolerance' })}
+
+
+ {
+ if (isNumber(parseFloat(e.target.value))) {
+ setPriceTolerance(e.target.value);
+ }
+ }, 300)}
+ endAdornment={
+
+ {intl.formatMessage({ defaultMessage: '%' })}
+
+ }
+ />
+
+
+
+ {priceTolerance > 1 ? (
+ theme.typography.pxToRem(12),
+ color: (theme) => theme.palette.warning.main,
+ fontWeight: 400,
+ fontStyle: 'normal',
+ }}
+ >
+ {intl.formatMessage({
+ defaultMessage: 'Your transaction may be frontrun',
+ })}
+
+ ) : null}
+
+
+
+ {intl.formatMessage({ defaultMessage: 'Gas Price' })}
+
+
+
+ {intl.formatMessage({ defaultMessage: 'GWEI' })}
+
+ }
+ />
+
+
+
+
+ >
+ );
+}
diff --git a/libs/defi/oeth/src/components/Swap/RedeemMix.stories.tsx b/libs/defi/oeth/src/components/Swap/RedeemMix.stories.tsx
new file mode 100644
index 000000000..2ac0534d4
--- /dev/null
+++ b/libs/defi/oeth/src/components/Swap/RedeemMix.stories.tsx
@@ -0,0 +1,94 @@
+import { Box } from '@mui/material';
+
+import { RedeemMix } from './RedeemMix';
+
+import type { Meta, StoryObj } from '@storybook/react';
+
+const meta: Meta = {
+ component: RedeemMix,
+ title: 'Swap/Redeem Mix',
+ args: {
+ selected: 4,
+ index: 2,
+ route: {
+ value: 282967.55,
+ quantity: 149.55,
+ name: 'Redeem for mix via OETH vault',
+ waitTime: '1 min',
+ transactionCost: 135.83,
+ rate: 0.995,
+ type: 'redeem',
+ tokenAbbreviation: '',
+ icon: [
+ 'https://app.oeth.com/images/currency/weth-icon-small.png',
+ 'https://app.oeth.com/images/currency/reth-icon-small.png',
+ 'https://app.oeth.com/images/currency/steth-icon-small.svg',
+ 'https://app.oeth.com/images/currency/frxeth-icon-small.svg',
+ ],
+ },
+ composition: [
+ {
+ name: 'wETH',
+ quantity: 117.0437,
+ value: 238378.36,
+ icon: 'https://app.oeth.com/images/currency/weth-icon-small.png',
+ },
+ {
+ name: 'frxETH',
+ quantity: 13.1245,
+ value: 17643.75,
+ icon: 'https://app.oeth.com/images/currency/frxeth-icon-small.svg',
+ },
+ {
+ name: 'rETH',
+ quantity: 13.1144,
+ value: 13138.96,
+ icon: 'https://app.oeth.com/images/currency/reth-icon-small.png',
+ },
+ {
+ name: 'sETH',
+ quantity: 4.8354,
+ value: 13138.96,
+ icon: 'https://app.oeth.com/images/currency/steth-icon-small.svg',
+ },
+ ],
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+export default meta;
+
+export const Default: StoryObj = {};
+
+export const Hover: StoryObj = {
+ parameters: {
+ pseudo: {
+ hover: true,
+ },
+ },
+};
+export const Selected: StoryObj = {
+ args: {
+ index: 4,
+ },
+};
+
+export const SmallMobile: StoryObj = {
+ parameters: {
+ viewport: {
+ defaultViewport: 'mobile1',
+ },
+ },
+};
+
+export const LargeMobile: StoryObj = {
+ parameters: {
+ viewport: {
+ defaultViewport: 'mobile2',
+ },
+ },
+};
diff --git a/libs/defi/oeth/src/components/Swap/RedeemMix.tsx b/libs/defi/oeth/src/components/Swap/RedeemMix.tsx
new file mode 100644
index 000000000..eaa64da7e
--- /dev/null
+++ b/libs/defi/oeth/src/components/Swap/RedeemMix.tsx
@@ -0,0 +1,191 @@
+import { Stack, Card, alpha, Box, Typography, Divider } from '@mui/material';
+import { Mix } from 'libs/shared/components/src/Mix';
+import { Redeem, Route } from './SwapRoute';
+import { useIntl } from 'react-intl';
+import { currencyFormat, quantityFormat } from '@origin/shared/components';
+import { SwapInfo } from './SwapInfo';
+import { Icon } from 'libs/shared/components/src/top-nav/Icon';
+
+interface Props {
+ onSelect: (index: number) => void;
+ index: number;
+ selected: number;
+ route: Route;
+ // no idea if this prop makes sense 🤪 -> prob it needs to be refactored
+ composition: {
+ icon: string;
+ name: string;
+ quantity: number;
+ value: number;
+ }[];
+}
+
+export function RedeemMix({
+ route,
+ index,
+ selected,
+ onSelect,
+ composition,
+}: Props) {
+ const intl = useIntl();
+ return (
+ `linear-gradient(var(--mui-palette-grey-800), var(--mui-palette-grey-800)) padding-box,
+ linear-gradient(90deg, ${alpha(
+ theme.palette.primary.main,
+ 0.4,
+ )} 0%, ${alpha(
+ theme.palette.primary.dark,
+ 0.4,
+ )} 100%) border-box;`,
+ },
+ }),
+ }}
+ role="button"
+ onClick={() => onSelect(index)}
+ >
+
+
+
+
+
+ {intl.formatNumber(route.quantity, quantityFormat)}
+ theme.typography.pxToRem(12),
+ fontWeight: 400,
+ fontStyle: 'normal',
+ lineHeight: (theme) => theme.typography.pxToRem(20),
+ }}
+ >
+ ({intl.formatNumber(route.value, currencyFormat)})
+
+
+
+ {route.name}
+
+
+
+
+
+ {intl.formatMessage({ defaultMessage: 'Gas:' })}
+
+ ~{intl.formatNumber(route.transactionCost, currencyFormat)}
+
+
+
+ {intl.formatMessage({ defaultMessage: 'Waiting time:' })}
+
+ {/* TODO better handling of this duration */}~
+ {(route as Redeem).waitTime}
+
+
+
+
+
+
+ {intl.formatMessage({ defaultMessage: 'Rate' })}
+
+ 1:{route.rate}
+
+
+
+
+
+
+ {composition.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+ {intl.formatNumber(item.quantity, quantityFormat)}
+
+
+ {intl.formatNumber(item.value, currencyFormat)}
+
+
+
+ ))}
+
+ );
+}
diff --git a/libs/defi/oeth/src/components/Swap/Swap.stories.tsx b/libs/defi/oeth/src/components/Swap/Swap.stories.tsx
new file mode 100644
index 000000000..3ab1e1c72
--- /dev/null
+++ b/libs/defi/oeth/src/components/Swap/Swap.stories.tsx
@@ -0,0 +1,25 @@
+import { Container } from '@mui/material';
+
+import { Swap } from './Swap';
+
+import type { Meta, StoryObj } from '@storybook/react';
+
+const meta: Meta = {
+ component: Swap,
+ title: 'Swap',
+ args: {
+ isLoading: false,
+ routes: [],
+ },
+ render: () => (
+
+
+
+ ),
+};
+
+export default meta;
+
+export const SwapComponent: StoryObj = {
+ name: 'Swap Component',
+};
diff --git a/libs/defi/oeth/src/components/Swap/Swap.tsx b/libs/defi/oeth/src/components/Swap/Swap.tsx
new file mode 100644
index 000000000..6850604fb
--- /dev/null
+++ b/libs/defi/oeth/src/components/Swap/Swap.tsx
@@ -0,0 +1,172 @@
+import { useState } from 'react';
+
+import { Stack } from '@mui/material';
+import {
+ ActionButton,
+ DropdownIcon,
+ SwapCard,
+ TokenListModal,
+} from '@origin/shared/components';
+import random from 'lodash/random';
+import { useIntl } from 'react-intl';
+
+import { GasPopover } from './GasPopover';
+import { SwapRoute } from './SwapRoute';
+
+import type { Option } from '@origin/shared/components';
+
+export function Swap() {
+ const intl = useIntl();
+
+ const [isSelectionModalOpen, setSelectionModal] = useState(false);
+ const [values, setValues] = useState<{
+ baseToken: Omit