From ac6095e2b278ae1f958aa9cd892baf9b931bfbaa Mon Sep 17 00:00:00 2001 From: Fernando Falci Date: Mon, 22 Apr 2024 11:42:54 +0200 Subject: [PATCH] chore: register domain --- package.json | 6 +- src/App.jsx | 33 ++++---- src/abi.js | 98 ++++++++++++++++++++++++ src/assets/{wallet.id.png => brand.png} | Bin src/assets/check-mark.png | Bin 0 -> 865 bytes src/assets/loading.png | Bin 0 -> 758 bytes src/components/Badge.jsx | 24 ++++++ src/components/Button.jsx | 27 +++++++ src/components/RegisterButton.jsx | 35 +++++++++ src/components/search/Search.jsx | 78 +++++++++++++++++++ src/components/search/SearchCTA.jsx | 49 ++++++++++++ src/components/search/SearchInput.jsx | 34 ++++++++ src/components/search/SearchPrice.jsx | 27 +++++++ src/components/search/SearchStatus.jsx | 51 ++++++++++++ src/constants.js | 23 ++++++ src/hooks/useDomainStatus.js | 51 ++++++++++++ src/hooks/useRegister.js | 69 +++++++++++++++++ src/main.jsx | 25 ++++-- src/types.js | 14 ++++ yarn.lock | 52 +++++++++---- 20 files changed, 658 insertions(+), 38 deletions(-) create mode 100644 src/abi.js rename src/assets/{wallet.id.png => brand.png} (100%) create mode 100644 src/assets/check-mark.png create mode 100644 src/assets/loading.png create mode 100644 src/components/Badge.jsx create mode 100644 src/components/Button.jsx create mode 100644 src/components/RegisterButton.jsx create mode 100644 src/components/search/Search.jsx create mode 100644 src/components/search/SearchCTA.jsx create mode 100644 src/components/search/SearchInput.jsx create mode 100644 src/components/search/SearchPrice.jsx create mode 100644 src/components/search/SearchStatus.jsx create mode 100644 src/constants.js create mode 100644 src/hooks/useDomainStatus.js create mode 100644 src/hooks/useRegister.js create mode 100644 src/types.js diff --git a/package.json b/package.json index 07b227e..aa4e373 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,13 @@ "dependencies": { "@rainbow-me/rainbowkit": "^2.0.5", "@tanstack/react-query": "^5.29.2", + "@uidotdev/usehooks": "^2.4.1", + "classnames": "^2.5.1", "react": "^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" }, "devDependencies": { diff --git a/src/App.jsx b/src/App.jsx index f107311..5062084 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,15 +1,24 @@ import { ConnectButton } from '@rainbow-me/rainbowkit'; -import walletLogo from './assets/wallet.id.png'; -import searchIcon from './assets/search.png'; +import brandLogo from './assets/brand.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() { + useDocumentTitle(PAGE_TITLE); return (
- +
@@ -18,22 +27,14 @@ function App() {
- Own your .wallet + {HERO_TEXT}
- Decentralized domains for websites, wallets and web3 + {SUB_TEXT}
-
- -
- -
-
+
@@ -41,7 +42,7 @@ function App() {
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 0000000000000000000000000000000000000000..7b2be676c20bd4dcba5c9f0141ca7c9807da753e GIT binary patch literal 865 zcmV-n1D^beP)>Tgq( zL@0#dmZOu9eTbrFBxY-Rdi4kqi6nUAdo@dJJf^2=z$qF%O7*$5B0>^MaOqn$D++i< zPZdN%r$-@-XdprMQ_5zujNlRTqljd(P*300{Ra99E;n8)FwF|0M~#P0B{P1KLj}hu z*TYwmD>wnU9=egVR`qnoGt))xG}Fvu{BjIMHl;GJ(^GKyu)0A%HZVCKrlTu01g<$09TxAA25`p|LRr;`Im7OylaDZ`W+h*|FG zg{9N$N5>!W^3MN(Yb9C5i_I6c@au*>*5l2FW=*4W889~1Q#S3x#{`N z`Gc9O*m8-CLvBD&5QQ3V;IZAtf&D)Nb5y7&8LJ#C75YdxIt9A@A>Q+vq>c5X4=AN> zVnLKoCE59%sY~(5F=!p*xa_}}mMm@9+ylCXG0FZ&#wtfK673pSuy(S7YX~VBmmIsd zdtW$^B!Y--_~0cOM?z3a?gq~t_447XJ=&$f2bS5{={dYLf8q^2MuX_}x%-RmR?lUI zX7eTjz#_Rrqc*x+=dK0MR^D*BFJ$$IipfxuNC#@U{JH`&Zhh-IbygnD^& zQc#h6^k@>@Cc@^Y`L!28XGD7a)~)FqS*x7VZ?fj_5yRSl9Fd>9m7fmLAd7l rVgc{j=HqwM()7hl==)!YposZZXi7|A#t-2MlPsu& z7;oFO2M$BM8*ePaea6oe4ZvVqMK?4i1`fe324=Mexgd6KItoHOV6NF>0j^V;-NV+} z9v<#h1!B$vlrDwcv^i}XtP{)jeNE!Qlaw9qZ|k79Qgah?yrDc*+=b{mD<1&!?UYjq z^Jfg~QI*78qm;Lb&(vPG-T4ljPMF*2zNhMISYi94+p+&&0Fu~AJ?_j6+n*^_S#Hm5 zJECjdi1&J1^&y3;k9eW<^c9vApNBRxJfd*r_IZS)Kb4YP7seHDLzBI+S`q;Ul)ipD zb5p$q*r)g~jwyX*5&`PW*nU={4x}#kdk}SDwAPvDzn2FQB7e|r)mgc&T_B&eH{NhcUYQ zw-s-Lb>Xsni&b~SJ9rNt;eve1+76GM`6!IRMfntl9q0hcvvxQOci@8ya7`8JFJ*&u_pUL}e*gdg07*qoM6N<$g7sHXj{pDw literal 0 HcmV?d00001 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"