diff --git a/dns/frontend/.eslintrc.cjs b/dns/frontend/.eslintrc.cjs new file mode 100644 index 00000000..d6c95379 --- /dev/null +++ b/dns/frontend/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/dns/frontend/.gitignore b/dns/frontend/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/dns/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/dns/frontend/README.md b/dns/frontend/README.md new file mode 100644 index 00000000..6c867152 --- /dev/null +++ b/dns/frontend/README.md @@ -0,0 +1,20 @@ +# ink! Frontend Example + +This is a vanilla [vite + typescript](https://vitejs.dev/) project to showcase the use of [`useinkathon`](https://github.com/scio-labs/use-inkathon). + +## Getting Started + +You can use the package manager of your choice to install the dependencies and start the project in development mode. We like `pnpm` right now. But this example should work with `npm` & `yarn` as well. + +```sh +pnpm install +pnpm dev +``` + +## Change the Code + +The actual interaction with the contract is all contained in the `./src/App.tsx` file. Every other file in the folder is only relevant for styling and bundling. + +## Demo + + \ No newline at end of file diff --git a/dns/frontend/demo.gif b/dns/frontend/demo.gif new file mode 100644 index 00000000..6a264718 Binary files /dev/null and b/dns/frontend/demo.gif differ diff --git a/dns/frontend/index.html b/dns/frontend/index.html new file mode 100644 index 00000000..7094f9d5 --- /dev/null +++ b/dns/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + ink! Frontend Example + + +
+ + + diff --git a/dns/frontend/package.json b/dns/frontend/package.json new file mode 100644 index 00000000..0b645829 --- /dev/null +++ b/dns/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "dns-frontend-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@polkadot/util-crypto": "^12.6.2", + "@scio-labs/use-inkathon": "^0.6.3", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", + "@vitejs/plugin-react-swc": "^3.5.0", + "autoprefixer": "^10.4.17", + "eslint": "^8.56.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.33", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3", + "vite": "^5.0.12" + } +} diff --git a/dns/frontend/postcss.config.js b/dns/frontend/postcss.config.js new file mode 100644 index 00000000..2e7af2b7 --- /dev/null +++ b/dns/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/dns/frontend/src/App.tsx b/dns/frontend/src/App.tsx new file mode 100644 index 00000000..bd0e05c0 --- /dev/null +++ b/dns/frontend/src/App.tsx @@ -0,0 +1,370 @@ +import { blake2AsHex } from "@polkadot/util-crypto"; +import { + SubstrateDeployment, + UseInkathonProvider, + contractQuery, + contractTx, + decodeOutput, + rococo, + useBalance, + useInkathon, + useRegisteredContract, +} from "@scio-labs/use-inkathon"; +import { useRef, useState } from "react"; + +import CONTRACT_METADATA from "./dns.json"; +const CONTRACT_NAME = "dns"; + +const getDeployments = async (): Promise => { + return [ + { + contractId: CONTRACT_NAME, + networkId: rococo.network, + abi: CONTRACT_METADATA, + address: "5GWCUiApMhV3QYK4RedaLpbhcCBWLeGVT2wtZPfCHhnHxoud", + }, + ]; +}; + +export default function WrappedApp() { + return ( + + + + ); +} + +function App() { + const { isConnected } = useInkathon(); + return ( +
+
+ + {isConnected && ( + <> + + + + + )} +
+
+ ); +} + +const ConnectionState = () => { + const { + connect, + disconnect, + isConnected, + activeChain, + activeAccount, + setActiveAccount, + accounts, + } = useInkathon(); + const { contract } = useRegisteredContract(CONTRACT_NAME); + const balance = useBalance(activeAccount?.address, true); + + if (!isConnected) { + return ( +
+ +
+ ); + } + + return ( +
+
+ {activeChain && ( +
+
Chain
+
{activeChain.name}
+
+ )} + + {activeAccount && accounts && ( +
+
Active Account
+ + +
+ {activeAccount.address} +
+
+ )} + + {balance && ( +
+
Account Balance
+
+ {balance.balanceFormatted} +
+
+ + Get Tokens from Testnet Faucet + +
+
+ )} + + {contract && ( +
+
Contract
+
+ {contract?.address.toHex()} +
+
+ )} +
+ +
+ ); +}; + +const RegisterName = () => { + const { api, activeAccount } = useInkathon(); + + const [name, setName] = useState(""); + const nameRef = useRef(null); + + const { contract } = useRegisteredContract("dns"); + + const { + loading, + error, + result, + execute: register, + } = usePromise(async () => { + if (!contract || !api || !activeAccount || !nameRef.current) return; + setName(nameRef.current.value); + return contractTx(api, activeAccount.address, contract, "register", {}, [ + blake2AsHex(nameRef.current.value), + ]); + }); + + return ( +
+
+

Step 1: Register a New Name

+

+ Claim ownership of the given name +

+
+ +
+ + +
+ + {error && ( + <> +
+
{error}
+ + )} + + {result && !!result.successEvent && ( + <> +
+
+ Name {name} registered! +
+ + )} +
+ ); +}; + +const SetAddress = () => { + const { api, activeAccount } = useInkathon(); + const [name, setName] = useState(""); + const [accountId, setAccountId] = useState(""); + + const { contract } = useRegisteredContract("dns"); + + const { execute, loading, result, error } = usePromise(async () => { + if (!contract || !api || !activeAccount || !name) return; + + return contractTx(api, activeAccount.address, contract, "setAddress", {}, [ + blake2AsHex(name), + accountId, + ]); + }); + + return ( +
+
+

+ Step 2: Set AccountId For Name +

+

+ Set the AccountId which the given name{" "} + should resolve to +

+
+ +
+ setName(event.target.value)} + id="name" + /> + setAccountId(event.target.value)} + id="account_id" + /> + +
+ + {error && ( + <> +
+
{error}
+ + )} + {result && !!result.successEvent && ( + <> +
+
+
+ Name {name} now resolves to: +
+
+ {accountId} +
+
+ + )} +
+ ); +}; + +const GetAddress = () => { + const { api } = useInkathon(); + const inputRef = useRef(null); + const [name, setName] = useState(""); + const [result, setResult] = useState>( + null + ); + const { contract } = useRegisteredContract("dns"); + + const getAddress = async (name: string) => { + if (!contract || !api || !name) return; + + const result = await contractQuery( + api, + "", + contract, + "getAddress", + undefined, + [blake2AsHex(name)] + ); + + setResult(decodeOutput(result, contract, "getAddress")); + setName(name); + }; + + return ( +
+
+

+ Step 3: Lookup Name +

+

+ Resolves Address for a given Name +

+
+ +
+ + +
+ {result && ( + <> +
+
+ {name} resolves to: +
+
{result.decodedOutput}
+ + )} +
+ ); +}; + +const usePromise = (promise: () => Promise) => { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const execute = async () => { + setLoading(true); + setError(null); + try { + const result = await promise(); + setResult(result); + } catch (error) { + if ( + typeof error === "object" && + error && + "errorMessage" in error && + typeof error.errorMessage === "string" + ) + setError(error.errorMessage); + else { + console.error(error); + setError("Unknown error, check console"); + } + } finally { + setLoading(false); + } + }; + + return { result, error, loading, execute }; +}; diff --git a/dns/frontend/src/dns.json b/dns/frontend/src/dns.json new file mode 100644 index 00000000..e41a57d6 --- /dev/null +++ b/dns/frontend/src/dns.json @@ -0,0 +1,787 @@ +{ + "source": { + "hash": "0x5450904551b4922e90662a0d278ca50988780bffc17ac871fceda420d85f8d79", + "language": "ink! 4.3.0", + "compiler": "rustc 1.72.1", + "build_info": { + "build_mode": "Debug", + "cargo_contract_version": "3.2.0", + "rust_toolchain": "stable-aarch64-apple-darwin", + "wasm_opt_settings": { + "keep_debug_symbols": false, + "optimization_passes": "Z" + } + } + }, + "contract": { + "name": "dns", + "version": "4.3.0", + "authors": [ + "Parity Technologies " + ] + }, + "spec": { + "constructors": [ + { + "args": [], + "default": false, + "docs": [ + "Creates a new domain name service contract." + ], + "label": "new", + "payable": false, + "returnType": { + "displayName": [ + "ink_primitives", + "ConstructorResult" + ], + "type": 3 + }, + "selector": "0x9bae9d5e" + } + ], + "docs": [], + "environment": { + "accountId": { + "displayName": [ + "AccountId" + ], + "type": 0 + }, + "balance": { + "displayName": [ + "Balance" + ], + "type": 12 + }, + "blockNumber": { + "displayName": [ + "BlockNumber" + ], + "type": 14 + }, + "chainExtension": { + "displayName": [ + "ChainExtension" + ], + "type": 15 + }, + "hash": { + "displayName": [ + "Hash" + ], + "type": 6 + }, + "maxEventTopics": 4, + "timestamp": { + "displayName": [ + "Timestamp" + ], + "type": 13 + } + }, + "events": [ + { + "args": [ + { + "docs": [], + "indexed": true, + "label": "name", + "type": { + "displayName": [ + "Hash" + ], + "type": 6 + } + }, + { + "docs": [], + "indexed": true, + "label": "from", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + } + ], + "docs": [ + "Emitted whenever a new name is being registered." + ], + "label": "Register" + }, + { + "args": [ + { + "docs": [], + "indexed": true, + "label": "name", + "type": { + "displayName": [ + "Hash" + ], + "type": 6 + } + }, + { + "docs": [], + "indexed": false, + "label": "from", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + }, + { + "docs": [], + "indexed": true, + "label": "old_address", + "type": { + "displayName": [ + "Option" + ], + "type": 11 + } + }, + { + "docs": [], + "indexed": true, + "label": "new_address", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + } + ], + "docs": [ + "Emitted whenever an address changes." + ], + "label": "SetAddress" + }, + { + "args": [ + { + "docs": [], + "indexed": true, + "label": "name", + "type": { + "displayName": [ + "Hash" + ], + "type": 6 + } + }, + { + "docs": [], + "indexed": false, + "label": "from", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + }, + { + "docs": [], + "indexed": true, + "label": "old_owner", + "type": { + "displayName": [ + "Option" + ], + "type": 11 + } + }, + { + "docs": [], + "indexed": true, + "label": "new_owner", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + } + ], + "docs": [ + "Emitted whenever a name is being transferred." + ], + "label": "Transfer" + } + ], + "lang_error": { + "displayName": [ + "ink", + "LangError" + ], + "type": 5 + }, + "messages": [ + { + "args": [ + { + "label": "name", + "type": { + "displayName": [ + "Hash" + ], + "type": 6 + } + } + ], + "default": false, + "docs": [ + " Register specific name with caller as owner." + ], + "label": "register", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 7 + }, + "selector": "0x229b553f" + }, + { + "args": [ + { + "label": "name", + "type": { + "displayName": [ + "Hash" + ], + "type": 6 + } + }, + { + "label": "new_address", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + } + ], + "default": false, + "docs": [ + " Set address for specific name." + ], + "label": "set_address", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 7 + }, + "selector": "0xb8a4d3d9" + }, + { + "args": [ + { + "label": "name", + "type": { + "displayName": [ + "Hash" + ], + "type": 6 + } + }, + { + "label": "to", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + } + ], + "default": false, + "docs": [ + " Transfer owner to another address." + ], + "label": "transfer", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 7 + }, + "selector": "0x84a15da1" + }, + { + "args": [ + { + "label": "name", + "type": { + "displayName": [ + "Hash" + ], + "type": 6 + } + } + ], + "default": false, + "docs": [ + " Get address for specific name." + ], + "label": "get_address", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 10 + }, + "selector": "0xd259f7ba" + }, + { + "args": [ + { + "label": "name", + "type": { + "displayName": [ + "Hash" + ], + "type": 6 + } + } + ], + "default": false, + "docs": [ + " Get owner of specific name." + ], + "label": "get_owner", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 10 + }, + "selector": "0x07fcd0b1" + } + ] + }, + "storage": { + "root": { + "layout": { + "struct": { + "fields": [ + { + "layout": { + "root": { + "layout": { + "leaf": { + "key": "0x9891106e", + "ty": 0 + } + }, + "root_key": "0x9891106e" + } + }, + "name": "name_to_address" + }, + { + "layout": { + "root": { + "layout": { + "leaf": { + "key": "0xd3cd158e", + "ty": 0 + } + }, + "root_key": "0xd3cd158e" + } + }, + "name": "name_to_owner" + }, + { + "layout": { + "leaf": { + "key": "0x00000000", + "ty": 0 + } + }, + "name": "default_address" + } + ], + "name": "DomainNameService" + } + }, + "root_key": "0x00000000" + } + }, + "types": [ + { + "id": 0, + "type": { + "def": { + "composite": { + "fields": [ + { + "type": 1, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_primitives", + "types", + "AccountId" + ] + } + }, + { + "id": 1, + "type": { + "def": { + "array": { + "len": 32, + "type": 2 + } + } + } + }, + { + "id": 2, + "type": { + "def": { + "primitive": "u8" + } + } + }, + { + "id": 3, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 4 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 5 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 4 + }, + { + "name": "E", + "type": 5 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 4, + "type": { + "def": { + "tuple": [] + } + } + }, + { + "id": 5, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 1, + "name": "CouldNotReadInput" + } + ] + } + }, + "path": [ + "ink_primitives", + "LangError" + ] + } + }, + { + "id": 6, + "type": { + "def": { + "composite": { + "fields": [ + { + "type": 1, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_primitives", + "types", + "Hash" + ] + } + }, + { + "id": 7, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 8 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 5 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 8 + }, + { + "name": "E", + "type": 5 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 8, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 4 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 9 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 4 + }, + { + "name": "E", + "type": 9 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 9, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 0, + "name": "NameAlreadyExists" + }, + { + "index": 1, + "name": "CallerIsNotOwner" + } + ] + } + }, + "path": [ + "dns", + "dns", + "Error" + ] + } + }, + { + "id": 10, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 0 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 5 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 0 + }, + { + "name": "E", + "type": 5 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 11, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 0, + "name": "None" + }, + { + "fields": [ + { + "type": 0 + } + ], + "index": 1, + "name": "Some" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 0 + } + ], + "path": [ + "Option" + ] + } + }, + { + "id": 12, + "type": { + "def": { + "primitive": "u128" + } + } + }, + { + "id": 13, + "type": { + "def": { + "primitive": "u64" + } + } + }, + { + "id": 14, + "type": { + "def": { + "primitive": "u32" + } + } + }, + { + "id": 15, + "type": { + "def": { + "variant": {} + }, + "path": [ + "ink_env", + "types", + "NoChainExtension" + ] + } + } + ], + "version": "4" +} \ No newline at end of file diff --git a/dns/frontend/src/index.css b/dns/frontend/src/index.css new file mode 100644 index 00000000..04e892ed --- /dev/null +++ b/dns/frontend/src/index.css @@ -0,0 +1,42 @@ + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer components { + + body { + @apply font-sans text-base antialiased leading-6; + } + + button, [type='submit'], [type='button'] { + @apply disabled:bg-gray-400 disabled:cursor-not-allowed disabled:opacity-50 bg-purple-500 hover:bg-purple-400 text-white font-bold py-2 px-4 border-b-4 border-purple-700 hover:border-purple-500 disabled:border-gray-500 rounded active:border-none active:mt-1; + } + + a{ + @apply text-purple-500 hover:text-purple-700 active:text-purple-700; + } + + select { + @apply bg-purple-500 px-4 my-1 py-2 rounded text-white font-bold border-b-4 border-purple-700; + } + + input { + @apply bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500; + } + + #root{ + } + + .card { + @apply flex flex-col gap-4 py-4 px-4 w-full rounded-lg overflow-hidden border-2 border-slate-300 ; + } + + .error { + @apply bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative; + } + + .success { + @apply bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative; + } +} diff --git a/dns/frontend/src/main.tsx b/dns/frontend/src/main.tsx new file mode 100644 index 00000000..7432d465 --- /dev/null +++ b/dns/frontend/src/main.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.tsx'; +import './index.css'; + +// biome-ignore lint/style/noNonNullAssertion: +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/dns/frontend/src/vite-env.d.ts b/dns/frontend/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/dns/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dns/frontend/tailwind.config.js b/dns/frontend/tailwind.config.js new file mode 100644 index 00000000..614c86b4 --- /dev/null +++ b/dns/frontend/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/dns/frontend/tsconfig.json b/dns/frontend/tsconfig.json new file mode 100644 index 00000000..a7fc6fbf --- /dev/null +++ b/dns/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/dns/frontend/tsconfig.node.json b/dns/frontend/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/dns/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/dns/frontend/vite.config.ts b/dns/frontend/vite.config.ts new file mode 100644 index 00000000..861b04b3 --- /dev/null +++ b/dns/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +})