From 0e2b68cdeaa95679464343f6c8f9250817de1035 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Fri, 17 May 2024 19:17:40 +0200 Subject: [PATCH 01/16] setup structure for creating token widgets --- web/package.json | 1 + web/src/context/Web3Provider.tsx | 4 +- .../DepositPaymentButton.tsx | 32 ++- .../TokenAndAmount/NativeToken.tsx | 17 -- .../TokenSelector/AddCustomTokenTab.tsx | 29 +++ .../TokenSelector/TokensTab.tsx | 30 +++ .../TokenSelector/alchemyConfig.ts | 9 + .../TokenAndAmount/TokenSelector/index.tsx | 175 ++++++++++++++++ .../TokenAndAmount/index.tsx | 6 +- web/src/utils/fetchNativeToken.ts | 7 + web/src/utils/fetchOwnedTokensFromAlchemy.ts | 24 +++ web/src/utils/fetchTokenInfo.ts | 12 ++ .../utils/mapWagmiNetworkToAlchemyNetwork.ts | 22 +++ yarn.lock | 186 +++++++++++++++++- 14 files changed, 521 insertions(+), 33 deletions(-) delete mode 100644 web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/NativeToken.tsx create mode 100644 web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx create mode 100644 web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx create mode 100644 web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/alchemyConfig.ts create mode 100644 web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx create mode 100644 web/src/utils/fetchNativeToken.ts create mode 100644 web/src/utils/fetchOwnedTokensFromAlchemy.ts create mode 100644 web/src/utils/fetchTokenInfo.ts create mode 100644 web/src/utils/mapWagmiNetworkToAlchemyNetwork.ts 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/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/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx index d10faa0..a3599db 100644 --- a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx +++ b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx @@ -5,10 +5,12 @@ import { Button } from "@kleros/ui-components-library"; import { useEscrowUniversalCreateNativeTransaction, usePrepareEscrowUniversalCreateNativeTransaction, + useEscrowUniversalCreateErc20Transaction, + usePrepareEscrowUniversalCreateErc20Transaction, } from "hooks/contracts/generated"; import { useNewTransactionContext } from "context/NewTransactionContext"; import { useAccount, useEnsAddress, usePublicClient } from "wagmi"; -import { parseEther } from "viem"; +import { parseEther, parseUnits } from "viem"; import { isUndefined } from "utils/index"; import { wrapWithToast } from "utils/wrapWithToast"; import { ethAddressPattern } from "../Terms/Payment/DestinationAddress"; @@ -25,7 +27,7 @@ const DepositPaymentButton: React.FC = () => { sendingQuantity, sellerAddress, deadline, - token, + sendingToken, resetContext, } = useNewTransactionContext(); @@ -52,6 +54,7 @@ const DepositPaymentButton: React.FC = () => { const deadlineTimestamp = new Date(deadline).getTime(); const timeoutPayment = (deadlineTimestamp - currentTime) / 1000; + const isNativeTransaction = sendingToken === "native"; const templateData = { $schema: "../NewDisputeTemplate.schema.json", @@ -82,7 +85,7 @@ const DepositPaymentButton: React.FC = () => { buyer: address, seller: sellerAddress, amount: sendingQuantity, - token: escrowType === "general" ? "native" : token, + token: isNativeTransaction ? "native" : sendingToken, timeoutPayment: timeoutPayment, transactionUri: transactionUri, }, @@ -97,22 +100,33 @@ const DepositPaymentButton: React.FC = () => { const stringifiedTemplateData = JSON.stringify(templateData); - const { config: createTransactionConfig } = usePrepareEscrowUniversalCreateNativeTransaction({ - enabled: !isUndefined(ensResult) && ethAddressPattern.test(finalRecipientAddress), + const transactionValue = isNativeTransaction ? parseEther(sendingQuantity) : parseUnits(sendingQuantity, 18); + + const { config: createNativeTransactionConfig } = usePrepareEscrowUniversalCreateNativeTransaction({ + enabled: !isUndefined(ensResult) && ethAddressPattern.test(finalRecipientAddress) && isNativeTransaction, + args: [BigInt(Math.floor(timeoutPayment)), transactionUri, finalRecipientAddress, stringifiedTemplateData, ""], // Add empty string for _templateDataMappings + value: transactionValue, + }); + + const { config: createERC20TransactionConfig } = usePrepareEscrowUniversalCreateErc20Transaction({ + enabled: !isUndefined(ensResult) && ethAddressPattern.test(finalRecipientAddress) && !isNativeTransaction, args: [ + transactionValue, + sendingToken, BigInt(Math.floor(timeoutPayment)), transactionUri, finalRecipientAddress, stringifiedTemplateData, - /* Assuming no template data mappings are needed*/ - , + "", // Add empty string for _templateDataMappings ], - value: parseEther(sendingQuantity), }); - const { writeAsync: createTransaction } = useEscrowUniversalCreateNativeTransaction(createTransactionConfig); + const { writeAsync: createNativeTransaction } = + useEscrowUniversalCreateNativeTransaction(createNativeTransactionConfig); + const { writeAsync: createERC20Transaction } = useEscrowUniversalCreateErc20Transaction(createERC20TransactionConfig); const handleCreateTransaction = () => { + const createTransaction = isNativeTransaction ? createNativeTransaction : createERC20Transaction; if (!isUndefined(createTransaction)) { setIsSending(true); wrapWithToast(async () => await createTransaction().then((response) => response.hash), publicClient) 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/AddCustomTokenTab.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx new file mode 100644 index 0000000..750ea6e --- /dev/null +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Button, Field } from "@kleros/ui-components-library"; + +interface IAddCustomTokenTab { + customToken: string; + setCustomToken: (value: string) => void; + handleAddCustomToken: () => void; + validateAddress: (address: string) => boolean; +} + +const AddCustomTokenTab: React.FC = ({ + customToken, + setCustomToken, + handleAddCustomToken, + validateAddress, +}) => { + const handleCustomTokenChange = (e: React.ChangeEvent) => { + setCustomToken(e.target.value); + }; + + return ( +
+ +
+ ); +}; + +export default AddCustomTokenTab; diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx new file mode 100644 index 0000000..bf795df --- /dev/null +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { Field } from "@kleros/ui-components-library"; +import { Item, TokenLabel, TokenLogo } from "."; + +interface ITokensTab { + searchQuery: string; + setSearchQuery: (value: string) => void; + filteredTokens: { label: string; value: string; logo: string }[]; + handleSelectToken: (value: string) => void; +} + +const TokensTab: React.FC = ({ searchQuery, setSearchQuery, filteredTokens, handleSelectToken }) => { + const handleSearch = (e: React.ChangeEvent) => { + setSearchQuery(e.target.value); + }; + + return ( +
+ + {filteredTokens.map((token) => ( + handleSelectToken(token.value)}> + + {token.label} + + ))} +
+ ); +}; + +export default TokensTab; diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/alchemyConfig.ts b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/alchemyConfig.ts new file mode 100644 index 0000000..80933df --- /dev/null +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/alchemyConfig.ts @@ -0,0 +1,9 @@ +import { alchemyApiKey } from "context/Web3Provider"; +import { mapWagmiNetworkToAlchemyNetwork } from "utils/mapWagmiNetworkToAlchemyNetwork"; + +const alchemyConfig = (chainId: number) => ({ + apiKey: alchemyApiKey, + network: mapWagmiNetworkToAlchemyNetwork(chainId), +}); + +export default alchemyConfig; 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..9476d4b --- /dev/null +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx @@ -0,0 +1,175 @@ +import React, { useState, useEffect, useRef } from "react"; +import styled from "styled-components"; +import Skeleton from "react-loading-skeleton"; +import "react-loading-skeleton/dist/skeleton.css"; +import { useClickAway } from "react-use"; +import { Tabs } from "@kleros/ui-components-library"; +import { useAccount, useNetwork } from "wagmi"; +import { Alchemy } from "alchemy-sdk"; +import alchemyConfig from "./alchemyConfig"; +import { useNewTransactionContext } from "context/NewTransactionContext"; +import { fetchNativeToken } from "utils/fetchNativeToken"; +import { fetchOwnedTokensFromAlchemy } from "utils/fetchOwnedTokensFromAlchemy"; +import { fetchTokenInfo } from "utils/fetchTokenInfo"; +import { validateAddress } from "../../../DestinationAddress"; +import TokensTab from "./TokensTab"; +import AddCustomTokenTab from "./AddCustomTokenTab"; +import { StyledModal } from "pages/MyTransactions/Modal/StyledModal"; +import { Overlay } from "components/Overlay"; + +const Container = styled.div` + position: relative; + width: 186px; + height: 45px; +`; + +const TokenSelectorWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + position: relative; +`; + +const DropdownButton = styled.div` + border: 1px solid ${({ theme }) => theme.stroke}; + border-radius: 3px; + padding: 8px; + 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; +`; + +export const Item = styled.div` + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + cursor: pointer; + &:hover { + background: ${({ theme }) => theme.mediumBlue}; + } +`; + +export const TokenLogo = styled.img` + width: 24px; + height: 24px; +`; + +export const TokenLabel = styled.span` + color: ${({ theme }) => theme.primaryText}; +`; + +const TokenSelector: React.FC = () => { + const { address } = useAccount(); + const { chain } = useNetwork(); + const { sendingToken, setSendingToken } = useNewTransactionContext(); + const [ownedTokens, setOwnedTokens] = useState([]); + const [customToken, setCustomToken] = useState(""); + const [isOpen, setIsOpen] = useState(false); + const [activeTab, setActiveTab] = useState("tokens"); + const [searchQuery, setSearchQuery] = useState(""); + const containerRef = useRef(null); + const [loading, setLoading] = useState(true); + const alchemy = new Alchemy(alchemyConfig(chain?.id)); + useClickAway(containerRef, () => setIsOpen(false)); + + useEffect(() => { + if (address && chain) { + fetchOwnedTokensFromAlchemy(alchemy, address, setOwnedTokens, setLoading, chain); + setSendingToken(fetchNativeToken(chain).value); + } + }, [address, chain, setSendingToken]); + + const handleAddCustomToken = async () => { + if (!validateAddress(customToken)) { + alert("Invalid address"); + return; + } + const tokenInfo = await fetchTokenInfo(alchemy, customToken); + setOwnedTokens([...ownedTokens, { label: tokenInfo.symbol, value: customToken, logo: tokenInfo.logo }]); + setCustomToken(""); + }; + + const handleSelectToken = (value: string) => { + setSendingToken(value); + setIsOpen(false); + }; + + const filteredTokens = ownedTokens.filter((token) => + token?.label?.toLowerCase().includes(searchQuery?.toLowerCase()) + ); + + return ( + + + setIsOpen(!isOpen)}> +
+ {loading ? ( + + ) : ( + sendingToken && ( + token.value === sendingToken)?.logo} + alt={`${sendingToken} logo`} + /> + ) + )} + {loading ? ( + + ) : sendingToken ? ( + ownedTokens.find((token) => token.value === sendingToken)?.label + ) : ( + "Select a token" + )} +
+ +
+ {isOpen && ( + <> + + + + {activeTab === "tokens" && ( + + )} + {activeTab === "addCustomToken" && ( + + )} + + + )} +
+
+ ); +}; + +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..fca3131 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx @@ -2,12 +2,12 @@ import React from "react"; import styled from "styled-components"; import { responsiveSize } from "styles/responsiveSize"; import AmountField from "./AmountField"; -import NativeToken from "./NativeToken"; +import TokenSelector from "./TokenSelector"; const Container = styled.div` display: flex; flex-direction: row; - gap: 8px; + gap: 24px; align-items: center; margin-bottom: ${responsiveSize(24, 18)}; margin-left: 36px; @@ -22,7 +22,7 @@ const TokenAndAmount: React.FC = ({ quantity, setQuantity }) => return ( - + ); }; diff --git a/web/src/utils/fetchNativeToken.ts b/web/src/utils/fetchNativeToken.ts new file mode 100644 index 0000000..c6a2900 --- /dev/null +++ b/web/src/utils/fetchNativeToken.ts @@ -0,0 +1,7 @@ +export const fetchNativeToken = (chain) => { + return { + label: chain?.nativeCurrency?.symbol || "Native Token", + value: "native", + logo: "https://assets.coingecko.com/coins/images/279/thumb/ethereum.png", + }; +}; diff --git a/web/src/utils/fetchOwnedTokensFromAlchemy.ts b/web/src/utils/fetchOwnedTokensFromAlchemy.ts new file mode 100644 index 0000000..2b72c45 --- /dev/null +++ b/web/src/utils/fetchOwnedTokensFromAlchemy.ts @@ -0,0 +1,24 @@ +import { fetchNativeToken } from "./fetchNativeToken"; +import { fetchTokenInfo } from "./fetchTokenInfo"; + +export const fetchOwnedTokensFromAlchemy = async (alchemy, address: string, setOwnedTokens, setLoading, chain) => { + try { + setLoading(true); + const nativeToken = fetchNativeToken(chain); + const balances = await alchemy.core.getTokenBalances(address); + const tokenList = balances.tokenBalances.map(async (token) => { + const tokenInfo = await fetchTokenInfo(alchemy, token.contractAddress); + return { + label: tokenInfo.symbol, + value: token.contractAddress, + logo: tokenInfo.logo, + }; + }); + const allTokens = [nativeToken, ...(await Promise.all(tokenList))]; + setOwnedTokens(allTokens); + setLoading(false); + } catch (error) { + console.error("Error fetching owned tokens:", error); + setLoading(false); + } +}; diff --git a/web/src/utils/fetchTokenInfo.ts b/web/src/utils/fetchTokenInfo.ts new file mode 100644 index 0000000..add28b3 --- /dev/null +++ b/web/src/utils/fetchTokenInfo.ts @@ -0,0 +1,12 @@ +export const fetchTokenInfo = async (alchemy, address: string) => { + try { + const metadata = await alchemy.core.getTokenMetadata(address); + return { + symbol: metadata.symbol?.toUpperCase() || "Unknown", + logo: metadata.logo || "https://via.placeholder.com/24", + }; + } catch (error) { + console.error("Error fetching token info:", error); + return { symbol: "Unknown", logo: "https://via.placeholder.com/24" }; + } +}; diff --git a/web/src/utils/mapWagmiNetworkToAlchemyNetwork.ts b/web/src/utils/mapWagmiNetworkToAlchemyNetwork.ts new file mode 100644 index 0000000..5b5eb84 --- /dev/null +++ b/web/src/utils/mapWagmiNetworkToAlchemyNetwork.ts @@ -0,0 +1,22 @@ +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 80001: + return Network.MATIC_MUMBAI; + case 42161: + return Network.ARB_MAINNET; + case 421614: + return Network.ARB_SEPOLIA; + default: + return Network.ARB_SEPOLIA; + } +} 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" From def02ffa127de41be31ca8472b82709b9ed0cfeb Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Fri, 17 May 2024 20:42:48 +0200 Subject: [PATCH 02/16] feat: correct creation and approval flow of the token --- .../DepositPaymentButton.tsx | 39 ++++++++++++++++--- .../TokenAndAmount/AmountField.tsx | 32 +++------------ .../TokenAndAmount/index.tsx | 30 +++++++++++++- .../utils/mapWagmiNetworkToAlchemyNetwork.ts | 2 - 4 files changed, 66 insertions(+), 37 deletions(-) diff --git a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx index a3599db..9aad4b3 100644 --- a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx +++ b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx @@ -7,9 +7,11 @@ import { 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 { useAccount, useEnsAddress, usePublicClient, useContractWrite, usePrepareContractWrite } from "wagmi"; import { parseEther, parseUnits } from "viem"; import { isUndefined } from "utils/index"; import { wrapWithToast } from "utils/wrapWithToast"; @@ -37,7 +39,9 @@ const DepositPaymentButton: React.FC = () => { const publicClient = usePublicClient(); const navigate = useNavigate(); const [isSending, setIsSending] = useState(false); + const [isApproved, setIsApproved] = useState(false); const { address } = useAccount(); + const { chain } = useNetwork(); useEffect(() => { const intervalId = setInterval(() => setCurrentTime(Date.now()), 1000); @@ -104,7 +108,7 @@ const DepositPaymentButton: React.FC = () => { const { config: createNativeTransactionConfig } = usePrepareEscrowUniversalCreateNativeTransaction({ enabled: !isUndefined(ensResult) && ethAddressPattern.test(finalRecipientAddress) && isNativeTransaction, - args: [BigInt(Math.floor(timeoutPayment)), transactionUri, finalRecipientAddress, stringifiedTemplateData, ""], // Add empty string for _templateDataMappings + args: [BigInt(Math.floor(timeoutPayment)), transactionUri, finalRecipientAddress, stringifiedTemplateData, ""], value: transactionValue, }); @@ -117,7 +121,7 @@ const DepositPaymentButton: React.FC = () => { transactionUri, finalRecipientAddress, stringifiedTemplateData, - "", // Add empty string for _templateDataMappings + "", ], }); @@ -125,7 +129,30 @@ const DepositPaymentButton: React.FC = () => { useEscrowUniversalCreateNativeTransaction(createNativeTransactionConfig); const { writeAsync: createERC20Transaction } = useEscrowUniversalCreateErc20Transaction(createERC20TransactionConfig); - const handleCreateTransaction = () => { + const { config: approveConfig } = usePrepareContractWrite({ + address: sendingToken, + abi: erc20ABI, + functionName: "approve", + args: [escrowUniversalAddress?.[chain?.id], transactionValue], + }); + + const { writeAsync: approve } = useContractWrite(approveConfig); + + const handleApproveToken = async () => { + if (!isUndefined(approve)) { + setIsSending(true); + try { + await wrapWithToast(async () => await approve().then((response) => response.hash), publicClient); + setIsApproved(true); + } catch (error) { + console.error("Approval failed:", error); + } finally { + setIsSending(false); + } + } + }; + + const handleCreateTransaction = async () => { const createTransaction = isNativeTransaction ? createNativeTransaction : createERC20Transaction; if (!isUndefined(createTransaction)) { setIsSending(true); @@ -149,8 +176,8 @@ const DepositPaymentButton: React.FC = () => { ); }; 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..d3902fe 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/AmountField.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/AmountField.tsx @@ -1,8 +1,6 @@ -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; @@ -20,33 +18,13 @@ const StyledField = styled(Field)` } `; -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); }; @@ -63,4 +41,4 @@ const Amount: React.FC = ({ quantity, setQuantity }) => { ); }; -export default Amount; +export default AmountField; 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 fca3131..d6130d0 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx @@ -1,5 +1,7 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import styled from "styled-components"; +import { useBalance, useAccount } from "wagmi"; +import { useNewTransactionContext } from "context/NewTransactionContext"; import { responsiveSize } from "styles/responsiveSize"; import AmountField from "./AmountField"; import TokenSelector from "./TokenSelector"; @@ -19,9 +21,33 @@ interface ITokenAndAmount { } const TokenAndAmount: React.FC = ({ quantity, setQuantity }) => { + const { address } = useAccount(); + const { sendingToken, setHasSufficientNativeBalance } = useNewTransactionContext(); + const { data: balanceData } = useBalance({ + address: address, + token: sendingToken === "native" ? undefined : sendingToken, + }); + 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]); + return ( - + ); diff --git a/web/src/utils/mapWagmiNetworkToAlchemyNetwork.ts b/web/src/utils/mapWagmiNetworkToAlchemyNetwork.ts index 5b5eb84..3940b35 100644 --- a/web/src/utils/mapWagmiNetworkToAlchemyNetwork.ts +++ b/web/src/utils/mapWagmiNetworkToAlchemyNetwork.ts @@ -10,8 +10,6 @@ export function mapWagmiNetworkToAlchemyNetwork(chainId: number) { return Network.OPT_MAINNET; case 137: return Network.MATIC_MAINNET; - case 80001: - return Network.MATIC_MUMBAI; case 42161: return Network.ARB_MAINNET; case 421614: From d5552521901dbf58cf74a300887fd7f9d810d973 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Sat, 18 May 2024 00:19:32 +0200 Subject: [PATCH 03/16] feat: correctly fetch and show erc20 token symbol in frontend, skeletons --- .../PreviewCard/EscrowTimeline/index.tsx | 6 ++-- .../PreviewCard/Terms/Description.tsx | 29 +++++++++++++------ .../components/PreviewCard/Terms/index.tsx | 4 --- web/src/components/PreviewCard/index.tsx | 8 +---- web/src/components/TransactionCard/index.tsx | 18 +++++++----- web/src/components/TransactionInfo/index.tsx | 10 +++++-- web/src/hooks/useERC20TokenSymbol.ts | 27 +++++++++++++++++ web/src/hooks/useEscrowTimelineItems.tsx | 26 ++++++++++------- .../TransactionDetails/index.tsx | 27 ++++++++++------- .../pages/NewTransaction/Preview/index.tsx | 7 ++--- .../TokenAndAmount/TokenSelector/index.tsx | 2 +- .../TokenSelector => utils}/alchemyConfig.ts | 0 12 files changed, 105 insertions(+), 59 deletions(-) create mode 100644 web/src/hooks/useERC20TokenSymbol.ts rename web/src/{pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector => utils}/alchemyConfig.ts (100%) 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..da0479d 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; @@ -34,16 +38,22 @@ const Description: React.FC = ({ deadlineDate, 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(deadlineDate).toLocaleString()}. + + ); - 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(deadlineDate).toLocaleString()}. + + ); 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..888b442 100644 --- a/web/src/components/PreviewCard/Terms/index.tsx +++ b/web/src/components/PreviewCard/Terms/index.tsx @@ -29,10 +29,8 @@ const Terms: React.FC = ({ escrowType, deliverableText, receivingQuantity, - receivingToken, buyerAddress, sendingQuantity, - sendingToken, sellerAddress, deadlineDate, assetSymbol, @@ -45,10 +43,8 @@ const Terms: React.FC = ({ escrowType={escrowType} deliverableText={deliverableText} receivingQuantity={receivingQuantity} - receivingToken={receivingToken} buyerAddress={buyerAddress} sendingQuantity={sendingQuantity} - sendingToken={sendingToken} sellerAddress={sellerAddress} deadlineDate={deadlineDate} assetSymbol={assetSymbol} diff --git a/web/src/components/PreviewCard/index.tsx b/web/src/components/PreviewCard/index.tsx index 2885d56..6768441 100644 --- a/web/src/components/PreviewCard/index.tsx +++ b/web/src/components/PreviewCard/index.tsx @@ -52,13 +52,11 @@ 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; assetSymbol: string; @@ -80,13 +78,11 @@ const PreviewCard: React.FC = ({ escrowTitle, deliverableText, receivingQuantity, - receivingToken, transactionCreationTimestamp, status, token, buyerAddress, sendingQuantity, - sendingToken, sellerAddress, deadlineDate, assetSymbol, @@ -119,10 +115,8 @@ const PreviewCard: React.FC = ({ escrowType, deliverableText, receivingQuantity, - receivingToken, buyerAddress, sendingQuantity, - sendingToken, sellerAddress, deadlineDate, assetSymbol, @@ -134,7 +128,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..333a578 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 { useERC20TokenSymbol } from "hooks/useERC20TokenSymbol"; import useFetchIpfsJson from "hooks/useFetchIpfsJson"; -import { useNavigateAndScrollTop } from "hooks/useNavigateAndScrollTop"; import { TransactionDetailsFragment } from "src/graphql/graphql"; -import { StyledSkeleton, StyledSkeletonTitle } from "../StyledSkeleton"; const StyledCard = styled(Card)` width: 100%; @@ -69,6 +70,7 @@ const TransactionCard: React.FC = ({ const transactionInfo = useFetchIpfsJson(transactionUri); const { isList } = useIsList(); const nativeTokenSymbol = useNativeTokenSymbol(); + const { erc20TokenSymbol } = useERC20TokenSymbol(token); const title = transactionInfo?.title; const navigateAndScrollTop = useNavigateAndScrollTop(); @@ -83,7 +85,7 @@ const TransactionCard: React.FC = ({ {!isUndefined(title) ? {title} : } = ({ )} = ({ return ( - {amount && assetSymbol ? ( + {amount ? ( + {amount} {!assetSymbol ? : assetSymbol} + + } displayAsList={displayAsList} isPreview={isPreview} /> @@ -134,4 +139,5 @@ const TransactionInfo: React.FC = ({ ); }; + export default TransactionInfo; diff --git a/web/src/hooks/useERC20TokenSymbol.ts b/web/src/hooks/useERC20TokenSymbol.ts new file mode 100644 index 0000000..e45d801 --- /dev/null +++ b/web/src/hooks/useERC20TokenSymbol.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 useERC20TokenSymbol = (tokenAddress: string) => { + const { chain } = useNetwork(); + const [erc20TokenSymbol, setErc20TokenSymbol] = useState(null); + + useEffect(() => { + const fetchTokenSymbol = async () => { + if (!tokenAddress) return; + const alchemy = new Alchemy(alchemyConfig(chain?.id)); + try { + const metadata = await alchemy.core.getTokenMetadata(tokenAddress); + setErc20TokenSymbol(metadata.symbol || null); + } catch (error) { + console.error("Error fetching token symbol:", error); + setErc20TokenSymbol(null); + } + }; + + fetchTokenSymbol(); + }, [tokenAddress, chain?.id]); + + return { erc20TokenSymbol }; +}; diff --git a/web/src/hooks/useEscrowTimelineItems.tsx b/web/src/hooks/useEscrowTimelineItems.tsx index f91364d..2b4140e 100644 --- a/web/src/hooks/useEscrowTimelineItems.tsx +++ b/web/src/hooks/useEscrowTimelineItems.tsx @@ -6,8 +6,8 @@ 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 { DisputeRequest, HasToPayFee, Payment, SettlementProposal, TransactionResolved } from "src/graphql/graphql"; +import { StyledSkeleton } from "components/StyledSkeleton"; interface TimelineItem { title: string; @@ -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/pages/MyTransactions/TransactionDetails/index.tsx b/web/src/pages/MyTransactions/TransactionDetails/index.tsx index c478357..c3de2b7 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 { useERC20TokenSymbol } from "hooks/useERC20TokenSymbol"; +import useFetchIpfsJson from "hooks/useFetchIpfsJson"; const Container = styled.div``; @@ -32,6 +33,9 @@ const TransactionDetails: React.FC = () => { const { data: escrowParameters } = useEscrowParametersQuery(); const { arbitrationCost } = useArbitrationCost(escrowParameters?.escrowParameters?.arbitratorExtraData); const nativeTokenSymbol = useNativeTokenSymbol(); + const { erc20TokenSymbol } = useERC20TokenSymbol(transactionDetails?.escrow?.token); + const { setTransactionDetails } = useTransactionDetailsContext(); + const { timestamp, transactionUri, @@ -46,14 +50,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 +75,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} + assetSymbol={!token ? nativeTokenSymbol : erc20TokenSymbol} overrideIsList={false} amount={!isUndefined(amount) ? formatEther(amount) : ""} isPreview={false} diff --git a/web/src/pages/NewTransaction/Preview/index.tsx b/web/src/pages/NewTransaction/Preview/index.tsx index 92781f5..d5364b7 100644 --- a/web/src/pages/NewTransaction/Preview/index.tsx +++ b/web/src/pages/NewTransaction/Preview/index.tsx @@ -7,6 +7,7 @@ import { responsiveSize } from "styles/responsiveSize"; import { useNewTransactionContext } from "context/NewTransactionContext"; import { useNativeTokenSymbol } from "hooks/useNativeTokenSymbol"; import { useAccount } from "wagmi"; +import { useERC20TokenSymbol } from "hooks/useERC20TokenSymbol"; const Container = styled.div` display: flex; @@ -20,7 +21,6 @@ const Preview: React.FC = () => { escrowType, deliverableText, receivingQuantity, - receivingToken, sellerAddress, sendingQuantity, sendingToken, @@ -29,6 +29,7 @@ const Preview: React.FC = () => { extraDescriptionUri, } = useNewTransactionContext(); const nativeTokenSymbol = useNativeTokenSymbol(); + const { erc20TokenSymbol } = useERC20TokenSymbol(sendingToken); const { address } = useAccount(); @@ -37,14 +38,12 @@ const Preview: React.FC = () => {
Date: Tue, 21 May 2024 13:02:30 +0200 Subject: [PATCH 04/16] feat: check if user has already allowed the token, fix dates, store tokens in localhost --- .../PreviewCard/Terms/Description.tsx | 4 +- web/src/hooks/useEscrowTimelineItems.tsx | 2 +- .../DepositPaymentButton.tsx | 40 +++++++++++-- .../NavigationButtons/NextButton.tsx | 11 +--- .../Terms/Payment/DestinationAddress.tsx | 12 +--- .../TokenSelector/AddCustomTokenTab.tsx | 57 ++++++++++++++----- .../TokenSelector/TokensTab.tsx | 2 +- .../TokenAndAmount/TokenSelector/index.tsx | 28 ++------- web/src/utils/fetchOwnedTokensFromAlchemy.ts | 1 + web/src/utils/validateAddress.ts | 6 ++ 10 files changed, 100 insertions(+), 63 deletions(-) create mode 100644 web/src/utils/validateAddress.ts diff --git a/web/src/components/PreviewCard/Terms/Description.tsx b/web/src/components/PreviewCard/Terms/Description.tsx index da0479d..7e2e83e 100644 --- a/web/src/components/PreviewCard/Terms/Description.tsx +++ b/web/src/components/PreviewCard/Terms/Description.tsx @@ -43,7 +43,7 @@ const Description: React.FC = ({ By Paying {sendingQuantity}{" "} {assetSymbol ? assetSymbol : }, address{" "} {buyerAddress} should receive "{deliverableText}" from address {sellerAddress} before the delivery deadline{" "} - {new Date(deadlineDate).toLocaleString()}. + {new Date(deadlineDate).toString()}. ); @@ -51,7 +51,7 @@ const Description: React.FC = ({ <> 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(deadlineDate).toLocaleString()}. + deadline {new Date(deadlineDate).toString()}. ); diff --git a/web/src/hooks/useEscrowTimelineItems.tsx b/web/src/hooks/useEscrowTimelineItems.tsx index 2b4140e..1e96938 100644 --- a/web/src/hooks/useEscrowTimelineItems.tsx +++ b/web/src/hooks/useEscrowTimelineItems.tsx @@ -6,8 +6,8 @@ import { resolutionToString } from "utils/resolutionToString"; import { formatTimeoutDuration } from "utils/formatTimeoutDuration"; import CheckCircleOutlineIcon from "components/StyledIcons/CheckCircleOutlineIcon"; import LawBalanceIcon from "components/StyledIcons/LawBalanceIcon"; -import { DisputeRequest, HasToPayFee, Payment, SettlementProposal, TransactionResolved } from "src/graphql/graphql"; import { StyledSkeleton } from "components/StyledSkeleton"; +import { DisputeRequest, HasToPayFee, Payment, SettlementProposal, TransactionResolved } from "src/graphql/graphql"; interface TimelineItem { title: string; diff --git a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx index 9aad4b3..9c67a77 100644 --- a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx +++ b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx @@ -11,11 +11,18 @@ import { } from "hooks/contracts/generated"; import { erc20ABI, useNetwork } from "wagmi"; import { useNewTransactionContext } from "context/NewTransactionContext"; -import { useAccount, useEnsAddress, usePublicClient, useContractWrite, usePrepareContractWrite } from "wagmi"; +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"; const StyledButton = styled(Button)``; @@ -35,6 +42,7 @@ const DepositPaymentButton: React.FC = () => { const [currentTime, setCurrentTime] = useState(Date.now()); const [finalRecipientAddress, setFinalRecipientAddress] = useState(sellerAddress); + const [checkedAllowance, setCheckedAllowance] = useState(false); const ensResult = useEnsAddress({ name: sellerAddress, chainId: 1 }); const publicClient = usePublicClient(); const navigate = useNavigate(); @@ -138,14 +146,36 @@ const DepositPaymentButton: React.FC = () => { const { writeAsync: approve } = useContractWrite(approveConfig); + const { data: allowance } = useContractRead({ + address: sendingToken, + abi: erc20ABI, + functionName: "allowance", + args: [address, escrowUniversalAddress?.[chain?.id]], + }); + + useEffect(() => { + if (allowance !== undefined && !checkedAllowance) { + setCheckedAllowance(true); + setIsApproved(allowance >= transactionValue); + } + }, [allowance, transactionValue, checkedAllowance]); + const handleApproveToken = async () => { if (!isUndefined(approve)) { setIsSending(true); try { - await wrapWithToast(async () => await approve().then((response) => response.hash), publicClient); - setIsApproved(true); + const wrapResult = await wrapWithToast( + async () => await approve().then((response) => response.hash), + publicClient + ); + if (wrapResult.status) { + setIsApproved(true); + } else { + setIsApproved(false); + } } catch (error) { console.error("Approval failed:", error); + setIsApproved(false); } finally { setIsSending(false); } @@ -176,7 +206,7 @@ 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/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/TokenSelector/AddCustomTokenTab.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx index 750ea6e..0bc8f3c 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx @@ -1,27 +1,56 @@ -import React from "react"; +import React, { useState } from "react"; import { Button, Field } from "@kleros/ui-components-library"; +import { fetchTokenInfo } from "utils/fetchTokenInfo"; +import { validateAddress } from "utils/validateAddress"; interface IAddCustomTokenTab { - customToken: string; - setCustomToken: (value: string) => void; - handleAddCustomToken: () => void; - validateAddress: (address: string) => boolean; + setOwnedTokens: (tokens: any[]) => void; + setActiveTab: (tab: string) => void; + alchemy: any; } -const AddCustomTokenTab: React.FC = ({ - customToken, - setCustomToken, - handleAddCustomToken, - validateAddress, -}) => { +const AddCustomTokenTab: React.FC = ({ setOwnedTokens, setActiveTab, alchemy }) => { + const [customToken, setCustomToken] = useState(""); + const [customTokenError, setCustomTokenError] = useState(""); + const handleCustomTokenChange = (e: React.ChangeEvent) => { - setCustomToken(e.target.value); + const value = e.target.value; + if (value.length <= 42) { + setCustomToken(value); + } + }; + + const handleAddCustomToken = async () => { + try { + const info = await fetchTokenInfo(alchemy, customToken); + if (info.symbol === "Unknown") { + throw new Error("Token information not found"); + } + + setOwnedTokens((prevTokens) => { + const newTokens = [...prevTokens, { label: info.symbol, value: customToken, logo: info.logo }]; + localStorage.setItem("ownedTokens", JSON.stringify(newTokens)); + return newTokens; + }); + setCustomToken(""); + setActiveTab("tokens"); + setCustomTokenError(""); + } catch (error) { + setCustomTokenError("Token information not found"); + } }; return (
- -
); }; diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx index bf795df..072310c 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Field } from "@kleros/ui-components-library"; -import { Item, TokenLabel, TokenLogo } from "."; +import { Item, TokenLabel, TokenLogo } from "../TokenSelector"; interface ITokensTab { searchQuery: string; 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 index c79bf00..50dfb00 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useRef } from "react"; import styled from "styled-components"; import Skeleton from "react-loading-skeleton"; -import "react-loading-skeleton/dist/skeleton.css"; import { useClickAway } from "react-use"; import { Tabs } from "@kleros/ui-components-library"; import { useAccount, useNetwork } from "wagmi"; @@ -10,12 +9,11 @@ import alchemyConfig from "utils/alchemyConfig"; import { useNewTransactionContext } from "context/NewTransactionContext"; import { fetchNativeToken } from "utils/fetchNativeToken"; import { fetchOwnedTokensFromAlchemy } from "utils/fetchOwnedTokensFromAlchemy"; -import { fetchTokenInfo } from "utils/fetchTokenInfo"; -import { validateAddress } from "../../../DestinationAddress"; +import { Overlay } from "components/Overlay"; import TokensTab from "./TokensTab"; import AddCustomTokenTab from "./AddCustomTokenTab"; import { StyledModal } from "pages/MyTransactions/Modal/StyledModal"; -import { Overlay } from "components/Overlay"; +import { useLocalStorage } from "hooks/useLocalStorage"; const Container = styled.div` position: relative; @@ -75,8 +73,7 @@ const TokenSelector: React.FC = () => { const { address } = useAccount(); const { chain } = useNetwork(); const { sendingToken, setSendingToken } = useNewTransactionContext(); - const [ownedTokens, setOwnedTokens] = useState([]); - const [customToken, setCustomToken] = useState(""); + const [ownedTokens, setOwnedTokens] = useLocalStorage("ownedTokens", []); const [isOpen, setIsOpen] = useState(false); const [activeTab, setActiveTab] = useState("tokens"); const [searchQuery, setSearchQuery] = useState(""); @@ -90,17 +87,7 @@ const TokenSelector: React.FC = () => { fetchOwnedTokensFromAlchemy(alchemy, address, setOwnedTokens, setLoading, chain); setSendingToken(fetchNativeToken(chain).value); } - }, [address, chain, setSendingToken]); - - const handleAddCustomToken = async () => { - if (!validateAddress(customToken)) { - alert("Invalid address"); - return; - } - const tokenInfo = await fetchTokenInfo(alchemy, customToken); - setOwnedTokens([...ownedTokens, { label: tokenInfo.symbol, value: customToken, logo: tokenInfo.logo }]); - setCustomToken(""); - }; + }, [address, chain]); const handleSelectToken = (value: string) => { setSendingToken(value); @@ -157,12 +144,7 @@ const TokenSelector: React.FC = () => { /> )} {activeTab === "addCustomToken" && ( - + )} diff --git a/web/src/utils/fetchOwnedTokensFromAlchemy.ts b/web/src/utils/fetchOwnedTokensFromAlchemy.ts index 2b72c45..97dfdd6 100644 --- a/web/src/utils/fetchOwnedTokensFromAlchemy.ts +++ b/web/src/utils/fetchOwnedTokensFromAlchemy.ts @@ -16,6 +16,7 @@ export const fetchOwnedTokensFromAlchemy = async (alchemy, address: string, setO }); const allTokens = [nativeToken, ...(await Promise.all(tokenList))]; setOwnedTokens(allTokens); + localStorage.setItem("ownedTokens", JSON.stringify(allTokens)); setLoading(false); } catch (error) { console.error("Error fetching owned tokens:", error); 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); +}; From b5564ccd888040e0c4ae37dea1530cd05266d0c5 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Tue, 21 May 2024 13:51:02 +0200 Subject: [PATCH 05/16] fix: fix dates --- .../PreviewCard/Terms/Description.tsx | 8 +++---- .../components/PreviewCard/Terms/index.tsx | 24 ++++++++++--------- web/src/components/PreviewCard/index.tsx | 11 ++++----- web/src/components/TransactionInfo/index.tsx | 8 +++---- .../TransactionDetails/index.tsx | 2 +- .../pages/NewTransaction/Preview/index.tsx | 18 +++++++------- 6 files changed, 36 insertions(+), 35 deletions(-) diff --git a/web/src/components/PreviewCard/Terms/Description.tsx b/web/src/components/PreviewCard/Terms/Description.tsx index 7e2e83e..208a386 100644 --- a/web/src/components/PreviewCard/Terms/Description.tsx +++ b/web/src/components/PreviewCard/Terms/Description.tsx @@ -21,7 +21,7 @@ interface IDescription { sendingQuantity: string; sendingToken: string; sellerAddress: string; - deadlineDate: Date; + deadline: number; assetSymbol: string; buyer: string; } @@ -35,7 +35,7 @@ const Description: React.FC = ({ sendingQuantity, sendingToken, sellerAddress, - deadlineDate, + deadline, assetSymbol, }) => { const generalEscrowSummary = ( @@ -43,7 +43,7 @@ const Description: React.FC = ({ By Paying {sendingQuantity}{" "} {assetSymbol ? assetSymbol : }, address{" "} {buyerAddress} should receive "{deliverableText}" from address {sellerAddress} before the delivery deadline{" "} - {new Date(deadlineDate).toString()}. + {new Date(deadline).toString()}. ); @@ -51,7 +51,7 @@ const Description: React.FC = ({ <> 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(deadlineDate).toString()}. + deadline {new Date(deadline).toString()}. ); diff --git a/web/src/components/PreviewCard/Terms/index.tsx b/web/src/components/PreviewCard/Terms/index.tsx index 888b442..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; @@ -32,7 +32,7 @@ const Terms: React.FC = ({ buyerAddress, sendingQuantity, sellerAddress, - deadlineDate, + deadline, assetSymbol, extraDescriptionUri, }) => { @@ -40,16 +40,18 @@ const Terms: React.FC = ({
- + ); }; diff --git a/web/src/components/PreviewCard/index.tsx b/web/src/components/PreviewCard/index.tsx index 6768441..27ddc3f 100644 --- a/web/src/components/PreviewCard/index.tsx +++ b/web/src/components/PreviewCard/index.tsx @@ -54,11 +54,10 @@ interface IPreviewCard { receivingQuantity: string; transactionCreationTimestamp: string; status: string; - token: string; buyerAddress: string; sendingQuantity: string; sellerAddress: string; - deadlineDate: string; + deadline: number; assetSymbol: string; overrideIsList: boolean; extraDescriptionUri: string; @@ -80,11 +79,10 @@ const PreviewCard: React.FC = ({ receivingQuantity, transactionCreationTimestamp, status, - token, buyerAddress, sendingQuantity, sellerAddress, - deadlineDate, + deadline, assetSymbol, overrideIsList, extraDescriptionUri, @@ -104,9 +102,8 @@ const PreviewCard: React.FC = ({ @@ -118,7 +115,7 @@ const PreviewCard: React.FC = ({ buyerAddress, sendingQuantity, sellerAddress, - deadlineDate, + deadline, assetSymbol, extraDescriptionUri, }} diff --git a/web/src/components/TransactionInfo/index.tsx b/web/src/components/TransactionInfo/index.tsx index 5ca5af8..e2012d3 100644 --- a/web/src/components/TransactionInfo/index.tsx +++ b/web/src/components/TransactionInfo/index.tsx @@ -71,7 +71,7 @@ const RestOfFieldsContainer = styled.div<{ isList?: boolean; isPreview?: boolean export interface ITransactionInfo { amount?: string; - deadlineDate: string; + deadline: number; assetSymbol?: string; status?: Statuses; overrideIsList?: boolean; @@ -83,7 +83,7 @@ export interface ITransactionInfo { const TransactionInfo: React.FC = ({ amount, assetSymbol, - deadlineDate, + deadline, sellerAddress, buyerAddress, overrideIsList, @@ -108,11 +108,11 @@ const TransactionInfo: React.FC = ({ isPreview={isPreview} /> ) : null} - {deadlineDate ? ( + {deadline ? ( diff --git a/web/src/pages/MyTransactions/TransactionDetails/index.tsx b/web/src/pages/MyTransactions/TransactionDetails/index.tsx index c3de2b7..5a2b751 100644 --- a/web/src/pages/MyTransactions/TransactionDetails/index.tsx +++ b/web/src/pages/MyTransactions/TransactionDetails/index.tsx @@ -78,7 +78,7 @@ const TransactionDetails: React.FC = () => { sellerAddress={seller} transactionCreationTimestamp={timestamp} sendingQuantity={!isUndefined(amount) ? formatEther(amount) : ""} - deadlineDate={new Date(deadline * 1000).toLocaleString()} + deadline={deadline * 1000} assetSymbol={!token ? nativeTokenSymbol : erc20TokenSymbol} overrideIsList={false} amount={!isUndefined(amount) ? formatEther(amount) : ""} diff --git a/web/src/pages/NewTransaction/Preview/index.tsx b/web/src/pages/NewTransaction/Preview/index.tsx index d5364b7..e625360 100644 --- a/web/src/pages/NewTransaction/Preview/index.tsx +++ b/web/src/pages/NewTransaction/Preview/index.tsx @@ -37,18 +37,20 @@ const Preview: React.FC = () => {
From 2a2cbc14510d9d693b90a5b14d4397c9268872ee Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Tue, 21 May 2024 18:14:46 +0200 Subject: [PATCH 06/16] refactor: refactor deposit-payment-button component, improving performance, unnecessary hook calls, and fixing approve behavior --- .../DepositPaymentButton.tsx | 98 ++++++++----------- 1 file changed, 43 insertions(+), 55 deletions(-) diff --git a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx index 9c67a77..c00375c 100644 --- a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx +++ b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx @@ -28,7 +28,6 @@ const StyledButton = styled(Button)``; const DepositPaymentButton: React.FC = () => { const { - escrowType, escrowTitle, deliverableText, transactionUri, @@ -42,14 +41,17 @@ const DepositPaymentButton: React.FC = () => { const [currentTime, setCurrentTime] = useState(Date.now()); const [finalRecipientAddress, setFinalRecipientAddress] = useState(sellerAddress); - const [checkedAllowance, setCheckedAllowance] = useState(false); - const ensResult = useEnsAddress({ name: sellerAddress, chainId: 1 }); const publicClient = usePublicClient(); const navigate = useNavigate(); - const [isSending, setIsSending] = useState(false); - const [isApproved, setIsApproved] = useState(false); + 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 === "native"; + const transactionValue = isNativeTransaction ? parseEther(sendingQuantity) : parseUnits(sendingQuantity, 18); useEffect(() => { const intervalId = setInterval(() => setCurrentTime(Date.now()), 1000); @@ -57,18 +59,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 isNativeTransaction = sendingToken === "native"; + const { data: allowance } = useContractRead({ + enabled: !isNativeTransaction, + address: sendingToken, + 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, @@ -108,27 +116,23 @@ const DepositPaymentButton: React.FC = () => { Seller: sellerAddress, }, version: "1.0", - }; - - const stringifiedTemplateData = JSON.stringify(templateData); - - const transactionValue = isNativeTransaction ? parseEther(sendingQuantity) : parseUnits(sendingQuantity, 18); + }); const { config: createNativeTransactionConfig } = usePrepareEscrowUniversalCreateNativeTransaction({ - enabled: !isUndefined(ensResult) && ethAddressPattern.test(finalRecipientAddress) && isNativeTransaction, - args: [BigInt(Math.floor(timeoutPayment)), transactionUri, finalRecipientAddress, stringifiedTemplateData, ""], + enabled: isNativeTransaction && ethAddressPattern.test(finalRecipientAddress), + args: [BigInt(Math.floor(timeoutPayment)), transactionUri, finalRecipientAddress, templateData, ""], value: transactionValue, }); const { config: createERC20TransactionConfig } = usePrepareEscrowUniversalCreateErc20Transaction({ - enabled: !isUndefined(ensResult) && ethAddressPattern.test(finalRecipientAddress) && !isNativeTransaction, + enabled: !isNativeTransaction && ethAddressPattern.test(finalRecipientAddress), args: [ transactionValue, sendingToken, BigInt(Math.floor(timeoutPayment)), transactionUri, finalRecipientAddress, - stringifiedTemplateData, + templateData, "", ], }); @@ -138,6 +142,7 @@ const DepositPaymentButton: React.FC = () => { const { writeAsync: createERC20Transaction } = useEscrowUniversalCreateErc20Transaction(createERC20TransactionConfig); const { config: approveConfig } = usePrepareContractWrite({ + enabled: !isNativeTransaction, address: sendingToken, abi: erc20ABI, functionName: "approve", @@ -146,20 +151,6 @@ const DepositPaymentButton: React.FC = () => { const { writeAsync: approve } = useContractWrite(approveConfig); - const { data: allowance } = useContractRead({ - address: sendingToken, - abi: erc20ABI, - functionName: "allowance", - args: [address, escrowUniversalAddress?.[chain?.id]], - }); - - useEffect(() => { - if (allowance !== undefined && !checkedAllowance) { - setCheckedAllowance(true); - setIsApproved(allowance >= transactionValue); - } - }, [allowance, transactionValue, checkedAllowance]); - const handleApproveToken = async () => { if (!isUndefined(approve)) { setIsSending(true); @@ -168,11 +159,7 @@ const DepositPaymentButton: React.FC = () => { async () => await approve().then((response) => response.hash), publicClient ); - if (wrapResult.status) { - setIsApproved(true); - } else { - setIsApproved(false); - } + setIsApproved(wrapResult.status); } catch (error) { console.error("Approval failed:", error); setIsApproved(false); @@ -186,19 +173,20 @@ const DepositPaymentButton: React.FC = () => { 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) { + resetContext(); + navigate("/my-transactions/display/1/desc/all"); + } + } catch (error) { + console.error("Transaction failed:", error); + } finally { + setIsSending(false); + } } }; From caa1dc6c582856bb813383b0ed89b952e681c834 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Tue, 21 May 2024 19:18:33 +0200 Subject: [PATCH 07/16] fix: padding issue creating extra width in the screen --- web/src/pages/NewTransaction/Preview/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/src/pages/NewTransaction/Preview/index.tsx b/web/src/pages/NewTransaction/Preview/index.tsx index e625360..355ecdb 100644 --- a/web/src/pages/NewTransaction/Preview/index.tsx +++ b/web/src/pages/NewTransaction/Preview/index.tsx @@ -3,7 +3,6 @@ import styled from "styled-components"; 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"; @@ -13,7 +12,6 @@ const Container = styled.div` display: flex; align-items: center; flex-direction: column; - padding: 0 ${responsiveSize(24, 136)}; `; const Preview: React.FC = () => { From d5b6c58ba4ca28380f01e84b6eafa2df43aed939 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Tue, 21 May 2024 20:45:18 +0200 Subject: [PATCH 08/16] feat: few performance improvements in hook calls, styling --- web/src/hooks/useERC20TokenSymbol.ts | 2 +- .../DepositPaymentButton.tsx | 6 +++++- .../pages/NewTransaction/Preview/index.tsx | 9 ++++---- .../TokenAndAmount/TokenSelector/index.tsx | 21 +++++++++++++++---- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/web/src/hooks/useERC20TokenSymbol.ts b/web/src/hooks/useERC20TokenSymbol.ts index e45d801..cfdb495 100644 --- a/web/src/hooks/useERC20TokenSymbol.ts +++ b/web/src/hooks/useERC20TokenSymbol.ts @@ -9,7 +9,7 @@ export const useERC20TokenSymbol = (tokenAddress: string) => { useEffect(() => { const fetchTokenSymbol = async () => { - if (!tokenAddress) return; + if (!tokenAddress || tokenAddress === "native") return; const alchemy = new Alchemy(alchemyConfig(chain?.id)); try { const metadata = await alchemy.core.getTokenMetadata(tokenAddress); diff --git a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx index c00375c..c24c025 100644 --- a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx +++ b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx @@ -125,7 +125,11 @@ const DepositPaymentButton: React.FC = () => { }); const { config: createERC20TransactionConfig } = usePrepareEscrowUniversalCreateErc20Transaction({ - enabled: !isNativeTransaction && ethAddressPattern.test(finalRecipientAddress), + enabled: + !isNativeTransaction && + !isUndefined(allowance) && + allowance >= transactionValue && + ethAddressPattern.test(finalRecipientAddress), args: [ transactionValue, sendingToken, diff --git a/web/src/pages/NewTransaction/Preview/index.tsx b/web/src/pages/NewTransaction/Preview/index.tsx index 355ecdb..a468ac7 100644 --- a/web/src/pages/NewTransaction/Preview/index.tsx +++ b/web/src/pages/NewTransaction/Preview/index.tsx @@ -1,11 +1,11 @@ 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 { useNewTransactionContext } from "context/NewTransactionContext"; import { useNativeTokenSymbol } from "hooks/useNativeTokenSymbol"; -import { useAccount } from "wagmi"; import { useERC20TokenSymbol } from "hooks/useERC20TokenSymbol"; const Container = styled.div` @@ -26,6 +26,7 @@ const Preview: React.FC = () => { deadline, extraDescriptionUri, } = useNewTransactionContext(); + const isNativeTransaction = sendingToken === "native"; const nativeTokenSymbol = useNativeTokenSymbol(); const { erc20TokenSymbol } = useERC20TokenSymbol(sendingToken); @@ -36,7 +37,7 @@ const Preview: React.FC = () => {
theme.primaryText}; `; +const DropdownContent = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const StyledLogoSkeleton = styled(Skeleton)` + width: 22.5px; + height: 22.5px; + margin-left: 2px; + border-radius: 50%; +`; + const TokenSelector: React.FC = () => { const { address } = useAccount(); const { chain } = useNetwork(); @@ -102,9 +115,9 @@ const TokenSelector: React.FC = () => { setIsOpen(!isOpen)}> -
+ {loading ? ( - + ) : ( sendingToken && ( { ) )} {loading ? ( - + ) : sendingToken ? ( ownedTokens.find((token) => token.value === sendingToken)?.label ) : ( "Select a token" )} -
+
{isOpen && ( From 4b4ff852865d6d07b7431c80f1c6cb5ccdb27a62 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Wed, 22 May 2024 19:17:51 +0200 Subject: [PATCH 09/16] feat: save tokens in localStorage and added custom tokens too --- web/src/hooks/useERC20TokenSymbol.ts | 2 +- .../TokenSelector/AddCustomTokenTab.tsx | 13 ++++--- .../TokenAndAmount/TokenSelector/index.tsx | 37 ++++++++++++------- web/src/utils/alchemyConfig.ts | 4 +- web/src/utils/fetchOwnedTokensFromAlchemy.ts | 25 ------------- web/src/utils/fetchTokenInfo.ts | 6 ++- web/src/utils/initializeTokens.ts | 31 ++++++++++++++++ 7 files changed, 67 insertions(+), 51 deletions(-) delete mode 100644 web/src/utils/fetchOwnedTokensFromAlchemy.ts create mode 100644 web/src/utils/initializeTokens.ts diff --git a/web/src/hooks/useERC20TokenSymbol.ts b/web/src/hooks/useERC20TokenSymbol.ts index cfdb495..deca518 100644 --- a/web/src/hooks/useERC20TokenSymbol.ts +++ b/web/src/hooks/useERC20TokenSymbol.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { Alchemy } from "alchemy-sdk"; import { useNetwork } from "wagmi"; -import alchemyConfig from "utils/alchemyConfig"; +import { alchemyConfig } from "utils/alchemyConfig"; export const useERC20TokenSymbol = (tokenAddress: string) => { const { chain } = useNetwork(); diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx index 0bc8f3c..c5acfcf 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx @@ -1,15 +1,16 @@ import React, { useState } from "react"; import { Button, Field } from "@kleros/ui-components-library"; +import { Alchemy } from "alchemy-sdk"; import { fetchTokenInfo } from "utils/fetchTokenInfo"; import { validateAddress } from "utils/validateAddress"; interface IAddCustomTokenTab { - setOwnedTokens: (tokens: any[]) => void; + setTokens: (tokens: any[]) => void; setActiveTab: (tab: string) => void; - alchemy: any; + alchemyInstance: Alchemy; } -const AddCustomTokenTab: React.FC = ({ setOwnedTokens, setActiveTab, alchemy }) => { +const AddCustomTokenTab: React.FC = ({ setTokens, setActiveTab, alchemyInstance }) => { const [customToken, setCustomToken] = useState(""); const [customTokenError, setCustomTokenError] = useState(""); @@ -22,14 +23,14 @@ const AddCustomTokenTab: React.FC = ({ setOwnedTokens, setAc const handleAddCustomToken = async () => { try { - const info = await fetchTokenInfo(alchemy, customToken); + const info = await fetchTokenInfo(customToken, alchemyInstance); if (info.symbol === "Unknown") { throw new Error("Token information not found"); } - setOwnedTokens((prevTokens) => { + setTokens((prevTokens) => { const newTokens = [...prevTokens, { label: info.symbol, value: customToken, logo: info.logo }]; - localStorage.setItem("ownedTokens", JSON.stringify(newTokens)); + localStorage.setItem("tokens", JSON.stringify(newTokens)); return newTokens; }); setCustomToken(""); 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 index f403b96..e646ac2 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx @@ -3,12 +3,11 @@ import styled from "styled-components"; import Skeleton from "react-loading-skeleton"; import { useClickAway } from "react-use"; import { Tabs } from "@kleros/ui-components-library"; -import { useAccount, useNetwork } from "wagmi"; import { Alchemy } from "alchemy-sdk"; -import alchemyConfig from "utils/alchemyConfig"; +import { useAccount, useNetwork } from "wagmi"; import { useNewTransactionContext } from "context/NewTransactionContext"; -import { fetchNativeToken } from "utils/fetchNativeToken"; -import { fetchOwnedTokensFromAlchemy } from "utils/fetchOwnedTokensFromAlchemy"; +import { initializeTokens } from "utils/initializeTokens"; +import { alchemyConfig } from "utils/alchemyConfig"; import { Overlay } from "components/Overlay"; import TokensTab from "./TokensTab"; import AddCustomTokenTab from "./AddCustomTokenTab"; @@ -86,30 +85,36 @@ const TokenSelector: React.FC = () => { const { address } = useAccount(); const { chain } = useNetwork(); const { sendingToken, setSendingToken } = useNewTransactionContext(); - const [ownedTokens, setOwnedTokens] = useLocalStorage("ownedTokens", []); + const [tokens, setTokens] = useLocalStorage("tokens", []); const [isOpen, setIsOpen] = useState(false); const [activeTab, setActiveTab] = useState("tokens"); const [searchQuery, setSearchQuery] = useState(""); const containerRef = useRef(null); const [loading, setLoading] = useState(true); - const alchemy = new Alchemy(alchemyConfig(chain?.id)); + const alchemyInstance = new Alchemy(alchemyConfig(chain?.id)); useClickAway(containerRef, () => setIsOpen(false)); useEffect(() => { if (address && chain) { - fetchOwnedTokensFromAlchemy(alchemy, address, setOwnedTokens, setLoading, chain); - setSendingToken(fetchNativeToken(chain).value); + initializeTokens(address, setTokens, setLoading, chain, alchemyInstance); } }, [address, chain]); + useEffect(() => { + if (tokens.length > 0) { + const nativeToken = tokens.find((token) => token.value === "native"); + setSendingToken(localStorage.getItem("selectedToken") || nativeToken.value); + } + }, [tokens]); + const handleSelectToken = (value: string) => { setSendingToken(value); + localStorage.setItem("selectedToken", value); setIsOpen(false); }; - const filteredTokens = ownedTokens.filter((token) => - token?.label?.toLowerCase().includes(searchQuery?.toLowerCase()) - ); + const filteredTokens = + tokens && tokens.filter((token) => token?.label?.toLowerCase().includes(searchQuery?.toLowerCase())); return ( @@ -121,7 +126,7 @@ const TokenSelector: React.FC = () => { ) : ( sendingToken && ( token.value === sendingToken)?.logo} + src={tokens.find((token) => token.value === sendingToken)?.logo} alt={`${sendingToken} logo`} /> ) @@ -129,7 +134,7 @@ const TokenSelector: React.FC = () => { {loading ? ( ) : sendingToken ? ( - ownedTokens.find((token) => token.value === sendingToken)?.label + tokens.find((token) => token.value === sendingToken)?.label ) : ( "Select a token" )} @@ -157,7 +162,11 @@ const TokenSelector: React.FC = () => { /> )} {activeTab === "addCustomToken" && ( - + )} diff --git a/web/src/utils/alchemyConfig.ts b/web/src/utils/alchemyConfig.ts index 80933df..b7ebe80 100644 --- a/web/src/utils/alchemyConfig.ts +++ b/web/src/utils/alchemyConfig.ts @@ -1,9 +1,7 @@ import { alchemyApiKey } from "context/Web3Provider"; import { mapWagmiNetworkToAlchemyNetwork } from "utils/mapWagmiNetworkToAlchemyNetwork"; -const alchemyConfig = (chainId: number) => ({ +export const alchemyConfig = (chainId: number) => ({ apiKey: alchemyApiKey, network: mapWagmiNetworkToAlchemyNetwork(chainId), }); - -export default alchemyConfig; diff --git a/web/src/utils/fetchOwnedTokensFromAlchemy.ts b/web/src/utils/fetchOwnedTokensFromAlchemy.ts deleted file mode 100644 index 97dfdd6..0000000 --- a/web/src/utils/fetchOwnedTokensFromAlchemy.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { fetchNativeToken } from "./fetchNativeToken"; -import { fetchTokenInfo } from "./fetchTokenInfo"; - -export const fetchOwnedTokensFromAlchemy = async (alchemy, address: string, setOwnedTokens, setLoading, chain) => { - try { - setLoading(true); - const nativeToken = fetchNativeToken(chain); - const balances = await alchemy.core.getTokenBalances(address); - const tokenList = balances.tokenBalances.map(async (token) => { - const tokenInfo = await fetchTokenInfo(alchemy, token.contractAddress); - return { - label: tokenInfo.symbol, - value: token.contractAddress, - logo: tokenInfo.logo, - }; - }); - const allTokens = [nativeToken, ...(await Promise.all(tokenList))]; - setOwnedTokens(allTokens); - localStorage.setItem("ownedTokens", JSON.stringify(allTokens)); - setLoading(false); - } catch (error) { - console.error("Error fetching owned tokens:", error); - setLoading(false); - } -}; diff --git a/web/src/utils/fetchTokenInfo.ts b/web/src/utils/fetchTokenInfo.ts index add28b3..9ae0643 100644 --- a/web/src/utils/fetchTokenInfo.ts +++ b/web/src/utils/fetchTokenInfo.ts @@ -1,6 +1,8 @@ -export const fetchTokenInfo = async (alchemy, address: string) => { +import { Alchemy } from "alchemy-sdk"; + +export const fetchTokenInfo = async (address: string, alchemyInstance: Alchemy) => { try { - const metadata = await alchemy.core.getTokenMetadata(address); + const metadata = await alchemyInstance.core.getTokenMetadata(address); return { symbol: metadata.symbol?.toUpperCase() || "Unknown", logo: metadata.logo || "https://via.placeholder.com/24", diff --git a/web/src/utils/initializeTokens.ts b/web/src/utils/initializeTokens.ts new file mode 100644 index 0000000..a114fdf --- /dev/null +++ b/web/src/utils/initializeTokens.ts @@ -0,0 +1,31 @@ +import { Alchemy } from "alchemy-sdk"; +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 = balances.tokenBalances.map(async (token) => { + const tokenInfo = await fetchTokenInfo(token.contractAddress, alchemyInstance); + return { + label: tokenInfo.symbol, + value: token.contractAddress, + logo: tokenInfo.logo, + }; + }); + const allTokens = [nativeToken, ...(await Promise.all(tokenList))]; + const customTokens = JSON.parse(localStorage.getItem("tokens")) || []; + const combinedTokens = [ + ...allTokens, + ...customTokens.filter((ct) => !allTokens.some((token) => token.value === ct.value)), + ]; + setTokens(combinedTokens); + localStorage.setItem("tokens", JSON.stringify(combinedTokens)); + setLoading(false); + } catch (error) { + console.error("Error initializing tokens:", error); + setLoading(false); + } +}; From c4b26cb1194e2b8e6b11f7f350b75ccd9913e436 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Wed, 22 May 2024 20:54:08 +0200 Subject: [PATCH 10/16] refactor: remove unnecessary hook call by refactoring code --- web/src/context/NewTransactionContext.tsx | 14 +++++----- .../DepositPaymentButton.tsx | 10 +++---- .../pages/NewTransaction/Preview/index.tsx | 6 ++--- .../TokenSelector/AddCustomTokenTab.tsx | 2 +- .../TokenSelector/TokensTab.tsx | 10 +++---- .../TokenAndAmount/TokenSelector/index.tsx | 27 ++++++------------- .../TokenAndAmount/index.tsx | 2 +- web/src/utils/fetchNativeToken.ts | 4 +-- web/src/utils/initializeTokens.ts | 6 ++--- 9 files changed, 35 insertions(+), 46 deletions(-) diff --git a/web/src/context/NewTransactionContext.tsx b/web/src/context/NewTransactionContext.tsx index 41dd623..50a7579 100644 --- a/web/src/context/NewTransactionContext.tsx +++ b/web/src/context/NewTransactionContext.tsx @@ -23,8 +23,8 @@ interface INewTransactionContext { setSellerAddress: (address: string) => void; sendingQuantity: string; setSendingQuantity: (quantity: string) => void; - sendingToken: string; - setSendingToken: (token: string) => void; + sendingToken: { address: string; symbol: string }; + setSendingToken: (token: { address: string; symbol: string }) => void; buyerAddress: string; setBuyerAddress: (address: string) => void; deadline: string; @@ -61,7 +61,7 @@ const NewTransactionContext = createContext({ setSellerAddress: () => {}, sendingQuantity: "", setSendingQuantity: () => {}, - sendingToken: "", + sendingToken: { address: "", symbol: "" }, setSendingToken: () => {}, buyerAddress: "", setBuyerAddress: () => {}, @@ -93,7 +93,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<{ address: string; symbol: string }>( + JSON.parse(localStorage.getItem("sendingToken")) || { address: "", symbol: "" } + ); const [buyerAddress, setBuyerAddress] = useState(localStorage.getItem("buyerAddress") || ""); const [isRecipientAddressResolved, setIsRecipientAddressResolved] = useState(false); const [deadline, setDeadline] = useState(localStorage.getItem("deadline") || ""); @@ -111,7 +113,7 @@ export const NewTransactionProvider: React.FC<{ children: React.ReactNode }> = ( setReceivingToken(""); setSellerAddress(""); setSendingQuantity(""); - setSendingToken(""); + setSendingToken({ address: "", symbol: "" }); setBuyerAddress(""); setDeadline(""); setNotificationEmail(""); @@ -128,7 +130,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/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx index c24c025..db60d0e 100644 --- a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx +++ b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx @@ -50,7 +50,7 @@ const DepositPaymentButton: React.FC = () => { const ensResult = useEnsAddress({ name: sellerAddress, chainId: 1 }); const deadlineTimestamp = new Date(deadline).getTime(); const timeoutPayment = (deadlineTimestamp - currentTime) / 1000; - const isNativeTransaction = sendingToken === "native"; + const isNativeTransaction = sendingToken?.address === "native"; const transactionValue = isNativeTransaction ? parseEther(sendingQuantity) : parseUnits(sendingQuantity, 18); useEffect(() => { @@ -64,7 +64,7 @@ const DepositPaymentButton: React.FC = () => { const { data: allowance } = useContractRead({ enabled: !isNativeTransaction, - address: sendingToken, + address: sendingToken?.address, abi: erc20ABI, functionName: "allowance", args: [address, escrowUniversalAddress?.[chain?.id]], @@ -105,7 +105,7 @@ const DepositPaymentButton: React.FC = () => { buyer: address, seller: sellerAddress, amount: sendingQuantity, - token: isNativeTransaction ? "native" : sendingToken, + token: isNativeTransaction ? "native" : sendingToken?.address, timeoutPayment: timeoutPayment, transactionUri: transactionUri, }, @@ -132,7 +132,7 @@ const DepositPaymentButton: React.FC = () => { ethAddressPattern.test(finalRecipientAddress), args: [ transactionValue, - sendingToken, + sendingToken?.address, BigInt(Math.floor(timeoutPayment)), transactionUri, finalRecipientAddress, @@ -147,7 +147,7 @@ const DepositPaymentButton: React.FC = () => { const { config: approveConfig } = usePrepareContractWrite({ enabled: !isNativeTransaction, - address: sendingToken, + address: sendingToken?.address, abi: erc20ABI, functionName: "approve", args: [escrowUniversalAddress?.[chain?.id], transactionValue], diff --git a/web/src/pages/NewTransaction/Preview/index.tsx b/web/src/pages/NewTransaction/Preview/index.tsx index a468ac7..891e45a 100644 --- a/web/src/pages/NewTransaction/Preview/index.tsx +++ b/web/src/pages/NewTransaction/Preview/index.tsx @@ -6,7 +6,6 @@ import PreviewCard from "components/PreviewCard"; import Header from "./Header"; import NavigationButtons from "../NavigationButtons"; import { useNativeTokenSymbol } from "hooks/useNativeTokenSymbol"; -import { useERC20TokenSymbol } from "hooks/useERC20TokenSymbol"; const Container = styled.div` display: flex; @@ -26,9 +25,8 @@ const Preview: React.FC = () => { deadline, extraDescriptionUri, } = useNewTransactionContext(); - const isNativeTransaction = sendingToken === "native"; + const isNativeTransaction = sendingToken.address === "native"; const nativeTokenSymbol = useNativeTokenSymbol(); - const { erc20TokenSymbol } = useERC20TokenSymbol(sendingToken); const { address } = useAccount(); @@ -37,7 +35,7 @@ const Preview: React.FC = () => {
= ({ setTokens, setActiveT } setTokens((prevTokens) => { - const newTokens = [...prevTokens, { label: info.symbol, value: customToken, logo: info.logo }]; + const newTokens = [...prevTokens, { symbol: info.symbol, address: customToken, logo: info.logo }]; localStorage.setItem("tokens", JSON.stringify(newTokens)); return newTokens; }); diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx index 072310c..882f02a 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx @@ -5,8 +5,8 @@ import { Item, TokenLabel, TokenLogo } from "../TokenSelector"; interface ITokensTab { searchQuery: string; setSearchQuery: (value: string) => void; - filteredTokens: { label: string; value: string; logo: string }[]; - handleSelectToken: (value: string) => void; + filteredTokens: { symbol: string; address: string; logo: string }[]; + handleSelectToken: (token: { symbol: string; address: string; logo: string }) => void; } const TokensTab: React.FC = ({ searchQuery, setSearchQuery, filteredTokens, handleSelectToken }) => { @@ -18,9 +18,9 @@ const TokensTab: React.FC = ({ searchQuery, setSearchQuery, filtered
{filteredTokens.map((token) => ( - handleSelectToken(token.value)}> - - {token.label} + handleSelectToken(token)}> + + {token.symbol} ))}
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 index e646ac2..0f8a78b 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx @@ -102,19 +102,19 @@ const TokenSelector: React.FC = () => { useEffect(() => { if (tokens.length > 0) { - const nativeToken = tokens.find((token) => token.value === "native"); - setSendingToken(localStorage.getItem("selectedToken") || nativeToken.value); + const nativeToken = tokens.find((token) => token.address === "native"); + setSendingToken(JSON.parse(localStorage.getItem("selectedToken")) || nativeToken); } }, [tokens]); - const handleSelectToken = (value: string) => { - setSendingToken(value); - localStorage.setItem("selectedToken", value); + const handleSelectToken = (token) => { + setSendingToken(token); + localStorage.setItem("selectedToken", JSON.stringify(token)); setIsOpen(false); }; const filteredTokens = - tokens && tokens.filter((token) => token?.label?.toLowerCase().includes(searchQuery?.toLowerCase())); + tokens && tokens.filter((token) => token?.symbol?.toLowerCase().includes(searchQuery?.toLowerCase())); return ( @@ -124,20 +124,9 @@ const TokenSelector: React.FC = () => { {loading ? ( ) : ( - sendingToken && ( - token.value === sendingToken)?.logo} - alt={`${sendingToken} logo`} - /> - ) - )} - {loading ? ( - - ) : sendingToken ? ( - tokens.find((token) => token.value === sendingToken)?.label - ) : ( - "Select a token" + sendingToken && )} + {loading ? : sendingToken ? sendingToken.symbol : "Select a token"} 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 d6130d0..c95f9f0 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx @@ -25,7 +25,7 @@ const TokenAndAmount: React.FC = ({ quantity, setQuantity }) => const { sendingToken, setHasSufficientNativeBalance } = useNewTransactionContext(); const { data: balanceData } = useBalance({ address: address, - token: sendingToken === "native" ? undefined : sendingToken, + token: sendingToken?.address === "native" ? undefined : sendingToken?.address, }); const [error, setError] = useState(""); diff --git a/web/src/utils/fetchNativeToken.ts b/web/src/utils/fetchNativeToken.ts index c6a2900..379bc46 100644 --- a/web/src/utils/fetchNativeToken.ts +++ b/web/src/utils/fetchNativeToken.ts @@ -1,7 +1,7 @@ export const fetchNativeToken = (chain) => { return { - label: chain?.nativeCurrency?.symbol || "Native Token", - value: "native", + symbol: chain?.nativeCurrency?.symbol || "Native Token", + address: "native", logo: "https://assets.coingecko.com/coins/images/279/thumb/ethereum.png", }; }; diff --git a/web/src/utils/initializeTokens.ts b/web/src/utils/initializeTokens.ts index a114fdf..677c536 100644 --- a/web/src/utils/initializeTokens.ts +++ b/web/src/utils/initializeTokens.ts @@ -10,8 +10,8 @@ export const initializeTokens = async (address: string, setTokens, setLoading, c const tokenList = balances.tokenBalances.map(async (token) => { const tokenInfo = await fetchTokenInfo(token.contractAddress, alchemyInstance); return { - label: tokenInfo.symbol, - value: token.contractAddress, + symbol: tokenInfo.symbol, + address: token.contractAddress, logo: tokenInfo.logo, }; }); @@ -19,7 +19,7 @@ export const initializeTokens = async (address: string, setTokens, setLoading, c const customTokens = JSON.parse(localStorage.getItem("tokens")) || []; const combinedTokens = [ ...allTokens, - ...customTokens.filter((ct) => !allTokens.some((token) => token.value === ct.value)), + ...customTokens.filter((customToken) => !allTokens.some((token) => token.address === customToken.address)), ]; setTokens(combinedTokens); localStorage.setItem("tokens", JSON.stringify(combinedTokens)); From 026635aedffa3cf48b3d91fcfa0bff6bd0073abc Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Thu, 23 May 2024 21:08:18 +0200 Subject: [PATCH 11/16] feat: simplify token widget, manage searching for address --- .../TokenSelector/AddCustomTokenTab.tsx | 59 -------- .../TokenSelector/TokensTab.tsx | 30 ---- .../TokenAndAmount/TokenSelector/index.tsx | 142 +++++++++++++----- web/src/utils/initializeTokens.ts | 20 +-- 4 files changed, 115 insertions(+), 136 deletions(-) delete mode 100644 web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx delete mode 100644 web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx deleted file mode 100644 index 04f7416..0000000 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useState } from "react"; -import { Button, Field } from "@kleros/ui-components-library"; -import { Alchemy } from "alchemy-sdk"; -import { fetchTokenInfo } from "utils/fetchTokenInfo"; -import { validateAddress } from "utils/validateAddress"; - -interface IAddCustomTokenTab { - setTokens: (tokens: any[]) => void; - setActiveTab: (tab: string) => void; - alchemyInstance: Alchemy; -} - -const AddCustomTokenTab: React.FC = ({ setTokens, setActiveTab, alchemyInstance }) => { - const [customToken, setCustomToken] = useState(""); - const [customTokenError, setCustomTokenError] = useState(""); - - const handleCustomTokenChange = (e: React.ChangeEvent) => { - const value = e.target.value; - if (value.length <= 42) { - setCustomToken(value); - } - }; - - const handleAddCustomToken = async () => { - try { - const info = await fetchTokenInfo(customToken, alchemyInstance); - if (info.symbol === "Unknown") { - throw new Error("Token information not found"); - } - - setTokens((prevTokens) => { - const newTokens = [...prevTokens, { symbol: info.symbol, address: customToken, logo: info.logo }]; - localStorage.setItem("tokens", JSON.stringify(newTokens)); - return newTokens; - }); - setCustomToken(""); - setActiveTab("tokens"); - setCustomTokenError(""); - } catch (error) { - setCustomTokenError("Token information not found"); - } - }; - - return ( -
- -
- ); -}; - -export default AddCustomTokenTab; diff --git a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx deleted file mode 100644 index 882f02a..0000000 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import { Field } from "@kleros/ui-components-library"; -import { Item, TokenLabel, TokenLogo } from "../TokenSelector"; - -interface ITokensTab { - searchQuery: string; - setSearchQuery: (value: string) => void; - filteredTokens: { symbol: string; address: string; logo: string }[]; - handleSelectToken: (token: { symbol: string; address: string; logo: string }) => void; -} - -const TokensTab: React.FC = ({ searchQuery, setSearchQuery, filteredTokens, handleSelectToken }) => { - const handleSearch = (e: React.ChangeEvent) => { - setSearchQuery(e.target.value); - }; - - return ( -
- - {filteredTokens.map((token) => ( - handleSelectToken(token)}> - - {token.symbol} - - ))} -
- ); -}; - -export default TokensTab; 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 index 0f8a78b..ac384ba 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx @@ -2,15 +2,13 @@ import React, { useState, useEffect, useRef } from "react"; import styled from "styled-components"; import Skeleton from "react-loading-skeleton"; import { useClickAway } from "react-use"; -import { Tabs } from "@kleros/ui-components-library"; +import { Searchbar } from "@kleros/ui-components-library"; 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 { Overlay } from "components/Overlay"; -import TokensTab from "./TokensTab"; -import AddCustomTokenTab from "./AddCustomTokenTab"; import { StyledModal } from "pages/MyTransactions/Modal/StyledModal"; import { useLocalStorage } from "hooks/useLocalStorage"; @@ -48,15 +46,25 @@ const DropdownArrow = styled.span` margin-left: 8px; `; -export const Item = styled.div` +export const Item = styled.div<{ selected: boolean }>` display: flex; align-items: center; gap: 8px; - padding: 8px; + padding: 10px 16px; cursor: pointer; &:hover { - background: ${({ theme }) => theme.mediumBlue}; + background: ${({ theme }) => theme.lightBlue}; } + ${({ selected, theme }) => + selected && + ` + background: ${theme.mediumBlue}; + border-left: 3px solid ${theme.primaryBlue}; + padding-left: 13px; + &:hover { + background: ${theme.mediumBlue}; + } + `} `; export const TokenLogo = styled.img` @@ -81,13 +89,40 @@ const StyledLogoSkeleton = styled(Skeleton)` border-radius: 50%; `; -const TokenSelector: React.FC = () => { +const ReStyledModal = styled(StyledModal)` + display: flex; + 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; +`; + +const TokenSelector = () => { const { address } = useAccount(); const { chain } = useNetwork(); const { sendingToken, setSendingToken } = useNewTransactionContext(); - const [tokens, setTokens] = useLocalStorage("tokens", []); + const [tokens, setTokens] = useLocalStorage("tokens", []); + const [filteredTokens, setFilteredTokens] = useState([]); const [isOpen, setIsOpen] = useState(false); - const [activeTab, setActiveTab] = useState("tokens"); const [searchQuery, setSearchQuery] = useState(""); const containerRef = useRef(null); const [loading, setLoading] = useState(true); @@ -101,11 +136,11 @@ const TokenSelector: React.FC = () => { }, [address, chain]); useEffect(() => { - if (tokens.length > 0) { + if (tokens?.length > 0) { const nativeToken = tokens.find((token) => token.address === "native"); setSendingToken(JSON.parse(localStorage.getItem("selectedToken")) || nativeToken); } - }, [tokens]); + }, [tokens, setSendingToken]); const handleSelectToken = (token) => { setSendingToken(token); @@ -113,8 +148,44 @@ const TokenSelector: React.FC = () => { setIsOpen(false); }; - const filteredTokens = - tokens && tokens.filter((token) => token?.symbol?.toLowerCase().includes(searchQuery?.toLowerCase())); + const handleSearch = async (query) => { + setSearchQuery(query); + + if (!query) { + setFilteredTokens(tokens); + return; + } + + const isAddress = query.startsWith("0x") && query.length === 42; + if (isAddress) { + try { + const metadata = await alchemyInstance.core.getTokenMetadata(query.toLowerCase()); + const resultToken = { + symbol: metadata.symbol, + address: query.toLowerCase(), + logo: metadata.logo || "https://via.placeholder.com/24", + }; + + const updatedTokens = [...tokens, resultToken]; + const uniqueTokens = Array.from(new Set(updatedTokens.map((a) => a.address))).map((address) => { + return updatedTokens.find((a) => a.address === address); + }); + + setFilteredTokens([resultToken]); + setTokens(uniqueTokens); + localStorage.setItem("tokens", JSON.stringify(uniqueTokens)); + } catch (error) { + console.error("Error fetching token info:", error); + } + } else { + const filteredTokens = tokens.filter((token) => token.symbol.toLowerCase().includes(query.toLowerCase())); + setFilteredTokens(filteredTokens); + } + }; + + const tokensToDisplay = searchQuery + ? filteredTokens + : [sendingToken, ...tokens.filter((token) => token.address !== sendingToken?.address)]; return ( @@ -126,38 +197,33 @@ const TokenSelector: React.FC = () => { ) : ( sendingToken && )} - {loading ? : sendingToken ? sendingToken.symbol : "Select a token"} + {loading ? : sendingToken?.symbol} {isOpen && ( <> - - + Select a token + handleSearch(e.target.value)} /> - {activeTab === "tokens" && ( - - )} - {activeTab === "addCustomToken" && ( - - )} - + + {tokensToDisplay?.map((token) => ( + handleSelectToken(token)} + selected={sendingToken?.address === token.address} + > + + {token.symbol} + + ))} + + )} diff --git a/web/src/utils/initializeTokens.ts b/web/src/utils/initializeTokens.ts index 677c536..34a8325 100644 --- a/web/src/utils/initializeTokens.ts +++ b/web/src/utils/initializeTokens.ts @@ -7,15 +7,17 @@ export const initializeTokens = async (address: string, setTokens, setLoading, c setLoading(true); const nativeToken = fetchNativeToken(chain); const balances = await alchemyInstance.core.getTokenBalances(address); - const tokenList = balances.tokenBalances.map(async (token) => { - const tokenInfo = await fetchTokenInfo(token.contractAddress, alchemyInstance); - return { - symbol: tokenInfo.symbol, - address: token.contractAddress, - logo: tokenInfo.logo, - }; - }); - const allTokens = [nativeToken, ...(await Promise.all(tokenList))]; + const tokenList = await Promise.all( + balances.tokenBalances.map(async (token) => { + const tokenInfo = await fetchTokenInfo(token.contractAddress, alchemyInstance); + return { + symbol: tokenInfo.symbol, + address: token.contractAddress, + logo: tokenInfo.logo, + }; + }) + ); + const allTokens = [nativeToken, ...tokenList]; const customTokens = JSON.parse(localStorage.getItem("tokens")) || []; const combinedTokens = [ ...allTokens, From e94026094c139d3632ac762068d0580557872398 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Fri, 24 May 2024 12:36:23 +0200 Subject: [PATCH 12/16] fix: tiny padding adjustment --- .../GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ac384ba..a799195 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx @@ -28,7 +28,7 @@ const TokenSelectorWrapper = styled.div` const DropdownButton = styled.div` border: 1px solid ${({ theme }) => theme.stroke}; border-radius: 3px; - padding: 8px; + padding: 9.5px 8px; cursor: pointer; background: ${({ theme }) => theme.whiteBackground}; color: ${({ theme }) => theme.primaryText}; From 1ff8771b64e319aa998bdc1961a0da2b81c42608 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Sat, 25 May 2024 00:31:01 +0530 Subject: [PATCH 13/16] feat(web): query-refetch-hook --- web/src/hooks/useQueryRefetch.ts | 28 +++++++++++++++++++ .../AcceptSettlementButton.tsx | 3 ++ .../ProposeSettlementButton.tsx | 8 ++++-- .../PreviewCardButtons/RaiseDisputeButton.tsx | 21 ++++++++------ .../PreviewCardButtons/TimeOutButton.tsx | 4 +++ .../Buttons/ReleasePaymentButton.tsx | 3 ++ .../DepositPaymentButton.tsx | 3 ++ 7 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 web/src/hooks/useQueryRefetch.ts 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/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/NewTransaction/NavigationButtons/DepositPaymentButton.tsx b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx index db60d0e..8bee098 100644 --- a/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx +++ b/web/src/pages/NewTransaction/NavigationButtons/DepositPaymentButton.tsx @@ -23,6 +23,7 @@ import { parseEther, parseUnits } from "viem"; import { isUndefined } from "utils/index"; import { wrapWithToast } from "utils/wrapWithToast"; import { ethAddressPattern } from "utils/validateAddress"; +import { useQueryRefetch } from "hooks/useQueryRefetch"; const StyledButton = styled(Button)``; @@ -43,6 +44,7 @@ const DepositPaymentButton: React.FC = () => { const [finalRecipientAddress, setFinalRecipientAddress] = useState(sellerAddress); const publicClient = usePublicClient(); const navigate = useNavigate(); + const refetchQuery = useQueryRefetch(); const [isSending, setIsSending] = useState(false); const [isApproved, setIsApproved] = useState(false); const { address } = useAccount(); @@ -183,6 +185,7 @@ const DepositPaymentButton: React.FC = () => { publicClient ); if (wrapResult.status) { + refetchQuery([["refetchOnBlock", "useMyTransactionsQuery"], ["useUserQuery"]]); resetContext(); navigate("/my-transactions/display/1/desc/all"); } From 09b3596a84b4bbc2d064782117a4deefca274484 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Fri, 24 May 2024 21:37:09 +0200 Subject: [PATCH 14/16] feat: modularize token-selector component, better default logo icons, abstract token metadata --- web/src/assets/svgs/icons/eth-token-icon.png | Bin 0 -> 15763 bytes web/src/components/TransactionCard/index.tsx | 5 +- web/src/hooks/useFilteredTokens.ts | 46 +++++ ...RC20TokenSymbol.ts => useTokenMetadata.ts} | 16 +- .../TransactionDetails/index.tsx | 5 +- .../TokenSelector/DropdownButton.tsx | 64 ++++++ .../TokenSelector/TokenItem.tsx | 35 ++++ .../TokenSelector/TokenListModal.tsx | 68 +++++++ .../TokenAndAmount/TokenSelector/index.tsx | 188 +----------------- web/src/utils/fetchNativeToken.ts | 6 +- web/src/utils/fetchTokenInfo.ts | 5 +- 11 files changed, 240 insertions(+), 198 deletions(-) create mode 100644 web/src/assets/svgs/icons/eth-token-icon.png create mode 100644 web/src/hooks/useFilteredTokens.ts rename web/src/hooks/{useERC20TokenSymbol.ts => useTokenMetadata.ts} (56%) create mode 100644 web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/DropdownButton.tsx create mode 100644 web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem.tsx create mode 100644 web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenListModal.tsx 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 0000000000000000000000000000000000000000..5366d8aabfb021ccda48ca8dbd1c861b529481c5 GIT binary patch literal 15763 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 P^iTWpHTAqpR{sA7!= = ({ const transactionInfo = useFetchIpfsJson(transactionUri); const { isList } = useIsList(); const nativeTokenSymbol = useNativeTokenSymbol(); - const { erc20TokenSymbol } = useERC20TokenSymbol(token); + const { tokenMetadata } = useTokenMetadata(token); + const erc20TokenSymbol = tokenMetadata?.symbol; const title = transactionInfo?.title; const navigateAndScrollTop = useNavigateAndScrollTop(); diff --git a/web/src/hooks/useFilteredTokens.ts b/web/src/hooks/useFilteredTokens.ts new file mode 100644 index 0000000..7e03a07 --- /dev/null +++ b/web/src/hooks/useFilteredTokens.ts @@ -0,0 +1,46 @@ +import { useEffect, useState } from "react"; +import { useTokenMetadata } from "./useTokenMetadata"; +import EthTokenIcon from "svgs/icons/eth-token-icon.png"; + +export const useFilteredTokens = (searchQuery: string, tokens, setTokens, sendingToken) => { + 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/useERC20TokenSymbol.ts b/web/src/hooks/useTokenMetadata.ts similarity index 56% rename from web/src/hooks/useERC20TokenSymbol.ts rename to web/src/hooks/useTokenMetadata.ts index deca518..878d882 100644 --- a/web/src/hooks/useERC20TokenSymbol.ts +++ b/web/src/hooks/useTokenMetadata.ts @@ -3,25 +3,25 @@ import { Alchemy } from "alchemy-sdk"; import { useNetwork } from "wagmi"; import { alchemyConfig } from "utils/alchemyConfig"; -export const useERC20TokenSymbol = (tokenAddress: string) => { +export const useTokenMetadata = (tokenAddress: string) => { const { chain } = useNetwork(); - const [erc20TokenSymbol, setErc20TokenSymbol] = useState(null); + const [tokenMetadata, setTokenMetadata] = useState(null); useEffect(() => { - const fetchTokenSymbol = async () => { + const fetchTokenMetadata = async () => { if (!tokenAddress || tokenAddress === "native") return; const alchemy = new Alchemy(alchemyConfig(chain?.id)); try { const metadata = await alchemy.core.getTokenMetadata(tokenAddress); - setErc20TokenSymbol(metadata.symbol || null); + setTokenMetadata(metadata); } catch (error) { - console.error("Error fetching token symbol:", error); - setErc20TokenSymbol(null); + console.error("Error fetching token metadata:", error); + setTokenMetadata(null); } }; - fetchTokenSymbol(); + fetchTokenMetadata(); }, [tokenAddress, chain?.id]); - return { erc20TokenSymbol }; + return { tokenMetadata }; }; diff --git a/web/src/pages/MyTransactions/TransactionDetails/index.tsx b/web/src/pages/MyTransactions/TransactionDetails/index.tsx index 5a2b751..f718fe5 100644 --- a/web/src/pages/MyTransactions/TransactionDetails/index.tsx +++ b/web/src/pages/MyTransactions/TransactionDetails/index.tsx @@ -12,8 +12,8 @@ import { useEscrowParametersQuery } from "queries/useEscrowParametersQuery"; import { useTransactionDetailsQuery } from "queries/useTransactionsQuery"; import { useArbitrationCost } from "queries/useArbitrationCostFromKlerosCore"; import { useNativeTokenSymbol } from "hooks/useNativeTokenSymbol"; -import { useERC20TokenSymbol } from "hooks/useERC20TokenSymbol"; import useFetchIpfsJson from "hooks/useFetchIpfsJson"; +import { useTokenMetadata } from "hooks/useTokenMetadata"; const Container = styled.div``; @@ -33,7 +33,8 @@ const TransactionDetails: React.FC = () => { const { data: escrowParameters } = useEscrowParametersQuery(); const { arbitrationCost } = useArbitrationCost(escrowParameters?.escrowParameters?.arbitratorExtraData); const nativeTokenSymbol = useNativeTokenSymbol(); - const { erc20TokenSymbol } = useERC20TokenSymbol(transactionDetails?.escrow?.token); + const { tokenMetadata } = useTokenMetadata(transactionDetails?.escrow?.token); + const erc20TokenSymbol = tokenMetadata?.symbol; const { setTransactionDetails } = useTransactionDetailsContext(); const { 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.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem.tsx new file mode 100644 index 0000000..2eefaca --- /dev/null +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div<{ selected: boolean }>` + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + cursor: pointer; + 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 TokenLogo = styled.img` + width: 24px; + height: 24px; +`; + +const TokenLabel = styled.span` + color: ${({ theme }) => theme.primaryText}; +`; + +const TokenItem = ({ token, selected, onSelect }) => ( + 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..e1cd412 --- /dev/null +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenListModal.tsx @@ -0,0 +1,68 @@ +import React, { useRef, useState } from "react"; +import styled 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"; + +const ReStyledModal = styled(StyledModal)` + display: flex; + 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 index a799195..feae307 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx @@ -1,129 +1,28 @@ import React, { useState, useEffect, useRef } from "react"; import styled from "styled-components"; -import Skeleton from "react-loading-skeleton"; import { useClickAway } from "react-use"; -import { Searchbar } from "@kleros/ui-components-library"; 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 { Overlay } from "components/Overlay"; -import { StyledModal } from "pages/MyTransactions/Modal/StyledModal"; import { useLocalStorage } from "hooks/useLocalStorage"; +import { DropdownButton } from "./DropdownButton"; +import { TokenListModal } from "./TokenListModal"; const Container = styled.div` - position: relative; - width: 186px; - height: 45px; -`; - -const TokenSelectorWrapper = styled.div` display: flex; flex-direction: column; gap: 8px; position: relative; `; -const DropdownButton = styled.div` - border: 1px solid ${({ theme }) => theme.stroke}; - border-radius: 3px; - padding: 9.5px 8px; - 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; -`; - -export const Item = styled.div<{ selected: boolean }>` - display: flex; - align-items: center; - gap: 8px; - padding: 10px 16px; - cursor: pointer; - &:hover { - background: ${({ theme }) => theme.lightBlue}; - } - ${({ selected, theme }) => - selected && - ` - background: ${theme.mediumBlue}; - border-left: 3px solid ${theme.primaryBlue}; - padding-left: 13px; - &:hover { - background: ${theme.mediumBlue}; - } - `} -`; - -export const TokenLogo = styled.img` - width: 24px; - height: 24px; -`; - -export const TokenLabel = styled.span` - color: ${({ theme }) => theme.primaryText}; -`; - -const DropdownContent = styled.div` - display: flex; - align-items: center; - gap: 8px; -`; - -const StyledLogoSkeleton = styled(Skeleton)` - width: 22.5px; - height: 22.5px; - margin-left: 2px; - border-radius: 50%; -`; - -const ReStyledModal = styled(StyledModal)` - display: flex; - 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; -`; - const TokenSelector = () => { const { address } = useAccount(); const { chain } = useNetwork(); const { sendingToken, setSendingToken } = useNewTransactionContext(); const [tokens, setTokens] = useLocalStorage("tokens", []); - const [filteredTokens, setFilteredTokens] = useState([]); const [isOpen, setIsOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); const containerRef = useRef(null); const [loading, setLoading] = useState(true); const alchemyInstance = new Alchemy(alchemyConfig(chain?.id)); @@ -148,86 +47,11 @@ const TokenSelector = () => { setIsOpen(false); }; - const handleSearch = async (query) => { - setSearchQuery(query); - - if (!query) { - setFilteredTokens(tokens); - return; - } - - const isAddress = query.startsWith("0x") && query.length === 42; - if (isAddress) { - try { - const metadata = await alchemyInstance.core.getTokenMetadata(query.toLowerCase()); - const resultToken = { - symbol: metadata.symbol, - address: query.toLowerCase(), - logo: metadata.logo || "https://via.placeholder.com/24", - }; - - const updatedTokens = [...tokens, resultToken]; - const uniqueTokens = Array.from(new Set(updatedTokens.map((a) => a.address))).map((address) => { - return updatedTokens.find((a) => a.address === address); - }); - - setFilteredTokens([resultToken]); - setTokens(uniqueTokens); - localStorage.setItem("tokens", JSON.stringify(uniqueTokens)); - } catch (error) { - console.error("Error fetching token info:", error); - } - } else { - const filteredTokens = tokens.filter((token) => token.symbol.toLowerCase().includes(query.toLowerCase())); - setFilteredTokens(filteredTokens); - } - }; - - const tokensToDisplay = searchQuery - ? filteredTokens - : [sendingToken, ...tokens.filter((token) => token.address !== sendingToken?.address)]; - return ( - - - setIsOpen(!isOpen)}> - - {loading ? ( - - ) : ( - sendingToken && - )} - {loading ? : sendingToken?.symbol} - - - - {isOpen && ( - <> - - - Select a token - handleSearch(e.target.value)} - /> - - {tokensToDisplay?.map((token) => ( - handleSelectToken(token)} - selected={sendingToken?.address === token.address} - > - - {token.symbol} - - ))} - - - - )} - - + + setIsOpen(!isOpen)} /> + {isOpen && } + ); }; diff --git a/web/src/utils/fetchNativeToken.ts b/web/src/utils/fetchNativeToken.ts index 379bc46..fea5078 100644 --- a/web/src/utils/fetchNativeToken.ts +++ b/web/src/utils/fetchNativeToken.ts @@ -1,7 +1,9 @@ +import EthTokenIcon from "svgs/icons/eth-token-icon.png"; + export const fetchNativeToken = (chain) => { return { - symbol: chain?.nativeCurrency?.symbol || "Native Token", + symbol: chain?.nativeCurrency?.symbol, address: "native", - logo: "https://assets.coingecko.com/coins/images/279/thumb/ethereum.png", + logo: EthTokenIcon, }; }; diff --git a/web/src/utils/fetchTokenInfo.ts b/web/src/utils/fetchTokenInfo.ts index 9ae0643..47a31d4 100644 --- a/web/src/utils/fetchTokenInfo.ts +++ b/web/src/utils/fetchTokenInfo.ts @@ -1,14 +1,15 @@ import { Alchemy } from "alchemy-sdk"; +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 || "https://via.placeholder.com/24", + logo: metadata.logo || EthTokenIcon, }; } catch (error) { console.error("Error fetching token info:", error); - return { symbol: "Unknown", logo: "https://via.placeholder.com/24" }; + return { symbol: "Unknown", logo: EthTokenIcon }; } }; From 5c6de4adf29021ba30f278ee1755048c8b0aeaab Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Mon, 27 May 2024 17:51:13 +0200 Subject: [PATCH 15/16] feat: add token balances in popup, styling, skeletons, maxbalance, itoken type --- web/src/context/NewTransactionContext.tsx | 18 +++-- web/src/hooks/useFilteredTokens.ts | 8 ++- .../TokenAndAmount/AmountField.tsx | 2 +- .../TokenAndAmount/MaxBalance.tsx | 68 +++++++++++++++++++ .../TokenSelector/TokenItem/Balance.tsx | 41 +++++++++++ .../{TokenItem.tsx => TokenItem/index.tsx} | 27 ++++++-- .../TokenAndAmount/TokenSelector/index.tsx | 2 +- .../TokenAndAmount/index.tsx | 38 +++++++++-- web/src/utils/fetchTokenInfo.ts | 4 +- web/src/utils/format.ts | 4 +- web/src/utils/getFormattedBalance.ts | 7 ++ web/src/utils/initializeTokens.ts | 5 +- 12 files changed, 196 insertions(+), 28 deletions(-) create mode 100644 web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/MaxBalance.tsx create mode 100644 web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem/Balance.tsx rename web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/{TokenItem.tsx => TokenItem/index.tsx} (57%) create mode 100644 web/src/utils/getFormattedBalance.ts diff --git a/web/src/context/NewTransactionContext.tsx b/web/src/context/NewTransactionContext.tsx index 50a7579..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: { address: string; symbol: string }; - setSendingToken: (token: { address: string; symbol: 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: { address: "", symbol: "" }, + sendingToken: { address: "native", symbol: "", logo: "" }, setSendingToken: () => {}, buyerAddress: "", setBuyerAddress: () => {}, @@ -93,8 +99,8 @@ 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<{ address: string; symbol: string }>( - JSON.parse(localStorage.getItem("sendingToken")) || { address: "", symbol: "" } + 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); @@ -113,7 +119,7 @@ export const NewTransactionProvider: React.FC<{ children: React.ReactNode }> = ( setReceivingToken(""); setSellerAddress(""); setSendingQuantity(""); - setSendingToken({ address: "", symbol: "" }); + setSendingToken({ address: "native", symbol: "", logo: "" }); setBuyerAddress(""); setDeadline(""); setNotificationEmail(""); diff --git a/web/src/hooks/useFilteredTokens.ts b/web/src/hooks/useFilteredTokens.ts index 7e03a07..a7cf974 100644 --- a/web/src/hooks/useFilteredTokens.ts +++ b/web/src/hooks/useFilteredTokens.ts @@ -1,8 +1,14 @@ 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, setTokens, sendingToken) => { +export const useFilteredTokens = ( + searchQuery: string, + tokens: IToken[], + setTokens: (tokens: IToken) => void, + sendingToken: IToken +) => { const { tokenMetadata } = useTokenMetadata( searchQuery.startsWith("0x") && searchQuery.length === 42 ? searchQuery : null ); 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 d3902fe..b1bd0ec 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/AmountField.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/AmountField.tsx @@ -34,7 +34,7 @@ const AmountField: React.FC = ({ quantity, setQuantity, error }) = value={quantity} onChange={handleWrite} type="number" - placeholder="eg. 3.6" + placeholder="Amount" variant={error ? "error" : undefined} message={error} /> 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/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.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem/index.tsx similarity index 57% rename from web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem.tsx rename to web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem/index.tsx index 2eefaca..05124bf 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenItem/index.tsx @@ -1,12 +1,13 @@ import React from "react"; import styled from "styled-components"; +import Balance from "./Balance"; const Container = styled.div<{ selected: boolean }>` display: flex; align-items: center; - gap: 8px; 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")}; @@ -16,6 +17,13 @@ const Container = styled.div<{ selected: boolean }>` } `; +const LogoAndLabel = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +`; + const TokenLogo = styled.img` width: 24px; height: 24px; @@ -25,11 +33,16 @@ const TokenLabel = styled.span` color: ${({ theme }) => theme.primaryText}; `; -const TokenItem = ({ token, selected, onSelect }) => ( - onSelect(token)}> - - {token.symbol} - -); +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/index.tsx b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx index feae307..89b4983 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx @@ -17,7 +17,7 @@ const Container = styled.div` position: relative; `; -const TokenSelector = () => { +const TokenSelector: React.FC = () => { const { address } = useAccount(); const { chain } = useNetwork(); const { sendingToken, setSendingToken } = useNewTransactionContext(); 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 c95f9f0..4d9d558 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx @@ -1,18 +1,33 @@ -import React, { useState, useEffect } 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 { responsiveSize } from "styles/responsiveSize"; +import { getFormattedBalance } from "utils/getFormattedBalance"; import AmountField from "./AmountField"; import TokenSelector from "./TokenSelector"; +import MaxBalance from "./MaxBalance"; const Container = styled.div` display: flex; flex-direction: row; + justify-content: center; gap: 24px; - align-items: center; - margin-bottom: ${responsiveSize(24, 18)}; - margin-left: 36px; + margin-bottom: ${responsiveSize(16, 0)}; + flex-wrap: wrap; + + ${landscapeStyle( + () => css` + margin-left: 24px; + ` + )} +`; + +const TokenSelectorAndMaxBalance = styled.div` + display: flex; + flex-direction: column; + gap: 4px; `; interface ITokenAndAmount { @@ -45,10 +60,19 @@ const TokenAndAmount: React.FC = ({ quantity, setQuantity }) => } }, [balanceData, quantity, setHasSufficientNativeBalance]); + const formattedBalance = useMemo(() => getFormattedBalance(balanceData, sendingToken), [balanceData, sendingToken]); + return ( - + + + + ); }; diff --git a/web/src/utils/fetchTokenInfo.ts b/web/src/utils/fetchTokenInfo.ts index 47a31d4..5278485 100644 --- a/web/src/utils/fetchTokenInfo.ts +++ b/web/src/utils/fetchTokenInfo.ts @@ -1,4 +1,5 @@ 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) => { @@ -7,7 +8,8 @@ export const fetchTokenInfo = async (address: string, alchemyInstance: Alchemy) 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 index 34a8325..f19a154 100644 --- a/web/src/utils/initializeTokens.ts +++ b/web/src/utils/initializeTokens.ts @@ -1,4 +1,5 @@ import { Alchemy } from "alchemy-sdk"; +import { IToken } from "context/NewTransactionContext"; import { fetchNativeToken } from "./fetchNativeToken"; import { fetchTokenInfo } from "./fetchTokenInfo"; @@ -12,9 +13,9 @@ export const initializeTokens = async (address: string, setTokens, setLoading, c const tokenInfo = await fetchTokenInfo(token.contractAddress, alchemyInstance); return { symbol: tokenInfo.symbol, - address: token.contractAddress, + address: tokenInfo.address, logo: tokenInfo.logo, - }; + } as IToken; }) ); const allTokens = [nativeToken, ...tokenList]; From 82dc13db8fef9cfcc7e12635d469a46b81b6266a Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Tue, 28 May 2024 13:59:52 +0530 Subject: [PATCH 16/16] fix(web): styling-of-payment-input-and-token-selector --- .../GeneralTransaction/TokenAndAmount/AmountField.tsx | 3 ++- .../TokenAndAmount/TokenSelector/TokenListModal.tsx | 11 +++++++++-- .../GeneralTransaction/TokenAndAmount/index.tsx | 6 ------ 3 files changed, 11 insertions(+), 9 deletions(-) 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 b1bd0ec..99b63f1 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/AmountField.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/AmountField.tsx @@ -3,7 +3,7 @@ import styled from "styled-components"; import { Field } from "@kleros/ui-components-library"; 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; @@ -15,6 +15,7 @@ const StyledField = styled(Field)` input { font-size: 16px; + padding-right: ${({ variant }) => (variant ? "40px" : "16px")}; } `; 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 index e1cd412..0f6d1a8 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenListModal.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokenListModal.tsx @@ -1,5 +1,5 @@ import React, { useRef, useState } from "react"; -import styled from "styled-components"; +import styled, { css } from "styled-components"; import { useClickAway } from "react-use"; import { Searchbar } from "@kleros/ui-components-library"; import { useNewTransactionContext } from "context/NewTransactionContext"; @@ -7,10 +7,17 @@ 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: 500px; + width: ${responsiveSize(320, 500)}; + ${landscapeStyle( + () => css` + width: 500px; + ` + )} `; const StyledSearchbar = styled(Searchbar)` 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 4d9d558..317435b 100644 --- a/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx +++ b/web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/index.tsx @@ -16,12 +16,6 @@ const Container = styled.div` gap: 24px; margin-bottom: ${responsiveSize(16, 0)}; flex-wrap: wrap; - - ${landscapeStyle( - () => css` - margin-left: 24px; - ` - )} `; const TokenSelectorAndMaxBalance = styled.div`