From 450f6a6cf88416ec5ead39907e27c67b1b3fbc2d Mon Sep 17 00:00:00 2001 From: mytonwalletorg Date: Thu, 23 Jan 2025 15:07:28 +0100 Subject: [PATCH] v3.2.9 --- changelogs/3.2.9.txt | 1 + package-lock.json | 4 +- package.json | 2 +- public/version.txt | 2 +- src/api/chains/ton/transactions.ts | 19 ++-- src/api/chains/ton/types.ts | 25 ++--- .../Card/CustomCardBackground.module.scss | 6 +- src/components/swap/SwapInitial.tsx | 14 +-- src/components/transfer/Transfer.module.scss | 5 - src/components/transfer/TransferInitial.tsx | 97 ++++++++----------- src/components/transfer/TransferModal.tsx | 2 +- src/components/ui/Modal.module.scss | 2 + src/components/ui/Modal.tsx | 13 +++ src/config.ts | 12 +-- src/global/actions/api/swap.ts | 12 ++- src/global/actions/api/transfer.ts | 3 +- src/global/actions/ui/initial.ts | 34 ++++++- src/global/actions/ui/misc.ts | 15 +++ src/global/actions/ui/transfer.ts | 11 ++- src/global/initialState.ts | 4 +- src/global/types.ts | 6 +- src/util/capacitor/notifications.ts | 8 +- src/util/fee/transferFee.ts | 51 ++++++++-- 23 files changed, 213 insertions(+), 135 deletions(-) create mode 100644 changelogs/3.2.9.txt diff --git a/changelogs/3.2.9.txt b/changelogs/3.2.9.txt new file mode 100644 index 00000000..619f4cd5 --- /dev/null +++ b/changelogs/3.2.9.txt @@ -0,0 +1 @@ +Bug fixes and performance improvements diff --git a/package-lock.json b/package-lock.json index dfd013c2..5cdad20b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mytonwallet", - "version": "3.2.8", + "version": "3.2.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytonwallet", - "version": "3.2.8", + "version": "3.2.9", "license": "GPL-3.0-or-later", "dependencies": { "@awesome-cordova-plugins/core": "6.9.0", diff --git a/package.json b/package.json index 3845f85b..72bc65fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mytonwallet", - "version": "3.2.8", + "version": "3.2.9", "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 f092941a..e650c01d 100644 --- a/public/version.txt +++ b/public/version.txt @@ -1 +1 @@ -3.2.8 +3.2.9 diff --git a/src/api/chains/ton/transactions.ts b/src/api/chains/ton/transactions.ts index 4f3060fe..e5043369 100644 --- a/src/api/chains/ton/transactions.ts +++ b/src/api/chains/ton/transactions.ts @@ -37,6 +37,7 @@ import { parseAccountId } from '../../../util/account'; import { bigintMultiplyToNumber } from '../../../util/bigint'; import { compareActivities } from '../../../util/compareActivities'; import { fromDecimal, toDecimal } from '../../../util/decimals'; +import { getDieselTokenAmount, isDieselAvailable } from '../../../util/fee/transferFee'; import { buildCollectionByKey, omit, pick } from '../../../util/iteratees'; import { logDebug, logDebugError } from '../../../util/logs'; import { updatePoisoningCache } from '../../../util/poisoningHash'; @@ -104,7 +105,6 @@ const PENDING_DIESEL_TIMEOUT = 15 * 60 * 1000; // 15 min const DIESEL_NOT_AVAILABLE: ApiFetchEstimateDieselResult = { status: 'not-available', - amount: { token: 0n, stars: 0n }, nativeAmount: 0n, remainingFee: 0n, realFee: 0n, @@ -277,10 +277,10 @@ export async function checkTransactionDraft( tokenBalance: balance, }); - if (result.diesel.status === 'not-available') { - isEnoughBalance = canTransferGasfully && amount <= balance; + if (isDieselAvailable(result.diesel)) { + isEnoughBalance = amount + getDieselTokenAmount(result.diesel) <= balance; } else { - isEnoughBalance = amount + result.diesel.amount.token <= balance; + isEnoughBalance = canTransferGasfully && amount <= balance; } } @@ -1338,20 +1338,21 @@ async function getDiesel({ ); const diesel: ApiFetchEstimateDieselResult = { status: rawDiesel.status, - amount: rawDiesel.status !== 'stars-fee' - ? { token: fromDecimal(rawDiesel.amount ?? '0', token.decimals), stars: 0n } - : { token: 0n, stars: fromDecimal(rawDiesel.amount ?? '0', 0) }, + amount: rawDiesel.amount === undefined + ? undefined + : fromDecimal(rawDiesel.amount, rawDiesel.status === 'stars-fee' ? 0 : token.decimals), nativeAmount: toncoinNeeded, remainingFee: toncoinBalance, realFee: fee.realFee, }; - if (diesel.status === 'not-available') { + const tokenAmount = getDieselTokenAmount(diesel); + if (tokenAmount === 0n) { return diesel; } tokenBalance ??= await getTokenBalanceWithMintless(network, address, tokenAddress); - const canPayDiesel = tokenBalance >= diesel.amount.token; + const canPayDiesel = tokenBalance >= tokenAmount; const isAwaitingNotExpiredPrevious = Boolean( rawDiesel.pendingCreatedAt && Date.now() - new Date(rawDiesel.pendingCreatedAt).getTime() < PENDING_DIESEL_TIMEOUT, diff --git a/src/api/chains/ton/types.ts b/src/api/chains/ton/types.ts index 6b4a70d2..3c060915 100644 --- a/src/api/chains/ton/types.ts +++ b/src/api/chains/ton/types.ts @@ -118,21 +118,16 @@ export type ApiFetchEstimateDieselResult = { status: DieselStatus; /** * The amount of the diesel itself. It will be sent together with the actual transfer. None of this will return back - * as the excess. Charged on top of the transferred amount. The token and stars amounts can't be non-zero - * simultaneously. Warning: the values can be zeros simultaneously, e.g. when the status is 'pending-previous'. + * as the excess. Undefined means that + * gasless transfer is not available, and the diesel shouldn't be shown as the fee; nevertheless, the status should + * be displayed by the UI. + * + * - If the status is not 'stars-fee', the value is measured in the transferred token and charged on top of the + * transferred amount. + * - If the status is 'stars-fee', the value is measured in Telegram stars, and the BigInt assumes 0 decimal places + * (i.e. the number is equal to the visible number of stars). */ - amount: { - /** Measured in the transferred token */ - token: bigint; - /** - * Measured in Telegram stars. The BigInt assumes 0 decimal places (i.e. the number is equal to the visible number - * of stars). - */ - stars: 0n; - } | { - token: 0n; - stars: bigint; - }; + amount?: bigint; /** * The native token amount covered by the diesel. Guaranteed to be > 0. */ @@ -143,7 +138,7 @@ export type ApiFetchEstimateDieselResult = { */ remainingFee: bigint; /** - * An approximate fee that will be actually spent. The difference between `nativeAmount+nativeRemainder` and this + * An approximate fee that will be actually spent. The difference between `nativeAmount+remainingFee` and this * number is called "excess" and will be returned back to the wallet. Measured in the native token. */ realFee: bigint; diff --git a/src/components/main/sections/Card/CustomCardBackground.module.scss b/src/components/main/sections/Card/CustomCardBackground.module.scss index 73d224b7..81bfdbda 100644 --- a/src/components/main/sections/Card/CustomCardBackground.module.scss +++ b/src/components/main/sections/Card/CustomCardBackground.module.scss @@ -94,14 +94,14 @@ padding: 1.5px !important; background-image: radial-gradient(30.47% 83.28% at 79.45% 3.2%, #FFFFFF 0%, rgba(255, 255, 255, 0) 100%), - linear-gradient(258.65deg, #141518 33.29%, #292929 48.38%) !important; + linear-gradient(258.65deg, #141518 33.29%, #292929 48.38%) !important; } :global(.MtwCard__platinum)::before, :global(.MtwCard__gold)::before, :global(.MtwCard__silver)::before { - background-image: radial-gradient(30.42% 83.05% at 79.4% 3.33%, #FFFFFF 0%, rgba(255, 255, 255, 0) 100%), - linear-gradient(258.63deg, #141518 33.33%, #292929 48.39%) !important; + background-image: radial-gradient(23.98% 49.81% at 73.98% 0.37%, #FFFFFF 0%, rgba(255, 255, 255, 0) 100%), + linear-gradient(258.61deg, rgba(140, 148, 176, 0.5) 33.38%, rgba(186, 188, 194, 0.85) 48.39%) !important; } .shadow { diff --git a/src/components/swap/SwapInitial.tsx b/src/components/swap/SwapInitial.tsx index 9550f530..d5930c2b 100644 --- a/src/components/swap/SwapInitial.tsx +++ b/src/components/swap/SwapInitial.tsx @@ -26,10 +26,9 @@ import { selectCurrentAccount, selectIsMultichainAccount, selectSwapTokens } fro import { bigintDivideToNumber, bigintMax } from '../../util/bigint'; import buildClassName from '../../util/buildClassName'; import { vibrate } from '../../util/capacitor'; -import { findChainConfig, getChainConfig } from '../../util/chain'; +import { findChainConfig } from '../../util/chain'; import { fromDecimal, toDecimal } from '../../util/decimals'; import { formatCurrency } from '../../util/formatNumber'; -import { getIsNativeToken } from '../../util/tokens'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; import { isBackgroundModeActive } from '../../hooks/useBackgroundMode'; @@ -150,7 +149,6 @@ function SwapInitial({ ); const nativeBalance = nativeUserTokenIn?.amount ?? 0n; const isNativeIn = currentTokenInSlug && currentTokenInSlug === nativeTokenInSlug; - const chainConfigIn = nativeUserTokenIn ? getChainConfig(nativeUserTokenIn.chain as ApiChain) : undefined; const isTonIn = tokenIn?.chain === 'ton'; const amountInBigint = amountIn && tokenIn ? fromDecimal(amountIn, tokenIn.decimals) : 0n; @@ -160,16 +158,8 @@ function SwapInitial({ const networkFeeBigint = (() => { let value = 0n; - if (!chainConfigIn) { - return value; - } - if (Number(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; } return value; @@ -222,7 +212,7 @@ function SwapInitial({ amountIn && tokenIn && amountInBigint > 0 - && amountInBigint <= balanceIn, + && amountInBigint <= maxAmount, ) || (tokenIn && !nativeTokenInSlug); const isEnoughFee = swapType !== SwapType.CrosschainToWallet diff --git a/src/components/transfer/Transfer.module.scss b/src/components/transfer/Transfer.module.scss index 8674e6e9..2b3ac1e1 100644 --- a/src/components/transfer/Transfer.module.scss +++ b/src/components/transfer/Transfer.module.scss @@ -545,11 +545,6 @@ textarea.inputStatic { margin-bottom: 2rem; } -.transitionSlide { - height: auto; - min-height: 100%; -} - .infoBox, .burnWarning, .error { align-self: center; diff --git a/src/components/transfer/TransferInitial.tsx b/src/components/transfer/TransferInitial.tsx index 265e8b49..ffa1816d 100644 --- a/src/components/transfer/TransferInitial.tsx +++ b/src/components/transfer/TransferInitial.tsx @@ -29,7 +29,9 @@ import { readClipboardContent } from '../../util/clipboard'; import { SECOND } from '../../util/dateFormat'; import { fromDecimal, toBig, toDecimal } from '../../util/decimals'; import dns from '../../util/dns'; -import { explainApiTransferFee, getMaxTransferAmount } from '../../util/fee/transferFee'; +import { + explainApiTransferFee, getMaxTransferAmount, isBalanceSufficientForTransfer, +} from '../../util/fee/transferFee'; import { formatCurrency, getShortCurrencySymbol } from '../../util/formatNumber'; import { isValidAddressOrDomain } from '../../util/isValidAddressOrDomain'; import { debounce } from '../../util/schedulers'; @@ -198,11 +200,7 @@ function TransferInitial({ return tokens?.find((token) => !token.tokenAddress && token.chain === chain); }, [tokens, chain])!; - const { status: dieselStatus, amount: dieselAmount } = diesel ?? {}; - const skipNextFeeEstimate = useRef(false); - const isToncoin = tokenSlug === TONCOIN.slug; - const isToncoinFullBalance = isToncoin && balance === amount; const shouldDisableClearButton = !toAddress && !amount && !(comment || binPayload) && !shouldEncrypt && !(nfts?.length && isStatic); @@ -217,11 +215,6 @@ function TransferInitial({ const withAddressClearButton = !!toAddress.length; const shortBaseSymbol = getShortCurrencySymbol(baseCurrency); - const additionalAmount = amount && isToncoin ? amount : 0n; - const isEnoughNativeCoin = isToncoinFullBalance - ? (fee !== undefined && fee < nativeToken.amount) - : (fee !== undefined && (fee + additionalAmount) <= nativeToken.amount); - const isNativeToken = getIsNativeToken(tokenSlug); const explainedFee = useMemo( () => explainApiTransferFee({ @@ -229,18 +222,17 @@ function TransferInitial({ }), [fee, realFee, diesel, chain, isNativeToken], ); - const isGaslessWithStars = dieselStatus === 'stars-fee'; - const isDieselAvailable = dieselStatus === 'available' || isGaslessWithStars; - const isDieselNotAuthorized = dieselStatus === 'not-authorized'; - const withDiesel = explainedFee.isGasless; - const isEnoughDiesel = withDiesel && amount && balance && dieselAmount - ? isGaslessWithStars - ? true - : balance - amount >= dieselAmount.token - : undefined; - const isInsufficientFee = (fee !== undefined && !isEnoughNativeCoin && !isDieselAvailable) - || (withDiesel && !isEnoughDiesel); - const isAmountGiven = amount !== undefined; + + // Note: this constant has 3 distinct meaningful values + const isEnoughBalance = isBalanceSufficientForTransfer({ + tokenBalance: balance, + nativeTokenBalance: nativeToken.amount, + transferAmount: isNftTransfer ? NFT_TRANSFER_AMOUNT : amount, + fullFee: explainedFee.fullFee?.terms, + canTransferFullBalance: explainedFee.canTransferFullBalance, + }); + + const isAmountMissing = !isNftTransfer && !amount; const isDisabledDebounce = useRef(false); @@ -251,7 +243,8 @@ function TransferInitial({ canTransferFullBalance: explainedFee.canTransferFullBalance, }); - const authorizeDieselInterval = isDieselNotAuthorized && isDieselAuthorizationStarted && tokenSlug && !isToncoin + const isDieselNotAuthorized = diesel?.status === 'not-authorized'; + const authorizeDieselInterval = isDieselNotAuthorized && isDieselAuthorizationStarted ? AUTHORIZE_DIESEL_INTERVAL_MS : undefined; @@ -260,9 +253,7 @@ function TransferInitial({ ); const updateDieselState = useLastCallback(() => { - if (tokenSlug) { - fetchTransferDieselState({ tokenSlug }); - } + fetchTransferDieselState({ tokenSlug }); }); useInterval(updateDieselState, authorizeDieselInterval); @@ -301,8 +292,7 @@ function TransferInitial({ // Note: this effect doesn't watch amount changes mainly because it's tricky to program a fee recalculation avoidance // when the amount changes due to a fee change. And it's not needed because the fee doesn't depend on the amount. useEffect(() => { - if (!(isAmountGiven || nfts?.length) || !isAddressValid || skipNextFeeEstimate.current) { - skipNextFeeEstimate.current = false; + if (isAmountMissing || !isAddressValid) { return; } @@ -330,7 +320,7 @@ function TransferInitial({ isDisabledDebounce.current = false; runFunction(); } - }, [isAmountGiven, binPayload, comment, isAddressValid, isNftTransfer, nfts, stateInit, toAddress, tokenSlug]); + }, [isAmountMissing, binPayload, comment, isAddressValid, isNftTransfer, nfts, stateInit, toAddress, tokenSlug]); const handleTokenChange = useLastCallback((slug: string) => { changeTransferToken({ tokenSlug: slug }); @@ -504,22 +494,23 @@ function TransferInitial({ setTransferComment({ comment: trimStringByMaxBytes(value, COMMENT_MAX_SIZE_BYTES) }); }); + const hasToAddressError = toAddress.length > 0 && !isAddressValid; + const isAmountGreaterThanBalance = !isNftTransfer && balance !== undefined && amount !== undefined + && amount > balance; + const hasAmountError = !isNftTransfer && ((amount ?? 0) < 0 || isEnoughBalance === false); + const isInsufficientFee = isEnoughBalance === false && !isAmountGreaterThanBalance; const isCommentRequired = Boolean(toAddress) && isMemoRequired; const hasCommentError = isCommentRequired && !comment; - const requiredAmount = isNftTransfer ? NFT_TRANSFER_AMOUNT : amount; - const isInsufficientBalance = balance !== undefined && amount !== undefined && amount > balance; - const hasToAddressError = toAddress.length > 0 && !isAddressValid; - const hasAmountError = Boolean(amount) && (amount < 0 || isInsufficientBalance || isInsufficientFee); - const canSubmit = Boolean(tokenSlug && isAddressValid && requiredAmount && balance && requiredAmount > 0 - && requiredAmount <= balance && !hasAmountError - && (isEnoughNativeCoin || isEnoughDiesel || isDieselNotAuthorized) && !hasCommentError - && (!isNftTransfer || Boolean(nfts?.length))); + const canSubmit = isDieselNotAuthorized || Boolean( + isAddressValid && !isAmountMissing && !hasAmountError && isEnoughBalance && !hasCommentError + && !(isNftTransfer && !nfts?.length), + ); const handleSubmit = useLastCallback((e) => { e.preventDefault(); - if (withDiesel && dieselStatus === 'not-authorized') { + if (isDieselNotAuthorized) { authorizeDiesel(); return; } @@ -530,19 +521,16 @@ function TransferInitial({ vibrate(); - // Removes an excessive loading animation appearing in the Confirm button of the NFT transfer confirmation modal - skipNextFeeEstimate.current = true; - submitTransferInitial({ - tokenSlug: tokenSlug!, - amount: isNftTransfer ? NFT_TRANSFER_AMOUNT : amount!, + tokenSlug, + amount: amount ?? 0n, toAddress, comment, binPayload, shouldEncrypt, nftAddresses: isNftTransfer ? nfts!.map(({ address }) => address) : undefined, - withDiesel, - isGaslessWithStars, + withDiesel: explainedFee.isGasless, + isGaslessWithStars: diesel?.status === 'stars-fee', stateInit, }); }); @@ -643,7 +631,7 @@ function TransferInitial({ let content: TeactNode = ' '; if (amount) { - if (isInsufficientBalance) { + if (isAmountGreaterThanBalance) { transitionKey = 1; content = {lang('Insufficient balance')}; } else if (isInsufficientFee) { @@ -812,28 +800,25 @@ function TransferInitial({ const withButton = isQrScannerSupported || withPasteButton || withAddressClearButton; function renderButtonText() { - if (withDiesel && !isDieselAvailable) { - if (dieselStatus === 'pending-previous') { - return lang('Awaiting Previous Fee'); - } else { - return lang('Authorize %token% Fee', { token: symbol! }); - } - } else { - return lang('$send_token_symbol', isNftTransfer ? 'NFT' : symbol || 'TON'); + if (diesel?.status === 'not-authorized') { + return lang('Authorize %token% Fee', { token: symbol! }); + } + if (diesel?.status === 'pending-previous' && isInsufficientFee) { + return lang('Awaiting Previous Fee'); } + return lang('$send_token_symbol', isNftTransfer ? 'NFT' : symbol || 'TON'); } const shouldIgnoreErrors = isAddressBookOpen && shouldRenderSuggestions; function renderFee() { - const shouldShowFull = isInsufficientFee && !isInsufficientBalance; let terms: FeeTerms | undefined; let precision: FeePrecision = 'exact'; if (isNftTransfer) { // NFT fee estimation is not supported yet. It will be added later. } else if (amount) { - const actualFee = shouldShowFull ? explainedFee.fullFee : explainedFee.realFee; + const actualFee = isInsufficientFee ? explainedFee.fullFee : explainedFee.realFee; if (actualFee) { ({ terms, precision } = actualFee); } diff --git a/src/components/transfer/TransferModal.tsx b/src/components/transfer/TransferModal.tsx index f3680196..97c61110 100644 --- a/src/components/transfer/TransferModal.tsx +++ b/src/components/transfer/TransferModal.tsx @@ -234,7 +234,7 @@ function TransferModal({ (Date.now()); + +export function closeModal() { + setModalCloseSignal(Date.now()); +} + function Modal({ dialogRef, title, @@ -119,6 +126,12 @@ function Modal({ } }, [isOpen, isCompact]); + useEffect(() => { + if (IS_DELEGATED_BOTTOM_SHEET || !isOpen) return undefined; + + return getModalCloseSignal.subscribe(onClose); + }, [isOpen, onClose]); + useEffect( () => (isOpen ? captureKeyboardListeners({ onEsc: { handler: onClose, shouldPreventDefault: IS_EXTENSION }, diff --git a/src/config.ts b/src/config.ts index c210293a..fdcc654d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -249,22 +249,12 @@ export const CHAIN_CONFIG = { isDnsSupported: true, addressRegex: /^([-\w_]{48}|0:[\da-h]{64})$/i, nativeToken: TONCOIN, - gas: { - maxSwap: 400_000_000n, // 0.4 TON - maxTransfer: 15_000_000n, // 0.015 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, - gas: { - maxSwap: undefined, - maxTransfer: 1_000_000n, // 1 TRX - maxTransferToken: 30_000_000n, // 30 TRX - }, mainnet: { apiUrl: TRON_MAINNET_API_URL, usdtAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', @@ -405,7 +395,9 @@ export const INIT_SWAP_ASSETS: Record = { }; export const DEFAULT_TRX_SWAP_FIRST_TOKEN_SLUG = TONCOIN.slug; +export const DEFAULT_SWAP_FISRT_TOKEN_SLUG = TONCOIN.slug; export const DEFAULT_SWAP_SECOND_TOKEN_SLUG = TON_USDT_SLUG; +export const DEFAULT_TRANSFER_TOKEN_SLUG = TONCOIN.slug; export const DEFAULT_CEX_SWAP_SECOND_TOKEN_SLUG = TRC20_USDT_MAINNET_SLUG; export const SWAP_DEX_LABELS: Record = { dedust: 'DeDust', diff --git a/src/global/actions/api/swap.ts b/src/global/actions/api/swap.ts index acc7e675..aa545aa0 100644 --- a/src/global/actions/api/swap.ts +++ b/src/global/actions/api/swap.ts @@ -26,6 +26,7 @@ import { import { DEFAULT_FEE, + DEFAULT_SWAP_FISRT_TOKEN_SLUG, DEFAULT_SWAP_SECOND_TOKEN_SLUG, IS_CAPACITOR, TONCOIN, @@ -232,9 +233,17 @@ addActionHandler('startSwap', async (global, actions, payload) => { addActionHandler('setDefaultSwapParams', (global, actions, payload) => { let { tokenInSlug: requiredTokenInSlug, tokenOutSlug: requiredTokenOutSlug } = payload ?? {}; + const { withResetAmount } = payload ?? {}; - requiredTokenInSlug = requiredTokenInSlug || TONCOIN.slug; + requiredTokenInSlug = requiredTokenInSlug || DEFAULT_SWAP_FISRT_TOKEN_SLUG; requiredTokenOutSlug = requiredTokenOutSlug || DEFAULT_SWAP_SECOND_TOKEN_SLUG; + if ( + global.currentSwap.tokenInSlug === requiredTokenInSlug + && global.currentSwap.tokenOutSlug === requiredTokenOutSlug + && !withResetAmount + ) { + return; + } global = updateCurrentSwap(global, { tokenInSlug: requiredTokenInSlug, @@ -247,6 +256,7 @@ addActionHandler('setDefaultSwapParams', (global, actions, payload) => { amountOutMin: '0', inputSource: SwapInputSource.In, isDexLabelChanged: undefined, + ...(withResetAmount ? { amountIn: undefined, amountOut: undefined } : undefined), }); setGlobal(global); }); diff --git a/src/global/actions/api/transfer.ts b/src/global/actions/api/transfer.ts index 87a5fe2d..a128ffe9 100644 --- a/src/global/actions/api/transfer.ts +++ b/src/global/actions/api/transfer.ts @@ -5,6 +5,7 @@ import { TransferState } from '../../types'; import { IS_CAPACITOR, NFT_BATCH_SIZE } from '../../../config'; import { vibrateOnError, vibrateOnSuccess } from '../../../util/capacitor'; +import { getDieselTokenAmount } from '../../../util/fee/transferFee'; import { callActionInNative } from '../../../util/multitab'; import { IS_DELEGATING_BOTTOM_SHEET } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; @@ -263,7 +264,7 @@ addActionHandler('submitTransferPassword', async (global, actions, { password }) shouldEncrypt, isBase64Data: Boolean(binPayload), withDiesel, - dieselAmount: diesel?.amount.token, + dieselAmount: diesel && getDieselTokenAmount(diesel), stateInit, isGaslessWithStars, }; diff --git a/src/global/actions/ui/initial.ts b/src/global/actions/ui/initial.ts index 60959e5a..80550eb0 100644 --- a/src/global/actions/ui/initial.ts +++ b/src/global/actions/ui/initial.ts @@ -5,7 +5,12 @@ import { ApiCommonError, ApiTransactionDraftError, ApiTransactionError } from '. import { AppState } from '../../types'; import { - DEFAULT_SWAP_SECOND_TOKEN_SLUG, IS_CAPACITOR, IS_EXTENSION, TONCOIN, + DEFAULT_SWAP_FISRT_TOKEN_SLUG, + DEFAULT_SWAP_SECOND_TOKEN_SLUG, + DEFAULT_TRANSFER_TOKEN_SLUG, + IS_CAPACITOR, + IS_EXTENSION, + TONCOIN, } from '../../../config'; import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { parseAccountId } from '../../../util/account'; @@ -168,8 +173,31 @@ addActionHandler('selectToken', (global, actions, { slug } = {}) => { actions.changeTransferToken({ tokenSlug: slug }); } } else { - actions.setDefaultSwapParams({ tokenInSlug: undefined, tokenOutSlug: undefined }); - actions.changeTransferToken({ tokenSlug: TONCOIN.slug }); + const currentActivityToken = global.byAccountId[global.currentAccountId!].currentTokenSlug; + + const isDefaultFirstTokenOutSwap = global.currentSwap.tokenOutSlug === DEFAULT_SWAP_FISRT_TOKEN_SLUG + && global.currentSwap.tokenInSlug === DEFAULT_SWAP_SECOND_TOKEN_SLUG; + + const shouldResetSwap = global.currentSwap.tokenOutSlug === currentActivityToken + && ( + ( + global.currentSwap.tokenInSlug === DEFAULT_SWAP_FISRT_TOKEN_SLUG + && global.currentSwap.tokenOutSlug !== DEFAULT_SWAP_SECOND_TOKEN_SLUG + ) + || isDefaultFirstTokenOutSwap + ); + + if (shouldResetSwap) { + actions.setDefaultSwapParams({ tokenInSlug: undefined, tokenOutSlug: undefined, withResetAmount: true }); + } + + const shouldResetTransfer = (global.currentTransfer.tokenSlug === currentActivityToken + && global.currentTransfer.tokenSlug !== DEFAULT_TRANSFER_TOKEN_SLUG) + && !global.currentTransfer.nfts?.length; + + if (shouldResetTransfer) { + actions.changeTransferToken({ tokenSlug: DEFAULT_TRANSFER_TOKEN_SLUG, withResetAmount: true }); + } } return updateCurrentAccountState(global, { currentTokenSlug: slug }); diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index beb11ca6..68dbad95 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -75,6 +75,7 @@ import { switchAccount } from '../api/auth'; import { reportAppLockActivityEvent } from '../../../components/AppLocked'; import { getCardNftImageUrl } from '../../../components/main/sections/Card/helpers/getCardNftImageUrl'; +import { closeModal } from '../../../components/ui/Modal'; const OPEN_LEDGER_TAB_DELAY = 500; const APP_VERSION_URL = IS_ANDROID_APP ? `${IS_PRODUCTION ? PRODUCTION_URL : BETA_URL}/version.txt` : 'version.txt'; @@ -819,6 +820,20 @@ addActionHandler('clearAccentColorFromNft', (global) => { }); }); +addActionHandler('closeAnyModal', () => { + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('closeAnyModal'); + return; + } + + closeModal(); +}); + +addActionHandler('closeAllEntities', (global, actions) => { + actions.closeAnyModal(); + actions.closeMediaViewer(); +}); + async function connectLedgerAndGetHardwareState() { const ledgerApi = await import('../../../util/ledger'); let newHardwareState; diff --git a/src/global/actions/ui/transfer.ts b/src/global/actions/ui/transfer.ts index 55838031..52b04984 100644 --- a/src/global/actions/ui/transfer.ts +++ b/src/global/actions/ui/transfer.ts @@ -27,12 +27,18 @@ addActionHandler('startTransfer', (global, actions, payload) => { } }); -addActionHandler('changeTransferToken', (global, actions, { tokenSlug }) => { +addActionHandler('changeTransferToken', (global, actions, { tokenSlug, withResetAmount }) => { const { amount, tokenSlug: currentTokenSlug } = global.currentTransfer; + if (tokenSlug === currentTokenSlug && !withResetAmount) { + return; + } + const currentToken = currentTokenSlug ? global.tokenInfo.bySlug[currentTokenSlug] : undefined; const newToken = global.tokenInfo.bySlug[tokenSlug]; - if (amount && currentToken?.decimals !== newToken?.decimals) { + if (withResetAmount) { + global = updateCurrentTransfer(global, { amount: undefined }); + } else if (amount && currentToken?.decimals !== newToken?.decimals) { global = updateCurrentTransfer(global, { amount: fromDecimal(toDecimal(amount, currentToken?.decimals), newToken?.decimals), }); @@ -43,6 +49,7 @@ addActionHandler('changeTransferToken', (global, actions, { tokenSlug }) => { fee: undefined, realFee: undefined, diesel: undefined, + nfts: undefined, })); }); diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 992a50e1..c4c6e5dc 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -13,10 +13,10 @@ import { ANIMATION_LEVEL_DEFAULT, DEFAULT_SLIPPAGE_VALUE, DEFAULT_STAKING_STATE, + DEFAULT_TRANSFER_TOKEN_SLUG, INIT_SWAP_ASSETS, THEME_DEFAULT, TOKEN_INFO, - TONCOIN, } from '../config'; import { IS_IOS_APP, USER_AGENT_LANG_CODE } from '../util/windowEnvironment'; @@ -37,7 +37,7 @@ export const INITIAL_STATE: GlobalState = { currentTransfer: { state: TransferState.None, - tokenSlug: TONCOIN.slug, + tokenSlug: DEFAULT_TRANSFER_TOKEN_SLUG, }, currentSwap: { diff --git a/src/global/types.ts b/src/global/types.ts index 0a560a08..620d85f5 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -763,7 +763,7 @@ export interface ActionPayloads { binPayload?: string; stateInit?: string; } | undefined; - changeTransferToken: { tokenSlug: string }; + changeTransferToken: { tokenSlug: string; withResetAmount?: boolean }; fetchTransferFee: { tokenSlug: string; toAddress: string; @@ -847,6 +847,8 @@ export interface ActionPayloads { }; closeHideNftModal: undefined; + closeAnyModal: undefined; + closeAllEntities: undefined; submitSignature: { password: string }; clearSignatureError: undefined; cancelSignature: undefined; @@ -982,7 +984,7 @@ export interface ActionPayloads { toAddress?: string; } | undefined; cancelSwap: { shouldReset?: boolean } | undefined; - setDefaultSwapParams: { tokenInSlug?: string; tokenOutSlug?: string } | undefined; + setDefaultSwapParams: { tokenInSlug?: string; tokenOutSlug?: string; withResetAmount?: boolean } | undefined; switchSwapTokens: undefined; setSwapTokenIn: { tokenSlug: string }; setSwapTokenOut: { tokenSlug: string }; diff --git a/src/util/capacitor/notifications.ts b/src/util/capacitor/notifications.ts index 414e7078..f1901587 100644 --- a/src/util/capacitor/notifications.ts +++ b/src/util/capacitor/notifications.ts @@ -79,7 +79,12 @@ export async function initNotificationsWithGlobal(global: GlobalState) { } function handlePushNotificationActionPerformed(notification: ActionPerformed) { - const { showAnyAccountTx, showAnyAccountTokenActivity, openAnyAccountStakingInfo } = getActions(); + const { + showAnyAccountTx, + showAnyAccountTokenActivity, + openAnyAccountStakingInfo, + closeAllEntities, + } = getActions(); const global = getGlobal(); const notificationData = notification.notification.data as MessageData; const { action, address } = notificationData; @@ -93,6 +98,7 @@ function handlePushNotificationActionPerformed(notification: ActionPerformed) { const network = 'mainnet'; + closeAllEntities(); if (action === 'nativeTx' || action === 'swap') { const { txId } = notificationData; showAnyAccountTx({ accountId, txId, network }); diff --git a/src/util/fee/transferFee.ts b/src/util/fee/transferFee.ts index 07903aa2..9f0d853a 100644 --- a/src/util/fee/transferFee.ts +++ b/src/util/fee/transferFee.ts @@ -39,7 +39,8 @@ type ExplainedTransferFee = { canTransferFullBalance: boolean; }; -type ApiFeeWithDiesel = ApiFee & { diesel: ApiFetchEstimateDieselResult }; +type AvailableDiesel = ApiFetchEstimateDieselResult & { amount: bigint }; +type ApiFeeWithDiesel = ApiFee & { diesel: AvailableDiesel }; type MaxTransferAmountInput = { /** The wallet balance of the transferred token. Undefined means that it's unknown. */ @@ -52,6 +53,13 @@ type MaxTransferAmountInput = { canTransferFullBalance: boolean; }; +type BalanceSufficientForTransferInput = Omit & { + /** The wallet balance of the native token of the transfer chain. Undefined means that it's unknown. */ + nativeTokenBalance: bigint | undefined; + /** The transferred amount. Undefined means that it's unknown. */ + transferAmount: bigint | undefined; +}; + /** * Converts the transfer fee data returned from API into data that is ready to be displayed in the transfer form UI. */ @@ -89,9 +97,38 @@ export function getMaxTransferAmount({ return bigintMax(tokenBalance - fee, 0n); } +/** + * Decides whether the balance is sufficient to transfer the amount and pay the fees. + * Returns undefined when it can't be calculated because of insufficient input data. + */ +export function isBalanceSufficientForTransfer({ + tokenBalance, + nativeTokenBalance, + transferAmount, + fullFee, + canTransferFullBalance, +}: BalanceSufficientForTransferInput) { + if (transferAmount === undefined || tokenBalance === undefined || nativeTokenBalance === undefined || !fullFee) { + return undefined; + } + + const isFullTokenTransfer = transferAmount === tokenBalance && canTransferFullBalance; + const tokenRequiredAmount = (fullFee.token ?? 0n) + (isFullTokenTransfer ? 0n : transferAmount); + const nativeTokenRequiredAmount = fullFee.native ?? 0n; + + return tokenRequiredAmount <= tokenBalance && nativeTokenRequiredAmount <= nativeTokenBalance; +} + +export function isDieselAvailable(diesel: ApiFetchEstimateDieselResult): diesel is AvailableDiesel { + return diesel.status !== 'not-available' && diesel.amount !== undefined; +} + +export function getDieselTokenAmount(diesel: ApiFetchEstimateDieselResult) { + return diesel.status === 'stars-fee' ? 0n : (diesel.amount ?? 0n); +} + function shouldUseDiesel(input: ApiFee): input is ApiFeeWithDiesel { - return input.diesel !== undefined - && input.diesel.status !== 'not-available'; + return input.diesel !== undefined && isDieselAvailable(input.diesel); } /** @@ -127,10 +164,9 @@ function explainGasfullTransferFee(input: ApiFee) { function explainGaslessTransferFee({ diesel }: ApiFeeWithDiesel) { const isStarsDiesel = diesel.status === 'stars-fee'; const dieselKey = isStarsDiesel ? 'stars' : 'token'; - const dieselAmount = diesel.amount[dieselKey]; - const realFeeInDiesel = convertFee(diesel.realFee, diesel.nativeAmount, dieselAmount); + const realFeeInDiesel = convertFee(diesel.realFee, diesel.nativeAmount, diesel.amount); // Cover as much displayed real fee as possible with diesel, because in the excess it will return as the native token. - const dieselRealFee = bigintMin(dieselAmount, realFeeInDiesel); + const dieselRealFee = bigintMin(diesel.amount, realFeeInDiesel); // Cover the remaining real fee with the native token. const nativeRealFee = bigintMax(0n, diesel.realFee - diesel.nativeAmount); @@ -140,8 +176,7 @@ function explainGaslessTransferFee({ diesel }: ApiFeeWithDiesel) { fullFee: { precision: 'lessThan', terms: { - token: diesel.amount.token, - stars: diesel.amount.stars, + [dieselKey]: diesel.amount, native: diesel.remainingFee, }, },