diff --git a/.eslintrc b/.eslintrc index eda8cb36..320370d8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -57,7 +57,7 @@ } } ], - "react/jsx-key": [2, { "checkFragmentShorthand": false }], + "react/jsx-key": [2, { "checkFragmentShorthand": true}], "arrow-body-style": "off", "no-else-return": "off", "no-plusplus": "off", @@ -95,15 +95,15 @@ "import/named": "off", "import/no-webpack-loader-syntax": "off", "import/extensions": "off", - "react/prop-types": "off", - "react/jsx-one-expression-per-line": "off", "react/button-has-type": "off", - "react/require-default-props": "off", + "react/jsx-one-expression-per-line": "off", "react/function-component-definition": "off", + "react/no-is-mounted": "off", + "react/prop-types": "off", + "react/require-default-props": "off", // Teact feature "react/style-prop-object": "off", "react/no-unknown-property": "off", - "react/no-is-mounted": "off", "react/jsx-no-bind": [ "error", { diff --git a/changelogs/3.0.31.txt b/changelogs/3.0.31.txt new file mode 100644 index 00000000..619f4cd5 --- /dev/null +++ b/changelogs/3.0.31.txt @@ -0,0 +1 @@ +Bug fixes and performance improvements diff --git a/package-lock.json b/package-lock.json index 0dc8355e..445d662b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mytonwallet", - "version": "3.0.30", + "version": "3.0.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytonwallet", - "version": "3.0.30", + "version": "3.0.31", "license": "GPL-3.0-or-later", "dependencies": { "@awesome-cordova-plugins/core": "6.9.0", diff --git a/package.json b/package.json index 3b2f4481..76a16cf9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mytonwallet", - "version": "3.0.30", + "version": "3.0.31", "description": "The most feature-rich web wallet and browser extension for TON – with support of multi-accounts, tokens (jettons), NFT, TON DNS, TON Sites, TON Proxy, and TON Magic.", "main": "index.js", "scripts": { diff --git a/public/version.txt b/public/version.txt index f9d0e255..f26c70e7 100644 --- a/public/version.txt +++ b/public/version.txt @@ -1 +1 @@ -3.0.30 +3.0.31 diff --git a/src/api/chains/ton/constants.ts b/src/api/chains/ton/constants.ts index 15c39851..fc989dfe 100644 --- a/src/api/chains/ton/constants.ts +++ b/src/api/chains/ton/constants.ts @@ -219,16 +219,26 @@ export const KnownContracts: Record = { hash: '14ce618a0e9a94adc99fa6e975219ddd675425b30dfa9728f98714c8dc55f9da', isSwapAllowed: true, }, - stonRouterV2: { - name: 'stonRouterV2', + stonRouterV2_1: { + name: 'stonRouterV2_1', hash: 'd61cb7fb7bee0cc414286a482fccdec53c3f8717e4aae4fc362d98ab6254e6cd', isSwapAllowed: true, }, - stonPoolV2: { - name: 'stonPoolV2', + stonPoolV2_1: { + name: 'stonPoolV2_1', hash: '16cc513c380e329f45d54f294787e2030e289799eca138961c1cd7e26e882c7c', isSwapAllowed: true, }, + stonRouterV2_2: { + name: 'stonRouterV2_2', + hash: '094b5084111addda1b6fac7007c8a8f85ff4ccc63475815ab3dfa3b5b4c6b102', + isSwapAllowed: true, + }, + stonPoolV2_2: { + name: 'stonPoolV2_2', + hash: '11eaf6db706e63adf9327897aaa845c77a631856abfc14375837f19b617cacb4', + isSwapAllowed: true, + }, stonPtonWalletV2: { name: 'stonPtonWalletV2', hash: '2761042202032258de9eb1b672e1ec2e4f13b2af00700195801ada33f7ced1b6', diff --git a/src/api/chains/ton/types.ts b/src/api/chains/ton/types.ts index ef5ae148..795df3b0 100644 --- a/src/api/chains/ton/types.ts +++ b/src/api/chains/ton/types.ts @@ -58,7 +58,8 @@ export type ContractName = ApiTonWalletVersion | 'v4R1' | 'highloadV2' | 'multisig' | 'multisigV2' | 'multisigNew' | 'nominatorPool' | 'vesting' | 'dedustPool' | 'dedustVaultNative' | 'dedustVaultJetton' -| 'stonPtonWallet' | 'stonRouter' | 'stonRouterV2' | 'stonPoolV2' | 'stonPtonWalletV2' +| 'stonPtonWallet' | 'stonRouter' | 'stonRouterV2_1' | 'stonPoolV2_1' +| 'stonRouterV2_2' | 'stonPoolV2_2' | 'stonPtonWalletV2' | 'megatonWtonMaster' | 'megatonRouter'; export type ContractInfo = { diff --git a/src/api/chains/tron/transfer.ts b/src/api/chains/tron/transfer.ts index 18d66468..ecf6c63b 100644 --- a/src/api/chains/tron/transfer.ts +++ b/src/api/chains/tron/transfer.ts @@ -18,9 +18,11 @@ import { getWalletBalance } from './wallet'; import type { ApiSubmitTransferTronResult } from './types'; import { hexToString } from '../../../util/stringFormat'; import { ONE_TRX } from './constants'; +import { getChainConfig } from '../../../util/chain'; const SIGNATURE_SIZE = 65; -const FEE_LIMIT_TRX = 35_000_000n; // 35 TRX + +const chainConfig = getChainConfig('tron'); export async function checkTransactionDraft( options: CheckTransactionDraftOptions, @@ -50,7 +52,12 @@ export async function checkTransactionDraft( if (tokenAddress) { const buildResult = await buildTrc20Transaction(tronWeb, { - toAddress, tokenAddress, amount, energyUnitFee, feeLimitTrx: FEE_LIMIT_TRX, fromAddress: address, + toAddress, + tokenAddress, + amount, + energyUnitFee, + feeLimitTrx: chainConfig.gas.maxTransferToken, + fromAddress: address, }); transaction = buildResult.transaction; @@ -109,7 +116,7 @@ export async function submitTransfer(options: ApiSubmitTransferOptions): Promise const privateKey = tronWeb.fromMnemonic(mnemonic!.join(' ')).privateKey.slice(2); if (tokenAddress) { - const feeLimitTrx = FEE_LIMIT_TRX; + const feeLimitTrx = chainConfig.gas.maxTransferToken; const { energyUnitFee } = await getChainParameters(network); const { transaction, energyFee } = await buildTrc20Transaction(tronWeb, { diff --git a/src/api/methods/other.ts b/src/api/methods/other.ts index 60ef6a4c..1cf5a965 100644 --- a/src/api/methods/other.ts +++ b/src/api/methods/other.ts @@ -1,12 +1,16 @@ import nacl from 'tweetnacl'; +import type { Theme } from '../../global/types'; import type { AccountCache } from '../common/cache'; import type { ApiChain, ApiNetwork } from '../types'; +import { logDebugError } from '../../util/logs'; import { setIsAppFocused } from '../../util/pauseOrFocus'; import chains from '../chains'; import { fetchStoredAccounts, fetchStoredTonWallet, updateStoredAccount } from '../common/accounts'; +import { callBackendGet } from '../common/backend'; import { updateAccountCache } from '../common/cache'; +import { handleServerError } from '../errors'; import { storage } from '../storages'; const SIGN_MESSAGE = Buffer.from('MyTonWallet_AuthToken_n6i0k4w8pb'); @@ -65,3 +69,16 @@ export function updateAccountMemoryCache(accountId: string, address: string, par } export { setIsAppFocused }; + +export async function getMoonpayOnrampUrl(address: string, theme: Theme) { + try { + return await callBackendGet<{ url: string }>('/onramp-url', { + address, + theme, + }); + } catch (err) { + logDebugError('getMoonpayOnrampUrl', err); + + return handleServerError(err); + } +} diff --git a/src/components/dapps/Dapp.module.scss b/src/components/dapps/Dapp.module.scss index 5efe875d..b75e1f7e 100644 --- a/src/components/dapps/Dapp.module.scss +++ b/src/components/dapps/Dapp.module.scss @@ -801,6 +801,18 @@ &:first-child { margin-left: 0.75rem; } + + @media (hover: hover) { + &:hover, + &:focus-visible { + .feedItemLogoImg { + transform: scale(1.1); + } + .feedItemAppNameMini, .feedItemAppNameTile { + color: var(--color-blue); + } + } + } } .feedItemAppNameMini { @@ -840,6 +852,24 @@ border-radius: 0.75rem; } +.feedItemLogoMini, .feedItemLogoTile { + overflow: hidden; +} + +.feedItemLogoImg { + transform-origin: center; + + width: 100%; + height: 100%; + + object-fit: cover; + + transition: transform 300ms; + :global(html.animation-level-0) & { + transition: none !important; + } +} + .feedSettingsIconContainer { display: flex; flex-direction: column; diff --git a/src/components/dapps/DappFeed.tsx b/src/components/dapps/DappFeed.tsx index 4a93e98c..f86d4510 100644 --- a/src/components/dapps/DappFeed.tsx +++ b/src/components/dapps/DappFeed.tsx @@ -6,6 +6,7 @@ import { SettingsState } from '../../global/types'; import { selectCurrentAccountState } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import useHorizontalScroll from '../../hooks/useHorizontalScroll'; import useLang from '../../hooks/useLang'; @@ -106,7 +107,7 @@ function DappFeed({ dapps: dappsFromState, dappLastOpenedDatesByOrigin = {} }: S } export default memo(withGlobal((global): StateProps => { - const { dapps } = global.settings; + const { dapps = MEMO_EMPTY_ARRAY } = selectCurrentAccountState(global) || {}; const { dappLastOpenedDatesByOrigin } = selectCurrentAccountState(global) || {}; return { dapps, dappLastOpenedDatesByOrigin }; })(DappFeed)); diff --git a/src/components/dapps/DappFeedItem.tsx b/src/components/dapps/DappFeedItem.tsx index b8861d65..5e7e2ce8 100644 --- a/src/components/dapps/DappFeedItem.tsx +++ b/src/components/dapps/DappFeedItem.tsx @@ -48,7 +48,12 @@ function DappFeedItem({ return (
- {lang('Icon')} + {lang('Icon')}
); } diff --git a/src/components/explore/hooks/useDappBridge.ts b/src/components/explore/hooks/useDappBridge.ts index d26f3fe6..1336f7d0 100644 --- a/src/components/explore/hooks/useDappBridge.ts +++ b/src/components/explore/hooks/useDappBridge.ts @@ -157,7 +157,8 @@ export function useDappBridge({ send: async (request: AppRequest) => { setRequestId(requestId + 1); - const isConnected = getGlobal().settings.dapps?.some((dapp) => dapp.origin === origin); + const global = getGlobal(); + const isConnected = global.byAccountId[global.currentAccountId!].dapps?.some((dapp) => dapp.origin === origin); if (!isConnected) { return { diff --git a/src/components/main/modals/OnRampWidgetModal.module.scss b/src/components/main/modals/OnRampWidgetModal.module.scss index fdd0a049..321c6e0c 100644 --- a/src/components/main/modals/OnRampWidgetModal.module.scss +++ b/src/components/main/modals/OnRampWidgetModal.module.scss @@ -5,18 +5,6 @@ .modalDialog { overflow: hidden; - height: 28rem; - - @supports (height: var(--safe-area-bottom)) { - height: calc(28rem + var(--safe-area-bottom)); - } - - :global(.is-native-bottom-sheet) & { - height: calc(28rem + var(--safe-area-bottom, 0rem)) !important; - } -} - -.modalDialogExtraHeight { height: 38.5rem; @supports (height: var(--safe-area-bottom)) { @@ -40,6 +28,10 @@ height: 100%; } +.iframe { + border: none !important; +} + .loaderContainer { position: absolute; diff --git a/src/components/main/modals/OnRampWidgetModal.tsx b/src/components/main/modals/OnRampWidgetModal.tsx index 2ca26a65..5b7fdc78 100644 --- a/src/components/main/modals/OnRampWidgetModal.tsx +++ b/src/components/main/modals/OnRampWidgetModal.tsx @@ -4,10 +4,13 @@ import React, { import { getActions, withGlobal } from '../../../global'; import type { ApiCountryCode } from '../../../api/types'; +import type { Theme } from '../../../global/types'; import { selectAccount } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; +import { callApi } from '../../../api'; +import useAppTheme from '../../../hooks/useAppTheme'; import useLang from '../../../hooks/useLang'; import Loading from '../../ui/Loading'; @@ -19,33 +22,55 @@ interface StateProps { isOpen?: boolean; address?: string; countryCode?: ApiCountryCode; + theme: Theme; } -const INITIAL_AMOUNT_USD = 50; const ANIMATION_TIMEOUT = 200; -function OnRampWidgetModal({ isOpen, address, countryCode }: StateProps) { +function OnRampWidgetModal({ + isOpen, address, countryCode, theme, +}: StateProps) { const { closeOnRampWidgetModal, + showError, } = getActions(); const lang = useLang(); const animationTimeoutRef = useRef(); const [isAnimationInProgress, setIsAnimationInProgress] = useState(true); const [isLoading, setIsLoading] = useState(true); - const withExtraHeight = countryCode === 'RU'; - - const iframeUrl = getIframeUrl(countryCode).replace('{address}', address ?? ''); + const [iframeSrc, setIframeSrc] = useState(''); + const appTheme = useAppTheme(theme); useEffect(() => { if (!isOpen) { setIsAnimationInProgress(true); setIsLoading(true); + setIframeSrc(''); } return () => window.clearTimeout(animationTimeoutRef.current); }, [isOpen]); + useEffect(() => { + if (!isOpen) return; + + if (countryCode === 'RU') { + setIframeSrc(`https://dreamwalkers.io/ru/mytonwallet/?wallet=${address}&give=CARDRUB&take=TON&type=buy`); + return; + } + + (async () => { + const response = await callApi('getMoonpayOnrampUrl', address!, appTheme); + + if (response && 'error' in response) { + showError({ error: response.error }); + } else { + setIframeSrc(response?.url || ''); + } + })(); + }, [address, appTheme, countryCode, isOpen]); + const onIframeLoaded = () => { setIsLoading(false); @@ -54,13 +79,32 @@ function OnRampWidgetModal({ isOpen, address, countryCode }: StateProps) { }, ANIMATION_TIMEOUT); }; + function renderContent() { + if (!iframeSrc) return undefined; + + return ( + + ); + } + return ( @@ -73,17 +117,7 @@ function OnRampWidgetModal({ isOpen, address, countryCode }: StateProps) { > - + {renderContent()} ); @@ -97,12 +131,6 @@ export default memo(withGlobal((global): StateProps => { isOpen: global.isOnRampWidgetModalOpen, address: addressByChain?.ton, countryCode, + theme: global.settings.theme, }; })(OnRampWidgetModal)); - -function getIframeUrl(counryCode?: ApiCountryCode) { - return counryCode === 'RU' - ? 'https://dreamwalkers.io/ru/mytonwallet/?wallet={address}&give=CARDRUB&take=TON&type=buy' - // eslint-disable-next-line max-len - : `https://widget.changelly.com?from=usd%2Ceur&to=ton&amount=${INITIAL_AMOUNT_USD}&address={address}&fromDefault=usd&toDefault=ton&merchant_id=DdrqYH0dBHq6kGlj&payment_id=&v=3&color=5f41ff&headerId=1&logo=hide&buyButtonTextId=1`; -} diff --git a/src/components/receive/ReceiveModal.module.scss b/src/components/receive/ReceiveModal.module.scss index 52acd2fe..5a493bc2 100644 --- a/src/components/receive/ReceiveModal.module.scss +++ b/src/components/receive/ReceiveModal.module.scss @@ -62,7 +62,7 @@ .contentTitleLedger { margin-top: 1rem; - margin-bottom: 0; + margin-bottom: 1rem; } .tabsInModal, diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx index 5a7b2d7f..fd96ca52 100644 --- a/src/components/settings/Settings.tsx +++ b/src/components/settings/Settings.tsx @@ -4,7 +4,7 @@ import React, { import { getActions, withGlobal } from '../../global'; import type { ApiTonWalletVersion } from '../../api/chains/ton/types'; -import type { ApiWalletInfo } from '../../api/types'; +import type { ApiDapp, ApiWalletInfo } from '../../api/types'; import type { Wallet } from './SettingsWalletVersion'; import { type GlobalState, SettingsState, type UserToken } from '../../global/types'; @@ -20,11 +20,12 @@ import { TELEGRAM_WEB_URL, TONCOIN, } from '../../config'; -import { selectCurrentAccountTokens, selectIsHardwareAccount } from '../../global/selectors'; +import { selectCurrentAccountState, selectCurrentAccountTokens, selectIsHardwareAccount } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import captureEscKeyListener from '../../util/captureEscKeyListener'; import { toBig, toDecimal } from '../../util/decimals'; import { formatCurrency, getShortCurrencySymbol } from '../../util/formatNumber'; +import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { openUrl } from '../../util/openUrl'; import resolveModalTransitionName from '../../util/resolveModalTransitionName'; import { captureControlledSwipe } from '../../util/swipeController'; @@ -96,6 +97,7 @@ type OwnProps = { type StateProps = { settings: GlobalState['settings']; + dapps: ApiDapp[]; isOpen?: boolean; tokens?: UserToken[]; isHardwareAccount?: boolean; @@ -119,9 +121,9 @@ function Settings({ isTonProxyEnabled, isTonMagicEnabled, isDeeplinkHookEnabled, - dapps, baseCurrency, }, + dapps, isOpen = false, tokens, isInsideModal, @@ -718,9 +720,11 @@ export default memo(withGlobal((global): StateProps => { const { currentVersion, byId: versionsById } = global.walletVersions ?? {}; const versions = versionsById?.[global.currentAccountId!]; + const { dapps = MEMO_EMPTY_ARRAY } = selectCurrentAccountState(global) || {}; return { settings: global.settings, + dapps, isOpen: global.areSettingsOpen, tokens: selectCurrentAccountTokens(global), isHardwareAccount, diff --git a/src/components/swap/SwapInitial.tsx b/src/components/swap/SwapInitial.tsx index 3b8979a3..780ec788 100644 --- a/src/components/swap/SwapInitial.tsx +++ b/src/components/swap/SwapInitial.tsx @@ -19,7 +19,6 @@ import { TONCOIN, TRX, } from '../../config'; -import { Big } from '../../lib/big.js'; import { selectCurrentAccount, selectIsMultichainAccount, selectSwapTokens } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { vibrate } from '../../util/capacitor'; @@ -27,6 +26,7 @@ import { findChainConfig, getChainConfig } from '../../util/chain'; import { fromDecimal, toDecimal } from '../../util/decimals'; import { formatCurrency } from '../../util/formatNumber'; import getSwapRate from '../../util/swap/getSwapRate'; +import { getIsNativeToken } from '../../util/tokens'; import { ONE_TRX } from '../../api/chains/tron/constants'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; @@ -146,26 +146,40 @@ function SwapInitial({ const isTonIn = tokenIn?.chain === 'ton'; const visibleNetworkFee = isTonIn ? realNetworkFee : networkFee; - const totalNativeAmount = useMemo( - () => { - if (!tokenIn || !amountIn || !chainConfigIn || !nativeUserTokenIn) { - return 0n; - } + const amountInBigint = amountIn && tokenIn ? fromDecimal(amountIn, tokenIn.decimals) : 0n; + const amountOutBigint = amountOut && tokenOut ? fromDecimal(amountOut, tokenOut.decimals) : 0n; + const balanceIn = useMemo(() => { + let value = tokenIn?.amount ?? 0n; + if (tokenIn?.slug === TRX.slug && isMultichainAccount) { + // We always need to leave 1 TRX on balance + value = value > ONE_TRX ? value - ONE_TRX : 0n; + } + return value; + }, [tokenIn, isMultichainAccount]); + const networkFeeBigint = useMemo(() => { + let value = 0n; - return fromDecimal(networkFee, nativeUserTokenIn.decimals) - + chainConfigIn.amountForNextSwap - + (isNativeIn ? fromDecimal(amountIn, nativeUserTokenIn.decimals) : 0n); - }, - [tokenIn, amountIn, networkFee, chainConfigIn, isNativeIn, nativeUserTokenIn], - ); + if (!chainConfigIn) { + return value; + } + + if (networkFee > 0) { + value = fromDecimal(networkFee, nativeUserTokenIn?.decimals); + } else if (swapType === SwapType.OnChain) { + value = chainConfigIn?.gas.maxSwap ?? 0n; + } else if (swapType === SwapType.CrosschainFromWallet) { + value = getIsNativeToken(tokenInSlug) ? chainConfigIn.gas.maxTransfer : chainConfigIn.gas.maxTransferToken; + } - const amountInBig = useMemo(() => Big(amountIn || 0), [amountIn]); - const amountOutBig = useMemo(() => Big(amountOut || 0), [amountOut]); - const tokenInAmountBig = useMemo(() => toDecimal(tokenIn?.amount ?? 0n, tokenIn?.decimals), [tokenIn]); - const amountOutValue = amountInBig.lte(0) && inputSource === SwapInputSource.In ? '' : amountOut?.toString(); + return value; + }, [networkFee, nativeUserTokenIn, chainConfigIn, swapType, tokenInSlug]); + const totalNativeAmount = networkFeeBigint + (isNativeIn ? amountInBigint : 0n); + const isEnoughNative = nativeUserTokenIn && nativeUserTokenIn.amount >= totalNativeAmount; + const amountOutValue = amountInBigint <= 0n && inputSource === SwapInputSource.In + ? '' + : amountOut?.toString(); const isErrorExist = errorType !== undefined; - const isEnoughNative = nativeUserTokenIn && nativeUserTokenIn.amount > totalNativeAmount; const isDieselSwap = swapType === SwapType.OnChain && !isEnoughNative @@ -174,9 +188,9 @@ function SwapInitial({ const isCorrectAmountIn = Boolean( amountIn - && tokenIn?.amount - && amountInBig.gt(0) - && amountInBig.lte(tokenInAmountBig), + && tokenIn + && amountInBigint > 0 + && amountInBigint <= balanceIn, ) || (tokenIn && !nativeTokenInSlug); const isEnoughFee = swapType !== SwapType.CrosschainToWallet @@ -184,7 +198,7 @@ function SwapInitial({ || (swapType === SwapType.OnChain && dieselStatus && ['available', 'not-authorized'].includes(dieselStatus)) : true; - const isCorrectAmountOut = amountOut && amountOutBig.gt(0); + const isCorrectAmountOut = amountOut && amountOutBigint > 0; const canSubmit = Boolean(isCorrectAmountIn && isCorrectAmountOut && isEnoughFee && !isEstimating && !isErrorExist); const isPriceImpactError = priceImpact >= MAX_PRICE_IMPACT_VALUE; @@ -279,16 +293,12 @@ function SwapInitial({ }, [tokenIn, tokenOut, isMultichainAccount]); const validateAmountIn = useLastCallback((amount: string | undefined) => { - if (swapType === SwapType.CrosschainToWallet && (!addressByChain?.tron || tokenIn?.chain !== 'tron')) { + if (swapType === SwapType.CrosschainToWallet || !amount || !tokenIn) { setHasAmountInError(false); return; } - const amountBig = amount === undefined || amount === '' ? undefined : Big(amount); - const hasError = amountBig !== undefined && ( - amountBig.lt(0) || (tokenIn?.amount !== undefined && amountBig.gt(tokenInAmountBig)) - ); - + const hasError = fromDecimal(amount, tokenIn.decimals) > balanceIn; setHasAmountInError(hasError); }); @@ -322,21 +332,23 @@ function SwapInitial({ (e: React.MouseEvent) => { e.preventDefault(); - if (!tokenIn?.amount) { - return; - } - vibrate(); - const balance = isMultichainAccount && tokenIn.slug === TRX.slug - ? tokenIn.amount - ONE_TRX - : tokenIn.amount; - const amountForNextSwap = isNativeIn && chainConfigIn ? chainConfigIn.amountForNextSwap : 0n; - const amountWithFee = amountForNextSwap > 0n && balance > amountForNextSwap - ? balance - amountForNextSwap - : balance; - const amount = toDecimal(amountWithFee, tokenIn.decimals); + let maxAmount = balanceIn; + + if (isNativeIn) { + maxAmount -= networkFeeBigint; + + if (swapType === SwapType.OnChain) { + const amountForNextSwap = chainConfigIn?.gas.maxSwap ?? 0n; + const shouldIgnoreNextSwap = amountInBigint > 0n && (maxAmount - amountInBigint) <= amountForNextSwap; + if (!shouldIgnoreNextSwap && maxAmount > amountForNextSwap) { + maxAmount -= amountForNextSwap; + } + } + } + const amount = toDecimal(maxAmount, tokenIn!.decimals); validateAmountIn(amount); setSwapAmountIn({ amount }); }, diff --git a/src/config.ts b/src/config.ts index 173bb90f..76cbdc06 100644 --- a/src/config.ts +++ b/src/config.ts @@ -221,14 +221,22 @@ export const CHAIN_CONFIG = { isDnsSupported: true, addressRegex: /^([-\w_]{48}|0:[\da-h]{64})$/i, nativeToken: TONCOIN, - amountForNextSwap: 500_000_000n, // 0.5 TON + gas: { + maxSwap: 400_000_000n, // 0.4 TON + maxTransfer: 10_000_000n, // 0.01 TON + maxTransferToken: 60_000_000n, // 0.06 TON + }, }, tron: { isMemoSupported: false, isDnsSupported: false, addressRegex: /^T[1-9A-HJ-NP-Za-km-z]{33}$/, nativeToken: TRX, - amountForNextSwap: 0n, + gas: { + maxSwap: undefined, + maxTransfer: 1_000_000n, // 1 TRX + maxTransferToken: 35_000_000n, // 35 TRX + }, mainnet: { apiUrl: 'https://api.trongrid.io', usdtAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', diff --git a/src/global/actions/api/auth.ts b/src/global/actions/api/auth.ts index d05f5f8f..d8335007 100644 --- a/src/global/actions/api/auth.ts +++ b/src/global/actions/api/auth.ts @@ -31,7 +31,6 @@ import { setIsPinAccepted, updateAuth, updateBiometrics, - updateConnectedDapps, updateCurrentAccountId, updateCurrentAccountState, updateHardware, @@ -596,7 +595,6 @@ addActionHandler('switchAccount', async (global, actions, payload) => { global = updateCurrentAccountId(global, accountId); global = clearCurrentTransfer(global); global = clearCurrentSwap(global); - global = updateConnectedDapps(global, {}); setGlobal(global); actions.clearSwapPairsCache(); diff --git a/src/global/actions/api/dapps.ts b/src/global/actions/api/dapps.ts index 31f09955..15e7ab02 100644 --- a/src/global/actions/api/dapps.ts +++ b/src/global/actions/api/dapps.ts @@ -273,7 +273,7 @@ addActionHandler('getDapps', async (global, actions) => { } global = getGlobal(); - global = updateConnectedDapps(global, { dapps: result }); + global = updateConnectedDapps(global, result); setGlobal(global); }); diff --git a/src/global/actions/api/swap.ts b/src/global/actions/api/swap.ts index 5d7553e4..a9925f5b 100644 --- a/src/global/actions/api/swap.ts +++ b/src/global/actions/api/swap.ts @@ -31,6 +31,7 @@ import { } from '../../../config'; import { Big } from '../../../lib/big.js'; import { vibrateOnError, vibrateOnSuccess } from '../../../util/capacitor'; +import { getChainConfig } from '../../../util/chain'; import { fromDecimal, getIsPositiveDecimal, roundDecimal, toDecimal, } from '../../../util/decimals'; @@ -260,7 +261,6 @@ addActionHandler('submitSwap', async (global, actions, { password }) => { global = getGlobal(); if (!buildResult || 'error' in buildResult) { - actions.showError({ error: buildResult?.error }); if (IS_CAPACITOR) { global = clearIsPinAccepted(global); void vibrateOnError(); @@ -269,6 +269,9 @@ addActionHandler('submitSwap', async (global, actions, { password }) => { isLoading: false, }); setGlobal(global); + + actions.showError({ error: buildResult?.error }); + return; } @@ -385,7 +388,9 @@ addActionHandler('submitSwapCex', async (global, actions, { password }) => { void vibrateOnError(); } setGlobal(global); + actions.showError({ error: ApiCommonError.Unexpected }); + return; } @@ -658,6 +663,10 @@ addActionHandler('estimateSwap', async (global, actions, { shouldBlock, isEnough ? SwapErrorType.NotEnoughForFee : undefined; + // `networkFee` here does not directly include transfer fee + const maxTransferFee = Number(toDecimal(getChainConfig('ton').gas.maxTransfer)); + const networkFee = estimate.networkFee + maxTransferFee; + global = updateCurrentSwap(global, { ...( global.currentSwap.inputSource === SwapInputSource.In @@ -672,7 +681,7 @@ addActionHandler('estimateSwap', async (global, actions, { shouldBlock, isEnough isEstimating: false, errorType, dieselStatus: estimate.dieselStatus, - networkFee: estimate.networkFee, + networkFee, realNetworkFee: estimate.realNetworkFee, }); setGlobal(global); diff --git a/src/global/cache.ts b/src/global/cache.ts index 9499cb15..f11e66b5 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -427,6 +427,11 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) { cached.stateVersion = 27; } + if (cached.stateVersion === 27) { + delete (cached.settings as any).dapps; + cached.stateVersion = 28; + } + // When adding migration here, increase `STATE_VERSION` } @@ -471,6 +476,7 @@ function reduceByAccountId(global: GlobalState) { 'blacklistedNftAddresses', 'whitelistedNftAddresses', 'dappLastOpenedDatesByOrigin', + 'dapps', ]); const accountTokens = selectAccountTokens(global, accountId); diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 80ddda88..a8cb7c4f 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -18,7 +18,7 @@ import { } from '../config'; import { IS_IOS_APP, USER_AGENT_LANG_CODE } from '../util/windowEnvironment'; -export const STATE_VERSION = 27; +export const STATE_VERSION = 28; export const INITIAL_STATE: GlobalState = { appState: AppState.Auth, @@ -71,7 +71,6 @@ export const INITIAL_STATE: GlobalState = { areTinyTransfersHidden: true, canPlaySounds: true, langCode: USER_AGENT_LANG_CODE, - dapps: [], byAccountId: {}, areTokensWithNoCostHidden: true, isSortByValueEnabled: true, diff --git a/src/global/reducers/dapp.ts b/src/global/reducers/dapp.ts index be9173d1..40b133c8 100644 --- a/src/global/reducers/dapp.ts +++ b/src/global/reducers/dapp.ts @@ -1,6 +1,9 @@ -import type { GlobalState } from '../types'; +import type { AccountState, GlobalState } from '../types'; import { TransferState } from '../types'; +import { selectCurrentAccountState } from '../selectors'; +import { updateCurrentAccountState } from './misc'; + export function updateDappConnectRequest(global: GlobalState, update: Partial) { return { ...global, @@ -47,32 +50,19 @@ export function clearCurrentDappTransfer(global: GlobalState) { }; } -export function updateConnectedDapps(global: GlobalState, update: Partial) { - return { - ...global, - settings: { - ...global.settings, - dapps: update.dapps ?? [], - }, - } as GlobalState; +export function updateConnectedDapps(global: GlobalState, update: AccountState['dapps']) { + return updateCurrentAccountState(global, { dapps: update }); } export function clearConnectedDapps(global: GlobalState) { - return { - ...global, - settings: { - ...global.settings, - dapps: [], - }, - } as GlobalState; + return updateCurrentAccountState(global, { dapps: undefined, dappLastOpenedDatesByOrigin: undefined }); } export function removeConnectedDapp(global: GlobalState, origin: string) { - return { - ...global, - settings: { - ...global.settings, - dapps: global.settings.dapps.filter((dapp) => dapp.origin !== origin), - }, - } as GlobalState; + const { dapps: connectedDapps, dappLastOpenedDatesByOrigin } = selectCurrentAccountState(global) || {}; + if (dappLastOpenedDatesByOrigin) delete dappLastOpenedDatesByOrigin[origin]; + return updateCurrentAccountState(global, { + dapps: connectedDapps!.filter((d) => d.origin !== origin), + dappLastOpenedDatesByOrigin, + }); } diff --git a/src/global/types.ts b/src/global/types.ts index 45f53fbf..1eeeae90 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -366,6 +366,8 @@ export interface AccountState { isDieselAuthorizationStarted?: boolean; isLongUnstakeRequested?: boolean; + + dapps?: ApiDapp[]; } export interface AccountSettings { @@ -572,7 +574,6 @@ export type GlobalState = { theme: Theme; animationLevel: AnimationLevel; langCode: LangCode; - dapps: ApiDapp[]; byAccountId: Record; areTinyTransfersHidden?: boolean; canPlaySounds?: boolean; diff --git a/webpack.config.ts b/webpack.config.ts index 63ae7170..2ce26ad4 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -44,7 +44,8 @@ const cspConnectSrcExtra = APP_ENV === 'development' ? `http://localhost:3000 ${process.env.CSP_CONNECT_SRC_EXTRA_URL}` : ''; const cspFrameSrcExtra = [ - 'https://widget.changelly.com/', + 'https://buy-sandbox.moonpay.com/', + 'https://buy.moonpay.com/', 'https://dreamwalkers.io/', 'https://avanchange.com/', 'https://pay.wata.pro/',