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 (
+
+
+
+
+
+
+
+
+ {form.errors.requests}
+
+ {form.errors.period}
+
+ {values}
+
+
+
+ );
+}
diff --git a/web-portal/frontend/components/common/SearchableMultiSelect.tsx b/web-portal/frontend/components/common/SearchableMultiSelect.tsx
index d1ae19e1..763f2346 100644
--- a/web-portal/frontend/components/common/SearchableMultiSelect.tsx
+++ b/web-portal/frontend/components/common/SearchableMultiSelect.tsx
@@ -39,6 +39,7 @@ export function SearchableMultiSelect({
onRemove={() => handleValueRemove(item)}
bg="blue"
c="#fff"
+ size="lg"
>
{item}
diff --git a/web-portal/frontend/components/dashboard/insights.tsx b/web-portal/frontend/components/dashboard/insights.tsx
index ef706ec3..c1a0ae86 100644
--- a/web-portal/frontend/components/dashboard/insights.tsx
+++ b/web-portal/frontend/components/dashboard/insights.tsx
@@ -6,7 +6,7 @@ import {
Card,
RingProgress,
} from "@mantine/core";
-import React, { useEffect, useState } from "react";
+import React from "react";
import classes from "@frontend/styles/insight.module.css";
import { AreaChart } from "@mantine/charts";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
diff --git a/web-portal/frontend/components/swap/Redeem.tsx b/web-portal/frontend/components/swap/Redeem.tsx
new file mode 100644
index 00000000..58fe9672
--- /dev/null
+++ b/web-portal/frontend/components/swap/Redeem.tsx
@@ -0,0 +1,7 @@
+export default function Redeem() {
+ return (
+
+
Redeem
+
+ );
+}
diff --git a/web-portal/frontend/components/swap/SearchableSelectModal.tsx b/web-portal/frontend/components/swap/SearchableSelectModal.tsx
new file mode 100644
index 00000000..86106c06
--- /dev/null
+++ b/web-portal/frontend/components/swap/SearchableSelectModal.tsx
@@ -0,0 +1,74 @@
+import React, { useState } from "react";
+import { Modal, TextInput, Text, Flex, Stack } from "@mantine/core";
+import _ from "lodash";
+import Image from "next/image";
+import { IToken } from "@frontend/utils/types";
+
+interface Props {
+ options: IToken[];
+ opened: boolean;
+ onSelect: (option: IToken) => void;
+ onClose: () => void;
+}
+
+export const SearchableSelectModal: React.FC = ({
+ options,
+ onSelect,
+ opened,
+ onClose,
+}) => {
+ const [searchValue, setSearchValue] = useState(null);
+
+ const onChange = (newValue: string) => {
+ setSearchValue(newValue);
+ };
+
+ const filteredOptions = _.filter(options, (option: IToken) =>
+ typeof searchValue === "string"
+ ? _.toLower(option.symbol).includes(_.toLower(searchValue))
+ : options,
+ );
+
+ return (
+
+ onChange(event.target.value)}
+ title="Select Token to swap"
+ />
+
+ {filteredOptions.map((option) => (
+ onSelect(option)}
+ align="center"
+ p={8}
+ >
+
+
+
+ {_.get(option, "symbol") + ` - ` + _.get(option, "name")}
+
+ {_.get(option, "address")}
+
+
+ ))}
+
+
+ );
+};
diff --git a/web-portal/frontend/components/swap/Swap.tsx b/web-portal/frontend/components/swap/Swap.tsx
new file mode 100644
index 00000000..04cbac9b
--- /dev/null
+++ b/web-portal/frontend/components/swap/Swap.tsx
@@ -0,0 +1,202 @@
+import { useState } from "react";
+import { Flex, Stack, Button, TextInput, Text, Select } from "@mantine/core";
+import { useAtomValue } from "jotai";
+import _ from "lodash";
+import Image from "next/image";
+import { karla } from "@frontend/utils/theme";
+import { tokenDataAtom } from "@frontend/utils/atoms";
+import { IToken } from "@frontend/utils/types";
+import { SearchableSelectModal } from "./SearchableSelectModal";
+import { useAccount, useBalance, useChainId } from "wagmi";
+import { formatGwei, zeroAddress } from "viem";
+
+import { chains } from "@frontend/utils/Web3Provider";
+import { useQuote } from "@frontend/utils/hooks";
+
+// Common styles for TextInput and Select components
+const commonStyles = {
+ input: {
+ outline: "none",
+ border: "none",
+ background: "none",
+ fontSize: 24,
+ },
+ label: {
+ color: "#000",
+ marginLeft: 10,
+ },
+};
+
+const chainOptions = _.map(chains, "name");
+
+export default function Swap({ defaultToken }: { defaultToken: IToken }) {
+ const tokenData = useAtomValue(tokenDataAtom);
+
+ const [swapValue, setSwapValue] = useState(0);
+
+ const chainId = useChainId();
+ const [selectedChainId, setSelectedChainId] = useState(chainId);
+ const [selectedTokenData, setSelectedTokenData] =
+ useState(defaultToken);
+
+ const handleTokenChange = (token: IToken) => {
+ setSelectedTokenData(token);
+ };
+
+ const [opened, setOpened] = useState(false);
+
+ const { address } = useAccount();
+
+ const { data: tokenBalance } = useBalance({
+ token:
+ selectedTokenData?.address != zeroAddress
+ ? selectedTokenData?.address
+ : undefined,
+ address,
+ chainId,
+ });
+
+ const filteredTokenData = _.filter(
+ tokenData,
+ (t) => t.chainId === selectedChainId,
+ );
+
+ const { data: quote } = useQuote({
+ sellToken: selectedTokenData?.address,
+ amount: String(swapValue * 10 ** selectedTokenData?.decimals),
+ });
+
+ console.log(String(swapValue * 10 ** selectedTokenData?.decimals));
+
+ return (
+
+ setOpened(false)}
+ opened={opened}
+ options={filteredTokenData}
+ onSelect={(token: IToken) => {
+ handleTokenChange(token);
+ setOpened(false);
+ }}
+ />
+
+
+ );
+}
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;
+}