diff --git a/packages/commonwealth/client/scripts/views/components/NewThreadFormLegacy/ToketWidget/ThreadTokenWidget.scss b/packages/commonwealth/client/scripts/views/components/NewThreadFormLegacy/ToketWidget/ThreadTokenWidget.scss index 1fa514db26a..0fa9f3690ab 100644 --- a/packages/commonwealth/client/scripts/views/components/NewThreadFormLegacy/ToketWidget/ThreadTokenWidget.scss +++ b/packages/commonwealth/client/scripts/views/components/NewThreadFormLegacy/ToketWidget/ThreadTokenWidget.scss @@ -115,6 +115,27 @@ font-size: 12px; margin-left: 4px; } + + .currency-dropdown-trigger { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + cursor: pointer; + padding: 4px; + border-radius: 4px; + transition: background-color 0.2s ease; + + &:hover { + background-color: colors.$neutral-100; + } + + &:focus { + outline: none; + background-color: colors.$neutral-100; + } + } } } diff --git a/packages/commonwealth/client/scripts/views/components/NewThreadFormLegacy/ToketWidget/ThreadTokenWidget.tsx b/packages/commonwealth/client/scripts/views/components/NewThreadFormLegacy/ToketWidget/ThreadTokenWidget.tsx index 90db7522a88..f70f5483c85 100644 --- a/packages/commonwealth/client/scripts/views/components/NewThreadFormLegacy/ToketWidget/ThreadTokenWidget.tsx +++ b/packages/commonwealth/client/scripts/views/components/NewThreadFormLegacy/ToketWidget/ThreadTokenWidget.tsx @@ -1,9 +1,12 @@ import { Community } from '@hicommonwealth/schemas'; +import { ZERO_ADDRESS } from '@hicommonwealth/shared'; import { notifyError, notifySuccess } from 'controllers/app/notifications'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useCreateThreadTokenTradeMutation } from 'state/api/threads'; +import { useGetERC20BalanceQuery } from 'state/api/tokens'; import FractionalValue from 'views/components/FractionalValue'; import { z } from 'zod'; +import { PopoverMenu } from '../../component_kit/CWPopoverMenu'; import { CWIcon } from '../../component_kit/cw_icons/cw_icon'; import { CWText } from '../../component_kit/cw_text'; import { CWButton } from '../../component_kit/new_designs/CWButton'; @@ -26,6 +29,14 @@ interface ThreadTokenWidgetProps { ) => void | Promise; } +type BuyCurrency = 'ETH' | 'COMMON' | 'PRIMARY'; + +interface CurrencyOption { + type: BuyCurrency; + symbol: string; + address: string; +} + const ThreadTokenWidget = ({ tokenizedThreadsEnabled = false, threadId, @@ -37,6 +48,8 @@ const ThreadTokenWidget = ({ onThreadCreated, }: ThreadTokenWidgetProps) => { const isThreadCreationMode = !threadId; + const [selectedBuyCurrency, setSelectedBuyCurrency] = + useState('PRIMARY'); const threadTokenWidgetHook = useThreadTokenWidget({ tokenizedThreadsEnabled, @@ -126,6 +139,124 @@ const ThreadTokenWidget = ({ const safeSetCurrentAmount = setCurrentAmount || (() => {}); + // TODO 13191: test with base network + const COMMON_TOKEN_ADDRESS = '0x4c87da04887a1F9F21F777E3A8dD55C3C9f84701'; + + // Get WETH address based on chain ID + // WETH addresses for different chains + const getWETHAddress = (chainId: number): string => { + const wethAddresses: Record = { + 1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // Ethereum Mainnet + 8453: '0x4200000000000000000000000000000000000006', // Base + 84532: '0x4200000000000000000000000000000000000006', // Base Sepolia (same as Base) + 10: '0x4200000000000000000000000000000000000006', // Optimism + 42161: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // Arbitrum + 56: '0x2170Ed0880ac9A755fd29B2688956BD959F933F8', // BSC + }; + return wethAddresses[chainId] || ZERO_ADDRESS; + }; + + const wethAddress = useMemo(() => getWETHAddress(ethChainId), [ethChainId]); + + // Define available buy currencies + const currencyOptions: CurrencyOption[] = useMemo(() => { + const options: CurrencyOption[] = [ + { + type: 'ETH', + symbol: 'WETH', + address: wethAddress, + }, + ]; + + // Add COMMON token if address is available + if (COMMON_TOKEN_ADDRESS) { + options.push({ + type: 'COMMON', + symbol: 'COMMON', + address: COMMON_TOKEN_ADDRESS, + }); + } + + // Add primary token (the pre-selected one) + if (primaryTokenAddress && primaryTokenSymbol) { + options.push({ + type: 'PRIMARY', + symbol: primaryTokenSymbol, + address: primaryTokenAddress, + }); + } + + return options; + }, [ + primaryTokenAddress, + primaryTokenSymbol, + COMMON_TOKEN_ADDRESS, + wethAddress, + ]); + + // Get selected currency details + const selectedCurrency = useMemo(() => { + return ( + currencyOptions.find((opt) => opt.type === selectedBuyCurrency) || + currencyOptions[0] + ); + }, [currencyOptions, selectedBuyCurrency]); + + // Get payment token address based on selected currency + const paymentTokenAddress = useMemo(() => { + return selectedCurrency?.address || primaryTokenAddress; + }, [selectedCurrency, primaryTokenAddress]); + + // Get currency symbol for display + const displayCurrencySymbol = useMemo(() => { + if (isThreadCreationMode || !isSellMode) { + return selectedCurrency?.symbol || primaryTokenSymbol; + } + return threadToken?.symbol || 'TOKEN'; + }, [ + isThreadCreationMode, + isSellMode, + selectedCurrency, + primaryTokenSymbol, + threadToken, + ]); + + // Fetch balance for selected currency + // For WETH, we fetch the ERC20 balance + const { + data: selectedCurrencyTokenBalance = '0.0', + isLoading: isLoadingSelectedCurrencyTokenBalance, + } = useGetERC20BalanceQuery({ + nodeRpc: chainRpc, + tokenAddress: paymentTokenAddress, + userAddress: selectedAddress, + enabled: + tokenizedThreadsEnabled && + !!selectedAddress && + !!paymentTokenAddress && + !!chainRpc && + !isSellMode, + }); + + // Get balance for selected currency + const selectedCurrencyBalance = useMemo(() => { + if (isSellMode) { + return userTokenBalance; + } + return selectedCurrencyTokenBalance; + }, [isSellMode, selectedCurrencyTokenBalance, userTokenBalance]); + + const isLoadingSelectedCurrencyBalance = useMemo(() => { + if (isSellMode) { + return isLoadingTokenBalance; + } + return isLoadingSelectedCurrencyTokenBalance; + }, [ + isSellMode, + isLoadingSelectedCurrencyTokenBalance, + isLoadingTokenBalance, + ]); + useEffect(() => { const fetchTokenGain = async () => { if ( @@ -193,7 +324,9 @@ const ThreadTokenWidget = ({ const handlePresetClick = (presetAmount: string) => { if (presetAmount === 'MAX') { - safeSetCurrentAmount(isSellMode ? userTokenBalance : userBalance); + safeSetCurrentAmount( + isSellMode ? userTokenBalance : selectedCurrencyBalance, + ); } else { safeSetCurrentAmount(presetAmount); } @@ -227,7 +360,7 @@ const ThreadTokenWidget = ({ return; } - if (parseFloat(safeCurrentAmount) > parseFloat(userBalance)) { + if (parseFloat(safeCurrentAmount) > parseFloat(selectedCurrencyBalance)) { notifyError('Insufficient balance'); return; } @@ -253,7 +386,7 @@ const ThreadTokenWidget = ({ amountIn: amountInWei, walletAddress: selectedAddress, minAmountOut: minAmountOut, - paymentTokenAddress: primaryTokenAddress, + paymentTokenAddress: paymentTokenAddress, }; const txReceipt = await buyThreadToken(payload); @@ -331,7 +464,7 @@ const ThreadTokenWidget = ({ tokenAddress: String(threadToken.token_address), amountToken: amountToken, walletAddress: selectedAddress, - paymentTokenAddress: primaryTokenAddress, + paymentTokenAddress: paymentTokenAddress, }; const txReceipt = await sellThreadToken(payload); @@ -418,9 +551,9 @@ const ThreadTokenWidget = ({
{isThreadCreationMode || !isSellMode - ? isLoadingBalance + ? isLoadingSelectedCurrencyBalance ? 'Loading...' - : `${userBalance} ${primaryTokenSymbol}` + : `${selectedCurrencyBalance} ${displayCurrencySymbol}` : isLoadingTokenBalance ? 'Loading...' : `${userTokenBalance} ${threadToken?.symbol || 'TOKEN'}`} @@ -440,18 +573,37 @@ const ThreadTokenWidget = ({
{parseFloat(safeCurrentAmount) > 0 - ? `${safeCurrentAmount} ${ - isThreadCreationMode || !isSellMode - ? primaryTokenSymbol - : threadToken?.symbol || 'TOKEN' - }` - : `0.000 ${ - isThreadCreationMode || !isSellMode - ? primaryTokenSymbol - : threadToken?.symbol || 'TOKEN' - }`} + ? `${safeCurrentAmount} ${displayCurrencySymbol}` + : `0.000 ${displayCurrencySymbol}`} - + {!isSellMode && ( + ({ + type: 'default' as const, + label: option.symbol, + onClick: () => { + setSelectedBuyCurrency(option.type); + }, + }))} + placement="bottom-end" + modifiers={[{ name: 'offset', options: { offset: [0, 8] } }]} + renderTrigger={(onClick, isOpen) => ( + + )} + /> + )} + {isSellMode && ( + + )}
@@ -517,7 +669,7 @@ const ThreadTokenWidget = ({ parseFloat(safeCurrentAmount) > parseFloat( isThreadCreationMode || !isSellMode - ? userBalance + ? selectedCurrencyBalance : userTokenBalance, ) || isWrongNetwork diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/TokenIntegration/ConnectTokenForm/ConnectTokenStep/ConnectTokenStep.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/TokenIntegration/ConnectTokenForm/ConnectTokenStep/ConnectTokenStep.tsx index c508903636b..eb3cab1b1eb 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/TokenIntegration/ConnectTokenForm/ConnectTokenStep/ConnectTokenStep.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/TokenIntegration/ConnectTokenForm/ConnectTokenStep/ConnectTokenStep.tsx @@ -113,7 +113,7 @@ const ConnectTokenStep = ({