├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── .sample.env
├── README.md
├── build
├── dark-theme.css
├── favicon.ico
├── light-theme.css
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
└── scaffold-eth.png
├── gulpfile.js
├── package-lock.json
├── package.json
├── public
├── Screen Shot 2023-05-21 at 1.26.31 PM.png
├── Screen Shot 2023-05-21 at 1.51.37 PM.png
├── Screen Shot 2023-05-21 at 12.51.58 PM.png
├── Screen Shot 2023-05-21 at 12.52.30 PM.png
├── Screen Shot 2023-05-21 at 12.53.04 PM.png
├── Screen Shot 2023-05-21 at 12.53.15 PM.png
├── Screen Shot 2023-05-21 at 12.53.27 PM.png
├── Screen Shot 2023-05-21 at 12.53.57 PM.png
├── Screen Shot 2023-05-21 at 12.55.51 PM.png
├── Screen Shot 2023-05-21 at 12.56.07 PM.png
├── Screen Shot 2023-05-21 at 12.56.40 PM.png
├── Screen Shot 2023-05-21 at 12.57.32 PM.png
├── Screen Shot 2023-05-21 at 12.57.45 PM.png
├── Screen Shot 2023-05-21 at 12.58.02 PM.png
├── Screen Shot 2023-05-21 at 12.58.16 PM.png
├── Screen Shot 2023-05-21 at 12.58.59 PM.png
├── Screen Shot 2023-05-21 at 2.03.52 PM.png
├── Screen Shot 2023-05-22 at 10.10.25 AM.png
├── Screen Shot 2023-05-22 at 10.17.46 AM.png
├── Screen Shot 2023-05-22 at 10.18.25 AM.png
├── Screen Shot 2023-05-22 at 12.09.39 PM.png
├── Screen Shot 2023-05-22 at 12.09.58 PM.png
├── dark-theme.css
├── favicon.ico
├── index.html
├── light-theme.css
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
└── scaffold-eth.png
├── scripts
├── create_contracts.js
├── ipfs.js
├── s3.js
└── watch.js
├── src
├── App.css
├── App.jsx
├── App.test.js
├── components
│ ├── Account.jsx
│ ├── Address.jsx
│ ├── AddressInput.jsx
│ ├── Balance.jsx
│ ├── Blockie.jsx
│ ├── BytesStringInput.jsx
│ ├── Contract
│ │ ├── DisplayVariable.jsx
│ │ ├── FunctionForm.jsx
│ │ ├── index.jsx
│ │ └── utils.js
│ ├── EtherInput.jsx
│ ├── Faucet.jsx
│ ├── GasGauge.jsx
│ ├── Header.jsx
│ ├── L2Bridge.jsx
│ ├── NewRpc.jsx
│ ├── Provider.jsx
│ ├── Ramp.jsx
│ ├── Swap.jsx
│ ├── ThemeSwitch.jsx
│ ├── Timeline.jsx
│ ├── TokenBalance.jsx
│ ├── Wallet.jsx
│ └── index.js
├── constants.js
├── contracts
│ ├── external_contracts.js
│ └── hardhat_contracts.json
├── ethereumLogo.png
├── helpers
│ ├── Transactor.js
│ ├── index.js
│ └── loadAppContracts.js
├── hooks
│ ├── ContractEvents.js
│ ├── Debounce.js
│ ├── ExternalContractLoader.js
│ ├── GasPrice.js
│ ├── LocalStorage.js
│ ├── TokenList.js
│ ├── index.js
│ └── useContractConfig.js
├── index.css
├── index.jsx
├── setupTests.js
├── themes
│ ├── dark-theme.less
│ └── light-theme.less
└── views
│ ├── ExampleUI.jsx
│ ├── Hints.jsx
│ ├── Subgraph.jsx
│ └── index.js
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | # folders
2 | build/
3 | node_modules/
4 | # files
5 | **/*.less
6 | **/*.css
7 | **/*.scss
8 | **/*.json
9 | **/*.png
10 | **/*.svg
11 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | },
5 | parser: "babel-eslint",
6 | // airbnb disabled after upgrade to cra 4 due to errors in our code
7 | extends: [/*"airbnb"*/ "plugin:prettier/recommended"],
8 | plugins: [
9 | /*"babel"*/
10 | ],
11 | rules: {
12 | "prettier/prettier": "warn",
13 | "prettier/prettier": [
14 | "warn",
15 | {
16 | endOfLine: "auto",
17 | },
18 | ],
19 | "import/prefer-default-export": "off",
20 | "prefer-destructuring": "off",
21 | "prefer-template": "off",
22 | "react/prop-types": "off",
23 | "react/destructuring-assignment": "off",
24 | "no-console": "off",
25 | "jsx-a11y/accessible-emoji": ["off"],
26 | "jsx-a11y/click-events-have-key-events": ["off"],
27 | "jsx-a11y/no-static-element-interactions": ["off"],
28 | "no-underscore-dangle": "off",
29 | "no-nested-ternary": "off",
30 | "no-restricted-syntax": "off",
31 | "no-plusplus": "off",
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSpacing": true,
4 | "printWidth": 120,
5 | "singleQuote": false,
6 | "tabWidth": 2,
7 | "trailingComma": "all"
8 | }
9 |
--------------------------------------------------------------------------------
/.sample.env:
--------------------------------------------------------------------------------
1 | REACT_APP_PROVIDER=https://rinkeby.infura.io/v3/2717afb6bf164045b5d5468031b93f87
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flashbots Bundler
2 |
3 | An "easy-to-use" react app for building FlashBots Bundles for token rescues.
4 |
5 | [Live App Here](https://bundler.lcfr.io)
6 |
7 | This app works by adding a FlashBots RPC that caches multiple transactions into a Bundle to be sent to a
8 | Flashbots relay.
9 |
10 | [About FlashBots](https://docs.flashbots.net/)
11 | [Understanding Bundles](https://docs.flashbots.net/flashbots-auction/searchers/advanced/understanding-bundles)
12 | [Transaction Caching](https://docs.flashbots.net/flashbots-protect/rpc/bundle-cache)
13 |
14 | ### TransferProxy
15 | I've written an optimized contract for the dapp for batch transferring ERC721 and also batch sending ERC1155 to multiple receipients:
16 | [TransferProxy Github Repo](https://github.com/lcfr-eth/transferProxy)
17 | Deployed: [0xf5028d67221f8d7e09dD53e5F9Aa09a194e33A6f](https://etherscan.io/address/0xf5028d67221f8d7e09dD53e5F9Aa09a194e33A6f)
18 |
19 | ### Why
20 | Most of the apps/programs allowing Flashbots bundles are still too hard for users to use.
21 |
22 | [The ScaffoldEth](https://github.com/scaffold-eth/scaffold-eth/tree/flashbots-bundler) code this was based off of was ineffecient for transferring large amounts of tokens such as ENS collectors who own 100+ tokens.
23 |
24 | [Python examples](https://github.com/lcfr-eth/ENSPublic/blob/main/ENSRescuer/rescuer.py) require knowledge of python etc.
25 |
26 | [Other node based examples](https://github.com/Arachnid/flashbots-ens-rescue/blob/master/src/index.ts) didn't take into account large token ownership and max bundle size
27 |
28 |
29 | ### IMPORTANT:
30 |
31 | When connected to the Flashbots RPC it will display a 100ETH balance.
32 | This is for gas calculations etc but cant be spent (obviously).
33 | 
34 |
35 | When submitting transactions to a bundle you have to set a custom Priority Fee in MetaMask advanced gas settings. This has to be done for EVERY transaction you submit to the bundle.
36 |
37 | 
38 |
39 | A good tip is usually 5-20 GWEI.
40 |
41 | 
42 |
43 | ### Generalized ENS/ERC721 counter sweeper flow:
44 |
45 | !! This only applies to ERC721 collections.
46 | !! Read below for ERC20 & ERC1155.
47 |
48 | Things you will need:
49 |
50 | The ENS collection address: 0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85.
51 | A list of all your name's tokenIds. Not just names but the tokenIds.
52 | This can be obtained from:
53 | [The ENS Subgraph](https://thegraph.com/hosted-service/subgraph/ensdomains/ens)
54 | [EnsVision](https://ens.vision)
55 | [OpenSea](https://opensea.io)
56 | [Etherscan](https://etherscan.io)
57 |
58 | A clean unhacked address with ETH to fund transactions and receive tokens.
59 |
60 | Fund the hacked wallet with names + Set needed approvals with a Bundle.
61 |
62 | 1) Click the "New Bundle" tab to generate a new bundle uuid + Swith to the provided RPC.
63 | 2) Click the "Send ETH" tab.
64 | 3) Connect a clean unhacked wallet to the dapp in metamask to fund the transactions from.
65 | 4) Enter the hacked wallets address to send funds to.
66 | 5) Click the button to estimate cost.
67 | 6) Copy the cost for two approvals to the amount box below.
68 | 7) Click Send to add the transaction to the bundle.
69 | 8) Click the "Set NFT Approvals" tab.
70 | 9) Connect the hacked wallet address to the dapp in metamask.
71 | 10) Enter the NFT collection address.
72 | 11) Enter the hacked wallet (that your connected from) address.
73 | 12) Enter your unhacked wallet address to give the permission to transfer the tokens.
74 | 13) Click to add the first setApproval transaction to the bundle.
75 | 14) Click to add the second setApproval transaction to the bundle.
76 | 15) Click the "Submit Bundle" tab to submit the first bundle when finished adding approvals.
77 |
78 | Transfer Names/Nfts
79 |
80 | 1) Optional: You can change back to Ethereum Mainnet in metamask RPC now and skip adding additional tip in metamask.
81 | 2) Click the Transfer NFTs tab.
82 | 3) Connect to the dapp in metamask with the unhacked address you entered previously that has funds to cover gas.
83 | 4) Enter the NFT Collection address (should be pre-filled).
84 | 5) Enter the hacked address that holds the tokens to transfer.
85 | 6) Enter the unhacked address to receive tokens that you should be connected from (should be pre-filled).
86 | 7) Enter the tokenIds below. One per line.
87 | 8) Click "add the transaction to bundle" to execute the transaction without any bundle.
88 | 9) Optional: If still connected to the Flashbots RPC click the "Submit Bundle" tabe.
89 |
90 | ### ERC1155
91 |
92 | 1) Click the "New Bundle" tab to generate a new bundle uuid + Swith to the provided RPC.
93 | 2) Click the "Send ETH" tab.
94 | 3) Connect a clean unhacked wallet to the dapp in metamask to fund the transactions from.
95 | 4) Enter the hacked wallets address to send funds to.
96 | 5) Click the button to estimate cost.
97 | 6) Copy the value to the Amount field below.
98 | 7) Click the "Click to send eth" button to add the transaction to the bundle.
99 | 8) Change to the hacked wallet address in MetaMask.
100 | 9) Click the "Transfer NFTs" tab.
101 | 10) Enter the ERC1155 token contract address.
102 | 11) Enter the hacked wallets address.
103 | 12) Enter the unhacked receiver wallet address.
104 | 13) Enter the tokenIds one per line
105 | 14) Click "Click to add transaction to bundle" to add to bundle.
106 | 15) Click the "Submit Bundle" tab to submit the bundle with two transactions.
107 |
108 | ### ERC20
109 |
110 | 1) Click the "New Bundle" tab to generate a new bundle uuid + Swith to the provided RPC.
111 | 2) Click the "Send ETH" tab.
112 | 3) Connect a clean unhacked wallet to the dapp in metamask to fund the transactions from.
113 | 4) Enter the hacked wallets address to send funds to.
114 | 5) Click the button to estimate cost.
115 | 6) Copy the value to the Amount field below.
116 | 7) Click the "Click to send eth" button to add the transaction to the bundle.
117 | 8) Change to the hacked wallet address in MetaMask.
118 | 9) Click the "ERC20 Tokens" tab.
119 | 10) Enter the ERC20 token contracts address.
120 | 11) Optional: Click to get the balanced owned by the hacked address.
121 | 12) Click the button "Click to transfer full balance" to add the transaction to the bundle.
122 | 13) Click the "Submit Bundle" tab to submit the bundle with two transactions.
123 |
124 |
125 | ### Technical
126 |
127 | ERC1155 tokens have a built in batch transfer function to transfer multiple tokens in the same transaction.
128 |
129 | ERC721 tokens lack such functionality. In order to batch transfer a large quantity of ERC721 tokens it requires a utility contract to batch the calls together.
130 |
131 | The UI detects if a supplied token contract is ERC721 or ERC1155. If the token contract is an ERC1155 contract then the UI will utilize the built in safeBatchTransferFrom() method.
132 |
133 | This also means a bundle only requires two transactions and no approvals. One transaction to fund the safeBatchTransferFrom and another transaction to call the safeBatchTransferFrom call.
134 |
135 | This is similar to rescuing ERC20 tokens. ERC20 tokens also only require two transactions in a single bundle as well. One transaction to fund the transfer call and then the actual transfer call/transaction.
136 |
137 | In contrast if the token contract is ERC721 then it requires two setApprovalForAll transactions to batch transfer the tokens using the transferProxy utility contract.
138 |
139 | ### Additional Notes
140 |
141 | When submitting bundles you will see an alert screen to let you know it was submitted:
142 |
143 | 
144 |
145 | Your bundle might not be included in a block the first time you submit it. An alert will inform you if your bundle was not mined:
146 | 
147 |
148 | If this happens then click the "Submit Bundle" tab and submit the bundle again.
149 |
150 | Its possible it might take multiple submissions.
151 |
152 | When your bundle is included you will see an alert like this:
153 | 
154 |
155 | Currently the UI will check if the transactions were mined for +10 blocks from the block its submitted on.
156 |
157 |
158 | If you forget to add the Priority Tip to a transaction in a bundle you will need to reset the wallet state and start over again by generating a new bundle uuid in the "New Bundle" tab.
159 |
160 | ### Thanks
161 | [Austin Griffith](https://twitter.com/austingriffith) and co for the [ScaffoldETH](https://github.com/scaffold-eth/scaffold-eth) repo for the original leg work and pretty CSS.
162 |
163 |
--------------------------------------------------------------------------------
/build/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/build/favicon.ico
--------------------------------------------------------------------------------
/build/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/build/logo192.png
--------------------------------------------------------------------------------
/build/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/build/logo512.png
--------------------------------------------------------------------------------
/build/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "🏗 Scaffold-Eth App",
3 | "start_url": ".",
4 | "name": "🏗 Scaffold-Eth App",
5 | "icons": [
6 | {
7 | "src": "favicon.ico",
8 | "sizes": "64x64 32x32 24x24 16x16",
9 | "type": "image/x-icon"
10 | },
11 | {
12 | "src": "logo192.png",
13 | "type": "image/png",
14 | "sizes": "192x192"
15 | },
16 | {
17 | "src": "logo512.png",
18 | "type": "image/png",
19 | "sizes": "512x512"
20 | }
21 | ],
22 | "start_url": ".",
23 | "display": "standalone",
24 | "theme_color": "#000000",
25 | "background_color": "#ffffff"
26 | }
27 |
--------------------------------------------------------------------------------
/build/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/build/scaffold-eth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/build/scaffold-eth.png
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require("gulp");
2 | const gulpless = require("gulp-less");
3 | const postcss = require("gulp-postcss");
4 | const debug = require("gulp-debug");
5 | var csso = require("gulp-csso");
6 | const autoprefixer = require("autoprefixer");
7 | const NpmImportPlugin = require("less-plugin-npm-import");
8 |
9 | gulp.task("less", function () {
10 | const plugins = [autoprefixer()];
11 |
12 | return gulp
13 | .src("src/themes/*-theme.less")
14 | .pipe(debug({ title: "Less files:" }))
15 | .pipe(
16 | gulpless({
17 | javascriptEnabled: true,
18 | plugins: [new NpmImportPlugin({ prefix: "~" })],
19 | }),
20 | )
21 | .pipe(postcss(plugins))
22 | .pipe(
23 | csso({
24 | debug: true,
25 | }),
26 | )
27 | .pipe(gulp.dest("./public"));
28 | });
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@scaffold-eth/react-app",
3 | "version": "1.0.0",
4 | "homepage": ".",
5 | "browserslist": {
6 | "production": [
7 | ">0.2%",
8 | "not dead",
9 | "not op_mini all"
10 | ],
11 | "development": [
12 | "last 1 chrome version",
13 | "last 1 firefox version",
14 | "last 1 safari version"
15 | ]
16 | },
17 | "dependencies": {
18 | "@ant-design/icons": "^4.2.2",
19 | "@apollo/client": "^3.3.21",
20 | "@apollo/react-hooks": "^4.0.0",
21 | "@ethersproject/contracts": "^5.0.5",
22 | "@flashbots/ethers-provider-bundle": "^0.5.0",
23 | "@portis/web3": "^4.0.5",
24 | "@ramp-network/ramp-instant-sdk": "^2.2.0",
25 | "@react-ui-org/react-ui": "^0.42.1",
26 | "@testing-library/jest-dom": "^5.11.4",
27 | "@testing-library/react": "^11.1.0",
28 | "@testing-library/user-event": "^12.1.8",
29 | "@uniswap/sdk": "^3.0.3",
30 | "@uniswap/v2-periphery": "^1.1.0-beta.0",
31 | "@walletconnect/web3-provider": "^1.5.2",
32 | "antd": "4.16.0",
33 | "apollo-boost": "^0.4.9",
34 | "apollo-client": "^2.6.10",
35 | "apollo-utilities": "^1.3.4",
36 | "arb-ts": "^0.0.18",
37 | "authereum": "^0.1.14",
38 | "axios": "^0.21.4",
39 | "bnc-notify": "^1.5.0",
40 | "dotenv": "^8.2.0",
41 | "eth-hooks": "2.3.8",
42 | "ethers": "^5.4.1",
43 | "fortmatic": "^2.2.1",
44 | "graphiql": "^1.0.5",
45 | "graphql": "^15.3.0",
46 | "isomorphic-fetch": "^3.0.0",
47 | "node-watch": "^0.7.1",
48 | "postcss": "^8.2.6",
49 | "qrcode.react": "^1.0.0",
50 | "react": "^17.0.2",
51 | "react-blockies": "^1.4.1",
52 | "react-css-theme-switcher": "^0.2.2",
53 | "react-dom": "^17.0.2",
54 | "react-json-view": "^1.21.3",
55 | "react-qr-reader": "^2.2.1",
56 | "react-router-dom": "^5.2.0",
57 | "react-scripts": "4.0.0",
58 | "uuidv4": "^6.2.13",
59 | "walletlink": "^2.1.5",
60 | "web3modal": "^1.9.1"
61 | },
62 | "devDependencies": {
63 | "@apollo/client": "^3.3.21",
64 | "@testing-library/dom": "^6.12.2",
65 | "@types/react": "^16.9.19",
66 | "autoprefixer": "^10.2.4",
67 | "chalk": "^4.1.0",
68 | "eslint": "^7.5.0",
69 | "eslint-config-airbnb": "^18.2.0",
70 | "eslint-config-prettier": "^8.3.0",
71 | "eslint-plugin-babel": "^5.3.1",
72 | "eslint-plugin-import": "^2.23.4",
73 | "eslint-plugin-jsx-a11y": "^6.4.1",
74 | "eslint-plugin-prettier": "^3.4.0",
75 | "eslint-plugin-react": "^7.22.0",
76 | "eslint-plugin-react-hooks": "^4.2.0",
77 | "gulp": "^4.0.2",
78 | "gulp-csso": "^4.0.1",
79 | "gulp-debug": "^4.0.0",
80 | "gulp-less": "^4.0.1",
81 | "gulp-postcss": "^9.0.0",
82 | "ipfs-http-client": "^45.0.0",
83 | "less-plugin-npm-import": "^2.1.0",
84 | "prettier": "^2.0.5",
85 | "s3-folder-upload": "^2.3.1",
86 | "surge": "^0.21.5"
87 | },
88 | "eslintConfig": {
89 | "extends": "react-app"
90 | },
91 | "scripts": {
92 | "build": "react-scripts build",
93 | "eject": "react-scripts eject",
94 | "prestart": "node ./scripts/create_contracts.js",
95 | "start": "react-scripts --openssl-legacy-provider start",
96 | "test": "react-scripts test",
97 | "lint": "eslint --config ./.eslintrc.js --ignore-path ./.eslintignore ./src/**/*",
98 | "ipfs": "node ./scripts/ipfs.js",
99 | "surge": "cp build/index.html build/200.html && surge ./build",
100 | "s3": "node ./scripts/s3.js",
101 | "ship": "yarn surge",
102 | "theme": "npx gulp less",
103 | "watch": "node ./scripts/watch.js",
104 | "prettier": "npx prettier --write . '!(node_module|build)/**/*'"
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 1.26.31 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 1.26.31 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 1.51.37 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 1.51.37 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.51.58 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.51.58 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.52.30 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.52.30 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.53.04 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.53.04 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.53.15 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.53.15 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.53.27 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.53.27 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.53.57 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.53.57 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.55.51 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.55.51 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.56.07 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.56.07 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.56.40 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.56.40 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.57.32 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.57.32 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.57.45 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.57.45 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.58.02 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.58.02 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.58.16 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.58.16 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 12.58.59 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 12.58.59 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-21 at 2.03.52 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-21 at 2.03.52 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-22 at 10.10.25 AM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-22 at 10.10.25 AM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-22 at 10.17.46 AM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-22 at 10.17.46 AM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-22 at 10.18.25 AM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-22 at 10.18.25 AM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-22 at 12.09.39 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-22 at 12.09.39 PM.png
--------------------------------------------------------------------------------
/public/Screen Shot 2023-05-22 at 12.09.58 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/Screen Shot 2023-05-22 at 12.09.58 PM.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
24 | Flashbots Bundler
25 |
26 |
27 | You need to enable JavaScript to run this app.
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "🏗 Scaffold-Eth App",
3 | "start_url": ".",
4 | "name": "🏗 Scaffold-Eth App",
5 | "icons": [
6 | {
7 | "src": "favicon.ico",
8 | "sizes": "64x64 32x32 24x24 16x16",
9 | "type": "image/x-icon"
10 | },
11 | {
12 | "src": "logo192.png",
13 | "type": "image/png",
14 | "sizes": "192x192"
15 | },
16 | {
17 | "src": "logo512.png",
18 | "type": "image/png",
19 | "sizes": "512x512"
20 | }
21 | ],
22 | "start_url": ".",
23 | "display": "standalone",
24 | "theme_color": "#000000",
25 | "background_color": "#ffffff"
26 | }
27 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/public/scaffold-eth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/public/scaffold-eth.png
--------------------------------------------------------------------------------
/scripts/create_contracts.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | if (!fs.existsSync("./src/contracts/hardhat_contracts.json")) {
4 | try {
5 | fs.writeFileSync("./src/contracts/hardhat_contracts.json", JSON.stringify({}));
6 |
7 | console.log("src/contracts/hardhat_contracts.json created.");
8 | } catch (error) {
9 | console.log(error);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/scripts/ipfs.js:
--------------------------------------------------------------------------------
1 | const ipfsAPI = require("ipfs-http-client");
2 | const chalk = require("chalk");
3 | const { clearLine } = require("readline");
4 |
5 | const { globSource } = ipfsAPI;
6 |
7 | const infura = { host: "ipfs.infura.io", port: "5001", protocol: "https" };
8 | // run your own ipfs daemon: https://docs.ipfs.io/how-to/command-line-quick-start/#install-ipfs
9 | // const localhost = { host: "localhost", port: "5001", protocol: "http" };
10 |
11 | const ipfs = ipfsAPI(infura);
12 |
13 | const ipfsGateway = "https://ipfs.io/ipfs/";
14 | const ipnsGateway = "https://ipfs.io/ipns/";
15 |
16 | const addOptions = {
17 | pin: true,
18 | };
19 |
20 | const pushDirectoryToIPFS = async path => {
21 | try {
22 | const response = await ipfs.add(globSource(path, { recursive: true }), addOptions);
23 | return response;
24 | } catch (e) {
25 | return {};
26 | }
27 | };
28 |
29 | const publishHashToIPNS = async ipfsHash => {
30 | try {
31 | const response = await ipfs.name.publish(`/ipfs/${ipfsHash}`);
32 | return response;
33 | } catch (e) {
34 | return {};
35 | }
36 | };
37 |
38 | const nodeMayAllowPublish = ipfsClient => {
39 | // You must have your own IPFS node in order to publish an IPNS name
40 | // This contains a blacklist of known nodes which do not allow users to publish IPNS names.
41 | const nonPublishingNodes = ["ipfs.infura.io"];
42 | const { host } = ipfsClient.getEndpointConfig();
43 | return !nonPublishingNodes.some(nodeUrl => host.includes(nodeUrl));
44 | };
45 |
46 | const deploy = async () => {
47 | console.log("🛰 Sending to IPFS...");
48 | const { cid } = await pushDirectoryToIPFS("./build");
49 | if (!cid) {
50 | console.log(`📡 App deployment failed`);
51 | return false;
52 | }
53 | console.log(`📡 App deployed to IPFS with hash: ${chalk.cyan(cid.toString())}`);
54 |
55 | console.log();
56 |
57 | let ipnsName = "";
58 | if (nodeMayAllowPublish(ipfs)) {
59 | console.log(`✍️ Publishing /ipfs/${cid.toString()} to IPNS...`);
60 | process.stdout.write(" Publishing to IPNS can take up to roughly two minutes.\r");
61 | ipnsName = (await publishHashToIPNS(cid.toString())).name;
62 | clearLine(process.stdout, 0);
63 | if (!ipnsName) {
64 | console.log(" Publishing IPNS name on node failed.");
65 | }
66 | console.log(`🔖 App published to IPNS with name: ${chalk.cyan(ipnsName)}`);
67 | console.log();
68 | }
69 |
70 | console.log("🚀 Deployment to IPFS complete!");
71 | console.log();
72 |
73 | console.log(`Use the link${ipnsName && "s"} below to access your app:`);
74 | console.log(` IPFS: ${chalk.cyan(`${ipfsGateway}${cid.toString()}`)}`);
75 | if (ipnsName) {
76 | console.log(` IPNS: ${chalk.cyan(`${ipnsGateway}${ipnsName}`)}`);
77 | console.log();
78 | console.log(
79 | "Each new deployment will have a unique IPFS hash while the IPNS name will always point at the most recent deployment.",
80 | );
81 | console.log(
82 | "It is recommended that you share the IPNS link so that people always see the newest version of your app.",
83 | );
84 | }
85 | console.log();
86 | return true;
87 | };
88 |
89 | deploy();
90 |
--------------------------------------------------------------------------------
/scripts/s3.js:
--------------------------------------------------------------------------------
1 | const s3FolderUpload = require("s3-folder-upload");
2 | const fs = require("fs");
3 |
4 | const directoryName = "build";
5 |
6 | const BUCKETNAME = ""; // <<---- SET YOUR BUCKET NAME AND CREATE aws.json ** see below vvvvvvvvvv
7 |
8 | if (!BUCKETNAME) {
9 | console.log("☢️ Enter a bucket name in packages/react-app/scripts/s3.js ");
10 | process.exit(1);
11 | }
12 |
13 | let credentials = {};
14 | try {
15 | credentials = JSON.parse(fs.readFileSync("aws.json"));
16 | } catch (e) {
17 | console.log(e);
18 | console.log(
19 | '☢️ Create an aws.json credentials file in packages/react-app/ like { "accessKeyId": "xxx", "secretAccessKey": "xxx", "region": "xxx" } ',
20 | );
21 | process.exit(1);
22 | }
23 | console.log("credentials", credentials);
24 |
25 | credentials.bucket = BUCKETNAME;
26 |
27 | // optional options to be passed as parameter to the method
28 | const options = {
29 | useFoldersForFileTypes: false,
30 | useIAMRoleCredentials: false,
31 | };
32 |
33 | // optional cloudfront invalidation rule
34 | // const invalidation = {
35 | // awsDistributionId: "",
36 | // awsInvalidationPath: "/*"
37 | // }
38 |
39 | s3FolderUpload(directoryName, credentials, options /* , invalidation */);
40 |
--------------------------------------------------------------------------------
/scripts/watch.js:
--------------------------------------------------------------------------------
1 | const watch = require("node-watch");
2 | const { exec } = require("child_process");
3 |
4 | const run = () => {
5 | console.log("Compiling & Generating...");
6 | exec("npx gulp less", function (error, stdout, stderr) {
7 | console.log(stdout);
8 | if (error) console.log(error);
9 | if (stderr) console.log(stderr);
10 | });
11 | };
12 |
13 | console.log("🔬 Watching Themes...");
14 | watch("./src/themes", { recursive: true }, function (evt, name) {
15 | console.log("%s changed.", name);
16 | run();
17 | });
18 | run();
19 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 | .center{
5 | width:50%;
6 | margin:0 auto;
7 | }
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import App from "./App";
4 |
5 | test("renders learn react link", () => {
6 | const { getByText } = render( );
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/components/Account.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "antd";
2 | import React from "react";
3 | import { useThemeSwitcher } from "react-css-theme-switcher";
4 | import Address from "./Address";
5 | import Balance from "./Balance";
6 | import Wallet from "./Wallet";
7 |
8 | /*
9 | ~ What it does? ~
10 |
11 | Displays an Address, Balance, and Wallet as one Account component,
12 | also allows users to log in to existing accounts and log out
13 |
14 | ~ How can I use? ~
15 |
16 |
27 |
28 | ~ Features ~
29 |
30 | - Provide address={address} and get balance corresponding to the given address
31 | - Provide localProvider={localProvider} to access balance on local network
32 | - Provide userProvider={userProvider} to display a wallet
33 | - Provide mainnetProvider={mainnetProvider} and your address will be replaced by ENS name
34 | (ex. "0xa870" => "user.eth")
35 | - Provide price={price} of ether and get your balance converted to dollars
36 | - Provide web3Modal={web3Modal}, loadWeb3Modal={loadWeb3Modal}, logoutOfWeb3Modal={logoutOfWeb3Modal}
37 | to be able to log in/log out to/from existing accounts
38 | - Provide blockExplorer={blockExplorer}, click on address and get the link
39 | (ex. by default "https://etherscan.io/" or for xdai "https://blockscout.com/poa/xdai/")
40 | */
41 |
42 | export default function Account({
43 | address,
44 | userSigner,
45 | localProvider,
46 | mainnetProvider,
47 | price,
48 | minimized,
49 | web3Modal,
50 | loadWeb3Modal,
51 | logoutOfWeb3Modal,
52 | blockExplorer,
53 | }) {
54 | const modalButtons = [];
55 | if (web3Modal) {
56 | if (web3Modal.cachedProvider) {
57 | modalButtons.push(
58 |
65 | logout
66 | ,
67 | );
68 | } else {
69 | modalButtons.push(
70 |
78 | connect
79 | ,
80 | );
81 | }
82 | }
83 |
84 | const { currentTheme } = useThemeSwitcher();
85 |
86 | const display = minimized ? (
87 | ""
88 | ) : (
89 |
90 | {address ? (
91 |
92 | ) : (
93 | "Connecting..."
94 | )}
95 | {/* */ }
96 |
104 |
105 | );
106 |
107 | return (
108 |
109 | {display}
110 | {modalButtons}
111 |
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/Address.jsx:
--------------------------------------------------------------------------------
1 | import { Skeleton, Typography } from "antd";
2 | import React from "react";
3 | import Blockies from "react-blockies";
4 | import { useThemeSwitcher } from "react-css-theme-switcher";
5 | import { useLookupAddress } from "eth-hooks/dapps/ens";
6 |
7 | // changed value={address} to address={address}
8 |
9 | /*
10 | ~ What it does? ~
11 |
12 | Displays an address with a blockie image and option to copy address
13 |
14 | ~ How can I use? ~
15 |
16 |
22 |
23 | ~ Features ~
24 |
25 | - Provide ensProvider={mainnetProvider} and your address will be replaced by ENS name
26 | (ex. "0xa870" => "user.eth")
27 | - Provide blockExplorer={blockExplorer}, click on address and get the link
28 | (ex. by default "https://etherscan.io/" or for xdai "https://blockscout.com/poa/xdai/")
29 | - Provide fontSize={fontSize} to change the size of address text
30 | */
31 |
32 | const { Text } = Typography;
33 |
34 | const blockExplorerLink = (address, blockExplorer) =>
35 | `${blockExplorer || "https://etherscan.io/"}${"address/"}${address}`;
36 |
37 | export default function Address(props) {
38 | const address = props.value || props.address;
39 |
40 | const ens = useLookupAddress(props.ensProvider, address);
41 |
42 | const { currentTheme } = useThemeSwitcher();
43 |
44 | if (!address) {
45 | return (
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | let displayAddress = address.substr(0, 6);
53 |
54 | const ensSplit = ens && ens.split(".");
55 | const validEnsCheck = ensSplit && ensSplit[ensSplit.length - 1] === "eth";
56 |
57 | if (validEnsCheck) {
58 | displayAddress = ens;
59 | } else if (props.size === "short") {
60 | displayAddress += "..." + address.substr(-4);
61 | } else if (props.size === "long") {
62 | displayAddress = address;
63 | }
64 |
65 | const etherscanLink = blockExplorerLink(address, props.blockExplorer);
66 | if (props.minimized) {
67 | return (
68 |
69 |
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | let text;
82 | if (props.onChange) {
83 | text = (
84 |
85 |
91 | {displayAddress}
92 |
93 |
94 | );
95 | } else {
96 | text = (
97 |
98 |
104 | {displayAddress}
105 |
106 |
107 | );
108 | }
109 |
110 | return (
111 |
112 |
113 |
114 |
115 |
116 | {text}
117 |
118 |
119 | );
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/AddressInput.jsx:
--------------------------------------------------------------------------------
1 | import { CameraOutlined, QrcodeOutlined } from "@ant-design/icons";
2 | import { Badge, Input } from "antd";
3 | import { useLookupAddress } from "eth-hooks/dapps/ens";
4 | import React, { useCallback, useState } from "react";
5 | import QrReader from "react-qr-reader";
6 | import Blockie from "./Blockie";
7 |
8 | // probably we need to change value={toAddress} to address={toAddress}
9 |
10 | /*
11 | ~ What it does? ~
12 |
13 | Displays an address input with QR scan option
14 |
15 | ~ How can I use? ~
16 |
17 |
24 |
25 | ~ Features ~
26 |
27 | - Provide ensProvider={mainnetProvider} and your address will be replaced by ENS name
28 | (ex. "0xa870" => "user.eth") or you can enter directly ENS name instead of address
29 | - Provide placeholder="Enter address" value for the input
30 | - Value of the address input is stored in value={toAddress}
31 | - Control input change by onChange={setToAddress}
32 | or onChange={address => { setToAddress(address);}}
33 | */
34 |
35 | export default function AddressInput(props) {
36 | const [value, setValue] = useState(props.value);
37 | const [scan, setScan] = useState(false);
38 |
39 | const currentValue = typeof props.value !== "undefined" ? props.value : value;
40 | const ens = useLookupAddress(props.ensProvider, currentValue);
41 |
42 | const scannerButton = (
43 | {
46 | setScan(!scan);
47 | }}
48 | >
49 | }>
50 |
51 | {" "}
52 | Scan
53 |
54 | );
55 |
56 | const { ensProvider, onChange } = props;
57 | const updateAddress = useCallback(
58 | async newValue => {
59 | if (typeof newValue !== "undefined") {
60 | let address = newValue;
61 | if (address.indexOf(".eth") > 0 || address.indexOf(".xyz") > 0) {
62 | try {
63 | const possibleAddress = await ensProvider.resolveName(address);
64 | if (possibleAddress) {
65 | address = possibleAddress;
66 | }
67 | // eslint-disable-next-line no-empty
68 | } catch (e) {}
69 | }
70 | setValue(address);
71 | if (typeof onChange === "function") {
72 | onChange(address);
73 | }
74 | }
75 | },
76 | [ensProvider, onChange],
77 | );
78 |
79 | const scanner = scan ? (
80 | {
89 | setScan(false);
90 | }}
91 | >
92 | {
96 | console.log("SCAN ERROR", e);
97 | setScan(false);
98 | }}
99 | onScan={newValue => {
100 | if (newValue) {
101 | console.log("SCAN VALUE", newValue);
102 | let possibleNewValue = newValue;
103 | if (possibleNewValue.indexOf("/") >= 0) {
104 | possibleNewValue = possibleNewValue.substr(possibleNewValue.lastIndexOf("0x"));
105 | console.log("CLEANED VALUE", possibleNewValue);
106 | }
107 | setScan(false);
108 | updateAddress(possibleNewValue);
109 | }
110 | }}
111 | style={{ width: "100%" }}
112 | />
113 |
114 | ) : (
115 | ""
116 | );
117 |
118 | return (
119 |
120 | {scanner}
121 | }
128 | value={ens || currentValue}
129 | addonAfter={scannerButton}
130 | onChange={e => {
131 | updateAddress(e.target.value);
132 | }}
133 | />
134 |
135 | );
136 | }
137 |
--------------------------------------------------------------------------------
/src/components/Balance.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useBalance } from "eth-hooks";
3 |
4 | const { utils } = require("ethers");
5 |
6 | /*
7 | ~ What it does? ~
8 |
9 | Displays a balance of given address in ether & dollar
10 |
11 | ~ How can I use? ~
12 |
13 |
18 |
19 | ~ If you already have the balance as a bignumber ~
20 |
24 |
25 | ~ Features ~
26 |
27 | - Provide address={address} and get balance corresponding to given address
28 | - Provide provider={mainnetProvider} to access balance on mainnet or any other network (ex. localProvider)
29 | - Provide price={price} of ether and get your balance converted to dollars
30 | */
31 |
32 | export default function Balance(props) {
33 | const [dollarMode, setDollarMode] = useState(true);
34 |
35 | // const [listening, setListening] = useState(false);
36 |
37 | const balance = useBalance(props.provider, props.address);
38 |
39 | let floatBalance = parseFloat("0.00");
40 |
41 | let usingBalance = balance;
42 |
43 | if (typeof props.balance !== "undefined") {
44 | usingBalance = props.balance;
45 | }
46 | if (typeof props.value !== "undefined") {
47 | usingBalance = props.value;
48 | }
49 |
50 | if (usingBalance) {
51 | const etherBalance = utils.formatEther(usingBalance);
52 | parseFloat(etherBalance).toFixed(2);
53 | floatBalance = parseFloat(etherBalance);
54 | }
55 |
56 | let displayBalance = floatBalance.toFixed(4);
57 |
58 | const price = props.price || props.dollarMultiplier || 1;
59 |
60 | if (dollarMode) {
61 | displayBalance = "$" + (floatBalance * price).toFixed(2);
62 | }
63 |
64 | return (
65 | {
73 | setDollarMode(!dollarMode);
74 | }}
75 | >
76 | {displayBalance}
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/Blockie.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Blockies from "react-blockies";
3 |
4 | // provides a blockie image for the address using "react-blockies" library
5 |
6 | export default function Blockie(props) {
7 | if (!props.address || typeof props.address.toLowerCase !== "function") {
8 | return ;
9 | }
10 | // eslint-disable-next-line react/jsx-props-no-spreading
11 | return ;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/BytesStringInput.jsx:
--------------------------------------------------------------------------------
1 | import { Input } from "antd";
2 | import React, { useEffect, useState } from "react";
3 |
4 | const { utils, constants } = require("ethers");
5 |
6 | /*
7 | ~ What it does? ~
8 |
9 | Displays input field with options to convert between STRING and BYTES32
10 |
11 | ~ How can I use? ~
12 |
13 | {
18 | setValue(value);
19 | }}
20 | />
21 |
22 | ~ Features ~
23 |
24 | - Provide value={value} to specify initial string
25 | - Provide placeholder="Enter value..." value for the input
26 | - Control input change by onChange={value => { setValue(value);}}
27 |
28 | */
29 |
30 | export default function BytesStringInput(props) {
31 | const [mode, setMode] = useState("STRING");
32 | const [display, setDisplay] = useState();
33 | const [value, setValue] = useState(constants.HashZero);
34 |
35 | // current value is the value in bytes32
36 | const currentValue = typeof props.value !== "undefined" ? props.value : value;
37 |
38 | const option = title => {
39 | return (
40 | {
43 | if (mode === "STRING") {
44 | setMode("BYTES32");
45 | if (!utils.isHexString(currentValue)) {
46 | /* in case user enters invalid bytes32 number,
47 | it considers it as string and converts to bytes32 */
48 | const changedValue = utils.formatBytes32String(currentValue);
49 | setDisplay(changedValue);
50 | } else {
51 | setDisplay(currentValue);
52 | }
53 | } else {
54 | setMode("STRING");
55 | if (currentValue && utils.isHexString(currentValue)) {
56 | setDisplay(utils.parseBytes32String(currentValue));
57 | } else {
58 | setDisplay(currentValue);
59 | }
60 | }
61 | }}
62 | >
63 | {title}
64 |
65 | );
66 | };
67 |
68 | let addonAfter;
69 | if (mode === "STRING") {
70 | addonAfter = option("STRING 🔀");
71 | } else {
72 | addonAfter = option("BYTES32 🔀");
73 | }
74 |
75 | useEffect(() => {
76 | if (!currentValue) {
77 | setDisplay("");
78 | }
79 | }, [currentValue]);
80 |
81 | return (
82 | {
88 | const newValue = e.target.value;
89 | if (mode === "STRING") {
90 | // const ethValue = parseFloat(newValue) / props.price;
91 | // setValue(ethValue);
92 | if (typeof props.onChange === "function") {
93 | props.onChange(utils.formatBytes32String(newValue));
94 | }
95 | setValue(utils.formatBytes32String(newValue));
96 | setDisplay(newValue);
97 | } else {
98 | if (typeof props.onChange === "function") {
99 | props.onChange(newValue);
100 | }
101 | setValue(newValue);
102 | setDisplay(newValue);
103 | }
104 | }}
105 | />
106 | );
107 | }
108 |
--------------------------------------------------------------------------------
/src/components/Contract/DisplayVariable.jsx:
--------------------------------------------------------------------------------
1 | import { Col, Divider, Row } from "antd";
2 | import React, { useCallback, useEffect, useState } from "react";
3 | import tryToDisplay from "./utils";
4 |
5 | const DisplayVariable = ({ contractFunction, functionInfo, refreshRequired, triggerRefresh }) => {
6 | const [variable, setVariable] = useState("");
7 |
8 | const refresh = useCallback(async () => {
9 | try {
10 | const funcResponse = await contractFunction();
11 | setVariable(funcResponse);
12 | triggerRefresh(false);
13 | } catch (e) {
14 | console.log(e);
15 | }
16 | }, [setVariable, contractFunction, triggerRefresh]);
17 |
18 | useEffect(() => {
19 | refresh();
20 | }, [refresh, refreshRequired, contractFunction]);
21 |
22 | return (
23 |
24 |
25 |
34 | {functionInfo.name}
35 |
36 |
37 | {tryToDisplay(variable)}
38 |
39 |
40 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default DisplayVariable;
53 |
--------------------------------------------------------------------------------
/src/components/Contract/FunctionForm.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Col, Divider, Input, Row, Tooltip } from "antd";
2 | import React, { useState } from "react";
3 | import Blockies from "react-blockies";
4 | import { Transactor } from "../../helpers";
5 | import tryToDisplay from "./utils";
6 |
7 | const { utils, BigNumber } = require("ethers");
8 |
9 | const getFunctionInputKey = (functionInfo, input, inputIndex) => {
10 | const name = input?.name ? input.name : "input_" + inputIndex + "_";
11 | return functionInfo.name + "_" + name + "_" + input.type;
12 | };
13 |
14 | export default function FunctionForm({ contractFunction, functionInfo, provider, gasPrice, triggerRefresh }) {
15 | const [form, setForm] = useState({});
16 | const [txValue, setTxValue] = useState();
17 | const [returnValue, setReturnValue] = useState();
18 |
19 | const tx = Transactor(provider, gasPrice);
20 |
21 | const inputs = functionInfo.inputs.map((input, inputIndex) => {
22 | const key = getFunctionInputKey(functionInfo, input, inputIndex);
23 |
24 | let buttons = "";
25 | if (input.type === "bytes32") {
26 | buttons = (
27 |
28 | {
32 | if (utils.isHexString(form[key])) {
33 | const formUpdate = { ...form };
34 | formUpdate[key] = utils.parseBytes32String(form[key]);
35 | setForm(formUpdate);
36 | } else {
37 | const formUpdate = { ...form };
38 | formUpdate[key] = utils.formatBytes32String(form[key]);
39 | setForm(formUpdate);
40 | }
41 | }}
42 | >
43 | #️⃣
44 |
45 |
46 | );
47 | } else if (input.type === "bytes") {
48 | buttons = (
49 |
50 | {
54 | if (utils.isHexString(form[key])) {
55 | const formUpdate = { ...form };
56 | formUpdate[key] = utils.toUtf8String(form[key]);
57 | setForm(formUpdate);
58 | } else {
59 | const formUpdate = { ...form };
60 | formUpdate[key] = utils.hexlify(utils.toUtf8Bytes(form[key]));
61 | setForm(formUpdate);
62 | }
63 | }}
64 | >
65 | #️⃣
66 |
67 |
68 | );
69 | } else if (input.type === "uint256") {
70 | buttons = (
71 |
72 | {
76 | const formUpdate = { ...form };
77 | formUpdate[key] = utils.parseEther(form[key]);
78 | setForm(formUpdate);
79 | }}
80 | >
81 | ✴️
82 |
83 |
84 | );
85 | } else if (input.type === "address") {
86 | const possibleAddress = form[key] && form[key].toLowerCase && form[key].toLowerCase().trim();
87 | if (possibleAddress && possibleAddress.length === 42) {
88 | buttons = (
89 |
90 |
91 |
92 | );
93 | }
94 | }
95 |
96 | return (
97 |
98 | {
105 | const formUpdate = { ...form };
106 | formUpdate[event.target.name] = event.target.value;
107 | setForm(formUpdate);
108 | }}
109 | suffix={buttons}
110 | />
111 |
112 | );
113 | });
114 |
115 | const txValueInput = (
116 |
117 |
setTxValue(e.target.value)}
120 | value={txValue}
121 | addonAfter={
122 |
123 |
124 |
125 |
126 | {
130 | const floatValue = parseFloat(txValue);
131 | if (floatValue) setTxValue("" + floatValue * 10 ** 18);
132 | }}
133 | >
134 | ✳️
135 |
136 |
137 |
138 |
139 |
140 | {
144 | setTxValue(BigNumber.from(txValue).toHexString());
145 | }}
146 | >
147 | #️⃣
148 |
149 |
150 |
151 |
152 |
153 | }
154 | />
155 |
156 | );
157 |
158 | if (functionInfo.payable) {
159 | inputs.push(txValueInput);
160 | }
161 |
162 | const buttonIcon =
163 | functionInfo.type === "call" ? (
164 | Read📡
165 | ) : (
166 | Send💸
167 | );
168 | inputs.push(
169 |
170 |
setReturnValue(e.target.value)}
172 | defaultValue=""
173 | bordered={false}
174 | disabled
175 | value={returnValue}
176 | suffix={
177 |
{
181 | const args = functionInfo.inputs.map((input, inputIndex) => {
182 | const key = getFunctionInputKey(functionInfo, input, inputIndex);
183 | let value = form[key];
184 | if (input.baseType === "array") {
185 | value = JSON.parse(value);
186 | } else if (input.type === "bool") {
187 | if (value === "true" || value === "1" || value === "0x1" || value === "0x01" || value === "0x0001") {
188 | value = 1;
189 | } else {
190 | value = 0;
191 | }
192 | }
193 | return value;
194 | });
195 |
196 | let result;
197 | if (functionInfo.stateMutability === "view" || functionInfo.stateMutability === "pure") {
198 | try {
199 | const returned = await contractFunction(...args);
200 | result = tryToDisplay(returned);
201 | } catch (err) {
202 | console.error(err);
203 | }
204 | } else {
205 | const overrides = {};
206 | if (txValue) {
207 | overrides.value = txValue; // ethers.utils.parseEther()
208 | }
209 | if (gasPrice) {
210 | overrides.gasPrice = gasPrice;
211 | }
212 | // Uncomment this if you want to skip the gas estimation for each transaction
213 | // overrides.gasLimit = hexlify(1200000);
214 |
215 | // console.log("Running with extras",extras)
216 | const returned = await tx(contractFunction(...args, overrides));
217 | result = tryToDisplay(returned);
218 | }
219 |
220 | console.log("SETTING RESULT:", result);
221 | setReturnValue(result);
222 | triggerRefresh(true);
223 | }}
224 | >
225 | {buttonIcon}
226 |
227 | }
228 | />
229 |
,
230 | );
231 |
232 | return (
233 |
234 |
235 |
244 | {functionInfo.name}
245 |
246 | {inputs}
247 |
248 |
249 |
250 | );
251 | }
252 |
--------------------------------------------------------------------------------
/src/components/Contract/index.jsx:
--------------------------------------------------------------------------------
1 | import { Card } from "antd";
2 | import React, { useMemo, useState } from "react";
3 | import { useContractExistsAtAddress, useContractLoader } from "eth-hooks";
4 | import { useEventListener } from "eth-hooks/events/useEventListener";
5 | import Account from "../Account";
6 | import Address from "../Address";
7 | import DisplayVariable from "./DisplayVariable";
8 | import FunctionForm from "./FunctionForm";
9 | import { useContractEvents } from "../../hooks";
10 |
11 | const noContractDisplay = (
12 |
13 | Loading...{" "}
14 |
15 | You need to run{" "}
16 |
20 | yarn run chain
21 | {" "}
22 | and{" "}
23 |
27 | yarn run deploy
28 | {" "}
29 | to see your contract here.
30 |
31 |
32 |
33 | ☢️
34 |
35 | Warning: You might need to run
36 |
40 | yarn run deploy
41 | {" "}
42 | again after the frontend comes up!
43 |
44 |
45 | );
46 |
47 | const noEventsDisplay = No Events found!!
;
48 |
49 | const isQueryable = fn => (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length === 0;
50 |
51 | export default function Contract({
52 | customContract,
53 | account,
54 | gasPrice,
55 | signer,
56 | provider,
57 | name,
58 | show,
59 | price,
60 | blockExplorer,
61 | chainId,
62 | contractConfig,
63 | }) {
64 | const contracts = useContractLoader(provider, contractConfig, chainId);
65 | let contract;
66 | if (!customContract) {
67 | contract = contracts ? contracts[name] : "";
68 | } else {
69 | contract = customContract;
70 | }
71 |
72 | const address = contract ? contract.address : "";
73 | const contractIsDeployed = useContractExistsAtAddress(provider, address);
74 |
75 | const displayedContractFunctions = useMemo(() => {
76 | const results = contract
77 | ? Object.entries(contract.interface.functions).filter(
78 | fn => fn[1]["type"] === "function" && !(show && show.indexOf(fn[1]["name"]) < 0),
79 | )
80 | : [];
81 | return results;
82 | }, [contract, show]);
83 |
84 | const [refreshRequired, triggerRefresh] = useState(false);
85 | const contractEventsDisplay = useContractEvents(contract, "");
86 |
87 | const contractDisplay = displayedContractFunctions.map(contractFuncInfo => {
88 | const contractFunc =
89 | contractFuncInfo[1].stateMutability === "view" || contractFuncInfo[1].stateMutability === "pure"
90 | ? contract[contractFuncInfo[0]]
91 | : contract.connect(signer)[contractFuncInfo[0]];
92 |
93 | if (typeof contractFunc === "function") {
94 | if (isQueryable(contractFuncInfo[1])) {
95 | // If there are no inputs, just display return value
96 | return (
97 |
104 | );
105 | }
106 |
107 | // If there are inputs, display a form to allow users to provide these
108 | return (
109 |
117 | );
118 | }
119 | return null;
120 | });
121 |
122 | return (
123 |
124 |
127 | {name}
128 |
129 |
137 | {account}
138 |
139 |
140 | }
141 | size="large"
142 | style={{ marginTop: 25, width: "100%" }}
143 | loading={contractDisplay && contractDisplay.length <= 0}
144 | >
145 | {contractIsDeployed ? contractDisplay : noContractDisplay}
146 |
147 | {contractIsDeployed ? contractEventsDisplay : noEventsDisplay}
148 |
149 | );
150 | }
151 |
--------------------------------------------------------------------------------
/src/components/Contract/utils.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Address } from "..";
3 |
4 | const { utils } = require("ethers");
5 |
6 | const tryToDisplay = thing => {
7 | if (thing && thing.toNumber) {
8 | try {
9 | return thing.toNumber();
10 | } catch (e) {
11 | return "Ξ" + utils.formatUnits(thing, "ether");
12 | }
13 | }
14 | if (thing && thing.indexOf && thing.indexOf("0x") === 0 && thing.length === 42) {
15 | return ;
16 | }
17 | return JSON.stringify(thing);
18 | };
19 |
20 | export default tryToDisplay;
21 |
--------------------------------------------------------------------------------
/src/components/EtherInput.jsx:
--------------------------------------------------------------------------------
1 | import { Input } from "antd";
2 | import React, { useEffect, useState } from "react";
3 |
4 | // small change in useEffect, display currentValue if it's provided by user
5 |
6 | /*
7 | ~ What it does? ~
8 |
9 | Displays input field for ETH/USD amount, with an option to convert between ETH and USD
10 |
11 | ~ How can I use? ~
12 |
13 | {
19 | setAmount(value);
20 | }}
21 | />
22 |
23 | ~ Features ~
24 |
25 | - Provide price={price} of ether and easily convert between USD and ETH
26 | - Provide value={value} to specify initial amount of ether
27 | - Provide placeholder="Enter amount" value for the input
28 | - Control input change by onChange={value => { setAmount(value);}}
29 | */
30 |
31 | export default function EtherInput(props) {
32 | const [mode, setMode] = useState("ETH");
33 | const [display, setDisplay] = useState();
34 | const [value, setValue] = useState();
35 |
36 | const currentValue = typeof props.value !== "undefined" ? props.value : value;
37 |
38 | const option = title => {
39 | if (!props.price) return "";
40 | return (
41 | {
44 | if (mode === "USD") {
45 | setMode("ETH");
46 | setDisplay(currentValue);
47 | } else {
48 | setMode("USD");
49 | if (currentValue) {
50 | const usdValue = "" + (parseFloat(currentValue) * props.price).toFixed(2);
51 | setDisplay(usdValue);
52 | } else {
53 | setDisplay(currentValue);
54 | }
55 | }
56 | }}
57 | >
58 | {title}
59 |
60 | );
61 | };
62 |
63 | let prefix;
64 | let addonAfter;
65 | if (mode === "USD") {
66 | prefix = "$";
67 | addonAfter = option("USD 🔀");
68 | } else {
69 | prefix = "Ξ";
70 | addonAfter = option("ETH 🔀");
71 | }
72 |
73 | useEffect(() => {
74 | if (!currentValue) {
75 | setDisplay("");
76 | }
77 | }, [currentValue]);
78 |
79 | return (
80 | {
87 | const newValue = e.target.value;
88 | if (mode === "USD") {
89 | const possibleNewValue = parseFloat(newValue);
90 | if (possibleNewValue) {
91 | const ethValue = possibleNewValue / props.price;
92 | setValue(ethValue);
93 | if (typeof props.onChange === "function") {
94 | props.onChange(ethValue);
95 | }
96 | setDisplay(newValue);
97 | } else {
98 | setDisplay(newValue);
99 | }
100 | } else {
101 | setValue(newValue);
102 | if (typeof props.onChange === "function") {
103 | props.onChange(newValue);
104 | }
105 | setDisplay(newValue);
106 | }
107 | }}
108 | />
109 | );
110 | }
111 |
--------------------------------------------------------------------------------
/src/components/Faucet.jsx:
--------------------------------------------------------------------------------
1 | import { SendOutlined } from "@ant-design/icons";
2 | import { Button, Input, Tooltip } from "antd";
3 | // import { useLookupAddress } from "eth-hooks/dapps/ens";
4 | import React, { useCallback, useState, useEffect } from "react";
5 | import Blockies from "react-blockies";
6 | import { Transactor } from "../helpers";
7 | import Wallet from "./Wallet";
8 |
9 | const { utils } = require("ethers");
10 |
11 | // improved a bit by converting address to ens if it exists
12 | // added option to directly input ens name
13 | // added placeholder option
14 |
15 | /*
16 | ~ What it does? ~
17 |
18 | Displays a local faucet to send ETH to given address, also wallet is provided
19 |
20 | ~ How can I use? ~
21 |
22 |
28 |
29 | ~ Features ~
30 |
31 | - Provide price={price} of ether and convert between USD and ETH in a wallet
32 | - Provide localProvider={localProvider} to be able to send ETH to given address
33 | - Provide ensProvider={mainnetProvider} and your address will be replaced by ENS name
34 | (ex. "0xa870" => "user.eth") or you can enter directly ENS name instead of address
35 | works both in input field & wallet
36 | - Provide placeholder="Send local faucet" value for the input
37 | */
38 |
39 | export default function Faucet(props) {
40 | const [address, setAddress] = useState();
41 | const [faucetAddress, setFaucetAddress] = useState();
42 |
43 | const { price, placeholder, localProvider, ensProvider, onChange } = props;
44 |
45 | useEffect(() => {
46 | const getFaucetAddress = async () => {
47 | if (localProvider) {
48 | const _faucetAddress = await localProvider.listAccounts();
49 | setFaucetAddress(_faucetAddress[0]);
50 | //console.log(_faucetAddress);
51 | }
52 | };
53 | getFaucetAddress();
54 | }, [localProvider]);
55 |
56 | let blockie;
57 | if (address && typeof address.toLowerCase === "function") {
58 | blockie = ;
59 | } else {
60 | blockie =
;
61 | }
62 |
63 | // const ens = useLookupAddress(ensProvider, address);
64 |
65 | const updateAddress = useCallback(
66 | async newValue => {
67 | if (typeof newValue !== "undefined" && utils.isAddress(newValue)) {
68 | let newAddress = newValue;
69 | // if (newAddress.indexOf(".eth") > 0 || newAddress.indexOf(".xyz") > 0) {
70 | // try {
71 | // const possibleAddress = await ensProvider.resolveName(newAddress);
72 | // if (possibleAddress) {
73 | // newAddress = possibleAddress;
74 | // }
75 | // // eslint-disable-next-line no-empty
76 | // } catch (e) { }
77 | // }
78 | setAddress(newAddress);
79 | }
80 | },
81 | [ensProvider, onChange],
82 | );
83 |
84 | const tx = Transactor(localProvider);
85 |
86 | return (
87 |
88 | {
95 | // setAddress(e.target.value);
96 | updateAddress(e.target.value);
97 | }}
98 | suffix={
99 |
100 | {
102 | tx({
103 | to: address,
104 | value: utils.parseEther("0.01"),
105 | });
106 | setAddress("");
107 | }}
108 | shape="circle"
109 | icon={ }
110 | />
111 |
118 |
119 | }
120 | />
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/src/components/GasGauge.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "antd";
2 | import React from "react";
3 |
4 | // added display of 0 instead of NaN if gas price is not provided
5 |
6 | /*
7 | ~ What it does? ~
8 |
9 | Displays gas gauge
10 |
11 | ~ How can I use? ~
12 |
13 |
16 |
17 | ~ Features ~
18 |
19 | - Provide gasPrice={gasPrice} and get current gas gauge
20 | */
21 |
22 | export default function GasGauge(props) {
23 | return (
24 | {
26 | window.open("https://ethgasstation.info/");
27 | }}
28 | size="large"
29 | shape="round"
30 | >
31 |
32 |
33 | ⛽️
34 |
35 |
36 | {typeof props.gasPrice === "undefined" ? 0 : parseInt(props.gasPrice, 10) / 10 ** 9}g
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import { PageHeader } from "antd";
2 | import React from "react";
3 |
4 | // displays a page header
5 |
6 | export default function Header() {
7 | return (
8 |
9 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/L2Bridge.jsx:
--------------------------------------------------------------------------------
1 | import { utils, ethers } from "ethers";
2 | import { Button, Input, Form, Select, InputNumber, Table, Radio } from "antd";
3 | import React, { useState, useEffect } from "react";
4 | import { useContractLoader, useOnBlock } from "eth-hooks";
5 | import { NETWORKS } from "../constants";
6 | import { Transactor } from "../helpers";
7 |
8 | /*
9 | This is a component for bridging between L1 & L2
10 | Currently it supports Testnet deposits for Arbitrum & Optimism
11 |
12 | __ _______ _____
13 | \ \ / /_ _| __ \
14 | \ \ /\ / / | | | |__) |
15 | \ \/ \/ / | | | ___/
16 | \ /\ / _| |_| |
17 | \/ \/ |_____|_|
18 |
19 |
20 | */
21 |
22 | export default function L2ArbitrumBridge({ address, userSigner }) {
23 | const [L1EthBalance, setL1EthBalance] = useState("...");
24 | const [L2EthBalance, setL2EthBalance] = useState("...");
25 | const [L1Provider, setL1Provider] = useState("");
26 | const [L2Provider, setL2Provider] = useState("");
27 | const [rollup, setRollup] = useState("arbitrum");
28 | const [environment, setEnvironment] = useState("test");
29 |
30 | const rollupConfig = {
31 | arbitrum: {
32 | test: { L1: NETWORKS.rinkeby, L2: NETWORKS.rinkebyArbitrum },
33 | main: { L1: NETWORKS.mainnet, L2: NETWORKS.arbitrum },
34 | local: { L1: NETWORKS.localArbitrumL1, L2: NETWORKS.localArbitrum },
35 | },
36 | optimism: {
37 | test: { L1: NETWORKS.kovan, L2: NETWORKS.kovanOptimism },
38 | local: { L1: NETWORKS.localOptimismL1, L2: NETWORKS.localOptimism },
39 | },
40 | };
41 |
42 | const activeConfig = rollupConfig[rollup][environment];
43 |
44 | const selectedChainId =
45 | userSigner && userSigner.provider && userSigner.provider._network && userSigner.provider._network.chainId;
46 |
47 | const tx = Transactor(userSigner);
48 |
49 | useEffect(() => {
50 | async function setProviders() {
51 | const L1 = activeConfig.L1;
52 | const L2 = activeConfig.L2;
53 | setL1Provider(new ethers.providers.StaticJsonRpcProvider(L1.rpcUrl));
54 | setL2Provider(new ethers.providers.StaticJsonRpcProvider(L2.rpcUrl));
55 | setL1EthBalance("...");
56 | setL2EthBalance("...");
57 | }
58 | setProviders();
59 | }, [rollup]);
60 |
61 | const contracts = useContractLoader(userSigner, { externalContracts: L1BridgeMetadata, hardhatContracts: {} });
62 |
63 | useOnBlock(L1Provider, async () => {
64 | console.log(`⛓ A new mainnet block is here: ${L1Provider._lastBlockNumber}`);
65 | const yourL1Balance = await L1Provider.getBalance(address);
66 | setL1EthBalance(yourL1Balance ? ethers.utils.formatEther(yourL1Balance) : "...");
67 | const yourL2Balance = await L2Provider.getBalance(address);
68 | setL2EthBalance(yourL2Balance ? ethers.utils.formatEther(yourL2Balance) : "...");
69 | });
70 |
71 | const { Option } = Select;
72 | const formItemLayout = {
73 | labelCol: {
74 | xs: { span: 24 },
75 | sm: { span: 8 },
76 | },
77 | wrapperCol: {
78 | xs: { span: 24 },
79 | sm: { span: 12 },
80 | },
81 | };
82 | const tailFormItemLayout = {
83 | wrapperCol: {
84 | xs: {
85 | span: 24,
86 | offset: 0,
87 | },
88 | sm: {
89 | span: 12,
90 | offset: 8,
91 | },
92 | },
93 | };
94 |
95 | const columns = [
96 | {
97 | title: "",
98 | dataIndex: "token",
99 | key: "token",
100 | align: "center",
101 | },
102 | {
103 | title: `${activeConfig.L1.name} L1 Balance`,
104 | dataIndex: "l1",
105 | key: "l1",
106 | align: "center",
107 | },
108 | {
109 | title: `${activeConfig.L1.name} ${rollup} Balance`,
110 | dataIndex: "l2",
111 | key: "l2",
112 | align: "center",
113 | },
114 | ];
115 |
116 | const data = [
117 | {
118 | key: "1",
119 | token: "ETH",
120 | l1: "Ξ" + L1EthBalance,
121 | l2: "Ξ" + L2EthBalance,
122 | },
123 | ];
124 |
125 | const [form] = Form.useForm();
126 |
127 | const onAssetChange = value => {
128 | console.log(value);
129 | };
130 |
131 | async function onFinish(values) {
132 | console.log(contracts);
133 | console.log(values.amount.toString());
134 | console.log(rollup);
135 | let newTx;
136 | try {
137 | if (rollup === "arbitrum") {
138 | newTx = await tx(
139 | contracts.Inbox.depositEth(1_300_000, {
140 | value: utils.parseEther(values.amount.toString()),
141 | gasLimit: 300000,
142 | }),
143 | );
144 | } else if (rollup === "optimism") {
145 | newTx = await tx(
146 | contracts.OVM_L1StandardBridge.depositETH(1_300_000, "0x", {
147 | value: utils.parseEther(values.amount.toString()),
148 | }),
149 | );
150 | }
151 | await newTx.wait();
152 | console.log("woop!");
153 | } catch (e) {
154 | console.log(e);
155 | console.log("something went wrong!");
156 | }
157 | }
158 |
159 | const onReset = () => {
160 | form.resetFields();
161 | };
162 |
163 | const wrongNetwork = selectedChainId !== activeConfig.L1.chainId;
164 |
165 | return (
166 |
167 |
168 |
Welcome to the L2 Deposit Bridge!
169 |
{
172 | setRollup(e.target.value);
173 | }}
174 | style={{ marginBottom: 10 }}
175 | >
176 | Arbitrum
177 | Optimism
178 |
179 |
180 |
181 |
182 |
198 |
199 | ETH
200 |
201 | ERC-20
202 |
203 |
204 |
205 |
206 |
207 |
208 |
217 |
218 |
219 |
220 |
221 | {wrongNetwork ? `Switch wallet to ${activeConfig.L1.name}` : "Deposit"}
222 |
223 |
224 |
225 |
226 |
227 | );
228 | }
229 |
230 | // Arbitrum Inbox https://rinkeby.etherscan.io/address/0xa157dc79ca26d69c3b1282d03ec42bdee2790a8f#code
231 | const ArbitrumInboxABI = [
232 | {
233 | anonymous: false,
234 | inputs: [
235 | { indexed: true, internalType: "uint256", name: "messageNum", type: "uint256" },
236 | { indexed: false, internalType: "bytes", name: "data", type: "bytes" },
237 | ],
238 | name: "InboxMessageDelivered",
239 | type: "event",
240 | },
241 | {
242 | anonymous: false,
243 | inputs: [{ indexed: true, internalType: "uint256", name: "messageNum", type: "uint256" }],
244 | name: "InboxMessageDeliveredFromOrigin",
245 | type: "event",
246 | },
247 | {
248 | anonymous: false,
249 | inputs: [{ indexed: false, internalType: "address", name: "newSource", type: "address" }],
250 | name: "WhitelistSourceUpdated",
251 | type: "event",
252 | },
253 | {
254 | inputs: [],
255 | name: "bridge",
256 | outputs: [{ internalType: "contract IBridge", name: "", type: "address" }],
257 | stateMutability: "view",
258 | type: "function",
259 | },
260 | {
261 | inputs: [
262 | { internalType: "address", name: "destAddr", type: "address" },
263 | { internalType: "uint256", name: "l2CallValue", type: "uint256" },
264 | { internalType: "uint256", name: "maxSubmissionCost", type: "uint256" },
265 | { internalType: "address", name: "excessFeeRefundAddress", type: "address" },
266 | { internalType: "address", name: "callValueRefundAddress", type: "address" },
267 | { internalType: "uint256", name: "maxGas", type: "uint256" },
268 | { internalType: "uint256", name: "gasPriceBid", type: "uint256" },
269 | { internalType: "bytes", name: "data", type: "bytes" },
270 | ],
271 | name: "createRetryableTicket",
272 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
273 | stateMutability: "payable",
274 | type: "function",
275 | },
276 | {
277 | inputs: [{ internalType: "uint256", name: "maxSubmissionCost", type: "uint256" }],
278 | name: "depositEth",
279 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
280 | stateMutability: "payable",
281 | type: "function",
282 | },
283 | {
284 | inputs: [
285 | { internalType: "contract IBridge", name: "_bridge", type: "address" },
286 | { internalType: "address", name: "_whitelist", type: "address" },
287 | ],
288 | name: "initialize",
289 | outputs: [],
290 | stateMutability: "nonpayable",
291 | type: "function",
292 | },
293 | {
294 | inputs: [],
295 | name: "isMaster",
296 | outputs: [{ internalType: "bool", name: "", type: "bool" }],
297 | stateMutability: "view",
298 | type: "function",
299 | },
300 | {
301 | inputs: [
302 | { internalType: "uint256", name: "maxGas", type: "uint256" },
303 | { internalType: "uint256", name: "gasPriceBid", type: "uint256" },
304 | { internalType: "address", name: "destAddr", type: "address" },
305 | { internalType: "uint256", name: "amount", type: "uint256" },
306 | { internalType: "bytes", name: "data", type: "bytes" },
307 | ],
308 | name: "sendContractTransaction",
309 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
310 | stateMutability: "nonpayable",
311 | type: "function",
312 | },
313 | {
314 | inputs: [
315 | { internalType: "uint256", name: "maxGas", type: "uint256" },
316 | { internalType: "uint256", name: "gasPriceBid", type: "uint256" },
317 | { internalType: "address", name: "destAddr", type: "address" },
318 | { internalType: "bytes", name: "data", type: "bytes" },
319 | ],
320 | name: "sendL1FundedContractTransaction",
321 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
322 | stateMutability: "payable",
323 | type: "function",
324 | },
325 | {
326 | inputs: [
327 | { internalType: "uint256", name: "maxGas", type: "uint256" },
328 | { internalType: "uint256", name: "gasPriceBid", type: "uint256" },
329 | { internalType: "uint256", name: "nonce", type: "uint256" },
330 | { internalType: "address", name: "destAddr", type: "address" },
331 | { internalType: "bytes", name: "data", type: "bytes" },
332 | ],
333 | name: "sendL1FundedUnsignedTransaction",
334 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
335 | stateMutability: "payable",
336 | type: "function",
337 | },
338 | {
339 | inputs: [{ internalType: "bytes", name: "messageData", type: "bytes" }],
340 | name: "sendL2Message",
341 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
342 | stateMutability: "nonpayable",
343 | type: "function",
344 | },
345 | {
346 | inputs: [{ internalType: "bytes", name: "messageData", type: "bytes" }],
347 | name: "sendL2MessageFromOrigin",
348 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
349 | stateMutability: "nonpayable",
350 | type: "function",
351 | },
352 | {
353 | inputs: [
354 | { internalType: "uint256", name: "maxGas", type: "uint256" },
355 | { internalType: "uint256", name: "gasPriceBid", type: "uint256" },
356 | { internalType: "uint256", name: "nonce", type: "uint256" },
357 | { internalType: "address", name: "destAddr", type: "address" },
358 | { internalType: "uint256", name: "amount", type: "uint256" },
359 | { internalType: "bytes", name: "data", type: "bytes" },
360 | ],
361 | name: "sendUnsignedTransaction",
362 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
363 | stateMutability: "nonpayable",
364 | type: "function",
365 | },
366 | {
367 | inputs: [{ internalType: "address", name: "newSource", type: "address" }],
368 | name: "updateWhitelistSource",
369 | outputs: [],
370 | stateMutability: "nonpayable",
371 | type: "function",
372 | },
373 | {
374 | inputs: [],
375 | name: "whitelist",
376 | outputs: [{ internalType: "address", name: "", type: "address" }],
377 | stateMutability: "view",
378 | type: "function",
379 | },
380 | ];
381 |
382 | // https://github.com/ethereum-optimism/optimism/blob/2bd49730fa8d2c10953873f0ccc792198a49d5c9/packages/contracts/contracts/optimistic-ethereum/iOVM/bridge/tokens/iOVM_L1StandardBridge.sol
383 | const OVM_L1StandardBridgeABI = [
384 | "function depositETH(uint32 _l2Gas,bytes calldata _data) external payable",
385 | "function depositETHTo(address _to,uint32 _l2Gas,bytes calldata _data) external payable",
386 | "function finalizeETHWithdrawal (address _from,address _to,uint _amount,bytes calldata _data) external",
387 | ];
388 |
389 | const L1BridgeMetadata = {
390 | // Arbitrium Contract's
391 | 44010: {
392 | contracts: {
393 | Inbox: {
394 | address: "0xA4d796Ad4e79aFB703340a596AEd88f8a5924183",
395 | abi: ArbitrumInboxABI,
396 | },
397 | },
398 | },
399 | 4: {
400 | contracts: {
401 | Inbox: {
402 | address: "0x578bade599406a8fe3d24fd7f7211c0911f5b29e",
403 | abi: ArbitrumInboxABI,
404 | },
405 | },
406 | },
407 | // Optimism Contract's
408 | 31337: {
409 | contracts: {
410 | OVM_L1StandardBridge: {
411 | address: "0x998abeb3E57409262aE5b751f60747921B33613E",
412 | abi: OVM_L1StandardBridgeABI,
413 | },
414 | },
415 | },
416 | 42: {
417 | contracts: {
418 | OVM_L1StandardBridge: {
419 | address: "0x22F24361D548e5FaAfb36d1437839f080363982B",
420 | abi: OVM_L1StandardBridgeABI,
421 | },
422 | },
423 | },
424 | };
425 |
--------------------------------------------------------------------------------
/src/components/NewRpc.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button } from "antd";
3 |
4 | let counter = 0;
5 |
6 | export default function AddRPC({ chainName, rpcUrl }) {
7 | return (
8 |
11 | );
12 | }
13 |
14 | // make exportable //
15 | const AddCustomRPCButton = ({ chainName, rpcUrl }) => {
16 | const handleAddCustomRPC = async () => {
17 | if (!window.ethereum || !window.ethereum.request) {
18 | console.error("MetaMask Ethereum provider is not available");
19 | return;
20 | }
21 |
22 | try {
23 | await window.ethereum.request({
24 | method: "wallet_addEthereumChain",
25 | params: [
26 | {
27 | chainId: "0x1", // Replace with the desired Chain ID
28 | chainName: chainName + " " + counter, // Replace with the desired network name
29 | nativeCurrency: {
30 | name: "ETH",
31 | symbol: "ETH",
32 | decimals: 18,
33 | },
34 | rpcUrls: [rpcUrl.toString()], // Replace with the desired RPC URL
35 | blockExplorerUrls: ["https://etherscan.io"], // Replace with the desired block explorer URL
36 | },
37 | ],
38 | });
39 | console.log("Custom RPC network added to MetaMask");
40 | } catch (error) {
41 | console.error("Failed to add custom RPC network to MetaMask:", error);
42 | }
43 | };
44 |
45 | return (
46 |
47 | Click to add your personal Flashbots RPC to MetaMask
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/components/Provider.jsx:
--------------------------------------------------------------------------------
1 | import { Badge, Button } from "antd";
2 | import { useBlockNumber, usePoller } from "eth-hooks";
3 | import React, { useState } from "react";
4 | // import { WalletOutlined } from '@ant-design/icons';
5 | import Address from "./Address";
6 |
7 | export default function Provider(props) {
8 | const [showMore, setShowMore] = useState(false);
9 | const [status, setStatus] = useState("processing");
10 | const [network, setNetwork] = useState();
11 | const [signer, setSigner] = useState();
12 | const [address, setAddress] = useState();
13 |
14 | const blockNumber = useBlockNumber(props.provider);
15 |
16 | usePoller(async () => {
17 | if (props.provider && typeof props.provider.getNetwork === "function") {
18 | try {
19 | const newNetwork = await props.provider.getNetwork();
20 | setNetwork(newNetwork);
21 | if (newNetwork.chainId > 0) {
22 | setStatus("success");
23 | } else {
24 | setStatus("warning");
25 | }
26 | } catch (e) {
27 | console.log(e);
28 | setStatus("processing");
29 | }
30 | try {
31 | const newSigner = await props.provider.getSigner();
32 | setSigner(newSigner);
33 | const newAddress = await newSigner.getAddress();
34 | setAddress(newAddress);
35 | // eslint-disable-next-line no-empty
36 | } catch (e) {}
37 | }
38 | }, 1377);
39 |
40 | if (
41 | typeof props.provider === "undefined" ||
42 | typeof props.provider.getNetwork !== "function" ||
43 | !network ||
44 | !network.chainId
45 | ) {
46 | return (
47 | {
51 | setShowMore(!showMore);
52 | }}
53 | >
54 | {props.name}
55 |
56 | );
57 | }
58 |
59 | let showExtra = "";
60 | if (showMore) {
61 | showExtra = (
62 |
63 |
64 | id:
65 | {network ? network.chainId : ""}
66 |
67 |
68 | name:
69 | {network ? network.name : ""}
70 |
71 |
72 | );
73 | }
74 |
75 | let showWallet = "";
76 | if (typeof signer !== "undefined" && address) {
77 | showWallet = (
78 |
79 |
80 |
81 |
82 |
83 | );
84 | }
85 |
86 | return (
87 | {
91 | setShowMore(!showMore);
92 | }}
93 | >
94 | {props.name} {showWallet} #{blockNumber} {showExtra}
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/src/components/Ramp.jsx:
--------------------------------------------------------------------------------
1 | import { DollarCircleOutlined } from "@ant-design/icons";
2 | import { RampInstantSDK } from "@ramp-network/ramp-instant-sdk";
3 | import { Button, Divider, Modal } from "antd";
4 | import React, { useState } from "react";
5 |
6 | // added display of 0 if price={price} is not provided
7 |
8 | /*
9 | ~ What it does? ~
10 |
11 | Displays current ETH price and gives options to buy ETH through Wyre/Ramp/Coinbase
12 | or get through Rinkeby/Ropsten/Kovan/Goerli
13 |
14 | ~ How can I use? ~
15 |
16 |
20 |
21 | ~ Features ~
22 |
23 | - Ramp opens directly in the application, component uses RampInstantSDK
24 | - Provide price={price} and current ETH price will be displayed
25 | - Provide address={address} and your address will be pasted into Wyre/Ramp instantly
26 | */
27 |
28 | export default function Ramp(props) {
29 | const [modalUp, setModalUp] = useState("down");
30 |
31 | const type = "default";
32 |
33 | const allFaucets = [];
34 | for (const n in props.networks) {
35 | if (props.networks[n].chainId !== 31337 && props.networks[n].chainId !== 1) {
36 | allFaucets.push(
37 |
38 | {
44 | window.open(props.networks[n].faucet);
45 | }}
46 | >
47 | {props.networks[n].name}
48 |
49 |
,
50 | );
51 | }
52 | }
53 |
54 | return (
55 |
56 |
{
60 | setModalUp("up");
61 | }}
62 | >
63 | {" "}
64 | {typeof props.price === "undefined" ? 0 : props.price.toFixed(2)}
65 |
66 |
{
70 | setModalUp("down");
71 | }}
72 | footer={[
73 | {
76 | setModalUp("down");
77 | }}
78 | >
79 | cancel
80 | ,
81 | ]}
82 | >
83 |
84 | {
89 | window.open("https://pay.sendwyre.com/purchase?destCurrency=ETH&sourceAmount=25&dest=" + props.address);
90 | }}
91 | >
92 |
93 |
94 | 🇺🇸
95 |
96 |
97 | Wyre
98 |
99 |
100 |
101 | {" "}
102 | {
107 | new RampInstantSDK({
108 | hostAppName: "scaffold-eth",
109 | hostLogoUrl: "https://scaffoldeth.io/scaffold-eth.png",
110 | swapAmount: "100000000000000000", // 0.1 ETH in wei ?
111 | swapAsset: "ETH",
112 | userAddress: props.address,
113 | })
114 | .on("*", event => console.log(event))
115 | .show();
116 | }}
117 | >
118 |
119 |
120 | 🇬🇧
121 |
122 |
123 | Ramp
124 |
125 |
126 |
127 |
128 | {
133 | window.open("https://www.coinbase.com/buy-ethereum");
134 | }}
135 | >
136 |
137 | 🏦
138 |
139 | Coinbase
140 |
141 |
142 |
143 |
144 |
145 | Testnet ETH
146 |
147 | {allFaucets}
148 |
149 |
150 | );
151 | }
152 |
--------------------------------------------------------------------------------
/src/components/Swap.jsx:
--------------------------------------------------------------------------------
1 | import { RetweetOutlined, SettingOutlined } from "@ant-design/icons";
2 | import { ChainId, Fetcher, Percent, Token, TokenAmount, Trade, WETH } from "@uniswap/sdk";
3 | import { abi as IUniswapV2Router02ABI } from "@uniswap/v2-periphery/build/IUniswapV2Router02.json";
4 | import {
5 | Button,
6 | Card,
7 | Descriptions,
8 | Divider,
9 | Drawer,
10 | InputNumber,
11 | Modal,
12 | notification,
13 | Row,
14 | Select,
15 | Space,
16 | Tooltip,
17 | Typography,
18 | } from "antd";
19 | import { useBlockNumber, usePoller } from "eth-hooks";
20 | import { ethers } from "ethers";
21 | import React, { useEffect, useState } from "react";
22 | import { useDebounce } from "../hooks";
23 |
24 | const { Option } = Select;
25 | const { Text } = Typography;
26 |
27 | export const ROUTER_ADDRESS = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D";
28 |
29 | export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
30 |
31 | const erc20Abi = [
32 | "function balanceOf(address owner) view returns (uint256)",
33 | "function approve(address _spender, uint256 _value) public returns (bool success)",
34 | "function allowance(address _owner, address _spender) public view returns (uint256 remaining)",
35 | ];
36 |
37 | const makeCall = async (callName, contract, args, metadata = {}) => {
38 | if (contract[callName]) {
39 | let result;
40 | if (args) {
41 | result = await contract[callName](...args, metadata);
42 | } else {
43 | result = await contract[callName]();
44 | }
45 | return result;
46 | }
47 | return undefined;
48 | console.log("no call of that name!");
49 | };
50 |
51 | const defaultToken = "ETH";
52 | const defaultTokenOut = "DAI";
53 | const defaultSlippage = "0.5";
54 | const defaultTimeLimit = 60 * 10;
55 |
56 | const tokenListToObject = array =>
57 | array.reduce((obj, item) => {
58 | obj[item.symbol] = new Token(item.chainId, item.address, item.decimals, item.symbol, item.name);
59 | return obj;
60 | }, {});
61 |
62 | function Swap({ selectedProvider, tokenListURI }) {
63 | const [tokenIn, setTokenIn] = useState(defaultToken);
64 | const [tokenOut, setTokenOut] = useState(defaultTokenOut);
65 | const [exact, setExact] = useState();
66 | const [amountIn, setAmountIn] = useState();
67 | const [amountInMax, setAmountInMax] = useState();
68 | const [amountOut, setAmountOut] = useState();
69 | const [amountOutMin, setAmountOutMin] = useState();
70 | const [trades, setTrades] = useState();
71 | const [routerAllowance, setRouterAllowance] = useState();
72 | const [balanceIn, setBalanceIn] = useState();
73 | const [balanceOut, setBalanceOut] = useState();
74 | const [slippageTolerance, setSlippageTolerance] = useState(
75 | new Percent(Math.round(defaultSlippage * 100).toString(), "10000"),
76 | );
77 | const [timeLimit, setTimeLimit] = useState(defaultTimeLimit);
78 | const [swapping, setSwapping] = useState(false);
79 | const [approving, setApproving] = useState(false);
80 | const [settingsVisible, setSettingsVisible] = useState(false);
81 | const [swapModalVisible, setSwapModalVisible] = useState(false);
82 |
83 | const [tokenList, setTokenList] = useState([]);
84 |
85 | const [tokens, setTokens] = useState();
86 |
87 | const [invertPrice, setInvertPrice] = useState(false);
88 |
89 | const blockNumber = useBlockNumber(selectedProvider, 3000);
90 |
91 | const signer = selectedProvider.getSigner();
92 | const routerContract = new ethers.Contract(ROUTER_ADDRESS, IUniswapV2Router02ABI, signer);
93 |
94 | const _tokenListUri = tokenListURI || "https://gateway.ipfs.io/ipns/tokens.uniswap.org";
95 |
96 | const debouncedAmountIn = useDebounce(amountIn, 500);
97 | const debouncedAmountOut = useDebounce(amountOut, 500);
98 |
99 | const activeChainId = process.env.REACT_APP_NETWORK === "kovan" ? ChainId.KOVAN : ChainId.MAINNET;
100 |
101 | useEffect(() => {
102 | const getTokenList = async () => {
103 | console.log(_tokenListUri);
104 | try {
105 | const tokenListResponse = await fetch(_tokenListUri);
106 | const tokenListJson = await tokenListResponse.json();
107 | const filteredTokens = tokenListJson.tokens.filter(function (t) {
108 | return t.chainId === activeChainId;
109 | });
110 | const ethToken = WETH[activeChainId];
111 | ethToken.name = "Ethereum";
112 | ethToken.symbol = "ETH";
113 | ethToken.logoURI =
114 | "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png";
115 | const _tokenList = [ethToken, ...filteredTokens];
116 | setTokenList(_tokenList);
117 | const _tokens = tokenListToObject(_tokenList);
118 | setTokens(_tokens);
119 | } catch (e) {
120 | console.log(e);
121 | }
122 | };
123 | getTokenList();
124 | }, [tokenListURI]);
125 |
126 | const getTrades = async () => {
127 | if (tokenIn && tokenOut && (amountIn || amountOut)) {
128 | const pairs = arr => arr.map((v, i) => arr.slice(i + 1).map(w => [v, w])).flat();
129 |
130 | const baseTokens = tokenList
131 | .filter(function (t) {
132 | return ["DAI", "USDC", "USDT", "COMP", "ETH", "MKR", "LINK", tokenIn, tokenOut].includes(t.symbol);
133 | })
134 | .map(el => {
135 | return new Token(el.chainId, el.address, el.decimals, el.symbol, el.name);
136 | });
137 |
138 | const listOfPairwiseTokens = pairs(baseTokens);
139 |
140 | const getPairs = async list => {
141 | const listOfPromises = list.map(item => Fetcher.fetchPairData(item[0], item[1], selectedProvider));
142 | return Promise.all(listOfPromises.map(p => p.catch(() => undefined)));
143 | };
144 |
145 | const listOfPairs = await getPairs(listOfPairwiseTokens);
146 |
147 | let bestTrade;
148 |
149 | if (exact === "in") {
150 | setAmountInMax();
151 | bestTrade = Trade.bestTradeExactIn(
152 | listOfPairs.filter(item => item),
153 | new TokenAmount(tokens[tokenIn], ethers.utils.parseUnits(amountIn.toString(), tokens[tokenIn].decimals)),
154 | tokens[tokenOut],
155 | { maxNumResults: 3, maxHops: 1 },
156 | );
157 | if (bestTrade[0]) {
158 | setAmountOut(bestTrade[0].outputAmount.toSignificant(6));
159 | } else {
160 | setAmountOut();
161 | }
162 | } else if (exact === "out") {
163 | setAmountOutMin();
164 | bestTrade = Trade.bestTradeExactOut(
165 | listOfPairs.filter(item => item),
166 | tokens[tokenIn],
167 | new TokenAmount(tokens[tokenOut], ethers.utils.parseUnits(amountOut.toString(), tokens[tokenOut].decimals)),
168 | { maxNumResults: 3, maxHops: 1 },
169 | );
170 | if (bestTrade[0]) {
171 | setAmountIn(bestTrade[0].inputAmount.toSignificant(6));
172 | } else {
173 | setAmountIn();
174 | }
175 | }
176 |
177 | setTrades(bestTrade);
178 |
179 | console.log(bestTrade);
180 | }
181 | };
182 |
183 | useEffect(() => {
184 | getTrades();
185 | }, [tokenIn, tokenOut, debouncedAmountIn, debouncedAmountOut, slippageTolerance, selectedProvider]);
186 |
187 | useEffect(() => {
188 | if (trades && trades[0]) {
189 | if (exact === "in") {
190 | setAmountOutMin(trades[0].minimumAmountOut(slippageTolerance));
191 | } else if (exact === "out") {
192 | setAmountInMax(trades[0].maximumAmountIn(slippageTolerance));
193 | }
194 | }
195 | }, [slippageTolerance, amountIn, amountOut, trades]);
196 |
197 | const getBalance = async (_token, _account, _contract) => {
198 | let newBalance;
199 | if (_token === "ETH") {
200 | newBalance = await selectedProvider.getBalance(_account);
201 | } else {
202 | newBalance = await makeCall("balanceOf", _contract, [_account]);
203 | }
204 | return newBalance;
205 | };
206 |
207 | const getAccountInfo = async () => {
208 | if (tokens) {
209 | const accountList = await selectedProvider.listAccounts();
210 |
211 | if (tokenIn) {
212 | const tempContractIn = new ethers.Contract(tokens[tokenIn].address, erc20Abi, selectedProvider);
213 | const newBalanceIn = await getBalance(tokenIn, accountList[0], tempContractIn);
214 | setBalanceIn(newBalanceIn);
215 |
216 | let allowance;
217 |
218 | if (tokenIn === "ETH") {
219 | setRouterAllowance();
220 | } else {
221 | allowance = await makeCall("allowance", tempContractIn, [accountList[0], ROUTER_ADDRESS]);
222 | setRouterAllowance(allowance);
223 | }
224 | }
225 |
226 | if (tokenOut) {
227 | const tempContractOut = new ethers.Contract(tokens[tokenOut].address, erc20Abi, selectedProvider);
228 | const newBalanceOut = await getBalance(tokenOut, accountList[0], tempContractOut);
229 | setBalanceOut(newBalanceOut);
230 | }
231 | }
232 | };
233 |
234 | usePoller(getAccountInfo, 6000);
235 |
236 | const route = trades
237 | ? trades.length > 0
238 | ? trades[0].route.path.map(function (item) {
239 | return item.symbol;
240 | })
241 | : []
242 | : [];
243 |
244 | const updateRouterAllowance = async newAllowance => {
245 | setApproving(true);
246 | try {
247 | const tempContract = new ethers.Contract(tokens[tokenIn].address, erc20Abi, signer);
248 | const result = await makeCall("approve", tempContract, [ROUTER_ADDRESS, newAllowance]);
249 | console.log(result);
250 | setApproving(false);
251 | return true;
252 | } catch (e) {
253 | notification.open({
254 | message: "Approval unsuccessful",
255 | description: `Error: ${e.message}`,
256 | });
257 | }
258 | };
259 |
260 | const approveRouter = async () => {
261 | const approvalAmount =
262 | exact === "in"
263 | ? ethers.utils.hexlify(ethers.utils.parseUnits(amountIn.toString(), tokens[tokenIn].decimals))
264 | : amountInMax.raw.toString();
265 | console.log(approvalAmount);
266 | const approval = updateRouterAllowance(approvalAmount);
267 | if (approval) {
268 | notification.open({
269 | message: "Token transfer approved",
270 | description: `You can now swap up to ${amountIn} ${tokenIn}`,
271 | });
272 | }
273 | };
274 |
275 | const removeRouterAllowance = async () => {
276 | const approvalAmount = ethers.utils.hexlify(0);
277 | console.log(approvalAmount);
278 | const removal = updateRouterAllowance(approvalAmount);
279 | if (removal) {
280 | notification.open({
281 | message: "Token approval removed",
282 | description: `The router is no longer approved for ${tokenIn}`,
283 | });
284 | }
285 | };
286 |
287 | const executeSwap = async () => {
288 | setSwapping(true);
289 | try {
290 | let args;
291 | const metadata = {};
292 |
293 | let call;
294 | const deadline = Math.floor(Date.now() / 1000) + timeLimit;
295 | const path = trades[0].route.path.map(function (item) {
296 | return item.address;
297 | });
298 | console.log(path);
299 | const accountList = await selectedProvider.listAccounts();
300 | const address = accountList[0];
301 |
302 | if (exact === "in") {
303 | const _amountIn = ethers.utils.hexlify(ethers.utils.parseUnits(amountIn.toString(), tokens[tokenIn].decimals));
304 | const _amountOutMin = ethers.utils.hexlify(ethers.BigNumber.from(amountOutMin.raw.toString()));
305 | if (tokenIn === "ETH") {
306 | call = "swapExactETHForTokens";
307 | args = [_amountOutMin, path, address, deadline];
308 | metadata.value = _amountIn;
309 | } else {
310 | call = tokenOut === "ETH" ? "swapExactTokensForETH" : "swapExactTokensForTokens";
311 | args = [_amountIn, _amountOutMin, path, address, deadline];
312 | }
313 | } else if (exact === "out") {
314 | const _amountOut = ethers.utils.hexlify(
315 | ethers.utils.parseUnits(amountOut.toString(), tokens[tokenOut].decimals),
316 | );
317 | const _amountInMax = ethers.utils.hexlify(ethers.BigNumber.from(amountInMax.raw.toString()));
318 | if (tokenIn === "ETH") {
319 | call = "swapETHForExactTokens";
320 | args = [_amountOut, path, address, deadline];
321 | metadata.value = _amountInMax;
322 | } else {
323 | call = tokenOut === "ETH" ? "swapTokensForExactETH" : "swapTokensForExactTokens";
324 | args = [_amountOut, _amountInMax, path, address, deadline];
325 | }
326 | }
327 | console.log(call, args, metadata);
328 | const result = await makeCall(call, routerContract, args, metadata);
329 | console.log(result);
330 | notification.open({
331 | message: "Swap complete 🦄",
332 | description: (
333 | <>
334 | {`Swapped ${tokenIn} for ${tokenOut}, transaction: `}
335 | {result.hash}
336 | >
337 | ),
338 | });
339 | setSwapping(false);
340 | } catch (e) {
341 | console.log(e);
342 | setSwapping(false);
343 | notification.open({
344 | message: "Swap unsuccessful",
345 | description: `Error: ${e.message}`,
346 | });
347 | }
348 | };
349 |
350 | const showSwapModal = () => {
351 | setSwapModalVisible(true);
352 | };
353 |
354 | const handleSwapModalOk = () => {
355 | setSwapModalVisible(false);
356 | executeSwap();
357 | };
358 |
359 | const handleSwapModalCancel = () => {
360 | setSwapModalVisible(false);
361 | };
362 |
363 | const insufficientBalance = balanceIn
364 | ? parseFloat(ethers.utils.formatUnits(balanceIn, tokens[tokenIn].decimals)) < amountIn
365 | : null;
366 | const inputIsToken = tokenIn !== "ETH";
367 | const insufficientAllowance = !inputIsToken
368 | ? false
369 | : routerAllowance
370 | ? parseFloat(ethers.utils.formatUnits(routerAllowance, tokens[tokenIn].decimals)) < amountIn
371 | : null;
372 | const formattedBalanceIn = balanceIn
373 | ? parseFloat(ethers.utils.formatUnits(balanceIn, tokens[tokenIn].decimals)).toPrecision(6)
374 | : null;
375 | const formattedBalanceOut = balanceOut
376 | ? parseFloat(ethers.utils.formatUnits(balanceOut, tokens[tokenOut].decimals)).toPrecision(6)
377 | : null;
378 |
379 | const metaIn =
380 | tokens && tokenList && tokenIn
381 | ? tokenList.filter(function (t) {
382 | return t.address === tokens[tokenIn].address;
383 | })[0]
384 | : null;
385 | const metaOut =
386 | tokens && tokenList && tokenOut
387 | ? tokenList.filter(function (t) {
388 | return t.address === tokens[tokenOut].address;
389 | })[0]
390 | : null;
391 |
392 | const cleanIpfsURI = uri => {
393 | try {
394 | return uri.replace("ipfs://", "https://ipfs.io/ipfs/");
395 | } catch (e) {
396 | console.log(e, uri);
397 | return uri;
398 | }
399 | };
400 |
401 | const logoIn = metaIn ? cleanIpfsURI(metaIn.logoURI) : null;
402 | const logoOut = metaOut ? cleanIpfsURI(metaOut.logoURI) : null;
403 |
404 | const rawPrice = trades && trades[0] ? trades[0].executionPrice : null;
405 | const price = rawPrice ? rawPrice.toSignificant(7) : null;
406 | const priceDescription = rawPrice
407 | ? invertPrice
408 | ? `${rawPrice.invert().toSignificant(7)} ${tokenIn} per ${tokenOut}`
409 | : `${price} ${tokenOut} per ${tokenIn}`
410 | : null;
411 |
412 | const priceWidget = (
413 |
414 | {priceDescription}
415 | {
418 | setInvertPrice(!invertPrice);
419 | }}
420 | >
421 |
422 |
423 |
424 | );
425 |
426 | const swapModal = (
427 |
428 |
429 |
430 |
431 | {amountIn}
432 | {tokenIn}
433 |
434 |
435 |
436 | ↓
437 |
438 |
439 |
440 |
441 | {amountOut}
442 | {tokenOut}
443 |
444 |
445 |
446 | {priceWidget}
447 |
448 | {trades && ((amountOutMin && exact === "in") || (amountInMax && exact === "out"))
449 | ? exact === "in"
450 | ? `Output is estimated. You will receive at least ${amountOutMin.toSignificant(
451 | 6,
452 | )} ${tokenOut} or the transaction will revert.`
453 | : `Input is estimated. You will sell at most ${amountInMax.toSignificant(
454 | 6,
455 | )} ${tokenIn} or the transaction will revert.`
456 | : null}
457 |
458 |
459 | );
460 |
461 | return (
462 |
465 |
466 | Uniswapper
467 |
468 | }
469 | extra={
470 | {
473 | setSettingsVisible(true);
474 | }}
475 | >
476 |
477 |
478 | }
479 | >
480 |
481 |
482 |
488 |
489 | {
492 | setAmountOut();
493 | setAmountIn(ethers.utils.formatUnits(balanceIn, tokens[tokenIn].decimals));
494 | setAmountOutMin();
495 | setAmountInMax();
496 | setExact("in");
497 | }}
498 | >
499 | {formattedBalanceIn}
500 |
501 | >
502 | }
503 | style={{ width: 400, textAlign: "left" }}
504 | >
505 | {
511 | setAmountOut();
512 | setTrades();
513 | setAmountIn(e);
514 | setExact("in");
515 | }}
516 | />
517 | {
525 | console.log(value);
526 | if (value === tokenOut) {
527 | console.log("switch!", tokenIn);
528 | setTokenOut(tokenIn);
529 | setAmountOut(amountIn);
530 | setBalanceOut(balanceIn);
531 | }
532 | setTokenIn(value);
533 | setTrades();
534 | setAmountIn();
535 | setExact("out");
536 | setBalanceIn();
537 | }}
538 | filterOption={(input, option) => option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
539 | optionFilterProp="children"
540 | >
541 | {tokenList.map(token => (
542 |
543 | {token.symbol}
544 |
545 | ))}
546 |
547 |
548 |
549 |
550 | ")}>
551 | ↓
552 |
553 |
554 |
555 |
561 |
562 | {formattedBalanceOut}
563 | >
564 | }
565 | style={{ width: 400, textAlign: "left" }}
566 | >
567 | {
573 | setAmountOut(e);
574 | setAmountIn();
575 | setTrades();
576 | setExact("out");
577 | }}
578 | />
579 | {
586 | console.log(value, tokenIn, tokenOut);
587 | if (value === tokenIn) {
588 | console.log("switch!", tokenOut);
589 | setTokenIn(tokenOut);
590 | setAmountIn(amountOut);
591 | setBalanceIn(balanceOut);
592 | }
593 | setTokenOut(value);
594 | setExact("in");
595 | setAmountOut();
596 | setTrades();
597 | setBalanceOut();
598 | }}
599 | filterOption={(input, option) => option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
600 | optionFilterProp="children"
601 | >
602 | {tokenList.map(token => (
603 |
604 | {token.symbol}
605 |
606 | ))}
607 |
608 |
609 |
610 |
611 | {priceDescription ? priceWidget : null}
612 |
613 |
614 |
615 | {inputIsToken ? (
616 |
617 | {!insufficientAllowance && amountIn && amountOut ? "Approved" : "Approve"}
618 |
619 | ) : null}
620 |
626 | {insufficientBalance ? "Insufficient balance" : "Swap!"}
627 |
628 | {swapModal}
629 |
630 |
631 |
632 | {
635 | setSettingsVisible(false);
636 | }}
637 | width={500}
638 | >
639 |
640 | {blockNumber}
641 |
642 |
643 | {routerAllowance ? ethers.utils.formatUnits(routerAllowance, tokens[tokenIn].decimals) : null}
644 | {routerAllowance > 0 ? Remove Allowance : null}
645 |
646 |
647 | {route.join("->")}
648 | {exact}
649 |
650 | {trades ? (trades.length > 0 ? trades[0].executionPrice.toSignificant(6) : null) : null}
651 |
652 |
653 | {trades ? (trades.length > 0 ? trades[0].nextMidPrice.toSignificant(6) : null) : null}
654 |
655 |
656 | {trades ? (trades.length > 0 ? trades[0].priceImpact.toSignificant(6) : null) : null}
657 |
658 |
659 | `${value}%`}
665 | parser={value => value.replace("%", "")}
666 | onChange={value => {
667 | console.log(value);
668 |
669 | const slippagePercent = new Percent(Math.round(value * 100).toString(), "10000");
670 | setSlippageTolerance(slippagePercent);
671 | }}
672 | />
673 |
674 | {amountInMax ? amountInMax.toExact() : null}
675 | {amountOutMin ? amountOutMin.toExact() : null}
676 |
677 | {
682 | console.log(value);
683 | setTimeLimit(value);
684 | }}
685 | />
686 |
687 |
688 |
689 |
690 | );
691 | }
692 |
693 | export default Swap;
694 |
--------------------------------------------------------------------------------
/src/components/ThemeSwitch.jsx:
--------------------------------------------------------------------------------
1 | import { Switch } from "antd";
2 | import React, { useEffect, useState } from "react";
3 | import { useThemeSwitcher } from "react-css-theme-switcher";
4 |
5 | export default function ThemeSwitcher() {
6 | const theme = window.localStorage.getItem("theme");
7 | const [isDarkMode, setIsDarkMode] = useState(!(!theme || theme === "light"));
8 | const { switcher, currentTheme, status, themes } = useThemeSwitcher();
9 |
10 | useEffect(() => {
11 | window.localStorage.setItem("theme", currentTheme);
12 | }, [currentTheme]);
13 |
14 | const toggleTheme = isChecked => {
15 | setIsDarkMode(isChecked);
16 | switcher({ theme: isChecked ? themes.dark : themes.light });
17 | };
18 |
19 | // Avoid theme change flicker
20 | // if (status === "loading") {
21 | // return null;
22 | // }
23 |
24 | return (
25 |
26 | {currentTheme === "light" ? "☀️" : "🌜"}
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/Timeline.jsx:
--------------------------------------------------------------------------------
1 | import { DownloadOutlined, EditOutlined, SendOutlined } from "@ant-design/icons";
2 | import { Timeline, Typography } from "antd";
3 | import React from "react";
4 | import Blockies from "react-blockies";
5 |
6 | const { Text } = Typography;
7 |
8 | // displays a timeline for scaffold-eth usage
9 |
10 | export default function TimelineDisplay(props) {
11 | return (
12 |
13 |
14 |
15 | Clone and Install from the{" "}
16 |
17 | github repo
18 |
19 |
20 |
21 |
22 |
23 |
24 | Start your frontend app with: yarn start
25 |
26 |
27 |
28 |
29 |
30 | Start your local blockchain with: yarn run chain (and refresh)
31 |
32 |
33 |
34 |
35 |
36 | Compile and deploy your smart contract: yarn run deploy
37 |
38 |
39 |
40 |
41 |
42 | Fix error in SmartContractWallet.sol then: yarn run deploy
43 |
44 |
45 |
46 | } color={props.hasEther ? "green" : "blue"}>
47 |
48 | Send test ether to your{" "}
49 | address using
50 | (bottom left) faucet
51 |
52 |
53 |
54 | }
56 | color={props.contractHasEther ? "green" : "blue"}
57 | >
58 |
59 | Deposit some funds into your{" "}
60 | {" "}
61 | smart contract wallet
62 |
63 |
64 |
65 | }
67 | color={props.amOwnerOfContract ? "green" : "blue"}
68 | >
69 |
70 | Set owner of your{" "}
71 | {" "}
72 | smart contract wallet to your{" "}
73 | address
74 |
75 |
76 |
77 |
78 |
79 | Yikes, anyone can take ownership of SmartContractWallet.sol
80 |
81 |
82 |
83 |
84 |
85 | Test your contract with buidler/test/myTest.js then:
86 | yarn run test
87 |
88 |
89 |
90 |
91 |
92 | Build something awesome with 🏗 scaffold-eth and{" "}
93 |
94 | @ me
95 |
96 | !
97 |
98 |
99 |
100 |
101 |
102 | Read more about{" "}
103 |
104 | Ethereum
105 |
106 | ,{" "}
107 |
108 | Solidity
109 |
110 | , and{" "}
111 |
112 | Buidler
113 |
114 |
115 |
116 |
117 | );
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/TokenBalance.jsx:
--------------------------------------------------------------------------------
1 | import { useTokenBalance } from "eth-hooks/erc/erc-20/useTokenBalance";
2 | import React, { useState } from "react";
3 |
4 | import { utils } from "ethers";
5 |
6 | export default function TokenBalance(props) {
7 | const [dollarMode, setDollarMode] = useState(true);
8 |
9 | const tokenContract = props.contracts && props.contracts[props.name];
10 | const balance = useTokenBalance(tokenContract, props.address, 1777);
11 |
12 | let floatBalance = parseFloat("0.00");
13 |
14 | let usingBalance = balance;
15 |
16 | if (typeof props.balance !== "undefined") {
17 | usingBalance = props.balance;
18 | }
19 |
20 | if (usingBalance) {
21 | const etherBalance = utils.formatEther(usingBalance);
22 | parseFloat(etherBalance).toFixed(2);
23 | floatBalance = parseFloat(etherBalance);
24 | }
25 |
26 | let displayBalance = floatBalance.toFixed(4);
27 |
28 | if (props.dollarMultiplier && dollarMode) {
29 | displayBalance = "$" + (floatBalance * props.dollarMultiplier).toFixed(2);
30 | }
31 |
32 | return (
33 | {
41 | setDollarMode(!dollarMode);
42 | }}
43 | >
44 | {props.img} {displayBalance}
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Wallet.jsx:
--------------------------------------------------------------------------------
1 | import { KeyOutlined, QrcodeOutlined, SendOutlined, WalletOutlined } from "@ant-design/icons";
2 | import { Button, Modal, Spin, Tooltip, Typography } from "antd";
3 | import { ethers } from "ethers";
4 | import QR from "qrcode.react";
5 | import React, { useState, useEffect } from "react";
6 | import { Transactor } from "../helpers";
7 | import Address from "./Address";
8 | import AddressInput from "./AddressInput";
9 | import Balance from "./Balance";
10 | import EtherInput from "./EtherInput";
11 |
12 | const { Text, Paragraph } = Typography;
13 |
14 | /*
15 | ~ What it does? ~
16 |
17 | Displays a wallet where you can specify address and send USD/ETH, with options to
18 | scan address, to convert between USD and ETH, to see and generate private keys,
19 | to send, receive and extract the burner wallet
20 |
21 | ~ How can I use? ~
22 |
23 |
30 |
31 | ~ Features ~
32 |
33 | - Provide provider={userProvider} to display a wallet
34 | - Provide address={address} if you want to specify address, otherwise
35 | your default address will be used
36 | - Provide ensProvider={mainnetProvider} and your address will be replaced by ENS name
37 | (ex. "0xa870" => "user.eth") or you can enter directly ENS name instead of address
38 | - Provide price={price} of ether and easily convert between USD and ETH
39 | - Provide color to specify the color of wallet icon
40 | */
41 |
42 | export default function Wallet(props) {
43 | const [signerAddress, setSignerAddress] = useState();
44 | useEffect(() => {
45 | async function getAddress() {
46 | if (props.signer) {
47 | const newAddress = await props.signer.getAddress();
48 | setSignerAddress(newAddress);
49 | }
50 | }
51 | getAddress();
52 | }, [props.signer]);
53 |
54 | const selectedAddress = props.address || signerAddress;
55 |
56 | const [open, setOpen] = useState();
57 | const [qr, setQr] = useState();
58 | const [amount, setAmount] = useState();
59 | const [toAddress, setToAddress] = useState();
60 | const [pk, setPK] = useState();
61 |
62 | const providerSend = props.provider ? (
63 |
64 | {
66 | setOpen(!open);
67 | }}
68 | rotate={-90}
69 | style={{
70 | padding: 7,
71 | color: props.color ? props.color : "",
72 | cursor: "pointer",
73 | fontSize: 28,
74 | verticalAlign: "middle",
75 | }}
76 | />
77 |
78 | ) : (
79 | ""
80 | );
81 |
82 | let display;
83 | let receiveButton;
84 | let privateKeyButton;
85 | if (qr) {
86 | display = (
87 |
88 |
89 | {selectedAddress}
90 |
91 |
99 |
100 | );
101 | receiveButton = (
102 | {
105 | setQr("");
106 | }}
107 | >
108 | Hide
109 |
110 | );
111 | privateKeyButton = (
112 | {
115 | setPK(selectedAddress);
116 | setQr("");
117 | }}
118 | >
119 | Private Key
120 |
121 | );
122 | } else if (pk) {
123 | const pk = localStorage.getItem("metaPrivateKey");
124 | const wallet = new ethers.Wallet(pk);
125 |
126 | if (wallet.address !== selectedAddress) {
127 | display = (
128 |
129 | *injected account*, private key unknown
130 |
131 | );
132 | } else {
133 | const extraPkDisplayAdded = {};
134 | const extraPkDisplay = [];
135 | extraPkDisplayAdded[wallet.address] = true;
136 | extraPkDisplay.push(
137 | ,
142 | );
143 | for (const key in localStorage) {
144 | if (key.indexOf("metaPrivateKey_backup") >= 0) {
145 | console.log(key);
146 | const pastpk = localStorage.getItem(key);
147 | const pastwallet = new ethers.Wallet(pastpk);
148 | if (!extraPkDisplayAdded[pastwallet.address] /* && selectedAddress!=pastwallet.address */) {
149 | extraPkDisplayAdded[pastwallet.address] = true;
150 | extraPkDisplay.push(
151 | ,
157 | );
158 | }
159 | }
160 | }
161 |
162 | display = (
163 |
164 |
Private Key:
165 |
166 |
167 | {pk}
168 |
169 |
170 |
171 |
172 |
173 | Point your camera phone at qr code to open in
174 |
175 | burner wallet
176 |
177 | :
178 |
179 |
187 |
188 |
189 | {"https://xdai.io/" + pk}
190 |
191 |
192 | {extraPkDisplay ? (
193 |
194 |
Known Private Keys:
195 | {extraPkDisplay}
196 | {
198 | const currentPrivateKey = window.localStorage.getItem("metaPrivateKey");
199 | if (currentPrivateKey) {
200 | window.localStorage.setItem("metaPrivateKey_backup" + Date.now(), currentPrivateKey);
201 | }
202 | const randomWallet = ethers.Wallet.createRandom();
203 | const privateKey = randomWallet._signingKey().privateKey;
204 | window.localStorage.setItem("metaPrivateKey", privateKey);
205 | window.location.reload();
206 | }}
207 | >
208 | Generate
209 |
210 |
211 | ) : (
212 | ""
213 | )}
214 |
215 | );
216 | }
217 |
218 | receiveButton = (
219 | {
222 | setQr(selectedAddress);
223 | setPK("");
224 | }}
225 | >
226 | Receive
227 |
228 | );
229 | privateKeyButton = (
230 | {
233 | setPK("");
234 | setQr("");
235 | }}
236 | >
237 | Hide
238 |
239 | );
240 | } else {
241 | const inputStyle = {
242 | padding: 10,
243 | };
244 |
245 | display = (
246 |
247 |
256 |
257 | {
261 | setAmount(value);
262 | }}
263 | />
264 |
265 |
266 | );
267 | receiveButton = (
268 | {
271 | setQr(selectedAddress);
272 | setPK("");
273 | }}
274 | >
275 | Receive
276 |
277 | );
278 | privateKeyButton = (
279 | {
282 | setPK(selectedAddress);
283 | setQr("");
284 | }}
285 | >
286 | Private Key
287 |
288 | );
289 | }
290 |
291 | return (
292 |
293 | {providerSend}
294 |
298 | {selectedAddress ? : }
299 |
300 |
301 |
302 |
303 | }
304 | onOk={() => {
305 | setQr();
306 | setPK();
307 | setOpen(!open);
308 | }}
309 | onCancel={() => {
310 | setQr();
311 | setPK();
312 | setOpen(!open);
313 | }}
314 | footer={[
315 | privateKeyButton,
316 | receiveButton,
317 | {
323 | const tx = Transactor(props.signer || props.provider);
324 |
325 | let value;
326 | try {
327 | value = ethers.utils.parseEther("" + amount);
328 | } catch (e) {
329 | // failed to parseEther, try something else
330 | value = ethers.utils.parseEther("" + parseFloat(amount).toFixed(8));
331 | }
332 |
333 | tx({
334 | to: toAddress,
335 | value,
336 | });
337 | setOpen(!open);
338 | setQr();
339 | }}
340 | >
341 | Send
342 | ,
343 | ]}
344 | >
345 | {display}
346 |
347 |
348 | );
349 | }
350 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Account } from "./Account";
2 | export { default as Address } from "./Address";
3 | export { default as AddressInput } from "./AddressInput";
4 | export { default as Balance } from "./Balance";
5 | export { default as Blockie } from "./Blockie";
6 | export { default as BytesStringInput } from "./BytesStringInput";
7 | export { default as Contract } from "./Contract";
8 | export { default as EtherInput } from "./EtherInput";
9 | export { default as Faucet } from "./Faucet";
10 | export { default as GasGauge } from "./GasGauge";
11 | export { default as Header } from "./Header";
12 | export { default as Provider } from "./Provider";
13 | export { default as Ramp } from "./Ramp";
14 | export { default as Swap } from "./Swap";
15 | export { default as ThemeSwitch } from "./ThemeSwitch";
16 | export { default as Timeline } from "./Timeline";
17 | export { default as TokenBalance } from "./TokenBalance";
18 | export { default as Wallet } from "./Wallet";
19 | export { default as L2Bridge } from "./L2Bridge";
20 | export { default as AddRPC } from "./NewRpc";
21 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | // MY INFURA_ID, SWAP IN YOURS FROM https://infura.io/dashboard/ethereum
2 | export const INFURA_ID = "a257d2529d6f4304aa6994449fe79001";
3 |
4 | // MY ETHERSCAN_ID, SWAP IN YOURS FROM https://etherscan.io/myapikey
5 | export const ETHERSCAN_KEY = "PSW8C433Q667DVEX5BCRMGNAH9FSGFZ7Q8";
6 |
7 | // BLOCKNATIVE ID FOR Notify.js:
8 | export const BLOCKNATIVE_DAPPID = "0b58206a-f3c0-4701-a62f-73c7243e8c77";
9 |
10 | export const NETWORKS = {
11 | localhost: {
12 | name: "localhost",
13 | color: "#666666",
14 | chainId: 31337,
15 | blockExplorer: "",
16 | rpcUrl: "http://" + window.location.hostname + ":8545",
17 | },
18 | mainnet: {
19 | name: "mainnet",
20 | color: "#ff8b9e",
21 | chainId: 1,
22 | rpcUrl: `https://rpc.flashbots.net?bundle=89e241e3-e183-4280-84ad-d968b4424c55`,
23 | blockExplorer: "https://etherscan.io/",
24 | },
25 | kovan: {
26 | name: "kovan",
27 | color: "#7003DD",
28 | chainId: 42,
29 | rpcUrl: `https://kovan.infura.io/v3/${INFURA_ID}`,
30 | blockExplorer: "https://kovan.etherscan.io/",
31 | faucet: "https://gitter.im/kovan-testnet/faucet", // https://faucet.kovan.network/
32 | },
33 | rinkeby: {
34 | name: "rinkeby",
35 | color: "#e0d068",
36 | chainId: 4,
37 | rpcUrl: `https://rinkeby.infura.io/v3/${INFURA_ID}`,
38 | faucet: "https://faucet.rinkeby.io/",
39 | blockExplorer: "https://rinkeby.etherscan.io/",
40 | },
41 | ropsten: {
42 | name: "ropsten",
43 | color: "#F60D09",
44 | chainId: 3,
45 | faucet: "https://faucet.ropsten.be/",
46 | blockExplorer: "https://ropsten.etherscan.io/",
47 | rpcUrl: `https://ropsten.infura.io/v3/${INFURA_ID}`,
48 | },
49 | goerli: {
50 | name: "goerli",
51 | color: "#0975F6",
52 | chainId: 5,
53 | faucet: "https://goerli-faucet.slock.it/",
54 | blockExplorer: "https://goerli.etherscan.io/",
55 | rpcUrl: `https://goerli.infura.io/v3/${INFURA_ID}`,
56 | },
57 | xdai: {
58 | name: "xdai",
59 | color: "#48a9a6",
60 | chainId: 100,
61 | price: 1,
62 | gasPrice: 1000000000,
63 | rpcUrl: "https://rpc.gnosischain.com",
64 | faucet: "https://xdai-faucet.top/",
65 | blockExplorer: "https://blockscout.com/poa/xdai/",
66 | },
67 | matic: {
68 | name: "matic",
69 | color: "#2bbdf7",
70 | chainId: 137,
71 | price: 1,
72 | gasPrice: 1000000000,
73 | rpcUrl: "https://rpc-mainnet.maticvigil.com",
74 | faucet: "https://faucet.matic.network/",
75 | blockExplorer: "https://explorer-mainnet.maticvigil.com//",
76 | },
77 | mumbai: {
78 | name: "mumbai",
79 | color: "#92D9FA",
80 | chainId: 80001,
81 | price: 1,
82 | gasPrice: 1000000000,
83 | rpcUrl: "https://rpc-mumbai.maticvigil.com",
84 | faucet: "https://faucet.matic.network/",
85 | blockExplorer: "https://mumbai-explorer.matic.today/",
86 | },
87 | localArbitrum: {
88 | name: "localArbitrum",
89 | color: "#50a0ea",
90 | chainId: 153869338190755,
91 | blockExplorer: "",
92 | rpcUrl: `http://localhost:8547`,
93 | },
94 | localArbitrumL1: {
95 | name: "localArbitrumL1",
96 | color: "#50a0ea",
97 | chainId: 44010,
98 | blockExplorer: "",
99 | rpcUrl: `http://localhost:7545`,
100 | },
101 | rinkebyArbitrum: {
102 | name: "Arbitrum Testnet",
103 | color: "#50a0ea",
104 | chainId: 421611,
105 | blockExplorer: "https://rinkeby-explorer.arbitrum.io/#/",
106 | rpcUrl: `https://rinkeby.arbitrum.io/rpc`,
107 | },
108 | arbitrum: {
109 | name: "Arbitrum",
110 | color: "#50a0ea",
111 | chainId: 42161,
112 | blockExplorer: "https://explorer.arbitrum.io/#/",
113 | rpcUrl: `https://arb1.arbitrum.io/rpc`,
114 | gasPrice: 0,
115 | },
116 | localOptimismL1: {
117 | name: "localOptimismL1",
118 | color: "#f01a37",
119 | chainId: 31337,
120 | blockExplorer: "",
121 | rpcUrl: "http://" + window.location.hostname + ":9545",
122 | },
123 | localOptimism: {
124 | name: "localOptimism",
125 | color: "#f01a37",
126 | chainId: 420,
127 | blockExplorer: "",
128 | rpcUrl: "http://" + window.location.hostname + ":8545",
129 | gasPrice: 0,
130 | },
131 | kovanOptimism: {
132 | name: "kovanOptimism",
133 | color: "#f01a37",
134 | chainId: 69,
135 | blockExplorer: "https://kovan-optimistic.etherscan.io/",
136 | rpcUrl: `https://kovan.optimism.io`,
137 | gasPrice: 0,
138 | },
139 | optimism: {
140 | name: "optimism",
141 | color: "#f01a37",
142 | chainId: 10,
143 | blockExplorer: "https://optimistic.etherscan.io/",
144 | rpcUrl: `https://mainnet.optimism.io`,
145 | },
146 | localAvalanche: {
147 | name: "localAvalanche",
148 | color: "#666666",
149 | chainId: 43112,
150 | blockExplorer: "",
151 | rpcUrl: `http://localhost:9650/ext/bc/C/rpc`,
152 | gasPrice: 225000000000,
153 | },
154 | fujiAvalanche: {
155 | name: "fujiAvalanche",
156 | color: "#666666",
157 | chainId: 43113,
158 | blockExplorer: "https://cchain.explorer.avax-test.network/",
159 | rpcUrl: `https://api.avax-test.network/ext/bc/C/rpc`,
160 | gasPrice: 225000000000,
161 | },
162 | mainnetAvalanche: {
163 | name: "mainnetAvalanche",
164 | color: "#666666",
165 | chainId: 43114,
166 | blockExplorer: "https://cchain.explorer.avax.network/",
167 | rpcUrl: `https://api.avax.network/ext/bc/C/rpc`,
168 | gasPrice: 25000000000,
169 | },
170 | testnetHarmony: {
171 | name: "Harmony Testnet",
172 | color: "#00b0ef",
173 | chainId: 1666700000,
174 | blockExplorer: "https://explorer.pops.one/",
175 | rpcUrl: `https://api.s0.b.hmny.io`,
176 | gasPrice: 1000000000,
177 | },
178 | mainnetHarmony: {
179 | name: "Harmony Mainnet",
180 | color: "#00b0ef",
181 | chainId: 1666600000,
182 | blockExplorer: "https://explorer.harmony.one/",
183 | rpcUrl: `https://api.harmony.one`,
184 | gasPrice: 1000000000,
185 | },
186 | };
187 |
188 | export const NETWORK = chainId => {
189 | for (const n in NETWORKS) {
190 | if (NETWORKS[n].chainId === chainId) {
191 | return NETWORKS[n];
192 | }
193 | }
194 | };
195 |
--------------------------------------------------------------------------------
/src/contracts/external_contracts.js:
--------------------------------------------------------------------------------
1 | const ERC20ABI = [
2 | {
3 | constant: true,
4 | inputs: [],
5 | name: "name",
6 | outputs: [
7 | {
8 | name: "",
9 | type: "string",
10 | },
11 | ],
12 | payable: false,
13 | stateMutability: "view",
14 | type: "function",
15 | },
16 | {
17 | constant: false,
18 | inputs: [
19 | {
20 | name: "_spender",
21 | type: "address",
22 | },
23 | {
24 | name: "_value",
25 | type: "uint256",
26 | },
27 | ],
28 | name: "approve",
29 | outputs: [
30 | {
31 | name: "",
32 | type: "bool",
33 | },
34 | ],
35 | payable: false,
36 | stateMutability: "nonpayable",
37 | type: "function",
38 | },
39 | {
40 | constant: true,
41 | inputs: [],
42 | name: "totalSupply",
43 | outputs: [
44 | {
45 | name: "",
46 | type: "uint256",
47 | },
48 | ],
49 | payable: false,
50 | stateMutability: "view",
51 | type: "function",
52 | },
53 | {
54 | constant: false,
55 | inputs: [
56 | {
57 | name: "_from",
58 | type: "address",
59 | },
60 | {
61 | name: "_to",
62 | type: "address",
63 | },
64 | {
65 | name: "_value",
66 | type: "uint256",
67 | },
68 | ],
69 | name: "transferFrom",
70 | outputs: [
71 | {
72 | name: "",
73 | type: "bool",
74 | },
75 | ],
76 | payable: false,
77 | stateMutability: "nonpayable",
78 | type: "function",
79 | },
80 | {
81 | constant: true,
82 | inputs: [],
83 | name: "decimals",
84 | outputs: [
85 | {
86 | name: "",
87 | type: "uint8",
88 | },
89 | ],
90 | payable: false,
91 | stateMutability: "view",
92 | type: "function",
93 | },
94 | {
95 | constant: true,
96 | inputs: [
97 | {
98 | name: "_owner",
99 | type: "address",
100 | },
101 | ],
102 | name: "balanceOf",
103 | outputs: [
104 | {
105 | name: "balance",
106 | type: "uint256",
107 | },
108 | ],
109 | payable: false,
110 | stateMutability: "view",
111 | type: "function",
112 | },
113 | {
114 | constant: true,
115 | inputs: [],
116 | name: "symbol",
117 | outputs: [
118 | {
119 | name: "",
120 | type: "string",
121 | },
122 | ],
123 | payable: false,
124 | stateMutability: "view",
125 | type: "function",
126 | },
127 | {
128 | constant: false,
129 | inputs: [
130 | {
131 | name: "_to",
132 | type: "address",
133 | },
134 | {
135 | name: "_value",
136 | type: "uint256",
137 | },
138 | ],
139 | name: "transfer",
140 | outputs: [
141 | {
142 | name: "",
143 | type: "bool",
144 | },
145 | ],
146 | payable: false,
147 | stateMutability: "nonpayable",
148 | type: "function",
149 | },
150 | {
151 | constant: true,
152 | inputs: [
153 | {
154 | name: "_owner",
155 | type: "address",
156 | },
157 | {
158 | name: "_spender",
159 | type: "address",
160 | },
161 | ],
162 | name: "allowance",
163 | outputs: [
164 | {
165 | name: "",
166 | type: "uint256",
167 | },
168 | ],
169 | payable: false,
170 | stateMutability: "view",
171 | type: "function",
172 | },
173 | {
174 | payable: true,
175 | stateMutability: "payable",
176 | type: "fallback",
177 | },
178 | {
179 | anonymous: false,
180 | inputs: [
181 | {
182 | indexed: true,
183 | name: "owner",
184 | type: "address",
185 | },
186 | {
187 | indexed: true,
188 | name: "spender",
189 | type: "address",
190 | },
191 | {
192 | indexed: false,
193 | name: "value",
194 | type: "uint256",
195 | },
196 | ],
197 | name: "Approval",
198 | type: "event",
199 | },
200 | {
201 | anonymous: false,
202 | inputs: [
203 | {
204 | indexed: true,
205 | name: "from",
206 | type: "address",
207 | },
208 | {
209 | indexed: true,
210 | name: "to",
211 | type: "address",
212 | },
213 | {
214 | indexed: false,
215 | name: "value",
216 | type: "uint256",
217 | },
218 | ],
219 | name: "Transfer",
220 | type: "event",
221 | },
222 | ];
223 | const DAIABI = [
224 | {
225 | inputs: [
226 | {
227 | internalType: "uint256",
228 | name: "chainId_",
229 | type: "uint256",
230 | },
231 | ],
232 | payable: false,
233 | stateMutability: "nonpayable",
234 | type: "constructor",
235 | },
236 | {
237 | anonymous: false,
238 | inputs: [
239 | {
240 | indexed: true,
241 | internalType: "address",
242 | name: "src",
243 | type: "address",
244 | },
245 | {
246 | indexed: true,
247 | internalType: "address",
248 | name: "guy",
249 | type: "address",
250 | },
251 | {
252 | indexed: false,
253 | internalType: "uint256",
254 | name: "wad",
255 | type: "uint256",
256 | },
257 | ],
258 | name: "Approval",
259 | type: "event",
260 | },
261 | {
262 | anonymous: true,
263 | inputs: [
264 | {
265 | indexed: true,
266 | internalType: "bytes4",
267 | name: "sig",
268 | type: "bytes4",
269 | },
270 | {
271 | indexed: true,
272 | internalType: "address",
273 | name: "usr",
274 | type: "address",
275 | },
276 | {
277 | indexed: true,
278 | internalType: "bytes32",
279 | name: "arg1",
280 | type: "bytes32",
281 | },
282 | {
283 | indexed: true,
284 | internalType: "bytes32",
285 | name: "arg2",
286 | type: "bytes32",
287 | },
288 | {
289 | indexed: false,
290 | internalType: "bytes",
291 | name: "data",
292 | type: "bytes",
293 | },
294 | ],
295 | name: "LogNote",
296 | type: "event",
297 | },
298 | {
299 | anonymous: false,
300 | inputs: [
301 | {
302 | indexed: true,
303 | internalType: "address",
304 | name: "src",
305 | type: "address",
306 | },
307 | {
308 | indexed: true,
309 | internalType: "address",
310 | name: "dst",
311 | type: "address",
312 | },
313 | {
314 | indexed: false,
315 | internalType: "uint256",
316 | name: "wad",
317 | type: "uint256",
318 | },
319 | ],
320 | name: "Transfer",
321 | type: "event",
322 | },
323 | {
324 | constant: true,
325 | inputs: [],
326 | name: "DOMAIN_SEPARATOR",
327 | outputs: [
328 | {
329 | internalType: "bytes32",
330 | name: "",
331 | type: "bytes32",
332 | },
333 | ],
334 | payable: false,
335 | stateMutability: "view",
336 | type: "function",
337 | },
338 | {
339 | constant: true,
340 | inputs: [],
341 | name: "PERMIT_TYPEHASH",
342 | outputs: [
343 | {
344 | internalType: "bytes32",
345 | name: "",
346 | type: "bytes32",
347 | },
348 | ],
349 | payable: false,
350 | stateMutability: "view",
351 | type: "function",
352 | },
353 | {
354 | constant: true,
355 | inputs: [
356 | {
357 | internalType: "address",
358 | name: "",
359 | type: "address",
360 | },
361 | {
362 | internalType: "address",
363 | name: "",
364 | type: "address",
365 | },
366 | ],
367 | name: "allowance",
368 | outputs: [
369 | {
370 | internalType: "uint256",
371 | name: "",
372 | type: "uint256",
373 | },
374 | ],
375 | payable: false,
376 | stateMutability: "view",
377 | type: "function",
378 | },
379 | {
380 | constant: false,
381 | inputs: [
382 | {
383 | internalType: "address",
384 | name: "usr",
385 | type: "address",
386 | },
387 | {
388 | internalType: "uint256",
389 | name: "wad",
390 | type: "uint256",
391 | },
392 | ],
393 | name: "approve",
394 | outputs: [
395 | {
396 | internalType: "bool",
397 | name: "",
398 | type: "bool",
399 | },
400 | ],
401 | payable: false,
402 | stateMutability: "nonpayable",
403 | type: "function",
404 | },
405 | {
406 | constant: true,
407 | inputs: [
408 | {
409 | internalType: "address",
410 | name: "",
411 | type: "address",
412 | },
413 | ],
414 | name: "balanceOf",
415 | outputs: [
416 | {
417 | internalType: "uint256",
418 | name: "",
419 | type: "uint256",
420 | },
421 | ],
422 | payable: false,
423 | stateMutability: "view",
424 | type: "function",
425 | },
426 | {
427 | constant: false,
428 | inputs: [
429 | {
430 | internalType: "address",
431 | name: "usr",
432 | type: "address",
433 | },
434 | {
435 | internalType: "uint256",
436 | name: "wad",
437 | type: "uint256",
438 | },
439 | ],
440 | name: "burn",
441 | outputs: [],
442 | payable: false,
443 | stateMutability: "nonpayable",
444 | type: "function",
445 | },
446 | {
447 | constant: true,
448 | inputs: [],
449 | name: "decimals",
450 | outputs: [
451 | {
452 | internalType: "uint8",
453 | name: "",
454 | type: "uint8",
455 | },
456 | ],
457 | payable: false,
458 | stateMutability: "view",
459 | type: "function",
460 | },
461 | {
462 | constant: false,
463 | inputs: [
464 | {
465 | internalType: "address",
466 | name: "guy",
467 | type: "address",
468 | },
469 | ],
470 | name: "deny",
471 | outputs: [],
472 | payable: false,
473 | stateMutability: "nonpayable",
474 | type: "function",
475 | },
476 | {
477 | constant: false,
478 | inputs: [
479 | {
480 | internalType: "address",
481 | name: "usr",
482 | type: "address",
483 | },
484 | {
485 | internalType: "uint256",
486 | name: "wad",
487 | type: "uint256",
488 | },
489 | ],
490 | name: "mint",
491 | outputs: [],
492 | payable: false,
493 | stateMutability: "nonpayable",
494 | type: "function",
495 | },
496 | {
497 | constant: false,
498 | inputs: [
499 | {
500 | internalType: "address",
501 | name: "src",
502 | type: "address",
503 | },
504 | {
505 | internalType: "address",
506 | name: "dst",
507 | type: "address",
508 | },
509 | {
510 | internalType: "uint256",
511 | name: "wad",
512 | type: "uint256",
513 | },
514 | ],
515 | name: "move",
516 | outputs: [],
517 | payable: false,
518 | stateMutability: "nonpayable",
519 | type: "function",
520 | },
521 | {
522 | constant: true,
523 | inputs: [],
524 | name: "name",
525 | outputs: [
526 | {
527 | internalType: "string",
528 | name: "",
529 | type: "string",
530 | },
531 | ],
532 | payable: false,
533 | stateMutability: "view",
534 | type: "function",
535 | },
536 | {
537 | constant: true,
538 | inputs: [
539 | {
540 | internalType: "address",
541 | name: "",
542 | type: "address",
543 | },
544 | ],
545 | name: "nonces",
546 | outputs: [
547 | {
548 | internalType: "uint256",
549 | name: "",
550 | type: "uint256",
551 | },
552 | ],
553 | payable: false,
554 | stateMutability: "view",
555 | type: "function",
556 | },
557 | {
558 | constant: false,
559 | inputs: [
560 | {
561 | internalType: "address",
562 | name: "holder",
563 | type: "address",
564 | },
565 | {
566 | internalType: "address",
567 | name: "spender",
568 | type: "address",
569 | },
570 | {
571 | internalType: "uint256",
572 | name: "nonce",
573 | type: "uint256",
574 | },
575 | {
576 | internalType: "uint256",
577 | name: "expiry",
578 | type: "uint256",
579 | },
580 | {
581 | internalType: "bool",
582 | name: "allowed",
583 | type: "bool",
584 | },
585 | {
586 | internalType: "uint8",
587 | name: "v",
588 | type: "uint8",
589 | },
590 | {
591 | internalType: "bytes32",
592 | name: "r",
593 | type: "bytes32",
594 | },
595 | {
596 | internalType: "bytes32",
597 | name: "s",
598 | type: "bytes32",
599 | },
600 | ],
601 | name: "permit",
602 | outputs: [],
603 | payable: false,
604 | stateMutability: "nonpayable",
605 | type: "function",
606 | },
607 | {
608 | constant: false,
609 | inputs: [
610 | {
611 | internalType: "address",
612 | name: "usr",
613 | type: "address",
614 | },
615 | {
616 | internalType: "uint256",
617 | name: "wad",
618 | type: "uint256",
619 | },
620 | ],
621 | name: "pull",
622 | outputs: [],
623 | payable: false,
624 | stateMutability: "nonpayable",
625 | type: "function",
626 | },
627 | {
628 | constant: false,
629 | inputs: [
630 | {
631 | internalType: "address",
632 | name: "usr",
633 | type: "address",
634 | },
635 | {
636 | internalType: "uint256",
637 | name: "wad",
638 | type: "uint256",
639 | },
640 | ],
641 | name: "push",
642 | outputs: [],
643 | payable: false,
644 | stateMutability: "nonpayable",
645 | type: "function",
646 | },
647 | {
648 | constant: false,
649 | inputs: [
650 | {
651 | internalType: "address",
652 | name: "guy",
653 | type: "address",
654 | },
655 | ],
656 | name: "rely",
657 | outputs: [],
658 | payable: false,
659 | stateMutability: "nonpayable",
660 | type: "function",
661 | },
662 | {
663 | constant: true,
664 | inputs: [],
665 | name: "symbol",
666 | outputs: [
667 | {
668 | internalType: "string",
669 | name: "",
670 | type: "string",
671 | },
672 | ],
673 | payable: false,
674 | stateMutability: "view",
675 | type: "function",
676 | },
677 | {
678 | constant: true,
679 | inputs: [],
680 | name: "totalSupply",
681 | outputs: [
682 | {
683 | internalType: "uint256",
684 | name: "",
685 | type: "uint256",
686 | },
687 | ],
688 | payable: false,
689 | stateMutability: "view",
690 | type: "function",
691 | },
692 | {
693 | constant: false,
694 | inputs: [
695 | {
696 | internalType: "address",
697 | name: "dst",
698 | type: "address",
699 | },
700 | {
701 | internalType: "uint256",
702 | name: "wad",
703 | type: "uint256",
704 | },
705 | ],
706 | name: "transfer",
707 | outputs: [
708 | {
709 | internalType: "bool",
710 | name: "",
711 | type: "bool",
712 | },
713 | ],
714 | payable: false,
715 | stateMutability: "nonpayable",
716 | type: "function",
717 | },
718 | {
719 | constant: false,
720 | inputs: [
721 | {
722 | internalType: "address",
723 | name: "src",
724 | type: "address",
725 | },
726 | {
727 | internalType: "address",
728 | name: "dst",
729 | type: "address",
730 | },
731 | {
732 | internalType: "uint256",
733 | name: "wad",
734 | type: "uint256",
735 | },
736 | ],
737 | name: "transferFrom",
738 | outputs: [
739 | {
740 | internalType: "bool",
741 | name: "",
742 | type: "bool",
743 | },
744 | ],
745 | payable: false,
746 | stateMutability: "nonpayable",
747 | type: "function",
748 | },
749 | {
750 | constant: true,
751 | inputs: [],
752 | name: "version",
753 | outputs: [
754 | {
755 | internalType: "string",
756 | name: "",
757 | type: "string",
758 | },
759 | ],
760 | payable: false,
761 | stateMutability: "view",
762 | type: "function",
763 | },
764 | {
765 | constant: true,
766 | inputs: [
767 | {
768 | internalType: "address",
769 | name: "",
770 | type: "address",
771 | },
772 | ],
773 | name: "wards",
774 | outputs: [
775 | {
776 | internalType: "uint256",
777 | name: "",
778 | type: "uint256",
779 | },
780 | ],
781 | payable: false,
782 | stateMutability: "view",
783 | type: "function",
784 | },
785 | ];
786 |
787 | // Mainnet DAI, Optimism and Arbitrium Rollup Contracts with local addresses
788 | module.exports = {
789 | 1: {
790 | contracts: {
791 | DAI: {
792 | address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
793 | abi: DAIABI,
794 | },
795 | UNI: {
796 | address: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
797 | abi: ERC20ABI,
798 | },
799 | },
800 | },
801 | };
802 |
--------------------------------------------------------------------------------
/src/contracts/hardhat_contracts.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/src/ethereumLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcfr-eth/FlashbotsBundlerUI/d1f0111e7ba12226ebdf6fdbadd9b9fbfeff5ec5/src/ethereumLogo.png
--------------------------------------------------------------------------------
/src/helpers/Transactor.js:
--------------------------------------------------------------------------------
1 | import { notification } from "antd";
2 | import Notify from "bnc-notify";
3 | import { BLOCKNATIVE_DAPPID } from "../constants";
4 |
5 | const { ethers } = require("ethers");
6 |
7 | // this should probably just be renamed to "notifier"
8 | // it is basically just a wrapper around BlockNative's wonderful Notify.js
9 | // https://docs.blocknative.com/notify
10 | const callbacks = {};
11 |
12 | const DEBUG = true;
13 |
14 | export default function Transactor(providerOrSigner, gasPrice, etherscan) {
15 | if (typeof providerOrSigner !== "undefined") {
16 | // eslint-disable-next-line consistent-return
17 | return async (tx, callback) => {
18 | let signer;
19 | let network;
20 | let provider;
21 | if (ethers.Signer.isSigner(providerOrSigner) === true) {
22 | provider = providerOrSigner.provider;
23 | signer = providerOrSigner;
24 | network = providerOrSigner.provider && (await providerOrSigner.provider.getNetwork());
25 | } else if (providerOrSigner._isProvider) {
26 | provider = providerOrSigner;
27 | signer = providerOrSigner.getSigner();
28 | network = await providerOrSigner.getNetwork();
29 | }
30 |
31 | console.log("network", network);
32 | var options = null;
33 | var notify = null;
34 | options = {
35 | dappId: BLOCKNATIVE_DAPPID, // GET YOUR OWN KEY AT https://account.blocknative.com
36 | system: "ethereum",
37 | networkId: network.chainId,
38 | // darkMode: Boolean, // (default: false)
39 | transactionHandler: txInformation => {
40 | if (DEBUG) console.log("HANDLE TX", txInformation);
41 | const possibleFunction = callbacks[txInformation.transaction.hash];
42 | if (typeof possibleFunction === "function") {
43 | possibleFunction(txInformation.transaction);
44 | }
45 | },
46 | };
47 |
48 | notify = Notify(options);
49 |
50 | let etherscanNetwork = "";
51 | if (network.name && network.chainId > 1) {
52 | etherscanNetwork = network.name + ".";
53 | }
54 |
55 | let etherscanTxUrl = "https://" + etherscanNetwork + "etherscan.io/tx/";
56 | if (network.chainId === 100) {
57 | etherscanTxUrl = "https://blockscout.com/poa/xdai/tx/";
58 | }
59 |
60 | try {
61 | let result;
62 | if (tx instanceof Promise) {
63 | if (DEBUG) console.log("AWAITING TX", tx);
64 | result = await tx;
65 | } else {
66 | if (!tx.gasPrice) {
67 | tx.gasPrice = gasPrice || ethers.utils.parseUnits("4.1", "gwei");
68 | }
69 | if (!tx.gasLimit) {
70 | tx.gasLimit = ethers.utils.hexlify(120000);
71 | }
72 | if (DEBUG) console.log("RUNNING TX", tx);
73 | result = await signer.sendTransaction(tx);
74 | }
75 | if (DEBUG) console.log("RESULT:", result);
76 | // console.log("Notify", notify);
77 |
78 | if (callback) {
79 | callbacks[result.hash] = callback;
80 | }
81 |
82 | // if it is a valid Notify.js network, use that, if not, just send a default notification
83 | if (notify && [1, 3, 4, 5, 42, 100].indexOf(network.chainId) >= 0) {
84 | const { emitter } = notify.hash(result.hash);
85 | emitter.on("all", transaction => {
86 | return {
87 | onclick: () => window.open((etherscan || etherscanTxUrl) + transaction.hash),
88 | };
89 | });
90 | } else {
91 | notification.info({
92 | message: "Local Transaction Sent",
93 | description: result.hash,
94 | placement: "bottomRight",
95 | });
96 | // on most networks BlockNative will update a transaction handler,
97 | // but locally we will set an interval to listen...
98 | if (callback) {
99 | const txResult = await tx;
100 | const listeningInterval = setInterval(async () => {
101 | console.log("CHECK IN ON THE TX", txResult, provider);
102 | const currentTransactionReceipt = await provider.getTransactionReceipt(txResult.hash);
103 | if (currentTransactionReceipt && currentTransactionReceipt.confirmations) {
104 | callback({ ...txResult, ...currentTransactionReceipt });
105 | clearInterval(listeningInterval);
106 | }
107 | }, 500);
108 | }
109 | }
110 |
111 | if (typeof result.wait === "function") {
112 | await result.wait();
113 | }
114 |
115 | return result;
116 | } catch (e) {
117 | if (DEBUG) console.log(e);
118 | // Accounts for Metamask and default signer on all networks
119 | let message =
120 | e.data && e.data.message
121 | ? e.data.message
122 | : e.error && JSON.parse(JSON.stringify(e.error)).body
123 | ? JSON.parse(JSON.parse(JSON.stringify(e.error)).body).error.message
124 | : e.data
125 | ? e.data
126 | : JSON.stringify(e);
127 | if (!e.error && e.message) {
128 | message = e.message;
129 | }
130 |
131 | console.log("Attempt to clean up:", message);
132 | try {
133 | let obj = JSON.parse(message);
134 | if (obj && obj.body) {
135 | let errorObj = JSON.parse(obj.body);
136 | if (errorObj && errorObj.error && errorObj.error.message) {
137 | message = errorObj.error.message;
138 | }
139 | }
140 | } catch (e) {
141 | //ignore
142 | }
143 |
144 | notification.error({
145 | message: "Transaction Error",
146 | description: message,
147 | });
148 | if (callback && typeof callback === "function") {
149 | callback(e);
150 | }
151 | }
152 | };
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/helpers/index.js:
--------------------------------------------------------------------------------
1 | export { default as Transactor } from "./Transactor";
2 |
--------------------------------------------------------------------------------
/src/helpers/loadAppContracts.js:
--------------------------------------------------------------------------------
1 | const contractListPromise = import("../contracts/hardhat_contracts.json");
2 | // @ts-ignore
3 | const externalContractsPromise = import("../contracts/external_contracts");
4 |
5 | export const loadAppContracts = async () => {
6 | const config = {};
7 | config.deployedContracts = (await contractListPromise).default ?? {};
8 | config.externalContracts = (await externalContractsPromise).default ?? {};
9 | return config;
10 | };
11 |
--------------------------------------------------------------------------------
/src/hooks/ContractEvents.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useState } from "react";
2 | import { useEventListener } from "eth-hooks/events/useEventListener";
3 | import { Card, List } from "antd";
4 |
5 | export default function useContractEvents(contract, show) {
6 | const displayedContractEvents = useMemo(() => {
7 | const eventsList = contract
8 | ? Object.entries(contract.interface.events).filter(
9 | fn => fn[1]["type"] === "event" && !(show && show.indexOf(fn[1]["name"]) < 0),
10 | )
11 | : [];
12 | return eventsList;
13 | }, [contract, show]);
14 |
15 | console.log("===> useContractEvents ---> displayedContractEvents: ", displayedContractEvents);
16 |
17 | return (
18 |
19 |
Events:
20 | {
24 | return (
25 |
26 | {item[0]}
27 | {
30 | return (
31 |
32 | {paramItem.name}
33 |
34 | );
35 | }}>
36 |
37 |
38 | );
39 | }}
40 | />
41 |
42 | );
43 | }
--------------------------------------------------------------------------------
/src/hooks/Debounce.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export default function useDebounce(value, delay) {
4 | const [debouncedValue, setDebouncedValue] = useState(value);
5 |
6 | useEffect(() => {
7 | const handler = setTimeout(() => {
8 | setDebouncedValue(value);
9 | }, delay);
10 |
11 | return () => {
12 | clearTimeout(handler);
13 | };
14 | }, [value]);
15 |
16 | return debouncedValue;
17 | }
18 |
--------------------------------------------------------------------------------
/src/hooks/ExternalContractLoader.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | import { Contract } from "@ethersproject/contracts";
3 | import { useState, useEffect } from "react";
4 |
5 | /*
6 | when you want to load an existing contract using just the provider, address, and ABI
7 | */
8 |
9 | /*
10 | ~ What it does? ~
11 |
12 | Enables you to load an existing mainnet DAI contract using the provider, address and abi
13 |
14 | ~ How can I use? ~
15 |
16 | const mainnetDAIContract = useExternalContractLoader(mainnetProvider, DAI_ADDRESS, DAI_ABI)
17 |
18 | ~ Features ~
19 |
20 | - Specify mainnetProvider
21 | - Specify DAI_ADDRESS and DAI_ABI, you can load/write them using constants.js
22 | */
23 | export default function useExternalContractLoader(provider, address, ABI, optionalBytecode) {
24 | const [contract, setContract] = useState();
25 | useEffect(() => {
26 | async function loadContract() {
27 | if (typeof provider !== "undefined" && address && ABI) {
28 | try {
29 | // we need to check to see if this provider has a signer or not
30 | let signer;
31 | const accounts = await provider.listAccounts();
32 | if (accounts && accounts.length > 0) {
33 | signer = provider.getSigner();
34 | } else {
35 | signer = provider;
36 | }
37 |
38 | const customContract = new Contract(address, ABI, signer);
39 | if (optionalBytecode) customContract.bytecode = optionalBytecode;
40 |
41 | setContract(customContract);
42 | } catch (e) {
43 | console.log("ERROR LOADING EXTERNAL CONTRACT AT " + address + " (check provider, address, and ABI)!!", e);
44 | }
45 | }
46 | }
47 | loadContract();
48 | }, [provider, address, ABI, optionalBytecode]);
49 | return contract;
50 | }
51 |
--------------------------------------------------------------------------------
/src/hooks/GasPrice.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { usePoller } from "eth-hooks";
3 | import { useState } from "react";
4 |
5 | export default function useGasPrice(targetNetwork, speed) {
6 | const [gasPrice, setGasPrice] = useState();
7 | const loadGasPrice = async () => {
8 | if (targetNetwork.hasOwnProperty("gasPrice")) {
9 | setGasPrice(targetNetwork.gasPrice);
10 | } else {
11 | axios
12 | .get("https://ethgasstation.info/json/ethgasAPI.json")
13 | .then(response => {
14 | const newGasPrice = response.data[speed || "fast"] * 100000000;
15 | if (newGasPrice !== gasPrice) {
16 | setGasPrice(newGasPrice);
17 | }
18 | })
19 | .catch(error => console.log(error));
20 | }
21 | };
22 |
23 | usePoller(loadGasPrice, 39999);
24 | return gasPrice;
25 | }
26 |
--------------------------------------------------------------------------------
/src/hooks/LocalStorage.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | // Hook from useHooks! (https://usehooks.com/useLocalStorage/)
3 | export default function useLocalStorage(key, initialValue) {
4 | // State to store our value
5 | // Pass initial state function to useState so logic is only executed once
6 | const [storedValue, setStoredValue] = useState(() => {
7 | try {
8 | // Get from local storage by key
9 | const item = window.localStorage.getItem(key);
10 | const parsedItem = item ? JSON.parse(item) : initialValue;
11 |
12 | if (typeof parsedItem === "object" && parsedItem !== null && "value" in parsedItem) {
13 | // const now = new Date();
14 | // if (ttl && now.getTime() > parsedItem.expiry) {
15 | // If the item is expired, delete the item from storage
16 | // and return null
17 | // window.localStorage.removeItem(key);
18 | // return initialValue;
19 | // }
20 | return parsedItem.value;
21 | }
22 | // Parse stored json or if none return initialValue
23 | return parsedItem;
24 | } catch (error) {
25 | // If error also return initialValue
26 | console.log(error);
27 | return initialValue;
28 | }
29 | });
30 |
31 | // Return a wrapped version of useState's setter function that ...
32 | // ... persists the new value to localStorage.
33 | const setValue = value => {
34 | try {
35 | // Allow value to be a function so we have same API as useState
36 | const valueToStore = value instanceof Function ? value(storedValue) : value;
37 | // Save state
38 | setStoredValue(valueToStore);
39 | // Save to local storage
40 | window.localStorage.setItem(key, JSON.stringify(valueToStore));
41 | } catch (error) {
42 | // A more advanced implementation would handle the error case
43 | console.log(error);
44 | }
45 | };
46 |
47 | return [storedValue, setValue];
48 | }
49 |
--------------------------------------------------------------------------------
/src/hooks/TokenList.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | /*
4 | ~ What it does? ~
5 |
6 | Gets a tokenlist (see more at https://tokenlists.org/), returning the .tokens only
7 |
8 | ~ How can I use? ~
9 |
10 | const tokenList = useTokenList(); <- default returns the Unsiwap tokens
11 | const tokenList = useTokenList("https://gateway.ipfs.io/ipns/tokens.uniswap.org");
12 |
13 | ~ Features ~
14 |
15 | - Optional - specify chainId to filter by chainId
16 | */
17 |
18 | const useTokenList = (tokenListUri, chainId) => {
19 | const [tokenList, setTokenList] = useState([]);
20 |
21 | const _tokenListUri = tokenListUri || "https://gateway.ipfs.io/ipns/tokens.uniswap.org";
22 |
23 | useEffect(() => {
24 | const getTokenList = async () => {
25 | try {
26 | const tokenList = await fetch(_tokenListUri);
27 | const tokenListJson = await tokenList.json();
28 | let _tokenList;
29 |
30 | if (chainId) {
31 | _tokenList = tokenListJson.tokens.filter(function (t) {
32 | return t.chainId === chainId;
33 | });
34 | } else {
35 | _tokenList = tokenListJson;
36 | }
37 |
38 | setTokenList(_tokenList.tokens);
39 | } catch (e) {
40 | console.log(e);
41 | }
42 | };
43 | getTokenList();
44 | }, [tokenListUri]);
45 |
46 | return tokenList;
47 | };
48 |
49 | export default useTokenList;
50 |
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export { default as useDebounce } from "./Debounce";
2 | export { default as useLocalStorage } from "./LocalStorage";
3 | export * from "./useContractConfig";
4 | export { default as useExternalContractLoader } from "./ExternalContractLoader";
5 | export { default as useContractEvents } from "./ContractEvents";
6 |
--------------------------------------------------------------------------------
/src/hooks/useContractConfig.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { loadAppContracts } from "../helpers/loadAppContracts";
3 |
4 | export const useContractConfig = () => {
5 | const [contractsConfig, setContractsConfig] = useState({});
6 |
7 | useEffect(() => {
8 | const loadFunc = async () => {
9 | const result = await loadAppContracts();
10 | setContractsConfig(result);
11 | };
12 | void loadFunc();
13 | }, []);
14 | return contractsConfig;
15 | };
16 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | code {
6 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
7 | }
8 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
2 | import React from "react";
3 | import { ThemeSwitcherProvider } from "react-css-theme-switcher";
4 | import ReactDOM from "react-dom";
5 | import App from "./App";
6 | import "./index.css";
7 |
8 | const themes = {
9 | dark: `${process.env.PUBLIC_URL}/dark-theme.css`,
10 | light: `${process.env.PUBLIC_URL}/light-theme.css`,
11 | };
12 |
13 | const prevTheme = window.localStorage.getItem("theme");
14 |
15 | const subgraphUri = "http://localhost:8000/subgraphs/name/scaffold-eth/your-contract";
16 |
17 | const client = new ApolloClient({
18 | uri: subgraphUri,
19 | cache: new InMemoryCache(),
20 | });
21 |
22 | ReactDOM.render(
23 |
24 |
25 |
26 |
27 | ,
28 | document.getElementById("root"),
29 | );
30 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom/extend-expect";
6 |
--------------------------------------------------------------------------------
/src/themes/dark-theme.less:
--------------------------------------------------------------------------------
1 | @import "~antd/lib/style/color/colorPalette.less";
2 | @import "~antd/dist/antd.less";
3 | @import "~antd/lib/style/themes/dark.less";
4 |
5 | // These are shared variables that can be extracted to their own file
6 | @primary-color: #2caad9;
7 | @border-radius-base: 4px;
8 |
9 | @component-background: #212121;
10 | @body-background: #212121;
11 | @popover-background: #212121;
12 | @border-color-base: #6f6c6c;
13 | @border-color-split: #424242;
14 | @table-header-sort-active-bg: #424242;
15 | @card-skeleton-bg: #424242;
16 | @skeleton-color: #424242;
17 | @table-header-sort-active-bg: #424242;
18 |
19 | .highlight {
20 | background-color: #3f3f3f;
21 | }
22 |
--------------------------------------------------------------------------------
/src/themes/light-theme.less:
--------------------------------------------------------------------------------
1 | @import "~antd/lib/style/color/colorPalette.less";
2 | @import "~antd/dist/antd.less";
3 | @import "~antd/lib/style/themes/default.less";
4 |
5 | // These are shared variables that can be extracted to their own file
6 | // @primary-color: #00adb5;
7 | @border-radius-base: 4px;
8 |
9 | .highlight {
10 | background-color: #f9f9f9;
11 | }
12 |
--------------------------------------------------------------------------------
/src/views/ExampleUI.jsx:
--------------------------------------------------------------------------------
1 | import { SyncOutlined } from "@ant-design/icons";
2 | import { utils } from "ethers";
3 | import { Button, Card, DatePicker, Divider, Input, List, Progress, Slider, Spin, Switch } from "antd";
4 | import React, { useState } from "react";
5 | import { Address, Balance } from "../components";
6 |
7 | export default function ExampleUI({
8 | purpose,
9 | setPurposeEvents,
10 | address,
11 | mainnetProvider,
12 | localProvider,
13 | yourLocalBalance,
14 | price,
15 | tx,
16 | readContracts,
17 | writeContracts,
18 | }) {
19 | const [newPurpose, setNewPurpose] = useState("loading...");
20 |
21 | return (
22 |
23 | {/*
24 | ⚙️ Here is an example UI that displays and sets the purpose in your smart contract:
25 | */}
26 |
27 |
Example UI:
28 |
purpose: {purpose}
29 |
30 |
31 | {
33 | setNewPurpose(e.target.value);
34 | }}
35 | />
36 | {
39 | /* look how you call setPurpose on your contract: */
40 | /* notice how you pass a call back for tx updates too */
41 | const result = tx(writeContracts.YourContract.setPurpose(newPurpose), update => {
42 | console.log("📡 Transaction Update:", update);
43 | if (update && (update.status === "confirmed" || update.status === 1)) {
44 | console.log(" 🍾 Transaction " + update.hash + " finished!");
45 | console.log(
46 | " ⛽️ " +
47 | update.gasUsed +
48 | "/" +
49 | (update.gasLimit || update.gas) +
50 | " @ " +
51 | parseFloat(update.gasPrice) / 1000000000 +
52 | " gwei",
53 | );
54 | }
55 | });
56 | console.log("awaiting metamask/web3 confirm result...", result);
57 | console.log(await result);
58 | }}
59 | >
60 | Set Purpose!
61 |
62 |
63 |
64 | Your Address:
65 |
66 |
67 | ENS Address Example:
68 |
73 |
74 | {/* use utils.formatEther to display a BigNumber: */}
75 |
Your Balance: {yourLocalBalance ? utils.formatEther(yourLocalBalance) : "..."}
76 |
OR
77 |
78 |
79 |
🐳 Example Whale Balance:
80 |
81 |
82 | {/* use utils.formatEther to display a BigNumber: */}
83 |
Your Balance: {yourLocalBalance ? utils.formatEther(yourLocalBalance) : "..."}
84 |
85 | Your Contract Address:
86 |
91 |
92 |
93 | {
95 | /* look how you call setPurpose on your contract: */
96 | tx(writeContracts.YourContract.setPurpose("🍻 Cheers"));
97 | }}
98 | >
99 | Set Purpose to "🍻 Cheers"
100 |
101 |
102 |
103 | {
105 | /*
106 | you can also just craft a transaction and send it to the tx() transactor
107 | here we are sending value straight to the contract's address:
108 | */
109 | tx({
110 | to: writeContracts.YourContract.address,
111 | value: utils.parseEther("0.001"),
112 | });
113 | /* this should throw an error about "no fallback nor receive function" until you add it */
114 | }}
115 | >
116 | Send Value
117 |
118 |
119 |
120 | {
122 | /* look how we call setPurpose AND send some value along */
123 | tx(
124 | writeContracts.YourContract.setPurpose("💵 Paying for this one!", {
125 | value: utils.parseEther("0.001"),
126 | }),
127 | );
128 | /* this will fail until you make the setPurpose function payable */
129 | }}
130 | >
131 | Set Purpose With Value
132 |
133 |
134 |
135 | {
137 | /* you can also just craft a transaction and send it to the tx() transactor */
138 | tx({
139 | to: writeContracts.YourContract.address,
140 | value: utils.parseEther("0.001"),
141 | data: writeContracts.YourContract.interface.encodeFunctionData("setPurpose(string)", [
142 | "🤓 Whoa so 1337!",
143 | ]),
144 | });
145 | /* this should throw an error about "no fallback nor receive function" until you add it */
146 | }}
147 | >
148 | Another Example
149 |
150 |
151 |
152 |
153 | {/*
154 | 📑 Maybe display a list of events?
155 | (uncomment the event and emit line in YourContract.sol! )
156 | */}
157 |
158 |
Events:
159 |
{
163 | return (
164 |
165 |
166 | {item[1]}
167 |
168 | );
169 | }}
170 | />
171 |
172 |
173 |
174 |
175 | Check out all the{" "}
176 |
181 | 📦 components
182 |
183 |
184 |
185 |
186 |
193 |
194 |
195 | Buttons
196 |
197 |
198 |
199 | Icons
200 |
201 |
202 |
203 | Date Pickers?
204 |
205 | {}} />
206 |
207 |
208 |
209 |
210 | {}} />
211 |
212 |
213 |
214 | {}} />
215 |
216 |
217 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 | );
228 | }
229 |
--------------------------------------------------------------------------------
/src/views/Hints.jsx:
--------------------------------------------------------------------------------
1 | import { utils } from "ethers";
2 | import { Select } from "antd";
3 | import React, { useState } from "react";
4 | import { Address, AddressInput } from "../components";
5 | import { useTokenList } from "eth-hooks/dapps/dex";
6 |
7 | const { Option } = Select;
8 |
9 | export default function Hints({ yourLocalBalance, mainnetProvider, price, address }) {
10 | // Get a list of tokens from a tokenlist -> see tokenlists.org!
11 | const [selectedToken, setSelectedToken] = useState("Pick a token!");
12 | const listOfTokens = useTokenList(
13 | "https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json",
14 | );
15 |
16 | return (
17 |
18 |
19 | 👷
20 | Edit your contract in
21 |
25 | packages/hardhat/contracts
26 |
27 |
28 |
29 |
30 | 🛰
31 | compile/deploy with
32 |
36 | yarn run deploy
37 |
38 |
39 |
40 |
41 | 🚀
42 | Your contract artifacts are automatically injected into your frontend at
43 |
47 | packages/react-app/src/contracts/
48 |
49 |
50 |
51 |
52 | 🎛
53 | Edit your frontend in
54 |
58 | packages/reactapp/src/App.js
59 |
60 |
61 |
62 |
63 | 🔭
64 | explore the
65 |
76 | 🖇 hooks
77 |
78 | and
79 |
83 | 📦 components
84 |
85 |
86 |
87 |
88 | for example, the
89 |
93 | useBalance()
94 | {" "}
95 | hook keeps track of your balance: {utils.formatEther(yourLocalBalance || 0)}
96 |
97 |
98 |
99 |
105 |
{
109 | console.log(`selected ${value}`);
110 | setSelectedToken(value);
111 | }}
112 | filterOption={(input, option) => option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
113 | optionFilterProp="children"
114 | >
115 | {listOfTokens.map(token => (
116 |
117 | {token.symbol}
118 |
119 | ))}
120 |
121 |
122 |
123 |
124 | as you build your app you'll need web3 specific components like an
125 |
129 | {" "}
130 |
131 | component:
132 |
135 |
(try putting in your address, an ens address, or scanning a QR code)
136 |
137 |
138 |
139 | this balance could be multiplied by
140 |
144 | price
145 | {" "}
146 | that is loaded with the
147 |
151 | usePrice
152 | {" "}
153 | hook with the current value: ${price}
154 |
155 |
156 |
157 |
💧
158 | use the
faucet to send funds to
159 |
163 | {address}
164 |
165 |
166 |
167 |
168 | 📡
169 | deploy to a testnet or mainnet by editing
170 |
174 | packages/hardhat/hardhat.config.js
175 |
176 | and running
177 |
181 | yarn run deploy
182 |
183 |
184 |
185 |
186 |
🔑
187 |
191 | yarn run generate
192 |
193 | will create a deployer account in
194 |
198 | packages/hardhat
199 |
200 |
201 | (use{" "}
202 |
211 | yarn run account
212 | {" "}
213 | to display deployer address and balance)
214 |
215 |
216 |
217 |
218 | ⚙️
219 | build your app with
220 |
224 | yarn run build
225 |
226 |
227 |
228 |
229 | 🚢
230 | ship it!
231 |
235 | yarn run surge
236 |
237 | or
238 |
242 | yarn run s3
243 |
244 | or
245 |
249 | yarn run ipfs
250 |
251 |
252 |
253 |
265 |
266 | 🛠 Check out your browser's developer console for more... (inspect console) 🚀
267 |
268 |
269 | );
270 | }
271 |
--------------------------------------------------------------------------------
/src/views/Subgraph.jsx:
--------------------------------------------------------------------------------
1 | import { gql, useQuery } from "@apollo/client";
2 | import { Button, Input, Table, Typography } from "antd";
3 | import "antd/dist/antd.css";
4 | import GraphiQL from "graphiql";
5 | import "graphiql/graphiql.min.css";
6 | import fetch from "isomorphic-fetch";
7 | import React, { useState } from "react";
8 | import { Address } from "../components";
9 |
10 | const highlight = {
11 | marginLeft: 4,
12 | marginRight: 8,
13 | /* backgroundColor: "#f9f9f9", */ padding: 4,
14 | borderRadius: 4,
15 | fontWeight: "bolder",
16 | };
17 |
18 | function Subgraph(props) {
19 | function graphQLFetcher(graphQLParams) {
20 | return fetch(props.subgraphUri, {
21 | method: "post",
22 | headers: { "Content-Type": "application/json" },
23 | body: JSON.stringify(graphQLParams),
24 | }).then(response => response.json());
25 | }
26 |
27 | const EXAMPLE_GRAPHQL = `
28 | {
29 | purposes(first: 25, orderBy: createdAt, orderDirection: desc) {
30 | id
31 | purpose
32 | createdAt
33 | sender {
34 | id
35 | }
36 | }
37 | senders {
38 | id
39 | address
40 | purposeCount
41 | }
42 | }
43 | `;
44 | const EXAMPLE_GQL = gql(EXAMPLE_GRAPHQL);
45 | const { loading, data } = useQuery(EXAMPLE_GQL, { pollInterval: 2500 });
46 |
47 | const purposeColumns = [
48 | {
49 | title: "Purpose",
50 | dataIndex: "purpose",
51 | key: "purpose",
52 | },
53 | {
54 | title: "Sender",
55 | key: "id",
56 | render: record => ,
57 | },
58 | {
59 | title: "createdAt",
60 | key: "createdAt",
61 | dataIndex: "createdAt",
62 | render: d => new Date(d * 1000).toISOString(),
63 | },
64 | ];
65 |
66 | const [newPurpose, setNewPurpose] = useState("loading...");
67 |
68 | const deployWarning = (
69 | Warning: 🤔 Have you deployed your subgraph yet?
70 | );
71 |
72 | return (
73 | <>
74 |
75 | You will find that parsing/tracking events with the{" "}
76 |
77 | useEventListener
78 | {" "}
79 | hook becomes a chore for every new project.
80 |
81 |
92 |
93 |
94 | 🚮
95 | Clean up previous data:
96 |
97 | yarn clean-graph-node
98 |
99 |
100 |
101 |
102 |
📡
103 | Spin up a local graph node by running
104 |
105 | yarn run-graph-node
106 |
107 |
108 | {" "}
109 | (requires{" "}
110 |
111 | {" "}
112 | Docker
113 |
114 | ){" "}
115 |
116 |
117 |
118 |
119 | 📝
120 | Create your local subgraph by running
121 |
122 | yarn graph-create-local
123 |
124 | (only required once!)
125 |
126 |
127 |
128 | 🚢
129 | Deploy your local subgraph by running
130 |
131 | yarn graph-ship-local
132 |
133 |
134 |
135 |
136 |
🖍️
137 | Edit your
local subgraph in
138 |
139 | packages/subgraph/src
140 |
141 | (learn more about subgraph definition{" "}
142 |
143 | here
144 |
145 | )
146 |
147 |
148 |
149 | 🤩
150 | Deploy your contracts and your subgraph in one go by running
151 |
152 | yarn deploy-and-graph
153 |
154 |
155 |
156 |
157 |
158 | {
160 | setNewPurpose(e.target.value);
161 | }}
162 | />
163 | {
165 | console.log("newPurpose", newPurpose);
166 | /* look how you call setPurpose on your contract: */
167 | props.tx(props.writeContracts.YourContract.setPurpose(newPurpose));
168 | }}
169 | >
170 | Set Purpose
171 |
172 |
173 |
174 | {data ? (
175 |
176 | ) : (
177 |
{loading ? "Loading..." : deployWarning}
178 | )}
179 |
180 |
181 |
182 |
183 |
184 |
185 | ...
186 | >
187 | );
188 | }
189 |
190 | export default Subgraph;
191 |
--------------------------------------------------------------------------------
/src/views/index.js:
--------------------------------------------------------------------------------
1 | export { default as ExampleUI } from "./ExampleUI";
2 | export { default as Hints } from "./Hints";
3 | export { default as Subgraph } from "./Subgraph";
4 |
--------------------------------------------------------------------------------