diff --git a/changelogs/1.20.17.txt b/changelogs/1.20.17.txt new file mode 100644 index 00000000..619f4cd5 --- /dev/null +++ b/changelogs/1.20.17.txt @@ -0,0 +1 @@ +Bug fixes and performance improvements diff --git a/package-lock.json b/package-lock.json index 53db3936..e891e2eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mytonwallet", - "version": "1.20.16", + "version": "1.20.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytonwallet", - "version": "1.20.16", + "version": "1.20.17", "license": "GPL-3.0-or-later", "dependencies": { "@awesome-cordova-plugins/core": "^6.6.0", diff --git a/package.json b/package.json index 81bd0ced..7ce936ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mytonwallet", - "version": "1.20.16", + "version": "1.20.17", "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 a46bb8ef..e268e252 100644 --- a/public/version.txt +++ b/public/version.txt @@ -1 +1 @@ -1.20.16 +1.20.17 diff --git a/src/api/blockchains/ton/contracts/NominatorPool.ts b/src/api/blockchains/ton/contracts/NominatorPool.ts index e4b0a347..f9c4ff51 100644 --- a/src/api/blockchains/ton/contracts/NominatorPool.ts +++ b/src/api/blockchains/ton/contracts/NominatorPool.ts @@ -1,5 +1,5 @@ import type { - Cell, Contract, ContractProvider, TupleItem, + Cell, Contract, ContractProvider, } from '@ton/core'; import { Address, beginCell, contractAddress, TupleReader, @@ -33,11 +33,12 @@ export class NominatorPool implements Contract { withdrawRequested: boolean; }[]> { const res = await provider.get('list_nominators', []); + const tupleReader = (res.stack as TupleReader).readTuple(); + const itemsArray = (tupleReader as any).items as bigint[][]; - const items = (res.stack as any).items[0].items; - - return items.map((item: { items: TupleItem[] }) => { - const tuple = new TupleReader(item.items); + return itemsArray.map((items: bigint[]) => { + const tupleItems = items.map((value) => ({ type: 'int' as const, value })); + const tuple = new TupleReader(tupleItems); const hash = tuple.readBigNumber().toString(16).padStart(64, '0'); const address = Address.parse(`0:${hash}`); diff --git a/src/api/blockchains/ton/nfts.ts b/src/api/blockchains/ton/nfts.ts index 8c69ad5e..ceda5f93 100644 --- a/src/api/blockchains/ton/nfts.ts +++ b/src/api/blockchains/ton/nfts.ts @@ -6,6 +6,7 @@ import type { ApiNft, ApiNftUpdate } from '../../types'; import type { ApiCheckTransactionDraftResult } from './types'; import { + BURN_ADDRESS, NFT_BATCH_SIZE, NOTCOIN_EXCHANGERS, NOTCOIN_FORWARD_TON_AMOUNT, @@ -156,7 +157,8 @@ export async function submitNftTransfers(options: { const messages = nftAddresses.map((nftAddress, index) => { const nft = nfts?.[index]; - const isNotcoinBurn = nft?.collectionAddress === NOTCOIN_VOUCHERS_ADDRESS; + const isNotcoinBurn = nft?.collectionAddress === NOTCOIN_VOUCHERS_ADDRESS + && (toAddress === BURN_ADDRESS || NOTCOIN_EXCHANGERS.includes(toAddress as any)); const payload = isNotcoinBurn ? buildNotcoinVoucherExchange(fromAddress, nftAddress, nft!.index) : buildNftTransferPayload(fromAddress, toAddress, comment); diff --git a/src/api/blockchains/ton/transactions.ts b/src/api/blockchains/ton/transactions.ts index 81cba36b..1fd82f66 100644 --- a/src/api/blockchains/ton/transactions.ts +++ b/src/api/blockchains/ton/transactions.ts @@ -41,6 +41,7 @@ import { parseTxId } from './util'; import { fetchAddressBook, fetchLatestTxId, fetchTransactions } from './util/apiV3'; import { decryptMessageComment, encryptMessageComment } from './util/encryption'; import { buildNft, parseWalletTransactionBody } from './util/metadata'; +import { sendExternal } from './util/sendExternal'; import { fetchNftItems } from './util/tonapiio'; import { commentToBytes, @@ -52,7 +53,6 @@ import { parseAddress, parseBase64, resolveTokenWalletAddress, - sendExternal, toBase64Address, } from './util/tonCore'; import { fetchStoredAccount, fetchStoredAddress } from '../../common/accounts'; @@ -169,9 +169,8 @@ export async function checkTransactionDraft( const account = await fetchStoredAccount(accountId); const { address } = account; - const isLedger = !!account.ledger; - if (data && typeof data === 'string' && !isBase64Data && !isLedger) { + if (data && typeof data === 'string' && !isBase64Data) { data = commentToBytes(data); } diff --git a/src/api/blockchains/ton/util/TonClient.ts b/src/api/blockchains/ton/util/TonClient.ts index f88c9d55..240c7e8f 100644 --- a/src/api/blockchains/ton/util/TonClient.ts +++ b/src/api/blockchains/ton/util/TonClient.ts @@ -11,6 +11,7 @@ import { logDebug } from '../../../../util/logs'; axiosRetry(axios, { retries: DEFAULT_RETRIES, + shouldResetTimeout: true, retryDelay: (retryCount) => { return retryCount * DEFAULT_ERROR_PAUSE; }, diff --git a/src/api/blockchains/ton/util/sendExternal.ts b/src/api/blockchains/ton/util/sendExternal.ts new file mode 100644 index 00000000..9f09a862 --- /dev/null +++ b/src/api/blockchains/ton/util/sendExternal.ts @@ -0,0 +1,51 @@ +import type { Cell } from '@ton/core'; +import { beginCell, external, storeMessage } from '@ton/core'; + +import type { TonClient } from './TonClient'; +import type { TonWallet } from './tonCore'; + +import { dieselSendBoc } from './diesel'; + +export async function sendExternal( + client: TonClient, + wallet: TonWallet, + message: Cell, + withDiesel?: boolean, +) { + const { + address, + init, + } = wallet; + + let neededInit: { data: Cell; code: Cell } | undefined; + if (init && !await client.isContractDeployed(address)) { + neededInit = init; + } + + const ext = external({ + to: address, + init: neededInit ? { + code: neededInit.code, + data: neededInit.data, + } : undefined, + body: message, + }); + + const cell = beginCell() + .store(storeMessage(ext)) + .endCell(); + + const msgHash = cell.hash().toString('base64'); + const boc = cell.toBoc().toString('base64'); + + if (withDiesel) { + await dieselSendBoc(boc); + } else { + await client.sendFile(boc); + } + + return { + boc, + msgHash, + }; +} diff --git a/src/api/blockchains/ton/util/tonCore.ts b/src/api/blockchains/ton/util/tonCore.ts index 3a6ba26e..1846e27b 100644 --- a/src/api/blockchains/ton/util/tonCore.ts +++ b/src/api/blockchains/ton/util/tonCore.ts @@ -1,7 +1,5 @@ import type { OpenedContract } from '@ton/core'; -import { - Address, beginCell, Builder, Cell, external, storeMessage, -} from '@ton/core'; +import { Address, Builder, Cell } from '@ton/core'; import axios from 'axios'; import { WalletContractV1R1 } from '@ton/ton/dist/wallets/WalletContractV1R1'; import { WalletContractV1R2 } from '@ton/ton/dist/wallets/WalletContractV1R2'; @@ -32,9 +30,13 @@ import { JettonWallet } from '../contracts/JettonWallet'; import { hexToBytes } from '../../../common/utils'; import { getEnvironment } from '../../../environment'; import { - DEFAULT_IS_BOUNCEABLE, DNS_ZONES_MAP, JettonOpCode, LiquidStakingOpCode, OpCode, WORKCHAIN, + DEFAULT_IS_BOUNCEABLE, + DNS_ZONES_MAP, + JettonOpCode, + LiquidStakingOpCode, + OpCode, + WORKCHAIN, } from '../constants'; -import { dieselSendBoc } from './diesel'; import { generateQueryId } from './index'; import { TonClient } from './TonClient'; @@ -225,6 +227,12 @@ export function packBytesAsSnake(bytes: Uint8Array, maxBytes = TON_MAX_COMMENT_B return bytes; } + return packBytesAsSnakeCell(bytes); +} + +export function packBytesAsSnakeCell(bytes: Uint8Array): Cell { + const buffer = Buffer.from(bytes); + const mainBuilder = new Builder(); let prevBuilder: Builder | undefined; let currentBuilder = mainBuilder; @@ -365,38 +373,3 @@ export async function getDnsItemDomain(network: ApiNetwork, address: Address | s return `${base}${zone}`; } - -export async function sendExternal( - client: TonClient, - wallet: TonWallet, - message: Cell, - withDiesel?: boolean, -) { - const { address, init } = wallet; - - let neededInit: { data: Cell; code: Cell } | undefined; - if (init && !await client.isContractDeployed(address)) { - neededInit = init; - } - - const ext = external({ - to: address, - init: neededInit ? { code: neededInit.code, data: neededInit.data } : undefined, - body: message, - }); - - const cell = beginCell() - .store(storeMessage(ext)) - .endCell(); - - const msgHash = cell.hash().toString('base64'); - const boc = cell.toBoc().toString('base64'); - - if (withDiesel) { - await dieselSendBoc(boc); - } else { - await client.sendFile(boc); - } - - return { boc, msgHash }; -} diff --git a/src/components/settings/SettingsWalletVersion.tsx b/src/components/settings/SettingsWalletVersion.tsx index 1e06339f..85048bdc 100644 --- a/src/components/settings/SettingsWalletVersion.tsx +++ b/src/components/settings/SettingsWalletVersion.tsx @@ -117,7 +117,7 @@ function SettingsWalletVersion({
{lang('$read_more_about_wallet_version', { ton_link: ( - + ton.org ), diff --git a/src/config.ts b/src/config.ts index 213681ac..bc948505 100644 --- a/src/config.ts +++ b/src/config.ts @@ -123,7 +123,7 @@ export const PROXY_HOSTS = process.env.PROXY_HOSTS; export const TINY_TRANSFER_MAX_COST = 0.01; -export const LANG_CACHE_NAME = 'mtw-lang-118'; +export const LANG_CACHE_NAME = 'mtw-lang-120'; export const LANG_LIST: LangItem[] = [{ langCode: 'en', @@ -292,7 +292,7 @@ export const BURN_ADDRESS = 'UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJKZ'; export const DEFAULT_WALLET_VERSION: ApiWalletVersion = 'v4R2'; export const POPULAR_WALLET_VERSIONS: ApiWalletVersion[] = ['v3R1', 'v3R2', 'v4R2', 'W5']; -export const DEFAULT_TIMEOUT = 5000; +export const DEFAULT_TIMEOUT = 10000; export const DEFAULT_RETRIES = 3; export const DEFAULT_ERROR_PAUSE = 500; diff --git a/src/global/actions/api/swap.ts b/src/global/actions/api/swap.ts index 44813333..525f7171 100644 --- a/src/global/actions/api/swap.ts +++ b/src/global/actions/api/swap.ts @@ -39,6 +39,8 @@ const PAIRS_CACHE: Record { }); addActionHandler('estimateSwap', async (global, actions, { shouldBlock, isEnoughToncoin }) => { + if (isEstimateSwapBeingExecuted) return; + + isEstimateSwapBeingExecuted = true; const resetParams = { amountOutMin: '0', transactionFee: '0', @@ -547,6 +552,7 @@ addActionHandler('estimateSwap', async (global, actions, { shouldBlock, isEnough ...resetParams, }); setGlobal(global); + isEstimateSwapBeingExecuted = false; return; } @@ -565,6 +571,7 @@ addActionHandler('estimateSwap', async (global, actions, { shouldBlock, isEnough errorType: SwapErrorType.InvalidPair, }); setGlobal(global); + isEstimateSwapBeingExecuted = false; return; } @@ -593,6 +600,7 @@ addActionHandler('estimateSwap', async (global, actions, { shouldBlock, isEnough shouldTryDiesel: isEnoughToncoin === false, }); + isEstimateSwapBeingExecuted = false; global = getGlobal(); if (!estimate || 'error' in estimate) { @@ -648,6 +656,9 @@ addActionHandler('estimateSwap', async (global, actions, { shouldBlock, isEnough }); addActionHandler('estimateSwapCex', async (global, actions, { shouldBlock }) => { + if (isEstimateCexSwapBeingExecuted) return; + + isEstimateCexSwapBeingExecuted = true; const amount = global.currentSwap.inputSource === SwapInputSource.In ? { amountOut: undefined } : { amountIn: undefined }; @@ -681,6 +692,7 @@ addActionHandler('estimateSwapCex', async (global, actions, { shouldBlock }) => ...resetParams, }); setGlobal(global); + isEstimateCexSwapBeingExecuted = false; return; } @@ -716,6 +728,7 @@ addActionHandler('estimateSwapCex', async (global, actions, { shouldBlock }) => }); global = getGlobal(); + isEstimateCexSwapBeingExecuted = false; if (!estimate || 'errors' in estimate) { global = updateCurrentSwap(global, { diff --git a/src/i18n/ru.yaml b/src/i18n/ru.yaml index c93fe634..e27fbfe8 100644 --- a/src/i18n/ru.yaml +++ b/src/i18n/ru.yaml @@ -190,7 +190,7 @@ View Address on %ton_explorer_name%: Открыть в %ton_explorer_name% No suggestions, you're on your own!: Ничего не найдено :( Play Sounds: Включить звуки Focus on asset value rather than current balance: Фокус на стоимости активов вместо текущего баланса -$tiny_transfers_help: Отключите эту опцию, чтобы показывать переводы стоимостью менее $%value%. Имейте в виду, что такие переводы часто используются для спама и мошенничества. +$tiny_transfers_help: Скрыть переводы стоимостью менее $%value%. Имейте в виду — такие суммы часто отправляют спамеры и мошенники. Today: Сегодня Yesterday: Вчера Now: Сейчас @@ -279,9 +279,9 @@ $dapps-description: Вы можете подключиться к децентр Logged in with MyTonWallet: Подключены с помощью MyTonWallet No active connections: Нет активных подключений Other: Другие -Assets & Activity: Активы и Лента +Assets & Activity: Активы и лента Token Settings: Настройки токенов -My Tokens: Мои Токены +My Tokens: Мои токены Add Token: Добавить токен Delete Token: Удалить токен Are you sure you want to delete?: Вы уверены, что хотите удалить %token%? @@ -528,10 +528,7 @@ Required: Обязательно Comment is too long.: Комментарий слишком длинный. Memo: Memo Memo was copied!: Memo был скопирован! -$hide_tokens_no_cost_help: | - Выключите эту опцию, чтобы отображать в вашей учётной записи токены со стоимостью менее $%value%. - Пожалуйста, имейте в виду, что такие жетоны часто используются для спама и мошенничества. - Вы также можете выборочно включить и отключить конкретные токены, используя список ниже на этом экране. +$hide_tokens_no_cost_help: Скрыть токены стоимостью менее $%value%. Имейте в виду — такие токены часто отправляют спамеры и мошенники. На этом же экране ниже вы можете скрыть отдельные токены. No internet connection. Please check your connection and try again.: Отсутствует подключение к интернету. Пожалуйста, проверьте своё соединение и попробуйте снова. To use this feature, first enable Face ID in your phone settings.: Чтобы использовать эту функцию, сначала активируйте идентификацию лица в настройках телефона. To use this feature, first enable biometrics in your phone settings.: Чтобы использовать эту функцию, сначала активируйте биометрию в настройках телефона. @@ -608,7 +605,7 @@ until %date%: до %date% "%volume% in %amount% parts": "%volume% в %amount% частях" Unfreeze: Разморозить Claim: Заклеймить -Confirm Unfreezing: Подтверждение размораживания +Confirm Unfreezing: Подтвердите размораживание Insufficient Balance for Fee.: Недостаточный баланс для комиссии. $fee_value_bold: "**Комиссия:** %fee%" MyTonWallet Features: Функции MyTonWallet diff --git a/src/util/fetch.ts b/src/util/fetch.ts index abd3fd34..e0e9b5d5 100644 --- a/src/util/fetch.ts +++ b/src/util/fetch.ts @@ -5,7 +5,7 @@ import { pause } from './schedulers'; type QueryParams = Record; -const DEFAULT_TIMEOUTS = [15000, 30000]; // 15, 15, 30 sec +const MAX_TIMEOUT = 30000; // 30 sec export async function fetchJson(url: string | URL, data?: QueryParams, init?: RequestInit) { const urlObject = new URL(url); @@ -37,7 +37,7 @@ export async function fetchWithRetry(url: string | URL, init?: RequestInit, opti }) { const { retries = DEFAULT_RETRIES, - timeouts = DEFAULT_TIMEOUTS, + timeouts = DEFAULT_TIMEOUT, shouldSkipRetryFn = isNotTemporaryError, } = options ?? {}; @@ -52,7 +52,7 @@ export async function fetchWithRetry(url: string | URL, init?: RequestInit, opti const timeout = Array.isArray(timeouts) ? timeouts[i - 1] ?? timeouts[timeouts.length - 1] - : timeouts; + : Math.min(timeouts * i, MAX_TIMEOUT); const response = await fetchWithTimeout(url, init, timeout); statusCode = response.status; diff --git a/src/util/ledger/index.ts b/src/util/ledger/index.ts index f202c3dc..0752d7ed 100644 --- a/src/util/ledger/index.ts +++ b/src/util/ledger/index.ts @@ -23,6 +23,7 @@ import type { LedgerWalletInfo } from './types'; import { ApiTransactionError } from '../../api/types'; import { + BURN_ADDRESS, NOTCOIN_EXCHANGERS, NOTCOIN_VOUCHERS_ADDRESS, ONE_TON, TONCOIN_SLUG, } from '../../config'; import { callApi } from '../../api'; @@ -38,7 +39,11 @@ import { WALLET_IS_BOUNCEABLE, WORKCHAIN, } from '../../api/blockchains/ton/constants'; -import { toBase64Address } from '../../api/blockchains/ton/util/tonCore'; +import { + commentToBytes, + packBytesAsSnakeCell, + toBase64Address, +} from '../../api/blockchains/ton/util/tonCore'; import { ApiHardwareBlindSigningNotEnabled, ApiUnsupportedVersionError, @@ -49,6 +54,7 @@ import { parseAccountId } from '../account'; import compareVersions from '../compareVersions'; import { logDebugError } from '../logs'; import { pause } from '../schedulers'; +import { isValidLedgerComment } from './utils'; type TransactionParams = { to: Address; @@ -274,8 +280,10 @@ export async function submitLedgerTransfer( )); isBounceable = true; } else if (comment) { - if (isUnsafeSupported) { + if (isValidLedgerComment(comment)) { payload = { type: 'comment', text: comment }; + } else if (isUnsafeSupported) { + payload = { type: 'unsafe', message: buildCommentPayload(comment) }; } else { return { error: ApiTransactionError.NotSupportedHardwareOperation, @@ -355,12 +363,15 @@ export async function submitLedgerNftTransfer(options: { const { seqno } = walletInfo!; - const isNotcoinBurn = nft?.collectionAddress === NOTCOIN_VOUCHERS_ADDRESS; + const isNotcoinBurn = nft?.collectionAddress === NOTCOIN_VOUCHERS_ADDRESS + && (toAddress === BURN_ADDRESS || NOTCOIN_EXCHANGERS.includes(toAddress as any)); // eslint-disable-next-line no-null/no-null let forwardPayload: Cell | null = null; + let forwardAmount = NFT_TRANSFER_TONCOIN_FORWARD_AMOUNT; if (isNotcoinBurn) { ({ forwardPayload, toAddress } = buildNotcoinVoucherExchange(nftAddress, nft!.index)); + forwardAmount = 50000000n; } else if (comment) { forwardPayload = buildCommentPayload(comment); } @@ -380,7 +391,7 @@ export async function submitLedgerNftTransfer(options: { responseDestination: Address.parse(fromAddress!), // eslint-disable-next-line no-null/no-null customPayload: null, - forwardAmount: NFT_TRANSFER_TONCOIN_FORWARD_AMOUNT, + forwardAmount, forwardPayload, }, }); @@ -458,10 +469,8 @@ export async function buildLedgerTokenTransfer( } function buildCommentPayload(comment: string) { - return new Builder() - .storeUint(0, 32) - .storeStringTail(comment) - .endCell(); + const bytes = commentToBytes(comment); + return packBytesAsSnakeCell(bytes); } export async function signLedgerTransactions(accountId: string, messages: ApiDappTransfer[], options?: { diff --git a/src/util/ledger/utils.ts b/src/util/ledger/utils.ts new file mode 100644 index 00000000..29083237 --- /dev/null +++ b/src/util/ledger/utils.ts @@ -0,0 +1,11 @@ +import { isAscii } from '../stringFormat'; + +const MAX_COMMENT_SIZE = 120; + +export function isValidLedgerComment(comment: string) { + return isAscii(comment) && isLedgerCommentLengthValid(comment); +} + +export function isLedgerCommentLengthValid(comment: string) { + return comment.length <= MAX_COMMENT_SIZE; +}