Twitter
@@ -53,7 +54,7 @@ function App() {
Opensea
diff --git a/src/abi.js b/src/abi.js
new file mode 100644
index 0000000..32a968d
--- /dev/null
+++ b/src/abi.js
@@ -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: [],
+ },
+];
diff --git a/src/assets/wallet.id.png b/src/assets/brand.png
similarity index 100%
rename from src/assets/wallet.id.png
rename to src/assets/brand.png
diff --git a/src/assets/check-mark.png b/src/assets/check-mark.png
new file mode 100644
index 0000000..7b2be67
Binary files /dev/null and b/src/assets/check-mark.png differ
diff --git a/src/assets/loading.png b/src/assets/loading.png
new file mode 100644
index 0000000..b8bfbfa
Binary files /dev/null and b/src/assets/loading.png differ
diff --git a/src/components/Badge.jsx b/src/components/Badge.jsx
new file mode 100644
index 0000000..affc255
--- /dev/null
+++ b/src/components/Badge.jsx
@@ -0,0 +1,24 @@
+import cn from 'classnames';
+import PropTypes from 'prop-types';
+
+export const Badge = ({ children, variant, title }) => (
+
+ {children}
+
+);
+
+Badge.propTypes = {
+ children: PropTypes.node,
+ variant: PropTypes.oneOf(['success', 'premium', 'taken']),
+ title: PropTypes.string,
+};
diff --git a/src/components/Button.jsx b/src/components/Button.jsx
new file mode 100644
index 0000000..b57bd52
--- /dev/null
+++ b/src/components/Button.jsx
@@ -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.propTypes = {
+ children: PropTypes.node,
+ onClick: PropTypes.func,
+ loading: PropTypes.bool,
+};
diff --git a/src/components/RegisterButton.jsx b/src/components/RegisterButton.jsx
new file mode 100644
index 0000000..41da596
--- /dev/null
+++ b/src/components/RegisterButton.jsx
@@ -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 (
+
+ );
+};
+
+RegisterButton.propTypes = {
+ details: domainDetails,
+};
diff --git a/src/components/search/Search.jsx b/src/components/search/Search.jsx
new file mode 100644
index 0000000..2092aed
--- /dev/null
+++ b/src/components/search/Search.jsx
@@ -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 (
+ <>
+
+
+ {show && (
+
+
+
+
+
+ {label}
+
+
+ .{TLD}
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ {DEV_MODE && (
+
+
+ TESTNET
+
+ {error && (
+
+ {error}
+
+ )}
+ {data && (
+
+ {JSON.stringify(data, null, 2)}
+
+ )}
+
+ )}
+ >
+ );
+};
diff --git a/src/components/search/SearchCTA.jsx b/src/components/search/SearchCTA.jsx
new file mode 100644
index 0000000..ce243f1
--- /dev/null
+++ b/src/components/search/SearchCTA.jsx
@@ -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
;
+ }
+
+ if (isConnected && details.owner === address) {
+ const url = `https://hns.id/domain/${details.label}.${TLD}`;
+ return (
+
+
+ Manage Domain
+
+
+ );
+ }
+
+ const canRegister =
+ details.publicRegistrationOpen &&
+ details.priceInWei > 0n &&
+ details.labelValid &&
+ details.isAvailable &&
+ (!details.reservedAddress || details.reservedAddress === address);
+
+ if (!canRegister) {
+ return null;
+ }
+
+ if (!isConnected) {
+ return
;
+ }
+
+ return
;
+};
+
+SearchCTA.propTypes = {
+ details: domainDetails,
+};
diff --git a/src/components/search/SearchInput.jsx b/src/components/search/SearchInput.jsx
new file mode 100644
index 0000000..ebc463a
--- /dev/null
+++ b/src/components/search/SearchInput.jsx
@@ -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 (
+
+
+
+
+
+
+ );
+};
+
+SearchInput.propTypes = {
+ expand: PropTypes.bool,
+ onChange: PropTypes.func,
+};
diff --git a/src/components/search/SearchPrice.jsx b/src/components/search/SearchPrice.jsx
new file mode 100644
index 0000000..5c17360
--- /dev/null
+++ b/src/components/search/SearchPrice.jsx
@@ -0,0 +1,27 @@
+import Skeleton from 'react-loading-skeleton';
+import { domainDetails } from '../../types';
+
+export const SearchPrice = ({ details }) => {
+ if (!details) {
+ return
;
+ }
+
+ if (!details.isAvailable || details.priceInWei === 0n) {
+ return null;
+ }
+
+ return (
+
+
+ ${details.priceInDollars}
+
+
+ / year
+
+
+ );
+};
+
+SearchPrice.propTypes = {
+ details: domainDetails,
+};
diff --git a/src/components/search/SearchStatus.jsx b/src/components/search/SearchStatus.jsx
new file mode 100644
index 0000000..772cc83
--- /dev/null
+++ b/src/components/search/SearchStatus.jsx
@@ -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
;
+ }
+
+ if (isConnected && details.owner === address) {
+ return
;
+ }
+
+ if (!details.isAvailable) {
+ return
Taken;
+ }
+
+ if (!details.labelValid) {
+ return
Invalid Domain;
+ }
+
+ if (!details.publicRegistrationOpen) {
+ return
Coming Soon;
+ }
+
+ if (details.reservedAddress && details.reservedAddress !== address) {
+ return (
+
+ Reserved
+
+ );
+ }
+
+ if (details.isPremium) {
+ return
Premium;
+ }
+
+ return
Available;
+};
+
+SearchStatus.propTypes = {
+ details: domainDetails,
+};
diff --git a/src/constants.js b/src/constants.js
new file mode 100644
index 0000000..094e246
--- /dev/null
+++ b/src/constants.js
@@ -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';
diff --git a/src/hooks/useDomainStatus.js b/src/hooks/useDomainStatus.js
new file mode 100644
index 0000000..b9bac8a
--- /dev/null
+++ b/src/hooks/useDomainStatus.js
@@ -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;
+ },
+ },
+ });
+};
diff --git a/src/hooks/useRegister.js b/src/hooks/useRegister.js
new file mode 100644
index 0000000..d21af68
--- /dev/null
+++ b/src/hooks/useRegister.js
@@ -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);
+};
diff --git a/src/main.jsx b/src/main.jsx
index 185b256..243ff51 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -2,29 +2,42 @@ import { getDefaultConfig, RainbowKitProvider } from '@rainbow-me/rainbowkit';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import ReactDOM from 'react-dom/client';
+import { ToastContainer } from 'react-toastify';
import { WagmiProvider } from 'wagmi';
-import { optimism } from 'wagmi/chains';
+import { optimism, optimismSepolia } from 'wagmi/chains';
+
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 'react-loading-skeleton/dist/skeleton.css';
+import 'react-toastify/dist/ReactToastify.css';
import './index.css';
-import { CustomAvatar } from './CustomAvatar.jsx';
const queryClient = new QueryClient();
const config = getDefaultConfig({
- appName: 'Wallet.id',
- projectId: 'YOUR_PROJECT_ID',
- chains: [optimism],
- ssr: true, // If your dApp uses server side rendering (SSR)
+ appName: WALLET_CONNECT_APP_NAME,
+ projectId: WALLET_CONNECT_PROJECT_ID,
+ chains: [DEV_MODE ? optimismSepolia : optimism],
});
+BigInt.prototype.toJSON = function () {
+ return this.toString();
+};
+
ReactDOM.createRoot(document.getElementById('root')).render(
+
diff --git a/src/types.js b/src/types.js
new file mode 100644
index 0000000..542f02f
--- /dev/null
+++ b/src/types.js
@@ -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,
+});
diff --git a/yarn.lock b/yarn.lock
index 92fe633..cc9ef4f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1454,6 +1454,11 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
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":
version "1.2.0"
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:
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:
version "4.0.0"
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"
shallow-clone "^3.0.0"
-clsx@2.1.0:
+clsx@2.1.0, clsx@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
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"
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:
version "11.26.1"
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"
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:
version "18.2.0"
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"
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:
version "1.21.4"
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"
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:
version "5.2.9"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.9.tgz#cd9a356c6ff5f7456c09c5ce74068ffa8df743d9"