├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── README.md
├── config-overrides.js
├── package.json
├── public
├── CNAME
├── favicon.ico
├── index.html
├── logo.png
├── logo.svg
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.js
├── DefaultApp.js
├── GnosisSafeApp.js
├── components
│ ├── AccountModal.js
│ ├── BuyDomain.js
│ ├── ConnectButton.js
│ ├── ConnectModal.js
│ ├── DeprecatedNetwork.js
│ ├── DomainEventsGraph.js
│ ├── DomainEventsTable.js
│ ├── DomainInfo.js
│ ├── DomainList.js
│ ├── Domains.js
│ ├── EtherscanAddress.js
│ ├── EtherscanBlock.js
│ ├── EtherscanTransaction.js
│ ├── Footer.js
│ ├── GlobalStyle.js
│ ├── Header.js
│ ├── Identicon.js
│ ├── Lookup.js
│ ├── MintDomainForm.js
│ ├── RecordsForm.js
│ └── Search.js
├── connectors.js
├── hooks.js
├── images
│ ├── cw.svg
│ ├── mm.png
│ └── wc.svg
├── index.css
├── index.js
├── reportWebVitals.js
├── services
│ └── udRegistry.js
├── setupTests.js
├── sources
│ ├── blockchain.js
│ ├── ethereum.js
│ └── thegraph.js
└── utils
│ ├── address.js
│ ├── config.js
│ ├── constants.js
│ ├── contract.js
│ └── namehash.js
└── yarn.lock
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the action will run.
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the main branch
8 | push:
9 | branches: [ main ]
10 |
11 | # Allows you to run this workflow manually from the Actions tab
12 | workflow_dispatch:
13 |
14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
15 | jobs:
16 | # This workflow contains a single job called "build"
17 | build:
18 | # The type of runner that the job will run on
19 | runs-on: ubuntu-latest
20 |
21 | # Steps represent a sequence of tasks that will be executed as part of the job
22 | steps:
23 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
24 | - uses: actions/checkout@v2
25 |
26 | - name: Setup Node
27 | uses: actions/setup-node@v1
28 | with:
29 | node-version: '16.x'
30 |
31 | - run: yarn install
32 |
33 | - name: Build dApp
34 | run: CI=false REACT_APP_GITHUB_REF_SHA=${GITHUB_REF#refs/heads/}.${GITHUB_SHA::7} yarn build
35 |
36 | - name: Upload dApp to gh-pages
37 | uses: peaceiris/actions-gh-pages@v3
38 | with:
39 | github_token: ${{ secrets.ACTIONS_DEPLOY_KEY }}
40 | publish_dir: ./build
41 | cname: web3domain.xyz
42 |
43 | - name: Upload dApp to IPFS
44 | uses: aquiladev/ipfs-action@v0.3.1-alpha.3
45 | id: ipfs_dapp_upload
46 | with:
47 | path: ./build
48 | service: infura
49 | infuraProjectId: ${{ secrets.INFURA_PROJECT_ID }}
50 | infuraProjectSecret: ${{ secrets.INFURA_PROJECT_SECRET }}
51 | timeout: 120000
52 | verbose: true
53 |
54 | - name: Build Gnosis Safe App
55 | run: rm -rf ./build |
56 | CI=false REACT_APP_SAPP_TARGET=true yarn build
57 |
58 | - name: Upload sApp to IPFS
59 | uses: aquiladev/ipfs-action@v0.3.1-alpha.3
60 | id: ipfs_sapp_upload
61 | with:
62 | path: ./build
63 | service: infura
64 | infuraProjectId: ${{ secrets.INFURA_PROJECT_ID }}
65 | infuraProjectSecret: ${{ secrets.INFURA_PROJECT_SECRET }}
66 | timeout: 120000
67 | verbose: true
68 |
69 | - name: Bump version and push tag
70 | id: tag_version
71 | uses: mathieudutour/github-tag-action@v5.4
72 | with:
73 | github_token: ${{ secrets.GITHUB_TOKEN }}
74 |
75 | - name: Create Release
76 | uses: actions/create-release@v1
77 | env:
78 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
79 | with:
80 | tag_name: ${{ steps.tag_version.outputs.new_tag }}
81 | release_name: Release ${{ steps.tag_version.outputs.new_tag }}
82 | body: |
83 | GitHub hosted app [https://web3domain.xyz/](https://web3domain.xyz/)
84 | dApp [https://cloudflare-ipfs.com/ipfs/${{ steps.ipfs_dapp_upload.outputs.hash }}/](https://cloudflare-ipfs.com/ipfs/${{ steps.ipfs_dapp_upload.outputs.hash }}/)
85 | Gnosis Safe App [https://cloudflare-ipfs.com/ipfs/${{ steps.ipfs_sapp_upload.outputs.hash }}/](https://cloudflare-ipfs.com/ipfs/${{ steps.ipfs_sapp_upload.outputs.hash }}/)
86 | Note: [Gnosis Safe App guideline](https://github.com/aquiladev/web3-domain-manager/blob/main/README.md#gnosis-safe-app)
87 |
88 | ${{ steps.tag_version.outputs.changelog }}
89 | draft: false
90 | prerelease: false
91 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
106 |
107 | # dependencies
108 | /node_modules
109 | /.pnp
110 | .pnp.js
111 |
112 | # testing
113 | /coverage
114 |
115 | # production
116 | /build
117 |
118 | # misc
119 | .DS_Store
120 | .env.local
121 | .env.development.local
122 | .env.test.local
123 | .env.production.local
124 |
125 | npm-debug.log*
126 | yarn-debug.log*
127 | yarn-error.log*
128 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Web3 Domain Manager
2 |
3 | Decentralized application for blockchain domain management.
4 |
5 | ## TLDs
6 | - .crypto
7 | - .wallet
8 | - .bitcoin
9 | - .x
10 | - .888
11 | - .nft
12 | - .dao
13 | - .blockchain
14 |
15 | ## Blockchain
16 | The dapp works with Ethereum blockchain.
17 |
18 | Supported networks: Mainnet, Goerli, Polygon, Mumbai
19 |
20 | ## Gnosis Safe App
21 | [Gnosis Safe](https://gnosis-safe.io/) is a smart contract wallet running on Ethereum that requires a minimum number of people to approve a transaction before it can occur (M-of-N). If for example you have 3 main stakeholders in your business, you are able to set up the wallet to require approval from all 3 people before the transaction is sent. This assures that no single person could compromise the funds.
22 |
23 | In order to use this dApp in Gnosis Safe you need to open Gnosis Safe and add Web3 Domain Manager.
24 |
25 | Gnosis Safe has multiple environments:
26 | - Mainnet - https://gnosis-safe.io/app/
27 | - Goerli - https://gnosis-safe.io/app/goerli/
28 |
29 | Use latest [Web3 Domain Manager release Gnosis Safe App version](https://github.com/aquiladev/web3-domain-manager/releases/latest)
30 |
--------------------------------------------------------------------------------
/config-overrides.js:
--------------------------------------------------------------------------------
1 | /* config-overrides.js */
2 | // const path = require("path");
3 | // const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
4 |
5 | module.exports = {
6 | // The function to use to create a webpack dev server configuration when running the development
7 | // server with 'npm run start' or 'yarn start'.
8 | // Example: set the dev server to use a specific certificate in https.
9 | devServer: function(configFunction) {
10 | // Return the replacement function for create-react-app to use to generate the Webpack
11 | // Development Server config. "configFunction" is the function that would normally have
12 | // been used to generate the Webpack Development server config - you can use it to create
13 | // a starting configuration to then modify instead of having to create a config from scratch.
14 | return function(proxy, allowedHost) {
15 | // Create the default config by calling configFunction with the proxy/allowedHost parameters
16 | const config = configFunction(proxy, allowedHost);
17 |
18 | config.headers = {
19 | "Access-Control-Allow-Origin": "*",
20 | "Access-Control-Allow-Methods": "GET",
21 | "Access-Control-Allow-Headers":
22 | "X-Requested-With, content-type, Authorization",
23 | };
24 |
25 | // Return your customised Webpack Development Server config.
26 | return config;
27 | };
28 | },
29 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web3-domain-manager",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": ".",
6 | "dependencies": {
7 | "@apollo/client": "^3.7.1",
8 | "@gitgraph/react": "1.5.4",
9 | "@gnosis.pm/safe-apps-provider": "0.9.2",
10 | "@gnosis.pm/safe-apps-react-sdk": "4.0.8",
11 | "@gnosis.pm/safe-react-components": "0.5.0",
12 | "@material-ui/core": "4.12.4",
13 | "@material-ui/icons": "4.11.2",
14 | "@material-ui/lab": "4.0.0-alpha.57",
15 | "@metamask/jazzicon": "^2.0.0",
16 | "@testing-library/jest-dom": "^5.11.4",
17 | "@testing-library/react": "^11.1.0",
18 | "@testing-library/user-event": "^12.1.10",
19 | "@web3-react/core": "^6.1.9",
20 | "@web3-react/injected-connector": "6.0.7",
21 | "@web3-react/walletconnect-connector": "^6.2.4",
22 | "@web3-react/walletlink-connector": "^6.2.3",
23 | "crypto-js": "^4.0.0",
24 | "ethers": "^5.5.1",
25 | "graphql": "^16.6.0",
26 | "react": "^17.0.1",
27 | "react-app-rewired": "2.1.8",
28 | "react-copy-to-clipboard": "^5.1.0",
29 | "react-dom": "^17.0.1",
30 | "react-ga4": "^2.1.0",
31 | "react-github-btn": "^1.4.0",
32 | "react-router-dom": "^5.3.0",
33 | "react-scripts": "4.0.1",
34 | "styled-components": "5.2.3",
35 | "uns": "https://github.com/unstoppabledomains/uns#v0.8.36",
36 | "web-vitals": "^0.2.4"
37 | },
38 | "scripts": {
39 | "start": "react-app-rewired start",
40 | "start:sapp": "REACT_APP_SAPP_TARGET=true react-app-rewired start",
41 | "build": "react-app-rewired build",
42 | "test": "react-app-rewired test",
43 | "eject": "react-scripts eject"
44 | },
45 | "eslintConfig": {
46 | "extends": [
47 | "react-app",
48 | "react-app/jest"
49 | ]
50 | },
51 | "browserslist": {
52 | "production": [
53 | ">0.2%",
54 | "not dead",
55 | "not op_mini all"
56 | ],
57 | "development": [
58 | "last 1 chrome version",
59 | "last 1 firefox version",
60 | "last 1 safari version"
61 | ]
62 | }
63 | }
--------------------------------------------------------------------------------
/public/CNAME:
--------------------------------------------------------------------------------
1 | web3domain.xyz
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aquiladev/web3-domain-manager/b08dc8a313e2becced2126e6dc90054bcbf8206f/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
18 |
19 |
28 | Web3 Domain Manager
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aquiladev/web3-domain-manager/b08dc8a313e2becced2126e6dc90054bcbf8206f/public/logo.png
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aquiladev/web3-domain-manager/b08dc8a313e2becced2126e6dc90054bcbf8206f/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aquiladev/web3-domain-manager/b08dc8a313e2becced2126e6dc90054bcbf8206f/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "web3_domain_manager",
3 | "name": "Web3 Domain Manager",
4 | "description": "Create and manage blockchain domains (.crypto, .wallet, .bitcoin, .x, .888, .nft, .dao, .blockchain)",
5 | "iconPath": "logo.png",
6 | "icons": [
7 | {
8 | "src": "favicon.ico",
9 | "sizes": "64x64 32x32 24x24 16x16",
10 | "type": "image/x-icon"
11 | },
12 | {
13 | "src": "logo192.png",
14 | "type": "image/png",
15 | "sizes": "192x192"
16 | },
17 | {
18 | "src": "logo512.png",
19 | "type": "image/png",
20 | "sizes": "512x512"
21 | }
22 | ],
23 | "start_url": ".",
24 | "display": "standalone",
25 | "theme_color": "#000000",
26 | "background_color": "#ffffff"
27 | }
28 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import DefaultApp from "./DefaultApp";
4 | import GnosisSafeApp from "./GnosisSafeApp";
5 |
6 | export default function () {
7 | return process.env.REACT_APP_SAPP_TARGET ? : ;
8 | }
9 |
--------------------------------------------------------------------------------
/src/DefaultApp.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { HashRouter, Switch, Route } from "react-router-dom";
3 | import {
4 | makeStyles,
5 | createMuiTheme,
6 | ThemeProvider,
7 | } from "@material-ui/core/styles";
8 | import {
9 | Web3ReactProvider,
10 | useWeb3React,
11 | UnsupportedChainIdError,
12 | } from "@web3-react/core";
13 | import {
14 | NoEthereumProviderError,
15 | UserRejectedRequestError as UserRejectedRequestErrorInjected,
16 | } from "@web3-react/injected-connector";
17 | import { UserRejectedRequestError as UserRejectedRequestErrorWalletConnect } from "@web3-react/walletconnect-connector";
18 | import { ethers } from "ethers";
19 |
20 | import Container from "@material-ui/core/Container";
21 | import Alert from "@material-ui/lab/Alert";
22 |
23 | import GlobalStyle from "./components/GlobalStyle";
24 | import { useEagerConnect, useInactiveListener } from "./hooks";
25 | import Domains from "./components/Domains";
26 | import Lookup from "./components/Lookup";
27 | import Search from "./components/Search";
28 | import Header from "./components/Header";
29 | import Footer from "./components/Footer";
30 | import DeprecatedNetwork from "./components/DeprecatedNetwork";
31 |
32 | const theme = createMuiTheme({
33 | overrides: {
34 | MuiFilledInput: {
35 | input: {
36 | paddingTop: "13px",
37 | },
38 | },
39 | MuiDialogActions: {
40 | root: {
41 | display: "block",
42 | },
43 | },
44 | },
45 | });
46 |
47 | const useStyles = makeStyles((theme) => ({
48 | root: {
49 | flexGrow: 1,
50 | overflow: "hidden",
51 | minHeight: "100%",
52 | },
53 | content: {
54 | paddingTop: 64,
55 | minHeight: 100,
56 | },
57 | title: {
58 | fontSize: "1rem",
59 | [theme.breakpoints.up("sm")]: {
60 | fontSize: "1.5rem",
61 | },
62 | },
63 | backdrop: {
64 | zIndex: theme.zIndex.drawer + 1,
65 | color: "#fff",
66 | },
67 | grow: {
68 | flexGrow: 1,
69 | paddingLeft: 30,
70 | },
71 | navButton: {
72 | color: "white",
73 | },
74 | }));
75 |
76 | function getErrorMessage(error) {
77 | console.error(error);
78 | if (error instanceof NoEthereumProviderError) {
79 | return "No Ethereum browser extension detected, install MetaMask on desktop or visit from a dApp browser on mobile.";
80 | } else if (error instanceof UnsupportedChainIdError) {
81 | return "You're connected to an unsupported network.";
82 | } else if (
83 | error instanceof UserRejectedRequestErrorInjected ||
84 | error instanceof UserRejectedRequestErrorWalletConnect
85 | ) {
86 | return "Please authorize this website to access your Ethereum account.";
87 | } else {
88 | return "An unknown error occurred. Check the console for more details.";
89 | }
90 | }
91 |
92 | function getLibrary(provider) {
93 | return new ethers.providers.Web3Provider(provider);
94 | }
95 |
96 | function DefaultApp() {
97 | const classes = useStyles();
98 |
99 | const { connector, library, account, chainId, active, error } =
100 | useWeb3React();
101 |
102 | const [activatingConnector, setActivatingConnector] = useState();
103 |
104 | useEffect(() => {
105 | if (activatingConnector && activatingConnector === connector) {
106 | setActivatingConnector(undefined);
107 | }
108 | }, [activatingConnector, connector]);
109 |
110 | const triedEager = useEagerConnect();
111 | useInactiveListener(!triedEager || !!activatingConnector);
112 |
113 | return (
114 |
115 |
116 |
117 |
118 |
119 |
120 | {!!error && (
121 |
131 | {getErrorMessage(error)}
132 |
133 | )}
134 | {!active && (
135 |
142 | Please connect your wallet
143 |
144 | )}
145 |
146 |
147 | {active && }
148 |
149 |
150 | {active && }
151 |
152 |
153 | {active && }
154 |
155 |
156 | {account && (
157 |
162 | )}
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | );
171 | }
172 |
173 | export default function () {
174 | return (
175 |
176 |
177 |
178 |
179 | );
180 | }
181 |
--------------------------------------------------------------------------------
/src/GnosisSafeApp.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import { ThemeProvider } from "styled-components";
4 | import { theme, Loader, Title } from "@gnosis.pm/safe-react-components";
5 | import SafeProvider, { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk";
6 | import { SafeAppProvider } from "@gnosis.pm/safe-apps-provider";
7 | import { ethers } from "ethers";
8 | import AppBar from "@material-ui/core/AppBar";
9 | import Toolbar from "@material-ui/core/Toolbar";
10 | import Typography from "@material-ui/core/Typography";
11 | import Container from "@material-ui/core/Container";
12 |
13 | import GlobalStyle from "./components/GlobalStyle";
14 | import Domains from "./components/Domains";
15 | import DeprecatedNetwork from "./components/DeprecatedNetwork";
16 |
17 | const useStyles = makeStyles((theme) => ({
18 | root: {
19 | flexGrow: 1,
20 | overflow: "hidden",
21 | },
22 | title: {
23 | display: "none",
24 | [theme.breakpoints.up("sm")]: {
25 | display: "block",
26 | },
27 | },
28 | content: {
29 | paddingTop: 64,
30 | minHeight: 100,
31 | },
32 | backdrop: {
33 | zIndex: theme.zIndex.drawer + 1,
34 | color: "#fff",
35 | },
36 | grow: {
37 | flexGrow: 1,
38 | paddingLeft: 30,
39 | },
40 | navButton: {
41 | color: "white",
42 | },
43 | }));
44 |
45 | const NETWORK_CHAIN_ID = {
46 | mainnet: 1,
47 | goerli: 5,
48 | };
49 |
50 | function GnosisSafeApp() {
51 | const classes = useStyles();
52 |
53 | const { sdk, safe } = useSafeAppsSDK();
54 | const chainId = NETWORK_CHAIN_ID[safe.network.toLowerCase()];
55 | const web3Provider = useMemo(() => {
56 | return new ethers.providers.Web3Provider(
57 | new SafeAppProvider(safe, sdk),
58 | chainId
59 | );
60 | }, [sdk, safe, chainId]);
61 |
62 | return (
63 |
64 |
65 |
66 |
67 | Web3 Domain Manager (beta)
68 |
69 |
70 | {safe.safeAddress}
71 |
72 |
73 |
74 |
75 |
80 |
81 |
82 | );
83 | }
84 |
85 | export default function () {
86 | return (
87 |
88 |
91 | Waiting for Gnosis Safe...
92 |
93 | >
94 | }
95 | >
96 |
97 |
98 |
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/src/components/AccountModal.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import { useWeb3React } from "@web3-react/core";
4 | import Popover from "@material-ui/core/Popover";
5 | import Container from "@material-ui/core/Container";
6 | import Box from "@material-ui/core/Box";
7 | import Typography from "@material-ui/core/Typography";
8 | import Button from "@material-ui/core/Button";
9 | import { CopyToClipboard } from "react-copy-to-clipboard";
10 | import CopyIcon from "@material-ui/icons/FileCopy";
11 |
12 | import Identicon from "./Identicon";
13 |
14 | const useStyles = makeStyles(() => ({
15 | modal: {
16 | padding: 20,
17 | },
18 | box: {
19 | display: "flex",
20 | },
21 | account: {
22 | paddingLeft: 8,
23 | },
24 | btn: {
25 | marginTop: 10,
26 | width: "100%",
27 | },
28 | }));
29 |
30 | export default function AccountModal({ anchorEl, onClose }) {
31 | const classes = useStyles();
32 |
33 | const { account, deactivate, connector } = useWeb3React();
34 |
35 | const open = Boolean(anchorEl);
36 |
37 | const handleClose = () => {
38 | onClose && onClose();
39 | };
40 |
41 | return (
42 |
55 |
56 |
57 |
58 |
59 | {account &&
60 | `${account.slice(0, 6)}...${account.slice(
61 | account.length - 4,
62 | account.length
63 | )}`}
64 |
65 |
66 |
70 |
71 |
72 |
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/BuyDomain.js:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import React, { useState } from "react";
3 | import { useWeb3React } from "@web3-react/core";
4 | import { makeStyles } from "@material-ui/core/styles";
5 | import Typography from "@material-ui/core/Typography";
6 | import { Chip } from "@material-ui/core";
7 | import Accordion from "@material-ui/core/Accordion";
8 | import AccordionSummary from "@material-ui/core/AccordionSummary";
9 | import Button from "@material-ui/core/Button";
10 | import Alert from "@material-ui/lab/Alert";
11 |
12 | import { getPurchaseParams } from "../services/udRegistry";
13 |
14 | const useStyles = makeStyles((theme) => ({
15 | grow: {
16 | flexGrow: 1,
17 | paddingLeft: 30,
18 | },
19 | buy: {
20 | marginLeft: 16,
21 | fontWeight: "bold",
22 | },
23 | acc: {
24 | marginBottom: 16,
25 | },
26 | accSum: {
27 | "& > .Mui-expanded": {
28 | margin: "initial",
29 | },
30 | },
31 | }));
32 |
33 | const BuyDomain = ({ library, name, status, price }) => {
34 | const classes = useStyles();
35 | const { account } = useWeb3React();
36 |
37 | const [fetched, setFetched] = useState(true);
38 | const [error, setError] = useState(undefined);
39 |
40 | const handleBuy = async () => {
41 | try {
42 | setError(undefined);
43 | setFetched(false);
44 | const res = await getPurchaseParams(name, account);
45 | console.log("RES", res);
46 |
47 | const { params: txParams } = res.tx;
48 | const provider = new ethers.providers.Web3Provider(library.provider);
49 | const params = {
50 | to: txParams.to,
51 | data: txParams.data,
52 | value: txParams.value,
53 | };
54 |
55 | const signer = provider.getSigner();
56 | await signer.sendTransaction(params);
57 | } catch (error) {
58 | console.error(error);
59 | setError(error.message);
60 | } finally {
61 | setFetched(true);
62 | }
63 | };
64 |
65 | return (
66 | <>
67 |
68 |
69 |
70 | {name}
71 |
72 |
77 |
78 |
79 | USD {(price / 100).toFixed(2)}
80 |
81 |
90 |
91 |
92 | {error && {error}}
93 | >
94 | );
95 | };
96 |
97 | export default BuyDomain;
98 |
--------------------------------------------------------------------------------
/src/components/ConnectButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Button from '@material-ui/core/Button';
4 | import Box from '@material-ui/core/Box';
5 | import { Typography } from '@material-ui/core';
6 | import { useWeb3React } from '@web3-react/core';
7 | import { formatEther } from "@ethersproject/units";
8 |
9 | import Identicon from './Identicon';
10 | import AccountModal from './AccountModal';
11 | import ConnectModal from './ConnectModal';
12 | import { CHAIN_ID_NETWORK } from './../utils/constants';
13 |
14 | const useStyles = makeStyles((theme) => ({
15 | box_net: {
16 | display: 'flex',
17 | alignItems: 'center',
18 | background: theme.palette.grey[200],
19 | borderRadius: 6,
20 | padding: 8,
21 | marginRight: 8,
22 | },
23 | box_acc: {
24 | display: 'flex',
25 | alignItems: 'center',
26 | background: theme.palette.grey[300],
27 | borderRadius: 6,
28 | paddingLeft: 8,
29 | },
30 | info_btn: {
31 | margin: '2px 2px 2px 6px',
32 | background: theme.palette.grey[500],
33 | color: 'white',
34 | textTransform: 'none',
35 | },
36 | info_btn_text: {
37 | paddingRight: 6,
38 | },
39 | connect_btn: {
40 | background: theme.palette.grey[400],
41 | }
42 | }));
43 |
44 | export default function ConnectButton() {
45 | const classes = useStyles();
46 |
47 | const { account, library, chainId } = useWeb3React();
48 |
49 | const [balance, setBalance] = React.useState();
50 | const [modalEl, setModalEl] = React.useState(null);
51 | const [connect, setConnect] = React.useState();
52 |
53 | React.useEffect(() => {
54 | if (!!account && !!library) {
55 | let stale = false
56 |
57 | library
58 | .getBalance(account)
59 | .then((balance) => {
60 | if (!stale) {
61 | setBalance(balance)
62 | }
63 | })
64 | .catch(() => {
65 | if (!stale) {
66 | setBalance(null)
67 | }
68 | })
69 |
70 | return () => {
71 | stale = true
72 | setBalance(undefined)
73 | }
74 | }
75 | }, [account, library, chainId])
76 |
77 | return account ? (
78 | <>
79 |
80 |
81 | {CHAIN_ID_NETWORK[chainId] || chainId}
82 |
83 |
84 |
85 | {balance && parseFloat(formatEther(balance)).toFixed(3)} ETH
86 |
96 | setModalEl(null)}/>
97 |
98 | >
99 | ) : (
100 | <>
101 |
107 | setConnect(false) } />
108 | >
109 | );
110 | }
111 |
--------------------------------------------------------------------------------
/src/components/ConnectModal.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import { useWeb3React } from "@web3-react/core";
4 | import Dialog from "@material-ui/core/Dialog";
5 | import DialogContent from "@material-ui/core/DialogContent";
6 | import MuiDialogTitle from "@material-ui/core/DialogTitle";
7 | import Slide from "@material-ui/core/Slide";
8 | import IconButton from "@material-ui/core/IconButton";
9 | import CloseIcon from "@material-ui/icons/Close";
10 | import { Typography, Button } from "@material-ui/core";
11 |
12 | import mmLogo from "./../images/mm.png";
13 | import wcLogo from "./../images/wc.svg";
14 | import cwLogo from "./../images/cw.svg";
15 |
16 | import { injected, walletconnect, walletlink } from "./../connectors";
17 |
18 | const connectorsByName = {
19 | MetaMask: injected,
20 | WalletConnect: walletconnect,
21 | "Coinbase Wallet": walletlink,
22 | };
23 |
24 | const connectorsLogo = {
25 | MetaMask: mmLogo,
26 | WalletConnect: wcLogo,
27 | "Coinbase Wallet": cwLogo,
28 | };
29 |
30 | const useStyles = makeStyles((theme) => ({
31 | root: {
32 | margin: 0,
33 | padding: theme.spacing(2),
34 | },
35 | closeButton: {
36 | position: "absolute",
37 | right: theme.spacing(1),
38 | top: theme.spacing(1),
39 | color: theme.palette.grey[500],
40 | },
41 | btn: {
42 | minWidth: 300,
43 | padding: 16,
44 | justifyContent: "flex-start",
45 | textTransform: "none",
46 | },
47 | grow: {
48 | flexGrow: 1,
49 | },
50 | img: {
51 | width: "auto",
52 | maxWidth: 26,
53 | maxHeight: 24,
54 | margin: "0 auto",
55 | },
56 | }));
57 |
58 | const Transition = React.forwardRef(function Transition(props, ref) {
59 | return ;
60 | });
61 |
62 | const DialogTitle = (props) => {
63 | const classes = useStyles();
64 | const { children, onClose, ...other } = props;
65 | return (
66 |
67 | {children}
68 | {onClose ? (
69 |
74 |
75 |
76 | ) : null}
77 |
78 | );
79 | };
80 |
81 | export default function ConnectModal({ isOpen, onClose }) {
82 | const classes = useStyles();
83 |
84 | const { activate } = useWeb3React();
85 |
86 | const handleClose = () => {
87 | onClose && onClose();
88 | };
89 |
90 | return (
91 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/src/components/DeprecatedNetwork.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Alert from "@material-ui/lab/Alert";
3 |
4 | export default function DeprecatedNetwork({ chainId }) {
5 | return (
6 | <>
7 | {chainId === 4 ? (
8 |
9 | Rinkeby network support is deprecated, it will be disabled soon.
10 |
11 | ) : (
12 | ""
13 | )}
14 | >
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/DomainEventsGraph.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import {
4 | Gitgraph,
5 | templateExtend,
6 | Orientation,
7 | TemplateName
8 | } from "@gitgraph/react";
9 |
10 | const useStyles = makeStyles(() => ({
11 | header: {
12 | paddingTop: 30
13 | },
14 | }));
15 |
16 | const DomainEventsGraph = ({ events }) => {
17 | const classes = useStyles();
18 |
19 | const _template = templateExtend(TemplateName.Metro, {
20 | branch: {
21 | lineWidth: 3
22 | },
23 | commit: {
24 | spacing: 40,
25 | message: {
26 | displayHash: false,
27 | displayAuthor: false,
28 | },
29 | dot: {
30 | size: 5,
31 | }
32 | },
33 | tag: {
34 | pointerWidth: 6,
35 | },
36 | });
37 |
38 | return (
39 | <>
40 |
44 | {(gitgraph) => {
45 | const master = gitgraph.branch("Registry");
46 | if (events) {
47 | events.events.map(e => {
48 | // master.commit({
49 | // subject: e.event,
50 | // tag: e.blockNumber.toString()
51 | // });
52 | master.commit(e.event);
53 | master.tag(e.blockNumber.toString());
54 | })
55 | }
56 | }}
57 |
58 | >
59 | );
60 | };
61 |
62 | export default DomainEventsGraph;
63 |
--------------------------------------------------------------------------------
/src/components/DomainEventsTable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Table from '@material-ui/core/Table';
4 | import TableBody from '@material-ui/core/TableBody';
5 | import TableCell from '@material-ui/core/TableCell';
6 | import TableContainer from '@material-ui/core/TableContainer';
7 | import TableHead from '@material-ui/core/TableHead';
8 | import TableRow from '@material-ui/core/TableRow';
9 | import CircularProgress from '@material-ui/core/CircularProgress';
10 |
11 | import { ZERO_ADDRESS } from './../utils/constants';
12 | import EtherscanAddress from './EtherscanAddress';
13 | import EtherscanBlock from './EtherscanBlock';
14 | import EtherscanTransaction from './EtherscanTransaction';
15 |
16 | const useStyles = makeStyles(() => ({
17 | header: {
18 | paddingTop: 30
19 | },
20 | loader: {
21 | width: '100%',
22 | display: 'flex',
23 | flexDirection: 'column',
24 | alignItems: 'center',
25 | },
26 | }));
27 |
28 | const renderEventType = (event) => {
29 | switch (event.event) {
30 | case 'Transfer':
31 | return <>{
32 | event.args.from === ZERO_ADDRESS ?
33 | 'Mint' :
34 | event.args.to === ZERO_ADDRESS ?
35 | 'Burn' :
36 | 'Transfer'
37 | }>;
38 | default:
39 | return <>{event.event}>;
40 | }
41 | }
42 |
43 | const renderEvent = (event, chainId) => {
44 | const data = Object.keys(event.args)
45 | .filter(key => !(+key) && key !== '0')
46 | .reduce((obj, key) => {
47 | obj[key] = event.args[key];
48 | return obj;
49 | }, {});
50 |
51 | switch (event.event) {
52 | case 'Approval':
53 | return <>Approved operator {event.args.approved} for token {event.args.tokenId.toHexString()}>;
54 | case 'ApprovalForAll':
55 | return <>{event.args.approved ? 'Approved' : 'Disapproved'} operator {event.args.operator}>;
56 | case 'NewURI':
57 | return <>{event.args.uri}>;
58 | case 'Resolve':
59 | return <>Set resolver {
60 |
61 | }>;
62 | case 'Sync':
63 | return <>Set record with key hash {event.args.updateId.toHexString()} (Resolver: {
64 |
65 | })
>;
66 | case 'Transfer':
67 | return <>
68 | Transfer to {
69 |
70 | }
71 | {event.args.from !== ZERO_ADDRESS &&
72 | (From: {})
73 | }
74 | >;
75 | default:
76 | return <>{JSON.stringify(data, null, ' ')}>;
77 | }
78 | }
79 |
80 | const DomainEventsTable = ({ events, chainId }) => {
81 | const classes = useStyles();
82 |
83 | return (
84 | <>
85 | {events && events.isFetched &&
86 |
87 |
88 |
89 |
90 | Block
91 | Tx
92 | Event
93 | Data
94 |
95 |
96 |
97 | {events && events.events.map((event, i) =>
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | {renderEventType(event, chainId)}
106 | {renderEvent(event, chainId)}
107 |
108 | )}
109 |
110 |
111 |
112 | }
113 | {events && !events.isFetched &&
114 |
115 |
116 |
117 | }
118 | >
119 | );
120 | };
121 |
122 | export default DomainEventsTable;
123 |
--------------------------------------------------------------------------------
/src/components/DomainInfo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import Grid from "@material-ui/core/Grid";
4 | import Typography from "@material-ui/core/Typography";
5 |
6 | import EtherscanAddress from "./EtherscanAddress";
7 |
8 | const useStyles = makeStyles(() => ({
9 | header: {
10 | paddingTop: 30,
11 | },
12 | }));
13 |
14 | const DomainInfo = ({ domain, chainId }) => {
15 | const classes = useStyles();
16 |
17 | const records = Object.entries((domain || {}).records || []).filter(
18 | ([_, val]) => !!val
19 | );
20 | const recordsRaw = records.map(([key, val]) => {
21 | return (
22 |
23 |
24 |
25 | {key}
26 |
27 |
28 |
29 | {val}
30 |
31 |
32 | );
33 | });
34 |
35 | return (
36 | <>
37 |
38 |
39 | ID
40 |
41 |
42 | {domain.id}
43 |
44 |
45 |
46 |
47 | Registry
48 |
49 |
50 |
51 |
56 |
57 |
58 |
59 |
60 |
61 | Resolver
62 |
63 |
64 |
65 |
69 |
70 |
71 |
72 |
73 |
74 | Owner
75 |
76 |
77 |
78 |
82 |
83 |
84 |
85 | {records.length ? (
86 | <>
87 |
88 |
89 | Records
90 |
91 |
92 | {recordsRaw}
93 | >
94 | ) : (
95 | <>>
96 | )}
97 | >
98 | );
99 | };
100 |
101 | export default DomainInfo;
102 |
--------------------------------------------------------------------------------
/src/components/DomainList.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Backdrop from '@material-ui/core/Backdrop';
4 | import CircularProgress from '@material-ui/core/CircularProgress';
5 | import Divider from '@material-ui/core/Divider';
6 | import Typography from '@material-ui/core/Typography';
7 | import Accordion from '@material-ui/core/Accordion';
8 | import AccordionDetails from '@material-ui/core/AccordionDetails';
9 | import AccordionSummary from '@material-ui/core/AccordionSummary';
10 | import AccordionActions from '@material-ui/core/AccordionActions';
11 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
12 | import Tabs from '@material-ui/core/Tabs';
13 | import Tab from '@material-ui/core/Tab';
14 | import Box from '@material-ui/core/Box';
15 |
16 | import DomainInfo from './DomainInfo';
17 | import DomainEventsTable from './DomainEventsTable';
18 |
19 | const useStyles = makeStyles((theme) => ({
20 | backdrop: {
21 | zIndex: theme.zIndex.drawer + 1,
22 | color: '#fff',
23 | },
24 | tabs: {
25 | width: '100%',
26 | }
27 | }));
28 |
29 | const TabPanel = (props) => {
30 | const { children, display, index, ...other } = props;
31 |
32 | return (
33 |
38 | {display && (
39 |
40 | {children}
41 |
42 | )}
43 |
44 | );
45 | }
46 |
47 | const DomainList = ({ chainId, isFetching, domains, onEventsLoad, onDomainSelect, actions }) => {
48 | const classes = useStyles();
49 |
50 | const [expanded, setExpanded] = useState(false);
51 | const [domainTab, setDomainTab] = useState(undefined);
52 | const [events, setEvents] = useState({});
53 |
54 | useEffect(() => {
55 | if(domains && domains.length === 1) {
56 | selectDomain(domains[0])(undefined, true);
57 | }
58 | }, [domains]);
59 |
60 | const selectDomain = (domain) => (_, isExpanded) => {
61 | setExpanded(isExpanded ? domain.id : false);
62 | setDomainTab(domain.id);
63 | onDomainSelect && onDomainSelect(domain);
64 | };
65 |
66 | const selectDomainEvents = (domain) => async (_, tab) => {
67 | const events = await onEventsLoad(domain);
68 | setEvents(events);
69 | setDomainTab(tab);
70 | };
71 |
72 | return (
73 | <>
74 | {
75 |
76 |
77 |
78 | }
79 | {domains && domains.length ?
80 |
81 | {domains.map(domain => (
82 |
87 |
90 | :
91 | }>
92 | {domain.name || `[${domain.id}]`}
93 |
94 |
95 | {domainTab && domainTab.startsWith(domain.id) &&
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | }
109 |
110 |
111 | {actions &&
112 |
113 | {actions}
114 |
115 | }
116 |
117 | ))}
118 |
:
119 | <>>
120 | }
121 | >
122 | )
123 | }
124 |
125 | export default DomainList;
126 |
--------------------------------------------------------------------------------
/src/components/Domains.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { ethers } from "ethers";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import Container from "@material-ui/core/Container";
5 | import Button from "@material-ui/core/Button";
6 | import Dialog from "@material-ui/core/Dialog";
7 | import DialogActions from "@material-ui/core/DialogActions";
8 | import DialogContent from "@material-ui/core/DialogContent";
9 | import DialogTitle from "@material-ui/core/DialogTitle";
10 | import Slide from "@material-ui/core/Slide";
11 | import TextField from "@material-ui/core/TextField";
12 | import Grid from "@material-ui/core/Grid";
13 | import Backdrop from "@material-ui/core/Backdrop";
14 | import CircularProgress from "@material-ui/core/CircularProgress";
15 | import Alert from "@material-ui/lab/Alert";
16 | import Typography from "@material-ui/core/Typography";
17 | import RefreshIcon from "@material-ui/icons/Refresh";
18 |
19 | import NetworkConfig from "uns/uns-config.json";
20 | import cnsRegistryJson from "uns/artifacts/CNSRegistry.json";
21 | import unsRegistryJson from "uns/artifacts/UNSRegistry.json";
22 | import resolverJson from "uns/artifacts/Resolver.json";
23 | import mintingManagerJson from "uns/artifacts/MintingManager.json";
24 |
25 | import DomainList from "./DomainList";
26 | import { createContract } from "../utils/contract";
27 | import { isAddress } from "../utils/address";
28 | import RecordsForm from "./RecordsForm";
29 | import MintDomainForm from "./MintDomainForm";
30 | import { ZERO_ADDRESS } from "./../utils/constants";
31 | import { getAccount, getDomain } from "../sources/thegraph";
32 |
33 | const Transition = React.forwardRef(function Transition(props, ref) {
34 | return ;
35 | });
36 |
37 | const useStyles = makeStyles((theme) => ({
38 | header: {
39 | display: "flex",
40 | padding: "10px 0",
41 | },
42 | form: {
43 | minWidth: 600,
44 | display: "flex",
45 | [theme.breakpoints.down("sm")]: {
46 | minWidth: "initial",
47 | },
48 | },
49 | grow: {
50 | flexGrow: 1,
51 | },
52 | backdrop: {
53 | zIndex: theme.zIndex.drawer + 1,
54 | color: "#fff",
55 | },
56 | tabs: {
57 | width: "100%",
58 | },
59 | btn: {
60 | margin: "0 10px",
61 | },
62 | noDomains: {
63 | textAlign: "center",
64 | },
65 | }));
66 |
67 | // NOTE: It is not possible to use `useWeb3React` context here, because Gnosis Safe provider
68 | const Domains = ({ library, account, chainId }) => {
69 | const classes = useStyles();
70 | const stateKey = `${account}_${chainId}`;
71 |
72 | const [data, setData] = useState({
73 | [stateKey]: {
74 | isFetched: false,
75 | domains: [],
76 | },
77 | });
78 | const [fetched, setFetched] = useState(true);
79 | const [domainTab, setDomainTab] = React.useState(undefined);
80 | const [domain, setDomain] = useState(undefined);
81 |
82 | const [defaultResolverError, setDefaultResolverError] =
83 | React.useState(undefined);
84 | const [defaultResolving, setDefaultResolving] = React.useState(false);
85 |
86 | const [receiver, setReceiver] = React.useState();
87 | const [transferError, setTransferError] = React.useState(undefined);
88 | const [transferring, setTransferring] = React.useState(false);
89 |
90 | const [records, setRecords] = useState(undefined);
91 | const [updateError, setUpdateError] = React.useState(undefined);
92 | const [updating, setUpdating] = React.useState(false);
93 |
94 | const [allowMinting, setAllowMinting] = useState(false);
95 | const [domainToMint, setDomainToMint] = useState(false);
96 | const [mintError, setMintError] = React.useState(undefined);
97 | const [minting, setMinting] = React.useState(false);
98 |
99 | const { contracts } = NetworkConfig.networks[chainId];
100 | const cnsRegistry = createContract(
101 | library,
102 | chainId,
103 | cnsRegistryJson.abi,
104 | contracts.CNSRegistry
105 | );
106 | const unsRegistry = createContract(
107 | library,
108 | chainId,
109 | unsRegistryJson.abi,
110 | contracts.UNSRegistry
111 | );
112 | const mintingManager = createContract(
113 | library,
114 | chainId,
115 | mintingManagerJson.abi,
116 | contracts.MintingManager
117 | );
118 |
119 | const handleTransferOpen = (_domain) => () => {
120 | setDomain(_domain);
121 | };
122 |
123 | const handleRecordsOpen = (_domain) => () => {
124 | setRecords(_domain);
125 | setUpdateError();
126 | };
127 |
128 | const handleTransferClose = () => {
129 | if (transferring) {
130 | return;
131 | }
132 |
133 | setDomain();
134 | setReceiver();
135 | setTransferError();
136 | };
137 |
138 | const handleTransfer = async (_domain, receiver) => {
139 | console.debug(account, receiver, _domain.id);
140 |
141 | setTransferError();
142 | if (!isAddress(receiver)) {
143 | setTransferError("Recipient address is invalid");
144 | return;
145 | }
146 |
147 | try {
148 | setTransferring(true);
149 |
150 | const registry =
151 | unsRegistry.address.toLowerCase() === _domain.registry
152 | ? unsRegistry
153 | : cnsRegistry;
154 | await registry["safeTransferFrom(address,address,uint256)"](
155 | account,
156 | receiver,
157 | _domain.id
158 | );
159 |
160 | setDomain();
161 | await updateDomainState(_domain);
162 | } catch (error) {
163 | console.error(error);
164 | setTransferError(error && error.message);
165 | return;
166 | } finally {
167 | setTransferring(false);
168 | }
169 | };
170 |
171 | const setDefaultResolver = (_domain) => async () => {
172 | console.debug("DEFAULT RESOLVER", _domain.id);
173 | setDefaultResolverError();
174 |
175 | try {
176 | setDefaultResolving(true);
177 | await cnsRegistry.resolveTo(contracts.Resolver.address, _domain.id);
178 |
179 | await updateDomainState(_domain);
180 | } catch (error) {
181 | console.error(error);
182 | setDefaultResolverError(error && error.message);
183 | return;
184 | } finally {
185 | setDefaultResolving(false);
186 | }
187 | };
188 |
189 | const handleUpdate = async (_domain, records) => {
190 | console.debug("UPDATE", _domain, records);
191 | setUpdateError();
192 |
193 | try {
194 | setUpdating(true);
195 | const provider = new ethers.providers.Web3Provider(library.provider);
196 | const resolver = new ethers.Contract(
197 | _domain.resolver,
198 | resolverJson.abi,
199 | provider.getSigner()
200 | );
201 | const keysToUpdate = records.map((r) => r.key);
202 | const valuesToUpdate = records.map((r) => r.newValue || "");
203 | await resolver.setMany(keysToUpdate, valuesToUpdate, _domain.id);
204 |
205 | setRecords();
206 | await updateDomainState(_domain);
207 | } catch (error) {
208 | console.error(error);
209 | setUpdateError(error && error.message);
210 | return;
211 | } finally {
212 | setUpdating(false);
213 | }
214 | };
215 |
216 | const handleMint = async (tld, domainName) => {
217 | console.debug("MINT", tld, domainName);
218 | setMintError();
219 |
220 | try {
221 | setMinting(true);
222 | await mintingManager.claim(tld, domainName);
223 | setDomainToMint(false);
224 | await loadTokens();
225 | } catch (error) {
226 | console.error(error);
227 | setMintError(error && error.message);
228 | return;
229 | } finally {
230 | setMinting(false);
231 | }
232 | };
233 |
234 | const handleRefresh = (_domain) => async () => {
235 | await updateDomainState(_domain);
236 | };
237 |
238 | const initMinting = async () => {
239 | console.debug("Initiating minting...");
240 | const paused = await mintingManager.paused();
241 | setAllowMinting(!paused);
242 | };
243 |
244 | const loadTokens = async () => {
245 | setFetched(false);
246 |
247 | const _account = await getAccount(chainId, account);
248 | const _data = {
249 | ...data,
250 | [stateKey]: {
251 | isFetched: true,
252 | domains: (_account.domains || []).sort((a, b) => {
253 | return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
254 | }),
255 | },
256 | };
257 | console.debug("Update state", _data);
258 | setData(_data);
259 | setFetched(true);
260 | };
261 |
262 | const updateDomainState = async (domain) => {
263 | const _domain = await getDomain(chainId, domain.id, true);
264 | const domains = data[stateKey].domains
265 | .map((d) => (_domain && d.id === _domain.id ? { ...d, ..._domain } : d))
266 | .filter((d) => _domain || d.id !== domain.id);
267 |
268 | const _data = {
269 | ...data,
270 | [stateKey]: {
271 | isFetched: true,
272 | domains,
273 | },
274 | };
275 |
276 | console.debug("Update domain state", _data);
277 | setData(_data);
278 | setDomainTab(_domain);
279 | };
280 |
281 | const loadDomainEvents = (domain) => {
282 | console.debug("Loading DOMAIN events...");
283 |
284 | const registry =
285 | unsRegistry.address.toLowerCase() === domain.registry
286 | ? unsRegistry
287 | : cnsRegistry;
288 | return registry.source.fetchEvents(domain).then((domainEvents) => {
289 | console.debug("Loaded DOMAIN events", domainEvents);
290 |
291 | return {
292 | isFetched: true,
293 | events: domainEvents || [],
294 | };
295 | });
296 | };
297 |
298 | useEffect(() => {
299 | if (!data[stateKey] || !data[stateKey].isFetched) {
300 | initMinting();
301 | loadTokens();
302 | }
303 | }, [data, stateKey]);
304 |
305 | const _domains = data && (data[stateKey] || {}).domains;
306 | return (
307 |
308 | {_domains && _domains.length ? (
309 |
310 |
311 | Domains ({_domains.length})
312 |
313 |
323 |
324 | ) : (
325 | <>>
326 | )}
327 | {
333 | setDomainTab(domain);
334 | setDefaultResolverError();
335 | }}
336 | actions={
337 | <>
338 | {domainTab && domainTab.resolver === ZERO_ADDRESS ? (
339 |
346 | ) : (
347 |
354 | )}
355 |
362 |
363 | {defaultResolverError && (
364 |
{defaultResolverError}
365 | )}
366 |
367 |
374 | >
375 | }
376 | />
377 |
428 |
453 |
473 | {fetched &&
474 | data[stateKey] &&
475 | data[stateKey].domains &&
476 | !data[stateKey].domains.length && (
477 |
478 | No domains found.
479 |
490 | OR Buy here
491 |
492 | )}
493 | {
494 |
495 |
496 |
497 | }
498 |
499 | );
500 | };
501 |
502 | export default Domains;
503 |
--------------------------------------------------------------------------------
/src/components/EtherscanAddress.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from '@material-ui/core/Link';
3 |
4 | import { ZERO_ADDRESS, ETHERSCAN_MAP } from './../utils/constants';
5 |
6 | const EtherscanAddress = ({ address, chainId, label }) => {
7 | return (
8 | <>
9 | {
10 | address === ZERO_ADDRESS ?
11 | address :
12 |
16 | {label || address}
17 |
18 | }
19 | >
20 | );
21 | }
22 |
23 | export default EtherscanAddress;
--------------------------------------------------------------------------------
/src/components/EtherscanBlock.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from '@material-ui/core/Link';
3 |
4 | import { ETHERSCAN_MAP } from './../utils/constants';
5 |
6 | const EtherscanBlock = ({ blockNumber, chainId }) => {
7 | return (
8 |
12 | {blockNumber}
13 |
14 | );
15 | }
16 |
17 | export default EtherscanBlock;
--------------------------------------------------------------------------------
/src/components/EtherscanTransaction.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from '@material-ui/core/Link';
3 |
4 | import { ETHERSCAN_MAP } from './../utils/constants';
5 |
6 | const EtherscanTransaction = ({ transactionHash, chainId }) => {
7 | return (
8 |
12 | {transactionHash.substr(0, 8)}...
13 |
14 | );
15 | }
16 |
17 | export default EtherscanTransaction;
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import AppBar from "@material-ui/core/AppBar";
4 | import Toolbar from "@material-ui/core/Toolbar";
5 | import Typography from "@material-ui/core/Typography";
6 | import GitHubButton from "react-github-btn";
7 |
8 | const useStyles = makeStyles(() => ({
9 | footer: {
10 | alignItems: "center",
11 | background: "transparent",
12 | boxShadow: "none",
13 | },
14 | info: {
15 | marginTop: 20,
16 | },
17 | }));
18 |
19 | export default function Footer() {
20 | const classes = useStyles();
21 |
22 | return (
23 |
24 |
29 | This is an open-source project for managing blockchain domains. It uses
30 | Ethereum/Polygon blockchain and TheGraph as data sources.
31 |
32 |
33 |
34 |
40 | Star
41 |
42 |
43 |
44 |
50 | Issue
51 |
52 |
53 |
54 |
59 | Follow @aquiladev
60 |
61 |
62 | {process.env.REACT_APP_GITHUB_REF_SHA && (
63 |
64 | version: {process.env.REACT_APP_GITHUB_REF_SHA}
65 |
66 | )}
67 |
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/GlobalStyle.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 | import avertaFont from '@gnosis.pm/safe-react-components/dist/fonts/averta-normal.woff2';
3 | import avertaBoldFont from '@gnosis.pm/safe-react-components/dist/fonts/averta-bold.woff2';
4 |
5 | const GlobalStyle = createGlobalStyle`
6 | html {
7 | height: 100%
8 | }
9 | body {
10 | height: 100%;
11 | margin: 0px;
12 | padding: 0px;
13 | }
14 | @font-face {
15 | font-family: 'Averta';
16 | src: local('Averta'), local('Averta Bold'),
17 | url(${avertaFont}) format('woff2'),
18 | url(${avertaBoldFont}) format('woff');
19 | }
20 | #root {
21 | height: 100%;
22 | }
23 | #records-form .MuiFilledInput-input {
24 | padding-top: 13px;
25 | }
26 | .MuiFormControl-root,
27 | .MuiInputBase-root {
28 | width: 100% !important;
29 | }
30 | .MuiAccordionSummary-content {
31 | overflow: auto;
32 | }
33 | `;
34 |
35 | export default GlobalStyle;
36 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Link, useHistory } from "react-router-dom";
3 | import { alpha, makeStyles } from "@material-ui/core/styles";
4 | import AppBar from "@material-ui/core/AppBar";
5 | import Toolbar from "@material-ui/core/Toolbar";
6 | import Typography from "@material-ui/core/Typography";
7 | import SearchIcon from "@material-ui/icons/Search";
8 | import Button from "@material-ui/core/Button";
9 | import Menu from "@material-ui/core/Menu";
10 | import MenuItem from "@material-ui/core/MenuItem";
11 | import MoreVertIcon from "@material-ui/icons/MoreVert";
12 | import BarChartIcon from "@material-ui/icons/BarChart";
13 | import CodeIcon from "@material-ui/icons/Code";
14 | import InputBase from "@material-ui/core/InputBase";
15 |
16 | import ConnectButton from "./ConnectButton";
17 |
18 | const useStyles = makeStyles((theme) => ({
19 | title: {
20 | fontSize: "1rem",
21 | color: theme.palette.grey[800],
22 | textDecoration: "none",
23 | [theme.breakpoints.up("sm")]: {
24 | fontSize: "1.5rem",
25 | },
26 | },
27 | grow: {
28 | flexGrow: 1,
29 | paddingLeft: 30,
30 | },
31 | navBtn: {
32 | color: theme.palette.grey[800],
33 | marginLeft: 6,
34 | padding: "6px 0",
35 | minWidth: "inherit",
36 | textDecoration: "none",
37 | },
38 | menuLink: {
39 | color: "inherit",
40 | textDecoration: "none",
41 | },
42 | search: {
43 | position: "relative",
44 | borderRadius: theme.shape.borderRadius,
45 | backgroundColor: alpha(theme.palette.common.black, 0.15),
46 | "&:hover": {
47 | backgroundColor: alpha(theme.palette.common.black, 0.25),
48 | },
49 | marginRight: theme.spacing(2),
50 | marginLeft: 0,
51 | width: "100%",
52 | [theme.breakpoints.up("sm")]: {
53 | marginLeft: theme.spacing(3),
54 | width: "auto",
55 | },
56 | },
57 | searchIcon: {
58 | padding: theme.spacing(0, 2),
59 | height: "100%",
60 | position: "absolute",
61 | pointerEvents: "none",
62 | display: "flex",
63 | alignItems: "center",
64 | justifyContent: "center",
65 | },
66 | inputRoot: {
67 | color: "inherit",
68 | },
69 | inputInput: {
70 | padding: theme.spacing(1, 1, 1, 0),
71 | // vertical padding + font size from searchIcon
72 | paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
73 | transition: theme.transitions.create("width"),
74 | width: "100%",
75 | [theme.breakpoints.up("md")]: {
76 | width: "20ch",
77 | },
78 | },
79 | }));
80 |
81 | export default function Header({ active }) {
82 | const classes = useStyles();
83 | const history = useHistory();
84 |
85 | const [anchorEl, setAnchorEl] = useState(null);
86 | const [domainName, setDomainName] = useState();
87 | const open = Boolean(anchorEl);
88 |
89 | const handleOpen = (event) => {
90 | setAnchorEl(event.currentTarget);
91 | };
92 |
93 | const handleClose = () => {
94 | setAnchorEl(null);
95 | };
96 |
97 | const handleChange = (e) => {
98 | setDomainName(e.target.value);
99 | };
100 |
101 | const keyPress = (e) => {
102 | if (e.charCode === 13) {
103 | setDomainName("");
104 | history.push(`/search/${domainName}`);
105 | }
106 | };
107 |
108 | return (
109 |
189 | );
190 | }
191 |
--------------------------------------------------------------------------------
/src/components/Identicon.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import Jazzicon from "@metamask/jazzicon";
4 |
5 | const useStyles = makeStyles(() => ({
6 | icon: {
7 | height: "1rem",
8 | width: "1rem",
9 | borderRadius: "1.125rem",
10 | borderColor: "black",
11 | },
12 | }));
13 |
14 | export default function Identicon({ account, size = 16 }) {
15 | const classes = useStyles();
16 | const ref = useRef();
17 |
18 | useEffect(() => {
19 | if (account && ref.current) {
20 | ref.current.innerHTML = "";
21 | ref.current.appendChild(
22 | Jazzicon(size, parseInt(account.slice(2, 10), 16))
23 | );
24 | }
25 | }, [account, size]);
26 |
27 | return ;
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Lookup.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useParams } from "react-router-dom";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import { ethers } from "ethers";
5 | import Container from "@material-ui/core/Container";
6 | import Paper from "@material-ui/core/Paper";
7 | import InputBase from "@material-ui/core/InputBase";
8 | import IconButton from "@material-ui/core/IconButton";
9 | import SearchIcon from "@material-ui/icons/Search";
10 | import Alert from "@material-ui/lab/Alert";
11 | import CircularProgress from "@material-ui/core/CircularProgress";
12 |
13 | import NetworkConfig from "uns/uns-config.json";
14 | import supportedKeys from "uns/resolver-keys.json";
15 |
16 | import cnsRegistryJson from "uns/artifacts/CNSRegistry.json";
17 | import unsRegistryJson from "uns/artifacts/UNSRegistry.json";
18 | import proxyReaderJson from "uns/artifacts/ProxyReader.json";
19 |
20 | import DomainList from "./DomainList";
21 | import { createContract } from "../utils/contract";
22 |
23 | const useStyles = makeStyles((theme) => ({
24 | root: {
25 | marginTop: 40,
26 | padding: "2px 4px",
27 | display: "flex",
28 | alignItems: "center",
29 | },
30 | input: {
31 | marginLeft: theme.spacing(1),
32 | flex: 1,
33 | },
34 | iconButton: {
35 | padding: 10,
36 | },
37 | loader: {
38 | width: "100%",
39 | display: "flex",
40 | flexDirection: "column",
41 | alignItems: "center",
42 | },
43 | }));
44 |
45 | const Lookup = ({ library, chainId }) => {
46 | const classes = useStyles();
47 | const { domain: domainParam } = useParams();
48 |
49 | const [domainName, setDomainName] = useState(domainParam);
50 | const [domain, setDomain] = useState(undefined);
51 | const [fetched, setFetched] = useState(true);
52 | const [error, setError] = useState(undefined);
53 |
54 | const { contracts } = NetworkConfig.networks[chainId];
55 | const cnsRegistry = createContract(
56 | library,
57 | chainId,
58 | cnsRegistryJson.abi,
59 | contracts.CNSRegistry
60 | );
61 | const unsRegistry = createContract(
62 | library,
63 | chainId,
64 | unsRegistryJson.abi,
65 | contracts.UNSRegistry
66 | );
67 | const proxyReader = createContract(
68 | library,
69 | chainId,
70 | proxyReaderJson.abi,
71 | contracts.ProxyReader
72 | );
73 |
74 | const _keys = Object.keys(supportedKeys.keys);
75 |
76 | useEffect(() => {
77 | if (domainName) {
78 | search();
79 | }
80 | }, [domainName]);
81 |
82 | const search = async () => {
83 | try {
84 | setError(undefined);
85 | if (domain && domainName === domain.name) {
86 | return;
87 | }
88 |
89 | if (ethers.utils.isHexString(domainName) && domainName.length === 66) {
90 | setDomainName(await fetchName(domainName));
91 | return;
92 | }
93 |
94 | setDomain(undefined);
95 | const tokenId = ethers.utils.namehash(domainName);
96 | console.debug(domainName, tokenId);
97 | await loadData(tokenId, domainName);
98 | } catch (error) {
99 | setError(error.message);
100 | }
101 | };
102 |
103 | const fetchName = async (token) => {
104 | const registry = (await unsRegistry.exists(token))
105 | ? unsRegistry
106 | : cnsRegistry;
107 | const events = await registry.source.fetchNewURIEvents([token]);
108 | if (!events.length) {
109 | throw new Error("Token not found");
110 | }
111 | return events.find((e) => e.args.tokenId.toHexString() === token).args.uri;
112 | };
113 |
114 | const loadData = async (tokenId, name) => {
115 | setFetched(false);
116 |
117 | console.debug("Fetching state...");
118 | const data = await proxyReader.callStatic.getData(_keys, tokenId);
119 | console.debug("Fetched state", data);
120 |
121 | const records = {};
122 | _keys.forEach((k, i) => (records[k] = data[2][i]));
123 |
124 | const _domain = {
125 | id: tokenId,
126 | name,
127 | registry:
128 | data.resolver === unsRegistry.address.toLowerCase()
129 | ? unsRegistry.address
130 | : cnsRegistry.address,
131 | type: data.resolver === unsRegistry.address ? "uns" : "cns",
132 | owner: data.owner,
133 | resolver: data.resolver,
134 | records,
135 | };
136 |
137 | console.debug("Update state", _domain);
138 | setFetched(true);
139 | setDomain(_domain);
140 | };
141 |
142 | const loadDomainEvents = (domain) => {
143 | console.debug("Loading DOMAIN events...");
144 |
145 | const registry =
146 | unsRegistry.address.toLowerCase() === domain.registry
147 | ? unsRegistry
148 | : cnsRegistry;
149 | return registry.source.fetchEvents(domain).then((domainEvents) => {
150 | console.debug("Loaded DOMAIN events", domainEvents);
151 |
152 | return {
153 | isFetched: true,
154 | events: domainEvents || [],
155 | };
156 | });
157 | };
158 |
159 | const handleChange = (e) => {
160 | setDomainName(e.target.value);
161 | };
162 |
163 | const keyPress = (e) => {
164 | if (e.charCode === 13) {
165 | search();
166 | }
167 | };
168 |
169 | return (
170 |
171 |
172 |
185 |
190 |
191 |
192 |
193 | {error && {error}}
194 | {!fetched && (
195 |
196 |
197 |
198 | )}
199 | {fetched && domain && (
200 |
201 |
207 |
208 | )}
209 |
210 | );
211 | };
212 |
213 | export default Lookup;
214 |
--------------------------------------------------------------------------------
/src/components/MintDomainForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Grid from '@material-ui/core/Grid';
4 | import Button from '@material-ui/core/Button';
5 | import TextField from '@material-ui/core/TextField';
6 | import MenuItem from '@material-ui/core/MenuItem';
7 | import Backdrop from '@material-ui/core/Backdrop';
8 | import CircularProgress from '@material-ui/core/CircularProgress';
9 | import Alert from '@material-ui/lab/Alert';
10 |
11 | const useStyles = makeStyles((theme) => ({
12 | form: {
13 | minWidth: 600,
14 | [theme.breakpoints.down('sm')]: {
15 | minWidth: 'initial',
16 | }
17 | },
18 | domainName: {
19 | display: 'flex',
20 | },
21 | grow: {
22 | marginLeft: 8,
23 | flexGrow: 1,
24 | },
25 | backdrop: {
26 | zIndex: theme.zIndex.drawer + 1,
27 | color: '#fff',
28 | },
29 | prefix: {
30 | },
31 | actions: {
32 | display: 'flex',
33 | paddingTop: 20,
34 | paddingBottom: 8,
35 | },
36 | }));
37 |
38 | const MintDomainForm = ({ minting, error, onMint, onCancel }) => {
39 | const classes = useStyles();
40 |
41 | const [domainName, setDomainName] = useState('');
42 | const [tld, setTLD] = useState('0x0f4a10a4f46c288cea365fcf45cccf0e9d901b945b9829ccdb54c10dc3cb7a6f');
43 |
44 | const mint = () => {
45 | onMint && onMint(tld, domainName);
46 | }
47 |
48 | return (
49 | <>
50 |
51 |
52 |
53 |
61 |
62 |
63 | { setDomainName(event.target.value); }} />
68 |
69 |
70 | { setTLD(event.target.value) }}
76 | className={classes.grow} select>
77 |
78 | {/* */}
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | {error &&
91 |
92 | {error}
93 |
94 | }
95 |
96 |
97 |
100 |
107 |
108 |
109 |
110 | {
111 |
112 |
113 |
114 | }
115 | >
116 | );
117 | }
118 |
119 | export default MintDomainForm;
120 |
--------------------------------------------------------------------------------
/src/components/RecordsForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import Grid from "@material-ui/core/Grid";
4 | import IconButton from "@material-ui/core/IconButton";
5 | import DeleteIcon from "@material-ui/icons/Delete";
6 | import AddIcon from "@material-ui/icons/Add";
7 | import Autocomplete from "@material-ui/lab/Autocomplete";
8 | import TextField from "@material-ui/core/TextField";
9 | import Button from "@material-ui/core/Button";
10 | import Backdrop from "@material-ui/core/Backdrop";
11 | import CircularProgress from "@material-ui/core/CircularProgress";
12 | import Alert from "@material-ui/lab/Alert";
13 |
14 | import supportedKeys from "uns/resolver-keys.json";
15 |
16 | const useStyles = makeStyles((theme) => ({
17 | form: {
18 | minWidth: 600,
19 | [theme.breakpoints.down("sm")]: {
20 | minWidth: "initial",
21 | },
22 | },
23 | record: {
24 | paddingTop: 8,
25 | paddingBottom: 8,
26 | },
27 | recordKey: {
28 | fontWeight: "bold",
29 | },
30 | recordKeySelectContainer: {
31 | marginRight: 10,
32 | },
33 | recordKeySelect: {
34 | width: "100%",
35 | },
36 | recordValue: {
37 | display: "flex",
38 | },
39 | grow: {
40 | flexGrow: 1,
41 | },
42 | backdrop: {
43 | zIndex: theme.zIndex.drawer + 1,
44 | color: "#fff",
45 | },
46 | actions: {
47 | display: "flex",
48 | paddingTop: 20,
49 | paddingBottom: 8,
50 | },
51 | }));
52 |
53 | const RecordsForm = ({ records, updating, error, onUpdate, onCancel }) => {
54 | const classes = useStyles();
55 |
56 | const [recordKey, setRecordKey] = useState("");
57 | const [recordValue, setRecordValue] = useState("");
58 | const [form, setForm] = useState({
59 | records: [],
60 | displayable: [],
61 | fillableKeys: [],
62 | });
63 |
64 | useEffect(() => {
65 | const filledRecords = Object.entries(records)
66 | .filter(([_, val]) => !!val)
67 | .map(([key, value]) => {
68 | return { key, value, newValue: value };
69 | });
70 | const filledKeys = filledRecords.map((x) => x.key);
71 | const fillableKeys = Object.keys(supportedKeys.keys).filter(
72 | (x) => !filledKeys.includes(x)
73 | );
74 |
75 | setForm({
76 | records: filledRecords,
77 | displayable: filledRecords,
78 | fillableKeys: fillableKeys,
79 | });
80 | }, [records]);
81 |
82 | const updateRecord = (_record) => (event) => {
83 | const value = event.target.value;
84 | const _records = form.records.map((r) => {
85 | if (r.key === _record) {
86 | r.newValue = value;
87 | r.muted = r.value !== value;
88 | }
89 | return r;
90 | });
91 |
92 | const filledKeys = _records.map((x) => x.key);
93 | const fillableKeys = Object.keys(supportedKeys.keys).filter(
94 | (x) => !filledKeys.includes(x)
95 | );
96 |
97 | setForm({
98 | records: _records,
99 | displayable: _records.filter((r) => !!r.newValue),
100 | fillableKeys: fillableKeys,
101 | muted: _records.some((r) => r.muted),
102 | });
103 | };
104 |
105 | const deleteRecord = (_record) => () => {
106 | const _records = form.records.map((r) => {
107 | if (r.key === _record) {
108 | r.newValue = undefined;
109 | r.muted = r.value !== undefined;
110 | }
111 | return r;
112 | });
113 | const filledKeys = _records.map((x) => x.key);
114 | const fillableKeys = Object.keys(supportedKeys.keys).filter(
115 | (x) => !filledKeys.includes(x)
116 | );
117 |
118 | setForm({
119 | records: _records,
120 | displayable: _records.filter((r) => !!r.newValue),
121 | fillableKeys: fillableKeys,
122 | muted: _records.some((r) => r.muted),
123 | });
124 | };
125 |
126 | const addRecord = () => {
127 | if (!recordKey || !recordValue) {
128 | return;
129 | }
130 |
131 | const _records = [
132 | ...form.records,
133 | {
134 | key: recordKey,
135 | value: undefined,
136 | newValue: recordValue,
137 | muted: recordValue !== undefined,
138 | },
139 | ];
140 | const filledKeys = _records.map((x) => x.key);
141 | const fillableKeys = Object.keys(supportedKeys.keys).filter(
142 | (x) => !filledKeys.includes(x)
143 | );
144 |
145 | setForm({
146 | records: _records,
147 | displayable: _records.filter((r) => !!r.newValue),
148 | fillableKeys: fillableKeys,
149 | muted: _records.some((r) => r.muted),
150 | });
151 | setRecordValue("");
152 | setRecordKey("");
153 | };
154 |
155 | const update = () => {
156 | onUpdate && onUpdate(form.records.filter((r) => r.muted));
157 | };
158 |
159 | return (
160 | <>
161 |
162 | {form.displayable.map(({ key, newValue }) => {
163 | return (
164 |
171 |
172 | {key}
173 |
174 |
175 |
182 |
183 |
184 |
185 |
186 |
187 | );
188 | })}
189 |
190 |
191 |
192 |
{
197 | setRecordKey(value);
198 | }}
199 | renderInput={(params) => (
200 |
201 | )}
202 | />
203 |
204 |
205 |
206 | {
213 | setRecordValue(event.target.value);
214 | }}
215 | />
216 |
221 |
222 |
223 |
224 |
225 |
226 | {error && (
227 |
228 | {error}
229 |
230 | )}
231 |
232 |
233 |
241 |
249 |
250 |
251 |
252 | {
253 |
254 |
255 |
256 | }
257 | >
258 | );
259 | };
260 |
261 | export default RecordsForm;
262 |
--------------------------------------------------------------------------------
/src/components/Search.js:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import React, { useEffect, useState } from "react";
3 | import { useHistory, useParams } from "react-router-dom";
4 | import { makeStyles } from "@material-ui/core/styles";
5 | import Container from "@material-ui/core/Container";
6 | import Paper from "@material-ui/core/Paper";
7 | import InputBase from "@material-ui/core/InputBase";
8 | import IconButton from "@material-ui/core/IconButton";
9 | import SearchIcon from "@material-ui/icons/Search";
10 | import Alert from "@material-ui/lab/Alert";
11 | import CircularProgress from "@material-ui/core/CircularProgress";
12 |
13 | import NetworkConfig from "uns/uns-config.json";
14 |
15 | import cnsRegistryJson from "uns/artifacts/CNSRegistry.json";
16 | import unsRegistryJson from "uns/artifacts/UNSRegistry.json";
17 |
18 | import DomainList from "./DomainList";
19 | import BuyDomain from "./BuyDomain";
20 | import { createContract } from "../utils/contract";
21 | import { getDomain } from "../sources/thegraph";
22 | import { getAvailability } from "../services/udRegistry";
23 |
24 | const useStyles = makeStyles((theme) => ({
25 | root: {
26 | marginTop: 40,
27 | marginBottom: 16,
28 | padding: "2px 4px",
29 | display: "flex",
30 | alignItems: "center",
31 | },
32 | input: {
33 | marginLeft: theme.spacing(1),
34 | flex: 1,
35 | },
36 | iconButton: {
37 | padding: 10,
38 | },
39 | loader: {
40 | width: "100%",
41 | display: "flex",
42 | flexDirection: "column",
43 | alignItems: "center",
44 | },
45 | }));
46 |
47 | const Search = ({ library, chainId }) => {
48 | const classes = useStyles();
49 | const history = useHistory();
50 | const { domain: domainParam } = useParams();
51 |
52 | const [domainName, setDomainName] = useState();
53 | const [domain, setDomain] = useState(undefined);
54 | const [availability, setAvailability] = useState(undefined);
55 | const [fetched, setFetched] = useState(true);
56 | const [error, setError] = useState(undefined);
57 |
58 | const { contracts } = NetworkConfig.networks[chainId];
59 | const cnsRegistry = createContract(
60 | library,
61 | chainId,
62 | cnsRegistryJson.abi,
63 | contracts.CNSRegistry
64 | );
65 | const unsRegistry = createContract(
66 | library,
67 | chainId,
68 | unsRegistryJson.abi,
69 | contracts.UNSRegistry
70 | );
71 |
72 | useEffect(() => {
73 | if (domainParam) {
74 | setDomainName(domainParam);
75 | search(domainParam);
76 | }
77 | }, [domainParam]);
78 |
79 | const search = async (name) => {
80 | try {
81 | setError(undefined);
82 | if (domain && name === domain.name) {
83 | return;
84 | }
85 | setDomain(undefined);
86 | setAvailability(undefined);
87 | setFetched(false);
88 | const tokenId = ethers.utils.namehash(name);
89 | console.debug(name, tokenId);
90 |
91 | // It is possyble to buy domain on Polygon directly from the dapp
92 | if (chainId === 137) {
93 | const [_domain, _availabilityResp] = await Promise.all([
94 | getDomain(chainId, tokenId, true),
95 | getAvailability(name),
96 | ]);
97 |
98 | const _availabilityStatus = _availabilityResp?.availability?.status;
99 | if (_availabilityStatus === "AVAILABLE") {
100 | setAvailability(_availabilityResp);
101 | } else if (_domain?.id) {
102 | setDomain(_domain);
103 | }
104 | } else {
105 | const _domain = await getDomain(chainId, tokenId, true);
106 | setDomain(_domain);
107 | }
108 | } catch (error) {
109 | setError(error.message);
110 | } finally {
111 | setFetched(true);
112 | }
113 | };
114 |
115 | const loadDomainEvents = (domain) => {
116 | console.debug("Loading DOMAIN events...");
117 |
118 | const registry =
119 | unsRegistry.address.toLowerCase() === domain.registry
120 | ? unsRegistry
121 | : cnsRegistry;
122 | return registry.source.fetchEvents(domain).then((domainEvents) => {
123 | console.debug("Loaded DOMAIN events", domainEvents);
124 |
125 | return {
126 | isFetched: true,
127 | events: domainEvents || [],
128 | };
129 | });
130 | };
131 |
132 | const handleChange = (e) => {
133 | setDomainName(String(e.target.value).trim());
134 | };
135 |
136 | const keyPress = (e) => {
137 | if (e.charCode === 13) {
138 | history.push(`/search/${domainName}`);
139 | }
140 | };
141 |
142 | return (
143 |
144 |
145 |
157 |
162 |
163 |
164 |
165 | {error && {error}}
166 | {!fetched && (
167 |
168 |
169 |
170 | )}
171 | {fetched && availability && (
172 |
173 |
179 |
180 | )}
181 | {fetched && domain && (
182 |
183 |
189 |
190 | )}
191 |
192 | );
193 | };
194 |
195 | export default Search;
196 |
--------------------------------------------------------------------------------
/src/connectors.js:
--------------------------------------------------------------------------------
1 | import { InjectedConnector } from "@web3-react/injected-connector";
2 | import { WalletConnectConnector } from "@web3-react/walletconnect-connector";
3 | import { WalletLinkConnector } from "@web3-react/walletlink-connector";
4 |
5 | import { config } from "./utils/config";
6 |
7 | export const POLLING_INTERVAL = 12000;
8 |
9 | export const injected = new InjectedConnector({
10 | supportedChainIds: [1, 5, 137, 80001],
11 | });
12 |
13 | export const walletconnect = new WalletConnectConnector({
14 | rpc: {
15 | 1: config[1].rpcUrl,
16 | 5: config[5].rpcUrl,
17 | 137: config[137].rpcUrl,
18 | 80001: config[80001].rpcUrl,
19 | },
20 | qrcode: true,
21 | pollingInterval: POLLING_INTERVAL,
22 | });
23 |
24 | export const walletlink = new WalletLinkConnector({
25 | url: config[1].rpcUrl,
26 | appName: "Web3 Domain Manager",
27 | supportedChainIds: [1, 5, 137, 80001],
28 | });
29 |
--------------------------------------------------------------------------------
/src/hooks.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useWeb3React } from '@web3-react/core';
3 |
4 | import { injected } from './connectors';
5 |
6 | export function useEagerConnect() {
7 | const { activate, active } = useWeb3React()
8 |
9 | const [tried, setTried] = useState(false)
10 |
11 | useEffect(() => {
12 | injected.isAuthorized().then((isAuthorized) => {
13 | if (isAuthorized) {
14 | activate(injected, undefined, true).catch(() => {
15 | setTried(true)
16 | })
17 | } else {
18 | setTried(true)
19 | }
20 | })
21 | }, []) // intentionally only running on mount (make sure it's only mounted once :))
22 |
23 | // if the connection worked, wait until we get confirmation of that to flip the flag
24 | useEffect(() => {
25 | if (!tried && active) {
26 | setTried(true)
27 | }
28 | }, [tried, active])
29 |
30 | return tried
31 | }
32 |
33 | export function useInactiveListener(suppress = false) {
34 | const { active, error, activate } = useWeb3React()
35 |
36 | useEffect(() => {
37 | const { ethereum } = window;
38 | if (ethereum && ethereum.on && !active && !error && !suppress) {
39 | const handleConnect = () => {
40 | console.log("Handling 'connect' event")
41 | activate(injected)
42 | }
43 | const handleChainChanged = (chainId) => {
44 | console.log("Handling 'chainChanged' event with payload", chainId)
45 | activate(injected)
46 | }
47 | const handleAccountsChanged = (accounts) => {
48 | console.log("Handling 'accountsChanged' event with payload", accounts)
49 | if (accounts.length > 0) {
50 | activate(injected)
51 | }
52 | }
53 | const handleNetworkChanged = (networkId) => {
54 | console.log("Handling 'networkChanged' event with payload", networkId)
55 | activate(injected)
56 | }
57 |
58 | ethereum.on('connect', handleConnect)
59 | ethereum.on('chainChanged', handleChainChanged)
60 | ethereum.on('accountsChanged', handleAccountsChanged)
61 | ethereum.on('networkChanged', handleNetworkChanged)
62 |
63 | return () => {
64 | if (ethereum.removeListener) {
65 | ethereum.removeListener('connect', handleConnect)
66 | ethereum.removeListener('chainChanged', handleChainChanged)
67 | ethereum.removeListener('accountsChanged', handleAccountsChanged)
68 | ethereum.removeListener('networkChanged', handleNetworkChanged)
69 | }
70 | }
71 | }
72 | }, [active, error, suppress, activate])
73 | }
74 |
--------------------------------------------------------------------------------
/src/images/cw.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/images/mm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aquiladev/web3-domain-manager/b08dc8a313e2becced2126e6dc90054bcbf8206f/src/images/mm.png
--------------------------------------------------------------------------------
/src/images/wc.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | background-color: #f5f5f5;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { StrictMode } from "react";
2 | import ReactDOM from "react-dom";
3 | import ReactGA from "react-ga4";
4 |
5 | import "./index.css";
6 | import App from "./App";
7 | import reportWebVitals from "./reportWebVitals";
8 |
9 | ReactGA.initialize("G-E307TY909N");
10 | ReactGA.send({
11 | hitType: "pageview",
12 | page: window.location.pathname + window.location.search,
13 | });
14 |
15 | ReactDOM.render(
16 |
17 |
18 | ,
19 | document.getElementById("root")
20 | );
21 |
22 | // If you want to start measuring performance in your app, pass a function
23 | // to log results (for example: reportWebVitals(console.log))
24 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
25 | reportWebVitals();
26 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/services/udRegistry.js:
--------------------------------------------------------------------------------
1 | const BASE_URL = "https://api.unstoppabledomains.com";
2 |
3 | export const getAvailability = async (name) => {
4 | if (!name) {
5 | throw new Error(`Name is undefined [name: ${name}]`);
6 | }
7 |
8 | const url = `${BASE_URL}/registry/v1/domains/${name}`;
9 | return fetch(url).then((response) => response.json());
10 | };
11 |
12 | export const getPurchaseParams = async (name, owner) => {
13 | if (!name) {
14 | throw new Error(`Name is undefined [name: ${name}]`);
15 | }
16 |
17 | const url = `${BASE_URL}/registry/v1/domains/${name}/parameters/purchase`;
18 | return fetch(url, {
19 | method: "POST",
20 | headers: { "Content-Type": "application/json" },
21 | body: JSON.stringify({
22 | owner: { address: owner },
23 | records: {},
24 | currency: "MATIC",
25 | }),
26 | }).then((response) => response.json());
27 | };
28 |
--------------------------------------------------------------------------------
/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';
6 |
--------------------------------------------------------------------------------
/src/sources/blockchain.js:
--------------------------------------------------------------------------------
1 | import supportedKeys from "uns/resolver-keys.json";
2 |
3 | const _keys = Object.keys(supportedKeys.keys);
4 |
5 | export async function getDomainName(registry, tokenId) {
6 | const events = await registry.source.fetchNewURIEvents(tokenId);
7 | if (!events || !events.length) return tokenId;
8 |
9 | return events[0].args.uri;
10 | }
11 |
12 | export const fetchTokens = (registry, account, type) => {
13 | return registry.source.fetchTransferEvents(account).then(async (events) => {
14 | console.debug(`Loaded events from registry ${registry.address}`, events);
15 |
16 | const _tokens = [];
17 | const _distinct = [];
18 | events.forEach(async (e) => {
19 | if (!_distinct.includes(e.args.tokenId.toString())) {
20 | _tokens.push({
21 | tokenId: e.args.tokenId.toHexString(),
22 | registry: registry.address,
23 | type,
24 | });
25 | _distinct.push(e.args.tokenId.toString());
26 | }
27 | });
28 | return _tokens;
29 | });
30 | };
31 |
32 | export const fetchNames = async (source, tokens) => {
33 | if (!tokens.length) return [];
34 |
35 | const events = await source.fetchNewURIEvents(tokens);
36 | return tokens.map((t) => {
37 | const event = events.find((e) => e.args.tokenId.toHexString() === t);
38 | return {
39 | tokenId: t,
40 | name: !!event ? event.args.uri : t,
41 | };
42 | });
43 | };
44 |
45 | export const fetchDomain = async (
46 | domain,
47 | registry,
48 | proxyReader,
49 | account,
50 | names
51 | ) => {
52 | const _data = await proxyReader.callStatic.getData(_keys, domain.id);
53 |
54 | const records = {};
55 | _keys.forEach((k, i) => (records[k] = _data[2][i]));
56 |
57 | const name = names && names.find((n) => n.tokenId === domain.id);
58 | domain.name = name ? name.name : await getDomainName(registry, domain.id);
59 | domain.owner = _data.owner;
60 | domain.removed = _data.owner !== account;
61 | domain.resolver = _data.resolver;
62 | domain.records = records;
63 | domain.loading = false;
64 |
65 | return domain;
66 | };
67 |
68 | export const fetchDomains = async (cnsRegistry, unsRegistry) => {
69 | const domains = [];
70 |
71 | console.debug("Loading events...");
72 | const [cnsTokens, unsTokens] = await Promise.all([
73 | fetchTokens(cnsRegistry, "cns"),
74 | fetchTokens(unsRegistry, "uns"),
75 | ]);
76 |
77 | const names = await Promise.all([
78 | fetchNames(
79 | cnsRegistry.source,
80 | cnsTokens.map((t) => t.tokenId)
81 | ),
82 | fetchNames(
83 | unsRegistry.source,
84 | unsTokens.map((t) => t.tokenId)
85 | ),
86 | ]).then((x) => x.flat());
87 |
88 | for (const token of cnsTokens.concat(unsTokens)) {
89 | // const registry =
90 | // unsRegistry.address === token.registry ? unsRegistry : cnsRegistry;
91 |
92 | const domain = {
93 | id: token.tokenId,
94 | name: token.tokenId,
95 | registry: token.registry,
96 | type: token.type,
97 | loading: true,
98 | };
99 | domains.push(domain);
100 |
101 | // fetchDomain(domain, registry, names).then((dd) => {
102 | // domains.map((d) => {
103 | // return d.id === dd.id ? { ...d, ...dd } : d;
104 | // });
105 |
106 | // setData({
107 | // ...data,
108 | // [stateKey]: {
109 | // isFetched: true,
110 | // domains: domains.filter((d) => !d.removed),
111 | // },
112 | // });
113 | // });
114 | }
115 |
116 | return domains.filter((d) => !d.removed);
117 | };
118 |
--------------------------------------------------------------------------------
/src/sources/ethereum.js:
--------------------------------------------------------------------------------
1 | import NetworkConfig from "uns/uns-config.json";
2 |
3 | import { DOMAIN_EVENTS } from "./../utils/constants";
4 |
5 | export default class EthereumEventSource {
6 | constructor(library, chainId, contract) {
7 | this.library = library;
8 | this.chainId = chainId;
9 | this.contract = contract;
10 |
11 | const { contracts } = NetworkConfig.networks[chainId];
12 | const config = Object.values(contracts).find(
13 | (x) => x.address.toLowerCase() === contract.address.toLowerCase()
14 | );
15 | if (!config) {
16 | throw new Error(
17 | `Config of contract ${contract.address} not found (chainId: ${chainId})`
18 | );
19 | }
20 | this.config = config;
21 |
22 | this.onProgressObservers = [];
23 | }
24 |
25 | onProgress(fn) {
26 | this.onProgressObservers.push(fn);
27 | }
28 |
29 | async fetchEvents(domain) {
30 | const contractEvents = Object.keys(this.contract.filters);
31 | const filtersMap = {
32 | Approval: (tokenId) =>
33 | this.contract.filters.Approval(undefined, undefined, tokenId),
34 | NewURI: (tokenId) => this.contract.filters.NewURI(tokenId),
35 | Transfer: (tokenId) =>
36 | this.contract.filters.Transfer(undefined, undefined, tokenId),
37 | Resolve: (tokenId) => this.contract.filters.Resolve(tokenId),
38 | Sync: (tokenId) =>
39 | this.contract.filters.Sync(undefined, undefined, tokenId),
40 | Set: (tokenId) => this.contract.filters.Set(tokenId),
41 | };
42 |
43 | let current = 1;
44 | return Promise.all(
45 | DOMAIN_EVENTS.filter((e) => contractEvents.includes(e)).map((event) => {
46 | const filter = filtersMap[event](domain.id);
47 | return this.contract
48 | .queryFilter(filter, this.config.deploymentBlock)
49 | .then(
50 | this._progress({
51 | loaded: current++,
52 | total: DOMAIN_EVENTS.length,
53 | })
54 | );
55 | })
56 | ).then((x) => x.flat().sort((a, b) => a.blockNumber - b.blockNumber));
57 | }
58 |
59 | async fetchTransferEvents(address) {
60 | const filter = this.contract.filters.Transfer(undefined, address);
61 | return this._fetchEvents(
62 | this.contract,
63 | filter,
64 | parseInt(this.config.deploymentBlock, 16)
65 | );
66 | }
67 |
68 | async fetchNewURIEvents(tokenOrArray) {
69 | const filter = this.contract.filters.NewURI(tokenOrArray);
70 | return this._fetchEvents(
71 | this.contract,
72 | filter,
73 | parseInt(this.config.deploymentBlock, 16)
74 | );
75 | }
76 |
77 | async _fetchEvents(contract, filter, fromBlock, toBlock, limit = 1000000) {
78 | if (!toBlock) {
79 | toBlock = await contract.provider.getBlockNumber();
80 | }
81 |
82 | if (fromBlock > toBlock) {
83 | return [];
84 | }
85 |
86 | const _toBlock = Math.min(fromBlock + limit, toBlock);
87 | console.log(
88 | `Fetching events blocks [${contract.address}: ${fromBlock}-${_toBlock}][limit: ${limit}]`
89 | );
90 |
91 | try {
92 | const events = await contract.queryFilter(filter, fromBlock, _toBlock);
93 | const nextLimit = Math.min(Math.floor(limit * 2), 1000000);
94 | return events.concat(
95 | await this._fetchEvents(
96 | contract,
97 | filter,
98 | _toBlock + 1,
99 | toBlock,
100 | nextLimit
101 | )
102 | );
103 | } catch (err) {
104 | if (
105 | err.message.includes("query returned more than 10000 results") ||
106 | err.message.includes("timeout")
107 | ) {
108 | return this._fetchEvents(
109 | contract,
110 | filter,
111 | fromBlock,
112 | toBlock,
113 | Math.floor(limit / 10)
114 | );
115 | }
116 |
117 | console.log("FAIL", err);
118 | throw err;
119 | }
120 | }
121 |
122 | _progress(data) {
123 | this.onProgressObservers.forEach((fn) => fn(data));
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/sources/thegraph.js:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client";
2 |
3 | import { config } from "../utils/config";
4 |
5 | export const getAccount = async (chainId, address, force = false) => {
6 | if (!chainId || !address) {
7 | throw new Error(
8 | `Invalid arguments [chainId: ${chainId}, account: ${address}]`
9 | );
10 | }
11 |
12 | const { client } = config[chainId];
13 | if (!client) {
14 | throw new Error(`Unsupported chain ${chainId}`);
15 | }
16 |
17 | const { data } = await client.query({
18 | query: gql`
19 | query GetAccount($id: ID!) {
20 | account(id: $id) {
21 | id
22 | reverse {
23 | id
24 | name
25 | }
26 | domains {
27 | id
28 | name
29 | registry
30 | owner {
31 | id
32 | }
33 | resolver {
34 | address
35 | records {
36 | key
37 | value
38 | }
39 | }
40 | }
41 | }
42 | }
43 | `,
44 | variables: { id: String(address).toLowerCase() },
45 | fetchPolicy: force ? "no-cache" : undefined,
46 | });
47 |
48 | const { account } = data;
49 | if (!account) {
50 | return {};
51 | }
52 |
53 | const domains = _parseDomains(account.domains);
54 |
55 | // Domain name lookup in linked network
56 | const unknownTokens = domains.filter((d) => !d.name).map((d) => d.id);
57 | if (unknownTokens.length) {
58 | const map = await _getDomainNamesMap(
59 | config[chainId].linkedChainId,
60 | unknownTokens
61 | );
62 | console.log(
63 | "NO_NAME_DOMAINS",
64 | domains.filter((d) => !d.name).map((d) => d.id),
65 | map
66 | );
67 | domains.map((d) => {
68 | if (!d.name && !!map[d.id]) {
69 | d.name = map[d.id];
70 | }
71 | return d;
72 | });
73 | }
74 |
75 | return {
76 | id: account.id,
77 | domains,
78 | };
79 | };
80 |
81 | export const getDomain = async (chainId, tokenId, force = false) => {
82 | if (!chainId || !tokenId) {
83 | throw new Error(
84 | `Invalid arguments [chainId: ${chainId}, tokenId: ${tokenId}]`
85 | );
86 | }
87 |
88 | const { client } = config[chainId];
89 | if (!client) {
90 | throw new Error(`Unsupported chain ${chainId}`);
91 | }
92 |
93 | const { data } = await client.query({
94 | query: gql`
95 | query GetDomain($id: ID!) {
96 | domain(id: $id) {
97 | id
98 | name
99 | registry
100 | owner {
101 | id
102 | }
103 | resolver {
104 | address
105 | records {
106 | key
107 | value
108 | }
109 | }
110 | }
111 | }
112 | `,
113 | variables: { id: String(tokenId).toLowerCase() },
114 | fetchPolicy: force ? "no-cache" : undefined,
115 | });
116 |
117 | const { domain } = data;
118 | if (!domain) {
119 | return {};
120 | }
121 |
122 | return {
123 | id: domain.id,
124 | name: domain.name,
125 | owner: domain.owner.id,
126 | registry: domain.registry,
127 | resolver: (domain.resolver || {}).address,
128 | records: _parseRecords(domain.resolver),
129 | };
130 | };
131 |
132 | const _parseDomains = (domains) => {
133 | if (!domains || !Array(domains).length) {
134 | return [];
135 | }
136 |
137 | return Object.values(domains).map((d) => {
138 | return {
139 | id: d.id,
140 | name: d.name,
141 | owner: d.owner.id,
142 | registry: d.registry,
143 | resolver: (d.resolver || {}).address,
144 | records: _parseRecords(d.resolver),
145 | };
146 | });
147 | };
148 |
149 | const _parseRecords = (resolver) => {
150 | if (!resolver || !resolver.records) {
151 | return {};
152 | }
153 |
154 | return Object.values(resolver.records).reduce((obj, r) => {
155 | obj[r.key] = r.value;
156 | return obj;
157 | }, {});
158 | };
159 |
160 | const _getDomainNamesMap = async (chainId, tokenIds) => {
161 | const { client } = config[chainId];
162 | if (!client) {
163 | throw new Error(`Unsupported chain ${chainId}`);
164 | }
165 |
166 | const { data } = await client.query({
167 | query: gql`
168 | query filteredDomains($ids: [ID!]!) {
169 | domains(where: { id_in: $ids }) {
170 | id
171 | name
172 | }
173 | }
174 | `,
175 | variables: {
176 | ids: tokenIds,
177 | },
178 | });
179 | const { domains } = data;
180 | if (!domains) {
181 | return {};
182 | }
183 |
184 | return Object.values(domains).reduce((obj, key) => {
185 | obj[key.id] = key.name;
186 | return obj;
187 | }, {});
188 | };
189 |
--------------------------------------------------------------------------------
/src/utils/address.js:
--------------------------------------------------------------------------------
1 | import sha3 from 'crypto-js/sha3';
2 |
3 | export function isAddress(address) {
4 | if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) {
5 | // check if it has the basic requirements of an address
6 | return false;
7 | } else if (/^(0x)?[0-9a-f]{40}$/.test(address) || /^(0x)?[0-9A-F]{40}$/.test(address)) {
8 | // If it's all small caps or all all caps, return true
9 | return true;
10 | } else {
11 | // Otherwise check each case
12 | return isChecksumAddress(address);
13 | }
14 | };
15 |
16 | export function isChecksumAddress(address) {
17 | // Check each case
18 | address = address.replace('0x', '');
19 | var addressHash = sha3(address.toLowerCase());
20 | for (var i = 0; i < 40; i++) {
21 | // the nth letter should be uppercase if the nth digit of casemap is 1
22 | if ((parseInt(addressHash[i], 16) > 7 && address[i].toUpperCase() !== address[i]) ||
23 | (parseInt(addressHash[i], 16) <= 7 && address[i].toLowerCase() !== address[i])) {
24 | return false;
25 | }
26 | }
27 | return true;
28 | };
29 |
--------------------------------------------------------------------------------
/src/utils/config.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient, InMemoryCache } from "@apollo/client";
2 |
3 | export const config = {
4 | 1: {
5 | rpcUrl: "https://mainnet.infura.io/v3/3947c045ca5a4d68bff484fb038fb11c",
6 | linkedChainId: 137,
7 | client: new ApolloClient({
8 | uri: "https://api.thegraph.com/subgraphs/name/aquiladev/uns",
9 | cache: new InMemoryCache(),
10 | }),
11 | },
12 | 5: {
13 | rpcUrl: "https://goerli.infura.io/v3/3947c045ca5a4d68bff484fb038fb11c",
14 | linkedChainId: 80001,
15 | client: new ApolloClient({
16 | uri: "https://api.thegraph.com/subgraphs/name/aquiladev/uns-goerli",
17 | cache: new InMemoryCache(),
18 | }),
19 | },
20 | 137: {
21 | rpcUrl:
22 | "https://polygon-mainnet.infura.io/v3/3947c045ca5a4d68bff484fb038fb11c",
23 | linkedChainId: 1,
24 | client: new ApolloClient({
25 | uri: "https://api.thegraph.com/subgraphs/name/aquiladev/uns-polygon",
26 | cache: new InMemoryCache(),
27 | }),
28 | },
29 | 80001: {
30 | rpcUrl:
31 | "https://polygon-mumbai.infura.io/v3/3947c045ca5a4d68bff484fb038fb11c",
32 | linkedChainId: 5,
33 | client: new ApolloClient({
34 | uri: "https://api.thegraph.com/subgraphs/name/aquiladev/uns-mumbai",
35 | cache: new InMemoryCache(),
36 | }),
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const DOMAIN_EVENTS = [
2 | "Approval",
3 | "NewURI",
4 | "Transfer",
5 | "Resolve",
6 | "Sync",
7 | "Set",
8 | ];
9 |
10 | export const ETHERSCAN_MAP = {
11 | 1: "https://etherscan.io/",
12 | 5: "https://goerli.etherscan.io/",
13 | 137: "https://polygonscan.com/",
14 | 80001: "https://mumbai.polygonscan.com/",
15 | };
16 |
17 | export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
18 |
19 | export const CHAIN_ID_NETWORK = {
20 | 1: "Mainnet",
21 | 3: "Ropsten",
22 | 4: "Rinkeby",
23 | 5: "Goerli",
24 | 8: "Ubiq",
25 | 18: "ThundercoreTestnet",
26 | 42: "Kovan",
27 | 30: "Orchid",
28 | 31: "OrchidTestnet",
29 | 61: "Classic",
30 | 77: "Sokol",
31 | 99: "Core",
32 | 100: "xDai",
33 | 108: "Thundercore",
34 | 122: "Fuse",
35 | 137: "Polygon Mainnet",
36 | 163: "Lightstreams",
37 | 80001: "Polygon Mumbai",
38 | };
39 |
--------------------------------------------------------------------------------
/src/utils/contract.js:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 |
3 | import EthereumEventSource from "../sources/ethereum";
4 |
5 | export function createContract(library, chainId, abi, config) {
6 | const provider = new ethers.providers.Web3Provider(library.provider);
7 | const signer = provider.getSigner();
8 | const contract = new ethers.Contract(config.address, abi, signer);
9 |
10 | contract.source = new EthereumEventSource(library, chainId, contract);
11 | contract.source.onProgress(console.log);
12 | return contract;
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/namehash.js:
--------------------------------------------------------------------------------
1 | import { ethers, BigNumber } from 'ethers';
2 |
3 | export default function hamehash(domain) {
4 | domain = domain ? domain.trim().toLowerCase() : '';
5 | try {
6 | return BigNumber.from(domain).toHexString();
7 | } catch {}
8 |
9 | ensureSupportedTLD(domain);
10 | return ethers.utils.namehash.hash(domain)
11 | }
12 |
13 | function ensureSupportedTLD(domain) {
14 | if (!isSupportedTLD(domain)) {
15 | throw new Error('Domain is not supported', {
16 | domain,
17 | });
18 | }
19 | }
20 |
21 | function isSupportedTLD(domain) {
22 | return (
23 | ['crypto','coin','wallet','bitcoin','x','888','nft','dao','blockchain'].includes(domain) ||
24 | (domain.indexOf('.') > 0 &&
25 | /^.{1,}\.(crypto|coin|wallet|bitcoin|x|888|nft|dao|blockchain)$/.test(domain) &&
26 | domain.split('.').every(v => !!v.length))
27 | );
28 | }
29 |
--------------------------------------------------------------------------------