diff --git a/.env.template b/.env.template
new file mode 100644
index 0000000..1c79980
--- /dev/null
+++ b/.env.template
@@ -0,0 +1,2 @@
+NEXT_PUBLIC_INFURA_ID=
+FLASHBOTS_API_URL=https://blocks.flashbots.net/v1/blocks
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..e6e997f
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,84 @@
+{
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": 2020,
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "jsx": true
+ }
+ },
+ "plugins": ["@typescript-eslint", "ternary"],
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:react/recommended",
+ "plugin:react-hooks/recommended",
+ "plugin:ternary/recommended"
+ ],
+ "settings": {
+ "react": {
+ "version": "detect"
+ }
+ },
+ "rules": {
+ "comma-spacing": ["error", { "before": false, "after": true }],
+ "indent": ["error", 2],
+ "linebreak-style": ["error", "unix"],
+ "eol-last": ["error", "always"],
+ "quotes": ["error", "double"],
+ "semi": ["error", "always"],
+ "max-len": [
+ "error",
+ {
+ "code": 300
+ }
+ ],
+ "react-hooks/rules-of-hooks": "error",
+ "react-hooks/exhaustive-deps": "warn",
+ "react/prop-types": 0,
+ "react/jsx-max-props-per-line": [2, {
+ "maximum": 1,
+ "when": "always"
+ }],
+ "react/jsx-fragments": "error",
+ "arrow-spacing": "error",
+ "space-infix-ops": "error",
+ "no-trailing-spaces": ["error", { "ignoreComments": true }],
+ "comma-dangle": ["error", "never"],
+ "object-curly-spacing": ["error", "always"],
+ "space-in-parens": ["error", "never"],
+ "ternary/no-unreachable": "off",
+ "ternary/nesting": "off",
+ "@typescript-eslint/no-var-requires": "off",
+ "@typescript-eslint/no-unused-vars": "error",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/camelcase": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off",
+ "@typescript-eslint/member-delimiter-style" : ["warn", {
+ "multiline": {
+ "delimiter": "none",
+ "requireLast": true
+ },
+ "singleline": {
+ "delimiter": "semi",
+ "requireLast": false
+ }
+ }]
+ },
+ "ignorePatterns": [
+ ".github/**",
+ ".vscode/**",
+ ".yarn/**",
+ "**/dist/*",
+ "**/node_modules/*"
+ ],
+ "env": {
+ "browser": true,
+ "amd": true,
+ "node": true
+ }
+}
diff --git a/.gitignore b/.gitignore
index 1437c53..3f21c02 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,7 @@ yarn-debug.log*
yarn-error.log*
# local env files
+.env
.env.local
.env.development.local
.env.test.local
@@ -32,3 +33,4 @@ yarn-error.log*
# vercel
.vercel
+
diff --git a/README.md b/README.md
index 9559ce4..a7e1b17 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,14 @@
Deployed at https://flashbots-explorer.marto.lol/
+## Installation
+
+Firstly, please run preferably `yarn` or alternatively `npm i` to install the dependancies
+
+Then make a copy of the `.env.template` file and rename it to `.env`
+
+For now, you dont need to make any changes to the file.
+
## Development
This is a create next app project so:
diff --git a/components/Address.tsx b/components/Address.tsx
new file mode 100644
index 0000000..92688fb
--- /dev/null
+++ b/components/Address.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+
+export const Address = ({ address } : { address: string }) => {
+ const size = 6;
+ const shorten = (address: string): string => address.slice(0, size) + "..." + address.slice(-size);
+ return
+ { shorten(address) }
+ ;
+};
diff --git a/components/BundleModal.tsx b/components/BundleModal.tsx
deleted file mode 100644
index da8c8fd..0000000
--- a/components/BundleModal.tsx
+++ /dev/null
@@ -1,344 +0,0 @@
-/* This example requires Tailwind CSS v2.0+ */
-import { Fragment, useRef, useState, useEffect } from 'react';
-import { useRouter } from 'next/router';
-import { Dialog, Transition } from '@headlessui/react';
-import { getReceipts } from '../lib/getReceipts';
-
-export default function BundleModal({ open, bundle, setOpen }) {
- const cancelButtonRef = useRef();
- const router = useRouter();
- const blockNumber = Number(router.query.block);
- const goToBlock = (blockNumber: number) => router.push(`/?block=${blockNumber}`, undefined, { shallow: true });
- const goToPrevBlock = () => goToBlock(blockNumber - 1);
- const goToNextBlock = () => goToBlock(blockNumber + 1);
-
- const handleUserKeyPress = ({ keyCode }) => {
- if (keyCode === 37) {
- goToNextBlock();
- } else if (keyCode === 39) {
- goToPrevBlock();
- }
- };
-
- const close = () => {
- setOpen(false);
- if (bundle) {
- const { pathname, query } = router;
- delete query.block;
- router.push({ pathname, query });
- }
- };
-
- useEffect(() => {
- if (router.query.block === undefined) {
- setOpen(false);
- } else {
- window.addEventListener('keydown', handleUserKeyPress);
- return () => {
- window.removeEventListener('keydown', handleUserKeyPress);
- };
- }
- }, [router.query.block]);
-
- return (
-
-
-
- )
-}
-
-const Bundle = ({ bundle }) => {
- return
-
-
-
-
-
-
-
-
- | Hash |
- From |
- To |
- Assets |
- Gas used |
- Gas price |
- Coinbase transfer |
- Miner reward |
-
-
-
- {
- bundle.transactions.length > 1
- ? bundle.transactions.map((sb, i) => )
- :
- }
-
-
- |
-
- { bundle.transactions.length } bundles
- |
-
- { bundle.transactions.reduce((acc, b) => acc + b.length, 0) } transactions
- |
- |
-
- { bundle.transactions.reduce((acc, txs) => acc + txs.reduce((ac2, tx) => ac2 + tx.gas_used, 0), 0)
- }
- |
-
- { Math.round(bundle.miner_reward / bundle.gas_used / (10 ** 9)) } gwei
- |
-
- Ξ { bundle.transactions.reduce((acc, txs) => acc + Number(summarizeFp(txs, 'coinbase_transfer')), 0).toFixed(4) }
- |
-
- Ξ { bundle.transactions.reduce((acc, txs) => acc + Number(summarizeFp(txs, 'total_miner_reward')), 0).toFixed(4) }
- |
-
-
-
-
-
-
-
;
-}
-
-function SubBundle({ subBundle, index } : { subBundle: any[], index?: number }) {
- return <>
- {
- subBundle.map((transaction, index) => )
- }
- {
- index === undefined
- ? <>>
- :
- |
- #{ index + 1 }
- |
- |
-
- { Math.round(subBundle.reduce((acc, tx) => acc + tx.gas_used, 0)) }
- |
- |
-
- Ξ { summarizeFp(subBundle, 'coinbase_transfer') }
- |
-
- Ξ { summarizeFp(subBundle, 'total_miner_reward') }
- |
-
- }
- >;
-}
-
-const summarizeFp = (x, c): number => (x.reduce((acc, tx) => acc + tx[c] / 10 ** 18, 0)).toFixed(4);
-
-const ExternalLinkIcon = ;
-
-function BundleTransaction({ transaction, index }) {
- const [logs, setLogs] = useState([]);
-
- const router = useRouter();
- const onClick = (e, from) => {
- e.preventDefault();
- router.push(`/?from=${from}`, undefined, { shallow: true });
- };
-
- useEffect( () => {
- const getLogs = async () => setLogs(await getReceipts(transaction));
- getLogs();
- }, [transaction]);
-
- const coins = logs.reduce((acc, curr) => {
- if (curr.coin.name && (curr.coin.value || acc[curr.coin.name] === undefined)) {
- acc[curr.coin.name] = {
- event: curr.coin.event,
- address: curr.coin.address,
- logo: curr.coin.logo,
- value: curr.coin.value,
- ethValue: curr.coin.ethValue
- }
- }
- return acc;
- }, {});
-
- // block_number: 12358944
- // coinbase_transfer: "9785908415014455"
- // eoa_address: "0x07A962Ea36DdddA0c6e594F8A29b89aC06EC8FB7"
- // gas_price: "0"
- // gas_used: 89458
- // to_address: "0xa57Bd00134B2850B2a1c55860c9e9ea100fDd6CF"
- // total_miner_reward: "9785908415014455"
- // transaction_hash: "0xedbaa982717813b69e215fe08525ae85c3686a095a1b908714ef8755f58e754d"
- // tx_index: 0
- return
-
- |
-
- { ExternalLinkIcon }
- { transaction?.transaction_hash.slice(0, 10) }...
-
- |
-
-
- |
-
-
- |
-
- {
- Object.keys(coins).map(coin => )
- }
- |
-
-
- { Math.round(transaction?.gas_used) }
-
- |
-
-
- { Math.round(transaction?.gas_price / (10 ** 9)) } gwei
-
- |
-
-
- Ξ { (transaction?.coinbase_transfer / (10 ** 18)).toFixed(4) }
-
- |
-
-
- Ξ { (transaction?.total_miner_reward / (10 ** 18)).toFixed(4) }
-
- |
-
- ;
-}
-
-const Error = ({ blockNumber }) =>
-
-
Oops, no bundles found in this block! Have this instead: 🍌
-
;
-
-function Title({ blockNumber }) {
- return
- Bundles in #
-
- { blockNumber }
-
- ;
-}
-
-function Address({ address } : { address: string }) {
- const size = 6;
- const shorten = (address: string): string => address.slice(0, size) + '...' + address.slice(-size);
- return
- { shorten(address) }
- ;
-}
-
-const now = () => {
- return randomMaxMin(Date.now(), Date.now()*10000);
-};
-
-const randomMaxMin = (max, min) => {
- return Math.floor(Math.random() * (max - min + 1)) + min;
-};
diff --git a/components/Bundles.tsx b/components/Bundles.tsx
deleted file mode 100644
index dfc0813..0000000
--- a/components/Bundles.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import { useState, useEffect } from 'react';
-import { useRouter } from 'next/router';
-import BundleModal from './BundleModal';
-import styles from '../styles/Home.module.css';
-import { getBlocks } from '../lib/api';
-
-export default function Bundles({ bundles }) {
- const router = useRouter();
- const [openModal, setOpenModal] = useState(false);
- const [bundle, setBundle] = useState(undefined);
- const [searchValue, setSearch] = useState(undefined);
-
- useEffect(() => {
- if (router.query.block) {
- findBundleAndOpen(router.query.block as unknown as string);
- }
- }, [router.query.block]);
-
- const setBundleAndOpen = bundle => {
- if (bundle !== undefined) {
- router.push(`/?block=${bundle?.block_number}`, undefined, { shallow: true });
- }
- setBundle(bundle);
- setOpenModal(true);
- };
-
- const findBundleAndOpen = async (blockNumber: string) => {
- const local = bundles.find(b => b.block_number == blockNumber);
- if (local) {
- setBundleAndOpen(local);
- } else {
- try {
- const blocks = await getBlocks({ block_number: blockNumber })
- if (blocks) {
- setBundleAndOpen(blocks[0]);
- }
- } catch (e) {
- setBundleAndOpen(undefined);
- }
- }
- };
-
- const submit = e => {
- e.preventDefault();
- findBundleAndOpen(searchValue);
- };
-
- return
-
-
-
-
-
-
-
-
- | Block number |
- Miner reward |
- Gas used |
- Effective gas price |
- Bundles |
- Inspect |
-
-
-
- { bundles?.sort(sortBlocks).map((b, i) => ) }
-
-
-
-
-
-
;
-};
-
-function sortBlocks(a, b): number {
- if (a.block_number < b.block_number) return 1;
- if (a.block_number > b.block_number) return -1;
- return 0;
-}
-
-const ExternalLinkIcon = ;
-
-const OpenBookIcon = ;
-
-function Bundle({ index, bundle, setBundleAndOpen }) {
- const onClick = e => {
- e.preventDefault();
- setBundleAndOpen(bundle);
- };
-
- return
- |
-
- { ExternalLinkIcon }
- { bundle?.block_number }
-
- |
-
-
- Ξ { (bundle?.miner_reward / (10 ** 18)).toFixed(4) }
-
- |
-
- { bundle?.gas_used}
- |
-
-
- { Math.round(bundle?.miner_reward / bundle?.gas_used / (10 ** 9)) } gwei
-
- |
-
-
- { bundle.transactions.length }
-
- |
-
- { OpenBookIcon }
- |
-
;
-}
diff --git a/components/bundles/Bundle.tsx b/components/bundles/Bundle.tsx
new file mode 100644
index 0000000..c50ebd5
--- /dev/null
+++ b/components/bundles/Bundle.tsx
@@ -0,0 +1,88 @@
+import { Dialog } from "@headlessui/react";
+import React from "react";
+import { Block } from "../../context/BundleData/BundleDataProvider";
+import { summarizeFp } from "./Helpers";
+import { SubBundle } from "./Subbundle";
+
+interface IBundle {
+ bundle: Block
+}
+
+export const Bundle = ({ bundle }: IBundle) => {
+ return
+
+ Bundles in #
+
+ { bundle?.block_number }
+
+
+
+
+
+
+
+
+
+ | Hash |
+ From |
+ To |
+ Assets |
+ Gas used |
+ Gas price |
+ Coinbase transfer |
+ Miner reward |
+
+
+
+ {
+ bundle.transactions.length > 1
+ ? bundle.transactions.map((sb, i) => )
+ :
+ }
+
+
+ |
+
+ { bundle.transactions.length } bundles
+ |
+
+ { bundle.transactions.reduce((acc, b) => acc + b.length, 0) } transactions
+ |
+ |
+
+ { bundle.transactions.reduce((acc, txs) => acc + txs.reduce((ac2, tx) => ac2 + tx.gas_used, 0), 0)
+ }
+ |
+
+ { Math.round(bundle.miner_reward / bundle.gas_used / (10 ** 9)) } gwei
+ |
+
+ Ξ { bundle.transactions.reduce((acc, txs) => acc + Number(summarizeFp(txs, "coinbase_transfer")), 0).toFixed(4) }
+ |
+
+ Ξ { bundle.transactions.reduce((acc, txs) => acc + Number(summarizeFp(txs, "total_miner_reward")), 0).toFixed(4) }
+ |
+
+
+
+
+
+
+
;
+};
diff --git a/components/bundles/BundleModal.tsx b/components/bundles/BundleModal.tsx
new file mode 100644
index 0000000..a37f7b4
--- /dev/null
+++ b/components/bundles/BundleModal.tsx
@@ -0,0 +1,150 @@
+/* This example requires Tailwind CSS v2.0+ */
+import React, { Fragment, useRef, useCallback, useEffect } from "react";
+import { Dialog, Transition } from "@headlessui/react";
+import { Bundle } from "../bundles/Bundle";
+import { BundleError } from "../errors/BundleError";
+import { Block } from "../../context/BundleData/BundleDataProvider";
+import { useRouter } from "next/router";
+
+interface IBundleModal {
+ open: boolean
+ bundle: Block
+ close: () => void
+}
+
+export default function BundleModal({ open, bundle, close }: IBundleModal) {
+ const cancelButtonRef = useRef();
+ const router = useRouter();
+ const blockNumber = Number(router.query.block);
+
+ const goToBlock = useCallback((blockNumber: number) => router.push(`/?block=${blockNumber}`, undefined, { shallow: true }), [router]);
+ const goToPrevBlock = useCallback(() => goToBlock(blockNumber - 1), [goToBlock, blockNumber]);
+ const goToNextBlock = useCallback(() => goToBlock(blockNumber + 1), [goToBlock, blockNumber]);
+
+ const handleUserKeyPress = useCallback(({ keyCode }) => {
+ if (keyCode === 37) {
+ goToNextBlock();
+ } else if (keyCode === 39) {
+ goToPrevBlock();
+ }
+ }, [goToNextBlock, goToPrevBlock]);
+
+ const handleClose = useCallback(() => {
+ close();
+ if (bundle) {
+ const { pathname, query } = router;
+ delete query.block;
+ router.push({ pathname, query });
+ }
+ }, [close, bundle, router]);
+
+ useEffect(() => {
+ if (router.query.block === undefined) {
+ close();
+ } else {
+ window.addEventListener("keydown", handleUserKeyPress);
+ return () => {
+ window.removeEventListener("keydown", handleUserKeyPress);
+ };
+ }
+ }, [router.query.block, close, handleUserKeyPress]);
+
+ return (
+
+
+
+ );
+}
diff --git a/components/bundles/BundleRow.tsx b/components/bundles/BundleRow.tsx
new file mode 100644
index 0000000..3984a4f
--- /dev/null
+++ b/components/bundles/BundleRow.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+import { Block } from "../../context/BundleData/BundleDataProvider";
+import { ExternalLinkIcon } from "../icons/externalLink.icon";
+import { OpenBookIcon } from "../icons/openBook.icon";
+
+interface IBundleRow {
+ index: number
+ bundle: Block
+ setBundleAndOpen: (bundle: Block) => void
+}
+
+export const BundleRow = ({ index, bundle, setBundleAndOpen }: IBundleRow) => {
+ const onClick = e => {
+ e.preventDefault();
+ setBundleAndOpen(bundle);
+ };
+
+ return
+ |
+
+ { ExternalLinkIcon }
+ { bundle?.block_number }
+
+ |
+
+
+ Ξ { (bundle?.miner_reward / (10 ** 18)).toFixed(4) }
+
+ |
+
+ { bundle?.gas_used}
+ |
+
+
+ { Math.round(bundle?.miner_reward / bundle?.gas_used / (10 ** 9)) } gwei
+
+ |
+
+
+ { bundle.transactions.length }
+
+ |
+
+ { OpenBookIcon }
+ |
+
;
+};
diff --git a/components/bundles/BundleTransaction.tsx b/components/bundles/BundleTransaction.tsx
new file mode 100644
index 0000000..7a12946
--- /dev/null
+++ b/components/bundles/BundleTransaction.tsx
@@ -0,0 +1,116 @@
+import { useRouter } from "next/router";
+import React, { Fragment, useEffect, useState } from "react";
+import { useTokenData } from "../../context/TokenData/TokenDataProvider";
+import { timeNow } from "../../helpers/general";
+import { Address } from "../Address";
+import { ExternalLinkIcon } from "../icons/externalLink.icon";
+
+export const BundleTransaction = (transaction, index: number) => {
+ const [logs, setLogs] = useState([]);
+ const { getReceipts } = useTokenData();
+ const router = useRouter();
+ const onClick = (e, from) => {
+ e.preventDefault();
+ router.push(`/?from=${from}`, undefined, { shallow: true });
+ };
+
+ useEffect(() => {
+ const getLogs = async () => setLogs(await getReceipts(transaction));
+ getLogs();
+ }, [transaction, getReceipts]);
+
+ const coins = logs.reduce((acc, curr) => {
+ if (curr.coin.name && (curr.coin.value || acc[curr.coin.name] === undefined)) {
+ acc[curr.coin.name] = {
+ event: curr.coin.event,
+ address: curr.coin.address,
+ logo: curr.coin.logo,
+ value: curr.coin.value,
+ ethValue: curr.coin.ethValue
+ };
+ }
+ return acc;
+ }, {});
+
+ // block_number: 12358944
+ // coinbase_transfer: "9785908415014455"
+ // eoa_address: "0x07A962Ea36DdddA0c6e594F8A29b89aC06EC8FB7"
+ // gas_price: "0"
+ // gas_used: 89458
+ // to_address: "0xa57Bd00134B2850B2a1c55860c9e9ea100fDd6CF"
+ // total_miner_reward: "9785908415014455"
+ // transaction_hash: "0xedbaa982717813b69e215fe08525ae85c3686a095a1b908714ef8755f58e754d"
+ // tx_index: 0
+ return
+
+ |
+
+ { ExternalLinkIcon }
+ { transaction?.transaction_hash.slice(0, 10) }...
+
+ |
+
+
+ |
+
+
+ |
+
+ {
+ Object.keys(coins).map((coin, index) => )
+ }
+ |
+
+
+ { Math.round(transaction?.gas_used) }
+
+ |
+
+
+ { Math.round(transaction?.gas_price / (10 ** 9)) } gwei
+
+ |
+
+
+ Ξ { (transaction?.coinbase_transfer / (10 ** 18)).toFixed(4) }
+
+ |
+
+
+ Ξ { (transaction?.total_miner_reward / (10 ** 18)).toFixed(4) }
+
+ |
+
+ ;
+};
+
diff --git a/components/bundles/BundlesOverview.tsx b/components/bundles/BundlesOverview.tsx
new file mode 100644
index 0000000..0adeb4e
--- /dev/null
+++ b/components/bundles/BundlesOverview.tsx
@@ -0,0 +1,167 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { useRouter } from "next/router";
+import styles from "../../styles/BundleOverview.module.css";
+import { BundleRow } from "./BundleRow";
+import { useBundleData } from "../../context/BundleData/BundleDataProvider";
+import BundleModal from "./BundleModal";
+import clsx from "clsx";
+
+export default function BundlesOverview() {
+ const router = useRouter();
+ const { blocks, setFilters, filters, page, morePages, setPage } = useBundleData();
+ const [bundle, setBundle] = useState(undefined);
+ const [searchValue, setSearch] = useState(undefined);
+ const [landingMutex, setLandingMutex] = useState(true);
+
+ const setBundleAndOpen = useCallback(bundle => {
+ // intermittent router.push insecure error
+ if (bundle !== undefined) {
+ router.push(`/?block=${bundle?.block_number}`, undefined, { shallow: true });
+ }
+ setBundle(bundle);
+ }, [router]);
+
+ useEffect(() => {
+ if (router.query.block && blocks.length > 0 && landingMutex) {
+ const blockNumber = router.query.block as unknown as string;
+ if (blockNumber) {
+ const local = blocks.find(b => b.block_number == blockNumber);
+ if (local) {
+ setBundleAndOpen(local);
+ setLandingMutex(false);
+ } else if(!filters.block_number) {
+ setFilters({
+ ...filters,
+ block_number: blockNumber
+ });
+ }
+ }
+ } else if (filters.block_number) {
+ const local = blocks.find(b => b.block_number == filters.block_number);
+ if (local) {
+ setFilters({
+ ...filters,
+ block_number: undefined
+ });
+ }
+ }
+ }, [router.query.block, blocks, filters, landingMutex, setBundleAndOpen, setFilters]);
+
+ const submit = e => {
+ e.preventDefault();
+ const local = blocks.find(b => b.block_number == searchValue);
+ if (local) {
+ setBundleAndOpen(local);
+ } else if(!filters.block_number) {
+ setFilters({
+ ...filters,
+ block_number: searchValue
+ });
+ }
+ };
+
+ function sortBlocks(a, b): number {
+ if (a.block_number < b.block_number) return 1;
+ if (a.block_number > b.block_number) return -1;
+ return 0;
+ }
+
+ return
+
setBundle(undefined) } />
+
+
+
+
+
+
+
+ | Block number |
+ Miner reward |
+ Gas used |
+ Effective gas price |
+ Bundles |
+ Inspect |
+
+
+
+ { blocks?.sort(sortBlocks)
+ .filter((block, index) => index < ((page - 1) * filters.limit) + filters.limit && index >= (page - 1) * filters.limit)
+ .map((b, i) => ) }
+
+
+ {
+ ((blocks.length > filters.limit) || page > 1) && (
+
+
+
+
+ )
+ }
+
+
+
+ ;
+}
diff --git a/components/bundles/Helpers.tsx b/components/bundles/Helpers.tsx
new file mode 100644
index 0000000..a5dd795
--- /dev/null
+++ b/components/bundles/Helpers.tsx
@@ -0,0 +1 @@
+export const summarizeFp = (x, c): number => (x.reduce((acc, tx) => acc + tx[c] / 10 ** 18, 0)).toFixed(4);
diff --git a/components/bundles/Subbundle.tsx b/components/bundles/Subbundle.tsx
new file mode 100644
index 0000000..5f80afd
--- /dev/null
+++ b/components/bundles/Subbundle.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+import { BundleTransaction } from "./BundleTransaction";
+import { summarizeFp } from "./Helpers";
+
+export const SubBundle = ({ subBundle, index } : { subBundle: any[]; index?: number }) => {
+ return <>
+ { subBundle.map(BundleTransaction) }
+ {
+ index === undefined
+ ? <>>
+ :
+ |
+ #{ index + 1 }
+ |
+ |
+
+ { Math.round(subBundle.reduce((acc, tx) => acc + tx.gas_used, 0)) }
+ |
+ |
+
+ Ξ { summarizeFp(subBundle, "coinbase_transfer") }
+ |
+
+ Ξ { summarizeFp(subBundle, "total_miner_reward") }
+ |
+
+ }
+ >;
+};
diff --git a/components/bundles/Title.tsx b/components/bundles/Title.tsx
new file mode 100644
index 0000000..6529aec
--- /dev/null
+++ b/components/bundles/Title.tsx
@@ -0,0 +1,15 @@
+import { Dialog } from "@headlessui/react";
+import React from "react";
+
+export const Title = ({ blockNumber }) => {
+ return
+ Bundles in #
+
+ { blockNumber }
+
+ ;
+};
diff --git a/components/errors/BundleError.tsx b/components/errors/BundleError.tsx
new file mode 100644
index 0000000..c4d6a47
--- /dev/null
+++ b/components/errors/BundleError.tsx
@@ -0,0 +1,7 @@
+import React from "react";
+import { Title } from "../bundles/Title";
+
+export const BundleError = ({ blockNumber }) =>
+
+
Oops, no bundles found in this block! Have this instead: 🍌
+
;
diff --git a/components/icons/externalLink.icon.tsx b/components/icons/externalLink.icon.tsx
new file mode 100644
index 0000000..147a9ef
--- /dev/null
+++ b/components/icons/externalLink.icon.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+
+export const ExternalLinkIcon =
+;
+
diff --git a/components/icons/openBook.icon.tsx b/components/icons/openBook.icon.tsx
new file mode 100644
index 0000000..795bc03
--- /dev/null
+++ b/components/icons/openBook.icon.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+
+export const OpenBookIcon =
+;
diff --git a/context/BundleData/BundleDataProvider.tsx b/context/BundleData/BundleDataProvider.tsx
new file mode 100644
index 0000000..fe4132f
--- /dev/null
+++ b/context/BundleData/BundleDataProvider.tsx
@@ -0,0 +1,175 @@
+import { useRouter } from "next/router";
+import * as React from "react";
+import { useCallback, useEffect } from "react";
+import { useState } from "react";
+
+type BundleDataContextProps = {
+ children: React.ReactNode | React.ReactNode[]
+}
+
+// If we want to display that theres more pages, we'll need to check on ther requests
+// const PAGES_AHEAD = 5
+
+// TODO Update
+export type Block = {
+ hash: string
+ block_number: string
+ miner_reward?: number
+ gas_used?: number
+ coinbase_transfers: number
+ gas_price: number
+ miner: string
+ // TODO: type this
+ transactions: any[]
+}
+
+
+export type Transaction = {
+ block_number: number
+ bundle_index: number
+ coinbase_transfer: number
+ eoa_address: string
+ gas_price: number
+ gas_used: number
+ to_address: string
+ total_miner_reward: number
+ transaction_hash: string
+ tx_index: number
+}
+
+export interface IBundleFilters {
+ from?: string | string[]
+ to?: string
+ block_number?: string
+ limit: number
+}
+
+interface IBundleDataContext {
+ blocks: Block[]
+ page: number
+ setPage: (page: number) => void
+ morePages: boolean
+ filters: IBundleFilters
+ setFilters: (newFilter: IBundleFilters) => void
+}
+
+const BundleDataContext = React.createContext(undefined);
+
+const BundleDataProvider = ({ children }: BundleDataContextProps) => {
+ const { query } = useRouter();
+ const [blocks, setBlocks] = useState([]);
+ const [page, setPage] = useState(1);
+ const [morePages, setMorePages] = useState(false);
+ const [filters, _setFilters] = useState({
+ limit: 10
+ });
+ const [pageMutex, setPageMutex] = useState(false);
+ // Update filter from query
+ useEffect(() => {
+ const { from } = query;
+ if (from && filters.from != from) {
+ _setFilters({
+ ...filters,
+ from: from
+ });
+ }
+ }, [query, filters]);
+
+ useEffect(() => {
+ // Change page back to first if filters are changed
+ setPage(1);
+ }, [filters, setPage]);
+
+ function getSubBundles(bundle) {
+ return bundle.transactions.reduce((acc, curr) => {
+ if (acc[curr.bundle_index]) {
+ acc[curr.bundle_index].push(curr);
+ } else {
+ acc[curr.bundle_index] = [curr];
+ }
+ return acc;
+ }, []);
+ }
+
+ const transformBundle = useCallback(bundle => {
+ bundle.transactions = getSubBundles(bundle);
+ return bundle;
+ }, []);
+
+ const getBlocks = useCallback(async () => {
+ // TODO: Type Safety
+ const params: Record = {};
+ Object.keys(filters).map(key => params[key] = filters[key]);
+ // This fetchs additional pages to a limit
+ // params["limit"] = `${Number(params["limit"]) * PAGES_AHEAD}`
+ params["limit"] = `${(Number(params["limit"]) * page) + 1}`;
+ const url = `${process.env.FLASHBOTS_API_URL}/?${new URLSearchParams(params)}`;
+ const res = await fetch(url);
+ const resJson = await res.json();
+ if (resJson.blocks !== undefined) {
+ const existingBlocknumbers = blocks.map(block => block.block_number);
+ const newBlocks = resJson.blocks
+ .map(block => transformBundle(block)).filter(newBlock => existingBlocknumbers.indexOf(newBlock.block_number) < 0);
+ if (newBlocks.length > 0) {
+ setBlocks([
+ ...blocks,
+ ...newBlocks
+ ]);
+ }
+ }
+ setPageMutex(false);
+ }, [filters, blocks, page, transformBundle]);
+
+ useEffect(() => {
+ setMorePages(blocks.length > filters.limit);
+ }, [blocks, filters.limit, setMorePages]);
+
+ // Automatically update when view is changed
+ useEffect(() => {
+ if (!pageMutex) {
+ if ((page * filters.limit) + 1 > blocks.length || page === 1) {
+ setPageMutex(true);
+ getBlocks();
+ }
+ }
+ }, [filters, page, blocks, pageMutex, getBlocks]);
+
+ const setFilters = useCallback((filter: IBundleFilters) => {
+ // This intermidiary function is for removing filters gracefully later on
+ const newItem: IBundleFilters = {
+ limit: 10
+ };
+ Object.keys(filter).map(key => {
+ if (filter[key]) {
+ newItem[key] = filter[key];
+ }
+ });
+
+ _setFilters(newItem);
+ }, [_setFilters]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+const useBundleData = () => {
+ const context = React.useContext(BundleDataContext);
+ if (context === undefined) {
+ throw new Error("useBundleData must be used within a BundleDataProvider");
+ }
+ return context;
+};
+
+export { BundleDataProvider, useBundleData };
diff --git a/context/TokenData/TokenDataProvider.tsx b/context/TokenData/TokenDataProvider.tsx
new file mode 100644
index 0000000..8c5b6f9
--- /dev/null
+++ b/context/TokenData/TokenDataProvider.tsx
@@ -0,0 +1,201 @@
+import * as React from "react";
+import { useCallback, useEffect } from "react";
+import { useState } from "react";
+import { addABI, decodeLogs } from "abi-decoder";
+import { Interface } from "@ethersproject/abi";
+const DEXES = ["COINGECKO"];
+
+export const eventsJson = [
+ //erc20
+ { "text_signature": "event Transfer(address indexed from, address indexed to, uint256 value)" },
+ { "text_signature": "event Approval(address indexed owner, address indexed spender, uint256 value)" },
+ //WETH
+ { "text_signature": "event Deposit(address indexed dst, uint wad)" },
+ { "text_signature": "event Withdrawal(address indexed src, uint wad)" },
+ //IUniswapExchange
+ { "text_signature": "event TokenPurchase(address indexed buyer, uint256 indexed eth_sold, uint256 indexed tokens_bought)" },
+ { "text_signature": "event EthPurchase(address indexed buyer, uint256 indexed tokens_sold, uint256 indexed eth_bought)" },
+ { "text_signature": "event AddLiquidity(address indexed provider, uint256 indexed eth_amount, uint256 indexed token_amount)" },
+ { "text_signature": "event RemoveLiquidity(address indexed provider, uint256 indexed eth_amount, uint256 indexed token_amount)" },
+ //IUniswapV2Pair
+ { "text_signature": "event Mint(address indexed sender, uint amount0, uint amount1)" },
+ { "text_signature": "event Burn(address indexed sender, uint amount0, uint amount1, address indexed to)" },
+ { "text_signature":
+ "event Swap(address indexed sender, uint amount0, uint amount1, uint amount0Out, uint amount1Out, address indexed to)" },
+ { "text_signature": "event Sync(uint112 reserve0, uint112 reserve1)" }
+];
+
+
+// const USDC = {
+// "chainId": 1,
+// "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
+// "name": "USD Coin",
+// "symbol": "USDC",
+// "decimals": 6,
+// "logoURI": "https://assets.coingecko.com/coins/images/6319/thumb/USD_Coin_icon.png?1547042389"
+// };
+
+const uniswapV2GQL = "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2";
+const ethQL = `{
+ bundles (first:1) {
+ ethPrice
+ }
+}`;
+
+
+type TokenDataContextProps = {
+ children: React.ReactNode | React.ReactNode[]
+}
+
+// TODO Update
+export type Token = {
+ address: string
+ coin: string
+ logo: string
+ decimals: number
+}
+
+export type Log = {
+
+}
+
+interface ITokenDataContextProps {
+ tokens: Token[]
+ getReceipts: (transaction: Record) => Promise
+}
+
+
+const TokenDataContext = React.createContext(undefined);
+
+const TokenDataProvider = ({ children }: TokenDataContextProps) => {
+ const [tokens, setTokens] = useState([]);
+
+ const loadTokens = useCallback(async () => {
+ DEXES.map(d => {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const { tokens } = require(`./tokensList/json${d}.json`);
+ console.log(tokens);
+ setTokens(tokens);
+ });
+ }, []);
+
+ const addEvents = useCallback(async () => {
+ eventsJson.map(async e => {
+ const { text_signature } = e;
+ try {
+ const i = new Interface([text_signature]);
+ await addABI(i.fragments);
+ } catch (e) {
+ console.log(e);
+ }
+ });
+ }, []);
+
+ const getEthPrice = async () => {
+ const res = await fetch(uniswapV2GQL, {
+ method: "POST",
+ headers: {
+ "Accept": "api_version=2",
+ "Content-Type": "application/graphql"
+ },
+ body: JSON.stringify({ query : ethQL })
+ });
+
+ const { data: { bundles } } = await res.json();
+
+ if (bundles.length > 0) {
+ return parseFloat(bundles[0].ethPrice).toFixed(6);
+ }
+
+ return "1";
+ };
+
+ const getAllLogs = useCallback(async (_logs) => {
+ const ethPrice = await getEthPrice();
+ return decodeLogs(_logs).map(log => {
+ const { coin, logo, decimals } = tokens.find(token => token.address === log.address);
+ let ethValue = "0";
+ let value;
+
+ log.events.map(e => {
+ if ((log.name == "Transfer" || log.name == "Swap") && e.type.match("uint")) {
+ value = parseFloat(`${e.value / 10 ** decimals}`).toFixed(2);
+ if (coin === "WETH") {
+ ethValue = parseFloat(`${value * parseFloat(ethPrice)}`).toFixed(2);
+ }
+ }
+ });
+
+ log.coin = {
+ address: log.address,
+ name: coin,
+ event: log.name,
+ logo,
+ decimals,
+ value,
+ ethValue
+ };
+
+ return log;
+ });
+ }, [tokens]);
+
+ const getReceipts = useCallback(async (transaction) => {
+ const jsonrpc = {
+ "jsonrpc": "2.0",
+ "method": "eth_getTransactionReceipt",
+ "params": [],
+ "id": 1
+ };
+
+ const params = {
+ method: "POST",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/json"
+ },
+ body: ""
+ };
+
+ const { transaction_hash } = transaction;
+ jsonrpc.params = [transaction_hash];
+ params.body = JSON.stringify(jsonrpc);
+
+ try {
+ const res = await fetch(`https://mainnet.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`, params);
+ const { result : { logs } } = await res.json();
+ return getAllLogs(logs);
+ } catch(e) {
+ console.log(e);
+ }
+
+ return [];
+ }, [getAllLogs]);
+
+ // Fetch initial
+ useEffect(() => {
+ addEvents();
+ loadTokens();
+ }, [addEvents, loadTokens]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+const useTokenData = () => {
+ const context = React.useContext(TokenDataContext);
+ if (context === undefined) {
+ throw new Error("useTokenData must be used within a TokenDataContextProvider");
+ }
+ return context;
+};
+
+export { TokenDataProvider, useTokenData };
diff --git a/tokensList/fetchTokensApiByDex.js b/context/TokenData/tokensList/fetchTokensApiByDex.js
similarity index 66%
rename from tokensList/fetchTokensApiByDex.js
rename to context/TokenData/tokensList/fetchTokensApiByDex.js
index c1db071..b47cdc6 100644
--- a/tokensList/fetchTokensApiByDex.js
+++ b/context/TokenData/tokensList/fetchTokensApiByDex.js
@@ -1,10 +1,11 @@
-const fs = require('fs');
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const fs = require("fs");
const dexUrl = {
- 'COINGECKO': "https://tokens.coingecko.com/uniswap/all.json"
+ "COINGECKO": "https://tokens.coingecko.com/uniswap/all.json"
};
-const tokensBySymbol = {}
+const tokensBySymbol = {};
const readFiles = async dex => {
const tokensUrl = dexUrl[dex];
@@ -12,25 +13,26 @@ const readFiles = async dex => {
const config = {
timeout: 3000,
uri: tokensUrl,
- method: 'GET',
- json: true,
+ method: "GET",
+ json: true
};
const tokensResponse = await fetch(config);
const { tokens } = tokensResponse;
if (!tokens) {
+ // eslint-disable-next-line no-undef
console.error(`error fetching data from ${this.name}: ${error}`);
return false;
} else {
tokens.forEach(token => {
tokensBySymbol[token.symbol] = token;
- })
- console.log('writing files -- ');
+ });
+ console.log("writing files -- ");
fs.writeFile(`${__dirname}/json${dex}.json`, global.JSON.stringify(tokensResponse), console.error);
fs.writeFile(`${__dirname}/tokens${dex}.json`, global.JSON.stringify(tokensBySymbol), console.error);
return true;
}
-}
+};
-readFiles('COINGECKO');
+readFiles("COINGECKO");
diff --git a/tokensList/jsonCOINGECKO.json b/context/TokenData/tokensList/jsonCOINGECKO.json
similarity index 100%
rename from tokensList/jsonCOINGECKO.json
rename to context/TokenData/tokensList/jsonCOINGECKO.json
diff --git a/tokensList/tokensCOINGECKO.json b/context/TokenData/tokensList/tokensCOINGECKO.json
similarity index 100%
rename from tokensList/tokensCOINGECKO.json
rename to context/TokenData/tokensList/tokensCOINGECKO.json
diff --git a/helpers/general.tsx b/helpers/general.tsx
new file mode 100644
index 0000000..c3c758c
--- /dev/null
+++ b/helpers/general.tsx
@@ -0,0 +1,7 @@
+export const timeNow = () => {
+ return randomMaxMin(Date.now(), Date.now() * 10000);
+};
+
+export const randomMaxMin = (max, min) => {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+};
diff --git a/lib/ABILogs.js b/lib/ABILogs.js
deleted file mode 100644
index 49a4dae..0000000
--- a/lib/ABILogs.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { addABI, decodeLogs } from "abi-decoder";
-import { Interface } from "@ethersproject/abi";
-import { getToken } from "./getToken";
-import { getEthPrice } from "./uniswapV2SubGraph";
-
-const eventsJson = [
- //erc20
- { "text_signature": "event Transfer(address indexed from, address indexed to, uint256 value)", },
- { "text_signature": "event Approval(address indexed owner, address indexed spender, uint256 value)", },
- //WETH
- { "text_signature": "event Deposit(address indexed dst, uint wad)", },
- { "text_signature": "event Withdrawal(address indexed src, uint wad)", },
- //IUniswapExchange
- { "text_signature": "event TokenPurchase(address indexed buyer, uint256 indexed eth_sold, uint256 indexed tokens_bought)" },
- { "text_signature": "event EthPurchase(address indexed buyer, uint256 indexed tokens_sold, uint256 indexed eth_bought)" },
- { "text_signature": "event AddLiquidity(address indexed provider, uint256 indexed eth_amount, uint256 indexed token_amount)" },
- { "text_signature": "event RemoveLiquidity(address indexed provider, uint256 indexed eth_amount, uint256 indexed token_amount)" },
- //IUniswapV2Pair
- { "text_signature": "event Mint(address indexed sender, uint amount0, uint amount1)" },
- { "text_signature": "event Burn(address indexed sender, uint amount0, uint amount1, address indexed to)" },
- { "text_signature": "event Swap(address indexed sender, uint amount0, uint amount1, uint amount0Out, uint amount1Out, address indexed to)" },
- { "text_signature": "event Sync(uint112 reserve0, uint112 reserve1)" }
-];
-
-const addEvents = async () => {
- eventsJson.map(async e => {
- let { text_signature } = e;
- try {
- let i = new Interface([text_signature]);
- await addABI(i.fragments);
- } catch (e) {
- console.log(e);
- }
- });
-};
-
-addEvents();
-
-export async function getAllLogs(_logs) {
- const ethPrice = await getEthPrice();
- return decodeLogs(_logs).map(log => {
- const { coin, logo, decimals } = getToken(log.address);
- let ethValue = 0;
- let value;
-
- log.events.map(e => {
- if ((log.name == "Transfer" || log.name == "Swap") && e.type.match("uint")) {
- value = parseFloat(e.value / 10 ** decimals).toFixed(2);
- if (coin === 'WETH') {
- ethValue = parseFloat(value * ethPrice).toFixed(2);
- }
- }
- });
-
- log.coin = {
- address: log.address,
- name: coin,
- event: log.name,
- logo,
- decimals,
- value,
- ethValue
- };
-
- return log;
- });
-}
diff --git a/lib/api.ts b/lib/api.ts
deleted file mode 100644
index b408d87..0000000
--- a/lib/api.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-export const API_URL = 'https://blocks.flashbots.net/v1/blocks';
-
-export async function getBlocks(params: Record = {}) {
- params.limit = '10';
- const url = `${API_URL}/?${new URLSearchParams(params)}`;
- const res = await fetch(url);
- const { blocks } = await res.json();
- return blocks.map(block => transformBundle(block));
-}
-
-function getSubBundles(bundle) {
- return bundle.transactions.reduce((acc, curr) => {
- if (acc[curr.bundle_index]) {
- acc[curr.bundle_index].push(curr);
- } else {
- acc[curr.bundle_index] = [curr];
- }
- return acc;
- }, []);
-}
-
-function transformBundle(bundle) {
- bundle.transactions = getSubBundles(bundle);
- return bundle;
-}
diff --git a/lib/ga.js b/lib/ga.js
index 2a5bf45..a558089 100644
--- a/lib/ga.js
+++ b/lib/ga.js
@@ -1,10 +1,10 @@
export const pageview = (url) => {
- window.gtag('config', process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS, {
+ window.gtag("config", process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS, {
page_path: url
});
-}
+};
// log specific events happening.
export const event = ({ action, params }) => {
- window.gtag('event', action, params);
-}
+ window.gtag("event", action, params);
+};
diff --git a/lib/getReceipts.js b/lib/getReceipts.js
deleted file mode 100644
index 120d546..0000000
--- a/lib/getReceipts.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { getAllLogs } from '../lib/ABILogs';
-
-export async function getReceipts(transaction) {
- const jsonrpc = {
- "jsonrpc": "2.0",
- "method": "eth_getTransactionReceipt",
- "params": [],
- "id": 1
- };
-
- const params = {
- method: "POST",
- headers: {
- "Accept": "application/json",
- "Content-Type": "application/json"
- },
- body: ""
- };
-
- const { transaction_hash } = transaction;
- jsonrpc.params = [transaction_hash];
- params.body = JSON.stringify(jsonrpc);
-
- try {
- const res = await fetch(`https://mainnet.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_ID}`, params);
- const { result : { logs } } = await res.json();
- return getAllLogs(logs);
- } catch(e) {
- console.log(e);
- }
-
- return [];
-}
diff --git a/lib/getToken.ts b/lib/getToken.ts
deleted file mode 100644
index 4af8341..0000000
--- a/lib/getToken.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-const DEXES = ["COINGECKO"];
-
-let tokensList = [];
-async function getTokens() {
- DEXES.map(d => {
- let { tokens } = require(`../tokensList/json${d}.json`)
- tokensList.push(tokens);
- })
-}
-
-getTokens();
-
-export function getToken(_address: string) {
- let c = {
- coin: "",
- logo: "",
- decimals: 18
- };
-
- for (let d in tokensList) {
- for (let t in tokensList[d]) {
- let o = tokensList[d][t];
- if (o.address === _address) {
- c.coin = o.symbol;
- c.logo = o.logoURI ? o.logoURI : "";
- c.decimals = o.decimals ? o.decimals : 0;
- break;
- }
- }
-
- if (c.coin !== "") {
- break;
- }
- }
- return c;
-}
diff --git a/lib/uniswapV2SubGraph.ts b/lib/uniswapV2SubGraph.ts
deleted file mode 100644
index 1ae9aff..0000000
--- a/lib/uniswapV2SubGraph.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-// const USDC = {
-// "chainId": 1,
-// "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
-// "name": "USD Coin",
-// "symbol": "USDC",
-// "decimals": 6,
-// "logoURI": "https://assets.coingecko.com/coins/images/6319/thumb/USD_Coin_icon.png?1547042389"
-// };
-
-const uniswapV2GQL = "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2";
-const ethQL = `{
- bundles (first:1) {
- ethPrice
- }
-}`;
-
-export async function getEthPrice() {
- const res = await fetch(uniswapV2GQL, {
- method: 'POST',
- headers: {
- 'Accept': 'api_version=2',
- 'Content-Type': 'application/graphql'
- },
- body: JSON.stringify({ query : ethQL })
- });
-
- const { data: { bundles } } = await res.json();
-
- if (bundles.length > 0) {
- return parseFloat(bundles[0].ethPrice).toFixed(6);
- }
-
- return 1;
-}
diff --git a/next.config.js b/next.config.js
new file mode 100644
index 0000000..608bd90
--- /dev/null
+++ b/next.config.js
@@ -0,0 +1,12 @@
+require("dotenv").config();
+
+module.exports = {
+ target: "serverless",
+ env: {
+ NEXT_PUBLIC_INFURA_ID: process.env.NEXT_PUBLIC_INFURA_ID,
+ FLASHBOTS_API_URL: process.env.FLASHBOTS_API_URL || "https://blocks.flashbots.net/v1/blocks"
+ },
+ webpack: (config) => {
+ return config;
+ }
+};
diff --git a/package.json b/package.json
index 078c9a3..1073d54 100644
--- a/package.json
+++ b/package.json
@@ -5,22 +5,34 @@
"scripts": {
"dev": "next dev",
"build": "next build",
- "start": "next start"
+ "start": "next start",
+ "lint": "eslint '**/**/*.{js,jsx,ts,tsx}'"
},
"dependencies": {
"@headlessui/react": "^1.1.1",
"abi-decoder": "^2.4.0",
+ "clsx": "^1.1.1",
"csvtojson": "^2.0.10",
- "fs": "^0.0.1-security",
+ "dotenv": "^10.0.0",
"next": "^10.2.3",
"react": "17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@types/react": "^17.0.4",
+ "@typescript-eslint/eslint-plugin": "^4.15.2",
+ "@typescript-eslint/parser": "^4.15.2",
"autoprefixer": "^10.2.5",
+ "eslint": "^6.8.0",
+ "eslint-plugin-cypress": "^2.11.3",
+ "eslint-plugin-react": "^7.22.0",
+ "eslint-plugin-react-hooks": "^4.2.0",
+ "eslint-plugin-ternary": "^1.0.4",
"postcss": "^8.2.13",
"tailwindcss": "^2.1.4",
"typescript": "^4.2.4"
+ },
+ "browser": {
+ "fs": false
}
}
diff --git a/pages/_app.js b/pages/_app.js
index 1040120..265c955 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -1,7 +1,8 @@
-import 'tailwindcss/tailwind.css'
+import React from "react";
+import "tailwindcss/tailwind.css";
function MyApp({ Component, pageProps }) {
- return
+ return ;
}
-export default MyApp
+export default MyApp;
diff --git a/pages/_document.js b/pages/_document.js
index 5839edd..658fcff 100644
--- a/pages/_document.js
+++ b/pages/_document.js
@@ -1,4 +1,5 @@
-import Document, { Html, Head, Main, NextScript } from 'next/document';
+import React from "react";
+import Document, { Html, Head, Main, NextScript } from "next/document";
export default class MyDocument extends Document {
render() {
@@ -19,7 +20,7 @@ export default class MyDocument extends Document {
gtag('config', '${process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS}', {
page_path: window.location.pathname,
});
- `,
+ `
}}
/>
@@ -28,6 +29,6 @@ export default class MyDocument extends Document {