From 5cd00b2b2de2e113179d71915389e0c56d48aad8 Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Thu, 9 May 2024 19:02:19 +0300 Subject: [PATCH 1/5] fix(mobile): User's QoL improvements (#834) * fix(mobile): App freeze on start * bump(mobile): 4.4.1 * fix(mobile): Validate messages properly * replace symbol with FAKE for blacklisted jettons * fix(mobile): Increase storage size * feature(mobile): Remove "time not synced" alert * fix(mobile): Increase asyncstorage size * fix(mobile): improves * fix(mobile): Fix jettons transfer, update cache policy --- .../src/service/transactionService.ts | 6 +- packages/@core-js/src/utils/tonProof.ts | 3 +- packages/mobile/android/app/build.gradle | 2 +- .../java/com/ton_keeper/MainApplication.java | 15 ++++ packages/mobile/android/gradle.properties | 1 + .../ios/ton_keeper.xcodeproj/project.pbxproj | 4 +- packages/mobile/src/blockchain/wallet.ts | 20 +++-- packages/mobile/src/core/Jetton/Jetton.tsx | 9 +- .../NFTOperations/Modals/SignRawModal.tsx | 21 ++++- .../TimeNotSynced/TimeNotSynced.tsx | 65 --------------- packages/mobile/src/core/NFTSend/NFTSend.tsx | 10 ++- packages/mobile/src/core/Swap/Swap.tsx | 7 -- .../src/core/TonConnect/TonConnectModal.tsx | 2 +- packages/mobile/src/database/main.ts | 19 ----- .../hooks/useDeeplinkingResolvers.ts | 17 ---- packages/mobile/src/store/main/index.ts | 18 ---- packages/mobile/src/store/main/interface.ts | 4 - packages/mobile/src/store/main/sagas.ts | 38 +-------- packages/mobile/src/store/wallet/sagas.ts | 3 - .../mobile/src/store/zustand/swap/index.ts | 2 - .../mobile/src/store/zustand/swap/types.ts | 46 ----------- .../src/store/zustand/swap/useSwapStore.ts | 82 ------------------- .../Wallet/hooks/useInternalNotifications.ts | 26 +----- .../src/tonconnect/ConnectReplyBuilder.ts | 35 ++++---- packages/mobile/src/tonconnect/TonConnect.ts | 10 --- packages/mobile/src/utils/proof.ts | 4 +- packages/mobile/src/utils/stats.ts | 16 ---- .../models/ActivityModel/ActivityModel.ts | 11 ++- .../JettonBalanceModel/JettonBalanceModel.ts | 6 +- packages/mobile/src/wallet/utils.ts | 1 + packages/shared/utils/blockchain.ts | 19 +++++ 31 files changed, 130 insertions(+), 392 deletions(-) delete mode 100644 packages/mobile/src/core/ModalContainer/TimeNotSynced/TimeNotSynced.tsx delete mode 100644 packages/mobile/src/store/zustand/swap/index.ts delete mode 100644 packages/mobile/src/store/zustand/swap/types.ts delete mode 100644 packages/mobile/src/store/zustand/swap/useSwapStore.ts diff --git a/packages/@core-js/src/service/transactionService.ts b/packages/@core-js/src/service/transactionService.ts index 5ee4dbdc3..cb4ee7d1a 100644 --- a/packages/@core-js/src/service/transactionService.ts +++ b/packages/@core-js/src/service/transactionService.ts @@ -12,11 +12,13 @@ import { import { Address as AddressFormatter } from '../formatters/Address'; import { OpCodes, WalletContract } from './contractService'; import { SignRawMessage } from '@tonkeeper/mobile/src/core/ModalContainer/NFTOperations/TxRequest.types'; +import { tk } from '@tonkeeper/mobile/src/wallet'; export type AnyAddress = string | Address | AddressFormatter; export interface TransferParams { seqno: number; + timeout?: number; sendMode?: number; secretKey: Buffer; messages: MessageRelaxed[]; @@ -35,7 +37,7 @@ export function tonAddress(address: AnyAddress) { export class TransactionService { public static TTL = 5 * 60; - private static getTimeout() { + public static getTimeout() { return Math.floor(Date.now() / 1e3) + TransactionService.TTL; } @@ -163,7 +165,7 @@ export class TransactionService { static createTransfer(contract, transferParams: TransferParams) { const transfer = contract.createTransfer({ - timeout: TransactionService.getTimeout(), + timeout: transferParams.timeout ?? TransactionService.getTimeout(), seqno: transferParams.seqno, secretKey: transferParams.secretKey, sendMode: diff --git a/packages/@core-js/src/utils/tonProof.ts b/packages/@core-js/src/utils/tonProof.ts index 02869087d..43f2b0b1f 100644 --- a/packages/@core-js/src/utils/tonProof.ts +++ b/packages/@core-js/src/utils/tonProof.ts @@ -4,6 +4,7 @@ import nacl from 'tweetnacl'; import naclUtils from 'tweetnacl-util'; const { createHash } = require('react-native-crypto'); import { Address } from '../formatters/Address'; +import { getRawTimeFromLiteserverSafely } from '@tonkeeper/shared/utils/blockchain'; export interface TonProofArgs { address: string; @@ -22,7 +23,7 @@ export async function createTonProof({ }: TonProofArgs) { try { const address = Address.parse(_addr).toRaw(); - const timestamp = Math.floor(Date.now() / 1000); + const timestamp = await getRawTimeFromLiteserverSafely(); const timestampBuffer = new Int64LE(timestamp).toBuffer(); const domainBuffer = Buffer.from(domain); diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 35f52069d..0cc75381b 100644 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -92,7 +92,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 433 - versionName "4.5.0" + versionName "4.4.1" missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'store', 'play' } diff --git a/packages/mobile/android/app/src/main/java/com/ton_keeper/MainApplication.java b/packages/mobile/android/app/src/main/java/com/ton_keeper/MainApplication.java index 6def30c19..dac4f818d 100644 --- a/packages/mobile/android/app/src/main/java/com/ton_keeper/MainApplication.java +++ b/packages/mobile/android/app/src/main/java/com/ton_keeper/MainApplication.java @@ -20,6 +20,9 @@ import java.util.List; +import java.lang.reflect.Field; +import android.database.CursorWindow; + public class MainApplication extends Application implements ReactApplication { private final ReactNativeHost mReactNativeHost = @@ -69,6 +72,18 @@ public void onCreate() { } ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); ApplicationLifecycleDispatcher.onApplicationCreate(this); + + // https://github.com/react-native-async-storage/async-storage/issues/537 + try { + Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize"); + field.setAccessible(true); + field.set(null, 300 * 1024 * 1024); + } catch (Exception e) { + if (BuildConfig.DEBUG) { + e.printStackTrace(); + } + } + } @Override diff --git a/packages/mobile/android/gradle.properties b/packages/mobile/android/gradle.properties index 70eb1b545..e9c5769d0 100644 --- a/packages/mobile/android/gradle.properties +++ b/packages/mobile/android/gradle.properties @@ -54,3 +54,4 @@ expo.webp.animated=false # Enable network inspector EX_DEV_CLIENT_NETWORK_INSPECTOR=true +AsyncStorage_db_size_in_MB=100 diff --git a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj index 56e3b688f..110e82934 100644 --- a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj @@ -1298,7 +1298,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.5.0; + MARKETING_VERSION = 4.4.1; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1332,7 +1332,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.5.0; + MARKETING_VERSION = 4.4.1; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/packages/mobile/src/blockchain/wallet.ts b/packages/mobile/src/blockchain/wallet.ts index 4c57219d9..0eb668ae2 100644 --- a/packages/mobile/src/blockchain/wallet.ts +++ b/packages/mobile/src/blockchain/wallet.ts @@ -30,6 +30,7 @@ import { NetworkOverloadedError, emulateBoc, sendBoc, + getTimeoutFromLiteserverSafely, } from '@tonkeeper/shared/utils/blockchain'; import { OperationEnum, TonAPI, TypeEnum } from '@tonkeeper/core/src/TonAPI'; import { setBalanceForEmulation } from '@tonkeeper/shared/utils/wallet'; @@ -45,6 +46,7 @@ const TonWeb = require('tonweb'); export const inscriptionTransferAmount = '0.05'; interface JettonTransferParams { + timeout?: number; seqno: number; jettonWalletAddress: string; recipient: Account; @@ -358,6 +360,7 @@ export class TonWallet { } createJettonTransfer({ + timeout, seqno, jettonWalletAddress, recipient, @@ -382,6 +385,7 @@ export class TonWallet { const jettonAmount = BigInt(amountNano); return TransactionService.createTransfer(contract, { + timeout, seqno, secretKey, messages: [ @@ -417,7 +421,10 @@ export class TonWallet { throw new Error(t('send_get_wallet_info_error')); } + const timeout = await getTimeoutFromLiteserverSafely(); + const boc = this.createJettonTransfer({ + timeout, seqno, jettonWalletAddress, recipient, @@ -484,7 +491,10 @@ export class TonWallet { ? tk.wallet.battery.excessesAccount : tk.wallet.address.ton.raw; + const timeout = await getTimeoutFromLiteserverSafely(); + const boc = this.createJettonTransfer({ + timeout, seqno, jettonWalletAddress, recipient, @@ -534,9 +544,6 @@ export class TonWallet { if (e instanceof NetworkOverloadedError) { throw e; } - if (!store.getState().main.isTimeSynced) { - throw new Error('wrong_time'); - } throw new Error(t('send_publish_tx_error')); } @@ -568,7 +575,11 @@ export class TonWallet { allowedDestinations: lockupConfig?.allowed_destinations, }, ); + + const timeout = await getTimeoutFromLiteserverSafely(); + return TransactionService.createTransfer(contract, { + timeout, seqno, secretKey, sendMode, @@ -747,9 +758,6 @@ export class TonWallet { if (e instanceof NetworkOverloadedError) { throw e; } - if (!store.getState().main.isTimeSynced) { - throw new Error('wrong_time'); - } throw new Error(t('send_publish_tx_error')); } diff --git a/packages/mobile/src/core/Jetton/Jetton.tsx b/packages/mobile/src/core/Jetton/Jetton.tsx index 99a1b5c5d..a9f0116b1 100644 --- a/packages/mobile/src/core/Jetton/Jetton.tsx +++ b/packages/mobile/src/core/Jetton/Jetton.tsx @@ -1,16 +1,13 @@ import React, { useCallback, useMemo } from 'react'; import { JettonProps } from './Jetton.interface'; import * as S from './Jetton.style'; -import { IconButton, PopupMenu, PopupMenuItem, Skeleton, SwapIcon, Text } from '$uikit'; -import { ns } from '$utils'; +import { PopupMenu, PopupMenuItem, Skeleton, Text } from '$uikit'; import { useJetton } from '$hooks/useJetton'; import { useTokenPrice } from '$hooks/useTokenPrice'; import { openDAppBrowser, openSend } from '$navigation'; import { formatter } from '$utils/formatter'; import { useNavigation } from '@tonkeeper/router'; -import { useSwapStore } from '$store/zustand/swap'; -import { shallow } from 'zustand/shallow'; import { useFlags } from '$utils/flags'; import { HideableAmount } from '$core/HideableAmount/HideableAmount'; import { Events, JettonVerification, SendAnalyticsFrom } from '$store/models'; @@ -54,12 +51,12 @@ export const Jetton: React.FC = ({ route }) => { const isWatchOnly = wallet && wallet.isWatchOnly; const fiatCurrency = useWalletCurrency(); - const shouldShowChart = jettonPrice.fiat !== 0; const shouldExcludeChartPeriods = config.get('exclude_jetton_chart_periods'); const nav = useNavigation(); - const showSwap = useSwapStore((s) => !!s.assets[jetton.jettonAddress], shallow); + const shouldShowChart = jettonPrice.fiat !== 0; + const showSwap = jettonPrice.fiat !== 0; const handleSend = useCallback(() => { trackEvent(Events.SendOpen, { from: SendAnalyticsFrom.TokenScreen }); diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx index 6552d6b32..1db31d942 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx @@ -1,6 +1,6 @@ import React, { memo, useEffect, useMemo } from 'react'; import { NFTOperationFooter, useNFTOperationState } from '../NFTOperationFooter'; -import { SignRawParams, TxBodyOptions } from '../TXRequest.types'; +import { SignRawMessage, SignRawParams, TxBodyOptions } from '../TXRequest.types'; import { useUnlockVault } from '../useUnlockVault'; import { calculateMessageTransferAmount, delay } from '$utils'; import { debugLog } from '$utils/debugLog'; @@ -54,7 +54,11 @@ import { JettonTransferAction, NftItemTransferAction } from 'tonapi-sdk-js'; import { TokenDetailsParams } from '../../../../components/TokenDetails/TokenDetails'; import { ModalStackRouteNames } from '$navigation'; import { CanceledActionError } from '$core/Send/steps/ConfirmStep/ActionErrors'; -import { emulateBoc, sendBoc } from '@tonkeeper/shared/utils/blockchain'; +import { + emulateBoc, + getTimeoutFromLiteserverSafely, + sendBoc, +} from '@tonkeeper/shared/utils/blockchain'; import { openAboutRiskAmountModal } from '@tonkeeper/shared/modals/AboutRiskAmountModal'; import { toNano } from '@ton/core'; import BigNumber from 'bignumber.js'; @@ -115,7 +119,10 @@ export const SignRawModal = memo((props) => { vault.workchain, ); + const timeout = await getTimeoutFromLiteserverSafely(); + const boc = TransactionService.createTransfer(contract, { + timeout, messages: TransactionService.parseSignRawMessages( params.messages, isBattery ? tk.wallet.battery.excessesAccount : undefined, @@ -352,6 +359,10 @@ export const SignRawModal = memo((props) => { ); }); +function isValidMessage(message: SignRawMessage): boolean { + return Address.isValid(message.address) && new BigNumber(message.amount).gt('0'); +} + export const openSignRawModal = async ( params: SignRawParams, options: TxBodyOptions, @@ -370,6 +381,10 @@ export const openSignRawModal = async ( try { Toast.loading(); + if (!params.messages.every((mes) => isValidMessage(mes))) { + throw new Error('Invalid message'); + } + if (isTonConnect) { await TonConnectRemoteBridge.closeOtherTransactions(); } @@ -383,7 +398,9 @@ export const openSignRawModal = async ( let consequences: MessageConsequences | null = null; let isBattery = false; try { + const timeout = await getTimeoutFromLiteserverSafely(); const boc = TransactionService.createTransfer(contract, { + timeout, messages: TransactionService.parseSignRawMessages(params.messages), seqno: await getWalletSeqno(wallet), secretKey: Buffer.alloc(64), diff --git a/packages/mobile/src/core/ModalContainer/TimeNotSynced/TimeNotSynced.tsx b/packages/mobile/src/core/ModalContainer/TimeNotSynced/TimeNotSynced.tsx deleted file mode 100644 index 9d97d9978..000000000 --- a/packages/mobile/src/core/ModalContainer/TimeNotSynced/TimeNotSynced.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { memo, useCallback, useEffect } from 'react'; -import { t } from '@tonkeeper/shared/i18n'; -import { Modal } from '@tonkeeper/uikit'; -import { push } from '$navigation/imperative'; -import { SheetActions, useNavigation } from '@tonkeeper/router'; -import { MainDB } from '$database'; -import { mainActions } from '$store/main'; -import { useDispatch } from 'react-redux'; -import { Button, Icon, Text } from '$uikit'; -import * as S from './TimeNotSynced.style'; -import { Linking, Platform } from 'react-native'; -import { Base64, delay } from '$utils'; - -export const TimeNotSyncedModal = memo(() => { - const dispatch = useDispatch(); - const nav = useNavigation(); - - useEffect(() => { - MainDB.setTimeSyncedDismissed(false); - dispatch(mainActions.setTimeSyncedDismissed(false)); - }, []); - - const handleOpenSettings = useCallback(async () => { - nav.goBack(); - await delay(400); - if (Platform.OS === 'ios') { - return Linking.openURL(Base64.decodeToStr('QXBwLXByZWZzOnJvb3Q=')); - } - Linking.sendIntent('android.settings.DATE_SETTINGS'); - }, []); - - return ( - - - - - - - {t('txActions.signRaw.wrongTime.title')} - - - {t('txActions.signRaw.wrongTime.description')} - - - - - - - - - - ); -}); - -export const openTimeNotSyncedModal = async () => { - push('SheetsProvider', { - $$action: SheetActions.ADD, - component: TimeNotSyncedModal, - path: 'TimeNotSynced', - }); - - return true; -}; diff --git a/packages/mobile/src/core/NFTSend/NFTSend.tsx b/packages/mobile/src/core/NFTSend/NFTSend.tsx index ba62d509b..af60b187d 100644 --- a/packages/mobile/src/core/NFTSend/NFTSend.tsx +++ b/packages/mobile/src/core/NFTSend/NFTSend.tsx @@ -35,7 +35,11 @@ import { delay } from '$utils'; import { Toast } from '$store'; import axios from 'axios'; import { useUnlockVault } from '$core/ModalContainer/NFTOperations/useUnlockVault'; -import { emulateBoc, sendBoc } from '@tonkeeper/shared/utils/blockchain'; +import { + emulateBoc, + getTimeoutFromLiteserverSafely, + sendBoc, +} from '@tonkeeper/shared/utils/blockchain'; import { checkIsInsufficient, openInsufficientFundsModal, @@ -150,7 +154,9 @@ export const NFTSend: FC = (props) => { wallet.config.workchain, ); + const timeout = await getTimeoutFromLiteserverSafely(); const boc = TransactionService.createTransfer(contract, { + timeout, messages: nftTransferMessages, seqno: await getWalletSeqno(), secretKey: Buffer.alloc(64), @@ -312,7 +318,9 @@ export const NFTSend: FC = (props) => { vault.workchain, ); + const timeout = await getTimeoutFromLiteserverSafely(); const boc = TransactionService.createTransfer(contract, { + timeout, messages: nftTransferMessages, seqno: await getWalletSeqno(), sendMode: 3, diff --git a/packages/mobile/src/core/Swap/Swap.tsx b/packages/mobile/src/core/Swap/Swap.tsx index e56f211ec..6d0f338c9 100644 --- a/packages/mobile/src/core/Swap/Swap.tsx +++ b/packages/mobile/src/core/Swap/Swap.tsx @@ -5,7 +5,6 @@ import { getTimeSec } from '$utils/getTimeSec'; import { useNavigation } from '@tonkeeper/router'; import { getCorrectUrl, getDomainFromURL, isAndroid } from '$utils'; import { logEvent } from '@amplitude/analytics-browser'; -import { checkIsTimeSynced } from '$navigation/hooks/useDeeplinkingResolvers'; import { useWebViewBridge } from '$hooks/jsBridge'; import { useWallet } from '@tonkeeper/shared/hooks'; import { config } from '$config'; @@ -70,12 +69,6 @@ export const Swap: FC = (props) => { new Promise((resolve, reject) => { const { valid_until } = request; - if (!checkIsTimeSynced()) { - reject(); - - return; - } - if (valid_until < getTimeSec()) { reject(); diff --git a/packages/mobile/src/core/TonConnect/TonConnectModal.tsx b/packages/mobile/src/core/TonConnect/TonConnectModal.tsx index 4b1b3d098..e3d371c29 100644 --- a/packages/mobile/src/core/TonConnect/TonConnectModal.tsx +++ b/packages/mobile/src/core/TonConnect/TonConnectModal.tsx @@ -174,7 +174,7 @@ export const TonConnectModal = (props: TonConnectModalProps) => { const { replyBuilder, requestPromise } = props; - const replyItems = replyBuilder.createReplyItems( + const replyItems = await replyBuilder.createReplyItems( address, privateKey, publicKey, diff --git a/packages/mobile/src/database/main.ts b/packages/mobile/src/database/main.ts index cef1554a7..2a34eb4b3 100644 --- a/packages/mobile/src/database/main.ts +++ b/packages/mobile/src/database/main.ts @@ -2,25 +2,6 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { LogItem } from '$store/main/interface'; -export class MainDB { - static async timeSyncedDismissedTimestamp(): Promise { - const timeSyncedDismissed = await AsyncStorage.getItem('timeSyncedDismissed'); - return ( - !!timeSyncedDismissed && - timeSyncedDismissed !== 'false' && - parseFloat(timeSyncedDismissed) - ); - } - - static async setTimeSyncedDismissed(isDismissed: false | number) { - if (isDismissed) { - await AsyncStorage.setItem('timeSyncedDismissed', isDismissed.toString()); - } else { - await AsyncStorage.setItem('timeSyncedDismissed', 'false'); - } - } -} - export async function getHiddenNotifications(): Promise { const raw = await AsyncStorage.getItem('mainnet_default_hidden_internal_notifications'); diff --git a/packages/mobile/src/navigation/hooks/useDeeplinkingResolvers.ts b/packages/mobile/src/navigation/hooks/useDeeplinkingResolvers.ts index 8fc0ba3b7..329a4506f 100644 --- a/packages/mobile/src/navigation/hooks/useDeeplinkingResolvers.ts +++ b/packages/mobile/src/navigation/hooks/useDeeplinkingResolvers.ts @@ -22,7 +22,6 @@ import { openSignRawModal } from '$core/ModalContainer/NFTOperations/Modals/Sign import { isSignRawParams } from '$utils/isSignRawParams'; import { AppStackRouteNames, MainStackRouteNames } from '$navigation/navigationNames'; import { TonConnectRemoteBridge } from '$tonconnect/TonConnectRemoteBridge'; -import { openTimeNotSyncedModal } from '$core/ModalContainer/TimeNotSynced/TimeNotSynced'; import { openAddressMismatchModal } from '$core/ModalContainer/AddressMismatch/AddressMismatch'; import { openTonConnect } from '$core/TonConnect/TonConnectModal'; import { useCallback, useRef } from 'react'; @@ -45,22 +44,10 @@ const getWallet = () => { return store.getState().wallet.wallet; }; -const getIsTimeSynced = () => { - return store.getState().main.isTimeSynced; -}; - const getExpiresSec = () => { return getTimeSec() + 10 * 60; }; -export function checkIsTimeSynced() { - if (!getIsTimeSynced()) { - openTimeNotSyncedModal(); - return false; - } - return true; -} - export function useDeeplinkingResolvers() { const deeplinking = useDeeplinking(); const dispatch = useDispatch(); @@ -529,10 +516,6 @@ export function useDeeplinkingResolvers() { const txBody = txRequest.body as any; const isSignRaw = isSignRawParams(txBody?.params); - if (!checkIsTimeSynced()) { - return Toast.hide(); - } - if ( txBody.expires_sec < getTimeSec() || (isSignRaw && txBody.params.valid_until < getTimeSec()) diff --git a/packages/mobile/src/store/main/index.ts b/packages/mobile/src/store/main/index.ts index 069ee45fb..5f0e85cc6 100644 --- a/packages/mobile/src/store/main/index.ts +++ b/packages/mobile/src/store/main/index.ts @@ -8,8 +8,6 @@ import { SetAccentAction, SetLogsAction, SetNotificationsAction, - SetTimeSyncedAction, - SetTimeSyncedDismissedAction, SetTonCustomIcon, SetUnlockedAction, UpdateBadHostsAction, @@ -19,8 +17,6 @@ import { walletWalletSelector } from '$store/wallet'; const initialState: MainState = { isInitiating: true, - isTimeSynced: true, - timeSyncedDismissedTimestamp: false, badHosts: [], isBadHostsDismissed: false, internalNotifications: [], @@ -45,11 +41,6 @@ export const { actions, reducer } = createSlice({ state.isUnlocked = action.payload; }, - getTimeSynced() {}, - setTimeSynced(state, action: SetTimeSyncedAction) { - state.isTimeSynced = action.payload; - }, - updateBadHosts(state, action: UpdateBadHostsAction) { if (JSON.stringify(state.badHosts) !== JSON.stringify(action.payload)) { state.isBadHostsDismissed = false; @@ -61,10 +52,6 @@ export const { actions, reducer } = createSlice({ state.isBadHostsDismissed = true; }, - setTimeSyncedDismissed(state, action: SetTimeSyncedDismissedAction) { - state.timeSyncedDismissedTimestamp = action.payload; - }, - loadNotifications() {}, setNotifications(state, action: SetNotificationsAction) { @@ -137,8 +124,3 @@ export const accentTonIconSelector = createSelector( customIconSelector, (wallet, tonCustomIcon) => (wallet ? tonCustomIcon : null), ); - -export const isTimeSyncedSelector = createSelector( - mainSelector, - (state) => state.isTimeSynced, -); diff --git a/packages/mobile/src/store/main/interface.ts b/packages/mobile/src/store/main/interface.ts index a20656d74..f75c5a435 100644 --- a/packages/mobile/src/store/main/interface.ts +++ b/packages/mobile/src/store/main/interface.ts @@ -11,8 +11,6 @@ export interface LogItem { export interface MainState { isInitiating: boolean; - isTimeSynced: boolean; - timeSyncedDismissedTimestamp: false | number; badHosts: string[]; isBadHostsDismissed: boolean; internalNotifications: InternalNotificationModel[]; @@ -23,8 +21,6 @@ export interface MainState { tonCustomIcon: AccentNFTIcon | null; } -export type SetTimeSyncedAction = PayloadAction; -export type SetTimeSyncedDismissedAction = PayloadAction; export type UpdateBadHostsAction = PayloadAction; export type SetNotificationsAction = PayloadAction; export type HideNotificationAction = PayloadAction; diff --git a/packages/mobile/src/store/main/sagas.ts b/packages/mobile/src/store/main/sagas.ts index 4c27c14c9..1e5ca05ae 100644 --- a/packages/mobile/src/store/main/sagas.ts +++ b/packages/mobile/src/store/main/sagas.ts @@ -11,7 +11,6 @@ import { getHiddenNotifications, getSavedLogs, hideNotification, - MainDB, setSavedLogs, } from '$database'; import { HideNotificationAction } from '$store/main/interface'; @@ -20,7 +19,6 @@ import { InternalNotificationModel } from '$store/models'; import { initStats, trackEvent, trackFirstLaunch } from '$utils/stats'; import { favoritesActions } from '$store/favorites'; -import { useSwapStore } from '$store/zustand/swap'; import { tk } from '$wallet'; import { config } from '$config'; @@ -35,8 +33,6 @@ function* initWorker() { } export function* initHandler() { - const timeSyncedDismissed = yield call(MainDB.timeSyncedDismissedTimestamp); - initStats(); trackFirstLaunch(); @@ -44,24 +40,14 @@ export function* initHandler() { yield call([tk, 'init']); - yield put( - batchActions( - mainActions.endInitiating(), - mainActions.setTimeSyncedDismissed(timeSyncedDismissed), - ), - ); + yield put(batchActions(mainActions.endInitiating())); const logs = yield call(getSavedLogs); yield put(mainActions.setLogs(logs)); - if (tk.wallet) { - useSwapStore.getState().actions.fetchAssets(); - } - yield put(mainActions.loadNotifications()); yield put(favoritesActions.loadSuggests()); - yield put(mainActions.getTimeSynced()); SplashScreen.hideAsync(); } @@ -99,27 +85,6 @@ function* hideNotificationWorker(action: HideNotificationAction) { } catch (e) {} } -function* getTimeSyncedWorker() { - try { - const endpoint = `${config.get('tonapiV2Endpoint')}/v2/liteserver/get_time`; - - const response = yield call(axios.get, endpoint, { - headers: { Authorization: `Bearer ${config.get('tonApiV2Key')}` }, - }); - const time = response?.data?.time; - const isSynced = Math.abs(Date.now() - time * 1000) <= 7000; - - if (isSynced) { - yield call(MainDB.setTimeSyncedDismissed, false); - yield put(mainActions.setTimeSyncedDismissed(false)); - } - - yield put(mainActions.setTimeSynced(isSynced)); - } catch (e) { - console.log(e); - } -} - function* addLogWorker() { try { const { logs } = yield select(mainSelector); @@ -130,7 +95,6 @@ function* addLogWorker() { export function* mainSaga() { yield all([ takeLatest(mainActions.init, initWorker), - takeLatest(mainActions.getTimeSynced, getTimeSyncedWorker), takeLatest(mainActions.loadNotifications, loadNotificationsWorker), takeLatest(mainActions.hideNotification, hideNotificationWorker), takeLatest(mainActions.addLog, addLogWorker), diff --git a/packages/mobile/src/store/wallet/sagas.ts b/packages/mobile/src/store/wallet/sagas.ts index eb0ae288b..e55951e47 100644 --- a/packages/mobile/src/store/wallet/sagas.ts +++ b/packages/mobile/src/store/wallet/sagas.ts @@ -22,7 +22,6 @@ import { WalletGetUnlockedVaultAction, } from '$store/wallet/interface'; -import { MainDB } from '$database'; import { Toast, useAddressUpdateStore, useConnectedAppsStore } from '$store'; import { t } from '@tonkeeper/shared/i18n'; import { getChainName } from '$shared/dynamicConfig'; @@ -351,8 +350,6 @@ function* sendCoinsWorker(action: SendCoinsAction) { e && debugLog(e.message); if (e && e.message === 'wrong_time') { - MainDB.setTimeSyncedDismissed(false); - yield put(mainActions.setTimeSyncedDismissed(false)); Alert.alert( t('send_sending_wrong_time_title'), t('send_sending_wrong_time_description'), diff --git a/packages/mobile/src/store/zustand/swap/index.ts b/packages/mobile/src/store/zustand/swap/index.ts deleted file mode 100644 index 66e4bd997..000000000 --- a/packages/mobile/src/store/zustand/swap/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './types'; -export * from './useSwapStore'; diff --git a/packages/mobile/src/store/zustand/swap/types.ts b/packages/mobile/src/store/zustand/swap/types.ts deleted file mode 100644 index ccd576489..000000000 --- a/packages/mobile/src/store/zustand/swap/types.ts +++ /dev/null @@ -1,46 +0,0 @@ -export interface ISwapAsset { - address: string; - symbol: string; -} - -export type SwapAssets = { - [key: string]: ISwapAsset; -}; - -export interface ISwapStore { - assets: SwapAssets; - actions: { - fetchAssets: () => Promise; - }; -} - -export interface StonFiItem { - address: string; //"EQCSqjXUUfo7txZVeIpiB5ObyJ_dBOOdtXQNBIwvjMefNpF0" - apy_1d: string; //"0.010509024542116885" - apy_7d: string; // "1.090410672685333" - apy_30d: string; // "1.090410672685333" - collected_token0_protocol_fee: string; //"309131" - collected_token1_protocol_fee: string; // "111845809" - deprecated: boolean; //false - lp_fee: string; //"20" - lp_total_supply: string; //"209838035" - protocol_fee: string; // "10" - protocol_fee_address: string; // "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c" - ref_fee: string; // "10" - reserve0: string; // "9998902465" - reserve1: string; // "4489590433195" - router_address: string; // "EQB3ncyBUTjZUA5EnFKR5_EnOMI9V1tTEAAPaiU71gc4TiUt" - token0_address: string; // "EQB-MPwrd1G6WKNkLz_VnV6WqBDd142KMQv-g1O-8QUA3728" - token1_address: string; // "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c" -} - -export interface StonFiAsset { - contract_address: string; //"EQCcLAW537KnRg_aSPrnQJoyYjOZkzqYp6FVmRUvN1crSazV" - decimals: number; //9 - default_symbol: boolean; //false - deprecated: boolean; //false - display_name: string; //"Ambra" - image_url: string; //"https://asset.ston.fi/img/EQCcLAW537KnRg_aSPrnQJoyYjOZkzqYp6FVmRUvN1crSazV" - kind: string; //"JETTON" - symbol: string; //"AMBR" -} diff --git a/packages/mobile/src/store/zustand/swap/useSwapStore.ts b/packages/mobile/src/store/zustand/swap/useSwapStore.ts deleted file mode 100644 index baa56267a..000000000 --- a/packages/mobile/src/store/zustand/swap/useSwapStore.ts +++ /dev/null @@ -1,82 +0,0 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { create } from 'zustand'; -import { createJSONStorage, persist } from 'zustand/middleware'; -import { ISwapStore, StonFiAsset, StonFiItem, SwapAssets } from './types'; - -const StonFiTon = 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c'; - -const initialState: Omit = { - assets: {}, -}; - -export const useSwapStore = create( - persist( - (set) => ({ - ...initialState, - actions: { - fetchAssets: async () => { - try { - const assets = await fetch('https://app.ston.fi/rpc', { - method: 'POST', - body: JSON.stringify({ - jsonrpc: '2.0', - id: Date.now(), - method: 'asset.list', - }), - headers: { 'content-type': 'application/json' }, - }); - - const result = await fetch('https://app.ston.fi/rpc', { - method: 'POST', - body: JSON.stringify({ - jsonrpc: '2.0', - id: Date.now(), - method: 'pool.list', - }), - headers: { 'content-type': 'application/json' }, - }); - - const data: StonFiItem[] = (await result.json()).result.pools; - const assetList: StonFiAsset[] = (await assets.json()).result.assets; - - const items = data.reduce((acc, item) => { - if (!acc[item.token0_address] && item.token0_address !== StonFiTon) { - const asset = assetList.find( - (a) => a.contract_address === item.token0_address, - ); - - if (asset) { - acc[item.token0_address] = { - address: item.token0_address, - symbol: asset.symbol, - }; - } - } - if (!acc[item.token1_address] && item.token1_address !== StonFiTon) { - const asset = assetList.find( - (a) => a.contract_address === item.token1_address, - ); - - if (asset) { - acc[item.token1_address] = { - address: item.token1_address, - symbol: asset.symbol, - }; - } - } - - return acc; - }, {} as SwapAssets); - - set({ assets: items }); - } catch {} - }, - }, - }), - { - name: 'swap', - storage: createJSONStorage(() => AsyncStorage), - partialize: ({ assets }) => ({ assets } as ISwapStore), - }, - ), -); diff --git a/packages/mobile/src/tabs/Wallet/hooks/useInternalNotifications.ts b/packages/mobile/src/tabs/Wallet/hooks/useInternalNotifications.ts index 5aed6aa05..84b806604 100644 --- a/packages/mobile/src/tabs/Wallet/hooks/useInternalNotifications.ts +++ b/packages/mobile/src/tabs/Wallet/hooks/useInternalNotifications.ts @@ -2,7 +2,6 @@ import { usePrevious } from '$hooks/usePrevious'; import { mainActions, mainSelector } from '$store/main'; import { InternalNotificationProps } from '$uikit/InternalNotification/InternalNotification.interface'; import { useNetInfo } from '@react-native-community/netinfo'; -import { MainDB } from '$database'; import { useEffect, useMemo, useState } from 'react'; import { Linking } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; @@ -31,13 +30,8 @@ export const useInternalNotifications = () => { } }, [netInfo.isConnected, prevNetInfo.isConnected, dispatch]); - const { - badHosts, - isBadHostsDismissed, - internalNotifications, - timeSyncedDismissedTimestamp, - isTimeSynced, - } = useSelector(mainSelector); + const { badHosts, isBadHostsDismissed, internalNotifications } = + useSelector(mainSelector); const addressUpdateDismissed = useAddressUpdateStore((s) => s.dismissed); const shouldShowAddressUpdate = useFlag('address_style_notice'); @@ -77,20 +71,6 @@ export const useInternalNotifications = () => { mode: 'danger', onClose: () => dispatch(mainActions.dismissBadHosts()), }); - } else if ( - !isTimeSynced && - (!timeSyncedDismissedTimestamp || - timeSyncedDismissedTimestamp < Date.now() - 7 * 24 * 60 * 60 * 1000) - ) { - result.push({ - title: t('notify_incorrect_time_err_title'), - caption: t('notify_incorrect_time_err_caption'), - mode: 'tertiary', - onClose: () => { - MainDB.setTimeSyncedDismissed(Date.now()); - dispatch(mainActions.setTimeSyncedDismissed(Date.now())); - }, - }); } if (wallet && !addressUpdateDismissed && shouldShowAddressUpdate) { @@ -139,8 +119,6 @@ export const useInternalNotifications = () => { netInfo.isConnected, badHosts, isBadHostsDismissed, - isTimeSynced, - timeSyncedDismissedTimestamp, wallet, addressUpdateDismissed, shouldShowAddressUpdate, diff --git a/packages/mobile/src/tonconnect/ConnectReplyBuilder.ts b/packages/mobile/src/tonconnect/ConnectReplyBuilder.ts index 3db9575d2..93bebf8e4 100644 --- a/packages/mobile/src/tonconnect/ConnectReplyBuilder.ts +++ b/packages/mobile/src/tonconnect/ConnectReplyBuilder.ts @@ -11,9 +11,9 @@ import nacl from 'tweetnacl'; import TonWeb from 'tonweb'; import { Buffer } from 'buffer'; import { getDomainFromURL } from '$utils'; -import { getTimeSec } from '$utils/getTimeSec'; import { Int64LE } from 'int64-buffer'; import { DAppManifest } from './models'; +import { getRawTimeFromLiteserverSafely } from '@tonkeeper/shared/utils/blockchain'; const { createHash } = require('react-native-crypto'); @@ -31,13 +31,13 @@ export class ConnectReplyBuilder { return getChainName() === 'mainnet' ? CHAIN.MAINNET : CHAIN.TESTNET; } - private createTonProofItem( + private async createTonProofItem( address: string, secretKey: Uint8Array, payload: string, - ): TonProofItemReply { + ): Promise { try { - const timestamp = getTimeSec(); + const timestamp = await getRawTimeFromLiteserverSafely(); const timestampBuffer = new Int64LE(timestamp).toBuffer(); const domain = getDomainFromURL(this.manifest.url); @@ -102,36 +102,41 @@ export class ConnectReplyBuilder { } } - createReplyItems( + async createReplyItems( addr: string, privateKey: Uint8Array, publicKey: Uint8Array, walletStateInit: string, isTestnet: boolean, - ): ConnectItemReply[] { + ): Promise { const address = new TonWeb.utils.Address(addr).toString(false, true, true); - const replyItems = this.request.items.map((requestItem): ConnectItemReply => { - switch (requestItem.name) { + const replyItems: ConnectItemReply[] = []; + for (const item of this.request.items) { + switch (item.name) { case 'ton_addr': - return { + replyItems.push({ name: 'ton_addr', address, network: isTestnet ? CHAIN.TESTNET : CHAIN.MAINNET, publicKey: Buffer.from(publicKey).toString('hex'), walletStateInit, - }; + }); + break; case 'ton_proof': - return this.createTonProofItem(address, privateKey, requestItem.payload); + replyItems.push( + await this.createTonProofItem(address, privateKey, item.payload), + ); + break; default: - return { - name: (requestItem as ConnectItem).name, + replyItems.push({ + name: (item as ConnectItem).name, error: { code: 400 }, - } as unknown as ConnectItemReply; + } as unknown as ConnectItemReply); } - }); + } return replyItems; } diff --git a/packages/mobile/src/tonconnect/TonConnect.ts b/packages/mobile/src/tonconnect/TonConnect.ts index 1d71495aa..c34ecc890 100644 --- a/packages/mobile/src/tonconnect/TonConnect.ts +++ b/packages/mobile/src/tonconnect/TonConnect.ts @@ -2,7 +2,6 @@ import { openSignRawModal } from '$core/ModalContainer/NFTOperations/Modals/Sign import { SignRawParams } from '$core/ModalContainer/NFTOperations/TXRequest.types'; import { TonConnectModalResponse } from '$core/TonConnect/models'; import { openTonConnect } from '$core/TonConnect/TonConnectModal'; -import { checkIsTimeSynced } from '$navigation/hooks/useDeeplinkingResolvers'; import { findConnectedAppByClientSessionId, findConnectedAppByUrl, @@ -321,15 +320,6 @@ class TonConnectService { }; const boc = await new Promise(async (resolve, reject) => { - if (!checkIsTimeSynced()) { - return reject( - new SendTransactionError( - request.id, - SEND_TRANSACTION_ERROR_CODES.USER_REJECTS_ERROR, - 'Wallet declined the request', - ), - ); - } const openModalResult = await openSignRawModal( txParams, { diff --git a/packages/mobile/src/utils/proof.ts b/packages/mobile/src/utils/proof.ts index 80e66e51f..91f0120cc 100644 --- a/packages/mobile/src/utils/proof.ts +++ b/packages/mobile/src/utils/proof.ts @@ -1,4 +1,3 @@ -import { getTimeSec } from '$utils/getTimeSec'; import { Int64LE } from 'int64-buffer'; import { Buffer } from 'buffer'; import nacl from 'tweetnacl'; @@ -7,6 +6,7 @@ const { createHash } = require('react-native-crypto'); import { ConnectApi, Configuration } from '@tonkeeper/core/src/legacy'; import { Address } from '@tonkeeper/core'; import { config } from '$config'; +import { getRawTimeFromLiteserverSafely } from '@tonkeeper/shared/utils/blockchain'; export interface TonProofArgs { address: string; @@ -37,7 +37,7 @@ export async function createTonProof({ if (!payload) { payload = (await connectApi.getTonConnectPayload()).payload; } - const timestamp = getTimeSec(); + const timestamp = await getRawTimeFromLiteserverSafely(); const timestampBuffer = new Int64LE(timestamp).toBuffer(); const domainBuffer = Buffer.from(domain); diff --git a/packages/mobile/src/utils/stats.ts b/packages/mobile/src/utils/stats.ts index ae3f9b379..efb4106c2 100644 --- a/packages/mobile/src/utils/stats.ts +++ b/packages/mobile/src/utils/stats.ts @@ -1,5 +1,4 @@ import { config } from '$config'; -import { init, logEvent } from '@amplitude/analytics-browser'; import AsyncStorage from '@react-native-async-storage/async-storage'; import Aptabase from '@aptabase/react-native'; import DeviceInfo from 'react-native-device-info'; @@ -19,20 +18,6 @@ export function initStats() { appVersion: DeviceInfo.getVersion(), }); } - init(config.get('amplitudeKey'), '-', { - minIdLength: 1, - deviceId: '-', - trackingOptions: { - ipAddress: false, - deviceModel: true, - language: false, - osName: true, - osVersion: true, - platform: true, - adid: false, - carrier: false, - }, - }); TrakingEnabled = true; } @@ -48,7 +33,6 @@ export async function trackEvent(name: string, params: any = {}) { Object.assign(params, { firebase_user_id: DeviceInfo.getUniqueId() }), ); } - logEvent(name, params); } catch (e) {} } diff --git a/packages/mobile/src/wallet/models/ActivityModel/ActivityModel.ts b/packages/mobile/src/wallet/models/ActivityModel/ActivityModel.ts index d37629b8f..4b24b3470 100644 --- a/packages/mobile/src/wallet/models/ActivityModel/ActivityModel.ts +++ b/packages/mobile/src/wallet/models/ActivityModel/ActivityModel.ts @@ -11,7 +11,11 @@ import { ActionItem, AnyActionItem, } from './ActivityModelTypes'; -import { AccountEvent, ActionStatusEnum } from '@tonkeeper/core/src/TonAPI'; +import { + AccountEvent, + ActionStatusEnum, + JettonVerificationType, +} from '@tonkeeper/core/src/TonAPI'; import { toLowerCaseFirstLetter } from '@tonkeeper/uikit'; import { Address } from '@tonkeeper/core'; import { TronEvent } from '@tonkeeper/core/src/TronAPI/TronAPIGenerated'; @@ -167,7 +171,10 @@ export class ActivityModel { type: ActionAmountType.Jetton, jettonAddress: payload.jetton.address, decimals: payload.jetton.decimals, - symbol: payload.jetton.symbol, + symbol: + payload.jetton.verification === JettonVerificationType.Blacklist + ? 'FAKE' + : payload.jetton.symbol, value: payload.amount, }; case ActionType.NftPurchase: diff --git a/packages/mobile/src/wallet/models/JettonBalanceModel/JettonBalanceModel.ts b/packages/mobile/src/wallet/models/JettonBalanceModel/JettonBalanceModel.ts index e934e3323..7fd14180b 100644 --- a/packages/mobile/src/wallet/models/JettonBalanceModel/JettonBalanceModel.ts +++ b/packages/mobile/src/wallet/models/JettonBalanceModel/JettonBalanceModel.ts @@ -1,5 +1,5 @@ import { Address, AmountFormatter } from '@tonkeeper/core'; -import { JettonBalance } from '@tonkeeper/core/src/TonAPI'; +import { JettonBalance, JettonVerificationType } from '@tonkeeper/core/src/TonAPI'; import { JettonMetadata, JettonVerification } from './types'; export class JettonBalanceModel { @@ -28,5 +28,9 @@ export class JettonBalanceModel { this.walletAddress = new Address(jettonBalance.wallet_address.address).toFriendly(); this.verification = jettonBalance.jetton .verification as unknown as JettonVerification; + + if (jettonBalance.jetton.verification === JettonVerificationType.Blacklist) { + this.metadata.symbol = 'FAKE'; + } } } diff --git a/packages/mobile/src/wallet/utils.ts b/packages/mobile/src/wallet/utils.ts index cb63ce07e..00a2edc07 100644 --- a/packages/mobile/src/wallet/utils.ts +++ b/packages/mobile/src/wallet/utils.ts @@ -8,6 +8,7 @@ export const createTonApiInstance = (isTestnet = false) => { return new TonAPI({ baseHeaders: () => ({ Authorization: `Bearer ${config.get('tonApiV2Key', isTestnet)}`, + 'Cache-Control': 'no-cache', }), baseUrl: () => config.get('tonapiIOEndpoint', isTestnet), }); diff --git a/packages/shared/utils/blockchain.ts b/packages/shared/utils/blockchain.ts index 342843c02..3edb8942d 100644 --- a/packages/shared/utils/blockchain.ts +++ b/packages/shared/utils/blockchain.ts @@ -3,6 +3,7 @@ import { tk } from '@tonkeeper/mobile/src/wallet'; import { ContentType, ServiceStatus } from '@tonkeeper/core/src/TonAPI'; import { TransactionService } from '@tonkeeper/core'; import { t } from '../i18n'; +import { Alert } from 'react-native'; export class NetworkOverloadedError extends Error {} @@ -66,3 +67,21 @@ export async function emulateBoc( return { emulateResult, battery: false }; } } + +export async function getRawTimeFromLiteserverSafely(): Promise { + try { + const res = await tk.wallet.tonapi.liteserver.getRawTime({ + headers: { + 'Cache-Control': 'no-cache', + }, + cache: 'no-cache', + }); + return res.time; + } catch (e) { + return Math.floor(Date.now() / 1e3); + } +} + +export async function getTimeoutFromLiteserverSafely() { + return (await getRawTimeFromLiteserverSafely()) + TransactionService.TTL; +} From b5e6fb4516200d27f82a271c15fc3c3ee0555df5 Mon Sep 17 00:00:00 2001 From: Andrey Sorokin Date: Thu, 9 May 2024 23:21:38 +0500 Subject: [PATCH 2/5] feat(mobile): add signer & ledger support (#831) * feat(mobile): signer support * feat(mobile): ledger support (#835) --- packages/@core-js/package.json | 6 +- .../src/legacy/contracts/LockupContractV1.ts | 13 + .../legacy/contracts/WalletContractV4R1.ts | 23 + .../@core-js/src/service/contractService.ts | 2 + .../src/service/transactionService.ts | 16 +- .../android/app/src/main/AndroidManifest.xml | 24 + .../ios/ton_keeper.xcodeproj/project.pbxproj | 2 +- .../ios/ton_keeper/SupportingFiles/Info.plist | 2 + packages/mobile/package.json | 9 +- packages/mobile/src/blockchain/wallet.ts | 236 +- .../LedgerConnectionSteps.tsx | 144 ++ .../LedgerConnectionSteps/LedgerView.tsx | 164 ++ .../components/LedgerConnectionSteps/index.ts | 1 + .../components/LedgerConnectionSteps/types.ts | 5 + packages/mobile/src/components/index.ts | 1 + packages/mobile/src/config/index.ts | 6 + .../core/CustomizeWallet/CustomizeWallet.tsx | 6 +- .../core/DAppBrowser/hooks/useDAppBridge.ts | 24 +- .../src/core/DevMenu/DevConfigScreen.tsx | 22 + .../src/core/ImportWallet/ImportWallet.tsx | 2 +- .../CreateSubscription/CreateSubscription.tsx | 1 - .../ModalContainer/LinkingDomainModal.tsx | 39 +- .../NFTOperations/Modals/SignRawModal.tsx | 38 +- .../NFTOperations/NFTOperationFooter.tsx | 5 + .../NFTOperations/NFTOperations.ts | 320 --- packages/mobile/src/core/NFT/NFT.tsx | 2 +- packages/mobile/src/core/NFTSend/NFTSend.tsx | 158 +- .../mobile/src/core/ScanQR/ScanQR.style.ts | 6 +- packages/mobile/src/core/ScanQR/ScanQR.tsx | 2 +- .../mobile/src/core/ScanQR/ScannerMask.tsx | 34 + packages/mobile/src/core/Send/Send.tsx | 38 +- .../AddressStep/AddressStep.interface.ts | 11 +- .../Send/steps/AddressStep/AddressStep.tsx | 9 +- .../components/CommentInput/CommentInput.tsx | 4 +- .../mobile/src/core/Settings/Settings.tsx | 29 +- .../src/core/StakingSend/StakingSend.tsx | 58 +- .../src/core/TonConnect/TonConnectModal.tsx | 6 +- packages/mobile/src/hooks/useMigration.ts | 2 +- packages/mobile/src/ledger/index.ts | 4 + .../src/ledger/useBluetoothAvailable.ts | 42 + .../mobile/src/ledger/useConnectLedger.ts | 103 + .../mobile/src/ledger/useLedgerAccounts.ts | 37 + packages/mobile/src/ledger/usePairLedger.ts | 43 + .../mobile/src/modals/LedgerConfirmModal.tsx | 119 + .../mobile/src/modals/PairLedgerModal.tsx | 121 ++ packages/mobile/src/modals/index.ts | 2 + .../ImportWalletStack/ImportWalletStack.tsx | 10 +- .../src/navigation/ImportWalletStack/types.ts | 10 +- .../src/navigation/MainStack/MainStack.tsx | 21 +- .../MainStack/TabStack/TabStack.tsx | 4 +- packages/mobile/src/navigation/ModalStack.tsx | 10 +- .../hooks/useDeeplinkingResolvers.ts | 91 +- .../ChooseLedgerWallets.tsx | 127 ++ .../src/screens/ChooseLedgerWallets/index.ts | 1 + .../screens/ChooseWallets/ChooseWallets.tsx | 30 +- .../PairSignerScreen/PairSignerScreen.tsx | 253 +++ .../src/screens/PairSignerScreen/index.ts | 1 + .../SignerConfirmScreen/QrCodeView.tsx | 46 + .../SignerConfirmScreen.tsx | 198 ++ .../src/screens/SignerConfirmScreen/index.ts | 1 + .../src/screens/StartScreen/StartScreen.tsx | 14 +- packages/mobile/src/screens/index.ts | 3 + .../mobile/src/store/subscriptions/sagas.ts | 5 - packages/mobile/src/store/wallet/interface.ts | 1 + packages/mobile/src/store/wallet/sagas.ts | 20 +- .../mobile/src/tabs/Wallet/WalletScreen.tsx | 17 +- .../WalletActionButtons.tsx | 10 +- packages/mobile/src/uikit/Tag/Tag.tsx | 13 +- .../mobile/src/utils/bluetoothPermissions.ts | 130 ++ packages/mobile/src/utils/ledger.ts | 11 + packages/mobile/src/wallet/Tonkeeper.ts | 170 +- .../mobile/src/wallet/Wallet/WalletBase.ts | 35 +- .../mobile/src/wallet/Wallet/WalletContent.ts | 3 + packages/mobile/src/wallet/WalletTypes.ts | 11 + .../src/wallet/managers/SignerManager.ts | 187 ++ packages/mobile/src/wallet/utils.ts | 2 + .../router/src/createModalStackNavigator.tsx | 8 +- .../EncryptedComment/EncryptedComment.tsx | 2 +- .../WalletListItem/WalletListItem.tsx | 4 +- packages/shared/hooks/useDangerLevel.ts | 2 +- .../shared/i18n/locales/tonkeeper/en.json | 51 +- .../shared/i18n/locales/tonkeeper/ru-RU.json | 1910 +++++++++-------- .../ActionModalContent.tsx | 4 +- packages/shared/modals/AddWalletModal.tsx | 131 +- packages/shared/modals/SwitchWalletModal.tsx | 4 +- .../uikit/assets/icons/png/ic-dot-16@4x.png | Bin 0 -> 658 bytes .../icons/png/ic-globe-outline-28@4x.png | Bin 0 -> 4445 bytes .../png/ic-import-wallet-outline-28@4x.png | Bin 0 -> 2381 bytes .../assets/icons/png/ic-ledger-28@4x.png | Bin 0 -> 394 bytes .../png/ic-magnifying-glass-outline-28@4x.png | Bin 0 -> 3010 bytes .../icons/png/ic-testnet-outline-28@4x.png | Bin 0 -> 3853 bytes .../uikit/assets/icons/svg/16/ic-dot-16.svg | 3 + .../icons/svg/28/ic-globe-outline-28.svg | 3 + .../svg/28/ic-import-wallet-outline-28.svg | 3 + .../assets/icons/svg/28/ic-ledger-28.svg | 3 + .../svg/28/ic-magnifying-glass-outline-28.svg | 3 + .../icons/svg/28/ic-testnet-outline-28.svg | 3 + packages/uikit/src/components/Icon/Icon.tsx | 1 + .../uikit/src/components/Icon/Icon.types.ts | 18 + .../src/components/Icon/IconList.native.ts | 6 + packages/uikit/src/components/Text/Text.tsx | 1 + .../Modal/ScreenModal/ScreenModalHeader.tsx | 14 +- .../ScreenModal/ScreenModalScrollView.tsx | 8 +- .../src/containers/Screen/ScreenHeader.tsx | 13 +- yarn.lock | 118 +- 105 files changed, 4002 insertions(+), 1687 deletions(-) create mode 100644 packages/mobile/src/components/LedgerConnectionSteps/LedgerConnectionSteps.tsx create mode 100644 packages/mobile/src/components/LedgerConnectionSteps/LedgerView.tsx create mode 100644 packages/mobile/src/components/LedgerConnectionSteps/index.ts create mode 100644 packages/mobile/src/components/LedgerConnectionSteps/types.ts delete mode 100644 packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts create mode 100644 packages/mobile/src/core/ScanQR/ScannerMask.tsx create mode 100644 packages/mobile/src/ledger/index.ts create mode 100644 packages/mobile/src/ledger/useBluetoothAvailable.ts create mode 100644 packages/mobile/src/ledger/useConnectLedger.ts create mode 100644 packages/mobile/src/ledger/useLedgerAccounts.ts create mode 100644 packages/mobile/src/ledger/usePairLedger.ts create mode 100644 packages/mobile/src/modals/LedgerConfirmModal.tsx create mode 100644 packages/mobile/src/modals/PairLedgerModal.tsx create mode 100644 packages/mobile/src/screens/ChooseLedgerWallets/ChooseLedgerWallets.tsx create mode 100644 packages/mobile/src/screens/ChooseLedgerWallets/index.ts create mode 100644 packages/mobile/src/screens/PairSignerScreen/PairSignerScreen.tsx create mode 100644 packages/mobile/src/screens/PairSignerScreen/index.ts create mode 100644 packages/mobile/src/screens/SignerConfirmScreen/QrCodeView.tsx create mode 100644 packages/mobile/src/screens/SignerConfirmScreen/SignerConfirmScreen.tsx create mode 100644 packages/mobile/src/screens/SignerConfirmScreen/index.ts create mode 100644 packages/mobile/src/utils/bluetoothPermissions.ts create mode 100644 packages/mobile/src/utils/ledger.ts create mode 100644 packages/mobile/src/wallet/managers/SignerManager.ts create mode 100644 packages/uikit/assets/icons/png/ic-dot-16@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-globe-outline-28@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-import-wallet-outline-28@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-ledger-28@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-magnifying-glass-outline-28@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-testnet-outline-28@4x.png create mode 100644 packages/uikit/assets/icons/svg/16/ic-dot-16.svg create mode 100644 packages/uikit/assets/icons/svg/28/ic-globe-outline-28.svg create mode 100644 packages/uikit/assets/icons/svg/28/ic-import-wallet-outline-28.svg create mode 100644 packages/uikit/assets/icons/svg/28/ic-ledger-28.svg create mode 100644 packages/uikit/assets/icons/svg/28/ic-magnifying-glass-outline-28.svg create mode 100644 packages/uikit/assets/icons/svg/28/ic-testnet-outline-28.svg diff --git a/packages/@core-js/package.json b/packages/@core-js/package.json index d21b70ab8..1f9d49dd7 100644 --- a/packages/@core-js/package.json +++ b/packages/@core-js/package.json @@ -15,9 +15,9 @@ "@aws-crypto/sha256-js": "^3.0.0", "@ethersproject/shims": "^5.7.0", "@noble/ed25519": "1.7.3", - "@ton/core": "^0.53.0", - "@ton/crypto": "^3.2.0", - "@ton/ton": "^13.9.0", + "@ton/core": "0.54.0", + "@ton/crypto": "3.2.0", + "@ton/ton": "https://github.com/tonkeeper/tonkeeper-ton#build9", "aes-js": "3.1.2", "bignumber.js": "^9.1.1", "ethers": "^6.7.1", diff --git a/packages/@core-js/src/legacy/contracts/LockupContractV1.ts b/packages/@core-js/src/legacy/contracts/LockupContractV1.ts index 7d259ce2a..2c397a185 100644 --- a/packages/@core-js/src/legacy/contracts/LockupContractV1.ts +++ b/packages/@core-js/src/legacy/contracts/LockupContractV1.ts @@ -11,6 +11,7 @@ import { SendMode, } from '@ton/core'; import { Maybe } from '@ton/ton/dist/utils/maybe'; +import { ExternallySingedAuthWallet3SendArgs } from '@ton/ton/dist/wallets/WalletContractV3'; import { createWalletTransferV3 } from '@ton/ton/dist/wallets/signing/createWalletTransfer'; export interface LockupContractV1AdditionalParams { @@ -150,6 +151,18 @@ export class LockupContractV1 implements Contract { }); } + createTransferAndSignRequestAsync(args: ExternallySingedAuthWallet3SendArgs) { + let sendMode = SendMode.PAY_GAS_SEPARATELY; + if (args.sendMode !== null && args.sendMode !== undefined) { + sendMode = args.sendMode; + } + return createWalletTransferV3({ + ...args, + sendMode, + walletId: this.walletId, + }); + } + /** * Create sender */ diff --git a/packages/@core-js/src/legacy/contracts/WalletContractV4R1.ts b/packages/@core-js/src/legacy/contracts/WalletContractV4R1.ts index d0a4d648b..4514800c8 100644 --- a/packages/@core-js/src/legacy/contracts/WalletContractV4R1.ts +++ b/packages/@core-js/src/legacy/contracts/WalletContractV4R1.ts @@ -11,7 +11,15 @@ import { SendMode, } from '@ton/core'; import { Maybe } from '@ton/ton/dist/utils/maybe'; +import { + ExternallySingedAuthWallet4SendArgs, + SingedAuthWallet4SendArgs, +} from '@ton/ton/dist/wallets/WalletContractV4'; import { createWalletTransferV4 } from '@ton/ton/dist/wallets/signing/createWalletTransfer'; +import { + ExternallySingedAuthSendArgs, + SingedAuthSendArgs, +} from '@ton/ton/dist/wallets/signing/singer'; export class WalletContractV4R1 implements Contract { static create(args: { @@ -123,6 +131,21 @@ export class WalletContractV4R1 implements Contract { }); } + /** + * Create signed transfer + */ + createTransferAndSignRequestAsync(args: ExternallySingedAuthWallet4SendArgs) { + let sendMode = SendMode.PAY_GAS_SEPARATELY; + if (args.sendMode !== null && args.sendMode !== undefined) { + sendMode = args.sendMode; + } + return createWalletTransferV4({ + ...args, + walletId: this.walletId, + sendMode, + }); + } + /** * Create sender */ diff --git a/packages/@core-js/src/service/contractService.ts b/packages/@core-js/src/service/contractService.ts index b9522cdf5..0f706bbca 100644 --- a/packages/@core-js/src/service/contractService.ts +++ b/packages/@core-js/src/service/contractService.ts @@ -8,6 +8,8 @@ import { import { WalletContractV3R1, WalletContractV3R2, WalletContractV4 } from '@ton/ton'; import nacl from 'tweetnacl'; +export type Signer = (message: Cell) => Promise; + export enum OpCodes { JETTON_TRANSFER = 0xf8a7ea5, NFT_TRANSFER = 0x5fcc3d14, diff --git a/packages/@core-js/src/service/transactionService.ts b/packages/@core-js/src/service/transactionService.ts index cb4ee7d1a..17626d87e 100644 --- a/packages/@core-js/src/service/transactionService.ts +++ b/packages/@core-js/src/service/transactionService.ts @@ -10,9 +10,8 @@ import { loadStateInit, } from '@ton/core'; import { Address as AddressFormatter } from '../formatters/Address'; -import { OpCodes, WalletContract } from './contractService'; +import { OpCodes, Signer, WalletContract } from './contractService'; import { SignRawMessage } from '@tonkeeper/mobile/src/core/ModalContainer/NFTOperations/TxRequest.types'; -import { tk } from '@tonkeeper/mobile/src/wallet'; export type AnyAddress = string | Address | AddressFormatter; @@ -20,7 +19,6 @@ export interface TransferParams { seqno: number; timeout?: number; sendMode?: number; - secretKey: Buffer; messages: MessageRelaxed[]; } @@ -41,7 +39,7 @@ export class TransactionService { return Math.floor(Date.now() / 1e3) + TransactionService.TTL; } - private static externalMessage(contract: WalletContract, seqno: number, body: Cell) { + public static externalMessage(contract: WalletContract, seqno: number, body: Cell) { return beginCell() .storeWritable( storeMessage( @@ -163,11 +161,15 @@ export class TransactionService { } } - static createTransfer(contract, transferParams: TransferParams) { - const transfer = contract.createTransfer({ + static async createTransfer( + contract: WalletContract, + signer: Signer, + transferParams: TransferParams, + ) { + const transfer = await contract.createTransferAndSignRequestAsync({ timeout: transferParams.timeout ?? TransactionService.getTimeout(), seqno: transferParams.seqno, - secretKey: transferParams.secretKey, + signer, sendMode: transferParams.sendMode ?? SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, messages: transferParams.messages, diff --git a/packages/mobile/android/app/src/main/AndroidManifest.xml b/packages/mobile/android/app/src/main/AndroidManifest.xml index 074ae426c..e3ab56a1d 100644 --- a/packages/mobile/android/app/src/main/AndroidManifest.xml +++ b/packages/mobile/android/app/src/main/AndroidManifest.xml @@ -19,6 +19,30 @@ + + + + + + + + + + + + + + diff --git a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj index 110e82934..529c7abc1 100644 --- a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj @@ -1056,7 +1056,7 @@ outputFileListPaths = ( ); outputPaths = ( - $SRCROOT/$PROJECT_NAME/Resources/Generated/R.generated.swift, + "$SRCROOT/$PROJECT_NAME/Resources/Generated/R.generated.swift", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/packages/mobile/ios/ton_keeper/SupportingFiles/Info.plist b/packages/mobile/ios/ton_keeper/SupportingFiles/Info.plist index 47c0122ee..00e402a62 100644 --- a/packages/mobile/ios/ton_keeper/SupportingFiles/Info.plist +++ b/packages/mobile/ios/ton_keeper/SupportingFiles/Info.plist @@ -97,6 +97,8 @@ + NSBluetoothAlwaysUsageDescription + Tonkeeper uses bluetooth to connect your hardware Ledger Nano X NSCameraUsageDescription Tonkeeper uses camera to scan QR codes NSFaceIDUsageDescription diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 00531a408..b1948c3a3 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -32,6 +32,8 @@ "@craftzdog/react-native-buffer": "^6.0.5", "@expo/react-native-action-sheet": "^4.0.1", "@gorhom/bottom-sheet": "^4.6.0", + "@ledgerhq/hw-transport": "^6.30.6", + "@ledgerhq/react-native-hw-transport-ble": "^6.32.5", "@rainbow-me/animated-charts": "https://github.com/tonkeeper/react-native-animated-charts#737b1633c41e13da437c8e111c4aedd15bd10558", "@react-native-async-storage/async-storage": "^1.23.1", "@react-native-community/clipboard": "^1.5.1", @@ -42,8 +44,9 @@ "@react-native-firebase/messaging": "18.5.0", "@reduxjs/toolkit": "^1.6.1", "@shopify/flash-list": "^1.5.0", - "@ton/core": "^0.53.0", - "@ton/ton": "^13.9.0", + "@ton-community/ton-ledger": "^7.0.1", + "@ton/core": "0.54.0", + "@ton/ton": "https://github.com/tonkeeper/tonkeeper-ton#build9", "@tonapps/tonlogin-client": "0.2.5", "@tonconnect/protocol": "^2.2.5", "@tonkeeper/core": "0.1.0", @@ -88,6 +91,7 @@ "react": "18.2.0", "react-native": "0.72.6", "react-native-apk-install": "0.1.0", + "react-native-ble-plx": "2.0.3", "react-native-camera": "^4.2.1", "react-native-config": "^1.5.1", "react-native-console-time-polyfill": "^1.2.3", @@ -133,6 +137,7 @@ "react-query": "^3.39.3", "react-redux": "^7.2.4", "redux-saga": "^1.1.3", + "rxjs": "^7.8.1", "stream-browserify": "^3.0.0", "styled-components": "^5.3.0", "text-encoding-polyfill": "^0.6.7", diff --git a/packages/mobile/src/blockchain/wallet.ts b/packages/mobile/src/blockchain/wallet.ts index 0eb668ae2..752f5eb97 100644 --- a/packages/mobile/src/blockchain/wallet.ts +++ b/packages/mobile/src/blockchain/wallet.ts @@ -1,13 +1,11 @@ import BigNumber from 'bignumber.js'; import { getUnixTime } from 'date-fns'; -import { store } from '$store'; import { UnlockedVault, Vault } from './vault'; import { Address as AddressFormatter, BASE_FORWARD_AMOUNT, ContractService, - contractVersionsMap, isActiveAccount, ONE_TON, TransactionService, @@ -25,7 +23,15 @@ import { } from '@tonkeeper/core/src/legacy'; import { tk } from '$wallet'; -import { Address, Cell, internal } from '@ton/core'; +import { + Address, + Cell, + beginCell, + toNano as tonCoreToNano, + internal, + SendMode, + comment, +} from '@ton/core'; import { NetworkOverloadedError, emulateBoc, @@ -33,11 +39,11 @@ import { getTimeoutFromLiteserverSafely, } from '@tonkeeper/shared/utils/blockchain'; import { OperationEnum, TonAPI, TypeEnum } from '@tonkeeper/core/src/TonAPI'; -import { setBalanceForEmulation } from '@tonkeeper/shared/utils/wallet'; +import { getWalletSeqno, setBalanceForEmulation } from '@tonkeeper/shared/utils/wallet'; import { WalletNetwork } from '$wallet/WalletTypes'; import { createTonApiInstance } from '$wallet/utils'; import { config } from '$config'; -import { toNano } from '$utils'; +import { Base64, toNano } from '$utils'; import { BatterySupportedTransaction } from '$wallet/managers/BatteryManager'; import { compareAddresses } from '$utils/address'; @@ -52,10 +58,9 @@ interface JettonTransferParams { recipient: Account; amountNano: string; payload: Cell | string; - vault: Vault; - secretKey?: Buffer; excessesAccount?: string | null; jettonTransferAmount?: bigint; + estimateFee?: boolean; } interface TonTransferParams { @@ -64,10 +69,8 @@ interface TonTransferParams { amount: string; payload?: Cell | string; sendMode?: number; - vault: Vault; - walletVersion?: string | null; - secretKey?: Buffer; bounce: boolean; + estimateFee?: boolean; } export class Wallet { @@ -218,7 +221,6 @@ export class TonWallet { } async createSubscription( - unlockedVault: UnlockedVault | Vault, beneficiaryAddress: string, amountNano: string, interval: number, @@ -232,7 +234,7 @@ export class TonWallet { throw new Error(t('send_get_wallet_info_error')); } - const walletAddress = await unlockedVault.tonWallet.getAddress(); + const walletAddress = new TonWeb.Address(tk.wallet.address.ton.raw); const startAt = getUnixTime(new Date()); const subscription = new TonWeb.SubscriptionContract(this.tonweb.provider, { @@ -248,21 +250,6 @@ export class TonWallet { const subscriptionAddress = await subscription.getAddress(); - const addr = AddressFormatter.parse(walletAddress).toRaw(); - const seqno = await this.getSeqno(addr); - - const params: any = { - seqno: seqno || 0, - pluginWc: 0, - amount: amountNano, - stateInit: (await subscription.createStateInit()).stateInit, - body: subscription.createBody(), - }; - if (!testOnly) { - params.secretKey = await (unlockedVault as UnlockedVault).getTonPrivateKey(); - } - - const tx = unlockedVault.tonWallet.methods.deployAndInstallPlugin(params); let feeNano: BigNumber; if (['empty', 'uninit'].includes(myinfo.status)) { feeNano = new BigNumber(Ton.toNano('0.1').toString()); @@ -276,8 +263,33 @@ export class TonWallet { }; } - const queryMsg = await tx.getQuery(); - const boc = TonWeb.utils.bytesToBase64(await queryMsg.toBoc(false)); + const signer = await tk.wallet.signer.getSigner(); + + const stateInitBoc = Base64.encodeBytes( + (await subscription.createStateInit()).stateInit.toBoc(false), + ); + const stateInit = Cell.fromBase64(stateInitBoc); + + const bodyBoc = Base64.encodeBytes(subscription.createBody().toBoc(false)); + const body = Cell.fromBase64(bodyBoc); + + const boc = await TransactionService.createTransfer(tk.wallet.contract, signer, { + seqno: await getWalletSeqno(), + messages: [ + internal({ + to: Address.parse(subscriptionAddress.toString(false)), + bounce: true, + value: amountNano, + body: beginCell() + .storeUint(1, 8) + .storeInt(0, 8) + .storeCoins(Number(amountNano)) + .storeRef(stateInit) + .storeRef(body) + .endCell(), + }), + ], + }); return { subscriptionAddress: subscriptionAddress.toString(false), @@ -288,11 +300,9 @@ export class TonWallet { }; } - async getCancelSubscriptionBoc( - unlockedVault: UnlockedVault, - subscriptionAddress: string, - ) { + async getCancelSubscriptionBoc(subscriptionAddress: string) { const isInstalled = this.isSubscriptionActive(subscriptionAddress); + if (!isInstalled) { return; } @@ -305,16 +315,27 @@ export class TonWallet { throw new Error(t('send_get_wallet_info_error')); } - const seqno = await this.getSeqno(walletAddress); - const tx = await unlockedVault.tonWallet.methods.removePlugin({ - secretKey: await unlockedVault.getTonPrivateKey(), - amount: Ton.toNano('0.007'), - seqno, - pluginAddress: subscriptionAddress, - }); + const pluginAddress = Address.parse(subscriptionAddress); - const query = await tx.getQuery(); - const boc = TonWeb.utils.bytesToBase64(await query.toBoc(false)); + const signer = await tk.wallet.signer.getSigner(); + + const boc = await TransactionService.createTransfer(tk.wallet.contract, signer, { + seqno: await getWalletSeqno(), + messages: [ + internal({ + to: pluginAddress, + bounce: true, + value: Ton.toNano('0.007'), + body: beginCell() + .storeUint(3, 8) + .storeInt(pluginAddress.workChain, 8) + .storeBuffer(pluginAddress.hash) + .storeCoins(Ton.toNano('0.007')) + .storeUint(0, 64) + .endCell(), + }), + ], + }); const [fee] = await this.calcFee(boc); if (fee.isGreaterThan(myinfo.balance)) { @@ -359,35 +380,51 @@ export class TonWallet { return ['empty', 'uninit', 'nonexist'].includes(info?.status ?? ''); } - createJettonTransfer({ + async createJettonTransfer({ timeout, seqno, jettonWalletAddress, recipient, amountNano, payload = '', - vault, - secretKey = Buffer.alloc(64), excessesAccount = null, jettonTransferAmount = ONE_TON, + estimateFee, }: JettonTransferParams) { - const version = vault.getVersion(); - const lockupConfig = vault.getLockupConfig(); - const contract = ContractService.getWalletContract( - contractVersionsMap[version ?? 'v4R2'], - Buffer.from(vault.tonPublicKey), - vault.workchain, - { - allowedDestinations: lockupConfig?.allowed_destinations, - }, - ); - const jettonAmount = BigInt(amountNano); - return TransactionService.createTransfer(contract, { + if (tk.wallet.isLedger && !estimateFee) { + const transfer = await tk.wallet.signer.signLedgerTransaction({ + to: Address.parse(jettonWalletAddress), + bounce: true, + amount: jettonTransferAmount, + sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, + seqno, + timeout: timeout ?? TransactionService.getTimeout(), + payload: { + type: 'jetton-transfer', + queryId: ContractService.getWalletQueryId(), + amount: jettonAmount, + destination: Address.parse(recipient.address), + responseDestination: Address.parse( + excessesAccount ?? tk.wallet.address.ton.raw, + ), + forwardAmount: BigInt(1), + forwardPayload: typeof payload === 'string' ? comment(payload) : payload, + customPayload: null, + }, + }); + + return TransactionService.externalMessage(tk.wallet.contract, seqno, transfer) + .toBoc() + .toString('base64'); + } + + const signer = await tk.wallet.signer.getSigner(estimateFee); + + return TransactionService.createTransfer(tk.wallet.contract, signer, { timeout, seqno, - secretKey, messages: [ internal({ to: Address.parse(jettonWalletAddress), @@ -408,7 +445,6 @@ export class TonWallet { jettonWalletAddress: string, address: string, amountNano: string, - vault: Vault, payload: Cell | string = '', ) { let recipient: Account; @@ -423,16 +459,16 @@ export class TonWallet { const timeout = await getTimeoutFromLiteserverSafely(); - const boc = this.createJettonTransfer({ + const boc = await this.createJettonTransfer({ timeout, seqno, jettonWalletAddress, recipient, amountNano, payload, - vault, excessesAccount: null, jettonTransferAmount: ONE_TON, + estimateFee: true, }); let [feeNano, isBattery] = await this.calcFee( @@ -451,7 +487,6 @@ export class TonWallet { jettonWalletAddress: string, address: string, amountNano: string, - unlockedVault: UnlockedVault, payload: Cell | string = '', sendWithBattery: boolean, forwardAmount: string, @@ -468,8 +503,6 @@ export class TonWallet { throw new Error(t('send_get_wallet_info_error')); } - const secretKey = await unlockedVault.getTonPrivateKey(); - await tk.wallet.jettons.load(); const balances = tk.wallet.jettons.state.data.jettonBalances; @@ -493,15 +526,13 @@ export class TonWallet { const timeout = await getTimeoutFromLiteserverSafely(); - const boc = this.createJettonTransfer({ + const boc = await this.createJettonTransfer({ timeout, seqno, jettonWalletAddress, recipient, amountNano, payload, - vault: unlockedVault, - secretKey: Buffer.from(secretKey), excessesAccount, jettonTransferAmount: BigInt(forwardAmount), }); @@ -559,29 +590,38 @@ export class TonWallet { amount, payload = '', sendMode = 3, - vault, bounce, - walletVersion = null, - secretKey = Buffer.alloc(64), + estimateFee, }: TonTransferParams) { - const version = vault.getVersion(); - const lockupConfig = vault.getLockupConfig(); - const contract = ContractService.getWalletContract( - contractVersionsMap[walletVersion ?? version ?? 'v4R2'], - Buffer.from(vault.tonPublicKey), - vault.workchain, - { - lockupPubKey: lockupConfig?.config_pubkey, - allowedDestinations: lockupConfig?.allowed_destinations, - }, - ); - const timeout = await getTimeoutFromLiteserverSafely(); - return TransactionService.createTransfer(contract, { + if (tk.wallet.isLedger && !estimateFee) { + const transfer = await tk.wallet.signer.signLedgerTransaction({ + to: Address.parse(recipient.address), + bounce, + amount: tonCoreToNano(amount), + sendMode, + seqno, + timeout, + payload: + typeof payload === 'string' && payload !== '' + ? { + type: 'comment', + text: payload, + } + : undefined, + }); + + return TransactionService.externalMessage(tk.wallet.contract, seqno, transfer) + .toBoc() + .toString('base64'); + } + + const signer = await tk.wallet.signer.getSigner(estimateFee); + + return TransactionService.createTransfer(tk.wallet.contract, signer, { timeout, seqno, - secretKey, sendMode, messages: [ internal({ @@ -599,7 +639,6 @@ export class TonWallet { type: TypeEnum, address: string, amount: string, - vault: Vault, payload: string = '', ) { const opTemplate = await this.tonapi.experimental.getInscriptionOpTemplate({ @@ -614,7 +653,6 @@ export class TonWallet { return this.estimateFee( opTemplate.destination, inscriptionTransferAmount, - vault, opTemplate.comment, 3, ); @@ -623,19 +661,14 @@ export class TonWallet { async estimateFee( address: string, amount: string, - vault: Vault, payload: Cell | string = '', sendMode = 3, - walletVersion: string | null = null, ) { let recipientInfo: Account; let seqno: number; try { - const fromAddress = walletVersion - ? await this.getAddressByWalletVersion(walletVersion) - : await this.getAddress(); recipientInfo = await this.getWalletInfo(address); - seqno = await this.getSeqno(fromAddress); + seqno = await this.getSeqno(tk.wallet.address.ton.raw); } catch (e) { throw new Error(t('send_get_wallet_info_error')); } @@ -646,11 +679,10 @@ export class TonWallet { amount, payload, sendMode, - vault, - walletVersion, bounce: isActiveAccount(recipientInfo.status) ? AddressFormatter.isBounceable(address) : false, + estimateFee: true, }); const [feeNano, isBattery] = await this.calcFee(boc); return [Ton.fromNano(feeNano.toString()), isBattery]; @@ -668,7 +700,6 @@ export class TonWallet { type: TypeEnum, address: string, amount: string, - vault: UnlockedVault, payload: string = '', ) { const opTemplate = await this.tonapi.experimental.getInscriptionOpTemplate({ @@ -683,7 +714,6 @@ export class TonWallet { return this.transfer( opTemplate.destination, inscriptionTransferAmount, - vault, opTemplate.comment, 3, ); @@ -692,25 +722,18 @@ export class TonWallet { async transfer( address: string, amount: string, - unlockedVault: UnlockedVault, payload: Cell | string = '', sendMode = 3, - walletVersion: string | null = null, ) { - const secretKey = await unlockedVault.getTonPrivateKey(); - // We need to check our seqno which is null if uninitialized. // Do not use wallet.methods.seqno().call() - it returns some garbage (85143). let fromInfo: Account; let recipientInfo: Account; let seqno: number; try { - const fromAddress = walletVersion - ? await this.getAddressByWalletVersion(walletVersion) - : await this.getAddress(); - fromInfo = await this.getWalletInfo(fromAddress); + fromInfo = await this.getWalletInfo(tk.wallet.address.ton.raw); recipientInfo = await this.getWalletInfo(address); - seqno = await this.getSeqno(fromAddress); + seqno = await this.getSeqno(tk.wallet.address.ton.raw); } catch (e) { throw new Error(t('send_get_wallet_info_error')); } @@ -723,9 +746,6 @@ export class TonWallet { amount, payload, sendMode, - vault: unlockedVault, - walletVersion, - secretKey: Buffer.from(secretKey), // We should keep bounce flag from user input. We should check contract status till Jan 1, 2024 according to internal Address reform roadmap bounce: isActiveAccount(recipientInfo.status) ? AddressFormatter.isBounceable(address) diff --git a/packages/mobile/src/components/LedgerConnectionSteps/LedgerConnectionSteps.tsx b/packages/mobile/src/components/LedgerConnectionSteps/LedgerConnectionSteps.tsx new file mode 100644 index 000000000..735cca486 --- /dev/null +++ b/packages/mobile/src/components/LedgerConnectionSteps/LedgerConnectionSteps.tsx @@ -0,0 +1,144 @@ +import { FC, useCallback } from 'react'; +import { Steezy } from '@tonkeeper/uikit/src/styles'; +import { Text, View, Icon, Loader, TouchableOpacity } from '@tonkeeper/uikit'; +import { t } from '@tonkeeper/shared/i18n'; +import { LedgerConnectionCurrentStep } from './types'; +import { LegderView } from './LedgerView'; +import { Linking, Platform } from 'react-native'; + +const LEDGER_LIVE_STORE_URL = Platform.select({ + ios: 'https://apps.apple.com/app/ledger-live/id1361671700', + android: 'https://play.google.com/store/apps/details?id=com.ledger.live', +}); + +const BUTTON_HIT_SLOP = { + top: 12, + bottom: 12, + left: 12, + right: 12, +}; + +const StepIcon: FC<{ state: 'future' | 'active' | 'completed' }> = ({ state }) => { + let content = ; + if (state === 'future') { + content = ; + } + + if (state === 'active') { + content = ; + } + + return {content}; +}; + +interface Props { + showConfirmTxStep?: boolean; + currentStep: LedgerConnectionCurrentStep; +} + +export const LedgerConnectionSteps: FC = (props) => { + const { currentStep, showConfirmTxStep } = props; + + const openLegderLive = useCallback(async () => { + try { + await Linking.openURL('ledgerlive://myledger?installApp=TON'); + } catch { + Linking.openURL(LEDGER_LIVE_STORE_URL!); + } + }, []); + + return ( + + + + + + + + {t('ledger.connect')} + + + + + + + + {t('ledger.open_ton_app')} + + {!showConfirmTxStep ? ( + + + {t('ledger.install_ton_app')} + + + ) : null} + + + {showConfirmTxStep ? ( + + + + + {t('ledger.confirm_tx')} + + + + ) : null} + + + ); +}; + +const styles = Steezy.create(({ colors, corners }) => ({ + container: { + backgroundColor: colors.backgroundContent, + overflow: 'hidden', + borderRadius: corners.medium, + marginBottom: 16, + }, + stepsContainer: { + paddingHorizontal: 16, + paddingVertical: 20, + }, + step: { + flexDirection: 'row', + marginBottom: 8, + alignItems: 'flex-start', + }, + stepText: { + flex: 1, + paddingLeft: 8, + }, + iconContainer: { + marginTop: 2, + }, +})); diff --git a/packages/mobile/src/components/LedgerConnectionSteps/LedgerView.tsx b/packages/mobile/src/components/LedgerConnectionSteps/LedgerView.tsx new file mode 100644 index 000000000..cc10f3d0e --- /dev/null +++ b/packages/mobile/src/components/LedgerConnectionSteps/LedgerView.tsx @@ -0,0 +1,164 @@ +import { Icon, Text, View, ns, useTheme } from '@tonkeeper/uikit'; +import { FC } from 'react'; +import Svg, { Path, Circle } from 'react-native-svg'; +import { LedgerConnectionCurrentStep } from './types'; +import { Steezy } from '$styles'; +import Animated, { + useAnimatedStyle, + withDelay, + withTiming, +} from 'react-native-reanimated'; +import { Platform } from 'react-native'; + +const BluetoothIcon = () => { + return ( + + + + + ); +}; + +const LedgerPicture = () => { + const theme = useTheme(); + return ( + + + + + + + + + + ); +}; + +const LEDGER_POS = ns(24); +const LEDGER_CONNECTED_POS = ns(-42); + +const fontFamily = Platform.select({ + ios: 'SFMono-Medium', + android: 'RobotoMono-Medium', +}); + +interface Props { + currentStep: LedgerConnectionCurrentStep; + showConfirmTxStep?: boolean; +} + +export const LegderView: FC = (props) => { + const { currentStep, showConfirmTxStep } = props; + + const bluetoothStyle = useAnimatedStyle( + () => ({ + opacity: currentStep === 'connect' ? withDelay(200, withTiming(1)) : withTiming(0), + }), + [currentStep], + ); + + const ledgerStyle = useAnimatedStyle( + () => ({ + transform: [ + { + translateX: + currentStep === 'connect' + ? withDelay(200, withTiming(LEDGER_POS)) + : withTiming(LEDGER_CONNECTED_POS, { duration: 350 }), + }, + ], + }), + [currentStep], + ); + + const textStyle = useAnimatedStyle( + () => ({ + opacity: currentStep === 'connect' ? withTiming(0) : withDelay(150, withTiming(1)), + }), + [currentStep], + ); + + return ( + + + + + + + + + {currentStep === 'all-completed' && showConfirmTxStep ? ( + + ) : ( + + {currentStep === 'confirm-tx' ? 'Review' : 'TON ready'} + + )} + + + + + ); +}; + +const styles = Steezy.create(({ colors }) => ({ + container: { + width: '100%', + paddingTop: 40, + paddingLeft: 40, + paddingBottom: 16, + flexDirection: 'row', + alignItems: 'center', + }, + ledger: { + position: 'relative', + transform: [{ translateX: LEDGER_POS }], + }, + textWrapper: { + position: 'absolute', + top: 8, + left: 52, + }, + textContainer: { + width: 89, + height: 40, + backgroundColor: colors.backgroundContent, + justifyContent: 'center', + alignItems: 'center', + borderRadius: 2, + }, +})); diff --git a/packages/mobile/src/components/LedgerConnectionSteps/index.ts b/packages/mobile/src/components/LedgerConnectionSteps/index.ts new file mode 100644 index 000000000..e9ce411be --- /dev/null +++ b/packages/mobile/src/components/LedgerConnectionSteps/index.ts @@ -0,0 +1 @@ +export * from './LedgerConnectionSteps'; diff --git a/packages/mobile/src/components/LedgerConnectionSteps/types.ts b/packages/mobile/src/components/LedgerConnectionSteps/types.ts new file mode 100644 index 000000000..bb1e95115 --- /dev/null +++ b/packages/mobile/src/components/LedgerConnectionSteps/types.ts @@ -0,0 +1,5 @@ +export type LedgerConnectionCurrentStep = + | 'connect' + | 'open-ton' + | 'confirm-tx' + | 'all-completed'; diff --git a/packages/mobile/src/components/index.ts b/packages/mobile/src/components/index.ts index d63c51d3d..9c1184d9f 100644 --- a/packages/mobile/src/components/index.ts +++ b/packages/mobile/src/components/index.ts @@ -1,2 +1,3 @@ export * from './CardsWidget'; export * from './SendScreenBatteryWidget'; +export * from './LedgerConnectionSteps'; diff --git a/packages/mobile/src/config/index.ts b/packages/mobile/src/config/index.ts index d826cfaee..1570e6ddb 100644 --- a/packages/mobile/src/config/index.ts +++ b/packages/mobile/src/config/index.ts @@ -43,6 +43,7 @@ export type AppConfigVars = { tonapiTestnetHost: string; tronapiHost: string; tronapiTestnetHost: string; + signerStoreUrl: string; batteryHost: string; batteryTestnetHost: string; @@ -62,6 +63,8 @@ export type AppConfigVars = { disable_battery_send: boolean; disable_show_unverified_token: boolean; disable_battery_promo_module: boolean; + disable_signer: boolean; + disable_ledger: boolean; disable_tonstakers: boolean; disable_holders_cards: boolean; exclude_jetton_chart_periods: boolean; @@ -88,6 +91,7 @@ const defaultConfig: Partial = { holdersService: 'https://card-dev.whales-api.com', tronapiHost: 'https://tron.tonkeeper.com', tronapiTestnetHost: 'https://testnet-tron.tonkeeper.com', + signerStoreUrl: 'https://play.google.com/store/apps/details?id=com.tonapps.signer', batteryHost: 'https://battery.tonkeeper.com', batteryTestnetHost: 'https://testnet-battery.tonkeeper.com', @@ -101,6 +105,8 @@ const defaultConfig: Partial = { disable_battery_send: false, disable_battery_iap_module: Platform.OS === 'android', // Enable for iOS, disable for Android disable_battery_promo_module: true, + disable_signer: true, + disable_ledger: true, disable_show_unverified_token: false, disable_tonstakers: false, diff --git a/packages/mobile/src/core/CustomizeWallet/CustomizeWallet.tsx b/packages/mobile/src/core/CustomizeWallet/CustomizeWallet.tsx index fdb3f146b..33c8243e7 100644 --- a/packages/mobile/src/core/CustomizeWallet/CustomizeWallet.tsx +++ b/packages/mobile/src/core/CustomizeWallet/CustomizeWallet.tsx @@ -62,7 +62,11 @@ export const CustomizeWallet: FC = memo((props) => { const themeName = useThemeName(); const [name, setName] = useState( - identifiers.length > 1 ? wallet.config.name.slice(0, -5) : wallet.config.name, + identifiers.length > 1 + ? wallet.isLedger + ? wallet.config.name.slice(0, -2) + : wallet.config.name.slice(0, -5) + : wallet.config.name, ); const [selectedColor, setSelectedColor] = useState(wallet.config.color); const [emoji, setEmoji] = useState(wallet.config.emoji); diff --git a/packages/mobile/src/core/DAppBrowser/hooks/useDAppBridge.ts b/packages/mobile/src/core/DAppBrowser/hooks/useDAppBridge.ts index 3b50d2c25..740e9c987 100644 --- a/packages/mobile/src/core/DAppBrowser/hooks/useDAppBridge.ts +++ b/packages/mobile/src/core/DAppBrowser/hooks/useDAppBridge.ts @@ -1,4 +1,10 @@ -import { AppRequest, ConnectEvent, RpcMethod, WalletEvent } from '@tonconnect/protocol'; +import { + AppRequest, + CONNECT_EVENT_ERROR_CODES, + ConnectEvent, + RpcMethod, + WalletEvent, +} from '@tonconnect/protocol'; import { useCallback, useMemo, useState } from 'react'; import { CURRENT_PROTOCOL_VERSION, TonConnect, tonConnectDeviceInfo } from '$tonconnect'; import { useWebViewBridge } from '../jsBridge'; @@ -9,6 +15,8 @@ import { useConnectedAppsStore, disableNotifications, } from '$store'; +import { tk } from '$wallet'; +import { ConnectEventError } from '$tonconnect/ConnectEventError'; export const useDAppBridge = (walletAddress: string, webViewUrl: string) => { const [connectEvent, setConnectEvent] = useState(null); @@ -37,6 +45,13 @@ export const useDAppBridge = (walletAddress: string, webViewUrl: string) => { protocolVersion: CURRENT_PROTOCOL_VERSION, isWalletBrowser: true, connect: async (protocolVersion, request) => { + if (tk.wallet.isExternal || tk.wallet.isWatchOnly) { + return new ConnectEventError( + CONNECT_EVENT_ERROR_CODES.METHOD_NOT_SUPPORTED, + '', + ); + } + const event = await TonConnect.connect( protocolVersion, request, @@ -50,6 +65,13 @@ export const useDAppBridge = (walletAddress: string, webViewUrl: string) => { return event; }, restoreConnection: async () => { + if (tk.wallet.isExternal || tk.wallet.isWatchOnly) { + return new ConnectEventError( + CONNECT_EVENT_ERROR_CODES.METHOD_NOT_SUPPORTED, + '', + ); + } + const event = await TonConnect.autoConnect(webViewUrl); setConnectEvent(event); diff --git a/packages/mobile/src/core/DevMenu/DevConfigScreen.tsx b/packages/mobile/src/core/DevMenu/DevConfigScreen.tsx index 1236ba1ff..bb1143815 100644 --- a/packages/mobile/src/core/DevMenu/DevConfigScreen.tsx +++ b/packages/mobile/src/core/DevMenu/DevConfigScreen.tsx @@ -68,6 +68,28 @@ export const DevConfigScreen = memo(() => { }) } /> + + } + /> + + } + /> diff --git a/packages/mobile/src/core/ImportWallet/ImportWallet.tsx b/packages/mobile/src/core/ImportWallet/ImportWallet.tsx index 020d5523c..977cf95fe 100644 --- a/packages/mobile/src/core/ImportWallet/ImportWallet.tsx +++ b/packages/mobile/src/core/ImportWallet/ImportWallet.tsx @@ -30,7 +30,7 @@ export const ImportWallet: FC<{ let walletsInfo: ImportWalletInfo[] | null = null; try { - walletsInfo = await tk.getWalletsInfo(mnemonic, isTestnet); + walletsInfo = await tk.getWalletsInfoByMnemonic(mnemonic, isTestnet); } catch {} const shouldChooseWallets = diff --git a/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.tsx b/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.tsx index 9fa748ae4..d446f0c4b 100644 --- a/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.tsx +++ b/packages/mobile/src/core/ModalContainer/CreateSubscription/CreateSubscription.tsx @@ -83,7 +83,6 @@ export const CreateSubscription: FC = ({ if (info && !subscription) { wallet!.ton .createSubscription( - wallet!.vault, info.address, info.amountNano, info.intervalSec, diff --git a/packages/mobile/src/core/ModalContainer/LinkingDomainModal.tsx b/packages/mobile/src/core/ModalContainer/LinkingDomainModal.tsx index 2f6dce143..96a1d3ebf 100644 --- a/packages/mobile/src/core/ModalContainer/LinkingDomainModal.tsx +++ b/packages/mobile/src/core/ModalContainer/LinkingDomainModal.tsx @@ -6,7 +6,6 @@ import { Base64, truncateDecimal } from '$utils'; import { debugLog } from '$utils/debugLog'; import React, { useEffect } from 'react'; import { ActionFooter, useActionFooter } from './NFTOperations/NFTOperationFooter'; -import { useUnlockVault } from './NFTOperations/useUnlockVault'; import * as S from './NFTOperations/NFTOperations.styles'; import BigNumber from 'bignumber.js'; import { Ton } from '$libs/Ton'; @@ -18,8 +17,9 @@ import { Modal } from '@tonkeeper/uikit'; import { push } from '$navigation/imperative'; import { SheetActions } from '@tonkeeper/router'; import { openReplaceDomainAddress } from './NFTOperations/ReplaceDomainAddressModal'; -import { Address } from '@tonkeeper/core'; +import { Address, TransactionService } from '@tonkeeper/core'; import { tk } from '$wallet'; +import { getWalletSeqno } from '@tonkeeper/shared/utils/wallet'; const TonWeb = require('tonweb'); @@ -60,7 +60,7 @@ export class LinkingDomainActions { */ public async calculateFee() { try { - const boc = await this.createBoc(); + const boc = await this.createBoc(true); const feeInfo = await tk.wallet.tonapi.wallet.emulateMessageToWallet({ boc }); const feeNano = new BigNumber(feeInfo.event.extra).multipliedBy(-1); @@ -78,10 +78,7 @@ export class LinkingDomainActions { /** * Creates boc with DNS-record */ - public async createBoc(secretKey?: Uint8Array) { - const curWallet = this.wallet.vault.tonWallet; - const seqno = await this.wallet.ton.getSeqno(await this.wallet.ton.getAddress()); - + public async createBoc(isEstimate?: boolean) { const address = this.walletAddress && new TonWeb.Address(this.walletAddress); const payload = await TonWeb.dns.DnsItem.createChangeContentEntryBody({ @@ -89,18 +86,22 @@ export class LinkingDomainActions { value: address ? TonWeb.dns.createSmartContractAddressRecord(address) : null, }); - const tx = curWallet.methods.transfer({ - toAddress: this.domainAddress, - amount: this.transferAmount, - seqno: seqno, - payload, + const payloadBoc = Base64.encodeBytes(await payload.toBoc(false)); + + const signer = await tk.wallet.signer.getSigner(isEstimate); + + const boc = await TransactionService.createTransfer(tk.wallet.contract, signer, { + messages: TransactionService.parseSignRawMessages([ + { + address: this.domainAddress, + amount: this.transferAmount, + payload: payloadBoc, + }, + ]), sendMode: 3, - secretKey, + seqno: await getWalletSeqno(tk.wallet), }); - const queryMsg = await tx.getQuery(); - const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - return boc; } } @@ -127,15 +128,11 @@ export const LinkingDomainModal: React.FC = ({ linkingActions.updateWalletAddress(walletAddress); }, [walletAddress]); - const unlockVault = useUnlockVault(); const handleConfirm = onConfirm(async ({ startLoading }) => { - const vault = await unlockVault(); - const privateKey = await vault.getTonPrivateKey(); - startLoading(); setIsDisabled(true); - const boc = await linkingActions.createBoc(privateKey); + const boc = await linkingActions.createBoc(); await tk.wallet.tonapi.blockchain.sendBlockchainMessage({ boc }, { format: 'text' }); }); diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx index 1db31d942..1ae7a00e5 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx @@ -1,7 +1,6 @@ import React, { memo, useEffect, useMemo } from 'react'; import { NFTOperationFooter, useNFTOperationState } from '../NFTOperationFooter'; import { SignRawMessage, SignRawParams, TxBodyOptions } from '../TXRequest.types'; -import { useUnlockVault } from '../useUnlockVault'; import { calculateMessageTransferAmount, delay } from '$utils'; import { debugLog } from '$utils/debugLog'; import { t } from '@tonkeeper/shared/i18n'; @@ -29,16 +28,10 @@ import { TonConnectRemoteBridge } from '$tonconnect/TonConnectRemoteBridge'; import { formatter } from '$utils/formatter'; import { tk } from '$wallet'; import { MessageConsequences } from '@tonkeeper/core/src/TonAPI'; -import { - Address, - ContractService, - contractVersionsMap, - TransactionService, -} from '@tonkeeper/core'; +import { Address, TransactionService } from '@tonkeeper/core'; import { ActionListItemByType } from '@tonkeeper/shared/components/ActivityList/ActionListItemByType'; import { useGetTokenPrice } from '$hooks/useTokenPrice'; import { formatValue, getActionTitle } from '@tonkeeper/shared/utils/signRaw'; -import { Buffer } from 'buffer'; import { trackEvent } from '$utils/stats'; import { Events, SendAnalyticsFrom } from '$store/models'; import { getWalletSeqno, setBalanceForEmulation } from '@tonkeeper/shared/utils/wallet'; @@ -62,6 +55,7 @@ import { import { openAboutRiskAmountModal } from '@tonkeeper/shared/modals/AboutRiskAmountModal'; import { toNano } from '@ton/core'; import BigNumber from 'bignumber.js'; +import { WalletContractV4 } from '@ton/ton'; interface SignRawModalProps { consequences?: MessageConsequences; @@ -93,7 +87,6 @@ export const SignRawModal = memo((props) => { } const { footerRef, onConfirm } = useNFTOperationState(options, wallet); - const unlockVault = useUnlockVault(); const fiatCurrency = useWalletCurrency(); const getTokenPrice = useGetTokenPrice(); @@ -108,20 +101,14 @@ export const SignRawModal = memo((props) => { await delay(200); throw new CanceledActionError(); } - const vault = await unlockVault(wallet.identifier); - const privateKey = await vault.getTonPrivateKey(); startLoading(); - const contract = ContractService.getWalletContract( - contractVersionsMap[vault.getVersion() ?? 'v4R2'], - Buffer.from(vault.tonPublicKey), - vault.workchain, - ); - const timeout = await getTimeoutFromLiteserverSafely(); - const boc = TransactionService.createTransfer(contract, { + const signer = await wallet.signer.getSigner(); + + const boc = await TransactionService.createTransfer(wallet.contract, signer, { timeout, messages: TransactionService.parseSignRawMessages( params.messages, @@ -129,7 +116,6 @@ export const SignRawModal = memo((props) => { ), seqno: await getWalletSeqno(wallet), sendMode: 3, - secretKey: Buffer.from(privateKey), }); await sendBoc(boc, isBattery); @@ -321,7 +307,7 @@ export const SignRawModal = memo((props) => { { if (error?.message) { ref.current?.setError(error.message); } + } else if (error instanceof SignerError) { + if (error?.message) { + ref.current?.setError(error.message); + } } else { ref.current?.setError(t('error_occurred')); debugLog(error); diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts deleted file mode 100644 index 8c9e4d655..000000000 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperations.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { - TransferMethodParams, - WalletContract, -} from 'tonweb/dist/types/contract/wallet/wallet-contract'; -import { DeployParams, NftTransferParams, SignRawParams } from './TXRequest.types'; -import TonWeb, { Method } from 'tonweb'; -import BigNumber from 'bignumber.js'; -import { Base64, truncateDecimal } from '$utils'; -import { Wallet } from 'blockchain'; -import { NFTOperationError } from './NFTOperationError'; -import { Address as AddressType } from 'tonweb/dist/types/utils/address'; -import { Address } from '@ton/core'; -import { t } from '@tonkeeper/shared/i18n'; -import { Ton } from '$libs/Ton'; -import { Configuration, NFTApi } from '@tonkeeper/core/src/legacy'; -import { tk } from '$wallet'; -import { config } from '$config'; -import { sendBoc } from '@tonkeeper/shared/utils/blockchain'; - -const { NftItem } = TonWeb.token.nft; - -type EstimateFeeTransferMethod = ( - params: Omit, -) => Method; -export class NFTOperations { - private tonwebWallet: WalletContract; - private wallet: Wallet; - private nftApi = new NFTApi( - new Configuration({ - basePath: config.get('tonapiV2Endpoint', tk.wallet.isTestnet), - headers: { - Authorization: `Bearer ${config.get('tonApiV2Key', tk.wallet.isTestnet)}`, - }, - }), - ); - - private myAddresses: { [key: string]: string } = {}; - - constructor(wallet: Wallet) { - this.tonwebWallet = wallet.vault.tonWallet; - this.wallet = wallet; - this.getMyAddresses(); - } - - public async transfer( - params: Omit, - options?: { useCurrentWallet?: boolean }, - ) { - let wallet: WalletContract; - if (options?.useCurrentWallet) { - wallet = this.getCurrentWallet(); - } else { - const ownerAddress = await this.getOwnerAddressByItem(params.nftItemAddress); - wallet = await this.getWalletByAddress(ownerAddress); - } - - const seqno = await this.getSeqno((await wallet.getAddress()).toString(false)); - const responseAddress = await wallet.getAddress(); - - const forwardPayload = new TextEncoder().encode(params.text ?? ''); - const forwardAmount = this.toNano(params.forwardAmount); - const amount = this.toNano(params.amount); - - const newOwnerAddress = new TonWeb.utils.Address(params.newOwnerAddress); - const nftItemAddress = new TonWeb.utils.Address(params.nftItemAddress); - const nftItem = new NftItem(wallet.provider, { address: nftItemAddress }); - - const payload = await nftItem.createTransferBody({ - responseAddress, - newOwnerAddress, - forwardPayload, - forwardAmount, - }); - - return this.methods(wallet, { - toAddress: nftItemAddress, - amount: amount, - sendMode: 3, - payload, - seqno, - }); - } - - public async deploy(params: DeployParams) { - const wallet = this.getCurrentWallet(); - const seqno = await this.getSeqno((await wallet.getAddress()).toString(false)); - - const stateInitCell = TonWeb.boc.Cell.oneFromBoc(params.stateInitHex); - const hashBytes = await stateInitCell.hash(); - const hashHex = TonWeb.utils.bytesToHex(hashBytes); - const address = new TonWeb.utils.Address(params.address); - const addressHashHex = TonWeb.utils.bytesToHex(address.hashPart); - - if (hashHex !== addressHashHex) { - throw new NFTOperationError('Hash part from StateInit does not match address'); - } - - return this.methods(wallet, { - amount: this.toNano(params.amount), - stateInit: stateInitCell, - toAddress: address, - sendMode: 3, - seqno, - }); - } - - private seeIfBounceable(address: string) { - try { - return Address.isFriendly(address) - ? Address.parseFriendly(address).isBounceable - : true; - } catch { - return true; - } - } - - public async signRaw(params: SignRawParams, sendMode = 3) { - const wallet = this.getCurrentWallet(); - - const signRawMethods = async (secretKey?: Uint8Array) => { - const seqno = await this.getSeqno((await wallet.getAddress()).toString(false)); - const signingMessage = (wallet as any).createSigningMessage(seqno); - - const messages = [...params.messages].splice(0, 4); - for (let message of messages) { - const isBounceable = this.seeIfBounceable(message.address); - const order = TonWeb.Contract.createCommonMsgInfo( - TonWeb.Contract.createInternalMessageHeader( - new TonWeb.Address(message.address).toString(true, true, isBounceable), - new TonWeb.utils.BN(this.toNano(message.amount)), - ), - Ton.base64ToCell(message.stateInit), - Ton.base64ToCell(message.payload), - ); - - signingMessage.bits.writeUint8(sendMode); - signingMessage.refs.push(order); - } - - return TonWeb.Contract.createMethod( - wallet.provider, - (wallet as any).createExternalMessage( - signingMessage, - secretKey, - seqno, - !secretKey, - ), - ); - }; - - return { - getBoc: async (): Promise => { - const methods = await signRawMethods(); - - const queryMsg = await methods.getQuery(); - const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - - return boc; - }, - estimateFee: async () => { - const methods = await signRawMethods(); - const queryMsg = await methods.getQuery(); - const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - const feeInfo = await tk.wallet.tonapi.wallet.emulateMessageToWallet({ boc }); - const fee = new BigNumber(feeInfo.event.extra).multipliedBy(-1).toNumber(); - - return truncateDecimal(Ton.fromNano(fee.toString()), 2, true); - }, - send: async (secretKey: Uint8Array, onDone?: (boc: string) => void) => { - const methods = await signRawMethods(secretKey); - - const queryMsg = await methods.getQuery(); - const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - - await sendBoc(boc, false); - - onDone?.(boc); - }, - }; - } - - private async getOwnerAddressByItem(nftItemAddress: string) { - const nftItemData = await this.nftApi.getNftItemsByAddresses({ - getAccountsRequest: { accountIds: [nftItemAddress] }, - }); - const ownerAddress = nftItemData.nftItems[0].owner?.address; - - if (!ownerAddress) { - throw new NFTOperationError('No ownerAddress'); - } - - const isTestnet = this.wallet.ton.isTestnet; - return new TonWeb.Address(ownerAddress).toString(true, true, true, isTestnet); - } - - private async getOwnerAddressByCollection(nftCollectionAddress: string) { - const nftCollection = await this.nftApi.getNftCollection({ - accountId: nftCollectionAddress, - }); - - const isTestnet = this.wallet.ton.isTestnet; - return new TonWeb.Address(nftCollection.owner?.address as string).toString( - true, - true, - true, - isTestnet, - ); - } - - // - // Utils - // - - private async getSeqno(address: string) { - const seqno = await this.wallet.ton.getSeqno(address); - return seqno ?? 0; - } - - public toNano(amount: string) { - return Ton.toNano(Ton.fromNano(amount)); - } - - private methods( - wallet: WalletContract, - params: Omit, - data?: T, - ) { - return { - getData: () => data!, - estimateFee: async () => { - const transfer = wallet.methods.transfer as EstimateFeeTransferMethod; - const methods = transfer(params); - const queryMsg = await methods.getQuery(); - const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - const feeInfo = await tk.wallet.tonapi.wallet.emulateMessageToWallet({ boc }); - const fee = new BigNumber(feeInfo.event.extra).multipliedBy(-1).toNumber(); - - return truncateDecimal(Ton.fromNano(fee.toString()), 2, true); - }, - send: async (secretKey: Uint8Array) => { - const myInfo = await this.wallet.ton.getWalletInfo( - (wallet.address as AddressType).toString(true, true, false), - ); - - let amountBN: BigNumber; - if (typeof params.amount === 'number') { - amountBN = new BigNumber(params.amount); - } else { - amountBN = new BigNumber(params.amount.toNumber()); - } - - const transfer = wallet.methods.transfer({ - ...params, - secretKey, - }); - - let feeNano: BigNumber; - try { - const query = await transfer.getQuery(); - const boc = Base64.encodeBytes(await query.toBoc(false)); - const feeInfo = await tk.wallet.tonapi.wallet.emulateMessageToWallet({ boc }); - feeNano = new BigNumber(feeInfo.event.extra).multipliedBy(-1); - } catch (e) { - throw new NFTOperationError(t('send_fee_estimation_error')); - } - - if ( - amountBN - .plus(params.sendMode === 128 ? 0 : feeNano) - .isGreaterThan(myInfo?.balance ?? '0') - ) { - throw new NFTOperationError(t('send_insufficient_funds')); - } - - const queryMsg = await transfer.getQuery(); - const boc = Base64.encodeBytes(await queryMsg.toBoc(false)); - - await sendBoc(boc, false); - }, - }; - } - - private async getMyAddresses() { - if (Object.keys(this.myAddresses).length > 0) { - return this.myAddresses; - } - - const addresses = await this.wallet.ton.getAllAddresses(); - - const reverse = Object.fromEntries( - Object.entries(addresses).map(([key, value]) => { - const address = new TonWeb.utils.Address(value as string).toString(false); - return [address, key]; - }), - ); - - this.myAddresses = reverse; - - return reverse; - } - - private async getWalletByAddress(unknownAddress: string): Promise { - const addresses = await this.getMyAddresses(); - - const address = new TonWeb.utils.Address(unknownAddress).toString(false); - const version = addresses[address]; - - if (!version) { - throw new NFTOperationError('Wrong owner address'); - } - - const wallet = this.wallet.vault.tonWalletByVersion(version); - await wallet.getAddress(); - return wallet; - } - - private getCurrentWallet(): WalletContract { - return this.wallet.vault.tonWallet; - } -} diff --git a/packages/mobile/src/core/NFT/NFT.tsx b/packages/mobile/src/core/NFT/NFT.tsx index e9e09b30b..d734aad68 100644 --- a/packages/mobile/src/core/NFT/NFT.tsx +++ b/packages/mobile/src/core/NFT/NFT.tsx @@ -243,7 +243,7 @@ export const NFT: React.FC = ({ oldNftItem, route }) => { {!isWatchOnly && isTonDiamondsNft && !flags.disable_apperance ? ( } /> ) : null} - {!isWatchOnly ? ( + {!isWatchOnly && !wallet.isLedger ? ( {nft.ownerAddress && (