├── .editorconfig
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .gitmodules
├── .gitpod.yml
├── .husky
└── post-checkout
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── docker
├── README.md
└── setup.sh
├── package.json
├── packages
├── hardhat
│ ├── .eslintrc.js
│ ├── contracts
│ │ └── YourContract.sol
│ ├── deploy
│ │ └── 00_deploy_your_contract.js
│ ├── example.env
│ ├── hardhat.config.js
│ ├── package.json
│ ├── scripts
│ │ ├── deploy.js
│ │ ├── publish.js
│ │ └── watch.js
│ └── test
│ │ └── myTest.js
├── react-app
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .prettierrc
│ ├── .sample.env
│ ├── gulpfile.js
│ ├── package.json
│ ├── public
│ │ ├── 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
│ │ ├── Events.jsx
│ │ ├── Faucet.jsx
│ │ ├── GasGauge.jsx
│ │ ├── Header.jsx
│ │ ├── L2Bridge.jsx
│ │ ├── Provider.jsx
│ │ ├── Ramp.jsx
│ │ ├── Swap.jsx
│ │ ├── ThemeSwitch.jsx
│ │ ├── Timeline.jsx
│ │ ├── TokenBalance.jsx
│ │ ├── Wallet.jsx
│ │ └── index.js
│ │ ├── constants.js
│ │ ├── contracts
│ │ └── external_contracts.js
│ │ ├── ethereumLogo.png
│ │ ├── helpers
│ │ ├── Transactor.js
│ │ ├── index.js
│ │ └── loadAppContracts.js
│ │ ├── hooks
│ │ ├── Debounce.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
├── services
│ ├── graph-node
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── bin
│ │ │ ├── create
│ │ │ ├── debug
│ │ │ ├── deploy
│ │ │ ├── reassign
│ │ │ └── remove
│ │ ├── build.sh
│ │ ├── cloudbuild.yaml
│ │ ├── docker-compose.yml
│ │ ├── hooks
│ │ │ └── post_checkout
│ │ ├── setup.sh
│ │ ├── start
│ │ ├── tag.sh
│ │ └── wait_for
│ └── package.json
└── subgraph
│ ├── package.json
│ └── src
│ ├── mapping.ts
│ ├── schema.graphql
│ └── subgraph.template.yaml
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [packages/**.js{,x}]
4 | indent_style = space
5 | indent_size = 2
6 |
7 | [*.{sol,yul}]
8 | indent_style = space
9 | indent_size = 4
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | packages/subgraph/subgraph.yaml
2 | packages/subgraph/generated
3 | packages/subgraph/abis/*
4 | packages/hardhat/*.txt
5 | **/aws.json
6 |
7 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
8 | **/node_modules
9 |
10 | packages/hardhat/artifacts*
11 | packages/hardhat/deployments
12 | packages/react-app/src/contracts/*
13 | !packages/react-app/src/contracts/external_contracts.js
14 | packages/hardhat/cache*
15 | packages/**/data
16 | !packages/react-app/src/contracts/contracts.js
17 |
18 |
19 | # ts
20 | packages/hardhat-ts/cache
21 | # secrets
22 | .secret
23 |
24 | packages/subgraph/config/config.json
25 | tenderly.yaml
26 |
27 | # dependencies
28 | /node_modules
29 | /.pnp
30 | .pnp.js
31 |
32 | # testing
33 | coverage
34 |
35 | # production
36 | build
37 | # yarn / eslint
38 | .yarn/cache
39 | .yarn/install-state.gz
40 | .yarn/build-state.yml
41 | .eslintcache
42 | # testing
43 | coverage
44 |
45 | # production
46 | build
47 |
48 | # Hardhat files
49 | cache
50 | artifacts
51 |
52 | # misc
53 | .DS_Store
54 | .env*
55 |
56 | # debug
57 | npm-debug.log*
58 | yarn-debug.log*
59 | yarn-error.log*
60 |
61 | .idea
62 |
63 | # Local Netlify folder
64 | .netlify
65 | *.tsbuildinfo
66 | *.stackdump
67 |
68 | # doc directory
69 | /doc
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "packages/services/arbitrum"]
2 | path = packages/services/arbitrum
3 | url = https://github.com/OffchainLabs/arbitrum
4 | branch = master
5 | [submodule "packages/services/optimism"]
6 | path = packages/services/optimism
7 | url = https://github.com/ethereum-optimism/optimism
8 | branch = regenesis/0.4.0
9 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | tasks:
2 | - name: App
3 | init: >
4 | yarn &&
5 | gp sync-done install
6 | command: REACT_APP_PROVIDER=$(gp url 8545) yarn start
7 | - name: Chain
8 | init: gp sync-await install
9 | command: yarn chain
10 | openMode: split-right
11 | - name: Deployment
12 | init: gp sync-await install
13 | command: yarn deploy
14 | openMode: split-right
15 | ports:
16 | - port: 3000
17 | onOpen: open-preview
18 | - port: 8545
19 | onOpen: ignore
20 | github:
21 | prebuilds:
22 | pullRequestsFromForks: true
23 | addComment: true
24 | vscode:
25 | extensions:
26 | - dbaeumer.vscode-eslint
27 | - esbenp.prettier-vscode
28 | - juanblanco.solidity
--------------------------------------------------------------------------------
/.husky/post-checkout:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn install
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "solidity.packageDefaultDependenciesDirectory": "node_modules/@scaffold-eth/hardhat/node_modules/",
3 | "solidity-va.test.defaultUnittestTemplate": "hardhat",
4 | "files.exclude": {
5 | "**/.git": true,
6 | "**/.svn": true,
7 | "**/.hg": true,
8 | "**/CVS": true,
9 | "**/.DS_Store": true,
10 | "**/.cache": true,
11 | "**/.vs/": true,
12 | "**/*.cs": true,
13 | "**/*.orig": true,
14 | "**/bin/": true,
15 | "**/build/": true,
16 | "**/debug/": true,
17 | "**/dist/": true,
18 | "**/node_modules/": true,
19 | "**/obj": true,
20 | "yarn-error.log": true,
21 | "**/yarn-error.log": true,
22 | "packages\\eth-hooks/lib": true
23 | },
24 | "explorerExclude.backup": null,
25 | "eslint.workingDirectories": [
26 |
27 | { "directory": "packages/eth-hooks", "changeProcessCWD": true },
28 | { "directory": "packages/hardhat-ts", "changeProcessCWD": true },
29 | { "directory": "packages/vite-app-ts", "changeProcessCWD": true },
30 | ],
31 | "search.exclude": {
32 | "**/yarn-error.log": true,
33 | "**/yarn.lock": true
34 | }
35 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Austin Griffith 2021
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🏗 Scaffold-ETH
2 |
3 | > everything you need to build on Ethereum! 🚀
4 |
5 | 🧪 Quickly experiment with Solidity using a frontend that adapts to your smart contract:
6 |
7 | 
8 |
9 |
10 | # 🏄♂️ Quick Start
11 |
12 | Prerequisites: [Node](https://nodejs.org/en/download/) plus [Yarn](https://classic.yarnpkg.com/en/docs/install/) and [Git](https://git-scm.com/downloads)
13 |
14 | > clone/fork 🏗 scaffold-eth:
15 |
16 | ```bash
17 | git clone https://github.com/scaffold-eth/scaffold-eth-examples.git
18 | ```
19 |
20 | > install and start your 👷 Hardhat chain:
21 |
22 | ```bash
23 | cd scaffold-eth-examples
24 | yarn install
25 | yarn chain
26 | ```
27 |
28 | > in a second terminal window, start your 📱 frontend:
29 |
30 | ```bash
31 | cd scaffold-eth-examples
32 | yarn start
33 | ```
34 |
35 | > in a third terminal window, 🛰 deploy your contract:
36 |
37 | ```bash
38 | cd scaffold-eth-examples
39 | yarn deploy
40 | ```
41 |
42 | 🔏 Edit your smart contract `YourContract.sol` in `packages/hardhat/contracts`
43 |
44 | 📝 Edit your frontend `App.jsx` in `packages/react-app/src`
45 |
46 | 💼 Edit your deployment scripts in `packages/hardhat/deploy`
47 |
48 | 📱 Open http://localhost:3000 to see the app
49 |
50 | # 📚 Documentation
51 |
52 | Documentation, tutorials, challenges, and many more resources, visit: [docs.scaffoldeth.io](https://docs.scaffoldeth.io)
53 |
54 | # 🔭 Learning Solidity
55 |
56 | 📕 Read the docs: https://docs.soliditylang.org
57 |
58 | 📚 Go through each topic from [solidity by example](https://solidity-by-example.org) editing `YourContract.sol` in **🏗 scaffold-eth**
59 |
60 | - [Primitive Data Types](https://solidity-by-example.org/primitives/)
61 | - [Mappings](https://solidity-by-example.org/mapping/)
62 | - [Structs](https://solidity-by-example.org/structs/)
63 | - [Modifiers](https://solidity-by-example.org/function-modifier/)
64 | - [Events](https://solidity-by-example.org/events/)
65 | - [Inheritance](https://solidity-by-example.org/inheritance/)
66 | - [Payable](https://solidity-by-example.org/payable/)
67 | - [Fallback](https://solidity-by-example.org/fallback/)
68 |
69 | 📧 Learn the [Solidity globals and units](https://solidity.readthedocs.io/en/v0.6.6/units-and-global-variables.html)
70 |
71 | # 🛠 Buidl
72 |
73 | Check out all the [active branches](https://github.com/austintgriffith/scaffold-eth/branches/active), [open issues](https://github.com/austintgriffith/scaffold-eth/issues), and join/fund the 🏰 [BuidlGuidl](https://BuidlGuidl.com)!
74 |
75 |
76 | - 🚤 [Follow the full Ethereum Speed Run](https://medium.com/@austin_48503/%EF%B8%8Fethereum-dev-speed-run-bd72bcba6a4c)
77 |
78 |
79 | - 🎟 [Create your first NFT](https://github.com/scaffold-eth/scaffold-eth-challenges/tree/challenge-0-simple-nft)
80 | - 🥩 [Build a staking smart contract](https://github.com/austintgriffith/scaffold-eth/tree/challenge-1-decentralized-staking)
81 | - 🏵 [Deploy a token and vendor](https://github.com/austintgriffith/scaffold-eth/tree/challenge-2-token-vendor)
82 | - 🎫 [Extend the NFT example to make a "buyer mints" marketplace](https://github.com/austintgriffith/scaffold-eth/tree/buyer-mints-nft)
83 | - 🎲 [Learn about commit/reveal](https://github.com/austintgriffith/scaffold-eth/tree/commit-reveal-with-frontend)
84 | - ✍️ [Learn how ecrecover works](https://github.com/austintgriffith/scaffold-eth/tree/signature-recover)
85 | - 👩👩👧👧 [Build a multi-sig that uses off-chain signatures](https://github.com/austintgriffith/scaffold-eth/tree/meta-multi-sig)
86 | - ⏳ [Extend the multi-sig to stream ETH](https://github.com/austintgriffith/scaffold-eth/tree/streaming-meta-multi-sig)
87 | - ⚖️ [Learn how a simple DEX works](https://medium.com/@austin_48503/%EF%B8%8F-minimum-viable-exchange-d84f30bd0c90)
88 | - 🦍 [Ape into learning!](https://github.com/austintgriffith/scaffold-eth/tree/aave-ape)
89 |
90 | # 💬 Support Chat
91 |
92 | Join the telegram [support chat 💬](https://t.me/joinchat/KByvmRe5wkR-8F_zz6AjpA) to ask questions and find others building with 🏗 scaffold-eth!
93 |
94 | ---
95 |
96 | 🙏 Please check out our [Gitcoin grant](https://gitcoin.co/grants/2851/scaffold-eth) too!
97 |
--------------------------------------------------------------------------------
/docker/README.md:
--------------------------------------------------------------------------------
1 | # 🏄♂️ Using Docker
2 |
3 | Prerequisite: [Docker](https://docs.docker.com/engine/install/)/)
4 |
5 | > clone/fork 🏗 scaffold-eth:
6 |
7 | ```bash
8 | git clone https://github.com/austintgriffith/scaffold-eth.git
9 | ```
10 |
11 | > [basic] run the script that sets the stack up and that's it (takes some minutes to finish):
12 |
13 | ```bash
14 | cd scaffold-eth
15 | ./docker/setup.sh start
16 | ```
17 |
18 | > [basic] to re-deploy your contracts (container must be up and running):
19 |
20 | ```bash
21 | ./docker/setup.sh deploy
22 | ```
23 |
24 | > [advanced] running front-end on a different port (eg. 8080):
25 |
26 | ```bash
27 | docker rm -f SCAFFOLD_ETH
28 |
29 | docker run \
30 | --name SCAFFOLD_ETH \
31 | -v `pwd`:/opt/scaffold-eth \
32 | -w /opt/scaffold-eth \
33 | -e PORT=8080 \
34 | -p 8080:8080 \
35 | -p 8545:8545 \
36 | -dt node:16
37 |
38 | ./docker/setup.sh start
39 | ```
40 |
41 | > [advanced] running the container in interactive mode (must run each tool manually):
42 |
43 | ```bash
44 | docker rm -f SCAFFOLD_ETH
45 |
46 | docker run \
47 | --name SCAFFOLD_ETH \
48 | -v `pwd`:/opt/scaffold-eth \
49 | -w /opt/scaffold-eth \
50 | -p 3000:3000 \
51 | -p 8545:8545 \
52 | --entrypoint /bin/bash \
53 | -ti node:16
54 | ```
55 |
56 | 🔏 Edit your smart contract `YourContract.sol` in `packages/hardhat/contracts`
57 |
58 | 📝 Edit your frontend `App.jsx` in `packages/react-app/src`
59 |
60 | 💼 Edit your deployment scripts in `packages/hardhat/deploy`
61 |
62 | 📱 Open http://localhost:3000 to see the app
63 |
--------------------------------------------------------------------------------
/docker/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ "$1" = "start" ]; then
4 | # Run Docker container
5 | # to run the frontend on a different port add the "-e PORT=8080" parameter and change "-p 8080:8080" one.
6 | docker restart SCAFFOLD_ETH || docker run \
7 | --name SCAFFOLD_ETH \
8 | -v `pwd`:/opt/scaffold-eth \
9 | -w /opt/scaffold-eth \
10 | -p 3000:3000 \
11 | -p 8545:8545 \
12 | -dt node:16
13 |
14 | docker exec -ti SCAFFOLD_ETH bash -c "yarn install"
15 | docker exec -dt SCAFFOLD_ETH bash -c "yarn chain"
16 | sleep 5
17 | docker exec -ti SCAFFOLD_ETH bash -c "yarn deploy"
18 | docker exec -dt SCAFFOLD_ETH bash -c "yarn start"
19 | else
20 | if [ "$1" = "deploy" ]; then
21 | docker exec -ti SCAFFOLD_ETH bash -c "yarn deploy"
22 | else
23 | echo "Invalid command. Choose 'start' or 'deploy'."
24 | fi
25 | fi
26 |
27 |
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@scaffold-eth/monorepo",
3 | "version": "1.0.0",
4 | "keywords": [
5 | "ethereum",
6 | "react",
7 | "uniswap",
8 | "workspaces",
9 | "yarn"
10 | ],
11 | "private": true,
12 | "scripts": {
13 | "react-app:build": "yarn workspace @scaffold-eth/react-app build --max-old-space-size=12288",
14 | "react-app:eject": "yarn workspace @scaffold-eth/react-app eject",
15 | "react-app:start": "yarn workspace @scaffold-eth/react-app start",
16 | "react-app:test": "yarn workspace @scaffold-eth/react-app test",
17 | "build": "yarn workspace @scaffold-eth/react-app build --max-old-space-size=12288",
18 | "prettier": "yarn workspace @scaffold-eth/react-app prettier",
19 | "chain": "yarn workspace @scaffold-eth/hardhat chain",
20 | "fork": "yarn workspace @scaffold-eth/hardhat fork",
21 | "node": "yarn workspace @scaffold-eth/hardhat chain",
22 | "test": "yarn workspace @scaffold-eth/hardhat test",
23 | "start": "yarn workspace @scaffold-eth/react-app start",
24 | "compile": "yarn workspace @scaffold-eth/hardhat compile",
25 | "deploy": "yarn workspace @scaffold-eth/hardhat deploy",
26 | "watch": "yarn workspace @scaffold-eth/hardhat watch",
27 | "accounts": "yarn workspace @scaffold-eth/hardhat accounts",
28 | "balance": "yarn workspace @scaffold-eth/hardhat balance",
29 | "send": "yarn workspace @scaffold-eth/hardhat send",
30 | "ipfs": "yarn workspace @scaffold-eth/react-app ipfs",
31 | "surge": "yarn workspace @scaffold-eth/react-app surge",
32 | "s3": "yarn workspace @scaffold-eth/react-app s3",
33 | "ship": "yarn workspace @scaffold-eth/react-app ship",
34 | "generate": "yarn workspace @scaffold-eth/hardhat generate",
35 | "account": "yarn workspace @scaffold-eth/hardhat account",
36 | "mineContractAddress": "cd packages/hardhat && npx hardhat mineContractAddress",
37 | "wallet": "cd packages/hardhat && npx hardhat wallet",
38 | "fundedwallet": "cd packages/hardhat && npx hardhat fundedwallet",
39 | "flatten": "cd packages/hardhat && npx hardhat flatten",
40 | "clean": "cd packages/hardhat && npx hardhat clean",
41 | "run-graph-node": "yarn workspace @scaffold-eth/services run-graph-node",
42 | "remove-graph-node": "yarn workspace @scaffold-eth/services remove-graph-node",
43 | "clean-graph-node": "yarn workspace @scaffold-eth/services clean-graph-node",
44 | "graph-prepare": "mustache packages/subgraph/config/config.json packages/subgraph/src/subgraph.template.yaml > packages/subgraph/subgraph.yaml",
45 | "graph-codegen": "yarn workspace @scaffold-eth/subgraph graph codegen",
46 | "graph-build": "yarn workspace @scaffold-eth/subgraph graph build",
47 | "graph-create-local": "yarn workspace @scaffold-eth/subgraph graph create --node http://localhost:8020/ scaffold-eth/your-contract",
48 | "graph-remove-local": "yarn workspace @scaffold-eth/subgraph graph remove --node http://localhost:8020/ scaffold-eth/your-contract",
49 | "graph-deploy-local": "yarn workspace @scaffold-eth/subgraph graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 scaffold-eth/your-contract",
50 | "graph-ship-local": "yarn graph-prepare && yarn graph-codegen && yarn graph-deploy-local",
51 | "deploy-and-graph": "yarn deploy && yarn graph-ship-local",
52 | "theme": "yarn workspace @scaffold-eth/react-app theme",
53 | "watch-theme": "yarn workspace @scaffold-eth/react-app watch",
54 | "postinstall": "husky install"
55 | },
56 | "workspaces": {
57 | "packages": [
58 | "packages/*"
59 | ],
60 | "nohoist": [
61 | "**/@graphprotocol/graph-ts",
62 | "**/@graphprotocol/graph-ts/**",
63 | "**/hardhat",
64 | "**/hardhat/**",
65 | "**/hardhat-ts",
66 | "**/hardhat-ts/**"
67 | ]
68 | },
69 | "dependencies": {},
70 | "devDependencies": {
71 | "husky": "^7.0.2"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/packages/hardhat/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | mocha: true,
4 | },
5 | extends: ["airbnb", "plugin:prettier/recommended"],
6 | plugins: ["babel"],
7 | rules: {
8 | "prettier/prettier": ["error"],
9 | "import/extensions": [
10 | "error",
11 | "ignorePackages",
12 | {
13 | js: "never",
14 | ts: "never",
15 | },
16 | ],
17 | "import/prefer-default-export": "off",
18 | "prefer-destructuring": "off",
19 | "prefer-template": "off",
20 | "no-console": "off",
21 | "func-names": "off",
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/packages/hardhat/contracts/YourContract.sol:
--------------------------------------------------------------------------------
1 | pragma solidity >=0.8.0 <0.9.0;
2 | //SPDX-License-Identifier: MIT
3 |
4 | import "hardhat/console.sol";
5 | // import "@openzeppelin/contracts/access/Ownable.sol";
6 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol
7 |
8 | contract YourContract {
9 |
10 | // event SetPurpose(address sender, string purpose);
11 |
12 | string public purpose = "Building Unstoppable Apps!!!";
13 |
14 | constructor() {
15 | // what should we do on deploy?
16 | }
17 |
18 | function setPurpose(string memory newPurpose) public {
19 | purpose = newPurpose;
20 | console.log(msg.sender,"set purpose to",purpose);
21 | // emit SetPurpose(msg.sender, purpose);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/hardhat/deploy/00_deploy_your_contract.js:
--------------------------------------------------------------------------------
1 | // deploy/00_deploy_your_contract.js
2 |
3 | const { ethers } = require("hardhat");
4 |
5 | const localChainId = "31337";
6 |
7 | module.exports = async ({ getNamedAccounts, deployments, getChainId }) => {
8 | const { deploy } = deployments;
9 | const { deployer } = await getNamedAccounts();
10 | const chainId = await getChainId();
11 |
12 | await deploy("YourContract", {
13 | // Learn more about args here: https://www.npmjs.com/package/hardhat-deploy#deploymentsdeploy
14 | from: deployer,
15 | // args: [ "Hello", ethers.utils.parseEther("1.5") ],
16 | log: true,
17 | });
18 |
19 | // Getting a previously deployed contract
20 | const YourContract = await ethers.getContract("YourContract", deployer);
21 | /* await YourContract.setPurpose("Hello");
22 |
23 | To take ownership of yourContract using the ownable library uncomment next line and add the
24 | address you want to be the owner.
25 | // yourContract.transferOwnership(YOUR_ADDRESS_HERE);
26 |
27 | //const yourContract = await ethers.getContractAt('YourContract', "0xaAC799eC2d00C013f1F11c37E654e59B0429DF6A") //<-- if you want to instantiate a version of a contract at a specific address!
28 | */
29 |
30 | /*
31 | //If you want to send value to an address from the deployer
32 | const deployerWallet = ethers.provider.getSigner()
33 | await deployerWallet.sendTransaction({
34 | to: "0x34aA3F359A9D614239015126635CE7732c18fDF3",
35 | value: ethers.utils.parseEther("0.001")
36 | })
37 | */
38 |
39 | /*
40 | //If you want to send some ETH to a contract on deploy (make your constructor payable!)
41 | const yourContract = await deploy("YourContract", [], {
42 | value: ethers.utils.parseEther("0.05")
43 | });
44 | */
45 |
46 | /*
47 | //If you want to link a library into your contract:
48 | // reference: https://github.com/austintgriffith/scaffold-eth/blob/using-libraries-example/packages/hardhat/scripts/deploy.js#L19
49 | const yourContract = await deploy("YourContract", [], {}, {
50 | LibraryName: **LibraryAddress**
51 | });
52 | */
53 |
54 | // Verify your contracts with Etherscan
55 | // You don't want to verify on localhost
56 | if (chainId !== localChainId) {
57 | await run("verify:verify", {
58 | address: YourContract.address,
59 | contract: "contracts/YourContract.sol:YourContract",
60 | contractArguments: [],
61 | });
62 | }
63 | };
64 | module.exports.tags = ["YourContract"];
65 |
--------------------------------------------------------------------------------
/packages/hardhat/example.env:
--------------------------------------------------------------------------------
1 | # This is a template for the environment variables you'll need for
2 | # deployment on Rinkeby and mainnet
3 |
4 | # To use, copy this file, rename it .env, and fill in the values
5 | # Everything will be interpreted as a string by JavaScript even though
6 | # you should not wrap values in quotation marks
7 |
8 | # Infura endpoints should be passed in WITHOUT the rest of the url
9 | # Private keys should be pasted WITHOUT the 0x prefix
10 |
11 | # Example
12 | EXAMPLE_INFURA_KEY=582dabbadbeef8...
13 | EXAMPLE_DEPLOYER_PRIV_KEY=deadbeef01EEdf972aBB...
14 |
15 | # Rinkeby
16 | RINKEBY_INFURA_KEY=
17 | RINKEBY_DEPLOYER_PRIV_KEY=
18 |
19 | # Kovan
20 | KOVAN_INFURA_KEY=
21 | KOVAN_DEPLOYER_PRIV_KEY=
22 |
23 | # mainnet
24 | MAINNET_INFURA_KEY=
25 | MAINNET_DEPLOYER_PRIV_KEY=
26 |
27 | # Ropsten
28 | ROPSTEN_INFURA_KEY=
29 | ROPSTEN_DEPLOYER_PRIV_KEY=
30 |
31 | # Goerli
32 | GOERLI_INFURA_KEY=
33 | GOERLI_DEPLOYER_PRIV_KEY=
34 |
35 | # xDai
36 | XDAI_DEPLOYER_PRIV_KEY=
--------------------------------------------------------------------------------
/packages/hardhat/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@scaffold-eth/hardhat",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "devDependencies": {
7 | "eslint": "^7.5.0",
8 | "eslint-config-airbnb": "^18.2.0",
9 | "eslint-config-prettier": "^6.11.0",
10 | "eslint-plugin-babel": "^5.3.1",
11 | "eslint-plugin-prettier": "^3.4.0"
12 | },
13 | "dependencies": {
14 | "@eth-optimism/hardhat-ovm": "^0.2.2",
15 | "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers",
16 | "@nomiclabs/hardhat-waffle": "^2.0.0",
17 | "@openzeppelin/contracts": "^4.3.3",
18 | "@tenderly/hardhat-tenderly": "^1.0.10",
19 | "@nomiclabs/hardhat-etherscan": "^2.1.7",
20 | "chai": "^4.2.0",
21 | "chalk": "^4.1.0",
22 | "dotenv": "^8.2.0",
23 | "ethereum-waffle": "^3.1.1",
24 | "ethers": "^5.4.4",
25 | "hardhat": "2.6.0",
26 | "hardhat-deploy": "^0.9.0",
27 | "hardhat-gas-reporter": "^1.0.4",
28 | "node-watch": "^0.7.0",
29 | "qrcode-terminal": "^0.12.0",
30 | "ramda": "^0.27.1"
31 | },
32 | "scripts": {
33 | "chain": "hardhat node --network hardhat --no-deploy",
34 | "fork": "hardhat node --no-deploy --network hardhat --fork https://mainnet.infura.io/v3/460f40a260564ac4a4f4b3fffb032dad",
35 | "test": "hardhat test --network hardhat",
36 | "compile": "hardhat compile",
37 | "deploy": "hardhat deploy --export-all ../react-app/src/contracts/hardhat_contracts.json",
38 | "postdeploy": "hardhat run scripts/publish.js",
39 | "watch": "node scripts/watch.js",
40 | "accounts": "hardhat accounts",
41 | "balance": "hardhat balance",
42 | "send": "hardhat send",
43 | "generate": "hardhat generate",
44 | "account": "hardhat account",
45 | "etherscan-verify": "hardhat etherscan-verify --api-key PSW8C433Q667DVEX5BCRMGNAH9FSGFZ7Q8"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/hardhat/scripts/deploy.js:
--------------------------------------------------------------------------------
1 | /* eslint no-use-before-define: "warn" */
2 | const fs = require("fs");
3 | const chalk = require("chalk");
4 | const { config, ethers, tenderly, run } = require("hardhat");
5 | const { utils } = require("ethers");
6 | const R = require("ramda");
7 |
8 | /*
9 |
10 | _______ _________ _______ _______
11 | ( ____ \\__ __/( ___ )( ____ )
12 | | ( \/ ) ( | ( ) || ( )|
13 | | (_____ | | | | | || (____)|
14 | (_____ ) | | | | | || _____)
15 | ) | | | | | | || (
16 | /\____) | | | | (___) || )
17 | \_______) )_( (_______)|/
18 |
19 | This deploy script is no longer in use, but is left for reference purposes!
20 |
21 | scaffold-eth now uses hardhat-deploy to manage deployments, see the /deploy folder
22 | And learn more here: https://www.npmjs.com/package/hardhat-deploy
23 |
24 | */
25 |
26 | const main = async () => {
27 | console.log("\n\n 📡 Deploying...\n");
28 |
29 | const yourContract = await deploy("YourContract"); // <-- add in constructor args like line 19 vvvv
30 | // use for local token bridging
31 | // const mockToken = await deploy("MockERC20") // <-- add in constructor args like line 19 vvvv
32 |
33 | //const yourContract = await ethers.getContractAt('YourContract', "0xaAC799eC2d00C013f1F11c37E654e59B0429DF6A") //<-- if you want to instantiate a version of a contract at a specific address!
34 | //const secondContract = await deploy("SecondContract")
35 |
36 | // const exampleToken = await deploy("ExampleToken")
37 | // const examplePriceOracle = await deploy("ExamplePriceOracle")
38 | // const smartContractWallet = await deploy("SmartContractWallet",[exampleToken.address,examplePriceOracle.address])
39 |
40 | /*
41 | //If you want to send value to an address from the deployer
42 | const deployerWallet = ethers.provider.getSigner()
43 | await deployerWallet.sendTransaction({
44 | to: "0x34aA3F359A9D614239015126635CE7732c18fDF3",
45 | value: ethers.utils.parseEther("0.001")
46 | })
47 | */
48 |
49 | /*
50 | //If you want to send some ETH to a contract on deploy (make your constructor payable!)
51 | const yourContract = await deploy("YourContract", [], {
52 | value: ethers.utils.parseEther("0.05")
53 | });
54 | */
55 |
56 | /*
57 | //If you want to link a library into your contract:
58 | // reference: https://github.com/austintgriffith/scaffold-eth/blob/using-libraries-example/packages/hardhat/scripts/deploy.js#L19
59 | const yourContract = await deploy("YourContract", [], {}, {
60 | LibraryName: **LibraryAddress**
61 | });
62 | */
63 |
64 | //If you want to verify your contract on tenderly.co (see setup details in the scaffold-eth README!)
65 | /*
66 | await tenderlyVerify(
67 | {contractName: "YourContract",
68 | contractAddress: yourContract.address
69 | })
70 | */
71 |
72 | console.log(
73 | " 💾 Artifacts (address, abi, and args) saved to: ",
74 | chalk.blue("packages/hardhat/artifacts/"),
75 | "\n\n"
76 | );
77 | };
78 |
79 | const deploy = async (
80 | contractName,
81 | _args = [],
82 | overrides = {},
83 | libraries = {}
84 | ) => {
85 | console.log(` 🛰 Deploying: ${contractName}`);
86 |
87 | const contractArgs = _args || [];
88 | const contractArtifacts = await ethers.getContractFactory(contractName, {
89 | libraries: libraries,
90 | });
91 | const deployed = await contractArtifacts.deploy(...contractArgs, overrides);
92 | const encoded = abiEncodeArgs(deployed, contractArgs);
93 | fs.writeFileSync(`artifacts/${contractName}.address`, deployed.address);
94 |
95 | let extraGasInfo = "";
96 | if (deployed && deployed.deployTransaction) {
97 | const gasUsed = deployed.deployTransaction.gasLimit.mul(
98 | deployed.deployTransaction.gasPrice
99 | );
100 | extraGasInfo = `${utils.formatEther(gasUsed)} ETH, tx hash ${
101 | deployed.deployTransaction.hash
102 | }`;
103 | }
104 |
105 | console.log(
106 | " 📄",
107 | chalk.cyan(contractName),
108 | "deployed to:",
109 | chalk.magenta(deployed.address)
110 | );
111 | console.log(" ⛽", chalk.grey(extraGasInfo));
112 |
113 | await tenderly.persistArtifacts({
114 | name: contractName,
115 | address: deployed.address,
116 | });
117 |
118 | if (!encoded || encoded.length <= 2) return deployed;
119 | fs.writeFileSync(`artifacts/${contractName}.args`, encoded.slice(2));
120 |
121 | return deployed;
122 | };
123 |
124 | // ------ utils -------
125 |
126 | // abi encodes contract arguments
127 | // useful when you want to manually verify the contracts
128 | // for example, on Etherscan
129 | const abiEncodeArgs = (deployed, contractArgs) => {
130 | // not writing abi encoded args if this does not pass
131 | if (
132 | !contractArgs ||
133 | !deployed ||
134 | !R.hasPath(["interface", "deploy"], deployed)
135 | ) {
136 | return "";
137 | }
138 | const encoded = utils.defaultAbiCoder.encode(
139 | deployed.interface.deploy.inputs,
140 | contractArgs
141 | );
142 | return encoded;
143 | };
144 |
145 | // checks if it is a Solidity file
146 | const isSolidity = (fileName) =>
147 | fileName.indexOf(".sol") >= 0 &&
148 | fileName.indexOf(".swp") < 0 &&
149 | fileName.indexOf(".swap") < 0;
150 |
151 | const readArgsFile = (contractName) => {
152 | let args = [];
153 | try {
154 | const argsFile = `./contracts/${contractName}.args`;
155 | if (!fs.existsSync(argsFile)) return args;
156 | args = JSON.parse(fs.readFileSync(argsFile));
157 | } catch (e) {
158 | console.log(e);
159 | }
160 | return args;
161 | };
162 |
163 | function sleep(ms) {
164 | return new Promise((resolve) => setTimeout(resolve, ms));
165 | }
166 |
167 | // If you want to verify on https://tenderly.co/
168 | const tenderlyVerify = async ({ contractName, contractAddress }) => {
169 | let tenderlyNetworks = [
170 | "kovan",
171 | "goerli",
172 | "mainnet",
173 | "rinkeby",
174 | "ropsten",
175 | "matic",
176 | "mumbai",
177 | "xDai",
178 | "POA",
179 | ];
180 | let targetNetwork = process.env.HARDHAT_NETWORK || config.defaultNetwork;
181 |
182 | if (tenderlyNetworks.includes(targetNetwork)) {
183 | console.log(
184 | chalk.blue(
185 | ` 📁 Attempting tenderly verification of ${contractName} on ${targetNetwork}`
186 | )
187 | );
188 |
189 | await tenderly.persistArtifacts({
190 | name: contractName,
191 | address: contractAddress,
192 | });
193 |
194 | let verification = await tenderly.verify({
195 | name: contractName,
196 | address: contractAddress,
197 | network: targetNetwork,
198 | });
199 |
200 | return verification;
201 | } else {
202 | console.log(
203 | chalk.grey(` 🧐 Contract verification not supported on ${targetNetwork}`)
204 | );
205 | }
206 | };
207 |
208 | main()
209 | .then(() => process.exit(0))
210 | .catch((error) => {
211 | console.error(error);
212 | process.exit(1);
213 | });
214 |
--------------------------------------------------------------------------------
/packages/hardhat/scripts/publish.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const chalk = require("chalk");
3 |
4 | const graphDir = "../subgraph";
5 | const deploymentsDir = "./deployments";
6 | const publishDir = "../react-app/src/contracts";
7 |
8 | function publishContract(contractName, networkName) {
9 | try {
10 | let contract = fs
11 | .readFileSync(`${deploymentsDir}/${networkName}/${contractName}.json`)
12 | .toString();
13 | contract = JSON.parse(contract);
14 | const graphConfigPath = `${graphDir}/config/config.json`;
15 | let graphConfig;
16 | try {
17 | if (fs.existsSync(graphConfigPath)) {
18 | graphConfig = fs.readFileSync(graphConfigPath).toString();
19 | } else {
20 | graphConfig = "{}";
21 | }
22 | } catch (e) {
23 | console.log(e);
24 | }
25 |
26 | graphConfig = JSON.parse(graphConfig);
27 | graphConfig[`${networkName}_${contractName}Address`] = contract.address;
28 |
29 | const folderPath = graphConfigPath.replace("/config.json", "");
30 | if (!fs.existsSync(folderPath)) {
31 | fs.mkdirSync(folderPath);
32 | }
33 | fs.writeFileSync(graphConfigPath, JSON.stringify(graphConfig, null, 2));
34 | if (!fs.existsSync(`${graphDir}/abis`)) fs.mkdirSync(`${graphDir}/abis`);
35 | fs.writeFileSync(
36 | `${graphDir}/abis/${networkName}_${contractName}.json`,
37 | JSON.stringify(contract.abi, null, 2)
38 | );
39 |
40 | //Hardhat Deploy writes a file with all ABIs in react-app/src/contracts/contracts.json
41 | //If you need the bytecodes and/or you want one file per ABIs, un-comment the following block.
42 | //Write the contracts ABI, address and bytecodes in case the front-end needs them
43 | // fs.writeFileSync(
44 | // `${publishDir}/${contractName}.address.js`,
45 | // `module.exports = "${contract.address}";`
46 | // );
47 | // fs.writeFileSync(
48 | // `${publishDir}/${contractName}.abi.js`,
49 | // `module.exports = ${JSON.stringify(contract.abi, null, 2)};`
50 | // );
51 | // fs.writeFileSync(
52 | // `${publishDir}/${contractName}.bytecode.js`,
53 | // `module.exports = "${contract.bytecode}";`
54 | // );
55 |
56 | return true;
57 | } catch (e) {
58 | console.log(
59 | "Failed to publish " + chalk.red(contractName) + " to the subgraph."
60 | );
61 | console.log(e);
62 | return false;
63 | }
64 | }
65 |
66 | async function main() {
67 | const directories = fs.readdirSync(deploymentsDir);
68 | directories.forEach(function (directory) {
69 | const files = fs.readdirSync(`${deploymentsDir}/${directory}`);
70 | files.forEach(function (file) {
71 | if (file.indexOf(".json") >= 0) {
72 | const contractName = file.replace(".json", "");
73 | publishContract(contractName, directory);
74 | }
75 | });
76 | });
77 | console.log("✅ Published contracts to the subgraph package.");
78 | }
79 | main()
80 | .then(() => process.exit(0))
81 | .catch((error) => {
82 | console.error(error);
83 | process.exit(1);
84 | });
85 |
--------------------------------------------------------------------------------
/packages/hardhat/scripts/watch.js:
--------------------------------------------------------------------------------
1 | const watch = require("node-watch");
2 | const { exec } = require("child_process");
3 |
4 | const run = () => {
5 | console.log("🛠 Compiling & Deploying...");
6 | exec("yarn deploy", 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 Contracts...");
14 | watch("./contracts", { recursive: true }, function (evt, name) {
15 | console.log("%s changed.", name);
16 | run();
17 | });
18 | run();
19 |
--------------------------------------------------------------------------------
/packages/hardhat/test/myTest.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("hardhat");
2 | const { use, expect } = require("chai");
3 | const { solidity } = require("ethereum-waffle");
4 |
5 | use(solidity);
6 |
7 | describe("My Dapp", function () {
8 | let myContract;
9 |
10 | // quick fix to let gas reporter fetch data from gas station & coinmarketcap
11 | before((done) => {
12 | setTimeout(done, 2000);
13 | });
14 |
15 | describe("YourContract", function () {
16 | it("Should deploy YourContract", async function () {
17 | const YourContract = await ethers.getContractFactory("YourContract");
18 |
19 | myContract = await YourContract.deploy();
20 | });
21 |
22 | describe("setPurpose()", function () {
23 | it("Should be able to set a new purpose", async function () {
24 | const newPurpose = "Test Purpose";
25 |
26 | await myContract.setPurpose(newPurpose);
27 | expect(await myContract.purpose()).to.equal(newPurpose);
28 | });
29 |
30 | // Uncomment the event and emit lines in YourContract.sol to make this test pass
31 |
32 | /*it("Should emit a SetPurpose event ", async function () {
33 | const [owner] = await ethers.getSigners();
34 |
35 | const newPurpose = "Another Test Purpose";
36 |
37 | expect(await myContract.setPurpose(newPurpose)).to.
38 | emit(myContract, "SetPurpose").
39 | withArgs(owner.address, newPurpose);
40 | });*/
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/packages/react-app/.eslintignore:
--------------------------------------------------------------------------------
1 | # folders
2 | build/
3 | node_modules/
4 | # files
5 | **/*.less
6 | **/*.css
7 | **/*.scss
8 | **/*.json
9 | **/*.png
10 | **/*.svg
11 |
--------------------------------------------------------------------------------
/packages/react-app/.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 |
--------------------------------------------------------------------------------
/packages/react-app/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSpacing": true,
4 | "printWidth": 120,
5 | "singleQuote": false,
6 | "tabWidth": 2,
7 | "trailingComma": "all"
8 | }
9 |
--------------------------------------------------------------------------------
/packages/react-app/.sample.env:
--------------------------------------------------------------------------------
1 | # REACT_APP_PROVIDER=https://rinkeby.infura.io/v3/2717afb6bf164045b5d5468031b93f87
2 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 | "@portis/web3": "^4.0.5",
22 | "@ramp-network/ramp-instant-sdk": "^2.2.0",
23 | "@testing-library/jest-dom": "^5.11.4",
24 | "@testing-library/react": "^11.1.0",
25 | "@testing-library/user-event": "^12.1.8",
26 | "@uniswap/sdk": "^3.0.3",
27 | "@uniswap/v2-periphery": "^1.1.0-beta.0",
28 | "@walletconnect/web3-provider": "^1.5.2",
29 | "antd": "4.16.0",
30 | "apollo-boost": "^0.4.9",
31 | "apollo-client": "^2.6.10",
32 | "apollo-utilities": "^1.3.4",
33 | "arb-ts": "^0.0.18",
34 | "authereum": "^0.1.14",
35 | "axios": "^0.21.4",
36 | "bnc-notify": "^1.5.0",
37 | "dotenv": "^8.2.0",
38 | "eth-hooks": "2.3.8",
39 | "ethers": "^5.4.1",
40 | "fortmatic": "^2.2.1",
41 | "graphiql": "^1.4.7",
42 | "graphql": "^15.3.0",
43 | "isomorphic-fetch": "^3.0.0",
44 | "node-watch": "^0.7.1",
45 | "postcss": "^8.2.6",
46 | "qrcode.react": "^1.0.0",
47 | "react": "^17.0.2",
48 | "react-blockies": "^1.4.1",
49 | "react-css-theme-switcher": "^0.2.2",
50 | "react-dom": "^17.0.2",
51 | "react-qr-reader": "^2.2.1",
52 | "react-router-dom": "^5.2.0",
53 | "react-scripts": "4.0.0",
54 | "walletlink": "^2.1.5",
55 | "web3modal": "^1.9.1"
56 | },
57 | "devDependencies": {
58 | "@apollo/client": "^3.3.21",
59 | "@testing-library/dom": "^6.12.2",
60 | "@types/react": "^16.9.19",
61 | "autoprefixer": "^10.2.4",
62 | "chalk": "^4.1.0",
63 | "eslint": "^7.5.0",
64 | "eslint-config-airbnb": "^18.2.0",
65 | "eslint-config-prettier": "^8.3.0",
66 | "eslint-plugin-babel": "^5.3.1",
67 | "eslint-plugin-import": "^2.23.4",
68 | "eslint-plugin-jsx-a11y": "^6.4.1",
69 | "eslint-plugin-prettier": "^3.4.0",
70 | "eslint-plugin-react": "^7.22.0",
71 | "eslint-plugin-react-hooks": "^4.2.0",
72 | "gulp": "^4.0.2",
73 | "gulp-csso": "^4.0.1",
74 | "gulp-debug": "^4.0.0",
75 | "gulp-less": "^4.0.1",
76 | "gulp-postcss": "^9.0.0",
77 | "ipfs-http-client": "^45.0.0",
78 | "less-plugin-npm-import": "^2.1.0",
79 | "prettier": "^2.0.5",
80 | "s3-folder-upload": "^2.3.1",
81 | "surge": "^0.21.5"
82 | },
83 | "eslintConfig": {
84 | "extends": "react-app"
85 | },
86 | "scripts": {
87 | "build": "react-scripts build",
88 | "eject": "react-scripts eject",
89 | "prestart": "node ./scripts/create_contracts.js",
90 | "start": "react-scripts start",
91 | "test": "react-scripts test",
92 | "lint": "eslint --config ./.eslintrc.js --ignore-path ./.eslintignore ./src/**/*",
93 | "ipfs": "node ./scripts/ipfs.js",
94 | "surge": "cp build/index.html build/200.html && surge ./build",
95 | "s3": "node ./scripts/s3.js",
96 | "ship": "yarn surge",
97 | "theme": "npx gulp less",
98 | "watch": "node ./scripts/watch.js",
99 | "prettier": "npx prettier --write . '!(node_module|build)/**/*'"
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/packages/react-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scaffold-eth/scaffold-eth-examples/896ea245d30fd8d697c543347a2e13717d394eca/packages/react-app/public/favicon.ico
--------------------------------------------------------------------------------
/packages/react-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
24 | Ethereum App
25 |
26 |
27 | You need to enable JavaScript to run this app.
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/packages/react-app/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scaffold-eth/scaffold-eth-examples/896ea245d30fd8d697c543347a2e13717d394eca/packages/react-app/public/logo192.png
--------------------------------------------------------------------------------
/packages/react-app/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scaffold-eth/scaffold-eth-examples/896ea245d30fd8d697c543347a2e13717d394eca/packages/react-app/public/logo512.png
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/packages/react-app/public/scaffold-eth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scaffold-eth/scaffold-eth-examples/896ea245d30fd8d697c543347a2e13717d394eca/packages/react-app/public/scaffold-eth.png
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/scripts/s3.js:
--------------------------------------------------------------------------------
1 | const s3FolderUpload = require("s3-folder-upload");
2 | const fs = require("fs");
3 |
4 | const directoryName = "build";
5 |
6 | const BUCKETNAME = "YOUR_BUCKET_NAME_HERE"; // <<---- 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 |
24 | credentials.bucket = BUCKETNAME;
25 |
26 | // optional options to be passed as parameter to the method
27 | const options = {
28 | useFoldersForFileTypes: false,
29 | useIAMRoleCredentials: false,
30 | };
31 |
32 | /////////////
33 | ///////////// First, let's automatically create the bucket if it doesn't exist...
34 | /////////////
35 |
36 | var AWS = require('aws-sdk');
37 | // Load credentials and set Region from JSON file
38 | AWS.config.loadFromPath('./aws.json');
39 |
40 | // Create S3 service object
41 | s3 = new AWS.S3({apiVersion: '2006-03-01'});
42 |
43 | // Create params JSON for S3.createBucket
44 | var bucketParams = {
45 | Bucket : BUCKETNAME,
46 | ACL : 'public-read'
47 | };
48 |
49 | // Create params JSON for S3.setBucketWebsite
50 | var staticHostParams = {
51 | Bucket: BUCKETNAME,
52 | WebsiteConfiguration: {
53 | ErrorDocument: {
54 | Key: 'index.html'
55 | },
56 | IndexDocument: {
57 | Suffix: 'index.html'
58 | },
59 | }
60 | };
61 |
62 | // Call S3 to create the bucket
63 | s3.createBucket(bucketParams, function(err, data) {
64 | if (err) {
65 | console.log("Error", err);
66 | } else {
67 | console.log("Bucket URL is ", data.Location);
68 | // Set the new policy on the newly created bucket
69 | s3.putBucketWebsite(staticHostParams, function(err, data) {
70 | if (err) {
71 | // Display error message
72 | console.log("Error", err);
73 | } else {
74 | // Update the displayed policy for the selected bucket
75 | console.log("Success... UPLOADING!", data);
76 |
77 | ///
78 | /// After the bucket is created, we upload to it:
79 | ///
80 | s3FolderUpload(directoryName, credentials, options /* , invalidation */);
81 | }
82 | });
83 | }
84 | });
85 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/Account.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "antd";
2 | import React from "react";
3 | import { useThemeSwitcher } from "react-css-theme-switcher";
4 | import Address from "./Address";
5 | import Balance from "./Balance";
6 | import Wallet from "./Wallet";
7 |
8 | /*
9 | ~ What it does? ~
10 |
11 | Displays an Address, Balance, and Wallet as one Account component,
12 | also allows users to log in to existing accounts and log out
13 |
14 | ~ How can I use? ~
15 |
16 |
27 |
28 | ~ Features ~
29 |
30 | - Provide address={address} and get balance corresponding to the given address
31 | - Provide localProvider={localProvider} to access balance on local network
32 | - Provide userProvider={userProvider} to display a wallet
33 | - Provide mainnetProvider={mainnetProvider} and your address will be replaced by ENS name
34 | (ex. "0xa870" => "user.eth")
35 | - Provide price={price} of ether and get your balance converted to dollars
36 | - Provide web3Modal={web3Modal}, loadWeb3Modal={loadWeb3Modal}, logoutOfWeb3Modal={logoutOfWeb3Modal}
37 | to be able to log in/log out to/from existing accounts
38 | - Provide blockExplorer={blockExplorer}, click on address and get the link
39 | (ex. by default "https://etherscan.io/" or for xdai "https://blockscout.com/poa/xdai/")
40 | */
41 |
42 | export default function Account({
43 | address,
44 | userSigner,
45 | localProvider,
46 | mainnetProvider,
47 | price,
48 | minimized,
49 | web3Modal,
50 | loadWeb3Modal,
51 | logoutOfWeb3Modal,
52 | blockExplorer,
53 | }) {
54 | const modalButtons = [];
55 | if (web3Modal) {
56 | if (web3Modal.cachedProvider) {
57 | modalButtons.push(
58 |
65 | logout
66 | ,
67 | );
68 | } else {
69 | modalButtons.push(
70 |
78 | connect
79 | ,
80 | );
81 | }
82 | }
83 |
84 | const { currentTheme } = useThemeSwitcher();
85 |
86 | const display = minimized ? (
87 | ""
88 | ) : (
89 |
90 | {address ? (
91 |
92 | ) : (
93 | "Connecting..."
94 | )}
95 |
96 |
104 |
105 | );
106 |
107 | return (
108 |
109 | {display}
110 | {modalButtons}
111 |
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/Contract/DisplayVariable.jsx:
--------------------------------------------------------------------------------
1 | import { Col, Divider, Row } from "antd";
2 | import React, { useCallback, useEffect, useState } from "react";
3 | import tryToDisplay from "./utils";
4 |
5 | const DisplayVariable = ({ contractFunction, functionInfo, refreshRequired, triggerRefresh }) => {
6 | const [variable, setVariable] = useState("");
7 |
8 | const refresh = useCallback(async () => {
9 | try {
10 | const funcResponse = await contractFunction();
11 | setVariable(funcResponse);
12 | triggerRefresh(false);
13 | } catch (e) {
14 | console.log(e);
15 | }
16 | }, [setVariable, contractFunction, triggerRefresh]);
17 |
18 | useEffect(() => {
19 | refresh();
20 | }, [refresh, refreshRequired, contractFunction]);
21 |
22 | return (
23 |
24 |
25 |
34 | {functionInfo.name}
35 |
36 |
37 | {tryToDisplay(variable)}
38 |
39 |
40 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default DisplayVariable;
53 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/Contract/FunctionForm.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Col, Divider, Input, Row, Tooltip } from "antd";
2 | import React, { useState } from "react";
3 | import Blockies from "react-blockies";
4 | import { Transactor } from "../../helpers";
5 | import tryToDisplay from "./utils";
6 |
7 | const { utils, BigNumber } = require("ethers");
8 |
9 | const getFunctionInputKey = (functionInfo, input, inputIndex) => {
10 | const name = input?.name ? input.name : "input_" + inputIndex + "_";
11 | return functionInfo.name + "_" + name + "_" + input.type;
12 | };
13 |
14 | export default function FunctionForm({ contractFunction, functionInfo, provider, gasPrice, triggerRefresh }) {
15 | const [form, setForm] = useState({});
16 | const [txValue, setTxValue] = useState();
17 | const [returnValue, setReturnValue] = useState();
18 |
19 | const tx = Transactor(provider, gasPrice);
20 |
21 | const inputs = functionInfo.inputs.map((input, inputIndex) => {
22 | const key = getFunctionInputKey(functionInfo, input, inputIndex);
23 |
24 | let buttons = "";
25 | if (input.type === "bytes32") {
26 | buttons = (
27 |
28 | {
32 | if (utils.isHexString(form[key])) {
33 | const formUpdate = { ...form };
34 | formUpdate[key] = utils.parseBytes32String(form[key]);
35 | setForm(formUpdate);
36 | } else {
37 | const formUpdate = { ...form };
38 | formUpdate[key] = utils.formatBytes32String(form[key]);
39 | setForm(formUpdate);
40 | }
41 | }}
42 | >
43 | #️⃣
44 |
45 |
46 | );
47 | } else if (input.type === "bytes") {
48 | buttons = (
49 |
50 | {
54 | if (utils.isHexString(form[key])) {
55 | const formUpdate = { ...form };
56 | formUpdate[key] = utils.toUtf8String(form[key]);
57 | setForm(formUpdate);
58 | } else {
59 | const formUpdate = { ...form };
60 | formUpdate[key] = utils.hexlify(utils.toUtf8Bytes(form[key]));
61 | setForm(formUpdate);
62 | }
63 | }}
64 | >
65 | #️⃣
66 |
67 |
68 | );
69 | } else if (input.type === "uint256") {
70 | buttons = (
71 |
72 | {
76 | const formUpdate = { ...form };
77 | formUpdate[key] = utils.parseEther(form[key]);
78 | setForm(formUpdate);
79 | }}
80 | >
81 | ✴️
82 |
83 |
84 | );
85 | } else if (input.type === "address") {
86 | const possibleAddress = form[key] && form[key].toLowerCase && form[key].toLowerCase().trim();
87 | if (possibleAddress && possibleAddress.length === 42) {
88 | buttons = (
89 |
90 |
91 |
92 | );
93 | }
94 | }
95 |
96 | return (
97 |
98 | {
105 | const formUpdate = { ...form };
106 | formUpdate[event.target.name] = event.target.value;
107 | setForm(formUpdate);
108 | }}
109 | suffix={buttons}
110 | />
111 |
112 | );
113 | });
114 |
115 | const txValueInput = (
116 |
117 |
setTxValue(e.target.value)}
120 | value={txValue}
121 | addonAfter={
122 |
123 |
124 |
125 |
126 | {
130 | const floatValue = parseFloat(txValue);
131 | if (floatValue) setTxValue("" + floatValue * 10 ** 18);
132 | }}
133 | >
134 | ✳️
135 |
136 |
137 |
138 |
139 |
140 | {
144 | setTxValue(BigNumber.from(txValue).toHexString());
145 | }}
146 | >
147 | #️⃣
148 |
149 |
150 |
151 |
152 |
153 | }
154 | />
155 |
156 | );
157 |
158 | if (functionInfo.payable) {
159 | inputs.push(txValueInput);
160 | }
161 |
162 | const buttonIcon =
163 | functionInfo.type === "call" ? (
164 | Read📡
165 | ) : (
166 | Send💸
167 | );
168 | inputs.push(
169 |
170 |
setReturnValue(e.target.value)}
172 | defaultValue=""
173 | bordered={false}
174 | disabled
175 | value={returnValue}
176 | suffix={
177 |
{
181 | const args = functionInfo.inputs.map((input, inputIndex) => {
182 | const key = getFunctionInputKey(functionInfo, input, inputIndex);
183 | let value = form[key];
184 | if (input.baseType === "array") {
185 | value = JSON.parse(value);
186 | } else if (input.type === "bool") {
187 | if (value === "true" || value === "1" || value === "0x1" || value === "0x01" || value === "0x0001") {
188 | value = 1;
189 | } else {
190 | value = 0;
191 | }
192 | }
193 | return value;
194 | });
195 |
196 | let result;
197 | if (functionInfo.stateMutability === "view" || functionInfo.stateMutability === "pure") {
198 | try {
199 | const returned = await contractFunction(...args);
200 | result = tryToDisplay(returned);
201 | } catch (err) {
202 | console.error(err);
203 | }
204 | } else {
205 | const overrides = {};
206 | if (txValue) {
207 | overrides.value = txValue; // ethers.utils.parseEther()
208 | }
209 | if (gasPrice) {
210 | overrides.gasPrice = gasPrice;
211 | }
212 | // Uncomment this if you want to skip the gas estimation for each transaction
213 | // overrides.gasLimit = hexlify(1200000);
214 |
215 | // console.log("Running with extras",extras)
216 | const returned = await tx(contractFunction(...args, overrides));
217 | result = tryToDisplay(returned);
218 | }
219 |
220 | console.log("SETTING RESULT:", result);
221 | setReturnValue(result);
222 | triggerRefresh(true);
223 | }}
224 | >
225 | {buttonIcon}
226 |
227 | }
228 | />
229 |
,
230 | );
231 |
232 | return (
233 |
234 |
235 |
244 | {functionInfo.name}
245 |
246 | {inputs}
247 |
248 |
249 |
250 | );
251 | }
252 |
--------------------------------------------------------------------------------
/packages/react-app/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 Account from "../Account";
5 | import DisplayVariable from "./DisplayVariable";
6 | import FunctionForm from "./FunctionForm";
7 |
8 | const noContractDisplay = (
9 |
10 | Loading...{" "}
11 |
12 | You need to run{" "}
13 |
17 | yarn run chain
18 | {" "}
19 | and{" "}
20 |
24 | yarn run deploy
25 | {" "}
26 | to see your contract here.
27 |
28 |
29 |
30 | ☢️
31 |
32 | Warning: You might need to run
33 |
37 | yarn run deploy
38 | {" "}
39 | again after the frontend comes up!
40 |
41 |
42 | );
43 |
44 | const isQueryable = fn => (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length === 0;
45 |
46 | export default function Contract({
47 | customContract,
48 | account,
49 | gasPrice,
50 | signer,
51 | provider,
52 | name,
53 | show,
54 | price,
55 | blockExplorer,
56 | chainId,
57 | contractConfig,
58 | }) {
59 | const contracts = useContractLoader(provider, contractConfig, chainId);
60 | let contract;
61 | if (!customContract) {
62 | contract = contracts ? contracts[name] : "";
63 | } else {
64 | contract = customContract;
65 | }
66 |
67 | const address = contract ? contract.address : "";
68 | const contractIsDeployed = useContractExistsAtAddress(provider, address);
69 |
70 | const displayedContractFunctions = useMemo(() => {
71 | const results = contract
72 | ? Object.entries(contract.interface.functions).filter(
73 | fn => fn[1]["type"] === "function" && !(show && show.indexOf(fn[1]["name"]) < 0),
74 | )
75 | : [];
76 | return results;
77 | }, [contract, show]);
78 |
79 | const [refreshRequired, triggerRefresh] = useState(false);
80 | const contractDisplay = displayedContractFunctions.map(contractFuncInfo => {
81 | const contractFunc =
82 | contractFuncInfo[1].stateMutability === "view" || contractFuncInfo[1].stateMutability === "pure"
83 | ? contract[contractFuncInfo[0]]
84 | : contract.connect(signer)[contractFuncInfo[0]];
85 |
86 | if (typeof contractFunc === "function") {
87 | if (isQueryable(contractFuncInfo[1])) {
88 | // If there are no inputs, just display return value
89 | return (
90 |
97 | );
98 | }
99 |
100 | // If there are inputs, display a form to allow users to provide these
101 | return (
102 |
110 | );
111 | }
112 | return null;
113 | });
114 |
115 | return (
116 |
117 |
120 | {name}
121 |
122 |
130 | {account}
131 |
132 |
133 | }
134 | size="large"
135 | style={{ marginTop: 25, width: "100%" }}
136 | loading={contractDisplay && contractDisplay.length <= 0}
137 | >
138 | {contractIsDeployed ? contractDisplay : noContractDisplay}
139 |
140 |
141 | );
142 | }
143 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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(props.price ? "USD" : "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 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/Events.jsx:
--------------------------------------------------------------------------------
1 | import { List } from "antd";
2 | import { useEventListener } from "eth-hooks/events/useEventListener";
3 | import { Address } from "../components";
4 |
5 | /*
6 | ~ What it does? ~
7 |
8 | Displays a lists of events
9 |
10 | ~ How can I use? ~
11 |
12 |
20 | */
21 |
22 | export default function Events({ contracts, contractName, eventName, localProvider, mainnetProvider, startBlock }) {
23 | // 📟 Listen for broadcast events
24 | const events = useEventListener(contracts, contractName, eventName, localProvider, startBlock);
25 |
26 | return (
27 |
28 |
Events:
29 |
{
33 | return (
34 |
35 |
36 | {item.args[1]}
37 |
38 | );
39 | }}
40 | />
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/Faucet.jsx:
--------------------------------------------------------------------------------
1 | import { SendOutlined } from "@ant-design/icons";
2 | import { Button, Input, Tooltip } from "antd";
3 | // import { useLookupAddress } from "eth-hooks/dapps/ens";
4 | import React, { useCallback, useState, useEffect } from "react";
5 | import Blockies from "react-blockies";
6 | import { Transactor } from "../helpers";
7 | import Wallet from "./Wallet";
8 |
9 | const { utils } = require("ethers");
10 |
11 | // improved a bit by converting address to ens if it exists
12 | // added option to directly input ens name
13 | // added placeholder option
14 |
15 | /*
16 | ~ What it does? ~
17 |
18 | Displays a local faucet to send ETH to given address, also wallet is provided
19 |
20 | ~ How can I use? ~
21 |
22 |
28 |
29 | ~ Features ~
30 |
31 | - Provide price={price} of ether and convert between USD and ETH in a wallet
32 | - Provide localProvider={localProvider} to be able to send ETH to given address
33 | - Provide ensProvider={mainnetProvider} and your address will be replaced by ENS name
34 | (ex. "0xa870" => "user.eth") or you can enter directly ENS name instead of address
35 | works both in input field & wallet
36 | - Provide placeholder="Send local faucet" value for the input
37 | */
38 |
39 | export default function Faucet(props) {
40 | const [address, setAddress] = useState();
41 | const [faucetAddress, setFaucetAddress] = useState();
42 |
43 | const { price, placeholder, localProvider, ensProvider, onChange } = props;
44 |
45 | useEffect(() => {
46 | const getFaucetAddress = async () => {
47 | if (localProvider) {
48 | const _faucetAddress = await localProvider.listAccounts();
49 | setFaucetAddress(_faucetAddress[0]);
50 | //console.log(_faucetAddress);
51 | }
52 | };
53 | getFaucetAddress();
54 | }, [localProvider]);
55 |
56 | let blockie;
57 | if (address && typeof address.toLowerCase === "function") {
58 | blockie = ;
59 | } else {
60 | blockie =
;
61 | }
62 |
63 | // const ens = useLookupAddress(ensProvider, address);
64 |
65 | const updateAddress = useCallback(
66 | async newValue => {
67 | if (typeof newValue !== "undefined" && utils.isAddress(newValue)) {
68 | let newAddress = newValue;
69 | // if (newAddress.indexOf(".eth") > 0 || newAddress.indexOf(".xyz") > 0) {
70 | // try {
71 | // const possibleAddress = await ensProvider.resolveName(newAddress);
72 | // if (possibleAddress) {
73 | // newAddress = possibleAddress;
74 | // }
75 | // // eslint-disable-next-line no-empty
76 | // } catch (e) { }
77 | // }
78 | setAddress(newAddress);
79 | }
80 | },
81 | [ensProvider, onChange],
82 | );
83 |
84 | const tx = Transactor(localProvider);
85 |
86 | return (
87 |
88 | {
95 | // setAddress(e.target.value);
96 | updateAddress(e.target.value);
97 | }}
98 | suffix={
99 |
100 | {
102 | tx({
103 | to: address,
104 | value: utils.parseEther("0.01"),
105 | });
106 | setAddress("");
107 | }}
108 | shape="circle"
109 | icon={ }
110 | />
111 |
118 |
119 | }
120 | />
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/GasGauge.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "antd";
2 | import React from "react";
3 |
4 | // added display of 0 instead of NaN if gas price is not provided
5 |
6 | /*
7 | ~ What it does? ~
8 |
9 | Displays gas gauge
10 |
11 | ~ How can I use? ~
12 |
13 |
16 |
17 | ~ Features ~
18 |
19 | - Provide gasPrice={gasPrice} and get current gas gauge
20 | */
21 |
22 | export default function GasGauge(props) {
23 | return (
24 | {
26 | window.open("https://ethgasstation.info/");
27 | }}
28 | size="large"
29 | shape="round"
30 | >
31 |
32 |
33 | ⛽️
34 |
35 |
36 | {typeof props.gasPrice === "undefined" ? 0 : parseInt(props.gasPrice, 10) / 10 ** 9}g
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/L2Bridge.jsx:
--------------------------------------------------------------------------------
1 | import { utils, ethers } from "ethers";
2 | import { Button, Input, Form, Select, InputNumber, Table, Radio } from "antd";
3 | import React, { useState, useEffect } from "react";
4 | import { useContractLoader, useOnBlock } from "eth-hooks";
5 | import { NETWORKS } from "../constants";
6 | import { Transactor } from "../helpers";
7 |
8 | /*
9 | This is a component for bridging between L1 & L2
10 | Currently it supports Testnet deposits for Arbitrum & Optimism
11 |
12 | __ _______ _____
13 | \ \ / /_ _| __ \
14 | \ \ /\ / / | | | |__) |
15 | \ \/ \/ / | | | ___/
16 | \ /\ / _| |_| |
17 | \/ \/ |_____|_|
18 |
19 |
20 | */
21 |
22 | export default function L2ArbitrumBridge({ address, userSigner }) {
23 | const [L1EthBalance, setL1EthBalance] = useState("...");
24 | const [L2EthBalance, setL2EthBalance] = useState("...");
25 | const [L1Provider, setL1Provider] = useState("");
26 | const [L2Provider, setL2Provider] = useState("");
27 | const [rollup, setRollup] = useState("arbitrum");
28 | const [environment, setEnvironment] = useState("test");
29 |
30 | const rollupConfig = {
31 | arbitrum: {
32 | test: { L1: NETWORKS.rinkeby, L2: NETWORKS.rinkebyArbitrum },
33 | main: { L1: NETWORKS.mainnet, L2: NETWORKS.arbitrum },
34 | local: { L1: NETWORKS.localArbitrumL1, L2: NETWORKS.localArbitrum },
35 | },
36 | optimism: {
37 | test: { L1: NETWORKS.kovan, L2: NETWORKS.kovanOptimism },
38 | local: { L1: NETWORKS.localOptimismL1, L2: NETWORKS.localOptimism },
39 | },
40 | };
41 |
42 | const activeConfig = rollupConfig[rollup][environment];
43 |
44 | const selectedChainId =
45 | userSigner && userSigner.provider && userSigner.provider._network && userSigner.provider._network.chainId;
46 |
47 | const tx = Transactor(userSigner);
48 |
49 | useEffect(() => {
50 | async function setProviders() {
51 | const L1 = activeConfig.L1;
52 | const L2 = activeConfig.L2;
53 | setL1Provider(new ethers.providers.StaticJsonRpcProvider(L1.rpcUrl));
54 | setL2Provider(new ethers.providers.StaticJsonRpcProvider(L2.rpcUrl));
55 | setL1EthBalance("...");
56 | setL2EthBalance("...");
57 | }
58 | setProviders();
59 | }, [rollup]);
60 |
61 | const contracts = useContractLoader(userSigner, { externalContracts: L1BridgeMetadata, hardhatContracts: {} });
62 |
63 | useOnBlock(L1Provider, async () => {
64 | console.log(`⛓ A new mainnet block is here: ${L1Provider._lastBlockNumber}`);
65 | const yourL1Balance = await L1Provider.getBalance(address);
66 | setL1EthBalance(yourL1Balance ? ethers.utils.formatEther(yourL1Balance) : "...");
67 | const yourL2Balance = await L2Provider.getBalance(address);
68 | setL2EthBalance(yourL2Balance ? ethers.utils.formatEther(yourL2Balance) : "...");
69 | });
70 |
71 | const { Option } = Select;
72 | const formItemLayout = {
73 | labelCol: {
74 | xs: { span: 24 },
75 | sm: { span: 8 },
76 | },
77 | wrapperCol: {
78 | xs: { span: 24 },
79 | sm: { span: 12 },
80 | },
81 | };
82 | const tailFormItemLayout = {
83 | wrapperCol: {
84 | xs: {
85 | span: 24,
86 | offset: 0,
87 | },
88 | sm: {
89 | span: 12,
90 | offset: 8,
91 | },
92 | },
93 | };
94 |
95 | const columns = [
96 | {
97 | title: "",
98 | dataIndex: "token",
99 | key: "token",
100 | align: "center",
101 | },
102 | {
103 | title: `${activeConfig.L1.name} L1 Balance`,
104 | dataIndex: "l1",
105 | key: "l1",
106 | align: "center",
107 | },
108 | {
109 | title: `${activeConfig.L1.name} ${rollup} Balance`,
110 | dataIndex: "l2",
111 | key: "l2",
112 | align: "center",
113 | },
114 | ];
115 |
116 | const data = [
117 | {
118 | key: "1",
119 | token: "ETH",
120 | l1: "Ξ" + L1EthBalance,
121 | l2: "Ξ" + L2EthBalance,
122 | },
123 | ];
124 |
125 | const [form] = Form.useForm();
126 |
127 | const onAssetChange = value => {
128 | console.log(value);
129 | };
130 |
131 | async function onFinish(values) {
132 | console.log(contracts);
133 | console.log(values.amount.toString());
134 | console.log(rollup);
135 | let newTx;
136 | try {
137 | if (rollup === "arbitrum") {
138 | newTx = await tx(
139 | contracts.Inbox.depositEth(1_300_000, {
140 | value: utils.parseEther(values.amount.toString()),
141 | gasLimit: 300000,
142 | }),
143 | );
144 | } else if (rollup === "optimism") {
145 | newTx = await tx(
146 | contracts.OVM_L1StandardBridge.depositETH(1_300_000, "0x", {
147 | value: utils.parseEther(values.amount.toString()),
148 | }),
149 | );
150 | }
151 | await newTx.wait();
152 | console.log("woop!");
153 | } catch (e) {
154 | console.log(e);
155 | console.log("something went wrong!");
156 | }
157 | }
158 |
159 | const onReset = () => {
160 | form.resetFields();
161 | };
162 |
163 | const wrongNetwork = selectedChainId !== activeConfig.L1.chainId;
164 |
165 | return (
166 |
167 |
168 |
Welcome to the L2 Deposit Bridge!
169 |
{
172 | setRollup(e.target.value);
173 | }}
174 | style={{ marginBottom: 10 }}
175 | >
176 | Arbitrum
177 | Optimism
178 |
179 |
180 |
181 |
182 |
198 |
199 | ETH
200 |
201 | ERC-20
202 |
203 |
204 |
205 |
206 |
207 |
208 |
217 |
218 |
219 |
220 |
221 | {wrongNetwork ? `Switch wallet to ${activeConfig.L1.name}` : "Deposit"}
222 |
223 |
224 |
225 |
226 |
227 | );
228 | }
229 |
230 | // Arbitrum Inbox https://rinkeby.etherscan.io/address/0xa157dc79ca26d69c3b1282d03ec42bdee2790a8f#code
231 | const ArbitrumInboxABI = [
232 | {
233 | anonymous: false,
234 | inputs: [
235 | { indexed: true, internalType: "uint256", name: "messageNum", type: "uint256" },
236 | { indexed: false, internalType: "bytes", name: "data", type: "bytes" },
237 | ],
238 | name: "InboxMessageDelivered",
239 | type: "event",
240 | },
241 | {
242 | anonymous: false,
243 | inputs: [{ indexed: true, internalType: "uint256", name: "messageNum", type: "uint256" }],
244 | name: "InboxMessageDeliveredFromOrigin",
245 | type: "event",
246 | },
247 | {
248 | anonymous: false,
249 | inputs: [{ indexed: false, internalType: "address", name: "newSource", type: "address" }],
250 | name: "WhitelistSourceUpdated",
251 | type: "event",
252 | },
253 | {
254 | inputs: [],
255 | name: "bridge",
256 | outputs: [{ internalType: "contract IBridge", name: "", type: "address" }],
257 | stateMutability: "view",
258 | type: "function",
259 | },
260 | {
261 | inputs: [
262 | { internalType: "address", name: "destAddr", type: "address" },
263 | { internalType: "uint256", name: "l2CallValue", type: "uint256" },
264 | { internalType: "uint256", name: "maxSubmissionCost", type: "uint256" },
265 | { internalType: "address", name: "excessFeeRefundAddress", type: "address" },
266 | { internalType: "address", name: "callValueRefundAddress", type: "address" },
267 | { internalType: "uint256", name: "maxGas", type: "uint256" },
268 | { internalType: "uint256", name: "gasPriceBid", type: "uint256" },
269 | { internalType: "bytes", name: "data", type: "bytes" },
270 | ],
271 | name: "createRetryableTicket",
272 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
273 | stateMutability: "payable",
274 | type: "function",
275 | },
276 | {
277 | inputs: [{ internalType: "uint256", name: "maxSubmissionCost", type: "uint256" }],
278 | name: "depositEth",
279 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
280 | stateMutability: "payable",
281 | type: "function",
282 | },
283 | {
284 | inputs: [
285 | { internalType: "contract IBridge", name: "_bridge", type: "address" },
286 | { internalType: "address", name: "_whitelist", type: "address" },
287 | ],
288 | name: "initialize",
289 | outputs: [],
290 | stateMutability: "nonpayable",
291 | type: "function",
292 | },
293 | {
294 | inputs: [],
295 | name: "isMaster",
296 | outputs: [{ internalType: "bool", name: "", type: "bool" }],
297 | stateMutability: "view",
298 | type: "function",
299 | },
300 | {
301 | inputs: [
302 | { internalType: "uint256", name: "maxGas", type: "uint256" },
303 | { internalType: "uint256", name: "gasPriceBid", type: "uint256" },
304 | { internalType: "address", name: "destAddr", type: "address" },
305 | { internalType: "uint256", name: "amount", type: "uint256" },
306 | { internalType: "bytes", name: "data", type: "bytes" },
307 | ],
308 | name: "sendContractTransaction",
309 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
310 | stateMutability: "nonpayable",
311 | type: "function",
312 | },
313 | {
314 | inputs: [
315 | { internalType: "uint256", name: "maxGas", type: "uint256" },
316 | { internalType: "uint256", name: "gasPriceBid", type: "uint256" },
317 | { internalType: "address", name: "destAddr", type: "address" },
318 | { internalType: "bytes", name: "data", type: "bytes" },
319 | ],
320 | name: "sendL1FundedContractTransaction",
321 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
322 | stateMutability: "payable",
323 | type: "function",
324 | },
325 | {
326 | inputs: [
327 | { internalType: "uint256", name: "maxGas", type: "uint256" },
328 | { internalType: "uint256", name: "gasPriceBid", type: "uint256" },
329 | { internalType: "uint256", name: "nonce", type: "uint256" },
330 | { internalType: "address", name: "destAddr", type: "address" },
331 | { internalType: "bytes", name: "data", type: "bytes" },
332 | ],
333 | name: "sendL1FundedUnsignedTransaction",
334 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
335 | stateMutability: "payable",
336 | type: "function",
337 | },
338 | {
339 | inputs: [{ internalType: "bytes", name: "messageData", type: "bytes" }],
340 | name: "sendL2Message",
341 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
342 | stateMutability: "nonpayable",
343 | type: "function",
344 | },
345 | {
346 | inputs: [{ internalType: "bytes", name: "messageData", type: "bytes" }],
347 | name: "sendL2MessageFromOrigin",
348 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
349 | stateMutability: "nonpayable",
350 | type: "function",
351 | },
352 | {
353 | inputs: [
354 | { internalType: "uint256", name: "maxGas", type: "uint256" },
355 | { internalType: "uint256", name: "gasPriceBid", type: "uint256" },
356 | { internalType: "uint256", name: "nonce", type: "uint256" },
357 | { internalType: "address", name: "destAddr", type: "address" },
358 | { internalType: "uint256", name: "amount", type: "uint256" },
359 | { internalType: "bytes", name: "data", type: "bytes" },
360 | ],
361 | name: "sendUnsignedTransaction",
362 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
363 | stateMutability: "nonpayable",
364 | type: "function",
365 | },
366 | {
367 | inputs: [{ internalType: "address", name: "newSource", type: "address" }],
368 | name: "updateWhitelistSource",
369 | outputs: [],
370 | stateMutability: "nonpayable",
371 | type: "function",
372 | },
373 | {
374 | inputs: [],
375 | name: "whitelist",
376 | outputs: [{ internalType: "address", name: "", type: "address" }],
377 | stateMutability: "view",
378 | type: "function",
379 | },
380 | ];
381 |
382 | // https://github.com/ethereum-optimism/optimism/blob/2bd49730fa8d2c10953873f0ccc792198a49d5c9/packages/contracts/contracts/optimistic-ethereum/iOVM/bridge/tokens/iOVM_L1StandardBridge.sol
383 | const OVM_L1StandardBridgeABI = [
384 | "function depositETH(uint32 _l2Gas,bytes calldata _data) external payable",
385 | "function depositETHTo(address _to,uint32 _l2Gas,bytes calldata _data) external payable",
386 | "function finalizeETHWithdrawal (address _from,address _to,uint _amount,bytes calldata _data) external",
387 | ];
388 |
389 | const L1BridgeMetadata = {
390 | // Arbitrium Contract's
391 | 44010: {
392 | contracts: {
393 | Inbox: {
394 | address: "0xA4d796Ad4e79aFB703340a596AEd88f8a5924183",
395 | abi: ArbitrumInboxABI,
396 | },
397 | },
398 | },
399 | 4: {
400 | contracts: {
401 | Inbox: {
402 | address: "0x578bade599406a8fe3d24fd7f7211c0911f5b29e",
403 | abi: ArbitrumInboxABI,
404 | },
405 | },
406 | },
407 | // Optimism Contract's
408 | 31337: {
409 | contracts: {
410 | OVM_L1StandardBridge: {
411 | address: "0x998abeb3E57409262aE5b751f60747921B33613E",
412 | abi: OVM_L1StandardBridgeABI,
413 | },
414 | },
415 | },
416 | 42: {
417 | contracts: {
418 | OVM_L1StandardBridge: {
419 | address: "0x22F24361D548e5FaAfb36d1437839f080363982B",
420 | abi: OVM_L1StandardBridgeABI,
421 | },
422 | },
423 | },
424 | };
425 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/Provider.jsx:
--------------------------------------------------------------------------------
1 | import { Badge, Button } from "antd";
2 | import { useBlockNumber, usePoller } from "eth-hooks";
3 | import React, { useState } from "react";
4 | // import { WalletOutlined } from '@ant-design/icons';
5 | import Address from "./Address";
6 |
7 | export default function Provider(props) {
8 | const [showMore, setShowMore] = useState(false);
9 | const [status, setStatus] = useState("processing");
10 | const [network, setNetwork] = useState();
11 | const [signer, setSigner] = useState();
12 | const [address, setAddress] = useState();
13 |
14 | const blockNumber = useBlockNumber(props.provider);
15 |
16 | usePoller(async () => {
17 | if (props.provider && typeof props.provider.getNetwork === "function") {
18 | try {
19 | const newNetwork = await props.provider.getNetwork();
20 | setNetwork(newNetwork);
21 | if (newNetwork.chainId > 0) {
22 | setStatus("success");
23 | } else {
24 | setStatus("warning");
25 | }
26 | } catch (e) {
27 | console.log(e);
28 | setStatus("processing");
29 | }
30 | try {
31 | const newSigner = await props.provider.getSigner();
32 | setSigner(newSigner);
33 | const newAddress = await newSigner.getAddress();
34 | setAddress(newAddress);
35 | // eslint-disable-next-line no-empty
36 | } catch (e) {}
37 | }
38 | }, 1377);
39 |
40 | if (
41 | typeof props.provider === "undefined" ||
42 | typeof props.provider.getNetwork !== "function" ||
43 | !network ||
44 | !network.chainId
45 | ) {
46 | return (
47 | {
51 | setShowMore(!showMore);
52 | }}
53 | >
54 | {props.name}
55 |
56 | );
57 | }
58 |
59 | let showExtra = "";
60 | if (showMore) {
61 | showExtra = (
62 |
63 |
64 | id:
65 | {network ? network.chainId : ""}
66 |
67 |
68 | name:
69 | {network ? network.name : ""}
70 |
71 |
72 | );
73 | }
74 |
75 | let showWallet = "";
76 | if (typeof signer !== "undefined" && address) {
77 | showWallet = (
78 |
79 |
80 |
81 |
82 |
83 | );
84 | }
85 |
86 | return (
87 | {
91 | setShowMore(!showMore);
92 | }}
93 | >
94 | {props.name} {showWallet} #{blockNumber} {showExtra}
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/Ramp.jsx:
--------------------------------------------------------------------------------
1 | import { DollarCircleOutlined } from "@ant-design/icons";
2 | import { RampInstantSDK } from "@ramp-network/ramp-instant-sdk";
3 | import { Button, Divider, Modal } from "antd";
4 | import React, { useState } from "react";
5 |
6 | // added display of 0 if price={price} is not provided
7 |
8 | /*
9 | ~ What it does? ~
10 |
11 | Displays current ETH price and gives options to buy ETH through Wyre/Ramp/Coinbase
12 | or get through Rinkeby/Ropsten/Kovan/Goerli
13 |
14 | ~ How can I use? ~
15 |
16 |
20 |
21 | ~ Features ~
22 |
23 | - Ramp opens directly in the application, component uses RampInstantSDK
24 | - Provide price={price} and current ETH price will be displayed
25 | - Provide address={address} and your address will be pasted into Wyre/Ramp instantly
26 | */
27 |
28 | export default function Ramp(props) {
29 | const [modalUp, setModalUp] = useState("down");
30 |
31 | const type = "default";
32 |
33 | const allFaucets = [];
34 | for (const n in props.networks) {
35 | if (props.networks[n].chainId !== 31337 && props.networks[n].chainId !== 1) {
36 | allFaucets.push(
37 |
38 | {
44 | window.open(props.networks[n].faucet);
45 | }}
46 | >
47 | {props.networks[n].name}
48 |
49 |
,
50 | );
51 | }
52 | }
53 |
54 | return (
55 |
56 |
{
60 | setModalUp("up");
61 | }}
62 | >
63 | {" "}
64 | {typeof props.price === "undefined" ? 0 : props.price.toFixed(2)}
65 |
66 |
{
70 | setModalUp("down");
71 | }}
72 | footer={[
73 | {
76 | setModalUp("down");
77 | }}
78 | >
79 | cancel
80 | ,
81 | ]}
82 | >
83 |
84 | {
89 | window.open("https://pay.sendwyre.com/purchase?destCurrency=ETH&sourceAmount=25&dest=" + props.address);
90 | }}
91 | >
92 |
93 |
94 | 🇺🇸
95 |
96 |
97 | Wyre
98 |
99 |
100 |
101 | {" "}
102 | {
107 | new RampInstantSDK({
108 | hostAppName: "scaffold-eth",
109 | hostLogoUrl: "https://scaffoldeth.io/scaffold-eth.png",
110 | swapAmount: "100000000000000000", // 0.1 ETH in wei ?
111 | swapAsset: "ETH",
112 | userAddress: props.address,
113 | })
114 | .on("*", event => console.log(event))
115 | .show();
116 | }}
117 | >
118 |
119 |
120 | 🇬🇧
121 |
122 |
123 | Ramp
124 |
125 |
126 |
127 |
128 | {
133 | window.open("https://www.coinbase.com/buy-ethereum");
134 | }}
135 | >
136 |
137 | 🏦
138 |
139 | Coinbase
140 |
141 |
142 |
143 |
144 |
145 | Testnet ETH
146 |
147 | {allFaucets}
148 |
149 |
150 | );
151 | }
152 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/Wallet.jsx:
--------------------------------------------------------------------------------
1 | import { KeyOutlined, QrcodeOutlined, SendOutlined, WalletOutlined } from "@ant-design/icons";
2 | import { Button, Modal, Spin, Tooltip, Typography } from "antd";
3 | import { ethers } from "ethers";
4 | import QR from "qrcode.react";
5 | import React, { useState, useEffect } from "react";
6 | import { Transactor } from "../helpers";
7 | import Address from "./Address";
8 | import AddressInput from "./AddressInput";
9 | import Balance from "./Balance";
10 | import EtherInput from "./EtherInput";
11 |
12 | const { Text, Paragraph } = Typography;
13 |
14 | /*
15 | ~ What it does? ~
16 |
17 | Displays a wallet where you can specify address and send USD/ETH, with options to
18 | scan address, to convert between USD and ETH, to see and generate private keys,
19 | to send, receive and extract the burner wallet
20 |
21 | ~ How can I use? ~
22 |
23 |
30 |
31 | ~ Features ~
32 |
33 | - Provide provider={userProvider} to display a wallet
34 | - Provide address={address} if you want to specify address, otherwise
35 | your default address will be used
36 | - Provide ensProvider={mainnetProvider} and your address will be replaced by ENS name
37 | (ex. "0xa870" => "user.eth") or you can enter directly ENS name instead of address
38 | - Provide price={price} of ether and easily convert between USD and ETH
39 | - Provide color to specify the color of wallet icon
40 | */
41 |
42 | export default function Wallet(props) {
43 | const [signerAddress, setSignerAddress] = useState();
44 | useEffect(() => {
45 | async function getAddress() {
46 | if (props.signer) {
47 | const newAddress = await props.signer.getAddress();
48 | setSignerAddress(newAddress);
49 | }
50 | }
51 | getAddress();
52 | }, [props.signer]);
53 |
54 | const selectedAddress = props.address || signerAddress;
55 |
56 | const [open, setOpen] = useState();
57 | const [qr, setQr] = useState();
58 | const [amount, setAmount] = useState();
59 | const [toAddress, setToAddress] = useState();
60 | const [pk, setPK] = useState();
61 |
62 | const providerSend = props.provider ? (
63 |
64 | {
66 | setOpen(!open);
67 | }}
68 | rotate={-90}
69 | style={{
70 | padding: 7,
71 | color: props.color ? props.color : "",
72 | cursor: "pointer",
73 | fontSize: 28,
74 | verticalAlign: "middle",
75 | }}
76 | />
77 |
78 | ) : (
79 | ""
80 | );
81 |
82 | let display;
83 | let receiveButton;
84 | let privateKeyButton;
85 | if (qr) {
86 | display = (
87 |
88 |
89 | {selectedAddress}
90 |
91 |
99 |
100 | );
101 | receiveButton = (
102 | {
105 | setQr("");
106 | }}
107 | >
108 | Hide
109 |
110 | );
111 | privateKeyButton = (
112 | {
115 | setPK(selectedAddress);
116 | setQr("");
117 | }}
118 | >
119 | Private Key
120 |
121 | );
122 | } else if (pk) {
123 | const pk = localStorage.getItem("metaPrivateKey");
124 | const wallet = new ethers.Wallet(pk);
125 |
126 | if (wallet.address !== selectedAddress) {
127 | display = (
128 |
129 | *injected account*, private key unknown
130 |
131 | );
132 | } else {
133 | const extraPkDisplayAdded = {};
134 | const extraPkDisplay = [];
135 | extraPkDisplayAdded[wallet.address] = true;
136 | extraPkDisplay.push(
137 | ,
142 | );
143 | for (const key in localStorage) {
144 | if (key.indexOf("metaPrivateKey_backup") >= 0) {
145 | console.log(key);
146 | const pastpk = localStorage.getItem(key);
147 | const pastwallet = new ethers.Wallet(pastpk);
148 | if (!extraPkDisplayAdded[pastwallet.address] /* && selectedAddress!=pastwallet.address */) {
149 | extraPkDisplayAdded[pastwallet.address] = true;
150 | extraPkDisplay.push(
151 | ,
157 | );
158 | }
159 | }
160 | }
161 |
162 | display = (
163 |
164 |
Private Key:
165 |
166 |
167 | {pk}
168 |
169 |
170 |
171 |
172 |
173 | Point your camera phone at qr code to open in
174 |
175 | burner wallet
176 |
177 | :
178 |
179 |
187 |
188 |
189 | {"https://xdai.io/" + pk}
190 |
191 |
192 | {extraPkDisplay ? (
193 |
194 |
Known Private Keys:
195 | {extraPkDisplay}
196 | {
198 | const currentPrivateKey = window.localStorage.getItem("metaPrivateKey");
199 | if (currentPrivateKey) {
200 | window.localStorage.setItem("metaPrivateKey_backup" + Date.now(), currentPrivateKey);
201 | }
202 | const randomWallet = ethers.Wallet.createRandom();
203 | const privateKey = randomWallet._signingKey().privateKey;
204 | window.localStorage.setItem("metaPrivateKey", privateKey);
205 | window.location.reload();
206 | }}
207 | >
208 | Generate
209 |
210 |
211 | ) : (
212 | ""
213 | )}
214 |
215 | );
216 | }
217 |
218 | receiveButton = (
219 | {
222 | setQr(selectedAddress);
223 | setPK("");
224 | }}
225 | >
226 | Receive
227 |
228 | );
229 | privateKeyButton = (
230 | {
233 | setPK("");
234 | setQr("");
235 | }}
236 | >
237 | Hide
238 |
239 | );
240 | } else {
241 | const inputStyle = {
242 | padding: 10,
243 | };
244 |
245 | display = (
246 |
247 |
256 |
257 | {
261 | setAmount(value);
262 | }}
263 | />
264 |
265 |
266 | );
267 | receiveButton = (
268 | {
271 | setQr(selectedAddress);
272 | setPK("");
273 | }}
274 | >
275 | Receive
276 |
277 | );
278 | privateKeyButton = (
279 | {
282 | setPK(selectedAddress);
283 | setQr("");
284 | }}
285 | >
286 | Private Key
287 |
288 | );
289 | }
290 |
291 | return (
292 |
293 | {providerSend}
294 |
298 | {selectedAddress ? : }
299 |
300 |
301 |
302 |
303 | }
304 | onOk={() => {
305 | setQr();
306 | setPK();
307 | setOpen(!open);
308 | }}
309 | onCancel={() => {
310 | setQr();
311 | setPK();
312 | setOpen(!open);
313 | }}
314 | footer={[
315 | privateKeyButton,
316 | receiveButton,
317 | {
323 | const tx = Transactor(props.signer || props.provider);
324 |
325 | let value;
326 | try {
327 | value = ethers.utils.parseEther("" + amount);
328 | } catch (e) {
329 | // failed to parseEther, try something else
330 | value = ethers.utils.parseEther("" + parseFloat(amount).toFixed(8));
331 | }
332 |
333 | tx({
334 | to: toAddress,
335 | value,
336 | });
337 | setOpen(!open);
338 | setQr();
339 | }}
340 | >
341 | Send
342 | ,
343 | ]}
344 | >
345 | {display}
346 |
347 |
348 | );
349 | }
350 |
--------------------------------------------------------------------------------
/packages/react-app/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 Events } from "./Events";
10 | export { default as Faucet } from "./Faucet";
11 | export { default as GasGauge } from "./GasGauge";
12 | export { default as Header } from "./Header";
13 | export { default as Provider } from "./Provider";
14 | export { default as Ramp } from "./Ramp";
15 | export { default as Swap } from "./Swap";
16 | export { default as ThemeSwitch } from "./ThemeSwitch";
17 | export { default as Timeline } from "./Timeline";
18 | export { default as TokenBalance } from "./TokenBalance";
19 | export { default as Wallet } from "./Wallet";
20 | export { default as L2Bridge } from "./L2Bridge";
21 |
--------------------------------------------------------------------------------
/packages/react-app/src/constants.js:
--------------------------------------------------------------------------------
1 | // MY INFURA_ID, SWAP IN YOURS FROM https://infura.io/dashboard/ethereum
2 | export const INFURA_ID = "7b0e75d38d424750b92791477924d133";
3 |
4 | // MY ETHERSCAN_ID, SWAP IN YOURS FROM https://etherscan.io/myapikey
5 | export const ETHERSCAN_KEY = "DNXJA8RX2Q3VZ4URQIWP7Z68CJXQZSC6AW";
6 |
7 | // BLOCKNATIVE ID FOR Notify.js:
8 | export const BLOCKNATIVE_DAPPID = "0b58206a-f3c0-4701-a62f-73c7243e8c77";
9 |
10 | export const ALCHEMY_KEY = "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF";
11 |
12 | export const NETWORKS = {
13 | localhost: {
14 | name: "localhost",
15 | color: "#666666",
16 | chainId: 31337,
17 | blockExplorer: "",
18 | rpcUrl: "http://" + (global.window ? window.location.hostname : "localhost") + ":8545",
19 | },
20 | mainnet: {
21 | name: "mainnet",
22 | color: "#ff8b9e",
23 | chainId: 1,
24 | rpcUrl: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_KEY}`,
25 | blockExplorer: "https://etherscan.io/",
26 | },
27 | kovan: {
28 | name: "kovan",
29 | color: "#7003DD",
30 | chainId: 42,
31 | rpcUrl: `https://kovan.infura.io/v3/${INFURA_ID}`,
32 | blockExplorer: "https://kovan.etherscan.io/",
33 | faucet: "https://gitter.im/kovan-testnet/faucet", // https://faucet.kovan.network/
34 | },
35 | rinkeby: {
36 | name: "rinkeby",
37 | color: "#e0d068",
38 | chainId: 4,
39 | rpcUrl: `https://rinkeby.infura.io/v3/${INFURA_ID}`,
40 | faucet: "https://faucet.rinkeby.io/",
41 | blockExplorer: "https://rinkeby.etherscan.io/",
42 | },
43 | ropsten: {
44 | name: "ropsten",
45 | color: "#F60D09",
46 | chainId: 3,
47 | faucet: "https://faucet.ropsten.be/",
48 | blockExplorer: "https://ropsten.etherscan.io/",
49 | rpcUrl: `https://ropsten.infura.io/v3/${INFURA_ID}`,
50 | },
51 | goerli: {
52 | name: "goerli",
53 | color: "#0975F6",
54 | chainId: 5,
55 | faucet: "https://goerli-faucet.slock.it/",
56 | blockExplorer: "https://goerli.etherscan.io/",
57 | rpcUrl: `https://goerli.infura.io/v3/${INFURA_ID}`,
58 | },
59 | xdai: {
60 | name: "xdai",
61 | color: "#48a9a6",
62 | chainId: 100,
63 | price: 1,
64 | gasPrice: 1000000000,
65 | rpcUrl: "https://dai.poa.network",
66 | faucet: "https://xdai-faucet.top/",
67 | blockExplorer: "https://blockscout.com/poa/xdai/",
68 | },
69 | matic: {
70 | name: "matic",
71 | color: "#2bbdf7",
72 | chainId: 137,
73 | price: 1,
74 | gasPrice: 1000000000,
75 | rpcUrl: "https://rpc-mainnet.maticvigil.com",
76 | faucet: "https://faucet.matic.network/",
77 | blockExplorer: "https://explorer-mainnet.maticvigil.com//",
78 | },
79 | mumbai: {
80 | name: "mumbai",
81 | color: "#92D9FA",
82 | chainId: 80001,
83 | price: 1,
84 | gasPrice: 1000000000,
85 | rpcUrl: "https://rpc-mumbai.maticvigil.com",
86 | faucet: "https://faucet.matic.network/",
87 | blockExplorer: "https://mumbai-explorer.matic.today/",
88 | },
89 | localArbitrum: {
90 | name: "localArbitrum",
91 | color: "#50a0ea",
92 | chainId: 153869338190755,
93 | blockExplorer: "",
94 | rpcUrl: `http://localhost:8547`,
95 | },
96 | localArbitrumL1: {
97 | name: "localArbitrumL1",
98 | color: "#50a0ea",
99 | chainId: 44010,
100 | blockExplorer: "",
101 | rpcUrl: `http://localhost:7545`,
102 | },
103 | rinkebyArbitrum: {
104 | name: "Arbitrum Testnet",
105 | color: "#50a0ea",
106 | chainId: 421611,
107 | blockExplorer: "https://rinkeby-explorer.arbitrum.io/#/",
108 | rpcUrl: `https://rinkeby.arbitrum.io/rpc`,
109 | },
110 | arbitrum: {
111 | name: "Arbitrum",
112 | color: "#50a0ea",
113 | chainId: 42161,
114 | blockExplorer: "https://explorer.arbitrum.io/#/",
115 | rpcUrl: `https://arb1.arbitrum.io/rpc`,
116 | gasPrice: 0,
117 | },
118 | localOptimismL1: {
119 | name: "localOptimismL1",
120 | color: "#f01a37",
121 | chainId: 31337,
122 | blockExplorer: "",
123 | rpcUrl: "http://" + (global.window ? window.location.hostname : "localhost") + ":9545",
124 | },
125 | localOptimism: {
126 | name: "localOptimism",
127 | color: "#f01a37",
128 | chainId: 420,
129 | blockExplorer: "",
130 | rpcUrl: "http://" + (global.window ? window.location.hostname : "localhost") + ":8545",
131 | gasPrice: 0,
132 | },
133 | kovanOptimism: {
134 | name: "kovanOptimism",
135 | color: "#f01a37",
136 | chainId: 69,
137 | blockExplorer: "https://kovan-optimistic.etherscan.io/",
138 | rpcUrl: `https://kovan.optimism.io`,
139 | gasPrice: 0,
140 | },
141 | optimism: {
142 | name: "optimism",
143 | color: "#f01a37",
144 | chainId: 10,
145 | blockExplorer: "https://optimistic.etherscan.io/",
146 | rpcUrl: `https://mainnet.optimism.io`,
147 | },
148 | localAvalanche: {
149 | name: "localAvalanche",
150 | color: "#666666",
151 | chainId: 43112,
152 | blockExplorer: "",
153 | rpcUrl: `http://localhost:9650/ext/bc/C/rpc`,
154 | gasPrice: 225000000000,
155 | },
156 | fujiAvalanche: {
157 | name: "fujiAvalanche",
158 | color: "#666666",
159 | chainId: 43113,
160 | blockExplorer: "https://cchain.explorer.avax-test.network/",
161 | rpcUrl: `https://api.avax-test.network/ext/bc/C/rpc`,
162 | gasPrice: 225000000000,
163 | },
164 | mainnetAvalanche: {
165 | name: "mainnetAvalanche",
166 | color: "#666666",
167 | chainId: 43114,
168 | blockExplorer: "https://cchain.explorer.avax.network/",
169 | rpcUrl: `https://api.avax.network/ext/bc/C/rpc`,
170 | gasPrice: 225000000000,
171 | },
172 | testnetHarmony: {
173 | name: "Harmony Testnet",
174 | color: "#00b0ef",
175 | chainId: 1666700000,
176 | blockExplorer: "https://explorer.pops.one/",
177 | rpcUrl: `https://api.s0.b.hmny.io`,
178 | gasPrice: 1000000000,
179 | },
180 | mainnetHarmony: {
181 | name: "Harmony Mainnet",
182 | color: "#00b0ef",
183 | chainId: 1666600000,
184 | blockExplorer: "https://explorer.harmony.one/",
185 | rpcUrl: `https://api.harmony.one`,
186 | gasPrice: 1000000000,
187 | },
188 | };
189 |
190 | export const NETWORK = chainId => {
191 | for (const n in NETWORKS) {
192 | if (NETWORKS[n].chainId === chainId) {
193 | return NETWORKS[n];
194 | }
195 | }
196 | };
197 |
--------------------------------------------------------------------------------
/packages/react-app/src/ethereumLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scaffold-eth/scaffold-eth-examples/896ea245d30fd8d697c543347a2e13717d394eca/packages/react-app/src/ethereumLogo.png
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/src/helpers/index.js:
--------------------------------------------------------------------------------
1 | export { default as Transactor } from "./Transactor";
2 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/src/hooks/LocalStorage.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | // Hook from useHooks! (https://usehooks.com/useLocalStorage/)
3 | export default function useLocalStorage(key, initialValue, ttl) {
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 && "expiry" in parsedItem && "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 | if (ttl) {
41 | const now = new Date();
42 |
43 | // `item` is an object which contains the original value
44 | // as well as the time when it's supposed to expire
45 | const item = {
46 | value: valueToStore,
47 | expiry: now.getTime() + ttl,
48 | };
49 | window.localStorage.setItem(key, JSON.stringify(item));
50 | } else {
51 | window.localStorage.setItem(key, JSON.stringify(valueToStore));
52 | }
53 | } catch (error) {
54 | // A more advanced implementation would handle the error case
55 | console.log(error);
56 | }
57 | };
58 |
59 | return [storedValue, setValue];
60 | }
61 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export { default as useDebounce } from "./Debounce";
2 | export { default as useLocalStorage } from "./LocalStorage";
3 | export * from "./useContractConfig";
4 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/react-app/src/views/ExampleUI.jsx:
--------------------------------------------------------------------------------
1 | import { SyncOutlined } from "@ant-design/icons";
2 | import { utils } from "ethers";
3 | import { Button, Card, DatePicker, Divider, Input, Progress, Slider, Spin, Switch } from "antd";
4 | import React, { useState } from "react";
5 | import { Address, Balance, Events } from "../components";
6 |
7 | export default function ExampleUI({
8 | purpose,
9 | address,
10 | mainnetProvider,
11 | localProvider,
12 | yourLocalBalance,
13 | price,
14 | tx,
15 | readContracts,
16 | writeContracts,
17 | }) {
18 | const [newPurpose, setNewPurpose] = useState("loading...");
19 |
20 | return (
21 |
22 | {/*
23 | ⚙️ Here is an example UI that displays and sets the purpose in your smart contract:
24 | */}
25 |
26 |
Example UI:
27 |
purpose: {purpose}
28 |
29 |
30 | {
32 | setNewPurpose(e.target.value);
33 | }}
34 | />
35 | {
38 | /* look how you call setPurpose on your contract: */
39 | /* notice how you pass a call back for tx updates too */
40 | const result = tx(writeContracts.YourContract.setPurpose(newPurpose), update => {
41 | console.log("📡 Transaction Update:", update);
42 | if (update && (update.status === "confirmed" || update.status === 1)) {
43 | console.log(" 🍾 Transaction " + update.hash + " finished!");
44 | console.log(
45 | " ⛽️ " +
46 | update.gasUsed +
47 | "/" +
48 | (update.gasLimit || update.gas) +
49 | " @ " +
50 | parseFloat(update.gasPrice) / 1000000000 +
51 | " gwei",
52 | );
53 | }
54 | });
55 | console.log("awaiting metamask/web3 confirm result...", result);
56 | console.log(await result);
57 | }}
58 | >
59 | Set Purpose!
60 |
61 |
62 |
63 | Your Address:
64 |
65 |
66 | ENS Address Example:
67 |
72 |
73 | {/* use utils.formatEther to display a BigNumber: */}
74 |
Your Balance: {yourLocalBalance ? utils.formatEther(yourLocalBalance) : "..."}
75 |
OR
76 |
77 |
78 |
🐳 Example Whale Balance:
79 |
80 |
81 | {/* use utils.formatEther to display a BigNumber: */}
82 |
Your Balance: {yourLocalBalance ? utils.formatEther(yourLocalBalance) : "..."}
83 |
84 | Your Contract Address:
85 |
90 |
91 |
92 | {
94 | /* look how you call setPurpose on your contract: */
95 | tx(writeContracts.YourContract.setPurpose("🍻 Cheers"));
96 | }}
97 | >
98 | Set Purpose to "🍻 Cheers"
99 |
100 |
101 |
102 | {
104 | /*
105 | you can also just craft a transaction and send it to the tx() transactor
106 | here we are sending value straight to the contract's address:
107 | */
108 | tx({
109 | to: writeContracts.YourContract.address,
110 | value: utils.parseEther("0.001"),
111 | });
112 | /* this should throw an error about "no fallback nor receive function" until you add it */
113 | }}
114 | >
115 | Send Value
116 |
117 |
118 |
119 | {
121 | /* look how we call setPurpose AND send some value along */
122 | tx(
123 | writeContracts.YourContract.setPurpose("💵 Paying for this one!", {
124 | value: utils.parseEther("0.001"),
125 | }),
126 | );
127 | /* this will fail until you make the setPurpose function payable */
128 | }}
129 | >
130 | Set Purpose With Value
131 |
132 |
133 |
134 | {
136 | /* you can also just craft a transaction and send it to the tx() transactor */
137 | tx({
138 | to: writeContracts.YourContract.address,
139 | value: utils.parseEther("0.001"),
140 | data: writeContracts.YourContract.interface.encodeFunctionData("setPurpose(string)", [
141 | "🤓 Whoa so 1337!",
142 | ]),
143 | });
144 | /* this should throw an error about "no fallback nor receive function" until you add it */
145 | }}
146 | >
147 | Another Example
148 |
149 |
150 |
151 |
152 | {/*
153 | 📑 Maybe display a list of events?
154 | (uncomment the event and emit line in YourContract.sol! )
155 | */}
156 |
164 |
165 |
166 |
167 | Check out all the{" "}
168 |
173 | 📦 components
174 |
175 |
176 |
177 |
178 |
185 |
186 |
187 | Buttons
188 |
189 |
190 |
191 | Icons
192 |
193 |
194 |
195 | Date Pickers?
196 |
197 | {}} />
198 |
199 |
200 |
201 |
202 | {}} />
203 |
204 |
205 |
206 | {}} />
207 |
208 |
209 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 | );
220 | }
221 |
--------------------------------------------------------------------------------
/packages/react-app/src/views/Hints.jsx:
--------------------------------------------------------------------------------
1 | import { utils } from "ethers";
2 | import { Select } from "antd";
3 | import React, { useState } from "react";
4 | import { Address, AddressInput } from "../components";
5 | import { useTokenList } from "eth-hooks/dapps/dex";
6 |
7 | const { Option } = Select;
8 |
9 | export default function Hints({ yourLocalBalance, mainnetProvider, price, address }) {
10 | // Get a list of tokens from a tokenlist -> see tokenlists.org!
11 | const [selectedToken, setSelectedToken] = useState("Pick a token!");
12 | const listOfTokens = useTokenList(
13 | "https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json",
14 | );
15 |
16 | return (
17 |
18 |
19 | 👷
20 | Edit your contract in
21 |
25 | packages/hardhat/contracts
26 |
27 |
28 |
29 |
30 | 🛰
31 | compile/deploy with
32 |
36 | yarn run deploy
37 |
38 |
39 |
40 |
41 | 🚀
42 | Your contract artifacts are automatically injected into your frontend at
43 |
47 | packages/react-app/src/contracts/
48 |
49 |
50 |
51 |
52 | 🎛
53 | Edit your frontend in
54 |
58 | packages/reactapp/src/App.js
59 |
60 |
61 |
62 |
63 | 🔭
64 | explore the
65 |
76 | 🖇 hooks
77 |
78 | and
79 |
83 | 📦 components
84 |
85 |
86 |
87 |
88 | for example, the
89 |
93 | useBalance()
94 | {" "}
95 | hook keeps track of your balance: {utils.formatEther(yourLocalBalance || 0)}
96 |
97 |
98 |
99 |
105 |
{
109 | console.log(`selected ${value}`);
110 | setSelectedToken(value);
111 | }}
112 | filterOption={(input, option) => option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
113 | optionFilterProp="children"
114 | >
115 | {listOfTokens.map(token => (
116 |
117 | {token.symbol}
118 |
119 | ))}
120 |
121 |
122 |
123 |
124 | as you build your app you'll need web3 specific components like an
125 |
129 | {" "}
130 |
131 | component:
132 |
135 |
(try putting in your address, an ens address, or scanning a QR code)
136 |
137 |
138 |
139 | this balance could be multiplied by
140 |
144 | price
145 | {" "}
146 | that is loaded with the
147 |
151 | usePrice
152 | {" "}
153 | hook with the current value: ${price}
154 |
155 |
156 |
157 |
💧
158 | use the
faucet to send funds to
159 |
163 | {address}
164 |
165 |
166 |
167 |
168 | 📡
169 | deploy to a testnet or mainnet by editing
170 |
174 | packages/hardhat/hardhat.config.js
175 |
176 | and running
177 |
181 | yarn run deploy
182 |
183 |
184 |
185 |
186 |
🔑
187 |
191 | yarn run generate
192 |
193 | will create a deployer account in
194 |
198 | packages/hardhat
199 |
200 |
201 | (use{" "}
202 |
211 | yarn run account
212 | {" "}
213 | to display deployer address and balance)
214 |
215 |
216 |
217 |
218 | ⚙️
219 | build your app with
220 |
224 | yarn run build
225 |
226 |
227 |
228 |
229 | 🚢
230 | ship it!
231 |
235 | yarn run surge
236 |
237 | or
238 |
242 | yarn run s3
243 |
244 | or
245 |
249 | yarn run ipfs
250 |
251 |
252 |
253 |
265 |
266 | 🛠 Check out your browser's developer console for more... (inspect console) 🚀
267 |
268 |
269 | );
270 | }
271 |
--------------------------------------------------------------------------------
/packages/react-app/src/views/Subgraph.jsx:
--------------------------------------------------------------------------------
1 | import { gql, useQuery } from "@apollo/client";
2 | import { Button, Input, Table, Typography } from "antd";
3 | import "antd/dist/antd.css";
4 | import GraphiQL from "graphiql";
5 | import "graphiql/graphiql.min.css";
6 | import fetch from "isomorphic-fetch";
7 | import React, { useState } from "react";
8 | import { Address } from "../components";
9 |
10 | const highlight = {
11 | marginLeft: 4,
12 | marginRight: 8,
13 | /* backgroundColor: "#f9f9f9", */ padding: 4,
14 | borderRadius: 4,
15 | fontWeight: "bolder",
16 | };
17 |
18 | function Subgraph(props) {
19 | function graphQLFetcher(graphQLParams) {
20 | return fetch(props.subgraphUri, {
21 | method: "post",
22 | headers: { "Content-Type": "application/json" },
23 | body: JSON.stringify(graphQLParams),
24 | }).then(response => response.json());
25 | }
26 |
27 | const EXAMPLE_GRAPHQL = `
28 | {
29 | purposes(first: 25, orderBy: createdAt, orderDirection: desc) {
30 | id
31 | purpose
32 | createdAt
33 | sender {
34 | id
35 | }
36 | }
37 | senders {
38 | id
39 | address
40 | purposeCount
41 | }
42 | }
43 | `;
44 | const EXAMPLE_GQL = gql(EXAMPLE_GRAPHQL);
45 | const { loading, data } = useQuery(EXAMPLE_GQL, { pollInterval: 2500 });
46 |
47 | const purposeColumns = [
48 | {
49 | title: "Purpose",
50 | dataIndex: "purpose",
51 | key: "purpose",
52 | },
53 | {
54 | title: "Sender",
55 | key: "id",
56 | render: record => ,
57 | },
58 | {
59 | title: "createdAt",
60 | key: "createdAt",
61 | dataIndex: "createdAt",
62 | render: d => new Date(d * 1000).toISOString(),
63 | },
64 | ];
65 |
66 | const [newPurpose, setNewPurpose] = useState("loading...");
67 |
68 | const deployWarning = (
69 | Warning: 🤔 Have you deployed your subgraph yet?
70 | );
71 |
72 | return (
73 | <>
74 |
75 | You will find that parsing/tracking events with the{" "}
76 |
77 | useEventListener
78 | {" "}
79 | hook becomes a chore for every new project.
80 |
81 |
92 |
93 |
94 | 🚮
95 | Clean up previous data:
96 |
97 | yarn clean-graph-node
98 |
99 |
100 |
101 |
102 |
📡
103 | Spin up a local graph node by running
104 |
105 | yarn run-graph-node
106 |
107 |
108 | {" "}
109 | (requires{" "}
110 |
111 | {" "}
112 | Docker
113 |
114 | ){" "}
115 |
116 |
117 |
118 |
119 | 📝
120 | Create your local subgraph by running
121 |
122 | yarn graph-create-local
123 |
124 | (only required once!)
125 |
126 |
127 |
128 | 🚢
129 | Deploy your local subgraph by running
130 |
131 | yarn graph-ship-local
132 |
133 |
134 |
135 |
136 |
🖍️
137 | Edit your
local subgraph in
138 |
139 | packages/subgraph/src
140 |
141 | (learn more about subgraph definition{" "}
142 |
143 | here
144 |
145 | )
146 |
147 |
148 |
149 | 🤩
150 | Deploy your contracts and your subgraph in one go by running
151 |
152 | yarn deploy-and-graph
153 |
154 |
155 |
156 |
157 |
158 | {
160 | setNewPurpose(e.target.value);
161 | }}
162 | />
163 | {
165 | console.log("newPurpose", newPurpose);
166 | /* look how you call setPurpose on your contract: */
167 | props.tx(props.writeContracts.YourContract.setPurpose(newPurpose));
168 | }}
169 | >
170 | Set Purpose
171 |
172 |
173 |
174 | {data ? (
175 |
176 | ) : (
177 |
{loading ? "Loading..." : deployWarning}
178 | )}
179 |
180 |
181 |
182 |
183 |
184 |
185 | ...
186 | >
187 | );
188 | }
189 |
190 | export default Subgraph;
191 |
--------------------------------------------------------------------------------
/packages/react-app/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 |
--------------------------------------------------------------------------------
/packages/services/graph-node/Dockerfile:
--------------------------------------------------------------------------------
1 | # Full build with debuginfo for graph-node
2 | #
3 | # The expectation if that the docker build uses the parent directory as PWD
4 | # by running something like the following
5 | # docker build --target STAGE -f docker/Dockerfile .
6 |
7 | FROM rust:latest as graph-node-build
8 |
9 | ARG COMMIT_SHA=unknown
10 | ARG REPO_NAME=unknown
11 | ARG BRANCH_NAME=unknown
12 | ARG TAG_NAME=unknown
13 |
14 | ADD . /graph-node
15 |
16 | RUN cd /graph-node \
17 | && RUSTFLAGS="-g" cargo install --locked --path node \
18 | && cargo clean \
19 | && objcopy --only-keep-debug /usr/local/cargo/bin/graph-node /usr/local/cargo/bin/graph-node.debug \
20 | && strip -g /usr/local/cargo/bin/graph-node \
21 | && cd /usr/local/cargo/bin \
22 | && objcopy --add-gnu-debuglink=graph-node.debug graph-node \
23 | && echo "REPO_NAME='$REPO_NAME'" > /etc/image-info \
24 | && echo "TAG_NAME='$TAG_NAME'" >> /etc/image-info \
25 | && echo "BRANCH_NAME='$BRANCH_NAME'" >> /etc/image-info \
26 | && echo "COMMIT_SHA='$COMMIT_SHA'" >> /etc/image-info \
27 | && echo "CARGO_VERSION='$(cargo --version)'" >> /etc/image-info \
28 | && echo "RUST_VERSION='$(rustc --version)'" >> /etc/image-info
29 |
30 | # The graph-node runtime image with only the executable
31 | FROM debian:buster-slim as graph-node
32 | ENV RUST_LOG ""
33 | ENV GRAPH_LOG ""
34 | ENV EARLY_LOG_CHUNK_SIZE ""
35 | ENV ETHEREUM_RPC_PARALLEL_REQUESTS ""
36 | ENV ETHEREUM_BLOCK_CHUNK_SIZE ""
37 |
38 | ENV postgres_host ""
39 | ENV postgres_user ""
40 | ENV postgres_pass ""
41 | ENV postgres_db ""
42 | # The full URL to the IPFS node
43 | ENV ipfs ""
44 | # The etherum network(s) to connect to. Set this to a space-separated
45 | # list of the networks where each entry has the form NAME:URL
46 | ENV ethereum ""
47 | # The role the node should have, one of index-node, query-node, or
48 | # combined-node
49 | ENV node_role "combined-node"
50 | # The name of this node
51 | ENV node_id "default"
52 |
53 | # HTTP port
54 | EXPOSE 8000
55 | # WebSocket port
56 | EXPOSE 8001
57 | # JSON-RPC port
58 | EXPOSE 8020
59 |
60 | RUN apt-get update \
61 | && apt-get install -y libpq-dev ca-certificates netcat
62 |
63 | ADD docker/wait_for docker/start /usr/local/bin/
64 | COPY --from=graph-node-build /usr/local/cargo/bin/graph-node /usr/local/bin
65 | COPY --from=graph-node-build /etc/image-info /etc/image-info
66 | COPY docker/Dockerfile /Dockerfile
67 | CMD start
68 |
69 | # Debug image to access core dumps
70 | FROM graph-node-build as graph-node-debug
71 | RUN apt-get update \
72 | && apt-get install -y curl gdb postgresql-client
73 |
74 | COPY docker/Dockerfile /Dockerfile
75 | COPY docker/bin/* /usr/local/bin/
76 |
--------------------------------------------------------------------------------
/packages/services/graph-node/README.md:
--------------------------------------------------------------------------------
1 | # Graph Node Docker Image
2 |
3 | Preconfigured Docker image for running a Graph Node.
4 |
5 | ## Usage
6 |
7 | ```sh
8 | docker run -it \
9 | -e postgres_host=[:] \
10 | -e postgres_user= \
11 | -e postgres_pass= \
12 | -e postgres_db= \
13 | -e ipfs=: \
14 | -e ethereum=: \
15 | graphprotocol/graph-node:latest
16 | ```
17 |
18 | ### Example usage
19 |
20 | ```sh
21 | docker run -it \
22 | -e postgres_host=host.docker.internal:5432
23 | -e postgres_user=graph-node \
24 | -e postgres_pass=oh-hello \
25 | -e postgres_db=graph-node \
26 | -e ipfs=host.docker.internal:5001 \
27 | -e ethereum=mainnet:http://localhost:8545/ \
28 | graphprotocol/graph-node:latest
29 | ```
30 |
31 | ## Docker Compose
32 |
33 | The Docker Compose setup requires an Ethereum network name and node
34 | to connect to. By default, it will use `mainnet:http://host.docker.internal:8545`
35 | in order to connect to an Ethereum node running on your host machine.
36 | You can replace this with anything else in `docker-compose.yaml`.
37 |
38 | > **Note for Linux users:** On Linux, `host.docker.internal` is not
39 | > currently supported. Instead, you will have to replace it with the
40 | > IP address of your Docker host (from the perspective of the Graph
41 | > Node container).
42 | > To do this, run:
43 | >
44 | > ```
45 | > CONTAINER_ID=$(docker container ls | grep graph-node | cut -d' ' -f1)
46 | > docker exec $CONTAINER_ID /bin/bash -c 'ip route | awk \'/^default via /{print $3}\''
47 | > ```
48 | >
49 | > This will print the host's IP address. Then, put it into `docker-compose.yml`:
50 | >
51 | > ```
52 | > sed -i -e 's/host.docker.internal//g' docker-compose.yml
53 | > ```
54 |
55 | After you have set up an Ethereum node—e.g. Ganache or Parity—simply
56 | clone this repository and run
57 |
58 | ```sh
59 | docker-compose up
60 | ```
61 |
62 | This will start IPFS, Postgres and Graph Node in Docker and create persistent
63 | data directories for IPFS and Postgres in `./data/ipfs` and `./data/postgres`. You
64 | can access these via:
65 |
66 | - Graph Node:
67 | - GraphiQL: `http://localhost:8000/`
68 | - HTTP: `http://localhost:8000/subgraphs/name/`
69 | - WebSockets: `ws://localhost:8001/subgraphs/name/`
70 | - Admin: `http://localhost:8020/`
71 | - IPFS:
72 | - `127.0.0.1:5001` or `/ip4/127.0.0.1/tcp/5001`
73 | - Postgres:
74 | - `postgresql://graph-node:let-me-in@localhost:5432/graph-node`
75 |
76 | Once this is up and running, you can use
77 | [`graph-cli`](https://github.com/graphprotocol/graph-cli) to create and
78 | deploy your subgraph to the running Graph Node.
79 |
--------------------------------------------------------------------------------
/packages/services/graph-node/bin/create:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | if [ $# != 1 ]; then
4 | echo "usage: create "
5 | exit 1
6 | fi
7 |
8 | api="http://index-node.default/"
9 |
10 | data=$(printf '{"jsonrpc": "2.0", "method": "subgraph_create", "params": {"name":"%s"}, "id":"1"}' "$1")
11 | curl -s -H "content-type: application/json" --data "$data" "$api"
12 |
--------------------------------------------------------------------------------
/packages/services/graph-node/bin/debug:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | if [ -f "$1" ]
4 | then
5 | exec rust-gdb -c "$1" /usr/local/cargo/bin/graph-node
6 | else
7 | echo "usage: debug "
8 | exit 1
9 | fi
10 |
--------------------------------------------------------------------------------
/packages/services/graph-node/bin/deploy:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | if [ $# != 3 ]; then
4 | echo "usage: deploy "
5 | exit 1
6 | fi
7 |
8 | api="http://index-node.default/"
9 |
10 | echo "Deploying $1 (deployment $2)"
11 | data=$(printf '{"jsonrpc": "2.0", "method": "subgraph_deploy", "params": {"name":"%s", "ipfs_hash":"%s", "node_id":"%s"}, "id":"1"}' "$1" "$2" "$3")
12 | curl -s -H "content-type: application/json" --data "$data" "$api"
13 |
--------------------------------------------------------------------------------
/packages/services/graph-node/bin/reassign:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | if [ $# -lt 3 ]; then
4 | echo "usage: reassign "
5 | exit 1
6 | fi
7 |
8 | api="http://index-node.default/"
9 |
10 | echo Assigning to "$3"
11 | data=$(printf '{"jsonrpc": "2.0", "method": "subgraph_reassign", "params": {"name":"%s", "ipfs_hash":"%s", "node_id":"%s"}, "id":"1"}' "$1" "$2" "$3")
12 | curl -s -H "content-type: application/json" --data "$data" "$api"
13 |
--------------------------------------------------------------------------------
/packages/services/graph-node/bin/remove:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | if [ $# != 1 ]; then
4 | echo "usage: create "
5 | exit 1
6 | fi
7 |
8 | api="http://index-node.default/"
9 |
10 | data=$(printf '{"jsonrpc": "2.0", "method": "subgraph_remove", "params": {"name":"%s"}, "id":"1"}' "$1")
11 | curl -s -H "content-type: application/json" --data "$data" "$api"
12 |
--------------------------------------------------------------------------------
/packages/services/graph-node/build.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | # This file is only here to ease testing/development. Official images are
4 | # built using the 'cloudbuild.yaml' file
5 |
6 | type -p podman > /dev/null && docker=podman || docker=docker
7 |
8 | cd $(dirname $0)/..
9 |
10 | if [ -d .git ]
11 | then
12 | COMMIT_SHA=$(git rev-parse HEAD)
13 | TAG_NAME=$(git tag --points-at HEAD)
14 | REPO_NAME="Checkout of $(git remote get-url origin) at $(git describe --dirty)"
15 | BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
16 | fi
17 | for stage in graph-node-build graph-node graph-node-debug
18 | do
19 | $docker build --target $stage \
20 | --build-arg "COMMIT_SHA=$COMMIT_SHA" \
21 | --build-arg "REPO_NAME=$REPO_NAME" \
22 | --build-arg "BRANCH_NAME=$BRANCH_NAME" \
23 | --build-arg "TAG_NAME=$TAG_NAME" \
24 | -t $stage \
25 | -f docker/Dockerfile .
26 | done
27 |
--------------------------------------------------------------------------------
/packages/services/graph-node/cloudbuild.yaml:
--------------------------------------------------------------------------------
1 | options:
2 | machineType: "N1_HIGHCPU_32"
3 | timeout: 1800s
4 | steps:
5 | - name: 'gcr.io/cloud-builders/docker'
6 | args: ['build', '--target', 'graph-node-build',
7 | '--build-arg', 'COMMIT_SHA=$COMMIT_SHA',
8 | '--build-arg', 'REPO_NAME=$REPO_NAME',
9 | '--build-arg', 'BRANCH_NAME=$BRANCH_NAME',
10 | '--build-arg', 'TAG_NAME=$TAG_NAME',
11 | '-t', 'gcr.io/$PROJECT_ID/graph-node-build:$SHORT_SHA',
12 | '-f', 'docker/Dockerfile', '.']
13 | - name: 'gcr.io/cloud-builders/docker'
14 | args: ['build', '--target', 'graph-node',
15 | '--build-arg', 'COMMIT_SHA=$COMMIT_SHA',
16 | '--build-arg', 'REPO_NAME=$REPO_NAME',
17 | '--build-arg', 'BRANCH_NAME=$BRANCH_NAME',
18 | '--build-arg', 'TAG_NAME=$TAG_NAME',
19 | '-t', 'gcr.io/$PROJECT_ID/graph-node:$SHORT_SHA',
20 | '-f', 'docker/Dockerfile', '.']
21 | - name: 'gcr.io/cloud-builders/docker'
22 | args: ['build', '--target', 'graph-node-debug',
23 | '--build-arg', 'COMMIT_SHA=$COMMIT_SHA',
24 | '--build-arg', 'REPO_NAME=$REPO_NAME',
25 | '--build-arg', 'BRANCH_NAME=$BRANCH_NAME',
26 | '--build-arg', 'TAG_NAME=$TAG_NAME',
27 | '-t', 'gcr.io/$PROJECT_ID/graph-node-debug:$SHORT_SHA',
28 | '-f', 'docker/Dockerfile', '.']
29 | - name: 'gcr.io/cloud-builders/docker'
30 | args: ['tag',
31 | 'gcr.io/$PROJECT_ID/graph-node:$SHORT_SHA',
32 | 'lutter/graph-node:$SHORT_SHA']
33 | - name: 'gcr.io/cloud-builders/docker'
34 | entrypoint: 'bash'
35 | args: ['docker/tag.sh']
36 | secretEnv: ['PASSWORD']
37 | env:
38 | - 'SHORT_SHA=$SHORT_SHA'
39 | - 'TAG_NAME=$TAG_NAME'
40 | - 'PROJECT_ID=$PROJECT_ID'
41 | - 'DOCKER_HUB_USER=$_DOCKER_HUB_USER'
42 | - 'BRANCH_NAME=$BRANCH_NAME'
43 | images:
44 | - 'gcr.io/$PROJECT_ID/graph-node-build:$SHORT_SHA'
45 | - 'gcr.io/$PROJECT_ID/graph-node:$SHORT_SHA'
46 | - 'gcr.io/$PROJECT_ID/graph-node-debug:$SHORT_SHA'
47 | substitutions:
48 | # The owner of the access token whose encrypted value is in PASSWORD
49 | _DOCKER_HUB_USER: "lutter"
50 | secrets:
51 | - kmsKeyName: projects/the-graph-staging/locations/global/keyRings/docker/cryptoKeys/docker-hub-push
52 | secretEnv:
53 | PASSWORD: 'CiQAdfFldbmUiHgGP1lPq6bAOfd+VQ/dFwyohB1IQwiwQg03ZE8STQDvWKpv6eJHVUN1YoFC5FcooJrH+Stvx9oMD7jBjgxEH5ngIiAysWP3E4Pgxt/73xnaanbM1EQ94eVFKCiY0GaEKFNu0BJx22vCYmU4'
54 |
--------------------------------------------------------------------------------
/packages/services/graph-node/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | graph-node:
4 | image: graphprotocol/graph-node:latest
5 | ports:
6 | - "8000:8000"
7 | - "8001:8001"
8 | - "8020:8020"
9 | - "8030:8030"
10 | - "8040:8040"
11 | depends_on:
12 | - ipfs
13 | - postgres
14 | environment:
15 | postgres_host: postgres
16 | postgres_user: graph-node
17 | postgres_pass: let-me-in
18 | postgres_db: graph-node
19 | ipfs: "ipfs:5001"
20 | ethereum: "localhost:http://host.docker.internal:8545"
21 | GRAPH_LOG: info
22 | ipfs:
23 | image: ipfs/go-ipfs:v0.4.23
24 | ports:
25 | - "5001:5001"
26 | volumes:
27 | - ./data/ipfs:/data/ipfs
28 | postgres:
29 | image: postgres
30 | ports:
31 | - "5432:5432"
32 | command: ["postgres", "-cshared_preload_libraries=pg_stat_statements"]
33 | environment:
34 | POSTGRES_USER: graph-node
35 | POSTGRES_PASSWORD: let-me-in
36 | POSTGRES_DB: graph-node
37 | volumes:
38 | - ./data/postgres:/var/lib/postgresql/data
39 |
--------------------------------------------------------------------------------
/packages/services/graph-node/hooks/post_checkout:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 | set -x
5 |
6 | echo "Setting SOURCE_BRANCH to ${SOURCE_BRANCH}"
7 |
8 | sed -i "s@^ENV SOURCE_BRANCH \"master\"@ENV SOURCE_BRANCH \"${SOURCE_BRANCH}\"@g" Dockerfile
9 |
--------------------------------------------------------------------------------
/packages/services/graph-node/setup.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 |
5 | if ! which docker 2>&1 > /dev/null; then
6 | echo "Please install 'docker' first"
7 | exit 1
8 | fi
9 |
10 | if ! which docker-compose 2>&1 > /dev/null; then
11 | echo "Please install 'docker-compose' first"
12 | exit 1
13 | fi
14 |
15 | if ! which jq 2>&1 > /dev/null; then
16 | echo "Please install 'jq' first"
17 | exit 1
18 | fi
19 |
20 | # Create the graph-node container
21 | docker-compose up --no-start graph-node
22 |
23 | # Start graph-node so we can inspect it
24 | docker-compose start graph-node
25 |
26 | # Identify the container ID
27 | CONTAINER_ID=$(docker container ls | grep graph-node | cut -d' ' -f1)
28 |
29 | # Inspect the container to identify the host IP address
30 | HOST_IP=$(docker inspect "$CONTAINER_ID" | jq -r .[0].NetworkSettings.Networks[].Gateway)
31 |
32 | echo "Host IP: $HOST_IP"
33 |
34 | # Inject the host IP into docker-compose.yml
35 | sed -i -e "s/host.docker.internal/$HOST_IP/g" docker-compose.yml
36 |
37 | function stop_graph_node {
38 | # Ensure graph-node is stopped
39 | docker-compose stop graph-node
40 | }
41 |
42 | trap stop_graph_node EXIT
43 |
--------------------------------------------------------------------------------
/packages/services/graph-node/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | save_coredumps() {
4 | graph_dir=/var/lib/graph
5 | datestamp=$(date +"%Y-%m-%dT%H:%M:%S")
6 | ls /core.* >& /dev/null && have_cores=yes || have_cores=no
7 | if [ -d "$graph_dir" -a "$have_cores" = yes ]
8 | then
9 | core_dir=$graph_dir/cores
10 | mkdir -p $core_dir
11 | exec >> "$core_dir"/messages 2>&1
12 | echo "${HOSTNAME##*-} Saving core dump on ${HOSTNAME} at ${datestamp}"
13 |
14 | dst="$core_dir/$datestamp-${HOSTNAME}"
15 | mkdir "$dst"
16 | cp /usr/local/bin/graph-node "$dst"
17 | cp /proc/loadavg "$dst"
18 | [ -f /Dockerfile ] && cp /Dockerfile "$dst"
19 | tar czf "$dst/etc.tgz" /etc/
20 | dmesg -e > "$dst/dmesg"
21 | # Capture environment variables, but filter out passwords
22 | env | sort | sed -r -e 's/^(postgres_pass|ELASTICSEARCH_PASSWORD)=.*$/\1=REDACTED/' > "$dst/env"
23 |
24 | for f in /core.*
25 | do
26 | echo "${HOSTNAME##*-} Found core dump $f"
27 | mv "$f" "$dst"
28 | done
29 | echo "${HOSTNAME##*-} Saving done"
30 | fi
31 | }
32 |
33 | wait_for_ipfs() {
34 | # Take the IPFS URL in $1 apart and extract host and port. If no explicit
35 | # host is given, use 443 for https, and 80 otherwise
36 | if [[ "$1" =~ ^((https?)://)?([^:/]+)(:([0-9]+))? ]]
37 | then
38 | proto=${BASH_REMATCH[2]:-http}
39 | host=${BASH_REMATCH[3]}
40 | port=${BASH_REMATCH[5]}
41 | if [ -z "$port" ]
42 | then
43 | [ "$proto" = "https" ] && port=443 || port=80
44 | fi
45 | wait_for "$host:$port" -t 120
46 | else
47 | echo "invalid IPFS URL: $1"
48 | exit 1
49 | fi
50 | }
51 |
52 | start_query_node() {
53 | export DISABLE_BLOCK_INGESTOR=true
54 | graph-node \
55 | --postgres-url "$postgres_url" \
56 | --ethereum-rpc $ethereum \
57 | --ipfs "$ipfs"
58 | }
59 |
60 | start_index_node() {
61 | # Only the index node with the name set in BLOCK_INGESTOR should ingest
62 | # blocks
63 | if [[ ${node_id} != "${BLOCK_INGESTOR}" ]]; then
64 | export DISABLE_BLOCK_INGESTOR=true
65 | fi
66 |
67 | graph-node \
68 | --node-id "${node_id//-/_}" \
69 | --postgres-url "$postgres_url" \
70 | --ethereum-rpc $ethereum \
71 | --ipfs "$ipfs"
72 | }
73 |
74 | start_combined_node() {
75 | graph-node \
76 | --postgres-url "$postgres_url" \
77 | --ethereum-rpc $ethereum \
78 | --ipfs "$ipfs"
79 | }
80 |
81 | postgres_url="postgresql://$postgres_user:$postgres_pass@$postgres_host/$postgres_db"
82 |
83 | wait_for_ipfs "$ipfs"
84 | wait_for "$postgres_host:5432" -t 120
85 | sleep 5
86 |
87 | trap save_coredumps EXIT
88 |
89 | export PGAPPNAME="${node_id-$HOSTNAME}"
90 |
91 | case "${node_role-combined-node}" in
92 | query-node)
93 | start_query_node
94 | ;;
95 | index-node)
96 | start_index_node
97 | ;;
98 | combined-node)
99 | start_combined_node
100 | ;;
101 | *)
102 | echo "Unknown mode for start-node: $1"
103 | echo "usage: start (combined-node|query-node|index-node)"
104 | exit 1
105 | esac
106 |
--------------------------------------------------------------------------------
/packages/services/graph-node/tag.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | # This script is used by cloud build to push Docker images into Docker hub
4 |
5 | tag_and_push() {
6 | tag=$1
7 | docker tag gcr.io/$PROJECT_ID/graph-node:$SHORT_SHA \
8 | graphprotocol/graph-node:$tag
9 | docker push graphprotocol/graph-node:$tag
10 |
11 | docker tag gcr.io/$PROJECT_ID/graph-node-debug:$SHORT_SHA \
12 | graphprotocol/graph-node-debug:$tag
13 | docker push graphprotocol/graph-node-debug:$tag
14 | }
15 |
16 | echo "Logging into Docker Hub"
17 | echo $PASSWORD | docker login --username="$DOCKER_HUB_USER" --password-stdin
18 |
19 | set -ex
20 |
21 | tag_and_push "$SHORT_SHA"
22 |
23 | # Builds on the master branch become the 'latest'
24 | [ "$BRANCH_NAME" = master ] && tag_and_push latest
25 | # Builds of tags set the tag in Docker Hub, too
26 | [ -n "$TAG_NAME" ] && tag_and_push "$TAG_NAME"
27 |
28 | exit 0
29 |
--------------------------------------------------------------------------------
/packages/services/graph-node/wait_for:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # POSIX compatible clone of wait-for-it.sh
4 | # This copy is from https://github.com/eficode/wait-for/commits/master
5 | # at commit 8d9b4446
6 |
7 | TIMEOUT=15
8 | QUIET=0
9 |
10 | echoerr() {
11 | if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
12 | }
13 |
14 | usage() {
15 | exitcode="$1"
16 | cat << USAGE >&2
17 | Usage:
18 | $cmdname host:port [-t timeout] [-- command args]
19 | -q | --quiet Do not output any status messages
20 | -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout
21 | -- COMMAND ARGS Execute command with args after the test finishes
22 | USAGE
23 | exit "$exitcode"
24 | }
25 |
26 | wait_for() {
27 | for i in `seq $TIMEOUT` ; do
28 | nc -z "$HOST" "$PORT" > /dev/null 2>&1
29 |
30 | result=$?
31 | if [ $result -eq 0 ] ; then
32 | if [ $# -gt 0 ] ; then
33 | exec "$@"
34 | fi
35 | exit 0
36 | fi
37 | sleep 1
38 | done
39 | echo "Operation timed out" >&2
40 | exit 1
41 | }
42 |
43 | while [ $# -gt 0 ]
44 | do
45 | case "$1" in
46 | *:* )
47 | HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
48 | PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
49 | shift 1
50 | ;;
51 | -q | --quiet)
52 | QUIET=1
53 | shift 1
54 | ;;
55 | -t)
56 | TIMEOUT="$2"
57 | if [ "$TIMEOUT" = "" ]; then break; fi
58 | shift 2
59 | ;;
60 | --timeout=*)
61 | TIMEOUT="${1#*=}"
62 | shift 1
63 | ;;
64 | --)
65 | shift
66 | break
67 | ;;
68 | --help)
69 | usage 0
70 | ;;
71 | *)
72 | echoerr "Unknown argument: $1"
73 | usage 1
74 | ;;
75 | esac
76 | done
77 |
78 | if [ "$HOST" = "" -o "$PORT" = "" ]; then
79 | echoerr "Error: you need to provide a host and port to test."
80 | usage 2
81 | fi
82 |
83 | wait_for "$@"
84 |
--------------------------------------------------------------------------------
/packages/services/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@scaffold-eth/services",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "submodule-init": "git submodule init && git submodule update --remote",
7 | "arbitrum-init": "cd arbitrum && git submodule init && git submodule update && yarn install",
8 | "arbitrum-build-l1": "cd arbitrum && yarn docker:build:geth",
9 | "arbitrum-run-l1": "cd arbitrum && yarn docker:geth",
10 | "arbitrum-init-l2": "cd arbitrum && yarn demo:initialize",
11 | "arbitrum-run-l2": "cd arbitrum && yarn demo:deploy",
12 | "run-optimism": "cd optimism/ops && make up",
13 | "stop-optimism": "cd optimism/ops && make down",
14 | "run-graph-node": "cd graph-node && docker-compose up",
15 | "remove-graph-node": "cd graph-node && docker-compose down",
16 | "clean-graph-node": "rm -rf graph-node/data/"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/subgraph/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@scaffold-eth/subgraph",
3 | "license": "UNLICENSED",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "codegen": "graph codegen",
7 | "build": "graph build",
8 | "deploy": "graph deploy --node https://api.thegraph.com/deploy/ --ipfs https://api.thegraph.com/ipfs/ GITHUB_USERNAME/your-contract",
9 | "create-local": "graph create --node http://localhost:8020/ scaffold-eth/your-contract",
10 | "remove-local": "graph remove --node http://localhost:8020/ scaffold-eth/your-contract",
11 | "deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 scaffold-eth/your-contract"
12 | },
13 | "dependencies": {
14 | "@graphprotocol/graph-cli": "^0.22.1",
15 | "@graphprotocol/graph-ts": "^0.22.1"
16 | },
17 | "devDependencies": {
18 | "mustache": "^3.1.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/subgraph/src/mapping.ts:
--------------------------------------------------------------------------------
1 | import { BigInt, Address } from "@graphprotocol/graph-ts";
2 | import {
3 | YourContract,
4 | SetPurpose,
5 | } from "../generated/YourContract/YourContract";
6 | import { Purpose, Sender } from "../generated/schema";
7 |
8 | export function handleSetPurpose(event: SetPurpose): void {
9 | let senderString = event.params.sender.toHexString();
10 |
11 | let sender = Sender.load(senderString);
12 |
13 | if (sender === null) {
14 | sender = new Sender(senderString);
15 | sender.address = event.params.sender;
16 | sender.createdAt = event.block.timestamp;
17 | sender.purposeCount = BigInt.fromI32(1);
18 | } else {
19 | sender.purposeCount = sender.purposeCount.plus(BigInt.fromI32(1));
20 | }
21 |
22 | let purpose = new Purpose(
23 | event.transaction.hash.toHex() + "-" + event.logIndex.toString()
24 | );
25 |
26 | purpose.purpose = event.params.purpose;
27 | purpose.sender = senderString;
28 | purpose.createdAt = event.block.timestamp;
29 | purpose.transactionHash = event.transaction.hash.toHex();
30 |
31 | purpose.save();
32 | sender.save();
33 | }
34 |
--------------------------------------------------------------------------------
/packages/subgraph/src/schema.graphql:
--------------------------------------------------------------------------------
1 | type Purpose @entity {
2 | id: ID!
3 | sender: Sender!
4 | purpose: String!
5 | createdAt: BigInt!
6 | transactionHash: String!
7 | }
8 |
9 | type Sender @entity {
10 | id: ID!
11 | address: Bytes!
12 | purposes: [Purpose!] @derivedFrom(field: "sender")
13 | createdAt: BigInt!
14 | purposeCount: BigInt!
15 | }
16 |
--------------------------------------------------------------------------------
/packages/subgraph/src/subgraph.template.yaml:
--------------------------------------------------------------------------------
1 | specVersion: 0.0.2
2 | schema:
3 | file: ./src/schema.graphql
4 | dataSources:
5 | - kind: ethereum/contract
6 | name: YourContract
7 | network: localhost
8 | source:
9 | address: "{{localhost_YourContractAddress}}"
10 | abi: YourContract
11 | startBlock: 1
12 | mapping:
13 | kind: ethereum/events
14 | apiVersion: 0.0.5
15 | language: wasm/assemblyscript
16 | entities:
17 | - Purpose
18 | - Sender
19 | abis:
20 | - name: YourContract
21 | file: ./abis/localhost_YourContract.json
22 | eventHandlers:
23 | - event: SetPurpose(address,string)
24 | handler: handleSetPurpose
25 | file: ./src/mapping.ts
26 |
--------------------------------------------------------------------------------