From 202c22bc1a904190625844934bae813886805869 Mon Sep 17 00:00:00 2001 From: mytonwalletorg Date: Tue, 12 Nov 2024 12:41:31 +0300 Subject: [PATCH] v3.0.34 --- README.md | 2 +- capacitor.config.ts | 2 + changelogs/3.0.34.txt | 1 + mobile/android/app/capacitor.build.gradle | 2 + mobile/android/capacitor.settings.gradle | 6 + mobile/ios/App/Podfile | 2 + mobile/ios/App/Podfile.lock | 14 +- package-lock.json | 24 +- package.json | 4 +- public/version.txt | 2 +- src/api/chains/ton/index.ts | 1 - src/api/chains/ton/polling.ts | 242 ++++++++---------- src/api/chains/ton/tokens.ts | 10 +- src/api/chains/ton/transactions.ts | 177 +++++++------ src/api/chains/ton/util/diesel.ts | 4 +- src/api/chains/ton/util/w5diesel.ts | 4 +- src/api/chains/tron/polling.ts | 3 +- src/api/common/backend.ts | 23 +- src/api/common/pendingTransfers.ts | 27 ++ src/api/common/tokens.ts | 17 +- src/api/constants.ts | 1 + src/api/extensionMethods/legacy.ts | 8 +- src/api/methods/dapps.ts | 2 +- src/api/methods/transactions.ts | 28 +- src/api/types/misc.ts | 4 +- src/api/types/updates.ts | 5 +- .../common/TransactionBanner.module.scss | 26 +- src/components/common/TransactionBanner.tsx | 21 +- src/components/dapps/Dapp.module.scss | 1 + src/components/dapps/DappConnectModal.tsx | 2 +- .../main/modals/TransactionModal.module.scss | 15 ++ .../main/modals/TransactionModal.tsx | 14 +- .../main/sections/Card/Card.module.scss | 2 + .../main/sections/Card/CardAddress.tsx | 2 +- .../main/sections/Content/Activities.tsx | 14 +- .../main/sections/Content/Assets.tsx | 11 - .../main/sections/Content/Content.tsx | 15 +- .../main/sections/Content/Transaction.tsx | 10 +- .../settings/SettingsDeveloperOptions.tsx | 69 +++-- src/components/settings/SettingsTokens.tsx | 10 +- src/components/staking/UnstakeModal.tsx | 11 +- src/components/swap/SwapInitial.tsx | 5 +- src/components/transfer/Transfer.module.scss | 6 + src/components/transfer/TransferInitial.tsx | 55 ++-- src/components/ui/PasswordForm.tsx | 2 +- src/config.ts | 22 +- src/global/actions/api/auth.ts | 2 + src/global/actions/api/wallet.ts | 42 ++- src/global/actions/apiUpdates/activities.ts | 4 +- src/global/actions/apiUpdates/initial.ts | 2 +- src/global/actions/ui/misc.ts | 39 +-- src/global/cache.ts | 39 +++ src/global/initialState.ts | 2 +- src/global/reducers/misc.ts | 58 +++-- src/global/selectors/tokens.ts | 14 +- src/global/types.ts | 6 +- src/i18n/de.yaml | 4 +- src/i18n/en.yaml | 4 +- src/i18n/es.yaml | 4 +- src/i18n/pl.yaml | 4 +- src/i18n/ru.yaml | 6 +- src/i18n/th.yaml | 4 +- src/i18n/tr.yaml | 4 +- src/i18n/uk.yaml | 4 +- src/i18n/zh-Hans.yaml | 4 +- src/i18n/zh-Hant.yaml | 4 +- src/util/PostMessageConnector.ts | 8 +- src/util/createPostMessageInterface.ts | 20 +- src/util/environment.ts | 5 + src/util/handleError.ts | 13 +- src/util/ledger/index.ts | 15 +- src/util/poisoningHash.ts | 52 ++++ src/util/tokens.ts | 13 +- 73 files changed, 821 insertions(+), 483 deletions(-) create mode 100644 changelogs/3.0.34.txt create mode 100644 src/api/common/pendingTransfers.ts create mode 100644 src/util/environment.ts create mode 100644 src/util/poisoningHash.ts diff --git a/README.md b/README.md index dccda59f..c1a871ad 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ brew install pkg-config cairo pango libpng jpeg giflib librsvg ### NPM Local Setup ```sh -mv .env.example .env +cp .env.example .env npm ci ``` diff --git a/capacitor.config.ts b/capacitor.config.ts index 467dd8b4..13b61d06 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -15,7 +15,9 @@ const config: CapacitorConfig = { includePlugins: [ '@capacitor-mlkit/barcode-scanning', '@capacitor/app', + '@capacitor/filesystem', '@capacitor/clipboard', + '@capacitor/share', '@capacitor/haptics', '@capacitor/status-bar', '@capgo/capacitor-native-biometric', diff --git a/changelogs/3.0.34.txt b/changelogs/3.0.34.txt new file mode 100644 index 00000000..619f4cd5 --- /dev/null +++ b/changelogs/3.0.34.txt @@ -0,0 +1 @@ +Bug fixes and performance improvements diff --git a/mobile/android/app/capacitor.build.gradle b/mobile/android/app/capacitor.build.gradle index 4382276b..45464b45 100644 --- a/mobile/android/app/capacitor.build.gradle +++ b/mobile/android/app/capacitor.build.gradle @@ -11,7 +11,9 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-mlkit-barcode-scanning') implementation project(':capacitor-app') + implementation project(':capacitor-filesystem') implementation project(':capacitor-clipboard') + implementation project(':capacitor-share') implementation project(':capacitor-haptics') implementation project(':capgo-capacitor-native-biometric') implementation project(':capgo-native-audio') diff --git a/mobile/android/capacitor.settings.gradle b/mobile/android/capacitor.settings.gradle index 80b86b87..15bdcd1c 100644 --- a/mobile/android/capacitor.settings.gradle +++ b/mobile/android/capacitor.settings.gradle @@ -8,9 +8,15 @@ project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../../node_m include ':capacitor-app' project(':capacitor-app').projectDir = new File('../../node_modules/@capacitor/app/android') +include ':capacitor-filesystem' +project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capacitor/filesystem/android') + include ':capacitor-clipboard' project(':capacitor-clipboard').projectDir = new File('../../node_modules/@capacitor/clipboard/android') +include ':capacitor-share' +project(':capacitor-share').projectDir = new File('../../node_modules/@capacitor/share/android') + include ':capacitor-haptics' project(':capacitor-haptics').projectDir = new File('../../node_modules/@capacitor/haptics/android') diff --git a/mobile/ios/App/Podfile b/mobile/ios/App/Podfile index dd38724a..a19b9417 100644 --- a/mobile/ios/App/Podfile +++ b/mobile/ios/App/Podfile @@ -15,7 +15,9 @@ def capacitor_pods pod 'CapacitorApp', :path => '../../../node_modules/@capacitor/app' pod 'CapacitorAppLauncher', :path => '../../../node_modules/@capacitor/app-launcher' pod 'CapacitorClipboard', :path => '../../../node_modules/@capacitor/clipboard' + pod 'CapacitorFilesystem', :path => '../../../node_modules/@capacitor/filesystem' pod 'CapacitorHaptics', :path => '../../../node_modules/@capacitor/haptics' + pod 'CapacitorShare', :path => '../../../node_modules/@capacitor/share' pod 'CapgoCapacitorNativeBiometric', :path => '../../../node_modules/@capgo/capacitor-native-biometric' pod 'CapgoNativeAudio', :path => '../../../node_modules/@capgo/native-audio' pod 'MauricewegnerCapacitorNavigationBar', :path => '../../../node_modules/@mauricewegner/capacitor-navigation-bar' diff --git a/mobile/ios/App/Podfile.lock b/mobile/ios/App/Podfile.lock index 6c0f429b..7a4d6b89 100644 --- a/mobile/ios/App/Podfile.lock +++ b/mobile/ios/App/Podfile.lock @@ -8,6 +8,8 @@ PODS: - CapacitorClipboard (6.0.1): - Capacitor - CapacitorCordova (6.1.2) + - CapacitorFilesystem (6.0.1): + - Capacitor - CapacitorHaptics (6.0.1): - Capacitor - CapacitorMlkitBarcodeScanning (6.2.0): @@ -20,6 +22,8 @@ PODS: - CapacitorSecureStoragePlugin (0.10.0): - Capacitor - SwiftKeychainWrapper + - CapacitorShare (6.0.2): + - Capacitor - CapgoCapacitorNativeBiometric (0.0.1): - Capacitor - CapgoNativeAudio (6.4.21): @@ -104,11 +108,13 @@ DEPENDENCIES: - "CapacitorAppLauncher (from `../../../node_modules/@capacitor/app-launcher`)" - "CapacitorClipboard (from `../../../node_modules/@capacitor/clipboard`)" - "CapacitorCordova (from `../../../node_modules/@capacitor/ios`)" + - "CapacitorFilesystem (from `../../../node_modules/@capacitor/filesystem`)" - "CapacitorHaptics (from `../../../node_modules/@capacitor/haptics`)" - "CapacitorMlkitBarcodeScanning (from `../../../node_modules/@capacitor-mlkit/barcode-scanning`)" - CapacitorNativeSettings (from `../../../node_modules/capacitor-native-settings`) - CapacitorPluginSafeArea (from `../../../node_modules/capacitor-plugin-safe-area`) - CapacitorSecureStoragePlugin (from `../../../node_modules/capacitor-secure-storage-plugin`) + - "CapacitorShare (from `../../../node_modules/@capacitor/share`)" - "CapgoCapacitorNativeBiometric (from `../../../node_modules/@capgo/capacitor-native-biometric`)" - "CapgoNativeAudio (from `../../../node_modules/@capgo/native-audio`)" - CordovaPlugins (from `../capacitor-cordova-ios-plugins`) @@ -146,6 +152,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/@capacitor/clipboard" CapacitorCordova: :path: "../../../node_modules/@capacitor/ios" + CapacitorFilesystem: + :path: "../../../node_modules/@capacitor/filesystem" CapacitorHaptics: :path: "../../../node_modules/@capacitor/haptics" CapacitorMlkitBarcodeScanning: @@ -156,6 +164,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/capacitor-plugin-safe-area" CapacitorSecureStoragePlugin: :path: "../../../node_modules/capacitor-secure-storage-plugin" + CapacitorShare: + :path: "../../../node_modules/@capacitor/share" CapgoCapacitorNativeBiometric: :path: "../../../node_modules/@capgo/capacitor-native-biometric" CapgoNativeAudio: @@ -187,11 +197,13 @@ SPEC CHECKSUMS: CapacitorAppLauncher: 9ac785e8d3936388249212b6e16cb32225960c5f CapacitorClipboard: 756cd7e83e8d5d19b0c74f40b57517c287bd5fe2 CapacitorCordova: f48c89f96c319101cd2f0ce8a2b7449b5fb8b3dd + CapacitorFilesystem: 37fb3aa5c945b4539ab11c74a5c57925a302bf24 CapacitorHaptics: fe689ade56ef20ec9b041a753c6da70c5d8ec9a9 CapacitorMlkitBarcodeScanning: 178fb57424ec688b6a2fceee506ecc1ea00d1c8d CapacitorNativeSettings: 1ce5585ff07b161616cd0a795702637316677af2 CapacitorPluginSafeArea: e1eca7f70974f0e270d96f70cd0a5f51523164b1 CapacitorSecureStoragePlugin: ced6025438fbbdbfb9fffec4398e748572fc147b + CapacitorShare: 591ae4693d85686ceb590db8e8b44aa014ec6490 CapgoCapacitorNativeBiometric: 44b0bb31118f6ed5171087a77a856a80a0cfa250 CapgoNativeAudio: f3cb18f75acfaec7c429e1ff9dc06e05c6605627 CordovaPlugins: b26881c27739f4c46bac6baf422f660500cd5561 @@ -215,6 +227,6 @@ SPEC CHECKSUMS: SinaKhMtwCapacitorStatusBar: e3fa73038e8dbac071751e1942eab65fc6c39a5f SwiftKeychainWrapper: 807ba1d63c33a7d0613288512399cd1eda1e470c -PODFILE CHECKSUM: 783aef605ceeac40dc316144a44e591e6373c25a +PODFILE CHECKSUM: 1bbc72e2fbfb2ae871c9a96aef2548830c3c0d6c COCOAPODS: 1.15.2 diff --git a/package-lock.json b/package-lock.json index 6d712b6f..d6029255 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mytonwallet", - "version": "3.0.33", + "version": "3.0.34", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytonwallet", - "version": "3.0.33", + "version": "3.0.34", "license": "GPL-3.0-or-later", "dependencies": { "@awesome-cordova-plugins/core": "6.9.0", @@ -17,8 +17,10 @@ "@capacitor/app-launcher": "6.0.2", "@capacitor/clipboard": "6.0.1", "@capacitor/core": "6.1.2", + "@capacitor/filesystem": "6.0.1", "@capacitor/haptics": "6.0.1", "@capacitor/ios": "6.1.2", + "@capacitor/share": "6.0.2", "@capgo/capacitor-native-biometric": "github:mytonwallet-org/capacitor-native-biometric#956d06d1ab78a839f1293921d0bff449edf4ef4d", "@capgo/native-audio": "6.4.21", "@ledgerhq/hw-transport-webhid": "6.29.4", @@ -3481,6 +3483,15 @@ "node": ">=4.2.0" } }, + "node_modules/@capacitor/filesystem": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-6.0.1.tgz", + "integrity": "sha512-eHhXm6tzBIQhErzFnfOE6eA1U+15DHc2212/COfzzGGRk/dyGympoVV3ct2YPVzvpTSxMEW3xFocORv/xD9gFg==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, "node_modules/@capacitor/haptics": { "version": "6.0.1", "license": "MIT", @@ -3495,6 +3506,15 @@ "@capacitor/core": "^6.1.0" } }, + "node_modules/@capacitor/share": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/share/-/share-6.0.2.tgz", + "integrity": "sha512-qQIeyjzFB1VgP4ojyNg2O98TCigDWLKZ5melVMSVeDO0YQov5OeammsVgaCGSzvYBPSgyOwX41QvcDWG4yHN1A==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, "node_modules/@capgo/capacitor-native-biometric": { "version": "6.0.0", "resolved": "git+ssh://git@github.com/mytonwallet-org/capacitor-native-biometric.git#956d06d1ab78a839f1293921d0bff449edf4ef4d", diff --git a/package.json b/package.json index c4c6df64..68b16999 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mytonwallet", - "version": "3.0.33", + "version": "3.0.34", "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": { @@ -185,8 +185,10 @@ "@capacitor/app-launcher": "6.0.2", "@capacitor/clipboard": "6.0.1", "@capacitor/core": "6.1.2", + "@capacitor/filesystem": "6.0.1", "@capacitor/haptics": "6.0.1", "@capacitor/ios": "6.1.2", + "@capacitor/share": "6.0.2", "@capgo/capacitor-native-biometric": "github:mytonwallet-org/capacitor-native-biometric#956d06d1ab78a839f1293921d0bff449edf4ef4d", "@capgo/native-audio": "6.4.21", "@ledgerhq/hw-transport-webhid": "6.29.4", diff --git a/public/version.txt b/public/version.txt index a0dc0584..f0470eab 100644 --- a/public/version.txt +++ b/public/version.txt @@ -1 +1 @@ -3.0.33 +3.0.34 diff --git a/src/api/chains/ton/index.ts b/src/api/chains/ton/index.ts index c2253100..0c9fcb3a 100644 --- a/src/api/chains/ton/index.ts +++ b/src/api/chains/ton/index.ts @@ -21,7 +21,6 @@ export { fetchAccountTransactionSlice, fetchTokenTransactionSlice, submitTransfer, - waitPendingTransfer, checkMultiTransactionDraft, submitMultiTransfer, getAllTransactionSlice, diff --git a/src/api/chains/ton/polling.ts b/src/api/chains/ton/polling.ts index e98f739c..3be3ebe4 100644 --- a/src/api/chains/ton/polling.ts +++ b/src/api/chains/ton/polling.ts @@ -1,6 +1,7 @@ import type { ApiBackendStakingState, ApiBalanceBySlug, + ApiNetwork, ApiNftUpdate, ApiStakingCommonData, ApiStakingState, @@ -27,7 +28,7 @@ import { import { getStakingCommonCache } from '../../common/cache'; import { isAlive, isUpdaterAlive } from '../../common/helpers'; import { processNftUpdates, updateAccountNfts } from '../../common/nft'; -import { addTokens, getTokensCache } from '../../common/tokens'; +import { addTokens } from '../../common/tokens'; import { txCallbacks } from '../../common/txCallbacks'; import { hexToBytes } from '../../common/utils'; import { FIRST_TRANSACTIONS_LIMIT, SEC } from '../../constants'; @@ -40,11 +41,6 @@ import { fetchTokenTransactionSlice, fixTokenActivitiesAddressForm, waitUntilTra import { fetchVestings } from './vesting'; import { getWalletInfo, getWalletVersionInfos, isAddressInitialized } from './wallet'; -type AccountBalanceCache = { - balance?: bigint; - tokenBalances?: ApiBalanceBySlug; -}; - const BALANCE_BASED_INTERVAL = 1.1 * SEC; const BALANCE_BASED_INTERVAL_WHEN_NOT_FOCUSED = 10 * SEC; const DOUBLE_CHECK_TOKENS_PAUSE = 30 * SEC; @@ -61,7 +57,7 @@ const VESTING_INTERVAL_WHEN_NOT_FOCUSED = 60 * SEC; const BALANCE_NOT_ACTIVE_ACCOUNTS_INTERVAL = 30 * SEC; const BALANCE_NOT_ACTIVE_ACCOUNTS_INTERVAL_WHEN_NOT_FOCUSED = 60 * SEC; -const lastBalanceCache: Record = {}; +const lastBalanceCache: Record = {}; export function setupPolling(accountId: string, onUpdate: OnApiUpdate, newestTxTimestamps: ApiTxTimestamps) { void setupBalanceBasedPolling(accountId, onUpdate, newestTxTimestamps); @@ -85,26 +81,17 @@ async function setupBalanceBasedPolling( let nftUpdates: ApiNftUpdate[]; let lastNftFullUpdate = 0; let doubleCheckTokensTime: number | undefined; - let tokenBalances: TokenBalanceParsed[] | undefined; - async function updateBalance(cache: AccountBalanceCache) { - const walletInfo = await getWalletInfo(network, address); - const { balance, lastTxId } = walletInfo ?? {}; - const isToncoinBalanceChanged = balance !== undefined && balance !== cache?.balance; - const balancesToUpdate: ApiBalanceBySlug = {}; + async function updateBalance(cache: ApiBalanceBySlug, newBalances: ApiBalanceBySlug, changedSlugs: string[]) { + const { balance, lastTxId } = await getWalletInfo(network, address); + const isToncoinBalanceChanged = balance !== undefined && balance !== cache[TONCOIN.slug]; + newBalances[TONCOIN.slug] = balance; if (isToncoinBalanceChanged) { - balancesToUpdate[TONCOIN.slug] = balance; - - lastBalanceCache[accountId] = { - ...lastBalanceCache[accountId], - balance, - }; + changedSlugs.push(TONCOIN.slug); } - return { - lastTxId, isToncoinBalanceChanged, balancesToUpdate, - }; + return { lastTxId, isToncoinBalanceChanged }; } async function updateNfts(isToncoinBalanceChanged: boolean) { @@ -136,66 +123,43 @@ async function setupBalanceBasedPolling( async function updateTokenBalances( isToncoinBalanceChanged: boolean, - cache: AccountBalanceCache, - balancesToUpdate: ApiBalanceBySlug, + cache: ApiBalanceBySlug, + newBalances: ApiBalanceBySlug, + changedSlugs: string[], ) { - const changedTokenSlugs: string[] = []; + let tokenBalances: TokenBalanceParsed[] = []; if (isToncoinBalanceChanged || (doubleCheckTokensTime && doubleCheckTokensTime < Date.now())) { doubleCheckTokensTime = isToncoinBalanceChanged ? Date.now() + DOUBLE_CHECK_TOKENS_PAUSE : undefined; - tokenBalances = await getAccountTokenBalances(accountId).catch(logAndRescue); - const slugsWithBalances = new Set(tokenBalances?.map(({ slug }) => slug)); - - const tokensCache = getTokensCache(); - const mintlessZeroBalances = Object.fromEntries( - Object.values(tokensCache) - .filter((token) => token.customPayloadApiUrl && !slugsWithBalances.has(token.slug)) - .map(({ slug }) => [slug, undefined]), - ); - throwErrorIfUpdaterNotAlive(onUpdate, accountId); - - if (tokenBalances) { - const tokens = tokenBalances.filter(Boolean).map(({ token }) => token); - await addTokens(tokens, onUpdate); - - const cachedTokenBalances = cache?.tokenBalances || {}; - tokenBalances.forEach(({ slug, balance: tokenBalance }) => { - const cachedBalance = cachedTokenBalances[slug]; - if (cachedBalance === tokenBalance) return; + tokenBalances = await getAccountTokenBalances(accountId); - changedTokenSlugs.push(slug); - balancesToUpdate[slug] = tokenBalance; - }); - Object.assign(balancesToUpdate, mintlessZeroBalances); + throwErrorIfUpdaterNotAlive(onUpdate, accountId); - lastBalanceCache[accountId] = { - ...lastBalanceCache[accountId], - tokenBalances: Object.fromEntries(tokenBalances.map( - ({ slug, balance: tokenBalance }) => [slug, tokenBalance || 0n], - )), - }; - } + const tokens = tokenBalances.filter(Boolean).map(({ token }) => token); + await addTokens(tokens, onUpdate); - if (Object.keys(balancesToUpdate).length > 0) { - onUpdate({ - type: 'updateBalances', - accountId, - balancesToUpdate, - }); - } + tokenBalances.forEach(({ slug, balance: tokenBalance }) => { + newBalances[slug] = tokenBalance; + if (cache[slug] !== tokenBalance) { + changedSlugs.push(slug); + } + }); } - onUpdate({ type: 'updatingStatus', kind: 'balance', isUpdating: false }); - - return changedTokenSlugs; + return tokenBalances; } - async function updateActivities(isToncoinBalanceChanged: boolean, changedTokenSlugs: string[], lastTxId?: string) { - if (isToncoinBalanceChanged || changedTokenSlugs.length) { + async function updateActivities( + tokenBalances: TokenBalanceParsed[], + changedSlugs: string[], + lastTxId?: string, + ) { + if (changedSlugs.length) { if (lastTxId) { await waitUntilTransactionAppears(network, address, lastTxId); } + const changedTokenSlugs = changedSlugs.filter((slug) => slug !== TONCOIN.slug); const newTxTimestamps = await processNewActivities( accountId, newestTxTimestamps, changedTokenSlugs, onUpdate, tokenBalances, ); @@ -210,20 +174,34 @@ async function setupBalanceBasedPolling( onUpdate({ type: 'updatingStatus', kind: 'activities', isUpdating: true }); onUpdate({ type: 'updatingStatus', kind: 'balance', isUpdating: true }); - const cache = lastBalanceCache[accountId]; + const newBalances: ApiBalanceBySlug = {}; + const changedSlugs: string[] = []; + const cache = lastBalanceCache[accountId] ?? {}; const { lastTxId, isToncoinBalanceChanged, - balancesToUpdate, - } = await updateBalance(cache); + } = await updateBalance(cache, newBalances, changedSlugs); throwErrorIfUpdaterNotAlive(onUpdate, accountId); - const changedTokenSlugs = await updateTokenBalances(isToncoinBalanceChanged, cache, balancesToUpdate); + const tokenBalances = await updateTokenBalances(isToncoinBalanceChanged, cache, newBalances, changedSlugs); + + lastBalanceCache[accountId] = newBalances; + + if (changedSlugs.length) { + onUpdate({ + type: 'updateBalances', + accountId, + chain: 'ton', + balances: newBalances, + }); + } + + onUpdate({ type: 'updatingStatus', kind: 'balance', isUpdating: false }); await Promise.all([ - updateActivities(isToncoinBalanceChanged, changedTokenSlugs, lastTxId), + updateActivities(tokenBalances, changedSlugs, lastTxId), updateNfts(isToncoinBalanceChanged), ]); @@ -446,8 +424,7 @@ async function setupVestingPolling(accountId: string, onUpdate: OnApiUpdate) { export async function setupInactiveAccountsBalancePolling(onUpdate: OnApiUpdate) { interface BalancePollingAccount { id: string; - chain: any; - network: string; + network: ApiNetwork; address: string; } let addressToAccountsMap: { [address: string]: BalancePollingAccount[] } = {}; @@ -467,7 +444,6 @@ export async function setupInactiveAccountsBalancePolling(onUpdate: OnApiUpdate) const balancePollingAccount: BalancePollingAccount = { id: accountId, - chain: 'ton', network, address, }; @@ -479,76 +455,49 @@ export async function setupInactiveAccountsBalancePolling(onUpdate: OnApiUpdate) }); } - async function updateBalance(account: BalancePollingAccount, cache: AccountBalanceCache) { - const walletInfo = await account.chain.getWalletInfo(account.network, account.address); - const { balance, lastTxId } = walletInfo ?? {}; - const isToncoinBalanceChanged = balance !== undefined && balance !== cache?.balance; - const balancesToUpdate: ApiBalanceBySlug = {}; + async function updateBalance( + account: BalancePollingAccount, + cache: ApiBalanceBySlug, + newBalances: ApiBalanceBySlug, + changedSlugs: string[], + ) { + const { balance, lastTxId } = await getWalletInfo(account.network, account.address); + const isToncoinBalanceChanged = balance !== undefined && balance !== cache[TONCOIN.slug]; + newBalances[TONCOIN.slug] = balance; if (isToncoinBalanceChanged) { - balancesToUpdate[TONCOIN.slug] = balance; - - lastBalanceCache[account.id] = { - ...lastBalanceCache[account.id], - balance, - }; + changedSlugs.push(TONCOIN.slug); } - return { - lastTxId, isToncoinBalanceChanged, balancesToUpdate, - }; + return { lastTxId, isToncoinBalanceChanged }; } async function updateTokenBalances( - isToncoinBalanceChanged: boolean, - cache: AccountBalanceCache, - balancesToUpdate: ApiBalanceBySlug, account: BalancePollingAccount, + cache: ApiBalanceBySlug, + newBalances: ApiBalanceBySlug, + changedSlugs: string[], ) { - const changedTokenSlugs: string[] = []; - - if (isToncoinBalanceChanged) { - const tokenBalances: TokenBalanceParsed[] | undefined = await account.chain - .getAccountTokenBalances(account.id).catch(logAndRescue); - - if (!activeAccountId) { - throw new AbortOperationError(); - } - throwErrorIfUpdaterNotAlive(localOnUpdate, activeAccountId); + const tokenBalances = await getAccountTokenBalances(account.id); - if (tokenBalances) { - const tokens = tokenBalances.filter(Boolean).map(({ token }) => token); - await addTokens(tokens, onUpdate); - - tokenBalances.forEach(({ slug, balance: tokenBalance }) => { - const cachedBalance = cache?.tokenBalances && cache.tokenBalances[slug]; - if (cachedBalance === tokenBalance) return; + if (!activeAccountId) { + throw new AbortOperationError(); + } + throwErrorIfUpdaterNotAlive(localOnUpdate, activeAccountId); - changedTokenSlugs.push(slug); - balancesToUpdate[slug] = tokenBalance; - }); + if (!tokenBalances) { + return; + } - lastBalanceCache[account.id] = { - ...lastBalanceCache[account.id], - tokenBalances: Object.fromEntries(tokenBalances.map( - ({ slug, balance: tokenBalance }) => [slug, tokenBalance], - )), - }; - } + const tokens = tokenBalances.filter(Boolean).map(({ token }) => token); + await addTokens(tokens, onUpdate); - if (Object.keys(balancesToUpdate).length > 0) { - // Notify all accounts with the same address - addressToAccountsMap[account.address].forEach((acc) => { - onUpdate({ - type: 'updateBalances', - accountId: acc.id, - balancesToUpdate, - }); - }); + tokenBalances.forEach(({ slug, balance: tokenBalance }) => { + newBalances[slug] = tokenBalance; + if (cache[slug] !== tokenBalance) { + changedSlugs.push(slug); } - } - - return changedTokenSlugs; + }); } while (isUpdaterAlive(localOnUpdate)) { @@ -557,15 +506,28 @@ export async function setupInactiveAccountsBalancePolling(onUpdate: OnApiUpdate) await updateAddressToAccountsMap(); for (const address of Object.keys(addressToAccountsMap)) { const account = addressToAccountsMap[address][0]; - const cache = lastBalanceCache[account.id]; - - const { - isToncoinBalanceChanged, - balancesToUpdate, - } = await updateBalance(account, cache); - - if (isUpdaterAlive(localOnUpdate)) { - await updateTokenBalances(isToncoinBalanceChanged, cache, balancesToUpdate, account); + const cache = lastBalanceCache[account.id] ?? {}; + const newBalances: ApiBalanceBySlug = {}; + const changedSlugs: string[] = []; + + const { isToncoinBalanceChanged } = await updateBalance(account, cache, newBalances, changedSlugs); + + if (isUpdaterAlive(localOnUpdate) && isToncoinBalanceChanged) { + await updateTokenBalances(account, cache, newBalances, changedSlugs); + + lastBalanceCache[account.id] = newBalances; + + if (changedSlugs.length) { + // Notify all accounts with the same address + addressToAccountsMap[account.address].forEach((acc) => { + onUpdate({ + type: 'updateBalances', + accountId: acc.id, + chain: 'ton', + balances: newBalances, + }); + }); + } } } } catch (err) { diff --git a/src/api/chains/ton/tokens.ts b/src/api/chains/ton/tokens.ts index 1476d150..5b00bb81 100644 --- a/src/api/chains/ton/tokens.ts +++ b/src/api/chains/ton/tokens.ts @@ -6,7 +6,6 @@ import type { AnyPayload, ApiTransactionExtra, JettonMetadata, TonTransferParams, } from './types'; -import { TINY_TOKENS } from '../../../config'; import { parseAccountId } from '../../../util/account'; import { fetchJsonWithProxy } from '../../../util/fetch'; import { logDebugError } from '../../../util/logs'; @@ -222,7 +221,7 @@ export async function buildTokenTransfer(options: { customPayload: customPayload ? Cell.fromBase64(customPayload) : undefined, }); - let toncoinAmount = TINY_TOKENS.has(tokenAddress) + let toncoinAmount = token.isTiny ? TINY_TOKEN_TRANSFER_AMOUNT : TOKEN_TRANSFER_AMOUNT; @@ -252,16 +251,17 @@ export async function getMintlessParams(options: { network, fromAddress, token, tokenWalletAddress, shouldSkipMintless, } = options; - let isTokenWalletDeployed = true; + const isMintlessToken = !!token.customPayloadApiUrl; + const isTokenWalletDeployed = isMintlessToken + ? !!(await isActiveSmartContract(network, tokenWalletAddress)) + : true; let customPayload: string | undefined; let stateInit: string | undefined; - const isMintlessToken = !!token.customPayloadApiUrl; let isMintlessClaimed: boolean | undefined; let mintlessTokenBalance: bigint | undefined; if (isMintlessToken && !shouldSkipMintless) { - isTokenWalletDeployed = !!(await isActiveSmartContract(network, tokenWalletAddress)); isMintlessClaimed = isTokenWalletDeployed && await checkMintlessTokenWalletIsClaimed(network, tokenWalletAddress); if (!isMintlessClaimed) { diff --git a/src/api/chains/ton/transactions.ts b/src/api/chains/ton/transactions.ts index 9bd95c19..02fc9d22 100644 --- a/src/api/chains/ton/transactions.ts +++ b/src/api/chains/ton/transactions.ts @@ -3,6 +3,7 @@ import { } from '@ton/core'; import type { DieselStatus } from '../../../global/types'; +import type Deferred from '../../../util/Deferred'; import type { CheckTransactionDraftOptions } from '../../methods/types'; import type { ApiAccountWithMnemonic, @@ -30,7 +31,7 @@ import type { TonWallet } from './util/tonCore'; import { ApiCommonError, ApiTransactionDraftError, ApiTransactionError } from '../../types'; import { - DEFAULT_FEE, DIESEL_ADDRESS, DIESEL_TOKENS, ONE_TON, TINY_TOKENS, TOKENS_WITH_STARS_FEE, TONCOIN, + DEFAULT_FEE, DIESEL_ADDRESS, ONE_TON, TONCOIN, } from '../../../config'; import { parseAccountId } from '../../../util/account'; import { bigintMultiplyToNumber } from '../../../util/bigint'; @@ -38,6 +39,7 @@ import { compareActivities } from '../../../util/compareActivities'; import { fromDecimal, toDecimal } from '../../../util/decimals'; import { buildCollectionByKey, omit } from '../../../util/iteratees'; import { logDebugError } from '../../../util/logs'; +import { updatePoisoningCache } from '../../../util/poisoningHash'; import { pause } from '../../../util/schedulers'; import withCacheAsync from '../../../util/withCacheAsync'; import { parseTxId } from './util'; @@ -61,8 +63,10 @@ import { import { fetchStoredAccount, fetchStoredTonWallet } from '../../common/accounts'; import { callBackendGet } from '../../common/backend'; import { updateTransactionMetadata } from '../../common/helpers'; +import { getPendingTransfer, waitAndCreatePendingTransfer } from '../../common/pendingTransfers'; import { getTokenByAddress, getTokenBySlug } from '../../common/tokens'; import { base64ToBytes, isKnownStakingPool } from '../../common/utils'; +import { MINUTE, SEC } from '../../constants'; import { ApiServerError, handleServerError } from '../../errors'; import { createBody } from './walletV5/walletV5'; import { ActionSendMsg, packActionsList } from './walletV5/walletV5Actions'; @@ -81,25 +85,20 @@ import { buildTokenTransfer, parseTokenTransaction, } from './tokens'; import { - getContractInfo, getTonWallet, - getWalletBalance, getWalletInfo, + getContractInfo, + getTonWallet, + getWalletBalance, + getWalletInfo, } from './wallet'; const GET_TRANSACTIONS_LIMIT = 128; const GET_TRANSACTIONS_MAX_LIMIT = 100; -const WAIT_TRANSFER_TIMEOUT = 5 * 60 * 1000; // 5 min -const WAIT_TRANSFER_PAUSE = 1000; // 1 sec. -const WAIT_TRANSACTION_PAUSE = 500; // 0.5 sec. +const WAIT_TRANSFER_TIMEOUT = MINUTE; +const WAIT_PAUSE = SEC; const MAX_BALANCE_WITH_CHECK_DIESEL = 100000000n; // 0.1 TON const PENDING_DIESEL_TIMEOUT = 15 * 60 * 1000; // 15 min -const pendingTransfers: Record; -}> = {}; - export const checkHasTransaction = withCacheAsync(async (network: ApiNetwork, address: string) => { const transactions = await fetchTransactions({ network, address, limit: 1 }); return Boolean(transactions.length); @@ -250,22 +249,27 @@ export async function checkTransactionDraft( if ( network === 'mainnet' && tokenAddress - && (DIESEL_TOKENS.has(tokenAddress) || TOKENS_WITH_STARS_FEE.has(tokenAddress)) && toncoinBalance < MAX_BALANCE_WITH_CHECK_DIESEL ) { - const isW5 = version === 'W5'; - const { - status: dieselStatus, - amount: dieselAmount, - isAwaitingNotExpiredPrevious, - } = await fetchEstimateDiesel(accountId, tokenAddress, isW5) || {}; - - if (!isEnoughBalance || isAwaitingNotExpiredPrevious) { - result.dieselStatus = dieselStatus; - result.dieselAmount = dieselAmount; - - if (dieselStatus !== 'not-available') { - return result; + const token = getTokenByAddress(tokenAddress)!; + + if (token.isGaslessEnabled || token.isStarsEnabled) { + const isW5 = version === 'W5'; + const { + status: dieselStatus, + amount: dieselAmount, + isAwaitingNotExpiredPrevious, + } = await fetchEstimateDiesel( + accountId, tokenAddress, isW5, token.isGaslessEnabled ? false : token.isStarsEnabled, + ) || {}; + + if (!isEnoughBalance || isAwaitingNotExpiredPrevious) { + result.dieselStatus = dieselStatus; + result.dieselAmount = dieselAmount; + + if (dieselStatus !== 'not-available') { + return result; + } } } } @@ -286,7 +290,13 @@ export async function checkTransactionDraft( } } -function estimateDiesel(address: string, tokenAddress: string, toncoinAmount: string, isW5: boolean, isStars: boolean) { +function estimateDiesel( + address: string, + tokenAddress: string, + toncoinAmount: string, + isW5?: boolean, + isStars?: boolean, +) { return callBackendGet<{ status: DieselStatus; amount?: string; @@ -394,7 +404,7 @@ export async function submitTransfer(options: ApiSubmitTransferOptions): Promise })); } - await waitPendingTransfer(network, fromAddress); + const { pendingTransfer } = await waitAndCreatePendingTransfer(network, fromAddress); const { balance } = await getWalletInfo(network, wallet!); const isFullTonBalance = !tokenAddress && balance === amount; @@ -424,7 +434,7 @@ export async function submitTransfer(options: ApiSubmitTransferOptions): Promise const client = getTonClient(network); const { msgHash, boc } = await sendExternal(client, wallet!, transaction); - addPendingTransfer(network, fromAddress, seqno, boc); + void retrySendBoc(network, fromAddress, boc, seqno, pendingTransfer); return { amount, seqno, encryptedComment, toAddress, msgHash, @@ -664,7 +674,8 @@ async function populateTransactions(network: ApiNetwork, transactions: ApiTransa const nftAddresses = new Set(); const addressesForFixFormat = new Set(); - for (const { extraData: { parsedPayload } } of transactions) { + for (const tx of transactions) { + const { extraData: { parsedPayload } } = tx; if (parsedPayload?.type === 'nft:ownership-assigned') { nftAddresses.add(parsedPayload.nftAddress); addressesForFixFormat.add(parsedPayload.prevOwner); @@ -672,6 +683,8 @@ async function populateTransactions(network: ApiNetwork, transactions: ApiTransa nftAddresses.add(parsedPayload.nftAddress); addressesForFixFormat.add(parsedPayload.newOwner); } + + updatePoisoningCache(tx); } if (nftAddresses.size) { @@ -898,9 +911,11 @@ export async function submitMultiTransfer({ }: SubmitMultiTransferOptions): Promise { const { network } = parseAccountId(accountId); + const account = await fetchStoredAccount(accountId); + const { address: fromAddress, isInitialized, version } = account.ton; + const { pendingTransfer } = await waitAndCreatePendingTransfer(network, fromAddress); + try { - const account = await fetchStoredAccount(accountId); - const { address: fromAddress, isInitialized, version } = account.ton; const wallet = await getTonWallet(accountId, account.ton); const privateKey = await fetchPrivateKey(accountId, password, account); @@ -909,8 +924,6 @@ export async function submitMultiTransfer({ totalAmount += BigInt(message.amount); }); - await waitPendingTransfer(network, fromAddress); - const { balance } = await getWalletInfo(network, wallet!); const gaslessType = isGasless ? version === 'W5' ? 'w5' : 'diesel' : undefined; @@ -933,7 +946,7 @@ export async function submitMultiTransfer({ ); if (!isGasless) { - addPendingTransfer(network, fromAddress, seqno, boc); + void retrySendBoc(network, fromAddress, boc, seqno, pendingTransfer); } const clearedMessages = messages.map((message) => { @@ -952,6 +965,8 @@ export async function submitMultiTransfer({ paymentLink, }; } catch (err) { + pendingTransfer.resolve(); + logDebugError('submitMultiTransfer', err); return { error: resolveTransactionError(err) }; } @@ -967,6 +982,7 @@ async function signMultiTransaction( withW5Gasless = false, ) { const { seqno } = await getWalletInfo(network, wallet); + if (!expireAt) { expireAt = Math.round(Date.now() / 1000) + TRANSFER_TIMEOUT_SEC; } @@ -1044,64 +1060,36 @@ function toExternalMessage( .endCell(); } -function addPendingTransfer(network: ApiNetwork, address: string, seqno: number, boc: string) { - const key = buildPendingTransferKey(network, address); - const timestamp = Date.now(); - const promise = retrySendBoc({ - network, address, boc, seqno, timestamp, - }); - - pendingTransfers[key] = { - timestamp, - seqno, - promise, - }; -} - -async function retrySendBoc({ - network, address, boc, seqno, timestamp, -}: { - network: ApiNetwork; - address: string; - boc: string; - seqno: number; - timestamp: number; -}) { +async function retrySendBoc( + network: ApiNetwork, + address: string, + boc: string, + seqno: number, + pendingTransfer?: Deferred, +) { const tonClient = getTonClient(network); - const waitUntil = timestamp + WAIT_TRANSFER_TIMEOUT; + const waitUntil = Date.now() + WAIT_TRANSFER_TIMEOUT; while (Date.now() < waitUntil) { - const error = await tonClient.sendFile(boc).catch((err) => String(err)); + const [error, walletInfo] = await Promise.all([ + tonClient.sendFile(boc).catch((err) => String(err)), + getWalletInfo(network, address).catch(() => undefined), + ]); // Errors mean that `seqno` was changed or not enough of balance if (error?.includes('exitcode=33') || error?.includes('inbound external message rejected by account')) { - return; + break; } - await pause(WAIT_TRANSFER_PAUSE); - const walletInfo = await getWalletInfo(network, address).catch(() => undefined); - // seqno here may change before exit code appears if (walletInfo && walletInfo.seqno > seqno) { - return; + break; } - await pause(WAIT_TRANSFER_PAUSE); + await pause(WAIT_PAUSE); } -} - -export async function waitPendingTransfer(network: ApiNetwork, address: string) { - const key = buildPendingTransferKey(network, address); - const pendingTransfer = pendingTransfers[key]; - if (!pendingTransfer) return; - await pendingTransfer.promise; - - delete pendingTransfers[key]; -} - -function buildPendingTransferKey(network: ApiNetwork, address: string) { - return `${network}:${address}`; + pendingTransfer?.resolve(); } async function calculateFee(network: ApiNetwork, wallet: TonWallet, transaction: Cell, isInitialized?: boolean) { @@ -1118,18 +1106,24 @@ async function calculateFee(network: ApiNetwork, wallet: TonWallet, transaction: return BigInt(fees.in_fwd_fee + fees.storage_fee + fees.gas_fee + fees.fwd_fee); } -export async function sendSignedMessage(accountId: string, message: ApiSignedTransfer) { +export async function sendSignedMessage(accountId: string, message: ApiSignedTransfer, pendingTransferId?: string) { const { network } = parseAccountId(accountId); const { address: fromAddress, publicKey, version } = await fetchStoredTonWallet(accountId); const client = getTonClient(network); const wallet = client.open(getTonWalletContract(publicKey, version!)); - const { base64, seqno } = message; - const { msgHash, boc } = await sendExternal(client, wallet, Cell.fromBase64(base64)); + const pendingTransfer = pendingTransferId ? getPendingTransfer(pendingTransferId) : undefined; - addPendingTransfer(network, fromAddress, seqno, boc); + try { + const { msgHash, boc } = await sendExternal(client, wallet, Cell.fromBase64(base64)); - return msgHash; + void retrySendBoc(network, fromAddress, boc, seqno, pendingTransfer); + + return msgHash; + } catch (err) { + pendingTransfer?.resolve(); + throw err; + } } export async function sendSignedMessages(accountId: string, messages: ApiSignedTransfer[]) { @@ -1145,10 +1139,12 @@ export async function sendSignedMessages(accountId: string, messages: ApiSignedT let firstBoc: string | undefined; const msgHashes: string[] = []; + let pendingTransfer: Deferred | undefined; + while (index < messages.length && attempt < attempts) { const { base64, seqno } = messages[index]; try { - await waitPendingTransfer(network, fromAddress); + ({ pendingTransfer } = await waitAndCreatePendingTransfer(network, fromAddress)); const { msgHash, boc } = await sendExternal(client, wallet, Cell.fromBase64(base64)); msgHashes.push(msgHash); @@ -1157,10 +1153,11 @@ export async function sendSignedMessages(accountId: string, messages: ApiSignedT firstBoc = boc; } - addPendingTransfer(network, fromAddress, seqno, boc); + void retrySendBoc(network, fromAddress, boc, seqno, pendingTransfer); index++; } catch (err) { + pendingTransfer?.resolve(); logDebugError('sendSignedMessages', err); } attempt++; @@ -1197,7 +1194,7 @@ export async function waitUntilTransactionAppears(network: ApiNetwork, address: if (latestTxId && parseTxId(latestTxId).lt >= lt) { return; } - await pause(WAIT_TRANSACTION_PAUSE); + await pause(WAIT_PAUSE); } } @@ -1236,7 +1233,7 @@ export async function fixTokenActivitiesAddressForm(network: ApiNetwork, activit } export async function fetchEstimateDiesel( - accountId: string, tokenAddress: string, isW5 = false, + accountId: string, tokenAddress: string, isW5?: boolean, isStars?: boolean, ) { const { network } = parseAccountId(accountId); if (network !== 'mainnet') return undefined; @@ -1246,12 +1243,12 @@ export async function fetchEstimateDiesel( const { address } = await fetchStoredTonWallet(accountId); const balance = await getWalletBalance(network, wallet); + const token = getTokenByAddress(tokenAddress)!; if (balance >= MAX_BALANCE_WITH_CHECK_DIESEL) return undefined; - const isStars = TOKENS_WITH_STARS_FEE.has(tokenAddress); const multiplier = isStars ? 1n : 2n; - const transferAmount = TINY_TOKENS.has(tokenAddress) ? TINY_TOKEN_TRANSFER_AMOUNT : TOKEN_TRANSFER_AMOUNT; + const transferAmount = token.isTiny ? TINY_TOKEN_TRANSFER_AMOUNT : TOKEN_TRANSFER_AMOUNT; const toncoinAmount = toDecimal((transferAmount + DEFAULT_FEE) * multiplier); const { diff --git a/src/api/chains/ton/util/diesel.ts b/src/api/chains/ton/util/diesel.ts index e35abd4a..15cc3ecb 100644 --- a/src/api/chains/ton/util/diesel.ts +++ b/src/api/chains/ton/util/diesel.ts @@ -3,5 +3,7 @@ import { callBackendPost } from '../../../common/backend'; const DIESEL_URL = '/diesel'; export function dieselSendBoc(boc: string) { - return callBackendPost<{ result: string; paymentLink?: string }>(`${DIESEL_URL}/sendBoc`, { boc }); + return callBackendPost<{ result: string; paymentLink?: string }>( + `${DIESEL_URL}/sendBoc`, { boc }, { shouldRetry: true }, + ); } diff --git a/src/api/chains/ton/util/w5diesel.ts b/src/api/chains/ton/util/w5diesel.ts index 617f8ac7..72f75c7a 100644 --- a/src/api/chains/ton/util/w5diesel.ts +++ b/src/api/chains/ton/util/w5diesel.ts @@ -3,5 +3,7 @@ import { callBackendPost } from '../../../common/backend'; const DIESEL_URL = '/diesel'; export function dieselW5SendRequest(boc: string) { - return callBackendPost<{ result: string; paymentLink?: string }>(`${DIESEL_URL}/w5/sendBoc`, { boc }); + return callBackendPost<{ result: string; paymentLink?: string }>( + `${DIESEL_URL}/w5/sendBoc`, { boc }, { shouldRetry: true }, + ); } diff --git a/src/api/chains/tron/polling.ts b/src/api/chains/tron/polling.ts index f3fc1b09..20bb42a5 100644 --- a/src/api/chains/tron/polling.ts +++ b/src/api/chains/tron/polling.ts @@ -39,7 +39,8 @@ export async function setupPolling(accountId: string, onUpdate: OnApiUpdate, new onUpdate({ type: 'updateBalances', accountId, - balancesToUpdate: { + chain: 'tron', + balances: { [TRX.slug]: trxBalance, [usdtSlug]: usdtBalance, }, diff --git a/src/api/common/backend.ts b/src/api/common/backend.ts index 267b1bca..271c8c0e 100644 --- a/src/api/common/backend.ts +++ b/src/api/common/backend.ts @@ -1,5 +1,5 @@ import { APP_VERSION, BRILLIANT_API_BASE_URL } from '../../config'; -import { fetchJson, handleFetchErrors } from '../../util/fetch'; +import { fetchJson, fetchWithRetry, handleFetchErrors } from '../../util/fetch'; const BAD_REQUEST_CODE = 400; @@ -7,10 +7,17 @@ export async function callBackendPost(path: string, data: AnyLiteral, options authToken?: string; isAllowBadRequest?: boolean; method?: string; + shouldRetry?: boolean; + retries?: number; + timeouts?: number | number[]; }): Promise { - const { authToken, isAllowBadRequest, method } = options ?? {}; + const { + authToken, isAllowBadRequest, method, shouldRetry, retries, timeouts, + } = options ?? {}; - const response = await fetch(`${BRILLIANT_API_BASE_URL}${path}`, { + const url = new URL(`${BRILLIANT_API_BASE_URL}${path}`); + + const init: RequestInit = { method: method ?? 'POST', headers: { 'Content-Type': 'application/json', @@ -18,7 +25,15 @@ export async function callBackendPost(path: string, data: AnyLiteral, options 'X-App-Version': APP_VERSION, }, body: JSON.stringify(data), - }); + }; + + const response = shouldRetry + ? await fetchWithRetry(url, init, { + retries, + timeouts, + shouldSkipRetryFn: (message) => !message?.includes('signal is aborted'), + }) + : await fetch(url.toString(), init); await handleFetchErrors(response, isAllowBadRequest ? [BAD_REQUEST_CODE] : undefined); diff --git a/src/api/common/pendingTransfers.ts b/src/api/common/pendingTransfers.ts new file mode 100644 index 00000000..eb8e2892 --- /dev/null +++ b/src/api/common/pendingTransfers.ts @@ -0,0 +1,27 @@ +import type { ApiNetwork } from '../types'; + +import Deferred from '../../util/Deferred'; +import generateUniqueId from '../../util/generateUniqueId'; + +const byId: Record = {}; +const byAddress: Record = {}; + +export async function waitAndCreatePendingTransfer(network: ApiNetwork, address: string) { + const key = `${network}:${address}`; + const prevPending = byAddress[key]; + + const id = generateUniqueId(); + const pendingTransfer = new Deferred(); + byAddress[key] = pendingTransfer; + byId[id] = pendingTransfer; + + if (prevPending) { + await prevPending.promise; + } + + return { id, pendingTransfer }; +} + +export function getPendingTransfer(id: string): Deferred | undefined { + return byId[id]; +} diff --git a/src/api/common/tokens.ts b/src/api/common/tokens.ts index 67369374..62533dc6 100644 --- a/src/api/common/tokens.ts +++ b/src/api/common/tokens.ts @@ -18,20 +18,29 @@ export async function loadTokensCache() { export async function addTokens(tokens: ApiToken[], onUpdate?: OnApiUpdate, shouldForceSend?: boolean) { const newTokens: ApiToken[] = []; - for (const token of tokens) { + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + const mergedToken = mergeTokenWithCache(token); + tokensCache[token.slug] = mergedToken; + if (!(token.slug in tokensCache)) { - newTokens.push(token); + newTokens.push(mergedToken); } - tokensCache[token.slug] = token; + tokens[i] = mergedToken; } await tokenRepository.bulkPut(tokens); - if ((shouldForceSend || newTokens.length) && onUpdate) { sendUpdateTokens(onUpdate); } } +export function mergeTokenWithCache(token: ApiToken): ApiToken { + const cacheToken = tokensCache[token.slug] || {}; + + return { ...cacheToken, ...token }; +} + export function getTokensCache() { return tokensCache; } diff --git a/src/api/constants.ts b/src/api/constants.ts index 0df41eed..0f4296d4 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -1,2 +1,3 @@ export const SEC = 1000; +export const MINUTE = 60 * SEC; export const FIRST_TRANSACTIONS_LIMIT = 50; diff --git a/src/api/extensionMethods/legacy.ts b/src/api/extensionMethods/legacy.ts index ac7a54d3..2e0ceb70 100644 --- a/src/api/extensionMethods/legacy.ts +++ b/src/api/extensionMethods/legacy.ts @@ -48,8 +48,12 @@ export async function requestAccounts() { return []; } - const { address } = await fetchStoredTonWallet(accountId); - return [address]; + const tonWallet = await fetchStoredTonWallet(accountId); + if (!tonWallet) { + return []; + } + + return [tonWallet.address]; } export async function requestWallets() { diff --git a/src/api/methods/dapps.ts b/src/api/methods/dapps.ts index db3e436c..6718d1e9 100644 --- a/src/api/methods/dapps.ts +++ b/src/api/methods/dapps.ts @@ -114,7 +114,7 @@ export async function updateDapp(accountId: string, origin: string, update: Part } export async function getDapp(accountId: string, origin: string): Promise { - return (await getAccountValue(accountId, 'dapps'))[origin]; + return (await getAccountValue(accountId, 'dapps'))?.[origin]; } export async function addDapp(accountId: string, dapp: ApiDapp) { diff --git a/src/api/methods/transactions.ts b/src/api/methods/transactions.ts index 24d34d9e..a52447f6 100644 --- a/src/api/methods/transactions.ts +++ b/src/api/methods/transactions.ts @@ -18,6 +18,7 @@ import { logDebugError } from '../../util/logs'; import chains from '../chains'; import { fetchStoredAccount, fetchStoredAddress, fetchStoredTonWallet } from '../common/accounts'; import { buildLocalTransaction } from '../common/helpers'; +import { getPendingTransfer, waitAndCreatePendingTransfer } from '../common/pendingTransfers'; import { handleServerError } from '../errors'; import { buildTokenSlug } from './tokens'; @@ -206,17 +207,19 @@ export async function submitTransfer( }; } -export async function waitLastTonTransfer(accountId: string) { - const chain = chains.ton; - +export async function waitAndCreateTonPendingTransfer(accountId: string) { const { network } = parseAccountId(accountId); const { address } = await fetchStoredTonWallet(accountId); - return chain.waitPendingTransfer(network, address); + return (await waitAndCreatePendingTransfer(network, address)).id; } -export async function sendSignedTransferMessage(accountId: string, message: ApiSignedTransfer) { - const msgHash = await ton.sendSignedMessage(accountId, message); +export async function sendSignedTransferMessage( + accountId: string, + message: ApiSignedTransfer, + pendingTransferId: string, +) { + const msgHash = await ton.sendSignedMessage(accountId, message, pendingTransferId); const localTransaction = createLocalTransaction(accountId, 'ton', { ...message.params, @@ -226,17 +229,8 @@ export async function sendSignedTransferMessage(accountId: string, message: ApiS return localTransaction.txId; } -export async function sendSignedTransferMessages(accountId: string, messages: ApiSignedTransfer[]) { - const result = await ton.sendSignedMessages(accountId, messages); - - for (let i = 0; i < result.successNumber; i++) { - createLocalTransaction(accountId, 'ton', { - ...messages[i].params, - inMsgHash: result.msgHashes[i], - }); - } - - return result; +export function cancelPendingTransfer(id: string) { + getPendingTransfer(id)?.resolve(); } export function decryptComment(accountId: string, encryptedComment: string, fromAddress: string, password: string) { diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 2be4a722..df435195 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -31,6 +31,9 @@ export interface ApiToken { keywords?: string[]; cmcSlug?: string; color?: string; + isGaslessEnabled?: boolean; + isStarsEnabled?: boolean; + isTiny?: boolean; customPayloadApiUrl?: string; } @@ -182,7 +185,6 @@ export enum ApiLiquidUnstakeMode { export type ApiLoyaltyType = 'black' | 'platinum' | 'gold' | 'silver' | 'standard'; export type ApiBalanceBySlug = Record; -export type ApiMaybeBalanceBySlug = Record; export type ApiWalletInfo = { address: string; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 15883805..22edb961 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -5,11 +5,11 @@ import type { ApiStakingCommonData, ApiSwapAsset, ApiVestingInfo } from './backe import type { ApiAnyDisplayError } from './errors'; import type { ApiBackendStakingState, + ApiBalanceBySlug, ApiBaseCurrency, ApiChain, ApiCountryCode, ApiDappTransfer, - ApiMaybeBalanceBySlug, ApiNft, ApiStakingState, ApiTokenWithPrice, @@ -21,7 +21,8 @@ import type { ApiDapp, ApiTonWallet } from './storage'; export type ApiUpdateBalances = { type: 'updateBalances'; accountId: string; - balancesToUpdate: ApiMaybeBalanceBySlug; + chain: ApiChain; + balances: ApiBalanceBySlug; }; export type ApiUpdateNewActivities = { diff --git a/src/components/common/TransactionBanner.module.scss b/src/components/common/TransactionBanner.module.scss index b7d8a4d2..3f8e6e09 100644 --- a/src/components/common/TransactionBanner.module.scss +++ b/src/components/common/TransactionBanner.module.scss @@ -19,7 +19,7 @@ background-color: var(--color-transaction-amount-purple-bg); - .tokenIcon { + .tokenIcon, .chainIcon { --color-background-first: var(--color-transaction-amount-purple-bg); } } @@ -29,16 +29,24 @@ background-color: var(--color-transaction-amount-green-bg); - .tokenIcon { + .tokenIcon, .chainIcon { --color-background-first: var(--color-transaction-amount-green-bg); } } } -.tokenIcon { +.tokenIcon, +.chainIcon { --color-background-first: var(--color-transaction-amount-bg); } +.nftIcon { + position: relative; + + width: 1.25rem; + height: 1.25rem; +} + .image { width: 1.25rem; height: 1.25rem; @@ -46,6 +54,18 @@ border-radius: 50%; } +.chainIcon { + position: absolute; + top: 0.625rem; + left: 0.625rem; + + width: 0.6875rem; + height: 0.6875rem; + + border-radius: 50%; + box-shadow: 0 0 0 0.0625rem var(--color-background-first); +} + .text { overflow: hidden; diff --git a/src/components/common/TransactionBanner.tsx b/src/components/common/TransactionBanner.tsx index ce27142b..28acedff 100644 --- a/src/components/common/TransactionBanner.tsx +++ b/src/components/common/TransactionBanner.tsx @@ -5,6 +5,7 @@ import type { ApiSwapAsset, ApiToken } from '../../api/types'; import type { UserSwapToken, UserToken } from '../../global/types'; import buildClassName from '../../util/buildClassName'; +import getChainNetworkIcon from '../../util/swap/getChainNetworkIcon'; import useLang from '../../hooks/useLang'; @@ -41,9 +42,25 @@ function TransactionBanner({ className, ); + function renderNftIcon() { + return ( +
+ + {withChainIcon && tokenIn?.chain && ( + + )} +
+ ); + } + return (
- {tokenIn && ( + {tokenIn && !imageUrl && ( )} - {imageUrl && } + {imageUrl && renderNftIcon()} {secondText ? ( diff --git a/src/components/dapps/Dapp.module.scss b/src/components/dapps/Dapp.module.scss index 749dde08..366f64a6 100644 --- a/src/components/dapps/Dapp.module.scss +++ b/src/components/dapps/Dapp.module.scss @@ -26,6 +26,7 @@ align-items: center; justify-self: center; + width: 100%; padding: 0.875rem 1rem; background-color: var(--color-background-first); diff --git a/src/components/dapps/DappConnectModal.tsx b/src/components/dapps/DappConnectModal.tsx index 6dad494a..a599e9ac 100644 --- a/src/components/dapps/DappConnectModal.tsx +++ b/src/components/dapps/DappConnectModal.tsx @@ -280,7 +280,7 @@ function DappConnectModal({ isOpen={isOpen} dialogClassName={styles.modalDialog} nativeBottomSheetKey="dapp-connect" - forceFullNative={renderingKey === DappConnectState.Password} + forceFullNative={renderingKey !== DappConnectState.Info} onClose={cancelDappConnectRequestConfirm} onCloseAnimationEnd={cancelDappConnectRequestConfirm} > diff --git a/src/components/main/modals/TransactionModal.module.scss b/src/components/main/modals/TransactionModal.module.scss index 7727c85c..bbb424fa 100644 --- a/src/components/main/modals/TransactionModal.module.scss +++ b/src/components/main/modals/TransactionModal.module.scss @@ -306,3 +306,18 @@ .qrCodeHidden { opacity: 0; } + +.scamWarning { + max-width: 80%; + margin: 0 auto 2rem; + padding: 0.5rem 0.75rem; + + font-size: 0.9375rem; + font-weight: 600; + line-height: 1.3125rem; + color: var(--color-transaction-red-text); + text-align: center; + + background-color: var(--color-transaction-red-background); + border-radius: var(--border-radius-buttons); +} diff --git a/src/components/main/modals/TransactionModal.tsx b/src/components/main/modals/TransactionModal.tsx index 69b272c1..e2f10eaf 100644 --- a/src/components/main/modals/TransactionModal.tsx +++ b/src/components/main/modals/TransactionModal.tsx @@ -23,6 +23,7 @@ import { vibrateOnSuccess } from '../../../util/capacitor'; import { formatFullDay, formatRelativeHumanDateTime, formatTime } from '../../../util/dateFormat'; import { toDecimal } from '../../../util/decimals'; import { handleOpenUrl } from '../../../util/openUrl'; +import { getIsTransactionWithPoisoning } from '../../../util/poisoningHash'; import resolveModalTransitionName from '../../../util/resolveModalTransitionName'; import { getNativeToken, getTransactionHashFromTxId } from '../../../util/tokens'; import { getExplorerName, getExplorerTransactionUrl } from '../../../util/url'; @@ -136,7 +137,8 @@ function TransactionModal({ return address && chain && savedAddresses?.find((item) => item.address === address && item.chain === chain)?.name; }, [address, chain, savedAddresses]); const addressName = savedAddressName || transaction?.metadata?.name; - const isScam = Boolean(transaction?.metadata?.isScam); + const isTransactionWithPoisoning = isIncoming && getIsTransactionWithPoisoning(renderedTransaction!); + const isScam = isTransactionWithPoisoning || Boolean(transaction?.metadata?.isScam); const isModalOpen = Boolean(transaction) && !isMediaViewerOpen; const transactionHash = chain && id ? getTransactionHashFromTxId(chain, id) : undefined; @@ -319,6 +321,14 @@ function TransactionModal({ ); } + function renderTransactionWithPoisoningWarning() { + return ( +
+ {lang('This address mimics another address that you previously interacted with.')} +
+ ); + } + function renderFee() { if (isIncoming || !fee || !nativeToken) { return undefined; @@ -401,6 +411,8 @@ function TransactionModal({ /> )} + {isTransactionWithPoisoning && renderTransactionWithPoisoningWarning()} + {!isUnstaking && ( <>
{lang(isIncoming ? 'Sender' : 'Recipient')}
diff --git a/src/components/main/sections/Card/Card.module.scss b/src/components/main/sections/Card/Card.module.scss index 202f6cbd..d2d6d8b1 100644 --- a/src/components/main/sections/Card/Card.module.scss +++ b/src/components/main/sections/Card/Card.module.scss @@ -241,6 +241,8 @@ &:global(.icon-ledger) { margin-inline-end: 0.25rem; + + color: var(--color-card-second-text); } } diff --git a/src/components/main/sections/Card/CardAddress.tsx b/src/components/main/sections/Card/CardAddress.tsx index 84cc01ae..1c7ff146 100644 --- a/src/components/main/sections/Card/CardAddress.tsx +++ b/src/components/main/sections/Card/CardAddress.tsx @@ -138,13 +138,13 @@ function CardAddress({ addressByChain, isTestnet, isHardwareAccount }: StateProp return (
+ {isHardwareAccount && } diff --git a/src/components/main/sections/Content/Activities.tsx b/src/components/main/sections/Content/Activities.tsx index 545cea74..5b7104bc 100644 --- a/src/components/main/sections/Content/Activities.tsx +++ b/src/components/main/sections/Content/Activities.tsx @@ -25,6 +25,7 @@ import { import buildClassName from '../../../../util/buildClassName'; import { formatHumanDay, getDayStartAt } from '../../../../util/dateFormat'; import { findLast } from '../../../../util/iteratees'; +import { getIsTransactionWithPoisoning } from '../../../../util/poisoningHash'; import { REM } from '../../../../util/windowEnvironment'; import { ANIMATED_STICKERS_PATHS } from '../../../ui/helpers/animatedAssets'; @@ -68,7 +69,7 @@ type StateProps = { savedAddresses?: SavedAddress[]; isMainHistoryEndReached?: boolean; isHistoryEndReachedBySlug?: Record; - exceptionSlugs?: string[]; + alwaysShownSlugs?: string[]; activitiesUpdateStartedAt?: number; theme: Theme; isFirstTransactionsLoaded?: boolean; @@ -111,7 +112,7 @@ function Activities({ savedAddresses, isMainHistoryEndReached, isHistoryEndReachedBySlug, - exceptionSlugs, + alwaysShownSlugs, activitiesUpdateStartedAt = 0, theme, isFirstTransactionsLoaded, @@ -175,8 +176,9 @@ function Activities({ && ( !areTinyTransfersHidden || !getIsTinyOrScamTransaction(activity, tokensBySlug![activity.slug]) - || exceptionSlugs?.includes(activity.slug) - ), + || alwaysShownSlugs?.includes(activity.slug) + ) + && !getIsTransactionWithPoisoning(activity), ); } }) as ApiActivity[]; @@ -186,7 +188,7 @@ function Activities({ } return allActivities; - }, [areTinyTransfersHidden, byId, exceptionSlugs, ids, slug, tokensBySlug]); + }, [areTinyTransfersHidden, byId, alwaysShownSlugs, ids, slug, tokensBySlug]); const { activityIds, activitiesById } = useMemo(() => { const activityIdList: string[] = []; @@ -491,7 +493,7 @@ export default memo( isMainHistoryEndReached, isHistoryEndReachedBySlug, currentActivityId: accountState?.currentActivityId, - exceptionSlugs: accountSettings?.exceptionSlugs, + alwaysShownSlugs: accountSettings?.alwaysShownSlugs, activitiesUpdateStartedAt: global.activitiesUpdateStartedAt, theme: global.settings.theme, isFirstTransactionsLoaded, diff --git a/src/components/main/sections/Content/Assets.tsx b/src/components/main/sections/Content/Assets.tsx index 3cf74890..d04d45c2 100644 --- a/src/components/main/sections/Content/Assets.tsx +++ b/src/components/main/sections/Content/Assets.tsx @@ -12,9 +12,7 @@ import { selectCurrentAccountStakingStatus, selectCurrentAccountState, selectCurrentAccountTokens, - selectIsFirstTransactionsLoaded, selectIsMultichainAccount, - selectIsNewWallet, selectMycoin, } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; @@ -31,7 +29,6 @@ import useVesting from '../../../../hooks/useVesting'; import InfiniteScroll from '../../../ui/InfiniteScroll'; import Loading from '../../../ui/Loading'; -import NewWalletGreeting from './NewWalletGreeting'; import Token from './Token'; import styles from './Assets.module.scss'; @@ -45,7 +42,6 @@ type OwnProps = { interface StateProps { tokens?: UserToken[]; - isNewWallet: boolean; vesting?: ApiVestingInfo[]; stakingStatus?: StakingStatus; stakingBalance?: bigint; @@ -64,7 +60,6 @@ const TOKEN_HEIGHT_REM = 4; function Assets({ isActive, tokens, - isNewWallet, vesting, stakingStatus, stakingBalance, @@ -94,8 +89,6 @@ function Assets({ const { isLandscape, isPortrait } = useDeviceScreen(); const appTheme = useAppTheme(theme); - const shouldShowGreeting = isNewWallet && isPortrait && !isSeparatePanel; - const { shouldRender: shouldRenderStakedToken, transitionClassNames: stakedTokenClassNames } = useShowTransition( Boolean(stakingStatus && toncoin), ); @@ -222,7 +215,6 @@ function Assets({
)} - {shouldShowGreeting && } {shouldRenderVestingToken && renderVestingToken()} {shouldRenderStakedToken && renderStakedToken()} {viewportSlugs?.map((tokenSlug, i) => renderToken(tokensBySlug![tokenSlug], i))} @@ -234,15 +226,12 @@ export default memo( withGlobal( (global): StateProps => { const tokens = selectCurrentAccountTokens(global); - const isFirstTransactionLoaded = selectIsFirstTransactionsLoaded(global, global.currentAccountId!); - const isNewWallet = selectIsNewWallet(global, isFirstTransactionLoaded); const accountState = selectCurrentAccountState(global); const { isInvestorViewEnabled } = global.settings; const stakingStatus = selectCurrentAccountStakingStatus(global); return { tokens, - isNewWallet, stakingStatus, vesting: accountState?.vesting?.info, stakingBalance: accountState?.staking?.balance, diff --git a/src/components/main/sections/Content/Content.tsx b/src/components/main/sections/Content/Content.tsx index 2399c573..694c127e 100644 --- a/src/components/main/sections/Content/Content.tsx +++ b/src/components/main/sections/Content/Content.tsx @@ -13,6 +13,7 @@ import { PORTRAIT_MIN_ASSETS_TAB_VIEW, } from '../../../../config'; import { + selectCurrentAccountStakingStatus, selectCurrentAccountState, selectCurrentAccountTokens, selectEnabledTokensCountMemoizedFor, @@ -52,6 +53,8 @@ interface StateProps { activeContentTab?: ContentTab; blacklistedNftAddresses?: string[]; whitelistedNftAddresses?: string[]; + hasStaking: boolean; + hasVesting: boolean; selectedNftsToHide?: { addresses: string[]; isCollection: boolean; @@ -70,6 +73,8 @@ function Content({ blacklistedNftAddresses, whitelistedNftAddresses, selectedNftsToHide, + hasStaking, + hasVesting, }: OwnProps & StateProps) { const { selectToken, @@ -139,8 +144,9 @@ function Content({ // eslint-disable-next-line no-null/no-null const transitionRef = useRef(null); - const shouldShowSeparateAssetsPanel = tokensCount > 0 - && tokensCount <= (isPortrait ? PORTRAIT_MIN_ASSETS_TAB_VIEW : LANDSCAPE_MIN_ASSETS_TAB_VIEW); + const totalTokensAmount = tokensCount + (hasVesting ? 1 : 0) + (hasStaking ? 1 : 0); + const shouldShowSeparateAssetsPanel = totalTokensAmount > 0 + && totalTokensAmount <= (isPortrait ? PORTRAIT_MIN_ASSETS_TAB_VIEW : LANDSCAPE_MIN_ASSETS_TAB_VIEW); const tabs = useMemo( () => [ ...( @@ -351,6 +357,7 @@ export default memo( blacklistedNftAddresses, whitelistedNftAddresses, selectedNftsToHide, + vesting, nfts: { byAddress: nfts, currentCollectionAddress, @@ -359,6 +366,8 @@ export default memo( } = selectCurrentAccountState(global) ?? {}; const tokens = selectCurrentAccountTokens(global); const tokensCount = selectEnabledTokensCountMemoizedFor(global.currentAccountId!)(tokens); + const hasVesting = Boolean(vesting?.info?.length); + const hasStaking = Boolean(selectCurrentAccountStakingStatus(global)); return { nfts, @@ -369,6 +378,8 @@ export default memo( blacklistedNftAddresses, whitelistedNftAddresses, selectedNftsToHide, + hasStaking, + hasVesting, }; }, (global, _, stickToFirst) => stickToFirst(global.currentAccountId), diff --git a/src/components/main/sections/Content/Transaction.tsx b/src/components/main/sections/Content/Transaction.tsx index 092e3e61..2121323d 100644 --- a/src/components/main/sections/Content/Transaction.tsx +++ b/src/components/main/sections/Content/Transaction.tsx @@ -6,13 +6,14 @@ import type { ApiTokenWithPrice, ApiTransactionActivity } from '../../../../api/ import type { AppTheme, SavedAddress } from '../../../../global/types'; import { MediaType } from '../../../../global/types'; -import { ANIMATED_STICKER_TINY_ICON_PX, TONCOIN } from '../../../../config'; +import { ANIMATED_STICKER_TINY_ICON_PX, TONCOIN, TRANSACTION_ADDRESS_SHIFT } from '../../../../config'; import { getIsTxIdLocal } from '../../../../global/helpers'; import { bigintAbs } from '../../../../util/bigint'; import buildClassName from '../../../../util/buildClassName'; import { formatTime } from '../../../../util/dateFormat'; import { toDecimal } from '../../../../util/decimals'; import { formatCurrencyExtended } from '../../../../util/formatNumber'; +import { getIsTransactionWithPoisoning } from '../../../../util/poisoningHash'; import { shortenAddress } from '../../../../util/shortenAddress'; import { ANIMATED_STICKERS_PATHS } from '../../../ui/helpers/animatedAssets'; @@ -39,8 +40,6 @@ type OwnProps = { onClick: (id: string) => void; }; -const ADDRESS_SHIFT = 4; - function Transaction({ ref, tokensBySlug, @@ -85,7 +84,8 @@ function Transaction({ }, [address, chain, savedAddresses]); const addressName = savedAddressName || metadata?.name; const isLocal = getIsTxIdLocal(txId); - const isScam = Boolean(metadata?.isScam); + const isTransactionWithPoisoning = isIncoming && getIsTransactionWithPoisoning(transaction); + const isScam = isTransactionWithPoisoning || Boolean(metadata?.isScam); const handleClick = useLastCallback(() => { onClick(txId); @@ -192,7 +192,7 @@ function Transaction({ aria-label={chain} /> )} - {addressName || shortenAddress(address, ADDRESS_SHIFT)} + {addressName || shortenAddress(address, TRANSACTION_ADDRESS_SHIFT)}
), })} diff --git a/src/components/settings/SettingsDeveloperOptions.tsx b/src/components/settings/SettingsDeveloperOptions.tsx index c8ccb342..be31be37 100644 --- a/src/components/settings/SettingsDeveloperOptions.tsx +++ b/src/components/settings/SettingsDeveloperOptions.tsx @@ -1,10 +1,14 @@ +import { Directory, Encoding, Filesystem } from '@capacitor/filesystem'; +import { Share } from '@capacitor/share'; import React, { memo } from '../../lib/teact/teact'; import { getActions } from '../../global'; import type { ApiNetwork } from '../../api/types'; +import { IS_CAPACITOR } from '../../config'; import buildClassName from '../../util/buildClassName'; import { getLogs } from '../../util/logs'; +import { IS_IOS } from '../../util/windowEnvironment'; import { callApi } from '../../api'; import useLang from '../../hooks/useLang'; @@ -41,26 +45,6 @@ function SettingsDeveloperOptions({ const lang = useLang(); const currentNetwork = NETWORK_OPTIONS[isTestnet ? 1 : 0].value; - const handleLogClick = useLastCallback(async () => { - const workerLogs = await callApi('getLogs') || []; - const uiLogs = getLogs(); - const logsString = JSON.stringify( - [...workerLogs, ...uiLogs].sort((a, b) => a.timestamp - b.timestamp), - undefined, - 2, - ); - - const blob = new Blob([logsString], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - - const link = document.createElement('a'); - link.href = url; - link.download = `mytonwallet_logs_${Date.now()}.json`; - link.click(); - - URL.revokeObjectURL(url); - }); - const handleNetworkChange = useLastCallback((newNetwork: string) => { if (currentNetwork === newNetwork) { return; @@ -106,7 +90,7 @@ function SettingsDeveloperOptions({ )} -
+
downloadLogs()}>
{lang('Download Logs')}
@@ -123,3 +107,46 @@ function SettingsDeveloperOptions({ } export default memo(SettingsDeveloperOptions); + +async function downloadLogs() { + const workerLogs = await callApi('getLogs') || []; + const uiLogs = getLogs(); + const logsString = JSON.stringify( + [...workerLogs, ...uiLogs].sort((a, b) => a.timestamp - b.timestamp), + undefined, + 2, + ); + + const blob = new Blob([logsString], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const filename = `mytonwallet_logs_${Date.now()}.json`; + + if (IS_CAPACITOR) { + const logFile = await Filesystem.writeFile({ + path: filename, + data: logsString, + directory: Directory.Cache, + encoding: Encoding.UTF8, + }); + + await Share.share({ + url: logFile.uri, + }); + } else if (navigator.share) { + const file = new File([blob], filename, { type: blob.type }); + + navigator.share({ + files: [file], + }); + } else if (IS_IOS) { + window.open(url, '_blank', 'noreferrer'); + } else { + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + } + + URL.revokeObjectURL(url); +} diff --git a/src/components/settings/SettingsTokens.tsx b/src/components/settings/SettingsTokens.tsx index 7e41e27f..5ec2d21b 100644 --- a/src/components/settings/SettingsTokens.tsx +++ b/src/components/settings/SettingsTokens.tsx @@ -56,7 +56,7 @@ function SettingsTokens({ openSettingsWithState, updateOrderedSlugs, rebuildOrderedSlugs, - toggleExceptionToken, + toggleTokenVisibility, } = getActions(); const lang = useLang(); const shortBaseSymbol = getShortCurrencySymbol(baseCurrency); @@ -122,12 +122,12 @@ function SettingsTokens({ }); }); - const handleExceptionToken = useLastCallback((slug: string, e: React.MouseEvent | React.TouchEvent) => { - if (slug === TONCOIN.slug) return; + const handleExceptionToken = useLastCallback((token: UserToken, e: React.MouseEvent | React.TouchEvent) => { + if (token.slug === TONCOIN.slug) return; e.preventDefault(); e.stopPropagation(); - toggleExceptionToken({ slug }); + toggleTokenVisibility({ slug: token.slug, shouldShow: Boolean(token.isDisabled) }); }); const handleDeleteToken = useLastCallback((token: UserToken, e: React.MouseEvent) => { @@ -168,7 +168,7 @@ function SettingsTokens({ parentRef={tokensRef} scrollRef={parentContainer} // eslint-disable-next-line react/jsx-no-bind - onClick={(e) => handleExceptionToken(slug, e)} + onClick={(e) => handleExceptionToken(token, e)} >
diff --git a/src/components/staking/UnstakeModal.tsx b/src/components/staking/UnstakeModal.tsx index 3925a193..ada24862 100644 --- a/src/components/staking/UnstakeModal.tsx +++ b/src/components/staking/UnstakeModal.tsx @@ -7,7 +7,7 @@ import type { ApiBaseCurrency, ApiStakingType } from '../../api/types'; import type { GlobalState, HardwareConnectState, Theme, UserToken, } from '../../global/types'; -import { StakingState } from '../../global/types'; +import { ActiveTab, StakingState } from '../../global/types'; import { ANIMATED_STICKER_TINY_ICON_PX, @@ -35,6 +35,7 @@ import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; import useAppTheme from '../../hooks/useAppTheme'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; +import { useDeviceScreen } from '../../hooks/useDeviceScreen'; import useForceUpdate from '../../hooks/useForceUpdate'; import useInterval from '../../hooks/useInterval'; import useLang from '../../hooks/useLang'; @@ -119,9 +120,11 @@ function UnstakeModal({ submitStakingHardware, fetchStakingHistory, openReceiveModal, + setLandscapeActionsActiveTabIndex, } = getActions(); const lang = useLang(); + const { isLandscape } = useDeviceScreen(); const isOpen = IS_OPEN_STATES.has(state); const tonToken = useMemo(() => tokens?.find(({ slug }) => slug === TONCOIN.slug), [tokens]); @@ -209,7 +212,11 @@ function UnstakeModal({ const handleGetTon = useLastCallback(() => { cancelStaking(); - openReceiveModal(); + if (isLandscape) { + setLandscapeActionsActiveTabIndex({ index: ActiveTab.Receive }); + } else { + openReceiveModal(); + } }); function renderTransactionBanner() { diff --git a/src/components/swap/SwapInitial.tsx b/src/components/swap/SwapInitial.tsx index c8fa1732..be5749bb 100644 --- a/src/components/swap/SwapInitial.tsx +++ b/src/components/swap/SwapInitial.tsx @@ -15,7 +15,6 @@ import { CHANGELLY_PRIVACY_POLICY, CHANGELLY_TERMS_OF_USE, DEFAULT_SWAP_SECOND_TOKEN_SLUG, - DIESEL_TOKENS, TONCOIN, TRX, } from '../../config'; @@ -181,10 +180,10 @@ function SwapInitial({ const isErrorExist = errorType !== undefined; - const isDieselSwap = swapType === SwapType.OnChain + const isDieselSwap = Boolean(swapType === SwapType.OnChain && !isEnoughNative && tokenIn?.tokenAddress - && DIESEL_TOKENS.has(tokenIn.tokenAddress); + && dieselStatus); const isCorrectAmountIn = Boolean( amountIn diff --git a/src/components/transfer/Transfer.module.scss b/src/components/transfer/Transfer.module.scss index e23441b1..920f0a8c 100644 --- a/src/components/transfer/Transfer.module.scss +++ b/src/components/transfer/Transfer.module.scss @@ -318,8 +318,14 @@ .savedAddressName { overflow: hidden; + display: flex; + align-items: baseline; margin-inline-end: auto; +} + +.savedAddressNameText { + overflow: hidden; font-size: 1rem; font-weight: 600; diff --git a/src/components/transfer/TransferInitial.tsx b/src/components/transfer/TransferInitial.tsx index 9d4edbb8..ad1bf4a4 100644 --- a/src/components/transfer/TransferInitial.tsx +++ b/src/components/transfer/TransferInitial.tsx @@ -17,6 +17,7 @@ import { import { Big } from '../../lib/big.js'; import renderText from '../../global/helpers/renderText'; import { + selectCurrentAccountSettings, selectCurrentAccountState, selectCurrentAccountTokens, selectIsHardwareAccount, @@ -82,6 +83,7 @@ interface StateProps { isMemoRequired?: boolean; baseCurrency?: ApiBaseCurrency; nfts?: ApiNft[]; + alwaysHiddenSlugs?: string[]; binPayload?: string; stateInit?: string; dieselAmount?: bigint; @@ -138,6 +140,7 @@ function TransferInitial({ binPayload, stateInit, dieselAmount, + alwaysHiddenSlugs, dieselStatus, isDieselAuthorizationStarted, isMultichainAccount, @@ -201,7 +204,7 @@ function TransferInitial({ return tokens?.find((token) => !token.tokenAddress && token.chain === chain); }, [tokens, chain])!; - const isUpdatingAmountDueToMaxChange = useRef(false); + const skipNextFeeEstimate = useRef(false); const [isMaxAmountSelected, setMaxAmountSelected] = useState(false); const [prevDieselAmount, setPrevDieselAmount] = useState(dieselAmount); @@ -233,7 +236,7 @@ function TransferInitial({ const isDieselNotAuthorized = dieselStatus === 'not-authorized'; const withDiesel = dieselStatus && dieselStatus !== 'not-available'; const isEnoughDiesel = withDiesel && amount && balance && dieselAmount - ? isGaslessWithStars || isUpdatingAmountDueToMaxChange.current + ? isGaslessWithStars || skipNextFeeEstimate.current ? true : balance - amount >= dieselAmount : undefined; @@ -281,7 +284,7 @@ function TransferInitial({ } return tokens.reduce((acc, token) => { - if (token.amount > 0 || token.slug === tokenSlug) { + if ((token.amount > 0 || token.slug === tokenSlug) && !alwaysHiddenSlugs?.includes(token.slug)) { acc.push({ value: token.slug, icon: ASSET_LOGO_PATHS[token.symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS] || token.image, @@ -292,7 +295,7 @@ function TransferInitial({ return acc; }, []); - }, [isMultichainAccount, tokenSlug, tokens]); + }, [alwaysHiddenSlugs, isMultichainAccount, tokenSlug, tokens]); const validateAndSetAmount = useLastCallback( (newAmount: bigint | undefined, noReset = false) => { @@ -348,7 +351,7 @@ function TransferInitial({ useEffect(() => { if (isMaxAmountSelected && dieselAmount && prevDieselAmount !== dieselAmount && maxAmount! > 0) { - isUpdatingAmountDueToMaxChange.current = true; + skipNextFeeEstimate.current = true; setMaxAmountSelected(false); setPrevDieselAmount(dieselAmount); @@ -364,9 +367,9 @@ function TransferInitial({ || hasToAddressError || !(amount || nfts?.length) || !isAddressValid - || isUpdatingAmountDueToMaxChange.current + || skipNextFeeEstimate.current ) { - isUpdatingAmountDueToMaxChange.current = false; + skipNextFeeEstimate.current = false; return; } @@ -618,6 +621,8 @@ function TransferInitial({ vibrate(); + skipNextFeeEstimate.current = true; + submitTransferInitial({ tokenSlug, amount: isNftTransfer ? NFT_TRANSFER_AMOUNT : amount!, @@ -641,18 +646,21 @@ function TransferInitial({ return undefined; } - return savedAddresses.filter( - (item) => doesSavedAddressFitSearch(item, toAddress), - ).map((item) => renderAddressItem({ - key: `saved-${item.address}-${item.chain}`, - address: item.address, - name: item.name, - chain: isMultichainAccount ? item.chain : undefined, - deleteLabel: lang('Delete'), - onClick: handleAddressBookItemClick, - onDeleteClick: handleDeleteSavedAddressClick, - })); - }, [savedAddresses, isMultichainAccount, lang, toAddress]); + return savedAddresses + .filter((item) => { + // NFT transfer is only available on the TON blockchain + return (!isNftTransfer || item.chain === 'ton') && doesSavedAddressFitSearch(item, toAddress); + }) + .map((item) => renderAddressItem({ + key: `saved-${item.address}-${item.chain}`, + address: item.address, + name: item.name, + chain: isMultichainAccount ? item.chain : undefined, + deleteLabel: lang('Delete'), + onClick: handleAddressBookItemClick, + onDeleteClick: handleDeleteSavedAddressClick, + })); + }, [savedAddresses, isNftTransfer, toAddress, isMultichainAccount, lang]); const renderedOtherAccounts = useMemo(() => { if (otherAccountIds.length === 0) return undefined; @@ -668,6 +676,8 @@ function TransferInitial({ const key = `${currentChain}:${currentAddress}`; if ( !uniqueAddresses.has(key) + // NFT transfer is only available on the TON blockchain + && (!isNftTransfer || currentChain === 'ton') && (isMultichainAccount || currentChain === TONCOIN.chain) && !addressesToBeIgnored.includes(`${currentChain}:${currentAddress}`) ) { @@ -696,7 +706,7 @@ function TransferInitial({ isHardware, onClick: handleAddressBookItemClick, })); - }, [otherAccountIds, savedAddresses, accounts, isMultichainAccount, toAddress]); + }, [otherAccountIds, savedAddresses, accounts, isMultichainAccount, isNftTransfer, toAddress]); const shouldRenderSuggestions = !!renderedSavedAddresses?.length || !!renderedOtherAccounts?.length; @@ -1068,6 +1078,7 @@ export default memo( binPayload, stateInit, tokens: selectCurrentAccountTokens(global), + alwaysHiddenSlugs: selectCurrentAccountSettings(global)?.alwaysHiddenSlugs, savedAddresses: accountState?.savedAddresses, isEncryptedCommentSupported: !isLedger && !nfts?.length && !isMemoRequired, isMemoRequired, @@ -1138,7 +1149,9 @@ function renderAddressItem({ className={styles.savedAddressItem} > - {name || shortenAddress(address)} + + {name || shortenAddress(address)} + {isHardware && } {isSavedAddress && ( diff --git a/src/components/ui/PasswordForm.tsx b/src/components/ui/PasswordForm.tsx index e256fb7d..3ab97cac 100644 --- a/src/components/ui/PasswordForm.tsx +++ b/src/components/ui/PasswordForm.tsx @@ -334,7 +334,7 @@ function PasswordForm({

diff --git a/src/config.ts b/src/config.ts index 611e6a09..c5fad4ab 100644 --- a/src/config.ts +++ b/src/config.ts @@ -61,6 +61,7 @@ export const ANIMATED_STICKER_BIG_SIZE_PX = 156; export const ANIMATED_STICKER_HUGE_SIZE_PX = 192; export const DEFAULT_LANDSCAPE_ACTION_TAB_ID = 0; +export const TRANSACTION_ADDRESS_SHIFT = 4; export const WHOLE_PART_DELIMITER = ' '; // https://www.compart.com/en/unicode/U+202F @@ -117,7 +118,7 @@ export const PROXY_HOSTS = process.env.PROXY_HOSTS; export const TINY_TRANSFER_MAX_COST = 0.01; -export const LANG_CACHE_NAME = 'mtw-lang-134'; +export const LANG_CACHE_NAME = 'mtw-lang-135'; export const LANG_LIST: LangItem[] = [{ langCode: 'en', @@ -248,8 +249,6 @@ export const CHAIN_CONFIG = { }, } as const; -export const NATIVE_TOKENS = [TONCOIN, TRX]; - export const TRC20_USDT_MAINNET_SLUG = 'tron-tr7nhqjekq'; export const TRC20_USDT_TESTNET_SLUG = 'tron-tg3xxyexbk'; export const TON_USDT_SLUG = 'ton-eqcxe6mutq'; @@ -375,7 +374,7 @@ export const INDEXED_DB_STORE_NAME = 'keyval'; export const WINDOW_PROVIDER_CHANNEL = 'windowProvider'; -export const PORTRAIT_MIN_ASSETS_TAB_VIEW = 5; +export const PORTRAIT_MIN_ASSETS_TAB_VIEW = 4; export const LANDSCAPE_MIN_ASSETS_TAB_VIEW = 6; export const DEFAULT_PRICE_CURRENCY = 'USD'; @@ -455,21 +454,6 @@ export const RE_TG_BOT_MENTION = /telegram[:\s-]*((@[a-z0-9_]+)|(https:\/\/)?(t\ export const DIESEL_ADDRESS = process.env.DIESEL_ADDRESS || 'EQDUkQbpTVIgt7v66-JTFR-3-eXRFz_4V66F-Ufn6vOg0D5s'; -export const DIESEL_TOKENS = new Set([ - 'EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT', // NOT - 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs', // USDT - 'EQCvxJy4eG8hyHBFsZ7eePxrRsUQSFE_jpptRAYBmcG_DOGS', // DOGS - 'EQD-cvR0Nz6XAyRBvbhz-abTrRC6sI5tvHvvpeQraV9UAAD7', // CATI - 'EQAJ8uWd7EBqsmpSWaRdf_I-8R8-XHwh3gsNKhy-UrdrPcUo', // HAMSTER -]); - -export const TINY_TOKENS = new Set([ - 'EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT', // NOT - 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs', // USDT - 'EQCvxJy4eG8hyHBFsZ7eePxrRsUQSFE_jpptRAYBmcG_DOGS', // DOGS -]); -export const TOKENS_WITH_STARS_FEE = new Set([]); - export const STARS_SYMBOL = '⭐️'; export const GIVEAWAY_CHECKIN_URL = process.env.GIVEAWAY_CHECKIN_URL || 'https://giveaway.mytonwallet.io'; diff --git a/src/global/actions/api/auth.ts b/src/global/actions/api/auth.ts index d8335007..bdab7963 100644 --- a/src/global/actions/api/auth.ts +++ b/src/global/actions/api/auth.ts @@ -18,6 +18,7 @@ import isMnemonicPrivateKey from '../../../util/isMnemonicPrivateKey'; import { cloneDeep, compact, omitUndefined } from '../../../util/iteratees'; import { getTranslation } from '../../../util/langProvider'; import { callActionInMain } from '../../../util/multitab'; +import { clearPoisoningCache } from '../../../util/poisoningHash'; import { pause } from '../../../util/schedulers'; import { IS_BIOMETRIC_AUTH_SUPPORTED, IS_DELEGATED_BOTTOM_SHEET, IS_ELECTRON } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; @@ -598,6 +599,7 @@ addActionHandler('switchAccount', async (global, actions, payload) => { setGlobal(global); actions.clearSwapPairsCache(); + clearPoisoningCache(); if (newNetwork) { actions.changeNetwork({ network: newNetwork }); } diff --git a/src/global/actions/api/wallet.ts b/src/global/actions/api/wallet.ts index db763020..9a2765a3 100644 --- a/src/global/actions/api/wallet.ts +++ b/src/global/actions/api/wallet.ts @@ -20,6 +20,7 @@ import { buildCollectionByKey, extractKey, findLast, pick, unique, } from '../../../util/iteratees'; import { callActionInMain, callActionInNative } from '../../../util/multitab'; +import { getIsTransactionWithPoisoning } from '../../../util/poisoningHash'; import { onTickEnd, pause } from '../../../util/schedulers'; import { IS_DELEGATED_BOTTOM_SHEET, IS_DELEGATING_BOTTOM_SHEET } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; @@ -27,13 +28,13 @@ import { ApiHardwareBlindSigningNotEnabled, ApiUserRejectsError } from '../../.. import { getIsSwapId, getIsTinyOrScamTransaction, getIsTxIdLocal } from '../../helpers'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { + changeBalance, clearCurrentTransfer, clearIsPinAccepted, setIsPinAccepted, updateAccountState, updateActivitiesIsHistoryEndReached, updateActivitiesIsLoading, - updateBalances, updateCurrentAccountSettings, updateCurrentAccountState, updateCurrentSignature, @@ -586,9 +587,17 @@ addActionHandler('fetchTokenTransactions', async (global, actions, { limit, slug break; } - const filteredResult = global.settings.areTinyTransfersHidden - ? result.filter((tx) => tx.kind === 'transaction' && !getIsTinyOrScamTransaction(tx)) - : result; + const { areTinyTransfersHidden } = global.settings; + + const filteredResult = result.filter((tx) => { + const shouldHide = tx.kind === 'transaction' + && ( + getIsTransactionWithPoisoning(tx) + || (areTinyTransfersHidden && getIsTinyOrScamTransaction(tx)) + ); + + return !shouldHide; + }); fetchedActivities = fetchedActivities.concat(result); shouldFetchMore = filteredResult.length < limit && fetchedActivities.length < limit; @@ -661,9 +670,17 @@ addActionHandler('fetchAllTransactions', async (global, actions, { limit, should break; } - const filteredResult = global.settings.areTinyTransfersHidden - ? result.filter((tx) => tx.kind === 'transaction' && !getIsTinyOrScamTransaction(tx)) - : result; + const { areTinyTransfersHidden } = global.settings; + + const filteredResult = result.filter((tx) => { + const shouldHide = tx.kind === 'transaction' + && ( + getIsTransactionWithPoisoning(tx) + || (areTinyTransfersHidden && getIsTinyOrScamTransaction(tx)) + ); + + return !shouldHide; + }); fetchedActivities = fetchedActivities.concat(result); shouldFetchMore = filteredResult.length < limit && fetchedActivities.length < limit; @@ -764,6 +781,7 @@ addActionHandler('addToken', (global, actions, { token }) => { } const { balances } = selectCurrentAccountState(global) ?? {}; + if (!balances?.bySlug[token.slug]) { global = updateCurrentAccountState(global, { balances: { @@ -776,11 +794,17 @@ addActionHandler('addToken', (global, actions, { token }) => { }); } + const settings = selectCurrentAccountSettings(global); + global = updateCurrentAccountSettings(global, { + importedSlugs: [...settings?.importedSlugs ?? [], token.slug], + }); + const accountSettings = selectCurrentAccountSettings(global) ?? {}; global = updateCurrentAccountSettings(global, { ...accountSettings, orderedSlugs: [...accountSettings.orderedSlugs ?? [], token.slug], - exceptionSlugs: unique([...accountSettings.exceptionSlugs ?? [], token.slug]), + alwaysShownSlugs: unique([...accountSettings.alwaysShownSlugs ?? [], token.slug]), + alwaysHiddenSlugs: accountSettings.alwaysHiddenSlugs?.filter((slug) => slug !== token.slug), deletedSlugs: accountSettings.deletedSlugs?.filter((slug) => slug !== token.slug), }); @@ -860,7 +884,7 @@ addActionHandler('importToken', async (global, actions, { address }) => { }, }); if (shouldUpdateBalance) { - global = updateBalances(global, global.currentAccountId!, { [token.slug]: 0n }); + global = changeBalance(global, global.currentAccountId!, token.slug, 0n); } setGlobal(global); }); diff --git a/src/global/actions/apiUpdates/activities.ts b/src/global/actions/apiUpdates/activities.ts index 2ecdf6cb..483d3ff9 100644 --- a/src/global/actions/apiUpdates/activities.ts +++ b/src/global/actions/apiUpdates/activities.ts @@ -5,6 +5,7 @@ import { IS_CAPACITOR, TONCOIN } from '../../../config'; import { groupBy } from '../../../util/iteratees'; import { callActionInMain, callActionInNative } from '../../../util/multitab'; import { playIncomingTransactionSound } from '../../../util/notificationSound'; +import { getIsTransactionWithPoisoning } from '../../../util/poisoningHash'; import { getIsTonToken } from '../../../util/tokens'; import { IS_DELEGATED_BOTTOM_SHEET, IS_DELEGATING_BOTTOM_SHEET } from '../../../util/windowEnvironment'; import { getIsTinyOrScamTransaction, getRealTxIdFromLocal } from '../../helpers'; @@ -126,7 +127,8 @@ addActionHandler('apiUpdate', (global, actions, update) => { && !( global.settings.areTinyTransfersHidden && getIsTinyOrScamTransaction(activity, global.tokenInfo?.bySlug[activity.slug!]) - ); + ) + && !getIsTransactionWithPoisoning(activity); }); if (shouldPlaySound) { diff --git a/src/global/actions/apiUpdates/initial.ts b/src/global/actions/apiUpdates/initial.ts index 4fbbead2..96addb29 100644 --- a/src/global/actions/apiUpdates/initial.ts +++ b/src/global/actions/apiUpdates/initial.ts @@ -29,7 +29,7 @@ import { selectAccountState, selectVestingPartsReadyToUnfreeze } from '../../sel addActionHandler('apiUpdate', (global, actions, update) => { switch (update.type) { case 'updateBalances': { - global = updateBalances(global, update.accountId, update.balancesToUpdate); + global = updateBalances(global, update.accountId, update.chain, update.balances); setGlobal(global); break; } diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index fa1b6609..06dad446 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -390,18 +390,11 @@ addActionHandler('closeSecurityWarning', (global) => { }); addActionHandler('toggleTokensWithNoCost', (global, actions, { isEnabled }) => { - global = updateSettings(global, { areTokensWithNoCostHidden: isEnabled }); - - const accountSettings = selectCurrentAccountSettings(global) ?? {}; - global = updateCurrentAccountSettings(global, { ...accountSettings, exceptionSlugs: [] }); - - return global; + return updateSettings(global, { areTokensWithNoCostHidden: isEnabled }); }); addActionHandler('toggleSortByValue', (global, actions, { isEnabled }) => { - return updateSettings(global, { - isSortByValueEnabled: isEnabled, - }); + return updateSettings(global, { isSortByValueEnabled: isEnabled }); }); addActionHandler('updateOrderedSlugs', (global, actions, { orderedSlugs }) => { @@ -412,21 +405,24 @@ addActionHandler('updateOrderedSlugs', (global, actions, { orderedSlugs }) => { }); }); -addActionHandler('toggleExceptionToken', (global, actions, { slug }) => { +addActionHandler('toggleTokenVisibility', (global, actions, { slug, shouldShow }) => { const accountSettings = selectCurrentAccountSettings(global) ?? {}; - const { exceptionSlugs = [] } = accountSettings; - const exceptionSlugsCopy = exceptionSlugs.slice(); - const slugIndexInAvailable = exceptionSlugsCopy.indexOf(slug); + const { alwaysShownSlugs = [], alwaysHiddenSlugs = [] } = accountSettings; + const alwaysShownSlugsSet = new Set(alwaysShownSlugs); + const alwaysHiddenSlugsSet = new Set(alwaysHiddenSlugs); - if (slugIndexInAvailable !== -1) { - exceptionSlugsCopy.splice(slugIndexInAvailable, 1); + if (shouldShow) { + alwaysHiddenSlugsSet.delete(slug); + alwaysShownSlugsSet.add(slug); } else { - exceptionSlugsCopy.push(slug); + alwaysShownSlugsSet.delete(slug); + alwaysHiddenSlugsSet.add(slug); } return updateCurrentAccountSettings(global, { ...accountSettings, - exceptionSlugs: exceptionSlugsCopy, + alwaysHiddenSlugs: Array.from(alwaysHiddenSlugsSet), + alwaysShownSlugs: Array.from(alwaysShownSlugsSet), }); }); @@ -435,8 +431,10 @@ addActionHandler('deleteToken', (global, actions, { slug }) => { return updateCurrentAccountSettings(global, { ...accountSettings, orderedSlugs: accountSettings.orderedSlugs?.filter((s) => s !== slug), - exceptionSlugs: accountSettings.exceptionSlugs?.filter((s) => s !== slug), + alwaysHiddenSlugs: accountSettings.alwaysHiddenSlugs?.filter((s) => s !== slug), + alwaysShownSlugs: accountSettings.alwaysShownSlugs?.filter((s) => s !== slug), deletedSlugs: [...accountSettings.deletedSlugs ?? [], slug], + importedSlugs: accountSettings.importedSlugs?.filter((s) => s !== slug), }); }); @@ -617,6 +615,11 @@ addActionHandler('closeMediaViewer', (global) => { }); addActionHandler('openReceiveModal', (global) => { + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('openReceiveModal'); + return; + } + setGlobal({ ...global, isReceiveModalOpen: true }); }); diff --git a/src/global/cache.ts b/src/global/cache.ts index f11e66b5..fa0b4876 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -19,6 +19,7 @@ import { bigintReviver } from '../util/bigint'; import { cloneDeep, mapValues, pick, pickTruthy, } from '../util/iteratees'; +import { clearPoisoningCache, updatePoisoningCache } from '../util/poisoningHash'; import { onBeforeUnload, throttle } from '../util/schedulers'; import { IS_ELECTRON } from '../util/windowEnvironment'; import { getIsTxIdLocal } from './helpers'; @@ -46,6 +47,9 @@ export function initCache() { addActionHandler('afterSignOut', (global, actions, payload) => { const { isFromAllAccounts } = payload || {}; + + clearPoisoningCache(); + if (!isFromAllAccounts) return; preloadedData = pick(global, ['swapTokenInfo', 'tokenInfo', 'restrictions']); @@ -101,6 +105,7 @@ export function loadCache(initialState: GlobalState): GlobalState { if (cached) { try { migrateCache(cached, initialState); + loadMemoryCache(cached); } catch (err) { // eslint-disable-next-line no-console console.error(err); @@ -432,9 +437,43 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) { cached.stateVersion = 28; } + if (cached.stateVersion === 28) { + const accountIds = Object.keys(cached.settings.byAccountId); + for (const accountId of accountIds) { + const exceptionSlugs = (cached.settings.byAccountId[accountId] as any).exceptionSlugs as string[] | undefined; + if (cached.settings.areTokensWithNoCostHidden) { + cached.settings.byAccountId[accountId].alwaysShownSlugs = exceptionSlugs; + } else { + cached.settings.byAccountId[accountId].alwaysHiddenSlugs = exceptionSlugs; + } + } + cached.stateVersion = 29; + } + // When adding migration here, increase `STATE_VERSION` } +function loadMemoryCache(cached: GlobalState) { + if (!cached.currentAccountId) return; + + const { byId, newestTransactionsBySlug } = cached.byAccountId[cached.currentAccountId].activities || {}; + + if (byId) { + Object.values(byId).forEach((tx) => { + if (tx.kind === 'transaction' && tx.isIncoming) { + updatePoisoningCache(tx); + } + }); + } + if (newestTransactionsBySlug) { + Object.values(newestTransactionsBySlug).forEach((tx) => { + if (tx.isIncoming) { + updatePoisoningCache(tx); + } + }); + } +} + function updateCache(force?: boolean) { if (GLOBAL_STATE_CACHE_DISABLED || !isCaching || (!force && getIsHeavyAnimating())) { return; diff --git a/src/global/initialState.ts b/src/global/initialState.ts index a8cb7c4f..a2d23e46 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 = 28; +export const STATE_VERSION = 29; export const INITIAL_STATE: GlobalState = { appState: AppState.Auth, diff --git a/src/global/reducers/misc.ts b/src/global/reducers/misc.ts index 19aca222..43f7c0ed 100644 --- a/src/global/reducers/misc.ts +++ b/src/global/reducers/misc.ts @@ -1,12 +1,20 @@ import type { - ApiChain, ApiMaybeBalanceBySlug, ApiSwapAsset, ApiTokenWithPrice, + ApiBalanceBySlug, + ApiChain, + ApiSwapAsset, + ApiTokenWithPrice, } from '../../api/types'; import type { Account, AccountState, GlobalState } from '../types'; import { POPULAR_WALLET_VERSIONS, TON_USDT_SLUG, TONCOIN } from '../../config'; import isPartialDeepEqual from '../../util/isPartialDeepEqual'; +import { getChainBySlug } from '../../util/tokens'; import { - selectAccount, selectAccountState, selectCurrentNetwork, selectNetworkAccounts, + selectAccount, + selectAccountSettings, + selectAccountState, + selectCurrentNetwork, + selectNetworkAccounts, } from '../selectors'; export function updateAuth(global: GlobalState, authUpdate: Partial) { @@ -107,36 +115,40 @@ export function renameAccount(global: GlobalState, accountId: string, title: str export function updateBalances( global: GlobalState, accountId: string, - balancesToUpdate: ApiMaybeBalanceBySlug, + chain: ApiChain, + chainBalances: ApiBalanceBySlug, ): GlobalState { - if (Object.keys(balancesToUpdate).length === 0) { - return global; - } - - const { balances } = selectAccountState(global, accountId) || {}; - - const updatedBalancesBySlug = { ...(balances?.bySlug || {}) }; + const balances: ApiBalanceBySlug = { ...chainBalances }; + const currentBalances = selectAccountState(global, accountId)?.balances?.bySlug ?? {}; + const importedSlugs = selectAccountSettings(global, accountId)?.importedSlugs ?? []; - for (const [slug, balance] of Object.entries(balancesToUpdate)) { - if (balance === undefined) { - if (updatedBalancesBySlug[slug]) { - updatedBalancesBySlug[slug] = 0n; - } - continue; + for (const [slug, balance] of Object.entries(currentBalances)) { + if (getChainBySlug(slug) !== chain) { + balances[slug] = balance; } - - updatedBalancesBySlug[slug] = balance; } - // Force balance value for USDT in Tonchain - if (!(TON_USDT_SLUG in updatedBalancesBySlug)) { - updatedBalancesBySlug[TON_USDT_SLUG] = 0n; + // Force balance value for USDT-TON and manual imported tokens + for (const slug of [...importedSlugs, TON_USDT_SLUG]) { + if (!(slug in balances)) { + balances[slug] = 0n; + } } return updateAccountState(global, accountId, { balances: { - ...balances, - bySlug: updatedBalancesBySlug, + bySlug: balances, + }, + }); +} + +export function changeBalance(global: GlobalState, accountId: string, slug: string, balance: bigint) { + return updateAccountState(global, accountId, { + balances: { + bySlug: { + ...selectAccountState(global, accountId)?.balances?.bySlug, + [slug]: balance, + }, }, }); } diff --git a/src/global/selectors/tokens.ts b/src/global/selectors/tokens.ts index 068f93a3..9bf0148b 100644 --- a/src/global/selectors/tokens.ts +++ b/src/global/selectors/tokens.ts @@ -44,16 +44,16 @@ const selectAccountTokensMemoizedFor = withCache((accountId: string) => memoize( const balanceBig = toBig(balance, decimals); const totalValue = balanceBig.mul(price).round(decimals).toString(); const hasCost = balanceBig.mul(priceUsd ?? 0).gte(TINY_TRANSFER_MAX_COST); - const isExcepted = accountSettings.exceptionSlugs?.includes(slug); - const isMycoinWithBalance = slug === MYCOIN_SLUG && balance; - const isDisabled = !( + + const isEnabled = ( ENABLED_TOKEN_SLUGS.includes(slug) - || (areTokensWithNoCostHidden && hasCost && !isExcepted) - || (areTokensWithNoCostHidden && !hasCost && !isMycoinWithBalance && isExcepted) - || (areTokensWithNoCostHidden && !hasCost && isMycoinWithBalance && !isExcepted) - || (!areTokensWithNoCostHidden && !isExcepted) + || !areTokensWithNoCostHidden + || (areTokensWithNoCostHidden && hasCost) + || accountSettings.alwaysShownSlugs?.includes(slug) ); + const isDisabled = !isEnabled || accountSettings.alwaysHiddenSlugs?.includes(slug); + return { chain, symbol, diff --git a/src/global/types.ts b/src/global/types.ts index 7f99c310..5b203719 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -372,8 +372,10 @@ export interface AccountState { export interface AccountSettings { orderedSlugs?: string[]; - exceptionSlugs?: string[]; + alwaysShownSlugs?: string[]; + alwaysHiddenSlugs?: string[]; deletedSlugs?: string[]; + importedSlugs?: string[]; } export interface SavedAddress { @@ -849,7 +851,7 @@ export interface ActionPayloads { toggleSortByValue: { isEnabled: boolean }; updateOrderedSlugs: { orderedSlugs: string[] }; rebuildOrderedSlugs: undefined; - toggleExceptionToken: { slug: string }; + toggleTokenVisibility: { slug: string; shouldShow: boolean }; addToken: { token: UserToken }; deleteToken: { slug: string }; importToken: { address: string; isSwap?: boolean }; diff --git a/src/i18n/de.yaml b/src/i18n/de.yaml index f79b59c0..e9955561 100644 --- a/src/i18n/de.yaml +++ b/src/i18n/de.yaml @@ -482,7 +482,6 @@ You can connect your biometric data for more convenience: Sie können Ihre biome Connect Touch ID: Touch ID verbinden Connect Face ID: Face ID verbinden Not Now: Nicht jetzt -Biometric Authentification: Biometrische Authentifizierung Touch ID: Touch ID Face ID: Face ID Turn Off Touch ID: Touch ID deaktivieren @@ -684,5 +683,6 @@ Never: Niemals 3 minutes: 3 Minuten 30 minutes: 30 Minuten Unlock: Entsperren -Biometric authentification failed: Biometrische Authentifizierung fehlgeschlagen +This address mimics another address that you previously interacted with.: Diese Adresse imitiert eine andere Adresse, mit der Sie zuvor interagiert haben. +Biometric authentication failed: Biometrische Authentifizierung fehlgeschlagen Reset biometrics in security settings, or use a passcode.: Setzen Sie die Biometrie in den Sicherheitseinstellungen zurück oder verwenden Sie einen Zugangscode. diff --git a/src/i18n/en.yaml b/src/i18n/en.yaml index 28bba5e6..053fc2c2 100644 --- a/src/i18n/en.yaml +++ b/src/i18n/en.yaml @@ -481,7 +481,6 @@ You can connect your biometric data for more convenience: You can connect your b Connect Touch ID: Connect Touch ID Connect Face ID: Connect Face ID Not Now: Not Now -Biometric Authentification: Biometric Authentification Touch ID: Touch ID Face ID: Face ID Turn Off Touch ID: Turn Off Touch ID @@ -684,5 +683,6 @@ Never: Never 3 minutes: 3 minutes 30 minutes: 30 minutes Unlock: Unlock -Biometric authentification failed: Biometric authentification failed +This address mimics another address that you previously interacted with.: This address mimics another address that you previously interacted with. +Biometric authentication failed: Biometric authentication failed Reset biometrics in security settings, or use a passcode.: Reset biometrics in security settings, or use a passcode. diff --git a/src/i18n/es.yaml b/src/i18n/es.yaml index fbcd09d5..039fb678 100644 --- a/src/i18n/es.yaml +++ b/src/i18n/es.yaml @@ -480,7 +480,6 @@ You can connect your biometric data for more convenience: Puede conectar sus dat Connect Touch ID: Conectar Touch ID Connect Face ID: Conectar Face ID Not Now: Ahora no -Biometric Authentification: Autenticación biométrica Touch ID: Touch ID Face ID: Face ID Turn Off Touch ID: Desactivar Touch ID @@ -682,5 +681,6 @@ Never: Nunca 3 minutes: 3 minutos 30 minutes: 30 minutos Unlock: Desbloquear -Biometric authentification failed: Fallo en la autenticación biométrica +This address mimics another address that you previously interacted with.: Esta dirección imita otra dirección con la que has interactuado anteriormente. +Biometric authentication failed: Fallo en la autenticación biométrica Reset biometrics in security settings, or use a passcode.: Restablezca la biometría en la configuración de seguridad, o use un código de acceso. diff --git a/src/i18n/pl.yaml b/src/i18n/pl.yaml index 24be3905..f2f659b2 100644 --- a/src/i18n/pl.yaml +++ b/src/i18n/pl.yaml @@ -485,7 +485,6 @@ You can connect your biometric data for more convenience: Możesz połączyć sw Connect Touch ID: Połącz Touch ID Connect Face ID: Połącz Face ID Not Now: Nie teraz -Biometric Authentification: Autentykacja biometryczna Touch ID: Touch ID Face ID: Face ID Turn Off Touch ID: Wyłącz Touch ID @@ -689,5 +688,6 @@ Never: Nigdy 3 minutes: 3 minuty 30 minutes: 30 minut Unlock: Odblokować -Biometric authentification failed: Autoryzacja biometryczna nie powiodła się +This address mimics another address that you previously interacted with.: Ten adres imituje inny adres, z którym wcześniej miałeś kontakt. +Biometric authentication failed: Niepowodzenie uwierzytelniania biometrycznego Reset biometrics in security settings, or use a passcode.: Zresetuj biometrię w ustawieniach zabezpieczeń lub użyj kodu dostępu. diff --git a/src/i18n/ru.yaml b/src/i18n/ru.yaml index b6977d57..8ed582ce 100644 --- a/src/i18n/ru.yaml +++ b/src/i18n/ru.yaml @@ -437,7 +437,7 @@ Enabling biometric confirmation will reset the password.: Включение б Biometric Registration: Подключение биометрии Step 1 of 2. Registration: Шаг 1 из 2. Регистрация Step 2 of 2. Verification: Шаг 2 из 2. Проверка -Download Logs: Загрузить журнал +Download Logs: Загрузить журнал Biometric setup failed.: Настроить биометрию не удалось. Biometric confirmation failed.: Подтвердить биометрию не удалось. Failed to disable biometrics.: Не удалось отключить биометрию. @@ -484,7 +484,6 @@ You can connect your biometric data for more convenience: Для большег Connect Touch ID: Подключить Touch ID Connect Face ID: Подключить Face ID Not Now: Не сейчас -Biometric Authentification: Биометрическая аутентификация Touch ID: Touch ID Face ID: Face ID Turn Off Touch ID: Отключить Touch ID @@ -683,5 +682,6 @@ Never: Никогда 3 minutes: 3 минуты 30 minutes: 30 минут Unlock: Разблокировать -Biometric authentification failed: Ошибка биометрической аутентификации +This address mimics another address that you previously interacted with.: Этот адрес имитирует другой адрес, с которым вы ранее взаимодействовали. +Biometric authentication failed: Ошибка биометрической аутентификации Reset biometrics in security settings, or use a passcode.: Сбросьте биометрию в настройках безопасности или используйте пароль. diff --git a/src/i18n/th.yaml b/src/i18n/th.yaml index 804f1d55..210ef44d 100644 --- a/src/i18n/th.yaml +++ b/src/i18n/th.yaml @@ -482,7 +482,6 @@ You can connect your biometric data for more convenience: คุณสามา Connect Touch ID: เชื่อมต่อ Touch ID Connect Face ID: เชื่อมต่อ Face ID Not Now: ไม่ตอนนี้ -Biometric Authentification: รับรองความปลอดภัยด้วย Biometric Touch ID: Touch ID Face ID: Face ID Turn Off Touch ID: ปิดการใช้งาน Touch ID @@ -686,5 +685,6 @@ Never: ไม่เคย 3 minutes: 3 นาที 30 minutes: 30 นาที Unlock: ปลดล็อก -Biometric authentification failed: การตรวจสอบอัตลักษณ์ทางชีวภาพล้มเหลว +This address mimics another address that you previously interacted with.: ที่อยู่นี้เลียนแบบที่อยู่อื่นที่คุณเคยโต้ตอบด้วย +Biometric authentication failed: การยืนยันตัวตนด้วยลายนิ้วมือล้มเหลว Reset biometrics in security settings, or use a passcode.: รีเซ็ตการตรวจสอบอัตลักษณ์ทางชีวภาพในการตั้งค่าความปลอดภัย หรือใช้รหัสผ่าน diff --git a/src/i18n/tr.yaml b/src/i18n/tr.yaml index 1c8018b9..b1b8dbf5 100644 --- a/src/i18n/tr.yaml +++ b/src/i18n/tr.yaml @@ -481,7 +481,6 @@ You can connect your biometric data for more convenience: Daha kolay kullanım i Connect Touch ID: Touch ID'yi Bağlayın Connect Face ID: Face ID'yi bağlayın Not Now: Şimdi değil -Biometric Authentification: Biyometrik Kimlik Doğrulama Touch ID: Touch ID Face ID: Face ID Turn Off Touch ID: Touch ID kapatılsın mı @@ -682,5 +681,6 @@ Never: Asla 30 seconds: 30 saniye 3 minutes: 3 dakika 30 minutes: 30 dakika -Biometric authentification failed: Biyometrik kimlik doğrulama başarısız oldu +This address mimics another address that you previously interacted with.: Bu adres, daha önce etkileşimde bulunduğunuz başka bir adresi taklit ediyor. +Biometric authentication failed: Biyometrik kimlik doğrulama başarısız oldu Reset biometrics in security settings, or use a passcode.: Güvenlik ayarlarında biyometrik kimlik doğrulamayı sıfırlayın veya bir şifre kullanın. diff --git a/src/i18n/uk.yaml b/src/i18n/uk.yaml index 32605710..eb5e333e 100644 --- a/src/i18n/uk.yaml +++ b/src/i18n/uk.yaml @@ -486,7 +486,6 @@ You can connect your biometric data for more convenience: Для більшої Connect Touch ID: Підключити Touch ID Connect Face ID: Підключити Face ID Not Now: Не зараз -Biometric Authentification: Біометрична аутентифікація Touch ID: Touch ID Face ID: Face ID Turn Off Touch ID: Відключити Touch ID @@ -688,5 +687,6 @@ Never: Ніколи 3 minutes: 3 хвилини 30 minutes: 30 хвилин Unlock: Розблокувати -Biometric authentification failed: Біометрична аутентифікація не вдалася +This address mimics another address that you previously interacted with.: Ця адреса імітує іншу адресу, з якою ви раніше взаємодіяли. +Biometric authentication failed: Біометрична аутентифікація не вдалася Reset biometrics in security settings, or use a passcode.: Скиньте біометрію в налаштуваннях безпеки або використовуйте код доступу. diff --git a/src/i18n/zh-Hans.yaml b/src/i18n/zh-Hans.yaml index 359b67c8..ff655eed 100644 --- a/src/i18n/zh-Hans.yaml +++ b/src/i18n/zh-Hans.yaml @@ -469,7 +469,6 @@ You can connect your biometric data for more convenience: 您可以连接您的 Connect Touch ID: 连接 Touch ID Connect Face ID: 连接 Face ID Not Now: 现在不要 -Biometric Authentification: 生物识别认证 Touch ID: Touch ID Face ID: Face ID Turn Off Touch ID: 关闭 Touch ID @@ -670,5 +669,6 @@ Never: 从不 3 minutes: 3分钟 30 minutes: 30分钟 Unlock: 解锁 -Biometric authentification failed: 生物识别认证失败 +This address mimics another address that you previously interacted with.: 此地址模仿了您之前交互过的另一个地址。 +Biometric authentication failed: 生物识别认证失败 Reset biometrics in security settings, or use a passcode.: 在安全设置中重置生物识别,或使用密码。 diff --git a/src/i18n/zh-Hant.yaml b/src/i18n/zh-Hant.yaml index 8f6420e4..487b1a04 100644 --- a/src/i18n/zh-Hant.yaml +++ b/src/i18n/zh-Hant.yaml @@ -469,7 +469,6 @@ You can connect your biometric data for more convenience: 您可以連接您的 Connect Touch ID: 連接 Touch ID Connect Face ID: 連接 Face ID Not Now: 現在不要 -Biometric Authentification: 生物辨識認證 Touch ID: Touch ID Face ID: Face ID Turn Off Touch ID: 關閉 Touch ID @@ -670,5 +669,6 @@ Never: 從不 3 minutes: 3分鐘 30 minutes: 30分鐘 Unlock: 解鎖 -Biometric authentification failed: 生物識別認證失敗 +This address mimics another address that you previously interacted with.: 此地址模仿了您之前互動過的另一個地址。 +Biometric authentication failed: 生物識別認證失敗 Reset biometrics in security settings, or use a passcode.: 在安全設定中重置生物識別,或使用密碼。 diff --git a/src/util/PostMessageConnector.ts b/src/util/PostMessageConnector.ts index 54a067a4..83d441ee 100644 --- a/src/util/PostMessageConnector.ts +++ b/src/util/PostMessageConnector.ts @@ -60,7 +60,7 @@ export type WorkerMessageData = { } | { channel?: string; type: 'unhandledError'; - error?: { message: string }; + error?: { message: string; stack?: string }; }; export interface WorkerMessageEvent { @@ -195,7 +195,11 @@ class ConnectorClass { const requestState = requestStates.get(data.messageId); requestState?.callback?.(...data.callbackArgs); } else if (data.type === 'unhandledError') { - throw new Error(data.error?.message); + const error = new Error(data.error?.message); + if (data.error?.stack) { + error.stack = data.error.stack; + } + throw error; } } diff --git a/src/util/createPostMessageInterface.ts b/src/util/createPostMessageInterface.ts index 8d0a47c4..70328c7d 100644 --- a/src/util/createPostMessageInterface.ts +++ b/src/util/createPostMessageInterface.ts @@ -199,12 +199,26 @@ function handleErrors(sendToOrigin: SendToOrigin) { self.onerror = (e) => { const message = e.error?.message || 'Uncaught exception in worker'; logDebugError(message, e.error); - sendToOrigin({ type: 'unhandledError', error: { message } }); + + sendToOrigin({ + type: 'unhandledError', + error: { + message, + stack: e.error?.stack, + }, + }); }; self.addEventListener('unhandledrejection', (e) => { - const message = e.reason?.message || 'Uncaught exception in worker'; + const message = e.reason?.message || 'Unhandled rejection in worker'; logDebugError(message, e.reason); - sendToOrigin({ type: 'unhandledError', error: { message } }); + + sendToOrigin({ + type: 'unhandledError', + error: { + message, + stack: e.reason?.stack, + }, + }); }); } diff --git a/src/util/environment.ts b/src/util/environment.ts new file mode 100644 index 00000000..46538af8 --- /dev/null +++ b/src/util/environment.ts @@ -0,0 +1,5 @@ +import { IS_EXTENSION } from '../config'; + +export const IS_EXTENSION_PAGE_SCRIPT = IS_EXTENSION + // eslint-disable-next-line no-restricted-globals + && !['chrome-extension:', 'moz-extension:'].includes(self.location.protocol); diff --git a/src/util/handleError.ts b/src/util/handleError.ts index 3a3497e2..575bb7a6 100644 --- a/src/util/handleError.ts +++ b/src/util/handleError.ts @@ -1,11 +1,14 @@ import { APP_ENV, DEBUG_ALERT_MSG } from '../config'; +import { IS_EXTENSION_PAGE_SCRIPT } from './environment'; import { logDebugError } from './logs'; import { throttle } from './schedulers'; -const noop = () => { -}; +const shouldShowAlert = (APP_ENV === 'development' || APP_ENV === 'staging') + && typeof window === 'object' + && !IS_EXTENSION_PAGE_SCRIPT; -const throttledAlert = typeof window !== 'undefined' ? throttle(window.alert, 1000) : noop; +// eslint-disable-next-line no-alert +const throttledAlert = throttle((message) => window.alert(message), 1000); // eslint-disable-next-line no-restricted-globals self.addEventListener('error', handleErrorEvent); @@ -24,7 +27,7 @@ function handleErrorEvent(e: ErrorEvent | PromiseRejectionEvent) { } export function handleError(err: Error | string) { - logDebugError('Unhadled UI Error', err); + logDebugError('handleError', err); const message = typeof err === 'string' ? err : err.message; const stack = typeof err === 'object' ? err.stack : undefined; @@ -33,7 +36,7 @@ export function handleError(err: Error | string) { return; } - if (APP_ENV === 'development' || APP_ENV === 'staging') { + if (shouldShowAlert) { throttledAlert(`${DEBUG_ALERT_MSG}\n\n${(message) || err}\n${stack}`); } } diff --git a/src/util/ledger/index.ts b/src/util/ledger/index.ts index 28260f93..7e9e960a 100644 --- a/src/util/ledger/index.ts +++ b/src/util/ledger/index.ts @@ -324,8 +324,7 @@ export async function submitLedgerTransfer( let { toAddress, amount } = options; const { network } = parseAccountId(accountId); - await callApi('waitLastTonTransfer', accountId); - + const pendingTransferId = await callApi('waitAndCreateTonPendingTransfer', accountId); const fromAddress = await callApi('fetchAddress', accountId, 'ton'); const [path, walletInfo, appInfo, account] = await Promise.all([ @@ -402,8 +401,10 @@ export async function submitLedgerTransfer( }, }; - return await callApi('sendSignedTransferMessage', accountId, message); + return await callApi('sendSignedTransferMessage', accountId, message, pendingTransferId!); } catch (err: any) { + await callApi('cancelPendingTransfer', pendingTransferId!); + handleLedgerErrors(err); logDebugError('submitLedgerTransfer', err); return undefined; @@ -425,7 +426,7 @@ export async function submitLedgerNftTransfer(options: { let { toAddress } = options; const { network } = parseAccountId(accountId); - await callApi('waitLastTonTransfer', accountId); + const pendingTransferId = await callApi('waitAndCreateTonPendingTransfer', accountId); const fromAddress = await callApi('fetchAddress', accountId, 'ton'); @@ -498,8 +499,10 @@ export async function submitLedgerNftTransfer(options: { }, }; - return await callApi('sendSignedTransferMessage', accountId, message); + return await callApi('sendSignedTransferMessage', accountId, message, pendingTransferId!); } catch (error) { + await callApi('cancelPendingTransfer', pendingTransferId!); + logDebugError('submitLedgerNftTransfer', error); return undefined; } @@ -574,8 +577,6 @@ export async function signLedgerTransactions(accountId: string, messages: ApiDap }): Promise { const { isTonConnect, vestingAddress } = options ?? {}; - await callApi('waitLastTonTransfer', accountId); - const { network } = parseAccountId(accountId); const [path, appInfo, account] = await Promise.all([ diff --git a/src/util/poisoningHash.ts b/src/util/poisoningHash.ts new file mode 100644 index 00000000..7dfd2a54 --- /dev/null +++ b/src/util/poisoningHash.ts @@ -0,0 +1,52 @@ +import type { ApiTransaction } from '../api/types'; + +import { TRANSACTION_ADDRESS_SHIFT } from '../config'; +import { shortenAddress } from './shortenAddress'; + +const cache: Map = new Map(); + +function getKey(address: string) { + return shortenAddress(address, TRANSACTION_ADDRESS_SHIFT)!; +} + +function addToCache(address: string, amount: bigint, timestamp: number) { + const key = getKey(address); + + cache.set(key, { + address, + amount, + timestamp, + }); +} + +function getFromCache(address: string) { + const key = getKey(address); + + return cache.get(key); +} + +export function updatePoisoningCache(tx: ApiTransaction) { + const { fromAddress: address, amount, timestamp } = tx; + + const cached = getFromCache(address); + + if (!cached || cached.timestamp < timestamp || (cached.timestamp === timestamp && cached.amount > amount)) { + addToCache(address, amount, timestamp); + } +} + +export function getIsTransactionWithPoisoning(tx: ApiTransaction) { + const { fromAddress: address } = tx; + + const cached = getFromCache(address); + + return cached && cached.address !== address; +} + +export function clearPoisoningCache() { + cache.clear(); +} diff --git a/src/util/tokens.ts b/src/util/tokens.ts index 99336c60..2099f321 100644 --- a/src/util/tokens.ts +++ b/src/util/tokens.ts @@ -1,12 +1,14 @@ import type { ApiChain, ApiToken } from '../api/types'; -import { NATIVE_TOKENS, TONCOIN } from '../config'; +import { CHAIN_CONFIG, TONCOIN } from '../config'; import { getChainConfig } from './chain'; -const nativeTokenSlugs = new Set(NATIVE_TOKENS.map(({ slug }) => slug)); +const chainByNativeSlug = Object.fromEntries( + Object.entries(CHAIN_CONFIG).map(([chain, { nativeToken }]) => [nativeToken.slug, chain]), +) as Record; export function getIsNativeToken(slug?: string) { - return slug ? nativeTokenSlugs.has(slug) : false; + return slug ? slug in chainByNativeSlug : false; } export function getIsTonToken(slug: string, withNative?: boolean) { @@ -23,3 +25,8 @@ export function getTransactionHashFromTxId(chain: ApiChain, txId: string) { const [, transactionHash] = (txId || '').split(':'); return transactionHash; } + +export function getChainBySlug(slug: string) { + const items = slug.split('-'); + return items.length > 1 ? items[0] as ApiChain : chainByNativeSlug[slug]; +}