├── .gitignore ├── .lintstagedrc.js ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-typescript.cjs └── releases │ └── yarn-3.2.3.cjs ├── .yarnrc.yml ├── CONTRIBUTING.md ├── LICENCE ├── README.md ├── assets ├── 1.png ├── 2.png ├── Custom-1.png ├── Custom-2.png ├── ERC1155-1.png ├── ERC20-1.png ├── ERC20-2.png ├── ERC721-1.png ├── ERC721-2.png ├── Failure-1.png ├── Info-1.png ├── Retry-1.png ├── Retry-2.png ├── Retry-3.png ├── Signing-1.png ├── Signing-2.png ├── Signing-3.png ├── Signing-4.png ├── Signing-5.png ├── Signing-6.png ├── Signing-7.png └── Success-1.png ├── docs ├── README.md ├── seq.md └── walkthrough.md ├── package-lock.json ├── package.json ├── packages ├── hardhat │ ├── .env.example │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierrc.json │ ├── Donation.sol │ │ └── DonationMultiSig.ts │ ├── contracts │ │ └── DonationMultiSig.sol │ ├── deploy │ │ ├── 00_deploy_your_contract.ts │ │ └── 99_generateTsAbis.ts │ ├── hardhat.config.ts │ ├── package.json │ ├── scripts │ │ ├── generateAccount.ts │ │ └── listAccount.ts │ └── tsconfig.json └── nextjs │ ├── .env.example │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .gitignore │ ├── .npmrc │ ├── .prettierrc.json │ ├── components │ ├── CustomButton │ │ ├── CustomButton.tsx │ │ └── customButton.module.css │ ├── CustomConnectButton │ │ └── CustomConnectButton.tsx │ ├── CustomPortal │ │ ├── CustomPortal.tsx │ │ └── customPortal.module.css │ ├── Footer.tsx │ ├── MetaHeader.tsx │ ├── Processes │ │ ├── BundlingProcess │ │ │ ├── BundlingProcess.tsx │ │ │ ├── SideBar │ │ │ │ ├── SideBar.tsx │ │ │ │ ├── SideBarStepInfo.tsx │ │ │ │ ├── SideBarSteps.tsx │ │ │ │ └── sidebar.module.css │ │ │ ├── Steps │ │ │ │ ├── AssetSelectionStep │ │ │ │ │ ├── AssetSelectionStep.tsx │ │ │ │ │ ├── AutoDetectedAssets │ │ │ │ │ │ ├── AutoDetectedAssetItem.tsx │ │ │ │ │ │ ├── AutoDetectedAssets.tsx │ │ │ │ │ │ └── autoDetectedAssets.module.css │ │ │ │ │ ├── ManualAssetSelection │ │ │ │ │ │ ├── AbiNinjaFlow │ │ │ │ │ │ │ └── AbiNinjaFlow.tsx │ │ │ │ │ │ ├── BasicFlow │ │ │ │ │ │ │ ├── BasicFlow.tsx │ │ │ │ │ │ │ ├── ERC1155Form.tsx │ │ │ │ │ │ │ ├── ERC20From.tsx │ │ │ │ │ │ │ ├── ERC721Form.tsx │ │ │ │ │ │ │ └── types.d.ts │ │ │ │ │ │ ├── CustomFlow │ │ │ │ │ │ │ └── CustomFlow.tsx │ │ │ │ │ │ ├── ManualAssetSelection.tsx │ │ │ │ │ │ ├── RawFlow │ │ │ │ │ │ │ └── RawFlow.tsx │ │ │ │ │ │ └── manualAssetSelection.module.css │ │ │ │ │ └── assetSelectionStep.module.css │ │ │ │ └── TransactionBundleStep │ │ │ │ │ ├── TransactionBundleStep.tsx │ │ │ │ │ ├── TransactionItem.tsx │ │ │ │ │ └── transactionBundleStep.module.css │ │ │ └── bundlingProcess.module.css │ │ ├── HackedAddressProcess │ │ │ ├── HackedAddressProcess.tsx │ │ │ └── hackedAddressProcess.module.css │ │ └── RecoveryProcess │ │ │ ├── RecoveryProcess.tsx │ │ │ └── recoveryProcess.module.css │ ├── Spinner.tsx │ ├── SwitchTheme.tsx │ ├── scaffold-eth │ │ ├── Address.tsx │ │ ├── Balance.tsx │ │ ├── BlockieAvatar.tsx │ │ ├── Contract │ │ │ ├── ContractInput.tsx │ │ │ ├── ContractReadMethods.tsx │ │ │ ├── ContractVariables.tsx │ │ │ ├── DisplayVariable.tsx │ │ │ ├── ReadOnlyFunctionForm.tsx │ │ │ ├── TxReceipt.tsx │ │ │ ├── WriteOnlyFunctionForm.tsx │ │ │ ├── index.tsx │ │ │ ├── utilsContract.tsx │ │ │ └── utilsDisplay.tsx │ │ ├── Faucet.tsx │ │ ├── FaucetButton.tsx │ │ ├── Input │ │ │ ├── AddressInput.tsx │ │ │ ├── Bytes32Input.tsx │ │ │ ├── BytesInput.tsx │ │ │ ├── EtherInput.tsx │ │ │ ├── InputBase.tsx │ │ │ ├── IntegerInput.tsx │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── RainbowKitCustomConnectButton.tsx │ │ └── index.tsx │ └── tabs │ │ ├── Tabs.tsx │ │ └── tabs.module.css │ ├── generated │ └── deployedContracts.ts │ ├── hooks │ ├── flashbotRecoveryBundle │ │ ├── useAutodetectAssets.ts │ │ ├── useGasEstimation.ts │ │ ├── useRecoveryProcess.ts │ │ └── useShowError.tsx │ └── scaffold-eth │ │ ├── index.ts │ │ ├── useAccountBalance.ts │ │ ├── useAnimationConfig.ts │ │ ├── useBurnerWallet.ts │ │ ├── useContractLogs.ts │ │ ├── useDeployedContractInfo.ts │ │ ├── useFetchBlocks.ts │ │ ├── useNativeCurrencyPrice.ts │ │ ├── useNetworkColor.ts │ │ ├── useOutsideClick.ts │ │ ├── useScaffoldContract.ts │ │ ├── useScaffoldContractRead.ts │ │ ├── useScaffoldContractWrite.ts │ │ ├── useScaffoldEventHistory.ts │ │ ├── useScaffoldEventSubscriber.ts │ │ └── useTransactor.tsx │ ├── interfaces.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── relay.ts │ └── index.tsx │ ├── postcss.config.js │ ├── public │ ├── assets │ │ ├── flashbotRecovery │ │ │ ├── back.svg │ │ │ ├── clock.svg │ │ │ ├── close.svg │ │ │ ├── coin.svg │ │ │ ├── empty.svg │ │ │ ├── error.svg │ │ │ ├── gas-illustration.svg │ │ │ ├── gas.svg │ │ │ ├── hacked.svg │ │ │ ├── illustration.svg │ │ │ ├── logo.svg │ │ │ ├── logout.svg │ │ │ ├── multiple-sign-illustration.svg │ │ │ ├── network-change.svg │ │ │ ├── refresh.svg │ │ │ ├── safe.svg │ │ │ ├── sign-illustration.svg │ │ │ ├── success.svg │ │ │ ├── telegram.svg │ │ │ ├── thumbnail.jpg │ │ │ ├── tips.svg │ │ │ ├── transactions.svg │ │ │ ├── twitter.svg │ │ │ └── video.svg │ │ ├── gradient-bg.png │ │ └── switch-button-on.png │ ├── favicon.ico │ ├── favicon.png │ ├── logo.svg │ └── thumbnail.png │ ├── react-app-env.d.ts │ ├── scaffold.config.ts │ ├── services │ ├── store │ │ └── store.ts │ └── web3 │ │ ├── wagmi-burner │ │ ├── BurnerConnector.ts │ │ ├── BurnerConnectorErrors.ts │ │ ├── BurnerConnectorTypes.ts │ │ └── burnerWalletConfig.ts │ │ ├── wagmiConfig.tsx │ │ └── wagmiConnectors.tsx │ ├── styles │ └── globals.css │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── types │ ├── abitype │ │ └── abi.d.ts │ ├── business.d.ts │ └── enums.ts │ └── utils │ ├── abiNinjaFlowUtils.ts │ ├── constants.ts │ └── scaffold-eth │ ├── block.ts │ ├── common.ts │ ├── contract.ts │ ├── contractNames.ts │ ├── decodeTxData.ts │ ├── fetchPriceFromUniswap.ts │ ├── index.ts │ ├── networks.ts │ └── notification.tsx └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # dependencies, yarn, etc 4 | # yarn / eslint 5 | .yarn/* 6 | !.yarn/patches 7 | !.yarn/plugins 8 | !.yarn/releases 9 | !.yarn/sdks 10 | !.yarn/versions 11 | .eslintcache 12 | .vscode/** 13 | .DS_Store 14 | notes.txt 15 | packages/nextjs/.env 16 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const buildNextEslintCommand = (filenames) => 4 | `yarn next:lint --fix --file ${filenames 5 | .map((f) => path.relative(path.join("packages", "nextjs"), f)) 6 | .join(" --file ")}`; 7 | 8 | const checkTypesNextCommand = () => "yarn next:check-types"; 9 | 10 | const buildHardhatEslintCommand = (filenames) => 11 | `yarn hardhat:lint-staged --fix ${filenames 12 | .map((f) => path.relative(path.join("packages", "hardhat"), f)) 13 | .join(" ")}`; 14 | 15 | module.exports = { 16 | "packages/nextjs/**/*.{ts,tsx}": [ 17 | buildNextEslintCommand, 18 | checkTypesNextCommand, 19 | ], 20 | "packages/hardhat/**/*.{ts,tsx}": [buildHardhatEslintCommand], 21 | }; 22 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableColors: true 2 | 3 | nmHoistingLimits: workspaces 4 | 5 | nodeLinker: node-modules 6 | 7 | plugins: 8 | - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs 9 | spec: "@yarnpkg/plugin-typescript" 10 | 11 | yarnPath: .yarn/releases/yarn-3.2.3.cjs 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome to Scaffold-ETH 2 Contributing Guide 2 | 3 | Thank you for investing your time in contributing to Scaffold-ETH 2! 4 | 5 | This guide aims to provide an overview of the contribution workflow to help us make the contribution process effective for everyone involved. 6 | 7 | ## About the Project 8 | 9 | Scaffold-ETH 2 is a minimal and forkable repo providing builders with a starter kit to build decentralized applications on Ethereum. 10 | 11 | Read the [README](README.md) to get an overview of the project. 12 | 13 | ### Vision 14 | 15 | The goal of Scaffold-ETH 2 is to provide the primary building blocks for a decentralized application. 16 | 17 | The repo can be forked to include integrations and more features, but we want to keep the master branch simple and minimal. 18 | 19 | ### Project Status 20 | 21 | The project is under active development. 22 | 23 | You can view the open Issues, follow the development process and contribute to the project. 24 | 25 | ## Getting started 26 | 27 | You can contribute to this repo in many ways: 28 | 29 | - Solve open issues 30 | - Report bugs or feature requests 31 | - Improve the documentation 32 | 33 | Contributions are made via Issues and Pull Requests (PRs). A few general guidelines for contributions: 34 | 35 | - Search for existing Issues and PRs before creating your own. 36 | - Contributions should only fix/add the functionality in the issue OR address style issues, not both. 37 | - If you're running into an error, please give context. Explain what you're trying to do and how to reproduce the error. 38 | - Please use the same formatting in the code repository. You can configure your IDE to do it by using the prettier / linting config files included in each package. 39 | - If applicable, please edit the README.md file to reflect the changes. 40 | 41 | ### Issues 42 | 43 | Issues should be used to report problems, request a new feature, or discuss potential changes before a PR is created. 44 | 45 | #### Solve an issue 46 | 47 | Scan through our [existing issues](https://github.com/scaffold-eth/scaffold-eth-2/issues) to find one that interests you. 48 | 49 | If a contributor is working on the issue, they will be assigned to the individual. If you find an issue to work on, you are welcome to assign it to yourself and open a PR with a fix for it. 50 | 51 | #### Create a new issue 52 | 53 | If a related issue doesn't exist, you can open a new issue. 54 | 55 | Some tips to follow when you are creating an issue: 56 | 57 | - Provide as much context as possible. Over-communicate to give the most details to the reader. 58 | - Include the steps to reproduce the issue or the reason for adding the feature. 59 | - Screenshots, videos etc., are highly appreciated. 60 | 61 | ### Pull Requests 62 | 63 | #### Pull Request Process 64 | 65 | We follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr) 66 | 67 | 1. Fork the repo 68 | 2. Clone the project 69 | 3. Create a new branch with a descriptive name 70 | 4. Commit your changes to the new branch 71 | 5. Push changes to your fork 72 | 6. Open a PR in our repository and tag one of the maintainers to review your PR 73 | 74 | Here are some tips for a high-quality pull request: 75 | 76 | - Create a title for the PR that accurately defines the work done. 77 | - Structure the description neatly to make it easy to consume by the readers. For example, you can include bullet points and screenshots instead of having one large paragraph. 78 | - Add the link to the issue if applicable. 79 | - Have a good commit message that summarises the work done. 80 | 81 | Once you submit your PR: 82 | 83 | - We may ask questions, request additional information or ask for changes to be made before a PR can be merged. Please note that these are to make the PR clear for everyone involved and aims to create a frictionless interaction process. 84 | - As you update your PR and apply changes, mark each conversation resolved. 85 | 86 | Once the PR is approved, we'll "squash-and-merge" to keep the git commit history clean. 87 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 BuidlGuidl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/1.png -------------------------------------------------------------------------------- /assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/2.png -------------------------------------------------------------------------------- /assets/Custom-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Custom-1.png -------------------------------------------------------------------------------- /assets/Custom-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Custom-2.png -------------------------------------------------------------------------------- /assets/ERC1155-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/ERC1155-1.png -------------------------------------------------------------------------------- /assets/ERC20-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/ERC20-1.png -------------------------------------------------------------------------------- /assets/ERC20-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/ERC20-2.png -------------------------------------------------------------------------------- /assets/ERC721-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/ERC721-1.png -------------------------------------------------------------------------------- /assets/ERC721-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/ERC721-2.png -------------------------------------------------------------------------------- /assets/Failure-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Failure-1.png -------------------------------------------------------------------------------- /assets/Info-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Info-1.png -------------------------------------------------------------------------------- /assets/Retry-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Retry-1.png -------------------------------------------------------------------------------- /assets/Retry-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Retry-2.png -------------------------------------------------------------------------------- /assets/Retry-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Retry-3.png -------------------------------------------------------------------------------- /assets/Signing-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Signing-1.png -------------------------------------------------------------------------------- /assets/Signing-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Signing-2.png -------------------------------------------------------------------------------- /assets/Signing-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Signing-3.png -------------------------------------------------------------------------------- /assets/Signing-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Signing-4.png -------------------------------------------------------------------------------- /assets/Signing-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Signing-5.png -------------------------------------------------------------------------------- /assets/Signing-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Signing-6.png -------------------------------------------------------------------------------- /assets/Signing-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Signing-7.png -------------------------------------------------------------------------------- /assets/Success-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuidlGuidl/hacked-wallet-recovery/f7f65cadfe041676ecda47b400b5f70a17ba081a/assets/Success-1.png -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Story and Caveats 2 | 3 | Compromised accounts need some funding to pay for the gas, so that they can transfer the locked assets to a safe account. Sending ether to hacked accounts can be frustrating as the malicious actor will drain any funds immediately, because they're operating bots that actively monitor the mempool. 4 | 5 | Flashbots operate on their own RPC network in which they receive bundles of transactions and they can push these bundles to Ethereum network without exposing individual transfers in the mempool. This prevents malicious actors from sniffing the intermediary funding transactions, making white hat recoveries possible. 6 | 7 | See the following links, but beware Flashbots have many use cases, so don't get lost within 8 | 9 | - [About FlashBots](https://docs.flashbots.net/) 10 | - [Understanding Bundles](https://docs.flashbots.net/flashbots-auction/searchers/advanced/understanding-bundles) 11 | 12 | Users need to build a bundle of signed transactions to send to the Flashbots network. Flashbots expose a [bundle caching API](https://docs.flashbots.net/flashbots-protect/rpc/bundle-cache) so that users can use their wallets to sign transactions one by one. These transactions will be cached by Flashbots' infrastructure remotely, to be submitted with a POST request. We abstract-away all these complexities and ease the ordering of signing and account switching operations, and provide instructive modals to the user to reduce error rate. 13 | 14 | Another point to note is these transactions won't be listed on Etherscan, and debugging a failing bundle can be a trouble. Users are recommended to build transactions carefully as we're lacking a debug mechanism. 15 | 16 | ## How it works 17 | 18 | flashbot-recovery-bundler introduces the concept of **recovery basket**, which is a list of transactions **from the compromised account to asset contract accounts**. Building these transactions and adding them the basket does not require wallet interaction, as it purely happens at the front-end. 19 | 20 | When the basket is done, we get an accurate estimation on the gas price. This gas price has nothing to do with the priority fee, and purely represents the total gas price to be paid to Ethereum validators. This estimation is crucial because should any transaction in the basket fail, the whole bundle fails. 21 | 22 | Users can then **start signing**, and the front-end will start a sequence of events consisting of 23 | 24 | - Switching to a personal Flashbot RPC, 25 | - Switching to the funding account, 26 | - Signing the gas-funding transaction, 27 | - Switching to the compromised account, 28 | - Signing all the recovery transactions one by one, 29 | - Submitting the bundle, 30 | - Watching the transactions and inform upon success or failure 31 | 32 | ## Handling priority fees 33 | 34 | Flashbots might require high bids to include your bundle in a block. It's recommended that **for each transaction you sign**, you set a very generous priority fee. When network is not congested, for example, 10 GWEI can be a good bid. In times of congestion this might need to go up way higher (no exact numbers). 35 | 36 | You will now you didn't set a nice bid when your bundle fails, and in that case, it's recommended to increase the priority fee and max base fee even further. See the screenshots in the walkthrough to have a more concrete idea. 37 | 38 | ## Careful with pending transactions 39 | 40 | If the compromised account or the funding account has any pending transactions in the wallet, please **clear activity data**. Otherwise when you join a new personal Flashbot RPC network, these unwanted transactions will also be included in the new bundle you're trying to build. This can cause trouble and be hard to debug. 41 | 42 | ## Messing up the state 43 | 44 | If you think the front-end does not behave logically, you might have messed with the state. The build uses local storage, so clearing the cookies and refreshing the page helps. 45 | 46 | ## Local Development 47 | 48 | Built on [scaffold-eth-2](https://github.com/scaffold-eth/scaffold-eth-2), so the same technical details apply here. 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "se-2", 3 | "version": "0.0.1", 4 | "private": true, 5 | "workspaces": { 6 | "packages": [ 7 | "packages/hardhat", 8 | "packages/nextjs" 9 | ] 10 | }, 11 | "scripts": { 12 | "account": "yarn workspace @se-2/hardhat account", 13 | "chain": "yarn workspace @se-2/hardhat chain", 14 | "fork": "yarn workspace @se-2/hardhat fork", 15 | "deploy": "yarn workspace @se-2/hardhat deploy", 16 | "verify": "yarn workspace @se-2/hardhat verify", 17 | "compile": "yarn workspace @se-2/hardhat compile", 18 | "generate": "yarn workspace @se-2/hardhat generate", 19 | "hardhat:lint": "yarn workspace @se-2/hardhat lint", 20 | "hardhat:lint-staged": "yarn workspace @se-2/hardhat lint-staged", 21 | "hardhat:test": "yarn workspace @se-2/hardhat test", 22 | "start": "yarn workspace @se-2/nextjs dev", 23 | "next:lint": "yarn workspace @se-2/nextjs lint", 24 | "next:format": "yarn workspace @se-2/nextjs format", 25 | "next:check-types": "yarn workspace @se-2/nextjs check-types", 26 | "postinstall": "husky install", 27 | "precommit": "lint-staged", 28 | "vercel": "yarn workspace @se-2/nextjs vercel", 29 | "vercel:yolo": "yarn workspace @se-2/nextjs vercel:yolo" 30 | }, 31 | "packageManager": "yarn@3.2.3", 32 | "devDependencies": { 33 | "husky": "^8.0.1", 34 | "lint-staged": "^13.0.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/hardhat/.env.example: -------------------------------------------------------------------------------- 1 | # This is a template for the environment variables 2 | # To use this template, copy this file, rename it .env, and fill in the values 3 | ALCHEMY_API_KEY= 4 | DEPLOYER_PRIVATE_KEY= 5 | ETHERSCAN_API_KEY= -------------------------------------------------------------------------------- /packages/hardhat/.eslintignore: -------------------------------------------------------------------------------- 1 | # folders 2 | artifacts 3 | cache 4 | contracts 5 | node_modules/ 6 | typechain-types 7 | # files 8 | **/*.json 9 | -------------------------------------------------------------------------------- /packages/hardhat/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "parser": "@typescript-eslint/parser", 6 | "extends": ["plugin:prettier/recommended", "plugin:@typescript-eslint/recommended"], 7 | "rules": { 8 | "@typescript-eslint/no-unused-vars": ["error"], 9 | "@typescript-eslint/no-explicit-any": ["off"], 10 | "prettier/prettier": [ 11 | "warn", 12 | { 13 | "endOfLine": "auto" 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/hardhat/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | typechain-types 7 | temp 8 | 9 | #Hardhat files 10 | cache 11 | artifacts 12 | 13 | #zkSync files 14 | artifacts-zk 15 | cache-zk 16 | 17 | deployments 18 | -------------------------------------------------------------------------------- /packages/hardhat/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "printWidth": 120, 4 | "tabWidth": 2, 5 | "trailingComma": "all", 6 | "overrides": [ 7 | { 8 | "files": "*.sol", 9 | "options": { 10 | "printWidth": 80, 11 | "tabWidth": 4, 12 | "useTabs": true, 13 | "singleQuote": false, 14 | "bracketSpacing": true, 15 | "explicitTypes": "always" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/hardhat/deploy/00_deploy_your_contract.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { DeployFunction } from "hardhat-deploy/types"; 3 | 4 | /** 5 | * Deploys a contract named "DonationMultiSig" using the deployer account and 6 | * constructor arguments set to the deployer address 7 | * 8 | * @param hre HardhatRuntimeEnvironment object. 9 | */ 10 | const deployDonationMultiSig: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 11 | /* 12 | On localhost, the deployer account is the one that comes with Hardhat, which is already funded. 13 | 14 | When deploying to live networks (e.g `yarn deploy --network goerli`), the deployer account 15 | should have sufficient balance to pay for the gas fees for contract creation. 16 | 17 | You can generate a random account with `yarn generate` which will fill DEPLOYER_PRIVATE_KEY 18 | with a random private key in the .env file (then used on hardhat.config.ts) 19 | You can run the `yarn account` command to check your balance in every network. 20 | */ 21 | const { deployer } = await hre.getNamedAccounts(); 22 | const { deploy } = hre.deployments; 23 | 24 | await deploy("DonationMultiSig", { 25 | from: deployer, 26 | args: [ 27 | ["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"], 28 | [10, 10], 29 | ], 30 | log: true, 31 | autoMine: true, 32 | }); 33 | 34 | // Get the deployed contract 35 | // const DonationMultiSig = await hre.ethers.getContract("DonationMultiSig", deployer); 36 | }; 37 | 38 | export default deployDonationMultiSig; 39 | 40 | // Tags are useful if you have multiple deploy files and only want to run one of them. 41 | // e.g. yarn deploy --tags DonationMultiSig 42 | deployDonationMultiSig.tags = ["DonationMultiSig"]; 43 | -------------------------------------------------------------------------------- /packages/hardhat/deploy/99_generateTsAbis.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * DON'T MODIFY OR DELETE THIS SCRIPT (unless you know what you're doing) 3 | * 4 | * This script generates the file containing the contracts Abi definitions. 5 | * These definitions are used to derive the types needed in the custom scaffold-eth hooks, for example. 6 | * This script should run as the last deploy script. 7 | * */ 8 | 9 | import * as fs from "fs"; 10 | import prettier from "prettier"; 11 | import { DeployFunction } from "hardhat-deploy/types"; 12 | 13 | function getDirectories(path: string) { 14 | return fs 15 | .readdirSync(path, { withFileTypes: true }) 16 | .filter(dirent => dirent.isDirectory()) 17 | .map(dirent => dirent.name); 18 | } 19 | 20 | function getContractNames(path: string) { 21 | return fs 22 | .readdirSync(path, { withFileTypes: true }) 23 | .filter(dirent => dirent.isFile() && dirent.name.endsWith(".json")) 24 | .map(dirent => dirent.name.split(".")[0]); 25 | } 26 | 27 | const DEPLOYMENTS_DIR = "./deployments"; 28 | 29 | function getContractDataFromDeployments() { 30 | if (!fs.existsSync(DEPLOYMENTS_DIR)) { 31 | throw Error("At least one other deployment script should exist to generate an actual contract."); 32 | } 33 | const output = {} as Record; 34 | for (const chainName of getDirectories(DEPLOYMENTS_DIR)) { 35 | const chainId = fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/.chainId`).toString(); 36 | const contracts = {} as Record; 37 | for (const contractName of getContractNames(`${DEPLOYMENTS_DIR}/${chainName}`)) { 38 | const { abi, address } = JSON.parse( 39 | fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/${contractName}.json`).toString(), 40 | ); 41 | contracts[contractName] = { address, abi }; 42 | } 43 | output[chainId] = [ 44 | { 45 | chainId, 46 | name: chainName, 47 | contracts, 48 | }, 49 | ]; 50 | } 51 | return output; 52 | } 53 | 54 | /** 55 | * Generates the TypeScript contract definition file based on the json output of the contract deployment scripts 56 | * This script should be run last. 57 | */ 58 | const generateTsAbis: DeployFunction = async function () { 59 | const TARGET_DIR = "../nextjs/generated/"; 60 | const allContractsData = getContractDataFromDeployments(); 61 | 62 | const fileContent = Object.entries(allContractsData).reduce((content, [chainId, chainConfig]) => { 63 | return `${content}${parseInt(chainId).toFixed(0)}:${JSON.stringify(chainConfig, null, 2)},`; 64 | }, ""); 65 | 66 | if (!fs.existsSync(TARGET_DIR)) { 67 | fs.mkdirSync(TARGET_DIR); 68 | } 69 | fs.writeFileSync( 70 | `${TARGET_DIR}deployedContracts.ts`, 71 | prettier.format(`const contracts = {${fileContent}} as const; \n\n export default contracts`, { 72 | parser: "typescript", 73 | }), 74 | ); 75 | 76 | console.log(`📝 Updated TypeScript contract definition file on ${TARGET_DIR}deployedContracts.ts`); 77 | }; 78 | 79 | export default generateTsAbis; 80 | 81 | // Tags are useful if you have multiple deploy files and only want to run one of them. 82 | // e.g. yarn deploy --tags generateTsAbis 83 | generateTsAbis.tags = ["generateTsAbis"]; 84 | 85 | generateTsAbis.runAtTheEnd = true; 86 | -------------------------------------------------------------------------------- /packages/hardhat/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | dotenv.config(); 3 | import { HardhatUserConfig } from "hardhat/config"; 4 | import "@nomicfoundation/hardhat-toolbox"; 5 | import "hardhat-deploy"; 6 | import "@matterlabs/hardhat-zksync-solc"; 7 | import "@matterlabs/hardhat-zksync-verify"; 8 | 9 | // If not set, it uses ours Alchemy's default API key. 10 | // You can get your own at https://dashboard.alchemyapi.io 11 | const providerApiKey = process.env.ALCHEMY_API_KEY || "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF"; 12 | // If not set, it uses the hardhat account 0 private key. 13 | const deployerPrivateKey = 14 | process.env.DEPLOYER_PRIVATE_KEY ?? "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; 15 | // If not set, it uses ours Etherscan default API key. 16 | const etherscanApiKey = process.env.ETHERSCAN_API_KEY || "DNXJA8RX2Q3VZ4URQIWP7Z68CJXQZSC6AW"; 17 | 18 | const config: HardhatUserConfig = { 19 | solidity: { 20 | version: "0.8.17", 21 | settings: { 22 | optimizer: { 23 | enabled: true, 24 | // https://docs.soliditylang.org/en/latest/using-the-compiler.html#optimizer-options 25 | runs: 200, 26 | }, 27 | }, 28 | }, 29 | defaultNetwork: "goerli", 30 | namedAccounts: { 31 | deployer: { 32 | // By default, it will take the first Hardhat account as the deployer 33 | default: 0, 34 | }, 35 | }, 36 | networks: { 37 | // View the networks that are pre-configured. 38 | // If the network you are looking for is not here you can add new network settings 39 | hardhat: { 40 | forking: { 41 | url: `https://eth-mainnet.alchemyapi.io/v2/${providerApiKey}`, 42 | enabled: process.env.MAINNET_FORKING_ENABLED === "true", 43 | }, 44 | }, 45 | mainnet: { 46 | url: `https://eth-mainnet.alchemyapi.io/v2/${providerApiKey}`, 47 | accounts: [deployerPrivateKey], 48 | }, 49 | sepolia: { 50 | url: `https://eth-sepolia.g.alchemy.com/v2/${providerApiKey}`, 51 | accounts: [deployerPrivateKey], 52 | }, 53 | goerli: { 54 | url: `https://eth-goerli.alchemyapi.io/v2/${providerApiKey}`, 55 | accounts: [deployerPrivateKey], 56 | }, 57 | arbitrum: { 58 | url: `https://arb-mainnet.g.alchemy.com/v2/${providerApiKey}`, 59 | accounts: [deployerPrivateKey], 60 | }, 61 | arbitrumGoerli: { 62 | url: `https://arb-goerli.g.alchemy.com/v2/${providerApiKey}`, 63 | accounts: [deployerPrivateKey], 64 | }, 65 | optimism: { 66 | url: `https://opt-mainnet.g.alchemy.com/v2/${providerApiKey}`, 67 | accounts: [deployerPrivateKey], 68 | }, 69 | optimismGoerli: { 70 | url: `https://opt-goerli.g.alchemy.com/v2/${providerApiKey}`, 71 | accounts: [deployerPrivateKey], 72 | }, 73 | polygon: { 74 | url: `https://polygon-mainnet.g.alchemy.com/v2/${providerApiKey}`, 75 | accounts: [deployerPrivateKey], 76 | }, 77 | polygonMumbai: { 78 | url: `https://polygon-mumbai.g.alchemy.com/v2/${providerApiKey}`, 79 | accounts: [deployerPrivateKey], 80 | }, 81 | zkSyncTestnet: { 82 | url: "https://testnet.era.zksync.dev", 83 | zksync: true, 84 | accounts: [deployerPrivateKey], 85 | verifyURL: "https://zksync2-testnet-explorer.zksync.dev/contract_verification", 86 | }, 87 | zkSync: { 88 | url: "https://mainnet.era.zksync.io", 89 | zksync: true, 90 | accounts: [deployerPrivateKey], 91 | verifyURL: "https://zksync2-mainnet-explorer.zksync.io/contract_verification", 92 | }, 93 | }, 94 | verify: { 95 | etherscan: { 96 | apiKey: `${etherscanApiKey}`, 97 | }, 98 | }, 99 | }; 100 | 101 | export default config; 102 | -------------------------------------------------------------------------------- /packages/hardhat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@se-2/hardhat", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "account": "hardhat run scripts/listAccount.ts", 6 | "chain": "hardhat node --network hardhat --no-deploy", 7 | "compile": "hardhat compile", 8 | "deploy": "hardhat deploy", 9 | "fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat --no-deploy", 10 | "generate": "hardhat run scripts/generateAccount.ts", 11 | "lint": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore ./*.ts ./deploy/**/*.ts ./scripts/**/*.ts ./test/**/*.ts", 12 | "lint-staged": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore", 13 | "test": "REPORT_GAS=true hardhat test --network hardhat", 14 | "verify": "hardhat etherscan-verify" 15 | }, 16 | "devDependencies": { 17 | "@ethersproject/abi": "^5.7.0", 18 | "@ethersproject/providers": "^5.7.1", 19 | "@matterlabs/hardhat-zksync-solc": "^0.3.17", 20 | "@matterlabs/hardhat-zksync-verify": "^0.1.8", 21 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.3", 22 | "@nomicfoundation/hardhat-network-helpers": "^1.0.6", 23 | "@nomicfoundation/hardhat-toolbox": "^2.0.0", 24 | "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@^0.3.0-beta.13", 25 | "@nomiclabs/hardhat-etherscan": "^3.1.0", 26 | "@typechain/ethers-v5": "^10.1.0", 27 | "@typechain/hardhat": "^6.1.3", 28 | "@types/eslint": "^8", 29 | "@types/mocha": "^9.1.1", 30 | "@types/prettier": "^2", 31 | "@types/qrcode": "^1", 32 | "@typescript-eslint/eslint-plugin": "latest", 33 | "@typescript-eslint/parser": "latest", 34 | "chai": "^4.3.6", 35 | "eslint": "^8.26.0", 36 | "eslint-config-prettier": "^8.5.0", 37 | "eslint-plugin-prettier": "^4.2.1", 38 | "ethers": "^5.7.1", 39 | "hardhat": "^2.11.2", 40 | "hardhat-deploy": "^0.11.26", 41 | "hardhat-gas-reporter": "^1.0.9", 42 | "prettier": "^2.8.4", 43 | "solidity-coverage": "^0.8.2", 44 | "ts-node": "^10.9.1", 45 | "typechain": "^8.1.0", 46 | "typescript": "^4.9.5" 47 | }, 48 | "dependencies": { 49 | "@openzeppelin/contracts": "^4.8.1", 50 | "dotenv": "^16.0.3", 51 | "envfile": "^6.18.0", 52 | "qrcode": "^1.5.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/hardhat/scripts/generateAccount.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { parse, stringify } from "envfile"; 3 | import * as fs from "fs"; 4 | 5 | const envFilePath = "./.env"; 6 | 7 | /** 8 | * Generate a new random private key and write it to the .env file 9 | * @param existingEnvConfig 10 | */ 11 | const setNewEnvConfig = (existingEnvConfig = {}) => { 12 | console.log("👛 Generating new Wallet"); 13 | const randomWallet = ethers.Wallet.createRandom(); 14 | 15 | const newEnvConfig = { 16 | ...existingEnvConfig, 17 | DEPLOYER_PRIVATE_KEY: randomWallet.privateKey, 18 | }; 19 | 20 | // Store in .env 21 | fs.writeFileSync(envFilePath, stringify(newEnvConfig)); 22 | console.log("📄 Private Key saved to packages/hardhat/.env file"); 23 | }; 24 | 25 | async function main() { 26 | if (!fs.existsSync(envFilePath)) { 27 | // No .env file yet. 28 | setNewEnvConfig(); 29 | return; 30 | } 31 | 32 | // .env file exists 33 | const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString()); 34 | if (existingEnvConfig.DEPLOYER_PRIVATE_KEY) { 35 | console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file"); 36 | return; 37 | } 38 | 39 | setNewEnvConfig(existingEnvConfig); 40 | } 41 | 42 | main().catch(error => { 43 | console.error(error); 44 | process.exitCode = 1; 45 | }); 46 | -------------------------------------------------------------------------------- /packages/hardhat/scripts/listAccount.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | dotenv.config(); 3 | import { ethers, Wallet } from "ethers"; 4 | import QRCode from "qrcode"; 5 | import { config } from "hardhat"; 6 | 7 | async function main() { 8 | const privateKey = process.env.DEPLOYER_PRIVATE_KEY; 9 | 10 | if (!privateKey) { 11 | console.log("🚫️ You don't have a deployer account. Run `yarn generate` first"); 12 | return; 13 | } 14 | 15 | // Get account from private key. 16 | const wallet = new Wallet(privateKey); 17 | const address = wallet.address; 18 | console.log(await QRCode.toString(address, { type: "terminal", small: true })); 19 | console.log("Public address:", address, "\n"); 20 | 21 | // Balance on each network 22 | const availableNetworks = config.networks; 23 | for (const networkName in availableNetworks) { 24 | try { 25 | const network = availableNetworks[networkName]; 26 | if (!("url" in network)) continue; 27 | const provider = new ethers.providers.JsonRpcProvider(network.url); 28 | const balance = await provider.getBalance(address); 29 | console.log("--", networkName, "-- 📡"); 30 | console.log(" balance:", +ethers.utils.formatEther(balance)); 31 | console.log(" nonce:", +(await provider.getTransactionCount(address))); 32 | } catch (e) { 33 | console.log("Can't connect to network", networkName); 34 | } 35 | } 36 | } 37 | 38 | main().catch(error => { 39 | console.error(error); 40 | process.exitCode = 1; 41 | }); 42 | -------------------------------------------------------------------------------- /packages/hardhat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/nextjs/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ALCHEMY_API_KEY= 2 | NEXT_PUBLIC_NETWORK_KEY= 3 | NEXT_PUBLIC_SHOW_DONATIONS= -------------------------------------------------------------------------------- /packages/nextjs/.eslintignore: -------------------------------------------------------------------------------- 1 | # folders 2 | .next 3 | node_modules/ 4 | # files 5 | **/*.less 6 | **/*.css 7 | **/*.scss 8 | **/*.json 9 | **/*.png 10 | **/*.svg 11 | **/generated/**/* 12 | -------------------------------------------------------------------------------- /packages/nextjs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["next/core-web-vitals", "plugin:prettier/recommended", "plugin:@typescript-eslint/recommended"], 4 | "rules": { 5 | "@typescript-eslint/no-unused-vars": ["warn"], 6 | "@typescript-eslint/no-explicit-any": ["off"], 7 | "@typescript-eslint/ban-ts-comment": ["off"], 8 | "prettier/prettier": [ 9 | "warn", 10 | { 11 | "endOfLine": "auto" 12 | } 13 | ], 14 | "@next/next/no-page-custom-font": ["off"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo -------------------------------------------------------------------------------- /packages/nextjs/.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies = false 2 | -------------------------------------------------------------------------------- /packages/nextjs/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "printWidth": 120, 4 | "tabWidth": 2, 5 | "trailingComma": "all", 6 | "importOrder": ["^react$", "^next/(.*)$", "", "^@heroicons/(.*)$", "^~~/(.*)$"], 7 | "importOrderSortSpecifiers": true 8 | } 9 | -------------------------------------------------------------------------------- /packages/nextjs/components/CustomButton/CustomButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./customButton.module.css"; 3 | 4 | interface IProps { 5 | text: string; 6 | onClick: () => void; 7 | disabled?: boolean; 8 | type: "btn-accent" | "btn-primary"; 9 | } 10 | export const CustomButton = ({ text, onClick, disabled = false, type }: IProps) => { 11 | return ( 12 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/nextjs/components/CustomButton/customButton.module.css: -------------------------------------------------------------------------------- 1 | .button{ 2 | width: 250px; 3 | margin: 0 auto; 4 | padding: 0px 48px; 5 | height: 40px; 6 | font-size: 16px; 7 | font-style: normal; 8 | font-weight: 700; 9 | line-height: normal; 10 | border: 1px solid; 11 | } 12 | .button:hover{ 13 | border: 1px solid; 14 | } 15 | -------------------------------------------------------------------------------- /packages/nextjs/components/CustomConnectButton/CustomConnectButton.tsx: -------------------------------------------------------------------------------- 1 | import { ConnectButton } from "@rainbow-me/rainbowkit"; 2 | import {BlockieAvatar } from "~~/components/scaffold-eth"; 3 | /** 4 | * Custom Wagmi Connect Button (watch balance + custom design) 5 | */ 6 | export const CustomConnectButton = () => { 7 | 8 | return ( 9 | 10 | {({ account, chain, openAccountModal, openConnectModal, mounted }) => { 11 | const connected = mounted && account && chain; 12 | 13 | return ( 14 | <> 15 | {(() => { 16 | if (!connected) { 17 | return ( 18 | 21 | ); 22 | } 23 | 24 | 25 | return ( 26 |
27 |
28 | 36 |
37 |
38 | ); 39 | })()} 40 | 41 | ); 42 | }} 43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/nextjs/components/CustomPortal/CustomPortal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import Image from "next/image"; 3 | import CloseSvg from "../../public/assets/flashbotRecovery/close.svg"; 4 | import styles from "./customPortal.module.css"; 5 | import { motion } from "framer-motion"; 6 | import { createPortal } from "react-dom"; 7 | import { CustomButton } from "~~/components/CustomButton/CustomButton"; 8 | 9 | interface IProps { 10 | title: string; 11 | image?: string; 12 | video?: string; 13 | children?: JSX.Element; 14 | close?: () => void; 15 | description: string; 16 | buttons?: { 17 | text: string; 18 | disabled: boolean; 19 | action: () => void; 20 | isSecondary?:boolean, 21 | }[]; 22 | indicator?: number; 23 | } 24 | export const CustomPortal = ({ indicator, title, image, children, video, description, buttons, close }: IProps) => { 25 | const [mounted, setMounted] = useState(false); 26 | 27 | useEffect(() => { 28 | setMounted(true); 29 | 30 | return () => setMounted(false); 31 | }, []); 32 | const portalSelector = document.querySelector("#myportal"); 33 | if (!portalSelector) { 34 | return <>; 35 | } 36 | 37 | return mounted 38 | ? createPortal( 39 | 45 | 51 | setMounted(false)}> 52 | {" "} 53 | {!!close ? {""} close()} /> : <>} 54 | 55 |
56 |

