chore: register domain
This commit is contained in:
parent
eb210480bc
commit
ac6095e2b2
@ -12,9 +12,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rainbow-me/rainbowkit": "^2.0.5",
|
"@rainbow-me/rainbowkit": "^2.0.5",
|
||||||
"@tanstack/react-query": "^5.29.2",
|
"@tanstack/react-query": "^5.29.2",
|
||||||
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"viem": "2.x",
|
"react-loading-skeleton": "^3.4.0",
|
||||||
|
"react-toastify": "^10.0.5",
|
||||||
|
"viem": "^2.9.25",
|
||||||
"wagmi": "^2.5.20"
|
"wagmi": "^2.5.20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
33
src/App.jsx
33
src/App.jsx
@ -1,15 +1,24 @@
|
|||||||
import { ConnectButton } from '@rainbow-me/rainbowkit';
|
import { ConnectButton } from '@rainbow-me/rainbowkit';
|
||||||
import walletLogo from './assets/wallet.id.png';
|
import brandLogo from './assets/brand.png';
|
||||||
import searchIcon from './assets/search.png';
|
|
||||||
import hnsLogo from './assets/hns.id.png';
|
import hnsLogo from './assets/hns.id.png';
|
||||||
|
import { Search } from './components/search/Search';
|
||||||
|
import {
|
||||||
|
HERO_TEXT,
|
||||||
|
PAGE_TITLE,
|
||||||
|
SUB_TEXT,
|
||||||
|
TLD,
|
||||||
|
TWITTER_HANDLE,
|
||||||
|
} from './constants';
|
||||||
|
import { useDocumentTitle } from '@uidotdev/usehooks';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
useDocumentTitle(PAGE_TITLE);
|
||||||
return (
|
return (
|
||||||
<div className="bg-neutral-50 h-screen flex">
|
<div className="bg-neutral-50 h-screen flex">
|
||||||
<div className="flex-col justify-between items-center flex max-w-7xl mx-auto flex-1">
|
<div className="flex-col justify-between items-center flex max-w-7xl mx-auto flex-1">
|
||||||
<div className="flex-col justify-center items-center gap-10 flex w-full">
|
<div className="flex-col justify-center items-center gap-10 flex w-full">
|
||||||
<div className="h-20 p-4 justify-between items-center inline-flex w-full ">
|
<div className="h-20 p-4 justify-between items-center inline-flex w-full ">
|
||||||
<img className="w-36" src={walletLogo} />
|
<img className="w-36" src={brandLogo} />
|
||||||
<div className="justify-start items-center gap-6 flex ">
|
<div className="justify-start items-center gap-6 flex ">
|
||||||
<ConnectButton />
|
<ConnectButton />
|
||||||
</div>
|
</div>
|
||||||
@ -18,22 +27,14 @@ function App() {
|
|||||||
<div className="flex-col justify-start items-center gap-4 fle">
|
<div className="flex-col justify-start items-center gap-4 fle">
|
||||||
<div className="self-stretch flex-col justify-start items-center gap-4 flex">
|
<div className="self-stretch flex-col justify-start items-center gap-4 flex">
|
||||||
<div className="self-stretch text-center text-neutral-950 text-5xl font-bold leading-relaxed">
|
<div className="self-stretch text-center text-neutral-950 text-5xl font-bold leading-relaxed">
|
||||||
Own your .wallet
|
{HERO_TEXT}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch h-10 text-center text-neutral-500 text-2xl font-bold leading-normal">
|
<div className="self-stretch h-10 text-center text-neutral-500 text-2xl font-bold leading-normal">
|
||||||
Decentralized domains for websites, wallets and web3
|
{SUB_TEXT}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-white rounded-2xl shadow border border-zinc-300 justify-between items-center inline-flex relative">
|
<Search />
|
||||||
<input
|
|
||||||
className="grow shrink basis-0 text-neutral-400 text-base font-medium leading-tight tracking-tight p-5 rounded-2xl "
|
|
||||||
placeholder="Find your .wallet"
|
|
||||||
/>
|
|
||||||
<div className="w-5 h-5 absolute right-3">
|
|
||||||
<img src={searchIcon} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-6 border-t border-gray-200 flex-col justify-start items-center gap-6 flex w-full">
|
<div className="px-4 py-6 border-t border-gray-200 flex-col justify-start items-center gap-6 flex w-full">
|
||||||
@ -41,7 +42,7 @@ function App() {
|
|||||||
<div className="justify-start items-center gap-6 flex">
|
<div className="justify-start items-center gap-6 flex">
|
||||||
<a
|
<a
|
||||||
className="text-neutral-700 text-sm font-medium uppercase"
|
className="text-neutral-700 text-sm font-medium uppercase"
|
||||||
href="https://twitter.com/walletdomain"
|
href={`https://twitter.com/${TWITTER_HANDLE}`}
|
||||||
>
|
>
|
||||||
Twitter
|
Twitter
|
||||||
</a>
|
</a>
|
||||||
@ -53,7 +54,7 @@ function App() {
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="text-neutral-700 text-sm font-medium uppercase"
|
className="text-neutral-700 text-sm font-medium uppercase"
|
||||||
href="https://opensea.io/collection/handshake-slds?search[stringTraits][0][name]=TLD&search[stringTraits][0][values][0]=wallet"
|
href={`https://opensea.io/collection/handshake-slds?search[stringTraits][0][name]=TLD&search[stringTraits][0][values][0]=${TLD}`}
|
||||||
>
|
>
|
||||||
Opensea
|
Opensea
|
||||||
</a>
|
</a>
|
||||||
|
98
src/abi.js
Normal file
98
src/abi.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
export const abi = [
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
name: 'getDomainDetails',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
name: '_buyer',
|
||||||
|
type: 'address',
|
||||||
|
internalType: 'address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '_registrationDays',
|
||||||
|
type: 'uint256',
|
||||||
|
internalType: 'uint256',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '_parentHash',
|
||||||
|
type: 'bytes32',
|
||||||
|
internalType: 'bytes32',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '_label',
|
||||||
|
type: 'string',
|
||||||
|
internalType: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
type: 'tuple',
|
||||||
|
internalType: 'struct DomainDetails',
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
name: 'isAvailable',
|
||||||
|
type: 'bool',
|
||||||
|
internalType: 'bool',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'labelValid',
|
||||||
|
type: 'bool',
|
||||||
|
internalType: 'bool',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'publicRegistrationOpen',
|
||||||
|
type: 'bool',
|
||||||
|
internalType: 'bool',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'owner',
|
||||||
|
type: 'address',
|
||||||
|
internalType: 'address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'expiry',
|
||||||
|
type: 'uint256',
|
||||||
|
internalType: 'uint256',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isPremium',
|
||||||
|
type: 'bool',
|
||||||
|
internalType: 'bool',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reservedAddress',
|
||||||
|
type: 'address',
|
||||||
|
internalType: 'address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'priceInDollars',
|
||||||
|
type: 'uint256',
|
||||||
|
internalType: 'uint256',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'priceInWei',
|
||||||
|
type: 'uint256',
|
||||||
|
internalType: 'uint256',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stateMutability: 'view',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stateMutability: 'payable',
|
||||||
|
type: 'function',
|
||||||
|
inputs: [
|
||||||
|
{ name: '_label', internalType: 'string', type: 'string' },
|
||||||
|
{ name: '_registrationLength', internalType: 'uint256', type: 'uint256' },
|
||||||
|
{ name: '_parentNamehash', internalType: 'bytes32', type: 'bytes32' },
|
||||||
|
{ name: '_recipient', internalType: 'address', type: 'address' },
|
||||||
|
{ name: 'v', internalType: 'uint8', type: 'uint8' },
|
||||||
|
{ name: 'r', internalType: 'bytes32', type: 'bytes32' },
|
||||||
|
{ name: 's', internalType: 'bytes32', type: 'bytes32' },
|
||||||
|
],
|
||||||
|
name: 'registerWithSignature',
|
||||||
|
outputs: [],
|
||||||
|
},
|
||||||
|
];
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
BIN
src/assets/check-mark.png
Normal file
BIN
src/assets/check-mark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 865 B |
BIN
src/assets/loading.png
Normal file
BIN
src/assets/loading.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 758 B |
24
src/components/Badge.jsx
Normal file
24
src/components/Badge.jsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import cn from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export const Badge = ({ children, variant, title }) => (
|
||||||
|
<div
|
||||||
|
title={title}
|
||||||
|
className={cn(
|
||||||
|
'px-3 py-2 bg-opacity-10 rounded-2xl justify-center items-center gap-2 flex text-xs font-semibold leading-none',
|
||||||
|
{
|
||||||
|
'bg-emerald-500 text-emerald-500': variant === 'success',
|
||||||
|
'bg-amber-500 text-amber-500': variant === 'premium',
|
||||||
|
'bg-neutral-500 text-neutral-500': variant === 'taken',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
Badge.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
variant: PropTypes.oneOf(['success', 'premium', 'taken']),
|
||||||
|
title: PropTypes.string,
|
||||||
|
};
|
27
src/components/Button.jsx
Normal file
27
src/components/Button.jsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import cn from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import icon from '../assets/loading.png';
|
||||||
|
|
||||||
|
export const Button = ({ children, onClick, loading }) => (
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'px-3 py-2 rounded-full justify-center flex',
|
||||||
|
'text-white text-sm font-semibold leading-none w-24 text-center',
|
||||||
|
loading ? 'bg-blue-400' : 'bg-blue-600'
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<img src={icon} alt="Loading" className="animate-spin w-4" />
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
Button.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
};
|
35
src/components/RegisterButton.jsx
Normal file
35
src/components/RegisterButton.jsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useRegister } from '../hooks/useRegister';
|
||||||
|
import { domainDetails } from '../types';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export const RegisterButton = ({ details }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const register = useRegister(details);
|
||||||
|
const client = useQueryClient();
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
setLoading(true);
|
||||||
|
register()
|
||||||
|
.then(() => {
|
||||||
|
toast.success('Domain registered');
|
||||||
|
client.refetchQueries();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.error(e.cause?.shortMessage || e.cause?.message);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={onClick} loading={loading}>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RegisterButton.propTypes = {
|
||||||
|
details: domainDetails,
|
||||||
|
};
|
78
src/components/search/Search.jsx
Normal file
78
src/components/search/Search.jsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useDebounce } from '@uidotdev/usehooks';
|
||||||
|
import { SearchInput } from './SearchInput';
|
||||||
|
import { SearchStatus } from './SearchStatus';
|
||||||
|
|
||||||
|
import { SearchCTA } from './SearchCTA';
|
||||||
|
import { SearchPrice } from './SearchPrice';
|
||||||
|
import { DEV_MODE, TLD } from '../../constants';
|
||||||
|
import { useDomainStatus } from '../../hooks/useDomainStatus';
|
||||||
|
|
||||||
|
export const Search = () => {
|
||||||
|
// "term" is the value from the input (first part if it contains a ".")
|
||||||
|
const [term, setTerm] = useState('');
|
||||||
|
|
||||||
|
// label is the same as term, but delayed 300ms (while the user is typing)
|
||||||
|
const label = useDebounce(term, 300);
|
||||||
|
|
||||||
|
// data contains all the info about the domain
|
||||||
|
const { data, failureReason } = useDomainStatus({ label });
|
||||||
|
const error = failureReason?.cause?.shortMessage;
|
||||||
|
|
||||||
|
// only _show_ the second line if we have a label
|
||||||
|
const show = !!label;
|
||||||
|
|
||||||
|
// if there's a "." (like "foo.bar"), we only use the first part (foo)
|
||||||
|
const onChange = (term) => setTerm(term.split('.').at(0));
|
||||||
|
|
||||||
|
// ensure the current data is related to the final search term
|
||||||
|
const safeData = term === data?.label ? data : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full bg-white rounded-2xl border border-zinc-300 shadow">
|
||||||
|
<SearchInput expand={show} onChange={onChange} />
|
||||||
|
{show && (
|
||||||
|
<div className="w-full px-5 pt-3 pb-4 bg-white flex-col justify-start items-start gap-4 inline-flex rounded-b-2xl">
|
||||||
|
<div className="self-stretch py-1 justify-between items-center inline-flex">
|
||||||
|
<div className="rounded justify-start items-center gap-2 flex">
|
||||||
|
<div>
|
||||||
|
<span className="text-neutral-950 text-xl font-medium leading-loose">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span className="text-neutral-400 text-xl font-medium leading-loose">
|
||||||
|
.{TLD}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchStatus details={safeData} />
|
||||||
|
</div>
|
||||||
|
<div className="justify-start items-center gap-4 flex">
|
||||||
|
<SearchPrice details={safeData} />
|
||||||
|
<SearchCTA details={safeData} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{DEV_MODE && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-red-600 font-semibold bg-yellow-300 px-4 py-2 rounded-md">
|
||||||
|
TESTNET
|
||||||
|
</span>
|
||||||
|
{error && (
|
||||||
|
<div className="p-2 text-red-600 bg-red-100 rounded-md">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data && (
|
||||||
|
<pre className="p-2 text-gray-500 bg-gray-100 border border-gray-500 rounded-lg">
|
||||||
|
{JSON.stringify(data, null, 2)}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
49
src/components/search/SearchCTA.jsx
Normal file
49
src/components/search/SearchCTA.jsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { ConnectButton } from '@rainbow-me/rainbowkit';
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
import { useAccount } from 'wagmi';
|
||||||
|
import { TLD } from '../../constants';
|
||||||
|
import { domainDetails } from '../../types';
|
||||||
|
import { RegisterButton } from '../RegisterButton';
|
||||||
|
|
||||||
|
export const SearchCTA = ({ details }) => {
|
||||||
|
const { address, isConnected } = useAccount();
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return <Skeleton className="w-24 h-7" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConnected && details.owner === address) {
|
||||||
|
const url = `https://hns.id/domain/${details.label}.${TLD}`;
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className=" px-3 py-2 bg-white rounded-full border border-blue-600 justify-center items-center gap-2.5 inline-flex"
|
||||||
|
href={url}
|
||||||
|
>
|
||||||
|
<div className="text-blue-600 text-sm font-semibold leading-none">
|
||||||
|
Manage Domain
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canRegister =
|
||||||
|
details.publicRegistrationOpen &&
|
||||||
|
details.priceInWei > 0n &&
|
||||||
|
details.labelValid &&
|
||||||
|
details.isAvailable &&
|
||||||
|
(!details.reservedAddress || details.reservedAddress === address);
|
||||||
|
|
||||||
|
if (!canRegister) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
return <ConnectButton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <RegisterButton details={details} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
SearchCTA.propTypes = {
|
||||||
|
details: domainDetails,
|
||||||
|
};
|
34
src/components/search/SearchInput.jsx
Normal file
34
src/components/search/SearchInput.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import searchIcon from '../../assets/search.png';
|
||||||
|
import { SEARCH_PLACEHOLDER } from '../../constants';
|
||||||
|
|
||||||
|
export const SearchInput = ({ expand, onChange }) => {
|
||||||
|
const handleChange = (e) => onChange(e.target.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full justify-between items-center inline-flex relative',
|
||||||
|
expand ? 'rounded-t-2xl' : 'rounded-2xl'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className={cn(
|
||||||
|
'grow shrink basis-0 rounded-t-2xl text-neutral-400 text-base font-medium leading-tight tracking-tight p-5 border-zinc-300 focus:outline-none',
|
||||||
|
expand ? 'rounded-t-2xl border-b' : 'rounded-2xl'
|
||||||
|
)}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={SEARCH_PLACEHOLDER}
|
||||||
|
/>
|
||||||
|
<div className="w-5 h-5 absolute right-4">
|
||||||
|
<img src={searchIcon} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SearchInput.propTypes = {
|
||||||
|
expand: PropTypes.bool,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
};
|
27
src/components/search/SearchPrice.jsx
Normal file
27
src/components/search/SearchPrice.jsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
import { domainDetails } from '../../types';
|
||||||
|
|
||||||
|
export const SearchPrice = ({ details }) => {
|
||||||
|
if (!details) {
|
||||||
|
return <Skeleton className="w-24 h-7" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!details.isAvailable || details.priceInWei === 0n) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-5 items-center gap-1">
|
||||||
|
<span className="text-neutral-900 text-sm font-bold leading-normal">
|
||||||
|
${details.priceInDollars}
|
||||||
|
</span>
|
||||||
|
<span className="text-neutral-400 text-xs font-bold leading-none">
|
||||||
|
/ year
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SearchPrice.propTypes = {
|
||||||
|
details: domainDetails,
|
||||||
|
};
|
51
src/components/search/SearchStatus.jsx
Normal file
51
src/components/search/SearchStatus.jsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { useAccount } from 'wagmi';
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
import { Badge } from '../Badge';
|
||||||
|
import { domainDetails } from '../../types';
|
||||||
|
|
||||||
|
import checkMark from '../../assets/check-mark.png';
|
||||||
|
|
||||||
|
export const SearchStatus = ({ details }) => {
|
||||||
|
const { address, isConnected } = useAccount();
|
||||||
|
|
||||||
|
if (!details) {
|
||||||
|
return <Skeleton className="w-20 h-7" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConnected && details.owner === address) {
|
||||||
|
return <img src={checkMark} className="w-6" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!details.isAvailable) {
|
||||||
|
return <Badge variant="taken">Taken</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!details.labelValid) {
|
||||||
|
return <Badge variant="taken">Invalid Domain</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!details.publicRegistrationOpen) {
|
||||||
|
return <Badge variant="taken">Coming Soon</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.reservedAddress && details.reservedAddress !== address) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="premium"
|
||||||
|
title={`Reserved for: ${details.reservedAddress}`}
|
||||||
|
>
|
||||||
|
Reserved
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.isPremium) {
|
||||||
|
return <Badge variant="premium">Premium</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Badge variant="success">Available</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
SearchStatus.propTypes = {
|
||||||
|
details: domainDetails,
|
||||||
|
};
|
23
src/constants.js
Normal file
23
src/constants.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export const DEV_MODE = location.hostname === 'localhost';
|
||||||
|
// export const DEV_MODE = false;
|
||||||
|
|
||||||
|
export const TLD = 'wallet';
|
||||||
|
|
||||||
|
export const HERO_TEXT = 'Own your .wallet';
|
||||||
|
export const SUB_TEXT = 'Decentralized domains for websites, wallets and web3';
|
||||||
|
export const SEARCH_PLACEHOLDER = 'Find your .wallet';
|
||||||
|
export const PAGE_TITLE = HERO_TEXT;
|
||||||
|
|
||||||
|
export const TWITTER_HANDLE = 'walletdomain';
|
||||||
|
|
||||||
|
// Check https://cloud.walletconnect.com/
|
||||||
|
export const WALLET_CONNECT_APP_NAME = 'Wallet.id';
|
||||||
|
export const WALLET_CONNECT_PROJECT_ID = 'b29f96b2f68f4ce904f96b3c225845c3';
|
||||||
|
|
||||||
|
export const STATUS_CONTRACT_ADDR = DEV_MODE
|
||||||
|
? '0x075489a52BcF5cd91c589046C3F5807e7fFC3647'
|
||||||
|
: '0xa89356391fB34e18360E79102536daD46F4a4199';
|
||||||
|
|
||||||
|
export const REGISTER_CONTRACT_ADDR = DEV_MODE
|
||||||
|
? '0x529B2b5B576c27769Ae0aB811F1655012f756C00'
|
||||||
|
: '0xfda87CC032cD641ac192027353e5B25261dfe6b3';
|
51
src/hooks/useDomainStatus.js
Normal file
51
src/hooks/useDomainStatus.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { namehash, zeroAddress } from 'viem';
|
||||||
|
import { useAccount, useReadContract } from 'wagmi';
|
||||||
|
import { abi } from '../abi';
|
||||||
|
import { STATUS_CONTRACT_ADDR, TLD } from '../constants';
|
||||||
|
|
||||||
|
export const useDomainStatus = ({
|
||||||
|
label,
|
||||||
|
buyer,
|
||||||
|
registrationDays = BigInt(365),
|
||||||
|
parentHash = namehash(TLD),
|
||||||
|
} = {}) => {
|
||||||
|
const { address } = useAccount();
|
||||||
|
|
||||||
|
return useReadContract({
|
||||||
|
address: STATUS_CONTRACT_ADDR,
|
||||||
|
functionName: 'getDomainDetails',
|
||||||
|
abi: abi,
|
||||||
|
args: [
|
||||||
|
buyer || address || zeroAddress,
|
||||||
|
registrationDays,
|
||||||
|
parentHash,
|
||||||
|
label,
|
||||||
|
],
|
||||||
|
query: {
|
||||||
|
enabled: !!label,
|
||||||
|
select: (data) => {
|
||||||
|
// remove 0x00
|
||||||
|
if (data.owner === zeroAddress) {
|
||||||
|
data.owner = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.reservedAddress === zeroAddress) {
|
||||||
|
data.reservedAddress = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.expiry === 0n) {
|
||||||
|
data.expiry = undefined;
|
||||||
|
} else {
|
||||||
|
data.expiry = new Date(Number(data.expiry) * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// price comes in cents: 100 = 1.00
|
||||||
|
data.priceInDollars = Number(data.priceInDollars / 100n).toFixed(2);
|
||||||
|
|
||||||
|
data.label = label;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
69
src/hooks/useRegister.js
Normal file
69
src/hooks/useRegister.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { useAccount, useSimulateContract, useWriteContract } from 'wagmi';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { namehash } from 'viem';
|
||||||
|
import { DEV_MODE, REGISTER_CONTRACT_ADDR, TLD } from '../constants';
|
||||||
|
import { abi } from '../abi';
|
||||||
|
|
||||||
|
const HashZero =
|
||||||
|
'0x0000000000000000000000000000000000000000000000000000000000000000';
|
||||||
|
|
||||||
|
const fetchSignature = async (buyer, subdomainHash, nonce) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
buyer,
|
||||||
|
subdomainHash,
|
||||||
|
nonce: nonce.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const host = DEV_MODE ? 'hnst.id' : 'hns.id';
|
||||||
|
|
||||||
|
return fetch(
|
||||||
|
`https://${host}/api/gateway/registration?${params.toString()}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
origin: window.location.origin,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => data.signature);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRegister = ({
|
||||||
|
label,
|
||||||
|
priceInWei,
|
||||||
|
years = 1,
|
||||||
|
nonce = 0,
|
||||||
|
} = {}) => {
|
||||||
|
const tldHash = namehash(TLD);
|
||||||
|
const nameHash = namehash(`${label}.${TLD}`);
|
||||||
|
const registrationDays = BigInt(years * 365);
|
||||||
|
|
||||||
|
const { address, isConnected } = useAccount();
|
||||||
|
|
||||||
|
const { data: signature } = useQuery({
|
||||||
|
queryKey: ['signature', address, nameHash, nonce.toString()],
|
||||||
|
queryFn: () => fetchSignature(address, nameHash, nonce),
|
||||||
|
enabled: isConnected,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = useSimulateContract({
|
||||||
|
abi,
|
||||||
|
address: REGISTER_CONTRACT_ADDR,
|
||||||
|
functionName: 'registerWithSignature',
|
||||||
|
account: address,
|
||||||
|
value: priceInWei,
|
||||||
|
enabled: !!signature && !!priceInWei && !!address,
|
||||||
|
args: [
|
||||||
|
label,
|
||||||
|
registrationDays,
|
||||||
|
tldHash,
|
||||||
|
address,
|
||||||
|
signature?.v ?? 0,
|
||||||
|
signature?.r ?? HashZero,
|
||||||
|
signature?.s ?? HashZero,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { writeContractAsync } = useWriteContract();
|
||||||
|
return () => writeContractAsync(data?.request);
|
||||||
|
};
|
25
src/main.jsx
25
src/main.jsx
@ -2,29 +2,42 @@ import { getDefaultConfig, RainbowKitProvider } from '@rainbow-me/rainbowkit';
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { ToastContainer } from 'react-toastify';
|
||||||
import { WagmiProvider } from 'wagmi';
|
import { WagmiProvider } from 'wagmi';
|
||||||
import { optimism } from 'wagmi/chains';
|
import { optimism, optimismSepolia } from 'wagmi/chains';
|
||||||
|
|
||||||
import App from './App.jsx';
|
import App from './App.jsx';
|
||||||
|
import {
|
||||||
|
DEV_MODE,
|
||||||
|
WALLET_CONNECT_APP_NAME,
|
||||||
|
WALLET_CONNECT_PROJECT_ID,
|
||||||
|
} from './constants.js';
|
||||||
|
import { CustomAvatar } from './CustomAvatar.jsx';
|
||||||
|
|
||||||
import '@rainbow-me/rainbowkit/styles.css';
|
import '@rainbow-me/rainbowkit/styles.css';
|
||||||
|
import 'react-loading-skeleton/dist/skeleton.css';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import { CustomAvatar } from './CustomAvatar.jsx';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const config = getDefaultConfig({
|
const config = getDefaultConfig({
|
||||||
appName: 'Wallet.id',
|
appName: WALLET_CONNECT_APP_NAME,
|
||||||
projectId: 'YOUR_PROJECT_ID',
|
projectId: WALLET_CONNECT_PROJECT_ID,
|
||||||
chains: [optimism],
|
chains: [DEV_MODE ? optimismSepolia : optimism],
|
||||||
ssr: true, // If your dApp uses server side rendering (SSR)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
BigInt.prototype.toJSON = function () {
|
||||||
|
return this.toString();
|
||||||
|
};
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<WagmiProvider config={config}>
|
<WagmiProvider config={config}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RainbowKitProvider avatar={CustomAvatar}>
|
<RainbowKitProvider avatar={CustomAvatar}>
|
||||||
<App />
|
<App />
|
||||||
|
<ToastContainer />
|
||||||
</RainbowKitProvider>
|
</RainbowKitProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</WagmiProvider>
|
</WagmiProvider>
|
||||||
|
14
src/types.js
Normal file
14
src/types.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export const domainDetails = PropTypes.shape({
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
|
labelValid: PropTypes.bool.isRequired,
|
||||||
|
publicRegistrationOpen: PropTypes.bool.isRequired,
|
||||||
|
owner: PropTypes.string,
|
||||||
|
expiry: PropTypes.instanceOf(Date),
|
||||||
|
isPremium: PropTypes.bool.isRequired,
|
||||||
|
reservedAddress: PropTypes.string,
|
||||||
|
priceInDollars: PropTypes.string.isRequired,
|
||||||
|
priceInWei: PropTypes.bigint.isRequired,
|
||||||
|
});
|
52
yarn.lock
52
yarn.lock
@ -1454,6 +1454,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||||
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
||||||
|
|
||||||
|
"@uidotdev/usehooks@^2.4.1":
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@uidotdev/usehooks/-/usehooks-2.4.1.tgz#4b733eaeae09a7be143c6c9ca158b56cc1ea75bf"
|
||||||
|
integrity sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==
|
||||||
|
|
||||||
"@ungap/structured-clone@^1.2.0":
|
"@ungap/structured-clone@^1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
|
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
|
||||||
@ -2314,6 +2319,11 @@ citty@^0.1.5, citty@^0.1.6:
|
|||||||
dependencies:
|
dependencies:
|
||||||
consola "^3.2.3"
|
consola "^3.2.3"
|
||||||
|
|
||||||
|
classnames@^2.5.1:
|
||||||
|
version "2.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
|
||||||
|
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
||||||
|
|
||||||
clipboardy@^4.0.0:
|
clipboardy@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-4.0.0.tgz#e73ced93a76d19dd379ebf1f297565426dffdca1"
|
resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-4.0.0.tgz#e73ced93a76d19dd379ebf1f297565426dffdca1"
|
||||||
@ -2350,7 +2360,7 @@ clone-deep@^4.0.1:
|
|||||||
kind-of "^6.0.2"
|
kind-of "^6.0.2"
|
||||||
shallow-clone "^3.0.0"
|
shallow-clone "^3.0.0"
|
||||||
|
|
||||||
clsx@2.1.0:
|
clsx@2.1.0, clsx@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
|
||||||
integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
|
integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
|
||||||
@ -4897,6 +4907,11 @@ react-is@^16.13.1, react-is@^16.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
|
react-loading-skeleton@^3.4.0:
|
||||||
|
version "3.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-loading-skeleton/-/react-loading-skeleton-3.4.0.tgz#c71a3a17259d08e4064974aa0b07f150a09dfd57"
|
||||||
|
integrity sha512-1oJEBc9+wn7BbkQQk7YodlYEIjgeR+GrRjD+QXkVjwZN7LGIcAFHrx4NhT7UHGBxNY1+zax3c+Fo6XQM4R7CgA==
|
||||||
|
|
||||||
react-native-webview@^11.26.0:
|
react-native-webview@^11.26.0:
|
||||||
version "11.26.1"
|
version "11.26.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-11.26.1.tgz#658c09ed5162dc170b361e48c2dd26c9712879da"
|
resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-11.26.1.tgz#658c09ed5162dc170b361e48c2dd26c9712879da"
|
||||||
@ -4938,6 +4953,13 @@ react-style-singleton@^2.2.1:
|
|||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
|
react-toastify@^10.0.5:
|
||||||
|
version "10.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-10.0.5.tgz#6b8f8386060c5c856239f3036d1e76874ce3bd1e"
|
||||||
|
integrity sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==
|
||||||
|
dependencies:
|
||||||
|
clsx "^2.1.0"
|
||||||
|
|
||||||
react@^18.2.0:
|
react@^18.2.0:
|
||||||
version "18.2.0"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
@ -5848,20 +5870,6 @@ valtio@1.11.2:
|
|||||||
proxy-compare "2.5.1"
|
proxy-compare "2.5.1"
|
||||||
use-sync-external-store "1.2.0"
|
use-sync-external-store "1.2.0"
|
||||||
|
|
||||||
viem@2.x:
|
|
||||||
version "2.9.21"
|
|
||||||
resolved "https://registry.yarnpkg.com/viem/-/viem-2.9.21.tgz#a7dd3d4c827088e5336e5a6b35ec0283d2938595"
|
|
||||||
integrity sha512-8GtxPjPGpiN5cmr19zSX9mb1LX/eON3MPxxAd3QmyUFn69Rp566zlREOqE7zM35y5yX59fXwnz6O3X7e9+C9zg==
|
|
||||||
dependencies:
|
|
||||||
"@adraffy/ens-normalize" "1.10.0"
|
|
||||||
"@noble/curves" "1.2.0"
|
|
||||||
"@noble/hashes" "1.3.2"
|
|
||||||
"@scure/bip32" "1.3.2"
|
|
||||||
"@scure/bip39" "1.2.1"
|
|
||||||
abitype "1.0.0"
|
|
||||||
isows "1.0.3"
|
|
||||||
ws "8.13.0"
|
|
||||||
|
|
||||||
viem@^1.0.0, viem@^1.1.4:
|
viem@^1.0.0, viem@^1.1.4:
|
||||||
version "1.21.4"
|
version "1.21.4"
|
||||||
resolved "https://registry.yarnpkg.com/viem/-/viem-1.21.4.tgz#883760e9222540a5a7e0339809202b45fe6a842d"
|
resolved "https://registry.yarnpkg.com/viem/-/viem-1.21.4.tgz#883760e9222540a5a7e0339809202b45fe6a842d"
|
||||||
@ -5876,6 +5884,20 @@ viem@^1.0.0, viem@^1.1.4:
|
|||||||
isows "1.0.3"
|
isows "1.0.3"
|
||||||
ws "8.13.0"
|
ws "8.13.0"
|
||||||
|
|
||||||
|
viem@^2.9.25:
|
||||||
|
version "2.9.25"
|
||||||
|
resolved "https://registry.yarnpkg.com/viem/-/viem-2.9.25.tgz#afcf320790e175b2afc83d29819f56cb50906f0d"
|
||||||
|
integrity sha512-W0QOXCsYQppnV89PQP0EnCvfZIEsDYqmpVakLPNrok4Q4B7651M3MV/sYifYcLWv3Mn4KUyMCUlVxlej6CfC/w==
|
||||||
|
dependencies:
|
||||||
|
"@adraffy/ens-normalize" "1.10.0"
|
||||||
|
"@noble/curves" "1.2.0"
|
||||||
|
"@noble/hashes" "1.3.2"
|
||||||
|
"@scure/bip32" "1.3.2"
|
||||||
|
"@scure/bip39" "1.2.1"
|
||||||
|
abitype "1.0.0"
|
||||||
|
isows "1.0.3"
|
||||||
|
ws "8.13.0"
|
||||||
|
|
||||||
vite@^5.2.0:
|
vite@^5.2.0:
|
||||||
version "5.2.9"
|
version "5.2.9"
|
||||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.9.tgz#cd9a356c6ff5f7456c09c5ce74068ffa8df743d9"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.9.tgz#cd9a356c6ff5f7456c09c5ce74068ffa8df743d9"
|
||||||
|
Loading…
Reference in New Issue
Block a user