diff --git a/changelogs/3.0.28.txt b/changelogs/3.0.28.txt new file mode 100644 index 00000000..619f4cd5 --- /dev/null +++ b/changelogs/3.0.28.txt @@ -0,0 +1 @@ +Bug fixes and performance improvements diff --git a/package-lock.json b/package-lock.json index 31b9d6f5..8c7b00de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mytonwallet", - "version": "3.0.27", + "version": "3.0.28", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytonwallet", - "version": "3.0.27", + "version": "3.0.28", "license": "GPL-3.0-or-later", "dependencies": { "@awesome-cordova-plugins/core": "6.8.0", diff --git a/package.json b/package.json index f756e08a..9f894974 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mytonwallet", - "version": "3.0.27", + "version": "3.0.28", "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 d0ecf17c..0baec4d1 100644 --- a/public/version.txt +++ b/public/version.txt @@ -1 +1 @@ -3.0.27 +3.0.28 diff --git a/src/api/chains/ton/tokens.ts b/src/api/chains/ton/tokens.ts index d9e8b373..b0f4c709 100644 --- a/src/api/chains/ton/tokens.ts +++ b/src/api/chains/ton/tokens.ts @@ -180,6 +180,7 @@ export async function buildTokenTransfer(options: { toAddress: string; amount: bigint; payload?: AnyPayload; + shouldSkipMintless?: boolean; }) { const { network, @@ -187,6 +188,7 @@ export async function buildTokenTransfer(options: { fromAddress, toAddress, amount, + shouldSkipMintless, } = options; let { payload } = options; @@ -201,7 +203,7 @@ export async function buildTokenTransfer(options: { customPayload, stateInit, } = await getMintlessParams({ - network, fromAddress, token, tokenWalletAddress, + network, fromAddress, token, tokenWalletAddress, shouldSkipMintless, }); if (isTokenWalletDeployed) { @@ -244,9 +246,10 @@ export async function getMintlessParams(options: { fromAddress: string; token: ApiToken; tokenWalletAddress: string; + shouldSkipMintless?: boolean; }) { const { - network, fromAddress, token, tokenWalletAddress, + network, fromAddress, token, tokenWalletAddress, shouldSkipMintless, } = options; let isTokenWalletDeployed = true; @@ -257,7 +260,7 @@ export async function getMintlessParams(options: { let isMintlessClaimed: boolean | undefined; let mintlessTokenBalance: bigint | undefined; - if (isMintlessToken) { + if (isMintlessToken && !shouldSkipMintless) { isTokenWalletDeployed = !!(await isActiveSmartContract(network, tokenWalletAddress)); isMintlessClaimed = isTokenWalletDeployed && await checkMintlessTokenWalletIsClaimed(network, tokenWalletAddress); diff --git a/src/api/chains/ton/transactions.ts b/src/api/chains/ton/transactions.ts index 1679d6a3..bcc0740d 100644 --- a/src/api/chains/ton/transactions.ts +++ b/src/api/chains/ton/transactions.ts @@ -22,6 +22,7 @@ import type { ApiSubmitTransferOptions, ApiSubmitTransferTonResult, ApiSubmitTransferWithDieselResult, + ApiTonWalletVersion, ApiTransactionExtra, TonTransferParams, } from './types'; @@ -461,7 +462,7 @@ export async function submitTransferWithDiesel(options: { const { network } = parseAccountId(accountId); - const [{ address: fromAddress, version }, keyPair] = await Promise.all([ + const [{ address: fromAddress }, keyPair] = await Promise.all([ fetchStoredTonWallet(accountId), fetchKeyPair(accountId, password), ]); @@ -495,24 +496,17 @@ export async function submitTransferWithDiesel(options: { fromAddress, toAddress: DIESEL_ADDRESS, amount: dieselAmount, + shouldSkipMintless: true, }), ['tokenWallet']), ); } - let result; - const gaslessType = version === 'W5' ? 'w5' : 'diesel'; - if (version === 'W5') { - result = await submitMultiTransfer({ - accountId, - password, - messages, - gaslessType, - }); - } else { - result = await submitMultiTransfer({ - accountId, password, messages, gaslessType, - }); - } + const result = await submitMultiTransfer({ + accountId, + password, + messages, + isGasless: true, + }); return { ...result, encryptedComment }; } catch (err) { @@ -845,7 +839,7 @@ export async function checkMultiTransactionDraft( let totalAmount: bigint = 0n; - const { isInitialized } = await fetchStoredTonWallet(accountId); + const { isInitialized, version } = await fetchStoredTonWallet(accountId); try { for (const { toAddress, amount } of messages) { @@ -870,7 +864,7 @@ export async function checkMultiTransactionDraft( const { balance } = await getWalletInfo(network, wallet); - const { transaction } = await signMultiTransaction(network, wallet, messages); + const { transaction } = await signMultiTransaction(network, wallet, messages, undefined, version); const realFee = await calculateFee(network, wallet, transaction, isInitialized); @@ -895,17 +889,17 @@ interface SubmitMultiTransferOptions { password: string; messages: TonTransferParams[]; expireAt?: number; - gaslessType?: GaslessType; + isGasless?: boolean; } export async function submitMultiTransfer({ - accountId, password, messages, expireAt, gaslessType, + accountId, password, messages, expireAt, isGasless, }: SubmitMultiTransferOptions): Promise { const { network } = parseAccountId(accountId); try { const account = await fetchStoredAccount(accountId); - const { address: fromAddress, isInitialized } = account.ton; + const { address: fromAddress, isInitialized, version } = account.ton; const wallet = await getTonWallet(accountId, account.ton); const privateKey = await fetchPrivateKey(accountId, password, account); @@ -918,12 +912,14 @@ export async function submitMultiTransfer({ const { balance } = await getWalletInfo(network, wallet!); - const isW5 = gaslessType === 'w5'; + const gaslessType = isGasless ? version === 'W5' ? 'w5' : 'diesel' : undefined; + const withW5Gasless = gaslessType === 'w5'; + const { seqno, transaction } = await signMultiTransaction( - network, wallet!, messages, privateKey, expireAt, isW5, + network, wallet!, messages, privateKey, version, expireAt, withW5Gasless, ); - if (!gaslessType) { + if (!isGasless) { const fee = await calculateFee(network, wallet!, transaction, isInitialized); if (balance < totalAmount + fee) { return { error: ApiTransactionError.InsufficientBalance }; @@ -935,7 +931,7 @@ export async function submitMultiTransfer({ client, wallet!, transaction, gaslessType, ); - if (!gaslessType) { + if (!isGasless) { addPendingTransfer(network, fromAddress, seqno, boc); } @@ -965,8 +961,9 @@ async function signMultiTransaction( wallet: TonWallet, messages: TonTransferParams[], privateKey: Uint8Array = new Uint8Array(64), + version: ApiTonWalletVersion, expireAt?: number, - withW5Diesel = false, + withW5Gasless = false, ) { const { seqno } = await getWalletInfo(network, wallet); if (!expireAt) { @@ -996,8 +993,13 @@ async function signMultiTransaction( }); }); + if (version === 'W5' && !withW5Gasless) { + // TODO Remove it. There is bug in @ton/ton library that causes transactions to be executed in reverse order. + preparedMessages.reverse(); + } + let transaction; - if (withW5Diesel) { + if (withW5Gasless) { const actionList = packActionsList(preparedMessages.map( (msg) => new ActionSendMsg(SendMode.PAY_GAS_SEPARATELY, msg), )); diff --git a/src/api/methods/swap.ts b/src/api/methods/swap.ts index f15fd5b2..041a7048 100644 --- a/src/api/methods/swap.ts +++ b/src/api/methods/swap.ts @@ -76,7 +76,7 @@ export async function swapSubmit( password: string, transfers: ApiSwapTransfer[], historyItem: ApiSwapHistoryItem, - withDiesel?: boolean, + isGasless?: boolean, ) { const { address } = await fetchStoredTonWallet(accountId); const transferList: TonTransferParams[] = transfers.map((transfer) => ({ @@ -89,13 +89,9 @@ export async function swapSubmit( transferList[0] = await ton.insertMintlessPayload('mainnet', address, historyItem.from, transferList[0]); } - const gaslessType = withDiesel ? 'diesel' : undefined; - - const result = await ton.submitMultiTransfer( - { - accountId, password, messages: transferList, gaslessType, - }, - ); + const result = await ton.submitMultiTransfer({ + accountId, password, messages: transferList, isGasless, + }); if ('error' in result) { return result; diff --git a/src/components/main/sections/Content/NftSelectionHeader.tsx b/src/components/main/sections/Content/NftSelectionHeader.tsx index 1f201af6..abb777be 100644 --- a/src/components/main/sections/Content/NftSelectionHeader.tsx +++ b/src/components/main/sections/Content/NftSelectionHeader.tsx @@ -25,6 +25,7 @@ import styles from './NftCollectionHeader.module.scss'; interface StateProps { byAddress?: Record; selectedAddresses?: string[]; + currentCollectionAddress?: string; } const MENU_ITEMS: DropdownItem[] = [{ @@ -37,11 +38,15 @@ const MENU_ITEMS: DropdownItem[] = [{ name: 'Burn', value: 'burn', isDangerous: true, +}, { + name: 'Select All', + value: 'select-all', + withSeparator: true, }]; -function NftSelectionHeader({ selectedAddresses, byAddress }: StateProps) { +function NftSelectionHeader({ selectedAddresses, byAddress, currentCollectionAddress }: StateProps) { const { - clearNftsSelection, startTransfer, burnNfts, openHideNftModal, + selectAllNfts, clearNftsSelection, startTransfer, burnNfts, openHideNftModal, } = getActions(); const lang = useLang(); @@ -120,6 +125,10 @@ function NftSelectionHeader({ selectedAddresses, byAddress }: StateProps) { handleBurnClick(); break; } + case 'select-all': { + selectAllNfts({ collectionAddress: currentCollectionAddress }); + break; + } } }); @@ -161,8 +170,8 @@ function NftSelectionHeader({ selectedAddresses, byAddress }: StateProps) { export default memo(withGlobal((global): StateProps => { const { - selectedAddresses, byAddress, + selectedAddresses, byAddress, currentCollectionAddress, } = selectCurrentAccountState(global)?.nfts || {}; - return { selectedAddresses, byAddress }; + return { selectedAddresses, byAddress, currentCollectionAddress }; })(NftSelectionHeader)); diff --git a/src/components/transfer/TransferInitial.tsx b/src/components/transfer/TransferInitial.tsx index be1c3acc..1240f792 100644 --- a/src/components/transfer/TransferInitial.tsx +++ b/src/components/transfer/TransferInitial.tsx @@ -106,6 +106,17 @@ const INPUT_CLEAR_BUTTON_ID = 'input-clear-button'; const runThrottled = throttle((cb) => cb(), 1500, true); +function doesSavedAddressFitSearch(savedAddress: SavedAddress, search: string) { + const searchQuery = search.toLowerCase(); + const { address, name } = savedAddress; + + return ( + address.toLowerCase().startsWith(searchQuery) + || address.toLowerCase().endsWith(searchQuery) + || name.toLowerCase().split(/\s+/).some((part) => part.startsWith(searchQuery)) + ); +} + function TransferInitial({ isStatic, tokenSlug = TONCOIN.slug, @@ -189,6 +200,10 @@ function TransferInitial({ return tokens?.find((token) => !token.tokenAddress && token.chain === chain); }, [tokens, chain])!; + const isUpdatingAmountDueToMaxChange = useRef(false); + const [isMaxAmountSelected, setMaxAmountSelected] = useState(false); + const [prevDieselAmount, setPrevDieselAmount] = useState(dieselAmount); + const isToncoin = tokenSlug === TONCOIN.slug; const toncoinToken = useMemo(() => tokens?.find((token) => token.slug === TONCOIN.slug), [tokens])!; const isToncoinFullBalance = isToncoin && balance === amount; @@ -217,18 +232,21 @@ function TransferInitial({ const isDieselNotAuthorized = dieselStatus === 'not-authorized'; const withDiesel = dieselStatus && dieselStatus !== 'not-available'; const isEnoughDiesel = withDiesel && amount && balance && dieselAmount - ? isGaslessWithStars + ? isGaslessWithStars || isUpdatingAmountDueToMaxChange.current ? true : balance - amount >= dieselAmount : undefined; const feeSymbol = isGaslessWithStars ? STARS_SYMBOL : symbol; - const maxAmount = withDiesel && dieselAmount && balance - ? isGaslessWithStars - ? balance - : balance - dieselAmount - : balance; + const maxAmount = useMemo(() => { + if (withDiesel && dieselAmount && balance) { + return isGaslessWithStars || isUpdatingAmountDueToMaxChange.current + ? balance + : balance - dieselAmount; + } + return balance; + }, [balance, dieselAmount, isGaslessWithStars, withDiesel]); const authorizeDieselInterval = isDieselNotAuthorized && isDieselAuthorizationStarted && tokenSlug && !isToncoin ? AUTHORIZE_DIESEL_INTERVAL_MS @@ -324,7 +342,14 @@ function TransferInitial({ }, [isToncoin, tokenSlug, amount, balance, fee, decimals, validateAndSetAmount, isDieselAvailable]); useEffect(() => { - if (!toAddress || hasToAddressError || !(amount || nfts?.length) || !isAddressValid) { + if ( + !toAddress + || hasToAddressError + || !(amount || nfts?.length) + || !isAddressValid + || isUpdatingAmountDueToMaxChange.current + ) { + isUpdatingAmountDueToMaxChange.current = false; return; } @@ -359,6 +384,18 @@ function TransferInitial({ tokenSlug, ]); + useEffect(() => { + if (isMaxAmountSelected && prevDieselAmount !== dieselAmount) { + isUpdatingAmountDueToMaxChange.current = true; + + setMaxAmountSelected(false); + setPrevDieselAmount(dieselAmount); + setTransferAmount({ amount: maxAmount }); + } + }, [ + dieselAmount, maxAmount, isMaxAmountSelected, prevDieselAmount, withDiesel, balance, isGaslessWithStars, + ]); + const handleTokenChange = useLastCallback( (slug: string) => { changeTransferToken({ tokenSlug: slug }); @@ -529,6 +566,7 @@ function TransferInitial({ vibrate(); + setMaxAmountSelected(true); setTransferAmount({ amount: maxAmount }); }); @@ -582,7 +620,7 @@ function TransferInitial({ } return savedAddresses.filter( - (item) => item.address.includes(toAddress) || item.name.includes(toAddress), + (item) => doesSavedAddressFitSearch(item, toAddress), ).map((item) => renderAddressItem({ key: `saved-${item.address}-${item.chain}`, address: item.address, @@ -625,7 +663,7 @@ function TransferInitial({ }, [] as (SavedAddress & { isHardware?: boolean })[]); return otherAccounts.filter( - (item) => item.address.includes(toAddress) || item.name.includes(toAddress), + (item) => doesSavedAddressFitSearch(item, toAddress), ).map(({ address, name, chain: addressChain, isHardware, }) => renderAddressItem({ @@ -638,8 +676,10 @@ function TransferInitial({ })); }, [otherAccountIds, savedAddresses, accounts, isMultichainAccount, toAddress]); + const shouldRenderSuggestions = !!renderedSavedAddresses?.length || !!renderedOtherAccounts?.length; + function renderAddressBook() { - if (!renderedSavedAddresses && !renderedOtherAccounts) return undefined; + if (!shouldRenderSuggestions) return undefined; return ( {renderedSavedAddresses} @@ -883,6 +923,8 @@ function TransferInitial({ } } + const shouldIgnoreErrors = isAddressBookOpen && shouldRenderSuggestions; + return ( <>
@@ -896,7 +938,7 @@ function TransferInitial({ label={lang('Recipient Address')} placeholder={lang('Wallet address or domain')} value={isAddressFocused ? toAddress : toAddressShort} - error={hasToAddressError ? (lang('Incorrect address') as string) : undefined} + error={hasToAddressError && !shouldIgnoreErrors ? (lang('Incorrect address') as string) : undefined} onInput={handleAddressInput} onFocus={handleAddressFocus} onBlur={handleAddressBlur} diff --git a/src/global/actions/ui/nfts.ts b/src/global/actions/ui/nfts.ts index 1675424c..e4c59e61 100644 --- a/src/global/actions/ui/nfts.ts +++ b/src/global/actions/ui/nfts.ts @@ -37,6 +37,33 @@ addActionHandler('selectNfts', (global, actions, { addresses }) => { setGlobal(global); }); +addActionHandler('selectAllNfts', (global, actions, { collectionAddress }) => { + const accountId = global.currentAccountId!; + const { + blacklistedNftAddresses, + whitelistedNftAddresses, + } = selectAccountState(global, accountId) || {}; + + const whitelistedNftAddressesSet = new Set(whitelistedNftAddresses); + const blacklistedNftAddressesSet = new Set(blacklistedNftAddresses); + const { nfts: accountNfts } = selectAccountState(global, accountId)!; + const nfts = Object.values(accountNfts!.byAddress).filter((nft) => ( + !nft.isHidden || whitelistedNftAddressesSet.has(nft.address) + ) && !blacklistedNftAddressesSet.has(nft.address) && ( + collectionAddress === undefined || ( + collectionAddress !== undefined && nft.collectionAddress === collectionAddress + ) + )); + + global = updateAccountState(global, accountId, { + nfts: { + ...accountNfts!, + selectedAddresses: nfts.map(({ address }) => address), + }, + }); + setGlobal(global); +}); + addActionHandler('clearNftSelection', (global, actions, { address }) => { const accountId = global.currentAccountId!; global = removeFromSelectedAddresses(global, accountId, address); diff --git a/src/global/types.ts b/src/global/types.ts index 90c29799..ac4c0bad 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -770,6 +770,7 @@ export interface ActionPayloads { openNftCollection: { address: string }; closeNftCollection: undefined; selectNfts: { addresses: string[] }; + selectAllNfts: { collectionAddress?: string }; clearNftSelection: { address: string }; clearNftsSelection: undefined; burnNfts: { nfts: ApiNft[] };