{title}

57 |
58 | {!!image ? {""} : <>} 59 | {!!indicator ?
Attempting in block #{indicator}
: <>} 60 |
61 | 62 | {!!video ? {""} : <>} 63 |

64 | {!!children ? children : <>} 65 | {buttons?.map((button,i) => { 66 | return button.action()} 72 | /> 73 | })} 74 |
75 |
76 |
, 77 | portalSelector, 78 | ) 79 | : null; 80 | }; 81 | -------------------------------------------------------------------------------- /packages/nextjs/components/CustomPortal/customPortal.module.css: -------------------------------------------------------------------------------- 1 | .modalContainer { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | backdrop-filter: blur(5px); 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | background: rgba(20, 28, 41, 0.4); 12 | z-index: 999; 13 | } 14 | 15 | .modal { 16 | display: inline-flex; 17 | width: 579px; 18 | flex-direction: column; 19 | align-items: center; 20 | gap: 20px; 21 | flex-shrink: 0; 22 | border-radius: 12px; 23 | box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.03); 24 | } 25 | 26 | .modalContent { 27 | display: inline-flex; 28 | flex-direction: column; 29 | align-items: center; 30 | gap: 20px; 31 | flex-shrink: 0; 32 | margin-bottom: 56px; 33 | } 34 | 35 | .close { 36 | align-self: flex-end; 37 | margin-top: 14px; 38 | margin-right: 14px; 39 | cursor: pointer; 40 | padding: 10px; 41 | padding-bottom: 4px; 42 | } 43 | 44 | .title { 45 | font-size: 24px; 46 | font-style: normal; 47 | font-weight: 700; 48 | line-height: 32px; 49 | margin: 0; 50 | } 51 | 52 | .image { 53 | height: 190px; 54 | width: 190px; 55 | margin: auto; 56 | } 57 | .indicator { 58 | text-align: center; 59 | font-size: 20px; 60 | font-style: normal; 61 | font-weight: 600; 62 | line-height: 100%; /* 20.033px */ 63 | } 64 | 65 | .text { 66 | font-size: 18px; 67 | font-style: normal; 68 | font-weight: 400; 69 | line-height: 32px; 70 | width: 80%; 71 | text-align: center; 72 | margin: 0; 73 | max-height: 250px; 74 | overflow-y: auto; 75 | } 76 | .text a{ 77 | text-decoration: underline; 78 | } 79 | -------------------------------------------------------------------------------- /packages/nextjs/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { ChatBubbleOvalLeftEllipsisIcon, HeartIcon } from "@heroicons/react/24/outline"; 2 | import { SwitchTheme } from "~~/components/SwitchTheme"; 3 | 4 | /** 5 | * Site footer 6 | */ 7 | export const Footer = () => { 8 | return ( 9 |
10 |
11 | 12 |
13 | 14 |
15 | 31 |
32 | 33 | 39 | 40 | 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/nextjs/components/MetaHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | 4 | type MetaHeaderProps = { 5 | title?: string; 6 | description?: string; 7 | image?: string; 8 | twitterCard?: string; 9 | children?: React.ReactNode; 10 | }; 11 | 12 | const baseUrl = "https://hackedwalletrecovery.com/"; 13 | 14 | export const MetaHeader = ({ 15 | title = "Hacked Wallet Recovery | Secure Ethereum Asset Recovery Tool", 16 | description = "Instantly recover assets from hacked Ethereum wallets using Flashbots. Bypass malicious sweepers, and securely transfer ERC20, NFTs, and other tokens to safety. Free, open-source tool trusted by the Web3 community.", 17 | image = "thumbnail.png", 18 | twitterCard = "summary_large_image", 19 | children, 20 | }: MetaHeaderProps) => { 21 | const imageUrl = baseUrl + image; 22 | 23 | return ( 24 | 25 | {title && ( 26 | <> 27 | {title} 28 | 29 | 30 | 31 | )} 32 | {description && ( 33 | <> 34 | 35 | 36 | 37 | 38 | )} 39 | {image && ( 40 | <> 41 | 42 | 43 | 44 | )} 45 | {twitterCard && } 46 | 47 | 48 | {children} 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/SideBar/SideBar.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import LogoSvg from "../../../../public/assets/flashbotRecovery/logo.svg"; 3 | import { SideBarSteps } from "./SideBarSteps"; 4 | import styles from "./sidebar.module.css"; 5 | import { Address } from "~~/components/scaffold-eth"; 6 | import LogoutSvg from "~~/public/assets/flashbotRecovery/logout.svg"; 7 | import { BundlingSteps } from "~~/types/enums"; 8 | 9 | interface ISideBar { 10 | activeStep: BundlingSteps; 11 | hackedAddress: string; 12 | safeAddress: string; 13 | } 14 | export const SideBar = ({ activeStep, hackedAddress, safeAddress }: ISideBar) => { 15 | const reload = () => { 16 | localStorage.clear(); 17 | window.location.reload(); 18 | }; 19 | 20 | return ( 21 |
22 |
23 |
24 | 25 |

Hacked Wallet Recovery

26 |
27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 | Hacked Address 35 | {""} reload()} /> 36 |
37 |
38 |
39 |
40 |
41 |
42 | Safe Address 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/SideBar/SideBarStepInfo.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./sidebar.module.css"; 2 | import { BundlingSteps } from "~~/types/enums"; 3 | 4 | interface IStepProps { 5 | index: BundlingSteps; 6 | activeStep: BundlingSteps; 7 | title: string; 8 | description: string; 9 | } 10 | export const SideBarStepInfo = ({ index, activeStep, title, description }: IStepProps) => { 11 | const isActive = index == activeStep; 12 | const isCompleted = activeStep > index; 13 | 14 | return ( 15 |
18 |
19 | {index} 20 |
21 |
22 |

{title}

23 |

{description}

24 |
25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/SideBar/SideBarSteps.tsx: -------------------------------------------------------------------------------- 1 | import { SideBarStepInfo } from "./SideBarStepInfo"; 2 | import styles from "./sidebar.module.css"; 3 | import { BundlingSteps } from "~~/types/enums"; 4 | 5 | interface ISideBar { 6 | activeStep: BundlingSteps; 7 | } 8 | export const SideBarSteps = ({ activeStep }: ISideBar) => { 9 | return ( 10 |
11 | 17 | 23 | 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/SideBar/sidebar.module.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | max-width: 530px; 3 | display: flex; 4 | width: auto; 5 | flex-direction: column; 6 | background-color: #243148; 7 | min-height: 100%; 8 | } 9 | 10 | .sidebarContent { 11 | margin-top: 56px; 12 | margin-left: 10px; 13 | flex-grow: 1; 14 | } 15 | 16 | .logoContainer { 17 | display: flex; 18 | align-items: center; 19 | font-size: 23px; 20 | font-style: normal; 21 | font-weight: 400; 22 | } 23 | .hackedAddressTitle{ 24 | display: flex; 25 | flex-direction: row; 26 | justify-content: space-between; 27 | width: 100%; 28 | } 29 | .logo { 30 | width: 60px; 31 | height: 60px; 32 | margin-right: 16px; 33 | } 34 | .steps { 35 | margin-top: 48px; 36 | margin-right: 43px; 37 | position: relative; 38 | flex-grow: 1; 39 | margin-left: 17px; 40 | 41 | } 42 | .step { 43 | display: flex; 44 | margin-top: 16px; 45 | } 46 | .badge { 47 | border-radius: 100px; 48 | border: 2px solid; 49 | display: flex; 50 | width: 34px; 51 | font-size: 20px; 52 | font-style: normal; 53 | font-weight: 600; 54 | height: 34px; 55 | flex-direction: column; 56 | justify-content: center; 57 | align-items: center; 58 | } 59 | .stepContainer { 60 | margin-left: 16px; 61 | } 62 | .title{ 63 | width: 180px; 64 | margin: 0; 65 | line-height: 28px; 66 | } 67 | .stepTitle { 68 | font-size: 18px; 69 | font-style: normal; 70 | font-weight: 600; 71 | margin: 0; 72 | display: flex; 73 | height: 34px; 74 | align-items: center; 75 | } 76 | .stepDescription { 77 | font-size: 16px; 78 | font-style: normal; 79 | font-weight: 400; 80 | line-height: 26px; 81 | margin: 0; 82 | } 83 | 84 | .addresess { 85 | display: flex; 86 | bottom: 0; 87 | width: 100%; 88 | border-radius: 12px; 89 | justify-content: space-around; 90 | align-self: center; 91 | width: 340px; 92 | margin-left: 40px; 93 | margin-bottom: 30px; 94 | } 95 | 96 | .addressContainer div span{ 97 | font-size: 12px; 98 | } 99 | 100 | .addressContainer { 101 | margin: 24px 0; 102 | margin-right: 0; 103 | } 104 | .addressContainer svg { 105 | display: none; 106 | } 107 | .completed { 108 | color: #7e92bd; 109 | } 110 | .completed .badge { 111 | background-color: #3a4a6e; 112 | } 113 | @media (min-width: 768px) { 114 | .sidebarContent { 115 | margin-left: 46px; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/AssetSelectionStep/AutoDetectedAssets/AutoDetectedAssets.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./autoDetectedAssets.module.css"; 2 | import { AutoDetectedAssetItem } from "~~/components/Processes/BundlingProcess/Steps/AssetSelectionStep/AutoDetectedAssets/AutoDetectedAssetItem"; 3 | import { IWrappedRecoveryTx } from "~~/hooks/flashbotRecoveryBundle/useAutodetectAssets"; 4 | 5 | interface IProps { 6 | isLoading: boolean; 7 | selectedAssets: number[]; 8 | accountAssets: IWrappedRecoveryTx[]; 9 | selectAsset: (index: number) => void; 10 | } 11 | export const AutoDetectedAssets = ({ isLoading, selectedAssets, selectAsset, accountAssets }: IProps) => { 12 | if (!isLoading && accountAssets.length === 0) { 13 | return ( 14 |
15 | 16 | We can't find any assets in this wallet. Try manually adding them if you are certain they exist. 17 | 18 |
19 | ); 20 | } 21 | return ( 22 |
23 | {isLoading && 24 | [1, 2, 3].map((_, i) => ( 25 | selectAsset(i)} 30 | /> 31 | ))} 32 | {!isLoading && 33 | accountAssets.map((item, i) => ( 34 | selectAsset(i)} 42 | /> 43 | ))} 44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/AssetSelectionStep/AutoDetectedAssets/autoDetectedAssets.module.css: -------------------------------------------------------------------------------- 1 | .assetItem { 2 | display: flex; 3 | height: 92px; 4 | max-width: 490px; 5 | margin: 0 auto; 6 | border-radius: 12px; 7 | cursor: pointer; 8 | margin-bottom: 12px; 9 | } 10 | .data { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | } 15 | .assetList { 16 | width: 100%; 17 | max-height: calc(100vh - 400px); 18 | height: calc(100vh - 400px); 19 | overflow: auto; 20 | margin: 36px 0; 21 | } 22 | 23 | .logo { 24 | width: 60px; 25 | height: 60px; 26 | margin-right: 16px; 27 | border-radius: 8px; 28 | object-fit: contain; 29 | } 30 | 31 | .logoContainer { 32 | display: flex; 33 | flex-direction: column; 34 | justify-content: center; 35 | margin-left: 16px; 36 | position: relative; 37 | } 38 | .coinTitle{ 39 | position: absolute; 40 | top: 38px; 41 | left: 14px; 42 | font-size: 10px; 43 | } 44 | 45 | .loader { 46 | height: 94px; 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | } 51 | 52 | .loading span { 53 | background-color: #3a4862; 54 | width: 100px; 55 | height: 24px; 56 | } 57 | .loading h3 { 58 | background-color: #3a4862; 59 | width: 130px; 60 | height: 24px; 61 | } 62 | 63 | .loading { 64 | animation: loading 1s infinite; 65 | } 66 | .noAssets{ 67 | display: flex; 68 | justify-content: center; 69 | align-items: center; 70 | width: 100%; 71 | } 72 | @keyframes loading { 73 | from { 74 | opacity: 0.3; 75 | } 76 | to { 77 | opacity: 0.8; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/AssetSelectionStep/ManualAssetSelection/AbiNinjaFlow/AbiNinjaFlow.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { ImpersonatorIframe, useImpersonatorIframe } from "@impersonator/iframe"; 3 | import { useReadLocalStorage } from "usehooks-ts"; 4 | import { mainnet } from "viem/chains"; 5 | import { getParsedError } from "~~/components/scaffold-eth"; 6 | import { IWrappedRecoveryTx } from "~~/hooks/flashbotRecoveryBundle/useAutodetectAssets"; 7 | import { CustomTx } from "~~/types/business"; 8 | import { notification } from "~~/utils/scaffold-eth"; 9 | 10 | const appUrl = "https://abi.ninja"; 11 | const selectedNetwork = mainnet; 12 | 13 | export const AbiNinjaFlow = ({ addUnsignedTx }: { addUnsignedTx: (asset: IWrappedRecoveryTx) => void }) => { 14 | const hackedWalletAddress = useReadLocalStorage("hackedAddress"); 15 | const { latestTransaction } = useImpersonatorIframe(); 16 | 17 | const isLatestTransactionPresent = Boolean(latestTransaction); 18 | useEffect(() => { 19 | const createCustomTx = async () => { 20 | try { 21 | // @ts-expect-error 22 | const contractAddress = latestTransaction.to; 23 | // @ts-expect-error 24 | const data = latestTransaction.data; 25 | // @ts-expect-error 26 | const value = latestTransaction.value; 27 | 28 | const customTx: CustomTx = { 29 | type: "custom-abininja", 30 | info: `Custom abininja call to ${contractAddress} with data ${data}`, 31 | toEstimate: { 32 | from: hackedWalletAddress as `0x${string}`, 33 | to: contractAddress, 34 | data: data, 35 | value: value, 36 | }, 37 | }; 38 | 39 | addUnsignedTx({ tx: customTx }); 40 | notification.success("Custom transaction added to the list"); 41 | } catch (e: any) { 42 | const message = getParsedError(e); 43 | notification.error(message); 44 | console.error(e); 45 | } 46 | }; 47 | 48 | if (isLatestTransactionPresent) { 49 | createCustomTx(); 50 | } 51 | // eslint-disable-next-line react-hooks/exhaustive-deps 52 | }, [isLatestTransactionPresent, hackedWalletAddress]); 53 | 54 | return ( 55 |
56 | {hackedWalletAddress && ( 57 |
58 |
59 |
60 | 68 |
69 |
70 |
71 | )} 72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/AssetSelectionStep/ManualAssetSelection/BasicFlow/BasicFlow.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import styles from "../manualAssetSelection.module.css"; 3 | import { ERC20Form } from "./ERC20From"; 4 | import { ERC721Form } from "./ERC721Form"; 5 | import { ERC1155Form } from "./ERC1155Form"; 6 | import { motion } from "framer-motion"; 7 | import { createPortal } from "react-dom"; 8 | import { CustomButton } from "~~/components/CustomButton/CustomButton"; 9 | import { IWrappedRecoveryTx } from "~~/hooks/flashbotRecoveryBundle/useAutodetectAssets"; 10 | 11 | enum ActiveAssetType { 12 | _, 13 | ERC20, 14 | ERC721, 15 | ERC1155, 16 | } 17 | 18 | interface IBasicFlowProps { 19 | safeAddress: string; 20 | hackedAddress: string; 21 | addAsset: (asset: IWrappedRecoveryTx) => void; 22 | } 23 | export const BasicFlow = ({ safeAddress, hackedAddress, addAsset }: IBasicFlowProps) => { 24 | const [activeAssetType, setActiveAssetType] = useState(ActiveAssetType._); 25 | return ( 26 | <> 27 | setActiveAssetType(ActiveAssetType._)} 32 | safeAddress={safeAddress} 33 | /> 34 |
    35 |
  • 36 |

    ERC20

    37 | setActiveAssetType(ActiveAssetType.ERC20)} /> 38 |
  • 39 |
  • 40 |

    ERC721

    41 | setActiveAssetType(ActiveAssetType.ERC721)} /> 42 |
  • 43 |
  • 44 |

    ERC1155

    45 | setActiveAssetType(ActiveAssetType.ERC1155)} /> 46 |
  • 47 |
48 | 49 | ); 50 | }; 51 | 52 | interface ITokenSelectionProps { 53 | tokenActive: number; 54 | close: () => void; 55 | hackedAddress: string; 56 | safeAddress: string; 57 | addAsset: (asset: IWrappedRecoveryTx) => void; 58 | } 59 | const TokenSelection = ({ close, addAsset, tokenActive, hackedAddress, safeAddress }: ITokenSelectionProps) => { 60 | const portalSelector = document.querySelector("#myportal2"); 61 | if (!portalSelector) { 62 | return <>; 63 | } 64 | 65 | return ( 66 | tokenActive != ActiveAssetType._ && 67 | createPortal( 68 | 74 |
75 |
76 |
77 | {tokenActive === ActiveAssetType.ERC20 && ( 78 | 79 | )} 80 | {tokenActive === ActiveAssetType.ERC721 && ( 81 | 82 | )} 83 | {tokenActive === ActiveAssetType.ERC1155 && ( 84 | 85 | )} 86 |
87 |
88 | , 89 | portalSelector, 90 | ) 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/AssetSelectionStep/ManualAssetSelection/BasicFlow/ERC20From.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Image from "next/image"; 3 | import BackSvg from "../../../../../../../public/assets/flashbotRecovery/back.svg"; 4 | import styles from "../manualAssetSelection.module.css"; 5 | import { BigNumber, ethers } from "ethers"; 6 | import { isAddress } from "viem"; 7 | import { usePublicClient } from "wagmi"; 8 | import { CustomButton } from "~~/components/CustomButton/CustomButton"; 9 | import { ITokenForm } from "~~/components/Processes/BundlingProcess/Steps/AssetSelectionStep/ManualAssetSelection/BasicFlow/types"; 10 | import { AddressInput } from "~~/components/scaffold-eth"; 11 | import { useShowError } from "~~/hooks/flashbotRecoveryBundle/useShowError"; 12 | import { ERC20Tx } from "~~/types/business"; 13 | import { ERC20_ABI } from "~~/utils/constants"; 14 | import { getTargetNetwork } from "~~/utils/scaffold-eth"; 15 | 16 | const erc20Interface = new ethers.utils.Interface(ERC20_ABI); 17 | 18 | export const ERC20Form = ({ hackedAddress, safeAddress, addAsset, close }: ITokenForm) => { 19 | const [contractAddress, setContractAddress] = useState(""); 20 | const { showError } = useShowError(); 21 | 22 | const publicClient = usePublicClient({ chainId: getTargetNetwork().id }); 23 | 24 | const addErc20TxToBasket = async () => { 25 | if (!isAddress(contractAddress)) { 26 | showError("Provide a contract first"); 27 | return; 28 | } 29 | 30 | let balance = 0; 31 | try { 32 | balance = (await publicClient.readContract({ 33 | address: contractAddress as `0x${string}`, 34 | abi: ERC20_ABI, 35 | functionName: "balanceOf", 36 | args: [hackedAddress], 37 | })) as number; 38 | } catch (err) { 39 | console.log(err); 40 | } 41 | 42 | if (balance == 0) { 43 | showError("Hacked account has no balance in given erc20 contract"); 44 | return; 45 | } 46 | 47 | let symbol = "ERC20"; 48 | try { 49 | symbol = (await publicClient.readContract({ 50 | address: contractAddress as `0x${string}`, 51 | abi: ERC20_ABI, 52 | functionName: "symbol", 53 | })) as string; 54 | } catch (err) { 55 | console.log(err); 56 | } 57 | 58 | const newErc20tx: ERC20Tx = { 59 | type: "erc20", 60 | info: "changeme", 61 | symbol, 62 | amount: balance.toString(), 63 | toEstimate: { 64 | from: hackedAddress as `0x${string}`, 65 | to: contractAddress as `0x${string}`, 66 | data: erc20Interface.encodeFunctionData("transfer", [ 67 | safeAddress, 68 | BigNumber.from(balance.toString()), 69 | ]) as `0x${string}`, 70 | }, 71 | }; 72 | addAsset({ tx: newErc20tx }); 73 | }; 74 | 75 | return ( 76 |
77 | {""} 78 |

{"ERC20"}

79 |
80 | 83 | setContractAddress(e)} 88 | /> 89 |
90 | addErc20TxToBasket()} /> 91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/AssetSelectionStep/ManualAssetSelection/BasicFlow/ERC721Form.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Image from "next/image"; 3 | import BackSvg from "../../../../../../../public/assets/flashbotRecovery/back.svg"; 4 | import styles from "../manualAssetSelection.module.css"; 5 | import { BigNumber, ethers } from "ethers"; 6 | import { isAddress } from "viem"; 7 | import { usePublicClient } from "wagmi"; 8 | import { CustomButton } from "~~/components/CustomButton/CustomButton"; 9 | import { ITokenForm } from "~~/components/Processes/BundlingProcess/Steps/AssetSelectionStep/ManualAssetSelection/BasicFlow/types"; 10 | import { AddressInput, InputBase } from "~~/components/scaffold-eth"; 11 | import { useShowError } from "~~/hooks/flashbotRecoveryBundle/useShowError"; 12 | import { ERC721Tx } from "~~/types/business"; 13 | import { ERC721_ABI } from "~~/utils/constants"; 14 | import { getTargetNetwork } from "~~/utils/scaffold-eth"; 15 | 16 | const erc721Interface = new ethers.utils.Interface(ERC721_ABI); 17 | 18 | export const ERC721Form = ({ hackedAddress, safeAddress, addAsset, close }: ITokenForm) => { 19 | const [contractAddress, setContractAddress] = useState(""); 20 | const [tokenId, setTokenId] = useState(""); 21 | const publicClient = usePublicClient({ chainId: getTargetNetwork().id }); 22 | const { showError } = useShowError(); 23 | const addErc721TxToBasket = async () => { 24 | if (!isAddress(contractAddress) || !tokenId) { 25 | showError("Provide a contract and a token ID"); 26 | return; 27 | } 28 | 29 | let ownerOfGivenTokenId; 30 | try { 31 | ownerOfGivenTokenId = await publicClient.readContract({ 32 | address: contractAddress as `0x${string}`, 33 | abi: ERC721_ABI, 34 | functionName: "ownerOf", 35 | args: [BigNumber.from(tokenId)], 36 | }); 37 | } catch (e) { 38 | console.error(e); 39 | } 40 | console.log("ownerOfGivenTokenId", ownerOfGivenTokenId); 41 | if (!ownerOfGivenTokenId || ownerOfGivenTokenId.toString() != hackedAddress) { 42 | showError(`Couldn't verify hacked account's ownership. Cannot add to the basket...`); 43 | return; 44 | } 45 | 46 | let symbol = "???"; 47 | try { 48 | symbol = (await publicClient.readContract({ 49 | address: contractAddress as `0x${string}`, 50 | abi: ERC721_ABI, 51 | functionName: "symbol", 52 | })) as string; 53 | } catch (err) { 54 | console.log(err); 55 | } 56 | 57 | const newErc721Tx: ERC721Tx = { 58 | type: "erc721", 59 | info: `ERC721 - ${symbol} #${tokenId}`, 60 | symbol, 61 | tokenId: tokenId, 62 | toEstimate: { 63 | from: hackedAddress as `0x${string}`, 64 | to: contractAddress as `0x${string}`, 65 | data: erc721Interface.encodeFunctionData("transferFrom", [ 66 | hackedAddress, 67 | safeAddress, 68 | BigNumber.from(tokenId), 69 | ]) as `0x${string}`, 70 | }, 71 | }; 72 | addAsset({ tx: newErc721Tx }); 73 | }; 74 | return ( 75 |
76 | {""} close()} /> 77 |

{"ERC721"}

78 |
79 | 82 | setContractAddress(e)} 87 | /> 88 |
89 | 92 | setTokenId(e)} /> 93 |
94 | addErc721TxToBasket()} /> 95 |
96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/AssetSelectionStep/ManualAssetSelection/BasicFlow/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface ITokenForm { 2 | hackedAddress: string; 3 | safeAddress: string; 4 | close: () => void; 5 | addAsset: (arg: RecoveryTx) => void; 6 | } 7 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/AssetSelectionStep/ManualAssetSelection/CustomFlow/CustomFlow.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import styles from "../manualAssetSelection.module.css"; 3 | import { AbiFunction } from "abitype"; 4 | import { parseAbiItem } from "viem"; 5 | import { AddressInput, CustomContractWriteForm, InputBase } from "~~/components/scaffold-eth"; 6 | import { IWrappedRecoveryTx } from "~~/hooks/flashbotRecoveryBundle/useAutodetectAssets"; 7 | 8 | interface ICustomFlowProps { 9 | hackedAddress: string; 10 | addAsset: (asset: IWrappedRecoveryTx) => void; 11 | } 12 | export const CustomFlow = ({ hackedAddress, addAsset }: ICustomFlowProps) => { 13 | const [customContractAddress, setCustomContractAddress] = useState(""); 14 | const [functionSignature, setFunctionSignatureCore] = useState(""); 15 | const setFunctionSignature = (newSig: string) => { 16 | let withoutFunctionKeyword = newSig.trimStart(); 17 | if (withoutFunctionKeyword.startsWith("function ")) { 18 | withoutFunctionKeyword = withoutFunctionKeyword.replace("function ", "").trimStart(); 19 | } 20 | setFunctionSignatureCore(withoutFunctionKeyword); 21 | }; 22 | 23 | const parsedFunctAbi = (() => { 24 | if (!customContractAddress || !functionSignature) { 25 | return null; 26 | } 27 | try { 28 | const parsedFunctAbi = parseAbiItem(`function ${functionSignature}`) as AbiFunction; 29 | return parsedFunctAbi; 30 | } catch (e) { 31 | return null; 32 | } 33 | })(); 34 | 35 | return ( 36 | <> 37 |
38 |
39 | 42 | 48 |
49 | 52 | 58 | {!!parsedFunctAbi && ( 59 |
60 | { 67 | setCustomContractAddress(""); 68 | setFunctionSignature(""); 69 | }} 70 | /> 71 |
72 | )} 73 |
74 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/AssetSelectionStep/ManualAssetSelection/ManualAssetSelection.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import CloseSvg from "../../../../../../public/assets/flashbotRecovery/close.svg"; 3 | import { AbiNinjaFlow } from "./AbiNinjaFlow/AbiNinjaFlow"; 4 | import { BasicFlow } from "./BasicFlow/BasicFlow"; 5 | import { CustomFlow } from "./CustomFlow/CustomFlow"; 6 | import { RawFlow } from "./RawFlow/RawFlow"; 7 | import styles from "./manualAssetSelection.module.css"; 8 | import { ImpersonatorIframeProvider } from "@impersonator/iframe"; 9 | import { motion } from "framer-motion"; 10 | import { createPortal } from "react-dom"; 11 | import { Tabs } from "~~/components/tabs/Tabs"; 12 | import { IWrappedRecoveryTx } from "~~/hooks/flashbotRecoveryBundle/useAutodetectAssets"; 13 | 14 | interface IProps { 15 | isVisible: boolean; 16 | close: () => void; 17 | hackedAddress: string; 18 | safeAddress: string; 19 | addAsset: (asset: IWrappedRecoveryTx) => void; 20 | } 21 | export const ManualAssetSelection = ({ isVisible, close, safeAddress, addAsset, hackedAddress }: IProps) => { 22 | const portalSelector = document.querySelector("#myportal"); 23 | if (!portalSelector || !isVisible) { 24 | return <>; 25 | } 26 | 27 | return createPortal( 28 | 34 |
35 | 36 | {" "} 37 | {!!close ? {""} close()} /> : <>} 38 | 39 |
40 |

{"Add assets manually"}

41 | 42 | {active => { 43 | const isBasic = active == 0; 44 | if (isBasic) { 45 | return ; 46 | } else if (active == 1) { 47 | return addAsset(item)} />; 48 | } else if (active == 2) { 49 | return addAsset(item)} />; 50 | } 51 | 52 | return ( 53 | // Adding the provider here instead of _app.tsx so that it resets the states on each render 54 | // because @impersonator/iframe uses react context and does not give api to reset the state 55 | 56 | addAsset(item)} /> 57 | 58 | ); 59 | }} 60 | 61 |
62 |
63 |
, 64 | portalSelector, 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/AssetSelectionStep/ManualAssetSelection/RawFlow/RawFlow.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import styles from "../manualAssetSelection.module.css"; 3 | import { parseTransaction } from "viem"; 4 | import { CustomButton } from "~~/components/CustomButton/CustomButton"; 5 | import { InputBase } from "~~/components/scaffold-eth"; 6 | import { IWrappedRecoveryTx } from "~~/hooks/flashbotRecoveryBundle/useAutodetectAssets"; 7 | import { CustomTx } from "~~/types/business"; 8 | 9 | interface IRawFlowProps { 10 | hackedAddress: string; 11 | addAsset: (asset: IWrappedRecoveryTx) => void; 12 | } 13 | export const RawFlow = ({ hackedAddress, addAsset }: IRawFlowProps) => { 14 | const [rawTx, setRawTx] = useState(""); 15 | const addRawTxToBasket = () => { 16 | const parsedTx = parseTransaction(rawTx as `0x${string}`); 17 | console.log(parsedTx); 18 | const { to, value, data = "0x" } = parsedTx; 19 | const customTx: CustomTx = { 20 | type: "custom", 21 | info: `Raw Transaction ${rawTx.substring(0, 40)}...`, 22 | toEstimate: { 23 | from: hackedAddress as `0x${string}`, 24 | to: to as `0x${string}`, 25 | value: value ? value.toString() : "0", 26 | data: (data as `0x${string}`) || "0x", 27 | }, 28 | }; 29 | addAsset({ tx: customTx }); 30 | }; 31 | 32 | const checkValid = () => { 33 | try { 34 | const parsed = parseTransaction(rawTx as `0x${string}`); 35 | if (parsed) { 36 | return true; 37 | } 38 | } catch (e) { 39 | return false; 40 | } 41 | return false; 42 | }; 43 | return ( 44 | <> 45 |
46 |
47 |
48 | 51 | 52 |
53 | addRawTxToBasket()} /> 54 |
55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/AssetSelectionStep/ManualAssetSelection/manualAssetSelection.module.css: -------------------------------------------------------------------------------- 1 | .modalContainer { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | backdrop-filter: blur(5px); 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | background: rgba(20, 28, 41, 0.4); 12 | } 13 | 14 | .modal { 15 | display: inline-flex; 16 | width: 579px; 17 | flex-direction: column; 18 | align-items: center; 19 | flex-shrink: 0; 20 | border-radius: 12px; 21 | box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.03); 22 | min-height: 600px; 23 | max-height: 900px; 24 | height: 80vh; 25 | } 26 | 27 | .modalContent { 28 | display: inline-flex; 29 | flex-direction: column; 30 | align-items: center; 31 | gap: 40px; 32 | flex-shrink: 0; 33 | margin-bottom: 56px; 34 | width: 90%; 35 | } 36 | 37 | .close { 38 | align-self: flex-end; 39 | margin-top: 14px; 40 | margin-right: 14px; 41 | cursor: pointer; 42 | padding: 10px; 43 | padding-bottom: 4px; 44 | } 45 | 46 | .title { 47 | font-size: 24px; 48 | font-style: normal; 49 | font-weight: 700; 50 | line-height: 32px; 51 | margin: 0; 52 | text-align: center; 53 | } 54 | 55 | .text { 56 | font-size: 18px; 57 | font-style: normal; 58 | font-weight: 400; 59 | line-height: 32px; 60 | width: 80%; 61 | text-align: center; 62 | margin: 0; 63 | } 64 | 65 | .basicItem { 66 | display: flex; 67 | padding: 16px; 68 | flex-direction: column; 69 | justify-content: center; 70 | align-items: center; 71 | gap: 24px; 72 | border-radius: 12px; 73 | margin-top: 24px; 74 | } 75 | 76 | .container, 77 | .containerCustom, 78 | .containerBasic { 79 | position: relative; 80 | margin-bottom: 0; 81 | display: flex; 82 | flex-direction: column; 83 | justify-content: flex-start; 84 | margin: 0 auto; 85 | max-width: 490px; 86 | } 87 | .containerCustom { 88 | height: 392px; 89 | width: 100%; 90 | padding: 0 10px; 91 | overflow-y: auto; 92 | } 93 | .containerBasic { 94 | height: 538px; 95 | width: 100%; 96 | padding: 0 10px; 97 | } 98 | 99 | .list { 100 | width: 490px; 101 | margin: 0 auto; 102 | } 103 | .label { 104 | font-size: 16px; 105 | font-style: normal; 106 | font-weight: 600; 107 | line-height: normal; 108 | margin-bottom: 12px; 109 | } 110 | .container > div { 111 | border-radius: 4px; 112 | padding: 4px; 113 | } 114 | .containerCustom > div { 115 | border-radius: 4px; 116 | padding: 4px; 117 | } 118 | .containerBasic > div { 119 | border-radius: 4px; 120 | padding: 4px; 121 | } 122 | .contract { 123 | margin-top: 20px; 124 | } 125 | 126 | .contract > div > div div { 127 | border-radius: 4px; 128 | padding: 4px; 129 | } 130 | .contract > div > div div input::placeholder { 131 | /* Chrome, Firefox, Opera, Safari 10.1+ */ 132 | color: rgb(156 163 175 / var(--tw-text-opacity)); 133 | opacity: 1; /* Firefox */ 134 | } 135 | .contract > div > div div:last-child { 136 | display: flex; 137 | justify-content: center; 138 | align-items: center; 139 | } 140 | .contract > div > div div:last-child button { 141 | background-color: #17a9e8; 142 | 143 | color: #293853; 144 | width: 230px; 145 | margin: 0 auto; 146 | padding: 0px 48px; 147 | height: 40px; 148 | font-size: 16px; 149 | font-style: normal; 150 | font-weight: 700; 151 | line-height: normal; 152 | border: 1px solid; 153 | } 154 | 155 | .bottom { 156 | flex-grow: 1; 157 | } 158 | .contract > div > div div:last-child button svg { 159 | display: none; 160 | } 161 | 162 | .back { 163 | position: absolute; 164 | top: 0; 165 | cursor: pointer; 166 | } 167 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/AssetSelectionStep/assetSelectionStep.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | 4 | margin-bottom: 0; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | .title { 12 | font-size: 23px; 13 | font-style: normal; 14 | font-weight: 600; 15 | line-height: 100%; 16 | margin: 0; 17 | width: 100%; 18 | text-align: center; 19 | } 20 | .titleContainer{ 21 | width: 100%; 22 | position: relative; 23 | max-width: 490px; 24 | } -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/TransactionBundleStep/TransactionBundleStep.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useCallback, useEffect } from "react"; 2 | import Image from "next/image"; 3 | import GasSvg from "../../../../../public/assets/flashbotRecovery/gas.svg"; 4 | import styles from "./transactionBundleStep.module.css"; 5 | import { BigNumber, ethers } from "ethers"; 6 | import { motion } from "framer-motion"; 7 | import { useInterval } from "usehooks-ts"; 8 | import { CustomButton } from "~~/components/CustomButton/CustomButton"; 9 | import { TransactionItem } from "~~/components/Processes/BundlingProcess/Steps/TransactionBundleStep/TransactionItem"; 10 | import { useGasEstimation } from "~~/hooks/flashbotRecoveryBundle/useGasEstimation"; 11 | import BackSvg from "~~/public/assets/flashbotRecovery/back.svg"; 12 | import { RecoveryTx } from "~~/types/business"; 13 | 14 | interface IProps { 15 | isVisible: boolean; 16 | clear: () => void; 17 | transactions: RecoveryTx[]; 18 | onBack: () => void; 19 | modifyTransactions: Dispatch>; 20 | onSubmit: () => void; 21 | totalGasEstimate: BigNumber; 22 | setTotalGasEstimate: Dispatch>; 23 | } 24 | 25 | export const TransactionBundleStep = ({ 26 | clear, 27 | onBack, 28 | isVisible, 29 | onSubmit, 30 | transactions, 31 | modifyTransactions, 32 | totalGasEstimate, 33 | setTotalGasEstimate, 34 | }: IProps) => { 35 | const { estimateTotalGasPrice } = useGasEstimation(); 36 | 37 | const updateGasEstimate = useCallback(async () => { 38 | if (transactions.length === 0) return; 39 | const estimate = await estimateTotalGasPrice(transactions, removeUnsignedTx, modifyTransactions); 40 | setTotalGasEstimate(estimate); 41 | }, [transactions, estimateTotalGasPrice, setTotalGasEstimate]); 42 | 43 | useEffect(() => { 44 | updateGasEstimate(); 45 | }, [updateGasEstimate]); 46 | 47 | useInterval(updateGasEstimate, transactions.length > 0 ? 10000 : null); 48 | 49 | const removeUnsignedTx = (txId: number) => { 50 | modifyTransactions((prev: RecoveryTx[]) => { 51 | if (txId < 0 || txId > prev.length) { 52 | return prev.filter(a => a); 53 | } 54 | delete prev[txId]; 55 | 56 | // When user removes the last item 57 | if (prev.length == 1 && txId == 0) { 58 | clear(); 59 | } 60 | 61 | return prev.filter(a => a); 62 | }); 63 | }; 64 | 65 | if (!isVisible) { 66 | return <>; 67 | } 68 | 69 | return ( 70 | 71 |
72 |
73 | {ethers.utils.formatEther(totalGasEstimate.toString())} 74 |
75 | 76 |
77 |
78 |
79 | {""} 80 |

Your transactions

81 |
82 |
83 | {transactions.map((item, i) => ( 84 | removeUnsignedTx(i)} tx={item} /> 85 | ))} 86 |
87 | 88 | Clear all 89 | 90 | onSubmit()} /> 91 |
92 | 93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/TransactionBundleStep/TransactionItem.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./transactionBundleStep.module.css"; 2 | import { motion } from "framer-motion"; 3 | import { Address } from "~~/components/scaffold-eth"; 4 | import { ERC20Tx, ERC721Tx, ERC1155Tx, RecoveryTx } from "~~/types/business"; 5 | import { extractAbiNinjaCallDetails, formatCalldataString } from "~~/utils/abiNinjaFlowUtils"; 6 | 7 | interface ITransactionProps { 8 | onDelete: () => void; 9 | tx?: RecoveryTx; 10 | } 11 | export const TransactionItem = ({ onDelete, tx }: ITransactionProps) => { 12 | const getTitle = () => { 13 | if (!tx) { 14 | return

; 15 | } 16 | if (tx.type == "erc721") { 17 | const typedTx = tx as ERC721Tx; 18 | return

{`${typedTx.symbol} - ${typedTx.tokenId} `}

; 19 | } 20 | if (tx.type == "erc1155") { 21 | const typedTx = tx as ERC1155Tx; 22 | return

{`${typedTx.info} `}

; 23 | } 24 | if (tx.type === "erc20") { 25 | const typedTx = tx as ERC20Tx; 26 | return

{`${typedTx.amount} ${typedTx.symbol}`}

; 27 | } 28 | if (tx.type === "custom-abininja") { 29 | const { contractAddress, data } = extractAbiNinjaCallDetails(tx.info); 30 | return ( 31 | <> 32 |
33 |

Custom call to :

34 |
35 |
36 |
37 |

With data :

38 |
42 |

{formatCalldataString(data)}

43 |
44 |
45 | 46 | ); 47 | } 48 | 49 | return

{tx.info}

; 50 | }; 51 | 52 | return ( 53 | 58 |
{getTitle()}
59 |
60 | X 61 |
62 |
63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/Steps/TransactionBundleStep/transactionBundleStep.module.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | width: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | align-items: center; 7 | margin-bottom: 0; 8 | } 9 | .mainContent{ 10 | width: 100%; 11 | margin-bottom: 0; 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | max-width: 600px; 17 | } 18 | .titleContainer{ 19 | width: 100%; 20 | position: relative; 21 | } 22 | .title{ 23 | font-size: 23px; 24 | font-style: normal; 25 | font-weight: 600; 26 | line-height: 100%; 27 | margin: 0; 28 | } 29 | .assetItem{ 30 | display: flex; 31 | height: 52px; 32 | max-width: 600px; 33 | width: 90%; 34 | margin: 0 auto; 35 | cursor: pointer; 36 | border-radius: 4px; 37 | margin-bottom: 12px; 38 | } 39 | .data{ 40 | display: flex; 41 | flex-direction: column; 42 | justify-content: center; 43 | flex-grow: 1; 44 | margin-left: 16px; 45 | } 46 | .data h3{ 47 | margin: 0; 48 | font-size: 18px; 49 | font-style: normal; 50 | font-weight: 500; 51 | } 52 | .assetList{ 53 | width: 100%; 54 | max-height: calc(100vh - 500px); 55 | height: calc(100vh - 500px); 56 | min-height: 200px; 57 | overflow: auto; 58 | margin: 36px 0; 59 | margin-bottom: 0; 60 | } 61 | 62 | .clear{ 63 | text-decoration: underline; 64 | width: 230px; 65 | margin-bottom: 42px; 66 | margin-top: 16px; 67 | text-align: center; 68 | cursor: pointer; 69 | } 70 | .clear:hover{ 71 | color:#17A9E8 72 | } 73 | .logo{ 74 | width: 60px; 75 | height: 60px; 76 | margin-right: 16px; 77 | } 78 | 79 | .close{ 80 | display: flex; 81 | flex-direction: column; 82 | justify-content: center; 83 | margin-left:16px; 84 | margin-right: 16px; 85 | } 86 | .gasContainer{ 87 | display: flex; 88 | justify-content: flex-end; 89 | align-items: center; 90 | flex-direction: row; 91 | width: 90%; 92 | } -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/BundlingProcess/bundlingProcess.module.css: -------------------------------------------------------------------------------- 1 | .bundling { 2 | display: flex; 3 | justify-content: flex-start; 4 | align-items: flex-start; 5 | width: 100%; 6 | height: 100vh; 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | bottom: 0; 11 | right: 0; 12 | } 13 | .content { 14 | display: flex; 15 | width: 100%; 16 | height: 100%; 17 | flex-direction: column; 18 | justify-content: center; 19 | align-items: center; 20 | } 21 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/HackedAddressProcess/HackedAddressProcess.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import React from "react"; 3 | import Image from "next/image"; 4 | import IllustrationSvg from "../../../public/assets/flashbotRecovery/logo.svg"; 5 | import styles from "./hackedAddressProcess.module.css"; 6 | import { isAddress } from "ethers/lib/utils"; 7 | import { AnimatePresence, motion } from "framer-motion"; 8 | import { CustomButton } from "~~/components/CustomButton/CustomButton"; 9 | import { AddressInput } from "~~/components/scaffold-eth"; 10 | 11 | interface IProps { 12 | isVisible: boolean; 13 | onSubmit: (hacked: string, safe: string) => void; 14 | } 15 | export const HackedAddressProcess = ({ isVisible, onSubmit }: IProps) => { 16 | const [hackedAddress, setHackedAddressCore] = useState(""); 17 | const [safeAddress, setSafeAddressCore] = useState(""); 18 | return ( 19 | 20 | {isVisible && ( 21 | 27 |

28 | Welcome to
29 | Hacked Wallet Recovery 30 |

31 | An ethereum icon with nfts and tokens around 36 |

Let's search what assets we can recover

37 |

38 | This app can help you move assets on Ethereum mainnet that are stuck in a wallet that has been hacked. As 39 | you probably have found out, it is common practice for hackers to sweep any funds going to or from your 40 | wallet. Using Flashbots we can get around their tactics and recover assets still in your wallet. 41 |

42 |
43 | 44 | 45 | setHackedAddressCore(val)} 50 | /> 51 |
52 | 53 | setSafeAddressCore(val)} 58 | /> 59 |
60 | { 65 | onSubmit(hackedAddress, safeAddress); 66 | }} 67 | /> 68 | 69 | )} 70 | 71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/HackedAddressProcess/hackedAddressProcess.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 90%; 3 | margin-bottom: 0; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | max-width: 490px; 9 | } 10 | 11 | .label { 12 | font-size: 16px; 13 | font-style: normal; 14 | font-weight: 600; 15 | line-height: normal; 16 | margin-bottom: 12px; 17 | } 18 | .container > div { 19 | border-radius: 4px; 20 | padding: 4px; 21 | width: 100%; 22 | } 23 | 24 | .container > div > input { 25 | border-radius: 0px; 26 | } 27 | .container > div > input::placeholder { 28 | color: #58688a; 29 | opacity: 1; 30 | } 31 | .container > div > input:not(:active) { 32 | background-color: transparent; 33 | } 34 | .title { 35 | text-align: center; 36 | font-size: 24px; 37 | font-style: normal; 38 | font-weight: 700; 39 | line-height: 32px; 40 | } 41 | .illustration { 42 | width: 100px; 43 | height: 100px; 44 | margin: 40px 0; 45 | } 46 | .text { 47 | font-size: 18px; 48 | font-style: normal; 49 | font-weight: 400; 50 | font-size: 18px; 51 | line-height: 32px; 52 | text-align: center; 53 | } 54 | .buttonContainer button { 55 | display: flex; 56 | 57 | height: 40px; 58 | padding: 0px 48px; 59 | } 60 | .buttonContainer > div > div > div { 61 | display: none; 62 | } 63 | .infoBox { 64 | font-size: 13px; 65 | background-color: rgb(36, 49, 72); 66 | line-height: 17px; 67 | padding: 10px; 68 | } 69 | -------------------------------------------------------------------------------- /packages/nextjs/components/Processes/RecoveryProcess/recoveryProcess.module.css: -------------------------------------------------------------------------------- 1 | .shareButtons { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | } 6 | .shareButtons a { 7 | margin: 0 10px; 8 | } 9 | .inputContainer{ 10 | width: 80%; 11 | position: relative; 12 | } 13 | .inputContainer > div { 14 | border-radius: 4px; 15 | padding: 4px; 16 | } 17 | 18 | .inputContainer label { 19 | font-size: 16px; 20 | font-style: normal; 21 | font-weight: 600; 22 | line-height: normal; 23 | margin-bottom: 12px; 24 | } 25 | .eth{ 26 | position: absolute; 27 | bottom: 12px; 28 | right: 14px; 29 | } 30 | 31 | .buttonContainer button{ 32 | display: flex; 33 | height: 40px; 34 | padding: 0px 48px; 35 | } 36 | 37 | .warning{ 38 | font-size: 14px; 39 | } 40 | 41 | .rpcContainer { 42 | width: 80%; 43 | position: relative; 44 | } 45 | 46 | .rpc p { 47 | font-size: 16px; 48 | font-style: normal; 49 | line-height: normal; 50 | margin-bottom: 2px; 51 | margin-top: 2px; 52 | } 53 | 54 | .rpcTitle { 55 | font-weight: 800; 56 | } 57 | 58 | .rpcTitle + p { 59 | margin-bottom: 16px; 60 | } -------------------------------------------------------------------------------- /packages/nextjs/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | export const Spinner = ({ width, height }: { width?: string; height?: string }) => { 2 | return ( 3 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/nextjs/components/SwitchTheme.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useDarkMode } from "usehooks-ts"; 3 | 4 | export const SwitchTheme = ({ className }: { className?: string }) => { 5 | const { isDarkMode, toggle } = useDarkMode(); 6 | 7 | useEffect(() => { 8 | const body = document.body; 9 | body.setAttribute("data-theme", isDarkMode ? "scaffoldEthDark" : "scaffoldEthDark"); 10 | }, [isDarkMode]); 11 | 12 | return ( 13 |
14 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Balance.tsx: -------------------------------------------------------------------------------- 1 | import { useAccountBalance } from "~~/hooks/scaffold-eth"; 2 | import { getTargetNetwork } from "~~/utils/scaffold-eth"; 3 | 4 | type TBalanceProps = { 5 | address?: string; 6 | className?: string; 7 | }; 8 | 9 | /** 10 | * Display (ETH & USD) balance of an ETH address. 11 | */ 12 | export const Balance = ({ address, className = "" }: TBalanceProps) => { 13 | const configuredNetwork = getTargetNetwork(); 14 | const { balance, price, isError, isLoading, onToggleBalance, isEthBalance } = useAccountBalance(address); 15 | 16 | if (!address || isLoading || balance === null) { 17 | return ( 18 |
19 |
20 |
21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | if (isError) { 28 | return ( 29 |
30 |
Error
31 |
32 | ); 33 | } 34 | 35 | return ( 36 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/BlockieAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { AvatarComponent } from "@rainbow-me/rainbowkit"; 2 | import Blockies from "react-blockies"; 3 | 4 | // Custom Avatar for RainbowKit 5 | export const BlockieAvatar: AvatarComponent = ({ address, ensImage, size }) => 6 | ensImage ? ( 7 | // Don't want to use nextJS Image here (and adding remote patterns for the URL) 8 | // eslint-disable-next-line 9 | {`${address} 10 | ) : ( 11 | 30 ? 10 : 3.75} /> 12 | ); 13 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from "react"; 2 | import { AbiParameter } from "abitype"; 3 | import { 4 | AddressInput, 5 | Bytes32Input, 6 | BytesInput, 7 | InputBase, 8 | IntegerInput, 9 | IntegerVariant, 10 | } from "~~/components/scaffold-eth"; 11 | 12 | type ContractInputProps = { 13 | setForm: Dispatch>>; 14 | form: Record | undefined; 15 | stateObjectKey: string; 16 | paramType: AbiParameter; 17 | }; 18 | 19 | /** 20 | * Generic Input component to handle input's based on their function param type 21 | */ 22 | export const ContractInput = ({ setForm, form, stateObjectKey, paramType }: ContractInputProps) => { 23 | const inputProps = { 24 | name: stateObjectKey, 25 | value: form?.[stateObjectKey], 26 | placeholder: paramType.name ? `${paramType.type} ${paramType.name}` : paramType.type, 27 | onChange: (value: any) => { 28 | setForm(form => ({ ...form, [stateObjectKey]: value })); 29 | }, 30 | }; 31 | 32 | if (paramType.type === "address") { 33 | return ; 34 | } else if (paramType.type === "bytes32") { 35 | return ; 36 | } else if (paramType.type === "bytes") { 37 | return ; 38 | } else if (paramType.type === "string") { 39 | return ; 40 | } else if (paramType.type.includes("int") && !paramType.type.includes("[")) { 41 | return ; 42 | } 43 | 44 | return ; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Contract/ContractReadMethods.tsx: -------------------------------------------------------------------------------- 1 | import { ReadOnlyFunctionForm } from "./ReadOnlyFunctionForm"; 2 | import { Abi, AbiFunction } from "abitype"; 3 | import { Contract, ContractName } from "~~/utils/scaffold-eth/contract"; 4 | 5 | export const ContractReadMethods = ({ deployedContractData }: { deployedContractData: Contract }) => { 6 | if (!deployedContractData) { 7 | return null; 8 | } 9 | 10 | const functionsToDisplay = ( 11 | ((deployedContractData.abi || []) as Abi).filter(part => part.type === "function") as AbiFunction[] 12 | ).filter(fn => { 13 | const isQueryableWithParams = 14 | (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length > 0; 15 | return isQueryableWithParams; 16 | }); 17 | 18 | if (!functionsToDisplay.length) { 19 | return <>No read methods; 20 | } 21 | 22 | return ( 23 | <> 24 | {functionsToDisplay.map(fn => ( 25 | 26 | ))} 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Contract/ContractVariables.tsx: -------------------------------------------------------------------------------- 1 | import { DisplayVariable } from "./DisplayVariable"; 2 | import { Abi, AbiFunction } from "abitype"; 3 | import { Contract, ContractName } from "~~/utils/scaffold-eth/contract"; 4 | 5 | export const ContractVariables = ({ 6 | refreshDisplayVariables, 7 | deployedContractData, 8 | }: { 9 | refreshDisplayVariables: boolean; 10 | deployedContractData: Contract; 11 | }) => { 12 | if (!deployedContractData) { 13 | return null; 14 | } 15 | 16 | const functionsToDisplay = ( 17 | (deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[] 18 | ).filter(fn => { 19 | const isQueryableWithNoParams = 20 | (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length === 0; 21 | return isQueryableWithNoParams; 22 | }); 23 | 24 | if (!functionsToDisplay.length) { 25 | return <>No contract variables; 26 | } 27 | 28 | return ( 29 | <> 30 | {functionsToDisplay.map(fn => ( 31 | 37 | ))} 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Contract/DisplayVariable.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Abi, AbiFunction } from "abitype"; 3 | import { Address } from "viem"; 4 | import { useContractRead } from "wagmi"; 5 | import { ArrowPathIcon } from "@heroicons/react/24/outline"; 6 | import { displayTxResult } from "~~/components/scaffold-eth"; 7 | import { useAnimationConfig } from "~~/hooks/scaffold-eth"; 8 | import { notification } from "~~/utils/scaffold-eth"; 9 | 10 | type DisplayVariableProps = { 11 | contractAddress: Address; 12 | abiFunction: AbiFunction; 13 | refreshDisplayVariables: boolean; 14 | }; 15 | 16 | export const DisplayVariable = ({ contractAddress, abiFunction, refreshDisplayVariables }: DisplayVariableProps) => { 17 | const { 18 | data: result, 19 | isFetching, 20 | refetch, 21 | } = useContractRead({ 22 | address: contractAddress, 23 | functionName: abiFunction.name, 24 | abi: [abiFunction] as Abi, 25 | onError: error => { 26 | notification.error(error.message); 27 | }, 28 | }); 29 | 30 | const { showAnimation } = useAnimationConfig(result); 31 | 32 | useEffect(() => { 33 | refetch(); 34 | }, [refetch, refreshDisplayVariables]); 35 | 36 | return ( 37 |
38 |
39 |

{abiFunction.name}

40 | 43 |
44 |
45 |
46 |
51 | {displayTxResult(result)} 52 |
53 |
54 |
55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Abi, AbiFunction } from "abitype"; 3 | import { Address } from "viem"; 4 | import { useContractRead } from "wagmi"; 5 | import { 6 | ContractInput, 7 | displayTxResult, 8 | getFunctionInputKey, 9 | getInitialFormState, 10 | getParsedContractFunctionArgs, 11 | } from "~~/components/scaffold-eth"; 12 | import { notification } from "~~/utils/scaffold-eth"; 13 | 14 | type TReadOnlyFunctionFormProps = { 15 | contractAddress: Address; 16 | abiFunction: AbiFunction; 17 | }; 18 | 19 | export const ReadOnlyFunctionForm = ({ contractAddress, abiFunction }: TReadOnlyFunctionFormProps) => { 20 | const [form, setForm] = useState>(() => getInitialFormState(abiFunction)); 21 | const [result, setResult] = useState(); 22 | 23 | const { isFetching, refetch } = useContractRead({ 24 | address: contractAddress, 25 | functionName: abiFunction.name, 26 | abi: [abiFunction] as Abi, 27 | args: getParsedContractFunctionArgs(form), 28 | enabled: false, 29 | onError: (error: any) => { 30 | notification.error(error.message); 31 | }, 32 | }); 33 | 34 | const inputElements = abiFunction.inputs.map((input, inputIndex) => { 35 | const key = getFunctionInputKey(abiFunction.name, input, inputIndex); 36 | return ( 37 | { 40 | setResult(undefined); 41 | setForm(updatedFormValue); 42 | }} 43 | form={form} 44 | stateObjectKey={key} 45 | paramType={input} 46 | /> 47 | ); 48 | }); 49 | 50 | return ( 51 |
52 |

{abiFunction.name}

53 | {inputElements} 54 |
55 |
56 | {result !== null && result !== undefined && ( 57 | 58 | Result: {displayTxResult(result)} 59 | 60 | )} 61 |
62 | 71 |
72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Contract/TxReceipt.tsx: -------------------------------------------------------------------------------- 1 | import { TransactionReceipt } from "viem"; 2 | import { displayTxResult } from "~~/components/scaffold-eth"; 3 | 4 | export const TxReceipt = ( 5 | txResult: string | number | bigint | Record | TransactionReceipt | undefined, 6 | ) => { 7 | return ( 8 |
9 | 10 |
11 | Transaction Receipt 12 |
13 |
14 |
{displayTxResult(txResult)}
15 |
16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Contract/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./ContractInput"; 2 | export * from "./DisplayVariable"; 3 | export * from "./ReadOnlyFunctionForm"; 4 | export * from "./TxReceipt"; 5 | export * from "./utilsContract"; 6 | export * from "./utilsDisplay"; 7 | export * from "./WriteOnlyFunctionForm"; 8 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Contract/utilsContract.tsx: -------------------------------------------------------------------------------- 1 | import { AbiFunction, AbiParameter } from "abitype"; 2 | import { BaseError as BaseViemError } from "viem"; 3 | 4 | /** 5 | * @dev utility function to generate key corresponding to function metaData 6 | * @param {AbiFunction} functionName 7 | * @param {utils.ParamType} input - object containing function name and input type corresponding to index 8 | * @param {number} inputIndex 9 | * @returns {string} key 10 | */ 11 | const getFunctionInputKey = (functionName: string, input: AbiParameter, inputIndex: number): string => { 12 | const name = input?.name || `input_${inputIndex}_`; 13 | return functionName + "_" + name + "_" + input.internalType + "_" + input.type; 14 | }; 15 | 16 | /** 17 | * @dev utility function to parse error 18 | * @param e - error object 19 | * @returns {string} parsed error string 20 | */ 21 | const getParsedError = (e: any | BaseViemError): string => { 22 | let message = e.message ?? "An unknown error occurred"; 23 | 24 | if (e instanceof BaseViemError) { 25 | if (e.details) { 26 | message = e.details; 27 | } else if (e.shortMessage) { 28 | message = e.shortMessage; 29 | } else if (e.message) { 30 | message = e.message; 31 | } else if (e.name) { 32 | message = e.name; 33 | } 34 | } 35 | 36 | return message; 37 | }; 38 | 39 | // This regex is used to identify array types in the form of `type[size]` 40 | const ARRAY_TYPE_REGEX = /\[.*\]$/; 41 | /** 42 | * @dev Parse form input with array support 43 | * @param {Record} form - form object containing key value pairs 44 | * @returns parsed error string 45 | */ 46 | const getParsedContractFunctionArgs = (form: Record) => { 47 | const keys = Object.keys(form); 48 | const parsedArguments = keys.map(key => { 49 | try { 50 | const keySplitArray = key.split("_"); 51 | const baseTypeOfArg = keySplitArray[keySplitArray.length - 1]; 52 | let valueOfArg = form[key]; 53 | 54 | if (ARRAY_TYPE_REGEX.test(baseTypeOfArg) || baseTypeOfArg === "tuple") { 55 | valueOfArg = JSON.parse(valueOfArg); 56 | } else if (baseTypeOfArg === "bool") { 57 | if (["true", "1", "0x1", "0x01", "0x0001"].includes(valueOfArg)) { 58 | valueOfArg = 1; 59 | } else { 60 | valueOfArg = 0; 61 | } 62 | } 63 | return valueOfArg; 64 | } catch (error: any) { 65 | // ignore error, it will be handled when sending/reading from a function 66 | } 67 | }); 68 | return parsedArguments; 69 | }; 70 | 71 | const getInitialFormState = (abiFunction: AbiFunction) => { 72 | const initialForm: Record = {}; 73 | if (!abiFunction.inputs) return initialForm; 74 | abiFunction.inputs.forEach((input, inputIndex) => { 75 | const key = getFunctionInputKey(abiFunction.name, input, inputIndex); 76 | initialForm[key] = ""; 77 | }); 78 | return initialForm; 79 | }; 80 | 81 | export { getFunctionInputKey, getInitialFormState, getParsedContractFunctionArgs, getParsedError }; 82 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Contract/utilsDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { TransactionBase, TransactionReceipt, formatEther } from "viem"; 3 | import { Address } from "~~/components/scaffold-eth"; 4 | import { replacer } from "~~/utils/scaffold-eth/common"; 5 | 6 | type DisplayContent = 7 | | string 8 | | number 9 | | bigint 10 | | Record 11 | | TransactionBase 12 | | TransactionReceipt 13 | | undefined 14 | | unknown; 15 | 16 | export const displayTxResult = ( 17 | displayContent: DisplayContent | DisplayContent[], 18 | asText = false, 19 | ): string | ReactElement | number => { 20 | if (displayContent == null) { 21 | return ""; 22 | } 23 | 24 | if (typeof displayContent === "bigint") { 25 | try { 26 | const asNumber = Number(displayContent); 27 | if (asNumber <= Number.MAX_SAFE_INTEGER && asNumber >= Number.MIN_SAFE_INTEGER) { 28 | return asNumber; 29 | } else { 30 | return "Ξ" + formatEther(displayContent); 31 | } 32 | } catch (e) { 33 | return "Ξ" + formatEther(displayContent); 34 | } 35 | } 36 | 37 | if (typeof displayContent === "string" && displayContent.indexOf("0x") === 0 && displayContent.length === 42) { 38 | return asText ? displayContent :
; 39 | } 40 | 41 | if (Array.isArray(displayContent)) { 42 | const mostReadable = (v: DisplayContent) => 43 | ["number", "boolean"].includes(typeof v) ? v : displayTxResultAsText(v); 44 | const displayable = JSON.stringify(displayContent.map(mostReadable), replacer); 45 | 46 | return asText ? ( 47 | displayable 48 | ) : ( 49 | {displayable.replaceAll(",", ",\n")} 50 | ); 51 | } 52 | 53 | return JSON.stringify(displayContent, replacer, 2); 54 | }; 55 | 56 | const displayTxResultAsText = (displayContent: DisplayContent) => displayTxResult(displayContent, true); 57 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/FaucetButton.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useIsMounted } from "usehooks-ts"; 3 | import { createWalletClient, http, parseEther } from "viem"; 4 | import { useAccount, useNetwork } from "wagmi"; 5 | import { hardhat } from "wagmi/chains"; 6 | import { BanknotesIcon } from "@heroicons/react/24/outline"; 7 | import { useAccountBalance, useTransactor } from "~~/hooks/scaffold-eth"; 8 | 9 | // Number of ETH faucet sends to an address 10 | const NUM_OF_ETH = "1"; 11 | const FAUCET_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; 12 | 13 | /** 14 | * FaucetButton button which lets you grab eth. 15 | */ 16 | export const FaucetButton = () => { 17 | const { address } = useAccount(); 18 | const { balance } = useAccountBalance(address); 19 | 20 | const { chain: ConnectedChain } = useNetwork(); 21 | const isMounted = useIsMounted(); 22 | 23 | const [loading, setLoading] = useState(false); 24 | const localWalletClient = createWalletClient({ 25 | chain: hardhat, 26 | transport: http(), 27 | }); 28 | const faucetTxn = useTransactor(localWalletClient); 29 | 30 | const sendETH = async () => { 31 | try { 32 | setLoading(true); 33 | await faucetTxn({ 34 | chain: hardhat, 35 | account: FAUCET_ADDRESS, 36 | to: address, 37 | value: parseEther(NUM_OF_ETH), 38 | }); 39 | setLoading(false); 40 | } catch (error) { 41 | console.error("⚡️ ~ file: FaucetButton.tsx:sendETH ~ error", error); 42 | setLoading(false); 43 | } 44 | }; 45 | 46 | // Render only on local chain 47 | if (ConnectedChain?.id !== hardhat.id || !isMounted()) { 48 | return null; 49 | } 50 | 51 | return ( 52 |
60 | 69 |
70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import Blockies from "react-blockies"; 3 | import { isAddress } from "viem"; 4 | import { Address } from "viem"; 5 | import { useEnsAddress, useEnsAvatar, useEnsName } from "wagmi"; 6 | import { CommonInputProps, InputBase } from "~~/components/scaffold-eth"; 7 | 8 | // ToDo: move this function to an utility file 9 | const isENS = (address = "") => address.endsWith(".eth") || address.endsWith(".xyz"); 10 | 11 | /** 12 | * Address input with ENS name resolution 13 | */ 14 | export const AddressInput = ({ value, name, placeholder, onChange }: CommonInputProps
) => { 15 | const { data: ensAddress, isLoading: isEnsAddressLoading } = useEnsAddress({ 16 | name: value, 17 | enabled: isENS(value), 18 | chainId: 1, 19 | cacheTime: 30_000, 20 | }); 21 | 22 | const [enteredEnsName, setEnteredEnsName] = useState(); 23 | const { data: ensName, isLoading: isEnsNameLoading } = useEnsName({ 24 | address: value, 25 | enabled: isAddress(value), 26 | chainId: 1, 27 | cacheTime: 30_000, 28 | }); 29 | 30 | const { data: ensAvatar } = useEnsAvatar({ 31 | name: ensName, 32 | enabled: Boolean(ensName), 33 | chainId: 1, 34 | cacheTime: 30_000, 35 | }); 36 | 37 | // ens => address 38 | useEffect(() => { 39 | if (!ensAddress) return; 40 | 41 | // ENS resolved successfully 42 | setEnteredEnsName(value); 43 | onChange(ensAddress); 44 | }, [ensAddress, onChange, value]); 45 | 46 | const handleChange = useCallback( 47 | (newValue: Address) => { 48 | setEnteredEnsName(undefined); 49 | onChange(newValue); 50 | }, 51 | [onChange], 52 | ); 53 | 54 | return ( 55 | 56 | name={name} 57 | placeholder={placeholder} 58 | error={ensAddress === null} 59 | value={value} 60 | onChange={handleChange} 61 | disabled={isEnsAddressLoading || isEnsNameLoading} 62 | prefix={ 63 | ensName && ( 64 |
65 | {ensAvatar ? ( 66 | 67 | { 68 | // eslint-disable-next-line 69 | {`${ensAddress} 70 | } 71 | 72 | ) : null} 73 | {enteredEnsName ?? ensName} 74 |
75 | ) 76 | } 77 | suffix={value && } 78 | /> 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Input/Bytes32Input.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { hexToString, isHex, stringToHex } from "viem"; 3 | import { CommonInputProps, InputBase } from "~~/components/scaffold-eth"; 4 | 5 | export const Bytes32Input = ({ value, onChange, name, placeholder }: CommonInputProps) => { 6 | const convertStringToBytes32 = useCallback(() => { 7 | if (!value) { 8 | return; 9 | } 10 | onChange(isHex(value) ? hexToString(value, { size: 32 }) : stringToHex(value, { size: 32 })); 11 | }, [onChange, value]); 12 | 13 | return ( 14 | 24 | # 25 |
26 | } 27 | /> 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { bytesToString, isHex, toBytes, toHex } from "viem"; 3 | import { CommonInputProps, InputBase } from "~~/components/scaffold-eth"; 4 | 5 | export const BytesInput = ({ value, onChange, name, placeholder }: CommonInputProps) => { 6 | const convertStringToBytes = useCallback(() => { 7 | onChange(isHex(value) ? bytesToString(toBytes(value)) : toHex(toBytes(value))); 8 | }, [onChange, value]); 9 | 10 | return ( 11 | 21 | # 22 |
23 | } 24 | /> 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Input/InputBase.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, ReactNode, useCallback } from "react"; 2 | import { CommonInputProps } from "~~/components/scaffold-eth"; 3 | 4 | type InputBaseProps = CommonInputProps & { 5 | error?: boolean; 6 | disabled?: boolean; 7 | prefix?: ReactNode; 8 | suffix?: ReactNode; 9 | }; 10 | 11 | export const InputBase = string } | undefined = string>({ 12 | name, 13 | value, 14 | onChange, 15 | placeholder, 16 | error, 17 | disabled, 18 | prefix, 19 | suffix, 20 | }: InputBaseProps) => { 21 | let modifier = ""; 22 | if (error) { 23 | modifier = "border-error"; 24 | } else if (disabled) { 25 | modifier = "border-disabled bg-base-300"; 26 | } 27 | 28 | const handleChange = useCallback( 29 | (e: ChangeEvent) => { 30 | onChange(e.target.value as unknown as T); 31 | }, 32 | [onChange], 33 | ); 34 | 35 | return ( 36 |
37 | {prefix} 38 | 48 | {suffix} 49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Input/IntegerInput.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { CommonInputProps, InputBase, IntegerVariant, isValidInteger } from "~~/components/scaffold-eth"; 3 | 4 | type IntegerInputProps = CommonInputProps & { 5 | variant?: IntegerVariant; 6 | }; 7 | 8 | export const IntegerInput = ({ 9 | value, 10 | onChange, 11 | name, 12 | placeholder, 13 | variant = IntegerVariant.UINT256, 14 | }: IntegerInputProps) => { 15 | const [inputError, setInputError] = useState(false); 16 | const multiplyBy1e18 = useCallback(() => { 17 | if (!value) { 18 | return; 19 | } 20 | if (typeof value === "bigint") { 21 | return onChange(value * 10n ** 18n); 22 | } 23 | return onChange(BigInt(Math.round(Number(value) * 10 ** 18))); 24 | }, [onChange, value]); 25 | 26 | useEffect(() => { 27 | if (isValidInteger(variant, value, false)) { 28 | setInputError(false); 29 | } else { 30 | setInputError(true); 31 | } 32 | }, [value, variant]); 33 | 34 | return ( 35 | 47 | 50 |
51 | ) 52 | } 53 | /> 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Input/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AddressInput"; 2 | export * from "./Bytes32Input"; 3 | export * from "./BytesInput"; 4 | export * from "./EtherInput"; 5 | export * from "./InputBase"; 6 | export * from "./IntegerInput"; 7 | export * from "./utils"; 8 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/Input/utils.ts: -------------------------------------------------------------------------------- 1 | export interface CommonInputProps { 2 | value: T; 3 | onChange: (newValue: T) => void; 4 | name?: string; 5 | placeholder?: string; 6 | } 7 | 8 | export enum IntegerVariant { 9 | UINT8 = "uint8", 10 | UINT16 = "uint16", 11 | UINT24 = "uint24", 12 | UINT32 = "uint32", 13 | UINT40 = "uint40", 14 | UINT48 = "uint48", 15 | UINT56 = "uint56", 16 | UINT64 = "uint64", 17 | UINT72 = "uint72", 18 | UINT80 = "uint80", 19 | UINT88 = "uint88", 20 | UINT96 = "uint96", 21 | UINT104 = "uint104", 22 | UINT112 = "uint112", 23 | UINT120 = "uint120", 24 | UINT128 = "uint128", 25 | UINT136 = "uint136", 26 | UINT144 = "uint144", 27 | UINT152 = "uint152", 28 | UINT160 = "uint160", 29 | UINT168 = "uint168", 30 | UINT176 = "uint176", 31 | UINT184 = "uint184", 32 | UINT192 = "uint192", 33 | UINT200 = "uint200", 34 | UINT208 = "uint208", 35 | UINT216 = "uint216", 36 | UINT224 = "uint224", 37 | UINT232 = "uint232", 38 | UINT240 = "uint240", 39 | UINT248 = "uint248", 40 | UINT256 = "uint256", 41 | INT8 = "int8", 42 | INT16 = "int16", 43 | INT24 = "int24", 44 | INT32 = "int32", 45 | INT40 = "int40", 46 | INT48 = "int48", 47 | INT56 = "int56", 48 | INT64 = "int64", 49 | INT72 = "int72", 50 | INT80 = "int80", 51 | INT88 = "int88", 52 | INT96 = "int96", 53 | INT104 = "int104", 54 | INT112 = "int112", 55 | INT120 = "int120", 56 | INT128 = "int128", 57 | INT136 = "int136", 58 | INT144 = "int144", 59 | INT152 = "int152", 60 | INT160 = "int160", 61 | INT168 = "int168", 62 | INT176 = "int176", 63 | INT184 = "int184", 64 | INT192 = "int192", 65 | INT200 = "int200", 66 | INT208 = "int208", 67 | INT216 = "int216", 68 | INT224 = "int224", 69 | INT232 = "int232", 70 | INT240 = "int240", 71 | INT248 = "int248", 72 | INT256 = "int256", 73 | } 74 | 75 | export const SIGNED_NUMBER_REGEX = /^-?\d+\.?\d*$/; 76 | export const UNSIGNED_NUMBER_REGEX = /^\.?\d+\.?\d*$/; 77 | 78 | export const isValidInteger = (dataType: IntegerVariant, value: bigint | string, strict = true) => { 79 | const isSigned = dataType.startsWith("i"); 80 | const bitcount = Number(dataType.substring(isSigned ? 3 : 4)); 81 | 82 | let valueAsBigInt; 83 | try { 84 | valueAsBigInt = BigInt(value); 85 | } catch (e) {} 86 | if (typeof valueAsBigInt !== "bigint") { 87 | if (strict) { 88 | return false; 89 | } 90 | if (!value || typeof value !== "string") { 91 | return true; 92 | } 93 | return isSigned ? SIGNED_NUMBER_REGEX.test(value) || value === "-" : UNSIGNED_NUMBER_REGEX.test(value); 94 | } else if (!isSigned && valueAsBigInt < 0) { 95 | return false; 96 | } 97 | const hexString = valueAsBigInt.toString(16); 98 | const significantHexDigits = hexString.match(/.*x0*(.*)$/)?.[1] ?? ""; 99 | if ( 100 | significantHexDigits.length * 4 > bitcount || 101 | (isSigned && significantHexDigits.length * 4 === bitcount && parseInt(significantHexDigits.slice(-1)?.[0], 16) < 8) 102 | ) { 103 | return false; 104 | } 105 | return true; 106 | }; 107 | -------------------------------------------------------------------------------- /packages/nextjs/components/scaffold-eth/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Address"; 2 | export * from "./Balance"; 3 | export * from "./BlockieAvatar"; 4 | export * from "./Contract"; 5 | export * from "./Faucet"; 6 | export * from "./FaucetButton"; 7 | export * from "./Input"; 8 | export * from "./RainbowKitCustomConnectButton"; 9 | -------------------------------------------------------------------------------- /packages/nextjs/components/tabs/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import styles from "./tabs.module.css"; 3 | 4 | interface IProps { 5 | children: (active: number) => any; 6 | tabTitles: string[]; 7 | } 8 | 9 | export const Tabs = ({ children, tabTitles }: IProps) => { 10 | const [active, setActive] = useState(0); 11 | 12 | return ( 13 |
14 |
15 | {tabTitles.map((title, i) => ( 16 | setActive(i)} 19 | className={`${styles.tabTitle} ${active !== i ? "text-secondary-content" : "text-secondary bg-neutral"}`} 20 | > 21 | {title} 22 | 23 | ))} 24 |
25 |
26 | <>{children(active)} 27 |
28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/nextjs/components/tabs/tabs.module.css: -------------------------------------------------------------------------------- 1 | .tabsContainer { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: column; 5 | justify-content: center; 6 | align-items: center; 7 | max-width: 940px; 8 | 9 | } 10 | .tabsHeader { 11 | display: flex; 12 | flex-direction: row; 13 | display: flex; 14 | width: 100%; 15 | max-width: 455px; 16 | min-width: 330px; 17 | padding: 4px; 18 | gap: 4px; 19 | border-radius: 8px; 20 | } 21 | .tabTitle { 22 | line-height: 16px; 23 | padding: 7px 0; 24 | gap: 10px; 25 | border-radius: 8px; 26 | width: 100%; 27 | text-align: center; 28 | box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.05); 29 | transition: all 0.3s; 30 | cursor: pointer; 31 | } 32 | .tabContent { 33 | width: 100%; 34 | } 35 | 36 | @media (max-width: 768px) { 37 | .tabContent { 38 | width: 96%; 39 | min-width: 330px; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/flashbotRecoveryBundle/useShowError.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { createContext, useContext } from "react"; 3 | 4 | interface IErrorContext { 5 | error: string; 6 | setError: (arg: string) => void; 7 | setIsFinalProcessError: (arg: boolean) => void; 8 | isFinalProcessError: boolean; 9 | } 10 | const initalValue: IErrorContext = { 11 | error: "", 12 | setError: () => ({}), 13 | isFinalProcessError: false, 14 | setIsFinalProcessError: () => ({}), 15 | }; 16 | export const ErrorContext = createContext(initalValue); 17 | 18 | export const ErrorProvider = ({ children }: any) => { 19 | const [error, setError] = useState(""); 20 | const [isFinalProcessError, setIsFinalProcessError] = useState(false); 21 | 22 | return ( 23 | setError(newErr), 27 | isFinalProcessError, 28 | setIsFinalProcessError, 29 | }} 30 | > 31 | {children} 32 | 33 | ); 34 | }; 35 | 36 | export const useShowError = () => { 37 | const { error, setError, setIsFinalProcessError, isFinalProcessError } = useContext(ErrorContext); 38 | const resetError = () => { 39 | setError(""); 40 | }; 41 | const showError = (newError: string, isFinal:boolean = false) => { 42 | setError(newError); 43 | setIsFinalProcessError(isFinal) 44 | }; 45 | return { 46 | error, 47 | isFinalProcessError, 48 | resetError, 49 | showError, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/scaffold-eth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useAccountBalance"; 2 | export * from "./useAnimationConfig"; 3 | export * from "./useBurnerWallet"; 4 | export * from "./useDeployedContractInfo"; 5 | export * from "./useNativeCurrencyPrice"; 6 | export * from "./useNetworkColor"; 7 | export * from "./useOutsideClick"; 8 | export * from "./useScaffoldContract"; 9 | export * from "./useScaffoldContractRead"; 10 | export * from "./useScaffoldContractWrite"; 11 | export * from "./useScaffoldEventSubscriber"; 12 | export * from "./useScaffoldEventHistory"; 13 | export * from "./useTransactor"; 14 | export * from "./useFetchBlocks"; 15 | export * from "./useContractLogs"; 16 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/scaffold-eth/useAccountBalance.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { useBalance } from "wagmi"; 3 | import { useGlobalState } from "~~/services/store/store"; 4 | import { getTargetNetwork } from "~~/utils/scaffold-eth"; 5 | 6 | export function useAccountBalance(address?: string) { 7 | const [isEthBalance, setIsEthBalance] = useState(true); 8 | const [balance, setBalance] = useState(null); 9 | const price = useGlobalState(state => state.nativeCurrencyPrice); 10 | 11 | const { 12 | data: fetchedBalanceData, 13 | isError, 14 | isLoading, 15 | } = useBalance({ 16 | address, 17 | watch: true, 18 | chainId: getTargetNetwork().id, 19 | }); 20 | 21 | const onToggleBalance = useCallback(() => { 22 | if (price > 0) { 23 | setIsEthBalance(!isEthBalance); 24 | } 25 | }, [isEthBalance, price]); 26 | 27 | useEffect(() => { 28 | if (fetchedBalanceData?.formatted) { 29 | setBalance(Number(fetchedBalanceData.formatted)); 30 | } 31 | }, [fetchedBalanceData]); 32 | 33 | return { balance, price, isError, isLoading, onToggleBalance, isEthBalance }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/scaffold-eth/useAnimationConfig.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const ANIMATION_TIME = 2000; 4 | 5 | export function useAnimationConfig(data: any) { 6 | const [showAnimation, setShowAnimation] = useState(false); 7 | const [prevData, setPrevData] = useState(); 8 | 9 | useEffect(() => { 10 | if (prevData !== undefined && prevData !== data) { 11 | setShowAnimation(true); 12 | setTimeout(() => setShowAnimation(false), ANIMATION_TIME); 13 | } 14 | setPrevData(data); 15 | }, [data, prevData]); 16 | 17 | return { 18 | showAnimation, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/scaffold-eth/useContractLogs.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Address, Log } from "viem"; 3 | import { usePublicClient } from "wagmi"; 4 | 5 | export const useContractLogs = (address: Address) => { 6 | const [logs, setLogs] = useState([]); 7 | const client = usePublicClient(); 8 | 9 | useEffect(() => { 10 | const fetchLogs = async () => { 11 | try { 12 | const existingLogs = await client.getLogs({ 13 | address: address, 14 | fromBlock: 0n, 15 | toBlock: "latest", 16 | }); 17 | setLogs(existingLogs); 18 | } catch (error) { 19 | console.error("Failed to fetch logs:", error); 20 | } 21 | }; 22 | fetchLogs(); 23 | 24 | return client.watchBlockNumber({ 25 | onBlockNumber: async (blockNumber, prevBlockNumber) => { 26 | const newLogs = await client.getLogs({ 27 | address: address, 28 | fromBlock: prevBlockNumber, 29 | toBlock: "latest", 30 | }); 31 | setLogs(prevLogs => [...prevLogs, ...newLogs]); 32 | }, 33 | }); 34 | }, [address, client]); 35 | 36 | return logs; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useIsMounted } from "usehooks-ts"; 3 | import { usePublicClient } from "wagmi"; 4 | import scaffoldConfig from "~~/scaffold.config"; 5 | import { Contract, ContractCodeStatus, ContractName, contracts } from "~~/utils/scaffold-eth/contract"; 6 | 7 | /** 8 | * Gets the matching contract info from the contracts file generated by `yarn deploy` 9 | * @param contractName - name of deployed contract 10 | */ 11 | export const useDeployedContractInfo = (contractName: TContractName) => { 12 | const isMounted = useIsMounted(); 13 | const deployedContract = contracts?.[scaffoldConfig.targetNetwork.id]?.[0]?.contracts?.[ 14 | contractName as ContractName 15 | ] as Contract; 16 | const [status, setStatus] = useState(ContractCodeStatus.LOADING); 17 | const publicClient = usePublicClient({ chainId: scaffoldConfig.targetNetwork.id }); 18 | 19 | useEffect(() => { 20 | const checkContractDeployment = async () => { 21 | if (!deployedContract) { 22 | setStatus(ContractCodeStatus.NOT_FOUND); 23 | return; 24 | } 25 | const code = await publicClient.getBytecode({ 26 | address: deployedContract.address, 27 | }); 28 | 29 | if (!isMounted()) { 30 | return; 31 | } 32 | // If contract code is `0x` => no contract deployed on that address 33 | if (code === "0x") { 34 | setStatus(ContractCodeStatus.NOT_FOUND); 35 | return; 36 | } 37 | setStatus(ContractCodeStatus.DEPLOYED); 38 | }; 39 | 40 | checkContractDeployment(); 41 | }, [isMounted, contractName, deployedContract, publicClient]); 42 | 43 | return { 44 | data: status === ContractCodeStatus.DEPLOYED ? deployedContract : undefined, 45 | isLoading: status === ContractCodeStatus.LOADING, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/scaffold-eth/useNativeCurrencyPrice.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useInterval } from "usehooks-ts"; 3 | import scaffoldConfig from "~~/scaffold.config"; 4 | import { fetchPriceFromUniswap } from "~~/utils/scaffold-eth"; 5 | 6 | const enablePolling = false; 7 | 8 | /** 9 | * Get the price of Native Currency based on Native Token/DAI trading pair from Uniswap SDK 10 | * @returns nativeCurrencyPrice: number 11 | */ 12 | export const useNativeCurrencyPrice = () => { 13 | const [nativeCurrencyPrice, setNativeCurrencyPrice] = useState(0); 14 | 15 | // Get the price of ETH from Uniswap on mount 16 | useEffect(() => { 17 | (async () => { 18 | const price = await fetchPriceFromUniswap(); 19 | setNativeCurrencyPrice(price); 20 | })(); 21 | }, []); 22 | 23 | // Get the price of ETH from Uniswap at a given interval 24 | useInterval( 25 | async () => { 26 | const price = await fetchPriceFromUniswap(); 27 | setNativeCurrencyPrice(price); 28 | }, 29 | enablePolling ? scaffoldConfig.pollingInterval : null, 30 | ); 31 | 32 | return nativeCurrencyPrice; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts: -------------------------------------------------------------------------------- 1 | import { useDarkMode } from "usehooks-ts"; 2 | import { getTargetNetwork } from "~~/utils/scaffold-eth"; 3 | 4 | const DEFAULT_NETWORK_COLOR: [string, string] = ["#666666", "#bbbbbb"]; 5 | 6 | /** 7 | * Gets the color of the target network 8 | */ 9 | export const useNetworkColor = () => { 10 | const { isDarkMode } = useDarkMode(); 11 | const colorConfig = getTargetNetwork().color ?? DEFAULT_NETWORK_COLOR; 12 | 13 | return Array.isArray(colorConfig) ? (isDarkMode ? colorConfig[1] : colorConfig[0]) : colorConfig; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/scaffold-eth/useOutsideClick.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | /** 4 | * Check if a click was made outside the passed ref 5 | */ 6 | export const useOutsideClick = (ref: React.RefObject, callback: { (): void }) => { 7 | useEffect(() => { 8 | function handleOutsideClick(event: MouseEvent) { 9 | if (!(event.target instanceof Element)) { 10 | return; 11 | } 12 | 13 | if (ref.current && !ref.current.contains(event.target)) { 14 | callback(); 15 | } 16 | } 17 | 18 | document.addEventListener("click", handleOutsideClick); 19 | return () => document.removeEventListener("click", handleOutsideClick); 20 | }, [ref, callback]); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts: -------------------------------------------------------------------------------- 1 | import { Abi } from "abitype"; 2 | import { getContract } from "viem"; 3 | import { GetWalletClientResult } from "wagmi/actions"; 4 | import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; 5 | import { ContractName } from "~~/utils/scaffold-eth/contract"; 6 | 7 | /** 8 | * Gets a deployed contract by contract name and returns a contract instance 9 | * @param config - The config settings 10 | * @param config.contractName - Deployed contract name 11 | * @param config.walletClient - An viem wallet client instance (optional) 12 | */ 13 | export const useScaffoldContract = ({ 14 | contractName, 15 | walletClient, 16 | }: { 17 | contractName: TContractName; 18 | walletClient?: GetWalletClientResult; 19 | }) => { 20 | const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo(contractName); 21 | 22 | // type GetWalletClientResult = WalletClient | null, hence narrowing it to undefined so that it can be passed to getContract 23 | const walletClientInstance = walletClient != null ? walletClient : undefined; 24 | 25 | let contract = undefined; 26 | if (deployedContractData) { 27 | contract = getContract({ 28 | address: deployedContractData.address, 29 | abi: deployedContractData.abi as Abi, 30 | walletClient: walletClientInstance, 31 | }); 32 | } 33 | 34 | return { 35 | data: contract, 36 | isLoading: deployedContractLoading, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/scaffold-eth/useScaffoldContractRead.ts: -------------------------------------------------------------------------------- 1 | import type { ExtractAbiFunctionNames } from "abitype"; 2 | import { useContractRead } from "wagmi"; 3 | import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; 4 | import { getTargetNetwork } from "~~/utils/scaffold-eth"; 5 | import { 6 | AbiFunctionReturnType, 7 | ContractAbi, 8 | ContractName, 9 | UseScaffoldReadConfig, 10 | } from "~~/utils/scaffold-eth/contract"; 11 | 12 | /** 13 | * @dev wrapper for wagmi's useContractRead hook which loads in deployed contract contract abi, address automatically 14 | * @param config - The config settings, including extra wagmi configuration 15 | * @param config.contractName - deployed contract name 16 | * @param config.functionName - name of the function to be called 17 | * @param config.args - args to be passed to the function call 18 | */ 19 | export const useScaffoldContractRead = < 20 | TContractName extends ContractName, 21 | TFunctionName extends ExtractAbiFunctionNames, "pure" | "view">, 22 | >({ 23 | contractName, 24 | functionName, 25 | args, 26 | ...readConfig 27 | }: UseScaffoldReadConfig) => { 28 | const { data: deployedContract } = useDeployedContractInfo(contractName); 29 | 30 | return useContractRead({ 31 | chainId: getTargetNetwork().id, 32 | functionName, 33 | address: deployedContract?.address, 34 | abi: deployedContract?.abi, 35 | watch: true, 36 | args, 37 | enabled: !Array.isArray(args) || !args.some(arg => arg === undefined), 38 | ...(readConfig as any), 39 | }) as Omit, "data" | "refetch"> & { 40 | data: AbiFunctionReturnType | undefined; 41 | refetch: (options?: { 42 | throwOnError: boolean; 43 | cancelRefetch: boolean; 44 | }) => Promise>; 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/scaffold-eth/useScaffoldContractWrite.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Abi, ExtractAbiFunctionNames } from "abitype"; 3 | import { parseEther } from "viem"; 4 | import { useContractWrite, useNetwork } from "wagmi"; 5 | import { getParsedError } from "~~/components/scaffold-eth"; 6 | import { useDeployedContractInfo, useTransactor } from "~~/hooks/scaffold-eth"; 7 | import { getTargetNetwork, notification } from "~~/utils/scaffold-eth"; 8 | import { ContractAbi, ContractName, UseScaffoldWriteConfig } from "~~/utils/scaffold-eth/contract"; 9 | 10 | type UpdatedArgs = Parameters>["writeAsync"]>[0]; 11 | 12 | /** 13 | * @dev wrapper for wagmi's useContractWrite hook(with config prepared by usePrepareContractWrite hook) which loads in deployed contract abi and address automatically 14 | * @param config - The config settings, including extra wagmi configuration 15 | * @param config.contractName - deployed contract name 16 | * @param config.functionName - name of the function to be called 17 | * @param config.args - arguments for the function 18 | * @param config.value - value in ETH that will be sent with transaction 19 | */ 20 | export const useScaffoldContractWrite = < 21 | TContractName extends ContractName, 22 | TFunctionName extends ExtractAbiFunctionNames, "nonpayable" | "payable">, 23 | >({ 24 | contractName, 25 | functionName, 26 | args, 27 | value, 28 | onBlockConfirmation, 29 | blockConfirmations, 30 | ...writeConfig 31 | }: UseScaffoldWriteConfig) => { 32 | const { data: deployedContractData } = useDeployedContractInfo(contractName); 33 | const { chain } = useNetwork(); 34 | const writeTx = useTransactor(); 35 | const [isMining, setIsMining] = useState(false); 36 | const configuredNetwork = getTargetNetwork(); 37 | 38 | const wagmiContractWrite = useContractWrite({ 39 | chainId: configuredNetwork.id, 40 | address: deployedContractData?.address, 41 | abi: deployedContractData?.abi as Abi, 42 | functionName: functionName as any, 43 | args: args as unknown[], 44 | value: value ? parseEther(value) : undefined, 45 | ...writeConfig, 46 | }); 47 | 48 | const sendContractWriteTx = async ({ 49 | args: newArgs, 50 | value: newValue, 51 | ...otherConfig 52 | }: { 53 | args?: UseScaffoldWriteConfig["args"]; 54 | value?: UseScaffoldWriteConfig["value"]; 55 | } & UpdatedArgs = {}) => { 56 | if (!deployedContractData) { 57 | notification.error("Target Contract is not deployed, did you forgot to run `yarn deploy`?"); 58 | return; 59 | } 60 | if (!chain?.id) { 61 | notification.error("Please connect your wallet"); 62 | return; 63 | } 64 | if (chain?.id !== configuredNetwork.id) { 65 | notification.error("You on the wrong network"); 66 | return; 67 | } 68 | 69 | if (wagmiContractWrite.writeAsync) { 70 | try { 71 | setIsMining(true); 72 | await writeTx( 73 | () => 74 | wagmiContractWrite.writeAsync({ 75 | args: newArgs ?? args, 76 | value: newValue ? parseEther(newValue) : value && parseEther(value), 77 | ...otherConfig, 78 | }), 79 | { onBlockConfirmation, blockConfirmations }, 80 | ); 81 | } catch (e: any) { 82 | const message = getParsedError(e); 83 | notification.error(message); 84 | } finally { 85 | setIsMining(false); 86 | } 87 | } else { 88 | notification.error("Contract writer error. Try again."); 89 | return; 90 | } 91 | }; 92 | 93 | return { 94 | ...wagmiContractWrite, 95 | isMining, 96 | // Overwrite wagmi's write async 97 | writeAsync: sendContractWriteTx, 98 | }; 99 | }; 100 | -------------------------------------------------------------------------------- /packages/nextjs/hooks/scaffold-eth/useScaffoldEventSubscriber.ts: -------------------------------------------------------------------------------- 1 | import { Abi, ExtractAbiEventNames } from "abitype"; 2 | import { Log } from "viem"; 3 | import { useContractEvent } from "wagmi"; 4 | import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; 5 | import { getTargetNetwork } from "~~/utils/scaffold-eth"; 6 | import { ContractAbi, ContractName, UseScaffoldEventConfig } from "~~/utils/scaffold-eth/contract"; 7 | 8 | /** 9 | * @dev wrapper for wagmi's useContractEvent 10 | * @param config - The config settings 11 | * @param config.contractName - deployed contract name 12 | * @param config.eventName - name of the event to listen for 13 | * @param config.listener - the callback that receives events. If only interested in 1 event, call `unwatch` inside of the listener 14 | */ 15 | export const useScaffoldEventSubscriber = < 16 | TContractName extends ContractName, 17 | TEventName extends ExtractAbiEventNames>, 18 | >({ 19 | contractName, 20 | eventName, 21 | listener, 22 | }: UseScaffoldEventConfig) => { 23 | const { data: deployedContractData } = useDeployedContractInfo(contractName); 24 | 25 | return useContractEvent({ 26 | address: deployedContractData?.address, 27 | abi: deployedContractData?.abi as Abi, 28 | chainId: getTargetNetwork().id, 29 | listener: listener as (logs: Log[]) => void, 30 | eventName, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/nextjs/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface BlocksApiResponseTransactionDetails { 2 | transaction_hash: string; 3 | tx_index: number; 4 | bundle_type: "rogue" | "flashbots" | "mempool"; 5 | bundle_index: number; 6 | block_number: number; 7 | eoa_address: string; 8 | to_address: string; 9 | gas_used: number; 10 | gas_price: string; 11 | coinbase_transfer: string; 12 | eth_sent_to_fee_recipient: string; 13 | total_miner_reward: string; 14 | fee_recipient_eth_diff: string; 15 | } 16 | export interface TransactionSimulationBase { 17 | txHash: string; 18 | gasUsed: number; 19 | gasFees: string; 20 | gasPrice: string; 21 | toAddress: string; 22 | fromAddress: string; 23 | coinbaseDiff: string; 24 | } 25 | 26 | export interface TransactionSimulationSuccess extends TransactionSimulationBase { 27 | value: string; 28 | ethSentToCoinbase: string; 29 | coinbaseDiff: string; 30 | } 31 | 32 | export interface TransactionSimulationRevert extends TransactionSimulationBase { 33 | error: string; 34 | revert: string; 35 | } 36 | 37 | export type TransactionSimulation = TransactionSimulationSuccess | TransactionSimulationRevert; 38 | -------------------------------------------------------------------------------- /packages/nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | reactStrictMode: true, 6 | typescript: { 7 | ignoreBuildErrors: true, 8 | }, 9 | eslint: { 10 | ignoreDuringBuilds: true, 11 | }, 12 | }; 13 | 14 | module.exports = nextConfig; 15 | -------------------------------------------------------------------------------- /packages/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@se-2/nextjs", 3 | "private": true, 4 | "version": "0.1.0", 5 | "scripts": { 6 | "dev": "next dev -p 3005", 7 | "start": "next dev", 8 | "build": "next build", 9 | "serve": "next start", 10 | "lint": "next lint", 11 | "format": "prettier --write . '!(node_modules|.next|contracts)/**/*'", 12 | "check-types": "tsc --noEmit --incremental", 13 | "vercel": "vercel", 14 | "vercel:yolo": "vercel --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true" 15 | }, 16 | "dependencies": { 17 | "@ethersproject/providers": "^5.7.2", 18 | "@flashbots/ethers-provider-bundle": "^0.6.2", 19 | "@heroicons/react": "^2.0.11", 20 | "@impersonator/iframe": "^0.1.1", 21 | "@rainbow-me/rainbowkit": "^1.0.4", 22 | "@uniswap/sdk-core": "^4.0.1", 23 | "@uniswap/v2-sdk": "^3.0.1", 24 | "alchemy-sdk": "^2.9.2", 25 | "daisyui": "^2.31.0", 26 | "ethers": "^5.0.0", 27 | "framer-motion": "^10.16.0", 28 | "next": "^13.1.6", 29 | "nextjs-progressbar": "^0.0.16", 30 | "node-gyp": "^9.4.0", 31 | "react": "^18.2.0", 32 | "react-blockies": "^1.4.1", 33 | "react-copy-to-clipboard": "^5.1.0", 34 | "react-dom": "^18.2.0", 35 | "react-fast-marquee": "^1.3.5", 36 | "react-hot-toast": "^2.4.0", 37 | "react-modal": "^3.16.1", 38 | "use-debounce": "^8.0.4", 39 | "usehooks-ts": "^2.7.2", 40 | "uuidv4": "^6.2.13", 41 | "viem": "^1.2.1", 42 | "wagmi": "^1.3.2", 43 | "zustand": "^4.1.2" 44 | }, 45 | "devDependencies": { 46 | "@trivago/prettier-plugin-sort-imports": "^4.1.1", 47 | "@types/node": "^17.0.35", 48 | "@types/react": "^18.0.9", 49 | "@types/react-blockies": "^1.4.1", 50 | "@types/react-copy-to-clipboard": "^5.0.4", 51 | "@types/react-dom": "^18.2.7", 52 | "@types/react-modal": "^3", 53 | "@typescript-eslint/eslint-plugin": "^5.39.0", 54 | "autoprefixer": "^10.4.12", 55 | "eslint": "^8.15.0", 56 | "eslint-config-next": "^13.1.6", 57 | "eslint-config-prettier": "^8.5.0", 58 | "eslint-plugin-prettier": "^4.2.1", 59 | "postcss": "^8.4.16", 60 | "prettier": "^2.8.4", 61 | "tailwindcss": "^3.1.8", 62 | "typescript": "^4.9.5", 63 | "vercel": "^28.15.1" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/nextjs/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import type { AppProps } from "next/app"; 3 | import { ImpersonatorIframeProvider } from "@impersonator/iframe"; 4 | import { RainbowKitProvider, darkTheme, lightTheme } from "@rainbow-me/rainbowkit"; 5 | import "@rainbow-me/rainbowkit/styles.css"; 6 | import NextNProgress from "nextjs-progressbar"; 7 | import { Toaster } from "react-hot-toast"; 8 | import { useDarkMode } from "usehooks-ts"; 9 | import { WagmiConfig } from "wagmi"; 10 | import { Footer } from "~~/components/Footer"; 11 | import { BlockieAvatar } from "~~/components/scaffold-eth"; 12 | import { ErrorProvider } from "~~/hooks/flashbotRecoveryBundle/useShowError"; 13 | import { useNativeCurrencyPrice } from "~~/hooks/scaffold-eth"; 14 | import { useGlobalState } from "~~/services/store/store"; 15 | import { wagmiConfig } from "~~/services/web3/wagmiConfig"; 16 | import { appChains } from "~~/services/web3/wagmiConnectors"; 17 | import "~~/styles/globals.css"; 18 | 19 | const ScaffoldEthApp = ({ Component, pageProps }: AppProps) => { 20 | const price = useNativeCurrencyPrice(); 21 | const setNativeCurrencyPrice = useGlobalState(state => state.setNativeCurrencyPrice); 22 | // This variable is required for initial client side rendering of correct theme for RainbowKit 23 | const [isDarkTheme, setIsDarkTheme] = useState(true); 24 | const { isDarkMode } = useDarkMode(); 25 | 26 | useEffect(() => { 27 | if (price > 0) { 28 | setNativeCurrencyPrice(price); 29 | } 30 | }, [setNativeCurrencyPrice, price]); 31 | 32 | useEffect(() => { 33 | setIsDarkTheme(isDarkMode); 34 | }, [isDarkMode]); 35 | 36 | return ( 37 | 38 | 39 | 44 | 45 |
46 | {/*
*/} 47 |
48 | 49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 | ); 57 | }; 58 | 59 | export default ScaffoldEthApp; 60 | -------------------------------------------------------------------------------- /packages/nextjs/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Head, Html, Main, NextScript } from "next/document"; 2 | 3 | export default class MyDocument extends Document { 4 | render() { 5 | const title = "Hacked Wallet Recovery | Secure Ethereum Asset Recovery Tool"; 6 | const description = 7 | "Instantly recover assets from hacked Ethereum wallets using Flashbots. Bypass malicious sweepers, and securely transfer ERC20, NFTs, and other tokens to safety. Free, open-source tool trusted by the Web3 community."; 8 | const baseUrl = "https://hackedwalletrecovery.com"; 9 | const imageUrl = `${baseUrl}/thumbnail.png`; 10 | 11 | return ( 12 | 13 | 14 | {/* Primary Meta Tags */} 15 | 16 | 17 | {/* Open Graph / Facebook */} 18 | 19 | 20 | 21 | 22 | 23 | 24 | {/* Twitter */} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {/* Favicons for different platforms */} 33 | 34 | {/* This is the key meta tag for Google search results favicon */} 35 | 36 | 37 | {/* Analytics */} 38 |