diff --git a/.release/.changeset/blue-goats-shave.md b/.release/.changeset/blue-goats-shave.md new file mode 100644 index 00000000..de1b2dfb --- /dev/null +++ b/.release/.changeset/blue-goats-shave.md @@ -0,0 +1,5 @@ +--- +"@bnb-chain/canonical-bridge-widget": patch +--- + +chore: Update confirmation popup amount styling diff --git a/.release/.changeset/pre.json b/.release/.changeset/pre.json new file mode 100644 index 00000000..dcc9414e --- /dev/null +++ b/.release/.changeset/pre.json @@ -0,0 +1,11 @@ +{ + "mode": "pre", + "tag": "alpha", + "initialVersions": { + "@bnb-chain/canonical-bridge-sdk": "0.4.7", + "@bnb-chain/canonical-bridge-widget": "0.5.18" + }, + "changesets": [ + "blue-goats-shave" + ] +} diff --git a/packages/canonical-bridge-sdk/src/shared/number.ts b/packages/canonical-bridge-sdk/src/shared/number.ts index 469abe4e..9259c5e2 100644 --- a/packages/canonical-bridge-sdk/src/shared/number.ts +++ b/packages/canonical-bridge-sdk/src/shared/number.ts @@ -18,7 +18,7 @@ export const formatNumber = ( useGrouping = true ) => { const num = removeAfterDecimals(value, decimals); - return num.toLocaleString('fullwide', { + return Number(num).toLocaleString('fullwide', { maximumFractionDigits: decimals, useGrouping, }); diff --git a/packages/canonical-bridge-widget/CHANGELOG.md b/packages/canonical-bridge-widget/CHANGELOG.md index 227c5abc..8cb8038f 100644 --- a/packages/canonical-bridge-widget/CHANGELOG.md +++ b/packages/canonical-bridge-widget/CHANGELOG.md @@ -1,5 +1,11 @@ # @bnb-chain/canonical-bridge-widget +## 0.5.19-alpha.0 + +### Patch Changes + +- chore: Update confirmation popup amount styling + ## 0.5.19 ### Patch Changes diff --git a/packages/canonical-bridge-widget/package.json b/packages/canonical-bridge-widget/package.json index 7d1c67dc..5c8ce318 100644 --- a/packages/canonical-bridge-widget/package.json +++ b/packages/canonical-bridge-widget/package.json @@ -1,6 +1,6 @@ { "name": "@bnb-chain/canonical-bridge-widget", - "version": "0.5.19", + "version": "0.5.19-alpha.0", "description": "canonical bridge widget", "author": "bnb-chain", "private": false, diff --git a/packages/canonical-bridge-widget/src/core/utils/number.ts b/packages/canonical-bridge-widget/src/core/utils/number.ts index 33fdec41..784a7d1b 100644 --- a/packages/canonical-bridge-widget/src/core/utils/number.ts +++ b/packages/canonical-bridge-widget/src/core/utils/number.ts @@ -1,6 +1,6 @@ export const formatNumber = (value: number, decimals = 18, useGrouping = true) => { const num = removeAfterDecimals(value, decimals); - return num.toLocaleString('fullwide', { + return Number(num).toLocaleString('fullwide', { maximumFractionDigits: decimals, useGrouping, }); diff --git a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/layerZero/components/LayerZeroOption.tsx b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/layerZero/components/LayerZeroOption.tsx index bdf8638e..a8aaacae 100644 --- a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/layerZero/components/LayerZeroOption.tsx +++ b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/layerZero/components/LayerZeroOption.tsx @@ -32,7 +32,6 @@ export const LayerZeroOption = () => { ? `${formatNumber( Number(formatUnits(BigInt(estimatedAmount?.['layerZero']), getToDecimals()['layerZero'])), 8, - false, )}` : '--'; }, [estimatedAmount, toTokenInfo, sendValue, getToDecimals]); diff --git a/packages/canonical-bridge-widget/src/modules/aggregator/providers/TokenPricesProvider.tsx b/packages/canonical-bridge-widget/src/modules/aggregator/providers/TokenPricesProvider.tsx index aed72812..476a8e96 100644 --- a/packages/canonical-bridge-widget/src/modules/aggregator/providers/TokenPricesProvider.tsx +++ b/packages/canonical-bridge-widget/src/modules/aggregator/providers/TokenPricesProvider.tsx @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { useCallback, useEffect } from 'react'; import { IBridgeToken } from '@bnb-chain/canonical-bridge-sdk'; -import { useBridgeConfig } from '@/CanonicalBridgeProvider'; +import { IBridgeConfig, useBridgeConfig } from '@/CanonicalBridgeProvider'; import { TIME } from '@/core/constants'; import { useAppDispatch, useAppSelector } from '@/modules/store/StoreProvider'; import { setIsLoadingTokenPrices, setTokenPrices } from '@/modules/aggregator/action'; @@ -21,27 +21,13 @@ interface ITokenPricesResponse { export function TokenPricesProvider() { const bridgeConfig = useBridgeConfig(); const dispatch = useAppDispatch(); + const { fetchApiTokenPrices } = useTokenPrice(); const { isLoading, data } = useQuery({ staleTime: TIME.MINUTE * 5, refetchInterval: TIME.MINUTE * 5, queryKey: ['tokenPrices'], - queryFn: async () => { - const { serverEndpoint } = bridgeConfig.http; - - const [cmcRes, llamaRes] = await Promise.allSettled([ - axios.get(`${serverEndpoint}/api/token/cmc`), - axios.get(`${serverEndpoint}/api/token/llama`), - ]); - - const cmcPrices = cmcRes.status === 'fulfilled' ? cmcRes.value.data.data : {}; - const llamaPrices = llamaRes.status === 'fulfilled' ? llamaRes.value.data.data : {}; - - return { - cmcPrices, - llamaPrices, - }; - }, + queryFn: async () => fetchApiTokenPrices(bridgeConfig), }); useEffect(() => { @@ -85,7 +71,25 @@ export function useTokenPrice() { [tokenPrices], ); + const fetchApiTokenPrices = useCallback(async (bridgeConfig: IBridgeConfig) => { + const { serverEndpoint } = bridgeConfig.http; + + const [cmcRes, llamaRes] = await Promise.allSettled([ + axios.get(`${serverEndpoint}/api/token/cmc`), + axios.get(`${serverEndpoint}/api/token/llama`), + ]); + + const cmcPrices = cmcRes.status === 'fulfilled' ? cmcRes.value.data.data : {}; + const llamaPrices = llamaRes.status === 'fulfilled' ? llamaRes.value.data.data : {}; + + return { + cmcPrices, + llamaPrices, + }; + }, []); + return { getTokenPrice, + fetchApiTokenPrices, }; } diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/Button/TransferConfirmButton.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/Button/TransferConfirmButton.tsx index 2bc02126..425fa01e 100644 --- a/packages/canonical-bridge-widget/src/modules/transfer/components/Button/TransferConfirmButton.tsx +++ b/packages/canonical-bridge-widget/src/modules/transfer/components/Button/TransferConfirmButton.tsx @@ -25,6 +25,7 @@ import { STARGATE_ENDPOINT, } from '@/core/constants'; import { useHandleTxFailure } from '@/modules/aggregator/hooks/useHandleTxFailure'; +import { usePriceValidation } from '@/modules/transfer/hooks/usePriceValidation'; export const TransferConfirmButton = ({ onClose, @@ -49,6 +50,7 @@ export const TransferConfirmButton = ({ const { formatMessage } = useIntl(); const theme = useTheme(); const { colorMode } = useColorMode(); + const { validateTokenPrice } = usePriceValidation(); const { address } = useAccount(); const { address: tronAddress, signTransaction } = useTronWallet(); @@ -104,6 +106,17 @@ export const TransferConfirmButton = ({ } try { + // Check whether token price exists + const result = await validateTokenPrice({ + tokenSymbol: selectedToken.symbol, + tokenAddress: selectedToken.address, + }); + if (!result) { + throw new Error( + `Can not get token price from API server: ${sendValue} ${selectedToken.symbol}`, + ); + } + setHash(null); setChosenBridge(''); setIsLoading(true); @@ -537,6 +550,7 @@ export const TransferConfirmButton = ({ signMessageAsync, signTransaction, handleFailure, + validateTokenPrice, ]); const isFeeLoading = isLoading || isGlobalFeeLoading || !transferActionInfo || !isTransferable; diff --git a/packages/canonical-bridge-widget/src/modules/transfer/hooks/useInputValidation.ts b/packages/canonical-bridge-widget/src/modules/transfer/hooks/useInputValidation.ts index e8313224..d8caf368 100644 --- a/packages/canonical-bridge-widget/src/modules/transfer/hooks/useInputValidation.ts +++ b/packages/canonical-bridge-widget/src/modules/transfer/hooks/useInputValidation.ts @@ -7,12 +7,20 @@ import { useAppSelector } from '@/modules/store/StoreProvider'; import { useSolanaBalance } from '@/modules/wallet/hooks/useSolanaBalance'; import { MIN_SOL_TO_ENABLED_TX } from '@/core/constants'; import { useIsWalletCompatible } from '@/modules/wallet/hooks/useIsWalletCompatible'; +import { useTokenUpperLimit } from '@/modules/aggregator/hooks/useTokenUpperLimit'; +import { useBridgeConfig } from '@/index'; export const useInputValidation = () => { const { data } = useSolanaBalance(); const isWalletCompatible = useIsWalletCompatible(); + const { + transfer: { dollarUpperLimit }, + } = useBridgeConfig(); const solBalance = Number(data?.formatted); const fromChain = useAppSelector((state) => state.transfer.fromChain); + const selectedToken = useAppSelector((state) => state.transfer.selectedToken); + + const priceInfo = useTokenUpperLimit(selectedToken); const validateInput = useCallback( ({ balance, @@ -39,6 +47,15 @@ export const useInputValidation = () => { isError: true, }; } + // Check upper limit + if (priceInfo?.upperLimit && Number(value) >= Number(priceInfo?.upperLimit)) { + return { + text: `Transfer value over $${formatNumber(dollarUpperLimit)} (${formatNumber( + priceInfo.upperLimit, + )} ${selectedToken?.symbol}) or equivalent is not allowed`, + isError: true, + }; + } // check if send amount is greater than token balance if (!!balance && value > balance) { return { text: `You have insufficient balance`, isError: true }; @@ -71,7 +88,14 @@ export const useInputValidation = () => { console.log(e); } }, - [fromChain?.chainType, solBalance, isWalletCompatible], + [ + fromChain?.chainType, + solBalance, + isWalletCompatible, + priceInfo, + dollarUpperLimit, + selectedToken?.symbol, + ], ); return { diff --git a/packages/canonical-bridge-widget/src/modules/transfer/hooks/usePriceValidation.ts b/packages/canonical-bridge-widget/src/modules/transfer/hooks/usePriceValidation.ts new file mode 100644 index 00000000..11509f1e --- /dev/null +++ b/packages/canonical-bridge-widget/src/modules/transfer/hooks/usePriceValidation.ts @@ -0,0 +1,33 @@ +import { useCallback } from 'react'; + +import { useBridgeConfig } from '@/index'; +import { useTokenPrice } from '@/modules/aggregator/providers/TokenPricesProvider'; + +export const usePriceValidation = () => { + const { fetchApiTokenPrices } = useTokenPrice(); + const bridgeConfig = useBridgeConfig(); + + const validateTokenPrice = useCallback( + async ({ tokenSymbol, tokenAddress }: { tokenSymbol: string; tokenAddress: string }) => { + const { cmcPrices, llamaPrices } = await fetchApiTokenPrices(bridgeConfig); + + const key1 = `${tokenSymbol?.toLowerCase()}:${tokenAddress?.toLowerCase()}`; + const key3 = tokenSymbol?.toLowerCase(); + const key2 = `ethereum:${key3}`; + let price = + cmcPrices?.[key1]?.price ?? + llamaPrices?.[key1]?.price ?? + cmcPrices?.[key2]?.price ?? + llamaPrices?.[key2]?.price ?? + cmcPrices?.[key3]?.price ?? + llamaPrices?.[key3]?.price; + if (price !== undefined) { + price = Number(price); + } + return price; + }, + [bridgeConfig, fetchApiTokenPrices], + ); + + return { validateTokenPrice }; +};