From d6a3c6cb6b01201619e90fb2d5188cfbd811ff83 Mon Sep 17 00:00:00 2001 From: Alexandru G Date: Mon, 22 Jan 2024 12:18:16 +0200 Subject: [PATCH] Add Types --- package.json | 1 + src/components/Amount.tsx | 41 +++++++++++++++---- src/components/AssetInput.tsx | 12 +++++- src/components/AssetList.tsx | 11 ++++- src/components/AssetSelect.tsx | 6 +++ src/components/TokenAvatar.tsx | 9 +++- src/constants.ts | 26 +++++++++--- src/hooks/token.ts | 21 ++++++---- src/hooks/types.ts | 11 +++++ src/pages/Create/components/Creator.tsx | 6 +-- src/pages/Dashboard/components/Govnft.tsx | 20 +++++---- src/pages/Delegate/components/DelegateNft.tsx | 4 +- src/pages/Transfer/components/TransferNft.tsx | 4 +- yarn.lock | 33 +++++++++++++++ 14 files changed, 163 insertions(+), 42 deletions(-) create mode 100644 src/hooks/types.ts diff --git a/package.json b/package.json index 28081147..f511db3e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@tanstack/react-query": "^5.17.15", "buffer": "^6.0.3", "dayjs": "^1.11.9", + "ethers": "^6.10.0", "events": "^3.3.0", "flowbite-react": "0.6.0", "lodash": "*", diff --git a/src/components/Amount.tsx b/src/components/Amount.tsx index 4ef76da3..8f7046da 100644 --- a/src/components/Amount.tsx +++ b/src/components/Amount.tsx @@ -1,34 +1,59 @@ +import { FixedNumber } from "ethers"; import { Spinner } from "flowbite-react"; -import { formatUnits, parseUnits } from "viem"; import { useAccount } from "wagmi"; +import { CURRENCY_MAXIMUM_FRACTION_DIGITS } from "../constants"; import { useTokens } from "../hooks/token"; +import { Address } from "../hooks/types"; import TokenAvatar from "./TokenAvatar"; +const NUMBER_FORMAT = "en-US"; + +function prettify(value: FixedNumber) { + const ndigits = + value.cmp(FixedNumber.fromString("1")) === -1 + ? CURRENCY_MAXIMUM_FRACTION_DIGITS + : 2; + + // most numbers will look OK with standard US number formatting (e.g. 12.56) + // however, 0.00034 will look like 0 + // let's remedy this with custom formatting based on decimals value + return new Intl.NumberFormat(NUMBER_FORMAT, { + maximumFractionDigits: ndigits, + }).format( + // @ts-expect-error: format takes a string as well not just a number, so we need to disable TS + // Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/format#syntax + value.round(ndigits).toString(), + ); +} + export default function Amount({ amount, decimals = null, tokenAddress = "", symbol = null, showLogo = true, +}: { + amount: bigint | number; + decimals?: number | null; + tokenAddress?: string; + symbol?: string | null; + showLogo?: boolean; }) { const { address: accountAddress } = useAccount(); const { data: tokens } = useTokens(accountAddress); - if (!amount) { + if (amount == null) { return ; } - const addr = String(tokenAddress).toLowerCase(); - const token = tokens.filter((asset) => asset.address.includes(addr)); + const addr = String(tokenAddress).toLowerCase() as Address; + const token = tokens?.filter((asset) => asset.address.includes(addr)); // @ts-ignore const amountDecimals = decimals || token?.decimals || 18; - const ndigits = amount < parseUnits("1", amountDecimals) ? 5 : 2; - const pretty = Number(formatUnits(amount, amountDecimals)) - .toFixed(ndigits) - .toString(); + const pretty = prettify(FixedNumber.fromValue(amount, amountDecimals)); if (showLogo) { return ( diff --git a/src/components/AssetInput.tsx b/src/components/AssetInput.tsx index 548fc7d6..e733e7cf 100644 --- a/src/components/AssetInput.tsx +++ b/src/components/AssetInput.tsx @@ -2,6 +2,7 @@ import { Spinner, TextInput } from "flowbite-react"; import { useEffect, useState } from "react"; import { formatUnits, parseUnits } from "viem"; +import { Token } from "../hooks/types"; import ActionLink from "./ActionLink"; import Amount from "./Amount"; import AssetSelect from "./AssetSelect"; @@ -16,6 +17,15 @@ export default function AssetInput({ disabled = false, validate = true, title = "", +}: { + assets: Token[]; + asset: Token | null; + setAsset: (asset: Token | null) => void; + amount: bigint; + setAmount: (amount: bigint) => void; + disabled?: boolean; + validate?: boolean; + title?: string; }) { const [invalid, setInvalid] = useState(false); @@ -81,7 +91,7 @@ export default function AssetInput({ e.target.select()} onChange={(e) => !disabled && setValue(e.target.value)} diff --git a/src/components/AssetList.tsx b/src/components/AssetList.tsx index f8304e5f..57b0f37b 100644 --- a/src/components/AssetList.tsx +++ b/src/components/AssetList.tsx @@ -3,10 +3,19 @@ import { isEmpty } from "lodash"; import { AlertCircle as AlertCircleleIcon } from "lucide-react"; import { isAddress } from "viem"; +import { Token } from "../hooks/types"; import Amount from "./Amount"; import TokenAvatar from "./TokenAvatar"; -export default function AssetsList({ assets, onSelect, search = null }) { +export default function AssetsList({ + assets, + onSelect, + search = null, +}: { + assets: Token[]; + onSelect: (asset: Token | null) => void; + search?: string; +}) { if (isAddress(search) && isEmpty(assets)) { return (
diff --git a/src/components/AssetSelect.tsx b/src/components/AssetSelect.tsx index 83bbc6a2..75f387cf 100644 --- a/src/components/AssetSelect.tsx +++ b/src/components/AssetSelect.tsx @@ -6,6 +6,7 @@ import { } from "lucide-react"; import { useEffect, useState } from "react"; +import { Token } from "../hooks/types"; import AssetList from "./AssetList"; import Modal from "./Modal"; import TokenAvatar from "./TokenAvatar"; @@ -15,6 +16,11 @@ export default function AssetSelect({ assets, onSelect, className = "", +}: { + selectedAsset: Token | null; + assets: Token[]; + onSelect: (asset: Token | null) => void; + className?: string; }) { const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); diff --git a/src/components/TokenAvatar.tsx b/src/components/TokenAvatar.tsx index 7ea2ce44..e61beddc 100644 --- a/src/components/TokenAvatar.tsx +++ b/src/components/TokenAvatar.tsx @@ -7,8 +7,15 @@ import { TOKEN_ASSETS_CDN, TOKEN_ICON, } from "../constants"; +import { Address } from "../hooks/types"; -export default function TokenAvatar({ address, className = null }) { +export default function TokenAvatar({ + address, + className = null, +}: { + address: Address; + className?: string; +}) { if (!address) { return <>; } diff --git a/src/constants.ts b/src/constants.ts index b5a3dbb0..94a5ccdb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,11 +1,13 @@ import { optimism } from "viem/chains"; -export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +import { Address } from "./hooks/types"; + +export const ZERO_ADDRESS: Address = + "0x0000000000000000000000000000000000000000"; export const TOKEN_ICON = "/svg/coin.svg"; export const RPC_URI = import.meta.env.VITE_RPC_URI; -export const TOKEN_ADDRESSES = String( - import.meta.env.VITE_TOKEN_ADDRESSES, -).split(","); +export const TOKEN_ADDRESSES: Address[] = + import.meta.env.VITE_TOKEN_ADDRESSES.split(","); export const TOKEN_ASSETS_CDN = String( import.meta.env.VITE_TOKEN_ASSETS_CDN, ).split(","); @@ -13,9 +15,17 @@ export const DEFAULT_CHAIN = optimism; export const NATIVE_TOKEN_LOGO = import.meta.env.VITE_NATIVE_TOKEN_LOGO; export const NATIVE_TOKEN = { ...DEFAULT_CHAIN.nativeCurrency, - wrappedAddress: import.meta.env.VITE_WRAPPED_NATIVE_TOKEN.toLowerCase(), - address: DEFAULT_CHAIN.nativeCurrency.symbol.toLowerCase(), + wrappedAddress: + import.meta.env.VITE_WRAPPED_NATIVE_TOKEN.toLowerCase() as Address, + /** + * TODO: This is an exception for the native token where "address" is "ETH" + * - Change this later so it's type-safe + * Discussion: https://github.com/velodrome-finance/app/pull/347#discussion_r1380073009 + */ + + address: DEFAULT_CHAIN.nativeCurrency.symbol.toLowerCase() as Address, }; + export const WALLETCONNECT_PROJECT_ID = import.meta.env .VITE_WALLETCONNECT_PROJECT_ID; @@ -23,3 +33,7 @@ export const WALLETCONNECT_PROJECT_ID = import.meta.env export const FEATURE_FLAGS = String( import.meta.env.VITE_FEATURE_FLAGS ).split(","); + +export const CURRENCY_MAXIMUM_FRACTION_DIGITS = parseInt( + import.meta.env.VITE_CURRENCY_MAXIMUM_FRACTION_DIGITS || "5", +); diff --git a/src/hooks/token.ts b/src/hooks/token.ts index 64310d1c..3101d06d 100644 --- a/src/hooks/token.ts +++ b/src/hooks/token.ts @@ -2,16 +2,19 @@ import { useQuery } from "@tanstack/react-query"; import { getBalance } from "@wagmi/core"; import { TOKEN_ADDRESSES } from "../constants"; -import rpc from "../rpc"; +import config from "../rpc"; +import { Address, Token } from "./types"; -async function fetchTokens(accountAddress) { - const tokens = TOKEN_ADDRESSES.map(async (tokenAddress) => { - const token = await getBalance(rpc, { - address: accountAddress, - token: tokenAddress as `0x${string}`, - }); - return { ...token, address: tokenAddress }; - }); +async function fetchTokens(accountAddress: Address): Promise { + const tokens = TOKEN_ADDRESSES.map( + async (tokenAddress: Address): Promise => { + const token = await getBalance(config, { + address: accountAddress, + token: tokenAddress, + }); + return { ...token, address: tokenAddress }; + }, + ); return Promise.all(tokens); } diff --git a/src/hooks/types.ts b/src/hooks/types.ts new file mode 100644 index 00000000..fb405f1b --- /dev/null +++ b/src/hooks/types.ts @@ -0,0 +1,11 @@ +import { Address } from "viem"; + +export type { Address }; + +export type Token = { + address: Address; + decimals: number; + formatted: string; + symbol: string; + value: bigint; +}; diff --git a/src/pages/Create/components/Creator.tsx b/src/pages/Create/components/Creator.tsx index da8b69c3..b46f5663 100644 --- a/src/pages/Create/components/Creator.tsx +++ b/src/pages/Create/components/Creator.tsx @@ -6,12 +6,12 @@ import { ToggleSwitch, } from "flowbite-react"; import { useEffect, useState } from "react"; -import { parseUnits } from "viem"; -import { isAddress } from "viem"; +import { isAddress, parseUnits } from "viem"; import { useAccount } from "wagmi"; import AssetInput from "../../../components/AssetInput"; import { useTokens } from "../../../hooks/token"; +import { Token } from "../../../hooks/types"; import Checklist from "./Checklist"; import Graph from "./Graph"; import Preview from "./Preview"; @@ -31,7 +31,7 @@ export default function Creator() { const { address: accountAddress } = useAccount(); const { data: tokens } = useTokens(accountAddress); - const [token, setToken] = useState(); + const [token, setToken] = useState(null); // Set default token if non selected useEffect(() => { diff --git a/src/pages/Dashboard/components/Govnft.tsx b/src/pages/Dashboard/components/Govnft.tsx index 69527bb3..1535205f 100644 --- a/src/pages/Dashboard/components/Govnft.tsx +++ b/src/pages/Dashboard/components/Govnft.tsx @@ -51,7 +51,7 @@ export default function Govnft({ withdraw }) { @@ -61,13 +61,15 @@ export default function Govnft({ withdraw }) {
Vesting
- +
+ +
Ends in 2 years
@@ -80,7 +82,7 @@ export default function Govnft({ withdraw }) { diff --git a/src/pages/Delegate/components/DelegateNft.tsx b/src/pages/Delegate/components/DelegateNft.tsx index d9223688..9247606b 100644 --- a/src/pages/Delegate/components/DelegateNft.tsx +++ b/src/pages/Delegate/components/DelegateNft.tsx @@ -44,7 +44,7 @@ export default function DelegateNft() { @@ -62,7 +62,7 @@ export default function DelegateNft() { diff --git a/src/pages/Transfer/components/TransferNft.tsx b/src/pages/Transfer/components/TransferNft.tsx index ce5ace51..95bcf650 100644 --- a/src/pages/Transfer/components/TransferNft.tsx +++ b/src/pages/Transfer/components/TransferNft.tsx @@ -44,7 +44,7 @@ export default function TransferNft() { @@ -62,7 +62,7 @@ export default function TransferNft() { diff --git a/yarn.lock b/yarn.lock index acdb495e..88f440ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1498,6 +1498,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.5.tgz#3af577099a99c61479149b716183e70b5239324a" integrity sha512-Ark2WDjjZO7GmvsyFFf81MXuGTA/d6oP38anyxWOL6EREyBKAxKoFHwBhaZxCfLRLpO8JgVXwqOwSwa7jRcjew== +"@types/node@18.15.13": + version "18.15.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" + integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== + "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" @@ -1999,6 +2004,11 @@ acorn@^8.8.2, acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +aes-js@4.0.0-beta.5: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" + integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== + ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -3216,6 +3226,19 @@ ethereum-cryptography@^2.0.0: "@scure/bip32" "1.3.1" "@scure/bip39" "1.2.1" +ethers@^6.10.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.10.0.tgz#20f3c63c60d59a993f8090ad423d8a3854b3b1cd" + integrity sha512-nMNwYHzs6V1FR3Y4cdfxSQmNgZsRj1RiTU25JwvnJLmyzw9z3SKxNc2XKDuiXXo/v9ds5Mp9m6HBabgYQQ26tA== + dependencies: + "@adraffy/ens-normalize" "1.10.0" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@types/node" "18.15.13" + aes-js "4.0.0-beta.5" + tslib "2.4.0" + ws "8.5.0" + eventemitter2@^6.4.5, eventemitter2@^6.4.7: version "6.4.9" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" @@ -6381,6 +6404,11 @@ tslib@1.14.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tslib@^2.0.0, tslib@^2.3.1: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" @@ -6901,6 +6929,11 @@ ws@8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +ws@8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + ws@^7.5.1: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"