├── .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 | 2 | 3 | 4 | 5 | 6 | 7 | 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 | 98 | Connect to a wallet 99 | 100 | {Object.keys(connectorsByName).map((name) => { 101 | return ( 102 |
103 | 117 |
118 | ); 119 | })} 120 |
121 |
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 | 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 | 384 | {!!domain && ( 385 | <> 386 | Transfer {domain.name} 387 | 388 | 389 | { 396 | setReceiver(event.target.value); 397 | }} 398 | /> 399 | 400 | {transferError && ( 401 | 402 | {transferError} 403 | 404 | )} 405 | 406 | 407 | 410 | 419 | 420 | { 421 | 422 | 423 | 424 | } 425 | 426 | )} 427 | 428 | 434 | {!!records && ( 435 | <> 436 | Records [{records.name}] 437 | 438 | { 443 | handleUpdate(records, _records); 444 | }} 445 | onCancel={() => { 446 | setRecords(); 447 | }} 448 | /> 449 | 450 | 451 | )} 452 | 453 | 459 | Mint free domain 460 | 461 | { 464 | handleMint(tld, domainName); 465 | }} 466 | onCancel={() => { 467 | setDomainToMint(false); 468 | }} 469 | error={mintError} 470 | /> 471 | 472 | 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 |
110 | 111 | 112 | 113 | 114 | Web3 Domain Manager (beta) 115 | 116 | 117 |
118 |
119 |
120 | 121 |
122 | 134 |
135 | 136 | 147 | 159 | {/* 160 |
Lookup
161 | 162 |
*/} 163 | 169 | 170 |
UNS stats
171 | 172 |
173 |
174 | 180 | 181 |
Code
182 | 183 |
184 |
185 |
186 |
187 |
188 |
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 | .crypto 78 | {/* .coin */} 79 | .wallet 80 | .bitcoin 81 | .x 82 | .888 83 | .nft 84 | .dao 85 | .blockchain 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/images/mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquiladev/web3-domain-manager/b08dc8a313e2becced2126e6dc90054bcbf8206f/src/images/mm.png -------------------------------------------------------------------------------- /src/images/wc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 | --------------------------------------------------------------------------------