diff --git a/web/package.json b/web/package.json index 93705b0..818bd1f 100644 --- a/web/package.json +++ b/web/package.json @@ -79,6 +79,7 @@ "@web3modal/ethereum": "^2.7.1", "@web3modal/react": "^2.2.2", "@yornaath/batshit": "^0.9.0", + "alchemy-sdk": "^3.3.1", "amqplib": "^0.10.3", "chart.js": "^3.9.1", "chartjs-adapter-moment": "^1.0.1", diff --git a/web/src/assets/svgs/icons/eth-token-icon.png b/web/src/assets/svgs/icons/eth-token-icon.png new file mode 100644 index 0000000..5366d8a Binary files /dev/null and b/web/src/assets/svgs/icons/eth-token-icon.png differ diff --git a/web/src/components/PreviewCard/EscrowTimeline/index.tsx b/web/src/components/PreviewCard/EscrowTimeline/index.tsx index db28977..4ec4a5a 100644 --- a/web/src/components/PreviewCard/EscrowTimeline/index.tsx +++ b/web/src/components/PreviewCard/EscrowTimeline/index.tsx @@ -12,7 +12,7 @@ interface IEscrowTimeline { isPreview: boolean; transactionCreationTimestamp: number; status: boolean; - token: string; + assetSymbol: string; buyerAddress: string; sellerAddress: string; payments: Payment[]; @@ -26,7 +26,7 @@ const EscrowTimeline: React.FC = ({ isPreview, transactionCreationTimestamp, status, - token, + assetSymbol, buyerAddress, sellerAddress, payments, @@ -41,7 +41,7 @@ const EscrowTimeline: React.FC = ({ isPreview, transactionCreationTimestamp, status, - token, + assetSymbol, buyerAddress, sellerAddress, payments, diff --git a/web/src/components/PreviewCard/Terms/Description.tsx b/web/src/components/PreviewCard/Terms/Description.tsx index d8d4723..208a386 100644 --- a/web/src/components/PreviewCard/Terms/Description.tsx +++ b/web/src/components/PreviewCard/Terms/Description.tsx @@ -8,6 +8,10 @@ const StyledP = styled.p` word-break: break-word; `; +const InlineBlockSpan = styled.span` + display: inline-block; +`; + interface IDescription { escrowType: string; deliverableText: string; @@ -17,7 +21,7 @@ interface IDescription { sendingQuantity: string; sendingToken: string; sellerAddress: string; - deadlineDate: Date; + deadline: number; assetSymbol: string; buyer: string; } @@ -31,19 +35,25 @@ const Description: React.FC = ({ sendingQuantity, sendingToken, sellerAddress, - deadlineDate, + deadline, assetSymbol, }) => { - const generalEscrowSummary = - `By Paying ${sendingQuantity + " " + assetSymbol}, address ${buyerAddress} should receive` + - ` "${deliverableText}" from address ${sellerAddress} before the delivery deadline ${new Date( - deadlineDate - )}.`; + const generalEscrowSummary = ( + <> + By Paying {sendingQuantity}{" "} + {assetSymbol ? assetSymbol : }, address{" "} + {buyerAddress} should receive "{deliverableText}" from address {sellerAddress} before the delivery deadline{" "} + {new Date(deadline).toString()}. + + ); - const cryptoSwapSummary = - `By Paying ${sendingQuantity + " " + sendingToken}, [Blockchain] address ${buyerAddress} should receive` + - ` ${receivingQuantity + " " + receivingToken} at the [Blockchain] address ${sellerAddress}` + - ` from [Blockchain] address TODO before the delivery deadline ${deadlineDate}.`; + const cryptoSwapSummary = ( + <> + By Paying {sendingQuantity} {sendingToken}, [Blockchain] address {buyerAddress} should receive {receivingQuantity}{" "} + {receivingToken} at the [Blockchain] address {sellerAddress} from [Blockchain] address TODO before the delivery + deadline {new Date(deadline).toString()}. + + ); return isUndefined(deliverableText) ? ( @@ -58,4 +68,5 @@ const Description: React.FC = ({ ); }; + export default Description; diff --git a/web/src/components/PreviewCard/Terms/index.tsx b/web/src/components/PreviewCard/Terms/index.tsx index b407eaf..590170b 100644 --- a/web/src/components/PreviewCard/Terms/index.tsx +++ b/web/src/components/PreviewCard/Terms/index.tsx @@ -19,7 +19,7 @@ interface ITerms { sendingQuantity: string; sendingToken: string; sellerAddress: string; - deadlineDate: Date; + deadline: number; assetSymbol: string; extraDescriptionUri: string; buyer: string; @@ -29,12 +29,10 @@ const Terms: React.FC = ({ escrowType, deliverableText, receivingQuantity, - receivingToken, buyerAddress, sendingQuantity, - sendingToken, sellerAddress, - deadlineDate, + deadline, assetSymbol, extraDescriptionUri, }) => { @@ -42,18 +40,18 @@ const Terms: React.FC = ({
- + ); }; diff --git a/web/src/components/PreviewCard/index.tsx b/web/src/components/PreviewCard/index.tsx index 2885d56..27ddc3f 100644 --- a/web/src/components/PreviewCard/index.tsx +++ b/web/src/components/PreviewCard/index.tsx @@ -52,15 +52,12 @@ interface IPreviewCard { escrowTitle: string; deliverableText: string; receivingQuantity: string; - receivingToken: string; transactionCreationTimestamp: string; status: string; - token: string; buyerAddress: string; sendingQuantity: string; - sendingToken: string; sellerAddress: string; - deadlineDate: string; + deadline: number; assetSymbol: string; overrideIsList: boolean; extraDescriptionUri: string; @@ -80,15 +77,12 @@ const PreviewCard: React.FC = ({ escrowTitle, deliverableText, receivingQuantity, - receivingToken, transactionCreationTimestamp, status, - token, buyerAddress, sendingQuantity, - sendingToken, sellerAddress, - deadlineDate, + deadline, assetSymbol, overrideIsList, extraDescriptionUri, @@ -108,9 +102,8 @@ const PreviewCard: React.FC = ({ @@ -119,12 +112,10 @@ const PreviewCard: React.FC = ({ escrowType, deliverableText, receivingQuantity, - receivingToken, buyerAddress, sendingQuantity, - sendingToken, sellerAddress, - deadlineDate, + deadline, assetSymbol, extraDescriptionUri, }} @@ -134,7 +125,7 @@ const PreviewCard: React.FC = ({ {...{ isPreview, status, - token, + assetSymbol, transactionCreationTimestamp, buyerAddress, sellerAddress, diff --git a/web/src/components/TransactionCard/index.tsx b/web/src/components/TransactionCard/index.tsx index a02329b..e11d81b 100644 --- a/web/src/components/TransactionCard/index.tsx +++ b/web/src/components/TransactionCard/index.tsx @@ -1,18 +1,19 @@ import React from "react"; import styled from "styled-components"; -import { formatEther } from "viem"; +import { responsiveSize } from "styles/responsiveSize"; import { Card } from "@kleros/ui-components-library"; +import { formatEther } from "viem"; import { useIsList } from "context/IsListProvider"; -import TransactionInfo from "../TransactionInfo"; -import StatusBanner from "./StatusBanner"; -import { responsiveSize } from "styles/responsiveSize"; import { mapStatusToEnum } from "utils/mapStatusToEnum"; import { isUndefined } from "utils/index"; +import { StyledSkeleton, StyledSkeletonTitle } from "../StyledSkeleton"; +import TransactionInfo from "../TransactionInfo"; +import StatusBanner from "./StatusBanner"; +import { useNavigateAndScrollTop } from "hooks/useNavigateAndScrollTop"; import { useNativeTokenSymbol } from "hooks/useNativeTokenSymbol"; import useFetchIpfsJson from "hooks/useFetchIpfsJson"; -import { useNavigateAndScrollTop } from "hooks/useNavigateAndScrollTop"; +import { useTokenMetadata } from "hooks/useTokenMetadata"; import { TransactionDetailsFragment } from "src/graphql/graphql"; -import { StyledSkeleton, StyledSkeletonTitle } from "../StyledSkeleton"; const StyledCard = styled(Card)` width: 100%; @@ -69,6 +70,8 @@ const TransactionCard: React.FC = ({ const transactionInfo = useFetchIpfsJson(transactionUri); const { isList } = useIsList(); const nativeTokenSymbol = useNativeTokenSymbol(); + const { tokenMetadata } = useTokenMetadata(token); + const erc20TokenSymbol = tokenMetadata?.symbol; const title = transactionInfo?.title; const navigateAndScrollTop = useNavigateAndScrollTop(); @@ -83,7 +86,7 @@ const TransactionCard: React.FC = ({ {!isUndefined(title) ? {title} : } = ({ )} = ({ amount, assetSymbol, - deadlineDate, + deadline, sellerAddress, buyerAddress, overrideIsList, @@ -94,20 +95,24 @@ const TransactionInfo: React.FC = ({ return ( - {amount && assetSymbol ? ( + {amount ? ( + {amount} {!assetSymbol ? : assetSymbol} + + } displayAsList={displayAsList} isPreview={isPreview} /> ) : null} - {deadlineDate ? ( + {deadline ? ( @@ -134,4 +139,5 @@ const TransactionInfo: React.FC = ({ ); }; + export default TransactionInfo; diff --git a/web/src/context/NewTransactionContext.tsx b/web/src/context/NewTransactionContext.tsx index 41dd623..81ae11d 100644 --- a/web/src/context/NewTransactionContext.tsx +++ b/web/src/context/NewTransactionContext.tsx @@ -1,5 +1,11 @@ import React, { createContext, useState, useContext, useEffect } from "react"; +export interface IToken { + symbol: string; + address: string; + logo: string; +} + interface INewTransactionContext { escrowType: string; setEscrowType: (type: string) => void; @@ -23,8 +29,8 @@ interface INewTransactionContext { setSellerAddress: (address: string) => void; sendingQuantity: string; setSendingQuantity: (quantity: string) => void; - sendingToken: string; - setSendingToken: (token: string) => void; + sendingToken: IToken; + setSendingToken: (token: IToken) => void; buyerAddress: string; setBuyerAddress: (address: string) => void; deadline: string; @@ -61,7 +67,7 @@ const NewTransactionContext = createContext({ setSellerAddress: () => {}, sendingQuantity: "", setSendingQuantity: () => {}, - sendingToken: "", + sendingToken: { address: "native", symbol: "", logo: "" }, setSendingToken: () => {}, buyerAddress: "", setBuyerAddress: () => {}, @@ -93,7 +99,9 @@ export const NewTransactionProvider: React.FC<{ children: React.ReactNode }> = ( const [receivingToken, setReceivingToken] = useState(localStorage.getItem("receivingToken") || ""); const [sellerAddress, setSellerAddress] = useState(localStorage.getItem("sellerAddress") || ""); const [sendingQuantity, setSendingQuantity] = useState(localStorage.getItem("sendingQuantity") || ""); - const [sendingToken, setSendingToken] = useState(localStorage.getItem("sendingToken") || ""); + const [sendingToken, setSendingToken] = useState( + JSON.parse(localStorage.getItem("sendingToken")) || { address: "native", symbol: "", logo: "" } + ); const [buyerAddress, setBuyerAddress] = useState(localStorage.getItem("buyerAddress") || ""); const [isRecipientAddressResolved, setIsRecipientAddressResolved] = useState(false); const [deadline, setDeadline] = useState(localStorage.getItem("deadline") || ""); @@ -111,7 +119,7 @@ export const NewTransactionProvider: React.FC<{ children: React.ReactNode }> = ( setReceivingToken(""); setSellerAddress(""); setSendingQuantity(""); - setSendingToken(""); + setSendingToken({ address: "native", symbol: "", logo: "" }); setBuyerAddress(""); setDeadline(""); setNotificationEmail(""); @@ -128,7 +136,7 @@ export const NewTransactionProvider: React.FC<{ children: React.ReactNode }> = ( localStorage.setItem("receivingToken", receivingToken); localStorage.setItem("buyerAddress", buyerAddress); localStorage.setItem("sendingQuantity", sendingQuantity); - localStorage.setItem("sendingToken", sendingToken); + localStorage.setItem("sendingToken", JSON.stringify(sendingToken)); localStorage.setItem("sellerAddress", sellerAddress); localStorage.setItem("deadline", deadline); localStorage.setItem("notificationEmail", notificationEmail); diff --git a/web/src/context/Web3Provider.tsx b/web/src/context/Web3Provider.tsx index 5759e22..81ed68e 100644 --- a/web/src/context/Web3Provider.tsx +++ b/web/src/context/Web3Provider.tsx @@ -11,8 +11,10 @@ import { useTheme } from "styled-components"; const chains = [arbitrumSepolia, mainnet, gnosisChiado]; const projectId = process.env.WALLETCONNECT_PROJECT_ID ?? ""; +export const alchemyApiKey = process.env.ALCHEMY_API_KEY ?? ""; + const { publicClient, webSocketPublicClient } = configureChains(chains, [ - alchemyProvider({ apiKey: process.env.ALCHEMY_API_KEY ?? "" }), + alchemyProvider({ apiKey: alchemyApiKey }), jsonRpcProvider({ rpc: () => ({ http: `https://rpc.chiadochain.net`, diff --git a/web/src/hooks/useEscrowTimelineItems.tsx b/web/src/hooks/useEscrowTimelineItems.tsx index f91364d..1e96938 100644 --- a/web/src/hooks/useEscrowTimelineItems.tsx +++ b/web/src/hooks/useEscrowTimelineItems.tsx @@ -6,7 +6,7 @@ import { resolutionToString } from "utils/resolutionToString"; import { formatTimeoutDuration } from "utils/formatTimeoutDuration"; import CheckCircleOutlineIcon from "components/StyledIcons/CheckCircleOutlineIcon"; import LawBalanceIcon from "components/StyledIcons/LawBalanceIcon"; -import { useNativeTokenSymbol } from "./useNativeTokenSymbol"; +import { StyledSkeleton } from "components/StyledSkeleton"; import { DisputeRequest, HasToPayFee, Payment, SettlementProposal, TransactionResolved } from "src/graphql/graphql"; interface TimelineItem { @@ -43,7 +43,7 @@ const useEscrowTimelineItems = ( isPreview: boolean, transactionCreationTimestamp: number, status: string, - token: string, + assetSymbol: string, buyer: string, seller: string, payments: Payment[], @@ -55,7 +55,6 @@ const useEscrowTimelineItems = ( settlementTimeout: number ): TimelineItem[] => { const theme = useTheme(); - const nativeTokenSymbol = useNativeTokenSymbol(); const [currentTime, setCurrentTime] = useState(Math.floor(Date.now() / 1000)); useEffect(() => { @@ -75,9 +74,12 @@ const useEscrowTimelineItems = ( payments?.forEach((payment) => { const isBuyer = payment.party.toLowerCase() === buyer.toLowerCase(); const formattedDate = getFormattedDate(new Date(payment.timestamp * 1000)); - const title = `The ${isBuyer ? "buyer" : "seller"} paid ${formatEther(payment.amount)} ${ - !token ? nativeTokenSymbol : token - }`; + const title = ( + <> + The {isBuyer ? "buyer" : "seller"} paid {formatEther(payment.amount)}{" "} + {assetSymbol ? assetSymbol : } + + ); timelineItems.push(createTimelineItem(formattedDate, title, "", theme.secondaryBlue)); }); @@ -103,9 +105,12 @@ const useEscrowTimelineItems = ( } answer [Timeout: ${formatTimeoutDuration(timeLeft)}]`; } - const title = `The ${proposal.party === "1" ? "buyer" : "seller"} proposed: Pay ${formatEther( - proposal.amount - )} ${!token ? nativeTokenSymbol : token}`; + const title = ( + <> + The {proposal.party === "1" ? "buyer" : "seller"} proposed: Pay {formatEther(proposal.amount)}{" "} + {assetSymbol ? assetSymbol : } + + ); timelineItems.push(createTimelineItem(formattedDate, title, subtitle, theme.warning)); }); @@ -168,10 +173,9 @@ const useEscrowTimelineItems = ( settlementTimeout, currentTime, theme, - token, + assetSymbol, buyer, seller, - nativeTokenSymbol, ]); }; diff --git a/web/src/hooks/useFilteredTokens.ts b/web/src/hooks/useFilteredTokens.ts new file mode 100644 index 0000000..a7cf974 --- /dev/null +++ b/web/src/hooks/useFilteredTokens.ts @@ -0,0 +1,52 @@ +import { useEffect, useState } from "react"; +import { useTokenMetadata } from "./useTokenMetadata"; +import { IToken } from "context/NewTransactionContext"; +import EthTokenIcon from "svgs/icons/eth-token-icon.png"; + +export const useFilteredTokens = ( + searchQuery: string, + tokens: IToken[], + setTokens: (tokens: IToken) => void, + sendingToken: IToken +) => { + const { tokenMetadata } = useTokenMetadata( + searchQuery.startsWith("0x") && searchQuery.length === 42 ? searchQuery : null + ); + const [filteredTokens, setFilteredTokens] = useState([]); + + useEffect(() => { + const handleSearch = async () => { + let filtered = []; + + if (!searchQuery) { + filtered = tokens; + if (sendingToken) { + filtered = [sendingToken, ...tokens.filter((token) => token.address !== sendingToken.address)]; + } + } else if (tokenMetadata) { + const resultToken = { + symbol: tokenMetadata.symbol, + address: searchQuery.toLowerCase(), + logo: tokenMetadata.logo || EthTokenIcon, + }; + + const updatedTokens = [...tokens, resultToken]; + const uniqueTokens = Array.from(new Set(updatedTokens.map((a) => a.address))).map((address) => { + return updatedTokens.find((a) => a.address === address); + }); + + filtered = [resultToken]; + setTokens(uniqueTokens); + localStorage.setItem("tokens", JSON.stringify(uniqueTokens)); + } else { + filtered = tokens.filter((token) => token.symbol.toLowerCase().includes(searchQuery.toLowerCase())); + } + + setFilteredTokens(filtered); + }; + + handleSearch(); + }, [searchQuery, tokens, setTokens, sendingToken, tokenMetadata]); + + return { filteredTokens }; +}; diff --git a/web/src/hooks/useQueryRefetch.ts b/web/src/hooks/useQueryRefetch.ts new file mode 100644 index 0000000..0df7b0e --- /dev/null +++ b/web/src/hooks/useQueryRefetch.ts @@ -0,0 +1,28 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useCallback } from "react"; + +/** + * + * @required This hook needs to be used within the QueryClientProvider + * @param queryKeys An Array of Query : [ [ "key1" ], [ "key2" ] ] + * @param waitFor Optional - Time to wait before refetching + * @returns refetchQuery - A function that takes in the query keys array to refetch + * @example refetchQuery([["refetchOnBlock","myQuery"],["myOtherQuery"]]) + * @warning The order of keys in the query key array matters + */ +export const useQueryRefetch = () => { + const queryClient = useQueryClient(); + + const refetchQuery = useCallback( + async (queryKeys: string[][], waitFor = 4000) => { + await new Promise((res) => setTimeout(() => res(true), waitFor)); + + for (const queryKey of queryKeys) { + queryClient.refetchQueries(queryKey); + } + }, + [queryClient] + ); + + return refetchQuery; +}; diff --git a/web/src/hooks/useTokenMetadata.ts b/web/src/hooks/useTokenMetadata.ts new file mode 100644 index 0000000..878d882 --- /dev/null +++ b/web/src/hooks/useTokenMetadata.ts @@ -0,0 +1,27 @@ +import { useState, useEffect } from "react"; +import { Alchemy } from "alchemy-sdk"; +import { useNetwork } from "wagmi"; +import { alchemyConfig } from "utils/alchemyConfig"; + +export const useTokenMetadata = (tokenAddress: string) => { + const { chain } = useNetwork(); + const [tokenMetadata, setTokenMetadata] = useState(null); + + useEffect(() => { + const fetchTokenMetadata = async () => { + if (!tokenAddress || tokenAddress === "native") return; + const alchemy = new Alchemy(alchemyConfig(chain?.id)); + try { + const metadata = await alchemy.core.getTokenMetadata(tokenAddress); + setTokenMetadata(metadata); + } catch (error) { + console.error("Error fetching token metadata:", error); + setTokenMetadata(null); + } + }; + + fetchTokenMetadata(); + }, [tokenAddress, chain?.id]); + + return { tokenMetadata }; +}; diff --git a/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/AcceptSettlementButton.tsx b/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/AcceptSettlementButton.tsx index 10b2137..e1da661 100644 --- a/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/AcceptSettlementButton.tsx +++ b/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/AcceptSettlementButton.tsx @@ -8,6 +8,7 @@ import { usePrepareEscrowUniversalAcceptSettlement, useEscrowUniversalAcceptSettlement, } from "hooks/contracts/generated"; +import { useQueryRefetch } from "hooks/useQueryRefetch"; interface IAcceptButton { toggleModal?: () => void; @@ -17,6 +18,7 @@ const AcceptButton: React.FC = ({ toggleModal }) => { const [isSending, setIsSending] = useState(false); const publicClient = usePublicClient(); const { id } = useTransactionDetailsContext(); + const refetchQuery = useQueryRefetch(); const { config: acceptSettlementConfig } = usePrepareEscrowUniversalAcceptSettlement({ args: [BigInt(id)], @@ -32,6 +34,7 @@ const AcceptButton: React.FC = ({ toggleModal }) => { if (!wrapResult.status) { setIsSending(false); } + refetchQuery([["refetchOnBlock", "useTransactionDetailsQuery"]]); }) .catch((error) => { console.error("Error raising dispute as buyer:", error); diff --git a/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/ProposeSettlementButton.tsx b/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/ProposeSettlementButton.tsx index 6c5a312..2ea5877 100644 --- a/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/ProposeSettlementButton.tsx +++ b/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/ProposeSettlementButton.tsx @@ -9,6 +9,7 @@ import { usePrepareEscrowUniversalProposeSettlement, useEscrowUniversalProposeSettlement, } from "hooks/contracts/generated"; +import { useQueryRefetch } from "hooks/useQueryRefetch"; interface IProposeSettlementButton { toggleModal?: () => void; @@ -26,6 +27,7 @@ const ProposeSettlementButton: React.FC = ({ const [isSending, setIsSending] = useState(false); const publicClient = usePublicClient(); const { id } = useTransactionDetailsContext(); + const refetchQuery = useQueryRefetch(); const { config: proposeSettlementConfig } = usePrepareEscrowUniversalProposeSettlement({ args: [BigInt(id), parseEther(amountProposed)], @@ -38,9 +40,9 @@ const ProposeSettlementButton: React.FC = ({ setIsSending(true); wrapWithToast(async () => await proposeSettlement().then((response) => response.hash), publicClient) .then((wrapResult) => { - if (wrapResult.status && toggleModal) { - toggleModal(); - } else if (wrapResult.status) { + if (wrapResult.status) { + toggleModal && toggleModal(); + refetchQuery([["refetchOnBlock", "useTransactionDetailsQuery"]]); } else { setIsSending(false); } diff --git a/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/RaiseDisputeButton.tsx b/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/RaiseDisputeButton.tsx index 55dd6e6..1422841 100644 --- a/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/RaiseDisputeButton.tsx +++ b/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/RaiseDisputeButton.tsx @@ -10,6 +10,7 @@ import { import { isUndefined } from "utils/index"; import { wrapWithToast } from "utils/wrapWithToast"; import { useTransactionDetailsContext } from "context/TransactionDetailsContext"; +import { useQueryRefetch } from "hooks/useQueryRefetch"; interface IRaiseDisputeButton { toggleModal?: () => void; @@ -23,6 +24,7 @@ const RaiseDisputeButton: React.FC = ({ toggleModal, button const publicClient = usePublicClient(); const { buyer, id } = useTransactionDetailsContext(); const isBuyer = useMemo(() => address?.toLowerCase() === buyer?.toLowerCase(), [address, buyer]); + const refetchQuery = useQueryRefetch(); const { config: payArbitrationFeeByBuyerConfig } = usePrepareEscrowUniversalPayArbitrationFeeByBuyer({ args: [BigInt(id)], @@ -34,17 +36,20 @@ const RaiseDisputeButton: React.FC = ({ toggleModal, button value: arbitrationCost, }); - const { writeAsync: payArbitrationFeeByBuyer } = useEscrowUniversalPayArbitrationFeeByBuyer(payArbitrationFeeByBuyerConfig); - const { writeAsync: payArbitrationFeeBySeller } = useEscrowUniversalPayArbitrationFeeBySeller(payArbitrationFeeBySellerConfig); + const { writeAsync: payArbitrationFeeByBuyer } = + useEscrowUniversalPayArbitrationFeeByBuyer(payArbitrationFeeByBuyerConfig); + const { writeAsync: payArbitrationFeeBySeller } = useEscrowUniversalPayArbitrationFeeBySeller( + payArbitrationFeeBySellerConfig + ); const handleRaiseDispute = () => { if (isBuyer && !isUndefined(payArbitrationFeeByBuyer)) { setIsSending(true); wrapWithToast(async () => await payArbitrationFeeByBuyer().then((response) => response.hash), publicClient) .then((wrapResult) => { - if (wrapResult.status && toggleModal) { - toggleModal(); - } else if (wrapResult.status) { + if (wrapResult.status) { + toggleModal && toggleModal(); + refetchQuery([["refetchOnBlock", "useTransactionDetailsQuery"]]); } else { setIsSending(false); } @@ -57,9 +62,9 @@ const RaiseDisputeButton: React.FC = ({ toggleModal, button setIsSending(true); wrapWithToast(async () => await payArbitrationFeeBySeller().then((response) => response.hash), publicClient) .then((wrapResult) => { - if (wrapResult.status && toggleModal) { - toggleModal(); - } else if (wrapResult.status) { + if (wrapResult.status) { + toggleModal && toggleModal(); + refetchQuery([["refetchOnBlock", "useTransactionDetailsQuery"]]); } else { setIsSending(false); } diff --git a/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/TimeOutButton.tsx b/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/TimeOutButton.tsx index 15e9a43..983a85b 100644 --- a/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/TimeOutButton.tsx +++ b/web/src/pages/MyTransactions/TransactionDetails/PreviewCardButtons/TimeOutButton.tsx @@ -10,6 +10,7 @@ import { import { isUndefined } from "utils/index"; import { wrapWithToast } from "utils/wrapWithToast"; import { useTransactionDetailsContext } from "context/TransactionDetailsContext"; +import { useQueryRefetch } from "hooks/useQueryRefetch"; const TimeOutButton: React.FC = () => { const { address } = useAccount(); @@ -17,6 +18,7 @@ const TimeOutButton: React.FC = () => { const publicClient = usePublicClient(); const { buyer, id } = useTransactionDetailsContext(); const isBuyer = useMemo(() => address?.toLowerCase() === buyer?.toLowerCase(), [address, buyer]); + const refetchQuery = useQueryRefetch(); const { config: timeOutByBuyerConfig } = usePrepareEscrowUniversalTimeOutByBuyer({ args: [BigInt(id)], @@ -36,6 +38,7 @@ const TimeOutButton: React.FC = () => { .then((wrapResult) => { if (!wrapResult.status) { setIsSending(false); + refetchQuery([["refetchOnBlock", "useTransactionDetailsQuery"]]); } }) .catch((error) => { @@ -48,6 +51,7 @@ const TimeOutButton: React.FC = () => { .then((wrapResult) => { if (!wrapResult.status) { setIsSending(false); + refetchQuery([["refetchOnBlock", "useTransactionDetailsQuery"]]); } }) .catch((error) => { diff --git a/web/src/pages/MyTransactions/TransactionDetails/WasItFulfilled/Buttons/ReleasePaymentButton.tsx b/web/src/pages/MyTransactions/TransactionDetails/WasItFulfilled/Buttons/ReleasePaymentButton.tsx index 1ac96d9..664671b 100644 --- a/web/src/pages/MyTransactions/TransactionDetails/WasItFulfilled/Buttons/ReleasePaymentButton.tsx +++ b/web/src/pages/MyTransactions/TransactionDetails/WasItFulfilled/Buttons/ReleasePaymentButton.tsx @@ -7,12 +7,14 @@ import { isUndefined } from "utils/index"; import { wrapWithToast } from "utils/wrapWithToast"; import { usePublicClient } from "wagmi"; import { useTransactionDetailsContext } from "context/TransactionDetailsContext"; +import { useQueryRefetch } from "hooks/useQueryRefetch"; const ReleasePaymentButton: React.FC = () => { const [isModalOpen, toggleModal] = useToggle(false); const [isSending, setIsSending] = useState(false); const publicClient = usePublicClient(); const { id, amount } = useTransactionDetailsContext(); + const refetchQuery = useQueryRefetch(); const { config: releaseFullPaymentConfig } = usePrepareEscrowUniversalPay({ args: [id, amount], @@ -27,6 +29,7 @@ const ReleasePaymentButton: React.FC = () => { .then((wrapResult) => { if (wrapResult.status) { toggleModal(); + refetchQuery([["refetchOnBlock", "useTransactionDetailsQuery"]]); } }) .catch((error) => { diff --git a/web/src/pages/MyTransactions/TransactionDetails/index.tsx b/web/src/pages/MyTransactions/TransactionDetails/index.tsx index c478357..f718fe5 100644 --- a/web/src/pages/MyTransactions/TransactionDetails/index.tsx +++ b/web/src/pages/MyTransactions/TransactionDetails/index.tsx @@ -8,11 +8,12 @@ import { isUndefined } from "utils/index"; import PreviewCard from "components/PreviewCard"; import WasItFulfilled from "./WasItFulfilled"; import InfoCards from "./InfoCards"; -import useFetchIpfsJson from "hooks/useFetchIpfsJson"; -import { useEscrowParametersQuery } from "hooks/queries/useEscrowParametersQuery"; +import { useEscrowParametersQuery } from "queries/useEscrowParametersQuery"; +import { useTransactionDetailsQuery } from "queries/useTransactionsQuery"; +import { useArbitrationCost } from "queries/useArbitrationCostFromKlerosCore"; import { useNativeTokenSymbol } from "hooks/useNativeTokenSymbol"; -import { useTransactionDetailsQuery } from "hooks/queries/useTransactionsQuery"; -import { useArbitrationCost } from "hooks/queries/useArbitrationCostFromKlerosCore"; +import useFetchIpfsJson from "hooks/useFetchIpfsJson"; +import { useTokenMetadata } from "hooks/useTokenMetadata"; const Container = styled.div``; @@ -32,6 +33,10 @@ const TransactionDetails: React.FC = () => { const { data: escrowParameters } = useEscrowParametersQuery(); const { arbitrationCost } = useArbitrationCost(escrowParameters?.escrowParameters?.arbitratorExtraData); const nativeTokenSymbol = useNativeTokenSymbol(); + const { tokenMetadata } = useTokenMetadata(transactionDetails?.escrow?.token); + const erc20TokenSymbol = tokenMetadata?.symbol; + const { setTransactionDetails } = useTransactionDetailsContext(); + const { timestamp, transactionUri, @@ -46,14 +51,19 @@ const TransactionDetails: React.FC = () => { settlementProposals, disputeRequest, resolvedEvents, - setTransactionDetails, } = useTransactionDetailsContext(); const transactionInfo = useFetchIpfsJson(transactionUri); useEffect(() => { - setTransactionDetails(transactionDetails?.escrow); - }, [transactionDetails, setTransactionDetails]); + if (transactionDetails?.escrow) { + const detailsWithSymbol = { + ...transactionDetails.escrow, + erc20TokenSymbol: token ? erc20TokenSymbol : nativeTokenSymbol, + }; + setTransactionDetails(detailsWithSymbol); + } + }, [transactionDetails, setTransactionDetails, erc20TokenSymbol, nativeTokenSymbol, token]); return ( @@ -66,13 +76,11 @@ const TransactionDetails: React.FC = () => { extraDescriptionUri={transactionInfo?.extraDescriptionUri} receivingQuantity={""} buyerAddress={buyer} - receivingToken={!token ? nativeTokenSymbol : token} sellerAddress={seller} transactionCreationTimestamp={timestamp} sendingQuantity={!isUndefined(amount) ? formatEther(amount) : ""} - sendingToken={!token ? nativeTokenSymbol : token} - deadlineDate={new Date(deadline * 1000).toLocaleString()} - assetSymbol={!token ? nativeTokenSymbol : token} + deadline={deadline * 1000} + assetSymbol={!token ? nativeTokenSymbol : erc20TokenSymbol} overrideIsList={false} amount={!isUndefined(amount) ? formatEther(amount) : ""} isPreview={false} diff --git a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx index d10faa0..8bee098 100644 --- a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx +++ b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx @@ -5,19 +5,30 @@ import { Button } from "@kleros/ui-components-library"; import { useEscrowUniversalCreateNativeTransaction, usePrepareEscrowUniversalCreateNativeTransaction, + useEscrowUniversalCreateErc20Transaction, + usePrepareEscrowUniversalCreateErc20Transaction, + escrowUniversalAddress, } from "hooks/contracts/generated"; +import { erc20ABI, useNetwork } from "wagmi"; import { useNewTransactionContext } from "context/NewTransactionContext"; -import { useAccount, useEnsAddress, usePublicClient } from "wagmi"; -import { parseEther } from "viem"; +import { + useAccount, + useEnsAddress, + usePublicClient, + useContractRead, + useContractWrite, + usePrepareContractWrite, +} from "wagmi"; +import { parseEther, parseUnits } from "viem"; import { isUndefined } from "utils/index"; import { wrapWithToast } from "utils/wrapWithToast"; -import { ethAddressPattern } from "../Terms/Payment/DestinationAddress"; +import { ethAddressPattern } from "utils/validateAddress"; +import { useQueryRefetch } from "hooks/useQueryRefetch"; const StyledButton = styled(Button)``; const DepositPaymentButton: React.FC = () => { const { - escrowType, escrowTitle, deliverableText, transactionUri, @@ -25,17 +36,24 @@ const DepositPaymentButton: React.FC = () => { sendingQuantity, sellerAddress, deadline, - token, + sendingToken, resetContext, } = useNewTransactionContext(); const [currentTime, setCurrentTime] = useState(Date.now()); const [finalRecipientAddress, setFinalRecipientAddress] = useState(sellerAddress); - const ensResult = useEnsAddress({ name: sellerAddress, chainId: 1 }); const publicClient = usePublicClient(); const navigate = useNavigate(); - const [isSending, setIsSending] = useState(false); + const refetchQuery = useQueryRefetch(); + const [isSending, setIsSending] = useState(false); + const [isApproved, setIsApproved] = useState(false); const { address } = useAccount(); + const { chain } = useNetwork(); + const ensResult = useEnsAddress({ name: sellerAddress, chainId: 1 }); + const deadlineTimestamp = new Date(deadline).getTime(); + const timeoutPayment = (deadlineTimestamp - currentTime) / 1000; + const isNativeTransaction = sendingToken?.address === "native"; + const transactionValue = isNativeTransaction ? parseEther(sendingQuantity) : parseUnits(sendingQuantity, 18); useEffect(() => { const intervalId = setInterval(() => setCurrentTime(Date.now()), 1000); @@ -43,17 +61,24 @@ const DepositPaymentButton: React.FC = () => { }, []); useEffect(() => { - if (ensResult.data) { - setFinalRecipientAddress(ensResult.data); - } else { - setFinalRecipientAddress(sellerAddress); - } + setFinalRecipientAddress(ensResult.data || sellerAddress); }, [sellerAddress, ensResult.data]); - const deadlineTimestamp = new Date(deadline).getTime(); - const timeoutPayment = (deadlineTimestamp - currentTime) / 1000; + const { data: allowance } = useContractRead({ + enabled: !isNativeTransaction, + address: sendingToken?.address, + abi: erc20ABI, + functionName: "allowance", + args: [address, escrowUniversalAddress?.[chain?.id]], + }); + + useEffect(() => { + if (!isUndefined(allowance)) { + setIsApproved(allowance >= transactionValue); + } + }, [allowance, transactionValue]); - const templateData = { + const templateData = JSON.stringify({ $schema: "../NewDisputeTemplate.schema.json", title: escrowTitle, description: deliverableText, @@ -82,7 +107,7 @@ const DepositPaymentButton: React.FC = () => { buyer: address, seller: sellerAddress, amount: sendingQuantity, - token: escrowType === "general" ? "native" : token, + token: isNativeTransaction ? "native" : sendingToken?.address, timeoutPayment: timeoutPayment, transactionUri: transactionUri, }, @@ -93,41 +118,82 @@ const DepositPaymentButton: React.FC = () => { Seller: sellerAddress, }, version: "1.0", - }; + }); - const stringifiedTemplateData = JSON.stringify(templateData); + const { config: createNativeTransactionConfig } = usePrepareEscrowUniversalCreateNativeTransaction({ + enabled: isNativeTransaction && ethAddressPattern.test(finalRecipientAddress), + args: [BigInt(Math.floor(timeoutPayment)), transactionUri, finalRecipientAddress, templateData, ""], + value: transactionValue, + }); - const { config: createTransactionConfig } = usePrepareEscrowUniversalCreateNativeTransaction({ - enabled: !isUndefined(ensResult) && ethAddressPattern.test(finalRecipientAddress), + const { config: createERC20TransactionConfig } = usePrepareEscrowUniversalCreateErc20Transaction({ + enabled: + !isNativeTransaction && + !isUndefined(allowance) && + allowance >= transactionValue && + ethAddressPattern.test(finalRecipientAddress), args: [ + transactionValue, + sendingToken?.address, BigInt(Math.floor(timeoutPayment)), transactionUri, finalRecipientAddress, - stringifiedTemplateData, - /* Assuming no template data mappings are needed*/ - , + templateData, + "", ], - value: parseEther(sendingQuantity), }); - const { writeAsync: createTransaction } = useEscrowUniversalCreateNativeTransaction(createTransactionConfig); + const { writeAsync: createNativeTransaction } = + useEscrowUniversalCreateNativeTransaction(createNativeTransactionConfig); + const { writeAsync: createERC20Transaction } = useEscrowUniversalCreateErc20Transaction(createERC20TransactionConfig); + + const { config: approveConfig } = usePrepareContractWrite({ + enabled: !isNativeTransaction, + address: sendingToken?.address, + abi: erc20ABI, + functionName: "approve", + args: [escrowUniversalAddress?.[chain?.id], transactionValue], + }); + + const { writeAsync: approve } = useContractWrite(approveConfig); + + const handleApproveToken = async () => { + if (!isUndefined(approve)) { + setIsSending(true); + try { + const wrapResult = await wrapWithToast( + async () => await approve().then((response) => response.hash), + publicClient + ); + setIsApproved(wrapResult.status); + } catch (error) { + console.error("Approval failed:", error); + setIsApproved(false); + } finally { + setIsSending(false); + } + } + }; - const handleCreateTransaction = () => { + const handleCreateTransaction = async () => { + const createTransaction = isNativeTransaction ? createNativeTransaction : createERC20Transaction; if (!isUndefined(createTransaction)) { setIsSending(true); - wrapWithToast(async () => await createTransaction().then((response) => response.hash), publicClient) - .then((wrapResult) => { - if (wrapResult.status) { - resetContext(); - navigate("/my-transactions/display/1/desc/all"); - } - }) - .catch((error) => { - console.error("Transaction failed:", error); - }) - .finally(() => { - setIsSending(false); - }); + try { + const wrapResult = await wrapWithToast( + async () => await createTransaction().then((response) => response.hash), + publicClient + ); + if (wrapResult.status) { + refetchQuery([["refetchOnBlock", "useMyTransactionsQuery"], ["useUserQuery"]]); + resetContext(); + navigate("/my-transactions/display/1/desc/all"); + } + } catch (error) { + console.error("Transaction failed:", error); + } finally { + setIsSending(false); + } } }; @@ -135,8 +201,8 @@ const DepositPaymentButton: React.FC = () => { ); }; diff --git a/web/src/pages/NewTransaction/NavigationButtons/NextButton.tsx b/web/src/pages/NewTransaction/NavigationButtons/NextButton.tsx index 8addfb3..13dca83 100644 --- a/web/src/pages/NewTransaction/NavigationButtons/NextButton.tsx +++ b/web/src/pages/NewTransaction/NavigationButtons/NextButton.tsx @@ -2,9 +2,9 @@ import React from "react"; import { Button } from "@kleros/ui-components-library"; import { useNavigate, useLocation } from "react-router-dom"; import { useNewTransactionContext } from "context/NewTransactionContext"; -import { validateAddress } from "../Terms/Payment/DestinationAddress"; import { EMAIL_REGEX } from "../Terms/Notifications/EmailField"; import { handleFileUpload } from "utils/handleFileUpload"; +import { validateAddress } from "utils/validateAddress"; interface INextButton { nextRoute: string; @@ -39,9 +39,7 @@ const NextButton: React.FC = ({ nextRoute }) => { const isSellerAddressValid = validateAddress(sellerAddress); const areSendingFieldsEmpty = - escrowType === "swap" - ? !sendingQuantity || !sendingToken || !sellerAddress - : !sendingQuantity || !sellerAddress; + escrowType === "swap" ? !sendingQuantity || !sendingToken || !sellerAddress : !sendingQuantity || !sellerAddress; const isEmailValid = notificationEmail === "" || EMAIL_REGEX.test(notificationEmail); @@ -59,10 +57,7 @@ const NextButton: React.FC = ({ nextRoute }) => { (location.pathname.includes("/new-transaction/title") && !escrowTitle) || (location.pathname.includes("/new-transaction/deliverable") && !isDeliverableValid) || (location.pathname.includes("/new-transaction/payment") && - (areSendingFieldsEmpty || - !isSellerAddressValid || - !isRecipientAddressResolved || - !hasSufficientNativeBalance)) || + (areSendingFieldsEmpty || !isSellerAddressValid || !isRecipientAddressResolved || !hasSufficientNativeBalance)) || (location.pathname.includes("/new-transaction/deadline") && (!deadline || isDeadlineInPast)) || (location.pathname.includes("/new-transaction/notifications") && !isEmailValid); diff --git a/web/src/pages/NewTransaction/Preview/index.tsx b/web/src/pages/NewTransaction/Preview/index.tsx index 92781f5..891e45a 100644 --- a/web/src/pages/NewTransaction/Preview/index.tsx +++ b/web/src/pages/NewTransaction/Preview/index.tsx @@ -1,18 +1,16 @@ import React from "react"; import styled from "styled-components"; +import { useAccount } from "wagmi"; +import { useNewTransactionContext } from "context/NewTransactionContext"; +import PreviewCard from "components/PreviewCard"; import Header from "./Header"; import NavigationButtons from "../NavigationButtons"; -import PreviewCard from "components/PreviewCard"; -import { responsiveSize } from "styles/responsiveSize"; -import { useNewTransactionContext } from "context/NewTransactionContext"; import { useNativeTokenSymbol } from "hooks/useNativeTokenSymbol"; -import { useAccount } from "wagmi"; const Container = styled.div` display: flex; align-items: center; flex-direction: column; - padding: 0 ${responsiveSize(24, 136)}; `; const Preview: React.FC = () => { @@ -20,7 +18,6 @@ const Preview: React.FC = () => { escrowType, deliverableText, receivingQuantity, - receivingToken, sellerAddress, sendingQuantity, sendingToken, @@ -28,6 +25,7 @@ const Preview: React.FC = () => { deadline, extraDescriptionUri, } = useNewTransactionContext(); + const isNativeTransaction = sendingToken.address === "native"; const nativeTokenSymbol = useNativeTokenSymbol(); const { address } = useAccount(); @@ -36,20 +34,20 @@ const Preview: React.FC = () => {
diff --git a/web/src/pages/NewTransaction/Terms/Payment/DestinationAddress.tsx b/web/src/pages/NewTransaction/Terms/Payment/DestinationAddress.tsx index f7cf068..c1c86a2 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/DestinationAddress.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/DestinationAddress.tsx @@ -1,11 +1,12 @@ import React, { useState, useEffect, useMemo } from "react"; import styled, { css } from "styled-components"; +import { responsiveSize } from "styles/responsiveSize"; import { landscapeStyle } from "styles/landscapeStyle"; import { Field } from "@kleros/ui-components-library"; -import { responsiveSize } from "styles/responsiveSize"; +import { useDebounce } from "react-use"; import { useEnsAddress } from "wagmi"; import { useNewTransactionContext } from "context/NewTransactionContext"; -import { useDebounce } from "react-use"; +import { ensDomainPattern, validateAddress } from "utils/validateAddress"; const StyledField = styled(Field)` width: 84vw; @@ -29,13 +30,6 @@ const StyledField = styled(Field)` )} `; -export const ethAddressPattern = /^0x[a-fA-F0-9]{40}$/; -export const ensDomainPattern = /^[a-zA-Z0-9-]{1,}\.eth$/; - -export const validateAddress = (input: string) => { - return ethAddressPattern.test(input) || ensDomainPattern.test(input); -}; - interface IDestinationAddress { recipientAddress: string; setRecipientAddress: (value: string) => void; diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/AmountField.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/AmountField.tsx index afd31c7..99b63f1 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/AmountField.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/AmountField.tsx @@ -1,11 +1,9 @@ -import React, { useState, useEffect } from "react"; +import React from "react"; import styled from "styled-components"; import { Field } from "@kleros/ui-components-library"; -import { useAccount, useBalance } from "wagmi"; -import { useNewTransactionContext } from "context/NewTransactionContext"; const StyledField = styled(Field)` - width: 132px; + width: 186px; input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; @@ -17,36 +15,17 @@ const StyledField = styled(Field)` input { font-size: 16px; + padding-right: ${({ variant }) => (variant ? "40px" : "16px")}; } `; -interface IAmount { +interface IAmountField { quantity: string; setQuantity: (value: string) => void; + error: string; } -const Amount: React.FC = ({ quantity, setQuantity }) => { - const { setHasSufficientNativeBalance } = useNewTransactionContext(); - const { address } = useAccount(); - const { data: balanceData } = useBalance({ address: address }); - const [error, setError] = useState(""); - - useEffect(() => { - const balanceAmount = balanceData ? parseFloat(balanceData.formatted) : 0; - const enteredAmount = parseFloat(quantity); - - if (quantity && balanceAmount < enteredAmount) { - setError("Insufficient balance"); - setHasSufficientNativeBalance(false); - } else if (enteredAmount === 0) { - setError("Amount is zero"); - setHasSufficientNativeBalance(false); - } else { - setError(""); - setHasSufficientNativeBalance(true); - } - }, [balanceData, quantity, setHasSufficientNativeBalance]); - +const AmountField: React.FC = ({ quantity, setQuantity, error }) => { const handleWrite = (event: React.ChangeEvent) => { setQuantity(event.target.value); }; @@ -56,11 +35,11 @@ const Amount: React.FC = ({ quantity, setQuantity }) => { value={quantity} onChange={handleWrite} type="number" - placeholder="eg. 3.6" + placeholder="Amount" variant={error ? "error" : undefined} message={error} /> ); }; -export default Amount; +export default AmountField; diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/MaxBalance.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/MaxBalance.tsx new file mode 100644 index 0000000..bed62d3 --- /dev/null +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/MaxBalance.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { landscapeStyle } from "styles/landscapeStyle"; +import styled, { css } from "styled-components"; +import Skeleton from "react-loading-skeleton"; +import { isUndefined } from "utils/index"; + +const Container = styled.div` + display: flex; + flex-direction: row; + align-self: center; + gap: 4px; + + ${landscapeStyle( + () => css` + align-self: flex-end; + ` + )} +`; + +const LabelAndBalance = styled.div` + display: flex; + flex-direction: row; + gap: 4px; +`; + +const Label = styled.p` + margin: 0; + color: ${({ theme }) => theme.secondaryText}; + font-size: 12px; +`; + +const Balance = styled.p` + margin: 0; + font-size: 12px; +`; + +const BalanceSkeleton = styled(Skeleton)` + width: 63px; + height: 16px; +`; + +const MaxButton = styled.p` + margin: 0; + color: ${({ theme }) => theme.primaryBlue}; + font-size: 12px; + cursor: pointer; +`; + +interface IMaxBalance { + formattedBalance: string; + rawBalance: number; + setQuantity: (value: string) => void; +} + +const MaxBalance: React.FC = ({ formattedBalance, rawBalance, setQuantity }) => { + return ( + + + + {isUndefined(formattedBalance) ? : {formattedBalance}} + + {!isUndefined(formattedBalance) ? ( + setQuantity(String(rawBalance))}>Max + ) : null} + + ); +}; +export default MaxBalance; diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/NativeToken.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/NativeToken.tsx deleted file mode 100644 index 25c4324..0000000 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/NativeToken.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import { useNativeTokenSymbol } from "hooks/useNativeTokenSymbol"; - -const StyledLabel = styled.label` - color: ${({ theme }) => theme.primaryText}; - font-weight: 600; - font-size: 16px; -`; - -const NativeToken: React.FC = () => { - const nativeTokenSymbol = useNativeTokenSymbol(); - - return {nativeTokenSymbol}; -}; - -export default NativeToken; diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/DropdownButton.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/DropdownButton.tsx new file mode 100644 index 0000000..1b4e654 --- /dev/null +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/DropdownButton.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import styled from "styled-components"; +import Skeleton from "react-loading-skeleton"; + +const Container = styled.div` + border: 1px solid ${({ theme }) => theme.stroke}; + border-radius: 3px; + width: 186px; + height: 45px; + position: relative; + padding: 9.5px 14px; + cursor: pointer; + background: ${({ theme }) => theme.whiteBackground}; + color: ${({ theme }) => theme.primaryText}; + display: flex; + align-items: center; + justify-content: space-between; +`; + +const DropdownArrow = styled.span` + border: solid ${({ theme }) => theme.stroke}; + border-width: 0 1px 1px 0; + display: inline-block; + padding: 3px; + transform: rotate(45deg); + margin-left: 8px; +`; + +const TokenLogo = styled.img` + width: 24px; + height: 24px; +`; + +const DropdownContent = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const LogoSkeleton = styled(Skeleton)` + width: 24px; + height: 24px; + border-radius: 50%; + margin-bottom: 2px; +`; + +const SymbolSkeleton = styled(Skeleton)` + width: 40px; + height: 16px; +`; + +export const DropdownButton = ({ loading, sendingToken, onClick }) => ( + + + {loading ? ( + + ) : ( + sendingToken && + )} + {loading ? : sendingToken?.symbol} + + + +); diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem/Balance.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem/Balance.tsx new file mode 100644 index 0000000..5f7d4b0 --- /dev/null +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem/Balance.tsx @@ -0,0 +1,41 @@ +import React, { useMemo } from "react"; +import styled from "styled-components"; +import Skeleton from "react-loading-skeleton"; +import { useAccount, useBalance } from "wagmi"; +import { IToken } from "context/NewTransactionContext"; +import { isUndefined } from "utils/index"; +import { getFormattedBalance } from "utils/getFormattedBalance"; + +const Container = styled.p` + color: ${({ theme }) => theme.primaryText}; + margin: 0; +`; + +const StyledAmountSkeleton = styled(Skeleton)` + width: 52px; + height: 20px; +`; + +interface IBalance { + token: IToken; +} + +const Balance: React.FC = ({ token }) => { + const { address } = useAccount(); + + const { data: balanceData } = useBalance({ + address: address, + token: token?.address === "native" ? undefined : token?.address, + }); + + const formattedBalance = useMemo(() => getFormattedBalance(balanceData, token), [balanceData, token]); + + return ( + + {isUndefined(formattedBalance) ? : null} + {!isUndefined(formattedBalance) && formattedBalance !== "0" ? formattedBalance : null} + + ); +}; + +export default Balance; diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem/index.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem/index.tsx new file mode 100644 index 0000000..05124bf --- /dev/null +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem/index.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import styled from "styled-components"; +import Balance from "./Balance"; + +const Container = styled.div<{ selected: boolean }>` + display: flex; + align-items: center; + padding: 10px 16px; + cursor: pointer; + justify-content: space-between; + background: ${({ theme, selected }) => (selected ? theme.mediumBlue : "transparent")}; + border-left: ${({ theme, selected }) => (selected ? `3px solid ${theme.primaryBlue}` : "none")}; + padding-left: ${({ selected }) => (selected ? "13px" : "16px")}; + + &:hover { + background: ${({ theme }) => theme.lightBlue}; + } +`; + +const LogoAndLabel = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +`; + +const TokenLogo = styled.img` + width: 24px; + height: 24px; +`; + +const TokenLabel = styled.span` + color: ${({ theme }) => theme.primaryText}; +`; + +const TokenItem = ({ token, selected, onSelect }) => { + return ( + onSelect(token)}> + + + {token.symbol} + + + + ); +}; + +export default TokenItem; diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenListModal.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenListModal.tsx new file mode 100644 index 0000000..0f6d1a8 --- /dev/null +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenListModal.tsx @@ -0,0 +1,75 @@ +import React, { useRef, useState } from "react"; +import styled, { css } from "styled-components"; +import { useClickAway } from "react-use"; +import { Searchbar } from "@kleros/ui-components-library"; +import { useNewTransactionContext } from "context/NewTransactionContext"; +import { Overlay } from "components/Overlay"; +import TokenItem from "./TokenItem"; +import { StyledModal } from "pages/MyTransactions/Modal/StyledModal"; +import { useFilteredTokens } from "hooks/useFilteredTokens"; +import { landscapeStyle } from "styles/landscapeStyle"; +import { responsiveSize } from "styles/responsiveSize"; + +const ReStyledModal = styled(StyledModal)` + display: flex; + width: ${responsiveSize(320, 500)}; + ${landscapeStyle( + () => css` + width: 500px; + ` + )} +`; + +const StyledSearchbar = styled(Searchbar)` + width: 100%; + input { + font-size: 16px; + } +`; + +const ItemsContainer = styled.div` + display: flex; + width: 100%; + flex-direction: column; + margin-top: 24px; +`; + +const StyledP = styled.p` + display: flex; + align-self: flex-start; + font-weight: 600; + margin: 0; + margin-bottom: 28px; +`; + +export const TokenListModal = ({ setIsOpen, tokens, setTokens, handleSelectToken }) => { + const [searchQuery, setSearchQuery] = useState(""); + const { sendingToken } = useNewTransactionContext(); + const { filteredTokens } = useFilteredTokens(searchQuery, tokens, setTokens, sendingToken); + const containerRef = useRef(null); + useClickAway(containerRef, () => setIsOpen(false)); + + return ( + <> + + + Select a token + setSearchQuery(e.target.value)} + /> + + {filteredTokens.map((token) => ( + + ))} + + + + ); +}; diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx new file mode 100644 index 0000000..89b4983 --- /dev/null +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx @@ -0,0 +1,58 @@ +import React, { useState, useEffect, useRef } from "react"; +import styled from "styled-components"; +import { useClickAway } from "react-use"; +import { Alchemy } from "alchemy-sdk"; +import { useAccount, useNetwork } from "wagmi"; +import { useNewTransactionContext } from "context/NewTransactionContext"; +import { initializeTokens } from "utils/initializeTokens"; +import { alchemyConfig } from "utils/alchemyConfig"; +import { useLocalStorage } from "hooks/useLocalStorage"; +import { DropdownButton } from "./DropdownButton"; +import { TokenListModal } from "./TokenListModal"; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + position: relative; +`; + +const TokenSelector: React.FC = () => { + const { address } = useAccount(); + const { chain } = useNetwork(); + const { sendingToken, setSendingToken } = useNewTransactionContext(); + const [tokens, setTokens] = useLocalStorage("tokens", []); + const [isOpen, setIsOpen] = useState(false); + const containerRef = useRef(null); + const [loading, setLoading] = useState(true); + const alchemyInstance = new Alchemy(alchemyConfig(chain?.id)); + useClickAway(containerRef, () => setIsOpen(false)); + + useEffect(() => { + if (address && chain) { + initializeTokens(address, setTokens, setLoading, chain, alchemyInstance); + } + }, [address, chain]); + + useEffect(() => { + if (tokens?.length > 0) { + const nativeToken = tokens.find((token) => token.address === "native"); + setSendingToken(JSON.parse(localStorage.getItem("selectedToken")) || nativeToken); + } + }, [tokens, setSendingToken]); + + const handleSelectToken = (token) => { + setSendingToken(token); + localStorage.setItem("selectedToken", JSON.stringify(token)); + setIsOpen(false); + }; + + return ( + + setIsOpen(!isOpen)} /> + {isOpen && } + + ); +}; + +export default TokenSelector; diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx index d1562bc..317435b 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx @@ -1,16 +1,27 @@ -import React from "react"; -import styled from "styled-components"; +import React, { useMemo, useState, useEffect } from "react"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; import { responsiveSize } from "styles/responsiveSize"; +import { useBalance, useAccount } from "wagmi"; +import { useNewTransactionContext } from "context/NewTransactionContext"; +import { getFormattedBalance } from "utils/getFormattedBalance"; import AmountField from "./AmountField"; -import NativeToken from "./NativeToken"; +import TokenSelector from "./TokenSelector"; +import MaxBalance from "./MaxBalance"; const Container = styled.div` display: flex; flex-direction: row; - gap: 8px; - align-items: center; - margin-bottom: ${responsiveSize(24, 18)}; - margin-left: 36px; + justify-content: center; + gap: 24px; + margin-bottom: ${responsiveSize(16, 0)}; + flex-wrap: wrap; +`; + +const TokenSelectorAndMaxBalance = styled.div` + display: flex; + flex-direction: column; + gap: 4px; `; interface ITokenAndAmount { @@ -19,10 +30,43 @@ interface ITokenAndAmount { } const TokenAndAmount: React.FC = ({ quantity, setQuantity }) => { + const { address } = useAccount(); + const { sendingToken, setHasSufficientNativeBalance } = useNewTransactionContext(); + const { data: balanceData } = useBalance({ + address: address, + token: sendingToken?.address === "native" ? undefined : sendingToken?.address, + }); + const [error, setError] = useState(""); + + useEffect(() => { + const balanceAmount = balanceData ? parseFloat(balanceData.formatted) : 0; + const enteredAmount = parseFloat(quantity); + + if (quantity && balanceAmount < enteredAmount) { + setError("Insufficient balance"); + setHasSufficientNativeBalance(false); + } else if (enteredAmount === 0) { + setError("Amount is zero"); + setHasSufficientNativeBalance(false); + } else { + setError(""); + setHasSufficientNativeBalance(true); + } + }, [balanceData, quantity, setHasSufficientNativeBalance]); + + const formattedBalance = useMemo(() => getFormattedBalance(balanceData, sendingToken), [balanceData, sendingToken]); + return ( - - + + + + + ); }; diff --git a/web/src/utils/alchemyConfig.ts b/web/src/utils/alchemyConfig.ts new file mode 100644 index 0000000..b7ebe80 --- /dev/null +++ b/web/src/utils/alchemyConfig.ts @@ -0,0 +1,7 @@ +import { alchemyApiKey } from "context/Web3Provider"; +import { mapWagmiNetworkToAlchemyNetwork } from "utils/mapWagmiNetworkToAlchemyNetwork"; + +export const alchemyConfig = (chainId: number) => ({ + apiKey: alchemyApiKey, + network: mapWagmiNetworkToAlchemyNetwork(chainId), +}); diff --git a/web/src/utils/fetchNativeToken.ts b/web/src/utils/fetchNativeToken.ts new file mode 100644 index 0000000..fea5078 --- /dev/null +++ b/web/src/utils/fetchNativeToken.ts @@ -0,0 +1,9 @@ +import EthTokenIcon from "svgs/icons/eth-token-icon.png"; + +export const fetchNativeToken = (chain) => { + return { + symbol: chain?.nativeCurrency?.symbol, + address: "native", + logo: EthTokenIcon, + }; +}; diff --git a/web/src/utils/fetchTokenInfo.ts b/web/src/utils/fetchTokenInfo.ts new file mode 100644 index 0000000..5278485 --- /dev/null +++ b/web/src/utils/fetchTokenInfo.ts @@ -0,0 +1,17 @@ +import { Alchemy } from "alchemy-sdk"; +import { IToken } from "context/NewTransactionContext"; +import EthTokenIcon from "svgs/icons/eth-token-icon.png"; + +export const fetchTokenInfo = async (address: string, alchemyInstance: Alchemy) => { + try { + const metadata = await alchemyInstance.core.getTokenMetadata(address); + return { + symbol: metadata.symbol?.toUpperCase() || "Unknown", + logo: metadata.logo || EthTokenIcon, + address, + } as IToken; + } catch (error) { + console.error("Error fetching token info:", error); + return { symbol: "Unknown", logo: EthTokenIcon }; + } +}; diff --git a/web/src/utils/format.ts b/web/src/utils/format.ts index 5b07d7f..409e991 100644 --- a/web/src/utils/format.ts +++ b/web/src/utils/format.ts @@ -14,10 +14,10 @@ export const formatValue = (value: string, fractionDigits, roundDown) => { return commify(units.toFixed(fractionDigits)); }; -export const formatPNK = (value: bigint, fractionDigits = 0, roundDown = false) => +export const formatPNK = (value: bigint, fractionDigits = 0, roundDown = true) => formatValue(formatUnitsWei(value), fractionDigits, roundDown); -export const formatETH = (value: bigint, fractionDigits = 4, roundDown = false) => +export const formatETH = (value: bigint, fractionDigits = 4, roundDown = true) => formatValue(formatEther(value), fractionDigits, roundDown); export const formatUSD = (value: number, fractionDigits = 2) => "$" + commify(Number(value).toFixed(fractionDigits)); diff --git a/web/src/utils/getFormattedBalance.ts b/web/src/utils/getFormattedBalance.ts new file mode 100644 index 0000000..27126c9 --- /dev/null +++ b/web/src/utils/getFormattedBalance.ts @@ -0,0 +1,7 @@ +import { formatETH, formatPNK } from "./format"; + +export const getFormattedBalance = (balanceData: any, token: any) => { + if (!balanceData) return undefined; + if (token?.symbol === "PNK") return formatPNK(balanceData.value); + return formatETH(balanceData.value); +}; diff --git a/web/src/utils/initializeTokens.ts b/web/src/utils/initializeTokens.ts new file mode 100644 index 0000000..f19a154 --- /dev/null +++ b/web/src/utils/initializeTokens.ts @@ -0,0 +1,34 @@ +import { Alchemy } from "alchemy-sdk"; +import { IToken } from "context/NewTransactionContext"; +import { fetchNativeToken } from "./fetchNativeToken"; +import { fetchTokenInfo } from "./fetchTokenInfo"; + +export const initializeTokens = async (address: string, setTokens, setLoading, chain, alchemyInstance: Alchemy) => { + try { + setLoading(true); + const nativeToken = fetchNativeToken(chain); + const balances = await alchemyInstance.core.getTokenBalances(address); + const tokenList = await Promise.all( + balances.tokenBalances.map(async (token) => { + const tokenInfo = await fetchTokenInfo(token.contractAddress, alchemyInstance); + return { + symbol: tokenInfo.symbol, + address: tokenInfo.address, + logo: tokenInfo.logo, + } as IToken; + }) + ); + const allTokens = [nativeToken, ...tokenList]; + const customTokens = JSON.parse(localStorage.getItem("tokens")) || []; + const combinedTokens = [ + ...allTokens, + ...customTokens.filter((customToken) => !allTokens.some((token) => token.address === customToken.address)), + ]; + setTokens(combinedTokens); + localStorage.setItem("tokens", JSON.stringify(combinedTokens)); + setLoading(false); + } catch (error) { + console.error("Error initializing tokens:", error); + setLoading(false); + } +}; diff --git a/web/src/utils/mapWagmiNetworkToAlchemyNetwork.ts b/web/src/utils/mapWagmiNetworkToAlchemyNetwork.ts new file mode 100644 index 0000000..3940b35 --- /dev/null +++ b/web/src/utils/mapWagmiNetworkToAlchemyNetwork.ts @@ -0,0 +1,20 @@ +import { Network } from "alchemy-sdk"; + +export function mapWagmiNetworkToAlchemyNetwork(chainId: number) { + switch (chainId) { + case 1: + return Network.ETH_MAINNET; + case 11155111: + return Network.ETH_SEPOLIA; + case 10: + return Network.OPT_MAINNET; + case 137: + return Network.MATIC_MAINNET; + case 42161: + return Network.ARB_MAINNET; + case 421614: + return Network.ARB_SEPOLIA; + default: + return Network.ARB_SEPOLIA; + } +} diff --git a/web/src/utils/validateAddress.ts b/web/src/utils/validateAddress.ts new file mode 100644 index 0000000..f42fc38 --- /dev/null +++ b/web/src/utils/validateAddress.ts @@ -0,0 +1,6 @@ +export const ethAddressPattern = /^0x[a-fA-F0-9]{40}$/; +export const ensDomainPattern = /^[a-zA-Z0-9-]{1,}\.eth$/; + +export const validateAddress = (input: string) => { + return ethAddressPattern.test(input) || ensDomainPattern.test(input); +}; diff --git a/yarn.lock b/yarn.lock index 3a40044..80c3446 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3364,7 +3364,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/providers@npm:5.7.2, @ethersproject/providers@npm:^5.7.1, @ethersproject/providers@npm:^5.7.2": +"@ethersproject/providers@npm:5.7.2, @ethersproject/providers@npm:^5.7.0, @ethersproject/providers@npm:^5.7.1, @ethersproject/providers@npm:^5.7.2": version: 5.7.2 resolution: "@ethersproject/providers@npm:5.7.2" dependencies: @@ -3479,7 +3479,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/units@npm:5.7.0": +"@ethersproject/units@npm:5.7.0, @ethersproject/units@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/units@npm:5.7.0" dependencies: @@ -4787,6 +4787,7 @@ __metadata: "@web3modal/ethereum": "npm:^2.7.1" "@web3modal/react": "npm:^2.2.2" "@yornaath/batshit": "npm:^0.9.0" + alchemy-sdk: "npm:^3.3.1" amqplib: "npm:^0.10.3" chart.js: "npm:^3.9.1" chartjs-adapter-moment: "npm:^1.0.1" @@ -10843,6 +10844,28 @@ __metadata: languageName: node linkType: hard +"alchemy-sdk@npm:^3.3.1": + version: 3.3.1 + resolution: "alchemy-sdk@npm:3.3.1" + dependencies: + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/abstract-provider": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/bytes": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/hash": "npm:^5.7.0" + "@ethersproject/networks": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@ethersproject/units": "npm:^5.7.0" + "@ethersproject/wallet": "npm:^5.7.0" + "@ethersproject/web": "npm:^5.7.0" + axios: "npm:^1.6.5" + sturdy-websocket: "npm:^0.2.1" + websocket: "npm:^1.0.34" + checksum: 10/7b00e5ba85bac77742db5b598eb8649b7c5c103d71fd9d6ecb3c3d158bdb9f51b76c3fb2fe717d5361e4a789e551c93654ee5ed477bc942b67b4cfc08e968d2c + languageName: node + linkType: hard + "amdefine@npm:>=0.0.4": version: 1.0.1 resolution: "amdefine@npm:1.0.1" @@ -11440,6 +11463,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.6.5": + version: 1.6.8 + resolution: "axios@npm:1.6.8" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 10/3f9a79eaf1d159544fca9576261ff867cbbff64ed30017848e4210e49f3b01e97cf416390150e6fdf6633f336cd43dc1151f890bbd09c3c01ad60bb0891eee63 + languageName: node + linkType: hard + "axobject-query@npm:^3.2.1": version: 3.2.1 resolution: "axobject-query@npm:3.2.1" @@ -12151,6 +12185,16 @@ __metadata: languageName: node linkType: hard +"bufferutil@npm:^4.0.1": + version: 4.0.8 + resolution: "bufferutil@npm:4.0.8" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10/d9337badc960a19d5a031db5de47159d7d8a11b6bab399bdfbf464ffa9ecd2972fef19bb61a7d2827e0c55f912c20713e12343386b86cb013f2b99c2324ab6a3 + languageName: node + linkType: hard + "builtin-modules@npm:^3.1.0": version: 3.3.0 resolution: "builtin-modules@npm:3.3.0" @@ -13943,6 +13987,16 @@ __metadata: languageName: node linkType: hard +"d@npm:1, d@npm:^1.0.1, d@npm:^1.0.2": + version: 1.0.2 + resolution: "d@npm:1.0.2" + dependencies: + es5-ext: "npm:^0.10.64" + type: "npm:^2.7.2" + checksum: 10/a3f45ef964622f683f6a1cb9b8dcbd75ce490cd2f4ac9794099db3d8f0e2814d412d84cd3fe522e58feb1f273117bb480f29c5381f6225f0abca82517caaa77a + languageName: node + linkType: hard + "damerau-levenshtein@npm:^1.0.8": version: 1.0.8 resolution: "damerau-levenshtein@npm:1.0.8" @@ -14035,7 +14089,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:2.6.9, debug@npm:^2.6.0": +"debug@npm:2.6.9, debug@npm:^2.2.0, debug@npm:^2.6.0": version: 2.6.9 resolution: "debug@npm:2.6.9" dependencies: @@ -14999,6 +15053,29 @@ __metadata: languageName: node linkType: hard +"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.62, es5-ext@npm:^0.10.63, es5-ext@npm:^0.10.64, es5-ext@npm:~0.10.14": + version: 0.10.64 + resolution: "es5-ext@npm:0.10.64" + dependencies: + es6-iterator: "npm:^2.0.3" + es6-symbol: "npm:^3.1.3" + esniff: "npm:^2.0.1" + next-tick: "npm:^1.1.0" + checksum: 10/0c5d8657708b1695ddc4b06f4e0b9fbdda4d2fe46d037b6bedb49a7d1931e542ec9eecf4824d59e1d357e93229deab014bb4b86485db2d41b1d68e54439689ce + languageName: node + linkType: hard + +"es6-iterator@npm:^2.0.3": + version: 2.0.3 + resolution: "es6-iterator@npm:2.0.3" + dependencies: + d: "npm:1" + es5-ext: "npm:^0.10.35" + es6-symbol: "npm:^3.1.1" + checksum: 10/dbadecf3d0e467692815c2b438dfa99e5a97cbbecf4a58720adcb467a04220e0e36282399ba297911fd472c50ae4158fffba7ed0b7d4273fe322b69d03f9e3a5 + languageName: node + linkType: hard + "es6-promise@npm:^4.0.3": version: 4.2.8 resolution: "es6-promise@npm:4.2.8" @@ -15015,6 +15092,16 @@ __metadata: languageName: node linkType: hard +"es6-symbol@npm:^3.1.1, es6-symbol@npm:^3.1.3": + version: 3.1.4 + resolution: "es6-symbol@npm:3.1.4" + dependencies: + d: "npm:^1.0.2" + ext: "npm:^1.7.0" + checksum: 10/3743119fe61f89e2f049a6ce52bd82fab5f65d13e2faa72453b73f95c15292c3cb9bdf3747940d504517e675e45fd375554c6b5d35d2bcbefd35f5489ecba546 + languageName: node + linkType: hard + "esbuild@npm:0.16.17": version: 0.16.17 resolution: "esbuild@npm:0.16.17" @@ -15592,6 +15679,18 @@ __metadata: languageName: node linkType: hard +"esniff@npm:^2.0.1": + version: 2.0.1 + resolution: "esniff@npm:2.0.1" + dependencies: + d: "npm:^1.0.1" + es5-ext: "npm:^0.10.62" + event-emitter: "npm:^0.3.5" + type: "npm:^2.7.2" + checksum: 10/f6a2abd2f8c5fe57c5fcf53e5407c278023313d0f6c3a92688e7122ab9ac233029fd424508a196ae5bc561aa1f67d23f4e2435b1a0d378030f476596129056ac + languageName: node + linkType: hard + "espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" @@ -15931,6 +16030,16 @@ __metadata: languageName: node linkType: hard +"event-emitter@npm:^0.3.5": + version: 0.3.5 + resolution: "event-emitter@npm:0.3.5" + dependencies: + d: "npm:1" + es5-ext: "npm:~0.10.14" + checksum: 10/a7f5ea80029193f4869782d34ef7eb43baa49cd397013add1953491b24588468efbe7e3cc9eb87d53f33397e7aab690fd74c079ec440bf8b12856f6bdb6e9396 + languageName: node + linkType: hard + "event-target-shim@npm:^5.0.0": version: 5.0.1 resolution: "event-target-shim@npm:5.0.1" @@ -16110,6 +16219,15 @@ __metadata: languageName: node linkType: hard +"ext@npm:^1.7.0": + version: 1.7.0 + resolution: "ext@npm:1.7.0" + dependencies: + type: "npm:^2.7.2" + checksum: 10/666a135980b002df0e75c8ac6c389140cdc59ac953db62770479ee2856d58ce69d2f845e5f2586716350b725400f6945e51e9159573158c39f369984c72dcd84 + languageName: node + linkType: hard + "extend@npm:^3.0.0, extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -16538,6 +16656,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.15.6": + version: 1.15.6 + resolution: "follow-redirects@npm:1.15.6" + peerDependenciesMeta: + debug: + optional: true + checksum: 10/70c7612c4cab18e546e36b991bbf8009a1a41cf85354afe04b113d1117569abf760269409cb3eb842d9f7b03d62826687086b081c566ea7b1e6613cf29030bf7 + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -22659,6 +22787,13 @@ __metadata: languageName: node linkType: hard +"next-tick@npm:^1.1.0": + version: 1.1.0 + resolution: "next-tick@npm:1.1.0" + checksum: 10/83b5cf36027a53ee6d8b7f9c0782f2ba87f4858d977342bfc3c20c21629290a2111f8374d13a81221179603ffc4364f38374b5655d17b6a8f8a8c77bdea4fe8b + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -27992,6 +28127,13 @@ __metadata: languageName: node linkType: hard +"sturdy-websocket@npm:^0.2.1": + version: 0.2.1 + resolution: "sturdy-websocket@npm:0.2.1" + checksum: 10/6de51bc70fe3995ecd9e443c7fc78dea24727871219a7cb58fc073f06acaa1f702b917dbb30da1a5a09ec480f4b797c86fb1f78721efbd55e34eec556448c5d9 + languageName: node + linkType: hard + "style-loader@npm:^3.3.1": version: 3.3.4 resolution: "style-loader@npm:3.3.4" @@ -29019,6 +29161,13 @@ __metadata: languageName: node linkType: hard +"type@npm:^2.7.2": + version: 2.7.2 + resolution: "type@npm:2.7.2" + checksum: 10/602f1b369fba60687fa4d0af6fcfb814075bcaf9ed3a87637fb384d9ff849e2ad15bc244a431f341374562e51a76c159527ffdb1f1f24b0f1f988f35a301c41d + languageName: node + linkType: hard + "typechain@npm:^8.3.0": version: 8.3.2 resolution: "typechain@npm:8.3.2" @@ -29599,6 +29748,16 @@ __metadata: languageName: node linkType: hard +"utf-8-validate@npm:^5.0.2": + version: 5.0.10 + resolution: "utf-8-validate@npm:5.0.10" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10/b89cbc13b4badad04828349ebb7aa2ab1edcb02b46ab12ce0ba5b2d6886d684ad4e93347819e3c8d36224c8742422d2dca69f5cc16c72ae4d7eeecc0c5cb544b + languageName: node + linkType: hard + "utf8@npm:3.0.0": version: 3.0.0 resolution: "utf8@npm:3.0.0" @@ -30268,6 +30427,20 @@ __metadata: languageName: node linkType: hard +"websocket@npm:^1.0.34": + version: 1.0.35 + resolution: "websocket@npm:1.0.35" + dependencies: + bufferutil: "npm:^4.0.1" + debug: "npm:^2.2.0" + es5-ext: "npm:^0.10.63" + typedarray-to-buffer: "npm:^3.1.5" + utf-8-validate: "npm:^5.0.2" + yaeti: "npm:^0.0.6" + checksum: 10/c05a80c536de7befadc530e5134947f7cc000493038ab78e3ed03080bb873b4ecedf95ea4e7087e6a98d04f02f31723bd98ec67f85e9159525a769b5a478fa8d + languageName: node + linkType: hard + "whatwg-encoding@npm:^1.0.5": version: 1.0.5 resolution: "whatwg-encoding@npm:1.0.5" @@ -30838,6 +31011,13 @@ __metadata: languageName: node linkType: hard +"yaeti@npm:^0.0.6": + version: 0.0.6 + resolution: "yaeti@npm:0.0.6" + checksum: 10/6db12c152f7c363b80071086a3ebf5032e03332604eeda988872be50d6c8469e1f13316175544fa320f72edad696c2d83843ad0ff370659045c1a68bcecfcfea + languageName: node + linkType: hard + "yallist@npm:^2.1.2": version: 2.1.2 resolution: "yallist@npm:2.1.2"