├── .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 | ![FBRPC](./public/Screen%20Shot%202023-05-22%20at%2010.10.25%20AM.png) 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 | ![FEE](./public/Screen%20Shot%202023-05-22%20at%2010.17.46%20AM.png) 38 | 39 | A good tip is usually 5-20 GWEI. 40 | 41 | ![FEE2](./public/Screen%20Shot%202023-05-22%20at%2010.18.25%20AM.png) 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 | ![submitted](public/Screen%20Shot%202023-05-21%20at%201.51.37%20PM.png) 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 | ![submitted2](public/Screen%20Shot%202023-05-21%20at%201.26.31%20PM.png) 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 | ![submitted3](public/Screen%20Shot%202023-05-21%20at%202.03.52%20PM.png) 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 | 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 | , 67 | ); 68 | } else { 69 | modalButtons.push( 70 | , 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 |

41 | 42 | 🔄 43 | 44 |

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 | 165 | ) : ( 166 | 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 | 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 | 189 | 198 | 204 | 205 | 206 | 207 | 208 | 217 | 218 | 219 | 220 | 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 |
9 | 10 |
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 | 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 | 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 | 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 | 49 |

, 50 | ); 51 | } 52 | } 53 | 54 | return ( 55 |
56 | 66 | { 70 | setModalUp("down"); 71 | }} 72 | footer={[ 73 | , 81 | ]} 82 | > 83 |

84 | 99 |

100 |

101 | {" "} 102 | 125 |

126 | 127 |

128 | 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 | 423 | 424 | ); 425 | 426 | const swapModal = ( 427 | 428 | 429 | 430 | {tokenIn} 431 | {amountIn} 432 | {tokenIn} 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | {tokenOut} 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 | uniswapLogo 466 | Uniswapper 467 | 468 | } 469 | extra={ 470 | 478 | } 479 | > 480 | 481 | 482 | 488 | {tokenIn} 489 | 501 | 502 | } 503 | style={{ width: 400, textAlign: "left" }} 504 | > 505 | { 511 | setAmountOut(); 512 | setTrades(); 513 | setAmountIn(e); 514 | setExact("in"); 515 | }} 516 | /> 517 | 547 | 548 | 549 | 550 | ")}> 551 | 552 | 553 | 554 | 555 | 561 | {tokenOut} 562 | 563 | 564 | } 565 | style={{ width: 400, textAlign: "left" }} 566 | > 567 | { 573 | setAmountOut(e); 574 | setAmountIn(); 575 | setTrades(); 576 | setExact("out"); 577 | }} 578 | /> 579 | 608 | 609 | 610 | 611 | {priceDescription ? priceWidget : null} 612 | 613 | 614 | 615 | {inputIsToken ? ( 616 | 619 | ) : null} 620 | 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 ? : 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 | 110 | ); 111 | privateKeyButton = ( 112 | 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 | 210 |
211 | ) : ( 212 | "" 213 | )} 214 |
215 | ); 216 | } 217 | 218 | receiveButton = ( 219 | 228 | ); 229 | privateKeyButton = ( 230 | 239 | ); 240 | } else { 241 | const inputStyle = { 242 | padding: 10, 243 | }; 244 | 245 | display = ( 246 |
247 |
248 | 255 |
256 |
257 | { 261 | setAmount(value); 262 | }} 263 | /> 264 |
265 |
266 | ); 267 | receiveButton = ( 268 | 277 | ); 278 | privateKeyButton = ( 279 | 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 | , 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 | 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 | 101 |
102 |
103 | 118 |
119 |
120 | 133 |
134 |
135 | 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 |
187 | There are tons of generic components included from{" "} 188 | 189 | 🐜 ant.design 190 | {" "} 191 | too! 192 |
193 | 194 |
195 | 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 |
218 | 219 |
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 |
100 | useTokenList() can get you an array of tokens from{" "} 101 | 102 | tokenlists.org! 103 | 104 |
105 | 121 |
122 | 123 |
124 | as you build your app you'll need web3 specific components like an 125 | 129 | {""} 130 | 131 | component: 132 |
133 | 134 |
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 |
254 | 💬 255 | for support, join this 256 | 260 | 261 | Telegram Chat 262 | 263 | 264 |
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 |
82 | Instead, you can use{" "} 83 | 84 | The Graph 85 | {" "} 86 | with 🏗 scaffold-eth ( 87 | 88 | learn more 89 | 90 | ): 91 |
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 | 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 | --------------------------------------------------------------------------------