diff --git a/packages/nextjs/components/flashbotRecovery/AssetSelectionStep/AssetSelectionStep.tsx b/packages/nextjs/components/flashbotRecovery/AssetSelectionStep/AssetSelectionStep.tsx
new file mode 100644
index 0000000..7c88b4b
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/AssetSelectionStep/AssetSelectionStep.tsx
@@ -0,0 +1,157 @@
+import React, { useEffect, useState } from "react";
+import Image from "next/image";
+import LogoSvg from "../../../public/assets/flashbotRecovery/logo.svg";
+import { CustomButton } from "../CustomButton/CustomButton";
+import { ManualAssetSelection } from "../ManualAssetSelection/ManualAssetSelection";
+import styles from "./assetSelectionStep.module.css";
+import { motion } from "framer-motion";
+import { useAutodetectAssets } from "~~/hooks/flashbotRecoveryBundle/useAutodetectAssets";
+import { RecoveryTx } from "~~/types/business";
+
+interface IProps {
+ isVisible: boolean;
+ hackedAddress: string;
+ safeAddress: string;
+ onSubmit: (txs: RecoveryTx[]) => void;
+}
+export const AssetSelectionStep = ({ isVisible, onSubmit, hackedAddress, safeAddress }: IProps) => {
+ const [isAddingManually, setIsAddingManually] = useState(false);
+ const { getAutodetectedAssets } = useAutodetectAssets();
+ const [selectedAssets, setSelectedAssets] = useState
([]);
+ const [accountAssets, setAccountAssets] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const onAssetSelected = (index: number) => {
+ const currentIndex = selectedAssets.indexOf(index);
+ let newAssets: number[] = [];
+ if (currentIndex === -1) {
+ newAssets.push(index);
+ newAssets.push(...selectedAssets);
+ } else {
+ newAssets = selectedAssets.filter(item => item !== index);
+ }
+ setSelectedAssets(newAssets);
+ };
+
+ useEffect(() => {
+ if (accountAssets.length > 0 || !isVisible) {
+ return;
+ }
+ init();
+ }, [isVisible]);
+
+ const init = async () => {
+ const result = await getAutodetectedAssets({ hackedAddress, safeAddress });
+ if (!result) {
+ return;
+ }
+ setAccountAssets(result);
+ setIsLoading(false);
+ };
+
+ const onAddAssetsClick = () => {
+ const txsToAdd = accountAssets.filter((item, i) => selectedAssets.indexOf(i) != -1);
+ onSubmit(txsToAdd);
+ };
+ if (!isVisible) {
+ return <>>;
+ }
+ return (
+
+ setIsAddingManually(false)}
+ addAsset={item => {
+ setAccountAssets(current => [...current, item])
+ setIsAddingManually(false)
+ }}
+ />
+ Your assets
+
+
+ {!!isLoading
+ ? [1, 2, 3].map((item, i) => (
+
onAssetSelected(i)}
+ />
+ ))
+ : accountAssets.map((item, i) => {
+ return (
+ onAssetSelected(i)}
+ />
+ );
+ })}
+
+ setIsAddingManually(true)} />
+
+ onAddAssetsClick()} />
+
+ );
+};
+
+interface IAssetProps {
+ onClick: () => void;
+ isSelected: boolean;
+ tx?: RecoveryTx;
+ isLoading: boolean;
+}
+
+const AssetItem = ({ onClick, isSelected, tx, isLoading }: IAssetProps) => {
+ const getSubtitleTitle = () => {
+ if (!tx) {
+ return "";
+ }
+ if (["erc1155", "erc721"].indexOf(tx.type) != -1) {
+ //@ts-ignore
+ return `Token ID: ${tx.tokenId!}`;
+ }
+ if (tx.type === "erc20") {
+ //@ts-ignore
+ return tx.value;
+ }
+ if (tx.type === "custom") {
+ //@ts-ignore
+ return tx.info.split(" to ")[1]
+ }
+ return "";
+ };
+ const getTitle = () => {
+ if (!tx) {
+ return "";
+ }
+ if (tx.type === "erc20") {
+ //@ts-ignore
+ return tx.symbol;
+ }
+ if (tx.type === "custom") {
+ //@ts-ignore
+ return tx.info.split(" to ")[0]
+ }
+ return tx.info;
+ };
+ return (
+ onClick()}
+ className={`${isSelected ? "bg-base-200" : ""} ${styles.assetItem} ${isLoading ? styles.loading : ""}`}
+ >
+
+
+
+
+
{getTitle()}
+ {getSubtitleTitle()}
+
+
+ );
+};
diff --git a/packages/nextjs/components/flashbotRecovery/AssetSelectionStep/assetSelectionStep.module.css b/packages/nextjs/components/flashbotRecovery/AssetSelectionStep/assetSelectionStep.module.css
new file mode 100644
index 0000000..fea315f
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/AssetSelectionStep/assetSelectionStep.module.css
@@ -0,0 +1,78 @@
+.container{
+ width: 100%;
+
+ margin-bottom: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.title{
+ font-size: 23px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 100%;
+ margin: 0;
+}
+.assetItem{
+ display: flex;
+ height: 92px;
+ max-width: 458px;
+ margin: 0 auto;
+ border-radius: 12px;
+ cursor: pointer;
+ margin-bottom: 12px;
+}
+.data{
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+.assetList{
+ width: 100%;
+ max-height: calc(100vh - 400px);
+ height: calc(100vh - 400px);
+ overflow: auto;
+ margin: 36px 0;
+}
+
+.logo{
+ width: 60px;
+ height: 60px;
+ margin-right: 16px;
+}
+
+.logoContainer{
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin-left:16px;
+}
+
+.loader{
+ height: 94px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.loading span{
+ background-color:#3a4862;
+ width: 100px;
+ height: 24px;
+}
+.loading h3{
+ background-color:#3a4862;
+ width: 130px;
+ height: 24px;
+}
+
+.loading{
+ animation: loading 1s infinite;
+}
+
+@keyframes loading {
+ from {opacity: 0.3;}
+ to {opacity: 0.8;}
+ }
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/ConnectStep/ConnectStep.tsx b/packages/nextjs/components/flashbotRecovery/ConnectStep/ConnectStep.tsx
new file mode 100644
index 0000000..58b3893
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/ConnectStep/ConnectStep.tsx
@@ -0,0 +1,44 @@
+import React, { useEffect } from "react";
+import Image from "next/image";
+import IllustrationSvg from "../../../public/assets/flashbotRecovery/logo.svg"
+import styles from "./connectStep.module.css"
+import { RainbowKitCustomConnectButton } from "~~/components/scaffold-eth";
+import { AnimatePresence, motion } from "framer-motion";
+interface IProps{
+ isVisible:boolean
+ safeAddress:string;
+ address:string;
+ setSafeAddress: (newAdd:string) => void
+}
+export const ConnectStep = ({isVisible, safeAddress, address, setSafeAddress}:IProps) => {
+
+
+ useEffect(() => {
+ if (!!safeAddress || !address) {
+ return;
+ }
+ setSafeAddress(address);
+ return () => {};
+ }, [address]);
+
+
+ return
+ {isVisible && (
+
+ Welcome to Flashbot Recovery
+
+
+ Connect your "Safe Wallet" where the assets will be send after the recovery process.
+
+
+
+
+
+ )}
+
+};
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/ConnectStep/connectStep.module.css b/packages/nextjs/components/flashbotRecovery/ConnectStep/connectStep.module.css
new file mode 100644
index 0000000..61b0125
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/ConnectStep/connectStep.module.css
@@ -0,0 +1,38 @@
+.container{
+ max-width: 580px;
+ margin: 20px;
+ margin-bottom: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+.title{
+ text-align: center;
+ font-size: 24px;
+ font-style: normal;
+ font-weight: 700;
+ line-height: 32px;
+}
+.illustration{
+ width: 250px;
+ height: 250px;
+ margin: 40px 0;
+}
+.text{
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 400;
+ font-size: 18px;
+ line-height: 32px;
+ text-align: center;
+}
+.buttonContainer button{
+ display: flex;
+
+height: 40px;
+padding: 0px 48px;
+}
+.buttonContainer > div > div >div{
+display: none;
+}
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/CustomButton/CustomButton.tsx b/packages/nextjs/components/flashbotRecovery/CustomButton/CustomButton.tsx
new file mode 100644
index 0000000..2128b52
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/CustomButton/CustomButton.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import styles from "./customButton.module.css";
+
+interface IProps {
+ text: string;
+ onClick: () => void;
+ disabled?: boolean;
+ type: "btn-accent" | "btn-primary";
+}
+export const CustomButton = ({ text, onClick, disabled = false, type }: IProps) => {
+ return (
+ onClick()}>
+ {text}
+
+ );
+};
diff --git a/packages/nextjs/components/flashbotRecovery/CustomButton/customButton.module.css b/packages/nextjs/components/flashbotRecovery/CustomButton/customButton.module.css
new file mode 100644
index 0000000..645cfc3
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/CustomButton/customButton.module.css
@@ -0,0 +1,14 @@
+.button{
+ width: 250px;
+ margin: 0 auto;
+ padding: 0px 48px;
+ height: 40px;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 700;
+ line-height: normal;
+ border: 1px solid;
+}
+.button:hover{
+ border: 1px solid;
+}
diff --git a/packages/nextjs/components/flashbotRecovery/CustomHeader/CustomHeader.tsx b/packages/nextjs/components/flashbotRecovery/CustomHeader/CustomHeader.tsx
new file mode 100644
index 0000000..cf41b8b
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/CustomHeader/CustomHeader.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import styles from "./customheader.module.css"
+import Image from 'next/image'
+import LogoSvg from "../../../public/assets/flashbotRecovery/logo.svg"
+
+export const CustomHeader = () => {
+ return (
+
+
+
+ Recovery Flashbot
+
+
+ )
+}
diff --git a/packages/nextjs/components/flashbotRecovery/CustomHeader/customheader.module.css b/packages/nextjs/components/flashbotRecovery/CustomHeader/customheader.module.css
new file mode 100644
index 0000000..0aeb387
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/CustomHeader/customheader.module.css
@@ -0,0 +1,26 @@
+.header{
+ width: 100%;
+}
+
+.logoContainer{
+ display: flex;
+ align-items: center;
+ margin-top: 20px;
+ margin-left: 10px;
+ font-size: 23px;
+font-style: normal;
+font-weight: 400;
+}
+
+.logo{
+ width: 60px;
+ height: 60px;
+ margin-right: 16px;
+}
+
+@media (min-width: 768px) {
+ .logoContainer{
+ margin-top: 56px;
+ margin-left: 96px;
+ }
+}
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/CustomPortal/CustomPortal.tsx b/packages/nextjs/components/flashbotRecovery/CustomPortal/CustomPortal.tsx
new file mode 100644
index 0000000..e07394e
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/CustomPortal/CustomPortal.tsx
@@ -0,0 +1,65 @@
+import { useEffect, useState } from "react";
+import Image from "next/image";
+import CloseSvg from "../../../public/assets/flashbotRecovery/close.svg";
+import { CustomButton } from "../CustomButton/CustomButton";
+import styles from "./customPortal.module.css";
+import { motion } from "framer-motion";
+import { createPortal } from "react-dom";
+
+interface IProps {
+ title: string;
+ image?: string;
+ video?: string;
+ close?:() => void;
+ description: string;
+ button?: {
+ text: string;
+ disabled:boolean;
+ action: () => void;
+ };
+ indicator?:number
+}
+export const CustomPortal = ({ indicator, title, image, video, description, button, close}: IProps) => {
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+
+ return () => setMounted(false);
+ }, []);
+ const portalSelector = document.querySelector("#myportal");
+ if (!portalSelector) {
+ return <>>;
+ }
+
+ return mounted
+ ? createPortal(
+
+
+
setMounted(false)}>
+ {" "}
+ {!!close ? close()}/>: <>>}
+
+
+
{title}
+
+ {!!image ?
: <>>}
+ {!!indicator ?
{indicator} BLOCKS
: <>>}
+
+
+ {!!video ?
: <>>}
+
{description}
+ {!!button ?
button.action()} /> : <>>}
+
+
+
+ ,
+ portalSelector,
+ )
+ : null;
+};
diff --git a/packages/nextjs/components/flashbotRecovery/CustomPortal/customPortal.module.css b/packages/nextjs/components/flashbotRecovery/CustomPortal/customPortal.module.css
new file mode 100644
index 0000000..56d1527
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/CustomPortal/customPortal.module.css
@@ -0,0 +1,72 @@
+.modalContainer {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 100vw;
+ height: 100vh;
+ backdrop-filter: blur(5px);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: rgba(20, 28, 41, 0.40);
+}
+
+.modal {
+ display: inline-flex;
+ width: 579px;
+ flex-direction: column;
+ align-items: center;
+ gap: 40px;
+ flex-shrink: 0;
+ border-radius: 12px;
+ box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.03);
+
+}
+
+.modalContent{
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 40px;
+ flex-shrink: 0;
+ margin-bottom: 56px;
+}
+
+.close{
+ align-self: flex-end;
+ margin-top: 14px;
+ margin-right: 14px;
+ cursor: pointer;
+ padding: 10px;
+ padding-bottom: 4px;
+}
+
+.title{
+ font-size: 24px;
+font-style: normal;
+font-weight: 700;
+line-height: 32px;
+margin:0;
+}
+
+.image{
+ height: 190px;
+ width: 190px;
+}
+.indicator{
+text-align: center;
+font-size: 20px;
+font-style: normal;
+font-weight: 600;
+line-height: 100%; /* 20.033px */
+}
+
+.text{
+ font-size: 18px;
+font-style: normal;
+font-weight: 400;
+line-height: 32px;
+width: 80%;
+text-align: center;
+margin: 0;
+}
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/HackedAddressStep/HackedAddressStep.tsx b/packages/nextjs/components/flashbotRecovery/HackedAddressStep/HackedAddressStep.tsx
new file mode 100644
index 0000000..8ff64f7
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/HackedAddressStep/HackedAddressStep.tsx
@@ -0,0 +1,44 @@
+import React, { useState } from "react";
+import { CustomButton } from "../CustomButton/CustomButton";
+import styles from "./hackedAddressStep.module.css";
+import { isAddress } from "ethers/lib/utils";
+import { AnimatePresence, motion } from "framer-motion";
+import { AddressInput } from "~~/components/scaffold-eth";
+
+interface IProps {
+ isVisible: boolean;
+ onSubmit: (address: string) => void;
+}
+export const HackedAddressStep = ({ isVisible, onSubmit }: IProps) => {
+ if (!isVisible) {
+ return <>>;
+ }
+
+ const [hackedAddress, setHackedAddress] = useState("");
+
+ return (
+
+
+ Hacked address
+
+
+
+ {
+ if (!isAddress(hackedAddress)) {
+ alert("Given hacked address is not a valid address");
+ return;
+ }
+ onSubmit(hackedAddress);
+ }}
+ />
+
+ );
+};
diff --git a/packages/nextjs/components/flashbotRecovery/HackedAddressStep/hackedAddressStep.module.css b/packages/nextjs/components/flashbotRecovery/HackedAddressStep/hackedAddressStep.module.css
new file mode 100644
index 0000000..540b8c6
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/HackedAddressStep/hackedAddressStep.module.css
@@ -0,0 +1,20 @@
+.container{
+ width: 90%;
+ margin-bottom: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ max-width: 458px;
+}
+
+.label{
+ font-size: 16px;
+font-style: normal;
+font-weight: 600;
+line-height: normal;
+margin-bottom: 12px;
+}
+.container > div{
+ border-radius: 4px;
+ padding: 4px;
+}
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/Layout/Layout.tsx b/packages/nextjs/components/flashbotRecovery/Layout/Layout.tsx
new file mode 100644
index 0000000..72e2b29
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/Layout/Layout.tsx
@@ -0,0 +1,97 @@
+import React from "react";
+import Image from "next/image";
+import LogoSvg from "../../../public/assets/flashbotRecovery/logo.svg";
+import styles from "./layout.module.css";
+import { motion } from "framer-motion";
+import { Address } from "~~/components/scaffold-eth";
+
+interface IProps {
+ children: JSX.Element;
+ stepActive: number;
+ safeAddress: string;
+ hackedAddress: string;
+}
+export const Layout = ({ children, stepActive, hackedAddress, safeAddress }: IProps) => {
+ return (
+
+
+
+
+
+ Recovery Flashbot
+
+
+ 1}
+ index={1}
+ title={"Enter the hacked address"}
+ description={"Provide the address that was hacked so we can search for your assets."}
+ />
+ 2}
+ index={2}
+ title={"Select your assets"}
+ description={
+ "Your assets will be listed, select the ones you want to transfer or add manually if you miss someone"
+ }
+ />
+
+ 3}
+ index={3}
+ title={"Confirm the bundle"}
+ description={"Review the transactions that are going to be generated to recover your assets"}
+ />
+
+ 4}
+ index={4}
+ title={"Recover your assets"}
+ description={
+ "Follow the steps to retrieve your assets, this is a critical process, so please be patient."
+ }
+ />
+
+
+
+
+ {children}
+
+ );
+};
+
+interface IStepProps {
+ isActive: boolean;
+ index: number;
+ title: string;
+ isCompleted:boolean;
+ description: string;
+}
+const Step = ({ isActive, isCompleted, index, title, description }: IStepProps) => {
+ return (
+
+
+ {index}
+
+
+
{title}
+
{description}
+
+
+ );
+};
diff --git a/packages/nextjs/components/flashbotRecovery/Layout/layout.module.css b/packages/nextjs/components/flashbotRecovery/Layout/layout.module.css
new file mode 100644
index 0000000..3efe352
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/Layout/layout.module.css
@@ -0,0 +1,121 @@
+.layout {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ width: 100%;
+ height: 100vh;
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+}
+.sidebar {
+ max-width: 530px;
+ display: flex;
+ width: auto;
+
+ background-color: #243148;
+ height: 100%;
+}
+
+.sidebarContent {
+ margin-top: 56px;
+ margin-left: 10px;
+}
+.content {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.logoContainer {
+ display: flex;
+ align-items: center;
+ font-size: 23px;
+ font-style: normal;
+ font-weight: 400;
+}
+
+.logo {
+ width: 60px;
+ height: 60px;
+ margin-right: 16px;
+}
+.steps {
+ margin-top: 48px;
+ margin-right: 63px;
+ position: relative;
+}
+.step {
+ display: flex;
+ margin-top: 16px;
+}
+.badge {
+ border-radius: 100px;
+ border: 2px solid;
+ display: flex;
+ width: 34px;
+ font-size: 20px;
+ font-style: normal;
+ font-weight: 600;
+ height: 34px;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+.stepContainer {
+ margin-left: 16px;
+}
+.stepTitle {
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 600;
+ margin: 0;
+ display: flex;
+ height: 34px;
+ align-items: center;
+}
+.stepDescription {
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 26px;
+ margin: 0;
+}
+
+.addresess {
+ display: flex;
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ border-radius: 12px;
+ justify-content: space-around;
+ max-width: 380px;
+ width: 34%;
+ left: 96px;
+ bottom: 26px;
+}
+
+.addressContainer {
+ margin: 24px 0;
+ margin-right: 0;
+}
+.addressContainer svg {
+ display: none;
+}
+.completed{
+ color: #7E92BD;
+}
+.completed .badge{
+ background-color: #3A4A6E ;
+
+}
+@media (min-width: 768px) {
+ .sidebarContent {
+ margin-left: 96px;
+ }
+}
diff --git a/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/BasicFlow/BasicFlow.tsx b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/BasicFlow/BasicFlow.tsx
new file mode 100644
index 0000000..1f4e84f
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/BasicFlow/BasicFlow.tsx
@@ -0,0 +1,105 @@
+import { useState } from "react";
+import { CustomButton } from "../../CustomButton/CustomButton";
+import { ERC20Form } from "./ERC20From";
+import { ERC721Form } from "./ERC721Form";
+import { ERC1155Form } from "./ERC1155Form";
+import styles from "../manualAssetSelection.module.css";
+import { motion } from "framer-motion";
+import { createPortal } from "react-dom";
+import { RecoveryTx } from "~~/types/business";
+
+interface IBasicFlowProps {
+ safeAddress: string;
+ hackedAddress: string;
+ addAsset: (asset: RecoveryTx) => void;
+}
+
+export interface ITokenForm {
+ hackedAddress: string;
+ safeAddress: string;
+ close: () => void;
+ addAsset: (arg: RecoveryTx) => void;
+}
+
+export const BasicFlow = ({ safeAddress, hackedAddress, addAsset }: IBasicFlowProps) => {
+ const [tokenActive, setTokenActive] = useState(0);
+ return (
+ <>
+ {
+ setTokenActive(0);
+ }}
+ safeAddress={safeAddress}
+ />
+
+
+ ERC20
+ setTokenActive(1)} />
+
+
+ ERC721
+ setTokenActive(2)} />
+
+
+ ERC1551
+ setTokenActive(3)} />
+
+
+ >
+ );
+};
+
+interface ITokenSelectionProps {
+ tokenActive: number;
+ close: () => void;
+ hackedAddress: string;
+ safeAddress: string;
+ addAsset: (asset: RecoveryTx) => void;
+}
+const TokenSelection = ({ close, addAsset, tokenActive, hackedAddress, safeAddress }: ITokenSelectionProps) => {
+ const portalSelector = document.querySelector("#myportal2");
+ if (!portalSelector) {
+ return <>>;
+ }
+
+ return tokenActive != 0
+ ? createPortal(
+
+
+
+
+ {tokenActive === 1 ? (
+
+ ) : (
+ <>>
+ )}
+ {tokenActive === 2 ? (
+
+ ) : (
+ <>>
+ )}
+ {tokenActive === 3 ? (
+
+ ) : (
+ <>>
+ )}
+
+
+ ,
+ portalSelector,
+ )
+ : null;
+};
diff --git a/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/BasicFlow/ERC1155Form.tsx b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/BasicFlow/ERC1155Form.tsx
new file mode 100644
index 0000000..9f391bd
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/BasicFlow/ERC1155Form.tsx
@@ -0,0 +1,94 @@
+import { useState } from "react";
+import styles from "../manualAssetSelection.module.css";
+import { usePublicClient } from "wagmi";
+import { ERC1155_ABI } from "~~/utils/constants";
+import { getTargetNetwork } from "~~/utils/scaffold-eth";
+import { ITokenForm } from "./BasicFlow";
+import { BigNumber, ethers } from "ethers";
+import { isAddress } from "viem";
+import Image from "next/image";
+import BackSvg from "../../../../public/assets/flashbotRecovery/back.svg";
+import { ERC1155Tx } from "~~/types/business";
+import { AddressInput, InputBase } from "~~/components/scaffold-eth";
+import { CustomButton } from "../../CustomButton/CustomButton";
+const erc1155Interface = new ethers.utils.Interface(ERC1155_ABI);
+
+export const ERC1155Form = ({ hackedAddress, safeAddress, addAsset, close }: ITokenForm) => {
+ const [contractAddress, setContractAddress] = useState("");
+ const [tokenIds, setTokenIds] = useState("");
+ const publicClient = usePublicClient({ chainId: getTargetNetwork().id });
+
+ const addErc1155TxToBasket = async () => {
+ if (!isAddress(contractAddress) || !tokenIds) {
+ alert("Provide a contract and a token ID");
+ return;
+ }
+
+ const currentIds = tokenIds
+ .split(",")
+ .map((a: any) => a)
+ .map((a: any) => BigNumber.from(a));
+ const balances = (await publicClient.readContract({
+ address: contractAddress as `0x${string}`,
+ abi: ERC1155_ABI,
+ functionName: "balanceOfBatch",
+ args: [Array(tokenIds.length).fill(hackedAddress), tokenIds],
+ })) as BigNumber[];
+ const tokenIdsWithInvalidBalances: BigNumber[] = [];
+ for (let i = 0; i < tokenIds.length; i++) {
+ if (!balances[i] || balances[i].toString() == "0") {
+ tokenIdsWithInvalidBalances.push(currentIds[i]);
+ }
+ }
+ if (tokenIdsWithInvalidBalances.length > 0) {
+ alert(`Remove following tokenIds as hacked account does not own them: ${tokenIdsWithInvalidBalances.toString()}`);
+ return;
+ }
+ const erc1155TokenIds = currentIds.map(t => t.toString());
+ const erc1155TokenBalances = balances.map(t => t.toString());
+
+ const newErc1155Tx: ERC1155Tx = {
+ type: "erc1155",
+ info: `ERC1155 for tokenIds ${erc1155TokenIds.toString()}`,
+ uri: "changeme",
+ tokenIds: erc1155TokenIds,
+ amounts: erc1155TokenBalances,
+ toSign: {
+ from: hackedAddress as `0x${string}`,
+ to: contractAddress as `0x${string}`,
+ data: erc1155Interface.encodeFunctionData("safeBatchTransferFrom", [
+ hackedAddress,
+ safeAddress,
+ erc1155TokenIds,
+ erc1155TokenBalances,
+ ethers.constants.HashZero,
+ ]) as `0x${string}`,
+ },
+ };
+ addAsset(newErc1155Tx);
+ };
+ return (
+
+
close()} />
+ {"ERC1155"}
+
+
+ Contract Address
+
+ setContractAddress(e)}
+ />
+
+
+ Token Ids
+
+ setTokenIds(e)} />
+
+ addErc1155TxToBasket()} />
+
+ );
+ };
+
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/BasicFlow/ERC20From.tsx b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/BasicFlow/ERC20From.tsx
new file mode 100644
index 0000000..664cfca
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/BasicFlow/ERC20From.tsx
@@ -0,0 +1,81 @@
+import { useState } from "react";
+import styles from "../manualAssetSelection.module.css";
+import { useContractRead } from "wagmi";
+import { ERC20_ABI } from "~~/utils/constants";
+import { getTargetNetwork } from "~~/utils/scaffold-eth";
+import { ITokenForm } from "./BasicFlow";
+import { BigNumber, ethers } from "ethers";
+import { isAddress } from "viem";
+import Image from "next/image";
+import BackSvg from "../../../../public/assets/flashbotRecovery/back.svg";
+import { ERC20Tx } from "~~/types/business";
+import { AddressInput } from "~~/components/scaffold-eth";
+import { CustomButton } from "../../CustomButton/CustomButton";
+const erc20Interface = new ethers.utils.Interface(ERC20_ABI);
+
+export const ERC20Form = ({ hackedAddress, safeAddress, addAsset, close }: ITokenForm) => {
+ const [contractAddress, setContractAddress] = useState("");
+
+ let erc20Balance: string = "NO INFO";
+ try {
+ let { data } = useContractRead({
+ chainId: getTargetNetwork().id,
+ functionName: "balanceOf",
+ address: contractAddress as `0x${string}`,
+ abi: ERC20_ABI,
+ watch: true,
+ args: [hackedAddress],
+ });
+ if (data) {
+ erc20Balance = BigNumber.from(data).toString();
+ if (erc20Balance == "0") erc20Balance = "NO INFO";
+ }
+ } catch (e) {
+ // Most probably the contract address is not valid as user is
+ // still typing, so ignore.
+ }
+
+ const addErc20TxToBasket = (balance: string) => {
+ if (!isAddress(contractAddress)) {
+ alert("Provide a contract first");
+ return;
+ }
+ if (balance == "NO INFO") {
+ alert("Hacked account has no balance in given erc20 contract");
+ return;
+ }
+
+ const newErc20tx: ERC20Tx = {
+ type: "erc20",
+ info: "changeme",
+ symbol: "changeme",
+ amount: balance,
+ toSign: {
+ from: hackedAddress as `0x${string}`,
+ to: contractAddress as `0x${string}`,
+ data: erc20Interface.encodeFunctionData("transfer", [safeAddress, BigNumber.from(balance)]) as `0x${string}`,
+ },
+ };
+ addAsset(newErc20tx);
+ };
+
+ return (
+
+
close()} />
+ {"ERC20"}
+
+
+ Contract Address
+
+ setContractAddress(e)}
+ />
+
+ addErc20TxToBasket(erc20Balance)} />
+
+ );
+ };
+
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/BasicFlow/ERC721Form.tsx b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/BasicFlow/ERC721Form.tsx
new file mode 100644
index 0000000..df4be1e
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/BasicFlow/ERC721Form.tsx
@@ -0,0 +1,83 @@
+import { useState } from "react";
+import styles from "../manualAssetSelection.module.css";
+import { usePublicClient } from "wagmi";
+import { ERC721_ABI } from "~~/utils/constants";
+import { getTargetNetwork } from "~~/utils/scaffold-eth";
+import { ITokenForm } from "./BasicFlow";
+import { BigNumber, ethers } from "ethers";
+import { isAddress } from "viem";
+import Image from "next/image";
+import BackSvg from "../../../../public/assets/flashbotRecovery/back.svg";
+import { AddressInput, InputBase } from "~~/components/scaffold-eth";
+import { CustomButton } from "../../CustomButton/CustomButton";
+import { ERC721Tx } from "~~/types/business";
+const erc721Interface = new ethers.utils.Interface(ERC721_ABI);
+
+export const ERC721Form = ({ hackedAddress, safeAddress, addAsset, close }: ITokenForm) => {
+ const [contractAddress, setContractAddress] = useState("");
+ const [tokenId, setTokenId] = useState("");
+ const publicClient = usePublicClient({ chainId: getTargetNetwork().id });
+
+ const addErc721TxToBasket = async () => {
+ if (!isAddress(contractAddress) || !tokenId) {
+ alert("Provide a contract and a token ID");
+ return;
+ }
+
+ let ownerOfGivenTokenId;
+ try {
+ ownerOfGivenTokenId = await publicClient.readContract({
+ address: contractAddress as `0x${string}`,
+ abi: ERC721_ABI,
+ functionName: "ownerOf",
+ args: [BigNumber.from(tokenId)],
+ });
+ } catch (e) {}
+
+ if (!ownerOfGivenTokenId || ownerOfGivenTokenId.toString() != hackedAddress) {
+ alert(`Couldn't verify hacked account's ownership. Cannot add to the basket...`);
+ return;
+ }
+
+ const newErc721Tx: ERC721Tx = {
+ type: "erc721",
+ info: `NFT recovery for tokenId ${tokenId}`,
+ symbol: "changeme",
+ tokenId: tokenId,
+ toSign: {
+ from: hackedAddress as `0x${string}`,
+ to: contractAddress as `0x${string}`,
+ data: erc721Interface.encodeFunctionData("transferFrom", [
+ hackedAddress,
+ safeAddress,
+ BigNumber.from(tokenId),
+ ]) as `0x${string}`,
+ },
+ };
+ addAsset(newErc721Tx);
+ };
+ return (
+
+
close()} />
+ {"ERC721"}
+
+
+ Contract Address
+
+ setContractAddress(e)}
+ />
+
+
+ Token Id
+
+ setTokenId(e)} />
+
+ addErc721TxToBasket()} />
+
+ );
+ };
+
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/CustomFlow/CustomFlow.tsx b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/CustomFlow/CustomFlow.tsx
new file mode 100644
index 0000000..710d6f0
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/CustomFlow/CustomFlow.tsx
@@ -0,0 +1,74 @@
+import { parseAbiItem } from "viem";
+import { AddressInput, CustomContractWriteForm, InputBase } from "~~/components/scaffold-eth";
+import { AbiFunction } from "abitype";
+import styles from "../manualAssetSelection.module.css";
+import { RecoveryTx } from "~~/types/business";
+import { useState } from "react";
+interface ICustomFlowProps {
+ hackedAddress: string;
+ addAsset: (asset: RecoveryTx) => void;
+ }
+ export const CustomFlow = ({ hackedAddress, addAsset }: ICustomFlowProps) => {
+ const [customContractAddress, setCustomContractAddress] = useState("");
+ const [customFunctionSignature, setCustomFunctionSignature] = useState("");
+
+ const getParsedAbi = () => {
+ if (!customContractAddress || !customFunctionSignature) {
+ return null;
+ }
+ try {
+ const parsedFunctAbi = parseAbiItem(customFunctionSignature) as AbiFunction;
+ return parsedFunctAbi;
+ } catch (e) {
+ return null;
+ }
+ };
+ const parsedFunctAbi = getParsedAbi();
+
+ return (
+ <>
+
+
+
+
+
+ Contract Address
+
+
setCustomContractAddress(e)}
+ />
+
+
+ Function to call
+
+ setCustomFunctionSignature(e)}
+ />
+ {!!parsedFunctAbi ? (
+
+ {
+ setCustomContractAddress("");
+ setCustomFunctionSignature("");
+ }}
+ />
+
+ ) : (
+ <>>
+ )}
+
+ >
+ );
+ };
+
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/ManualAssetSelection.tsx b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/ManualAssetSelection.tsx
new file mode 100644
index 0000000..ccb4841
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/ManualAssetSelection.tsx
@@ -0,0 +1,60 @@
+import Image from "next/image";
+import CloseSvg from "../../../public/assets/flashbotRecovery/close.svg";
+import { Tabs } from "../tabs/Tabs";
+import { BasicFlow } from "./BasicFlow/BasicFlow";
+import { CustomFlow } from "./CustomFlow/CustomFlow";
+import styles from "./manualAssetSelection.module.css";
+import { motion } from "framer-motion";
+import { createPortal } from "react-dom";
+import { RecoveryTx } from "~~/types/business";
+
+interface IProps {
+ isVisible: boolean;
+ close: () => void;
+ hackedAddress: string;
+ safeAddress: string;
+ addAsset: (asset: RecoveryTx) => void;
+}
+export const ManualAssetSelection = ({ isVisible, close,safeAddress, addAsset, hackedAddress }: IProps) => {
+ const portalSelector = document.querySelector("#myportal");
+ if (!portalSelector) {
+ return <>>;
+ }
+
+ return isVisible
+ ? createPortal(
+
+
+
+ {" "}
+ {!!close ? close()} /> : <>>}
+
+
+
{"Add assets manually"}
+
+ {active => {
+ const isBasic = active == 0;
+ if (isBasic) {
+ return (
+
+ );
+ }
+ return addAsset(item)} />;
+ }}
+
+
+
+ ,
+ portalSelector,
+ )
+ : null;
+};
diff --git a/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/manualAssetSelection.module.css b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/manualAssetSelection.module.css
new file mode 100644
index 0000000..551c70f
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/ManualAssetSelection/manualAssetSelection.module.css
@@ -0,0 +1,163 @@
+.modalContainer {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 100vw;
+ height: 100vh;
+ backdrop-filter: blur(5px);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: rgba(20, 28, 41, 0.40);
+}
+
+.modal {
+ display: inline-flex;
+ width: 579px;
+ flex-direction: column;
+ align-items: center;
+ flex-shrink: 0;
+ border-radius: 12px;
+ box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.03);
+ height: 650px;
+}
+
+.modalContent{
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 40px;
+ flex-shrink: 0;
+ margin-bottom: 56px;
+ width: 90%;
+}
+
+.close{
+ align-self: flex-end;
+ margin-top: 14px;
+ margin-right: 14px;
+ cursor: pointer;
+ padding: 10px;
+ padding-bottom: 4px;
+}
+
+.title{
+ font-size: 24px;
+font-style: normal;
+font-weight: 700;
+line-height: 32px;
+margin:0;
+text-align: center;
+}
+
+
+.text{
+ font-size: 18px;
+font-style: normal;
+font-weight: 400;
+line-height: 32px;
+width: 80%;
+text-align: center;
+margin: 0;
+}
+
+.basicItem{
+ display: flex;
+padding: 16px;
+flex-direction: column;
+justify-content: center;
+align-items: center;
+gap: 24px;
+border-radius: 12px;
+margin-top: 24px;
+}
+
+
+.container, .containerCustom, .containerBasic{
+ position: relative;
+ margin-bottom: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ margin: 0 auto;
+ max-width: 450px;
+}
+.containerCustom{
+ height: 392px;
+ width: 100%;
+ padding: 0 10px;
+ overflow-y: auto;
+}
+.containerBasic{
+ height: 538px;
+ width: 100%;
+ padding: 0 10px;
+}
+
+.list{
+ width: 450px;
+ margin: 0 auto;
+}
+.label{
+ font-size: 16px;
+font-style: normal;
+font-weight: 600;
+line-height: normal;
+margin-bottom: 12px;
+}
+.container > div{
+ border-radius: 4px;
+ padding: 4px;
+}
+.containerCustom > div{
+ border-radius: 4px;
+ padding: 4px;
+}
+.containerBasic > div{
+ border-radius: 4px;
+ padding: 4px;
+}
+.contract{
+ margin-top: 20px;
+}
+
+.contract > div >div div{
+ border-radius: 4px;
+ padding: 4px;
+}
+.contract > div >div div input::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
+ color: rgb(156 163 175 / var(--tw-text-opacity));
+ opacity: 1; /* Firefox */
+}
+.contract > div >div div:last-child{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ .contract > div >div div:last-child button{
+ background-color: #17A9E8;
+
+ color: #293853;
+ width: 230px;
+ margin: 0 auto;
+ padding: 0px 48px;
+ height: 40px;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 700;
+ line-height: normal;
+ border: 1px solid;
+ }
+
+.bottom{
+ flex-grow: 1;
+}
+ .contract > div >div div:last-child button svg{
+ display: none;
+ }
+
+ .back{
+ position: absolute;
+ top: 0;
+ cursor: pointer;
+ }
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/RecoveryProcess/RecoveryProcess.tsx b/packages/nextjs/components/flashbotRecovery/RecoveryProcess/RecoveryProcess.tsx
new file mode 100644
index 0000000..ed81d34
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/RecoveryProcess/RecoveryProcess.tsx
@@ -0,0 +1,148 @@
+import React from "react";
+import { CustomPortal } from "../CustomPortal/CustomPortal";
+import { RecoveryProcessStatus } from "~~/hooks/flashbotRecoveryBundle/useRecoveryProcess";
+import LogoSvg from "~~/public/assets/flashbotRecovery/logo.svg";
+import ClockSvg from "~~/public/assets/flashbotRecovery/clock.svg";
+import SuccessSvg from "~~/public/assets/flashbotRecovery/success.svg";
+import VideoSvg from "~~/public/assets/flashbotRecovery/video.svg";
+
+interface IProps {
+ recoveryStatus: RecoveryProcessStatus;
+ startSigning: () => void;
+ finishProcess: () => void;
+ startProcess: () => void;
+ connectedAddress: string;
+ safeAddress: string;
+ hackedAddress: string;
+ blockCountdown:number
+}
+export const RecoveryProcess = ({
+ recoveryStatus,
+ startSigning,
+ startProcess,
+ finishProcess,
+ blockCountdown,
+ connectedAddress,
+ safeAddress,
+ hackedAddress,
+}: IProps) => {
+
+ if (recoveryStatus == RecoveryProcessStatus.initial) {
+ return <>>;
+ }
+ if (recoveryStatus == RecoveryProcessStatus.gasCovered) {
+ alert("you already covered the gas. If you're in a confussy situation, clear cookies and refresh page.");
+ return;
+ }
+
+ if (recoveryStatus == RecoveryProcessStatus.cachedDataToClean) {
+ return (
+
+ );
+ }
+
+ if (recoveryStatus == RecoveryProcessStatus.noSafeAccountConnected) {
+ return (
+ startProcess(),
+ }}
+ image={LogoSvg}
+ />
+ );
+ }
+ if (
+ recoveryStatus == RecoveryProcessStatus.switchFlashbotNetworkAndPayBundleGas
+ ) {
+ return (
+
+ );
+ }
+
+ if(recoveryStatus == RecoveryProcessStatus.increaseGas){
+ return (
+
+ );
+ }
+ if (recoveryStatus == RecoveryProcessStatus.switchToHacked) {
+ return (
+ startSigning(),
+ }}
+ image={LogoSvg}
+ />
+ );
+ }
+ if (
+ recoveryStatus == RecoveryProcessStatus.signEachTransaction ||
+ recoveryStatus == RecoveryProcessStatus.allTxSigned ||
+ recoveryStatus == RecoveryProcessStatus.sendingBundle
+ ) {
+ return (
+
+ );
+ }
+ if (recoveryStatus == RecoveryProcessStatus.listeningToBundle) {
+ return (
+
+ );
+ }
+ if (recoveryStatus == RecoveryProcessStatus.success) {
+ return (
+ finishProcess(),
+ }}
+ image={SuccessSvg}
+ />
+ );
+ }
+
+ return <>>;
+};
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/RecoveryProcess/recoveryProcess.module.css b/packages/nextjs/components/flashbotRecovery/RecoveryProcess/recoveryProcess.module.css
new file mode 100644
index 0000000..e69de29
diff --git a/packages/nextjs/components/flashbotRecovery/TransactionBundleStep/transactionBundleStep.module.css b/packages/nextjs/components/flashbotRecovery/TransactionBundleStep/transactionBundleStep.module.css
new file mode 100644
index 0000000..a018b57
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/TransactionBundleStep/transactionBundleStep.module.css
@@ -0,0 +1,79 @@
+.container{
+ width: 99%;
+
+ margin-bottom: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.title{
+ font-size: 23px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 100%;
+ margin: 0;
+}
+.assetItem{
+ display: flex;
+ height: 52px;
+ max-width: 600px;
+ width: 90%;
+ margin: 0 auto;
+ cursor: pointer;
+ border-radius: 4px;
+ margin-bottom: 12px;
+}
+.data{
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ flex-grow: 1;
+ margin-left: 16px;
+}
+.data h3{
+ margin: 0;
+ font-size: 18px;
+font-style: normal;
+font-weight: 500;
+}
+.assetList{
+ width: 100%;
+ max-height: calc(100vh - 500px);
+ height: calc(100vh - 500px);
+ overflow: auto;
+ margin: 36px 0;
+ margin-bottom: 0;
+}
+
+.clear{
+ text-decoration: underline;
+ width: 230px;
+ margin-bottom: 42px;
+ margin-top: 16px;
+ text-align: center;
+ cursor: pointer;
+}
+.clear:hover{
+ color:#17A9E8
+}
+.logo{
+ width: 60px;
+ height: 60px;
+ margin-right: 16px;
+}
+
+.close{
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin-left:16px;
+ margin-right: 16px;
+}
+.gasContainer{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+}
\ No newline at end of file
diff --git a/packages/nextjs/components/flashbotRecovery/TransactionBundleStep/transactionBundleStep.tsx b/packages/nextjs/components/flashbotRecovery/TransactionBundleStep/transactionBundleStep.tsx
new file mode 100644
index 0000000..aa921b7
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/TransactionBundleStep/transactionBundleStep.tsx
@@ -0,0 +1,129 @@
+import React, { Dispatch, SetStateAction, useEffect } from "react";
+import Image from "next/image";
+import GasSvg from "../../../public/assets/flashbotRecovery/gas.svg";
+import { CustomButton } from "../CustomButton/CustomButton";
+import styles from "./transactionBundleStep.module.css";
+import { BigNumber, ethers } from "ethers";
+import { motion } from "framer-motion";
+import { RecoveryTx } from "~~/types/business";
+import { useInterval } from "usehooks-ts";
+import { useGasEstimation } from "~~/hooks/flashbotRecoveryBundle/useGasEstimation";
+
+interface IProps {
+ isVisible: boolean;
+ clear: () => void;
+ transactions: RecoveryTx[];
+ onAddMore: () => void;
+ modifyTransactions: Dispatch>;
+ onSubmit:(val:BigNumber) => void;
+ totalGasEstimate:BigNumber;
+ setTotalGasEstimate:Dispatch>;
+}
+
+export const TransactionBundleStep = ({
+ clear,
+ onAddMore,
+ isVisible,
+ onSubmit,
+ transactions,
+ modifyTransactions,
+ totalGasEstimate,
+ setTotalGasEstimate
+}: IProps) => {
+
+ const {estimateTotalGasPrice} = useGasEstimation()
+
+ useEffect(() => {
+ if(transactions.length == 0){
+ return
+ }
+ estimateTotalGasPrice(transactions, removeUnsignedTx).then(setTotalGasEstimate)
+ }, [transactions])
+
+ const updateTotalGasEstimate = async () => {
+ setTotalGasEstimate(await estimateTotalGasPrice(transactions, removeUnsignedTx));
+ };
+
+ useInterval(() => {
+ if(transactions.length == 0){
+ return
+ }
+ updateTotalGasEstimate();
+ }, 5000);
+
+ const removeUnsignedTx = (txId: number) => {
+ modifyTransactions((prev: RecoveryTx[]) => {
+ if (txId < 0 || txId > prev.length) {
+ return prev.filter(a => a);
+ }
+ delete prev[txId];
+
+ const newUnsignedTxArr = prev.filter(a => a);
+ return newUnsignedTxArr;
+ });
+ };
+
+ if (!isVisible) {
+ return <>>;
+ }
+ return (
+
+ Your transactions
+
+
+ {transactions.map((item, i) => {
+ return removeUnsignedTx(i)} tx={item} />;
+ })}
+
+ clear()}>
+ Clear all
+
+
+
+
+
{ethers.utils.formatEther(totalGasEstimate.toString())}
+
+
+ onAddMore()} />
+
+ onSubmit(totalGasEstimate)} />
+
+ );
+};
+
+interface ITransactionProps {
+ onDelete: () => void;
+ tx?: RecoveryTx;
+}
+
+const TransactionItem = ({ onDelete, tx }: ITransactionProps) => {
+ const getTitle = () => {
+ if (!tx) {
+ return "";
+ }
+ if (["erc1155", "erc721"].indexOf(tx.type) != -1) {
+ //@ts-ignore
+ return `${tx.info} (${tx.tokenId!})`;
+ }
+ if (tx.type === "erc20") {
+ //@ts-ignore
+ return `${tx.value} ${tx.symbol} `;
+ }
+ return tx.info;
+ };
+
+ return (
+
+
+
{getTitle()}
+
+ onDelete()}>
+ X
+
+
+ );
+};
diff --git a/packages/nextjs/components/flashbotRecovery/tabs/Tabs.tsx b/packages/nextjs/components/flashbotRecovery/tabs/Tabs.tsx
new file mode 100644
index 0000000..7192d18
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/tabs/Tabs.tsx
@@ -0,0 +1,30 @@
+import { useState } from "react";
+import styles from "./tabs.module.css";
+
+interface IProps {
+ children: (active: number) => any;
+ tabTitles: string[];
+}
+
+export const Tabs = ({ children, tabTitles }: IProps) => {
+ const [active, setActive] = useState(0);
+
+ return (
+
+
+ {tabTitles.map((title, i) => (
+ setActive(i)}
+ className={`${styles.tabTitle} ${active !== i ? "text-secondary-content" : "text-secondary bg-neutral"}`}
+ >
+ {title}
+
+ ))}
+
+
+ <>{children(active)}>
+
+
+ );
+};
diff --git a/packages/nextjs/components/flashbotRecovery/tabs/tabs.module.css b/packages/nextjs/components/flashbotRecovery/tabs/tabs.module.css
new file mode 100644
index 0000000..ec2f103
--- /dev/null
+++ b/packages/nextjs/components/flashbotRecovery/tabs/tabs.module.css
@@ -0,0 +1,41 @@
+.tabsContainer {
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ max-width: 940px;
+
+}
+.tabsHeader {
+ display: flex;
+ flex-direction: row;
+ display: flex;
+ width: 100%;
+ max-width: 455px;
+ min-width: 330px;
+ padding: 4px;
+ gap: 4px;
+ border-radius: 8px;
+}
+.tabTitle {
+ line-height: 16px;
+ padding: 7px 0;
+ gap: 10px;
+ border-radius: 8px;
+ width: 100%;
+ text-align: center;
+ box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.05);
+ transition: all 0.3s;
+ cursor: pointer;
+}
+.tabContent {
+ width: 100%;
+}
+
+@media (max-width: 768px) {
+ .tabContent {
+ width: 96%;
+ min-width: 330px;
+ }
+}
diff --git a/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx b/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx
index 981b0d2..fa8ba75 100644
--- a/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx
+++ b/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx
@@ -107,7 +107,7 @@ export const CustomContractWriteForm = ({
}
}}
>
- 🧺 add 🧺
+ add