From 25b393b8a097de6a74d5101b095e2df4e618cc00 Mon Sep 17 00:00:00 2001 From: mytonwalletorg Date: Fri, 19 Jan 2024 17:28:45 +0100 Subject: [PATCH] v1.17.14 --- capacitor.config.ts | 1 + changelogs/1.17.14.txt | 1 + mobile/android/app/capacitor.build.gradle | 1 + mobile/android/capacitor.settings.gradle | 3 + mobile/ios/App/Podfile | 1 + mobile/ios/App/Podfile.lock | 8 +- mobile/plugins/native-bottom-sheet/README.md | 62 ---- .../native-bottom-sheet/dist/docs.json | 78 ----- .../dist/esm/definitions.d.ts | 16 - .../dist/esm/definitions.js.map | 2 +- .../ios/Plugin/BottomSheetPlugin.m | 2 - .../ios/Plugin/BottomSheetPlugin.swift | 40 +-- .../native-bottom-sheet/src/definitions.ts | 13 - package-lock.json | 288 ++++++------------ package.json | 13 +- public/electronVersion.txt | 2 +- public/fallbackScript.js | 18 ++ public/version.txt | 2 +- src/api/blockchains/ton/constants.ts | 3 + src/api/blockchains/ton/nfts.ts | 36 ++- src/api/blockchains/ton/staking.ts | 109 ++++--- src/api/blockchains/ton/tokens.ts | 16 +- src/api/blockchains/ton/transactions.ts | 63 ++-- src/api/blockchains/ton/types.ts | 6 +- src/api/blockchains/ton/util/index.ts | 4 +- src/api/blockchains/ton/util/metadata.ts | 23 +- src/api/blockchains/ton/util/tonCore.ts | 20 +- src/api/blockchains/ton/util/tonapiio.ts | 102 ++----- src/api/blockchains/ton/util/tonweb.ts | 20 +- src/api/blockchains/ton/wallet.ts | 14 +- src/api/common/helpers.ts | 2 +- src/api/common/txCallbacks.ts | 4 +- src/api/environment.ts | 2 +- src/api/extensionMethods/legacy.ts | 10 +- src/api/extensionMethods/sites.ts | 2 +- src/api/methods/polling.ts | 9 +- src/api/methods/staking.ts | 22 +- src/api/methods/swap.ts | 7 +- src/api/methods/transactions.ts | 4 +- src/api/tonConnect/index.ts | 58 ++-- src/api/types/backend.ts | 3 +- src/api/types/misc.ts | 30 +- src/api/types/payload.ts | 24 +- src/api/types/updates.ts | 24 +- src/components/App.tsx | 8 +- src/components/common/SwapResult.tsx | 4 +- src/components/common/SwapTokensInfo.tsx | 6 +- src/components/common/TokenSelector.tsx | 31 +- src/components/common/TransactionAmount.tsx | 2 +- src/components/common/TransferResult.tsx | 25 +- src/components/dapps/DappLedgerWarning.tsx | 3 +- src/components/dapps/DappTransaction.tsx | 10 +- src/components/dapps/DappTransferInitial.tsx | 18 +- src/components/ledger/LedgerSelectWallets.tsx | 6 +- .../main/modals/SwapActivityModal.tsx | 8 +- .../main/modals/TransactionModal.module.scss | 21 +- .../main/modals/TransactionModal.tsx | 14 +- src/components/main/sections/Card/Card.tsx | 16 +- .../main/sections/Card/StickyCard.tsx | 2 +- .../main/sections/Card/TokenCard.tsx | 9 +- .../Card/helpers/calculateFullBalance.ts | 29 +- .../main/sections/Content/Assets.tsx | 5 +- .../main/sections/Content/Token.tsx | 15 +- .../main/sections/Content/Transaction.tsx | 7 +- src/components/receive/InvoiceModal.tsx | 20 +- src/components/settings/SettingsTokens.tsx | 10 +- src/components/staking/StakeModal.tsx | 5 +- src/components/staking/StakingInfoContent.tsx | 16 +- src/components/staking/StakingInitial.tsx | 38 +-- src/components/staking/UnstakeModal.tsx | 16 +- src/components/swap/SwapBlockchain.tsx | 26 +- src/components/swap/SwapComplete.tsx | 4 +- src/components/swap/SwapInitial.tsx | 51 ++-- src/components/swap/SwapModal.tsx | 4 +- src/components/swap/SwapSettingsModal.tsx | 9 +- src/components/swap/SwapWaitTokens.tsx | 4 +- .../swap/components/SwapSubmitButton.tsx | 8 +- src/components/transfer/TransferComplete.tsx | 11 +- src/components/transfer/TransferConfirm.tsx | 15 +- src/components/transfer/TransferInitial.tsx | 60 ++-- src/components/transfer/TransferModal.tsx | 5 +- src/components/ui/AmountWithFeeTextField.tsx | 4 +- src/components/ui/Modal.module.scss | 1 + src/components/ui/RichNumberField.tsx | 5 +- src/components/ui/RichNumberInput.tsx | 55 ++-- src/config.ts | 9 +- src/electron/deeplink.ts | 16 +- src/electron/utils.ts | 16 + src/global/actions/api/auth.ts | 19 +- src/global/actions/api/dapps.ts | 117 +++++-- src/global/actions/api/initial.ts | 7 +- src/global/actions/api/staking.ts | 23 +- src/global/actions/api/swap.ts | 67 ++-- src/global/actions/api/wallet.ts | 28 +- src/global/actions/apiUpdates/activities.ts | 6 +- src/global/actions/apiUpdates/dapp.ts | 36 +-- src/global/actions/apiUpdates/initial.ts | 19 +- src/global/actions/ui/initial.ts | 11 +- src/global/actions/ui/misc.ts | 5 +- src/global/cache.ts | 21 +- src/global/helpers/index.ts | 20 +- src/global/initialState.ts | 2 +- src/global/reducers/misc.ts | 34 +-- src/global/selectors/index.ts | 36 ++- src/global/types.ts | 54 ++-- src/hooks/useDelegatedBottomSheet.ts | 33 +- src/hooks/useDelegatingBottomSheet.ts | 12 +- src/i18n/ru.yaml | 2 +- src/index.html | 1 + src/index.tsx | 4 +- src/styles/index.scss | 23 ++ src/util/PostMessageConnector.ts | 23 +- src/util/bigint.ts | 28 ++ src/util/calcChangeValue.ts | 8 + src/util/clipboard.ts | 13 + src/util/createPostMessageInterface.ts | 9 +- src/util/decimals.ts | 29 ++ src/util/formatNumber.ts | 34 ++- src/util/ledger/index.ts | 17 +- src/util/ledger/types.ts | 2 +- src/util/multitab.ts | 66 +++- src/util/processDeeplink.ts | 3 +- src/util/safeNumberToString.ts | 10 - src/util/schedulers.ts | 14 + src/util/shiftDecimals.ts | 9 - src/util/stringFormat.ts | 7 + src/util/ton/deeplinks.ts | 11 +- src/util/ton/formatTransferUrl.ts | 2 +- src/util/windowEnvironment.ts | 1 - 129 files changed, 1348 insertions(+), 1333 deletions(-) create mode 100644 changelogs/1.17.14.txt create mode 100644 public/fallbackScript.js create mode 100644 src/util/bigint.ts create mode 100644 src/util/decimals.ts delete mode 100644 src/util/safeNumberToString.ts delete mode 100644 src/util/shiftDecimals.ts diff --git a/capacitor.config.ts b/capacitor.config.ts index 09ba03d5..22c1aad3 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -13,6 +13,7 @@ const config: CapacitorConfig = { includePlugins: [ '@capacitor-mlkit/barcode-scanning', '@capacitor/app', + '@capacitor/clipboard', '@capacitor/dialog', '@capacitor/haptics', '@capacitor/status-bar', diff --git a/changelogs/1.17.14.txt b/changelogs/1.17.14.txt new file mode 100644 index 00000000..e13dfb47 --- /dev/null +++ b/changelogs/1.17.14.txt @@ -0,0 +1 @@ +Bug fixes diff --git a/mobile/android/app/capacitor.build.gradle b/mobile/android/app/capacitor.build.gradle index 037cb0ff..d255d6e5 100644 --- a/mobile/android/app/capacitor.build.gradle +++ b/mobile/android/app/capacitor.build.gradle @@ -11,6 +11,7 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-mlkit-barcode-scanning') implementation project(':capacitor-app') + implementation project(':capacitor-clipboard') implementation project(':capacitor-dialog') implementation project(':capacitor-haptics') implementation project(':capacitor-status-bar') diff --git a/mobile/android/capacitor.settings.gradle b/mobile/android/capacitor.settings.gradle index 9242b899..7c538fb6 100644 --- a/mobile/android/capacitor.settings.gradle +++ b/mobile/android/capacitor.settings.gradle @@ -8,6 +8,9 @@ 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-clipboard' +project(':capacitor-clipboard').projectDir = new File('../../node_modules/@capacitor/clipboard/android') + include ':capacitor-dialog' project(':capacitor-dialog').projectDir = new File('../../node_modules/@capacitor/dialog/android') diff --git a/mobile/ios/App/Podfile b/mobile/ios/App/Podfile index 8424c7cc..ed4dbc2f 100644 --- a/mobile/ios/App/Podfile +++ b/mobile/ios/App/Podfile @@ -24,6 +24,7 @@ def capacitor_pods pod 'CapacitorCordova', :path => '../../../node_modules/@capacitor/ios' pod 'CapacitorMlkitBarcodeScanning', :path => '../../../node_modules/@capacitor-mlkit/barcode-scanning' pod 'CapacitorApp', :path => '../../../node_modules/@capacitor/app' + pod 'CapacitorClipboard', :path => '../../../node_modules/@capacitor/clipboard' pod 'CapacitorDialog', :path => '../../../node_modules/@capacitor/dialog' pod 'CapacitorHaptics', :path => '../../../node_modules/@capacitor/haptics' pod 'CapacitorStatusBar', :path => '../../../node_modules/@capacitor/status-bar' diff --git a/mobile/ios/App/Podfile.lock b/mobile/ios/App/Podfile.lock index c17de57d..2b96dbf4 100644 --- a/mobile/ios/App/Podfile.lock +++ b/mobile/ios/App/Podfile.lock @@ -3,6 +3,8 @@ PODS: - CapacitorCordova - CapacitorApp (5.0.6): - Capacitor + - CapacitorClipboard (5.0.6): + - Capacitor - CapacitorCordova (5.5.1) - CapacitorDialog (5.0.6): - Capacitor @@ -83,6 +85,7 @@ PODS: DEPENDENCIES: - "Capacitor (from `../../../node_modules/@capacitor/ios`)" - "CapacitorApp (from `../../../node_modules/@capacitor/app`)" + - "CapacitorClipboard (from `../../../node_modules/@capacitor/clipboard`)" - "CapacitorCordova (from `../../../node_modules/@capacitor/ios`)" - "CapacitorDialog (from `../../../node_modules/@capacitor/dialog`)" - "CapacitorHaptics (from `../../../node_modules/@capacitor/haptics`)" @@ -115,6 +118,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/@capacitor/ios" CapacitorApp: :path: "../../../node_modules/@capacitor/app" + CapacitorClipboard: + :path: "../../../node_modules/@capacitor/clipboard" CapacitorCordova: :path: "../../../node_modules/@capacitor/ios" CapacitorDialog: @@ -139,6 +144,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Capacitor: 9da0a2415e3b6098511f8b5ffdb578d91ee79f8f CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a + CapacitorClipboard: 77edf49827ea21da2a9c05c690a4a6a4d07199c4 CapacitorCordova: e128cc7688c070ca0bfa439898a5f609da8dbcfe CapacitorDialog: 0f3c15dfe9414b83bc64aef4078f1b92bcfead26 CapacitorHaptics: 1fffc1217c7e64a472d7845be50fb0c2f7d4204c @@ -163,6 +169,6 @@ SPEC CHECKSUMS: NativeBottomSheet: edfc1f70d3517ea92e70392db8c2de5ea3e6f15e PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 -PODFILE CHECKSUM: f2e7c24dfc22bd949d0c502101768c1e911d066f +PODFILE CHECKSUM: 05d9e9b62e3a715491a0613182dd0784570842b7 COCOAPODS: 1.14.3 diff --git a/mobile/plugins/native-bottom-sheet/README.md b/mobile/plugins/native-bottom-sheet/README.md index 921a08f5..6491fdd5 100644 --- a/mobile/plugins/native-bottom-sheet/README.md +++ b/mobile/plugins/native-bottom-sheet/README.md @@ -21,13 +21,9 @@ npx cap sync * [`openSelf(...)`](#openself) * [`closeSelf(...)`](#closeself) * [`setSelfSize(...)`](#setselfsize) -* [`callActionInMain(...)`](#callactioninmain) -* [`callActionInNative(...)`](#callactioninnative) * [`openInMain(...)`](#openinmain) * [`addListener('delegate', ...)`](#addlistenerdelegate) * [`addListener('move', ...)`](#addlistenermove) -* [`addListener('callActionInMain', ...)`](#addlistenercallactioninmain) -* [`addListener('callActionInNative', ...)`](#addlistenercallactioninnative) * [`addListener('openInMain', ...)`](#addlisteneropeninmain) * [Interfaces](#interfaces) * [Type Aliases](#type-aliases) @@ -129,32 +125,6 @@ setSelfSize(options: { size: 'half' | 'full'; }) => Promise -------------------- -### callActionInMain(...) - -```typescript -callActionInMain(options: { name: string; optionsJson: string; }) => Promise -``` - -| Param | Type | -| ------------- | --------------------------------------------------- | -| **`options`** | { name: string; optionsJson: string; } | - --------------------- - - -### callActionInNative(...) - -```typescript -callActionInNative(options: { name: string; optionsJson: string; }) => Promise -``` - -| Param | Type | -| ------------- | --------------------------------------------------- | -| **`options`** | { name: string; optionsJson: string; } | - --------------------- - - ### openInMain(...) ```typescript @@ -200,38 +170,6 @@ addListener(eventName: 'move', handler: () => void) => Promise void) => Promise & PluginListenerHandle -``` - -| Param | Type | -| --------------- | ------------------------------------------------------------------------- | -| **`eventName`** | 'callActionInMain' | -| **`handler`** | (options: { name: string; optionsJson: string; }) => void | - -**Returns:** Promise<PluginListenerHandle> & PluginListenerHandle - --------------------- - - -### addListener('callActionInNative', ...) - -```typescript -addListener(eventName: 'callActionInNative', handler: (options: { name: string; optionsJson: string; }) => void) => Promise & PluginListenerHandle -``` - -| Param | Type | -| --------------- | ------------------------------------------------------------------------- | -| **`eventName`** | 'callActionInNative' | -| **`handler`** | (options: { name: string; optionsJson: string; }) => void | - -**Returns:** Promise<PluginListenerHandle> & PluginListenerHandle - --------------------- - - ### addListener('openInMain', ...) ```typescript diff --git a/mobile/plugins/native-bottom-sheet/dist/docs.json b/mobile/plugins/native-bottom-sheet/dist/docs.json index 9a766b11..a3162923 100644 --- a/mobile/plugins/native-bottom-sheet/dist/docs.json +++ b/mobile/plugins/native-bottom-sheet/dist/docs.json @@ -123,38 +123,6 @@ "complexTypes": [], "slug": "setselfsize" }, - { - "name": "callActionInMain", - "signature": "(options: { name: string; optionsJson: string; }) => Promise", - "parameters": [ - { - "name": "options", - "docs": "", - "type": "{ name: string; optionsJson: string; }" - } - ], - "returns": "Promise", - "tags": [], - "docs": "", - "complexTypes": [], - "slug": "callactioninmain" - }, - { - "name": "callActionInNative", - "signature": "(options: { name: string; optionsJson: string; }) => Promise", - "parameters": [ - { - "name": "options", - "docs": "", - "type": "{ name: string; optionsJson: string; }" - } - ], - "returns": "Promise", - "tags": [], - "docs": "", - "complexTypes": [], - "slug": "callactioninnative" - }, { "name": "openInMain", "signature": "(options: { key: BottomSheetKeys; }) => Promise", @@ -220,52 +188,6 @@ ], "slug": "addlistenermove" }, - { - "name": "addListener", - "signature": "(eventName: 'callActionInMain', handler: (options: { name: string; optionsJson: string; }) => void) => Promise & PluginListenerHandle", - "parameters": [ - { - "name": "eventName", - "docs": "", - "type": "'callActionInMain'" - }, - { - "name": "handler", - "docs": "", - "type": "(options: { name: string; optionsJson: string; }) => void" - } - ], - "returns": "Promise & PluginListenerHandle", - "tags": [], - "docs": "", - "complexTypes": [ - "PluginListenerHandle" - ], - "slug": "addlistenercallactioninmain" - }, - { - "name": "addListener", - "signature": "(eventName: 'callActionInNative', handler: (options: { name: string; optionsJson: string; }) => void) => Promise & PluginListenerHandle", - "parameters": [ - { - "name": "eventName", - "docs": "", - "type": "'callActionInNative'" - }, - { - "name": "handler", - "docs": "", - "type": "(options: { name: string; optionsJson: string; }) => void" - } - ], - "returns": "Promise & PluginListenerHandle", - "tags": [], - "docs": "", - "complexTypes": [ - "PluginListenerHandle" - ], - "slug": "addlistenercallactioninnative" - }, { "name": "addListener", "signature": "(eventName: 'openInMain', handler: (options: { key: BottomSheetKeys; }) => void) => Promise & PluginListenerHandle", diff --git a/mobile/plugins/native-bottom-sheet/dist/esm/definitions.d.ts b/mobile/plugins/native-bottom-sheet/dist/esm/definitions.d.ts index 98461a31..631bb6c0 100644 --- a/mobile/plugins/native-bottom-sheet/dist/esm/definitions.d.ts +++ b/mobile/plugins/native-bottom-sheet/dist/esm/definitions.d.ts @@ -22,14 +22,6 @@ export interface BottomSheetPlugin { setSelfSize(options: { size: 'half' | 'full'; }): Promise; - callActionInMain(options: { - name: string; - optionsJson: string; - }): Promise; - callActionInNative(options: { - name: string; - optionsJson: string; - }): Promise; openInMain(options: { key: BottomSheetKeys; }): Promise; @@ -38,14 +30,6 @@ export interface BottomSheetPlugin { globalJson: string; }) => void): Promise & PluginListenerHandle; addListener(eventName: 'move', handler: () => void): Promise & PluginListenerHandle; - addListener(eventName: 'callActionInMain', handler: (options: { - name: string; - optionsJson: string; - }) => void): Promise & PluginListenerHandle; - addListener(eventName: 'callActionInNative', handler: (options: { - name: string; - optionsJson: string; - }) => void): Promise & PluginListenerHandle; addListener(eventName: 'openInMain', handler: (options: { key: BottomSheetKeys; }) => void): Promise & PluginListenerHandle; diff --git a/mobile/plugins/native-bottom-sheet/dist/esm/definitions.js.map b/mobile/plugins/native-bottom-sheet/dist/esm/definitions.js.map index 57e2116c..16c4acc5 100644 --- a/mobile/plugins/native-bottom-sheet/dist/esm/definitions.js.map +++ b/mobile/plugins/native-bottom-sheet/dist/esm/definitions.js.map @@ -1 +1 @@ -{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import { PluginListenerHandle } from '@capacitor/core';\n\nexport type BottomSheetKeys =\n 'initial'\n | 'receive'\n | 'invoice'\n | 'transfer'\n | 'swap'\n | 'stake'\n | 'unstake'\n | 'staking-info'\n | 'transaction-info'\n | 'swap-activity'\n | 'backup'\n | 'add-account'\n | 'settings'\n | 'qr-scanner'\n | 'dapp-connect'\n | 'dapp-transaction'\n | 'disclaimer'\n | 'backup-warning';\n\nexport interface BottomSheetPlugin {\n prepare(): Promise;\n\n applyScrollPatch(): Promise;\n\n clearScrollPatch(): Promise;\n\n delegate(options: { key: BottomSheetKeys, globalJson: string }): Promise;\n\n release(options: { key: BottomSheetKeys | '*' }): Promise;\n\n openSelf(options: { key: BottomSheetKeys, height: string, backgroundColor: string }): Promise;\n\n closeSelf(options: { key: BottomSheetKeys }): Promise;\n\n setSelfSize(options: { size: 'half' | 'full' }): Promise;\n\n callActionInMain(options: { name: string, optionsJson: string }): Promise;\n\n callActionInNative(options: { name: string, optionsJson: string }): Promise;\n\n openInMain(options: { key: BottomSheetKeys }): Promise;\n\n addListener(\n eventName: 'delegate',\n handler: (options: { key: BottomSheetKeys, globalJson: string }) => void,\n ): Promise & PluginListenerHandle;\n\n addListener(\n eventName: 'move',\n handler: () => void,\n ): Promise & PluginListenerHandle;\n\n addListener(\n eventName: 'callActionInMain',\n handler: (options: { name: string, optionsJson: string }) => void,\n ): Promise & PluginListenerHandle;\n\n addListener(\n eventName: 'callActionInNative',\n handler: (options: { name: string, optionsJson: string }) => void,\n ): Promise & PluginListenerHandle;\n\n addListener(\n eventName: 'openInMain',\n handler: (options: { key: BottomSheetKeys }) => void,\n ): Promise & PluginListenerHandle;\n}\n"]} \ No newline at end of file +{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import { PluginListenerHandle } from '@capacitor/core';\n\nexport type BottomSheetKeys =\n 'initial'\n | 'receive'\n | 'invoice'\n | 'transfer'\n | 'swap'\n | 'stake'\n | 'unstake'\n | 'staking-info'\n | 'transaction-info'\n | 'swap-activity'\n | 'backup'\n | 'add-account'\n | 'settings'\n | 'qr-scanner'\n | 'dapp-connect'\n | 'dapp-transaction'\n | 'disclaimer'\n | 'backup-warning';\n\nexport interface BottomSheetPlugin {\n prepare(): Promise;\n\n applyScrollPatch(): Promise;\n\n clearScrollPatch(): Promise;\n\n delegate(options: { key: BottomSheetKeys, globalJson: string }): Promise;\n\n release(options: { key: BottomSheetKeys | '*' }): Promise;\n\n openSelf(options: { key: BottomSheetKeys, height: string, backgroundColor: string }): Promise;\n\n closeSelf(options: { key: BottomSheetKeys }): Promise;\n\n setSelfSize(options: { size: 'half' | 'full' }): Promise;\n\n openInMain(options: { key: BottomSheetKeys }): Promise;\n\n addListener(\n eventName: 'delegate',\n handler: (options: { key: BottomSheetKeys, globalJson: string }) => void,\n ): Promise & PluginListenerHandle;\n\n addListener(\n eventName: 'move',\n handler: () => void,\n ): Promise & PluginListenerHandle;\n\n\n addListener(\n eventName: 'openInMain',\n handler: (options: { key: BottomSheetKeys }) => void,\n ): Promise & PluginListenerHandle;\n}\n"]} \ No newline at end of file diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.m b/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.m index 5083847e..f18a1f37 100644 --- a/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.m +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.m @@ -10,8 +10,6 @@ CAP_PLUGIN_METHOD(openSelf, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(closeSelf, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(setSelfSize, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(callActionInMain, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(callActionInNative, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(openInMain, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(applyScrollPatch, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(clearScrollPatch, CAPPluginReturnPromise); diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.swift b/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.swift index 725e38a3..eb23bf38 100644 --- a/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.swift +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.swift @@ -60,7 +60,7 @@ public class BottomSheetPlugin: CAPPlugin, FloatingPanelControllerDelegate { } } } - + @objc func applyScrollPatch(_ call: CAPPluginCall) { DispatchQueue.main.async { [self] in let topVc = bridge!.viewController!.parent!.presentingViewController as! CAPBridgeViewController @@ -69,7 +69,7 @@ public class BottomSheetPlugin: CAPPlugin, FloatingPanelControllerDelegate { call.resolve() } } - + @objc func clearScrollPatch(_ call: CAPPluginCall) { DispatchQueue.main.async { [self] in let topVc = bridge!.viewController!.parent!.presentingViewController as! CAPBridgeViewController @@ -78,7 +78,7 @@ public class BottomSheetPlugin: CAPPlugin, FloatingPanelControllerDelegate { call.resolve() } } - + @objc func delegate(_ call: CAPPluginCall) { ensureLocalOrigin() ensureDelegating() @@ -173,22 +173,6 @@ public class BottomSheetPlugin: CAPPlugin, FloatingPanelControllerDelegate { } } - @objc func callActionInMain(_ call: CAPPluginCall) { - ensureLocalOrigin() - ensureDelegated() - - call.resolve() - - DispatchQueue.main.async { [self] in - let topVc = bridge!.viewController!.parent!.presentingViewController as! CAPBridgeViewController - let topBottomSheetPlugin = topVc.bridge!.plugin(withName: "BottomSheet") as! BottomSheetPlugin - topBottomSheetPlugin.notifyListeners("callActionInMain", data: [ - "name": call.getString("name")!, - "optionsJson": call.getString("optionsJson")! - ]) - } - } - @objc func openInMain(_ call: CAPPluginCall) { ensureLocalOrigin() ensureDelegated() @@ -204,20 +188,6 @@ public class BottomSheetPlugin: CAPPlugin, FloatingPanelControllerDelegate { } } - @objc func callActionInNative(_ call: CAPPluginCall) { - ensureLocalOrigin() - ensureDelegating() - - call.resolve() - - DispatchQueue.main.async { [self] in - capVc!.bridge?.plugin(withName: "BottomSheet")!.notifyListeners("callActionInNative", data: [ - "name": call.getString("name")!, - "optionsJson": call.getString("optionsJson")! - ]) - } - } - // Extra security level, potentially redundant private func ensureLocalOrigin() { DispatchQueue.main.sync { [self] in @@ -363,11 +333,11 @@ extension BottomSheetPlugin: UIGestureRecognizerDelegate { if let mainGestureRecognizer = bridge!.webView!.scrollView.gestureRecognizers?.first(where: { $0 is UIPanGestureRecognizer }) { bridge!.webView!.scrollView.removeGestureRecognizer(mainGestureRecognizer) } - + if let fpcGestureRecognizer = fpc.view.gestureRecognizers?.first(where: { $0 is UIPanGestureRecognizer }) { fpc.view.removeGestureRecognizer(fpcGestureRecognizer) } - + fpc.panGestureRecognizer.isEnabled = false } diff --git a/mobile/plugins/native-bottom-sheet/src/definitions.ts b/mobile/plugins/native-bottom-sheet/src/definitions.ts index 9f907f78..93daeb18 100644 --- a/mobile/plugins/native-bottom-sheet/src/definitions.ts +++ b/mobile/plugins/native-bottom-sheet/src/definitions.ts @@ -37,10 +37,6 @@ export interface BottomSheetPlugin { setSelfSize(options: { size: 'half' | 'full' }): Promise; - callActionInMain(options: { name: string, optionsJson: string }): Promise; - - callActionInNative(options: { name: string, optionsJson: string }): Promise; - openInMain(options: { key: BottomSheetKeys }): Promise; addListener( @@ -53,15 +49,6 @@ export interface BottomSheetPlugin { handler: () => void, ): Promise & PluginListenerHandle; - addListener( - eventName: 'callActionInMain', - handler: (options: { name: string, optionsJson: string }) => void, - ): Promise & PluginListenerHandle; - - addListener( - eventName: 'callActionInNative', - handler: (options: { name: string, optionsJson: string }) => void, - ): Promise & PluginListenerHandle; addListener( eventName: 'openInMain', diff --git a/package-lock.json b/package-lock.json index 09a33eaf..ee6cf80b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "mytonwallet", - "version": "1.17.13", + "version": "1.17.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytonwallet", - "version": "1.17.13", + "version": "1.17.14", "license": "GPL-3.0-or-later", "dependencies": { "@capacitor-mlkit/barcode-scanning": "^5.3.0", "@capacitor/android": "^5.3.0", "@capacitor/app": "^5.0.6", + "@capacitor/clipboard": "^5.0.6", "@capacitor/core": "^5.5.1", "@capacitor/dialog": "^5.0.6", "@capacitor/haptics": "^5.0.6", @@ -21,7 +22,9 @@ "@ledgerhq/hw-transport-webhid": "^6.28.1", "@ledgerhq/hw-transport-webusb": "^6.28.1", "@mauricewegner/capacitor-navigation-bar": "^2.0.3", - "@ton-community/ton-ledger": "^6.0.0", + "@ton-community/ton-ledger": "^7.0.1", + "@ton/core": "^0.53.0", + "@ton/ton": "^13.9.0", "buffer": "^6.0.3", "capacitor-plugin-safe-area": "^2.0.5", "capacitor-splash-screen": "file:mobile/plugins/capacitor-splash-screen", @@ -30,9 +33,7 @@ "pako": "^2.1.0", "qr-code-styling": "github:troman29/qr-code-styling#c00d0", "qrcode-generator": "^1.4.4", - "ton": "^13.4.1", - "ton-core": "^0.49.0", - "tonapi-sdk-js": "^0.27.0", + "tonapi-sdk-js": "^1.0.1", "tonweb": "github:troman29/tonweb#8376c1c4eb003412d873dee6600cb9ea91cf6163", "tonweb-mnemonic": "^1.0.1", "tweetnacl": "^1.0.3", @@ -55,7 +56,7 @@ "@statoscope/cli": "^5.27.0", "@statoscope/webpack-plugin": "^5.27.0", "@testing-library/jest-dom": "^5.16.5", - "@tonconnect/protocol": "^2.2.5", + "@tonconnect/protocol": "^2.2.6", "@types/bn.js": "^5.1.1", "@types/chrome": "^0.0.191", "@types/croppie": "^2.6.1", @@ -3039,6 +3040,7 @@ }, "node_modules/@babel/runtime": { "version": "7.22.3", + "dev": true, "license": "MIT", "dependencies": { "regenerator-runtime": "^0.13.11" @@ -3287,6 +3289,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/@capacitor/clipboard": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/clipboard/-/clipboard-5.0.6.tgz", + "integrity": "sha512-VsokRAn+0HVWj6riSRdspczEfqFoHbrhS/XRhGoEPsj0uvYPSufy0Kb2dpnSqkeeElhh2Jvn8jmVAzII2XeR9w==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, "node_modules/@capacitor/core": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.5.1.tgz", @@ -5312,18 +5322,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/@react-dnd/asap": { - "version": "4.0.1", - "license": "MIT" - }, - "node_modules/@react-dnd/invariant": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/@react-dnd/shallowequal": { - "version": "2.0.0", - "license": "MIT" - }, "node_modules/@rushstack/eslint-patch": { "version": "1.3.0", "dev": true, @@ -5859,22 +5857,68 @@ } }, "node_modules/@ton-community/ton-ledger": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@ton-community/ton-ledger/-/ton-ledger-6.0.0.tgz", - "integrity": "sha512-aqqjOJOnW2T1GDPq3/FVnB6vpwNOyK8xmx5ya2/tm1GY9XF8B2ECGlxnZknPcAZh7uCUco6bMYXaVGTJotG8EA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@ton-community/ton-ledger/-/ton-ledger-7.0.1.tgz", + "integrity": "sha512-4QDjG9D/c3vKyApKa36KfqyE0UcPn/ExSJvCb8Cd03BdhMiZQNvm7tJsKg12lslPCIbl1dW9ZC6fg1RvqAAqnQ==", "dependencies": { "@ledgerhq/hw-transport": "^6.28.4", - "teslabot": "^1.5.0", - "ton-crypto": "^3.2.0" + "@ton/crypto": "^3.2.0", + "teslabot": "^1.5.0" + }, + "peerDependencies": { + "@ton/core": ">=0.52.2" + } + }, + "node_modules/@ton/core": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.53.0.tgz", + "integrity": "sha512-tB5RxXFS6Z/ivmsqMn/eebEZmkWXIAz+hS1PDZXhbBkcvnBTknZ12g2AFrZtXyuvpm8O6SbL15UI/3NgkyIWzQ==", + "dependencies": { + "symbol.inspect": "1.0.1" + }, + "peerDependencies": { + "@ton/crypto": ">=3.2.0" + } + }, + "node_modules/@ton/crypto": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.2.0.tgz", + "integrity": "sha512-50RkwReEuV2FkxSZ8ht/x9+n0ZGtwRKGsJ0ay4I/HFhkYVG/awIIBQeH0W4j8d5lADdO5h01UtX8PJ8AjiejjA==", + "dependencies": { + "@ton/crypto-primitives": "2.0.0", + "jssha": "3.2.0", + "tweetnacl": "1.0.3" + } + }, + "node_modules/@ton/crypto-primitives": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.0.0.tgz", + "integrity": "sha512-wttiNClmGbI6Dfy/8oyNnsIV0b/qYkCJz4Gn4eP62lJZzMtVQ94Ko7nikDX1EfYHkLI1xpOitWpW+8ZuG6XtDg==", + "dependencies": { + "jssha": "3.2.0" + } + }, + "node_modules/@ton/ton": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@ton/ton/-/ton-13.9.0.tgz", + "integrity": "sha512-bvDn9vv0rNsN/OH84Q4DKH3N21AD0MvTxXmnS0wPEOoU38F4mltXmA7an2SjaSgd9kAlsOSHa0EirkTie+Zitw==", + "dependencies": { + "axios": "^0.25.0", + "dataloader": "^2.0.0", + "symbol.inspect": "1.0.1", + "teslabot": "^1.3.0", + "zod": "^3.21.4" }, "peerDependencies": { - "ton-core": ">=0.49.1" + "@ton/core": ">=0.53.0", + "@ton/crypto": ">=3.2.0" } }, "node_modules/@tonconnect/protocol": { - "version": "2.2.5", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@tonconnect/protocol/-/protocol-2.2.6.tgz", + "integrity": "sha512-kyoDz5EqgsycYP+A+JbVsAUYHNT059BCrK+m0pqxykMODwpziuSAXfwAZmHcg8v7NB9VKYbdFY55xKeXOuEd0w==", "dev": true, - "license": "Apache-2.0", "dependencies": { "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" @@ -6190,7 +6234,7 @@ "version": "18.16.16", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.16.tgz", "integrity": "sha512-NpaM49IGQQAUlBhHMF82QH80J08os4ZmyF9MkpCzWAGuOHqE4gTEbhzd7L3l5LmWuZ6E0OiC1FweQ4tsiW35+g==", - "devOptional": true + "dev": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -6226,7 +6270,7 @@ }, "node_modules/@types/prop-types": { "version": "15.7.5", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/qs": { @@ -6241,7 +6285,7 @@ }, "node_modules/@types/react": { "version": "18.2.7", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -6273,7 +6317,7 @@ }, "node_modules/@types/scheduler": { "version": "0.16.3", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/semver": { @@ -7615,7 +7659,8 @@ }, "node_modules/axios": { "version": "0.25.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", "dependencies": { "follow-redirects": "^1.14.7" } @@ -9793,7 +9838,7 @@ }, "node_modules/csstype": { "version": "3.1.2", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/cubic2quad": { @@ -9817,7 +9862,8 @@ }, "node_modules/dataloader": { "version": "2.2.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.2.tgz", + "integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==" }, "node_modules/date-fns": { "version": "2.30.0", @@ -10216,15 +10262,6 @@ "node": ">=8" } }, - "node_modules/dnd-core": { - "version": "14.0.1", - "license": "MIT", - "dependencies": { - "@react-dnd/asap": "^4.0.0", - "@react-dnd/invariant": "^2.0.0", - "redux": "^4.1.1" - } - }, "node_modules/dns-equal": { "version": "1.0.0", "dev": true, @@ -12634,6 +12671,7 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -13572,17 +13610,6 @@ "tslib": "^2.0.3" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "license": "MIT" - }, "node_modules/hosted-git-info": { "version": "4.1.0", "dev": true, @@ -16416,6 +16443,7 @@ }, "node_modules/js-tokens": { "version": "4.0.0", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -17154,6 +17182,7 @@ }, "node_modules/loose-envify": { "version": "1.4.0", + "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -17519,10 +17548,6 @@ "node": ">= 4.0.0" } }, - "node_modules/memoize-one": { - "version": "6.0.0", - "license": "MIT" - }, "node_modules/memoizee": { "version": "0.4.15", "dev": true, @@ -20019,6 +20044,7 @@ }, "node_modules/react": { "version": "18.2.0", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -20027,91 +20053,11 @@ "node": ">=0.10.0" } }, - "node_modules/react-arborist": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "memoize-one": "^6.0.0", - "react-dnd": "^14.0.3", - "react-dnd-html5-backend": "^14.0.1", - "react-window": "^1.8.6" - }, - "peerDependencies": { - "react": ">= 16.14", - "react-dom": ">= 16.14" - } - }, - "node_modules/react-dnd": { - "version": "14.0.5", - "license": "MIT", - "dependencies": { - "@react-dnd/invariant": "^2.0.0", - "@react-dnd/shallowequal": "^2.0.0", - "dnd-core": "14.0.1", - "fast-deep-equal": "^3.1.3", - "hoist-non-react-statics": "^3.3.2" - }, - "peerDependencies": { - "@types/hoist-non-react-statics": ">= 3.3.1", - "@types/node": ">= 12", - "@types/react": ">= 16", - "react": ">= 16.14" - }, - "peerDependenciesMeta": { - "@types/hoist-non-react-statics": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-dnd-html5-backend": { - "version": "14.1.0", - "license": "MIT", - "dependencies": { - "dnd-core": "14.0.1" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, "node_modules/react-is": { "version": "18.2.0", "dev": true, "license": "MIT" }, - "node_modules/react-window": { - "version": "1.8.9", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.0.0", - "memoize-one": ">=3.1.1 <6" - }, - "engines": { - "node": ">8.0.0" - }, - "peerDependencies": { - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-window/node_modules/memoize-one": { - "version": "5.2.1", - "license": "MIT" - }, "node_modules/read-config-file": { "version": "6.3.2", "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", @@ -20305,13 +20251,6 @@ "node": ">=8" } }, - "node_modules/redux": { - "version": "4.2.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/regenerate": { "version": "1.4.2", "dev": true, @@ -20330,6 +20269,7 @@ }, "node_modules/regenerator-runtime": { "version": "0.13.11", + "dev": true, "license": "MIT" }, "node_modules/regenerator-transform": { @@ -20836,14 +20776,6 @@ "dev": true, "license": "ISC" }, - "node_modules/scheduler": { - "version": "0.23.0", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/schema-utils": { "version": "4.0.1", "dev": true, @@ -22804,54 +22736,10 @@ "node": ">=0.6" } }, - "node_modules/ton": { - "version": "13.5.0", - "license": "MIT", - "dependencies": { - "axios": "^0.25.0", - "dataloader": "^2.0.0", - "symbol.inspect": "1.0.1", - "teslabot": "^1.3.0", - "zod": "^3.21.4" - }, - "peerDependencies": { - "ton-core": ">=0.48.0", - "ton-crypto": ">=3.2.0" - } - }, - "node_modules/ton-core": { - "version": "0.49.1", - "license": "MIT", - "dependencies": { - "symbol.inspect": "1.0.1" - }, - "peerDependencies": { - "ton-crypto": ">=3.2.0" - } - }, - "node_modules/ton-crypto": { - "version": "3.2.0", - "license": "MIT", - "dependencies": { - "jssha": "3.2.0", - "ton-crypto-primitives": "2.0.0", - "tweetnacl": "1.0.3" - } - }, - "node_modules/ton-crypto-primitives": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "jssha": "3.2.0" - } - }, "node_modules/tonapi-sdk-js": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/tonapi-sdk-js/-/tonapi-sdk-js-0.27.0.tgz", - "integrity": "sha512-o46+lXEyBjHiznl/X9RYppgg7MufpkAg116nAgAgfONb7/qOm3LBhaSosZ2cDqTF2e8yr6S8zEaPH3qNePde9Q==", - "dependencies": { - "react-arborist": "^1.2.0" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tonapi-sdk-js/-/tonapi-sdk-js-1.0.1.tgz", + "integrity": "sha512-ZFPRZ2JQWQlNQs+ZggzdWS8m1pSFeklPmUt3asdnljMceRPopWUcyU2LGw2/GhIxde7XKqF/kimF/1EkSUV/og==" }, "node_modules/tonweb": { "version": "0.0.62", diff --git a/package.json b/package.json index 8fcc26f8..567246ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mytonwallet", - "version": "1.17.13", + "version": "1.17.14", "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": { @@ -85,7 +85,7 @@ "@statoscope/cli": "^5.27.0", "@statoscope/webpack-plugin": "^5.27.0", "@testing-library/jest-dom": "^5.16.5", - "@tonconnect/protocol": "^2.2.5", + "@tonconnect/protocol": "^2.2.6", "@types/bn.js": "^5.1.1", "@types/chrome": "^0.0.191", "@types/croppie": "^2.6.1", @@ -171,6 +171,7 @@ "@capacitor-mlkit/barcode-scanning": "^5.3.0", "@capacitor/android": "^5.3.0", "@capacitor/app": "^5.0.6", + "@capacitor/clipboard": "^5.0.6", "@capacitor/core": "^5.5.1", "@capacitor/dialog": "^5.0.6", "@capacitor/haptics": "^5.0.6", @@ -180,7 +181,9 @@ "@ledgerhq/hw-transport-webhid": "^6.28.1", "@ledgerhq/hw-transport-webusb": "^6.28.1", "@mauricewegner/capacitor-navigation-bar": "^2.0.3", - "@ton-community/ton-ledger": "^6.0.0", + "@ton-community/ton-ledger": "^7.0.1", + "@ton/core": "^0.53.0", + "@ton/ton": "^13.9.0", "buffer": "^6.0.3", "capacitor-plugin-safe-area": "^2.0.5", "capacitor-splash-screen": "file:mobile/plugins/capacitor-splash-screen", @@ -189,9 +192,7 @@ "pako": "^2.1.0", "qr-code-styling": "github:troman29/qr-code-styling#c00d0", "qrcode-generator": "^1.4.4", - "ton": "^13.4.1", - "ton-core": "^0.49.0", - "tonapi-sdk-js": "^0.27.0", + "tonapi-sdk-js": "^1.0.1", "tonweb": "github:troman29/tonweb#8376c1c4eb003412d873dee6600cb9ea91cf6163", "tonweb-mnemonic": "^1.0.1", "tweetnacl": "^1.0.3", diff --git a/public/electronVersion.txt b/public/electronVersion.txt index 47287f17..e85169c0 100644 --- a/public/electronVersion.txt +++ b/public/electronVersion.txt @@ -1 +1 @@ -1.17.13 +1.17.14 diff --git a/public/fallbackScript.js b/public/fallbackScript.js new file mode 100644 index 00000000..d98c5a00 --- /dev/null +++ b/public/fallbackScript.js @@ -0,0 +1,18 @@ +var APP_RENDERED_TIMEOUT = 5000; + +function checkAppRendered() { + if (document.documentElement.className.indexOf('is-rendered') !== -1) return; + + var messageEl = document.createElement('div'); + messageEl.className = 'browser-update-message'; + + var text = 'It looks like your browser is outdated. \nTry to update it.'; + if (window.navigator.userAgent.includes('Android')) { + text = 'It looks like your browser is outdated. \nPlease update Google Chrome and Android WebView apps.'; + } + messageEl.textContent = text; + + document.body.appendChild(messageEl); +} + +window.setTimeout(checkAppRendered, APP_RENDERED_TIMEOUT); diff --git a/public/version.txt b/public/version.txt index 47287f17..e85169c0 100644 --- a/public/version.txt +++ b/public/version.txt @@ -1 +1 @@ -1.17.13 +1.17.14 diff --git a/src/api/blockchains/ton/constants.ts b/src/api/blockchains/ton/constants.ts index f41d9449..3ea92c01 100644 --- a/src/api/blockchains/ton/constants.ts +++ b/src/api/blockchains/ton/constants.ts @@ -11,6 +11,9 @@ export const ATTEMPTS = 5; export const DEFAULT_DECIMALS = 9; export const DEFAULT_IS_BOUNCEABLE = false; +// Fee may change, so we add 5% for more reliability. This is only safe for low-fee blockchains such as TON. +export const FEE_FACTOR = 1.05; + export const LEDGER_SUPPORTED_PAYLOADS: ApiParsedPayload['type'][] = [ 'nft:transfer', 'tokens:transfer', diff --git a/src/api/blockchains/ton/nfts.ts b/src/api/blockchains/ton/nfts.ts index 83f62537..9b379234 100644 --- a/src/api/blockchains/ton/nfts.ts +++ b/src/api/blockchains/ton/nfts.ts @@ -71,17 +71,20 @@ export async function getNftUpdates(accountId: string, fromSec: number) { let to: string; let nftAddress: string; let rawNft: NftItem | undefined; - const isPurchase = !!action.nftPurchase; + const isPurchase = !!action.NftPurchase; - if (action.nftItemTransfer) { - const { sender, recipient, nft: rawNftAddress } = action.nftItemTransfer; + if (action.NftItemTransfer) { + const { sender, recipient, nft: rawNftAddress } = action.NftItemTransfer; if (!sender || !recipient) continue; to = toBase64Address(recipient.address); nftAddress = toBase64Address(rawNftAddress, true); - } else if (action.nftPurchase) { - const { buyer } = action.nftPurchase; + } else if (action.NftPurchase) { + const { buyer } = action.NftPurchase; to = toBase64Address(buyer.address); - rawNft = action.nftPurchase.nft; + rawNft = action.NftPurchase.nft; + if (!rawNft) { + continue; + } nftAddress = toBase64Address(rawNft.address, true); } else { continue; @@ -91,15 +94,18 @@ export async function getNftUpdates(accountId: string, fromSec: number) { if (!rawNft) { [rawNft] = await fetchNftItems(network, [nftAddress]); } - const nft = buildNft(rawNft); - - if (nft) { - updates.push({ - type: 'nftReceived', - accountId, - nftAddress, - nft, - }); + + if (rawNft) { + const nft = buildNft(rawNft); + + if (nft) { + updates.push({ + type: 'nftReceived', + accountId, + nftAddress, + nft, + }); + } } } else if (!isPurchase && await isActiveSmartContract(network, to)) { updates.push({ diff --git a/src/api/blockchains/ton/staking.ts b/src/api/blockchains/ton/staking.ts index 3c507a91..6edf42a8 100644 --- a/src/api/blockchains/ton/staking.ts +++ b/src/api/blockchains/ton/staking.ts @@ -1,5 +1,3 @@ -import BN from 'bn.js'; - import type { ApiBackendStakingState, ApiNetwork, @@ -12,34 +10,29 @@ import type { TonTransferParams } from './types'; import { ApiCommonError, ApiLiquidUnstakeMode, ApiTransactionDraftError } from '../../types'; import { - LIQUID_JETTON, LIQUID_POOL, STAKING_MIN_AMOUNT, TON_TOKEN_SLUG, + LIQUID_JETTON, LIQUID_POOL, ONE_TON, TON_TOKEN_SLUG, } from '../../../config'; -import { Big } from '../../../lib/big.js'; import { parseAccountId } from '../../../util/account'; +import { bigintDivideToNumber, bigintMultiplyToNumber } from '../../../util/bigint'; +import { fromDecimal } from '../../../util/decimals'; import { buildLiquidStakingDepositBody, buildLiquidStakingWithdrawBody, - fromNano, getTokenBalance, getTonWeb, resolveTokenWalletAddress, toBase64Address, - toNano, } from './util/tonweb'; import { NominatorPool } from './contracts/NominatorPool'; import { fetchStoredAddress } from '../../common/accounts'; import { apiDb } from '../../db'; -import { DEFAULT_DECIMALS, STAKE_COMMENT, UNSTAKE_COMMENT } from './constants'; +import { STAKE_COMMENT, UNSTAKE_COMMENT } from './constants'; import { checkTransactionDraft, submitTransfer } from './transactions'; import { isAddressInitialized } from './wallet'; -const LIQUID_STAKE_AMOUNT = '1'; -const LIQUID_UNSTAKE_AMOUNT = '1'; -const UNSTAKE_AMOUNT = '1'; - export async function checkStakeDraft( accountId: string, - amount: string, + amount: bigint, commonData: ApiStakingCommonData, backendState: ApiBackendStakingState, ) { @@ -47,15 +40,14 @@ export async function checkStakeDraft( let type: ApiStakingType; let result: CheckTransactionDraftResult; - const bigAmount = Big(fromNano(amount)); - if (staked?.type === 'nominators' && bigAmount.gte(STAKING_MIN_AMOUNT)) { + if (staked?.type === 'nominators' && amount >= ONE_TON) { type = 'nominators'; const poolAddress = backendState.nominatorsPool.address; - amount = new BN(amount).add(toNano(LIQUID_STAKE_AMOUNT)).toString(); + amount += ONE_TON; result = await checkTransactionDraft(accountId, TON_TOKEN_SLUG, poolAddress, amount, STAKE_COMMENT); - } else if (bigAmount.lt(STAKING_MIN_AMOUNT)) { + } else if (amount < ONE_TON) { return { error: ApiTransactionDraftError.InvalidAmount }; } else { type = 'liquid'; @@ -72,7 +64,7 @@ export async function checkStakeDraft( export async function checkUnstakeDraft( accountId: string, - amount: string, + amount: bigint, commonData: ApiStakingCommonData, backendState: ApiBackendStakingState, ) { @@ -82,30 +74,26 @@ export async function checkUnstakeDraft( let type: ApiStakingType; let result: CheckTransactionDraftResult; - let tokenAmount: string | undefined; + let tokenAmount: bigint | undefined; if (staked.type === 'nominators') { type = 'nominators'; const poolAddress = backendState.nominatorsPool.address; result = await checkTransactionDraft( - accountId, TON_TOKEN_SLUG, poolAddress, toNano(UNSTAKE_AMOUNT).toString(), UNSTAKE_COMMENT, + accountId, TON_TOKEN_SLUG, poolAddress, ONE_TON, UNSTAKE_COMMENT, ); } else if (staked.type === 'liquid') { type = 'liquid'; - const bigAmount = Big(fromNano(amount).toString()); - if (bigAmount.gt(staked.amount)) { + if (amount > staked.amount) { return { error: ApiTransactionDraftError.InsufficientBalance }; - } else if (bigAmount.eq(staked.amount)) { + } else if (amount === staked.amount) { tokenAmount = staked.tokenAmount; } else { - const { currentRate } = commonData.liquid; - tokenAmount = bigAmount.div(currentRate).toFixed(DEFAULT_DECIMALS); + tokenAmount = bigintDivideToNumber(amount, commonData.liquid.currentRate); } - tokenAmount = toNano(tokenAmount).toString(); - const params = await buildLiquidStakingWithdraw(network, address, tokenAmount); result = await checkTransactionDraft( accountId, TON_TOKEN_SLUG, params.toAddress, params.amount, params.payload, @@ -124,14 +112,14 @@ export async function checkUnstakeDraft( export async function submitStake( accountId: string, password: string, - amount: string, + amount: bigint, type: ApiStakingType, backendState: ApiBackendStakingState, ) { let result: SubmitTransferResult; if (type === 'liquid') { - amount = new BN(amount).add(toNano(LIQUID_STAKE_AMOUNT)).toString(); + amount += ONE_TON; result = await submitTransfer( accountId, password, @@ -159,7 +147,7 @@ export async function submitUnstake( accountId: string, password: string, type: ApiStakingType, - amount: string, + amount: bigint, backendState: ApiBackendStakingState, ) { const { network } = parseAccountId(accountId); @@ -184,7 +172,7 @@ export async function submitUnstake( password, TON_TOKEN_SLUG, toBase64Address(poolAddress, true), - toNano(UNSTAKE_AMOUNT).toString(), + ONE_TON, UNSTAKE_COMMENT, ); } @@ -195,7 +183,7 @@ export async function submitUnstake( export async function buildLiquidStakingWithdraw( network: ApiNetwork, address: string, - amount: string, + amount: bigint, mode: ApiLiquidUnstakeMode = ApiLiquidUnstakeMode.Default, ): Promise { const tokenWalletAddress = await resolveTokenWalletAddress(network, address, LIQUID_JETTON); @@ -208,7 +196,7 @@ export async function buildLiquidStakingWithdraw( }); return { - amount: toNano(LIQUID_UNSTAKE_AMOUNT).toString(), + amount: ONE_TON, toAddress: tokenWalletAddress, payload, }; @@ -223,8 +211,8 @@ export async function getStakingState( const address = toBase64Address(await fetchStoredAddress(accountId), true); const { currentRate, collection } = commonData.liquid; - const tokenBalance = Big(await getLiquidStakingTokenBalance(accountId)); - let unstakeAmount = Big(0); + const tokenBalance = await getLiquidStakingTokenBalance(accountId); + let unstakeAmount = 0n; if (collection) { const nfts = await apiDb.nfts.where({ @@ -234,27 +222,29 @@ export async function getStakingState( for (const nft of nfts) { const billAmount = nft.name?.match(/Bill for (?[\d.]+) Pool Jetton/)?.groups?.amount; - unstakeAmount = unstakeAmount.plus(billAmount ?? 0); + if (billAmount) { + unstakeAmount += fromDecimal(billAmount); + } } } - const { loyaltyType } = backendState; + const { loyaltyType, shouldUseNominators } = backendState; - const liquidAvailable = commonData.liquid.collection ? '0' : commonData.liquid.available; + const liquidAvailable = commonData.liquid.collection ? 0n : commonData.liquid.available; let liquidApy = commonData.liquid.apy; if (loyaltyType && loyaltyType in commonData.liquid.loyaltyApy) { liquidApy = commonData.liquid.loyaltyApy[loyaltyType]; } - if (tokenBalance.gt(0) || unstakeAmount.gt(0)) { - const fullTokenAmount = tokenBalance.plus(unstakeAmount); - const amount = Big(currentRate).times(fullTokenAmount).toFixed(DEFAULT_DECIMALS); + if (tokenBalance > 0n || unstakeAmount > 0n) { + const fullTokenAmount = tokenBalance + unstakeAmount; + const amount = bigintMultiplyToNumber(fullTokenAmount, currentRate); return { type: 'liquid', - tokenAmount: tokenBalance.toFixed(DEFAULT_DECIMALS), - amount: parseFloat(amount), - unstakeRequestAmount: unstakeAmount.toNumber(), + tokenAmount: tokenBalance, + amount, + unstakeRequestAmount: unstakeAmount, apy: liquidApy, instantAvailable: liquidAvailable, }; @@ -265,23 +255,30 @@ export async function getStakingState( const nominators = await nominatorPool.getListNominators(); const currentNominator = nominators.find((n) => n.address === address); - if (!currentNominator) { + if (currentNominator) { + return { + type: 'nominators', + amount: fromDecimal(currentNominator.amount), + pendingDepositAmount: fromDecimal(currentNominator.pendingDepositAmount), + isUnstakeRequested: currentNominator.withdrawRequested, + }; + } else if (shouldUseNominators) { + return { + type: 'nominators', + amount: 0n, + pendingDepositAmount: 0n, + isUnstakeRequested: false, + }; + } else { return { type: 'liquid', - tokenAmount: '0', - amount: 0, - unstakeRequestAmount: 0, + tokenAmount: 0n, + amount: 0n, + unstakeRequestAmount: 0n, apy: liquidApy, instantAvailable: liquidAvailable, }; } - - return { - type: 'nominators', - amount: parseFloat(currentNominator.amount), - pendingDepositAmount: parseFloat(currentNominator.pendingDepositAmount), - isUnstakeRequested: currentNominator.withdrawRequested, - }; } function getPoolContract(network: ApiNetwork, poolAddress: string) { @@ -291,7 +288,7 @@ function getPoolContract(network: ApiNetwork, poolAddress: string) { async function getLiquidStakingTokenBalance(accountId: string) { const { network } = parseAccountId(accountId); if (network !== 'mainnet') { - return '0'; + return 0n; } const address = await fetchStoredAddress(accountId); @@ -299,7 +296,7 @@ async function getLiquidStakingTokenBalance(accountId: string) { const isInitialized = await isAddressInitialized(network, walletAddress); if (!isInitialized) { - return '0'; + return 0n; } return getTokenBalance(network, walletAddress); diff --git a/src/api/blockchains/ton/tokens.ts b/src/api/blockchains/ton/tokens.ts index 221b7363..351df101 100644 --- a/src/api/blockchains/ton/tokens.ts +++ b/src/api/blockchains/ton/tokens.ts @@ -36,7 +36,7 @@ const { JettonWallet } = TonWeb.token.jetton; export type JettonWalletType = InstanceType; export type TokenBalanceParsed = { slug: string; - balance: string; + balance: bigint; token: ApiTokenSimple; jettonWallet: string; }; @@ -68,13 +68,13 @@ function parseTokenBalance(balanceRaw: JettonBalance): TokenBalanceParsed | unde } try { - const { balance, jetton, walletAddress } = balanceRaw; + const { balance, jetton, wallet_address: walletAddress } = balanceRaw; const minterAddress = toBase64Address(jetton.address, true); const token = buildTokenByMetadata(minterAddress, jetton); return { slug: token.slug, - balance, + balance: BigInt(balance), token, jettonWallet: toBase64Address(walletAddress.address), }; @@ -109,7 +109,7 @@ export function parseTokenTransaction( slug, fromAddress: isIncoming ? (address ?? tx.fromAddress) : walletAddress, toAddress: isIncoming ? walletAddress : address!, - amount: isIncoming ? jettonAmount.toString() : `-${jettonAmount}`, + amount: isIncoming ? jettonAmount : -jettonAmount, comment, encryptedComment, isIncoming, @@ -121,7 +121,7 @@ export async function buildTokenTransfer( slug: string, fromAddress: string, toAddress: string, - amount: string, + amount: bigint, payload?: AnyPayload, ) { const minterAddress = resolveTokenBySlug(slug).minterAddress!; @@ -136,14 +136,14 @@ export async function buildTokenTransfer( payload = buildTokenTransferBody({ tokenAmount: amount, toAddress, - forwardAmount: TOKEN_TRANSFER_TON_FORWARD_AMOUNT.toString(), + forwardAmount: TOKEN_TRANSFER_TON_FORWARD_AMOUNT, forwardPayload: payload, responseAddress: fromAddress, }); return { tokenWallet, - amount: TOKEN_TRANSFER_TON_AMOUNT.toString(), + amount: TOKEN_TRANSFER_TON_AMOUNT, toAddress: tokenWalletAddress, payload, }; @@ -158,7 +158,7 @@ export function getTokenWallet(network: ApiNetwork, tokenAddress: string) { } export async function getTokenWalletBalance(tokenWallet: JettonWalletType) { - return (await tokenWallet.getData()).balance.toString(); + return BigInt((await tokenWallet.getData()).balance.toString()); } export function getKnownTokens() { diff --git a/src/api/blockchains/ton/transactions.ts b/src/api/blockchains/ton/transactions.ts index 28487085..9ad80284 100644 --- a/src/api/blockchains/ton/transactions.ts +++ b/src/api/blockchains/ton/transactions.ts @@ -1,8 +1,8 @@ -import { Cell as TonCell } from 'ton-core'; import type { Method } from 'tonweb'; import TonWeb from 'tonweb'; import type { Cell } from 'tonweb/dist/types/boc/cell'; import type { WalletContract } from 'tonweb/dist/types/contract/wallet/wallet-contract'; +import { Cell as TonCell } from '@ton/core/dist/boc/Cell'; import type { ApiAnyDisplayError, @@ -17,8 +17,9 @@ import type { JettonWalletType } from './tokens'; import type { AnyPayload, ApiTransactionExtra, TonTransferParams } from './types'; import { ApiCommonError, ApiTransactionDraftError, ApiTransactionError } from '../../types'; -import { TON_TOKEN_SLUG } from '../../../config'; +import { ONE_TON, TON_TOKEN_SLUG } from '../../../config'; import { parseAccountId } from '../../../util/account'; +import { bigintMultiplyToNumber } from '../../../util/bigint'; import { compareActivities } from '../../../util/compareActivities'; import { omit } from '../../../util/iteratees'; import { isValidLedgerComment } from '../../../util/ledger/utils'; @@ -46,7 +47,9 @@ import { bytesToBase64, isKnownStakingPool } from '../../common/utils'; import { ApiServerError, handleServerError } from '../../errors'; import { resolveAddress } from './address'; import { fetchKeyPair, fetchPrivateKey } from './auth'; -import { ATTEMPTS, STAKE_COMMENT, UNSTAKE_COMMENT } from './constants'; +import { + ATTEMPTS, FEE_FACTOR, STAKE_COMMENT, UNSTAKE_COMMENT, +} from './constants'; import { buildTokenTransfer, getTokenWalletBalance, parseTokenTransaction, resolveTokenBySlug, } from './tokens'; @@ -55,7 +58,7 @@ import { } from './wallet'; export type CheckTransactionDraftResult = { - fee?: string; + fee?: bigint; addressName?: string; isScam?: boolean; resolvedAddress?: string; @@ -65,7 +68,7 @@ export type CheckTransactionDraftResult = { export type SubmitTransferResult = { normalizedAddress: string; - amount: string; + amount: bigint; seqno: number; encryptedComment?: string; } | { @@ -81,9 +84,9 @@ type SubmitMultiTransferResult = { error: string; }; -const { Address, fromNano } = TonWeb.utils; +const { Address } = TonWeb.utils; -const DEFAULT_FEE = '15000000'; +const DEFAULT_FEE = 15000000n; const DEFAULT_EXPIRE_AT_TIMEOUT_SEC = 60; // 60 sec. const GET_TRANSACTIONS_LIMIT = 50; const GET_TRANSACTIONS_MAX_LIMIT = 100; @@ -108,7 +111,7 @@ export async function checkTransactionDraft( accountId: string, tokenSlug: string, toAddress: string, - amount: string, + amount: bigint, data?: AnyPayload, stateInit?: Cell, shouldEncrypt?: boolean, @@ -173,7 +176,7 @@ export async function checkTransactionDraft( if (addressInfo?.name) result.addressName = addressInfo.name; if (addressInfo?.isScam) result.isScam = addressInfo.isScam; - if (BigInt(amount) < BigInt(0)) { + if (amount < 0n) { return { ...result, error: ApiTransactionDraftError.InvalidAmount, @@ -222,7 +225,7 @@ export async function checkTransactionDraft( } } else { const address = await fetchStoredAddress(accountId); - const tokenAmount: string = amount; + const tokenAmount: bigint = amount; let tokenWallet: JettonWalletType; ({ tokenWallet, @@ -232,7 +235,7 @@ export async function checkTransactionDraft( } = await buildTokenTransfer(network, tokenSlug, address, toAddress, amount, data)); const tokenBalance = await getTokenWalletBalance(tokenWallet!); - if (BigInt(tokenBalance) < BigInt(tokenAmount!)) { + if (tokenBalance < tokenAmount!) { return { ...result, error: ApiTransactionDraftError.InsufficientBalance, @@ -241,12 +244,14 @@ export async function checkTransactionDraft( } const isOurWalletInitialized = await isAddressInitialized(network, wallet); - result.fee = await calculateFee(isOurWalletInitialized, async () => (await signTransaction( + const realFee = await calculateFee(isOurWalletInitialized, async () => (await signTransaction( network, wallet, toAddress, amount, data, stateInit, )).query); + result.fee = bigintMultiplyToNumber(realFee, FEE_FACTOR); const balance = await getWalletBalance(network, wallet); - if (BigInt(balance) < BigInt(amount) + BigInt(result.fee)) { + + if (balance < amount + realFee) { return { ...result, error: ApiTransactionDraftError.InsufficientBalance, @@ -254,7 +259,7 @@ export async function checkTransactionDraft( } return result as { - fee: string; + fee: bigint; resolvedAddress: string; addressName?: string; isScam?: boolean; @@ -273,7 +278,7 @@ export async function submitTransfer( password: string, tokenSlug: string, toAddress: string, - amount: string, + amount: bigint, data?: AnyPayload, stateInit?: Cell, shouldEncrypt?: boolean, @@ -324,7 +329,7 @@ export async function submitTransfer( const { seqno, query } = await signTransaction(network, wallet!, toAddress, amount, data, stateInit, secretKey); const fee = await calculateFee(isInitialized, query); - if (BigInt(balance) < BigInt(amount) + BigInt(fee)) { + if (balance < amount + fee) { return { error: ApiTransactionError.InsufficientBalance }; } @@ -357,7 +362,7 @@ async function signTransaction( network: ApiNetwork, wallet: WalletContract, toAddress: string, - amount: string, + amount: bigint, payload?: string | Uint8Array | Cell, stateInit?: Cell, privateKey?: Uint8Array, @@ -367,7 +372,7 @@ async function signTransaction( const query = wallet.methods.transfer({ secretKey: privateKey as any, // Workaround for wrong typing toAddress, - amount, + amount: amount.toString(), payload, seqno: seqno || 0, sendMode: 3, @@ -472,10 +477,9 @@ function updateTransactionType(transaction: ApiTransactionExtra) { fromAddress, toAddress, comment, amount, extraData, } = transaction; - const amountNumber = Math.abs(Number(fromNano(amount))); let type: ApiTransactionType | undefined; - if (isKnownStakingPool(fromAddress) && amountNumber > 1) { + if (isKnownStakingPool(fromAddress) && amount > ONE_TON) { type = 'unstake'; } else if (isKnownStakingPool(toBase64Address(toAddress, true))) { if (comment === STAKE_COMMENT) { @@ -510,21 +514,21 @@ export async function checkMultiTransactionDraft(accountId: string, messages: To const { network } = parseAccountId(accountId); const result: { - fee?: string; - totalAmount?: string; + fee?: bigint; + totalAmount?: bigint; } = {}; let totalAmount: bigint = 0n; try { for (const { toAddress, amount } of messages) { - if (BigInt(amount) < BigInt(0)) { + if (amount < 0n) { return { ...result, error: ApiTransactionDraftError.InvalidAmount }; } if (!Address.isValid(toAddress)) { return { ...result, error: ApiTransactionDraftError.InvalidToAddress }; } - totalAmount += BigInt(amount); + totalAmount += amount; } const wallet = await pickAccountWallet(accountId); @@ -535,16 +539,17 @@ export async function checkMultiTransactionDraft(accountId: string, messages: To const { isInitialized, balance } = await getWalletInfo(network, wallet); - result.fee = await calculateFee(isInitialized, async () => (await signMultiTransaction( + const realFee = await calculateFee(isInitialized, async () => (await signMultiTransaction( network, wallet, messages, )).query); - result.totalAmount = totalAmount.toString(); + result.totalAmount = totalAmount; + result.fee = bigintMultiplyToNumber(realFee, FEE_FACTOR); - if (BigInt(balance) < totalAmount + BigInt(result.fee)) { + if (balance < totalAmount + realFee) { return { ...result, error: ApiTransactionDraftError.InsufficientBalance }; } - return result as { fee: string; totalAmount: string }; + return result as { fee: bigint; totalAmount: bigint }; } catch (err: any) { return handleServerError(err); } @@ -678,7 +683,7 @@ async function calculateFee(isInitialized: boolean, query: Method | (() => Promi if (isInitialized) { const allFees = await query.estimateFee(); const fees = allFees.source_fees; - return String(fees.in_fwd_fee + fees.storage_fee + fees.gas_fee + fees.fwd_fee); + return BigInt(fees.in_fwd_fee + fees.storage_fee + fees.gas_fee + fees.fwd_fee); } else { return DEFAULT_FEE; } diff --git a/src/api/blockchains/ton/types.ts b/src/api/blockchains/ton/types.ts index e9858134..42517674 100644 --- a/src/api/blockchains/ton/types.ts +++ b/src/api/blockchains/ton/types.ts @@ -29,16 +29,16 @@ export interface ApiTransactionExtra extends ApiTransaction { export interface TokenTransferBodyParams { queryId?: number; - tokenAmount: string; + tokenAmount: bigint; toAddress: string; responseAddress: string; - forwardAmount: string; + forwardAmount: bigint; forwardPayload?: AnyPayload; } export interface TonTransferParams { toAddress: string; - amount: string; + amount: bigint; payload?: AnyPayload; stateInit?: Cell; isBase64Payload?: boolean; diff --git a/src/api/blockchains/ton/util/index.ts b/src/api/blockchains/ton/util/index.ts index e787520c..e94752fd 100644 --- a/src/api/blockchains/ton/util/index.ts +++ b/src/api/blockchains/ton/util/index.ts @@ -1,5 +1,7 @@ +import { bigintReviver } from '../../../../util/bigint'; + export function cloneDeep(value: T): T { - return JSON.parse(JSON.stringify(value)); + return JSON.parse(JSON.stringify(value), bigintReviver); } export function stringifyTxId({ lt, hash }: { lt: number | string; hash: string }) { diff --git a/src/api/blockchains/ton/util/metadata.ts b/src/api/blockchains/ton/util/metadata.ts index fcdffe00..b709644d 100644 --- a/src/api/blockchains/ton/util/metadata.ts +++ b/src/api/blockchains/ton/util/metadata.ts @@ -1,7 +1,10 @@ -import type { Address, DictionaryValue } from 'ton-core'; -import { - BitReader, BitString, Builder, Cell, Dictionary, Slice, -} from 'ton-core'; +import type { Address, DictionaryValue } from '@ton/core'; +import { BitReader } from '@ton/core/dist/boc/BitReader'; +import { BitString } from '@ton/core/dist/boc/BitString'; +import { Builder } from '@ton/core/dist/boc/Builder'; +import { Cell } from '@ton/core/dist/boc/Cell'; +import { Slice } from '@ton/core/dist/boc/Slice'; +import { Dictionary } from '@ton/core/dist/dict/Dictionary'; import type { ApiNetwork, ApiParsedPayload } from '../../../types'; import type { ApiTransactionExtra, JettonMetadata } from '../types'; @@ -232,7 +235,7 @@ export async function parsePayloadSlice( return { type: 'encrypted-comment', encryptedComment }; } - const queryId = slice.loadUint(64).toString(); + const queryId = slice.loadUintBig(64); switch (opCode) { case JettonOpCode.Transfer: { @@ -248,7 +251,7 @@ export async function parsePayloadSlice( type: 'tokens:transfer-non-standard', queryId, destination: toBase64Address(destination), - amount: amount.toString(), + amount, slug, }; } @@ -267,11 +270,11 @@ export async function parsePayloadSlice( return { type: 'tokens:transfer', queryId, - amount: amount.toString(), + amount, destination: toBase64Address(destination), responseDestination: toBase64Address(responseDestination), customPayload: customPayload?.toBoc().toString('base64'), - forwardAmount: forwardAmount.toString(), + forwardAmount, forwardPayload: forwardPayload?.toBoc().toString('base64'), slug, }; @@ -299,7 +302,7 @@ export async function parsePayloadSlice( newOwner: toBase64Address(newOwner), responseDestination: toBase64Address(responseDestination), customPayload: customPayload?.toBoc().toString('base64'), - forwardAmount: forwardAmount.toString(), + forwardAmount, forwardPayload: forwardPayload?.toBoc().toString('base64'), nftAddress, nftName: nft?.metadata?.name, @@ -317,7 +320,7 @@ export async function parsePayloadSlice( return { type: 'tokens:burn', queryId, - amount: amount.toString(), + amount, address: toBase64Address(address), customPayload: customPayload?.toBoc().toString('base64'), slug, diff --git a/src/api/blockchains/ton/util/tonCore.ts b/src/api/blockchains/ton/util/tonCore.ts index 17022a17..4d4b6406 100644 --- a/src/api/blockchains/ton/util/tonCore.ts +++ b/src/api/blockchains/ton/util/tonCore.ts @@ -1,15 +1,13 @@ -import { - TonClient, - WalletContractV1R1, - WalletContractV1R2, - WalletContractV1R3, - WalletContractV2R1, - WalletContractV2R2, - WalletContractV3R1, - WalletContractV3R2, - WalletContractV4, -} from 'ton'; import axios from 'axios'; +import { TonClient } from '@ton/ton/dist/client/TonClient'; +import { WalletContractV1R1 } from '@ton/ton/dist/wallets/WalletContractV1R1'; +import { WalletContractV1R2 } from '@ton/ton/dist/wallets/WalletContractV1R2'; +import { WalletContractV1R3 } from '@ton/ton/dist/wallets/WalletContractV1R3'; +import { WalletContractV2R1 } from '@ton/ton/dist/wallets/WalletContractV2R1'; +import { WalletContractV2R2 } from '@ton/ton/dist/wallets/WalletContractV2R2'; +import { WalletContractV3R1 } from '@ton/ton/dist/wallets/WalletContractV3R1'; +import { WalletContractV3R2 } from '@ton/ton/dist/wallets/WalletContractV3R2'; +import { WalletContractV4 } from '@ton/ton/dist/wallets/WalletContractV4'; import type { ApiNetwork, ApiWalletVersion } from '../../../types'; import { WORKCHAIN } from '../../../types'; diff --git a/src/api/blockchains/ton/util/tonapiio.ts b/src/api/blockchains/ton/util/tonapiio.ts index 3dc91cf9..1170bf81 100644 --- a/src/api/blockchains/ton/util/tonapiio.ts +++ b/src/api/blockchains/ton/util/tonapiio.ts @@ -1,106 +1,64 @@ -import { - AccountsApi, - BlockchainApi, - Configuration, - NFTApi, - ResponseError, -} from 'tonapi-sdk-js'; +import { Api, HttpClient } from 'tonapi-sdk-js'; import type { ApiNetwork } from '../../../types'; import { TONAPIIO_MAINNET_URL, TONAPIIO_TESTNET_URL } from '../../../../config'; -import { logDebugError } from '../../../../util/logs'; import { getEnvironment } from '../../../environment'; const MAX_LIMIT = 1000; -let apiByNetwork: Record | undefined; +let apiByNetwork: Record> | undefined; function getApi(network: ApiNetwork) { if (!apiByNetwork) { const headers = getEnvironment().apiHeaders; - const configurationMainnet = new Configuration({ - basePath: TONAPIIO_MAINNET_URL, - ...(headers && { headers }), - }); - const configurationTestnet = new Configuration({ - basePath: TONAPIIO_TESTNET_URL, - ...(headers && { headers }), - }); - apiByNetwork = { - mainnet: { - blockchainApi: new BlockchainApi(configurationMainnet), - nftApi: new NFTApi(configurationMainnet), - accountsApi: new AccountsApi(configurationMainnet), - }, - testnet: { - blockchainApi: new BlockchainApi(configurationTestnet), - nftApi: new NFTApi(configurationTestnet), - accountsApi: new AccountsApi(configurationTestnet), - }, + mainnet: new Api(new HttpClient({ + baseUrl: TONAPIIO_MAINNET_URL, + ...(headers && { baseApiParams: { headers } }), + })), + testnet: new Api(new HttpClient({ + baseUrl: TONAPIIO_TESTNET_URL, + ...(headers && { baseApiParams: { headers } }), + })), }; } return apiByNetwork[network]; } -export function fetchJettonBalances(network: ApiNetwork, account: string) { - const api = getApi(network).accountsApi; - return tonapiioErrorHandler(async () => { - return (await api.getJettonsBalances({ accountId: account })).balances; - }, []); +export async function fetchJettonBalances(network: ApiNetwork, account: string) { + return (await getApi(network).accounts.getAccountJettonsBalances(account)).balances; } -export function fetchNftItems(network: ApiNetwork, addresses: string[]) { - const api = getApi(network).nftApi; - return tonapiioErrorHandler(async () => (await api.getNftItemsByAddresses({ - getAccountsRequest: { accountIds: addresses }, - })).nftItems, []); +export async function fetchNftItems(network: ApiNetwork, addresses: string[]) { + return (await getApi(network).nft.getNftItemsByAddresses({ + account_ids: addresses, + })).nft_items; } -export function fetchAccountNfts(network: ApiNetwork, address: string, options?: { +export async function fetchAccountNfts(network: ApiNetwork, address: string, options?: { collection?: string; offset?: number; limit?: number; }) { const { collection, offset, limit } = options ?? {}; - const api = getApi(network).accountsApi; - return tonapiioErrorHandler(async () => (await api.getNftItemsByOwner({ - accountId: address, - offset: offset ?? 0, - limit: limit ?? MAX_LIMIT, - indirectOwnership: true, - collection, - })).nftItems, []); + return (await getApi(network).accounts.getAccountNftItems( + address, + { + offset: offset ?? 0, + limit: limit ?? MAX_LIMIT, + indirect_ownership: true, + collection, + }, + )).nft_items; } -export function fetchAccountEvents(network: ApiNetwork, address: string, fromSec: number, limit?: number) { - const api = getApi(network).accountsApi; - return tonapiioErrorHandler(async () => (await api.getEventsByAccount({ - accountId: address, +export async function fetchAccountEvents(network: ApiNetwork, address: string, fromSec: number, limit?: number) { + return (await getApi(network).accounts.getAccountEvents(address, { limit: limit ?? MAX_LIMIT, - startDate: fromSec, - })).events, []); -} - -async function tonapiioErrorHandler(fn: () => Promise, defaultValue: T): Promise { - try { - return (await fn()) || defaultValue; - } catch (err: any) { - if (err instanceof ResponseError) { - const data = await err.response.json().catch(); - if (data?.error === 'entity not found') { - return defaultValue; - } - } - logDebugError('tonapiioErrorHandler', err); - throw err; - } + start_date: fromSec, + })).events; } diff --git a/src/api/blockchains/ton/util/tonweb.ts b/src/api/blockchains/ton/util/tonweb.ts index 1293d512..ba9cc0dd 100644 --- a/src/api/blockchains/ton/util/tonweb.ts +++ b/src/api/blockchains/ton/util/tonweb.ts @@ -135,7 +135,7 @@ function parseRawTransaction(rawTx: any): ApiTransactionExtra[] { now, lt, hash, - fee, + total_fees: fee, } = rawTx; const txId = stringifyTxId({ lt, hash }); @@ -154,9 +154,9 @@ function parseRawTransaction(rawTx: any): ApiTransactionExtra[] { isIncoming, fromAddress: toBase64Address(source), toAddress: toBase64Address(destination), - amount: isIncoming ? value : `-${value}`, + amount: isIncoming ? BigInt(value) : -BigInt(value), slug: TON_TOKEN_SLUG, - fee, + fee: BigInt(fee), extraData: { normalizedAddress, body: getRawBody(msg), @@ -208,11 +208,11 @@ export function buildTokenTransferBody(params: TokenTransferBodyParams) { const cell = new Cell(); cell.bits.writeUint(JettonOpCode.Transfer, 32); cell.bits.writeUint(queryId || 0, 64); - cell.bits.writeCoins(new BN(tokenAmount)); + cell.bits.writeCoins(toBN(tokenAmount)); cell.bits.writeAddress(new Address(toAddress)); cell.bits.writeAddress(new Address(responseAddress)); cell.bits.writeBit(false); // null custom_payload - cell.bits.writeCoins(new BN(forwardAmount || '0')); + cell.bits.writeCoins(toBN(forwardAmount ?? 0n)); if (forwardPayload instanceof Uint8Array) { const freeBytes = Math.round(cell.bits.getFreeBits() / 8); @@ -292,7 +292,7 @@ export function buildLiquidStakingDepositBody(queryId?: number) { export function buildLiquidStakingWithdrawBody(options: { queryId?: number; - amount: string | BN; + amount: bigint; responseAddress: AddressType; waitTillRoundEnd?: boolean; // opposite of request_immediate_withdrawal fillOrKill?: boolean; @@ -308,7 +308,7 @@ export function buildLiquidStakingWithdrawBody(options: { const cell = new Cell(); cell.bits.writeUint(JettonOpCode.Burn, 32); cell.bits.writeUint(queryId ?? 0, 64); - cell.bits.writeCoins(new BN(amount)); + cell.bits.writeCoins(toBN(amount)); cell.bits.writeAddress(new Address(responseAddress)); cell.bits.writeBit(1); cell.refs.push(customPayload); @@ -321,5 +321,9 @@ export async function getTokenBalance(network: ApiNetwork, walletAddress: string address: walletAddress, }); const wallletData = await jettonWallet.getData(); - return fromNano(wallletData.balance); + return BigInt(wallletData.balance.toString()); +} + +export function toBN(value: bigint) { + return new BN(value.toString()); } diff --git a/src/api/blockchains/ton/wallet.ts b/src/api/blockchains/ton/wallet.ts index a9ca64b3..0916ea0a 100644 --- a/src/api/blockchains/ton/wallet.ts +++ b/src/api/blockchains/ton/wallet.ts @@ -44,7 +44,7 @@ export async function getWalletInfo(network: ApiNetwork, walletOrAddress: Wallet isInitialized: boolean; isWallet: boolean; seqno: number; - balance: string; + balance: bigint; lastTxId?: string; }> { const address = typeof walletOrAddress === 'string' @@ -66,7 +66,7 @@ export async function getWalletInfo(network: ApiNetwork, walletOrAddress: Wallet isInitialized: accountState === 'active', isWallet, seqno, - balance: balance || '0', + balance: BigInt(balance || '0'), lastTxId: lt === '0' ? undefined : stringifyTxId({ lt, hash }), @@ -80,12 +80,8 @@ export async function getAccountBalance(accountId: string) { return getWalletBalance(network, address); } -export async function getWalletBalance( - network: ApiNetwork, walletOrAddress: WalletContract | string, -): Promise { - const { balance } = await getWalletInfo(network, walletOrAddress); - - return balance || '0'; +export async function getWalletBalance(network: ApiNetwork, walletOrAddress: WalletContract | string): Promise { + return (await getWalletInfo(network, walletOrAddress)).balance; } export async function getWalletSeqno(network: ApiNetwork, walletOrAddress: WalletContract | string): Promise { @@ -99,7 +95,7 @@ export async function pickBestWallet(network: ApiNetwork, publicKey: Uint8Array) const allWallets = await Promise.all(walletClasses.map(async (WalletClass) => { const wallet = new WalletClass(tonWeb.provider, { publicKey, wc: 0 }); const balance = await getWalletBalance(network, wallet); - if (balance === '0') { + if (balance === 0n) { return undefined; } diff --git a/src/api/common/helpers.ts b/src/api/common/helpers.ts index 0aab5b8a..1c42b138 100644 --- a/src/api/common/helpers.ts +++ b/src/api/common/helpers.ts @@ -45,7 +45,7 @@ export function buildLocalTransaction( txId: getNextLocalId(), timestamp: Date.now(), isIncoming: false, - amount: `-${amount}`, + amount: -amount, ...restParams, extraData: { normalizedAddress, diff --git a/src/api/common/txCallbacks.ts b/src/api/common/txCallbacks.ts index 42edf9cc..89157df0 100644 --- a/src/api/common/txCallbacks.ts +++ b/src/api/common/txCallbacks.ts @@ -4,11 +4,11 @@ import { createCallbackManager } from '../../util/callbacks'; export const txCallbacks = createCallbackManager(); -export function whenTxComplete(toAddress: string, amount: string) { +export function whenTxComplete(toAddress: string, amount: bigint) { return new Promise<{ result: boolean; transaction: ApiTransactionActivity }>((resolve) => { txCallbacks.addCallback( function callback(transaction: ApiTransactionActivity) { - if (transaction.toAddress === toAddress && transaction.amount === `-${amount}`) { + if (transaction.toAddress === toAddress && transaction.amount === -amount) { txCallbacks.removeCallback(callback); resolve({ result: true, transaction }); } diff --git a/src/api/environment.ts b/src/api/environment.ts index 036e1508..14e89d2e 100644 --- a/src/api/environment.ts +++ b/src/api/environment.ts @@ -27,7 +27,7 @@ export function setEnvironment(args: ApiInitArgs) { environment = { ...args, isDappSupported: IS_EXTENSION || IS_CAPACITOR || args.isElectron, - isSseSupported: args.isElectron || IS_CAPACITOR, + isSseSupported: args.isElectron || (IS_CAPACITOR && !args.isNativeBottomSheet), // eslint-disable-next-line no-restricted-globals apiHeaders: { 'X-App-Origin': args.isElectron ? ELECTRON_ORIGIN : self?.origin }, tonhttpapiMainnetKey: args.isElectron ? ELECTRON_TONHTTPAPI_MAINNET_API_KEY : TONHTTPAPI_MAINNET_API_KEY, diff --git a/src/api/extensionMethods/legacy.ts b/src/api/extensionMethods/legacy.ts index ba38074d..9b6fd1b0 100644 --- a/src/api/extensionMethods/legacy.ts +++ b/src/api/extensionMethods/legacy.ts @@ -77,8 +77,9 @@ export async function sendTransaction(params: { const accountId = await getCurrentAccountIdOrFail(); const { - value: amount, to: toAddress, data, dataType, stateInit, + value, to: toAddress, data, dataType, stateInit, } = params; + const amount = BigInt(value); let processedData; if (data) { @@ -162,7 +163,7 @@ async function sendLedgerTransaction( accountId: string, promiseId: string, promise: Promise, - fee: string, + fee: bigint, params: { to: string; value: string; @@ -174,8 +175,9 @@ async function sendLedgerTransaction( const { network } = parseAccountId(accountId); const fromAddress = await fetchStoredAddress(accountId); const { - to: toAddress, value: amount, data, dataType, stateInit, + to: toAddress, value, data, dataType, stateInit, } = params; + const amount = BigInt(value); let payloadBoc: string | undefined; @@ -204,7 +206,7 @@ async function sendLedgerTransaction( type: 'createTransaction', promiseId, toAddress, - amount, + amount: BigInt(amount), fee, ...(dataType === 'text' && { comment: data, diff --git a/src/api/extensionMethods/sites.ts b/src/api/extensionMethods/sites.ts index fab6afcc..60b8d744 100644 --- a/src/api/extensionMethods/sites.ts +++ b/src/api/extensionMethods/sites.ts @@ -73,7 +73,7 @@ export async function prepareTransaction(params: { onPopupUpdate({ type: 'prepareTransaction', toAddress, - amount, + amount: amount ? BigInt(amount) : undefined, comment, }); } diff --git a/src/api/methods/polling.ts b/src/api/methods/polling.ts index 51988a64..63960ad9 100644 --- a/src/api/methods/polling.ts +++ b/src/api/methods/polling.ts @@ -4,6 +4,7 @@ import type { TokenBalanceParsed } from '../blockchains/ton/tokens'; import type { ApiActivity, ApiBackendStakingState, + ApiBalanceBySlug, ApiBaseCurrency, ApiBaseToken, ApiNftUpdate, @@ -72,8 +73,8 @@ const prices: { }; let swapPollingAccountId: string | undefined; const lastBalanceCache: Record; + balance?: bigint; + tokenBalances?: ApiBalanceBySlug; }> = {}; export function initPolling(_onUpdate: OnApiUpdate, _isAccountActive: IsAccountActiveFn) { @@ -158,9 +159,9 @@ export async function setupBalanceBasedPolling(accountId: string, newestTxIds: A // Process TON balance const cache = lastBalanceCache[accountId]; const changedTokenSlugs: string[] = []; - const isTonBalanceChanged = balance && balance !== cache?.balance; + const isTonBalanceChanged = balance !== undefined && balance !== cache?.balance; - const balancesToUpdate: Record = {}; + const balancesToUpdate: ApiBalanceBySlug = {}; if (isTonBalanceChanged) { balancesToUpdate[TON_TOKEN_SLUG] = balance; diff --git a/src/api/methods/staking.ts b/src/api/methods/staking.ts index 0b5f0bc0..a4c41494 100644 --- a/src/api/methods/staking.ts +++ b/src/api/methods/staking.ts @@ -6,6 +6,7 @@ import type { } from '../types'; import { TON_TOKEN_SLUG } from '../../config'; +import { fromDecimal } from '../../util/decimals'; import { logDebugError } from '../../util/logs'; import blockchains from '../blockchains'; import { STAKE_COMMENT, UNSTAKE_COMMENT } from '../blockchains/ton/constants'; @@ -25,14 +26,14 @@ export function initStaking() { // onUpdate = _onUpdate; } -export async function checkStakeDraft(accountId: string, amount: string) { +export async function checkStakeDraft(accountId: string, amount: bigint) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; const backendState = await getBackendStakingState(accountId); return blockchain.checkStakeDraft(accountId, amount, stakingCommonData!, backendState!); } -export async function checkUnstakeDraft(accountId: string, amount: string) { +export async function checkUnstakeDraft(accountId: string, amount: bigint) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; const backendState = await getBackendStakingState(accountId); @@ -40,7 +41,7 @@ export async function checkUnstakeDraft(accountId: string, amount: string) { } export async function submitStake( - accountId: string, password: string, amount: string, type: ApiStakingType, fee?: string, + accountId: string, password: string, amount: bigint, type: ApiStakingType, fee?: bigint, ) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; const fromAddress = await fetchStoredAddress(accountId); @@ -60,7 +61,7 @@ export async function submitStake( fromAddress, toAddress: result.normalizedAddress, comment: STAKE_COMMENT, - fee: fee || '0', + fee: fee || 0n, type: 'stake', slug: TON_TOKEN_SLUG, }); @@ -75,8 +76,8 @@ export async function submitUnstake( accountId: string, password: string, type: ApiStakingType, - amount: string, - fee?: string, + amount: bigint, + fee?: bigint, ) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; const fromAddress = await fetchStoredAddress(accountId); @@ -94,7 +95,7 @@ export async function submitUnstake( fromAddress, toAddress: result.normalizedAddress, comment: UNSTAKE_COMMENT, - fee: fee || '0', + fee: fee || 0n, type: 'unstakeRequest', slug: TON_TOKEN_SLUG, }); @@ -125,6 +126,8 @@ export async function fetchBackendStakingState(address: string): Promise ({ ...transfer, + amount: BigInt(transfer.amount), isBase64Payload: true, })); const result = await ton.checkMultiTransactionDraft(accountId, transferList); @@ -66,12 +68,13 @@ export async function swapBuildTransfer(accountId: string, password: string, par export async function swapSubmit( accountId: string, password: string, - fee: string, + fee: bigint, transfers: ApiSwapTransfer[], historyItem: ApiSwapHistoryItem, ) { const transferList = transfers.map((transfer) => ({ ...transfer, + amount: BigInt(transfer.amount), isBase64Payload: true, })); const result = await ton.submitMultiTransfer(accountId, password, transferList); @@ -98,7 +101,7 @@ export async function swapSubmit( pendingLtRanges.push([lt, lt + SWAP_MAX_LT]); } - whenTxComplete(toAddress, amount) + whenTxComplete(toAddress, fromDecimal(amount)) // TODO .then(({ transaction }) => onTxComplete(transaction)); onUpdate({ diff --git a/src/api/methods/transactions.ts b/src/api/methods/transactions.ts index 16e10d87..5c03372e 100644 --- a/src/api/methods/transactions.ts +++ b/src/api/methods/transactions.ts @@ -45,7 +45,7 @@ export async function fetchAllActivitySlice(accountId: string, lastTxIds: ApiTxI } export function checkTransactionDraft( - accountId: string, slug: string, toAddress: string, amount: string, comment?: string, shouldEncrypt?: boolean, + accountId: string, slug: string, toAddress: string, amount: bigint, comment?: string, shouldEncrypt?: boolean, ) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; @@ -81,7 +81,7 @@ export async function submitTransfer(options: ApiSubmitTransferOptions, shouldCr toAddress: result.normalizedAddress, comment: shouldEncrypt ? undefined : comment, encryptedComment, - fee: fee || '0', + fee: fee || 0n, slug, }); diff --git a/src/api/tonConnect/index.ts b/src/api/tonConnect/index.ts index 25e6ad91..9b8df18a 100644 --- a/src/api/tonConnect/index.ts +++ b/src/api/tonConnect/index.ts @@ -34,6 +34,7 @@ import { parseAccountId } from '../../util/account'; import { isValidLedgerComment } from '../../util/ledger/utils'; import { logDebugError } from '../../util/logs'; import { fetchJsonMetadata } from '../../util/metadata'; +import safeExec from '../../util/safeExec'; import blockchains from '../blockchains'; import { parsePayloadBase64 } from '../blockchains/ton'; import { fetchKeyPair } from '../blockchains/ton/auth'; @@ -88,17 +89,9 @@ export async function connect( id: number, ): Promise { try { - onPopupUpdate({ - type: 'dappLoading', - connectionType: 'connect', - }); - const { origin } = await validateRequest(request, true); - const dapp = { - ...await fetchDappMetadata(message.manifestUrl, origin), - connectedAt: Date.now(), - ...('sseOptions' in request && { sse: request.sseOptions }), - }; + + const dappMetadata = fetchDappMetadata(message.manifestUrl, origin); const addressItem = message.items.find(({ name }) => name === 'ton_addr'); const proofItem = message.items.find(({ name }) => name === 'ton_proof') as TonProofItem | undefined; @@ -112,7 +105,13 @@ export async function connect( throw new errors.BadRequestError("Missing 'ton_addr'"); } - const isOpened = await openExtensionPopup(); + await openExtensionPopup(true); + + onPopupUpdate({ + type: 'dappLoading', + connectionType: 'connect', + }); + let accountId = await getCurrentAccountOrFail(); const isConnected = await isDappConnected(accountId, origin); @@ -123,12 +122,14 @@ export async function connect( } | undefined; if (!isConnected || proof) { - if (!isOpened) { - await openExtensionPopup(true); - } - const { promiseId, promise } = createDappPromise(); + const dapp = { + ...await dappMetadata, + connectedAt: Date.now(), + ...('sseOptions' in request && { sse: request.sseOptions }), + }; + onPopupUpdate({ type: 'dappConnect', promiseId, @@ -166,6 +167,13 @@ export async function connect( return result; } catch (err) { logDebugError('tonConnect:connect', err); + + safeExec(() => { + onPopupUpdate({ + type: 'dappCloseLoading', + }); + }); + return formatConnectError(id, err as Error); } } @@ -216,11 +224,6 @@ export async function sendTransaction( message: SendTransactionRpcRequest, ): Promise { try { - onPopupUpdate({ - type: 'dappLoading', - connectionType: 'sendTransaction', - }); - const { origin, accountId } = await validateRequest(request); const txPayload = JSON.parse(message.params[0]) as TransactionPayload; @@ -237,6 +240,11 @@ export async function sendTransaction( await openExtensionPopup(true); + onPopupUpdate({ + type: 'dappLoading', + connectionType: 'sendTransaction', + }); + const { preparedMessages, checkResult } = await checkTransactionMessages(accountId, messages); const dapp = (await getDappsByOrigin(accountId))[origin]; @@ -317,6 +325,12 @@ export async function sendTransaction( } catch (err) { logDebugError('tonConnect:sendTransaction', err); + safeExec(() => { + onPopupUpdate({ + type: 'dappCloseLoading', + }); + }); + let code = SEND_TRANSACTION_ERROR_CODES.UNKNOWN_ERROR; let errorMessage = 'Unhandled error'; let displayError: ApiAnyDisplayError | undefined; @@ -372,7 +386,7 @@ async function checkTransactionMessages(accountId: string, messages: Transaction return { toAddress: address, - amount, + amount: BigInt(amount), payload: payload ? ton.oneCellFromBoc(base64ToBytes(payload)) : undefined, stateInit: stateInit ? ton.oneCellFromBoc(base64ToBytes(stateInit)) : undefined, }; @@ -420,7 +434,7 @@ function prepareTransactionForRequest(network: ApiNetwork, messages: Transaction return { toAddress, - amount, + amount: BigInt(amount), rawPayload, payload, stateInit, diff --git a/src/api/types/backend.ts b/src/api/types/backend.ts index 515b079b..9fa9a0ec 100644 --- a/src/api/types/backend.ts +++ b/src/api/types/backend.ts @@ -125,7 +125,7 @@ export type ApiStakingCommonData = { nextRoundRate: number; collection?: string; apy: number; - available: string; + available: bigint; loyaltyApy: { [key in ApiLoyaltyType]: number; }; @@ -140,4 +140,5 @@ export type ApiStakingCommonData = { end: number; unlock: number; }; + bigInt: bigint; }; diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 57428b01..9df90f52 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -17,6 +17,7 @@ export interface AccountIdParsed { export interface ApiInitArgs { isElectron: boolean; + isNativeBottomSheet: boolean; } export interface ApiBaseToken { @@ -59,12 +60,12 @@ export type ApiTransactionType = 'stake' | 'unstake' | 'unstakeRequest' | 'swap' export interface ApiTransaction { txId: string; timestamp: number; - amount: string; + amount: bigint; fromAddress: string; toAddress: string; comment?: string; encryptedComment?: string; - fee: string; + fee: bigint; slug: string; isIncoming: boolean; type?: ApiTransactionType; @@ -93,16 +94,16 @@ export type ApiStakingType = 'nominators' | 'liquid'; export type ApiStakingState = { type: 'nominators'; - amount: number; - pendingDepositAmount: number; + amount: bigint; + pendingDepositAmount: bigint; isUnstakeRequested: boolean; } | { type: 'liquid'; - tokenAmount: string; - amount: number; - unstakeRequestAmount: number; + tokenAmount: bigint; + amount: bigint; + unstakeRequestAmount: bigint; apy: number; - instantAvailable: string; + instantAvailable: bigint; }; export interface ApiNominatorsPool { @@ -113,10 +114,11 @@ export interface ApiNominatorsPool { } export interface ApiBackendStakingState { - balance: number; - totalProfit: number; + balance: bigint; + totalProfit: bigint; nominatorsPool: ApiNominatorsPool; loyaltyType?: ApiLoyaltyType; + shouldUseNominators?: boolean; } export type ApiStakingHistory = { @@ -140,7 +142,7 @@ export type ApiDappRequest = { export interface ApiDappTransaction { toAddress: string; - amount: string; + amount: bigint; rawPayload?: string; payload?: ApiParsedPayload; stateInit?: string; @@ -151,9 +153,9 @@ export interface ApiSubmitTransferOptions { password: string; slug: string; toAddress: string; - amount: string; + amount: bigint; comment?: string; - fee?: string; + fee?: bigint; shouldEncrypt?: boolean; } @@ -182,3 +184,5 @@ export enum ApiLiquidUnstakeMode { } export type ApiLoyaltyType = 'black' | 'platinum' | 'gold' | 'silver' | 'standard'; + +export type ApiBalanceBySlug = Record; diff --git a/src/api/types/payload.ts b/src/api/types/payload.ts index cf2c0780..39dbca3e 100644 --- a/src/api/types/payload.ts +++ b/src/api/types/payload.ts @@ -10,11 +10,11 @@ export type ApiEncryptedCommentPayload = { export type ApiNftTransferPayload = { type: 'nft:transfer'; - queryId: string; + queryId: bigint; newOwner: string; responseDestination: string; customPayload?: string; - forwardAmount: string; + forwardAmount: bigint; forwardPayload?: string; // Specific to UI nftAddress: string; @@ -23,12 +23,12 @@ export type ApiNftTransferPayload = { export type ApiTokensTransferPayload = { type: 'tokens:transfer'; - queryId: string; - amount: string; + queryId: bigint; + amount: bigint; destination: string; responseDestination: string; customPayload?: string; - forwardAmount: string; + forwardAmount: bigint; forwardPayload?: string; // Specific to UI slug: string; @@ -36,8 +36,8 @@ export type ApiTokensTransferPayload = { export type ApiTokensTransferNonStandardPayload = { type: 'tokens:transfer-non-standard'; - queryId: string; - amount: string; + queryId: bigint; + amount: bigint; destination: string; // Specific to UI slug: string; @@ -50,8 +50,8 @@ export type ApiUnknownPayload = { export type ApiTokensBurnPayload = { type: 'tokens:burn'; - queryId: string; - amount: string; + queryId: bigint; + amount: bigint; address: string; customPayload?: string; // Specific to UI @@ -61,17 +61,17 @@ export type ApiTokensBurnPayload = { export type ApiLiquidStakingDepositPayload = { type: 'liquid-staking:deposit'; - queryId: string; + queryId: bigint; }; export type ApiLiquidStakingWithdrawalNftPayload = { type: 'liquid-staking:withdrawal-nft'; - queryId: string; + queryId: bigint; }; export type ApiLiquidStakingWithdrawalPayload = { type: 'liquid-staking:withdrawal'; - queryId: string; + queryId: bigint; }; export type ApiParsedPayload = ApiCommentPayload diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 28638f9a..889988a2 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -4,6 +4,7 @@ import type { ApiStakingCommonData, ApiSwapAsset } from './backend'; import type { ApiAnyDisplayError } from './errors'; import type { ApiBackendStakingState, + ApiBalanceBySlug, ApiBaseCurrency, ApiDappTransaction, ApiNft, @@ -13,17 +14,10 @@ import type { import type { ApiParsedPayload } from './payload'; import type { ApiAccount, ApiDapp } from './storage'; -export type ApiUpdateBalance = { - type: 'updateBalance'; - accountId: string; - slug: string; - balance: string; -}; - export type ApiUpdateBalances = { type: 'updateBalances'; accountId: string; - balancesToUpdate: Record; + balancesToUpdate: ApiBalanceBySlug; }; export type ApiUpdateNewActivities = { @@ -53,8 +47,8 @@ export type ApiUpdateCreateTransaction = { type: 'createTransaction'; promiseId: string; toAddress: string; - amount: string; - fee: string; + amount: bigint; + fee: bigint; comment?: string; rawPayload?: string; parsedPayload?: ApiParsedPayload; @@ -92,7 +86,7 @@ export type ApiUpdateDappSendTransactions = { accountId: string; dapp: ApiDapp; transactions: ApiDappTransaction[]; - fee: string; + fee: bigint; }; export type ApiUpdateDappConnect = { @@ -118,10 +112,14 @@ export type ApiUpdateDappLoading = { connectionType: 'connect' | 'sendTransaction'; }; +export type ApiUpdateDappCloseLoading = { + type: 'dappCloseLoading'; +}; + export type ApiUpdatePrepareTransaction = { type: 'prepareTransaction'; toAddress: string; - amount?: string; + amount?: bigint; comment?: string; }; @@ -164,7 +162,6 @@ export type ApiUpdateRegion = { }; export type ApiUpdate = - ApiUpdateBalance | ApiUpdateBalances | ApiUpdateNewActivities | ApiUpdateNewLocalTransaction @@ -178,6 +175,7 @@ export type ApiUpdate = | ApiUpdateDappConnect | ApiUpdateDappDisconnect | ApiUpdateDappLoading + | ApiUpdateDappCloseLoading | ApiUpdatePrepareTransaction | ApiUpdateShowError | ApiUpdateNfts diff --git a/src/components/App.tsx b/src/components/App.tsx index cddaa763..7940b24e 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,5 +1,7 @@ import { BarcodeScanner } from '@capacitor-mlkit/barcode-scanning'; -import React, { memo, useEffect, useState } from '../lib/teact/teact'; +import React, { + memo, useEffect, useLayoutEffect, useState, +} from '../lib/teact/teact'; import { getActions, withGlobal } from '../global'; import { AppState } from '../global/types'; @@ -117,6 +119,10 @@ function App({ }); }, [markInactive]); + useLayoutEffect(() => { + document.documentElement.classList.add('is-rendered'); + }, []); + useSyncEffect(() => { if (accountId) { mainKey += 1; diff --git a/src/components/common/SwapResult.tsx b/src/components/common/SwapResult.tsx index 258defb1..f9d828a6 100644 --- a/src/components/common/SwapResult.tsx +++ b/src/components/common/SwapResult.tsx @@ -18,8 +18,8 @@ import styles from './SwapResult.module.scss'; interface OwnProps { tokenIn?: UserSwapToken; tokenOut?: UserSwapToken; - amountIn?: number; - amountOut?: number; + amountIn?: string; + amountOut?: string; playAnimation?: boolean; firstButtonText?: string; secondButtonText?: string; diff --git a/src/components/common/SwapTokensInfo.tsx b/src/components/common/SwapTokensInfo.tsx index 56ff90c4..b754b0b4 100644 --- a/src/components/common/SwapTokensInfo.tsx +++ b/src/components/common/SwapTokensInfo.tsx @@ -12,16 +12,16 @@ import styles from './SwapTokensInfo.module.scss'; interface OwnProps { tokenIn?: UserSwapToken | ApiSwapAsset; - amountIn?: number; + amountIn?: string; tokenOut?: UserSwapToken | ApiSwapAsset; - amountOut?: number; + amountOut?: string; isError?: boolean; } function SwapTokensInfo({ tokenIn, amountIn, tokenOut, amountOut, isError = false, }: OwnProps) { - function renderTokenInfo(token?: UserSwapToken | ApiSwapAsset, amount = 0, isReceived = false) { + function renderTokenInfo(token?: UserSwapToken | ApiSwapAsset, amount = '0', isReceived = false) { const image = token?.image ?? ASSET_LOGO_PATHS[token?.symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS]; const amountWithSign = isReceived ? amount : -amount; return ( diff --git a/src/components/common/TokenSelector.tsx b/src/components/common/TokenSelector.tsx index 4e857aff..5593fac0 100644 --- a/src/components/common/TokenSelector.tsx +++ b/src/components/common/TokenSelector.tsx @@ -3,7 +3,7 @@ import React, { } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; -import type { ApiBaseCurrency, ApiToken } from '../../api/types'; +import type { ApiBalanceBySlug, ApiBaseCurrency } from '../../api/types'; import { type AssetPairs, SettingsState, @@ -12,7 +12,6 @@ import { } from '../../global/types'; import { ANIMATED_STICKER_MIDDLE_SIZE_PX, TON_BLOCKCHAIN } from '../../config'; -import { Big } from '../../lib/big.js/index.js'; import { selectAvailableUserForSwapTokens, selectCurrentAccountState, @@ -20,10 +19,12 @@ import { selectSwapTokens, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import { toDecimal } from '../../util/decimals'; import { formatCurrency, getShortCurrencySymbol, } from '../../util/formatNumber'; import { getIsAddressValid } from '../../util/getIsAddressValid'; +import { disableSwipeToClose, enableSwipeToClose } from '../../util/modalSwipeManager'; import getBlockchainNetworkIcon from '../../util/swap/getBlockchainNetworkIcon'; import getBlockchainNetworkName from '../../util/swap/getBlockchainNetworkName'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; @@ -53,8 +54,7 @@ interface StateProps { swapTokens?: UserSwapToken[]; tokenInSlug?: string; pairsBySlug?: Record; - balancesBySlug?: Record; - tokenInfoBySlug?: Record; + balancesBySlug?: ApiBalanceBySlug; baseCurrency?: ApiBaseCurrency; isLoading?: boolean; } @@ -88,7 +88,6 @@ function TokenSelector({ tokenInSlug, pairsBySlug, balancesBySlug, - tokenInfoBySlug, isActive, isLoading, onBack, @@ -118,6 +117,14 @@ function TokenSelector({ onBack, }); + useEffect(() => { + if (!isActive) return undefined; + + disableSwipeToClose(); + + return enableSwipeToClose; + }, [isActive]); + useFocusAfterAnimation(searchInputRef, !isActive); const { @@ -190,7 +197,7 @@ function TokenSelector({ const isKeyword = keywords?.some((key) => key.toLowerCase().includes(lowerCaseSearchValue)); return isName || isSymbol || isKeyword; - }).sort((a, b) => b.amount - a.amount) ?? []; + }).sort((a, b) => Number(b.amount - a.amount)) ?? []; }, [allUnimportedTonTokens, isInsideSettings, searchValue, swapTokensWithFilter]); const resetSearch = () => { @@ -295,17 +302,12 @@ function TokenSelector({ currentToken?.symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS ] ?? currentToken?.image; const blockchain = 'blockchain' in currentToken ? currentToken.blockchain : TON_BLOCKCHAIN; - const price = 'price' in currentToken - ? currentToken.price - : tokenInfoBySlug?.[currentToken.slug] - ? tokenInfoBySlug?.[currentToken.slug].quote.price - : 0; const isAvailable = !shouldFilter || currentToken.canSwap; const descriptionText = isAvailable ? getBlockchainNetworkName(blockchain) : lang('Unavailable'); - const currencyHoldings = Big(price).mul(currentToken.amount); + const currencyHoldings = currentToken.totalValue; const handleClick = isAvailable ? () => handleTokenClick(currentToken) : undefined; return ( @@ -354,14 +356,14 @@ function TokenSelector({ !isAvailable && styles.tokenTextDisabled, )} > - {formatCurrency(currentToken.amount, currentToken.symbol)} + {formatCurrency(toDecimal(currentToken.amount, token?.decimals), currentToken.symbol)} - {formatCurrency(currencyHoldings.toNumber(), shortBaseSymbol)} + {formatCurrency(currencyHoldings, shortBaseSymbol)} @@ -527,7 +529,6 @@ export default memo(withGlobal((global): StateProps => { baseCurrency, pairsBySlug: pairs?.bySlug, balancesBySlug: balances?.bySlug, - tokenInfoBySlug: global.tokenInfo.bySlug, }; })(TokenSelector)); diff --git a/src/components/common/TransactionAmount.tsx b/src/components/common/TransactionAmount.tsx index 1cf81eba..013a8c2a 100644 --- a/src/components/common/TransactionAmount.tsx +++ b/src/components/common/TransactionAmount.tsx @@ -7,7 +7,7 @@ import { formatCurrency, formatCurrencyExtended } from '../../util/formatNumber' import styles from './TransactionAmount.module.scss'; interface OwnProps { - amount: number; + amount: string; isIncoming?: boolean; isScam?: boolean; tokenSymbol?: string; diff --git a/src/components/common/TransferResult.tsx b/src/components/common/TransferResult.tsx index a1706fe2..88a48be6 100644 --- a/src/components/common/TransferResult.tsx +++ b/src/components/common/TransferResult.tsx @@ -1,7 +1,8 @@ import React, { memo } from '../../lib/teact/teact'; -import { TON_SYMBOL } from '../../config'; +import { DEFAULT_DECIMAL_PLACES, TON_SYMBOL } from '../../config'; import buildClassName from '../../util/buildClassName'; +import { toDecimal } from '../../util/decimals'; import { formatCurrency, formatCurrencyExtended } from '../../util/formatNumber'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; @@ -14,12 +15,13 @@ interface OwnProps { playAnimation?: boolean; color?: 'green' | 'purple'; noSign?: boolean; - amount?: number; + amount?: bigint; tokenSymbol?: string; + decimals?: number; precision?: number; - balance?: number; - operationAmount?: number; - fee?: number; + balance?: bigint; + operationAmount?: bigint; + fee?: bigint; firstButtonText?: string; secondButtonText?: string; onFirstButtonClick?: NoneToVoidFunction; @@ -28,8 +30,9 @@ interface OwnProps { function TransferResult({ playAnimation, - amount = 0, + amount = 0n, tokenSymbol = TON_SYMBOL, + decimals = DEFAULT_DECIMAL_PLACES, precision = 2, noSign, color, @@ -42,11 +45,13 @@ function TransferResult({ onSecondButtonClick, }: OwnProps) { const withBalanceChange = Boolean(balance !== undefined && operationAmount); - let finalBalance = withBalanceChange ? balance! + operationAmount! : 0; + let finalBalance = withBalanceChange ? balance! + operationAmount! : 0n; if (finalBalance && fee && tokenSymbol === TON_SYMBOL) { finalBalance -= fee; } - const [wholePart, fractionPart] = formatCurrencyExtended(amount, '', noSign).split('.'); + + const amountString = toDecimal(amount, decimals); + const [wholePart, fractionPart] = formatCurrencyExtended(amountString, '', noSign).split('.'); function renderButtons() { if (!firstButtonText && !secondButtonText) { @@ -78,9 +83,9 @@ function TransferResult({ {Boolean(withBalanceChange) && (
- {formatCurrency(balance!, tokenSymbol, precision)} + {formatCurrency(toDecimal(balance!, decimals), tokenSymbol, precision)}  →  - {formatCurrency(finalBalance, tokenSymbol, precision)} + {formatCurrency(toDecimal(finalBalance!, decimals), tokenSymbol, precision)}
)} diff --git a/src/components/dapps/DappLedgerWarning.tsx b/src/components/dapps/DappLedgerWarning.tsx index 99fc731a..3c98ef79 100644 --- a/src/components/dapps/DappLedgerWarning.tsx +++ b/src/components/dapps/DappLedgerWarning.tsx @@ -8,6 +8,7 @@ import { type Account, TransferState, type UserToken } from '../../global/types' import renderText from '../../global/helpers/renderText'; import { selectNetworkAccounts } from '../../global/selectors'; +import { toDecimal } from '../../util/decimals'; import { formatCurrency } from '../../util/formatNumber'; import useLang from '../../hooks/useLang'; @@ -46,7 +47,7 @@ function DappLedgerWarning({
{currentAccount?.title}
-
{formatCurrency(tonToken.amount, tonToken.symbol)}
+
{formatCurrency(toDecimal(tonToken.amount), tonToken.symbol)}
diff --git a/src/components/dapps/DappTransferInitial.tsx b/src/components/dapps/DappTransferInitial.tsx index 10226308..68e01792 100644 --- a/src/components/dapps/DappTransferInitial.tsx +++ b/src/components/dapps/DappTransferInitial.tsx @@ -8,9 +8,9 @@ import type { ApiDapp, ApiDappTransaction } from '../../api/types'; import type { Account, UserToken } from '../../global/types'; import { SHORT_FRACTION_DIGITS } from '../../config'; -import { bigStrToHuman } from '../../global/helpers'; import { selectCurrentAccountTokens, selectNetworkAccounts } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import { toDecimal } from '../../util/decimals'; import { formatCurrency } from '../../util/formatNumber'; import { shortenAddress } from '../../util/shortenAddress'; @@ -32,7 +32,7 @@ interface OwnProps { interface StateProps { currentAccount?: Account; transactions?: ApiDappTransaction[]; - fee?: string; + fee?: bigint; dapp?: ApiDapp; isLoading?: boolean; tokens?: UserToken[]; @@ -55,16 +55,16 @@ function DappTransferInitial({ const totalAmount = useMemo(() => { return renderingTransactions?.reduce((acc, { amount }) => { - return acc + bigStrToHuman(amount, tonToken.decimals); - }, fee ? bigStrToHuman(fee, tonToken.decimals) : 0) || 0; - }, [renderingTransactions, fee, tonToken.decimals]); + return acc + amount; + }, fee ?? 0n) || 0n; + }, [renderingTransactions, fee]); function renderDapp() { return (
{currentAccount?.title}
-
{formatCurrency(tonToken.amount, tonToken.symbol)}
+
{formatCurrency(toDecimal(tonToken.amount), tonToken.symbol)}
token.slug === slug)!; - extraText = `${formatCurrency(bigStrToHuman(amount, decimals), symbol, SHORT_FRACTION_DIGITS)} + `; + extraText = `${formatCurrency(toDecimal(amount, decimals), symbol, SHORT_FRACTION_DIGITS)} + `; } return ( @@ -108,7 +108,7 @@ function DappTransferInitial({ > {extraText} - {formatCurrency(bigStrToHuman(transaction.amount, tonToken.decimals), tonToken.symbol, SHORT_FRACTION_DIGITS)} + {formatCurrency(toDecimal(transaction.amount), tonToken.symbol, SHORT_FRACTION_DIGITS)} {' '} @@ -130,7 +130,7 @@ function DappTransferInitial({
diff --git a/src/components/ledger/LedgerSelectWallets.tsx b/src/components/ledger/LedgerSelectWallets.tsx index 2c0a975c..f866b21c 100644 --- a/src/components/ledger/LedgerSelectWallets.tsx +++ b/src/components/ledger/LedgerSelectWallets.tsx @@ -6,8 +6,8 @@ import { getActions } from '../../global'; import type { Account } from '../../global/types'; import type { LedgerWalletInfo } from '../../util/ledger/types'; -import { bigStrToHuman } from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; +import { toDecimal } from '../../util/decimals'; import { formatCurrency } from '../../util/formatNumber'; import { shortenAddress } from '../../util/shortenAddress'; @@ -89,7 +89,7 @@ function LedgerSelectWallets({ ); } - function renderAccount(address: string, balance: string, index: number, isConnected: boolean) { + function renderAccount(address: string, balance: bigint, index: number, isConnected: boolean) { const isActiveAccount = isConnected || selectedAccountIndices.includes(index); return ( @@ -100,7 +100,7 @@ function LedgerSelectWallets({ > - {formatCurrency(bigStrToHuman(balance), '', ACCOUNT_BALANCE_DECIMALS)} + {formatCurrency(toDecimal(balance), '', ACCOUNT_BALANCE_DECIMALS)}
diff --git a/src/components/main/modals/SwapActivityModal.tsx b/src/components/main/modals/SwapActivityModal.tsx index 9e92d7c2..32cf9d42 100644 --- a/src/components/main/modals/SwapActivityModal.tsx +++ b/src/components/main/modals/SwapActivityModal.tsx @@ -66,8 +66,8 @@ function SwapActivityModal({ activity, tokensBySlug }: StateProps) { let fromToken: ApiSwapAsset | undefined; let toToken: ApiSwapAsset | undefined; - let fromAmount = 0; - let toAmount = 0; + let fromAmount = '0'; + let toAmount = '0'; let isPending = true; let isError = false; let isCexError = false; @@ -83,8 +83,8 @@ function SwapActivityModal({ activity, tokensBySlug }: StateProps) { } = renderedActivity; fromToken = tokensBySlug?.[from]; toToken = tokensBySlug?.[to]; - fromAmount = Number(renderedActivity.fromAmount); - toAmount = Number(renderedActivity.toAmount); + fromAmount = renderedActivity.fromAmount; + toAmount = renderedActivity.toAmount; const isFromTon = from === TON_TOKEN_SLUG; diff --git a/src/components/main/modals/TransactionModal.module.scss b/src/components/main/modals/TransactionModal.module.scss index 1c1b058a..a70ab84a 100644 --- a/src/components/main/modals/TransactionModal.module.scss +++ b/src/components/main/modals/TransactionModal.module.scss @@ -1,11 +1,28 @@ -.passwordModal { - min-height: 24.5rem; +.modalDialog { + height: 30rem; + + @supports (height: env(safe-area-inset-bottom)) { + height: calc(30rem + env(safe-area-inset-bottom)); + } + + :global(.is-native-bottom-sheet) & { + height: calc(30rem + var(--safe-area-bottom, 0rem)) !important; + } + } .passwordFormContent { padding: 0 !important; } +.passwordFormContentInModal { + padding: 0 1rem 1rem; + + @supports (padding-bottom: env(safe-area-inset-bottom)) { + padding-bottom: max(env(safe-area-inset-bottom), 1rem); + } +} + .sticker { margin: -0.5rem auto 1.25rem; } diff --git a/src/components/main/modals/TransactionModal.tsx b/src/components/main/modals/TransactionModal.tsx index d2d96f12..3813f417 100644 --- a/src/components/main/modals/TransactionModal.tsx +++ b/src/components/main/modals/TransactionModal.tsx @@ -13,11 +13,13 @@ import { TONSCAN_BASE_MAINNET_URL, TONSCAN_BASE_TESTNET_URL, } from '../../../config'; -import { bigStrToHuman, getIsTxIdLocal } from '../../../global/helpers'; +import { getIsTxIdLocal } from '../../../global/helpers'; import { selectCurrentAccountState } from '../../../global/selectors'; +import { bigintAbs } from '../../../util/bigint'; import buildClassName from '../../../util/buildClassName'; import { vibrateOnSuccess } from '../../../util/capacitor'; import { formatFullDay, formatRelativeHumanDateTime, formatTime } from '../../../util/dateFormat'; +import { toDecimal } from '../../../util/decimals'; import resolveModalTransitionName from '../../../util/resolveModalTransitionName'; import { callApi } from '../../../api'; @@ -105,7 +107,6 @@ function TransactionModal({ const isStaking = Boolean(transaction?.type); const token = slug ? tokensBySlug?.[slug] : undefined; - const amountHuman = amount ? bigStrToHuman(amount, token?.decimals) : 0; const address = isIncoming ? fromAddress : toAddress; const addressName = (address && savedAddresses?.[address]) || transaction?.metadata?.name; const isScam = Boolean(transaction?.metadata?.isScam); @@ -164,7 +165,7 @@ function TransactionModal({ isPortrait, tokenSlug: slug || TON_TOKEN_SLUG, toAddress: address, - amount: Math.abs(amountHuman), + amount: bigintAbs(amount!), comment: !isIncoming ? comment : undefined, }); }); @@ -264,7 +265,7 @@ function TransactionModal({ return ( ); @@ -314,7 +315,7 @@ function TransactionModal({ @@ -386,7 +387,7 @@ function TransactionModal({ placeholder={lang('Enter your password')} error={passwordError} withCloseButton={IS_CAPACITOR} - containerClassName={styles.passwordFormContent} + containerClassName={IS_CAPACITOR ? styles.passwordFormContent : styles.passwordFormContentInModal} onSubmit={handlePasswordSubmit} onCancel={closePasswordSlide} onUpdate={clearPasswordError} @@ -404,6 +405,7 @@ function TransactionModal({ forceFullNative={currentSlide === SLIDES.password} dialogClassName={styles.modalDialog} onClose={handleClose} + onCloseAnimationEnd={closePasswordSlide} > )}
- {primaryValue !== 0 && ( + {primaryValue !== '0' && (
{changePrefix}   @@ -222,6 +227,9 @@ export default memo( (global): StateProps => { const { address } = selectAccount(global, global.currentAccountId!) || {}; const accountState = selectCurrentAccountState(global); + const stakingBalance = selectCurrentNetwork(global) === 'mainnet' + ? accountState?.staking?.balance + : 0n; return { address, @@ -230,7 +238,7 @@ export default memo( currentTokenSlug: accountState?.currentTokenSlug, isTestnet: global.settings.isTestnet, baseCurrency: global.settings.baseCurrency, - stakingBalance: accountState?.staking?.balance, + stakingBalance, }; }, (global, _, stickToFirst) => stickToFirst(global.currentAccountId), diff --git a/src/components/main/sections/Card/StickyCard.tsx b/src/components/main/sections/Card/StickyCard.tsx index d23030ae..5b7d13df 100644 --- a/src/components/main/sections/Card/StickyCard.tsx +++ b/src/components/main/sections/Card/StickyCard.tsx @@ -22,7 +22,7 @@ interface OwnProps { interface StateProps { tokens?: UserToken[]; baseCurrency?: ApiBaseCurrency; - stakingBalance?: number; + stakingBalance?: bigint; } function StickyCard({ diff --git a/src/components/main/sections/Card/TokenCard.tsx b/src/components/main/sections/Card/TokenCard.tsx index d9f14cf3..2224eaa5 100644 --- a/src/components/main/sections/Card/TokenCard.tsx +++ b/src/components/main/sections/Card/TokenCard.tsx @@ -9,6 +9,7 @@ import { selectCurrentAccountState } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; import { calcChangeValue } from '../../../../util/calcChangeValue'; import { formatShortDay } from '../../../../util/dateFormat'; +import { toBig, toDecimal } from '../../../../util/decimals'; import { formatCurrency, getShortCurrencySymbol } from '../../../../util/formatNumber'; import { round } from '../../../../util/round'; import { ASSET_LOGO_PATHS } from '../../../ui/helpers/assetLogos'; @@ -73,7 +74,7 @@ function TokenCard({ const [selectedHistoryIndex, setSelectedHistoryIndex] = useState(-1); const { - slug, symbol, amount, image, name, price: lastPrice, + slug, symbol, amount, image, name, price: lastPrice, decimals, } = token; const logoPath = slug === TON_TOKEN_SLUG @@ -99,9 +100,9 @@ function TokenCard({ ? token[currentChangePeriod] : undefined; - const value = amount * price; + const value = toBig(amount, decimals).mul(price).toString(); const changePrefix = change === undefined ? change : change > 0 ? '↑' : change < 0 ? '↓' : 0; - const changeValue = change ? Math.abs(round(calcChangeValue(value, change), 4)) : 0; + const changeValue = change ? Math.abs(round(calcChangeValue(Number(value), change), 4)) : 0; const changePercent = change ? Math.abs(round(change * 100, 2)) : 0; const withChange = Boolean(change !== undefined); @@ -131,7 +132,7 @@ function TokenCard({ {token.name}
- {formatCurrency(amount, symbol)} + {formatCurrency(toDecimal(amount, token.decimals), symbol)} {name} {token.slug === TON_TOKEN_SLUG && ( diff --git a/src/components/main/sections/Card/helpers/calculateFullBalance.ts b/src/components/main/sections/Card/helpers/calculateFullBalance.ts index 6e391d47..8157371b 100644 --- a/src/components/main/sections/Card/helpers/calculateFullBalance.ts +++ b/src/components/main/sections/Card/helpers/calculateFullBalance.ts @@ -1,30 +1,37 @@ import type { UserToken } from '../../../../../global/types'; import { TON_TOKEN_SLUG } from '../../../../../config'; -import { calcChangeValue } from '../../../../../util/calcChangeValue'; +import { Big } from '../../../../../lib/big.js'; +import { calcBigChangeValue } from '../../../../../util/calcChangeValue'; +import { toBig } from '../../../../../util/decimals'; import { formatInteger } from '../../../../../util/formatNumber'; -import { round } from '../../../../../util/round'; +import { round } from '../../../../../util/math'; import styles from '../Card.module.scss'; -export function calculateFullBalance(tokens: UserToken[], stakingBalance = 0) { +export function calculateFullBalance(tokens: UserToken[], stakingBalance = 0n) { const primaryValue = tokens.reduce((acc, token) => { - const amount = token.slug === TON_TOKEN_SLUG ? token.amount + stakingBalance : token.amount; - return acc + amount * token.price; - }, 0); + if (token.slug === TON_TOKEN_SLUG) { + const stakingAmount = toBig(stakingBalance, token.decimals).mul(token.price); + acc = acc.plus(stakingAmount); + } + + return acc.plus(token.totalValue); + }, Big(0)); + const [primaryWholePart, primaryFractionPart] = formatInteger(primaryValue).split('.'); - const changeValue = round(tokens.reduce((acc, token) => { - return acc + calcChangeValue(token.amount * token.price, token.change24h); - }, 0), 4); + const changeValue = tokens.reduce((acc, token) => { + return acc.plus(calcBigChangeValue(token.totalValue, token.change24h)); + }, Big(0)).round(4).toNumber(); - const changePercent = round(primaryValue ? (changeValue / (primaryValue - changeValue)) * 100 : 0, 2); + const changePercent = round(primaryValue ? (changeValue / (primaryValue.toNumber() - changeValue)) * 100 : 0, 2); const changeClassName = changePercent > 0 ? styles.changeCourseUp : (changePercent < 0 ? styles.changeCourseDown : undefined); const changePrefix = changeValue > 0 ? '↑' : changeValue < 0 ? '↓' : undefined; return { - primaryValue, + primaryValue: primaryValue.toString(), primaryWholePart, primaryFractionPart, changeClassName, diff --git a/src/components/main/sections/Content/Assets.tsx b/src/components/main/sections/Content/Assets.tsx index 62d6f68c..e36fc6ab 100644 --- a/src/components/main/sections/Content/Assets.tsx +++ b/src/components/main/sections/Content/Assets.tsx @@ -7,6 +7,7 @@ import type { UserToken } from '../../../../global/types'; import { TON_TOKEN_SLUG } from '../../../../config'; import { selectCurrentAccountState, selectCurrentAccountTokens, selectIsNewWallet } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; +import { toDecimal } from '../../../../util/decimals'; import { useDeviceScreen } from '../../../../hooks/useDeviceScreen'; import useShowTransition from '../../../../hooks/useShowTransition'; @@ -28,7 +29,7 @@ interface StateProps { tokens?: UserToken[]; isNewWallet: boolean; stakingStatus?: 'active' | 'unstakeRequested'; - stakingBalance?: number; + stakingBalance?: bigint; isInvestorViewEnabled?: boolean; apyValue: number; currentTokenSlug?: string; @@ -64,7 +65,7 @@ function Assets({ token={tonToken!} stakingStatus={stakingStatus} apyValue={apyValue} - amount={stakingBalance} + amount={stakingBalance === undefined ? undefined : toDecimal(stakingBalance)} isInvestorView={isInvestorViewEnabled} classNames={stakedTokenClassNames} onClick={onStakedTokenClick} diff --git a/src/components/main/sections/Content/Token.tsx b/src/components/main/sections/Content/Token.tsx index 76fb266e..f9a2f5a9 100644 --- a/src/components/main/sections/Content/Token.tsx +++ b/src/components/main/sections/Content/Token.tsx @@ -4,8 +4,10 @@ import type { ApiBaseCurrency } from '../../../../api/types'; import type { UserToken } from '../../../../global/types'; import { TON_TOKEN_SLUG } from '../../../../config'; +import { Big } from '../../../../lib/big.js'; import buildClassName from '../../../../util/buildClassName'; import { calcChangeValue } from '../../../../util/calcChangeValue'; +import { toDecimal } from '../../../../util/decimals'; import { formatCurrency, getShortCurrencySymbol } from '../../../../util/formatNumber'; import { round } from '../../../../util/round'; import { ASSET_LOGO_PATHS } from '../../../ui/helpers/assetLogos'; @@ -21,7 +23,7 @@ import styles from './Token.module.scss'; interface OwnProps { token: UserToken; stakingStatus?: 'active' | 'unstakeRequested'; - amount?: number; + amount?: string; isInvestorView?: boolean; classNames?: string; apyValue?: number; @@ -49,13 +51,14 @@ function Token({ price, change24h: change, image, + decimals, } = token; - const renderedAmount = amount ?? tokenAmount; + const renderedAmount = amount ?? toDecimal(tokenAmount, decimals); const logoPath = image || ASSET_LOGO_PATHS[symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS]; - const value = renderedAmount * price; + const value = Big(renderedAmount).mul(price).toString(); const changeClassName = change > 0 ? styles.change_up : change < 0 ? styles.change_down : undefined; - const changeValue = Math.abs(round(calcChangeValue(value, change), 4)); + const changeValue = Math.abs(round(calcChangeValue(Number(value), change), 4)); const changePercent = Math.abs(round(change * 100, 2)); const withApy = Boolean(apyValue) && slug === TON_TOKEN_SLUG; const fullClassName = buildClassName(styles.container, isActive && styles.active, classNames); @@ -131,7 +134,7 @@ function Token({ } function renderDefaultView() { - const totalAmount = renderedAmount * price; + const totalAmount = Big(renderedAmount).mul(price); const canRenderApy = Boolean(apyValue) && slug === TON_TOKEN_SLUG; return ( @@ -168,7 +171,7 @@ function Token({ />
- {totalAmount > 0 ? '≈' : ''}  + {totalAmount.gt(0) ? '≈' : ''} 
diff --git a/src/components/main/sections/Content/Transaction.tsx b/src/components/main/sections/Content/Transaction.tsx index c2bd0103..0e914d7a 100644 --- a/src/components/main/sections/Content/Transaction.tsx +++ b/src/components/main/sections/Content/Transaction.tsx @@ -4,9 +4,11 @@ import React, { memo } from '../../../../lib/teact/teact'; import type { ApiToken, ApiTransactionActivity } from '../../../../api/types'; import { TON_SYMBOL } from '../../../../config'; -import { bigStrToHuman, getIsTxIdLocal } from '../../../../global/helpers'; +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 { shortenAddress } from '../../../../util/shortenAddress'; @@ -62,7 +64,6 @@ function Transaction({ const isStaking = isStake || isUnstake || isUnstakeRequest; const token = tokensBySlug?.[slug]; - const amountHuman = bigStrToHuman(amount, token!.decimals); const address = isIncoming ? fromAddress : toAddress; const addressName = savedAddresses?.[address] || metadata?.name; const isLocal = getIsTxIdLocal(txId); @@ -131,7 +132,7 @@ function Transaction({
{formatCurrencyExtended( - isStaking ? Math.abs(amountHuman) : amountHuman, + toDecimal(isStaking ? bigintAbs(amount) : amount, token!.decimals), token?.symbol || TON_SYMBOL, isStaking, )} diff --git a/src/components/receive/InvoiceModal.tsx b/src/components/receive/InvoiceModal.tsx index 85d6dfbc..5237acb4 100644 --- a/src/components/receive/InvoiceModal.tsx +++ b/src/components/receive/InvoiceModal.tsx @@ -4,11 +4,11 @@ import { withGlobal } from '../../global'; import type { UserToken } from '../../global/types'; import type { DropdownItem } from '../ui/Dropdown'; -import { TON_TOKEN_SLUG } from '../../config'; -import { humanToBigStr } from '../../global/helpers'; +import { DEFAULT_DECIMAL_PLACES, TON_TOKEN_SLUG } from '../../config'; import renderText from '../../global/helpers/renderText'; import { selectAccount, selectCurrentAccountTokens } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import { fromDecimal, toDecimal } from '../../util/decimals'; import formatTransferUrl from '../../util/ton/formatTransferUrl'; import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; @@ -41,12 +41,12 @@ function InvoiceModal({ }: StateProps & OwnProps) { const lang = useLang(); - const [amount, setAmount] = useState(undefined); + const [amount, setAmount] = useState(undefined); const [comment, setComment] = useState(''); const [hasAmountError, setHasAmountError] = useState(false); - const invoiceAmount = amount ? humanToBigStr(amount) : undefined; - const invoiceUrl = address ? formatTransferUrl(address, invoiceAmount, comment) : ''; + const invoiceUrl = address ? formatTransferUrl(address, amount, comment) : ''; + const decimals = DEFAULT_DECIMAL_PLACES; // TODO Change it after token selection is supported const dropdownItems = useMemo(() => { if (!tokens) { @@ -66,15 +66,17 @@ function InvoiceModal({ }, []); }, [tokens]); - const handleAmountInput = useLastCallback((value?: number) => { + const handleAmountInput = useLastCallback((stringValue?: string) => { setHasAmountError(false); - if (value === undefined) { + if (stringValue === undefined) { setAmount(undefined); return; } - if (Number.isNaN(value) || value < 0) { + const value = fromDecimal(stringValue, decimals); + + if (value < 0) { setHasAmountError(true); return; } @@ -102,7 +104,7 @@ function InvoiceModal({ key="amount" id="amount" hasError={hasAmountError} - value={amount} + value={amount === undefined ? undefined : toDecimal(amount)} labelText={lang('Amount')} onChange={handleAmountInput} > diff --git a/src/components/settings/SettingsTokens.tsx b/src/components/settings/SettingsTokens.tsx index c112ae06..168596b6 100644 --- a/src/components/settings/SettingsTokens.tsx +++ b/src/components/settings/SettingsTokens.tsx @@ -8,7 +8,9 @@ import type { ApiBaseCurrency } from '../../api/types'; import { SettingsState, type UserToken } from '../../global/types'; import { TON_TOKEN_SLUG } from '../../config'; +import { bigintMultiplyToNumber } from '../../util/bigint'; import buildClassName from '../../util/buildClassName'; +import { toDecimal } from '../../util/decimals'; import { formatCurrency, getShortCurrencySymbol } from '../../util/formatNumber'; import { isBetween } from '../../util/math'; import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; @@ -132,7 +134,7 @@ function SettingsTokens({ const isTON = slug === TON_TOKEN_SLUG; const logoPath = image || ASSET_LOGO_PATHS[symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS]; - const totalAmount = amount * price; + const totalAmount = bigintMultiplyToNumber(amount, price); const isDragged = state.draggedIndex === index; const draggedTop = isSortByValueEnabled ? getOffsetByIndex(index) : getOffsetBySlug(slug, state.orderedTokenSlugs); @@ -141,7 +143,7 @@ function SettingsTokens({ const style = `top: ${isDragged ? draggedTop : top}px;`; const knobStyle = 'left: 1rem;'; - const isDeleteButtonVisible = amount === 0 && !isTON; + const isDeleteButtonVisible = amount === 0n && !isTON; const isDragDisabled = isSortByValueEnabled || tokens!.length <= 1; @@ -171,9 +173,9 @@ function SettingsTokens({ {name}
- + - + {isDeleteButtonVisible && ( <> diff --git a/src/components/staking/StakeModal.tsx b/src/components/staking/StakeModal.tsx index b2ea92c9..1c100714 100644 --- a/src/components/staking/StakeModal.tsx +++ b/src/components/staking/StakeModal.tsx @@ -7,6 +7,7 @@ import { StakingState } from '../../global/types'; import { IS_CAPACITOR, TON_TOKEN_SLUG } from '../../config'; import { selectCurrentAccountTokens } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import { toDecimal } from '../../util/decimals'; import { formatCurrency } from '../../util/formatNumber'; import resolveModalTransitionName from '../../util/resolveModalTransitionName'; import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; @@ -88,7 +89,7 @@ function StakeModal({ return (
{tonToken.symbol} - {formatCurrency(amount, tonToken.symbol)} + {formatCurrency(toDecimal(amount), tonToken.symbol)}
); } @@ -127,7 +128,7 @@ function StakeModal({ playAnimation={isActive} amount={renderedStakingAmount} noSign - balance={tonToken?.amount ?? 0} + balance={tonToken?.amount ?? 0n} operationAmount={amount ? -amount : undefined} firstButtonText={lang('View')} secondButtonText={lang('Stake More')} diff --git a/src/components/staking/StakingInfoContent.tsx b/src/components/staking/StakingInfoContent.tsx index 13f41461..76d8aaa9 100644 --- a/src/components/staking/StakingInfoContent.tsx +++ b/src/components/staking/StakingInfoContent.tsx @@ -10,8 +10,8 @@ import { TON_SYMBOL, TON_TOKEN_SLUG } from '../../config'; import { selectCurrentAccountState, selectCurrentAccountTokens } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { formatRelativeHumanDateTime } from '../../util/dateFormat'; +import { toBig, toDecimal } from '../../util/decimals'; import { formatCurrency } from '../../util/formatNumber'; -import { round } from '../../util/round'; import useForceUpdate from '../../hooks/useForceUpdate'; import useInterval from '../../hooks/useInterval'; @@ -36,9 +36,9 @@ interface OwnProps { } interface StateProps { - amount: number; + amount: bigint; apyValue: number; - totalProfit: number; + totalProfit: bigint; stakingHistory?: ApiStakingHistory; tokens?: UserToken[]; isUnstakeRequested?: boolean; @@ -91,8 +91,8 @@ function StakingInfoContent({ startStaking({ isUnstaking: true }); }); - const stakingResult = round(amount, STAKING_DECIMAL); - const balanceResult = round(amount + (amount / 100) * apyValue, STAKING_DECIMAL); + const stakingResult = toBig(amount).round(STAKING_DECIMAL).toString(); + const balanceResult = toBig(amount).mul((apyValue / 100) + 1).round(STAKING_DECIMAL).toString(); function renderUnstakeDescription() { return ( @@ -112,7 +112,7 @@ function StakingInfoContent({ {lang('$total', { value: ( - {formatCurrency(totalProfit, TON_SYMBOL)} + {formatCurrency(toDecimal(totalProfit), TON_SYMBOL)} ), })} @@ -211,9 +211,9 @@ export default memo(withGlobal((global): StateProps => { const accountState = selectCurrentAccountState(global); return { - amount: accountState?.staking?.balance || 0, + amount: accountState?.staking?.balance || 0n, apyValue: accountState?.staking?.apy || 0, - totalProfit: accountState?.staking?.totalProfit ?? 0, + totalProfit: accountState?.staking?.totalProfit ?? 0n, stakingHistory: accountState?.stakingHistory, tokens: selectCurrentAccountTokens(global), isUnstakeRequested: accountState?.staking?.isUnstakeRequested, diff --git a/src/components/staking/StakingInitial.tsx b/src/components/staking/StakingInitial.tsx index 5e187945..8217ade1 100644 --- a/src/components/staking/StakingInitial.tsx +++ b/src/components/staking/StakingInitial.tsx @@ -11,17 +11,16 @@ import { ANIMATED_STICKER_SMALL_SIZE_PX, DEFAULT_DECIMAL_PLACES, DEFAULT_FEE, - MIN_BALANCE_FOR_UNSTAKE, + MIN_BALANCE_FOR_UNSTAKE, ONE_TON, STAKING_FORWARD_AMOUNT, STAKING_MIN_AMOUNT, TON_TOKEN_SLUG, } from '../../config'; -import { bigStrToHuman } from '../../global/helpers'; import renderText from '../../global/helpers/renderText'; import { selectCurrentAccountState, selectCurrentAccountTokens } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import { fromDecimal, toBig, toDecimal } from '../../util/decimals'; import { formatCurrency, formatCurrencySimple } from '../../util/formatNumber'; -import { floor } from '../../util/round'; import { throttle } from '../../util/schedulers'; import { IS_DELEGATED_BOTTOM_SHEET } from '../../util/windowEnvironment'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; @@ -51,15 +50,13 @@ interface StateProps { isLoading?: boolean; apiError?: string; tokens?: UserToken[]; - fee?: string; - stakingBalance: number; + fee?: bigint; + stakingBalance: bigint; apyValue: number; } export const STAKING_DECIMAL = 2; -// Fee may change, so we add 5% for more reliability. This is only safe for low-fee blockchains such as TON. -const RESERVED_FEE_FACTOR = 1.05; const runThrottled = throttle((cb) => cb(), 1500, true); function StakingInitial({ @@ -77,7 +74,7 @@ function StakingInitial({ const lang = useLang(); const [isStakingInfoModalOpen, openStakingInfoModal, closeStakingInfoModal] = useFlag(); - const [amount, setAmount] = useState(); + const [amount, setAmount] = useState(); const [isNotEnough, setIsNotEnough] = useState(false); const [isInsufficientBalance, setIsInsufficientBalance] = useState(false); const [shouldUseAllBalance, setShouldUseAllBalance] = useState(false); @@ -86,10 +83,10 @@ function StakingInitial({ amount: balance, symbol, } = useMemo(() => tokens?.find(({ slug }) => slug === TON_TOKEN_SLUG), [tokens]) || {}; const hasAmountError = Boolean(isInsufficientBalance || apiError); - const calculatedFee = fee ? bigStrToHuman(fee) * RESERVED_FEE_FACTOR : DEFAULT_FEE; + const calculatedFee = fee ?? DEFAULT_FEE; const decimals = DEFAULT_DECIMAL_PLACES; - const validateAndSetAmount = useLastCallback((newAmount: number | undefined, noReset = false) => { + const validateAndSetAmount = useLastCallback((newAmount: bigint | undefined, noReset = false) => { if (!noReset) { setShouldUseAllBalance(false); setIsNotEnough(false); @@ -196,6 +193,11 @@ function StakingInitial({ submitStakingInitial({ amount }); }); + const handleAmountChange = useLastCallback((stringValue?: string) => { + const value = stringValue ? fromDecimal(stringValue, decimals) : undefined; + validateAndSetAmount(value); + }); + function getError() { if (isInsufficientBalance) { return lang('Insufficient balance'); @@ -210,7 +212,7 @@ function StakingInitial({ } const isFullBalanceSelected = balance && amount - && (balance >= amount && Number((balance - amount).toFixed(2)) < MIN_BALANCE_FOR_UNSTAKE); // TODO $decimals + && (balance >= amount && balance - amount < MIN_BALANCE_FOR_UNSTAKE); const balanceLink = lang('$max_balance', { balance: ( @@ -224,7 +226,7 @@ function StakingInitial({ const minusOneLink = ( - {formatCurrency(-Math.round(MIN_BALANCE_FOR_UNSTAKE), symbol)} + {formatCurrency(toDecimal(ONE_TON), symbol)} ); @@ -256,7 +258,7 @@ function StakingInitial({ lang('$min_value', { value: ( - {formatCurrency(STAKING_MIN_AMOUNT, 'TON')} + {formatCurrency(toDecimal(STAKING_MIN_AMOUNT), 'TON')} ), }) @@ -294,7 +296,9 @@ function StakingInitial({ } function renderStakingResult() { - const balanceResult = amount ? floor(amount! + (amount! / 100) * apyValue, STAKING_DECIMAL) : 0; + const balanceResult = amount + ? toBig(amount).mul((apyValue / 100) + 1).round(STAKING_DECIMAL).toString() + : '0'; return ( tokens?.find(({ slug }) => slug === TON_TOKEN_SLUG), [tokens]); const [renderedStakingBalance, setRenderedStakingBalance] = useState(stakingBalance); @@ -85,10 +86,9 @@ function UnstakeModal({ const [isLongUnstake, setIsLongUnstake] = useState(false); useEffect(() => { - const instantAvailable = Big(stakingInfo.liquid?.instantAvailable ?? 0); - const isInstantUnstake = stakingType === 'liquid' - ? Big(stakingBalance ?? 0).lte(instantAvailable) - : false; + const isInstantUnstake = Boolean( + stakingType === 'liquid' && (stakingBalance ?? 0n) < (stakingInfo.liquid?.instantAvailable ?? 0n), + ); setIsLongUnstake(!isInstantUnstake); }, [stakingType, stakingBalance, stakingInfo]); @@ -150,7 +150,7 @@ function UnstakeModal({ return (
{tonToken.symbol} - {formatCurrency(stakingBalance, tonToken.symbol)} + {formatCurrency(toDecimal(stakingBalance), tonToken.symbol)}
); } @@ -183,7 +183,7 @@ function UnstakeModal({ key="unstaking_amount" id="unstaking_amount" error={error ? lang(error) : undefined} - value={stakingBalance} + value={stakingBalance === undefined ? undefined : toDecimal(stakingBalance)} labelText={lang('Amount to unstake')} decimals={tonToken?.decimals} > diff --git a/src/components/swap/SwapBlockchain.tsx b/src/components/swap/SwapBlockchain.tsx index 44eb8e5b..345611d9 100644 --- a/src/components/swap/SwapBlockchain.tsx +++ b/src/components/swap/SwapBlockchain.tsx @@ -8,6 +8,7 @@ import { SwapState, SwapType, type UserSwapToken } from '../../global/types'; import { ANIMATED_STICKER_BIG_SIZE_PX, IS_FIREFOX_EXTENSION } from '../../config'; import buildClassName from '../../util/buildClassName'; +import { readClipboardContent } from '../../util/clipboard'; import { shortenAddress } from '../../util/shortenAddress'; import getBlockchainNetworkName from '../../util/swap/getBlockchainNetworkName'; import { IS_FIREFOX } from '../../util/windowEnvironment'; @@ -128,19 +129,20 @@ function SwapBlockchain({ setSwapCexAddress({ toAddress: newToAddress.trim() }); }); - const handlePasteClick = useLastCallback(() => { - navigator.clipboard - .readText() - .then((clipboardText) => { - setSwapCexAddress({ toAddress: clipboardText.trim() }); - validateToAddress(clipboardText.trim()); - }) - .catch(() => { - showNotification({ - message: lang('Error reading clipboard') as string, - }); - setShouldRenderPasteButton(false); + const handlePasteClick = useLastCallback(async () => { + try { + const { type, text } = await readClipboardContent(); + + if (type === 'text/plain') { + setSwapCexAddress({ toAddress: text.trim() }); + validateToAddress(text.trim()); + } + } catch (error) { + showNotification({ + message: lang('Error reading clipboard'), }); + setShouldRenderPasteButton(false); + } }); const submitPassword = useLastCallback(() => { diff --git a/src/components/swap/SwapComplete.tsx b/src/components/swap/SwapComplete.tsx index 96c773e3..78ecd7b1 100644 --- a/src/components/swap/SwapComplete.tsx +++ b/src/components/swap/SwapComplete.tsx @@ -18,8 +18,8 @@ interface OwnProps { isActive: boolean; tokenIn?: UserSwapToken; tokenOut?: UserSwapToken; - amountIn?: number; - amountOut?: number; + amountIn?: string; + amountOut?: string; swapType?: SwapType; toAddress?: string; onInfoClick: NoneToVoidFunction; diff --git a/src/components/swap/SwapInitial.tsx b/src/components/swap/SwapInitial.tsx index 2ddd58d2..c6f22b9e 100644 --- a/src/components/swap/SwapInitial.tsx +++ b/src/components/swap/SwapInitial.tsx @@ -16,8 +16,10 @@ import { TON_SYMBOL, TON_TOKEN_SLUG, } from '../../config'; +import { Big } from '../../lib/big.js'; import { selectSwapTokens } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import { fromDecimal, toDecimal } from '../../util/decimals'; import { formatCurrency, formatCurrencySimple } from '../../util/formatNumber'; import getSwapRate from '../../util/swap/getSwapRate'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; @@ -52,7 +54,7 @@ interface StateProps { const ESTIMATE_REQUEST_INTERVAL = 5_000; const ESTIMATE_REQUEST_DEBOUNCE_TIME = 500; -const DEFAULT_SWAP_FEE = 0.5; +const DEFAULT_SWAP_FEE = 500000000n; // 0.5 TON function SwapInitial({ currentSwap: { @@ -129,21 +131,32 @@ function SwapInitial({ const totalTonAmount = useMemo( () => { if (!tokenIn || !amountIn) { - return 0; + return 0n; } if (isTokenInTON) { - return amountIn + networkFee; + return fromDecimal(amountIn) + fromDecimal(networkFee); } - return networkFee; + return fromDecimal(networkFee); }, [tokenIn, amountIn, isTokenInTON, networkFee], ); + const amountInBig = useMemo(() => Big(amountIn || 0), [amountIn]); + const amountOutBig = useMemo(() => Big(amountOut || 0), [amountOut]); + const tokenInAmountBig = useMemo(() => toDecimal(tokenIn?.amount ?? 0n), [tokenIn]); + const isErrorExist = errorType !== undefined; const isEnoughTON = TON.amount > totalTonAmount; // eslint-disable-next-line max-len - const isCorrectAmountIn = (amountIn && tokenIn?.amount && amountIn > 0 && amountIn <= tokenIn?.amount && isEnoughTON) || swapType === SwapType.CrosschainToTon; - const isCorrectAmountOut = amountOut && amountOut > 0; + const isCorrectAmountIn = ( + amountIn + && tokenIn + && tokenIn.amount + && amountInBig.gt(0) + && amountInBig.lte(tokenInAmountBig) + && isEnoughTON + ) || swapType === SwapType.CrosschainToTon; + const isCorrectAmountOut = amountOut && amountOutBig.gt(0); const canSubmit = Boolean(isCorrectAmountIn && isCorrectAmountOut && !isEstimating && !isErrorExist); const isPriceImpactError = priceImpact >= MAX_PRICE_IMPACT_VALUE; @@ -212,15 +225,15 @@ function SwapInitial({ setSwapType({ type: SwapType.OnChain }); }, [tokenIn, tokenOut]); - const validateAmountIn = useLastCallback((amount: number | undefined) => { + const validateAmountIn = useLastCallback((amount: string | undefined) => { if (swapType === SwapType.CrosschainToTon) { setHasAmountInError(false); return; } - const hasError = amount !== undefined && ( - Number.isNaN(amount) || amount < 0 - || (tokenIn?.amount !== undefined && amount > tokenIn.amount) + const amountBig = amount === undefined ? undefined : Big(amount); + const hasError = amountBig !== undefined && ( + amountBig.lt(0) || (tokenIn?.amount !== undefined && amountBig.gt(tokenInAmountBig)) ); setHasAmountInError(hasError); @@ -231,24 +244,24 @@ function SwapInitial({ }, [amountIn, tokenIn, validateAmountIn, swapType]); const handleAmountInChange = useLastCallback( - (amount: number | undefined, noReset = false) => { + (value: string | undefined, noReset = false) => { if (!noReset) { setHasAmountInError(false); } - if (amount === undefined) { + if (!value) { setSwapAmountIn({ amount: undefined }); return; } - validateAmountIn(amount); - setSwapAmountIn({ amount }); + validateAmountIn(value); + setSwapAmountIn({ amount: value }); }, ); const handleAmountOutChange = useLastCallback( - (amount: number | undefined) => { - setSwapAmountOut({ amount }); + (value: string | undefined) => { + setSwapAmountOut({ amount: value }); }, ); @@ -265,7 +278,7 @@ function SwapInitial({ : tokenIn.amount; const newAmount = isTokenInTON ? amountWithFee : tokenIn.amount; - handleAmountInChange(newAmount); + handleAmountInChange(toDecimal(newAmount, tokenIn.decimals)); }, ); @@ -472,7 +485,7 @@ function SwapInitial({ labelText={lang('You sell')} className={styles.amountInput} hasError={hasAmountInError} - value={amountIn} + value={amountIn?.toString()} isLoading={isEstimating && inputSource === SwapInputSource.Out} onChange={handleAmountInChange} onPressEnter={handleSubmit} @@ -496,7 +509,7 @@ function SwapInitial({ id="swap-buy" labelText={lang('You buy')} className={styles.amountInput} - value={amountOut} + value={amountOut?.toString()} isLoading={isEstimating && inputSource === SwapInputSource.In} disabled={isReverseProhibited} onChange={handleAmountOutChange} diff --git a/src/components/swap/SwapModal.tsx b/src/components/swap/SwapModal.tsx index c398790c..79ac8ac3 100644 --- a/src/components/swap/SwapModal.tsx +++ b/src/components/swap/SwapModal.tsx @@ -44,8 +44,8 @@ function SwapModal({ state, tokenInSlug, tokenOutSlug, - amountIn = 0, - amountOut = 0, + amountIn = '0', + amountOut = '0', isLoading, error, activityId, diff --git a/src/components/swap/SwapSettingsModal.tsx b/src/components/swap/SwapSettingsModal.tsx index 949860be..8658fe84 100644 --- a/src/components/swap/SwapSettingsModal.tsx +++ b/src/components/swap/SwapSettingsModal.tsx @@ -66,6 +66,11 @@ function SwapSettingsModal({ setCurrentSlippage(slippage); }); + const handleInputChange = useLastCallback((stringValue?: string) => { + const value = stringValue ? Number(stringValue) : undefined; + setCurrentSlippage(value); + }); + function renderSlippageValues() { const slippageList = SLIPPAGE_VALUES.map((value, index) => { return ( @@ -140,11 +145,11 @@ function SwapSettingsModal({ diff --git a/src/components/swap/SwapWaitTokens.tsx b/src/components/swap/SwapWaitTokens.tsx index 76ac075a..3578f258 100644 --- a/src/components/swap/SwapWaitTokens.tsx +++ b/src/components/swap/SwapWaitTokens.tsx @@ -25,8 +25,8 @@ interface OwnProps { isActive: boolean; tokenIn?: UserSwapToken; tokenOut?: UserSwapToken; - amountIn?: number; - amountOut?: number; + amountIn?: string; + amountOut?: string; payinAddress?: string; onClose: NoneToVoidFunction; } diff --git a/src/components/swap/components/SwapSubmitButton.tsx b/src/components/swap/components/SwapSubmitButton.tsx index 8d2d8a7a..68174743 100644 --- a/src/components/swap/components/SwapSubmitButton.tsx +++ b/src/components/swap/components/SwapSubmitButton.tsx @@ -18,8 +18,8 @@ import styles from '../Swap.module.scss'; interface OwnProps { tokenIn?: UserSwapToken; tokenOut?: UserSwapToken; - amountIn?: number; - amountOut?: number; + amountIn?: string; + amountOut?: string; swapType?: SwapType; isEstimating?: boolean; isSending?: boolean; @@ -58,10 +58,10 @@ function SwapSubmitButton({ [SwapErrorType.InvalidPair]: lang('Invalid Pair'), [SwapErrorType.NotEnoughLiquidity]: lang('Insufficient liquidity'), [SwapErrorType.ChangellyMinSwap]: lang('Minimum amount', { - value: formatCurrencySimple(Number(limits?.fromMin ?? 0), tokenIn?.symbol ?? '', tokenIn?.decimals), + value: formatCurrencySimple(limits?.fromMin ?? '0', tokenIn?.symbol ?? '', tokenIn?.decimals), }), [SwapErrorType.ChangellyMaxSwap]: lang('Maximum amount', { - value: formatCurrencySimple(Number(limits?.fromMax ?? 0), tokenIn?.symbol ?? '', tokenIn?.decimals), + value: formatCurrencySimple(limits?.fromMax ?? '0', tokenIn?.symbol ?? '', tokenIn?.decimals), }), }; diff --git a/src/components/transfer/TransferComplete.tsx b/src/components/transfer/TransferComplete.tsx index 45b90c4c..430d7276 100644 --- a/src/components/transfer/TransferComplete.tsx +++ b/src/components/transfer/TransferComplete.tsx @@ -2,7 +2,6 @@ import React, { memo } from '../../lib/teact/teact'; import { getActions } from '../../global'; import { TON_TOKEN_SLUG } from '../../config'; -import { bigStrToHuman } from '../../global/helpers'; import { useDeviceScreen } from '../../hooks/useDeviceScreen'; import useHistoryBack from '../../hooks/useHistoryBack'; @@ -17,11 +16,11 @@ import modalStyles from '../ui/Modal.module.scss'; interface OwnProps { isActive?: boolean; - amount?: number; + amount?: bigint; symbol: string; - balance?: number; - fee?: string; - operationAmount?: number; + balance?: bigint; + fee?: bigint; + operationAmount?: bigint; txId?: string; tokenSlug?: string; toAddress?: string; @@ -77,7 +76,7 @@ function TransferComplete({ tokenSymbol={symbol} precision={AMOUNT_PRECISION} balance={balance} - fee={fee ? bigStrToHuman(fee) : 0} + fee={fee ?? 0n} operationAmount={operationAmount ? -operationAmount : undefined} firstButtonText={txId ? lang('Details') : undefined} secondButtonText={lang('Repeat')} diff --git a/src/components/transfer/TransferConfirm.tsx b/src/components/transfer/TransferConfirm.tsx index 358d3801..af95c7eb 100644 --- a/src/components/transfer/TransferConfirm.tsx +++ b/src/components/transfer/TransferConfirm.tsx @@ -4,8 +4,8 @@ import { getActions, withGlobal } from '../../global'; import type { GlobalState } from '../../global/types'; import { ANIMATED_STICKER_SMALL_SIZE_PX } from '../../config'; -import { bigStrToHuman } from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; +import { toDecimal } from '../../util/decimals'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; import useHistoryBack from '../../hooks/useHistoryBack'; @@ -25,6 +25,7 @@ interface OwnProps { isActive: boolean; savedAddresses?: Record; symbol: string; + decimals?: number; onBack: NoneToVoidFunction; onClose: NoneToVoidFunction; } @@ -45,7 +46,13 @@ function TransferConfirm({ isLoading, toAddressName, isToNewAddress, - }, symbol, isActive, savedAddresses, onBack, onClose, + }, + symbol, + decimals, + isActive, + savedAddresses, + onBack, + onClose, }: OwnProps & StateProps) { const { submitTransferConfirm } = getActions(); @@ -106,9 +113,9 @@ function TransferConfirm({ {renderComment()} diff --git a/src/components/transfer/TransferInitial.tsx b/src/components/transfer/TransferInitial.tsx index 555543be..f307ade6 100644 --- a/src/components/transfer/TransferInitial.tsx +++ b/src/components/transfer/TransferInitial.tsx @@ -11,7 +11,6 @@ import type { DropdownItem } from '../ui/Dropdown'; import { ElectronEvent } from '../../electron/types'; import { IS_FIREFOX_EXTENSION, TON_SYMBOL, TON_TOKEN_SLUG } from '../../config'; -import { bigStrToHuman } from '../../global/helpers'; import { selectCurrentAccountState, selectCurrentAccountTokens, @@ -19,6 +18,8 @@ import { } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { clearLaunchUrl, getLaunchUrl } from '../../util/capacitor'; +import { readClipboardContent } from '../../util/clipboard'; +import { fromDecimal, toBig, toDecimal } from '../../util/decimals'; import dns from '../../util/dns'; import { formatCurrency, @@ -59,11 +60,11 @@ interface OwnProps { interface StateProps { toAddress?: string; - amount?: number; + amount?: bigint; comment?: string; shouldEncrypt?: boolean; isLoading?: boolean; - fee?: string; + fee?: bigint; tokenSlug?: string; tokens?: UserToken[]; savedAddresses?: Record; @@ -77,9 +78,6 @@ const SHORT_ADDRESS_SHIFT = 14; const MIN_ADDRESS_LENGTH_TO_SHORTEN = SHORT_ADDRESS_SHIFT * 2; const COMMENT_DROPDOWN_ITEMS = [{ value: 'raw', name: 'Comment' }, { value: 'encrypted', name: 'Encrypted Message' }]; -// Fee may change, so we add 5% for more reliability. This is only safe for low-fee blockchains such as TON. -const RESERVED_FEE_FACTOR = 1.05; - const runThrottled = throttle((cb) => cb(), 1500, true); function TransferInitial({ @@ -137,7 +135,8 @@ function TransferInitial({ price, symbol, } = useMemo(() => tokens?.find((token) => token.slug === tokenSlug), [tokenSlug, tokens]) || {}; - const amountInCurrency = price && amount && !Number.isNaN(amount) ? amount * price : undefined; + + const amountInCurrency = price && amount ? toBig(amount).mul(price).round(decimals).toString() : undefined; const renderingAmountInCurrency = useCurrentOrPrev(amountInCurrency, true); const renderingFee = useCurrentOrPrev(fee, true); const withPasteButton = shouldRenderPasteButton && toAddress === ''; @@ -167,7 +166,7 @@ function TransferInitial({ }, [tokenSlug, tokens]); const validateAndSetAmount = useLastCallback( - (newAmount: number | undefined, noReset = false) => { + (newAmount: bigint | undefined, noReset = false) => { if (!noReset) { setHasAmountError(false); setIsInsufficientBalance(false); @@ -178,7 +177,7 @@ function TransferInitial({ return; } - if (Number.isNaN(newAmount) || newAmount < 0) { + if (newAmount < 0) { setHasAmountError(true); return; } @@ -194,8 +193,7 @@ function TransferInitial({ useEffect(() => { if (balance && amount === balance) { - const calculatedFee = fee ? bigStrToHuman(fee, decimals) : 0; - const reducedAmount = balance - calculatedFee * RESERVED_FEE_FACTOR; + const reducedAmount = balance - (fee ?? 0n); const newAmount = tokenSlug === TON_TOKEN_SLUG && reducedAmount > 0 ? reducedAmount : balance; validateAndSetAmount(newAmount); } else { @@ -223,7 +221,7 @@ function TransferInitial({ if (!params) return; setTransferToAddress({ toAddress: params.to }); - setTransferAmount({ amount: params.amount ? bigStrToHuman(params.amount) : undefined }); + setTransferAmount({ amount: params.amount }); setTransferComment({ comment: params.comment }); }); @@ -303,21 +301,18 @@ function TransferInitial({ onQrScanPress!(); }); - const handlePasteClick = useLastCallback(() => { - navigator.clipboard - .readText() - .then((clipboardText) => { - if (getIsAddressValid(clipboardText)) { - setTransferToAddress({ toAddress: clipboardText }); - validateToAddress(); - } - }) - .catch(() => { - showNotification({ - message: lang('Error reading clipboard') as string, - }); - setShouldRenderPasteButton(false); - }); + const handlePasteClick = useLastCallback(async () => { + try { + const { type, text } = await readClipboardContent(); + + if (type === 'text/plain' && getIsAddressValid(text.trim())) { + setTransferToAddress({ toAddress: text.trim() }); + validateToAddress(); + } + } catch (error) { + showNotification({ message: lang('Error reading clipboard') }); + setShouldRenderPasteButton(false); + } }); const handleSavedAddressClick = useLastCallback( @@ -338,7 +333,10 @@ function TransferInitial({ setSavedAddressForDeletion(undefined); }); - const handleAmountChange = useLastCallback(validateAndSetAmount); + const handleAmountChange = useLastCallback((stringValue?: string) => { + const value = stringValue ? fromDecimal(stringValue, decimals) : undefined; + validateAndSetAmount(value); + }); const handleMaxAmountClick = useLastCallback( (e: React.MouseEvent) => { @@ -418,7 +416,7 @@ function TransferInitial({ lang('$fee_value', { fee: ( - {formatCurrencyExtended(bigStrToHuman(renderingFee!), TON_SYMBOL, true)} + {formatCurrencyExtended(toDecimal(renderingFee!), TON_SYMBOL, true)} ), }) @@ -463,7 +461,7 @@ function TransferInitial({ function renderCurrencyValue() { return ( - ≈ {formatCurrency(renderingAmountInCurrency || 0, shortBaseSymbol)} + ≈ {formatCurrency(renderingAmountInCurrency || '0', shortBaseSymbol)} ); } @@ -521,7 +519,7 @@ function TransferInitial({ key="amount" id="amount" hasError={hasAmountError} - value={amount} + value={amount === undefined ? undefined : toDecimal(amount)} labelText={lang('Amount')} onChange={handleAmountChange} onPressEnter={handleSubmit} diff --git a/src/components/transfer/TransferModal.tsx b/src/components/transfer/TransferModal.tsx index 9562d7b0..d1da1462 100644 --- a/src/components/transfer/TransferModal.tsx +++ b/src/components/transfer/TransferModal.tsx @@ -10,6 +10,7 @@ import { IS_CAPACITOR } from '../../config'; import { selectCurrentAccountState, selectCurrentAccountTokens } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import captureKeyboardListeners from '../../util/captureKeyboardListeners'; +import { toDecimal } from '../../util/decimals'; import { formatCurrency } from '../../util/formatNumber'; import resolveModalTransitionName from '../../util/resolveModalTransitionName'; import { shortenAddress } from '../../util/shortenAddress'; @@ -78,6 +79,7 @@ function TransferModal({ const { screenHeight } = useWindowSize(); const selectedToken = useMemo(() => tokens?.find((token) => token.slug === tokenSlug), [tokenSlug, tokens]); + const decimals = selectedToken?.decimals; const [renderedTokenBalance, setRenderedTokenBalance] = useState(selectedToken?.amount); const renderedTransactionAmount = usePrevious(amount, true); const symbol = selectedToken?.symbol || ''; @@ -139,7 +141,7 @@ function TransferModal({ {symbol} {lang('%amount% to %address%', { - amount: {formatCurrency(amount!, symbol)}, + amount: {formatCurrency(toDecimal(amount!, decimals), symbol)}, address: {shortenAddress(toAddress!)}, })} @@ -162,6 +164,7 @@ function TransferModal({ { if (value) { - renderValue(floor(value, decimals)); + renderValue(value); } else if (zeroValue) { contentRef.current!.innerHTML = zeroValue; } diff --git a/src/components/ui/RichNumberInput.tsx b/src/components/ui/RichNumberInput.tsx index a4368094..8dd706f2 100644 --- a/src/components/ui/RichNumberInput.tsx +++ b/src/components/ui/RichNumberInput.tsx @@ -4,9 +4,7 @@ import React, { } from '../../lib/teact/teact'; import { DEFAULT_DECIMAL_PLACES, FRACTION_DIGITS } from '../../config'; -import { Big } from '../../lib/big.js'; import buildClassName from '../../util/buildClassName'; -import { round } from '../../util/round'; import { saveCaretPosition } from '../../util/saveCaretPosition'; import useFlag from '../../hooks/useFlag'; @@ -18,7 +16,7 @@ import styles from './Input.module.scss'; type OwnProps = { id?: string; labelText?: React.ReactNode; - value?: number; + value?: string; hasError?: boolean; isLoading?: boolean; suffix?: string; @@ -28,7 +26,7 @@ type OwnProps = { valueClassName?: string; cornerClassName?: string; children?: TeactNode; - onChange?: (value?: number) => void; + onChange?: (value?: string) => void; onBlur?: NoneToVoidFunction; onFocus?: NoneToVoidFunction; onPressEnter?: (e: React.KeyboardEvent) => void; @@ -89,32 +87,39 @@ function RichNumberInput({ const input = inputRef.current!; const content = isLoading ? handleLoadingHtml(input, parts) : handleNumberHtml(input, parts); - input.classList.toggle(styles.isEmpty, !content.length); + if (content.length) { + input.classList.remove(styles.isEmpty); + } else { + input.classList.add(styles.isEmpty); + } }); useLayoutEffect(() => { - const newValue = castValue(value); - - const parts = getParts(String(newValue)); - updateHtml(parts); - - if (value !== newValue) { - onChange?.(newValue); + if (value === undefined) { + updateHtml(); + } else { + updateHtml(getParts(value)); } - }, [decimals, onChange, updateHtml, value, suffix, isLoading, disabled]); + }, [updateHtml, value]); function handleChange(e: React.FormEvent) { const inputValue = e.currentTarget.innerText.trim(); - const parts = getParts(inputValue, decimals); + + const newValue = clearValue(inputValue, decimals); + + // TODO Убрать + // eslint-disable-next-line no-console + console.log({ value, inputValue, newValue }); + + const parts = getParts(newValue, decimals); const isEmpty = inputValue === ''; if (!parts && !isEmpty && value) { - updateHtml(getParts(String(value), decimals)); + updateHtml(getParts(value, decimals)); } else { updateHtml(parts); } - const newValue = castValue(Number(inputValue), decimals); if ((newValue || isEmpty) && newValue !== value) { onChange?.(newValue); } @@ -203,11 +208,6 @@ function RichNumberInput({ function getParts(value: string, decimals = DEFAULT_DECIMAL_PLACES) { const regex = getInputRegex(decimals); - // Correct problem with numbers like 1e-8 - if (value.includes('e-')) { - Big.NE = -decimals - 1; - return new Big(value).toString().match(regex) || undefined; - } return value.match(regex) || undefined; } @@ -216,18 +216,23 @@ export function getInputRegex(decimals: number) { return new RegExp(`^(\\d+)([.,])?(\\d{1,${decimals}})?`); } -function castValue(value?: number | string, decimals = DEFAULT_DECIMAL_PLACES) { - return value && Number.isFinite(Number(value)) ? round(value, decimals, Big.roundDown) : undefined; +function clearValue(value: string, decimals: number) { + return value + .replace(',', '.') // Replace comma to point + .replace(/[^\d.]/, '') // Remove incorrect symbols + .match(getInputRegex(decimals))?.[0] // Trim extra decimal places + .replace(/^0+(?=([1-9]|0\.))/, '') // Trim extra zeros at beginning + .replace(/^0+$/, '0') // Trim extra zeros (if only zeros are entered) + ?? ''; } export function buildContentHtml(values: RegExpMatchArray, suffix?: string, decimals = FRACTION_DIGITS) { const [, wholePart, dotPart, fractionPart] = values; - const wholeStr = String(parseInt(wholePart, 10)); // Properly handle leading zero const fractionStr = (fractionPart || dotPart) ? `.${(fractionPart || '').substring(0, decimals)}` : ''; const suffixStr = suffix ? ` ${suffix}` : ''; - return `${wholeStr}${fractionStr}${suffixStr}`; + return `${wholePart}${fractionStr}${suffixStr}`; } export default memo(RichNumberInput); diff --git a/src/config.ts b/src/config.ts index dcb0fe33..3a812f8e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -146,14 +146,15 @@ export const LANG_LIST: LangItem[] = [{ }]; export const STAKING_CYCLE_DURATION_MS = 131072000; // 36.4 hours -export const MIN_BALANCE_FOR_UNSTAKE = 1.02; -export const STAKING_FORWARD_AMOUNT = 1; -export const DEFAULT_FEE = 0.01; +export const ONE_TON = 1000000000n; +export const MIN_BALANCE_FOR_UNSTAKE = 1020000000n; // 1.02 TON +export const STAKING_FORWARD_AMOUNT = ONE_TON; +export const DEFAULT_FEE = 15000000n; // 0.015 TON export const STAKING_POOLS = process.env.STAKING_POOLS ? process.env.STAKING_POOLS.split(' ') : []; export const LIQUID_POOL = process.env.LIQUID_POOL || 'EQD2_4d91M4TVbEBVyBF8J1UwpMJc361LKVCz6bBlffMW05o'; export const LIQUID_JETTON = process.env.LIQUID_JETTON || 'EQCqC6EhRJ_tpWngKxL6dV0k6DSnRUrs9GSVkLbfdCqsj6TE'; -export const STAKING_MIN_AMOUNT = 1; +export const STAKING_MIN_AMOUNT = ONE_TON; export const TON_PROTOCOL = 'ton://'; export const TONCONNECT_PROTOCOL = 'tc://'; diff --git a/src/electron/deeplink.ts b/src/electron/deeplink.ts index 7ce36d79..32fddec4 100644 --- a/src/electron/deeplink.ts +++ b/src/electron/deeplink.ts @@ -4,7 +4,7 @@ import path from 'path'; import { ElectronAction, ElectronEvent } from './types'; import { - IS_LINUX, IS_MAC_OS, IS_WINDOWS, mainWindow, + focusMainWindow, IS_LINUX, IS_MAC_OS, IS_WINDOWS, mainWindow, } from './utils'; const TON_PROTOCOL = 'ton'; @@ -50,6 +50,7 @@ export function initDeeplink() { event.preventDefault(); deeplinkUrl = url; processDeeplink(); + focusMainWindow(); }); }); @@ -65,18 +66,7 @@ export function initDeeplink() { } processDeeplink(); - - if (mainWindow) { - if (!mainWindow.isVisible()) { - mainWindow.show(); - } - - if (mainWindow.isMinimized()) { - mainWindow.restore(); - } - - mainWindow.focus(); - } + focusMainWindow(); }); } diff --git a/src/electron/utils.ts b/src/electron/utils.ts index 2cc18363..b2bb58fe 100644 --- a/src/electron/utils.ts +++ b/src/electron/utils.ts @@ -62,6 +62,22 @@ export function setMainWindow(window: BrowserWindow) { mainWindow = window; } +export function focusMainWindow() { + if (!mainWindow) { + return; + } + + if (!mainWindow.isVisible()) { + mainWindow.show(); + } + + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } + + mainWindow.focus(); +} + export const forceQuit = { value: false, diff --git a/src/global/actions/api/auth.ts b/src/global/actions/api/auth.ts index 548cf2ef..ddc9698b 100644 --- a/src/global/actions/api/auth.ts +++ b/src/global/actions/api/auth.ts @@ -17,6 +17,7 @@ import { vibrateOnSuccess, } from '../../../util/capacitor'; import { cloneDeep } from '../../../util/iteratees'; +import { callActionInMain } from '../../../util/multitab'; import { pause } from '../../../util/schedulers'; import { IS_BIOMETRIC_AUTH_SUPPORTED, IS_DELEGATED_BOTTOM_SHEET, IS_ELECTRON } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; @@ -30,6 +31,7 @@ import { setIsPinAccepted, updateAuth, updateBiometrics, + updateCurrentAccountId, updateCurrentAccountState, updateHardware, updateSettings, @@ -45,8 +47,6 @@ import { selectNewestTxIds, } from '../../selectors'; -import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; - const CREATING_DURATION = 3300; const NATIVE_BIOMETRICS_PAUSE_MS = 750; @@ -362,7 +362,7 @@ addActionHandler('createHardwareAccounts', async (global, actions) => { } const { accountId, address, walletInfo } = wallet; - currentGlobal = { ...currentGlobal, currentAccountId: accountId }; + currentGlobal = updateCurrentAccountId(currentGlobal, accountId); currentGlobal = createAccount(currentGlobal, accountId, address, { isHardware: true, ...(walletInfo && { @@ -394,7 +394,7 @@ addActionHandler('createHardwareAccounts', async (global, actions) => { }); addActionHandler('afterCheckMnemonic', (global, actions) => { - global = { ...global, currentAccountId: global.auth.accountId! }; + global = updateCurrentAccountId(global, global.auth.accountId!); global = updateCurrentAccountState(global, {}); global = createAccount(global, global.auth.accountId!, global.auth.address!); setGlobal(global); @@ -414,7 +414,7 @@ addActionHandler('restartCheckMnemonicIndexes', (global) => { }); addActionHandler('skipCheckMnemonic', (global, actions) => { - global = { ...global, currentAccountId: global.auth.accountId! }; + global = updateCurrentAccountId(global, global.auth.accountId!); global = updateCurrentAccountState(global, { isBackupRequired: true, }); @@ -499,7 +499,7 @@ addActionHandler('confirmDisclaimer', (global, actions) => { addActionHandler('afterConfirmDisclaimer', (global, actions) => { const { accountId, address } = global.auth; - global = { ...global, currentAccountId: accountId }; + global = updateCurrentAccountId(global, accountId!); global = updateAuth(global, { state: AuthState.ready }); global = createAccount(global, accountId!, address!); setGlobal(global); @@ -553,11 +553,8 @@ addActionHandler('switchAccount', async (global, actions, payload) => { const newestTxIds = selectNewestTxIds(global, accountId); await callApi('activateAccount', accountId, newestTxIds); - global = { - ...getGlobal(), - currentAccountId: accountId, - }; - + global = getGlobal(); + global = updateCurrentAccountId(global, accountId); global = clearCurrentTransfer(global); global = clearCurrentSwap(global); setGlobal(global); diff --git a/src/global/actions/api/dapps.ts b/src/global/actions/api/dapps.ts index fff4ea3b..46f9b080 100644 --- a/src/global/actions/api/dapps.ts +++ b/src/global/actions/api/dapps.ts @@ -2,7 +2,8 @@ import { DappConnectState, TransferState } from '../../types'; import { IS_CAPACITOR } from '../../../config'; import { vibrateOnSuccess } from '../../../util/capacitor'; -import { pause } from '../../../util/schedulers'; +import { callActionInMain } from '../../../util/multitab'; +import { pause, waitFor } from '../../../util/schedulers'; import { IS_DELEGATED_BOTTOM_SHEET } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; import { ApiUserRejectsError } from '../../../api/errors'; @@ -15,13 +16,12 @@ import { removeConnectedDapp, setIsPinAccepted, updateConnectedDapps, + updateCurrentAccountId, updateCurrentDappTransfer, updateDappConnectRequest, } from '../../reducers'; import { selectAccount, selectIsHardwareAccount, selectNewestTxIds } from '../../selectors'; -import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; - const GET_DAPPS_PAUSE = 250; addActionHandler('submitDappConnectRequestConfirm', async (global, actions, { password, accountId }) => { @@ -126,19 +126,19 @@ addActionHandler( addActionHandler('cancelDappConnectRequestConfirm', (global) => { const { promiseId } = global.dappConnectRequest || {}; - - if (IS_CAPACITOR) { - global = clearIsPinAccepted(global); - setGlobal(global); - } - if (!promiseId) { return; } - void callApi('cancelDappRequest', promiseId!, 'Canceled by the user'); + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('cancelDappConnectRequestConfirm'); + } else { + void callApi('cancelDappRequest', promiseId, 'Canceled by the user'); + } - global = getGlobal(); + if (IS_CAPACITOR) { + global = clearIsPinAccepted(global); + } global = clearDappConnectRequest(global); setGlobal(global); }); @@ -148,18 +148,22 @@ addActionHandler('setDappConnectRequestState', (global, actions, { state }) => { }); addActionHandler('cancelDappTransfer', (global) => { - const { promiseId } = global.currentDappTransfer; + const { promiseId, state } = global.currentDappTransfer; - if (IS_CAPACITOR) { - global = clearIsPinAccepted(global); - setGlobal(global); + if (state === TransferState.None) { + return; } - if (promiseId) { + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('cancelDappTransfer'); + } else if (promiseId) { void callApi('cancelDappRequest', promiseId, 'Canceled by the user'); } - global = clearCurrentDappTransfer(getGlobal()); + if (IS_CAPACITOR) { + global = clearIsPinAccepted(global); + } + global = clearCurrentDappTransfer(global); setGlobal(global); }); @@ -272,16 +276,29 @@ addActionHandler('deleteAllDapps', (global) => { addActionHandler('deleteDapp', (global, actions, { origin }) => { const { currentAccountId } = global; - void callApi('deleteDapp', currentAccountId!, origin); + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('deleteDapp', { origin }); + } else { + void callApi('deleteDapp', currentAccountId!, origin); + } global = getGlobal(); global = removeConnectedDapp(global, origin); setGlobal(global); }); -addActionHandler('apiUpdateDappConnect', (global, actions, { +addActionHandler('apiUpdateDappConnect', async (global, actions, { accountId, dapp, permissions, promiseId, proof, }) => { + // We only need to apply changes in NBS when Dapp Connect Modal is already open + if (IS_DELEGATED_BOTTOM_SHEET) { + if (!(await waitFor(() => Boolean(getGlobal().dappConnectRequest), 300, 5))) { + return; + } + + global = getGlobal(); + } + const { isHardware } = selectAccount(global, accountId)!; global = updateDappConnectRequest(global, { @@ -310,10 +327,16 @@ addActionHandler('apiUpdateDappSendTransaction', async (global, actions, { const newestTxIds = selectNewestTxIds(global, accountId); await callApi('activateAccount', accountId, newestTxIds); global = getGlobal(); - setGlobal({ - ...global, - currentAccountId: accountId, - }); + setGlobal(updateCurrentAccountId(global, accountId)); + } + + // We only need to apply changes in NBS when Dapp Transaction Modal is already open + if (IS_DELEGATED_BOTTOM_SHEET) { + if (!(await waitFor(() => getGlobal().currentDappTransfer.state !== TransferState.None, 300, 5))) { + return; + } + + global = getGlobal(); } const state = selectIsHardwareAccount(global) && transactions.length > 1 @@ -331,3 +354,51 @@ addActionHandler('apiUpdateDappSendTransaction', async (global, actions, { }); setGlobal(global); }); + +addActionHandler('apiUpdateDappLoading', async (global, actions, { connectionType }) => { + // We only need to apply changes in NBS when Dapp Connect Modal is already open + if (IS_DELEGATED_BOTTOM_SHEET) { + if (!(await waitFor(() => { + global = getGlobal(); + return (connectionType === 'connect' && !!global.dappConnectRequest) + || (connectionType === 'sendTransaction' && global.currentDappTransfer.state !== TransferState.None); + }, 300, 5))) { + return; + } + + global = getGlobal(); + } + + if (connectionType === 'connect') { + global = updateDappConnectRequest(global, { + state: DappConnectState.Info, + }); + } else if (connectionType === 'sendTransaction') { + global = updateCurrentDappTransfer(global, { + state: TransferState.Initial, + }); + } + setGlobal(global); +}); + +addActionHandler('apiUpdateDappCloseLoading', async (global) => { + // We only need to apply changes in NBS when Dapp Modal is already open + if (IS_DELEGATED_BOTTOM_SHEET) { + if (!(await waitFor(() => { + global = getGlobal(); + return (Boolean(global.dappConnectRequest) || global.currentDappTransfer.state !== TransferState.None); + }, 300, 5))) { + return; + } + + global = getGlobal(); + } + + // But clear the state if a skeleton is displayed in the Modal + if (global.dappConnectRequest?.state === DappConnectState.Info) { + global = clearDappConnectRequest(global); + } else if (global.currentDappTransfer.state === TransferState.Initial) { + global = clearCurrentDappTransfer(global); + } + setGlobal(global); +}); diff --git a/src/global/actions/api/initial.ts b/src/global/actions/api/initial.ts index 6ab9d769..000b80d5 100644 --- a/src/global/actions/api/initial.ts +++ b/src/global/actions/api/initial.ts @@ -2,13 +2,16 @@ import { ElectronEvent } from '../../../electron/types'; import { DEFAULT_PRICE_CURRENCY, IS_EXTENSION } from '../../../config'; import { tonConnectGetDeviceInfo } from '../../../util/tonConnectEnvironment'; -import { IS_ELECTRON } from '../../../util/windowEnvironment'; +import { IS_DELEGATED_BOTTOM_SHEET, IS_ELECTRON } from '../../../util/windowEnvironment'; import { callApi, initApi } from '../../../api'; import { addActionHandler, getGlobal } from '../../index'; import { selectNewestTxIds } from '../../selectors'; addActionHandler('initApi', async (global, actions) => { - initApi(actions.apiUpdate, { isElectron: IS_ELECTRON }); + initApi(actions.apiUpdate, { + isElectron: IS_ELECTRON, + isNativeBottomSheet: IS_DELEGATED_BOTTOM_SHEET, + }); window.electron?.on(ElectronEvent.DEEPLINK_TONCONNECT, (params: { url: string }) => { const deviceInfo = tonConnectGetDeviceInfo(); diff --git a/src/global/actions/api/staking.ts b/src/global/actions/api/staking.ts index 671d09fb..1febdbb1 100644 --- a/src/global/actions/api/staking.ts +++ b/src/global/actions/api/staking.ts @@ -1,11 +1,10 @@ import { StakingState } from '../../types'; -import { DEFAULT_DECIMAL_PLACES, IS_CAPACITOR } from '../../../config'; -import { Big } from '../../../lib/big.js'; +import { IS_CAPACITOR } from '../../../config'; import { vibrateOnSuccess } from '../../../util/capacitor'; +import { callActionInMain } from '../../../util/multitab'; import { IS_DELEGATED_BOTTOM_SHEET } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; -import { humanToBigStr } from '../../helpers'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { clearIsPinAccepted, @@ -16,8 +15,6 @@ import { } from '../../reducers'; import { selectAccountState } from '../../selectors'; -import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; - addActionHandler('startStaking', (global, actions, payload) => { const isOpen = global.staking.state !== StakingState.None; if (IS_DELEGATED_BOTTOM_SHEET && !isOpen) { @@ -44,7 +41,7 @@ addActionHandler('fetchStakingFee', async (global, actions, payload) => { const result = await callApi( 'checkStakeDraft', currentAccountId, - humanToBigStr(amount!, DEFAULT_DECIMAL_PLACES), + amount!, ); if (!result || 'error' in result) { return; @@ -70,7 +67,7 @@ addActionHandler('submitStakingInitial', async (global, actions, payload) => { if (isUnstaking) { amount = selectAccountState(global, currentAccountId)!.staking!.balance; - const result = await callApi('checkUnstakeDraft', currentAccountId, humanToBigStr(amount)); + const result = await callApi('checkUnstakeDraft', currentAccountId, amount); global = getGlobal(); global = updateStaking(global, { isLoading: false }); @@ -92,7 +89,7 @@ addActionHandler('submitStakingInitial', async (global, actions, payload) => { const result = await callApi( 'checkStakeDraft', currentAccountId, - humanToBigStr(amount!, DEFAULT_DECIMAL_PLACES), + amount!, ); global = getGlobal(); global = updateStaking(global, { isLoading: false }); @@ -153,7 +150,7 @@ addActionHandler('submitStakingPassword', async (global, actions, payload) => { const { instantAvailable } = global.stakingInfo.liquid ?? {}; const stakingBalance = selectAccountState(global, currentAccountId!)!.staking!.balance; - const unstakeAmount = type === 'nominators' ? humanToBigStr(stakingBalance) : tokenAmount!; + const unstakeAmount = type === 'nominators' ? stakingBalance : tokenAmount!; const result = await callApi( 'submitUnstake', global.currentAccountId!, @@ -163,9 +160,9 @@ addActionHandler('submitStakingPassword', async (global, actions, payload) => { fee, ); - const isLongUnstakeRequested = type === 'liquid' - ? Boolean(instantAvailable) && Big(instantAvailable).lt(stakingBalance) - : true; + const isLongUnstakeRequested = Boolean( + type === 'nominators' || (type === 'liquid' && instantAvailable && instantAvailable < stakingBalance), + ); global = getGlobal(); global = updateAccountState(global, currentAccountId!, { isLongUnstakeRequested }); @@ -190,7 +187,7 @@ addActionHandler('submitStakingPassword', async (global, actions, payload) => { 'submitStake', global.currentAccountId!, password, - humanToBigStr(amount!, DEFAULT_DECIMAL_PLACES), + amount!, type!, fee, ); diff --git a/src/global/actions/api/swap.ts b/src/global/actions/api/swap.ts index e71b95fa..63feff60 100644 --- a/src/global/actions/api/swap.ts +++ b/src/global/actions/api/swap.ts @@ -17,14 +17,15 @@ import { import { IS_CAPACITOR, JWBTC_TOKEN_SLUG, TON_TOKEN_SLUG } from '../../../config'; import { Big } from '../../../lib/big.js'; import { vibrateOnSuccess } from '../../../util/capacitor'; -import safeNumberToString from '../../../util/safeNumberToString'; +import { + fromDecimal, getIsPositiveDecimal, roundDecimal, toDecimal, +} from '../../../util/decimals'; +import { callActionInMain } from '../../../util/multitab'; import { pause } from '../../../util/schedulers'; -import shiftDecimals from '../../../util/shiftDecimals'; import { buildSwapId } from '../../../util/swap/buildSwapId'; import { IS_DELEGATED_BOTTOM_SHEET } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; import { addActionHandler, getGlobal, setGlobal } from '../..'; -import { bigStrToHuman, humanToBigStr } from '../../helpers'; import { clearCurrentSwap, clearIsPinAccepted, @@ -33,8 +34,6 @@ import { } from '../../reducers'; import { selectAccount } from '../../selectors'; -import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; - const PAIRS_CACHE: Record = {}; const CACHE_DURATION = 15 * 60 * 1000; // 15 minutes @@ -57,8 +56,8 @@ function getSwapBuildOptions(global: GlobalState): ApiSwapBuildRequest { const tokenOut = global.swapTokenInfo!.bySlug[tokenOutSlug!]; const from = tokenIn.slug === TON_TOKEN_SLUG ? tokenIn.symbol : tokenIn.contract!; const to = tokenOut.slug === TON_TOKEN_SLUG ? tokenOut.symbol : tokenOut.contract!; - const fromAmount = safeNumberToString(amountIn!, tokenIn.decimals); - const toAmount = safeNumberToString(amountOut!, tokenOut.decimals); + const fromAmount = amountIn!; + const toAmount = amountOut!; const account = selectAccount(global, global.currentAccountId!); return { @@ -302,8 +301,8 @@ addActionHandler('submitSwapCexFromTon', async (global, actions, { password }) = accountId: global.currentAccountId!, slug: global.currentSwap.tokenInSlug!, toAddress: swapItem.swap.cex!.payinAddress, - amount: humanToBigStr(Number(swapItem.swap.fromAmount), asset.decimals), - fee: swapItem.swap.swapFee, + amount: fromDecimal(swapItem.swap.fromAmount, asset.decimals), // TODO + fee: fromDecimal(swapItem.swap.swapFee, asset.decimals), // TODO }; await pause(WAIT_FOR_CHANGELLY); @@ -409,16 +408,13 @@ addActionHandler('switchSwapTokens', (global) => { }); addActionHandler('setSwapTokenIn', (global, actions, { tokenSlug }) => { - const { tokenInSlug, amountIn, amountOut } = global.currentSwap; + const { amountIn, amountOut } = global.currentSwap; const isFilled = Boolean(amountIn || amountOut); - const oldTokenIn = global.swapTokenInfo!.bySlug[tokenInSlug!]; const newTokenIn = global.swapTokenInfo!.bySlug[tokenSlug!]; - const amount = amountIn - ? shiftDecimals(amountIn ?? 0, oldTokenIn.decimals, newTokenIn.decimals) - : amountIn; + const amount = amountIn ? roundDecimal(amountIn, newTokenIn.decimals) : amountIn; global = updateCurrentSwap(global, { - amountIn: amount === 0 ? undefined : amount, + amountIn: amount === '0' ? undefined : amount, tokenInSlug: tokenSlug, isEstimating: isFilled, shouldEstimate: true, @@ -427,16 +423,13 @@ addActionHandler('setSwapTokenIn', (global, actions, { tokenSlug }) => { }); addActionHandler('setSwapTokenOut', (global, actions, { tokenSlug }) => { - const { tokenOutSlug, amountIn, amountOut } = global.currentSwap; + const { amountIn, amountOut } = global.currentSwap; const isFilled = Boolean(amountIn || amountOut); - const oldTokenOut = global.swapTokenInfo!.bySlug[tokenOutSlug!]; const newTokenOut = global.swapTokenInfo!.bySlug[tokenSlug!]; - const amount = amountOut - ? shiftDecimals(amountOut ?? 0, oldTokenOut.decimals, newTokenOut.decimals) - : amountOut; + const amount = amountOut ? roundDecimal(amountOut, newTokenOut.decimals) : amountOut; global = updateCurrentSwap(global, { - amountOut: amount === 0 ? undefined : amount, + amountOut: amount === '0' ? undefined : amount, tokenOutSlug: tokenSlug, isEstimating: isFilled, shouldEstimate: true, @@ -445,7 +438,7 @@ addActionHandler('setSwapTokenOut', (global, actions, { tokenSlug }) => { }); addActionHandler('setSwapAmountIn', (global, actions, { amount }) => { - const isEstimating = Boolean(amount && amount > 0); + const isEstimating = Boolean(amount && getIsPositiveDecimal(amount)); global = updateCurrentSwap(global, { amountIn: amount, @@ -457,7 +450,7 @@ addActionHandler('setSwapAmountIn', (global, actions, { amount }) => { }); addActionHandler('setSwapAmountOut', (global, actions, { amount }) => { - const isEstimating = Boolean(amount && amount > 0); + const isEstimating = Boolean(amount && getIsPositiveDecimal(amount)); global = updateCurrentSwap(global, { amountOut: amount, @@ -533,8 +526,8 @@ addActionHandler('estimateSwap', async (global, actions, { shouldBlock }) => { const from = tokenIn.slug === TON_TOKEN_SLUG ? tokenIn.symbol : tokenIn.contract!; const to = tokenOut.slug === TON_TOKEN_SLUG ? tokenOut.symbol : tokenOut.contract!; - const fromAmount = safeNumberToString(global.currentSwap.amountIn ?? 0, tokenIn.decimals); - const toAmount = safeNumberToString(global.currentSwap.amountOut ?? 0, tokenOut.decimals); + const fromAmount = global.currentSwap.amountIn ?? '0'; + const toAmount = global.currentSwap.amountOut ?? '0'; const estimateAmount = global.currentSwap.inputSource === SwapInputSource.In ? { fromAmount } : { toAmount }; @@ -568,9 +561,9 @@ addActionHandler('estimateSwap', async (global, actions, { shouldBlock }) => { // Check for outdated response if ( (global.currentSwap.inputSource === SwapInputSource.In - && global.currentSwap.amountIn !== Number(estimate.fromAmount)) + && global.currentSwap.amountIn !== estimate.fromAmount) || (global.currentSwap.inputSource === SwapInputSource.Out - && global.currentSwap.amountOut !== Number(estimate.toAmount)) + && global.currentSwap.amountOut !== estimate.toAmount) ) { global = updateCurrentSwap(global, { ...resetParams, @@ -582,8 +575,8 @@ addActionHandler('estimateSwap', async (global, actions, { shouldBlock }) => { global = updateCurrentSwap(global, { ...( global.currentSwap.inputSource === SwapInputSource.In - ? { amountOut: Number(estimate.toAmount) } - : { amountIn: Number(estimate.fromAmount) } + ? { amountOut: estimate.toAmount } + : { amountIn: estimate.fromAmount } ), amountOutMin: estimate.toMinAmount, networkFee: estimate.networkFee, @@ -658,7 +651,7 @@ addActionHandler('estimateSwapCex', async (global, actions, { shouldBlock }) => const from = tokenIn.slug === TON_TOKEN_SLUG ? tokenIn.symbol : tokenIn.contract!; const to = tokenOut.slug === TON_TOKEN_SLUG ? tokenOut.symbol : tokenOut.contract!; - const fromAmount = safeNumberToString(global.currentSwap.amountIn ?? 0, tokenIn.decimals); + const fromAmount = global.currentSwap.amountIn ?? '0'; const estimate = await callApi('swapCexEstimate', { fromAmount, @@ -696,9 +689,9 @@ addActionHandler('estimateSwapCex', async (global, actions, { shouldBlock }) => // Check for outdated response if ( (global.currentSwap.inputSource === SwapInputSource.In - && global.currentSwap.amountIn !== Number(estimate.fromAmount)) + && global.currentSwap.amountIn !== estimate.fromAmount) || (global.currentSwap.inputSource === SwapInputSource.Out - && global.currentSwap.amountOut !== Number(estimate.toAmount)) + && global.currentSwap.amountOut !== estimate.toAmount) ) { global = updateCurrentSwap(global, { ...resetParams, @@ -717,17 +710,15 @@ addActionHandler('estimateSwapCex', async (global, actions, { shouldBlock }) => global.currentAccountId!, global.currentSwap.tokenInSlug!, account?.address!, - humanToBigStr(global.currentSwap.amountIn ?? 0, tokenIn.decimals), + fromDecimal(global.currentSwap.amountIn ?? 0, tokenIn.decimals), ); - networkFee = bigStrToHuman(txDraft?.fee ?? '0'); + networkFee = Number(toDecimal(txDraft?.fee ?? 0n)); } global = getGlobal(); - const realAmountOut = Big(estimate.toAmount); - global = updateCurrentSwap(global, { - amountOut: realAmountOut.eq(0) ? undefined : Number(realAmountOut.toFixed(tokenOut.decimals)), + amountOut: estimate.toAmount === '0' ? undefined : estimate.toAmount, limits: { fromMin: estimate.fromMin, fromMax: estimate.fromMax, @@ -735,7 +726,7 @@ addActionHandler('estimateSwapCex', async (global, actions, { shouldBlock }) => swapFee: estimate.swapFee, networkFee, realNetworkFee: networkFee, - amountOutMin: String(realAmountOut), + amountOutMin: estimate.toAmount, isEstimating: false, errorType: undefined, }); diff --git a/src/global/actions/api/wallet.ts b/src/global/actions/api/wallet.ts index 1ceee905..ae201098 100644 --- a/src/global/actions/api/wallet.ts +++ b/src/global/actions/api/wallet.ts @@ -11,13 +11,12 @@ import { compareActivities } from '../../../util/compareActivities'; import { buildCollectionByKey, findLast, mapValues, pick, unique, } from '../../../util/iteratees'; +import { callActionInMain } from '../../../util/multitab'; import { onTickEnd, pause } from '../../../util/schedulers'; import { IS_DELEGATED_BOTTOM_SHEET } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; import { ApiUserRejectsError } from '../../../api/errors'; -import { - getIsSwapId, getIsTinyTransaction, getIsTxIdLocal, humanToBigStr, -} from '../../helpers'; +import { getIsSwapId, getIsTinyTransaction, getIsTxIdLocal } from '../../helpers'; import { addActionHandler, getActions, getGlobal, setGlobal, } from '../../index'; @@ -42,8 +41,6 @@ import { selectLastTxIds, } from '../../selectors'; -import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; - const IMPORT_TOKEN_PAUSE = 250; addActionHandler('startTransfer', (global, actions, payload) => { @@ -112,7 +109,6 @@ addActionHandler('submitTransferInitial', async (global, actions, payload) => { const { tokenSlug, toAddress, amount, comment, shouldEncrypt, } = payload; - const { decimals } = global.tokenInfo!.bySlug[tokenSlug]; setGlobal(updateSendingLoading(global, true)); @@ -121,7 +117,7 @@ addActionHandler('submitTransferInitial', async (global, actions, payload) => { global.currentAccountId!, tokenSlug, toAddress, - humanToBigStr(amount, decimals), + amount, comment, shouldEncrypt, ); @@ -167,14 +163,13 @@ addActionHandler('fetchFee', async (global, actions, payload) => { const { tokenSlug, toAddress, amount, comment, shouldEncrypt, } = payload; - const { decimals } = global.tokenInfo!.bySlug[tokenSlug]; const result = await callApi( 'checkTransactionDraft', global.currentAccountId!, tokenSlug, toAddress, - humanToBigStr(amount, decimals), + amount, comment, shouldEncrypt, ); @@ -208,7 +203,6 @@ addActionHandler('submitTransferPassword', async (global, actions, { password }) fee, shouldEncrypt, } = global.currentTransfer; - const { decimals } = global.tokenInfo!.bySlug[tokenSlug!]; if (!(await callApi('verifyPassword', password))) { setGlobal(updateCurrentTransfer(getGlobal(), { error: 'Wrong password, please try again.' })); @@ -246,7 +240,7 @@ addActionHandler('submitTransferPassword', async (global, actions, { password }) password, slug: tokenSlug!, toAddress: resolvedAddress!, - amount: humanToBigStr(amount!, decimals), + amount: amount!, comment, fee, shouldEncrypt, @@ -286,7 +280,6 @@ addActionHandler('submitTransferHardware', async (global) => { parsedPayload, stateInit, } = global.currentTransfer; - const { decimals } = global.tokenInfo!.bySlug[tokenSlug!]; const accountId = global.currentAccountId!; @@ -301,7 +294,7 @@ addActionHandler('submitTransferHardware', async (global) => { if (promiseId) { const message: ApiDappTransaction = { toAddress: toAddress!, - amount: humanToBigStr(amount!, decimals), + amount: amount!, rawPayload, payload: parsedPayload, stateInit, @@ -328,7 +321,7 @@ addActionHandler('submitTransferHardware', async (global) => { password: '', slug: tokenSlug!, toAddress: resolvedAddress!, - amount: humanToBigStr(amount!, decimals), + amount: amount!, comment, fee, }; @@ -589,7 +582,7 @@ addActionHandler('addToken', (global, actions, { token }) => { const exceptionSlugsCopy = exceptionSlugs.slice(); const deletedSlugsCopy = deletedSlugs?.filter((slug) => slug !== token.slug); - if ((areTokensWithNoBalanceHidden && token.amount === 0) || (areTokensWithNoPriceHidden && token.price === 0)) { + if ((areTokensWithNoBalanceHidden && token.amount === 0n) || (areTokensWithNoPriceHidden && token.price === 0)) { exceptionSlugsCopy.push(token.slug); } @@ -598,7 +591,7 @@ addActionHandler('addToken', (global, actions, { token }) => { ...balances, bySlug: { ...balances?.bySlug, - [apiToken.slug]: '0', + [apiToken.slug]: 0n, }, }, }); @@ -670,7 +663,8 @@ addActionHandler('importToken', async (global, actions, { address, isSwap }) => 'decimals', 'keywords', ]), - amount: 0, + amount: 0n, + totalValue: '0', price: 0, change24h: 0, change7d: 0, diff --git a/src/global/actions/apiUpdates/activities.ts b/src/global/actions/apiUpdates/activities.ts index 92d4cacd..224e0250 100644 --- a/src/global/actions/apiUpdates/activities.ts +++ b/src/global/actions/apiUpdates/activities.ts @@ -3,7 +3,7 @@ import { TransferState } from '../../types'; import { IS_CAPACITOR } from '../../../config'; import { playIncomingTransactionSound } from '../../../util/appSounds'; import { compareActivities } from '../../../util/compareActivities'; -import { bigStrToHuman, getIsTinyTransaction } from '../../helpers'; +import { getIsTinyTransaction } from '../../helpers'; import { addActionHandler, setGlobal } from '../../index'; import { addLocalTransaction, @@ -27,13 +27,11 @@ addActionHandler('apiUpdate', (global, actions, update) => { transaction, transaction: { amount, txId }, } = update; - const { decimals } = global.tokenInfo!.bySlug[transaction.slug!]!; global = updateActivity(global, accountId, transaction); global = addLocalTransaction(global, accountId, transaction); - // TODO $decimal - if ((-bigStrToHuman(amount, decimals)).toFixed(decimals) === global.currentTransfer.amount?.toFixed(decimals)) { + if (-amount === global.currentTransfer.amount) { global = updateCurrentTransfer(global, { txId, state: TransferState.Complete, diff --git a/src/global/actions/apiUpdates/dapp.ts b/src/global/actions/apiUpdates/dapp.ts index 34ade47f..f86770ac 100644 --- a/src/global/actions/apiUpdates/dapp.ts +++ b/src/global/actions/apiUpdates/dapp.ts @@ -1,25 +1,21 @@ -import { DappConnectState, TransferState } from '../../types'; +import { TransferState } from '../../types'; import { TON_TOKEN_SLUG } from '../../../config'; +import { callActionInNative } from '../../../util/multitab'; import { IS_DELEGATING_BOTTOM_SHEET } from '../../../util/windowEnvironment'; -import { bigStrToHuman } from '../../helpers'; import { addActionHandler, setGlobal } from '../../index'; import { clearCurrentDappTransfer, clearCurrentSignature, clearCurrentTransfer, updateAccountState, - updateCurrentDappTransfer, updateCurrentSignature, updateCurrentTransfer, - updateDappConnectRequest, } from '../../reducers'; import { selectAccountState, } from '../../selectors'; -import { callActionInNative } from '../../../hooks/useDelegatedBottomSheet'; - addActionHandler('apiUpdate', (global, actions, update) => { switch (update.type) { case 'createTransaction': { @@ -38,7 +34,7 @@ addActionHandler('apiUpdate', (global, actions, update) => { global = updateCurrentTransfer(global, { state: TransferState.Confirm, toAddress, - amount: bigStrToHuman(amount), // TODO Unsafe? + amount, fee, comment, promiseId, @@ -107,18 +103,22 @@ addActionHandler('apiUpdate', (global, actions, update) => { } case 'dappLoading': { - const { connectionType } = update; + if (IS_DELEGATING_BOTTOM_SHEET) { + callActionInNative('apiUpdateDappLoading', update); + } - if (connectionType === 'connect') { - global = updateDappConnectRequest(global, { - state: DappConnectState.Info, - }); - } else if (connectionType === 'sendTransaction') { - global = updateCurrentDappTransfer(global, { - state: TransferState.Initial, - }); + actions.apiUpdateDappLoading(update); + + break; + } + + case 'dappCloseLoading': { + if (IS_DELEGATING_BOTTOM_SHEET) { + callActionInNative('apiUpdateDappCloseLoading'); } - setGlobal(global); + + actions.apiUpdateDappCloseLoading(); + break; } @@ -143,7 +143,7 @@ addActionHandler('apiUpdate', (global, actions, update) => { global = updateCurrentTransfer(global, { state: TransferState.Initial, toAddress, - amount: bigStrToHuman(amount || '0'), + amount: amount ?? 0n, comment, tokenSlug: TON_TOKEN_SLUG, }); diff --git a/src/global/actions/apiUpdates/initial.ts b/src/global/actions/apiUpdates/initial.ts index 8795a908..c36c46d7 100644 --- a/src/global/actions/apiUpdates/initial.ts +++ b/src/global/actions/apiUpdates/initial.ts @@ -9,7 +9,6 @@ import { updateAccount, updateAccountStakingState, updateAccountState, - updateBalance, updateBalances, updateNft, updateRestrictions, @@ -23,14 +22,6 @@ import { selectAccountState } from '../../selectors'; addActionHandler('apiUpdate', (global, actions, update) => { switch (update.type) { - case 'updateBalance': { - global = updateBalance(global, update.accountId, update.slug, update.balance); - setGlobal(global); - - actions.updateDeletionListForActiveTokens({ accountId: update.accountId }); - break; - } - case 'updateBalances': { global = updateBalances(global, update.accountId, update.balancesToUpdate); setGlobal(global); @@ -47,8 +38,8 @@ addActionHandler('apiUpdate', (global, actions, update) => { backendStakingState, } = update; - const oldBalance = selectAccountState(global, accountId)?.staking?.balance ?? 0; - let balance = 0; + const oldBalance = selectAccountState(global, accountId)?.staking?.balance ?? 0n; + let balance = 0n; if (stakingState.type === 'nominators') { balance = stakingState.amount + stakingState.pendingDepositAmount; @@ -70,7 +61,7 @@ addActionHandler('apiUpdate', (global, actions, update) => { type: stakingState.type, balance, isUnstakeRequested: !!stakingState.unstakeRequestAmount, - unstakeRequestedAmount: Number(stakingState.unstakeRequestAmount), + unstakeRequestedAmount: stakingState.unstakeRequestAmount, start: isPrevRoundUnlocked ? stakingCommonData.round.start : stakingCommonData.prevRound.start, end: isPrevRoundUnlocked ? stakingCommonData.round.unlock : stakingCommonData.prevRound.unlock, apy: stakingState.apy, @@ -86,9 +77,9 @@ addActionHandler('apiUpdate', (global, actions, update) => { let shouldOpenStakingInfo = false; if (balance !== oldBalance && global.staking.state !== StakingState.None) { - if (balance === 0) { + if (balance === 0n) { global = updateStaking(global, { state: StakingState.StakeInitial }); - } else if (oldBalance === 0) { + } else if (oldBalance === 0n) { shouldOpenStakingInfo = true; } } diff --git a/src/global/actions/ui/initial.ts b/src/global/actions/ui/initial.ts index 4be46cad..1de5a5a8 100644 --- a/src/global/actions/ui/initial.ts +++ b/src/global/actions/ui/initial.ts @@ -7,6 +7,7 @@ import { parseAccountId } from '../../../util/account'; import { initializeSoundsForSafari } from '../../../util/appSounds'; import { omit } from '../../../util/iteratees'; import { clearPreviousLangpacks, setLanguage } from '../../../util/langProvider'; +import { callActionInMain } from '../../../util/multitab'; import switchAnimationLevel from '../../../util/switchAnimationLevel'; import switchTheme, { setStatusBarStyle } from '../../../util/switchTheme'; import { @@ -25,7 +26,7 @@ import { callApi } from '../../../api'; import { addActionHandler, getActions, getGlobal, setGlobal, } from '../../index'; -import { updateCurrentAccountState } from '../../reducers'; +import { updateCurrentAccountId, updateCurrentAccountState } from '../../reducers'; import { selectCurrentNetwork, selectNetworkAccounts, @@ -33,8 +34,6 @@ import { selectNewestTxIds, } from '../../selectors'; -import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; - addActionHandler('init', (_, actions) => { requestMutation(() => { const { documentElement } = document; @@ -285,9 +284,10 @@ addActionHandler('signOut', async (global, actions, payload) => { return byId; }, {} as Record); + global = updateCurrentAccountId(global, nextAccountId); + global = { ...global, - currentAccountId: nextAccountId, accounts: { ...global.accounts!, byId: accountsById, @@ -317,9 +317,10 @@ addActionHandler('signOut', async (global, actions, payload) => { const accountsById = omit(global.accounts!.byId, [prevAccountId]); const byAccountId = omit(global.byAccountId, [prevAccountId]); + global = updateCurrentAccountId(global, nextAccountId); + global = { ...global, - currentAccountId: nextAccountId, accounts: { ...global.accounts!, byId: accountsById, diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index c6ff731d..a966b35a 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -13,6 +13,7 @@ import { getIsAddressValid } from '../../../util/getIsAddressValid'; import getIsAppUpdateNeeded from '../../../util/getIsAppUpdateNeeded'; import { unique } from '../../../util/iteratees'; import { onLedgerTabClose, openLedgerTab } from '../../../util/ledger/tab'; +import { callActionInMain } from '../../../util/multitab'; import { processDeeplink } from '../../../util/processDeeplink'; import { pause } from '../../../util/schedulers'; import { isTonConnectDeeplink } from '../../../util/ton/deeplinks'; @@ -40,8 +41,6 @@ import { selectFirstNonHardwareAccount, } from '../../selectors'; -import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; - const OPEN_LEDGER_TAB_DELAY = 500; const APP_VERSION_URL = 'version.txt'; @@ -488,7 +487,7 @@ addActionHandler('deleteToken', (global, actions, { slug }) => { const accountId = global.currentAccountId!; const { balances } = selectAccountState(global, accountId) ?? {}; - if (!balances?.bySlug[slug]) { + if (balances?.bySlug[slug] === undefined) { return; } diff --git a/src/global/cache.ts b/src/global/cache.ts index a38b96b8..a3eae999 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -15,6 +15,7 @@ import { } from '../config'; import { buildAccountId, parseAccountId } from '../util/account'; import authApi from '../util/authApi'; +import { bigintReviver } from '../util/bigint'; import { cloneDeep, mapValues, pick } from '../util/iteratees'; import { onBeforeUnload, onIdle, throttle, @@ -132,7 +133,7 @@ function readCache(initialState: GlobalState): GlobalState { } const json = localStorage.getItem(GLOBAL_STATE_CACHE_KEY); - let cached = json ? JSON.parse(json) as GlobalState : undefined; + let cached = json ? JSON.parse(json, bigintReviver) as GlobalState : undefined; if (DEBUG) { // eslint-disable-next-line no-console @@ -358,6 +359,24 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) { cached.stateVersion = 12; } + if (cached.stateVersion === 12) { + if (cached.byAccountId) { + for (const accountId of Object.keys(cached.byAccountId)) { + delete cached.byAccountId[accountId].activities; + + const { balances } = cached.byAccountId[accountId]; + if (balances) { + // eslint-disable-next-line @typescript-eslint/no-loop-func + balances.bySlug = Object.entries(balances.bySlug).reduce((acc, [slug, balance]) => { + acc[slug] = BigInt(balance); + return acc; + }, {} as Record); + } + } + } + cached.stateVersion = 13; + } + // When adding migration here, increase `STATE_VERSION` } diff --git a/src/global/helpers/index.ts b/src/global/helpers/index.ts index ec0c27dc..ca4139c6 100644 --- a/src/global/helpers/index.ts +++ b/src/global/helpers/index.ts @@ -1,25 +1,13 @@ import type { ApiToken, ApiTransaction } from '../../api/types'; -import { DEFAULT_DECIMAL_PLACES, TINY_TRANSFER_MAX_COST } from '../../config'; +import { TINY_TRANSFER_MAX_COST } from '../../config'; +import { toBig } from '../../util/decimals'; export function getIsTinyTransaction(transaction: ApiTransaction, token?: ApiToken) { if (!token) return false; const decimals = token.decimals; - const cost = Math.abs(bigStrToHuman(transaction.amount, decimals)) * token.quote.price; - return cost < TINY_TRANSFER_MAX_COST; -} - -export function bigStrToHuman(amount: string, decimalPlaces = DEFAULT_DECIMAL_PLACES) { - return divideBigInt(BigInt(amount), BigInt(10 ** decimalPlaces)); -} - -export function humanToBigStr(amount: number, decimalPlaces = DEFAULT_DECIMAL_PLACES) { - return String(Math.round(amount * (10 ** decimalPlaces))); -} - -function divideBigInt(a: bigint, b: bigint) { - const div = a / b; - return Number(div) + Number(a - div * b) / Number(b); + const cost = toBig(transaction.amount, decimals).abs().mul(token.quote.price); + return cost.lt(TINY_TRANSFER_MAX_COST); } export function getIsTxIdLocal(txId: string) { diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 94e2cd0f..c198a772 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 = 12; +export const STATE_VERSION = 13; export const INITIAL_STATE: GlobalState = { appState: AppState.Auth, diff --git a/src/global/reducers/misc.ts b/src/global/reducers/misc.ts index b03bcf96..9dd22a3a 100644 --- a/src/global/reducers/misc.ts +++ b/src/global/reducers/misc.ts @@ -1,4 +1,4 @@ -import type { ApiSwapAsset, ApiToken } from '../../api/types'; +import type { ApiBalanceBySlug, ApiSwapAsset, ApiToken } from '../../api/types'; import type { Account, AccountState, GlobalState } from '../types'; import { TON_TOKEN_SLUG } from '../../config'; @@ -82,29 +82,10 @@ export function renameAccount(global: GlobalState, accountId: string, title: str return updateAccount(global, accountId, { title }); } -export function updateBalance( - global: GlobalState, accountId: string, slug: string, balance: string, -): GlobalState { - const { balances } = selectAccountState(global, accountId) || {}; - if (balances?.bySlug[slug] === balance) { - return global; - } - - return updateAccountState(global, accountId, { - balances: { - ...balances, - bySlug: { - ...balances?.bySlug, - [slug]: balance, - }, - }, - }); -} - export function updateBalances( global: GlobalState, accountId: string, - balancesToUpdate: Record, + balancesToUpdate: ApiBalanceBySlug, ): GlobalState { if (Object.keys(balancesToUpdate).length === 0) { return global; @@ -252,3 +233,14 @@ export function updateRestrictions(global: GlobalState, partial: Partial, + balancesBySlug: ApiBalanceBySlug, tokenInfo: GlobalState['tokenInfo'], accountSettings: AccountSettings, isSortByValueEnabled: boolean = false, areTokensWithNoBalanceHidden: boolean = false, areTokensWithNoPriceHidden: boolean = false, ) => { - const getTotalValue = ({ price, amount }: UserToken) => price * amount; - return Object .entries(balancesBySlug) .filter(([slug]) => (slug in tokenInfo.bySlug)) @@ -38,10 +39,13 @@ const selectAccountTokensMemoized = memoized(( price, percentChange24h, percentChange7d, percentChange30d, history7d, history24h, history30d, }, } = tokenInfo.bySlug[slug]; - const amount = bigStrToHuman(balance, decimals); + + const amount = balance; + const totalValue = toBig(balance, decimals).mul(price).round(decimals).toString(); + const isException = accountSettings.exceptionSlugs?.includes(slug); let isDisabled = (areTokensWithNoPriceHidden && price === 0) - || (areTokensWithNoBalanceHidden && amount === 0); + || (areTokensWithNoBalanceHidden && amount === 0n); if (isException) { isDisabled = !isDisabled; @@ -67,11 +71,12 @@ const selectAccountTokensMemoized = memoized(( history30d, isDisabled, cmcSlug, + totalValue, } as UserToken; }) .sort((tokenA, tokenB) => { if (isSortByValueEnabled) { - return getTotalValue(tokenB) - getTotalValue(tokenA); + return Number(tokenB.totalValue) - Number(tokenA.totalValue); } if (!accountSettings.orderedSlugs) { @@ -105,7 +110,7 @@ export function selectCurrentAccountTokens(global: GlobalState) { function createTokenList( swapTokenInfo: GlobalState['swapTokenInfo'], - balancesBySlug: Record, + balancesBySlug: ApiBalanceBySlug, sortFn: (tokenA: ApiSwapAsset, tokenB: ApiSwapAsset) => number, filterFn?: (token: ApiSwapAsset) => boolean, ): UserSwapToken[] { @@ -114,7 +119,9 @@ function createTokenList( .map(([slug, { symbol, name, image, decimals, keywords, blockchain, contract, isPopular, }]) => { - const amount = bigStrToHuman(balancesBySlug[slug] ?? '0', decimals); + const amount = balancesBySlug[slug] ?? 0n; + const totalValue = toDecimal(amount, decimals); + return { symbol, slug, @@ -128,13 +135,14 @@ function createTokenList( keywords, blockchain, contract, + totalValue, } satisfies UserSwapToken; }) .sort(sortFn); } const selectPopularTokensMemoized = memoized( - (balancesBySlug: Record, swapTokenInfo: GlobalState['swapTokenInfo']) => { + (balancesBySlug: ApiBalanceBySlug, swapTokenInfo: GlobalState['swapTokenInfo']) => { const popularTokenOrder = [ 'TON', 'BTC', @@ -157,7 +165,7 @@ const selectPopularTokensMemoized = memoized( ); const selectSwapTokensMemoized = memoized( - (balancesBySlug: Record, swapTokenInfo: GlobalState['swapTokenInfo']) => { + (balancesBySlug: ApiBalanceBySlug, swapTokenInfo: GlobalState['swapTokenInfo']) => { const sortFn = (tokenA: ApiSwapAsset, tokenB: ApiSwapAsset) => ( tokenA.name.trim().toLowerCase().localeCompare(tokenB.name.trim().toLowerCase()) ); @@ -166,7 +174,7 @@ const selectSwapTokensMemoized = memoized( ); const selectAccountTokensForSwapMemoized = memoized(( - balancesBySlug: Record, + balancesBySlug: ApiBalanceBySlug, tokenInfo: GlobalState['tokenInfo'], swapTokenInfo: GlobalState['swapTokenInfo'], accountSettings: AccountSettings, @@ -228,7 +236,7 @@ export function selectSwapTokens(global: GlobalState) { export function selectIsNewWallet(global: GlobalState) { const tokens = selectCurrentAccountTokens(global); - return tokens?.length === 0 || (tokens?.length === 1 && tokens[0].amount === 0); + return tokens?.length === 0 || (tokens?.length === 1 && tokens[0].amount === 0n); } export function selectAccounts(global: GlobalState) { diff --git a/src/global/types.ts b/src/global/types.ts index 3cc6478b..56704a1c 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -1,7 +1,7 @@ import type { ApiTonConnectProof } from '../api/tonConnect/types'; import type { ApiActivity, - ApiAnyDisplayError, + ApiAnyDisplayError, ApiBalanceBySlug, ApiBaseCurrency, ApiDapp, ApiDappPermissions, @@ -19,6 +19,7 @@ import type { ApiTransactionActivity, ApiUpdate, ApiUpdateDappConnect, + ApiUpdateDappLoading, ApiUpdateDappSendTransactions, } from '../api/types'; import type { AuthConfig } from '../util/authApi/types'; @@ -193,7 +194,7 @@ export enum ContentTab { } export type UserToken = { - amount: number; + amount: bigint; name: string; symbol: string; image?: string; @@ -210,6 +211,7 @@ export type UserToken = { canSwap?: boolean; keywords?: string[]; cmcSlug?: string; + totalValue: string; }; export type UserSwapToken = { @@ -238,7 +240,7 @@ export interface AssetPairs { export interface AccountState { balances?: { - bySlug: Record; + bySlug: ApiBalanceBySlug; }; activities?: { isLoading?: boolean; @@ -265,15 +267,15 @@ export interface AccountState { // Staking staking?: { type: ApiStakingType; - balance: number; + balance: bigint; apy: number; isUnstakeRequested: boolean; start: number; end: number; - totalProfit: number; + totalProfit: bigint; // liquid - unstakeRequestedAmount?: number; - tokenBalance?: number; + unstakeRequestedAmount?: bigint; + tokenBalance?: bigint; isInstantUnstakeRequested?: boolean; }; stakingHistory?: ApiStakingHistory; @@ -331,8 +333,8 @@ export type GlobalState = { toAddressName?: string; resolvedAddress?: string; error?: string; - amount?: number; - fee?: string; + amount?: bigint; + fee?: bigint; comment?: string; promiseId?: string; txId?: string; @@ -348,8 +350,8 @@ export type GlobalState = { slippage: number; tokenInSlug?: string; tokenOutSlug?: string; - amountIn?: number; - amountOut?: number; + amountIn?: string; + amountOut?: string; amountOutMin?: string; transactionFee?: string; networkFee?: number; @@ -391,7 +393,7 @@ export type GlobalState = { isLoading?: boolean; transactions?: ApiDappTransaction[]; viewTransactionOnIdx?: number; - fee?: string; + fee?: bigint; dapp?: ApiDapp; error?: string; }; @@ -410,16 +412,16 @@ export type GlobalState = { state: StakingState; isLoading?: boolean; isUnstaking?: boolean; - amount?: number; - tokenAmount?: string; - fee?: string; + amount?: bigint; + tokenAmount?: bigint; + fee?: bigint; error?: string; type?: ApiStakingType; }; stakingInfo: { liquid?: { - instantAvailable: string; + instantAvailable: bigint; }; }; @@ -536,28 +538,28 @@ export interface ActionPayloads { closeHardwareWalletModal: undefined; resetHardwareWalletConnect: undefined; setTransferScreen: { state: TransferState }; - setTransferAmount: { amount?: number }; + setTransferAmount: { amount?: bigint }; setTransferToAddress: { toAddress?: string }; setTransferComment: { comment?: string }; setTransferShouldEncrypt: { shouldEncrypt?: boolean }; startTransfer: { isPortrait?: boolean; tokenSlug?: string; - amount?: number; + amount?: bigint; toAddress?: string; comment?: string; } | undefined; changeTransferToken: { tokenSlug: string }; fetchFee: { tokenSlug: string; - amount: number; + amount: bigint; toAddress: string; comment?: string; shouldEncrypt?: boolean; }; submitTransferInitial: { tokenSlug: string; - amount: number; + amount: bigint; toAddress: string; comment?: string; shouldEncrypt?: boolean; @@ -617,12 +619,12 @@ export interface ActionPayloads { // Staking startStaking: { isUnstaking?: boolean } | undefined; setStakingScreen: { state: StakingState }; - submitStakingInitial: { amount?: number; isUnstaking?: boolean } | undefined; + submitStakingInitial: { amount?: bigint; isUnstaking?: boolean } | undefined; submitStakingPassword: { password: string; isUnstaking?: boolean }; clearStakingError: undefined; cancelStaking: undefined; fetchStakingHistory: { limit?: number; offset?: number } | undefined; - fetchStakingFee: { amount: number }; + fetchStakingFee: { amount: bigint }; openStakingInfo: undefined; closeStakingInfo: undefined; @@ -685,17 +687,19 @@ export interface ActionPayloads { apiUpdateDappConnect: ApiUpdateDappConnect; apiUpdateDappSendTransaction: ApiUpdateDappSendTransactions; + apiUpdateDappLoading: ApiUpdateDappLoading; + apiUpdateDappCloseLoading: undefined; // Swap submitSwap: { password: string }; - startSwap: { tokenInSlug?: string; tokenOutSlug?: string; amountIn?: number; isPortrait?: boolean } | undefined; + startSwap: { tokenInSlug?: string; tokenOutSlug?: string; amountIn?: string; isPortrait?: boolean } | undefined; cancelSwap: { shouldReset?: boolean } | undefined; setDefaultSwapParams: { tokenInSlug?: string; tokenOutSlug?: string } | undefined; switchSwapTokens: undefined; setSwapTokenIn: { tokenSlug: string }; setSwapTokenOut: { tokenSlug: string }; - setSwapAmountIn: { amount?: number }; - setSwapAmountOut: { amount?: number }; + setSwapAmountIn: { amount?: string }; + setSwapAmountOut: { amount?: string }; setSlippage: { slippage: number }; loadSwapPairs: { tokenSlug: string; shouldForceUpdate?: boolean }; estimateSwap: { shouldBlock: boolean }; diff --git a/src/hooks/useDelegatedBottomSheet.ts b/src/hooks/useDelegatedBottomSheet.ts index 367cfcff..a86795c2 100644 --- a/src/hooks/useDelegatedBottomSheet.ts +++ b/src/hooks/useDelegatedBottomSheet.ts @@ -5,10 +5,11 @@ import type { BottomSheetKeys } from 'native-bottom-sheet'; import { BottomSheet } from 'native-bottom-sheet'; import { useEffect, useLayoutEffect, useState } from '../lib/teact/teact'; import { forceOnHeavyAnimationOnce } from '../lib/teact/teactn'; -import { getActions, setGlobal } from '../global'; +import { setGlobal } from '../global'; -import type { ActionPayloads, GlobalState } from '../global/types'; +import type { GlobalState } from '../global/types'; +import { bigintReviver } from '../util/bigint'; import cssColorToHex from '../util/cssColorToHex'; import { setStatusBarStyle } from '../util/switchTheme'; import { IS_DELEGATED_BOTTOM_SHEET } from '../util/windowEnvironment'; @@ -38,7 +39,7 @@ if (IS_DELEGATED_BOTTOM_SHEET) { controlledByMain.get(key)?.(); setGlobal( - JSON.parse(globalJson) as GlobalState, + JSON.parse(globalJson, bigintReviver) as GlobalState, { forceOutdated: true, forceSyncOnIOs: true }, ); }); @@ -46,14 +47,6 @@ if (IS_DELEGATED_BOTTOM_SHEET) { BottomSheet.addListener('move', () => { window.dispatchEvent(new Event('viewportmove')); }); - - BottomSheet.addListener( - 'callActionInNative', - ({ name, optionsJson }: { name: string; optionsJson: string }) => { - const action = getActions()[name as K]; - action((JSON.parse(optionsJson) || undefined) as ActionPayloads[K]); - }, - ); } export function useDelegatedBottomSheet( @@ -82,7 +75,7 @@ export function useDelegatedBottomSheet( BottomSheet.closeSelf({ key }); setStatusBarStyle(); } - }, [dialogRef, isOpen, key, onClose]); + }, [isOpen, dialogRef, key, onClose]); const isDelegatedAndOpen = IS_DELEGATED_BOTTOM_SHEET && key && isOpen; @@ -172,22 +165,6 @@ export function useOpenFromMainBottomSheet( }, [key, open]); } -export function callActionInMain(name: K, options?: ActionPayloads[K]) { - BottomSheet.callActionInMain({ - name, - // eslint-disable-next-line no-null/no-null - optionsJson: JSON.stringify(options ?? null), - }); -} - -export function callActionInNative(name: K, options?: ActionPayloads[K]) { - BottomSheet.callActionInNative({ - name, - // eslint-disable-next-line no-null/no-null - optionsJson: JSON.stringify(options ?? null), - }); -} - export function openInMain(key: BottomSheetKeys) { BottomSheet.openInMain({ key }); } diff --git a/src/hooks/useDelegatingBottomSheet.ts b/src/hooks/useDelegatingBottomSheet.ts index 86583977..2ab83725 100644 --- a/src/hooks/useDelegatingBottomSheet.ts +++ b/src/hooks/useDelegatingBottomSheet.ts @@ -2,9 +2,7 @@ import type { BottomSheetKeys } from 'native-bottom-sheet'; import { BottomSheet } from 'native-bottom-sheet'; import { useEffect } from '../lib/teact/teact'; import { forceOnHeavyAnimationOnce } from '../lib/teact/teactn'; -import { getActions, getGlobal } from '../global'; - -import type { ActionPayloads } from '../global/types'; +import { getGlobal } from '../global'; import { pause } from '../util/schedulers'; import { IS_DELEGATING_BOTTOM_SHEET } from '../util/windowEnvironment'; @@ -18,14 +16,6 @@ const controlledByNative = new Map(); if (IS_DELEGATING_BOTTOM_SHEET) { BottomSheet.prepare(); - BottomSheet.addListener( - 'callActionInMain', - ({ name, optionsJson }: { name: string; optionsJson: string }) => { - const action = getActions()[name as K]; - action((JSON.parse(optionsJson) || undefined) as ActionPayloads[K]); - }, - ); - BottomSheet.addListener( 'openInMain', ({ key }: { key: BottomSheetKeys }) => { diff --git a/src/i18n/ru.yaml b/src/i18n/ru.yaml index 193e1fe3..4c2999ad 100644 --- a/src/i18n/ru.yaml +++ b/src/i18n/ru.yaml @@ -251,7 +251,7 @@ InvalidToAddress: Некорректный адрес получателя DomainNotResolved: Ошибка резолвинга домена You can save this address for quick access while sending.: Вы можете сохранить этот адрес для быстрого доступа при отправке. Stake Again: Повторить депозит -Earned: Earned +Earned: Заработано Connect Dapp: Подключить приложение Select wallets to use on this dapp: Выберите кошельки для использования с этим приложением Connect: Подключить diff --git a/src/index.html b/src/index.html index 48bbcd14..7a78cc62 100644 --- a/src/index.html +++ b/src/index.html @@ -44,6 +44,7 @@ <% if (process.env.IS_EXTENSION !== '1') { %> <% } %> + diff --git a/src/index.tsx b/src/index.tsx index d4442039..d13e383e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -32,9 +32,9 @@ if (IS_CAPACITOR) { } if (IS_DELEGATING_BOTTOM_SHEET) { - initMultitab({ noPub: true }); + initMultitab({ noPubGlobal: true }); } else if (IS_DELEGATED_BOTTOM_SHEET) { - initMultitab({ noSub: true }); + initMultitab(); } (async () => { diff --git a/src/styles/index.scss b/src/styles/index.scss index ef6792d0..d809ec22 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -199,3 +199,26 @@ a { opacity: 1; } } + +.browser-update-message { + position: fixed; + z-index: var(--z-notification); + top: 50% ; + left: 50%; + transform: translate(-50%, -50%); + + min-width: 360px; + padding: 1.5rem; + + font-size: 2rem; + color: var(--color-gray-1); + text-align: center; + white-space: pre-wrap; + + background: var(--color-background-first); + box-shadow: var(--default-shadow); + + html.is-rendered & { + display: none; + } +} diff --git a/src/util/PostMessageConnector.ts b/src/util/PostMessageConnector.ts index 63ca91bb..b3beb933 100644 --- a/src/util/PostMessageConnector.ts +++ b/src/util/PostMessageConnector.ts @@ -1,3 +1,4 @@ +import { bigintReviver } from './bigint'; import generateUniqueId from './generateUniqueId'; export interface CancellableCallback { @@ -91,6 +92,7 @@ class ConnectorClass { public target: Worker | Window | chrome.runtime.Port, private onUpdate?: (update: ApiUpdate) => void, private channel?: string, + private shouldUseJson?: boolean, private targetOrigin = '*', ) { } @@ -161,7 +163,11 @@ class ConnectorClass { }); } - onMessage(data: WorkerMessageData) { + onMessage(data: WorkerMessageData | string) { + if (typeof data === 'string') { + data = JSON.parse(data, bigintReviver) as WorkerMessageData; + } + const { requestStates, channel } = this; if (data.channel !== channel) { return; @@ -190,10 +196,15 @@ class ConnectorClass { private postMessage(data: AnyLiteral) { data.channel = this.channel; + let rawData: AnyLiteral | string = data; + if (this.shouldUseJson) { + rawData = JSON.stringify(data); + } + if (this.target === window) { - this.target.postMessage(data, this.targetOrigin); + this.target.postMessage(rawData, this.targetOrigin); } else { - this.target.postMessage(data); + this.target.postMessage(rawData); } } } @@ -204,7 +215,7 @@ export function createConnector( channel?: string, targetOrigin?: string, ) { - const connector = new ConnectorClass(worker, onUpdate, channel, targetOrigin); + const connector = new ConnectorClass(worker, onUpdate, channel, undefined, targetOrigin); function handleMessage({ data }: WorkerMessageEvent | MessageEvent) { connector.onMessage(data); @@ -225,13 +236,13 @@ export function createExtensionConnector( getInitArgs?: () => any, channel?: string, ) { - const connector = new ConnectorClass(connect(), onUpdate, channel); + const connector = new ConnectorClass(connect(), onUpdate, channel, true); function connect() { // eslint-disable-next-line no-restricted-globals const port = self.chrome.runtime.connect({ name }); - port.onMessage.addListener((data: WorkerMessageData) => { + port.onMessage.addListener((data: string | WorkerMessageData) => { connector.onMessage(data); }); diff --git a/src/util/bigint.ts b/src/util/bigint.ts new file mode 100644 index 00000000..581f5e16 --- /dev/null +++ b/src/util/bigint.ts @@ -0,0 +1,28 @@ +import { ONE_TON } from '../config'; +import { fromDecimal } from './decimals'; + +const PREFIX = 'bigint:'; + +// @ts-ignore +BigInt.prototype.toJSON = function toJSON() { + return `${PREFIX}${this}`; +}; + +export function bigintReviver(this: any, key: string, value: any) { + if (typeof value === 'string' && value.startsWith(PREFIX)) { + return BigInt(value.slice(7)); + } + return value; +} + +export function bigintAbs(value: bigint) { + return value === -0n || value < 0n ? -value : value; +} + +export function bigintDivideToNumber(value: bigint, num: number) { + return (value * ONE_TON) / fromDecimal(num); +} + +export function bigintMultiplyToNumber(value: bigint, num: number) { + return (value * fromDecimal(num)) / ONE_TON; +} diff --git a/src/util/calcChangeValue.ts b/src/util/calcChangeValue.ts index ff60cd83..cfa46a69 100644 --- a/src/util/calcChangeValue.ts +++ b/src/util/calcChangeValue.ts @@ -1,3 +1,11 @@ +import { Big } from '../lib/big.js'; + export function calcChangeValue(currentPrice: number, changeFactor: number) { return currentPrice - currentPrice / (1 + changeFactor); } + +export function calcBigChangeValue(currentPrice: Big | string, changeFactor: Big | number) { + currentPrice = Big(currentPrice); + changeFactor = Big(changeFactor); + return currentPrice.minus(currentPrice.div(changeFactor.plus(1))); +} diff --git a/src/util/clipboard.ts b/src/util/clipboard.ts index 1cf99d44..bf5073be 100644 --- a/src/util/clipboard.ts +++ b/src/util/clipboard.ts @@ -1,3 +1,6 @@ +import { Clipboard } from '@capacitor/clipboard'; + +import { IS_CAPACITOR } from '../config'; import { logDebugError } from './logs'; export const CLIPBOARD_ITEM_SUPPORTED = window.navigator.clipboard && window.ClipboardItem; @@ -44,3 +47,13 @@ async function copyBlobToClipboard(pngBlob: Blob | null) { logDebugError('copyBlobToClipboard', err); } } + +export async function readClipboardContent() { + if (IS_CAPACITOR) { + const { value, type } = await Clipboard.read(); + return { text: value, type }; + } else { + const text = await navigator.clipboard.readText(); + return { text, type: 'text/plain' }; + } +} diff --git a/src/util/createPostMessageInterface.ts b/src/util/createPostMessageInterface.ts index df86fa7b..2e78fe25 100644 --- a/src/util/createPostMessageInterface.ts +++ b/src/util/createPostMessageInterface.ts @@ -4,6 +4,7 @@ import type { } from './PostMessageConnector'; import { DETACHED_TAB_URL } from './ledger/tab'; +import { bigintReviver } from './bigint'; import { logDebugError } from './logs'; declare const self: WorkerGlobalScope; @@ -67,12 +68,16 @@ export function createExtensionInterface( function sendToOrigin(data: WorkerMessageData) { data.channel = channel; - port.postMessage(data); + const json = JSON.stringify(data); + port.postMessage(json); } handleErrors(sendToOrigin); - port.onMessage.addListener((data: OriginMessageData) => { + port.onMessage.addListener((data: OriginMessageData | string) => { + if (typeof data === 'string') { + data = JSON.parse(data, bigintReviver) as OriginMessageData; + } if (data.channel === channel) { onMessage(api, data, sendToOrigin, dAppUpdater, origin); } diff --git a/src/util/decimals.ts b/src/util/decimals.ts new file mode 100644 index 00000000..1b0ad421 --- /dev/null +++ b/src/util/decimals.ts @@ -0,0 +1,29 @@ +import { DEFAULT_DECIMAL_PLACES } from '../config'; +import { Big } from '../lib/big.js'; + +Big.RM = 0; // RoundDown +Big.NE = -100000; // Disable exponential form +Big.PE = 100000; // Disable exponential form + +const ten = Big(10); + +export function fromDecimal(value: string | number, decimals?: number) { + return BigInt(Big(value).mul(ten.pow(decimals ?? DEFAULT_DECIMAL_PLACES)).round().toString()); +} + +export function toDecimal(value: bigint | number, decimals?: number) { + return toBig(value, decimals ?? DEFAULT_DECIMAL_PLACES).toString(); +} + +export function toBig(value: bigint | number, decimals?: number) { + if (decimals === undefined) decimals = DEFAULT_DECIMAL_PLACES; + return Big(value.toString()).div(ten.pow(decimals)).round(decimals); +} + +export function roundDecimal(value: string, decimals: number) { + return Big(value).round(decimals).toString(); +} + +export function getIsPositiveDecimal(value: string) { + return !value.startsWith('-'); +} diff --git a/src/util/formatNumber.ts b/src/util/formatNumber.ts index ba104f36..0addf375 100644 --- a/src/util/formatNumber.ts +++ b/src/util/formatNumber.ts @@ -1,13 +1,16 @@ import type { ApiBaseCurrency } from '../api/types'; import { DEFAULT_DECIMAL_PLACES, DEFAULT_PRICE_CURRENCY, SHORT_CURRENCY_SYMBOL_MAP } from '../config'; +import { Big } from '../lib/big.js'; +import { toDecimal } from './decimals'; import withCache from './withCache'; const SHORT_SYMBOLS = new Set(Object.values(SHORT_CURRENCY_SYMBOL_MAP)); -export const formatInteger = withCache((value: number, fractionDigits = 2, noRadix = false) => { - const dp = value >= 1 ? fractionDigits : DEFAULT_DECIMAL_PLACES; - const fixed = value.toFixed(dp); +export const formatInteger = withCache((value: number | Big | string, fractionDigits = 2, noRadix = false) => { + value = Big(value); + const dp = value.gte(1) ? fractionDigits : DEFAULT_DECIMAL_PLACES; + const fixed = value.round(dp).toString(); let [wholePart, fractionPart = ''] = fixed.split('.'); @@ -25,20 +28,26 @@ export const formatInteger = withCache((value: number, fractionDigits = 2, noRad ].filter(Boolean).join('.'); }); -export function formatCurrency(value: number, currency: string, fractionDigits?: number) { +export function formatCurrency(value: number | string | Big, currency: string, fractionDigits?: number) { const formatted = formatInteger(value, fractionDigits); return addCurrency(formatted, currency); } -export function formatCurrencyExtended(value: number, currency: string, noSign = false, fractionDigits?: number) { - const prefix = !noSign ? (value > 0 ? '+\u202F' : '\u2212\u202F') : ''; +export function formatCurrencyExtended( + value: number | string, currency: string, noSign = false, fractionDigits?: number, +) { + value = value.toString(); - return prefix + formatCurrency(noSign ? value : Math.abs(value), currency, fractionDigits); + const prefix = !noSign ? (!value.startsWith('-') ? '+\u202F' : '\u2212\u202F') : ''; + + return prefix + formatCurrency(noSign ? value : value.replace('-', ''), currency, fractionDigits); } -export function formatCurrencySimple(value: number, currency: string, decimals?: number) { - const stringValue = clearZeros(value.toFixed(decimals ?? DEFAULT_DECIMAL_PLACES)); - return addCurrency(stringValue, currency); +export function formatCurrencySimple(value: number | bigint | string, currency: string, decimals?: number) { + if (typeof value !== 'string') { + value = toDecimal(value, decimals); + } + return addCurrency(value, currency); } function addCurrency(value: number | string, currency: string) { @@ -47,11 +56,6 @@ function addCurrency(value: number | string, currency: string) { : `${value} ${currency}`; } -function clearZeros(value: string) { - if (value.indexOf('.') === -1) return value; - return value.replace(/\.?0*$/, ''); -} - export function formatCurrencyForBigValue(value: number, currency: string, threshold = 1000) { const formattedValue = formatCurrency(value, currency); diff --git a/src/util/ledger/index.ts b/src/util/ledger/index.ts index 96e2ecf8..b6071681 100644 --- a/src/util/ledger/index.ts +++ b/src/util/ledger/index.ts @@ -1,11 +1,12 @@ -import { - Address, Builder, Cell, SendMode, -} from 'ton-core'; import { StatusCodes } from '@ledgerhq/errors'; import TransportWebHID from '@ledgerhq/hw-transport-webhid'; import TransportWebUSB from '@ledgerhq/hw-transport-webusb'; import type { TonPayloadFormat } from '@ton-community/ton-ledger'; import { TonTransport } from '@ton-community/ton-ledger'; +import { Address } from '@ton/core/dist/address/Address'; +import { Builder } from '@ton/core/dist/boc/Builder'; +import { Cell } from '@ton/core/dist/boc/Cell'; +import { SendMode } from '@ton/core/dist/types/SendMode'; import type { ApiTonConnectProof } from '../../api/tonConnect/types'; import type { @@ -227,7 +228,7 @@ export async function buildLedgerTokenTransfer( slug: string, fromAddress: string, toAddress: string, - amount: string, + amount: bigint, comment?: string, ) { const { minterAddress } = (await callApi('resolveTokenBySlug', slug))!; @@ -249,7 +250,7 @@ export async function buildLedgerTokenTransfer( const payload: TonPayloadFormat = { type: 'jetton-transfer', queryId: 0n, - amount: BigInt(amount), + amount, destination: Address.parse(toAddress), responseDestination: Address.parse(fromAddress), // eslint-disable-next-line no-null/no-null @@ -259,7 +260,7 @@ export async function buildLedgerTokenTransfer( }; return { - amount: TOKEN_TRANSFER_TON_AMOUNT.toString(), + amount: TOKEN_TRANSFER_TON_AMOUNT, toAddress: tokenWalletAddress!, payload, }; @@ -388,7 +389,7 @@ export async function signLedgerTransactions( fromAddress: fromAddress!, toAddress: message.toAddress, comment: message.payload?.type === 'comment' ? message.payload.comment : undefined, - fee: '0', + fee: 0n, slug: TON_TOKEN_SLUG, }, }); @@ -436,7 +437,7 @@ export async function getNextLedgerWallets( continue; } - if (walletInfo.balance !== '0') { + if (walletInfo.balance !== 0n) { result.push(walletInfo); index += 1; continue; diff --git a/src/util/ledger/types.ts b/src/util/ledger/types.ts index b7e6023b..6dcd5e16 100644 --- a/src/util/ledger/types.ts +++ b/src/util/ledger/types.ts @@ -4,7 +4,7 @@ export interface LedgerWalletInfo { index: number; address: string; publicKey: string; - balance: string; + balance: bigint; version: ApiWalletVersion; driver: ApiLedgerDriver; diff --git a/src/util/multitab.ts b/src/util/multitab.ts index 6544a9f8..12d397c9 100644 --- a/src/util/multitab.ts +++ b/src/util/multitab.ts @@ -1,13 +1,13 @@ import { addCallback } from '../lib/teact/teactn'; -import { getGlobal, setGlobal } from '../global'; +import { getActions, getGlobal, setGlobal } from '../global'; -import type { GlobalState } from '../global/types'; +import type { ActionPayloads, GlobalState } from '../global/types'; import { MULTITAB_DATA_CHANNEL_NAME } from '../config'; import { deepDiff } from './deepDiff'; import { deepMerge } from './deepMerge'; import { omit } from './iteratees'; -import { IS_MULTITAB_SUPPORTED } from './windowEnvironment'; +import { IS_DELEGATED_BOTTOM_SHEET, IS_DELEGATING_BOTTOM_SHEET, IS_MULTITAB_SUPPORTED } from './windowEnvironment'; import { isBackgroundModeActive } from '../hooks/useBackgroundMode'; @@ -16,7 +16,21 @@ interface BroadcastChannelGlobalDiff { diff: any; } -type BroadcastChannelMessage = BroadcastChannelGlobalDiff; +interface BroadcastChannelCallActionInMain { + type: 'callActionInMain'; + name: K; + options?: ActionPayloads[K]; +} + +interface BroadcastChannelCallActionInNative { + type: 'callActionInNative'; + name: K; + options?: ActionPayloads[K]; +} + +type BroadcastChannelMessage = BroadcastChannelGlobalDiff +| BroadcastChannelCallActionInMain +| BroadcastChannelCallActionInNative; type EventListener = (type: 'message', listener: (event: { data: BroadcastChannelMessage }) => void) => void; export type TypedBroadcastChannel = { @@ -31,16 +45,14 @@ const channel = IS_MULTITAB_SUPPORTED let currentGlobal = getGlobal(); -export function initMultitab({ noPub, noSub }: { noPub?: boolean; noSub?: boolean } = {}) { +export function initMultitab({ noPubGlobal }: { noPubGlobal?: boolean } = {}) { if (!channel) return; - if (!noPub) { + if (!noPubGlobal) { addCallback(handleGlobalChange); } - if (!noSub) { - channel.addEventListener('message', handleMultitabMessage); - } + channel.addEventListener('message', handleMultitabMessage); } function handleGlobalChange(global: GlobalState) { @@ -70,11 +82,47 @@ function omitLocalOnlyKeys(global: GlobalState) { function handleMultitabMessage({ data }: { data: BroadcastChannelMessage }) { switch (data.type) { case 'globalDiffUpdate': { + if (IS_DELEGATED_BOTTOM_SHEET) return; + currentGlobal = deepMerge(getGlobal(), data.diff); setGlobal(currentGlobal); break; } + + case 'callActionInMain': { + if (!IS_DELEGATING_BOTTOM_SHEET) return; + + const { name, options } = data; + + getActions()[name](options as never); + break; + } + + case 'callActionInNative': { + if (!IS_DELEGATED_BOTTOM_SHEET) return; + + const { name, options } = data; + + getActions()[name](options as never); + break; + } } } + +export function callActionInMain(name: K, options?: ActionPayloads[K]) { + channel!.postMessage({ + type: 'callActionInMain', + name, + options, + }); +} + +export function callActionInNative(name: K, options?: ActionPayloads[K]) { + channel!.postMessage({ + type: 'callActionInNative', + name, + options, + }); +} diff --git a/src/util/processDeeplink.ts b/src/util/processDeeplink.ts index a129907d..6f986ff0 100644 --- a/src/util/processDeeplink.ts +++ b/src/util/processDeeplink.ts @@ -2,7 +2,6 @@ import { BottomSheet } from 'native-bottom-sheet'; import { getActions } from '../global'; import { TON_TOKEN_SLUG } from '../config'; -import { bigStrToHuman } from '../global/helpers'; import { parseTonDeeplink } from './ton/deeplinks'; import { pause } from './schedulers'; import { IS_DELEGATING_BOTTOM_SHEET } from './windowEnvironment'; @@ -22,7 +21,7 @@ export async function processDeeplink(url: string) { isPortrait: true, tokenSlug: TON_TOKEN_SLUG, toAddress: params.to, - amount: params.amount ? bigStrToHuman(params.amount) : undefined, + amount: params.amount, comment: params.comment, }); diff --git a/src/util/safeNumberToString.ts b/src/util/safeNumberToString.ts deleted file mode 100644 index d4e06447..00000000 --- a/src/util/safeNumberToString.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Big } from '../lib/big.js/index.js'; - -export default function safeNumberToString(value: number, decimals: number) { - const result = String(value); - if (result.includes('e-')) { - Big.NE = -decimals - 1; - return new Big(result).toString(); - } - return result; -} diff --git a/src/util/schedulers.ts b/src/util/schedulers.ts index 8a4e22ee..e7b781ca 100644 --- a/src/util/schedulers.ts +++ b/src/util/schedulers.ts @@ -197,3 +197,17 @@ export function onBeforeUnload(callback: NoneToVoidFunction, isLast = false) { beforeUnloadCallbacks = beforeUnloadCallbacks!.filter((cb) => cb !== callback); }; } + +export async function waitFor(cb: () => boolean, interval: number, attempts: number) { + let i = 0; + let result = cb(); + + while (!result && i < attempts) { + await pause(interval); + + i++; + result = cb(); + } + + return result; +} diff --git a/src/util/shiftDecimals.ts b/src/util/shiftDecimals.ts deleted file mode 100644 index a9a50d42..00000000 --- a/src/util/shiftDecimals.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default function shiftDecimals( - amount: number, - fromDecimals: number, - toDecimals: number, -): number { - if (toDecimals >= fromDecimals) return amount; - - return Number(amount.toFixed(toDecimals)); -} diff --git a/src/util/stringFormat.ts b/src/util/stringFormat.ts index 83cd9e29..f67ae742 100644 --- a/src/util/stringFormat.ts +++ b/src/util/stringFormat.ts @@ -6,3 +6,10 @@ export function isAscii(str: string) { } return true; } + +export function insertSubstring(str: string, start: number, newSubStr: string) { + if (start < 0) { + start = str.length - start; + } + return str.slice(0, start) + newSubStr + str.slice(start); +} diff --git a/src/util/ton/deeplinks.ts b/src/util/ton/deeplinks.ts index dadaf686..803488ad 100644 --- a/src/util/ton/deeplinks.ts +++ b/src/util/ton/deeplinks.ts @@ -7,10 +7,15 @@ export function parseTonDeeplink(value: string | unknown) { try { const url = new URL(value); + + const to = url.pathname.replace(/.*\//, ''); + const amount = url.searchParams.get('amount') ?? undefined; + const comment = url.searchParams.get('text') ?? undefined; + return { - to: url.pathname.replace(/.*\//, ''), - amount: url.searchParams.get('amount') ?? undefined, - comment: url.searchParams.get('text') ?? undefined, + to, + amount: amount ? BigInt(amount) : undefined, + comment, }; } catch (err) { return undefined; diff --git a/src/util/ton/formatTransferUrl.ts b/src/util/ton/formatTransferUrl.ts index 859abd91..4ee37aa5 100644 --- a/src/util/ton/formatTransferUrl.ts +++ b/src/util/ton/formatTransferUrl.ts @@ -1,5 +1,5 @@ // https://github.com/toncenter/tonweb/blob/944455da2effaa307a2030d00e19a37e6e94897c/src/utils/index.js#L94-L109 -export default function formatTransferUrl(address: string, amount?: string, text?: string) { +export default function formatTransferUrl(address: string, amount?: bigint, text?: string) { const url = `ton://transfer/${address}`; const params = []; diff --git a/src/util/windowEnvironment.ts b/src/util/windowEnvironment.ts index 8bdf2cb4..b63b2b76 100644 --- a/src/util/windowEnvironment.ts +++ b/src/util/windowEnvironment.ts @@ -51,7 +51,6 @@ export const IS_ELECTRON = Boolean(window.electron); export const DEFAULT_LANG_CODE = 'en'; export const USER_AGENT_LANG_CODE = getBrowserLanguage(); export const DPR = window.devicePixelRatio || 1; -// N.B. Google Chrome on Android supports webusb pretty well, so can use Ledger there export const IS_LEDGER_SUPPORTED = !(IS_IOS || (IS_ANDROID && IS_CAPACITOR) || IS_FIREFOX_EXTENSION); export const IS_LEDGER_EXTENSION_TAB = global.location.hash.startsWith('#detached'); // Disable biometric auth on electron for now until this issue is fixed: