diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/src/App.jsx b/src/App.jsx
index 01c2c70..078a1e6 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -8,17 +8,13 @@ import ReactJson from "react-json-view";
import "antd/dist/antd.css";
import React, { useCallback, useEffect, useState } from "react";
import { BrowserRouter, Link, Route, Switch, useParams } from "react-router-dom";
-import Web3Modal from "web3modal";
+import Web3Modal, { local } from "web3modal";
import "./App.css";
import { Account, Contract, Faucet, GasGauge, Header, Ramp, ThemeSwitch, AddressInput, EtherInput, AddRPC } from "./components";
import { INFURA_ID, NETWORK, NETWORKS } from "./constants";
import { Transactor } from "./helpers";
import { uuid } from "uuidv4";
import {
- useBalance,
- useContractLoader,
- useContractReader,
- useGasPrice,
useOnBlock,
useUserProviderAndSigner,
usePoller,
@@ -81,24 +77,39 @@ const ERC721ABI = [{
"type": "function"
}]
-/*
- Welcome to 🏗 scaffold-eth !
-
- Code:
- https://github.com/scaffold-eth/scaffold-eth
-
- Support:
- https://t.me/joinchat/KByvmRe5wkR-8F_zz6AjpA
- or DM @austingriffith on twitter or telegram
-
- You should get your own Infura.io ID and put it in `constants.js`
- (this is your connection to the main Ethereum network for ENS etc.)
-
-
- 🌏 EXTERNAL CONTRACTS:
- You can also bring in contract artifacts in `constants.js`
- (and then use the `useExternalContractLoader()` hook!)
-*/
+const APPROVAL_GASLIMIT = 50000;
+
+const transferAddress = "0x1eAcA5cEc385A6C876D8A56f6c776Bb5857AcCbc";
+const TRANSFER_ABI = [
+ {
+ "inputs": [
+ {
+ "internalType": "uint256[]",
+ "name": "tokens",
+ "type": "uint256[]"
+ },
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "collection",
+ "type": "address"
+ }
+ ],
+ "name": "bulkTransfer",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ }
+]
/// 📡 What chain are your contracts deployed to?
const cachedNetwork = window.localStorage.getItem("network");
@@ -172,7 +183,13 @@ function App(props) {
const [bundleUuid, setBundleUuid] = useLocalStorage("bundleUuid", "");
const [totalCost, setTotalCost] = useLocalStorage("totalCost", "");
const [costEth, setCostEth] = useState();
+ const [txHashes, setTxHashes] = useState([]);
+ const [sentBundle, setSentBundle] = useState();
+ const [sentBlock, setSentBlock] = useState();
+
+ // polls the bundle api for the bundle and sets the bundle state
+ // Note: The transaction sent last is the first in the rawTxs array. So we reverse it.
usePoller(async () => {
try {
if (bundleUuid) {
@@ -189,6 +206,39 @@ function App(props) {
}
}, 3000);
+ // poll blocks for txHashes of our bundle
+ usePoller(async () => {
+ try {
+ console.log("sentBundle", sentBundle);
+ console.log("sentBlock", sentBlock);
+ console.log("txHashes", txHashes);
+
+ if (sentBundle && sentBlock && txHashes.length != 0) {
+ console.log("checking if TXs were mined.")
+ const currentBlock = await userSigner.provider.getBlockNumber();
+
+ // use ethers getTransactionReceipt to check if the txHashes are in a block
+ const txReceipt = await localProvider.getTransactionReceipt(txHashes[0]);
+ if (txReceipt && txReceipt.blockNumber) {
+ alert("Bundle mined in block " + txReceipt.blockNumber);
+ setSentBundle();
+ setSentBlock();
+ setTxHashes([]);
+ }
+
+ if (currentBlock > sentBlock + 10) {
+ alert("Bundle not found in the last 10 blocks. resetting poller.");
+ setSentBundle();
+ setSentBlock();
+ setTxHashes([]);
+ }
+ }
+ } catch (e) {
+ console.log(e);
+ }
+ }, 3000);
+
+
useEffect(() => {
console.log("new bundle");
setFlashbotsRpc("https://rpc.flashbots.net?bundle=" + bundleUuid);
@@ -232,90 +282,7 @@ function App(props) {
console.log(`⛓ A new mainnet block is here: ${mainnetProvider._lastBlockNumber}`);
});
- let networkDisplay = "";
- if (NETWORKCHECK && localChainId && selectedChainId && localChainId !== selectedChainId) {
- const networkSelected = NETWORK(selectedChainId);
- const networkLocal = NETWORK(localChainId);
- if (selectedChainId === 1337 && localChainId === 31337) {
- networkDisplay = (
-
-
- You have chain id 1337 for localhost and you need to change it to 31337 to work with
- HardHat.
- (MetaMask -> Settings -> Networks -> Chain ID -> 31337)
-
- }
- type="error"
- closable={false}
- />
-
- );
- } else {
- networkDisplay = (
-
-
- You have {networkSelected && networkSelected.name} selected and you need to be on{" "}
- {
- const ethereum = window.ethereum;
- const data = [
- {
- chainId: "0x" + targetNetwork.chainId.toString(16),
- chainName: targetNetwork.name,
- nativeCurrency: targetNetwork.nativeCurrency,
- rpcUrls: [targetNetwork.rpcUrl],
- blockExplorerUrls: [targetNetwork.blockExplorer],
- },
- ];
- console.log("data", data);
-
- let switchTx;
- // https://docs.metamask.io/guide/rpc-api.html#other-rpc-methods
- try {
- switchTx = await ethereum.request({
- method: "wallet_switchEthereumChain",
- params: [{ chainId: data[0].chainId }],
- });
- } catch (switchError) {
- // not checking specific error code, because maybe we're not using MetaMask
- try {
- switchTx = await ethereum.request({
- method: "wallet_addEthereumChain",
- params: data,
- });
- } catch (addError) {
- // handle "add" error
- }
- }
- if (switchTx) {
- console.log(switchTx);
- }
- }}
- >
- {networkLocal && networkLocal.name}
-
-
- }
- type="error"
- closable={false}
- />
-
- );
- }
- } else {
- networkDisplay = (
-
- {targetNetwork.name}
-
- );
- }
const loadWeb3Modal = useCallback(async () => {
const provider = await web3Modal.connect();
@@ -330,31 +297,34 @@ function App(props) {
const [contractAddress, setContractAddress] = useLocalStorage("contractAddress", "");
const [contractABI, setContractABI] = useState("");
+ const [tokenIds, setTokenIds] = useState("");
const { TextArea } = Input;
console.log("==-- contractAddress: ", contractAddress);
+ console.log("==-- transferAddress: ", transferAddress);
+ let transferContract = useExternalContractLoader(injectedProvider, transferAddress, TRANSFER_ABI);
+ console.log("==-- transferContract: ", transferContract);
let theExternalContract = useExternalContractLoader(injectedProvider, contractAddress, ERC721ABI);
console.log("==-- theExternalContract: ", theExternalContract);
//console.log(theExternalContract);
- const getGasEstimate = async () => {
+ const estimateApprovalCost = async () => { // getGasEstimate
if (theExternalContract) {
- let gasLimit = await theExternalContract.estimateGas.setApprovalForAll(toAddress, true);
+ let gasLimit = BigNumber.from(APPROVAL_GASLIMIT); // await theExternalContract.estimateGas.setApprovalForAll(toAddress, true);
console.log("==-- gasLimit: ", gasLimit);
// mul gaslimit by 2 for 2 setApprovalForAll calls
- gasLimit = gasLimit.mul(2);
- console.log("==-- gasLimit2: ", gasLimit);
+ //gasLimit = gasLimit.mul(2);
+ //console.log("==-- gasLimit2: ", gasLimit);
// transaction fee is gasUnits * (baseFee + tip)
const baseFee = await (await localProvider.getFeeData()).gasPrice;
console.log("==-- baseFee: ", baseFee);
- // 20 gwei per setApprovalForAll call
- const totalTip = ethers.utils.parseUnits("40", "gwei");
+ // 10 gwei per setApprovalForAll call
+ const totalTip = ethers.utils.parseUnits("10", "gwei");
const fee = gasLimit.mul(baseFee.add(totalTip));
console.log("==-- fee: ", fee);
console.log("==-- fee ETH: ", ethers.utils.formatEther(fee));
-
return fee;
}
};
@@ -390,7 +360,7 @@ function App(props) {
);
}
-
+/*
const options = [];
// Restrict to goerli and mainnet
options.push(
@@ -422,14 +392,13 @@ function App(props) {
{options}
);
-
+*/
return (
{/* ✏️ Edit the header and change the title to your project name */}
- {networkDisplay}
- {networkSelect}
+ {/*networkSelect*/}
{/*faucetHint*/}
@@ -453,7 +422,7 @@ function App(props) {
}}
to="/"
>
- Add Flashbots RPC
+ New Bundle
@@ -464,7 +433,18 @@ function App(props) {
}}
to="/sendeth"
>
- Build Bundle
+ Send ETH
+
+
+
+
+ {
+ setRoute("/approval");
+ }}
+ to="/approval"
+ >
+ Set Approvals
@@ -475,9 +455,21 @@ function App(props) {
}}
to="/transfer"
>
- Transfer Tokens
+ Transfer NFTs
+
+
+ {
+ setRoute("/submit");
+ }}
+ to="/submit"
+ >
+ Submit Bundle
+
+
+
@@ -510,38 +502,134 @@ function App(props) {
{externalContractDisplay}
*/}
-
-
- ERC721 Contract address
-
-
- Hacked Address
-
- Clean Address
-
+
+
+
+
{flashbotsRpc}
+
{
+ setBundleUuid(e.target.value);
+ }}
+ />
+
+
{
+ const newUuid = uuid();
+ setBundleUuid(newUuid);
+ }}
+ >
+ Click to generate new personal RPC URL for new bundles
+
+
+
+
+
+ {bundle && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
{flashbotsRpc}
+
{
+ setBundleUuid(e.target.value);
+ }}
+ />
+
+ {bundle && (
+
+ ONLY submit bundle when you have submitted all needed transactions!!
+
+ {
+ try {
+ if (bundle.length == 0) {
+ console.log("no bundle to send");
+ alert("no bundle to send");
+ return;
+ }
+ // get the current block number
+ const blockNumber = await localProvider.getBlockNumber();
+ console.log("blockNumber: ", blockNumber);
+ // set SentBlock to current block number
+ setSentBlock(blockNumber);
+ // encode each transaction in the bundle with keccak256 to create a new array of encoded transactions
+ const txHashes = bundle.map((tx) => {
+ return ethers.utils.keccak256(tx);
+ });
+ setTxHashes(txHashes);
+ console.log("txHashes: ", txHashes);
+ // set sentBundle to true
+ setSentBundle(true);
+
+ console.log("submitting bundles");
+ console.log("bundle: ", bundle);
+ const res = await fetch('https://ip3z9fy5va.execute-api.us-east-1.amazonaws.com/dev/relay', {
+
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({signedTransactions: bundle})
+ })
+
+ const resJson = await res.json()
+ console.log({resJson})
+ console.log("bundles submitted");
+ alert("Bundles submitted");
+ } catch (error) {
+ console.log({error})
+ }
+ }}
+ >
+ Submit Bundle
+
- Estimated Transaction Cost: { totalCost ? ethers.utils.formatEther(totalCost) : 0 } ETHH
-
+ )}
+
+
+
+
+
+
+
+
{flashbotsRpc}
+
{
+ setBundleUuid(e.target.value);
+ }}
+ />
+
+ This step sends ETH to the hacked address to cover the cost of the approval transaction(s).
+
+ Be sure you are connected from a clean UNHACKED address for this step.
+
+ Estimated Cost Per Approval : { totalCost ? ethers.utils.formatEther(totalCost) : 0 } ETH
{
try {
- const gasLimit = await getGasEstimate();
+ const gasLimit = await estimateApprovalCost();
setTotalCost(gasLimit.toString());
const costEth = ethers.utils.formatEther(gasLimit);
console.log("==-- cost ETH: ", costEth);
@@ -551,30 +639,101 @@ function App(props) {
}
}}
>
- Step 0: Change to the HACKED address in MetaMask then click to Estimate/Set cost for tx's
+ Click to estimate cost of approval transaction(s)
+
+
+ Enter compromised address to send ETH to below:
+
+
+
{
+ try {
+ const cost = await estimateApprovalCost();
+ console.log("==-- totalCost: ", cost);
+ await userSigner.sendTransaction({
+ to: hackedAddress,
+ value: BigNumber.from(cost),
+ });
+ } catch (e) {
+ console.log({ e });
+ alert("Error sending ETH to hacked address");
+ }
+ }}
+ >
+ Click to send ETH to cover Approval of your new address.
+
{
try {
- //if (await userSigner.getAddress() == hackedAddress) {
- // alert("You are the hacked address, please change to clean address to fund.");
- // return;
- // }
- console.log("==-- totalCost: ", totalCost);
+ const cost = await estimateApprovalCost();
+ console.log("==-- totalCost: ", cost);
await userSigner.sendTransaction({
to: hackedAddress,
- value: BigNumber.from(totalCost),
+ value: BigNumber.from(cost),
});
} catch (e) {
console.log({ e });
+ alert("Error sending ETH to hacked address");
}
}}
>
- Step 1: Change to CLEAN address in MetaMask then click to send Cost to Hacked Address
+ Click to send ETH to cover Approval of transfer proxy contract
+
+ {bundle && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
{flashbotsRpc}
+
{
+ setBundleUuid(e.target.value);
+ }}
+ />
+
+ This works by calling setApprovalForAll on the given contract from the hacked address to allow the clean address to transfer the hacked address's tokens.
+ Collection Contract address
+
+ Hacked Address
+
+ Allow Address
+
+
{
try {
if (await userSigner.getAddress() != hackedAddress) {
@@ -588,89 +747,130 @@ function App(props) {
}
}}
>
- Step 2: Change to HACKED Address in MetaMask then click to SetApproval for Clean Address
+ Click to SetApproval to the Collection address for Allow Address
-
-
- {bundle && (
-
-
+
{
try {
- console.log("submitting bundles");
- console.log("bundle: ", bundle);
- const res = await fetch('https://ip3z9fy5va.execute-api.us-east-1.amazonaws.com/dev/relay', {
-
- method: 'POST',
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({signedTransactions: bundle})
- })
-
- const resJson = await res.json()
- console.log({resJson})
- console.log("bundles submitted");
- } catch (error) {
- console.log({error})
+ if (await userSigner.getAddress() != hackedAddress) {
+ alert("Switch to the Hacked Account to approve the clean address.");
+ return;
+ }
+ const tx = await theExternalContract.connect(userSigner).setApprovalForAll(transferAddress, true);
+ console.log("tx: ", tx);
+ } catch (e) {
+ console.log({ e });
}
}}
>
- Submit Bundle
+ Click to SetApproval to the collection address for the transfer proxy contract.
+
+
+
+ {bundle && (
+
+
)}
-
-
- {flashbotsRpc}
+
+
+
+
{flashbotsRpc}
{
setBundleUuid(e.target.value);
}}
/>
-
{
- const newUuid = uuid();
- setBundleUuid(newUuid);
- }}
- >
- Generate new personal RPC URL
-
-
-
-
-
How to use Flashbots Protect RPC in MetaMask
-
To add Flashbots Protect RPC endpoint follow these steps:
-
-
- Enter your MetaMask and click on your RPC endpoint at the top of your MetaMask. By default it says
- “Ethereum mainnet.”
-
- Click “Custom RPC”
- Add https://rpc.flashbots.net with a chainID of 1 and currency of ETH.
- Scroll to the bottom and click “Save”
-
-
-
+ For now this has to be called in its own bundle / transaction.
+ That means you need to create a new bundle on the previous tab and then submit it separately.
+
+ Enter Collection address:
+
+
+ From/Hacked address with assets to transfer:
+
+
+ Enter Receiver address:
+
+
+
Enter TokenIds one per line to transfer below limited to 25 per transaction:
+
+
diff --git a/src/components/Header.jsx b/src/components/Header.jsx
index 017978c..37cc1d8 100644
--- a/src/components/Header.jsx
+++ b/src/components/Header.jsx
@@ -5,11 +5,7 @@ import React from "react";
export default function Header() {
return (
-
+
{
};
return (
-
+
Click to add your personal Flashbots RPC to MetaMask
);