diff --git a/web-portal/frontend/components/apps/appRuleModal.tsx b/web-portal/frontend/components/apps/appRuleModal.tsx index fc1ebefb..b3f81f5c 100644 --- a/web-portal/frontend/components/apps/appRuleModal.tsx +++ b/web-portal/frontend/components/apps/appRuleModal.tsx @@ -10,6 +10,7 @@ import ApprovedChainForm from "./forms/approvedChainsForm"; import AllowedUserAgentsForm from "./forms/allowedUserAgentsForm"; import AllowedOriginsForm from "./forms/allowedOriginsForm"; import _ from "lodash"; +import RateLimitForm from "./forms/rateLimitForm"; export default function CreateAppRuleModal() { const searchParams = useSearchParams(); @@ -29,6 +30,7 @@ export default function CreateAppRuleModal() { {shouldOpen === "secret-key" && } {shouldOpen === "approved-chains" && } {shouldOpen === "allowed-user-agents" && } + {shouldOpen === "rate-limit" && } ); } diff --git a/web-portal/frontend/components/apps/appRules.tsx b/web-portal/frontend/components/apps/appRules.tsx index e5347065..13fe1273 100644 --- a/web-portal/frontend/components/apps/appRules.tsx +++ b/web-portal/frontend/components/apps/appRules.tsx @@ -30,7 +30,10 @@ const AppRules: React.FC<{ rule: Partial }> = ({ rule }) => { color: "white", }} > - {item.value} + {item.value + ?.replace("P1D", "Daily") + .replace("P1W", "Weekly") + .replace("P1M", "Monthly")} )); diff --git a/web-portal/frontend/components/apps/forms/allowedOriginsForm.tsx b/web-portal/frontend/components/apps/forms/allowedOriginsForm.tsx index 0a89dbfa..8dc9e8f9 100644 --- a/web-portal/frontend/components/apps/forms/allowedOriginsForm.tsx +++ b/web-portal/frontend/components/apps/forms/allowedOriginsForm.tsx @@ -3,12 +3,7 @@ import _ from "lodash"; import { useForm, matches } from "@mantine/form"; import { IconPlus } from "@tabler/icons-react"; import { useUpdateRuleMutation } from "@frontend/utils/hooks"; -import { - useParams, - usePathname, - useRouter, - useSearchParams, -} from "next/navigation"; +import { useParams, useSearchParams } from "next/navigation"; import { useAtom, useAtomValue } from "jotai"; import { existingRuleValuesAtom, ruleTypesAtom } from "@frontend/utils/atoms"; import { IRuleType } from "@frontend/utils/types"; @@ -16,8 +11,6 @@ import { IRuleType } from "@frontend/utils/types"; export default function AllowedOriginsForm() { const appId = useParams()?.app as string; const searchParams = useSearchParams(); - const router = useRouter(); - const path = usePathname(); const rule = searchParams?.get("rule") as string; const { mutateAsync, isPending, isSuccess } = useUpdateRuleMutation( appId, @@ -38,6 +31,9 @@ export default function AllowedOriginsForm() { "Enter a valid url that starts with http or https", ), }, + initialValues: { + url: "", + }, }); const handleValueRemove = (val: string) => @@ -77,7 +73,7 @@ export default function AllowedOriginsForm() { onClick={() => { if (formValidation().hasErrors) return; setValue((current: any) => [form.values.url, ...current]), - form.setFieldValue("url", ""); + form.reset(); }} > diff --git a/web-portal/frontend/components/apps/forms/allowedUserAgentsForm.tsx b/web-portal/frontend/components/apps/forms/allowedUserAgentsForm.tsx index 7e6dd49d..5336875f 100644 --- a/web-portal/frontend/components/apps/forms/allowedUserAgentsForm.tsx +++ b/web-portal/frontend/components/apps/forms/allowedUserAgentsForm.tsx @@ -19,7 +19,7 @@ export default function AllowedUserAgentsForm() { ); const [value, setValue] = useAtom(existingRuleValuesAtom); const ruleTypes = useAtomValue(ruleTypesAtom); - const router = useRouter(); + const validationRule = _.get( _.find(ruleTypes, (r: IRuleType) => r.name === rule), "validationValue", @@ -29,6 +29,9 @@ export default function AllowedUserAgentsForm() { validate: { userAgent: matches(ruleRegex, "Enter a valid url user agent string"), }, + initialValues: { + userAgent: "", + }, }); const handleValueRemove = (val: string) => @@ -67,7 +70,7 @@ export default function AllowedUserAgentsForm() { onClick={() => { if (formValidation().hasErrors) return; setValue((current: any) => [form.values.userAgent, ...current]), - form.setFieldValue("userAgent", ""); + form.reset(); }} > diff --git a/web-portal/frontend/components/apps/forms/approvedChainsForm.tsx b/web-portal/frontend/components/apps/forms/approvedChainsForm.tsx index 7b12e076..a93f3af9 100644 --- a/web-portal/frontend/components/apps/forms/approvedChainsForm.tsx +++ b/web-portal/frontend/components/apps/forms/approvedChainsForm.tsx @@ -8,13 +8,12 @@ import { IEndpoint } from "@frontend/utils/types"; import { SearchableMultiSelect } from "@frontend/components/common/SearchableMultiSelect"; import { useUpdateRuleMutation } from "@frontend/utils/hooks"; -import { useSearchParams, useParams, useRouter } from "next/navigation"; +import { useSearchParams, useParams } from "next/navigation"; export default function ApprovedChainForm() { const list = useAtomValue(endpointsAtom) as IEndpoint[]; const items = _.map(list, "name"); - const router = useRouter(); const appId = useParams()?.app as string; const searchParams = useSearchParams(); const rule = searchParams?.get("rule") as string; diff --git a/web-portal/frontend/components/apps/forms/rateLimitForm.tsx b/web-portal/frontend/components/apps/forms/rateLimitForm.tsx new file mode 100644 index 00000000..15dee657 --- /dev/null +++ b/web-portal/frontend/components/apps/forms/rateLimitForm.tsx @@ -0,0 +1,152 @@ +import _ from "lodash"; +import { useAtom, useAtomValue } from "jotai"; +import { existingRuleValuesAtom, ruleTypesAtom } from "@frontend/utils/atoms"; +import { useForm } from "@mantine/form"; +import { + Button, + Flex, + Pill, + Stack, + Text, + Select, + NumberInput, +} from "@mantine/core"; +import { IconPlus } from "@tabler/icons-react"; +import { IRuleType } from "@frontend/utils/types"; + +import { useUpdateRuleMutation } from "@frontend/utils/hooks"; +import { useSearchParams, useParams } from "next/navigation"; + +const periodOptions = [ + { label: "Weekly", value: "P1W" }, + { label: "Monthly", value: "P1M" }, + { label: "Daily", value: "P1D" }, +]; + +export default function RateLimitForm() { + const appId = useParams()?.app as string; + const searchParams = useSearchParams(); + const rule = searchParams?.get("rule") as string; + const { mutateAsync, isPending, isSuccess } = useUpdateRuleMutation( + appId, + rule, + ); + + const [value, setValue] = useAtom(existingRuleValuesAtom); + const ruleTypes = useAtomValue(ruleTypesAtom); + + const validationRule = _.get( + _.find(ruleTypes, (r: IRuleType) => r.name === rule), + "validationValue", + ); + const ruleRegex = new RegExp(validationRule as string); + const form = useForm({ + validate: { + requests: (val) => { + return _.isInteger(Number(val)) && Number(val) > 0 + ? null + : "Please enter a valid number"; + }, + period: (val: string) => { + const isValid = periodOptions.some((option) => { + return option.value === val; + }); + + const alreadyExists = value.some((v) => v.includes(val)); + if (alreadyExists) { + return "Period already exists"; + } + return isValid ? null : "Please select a valid period"; + }, + }, + initialValues: { + requests: "", + period: "", + }, + }); + + const handleValueRemove = (val: string) => + setValue((current) => current.filter((v) => v !== val)); + + const formValidation = () => form.validate(); + + const values = value?.map((val) => ( + handleValueRemove(val)} + size="lg" + m={2} + bg="blue" + style={{ + color: "white", + }} + > + {val + ?.replace("P1D", "Daily") + .replace("P1W", "Weekly") + .replace("P1M", "Monthly")} + + )); + + const handleSubmit = () => { + const isValid = value.map((v) => (ruleRegex.test(v) ? true : false)); + isValid.includes(false) + ? form.setErrors({ requests: "Invalid value" }) + : mutateAsync(value); + }; + + return ( + + + + { + const selectedChain = _.find( + chains, + (c) => _.toLower(c.name) === _.toLower(val as string), + ); + setSelectedChainId(selectedChain?.id as number); + }} + label="Select Network" + /> + + + + setSwapValue(parseFloat(e.target.value))} + styles={{ + ...commonStyles, + input: { ...commonStyles.input, fill: "#fff" }, + }} + error={ + swapValue > Number(_.get(tokenBalance, "formatted")) + ? "Not enough balance" + : undefined + } + /> + + + + + + ${(Number(swapValue || 0) * 0.01).toFixed(6)} + + + {Number(_.get(tokenBalance, "formatted") ?? 0).toFixed(6)} + + + setSwapValue(Number(_.get(tokenBalance, "formatted"))) + } + > + max + + + + + + + + + + + + ); +} diff --git a/web-portal/frontend/next.config.js b/web-portal/frontend/next.config.js index e953c480..1cfc9ccc 100644 --- a/web-portal/frontend/next.config.js +++ b/web-portal/frontend/next.config.js @@ -12,6 +12,10 @@ const nextConfig = { hostname: "api.web3modal.com", port: "", }, + { + protocol: "https", + hostname: "ethereum-optimism.github.io", + }, ], }, async rewrites() { diff --git a/web-portal/frontend/pages/swap/index.tsx b/web-portal/frontend/pages/swap/index.tsx new file mode 100644 index 00000000..36c11147 --- /dev/null +++ b/web-portal/frontend/pages/swap/index.tsx @@ -0,0 +1,99 @@ +import DashboardLayout from "@frontend/components/dashboard/layout"; +import { Stack, Tabs, rem } from "@mantine/core"; +import { useEffect, useState } from "react"; +import { crimson } from "@frontend/utils/theme"; +import Swap from "@frontend/components/swap/Swap"; +import Redeem from "@frontend/components/swap/Redeem"; +import classes from "@frontend/styles/tabs.module.css"; +import { IToken } from "@frontend/utils/types"; +import { useSetAtom } from "jotai"; +import { tokenDataAtom } from "@frontend/utils/atoms"; +import { useChainId } from "wagmi"; +import _ from "lodash"; + +export default function SwapOrRedeem({ + data, + defaultToken, +}: { + data: IToken[]; + defaultToken: IToken; +}) { + const [value, setValue] = useState("swap"); + const setTokenData = useSetAtom(tokenDataAtom); + + useEffect(() => { + setTokenData(data); + }); + + return ( + + + setValue(value === "swap" ? "redeem" : "swap")} + > + + + Swap + + + Redeem + + + + + + + + + + + + + ); +} + +export async function getServerSideProps() { + const res = await fetch("https://static.optimism.io/optimism.tokenlist.json"); + const data = await res.json(); + + const { tokens } = data; + const defaultToken = _.filter(tokens, { name: "Ether" })[0]; + return { + props: { + data: tokens satisfies IToken[], + defaultToken: defaultToken satisfies IToken, + }, + }; +} diff --git a/web-portal/frontend/styles/tabs.module.css b/web-portal/frontend/styles/tabs.module.css index 2c864e9c..d043c96d 100644 --- a/web-portal/frontend/styles/tabs.module.css +++ b/web-portal/frontend/styles/tabs.module.css @@ -6,3 +6,8 @@ .tab:hover { background-color: transparent; } + +.tabLabel { + font-size: 1.1rem; + font-weight: 700; +} diff --git a/web-portal/frontend/utils/Web3Provider.tsx b/web-portal/frontend/utils/Web3Provider.tsx index 1e8c3800..31d82f1a 100644 --- a/web-portal/frontend/utils/Web3Provider.tsx +++ b/web-portal/frontend/utils/Web3Provider.tsx @@ -1,12 +1,17 @@ import { defaultWagmiConfig } from "@web3modal/wagmi/react/config"; import { cookieStorage, createStorage } from "wagmi"; -import { mainnet, sepolia } from "wagmi/chains"; +import { + arbitrum, + base, + mainnet, + optimism, + polygon, + sepolia, +} from "wagmi/chains"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Provider as JotaiProvider, useSetAtom } from "jotai"; import { State, WagmiProvider } from "wagmi"; import { ReactNode } from "react"; -import { useSession } from "./hooks"; -import { sessionAtom } from "./atoms"; export const projectId = String( process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, @@ -24,7 +29,15 @@ const metadata = { }; // Create wagmiConfig -const chains = [mainnet, sepolia] as const; +export const chains = [ + mainnet, + sepolia, + optimism, + base, + arbitrum, + polygon, +] as const; + export const config = defaultWagmiConfig({ chains, projectId, diff --git a/web-portal/frontend/utils/atoms.ts b/web-portal/frontend/utils/atoms.ts index d7e18d0f..9a703574 100644 --- a/web-portal/frontend/utils/atoms.ts +++ b/web-portal/frontend/utils/atoms.ts @@ -1,8 +1,9 @@ import { atom } from "jotai"; -import { IEndpoint, ISession, IRuleType } from "./types"; +import { IEndpoint, ISession, IRuleType, IToken } from "./types"; export const sessionAtom = atom({}); export const appsAtom = atom([]); export const endpointsAtom = atom([]); export const ruleTypesAtom = atom([]); export const existingRuleValuesAtom = atom([]); export const billingHistoryAtom = atom([]); +export const tokenDataAtom = atom([]); diff --git a/web-portal/frontend/utils/hooks.ts b/web-portal/frontend/utils/hooks.ts index 0b5ad342..47da3ef4 100644 --- a/web-portal/frontend/utils/hooks.ts +++ b/web-portal/frontend/utils/hooks.ts @@ -189,3 +189,31 @@ export const useSecretKeyMutation = (appId: string) => { }, }); }; + +export const useQuote = ({ + sellToken, + amount, +}: { + sellToken: string; + amount: string; +}) => { + const fetchQuote = async () => { + const response = await fetch( + `https://api.0x.org/swap/v1/price?sellToken=${sellToken}&buyToken=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2&sellAmount=${amount}`, + { + headers: { + "Content-Type": "application/json", + "0x-api-key": "api-key", + }, + }, + ); + if (!response.ok) { + throw new Error("Failed to fetch quote"); + } + return response.json(); + }; + return useQuery({ + queryKey: ["quote", sellToken], + queryFn: fetchQuote, + }); +}; diff --git a/web-portal/frontend/utils/siwe.ts b/web-portal/frontend/utils/siwe.ts index a08848d2..230112b9 100644 --- a/web-portal/frontend/utils/siwe.ts +++ b/web-portal/frontend/utils/siwe.ts @@ -1,7 +1,6 @@ import { SiweMessage } from "siwe"; import Cookies from "js-cookie"; import { createSIWEConfig } from "@web3modal/siwe"; -import { revalidatePath } from "next/cache"; export const getNonce = async () => { const res = await fetch("/api/siwe", { method: "PUT" }); diff --git a/web-portal/frontend/utils/tokens.ts b/web-portal/frontend/utils/tokens.ts new file mode 100644 index 00000000..c236f9be --- /dev/null +++ b/web-portal/frontend/utils/tokens.ts @@ -0,0 +1,9 @@ +export const popularTokens = [ + "ETH", + "WETH", + "WBTC", + "USDC", + "USDT", + "DAI", + "MATIC", +]; diff --git a/web-portal/frontend/utils/types.ts b/web-portal/frontend/utils/types.ts index a709ce09..953c84da 100644 --- a/web-portal/frontend/utils/types.ts +++ b/web-portal/frontend/utils/types.ts @@ -76,3 +76,13 @@ export interface IBill { createdAt?: string; transactionType?: string; } + +export interface IToken { + chainId: number; + address: Address; + name: string; + symbol: string; + decimals: number; + logoURI: string; + extensions?: any; +}