Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mobile): ledger support #835

Merged
merged 7 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/@core-js/src/service/transactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,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(
Expand Down
24 changes: 24 additions & 0 deletions packages/mobile/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,30 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.NFC" />

<!-- Bluetooth permissions for SDK API 30+ -->
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Bluetooth permissions for SDK API 18-30 -->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
tools:node="replace" />

<!-- Bluetooth permissions for SDK API 23-30 -->
<uses-permission-sdk-23
android:name="android.permission.ACCESS_FINE_LOCATION"
tools:node="remove" />

<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />

<!-- TODO: review deprecated permissions -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/mobile/ios/ton_keeper/SupportingFiles/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@
</dict>
</dict>
</dict>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Tonkeeper uses bluetooth to connect your hardware Ledger Nano X</string>
<key>NSCameraUsageDescription</key>
<string>Tonkeeper uses camera to scan QR codes</string>
<key>NSFaceIDUsageDescription</key>
Expand Down
5 changes: 5 additions & 0 deletions packages/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -42,6 +44,7 @@
"@react-native-firebase/messaging": "18.5.0",
"@reduxjs/toolkit": "^1.6.1",
"@shopify/flash-list": "^1.5.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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
63 changes: 60 additions & 3 deletions packages/mobile/src/blockchain/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ import {
} from '@tonkeeper/core/src/legacy';

import { tk } from '$wallet';
import { Address, Cell, beginCell, internal } from '@ton/core';
import {
Address,
Cell,
beginCell,
toNano as tonCoreToNano,
internal,
SendMode,
comment,
} from '@ton/core';
import {
NetworkOverloadedError,
emulateBoc,
Expand Down Expand Up @@ -383,10 +391,37 @@ export class TonWallet {
jettonTransferAmount = ONE_TON,
estimateFee,
}: JettonTransferParams) {
const signer = await tk.wallet.signer.getSigner(estimateFee);

const jettonAmount = BigInt(amountNano);

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,
Expand Down Expand Up @@ -560,6 +595,28 @@ export class TonWallet {
}: TonTransferParams) {
const timeout = await getTimeoutFromLiteserverSafely();

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, {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = <Icon name="ic-done-16" color="accentGreen" />;
if (state === 'future') {
content = <Icon name="ic-dot-16" color="iconTertiary" />;
}

if (state === 'active') {
content = <Loader size="small" color="iconTertiary" />;
}

return <View style={styles.iconContainer}>{content}</View>;
};

interface Props {
showConfirmTxStep?: boolean;
currentStep: LedgerConnectionCurrentStep;
}

export const LedgerConnectionSteps: FC<Props> = (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 (
<View style={styles.container}>
<LegderView currentStep={currentStep} showConfirmTxStep={showConfirmTxStep} />
<View style={styles.stepsContainer}>
<View style={styles.step}>
<StepIcon state={currentStep === 'connect' ? 'active' : 'completed'} />
<View style={styles.stepText}>
<Text
type="body2"
color={currentStep !== 'connect' ? 'accentGreen' : 'textPrimary'}
>
{t('ledger.connect')}
</Text>
</View>
</View>
<View style={styles.step}>
<StepIcon
state={
currentStep === 'open-ton'
? 'active'
: currentStep === 'connect'
? 'future'
: 'completed'
}
/>
<View style={styles.stepText}>
<Text
type="body2"
color={
currentStep === 'all-completed' || currentStep === 'confirm-tx'
? 'accentGreen'
: 'textPrimary'
}
>
{t('ledger.open_ton_app')}
</Text>
{!showConfirmTxStep ? (
<TouchableOpacity hitSlop={BUTTON_HIT_SLOP} onPress={openLegderLive}>
<Text type="body2" color="accentBlue">
{t('ledger.install_ton_app')}
</Text>
</TouchableOpacity>
) : null}
</View>
</View>
{showConfirmTxStep ? (
<View style={styles.step}>
<StepIcon
state={
currentStep === 'confirm-tx'
? 'active'
: currentStep === 'connect' || currentStep === 'open-ton'
? 'future'
: 'completed'
}
/>
<View style={styles.stepText}>
<Text
type="body2"
color={currentStep === 'all-completed' ? 'accentGreen' : 'textPrimary'}
>
{t('ledger.confirm_tx')}
</Text>
</View>
</View>
) : null}
</View>
</View>
);
};

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,
},
}));
Loading