Skip to content

Commit 6c8c5c1

Browse files
authored
Bluetooth support (#20)
* Add bluetooth support * Add temp page whitelist for bluetooth * Add Ledger transport Initialized helper * Add modal for device connection
1 parent ab0a74e commit 6c8c5c1

File tree

6 files changed

+139
-39
lines changed

6 files changed

+139
-39
lines changed

package-lock.json

Lines changed: 25 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@emotion/server": "^11.11.0",
1919
"@ledgerhq/errors": "^6.16.0",
2020
"@ledgerhq/hw-transport": "^6.30.0",
21+
"@ledgerhq/hw-transport-web-ble": "^6.29.4",
2122
"@ledgerhq/hw-transport-webhid": "^6.28.0",
2223
"@mantine/core": "^7.1.5",
2324
"@mantine/form": "^7.2.2",

src/app/page.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const WHITELIST = [
6767
'preview.kasvault.io',
6868
'privatepreview.kasvault.io',
6969
'kasvault.vercel.app',
70+
'bluetooth.kasvault.io',
7071
];
7172

7273
export default function Home() {
@@ -87,7 +88,10 @@ export default function Home() {
8788
}
8889
}
8990

90-
setIsShowDemo(window.location.hostname !== 'kasvault.io');
91+
setIsShowDemo(
92+
window.location.hostname === 'preview.kasvault.io' ||
93+
window.location.search.includes('demo'),
94+
);
9195
}, []);
9296

9397
const smallStyles = width <= 48 * 16 ? { fontSize: '1rem' } : {};
@@ -107,7 +111,23 @@ export default function Home() {
107111
</h2>
108112
<Text>(Replaced with bluetooth in the future)</Text>
109113
</Stack>
110-
) : null;
114+
) : (
115+
<Stack
116+
className={styles.card}
117+
onClick={() => {
118+
getAppData(navigate, 'bluetooth');
119+
}}
120+
align='center'
121+
>
122+
<h3>
123+
<Group style={smallStyles}>
124+
<IconBluetooth style={smallStyles} /> Connect with Bluetooth
125+
<span>-&gt;</span>
126+
</Group>
127+
</h3>
128+
<Text>Nano X, Stax and Flex</Text>
129+
</Stack>
130+
);
111131

112132
return (
113133
<Stack className={styles.main}>
@@ -138,11 +158,11 @@ export default function Home() {
138158
}}
139159
align='center'
140160
>
141-
<h2>
161+
<h3>
142162
<Group style={smallStyles}>
143163
<IconUsb /> Connect with USB <span>-&gt;</span>
144164
</Group>
145-
</h2>
165+
</h3>
146166

147167
<Text>All Ledger devices</Text>
148168
</Stack>

src/app/wallet/page.tsx

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import {
66
fetchAddressDetails,
77
initTransport,
88
fetchAddressBalance,
9+
isLedgerTransportInitialized,
910
} from '../../lib/ledger';
1011
import { useState, useEffect } from 'react';
11-
import { Stack, Tabs, Breadcrumbs, Anchor, Button, Center } from '@mantine/core';
12+
import { Stack, Tabs, Breadcrumbs, Anchor, Button, Center, Modal, Text } from '@mantine/core';
1213
import Header from '../../components/header';
1314
import AddressesTab from './addresses-tab';
1415
import OverviewTab from './overview-tab';
@@ -279,6 +280,7 @@ export default function Dashboard() {
279280
const [enableGenerate, setEnableGenerate] = useState(false);
280281
const [mempoolEntryToReplace, setMempoolEntryToReplace] = useState<IMempoolEntry | null>(null);
281282
const [pendingTxId, setPendingTxId] = useState<string | null>(null);
283+
const [showConnectModal, setShowConnectModal] = useState(false);
282284

283285
const { ref: containerRef, width: containerWidth, height: containerHeight } = useElementSize();
284286

@@ -376,27 +378,32 @@ export default function Dashboard() {
376378

377379
let unloaded = false;
378380

379-
initTransport(deviceType)
380-
.then(() => {
381-
if (!unloaded) {
382-
setTransportInitialized(true);
383-
384-
return getXPubFromLedger().then((xpub) =>
385-
setBIP32Base(new KaspaBIP32(xpub.compressedPublicKey, xpub.chainCode)),
386-
);
387-
}
381+
if (!isLedgerTransportInitialized()) {
382+
setShowConnectModal(true);
383+
} else {
384+
initTransport(deviceType)
385+
.then(() => {
386+
if (!unloaded) {
387+
setTransportInitialized(true);
388+
389+
return getXPubFromLedger().then((xpub) =>
390+
setBIP32Base(new KaspaBIP32(xpub.compressedPublicKey, xpub.chainCode)),
391+
);
392+
}
388393

389-
return null;
390-
})
391-
.catch((e) => {
392-
notifications.show({
393-
title: 'Error',
394-
color: 'red',
395-
message: 'Please make sure your device is unlocked and the Kaspa app is open',
396-
autoClose: false,
394+
return null;
395+
})
396+
.catch((e) => {
397+
notifications.show({
398+
title: 'Error',
399+
color: 'red',
400+
message:
401+
'Please make sure your device is unlocked and the Kaspa app is open',
402+
autoClose: false,
403+
});
404+
console.error(e);
397405
});
398-
console.error(e);
399-
});
406+
}
400407

401408
return () => {
402409
unloaded = true;
@@ -426,7 +433,7 @@ export default function Dashboard() {
426433
return;
427434
}
428435

429-
if (deviceType === 'usb') {
436+
if (deviceType === 'usb' || deviceType === 'bluetooth') {
430437
loadOrScanAddressBatch(bip32base, setAddresses, setRawAddresses, userSettings).finally(
431438
() => {
432439
setEnableGenerate(true);
@@ -455,6 +462,51 @@ export default function Dashboard() {
455462
<Breadcrumbs>{breadcrumbs}</Breadcrumbs>
456463
</Header>
457464

465+
<Modal
466+
centered
467+
opened={showConnectModal}
468+
withCloseButton={false}
469+
onClose={() => setShowConnectModal(false)}
470+
title={'Connect Ledger Device via ' + deviceType}
471+
>
472+
<Stack>
473+
<Text>Please connect your Ledger device and open the Kaspa app.</Text>
474+
<Button
475+
onClick={() => {
476+
setShowConnectModal(false);
477+
initTransport(deviceType)
478+
.then(() => {
479+
setTransportInitialized(true);
480+
return getXPubFromLedger().then((xpub) =>
481+
setBIP32Base(
482+
new KaspaBIP32(
483+
xpub.compressedPublicKey,
484+
xpub.chainCode,
485+
),
486+
),
487+
);
488+
})
489+
.catch((e) => {
490+
if (e.name === 'TransportOpenUserCancelled') {
491+
setShowConnectModal(true);
492+
} else {
493+
notifications.show({
494+
title: 'Error',
495+
color: 'red',
496+
message:
497+
'Please make sure your device is unlocked and the Kaspa app is open',
498+
autoClose: false,
499+
});
500+
}
501+
console.error(e);
502+
});
503+
}}
504+
>
505+
Connect Device
506+
</Button>
507+
</Stack>
508+
</Modal>
509+
458510
<Center>
459511
<Tabs
460512
value={activeTab}

src/components/send-form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export default function SendForm(props: SendFormProps) {
181181

182182
if (deviceType == 'demo') {
183183
simulateConfirmation(notifId);
184-
} else if (deviceType == 'usb') {
184+
} else if (deviceType == 'usb' || deviceType == 'bluetooth') {
185185
try {
186186
const { tx } = createTransaction(
187187
kasToSompi(Number(form.values.amount)),

src/lib/ledger.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import TransportWebHID from '@ledgerhq/hw-transport-webhid';
2+
import BluetoothTransport from '@ledgerhq/hw-transport-web-ble';
23
import axios from 'axios';
34
import axiosRetry from 'axios-retry';
45

@@ -19,6 +20,7 @@ let transportState = {
1920
transport: null,
2021
initPromise: null,
2122
type: null,
23+
initialized: false,
2224
};
2325

2426
const kaspaState = {
@@ -156,13 +158,25 @@ export async function initTransport(type = 'usb') {
156158
return await transportState.initPromise;
157159
}
158160

159-
transportState.initPromise = TransportWebHID.create();
161+
if (type === 'usb') {
162+
transportState.initPromise = TransportWebHID.create();
163+
} else if (type === 'bluetooth') {
164+
transportState.initPromise = BluetoothTransport.create();
165+
} else {
166+
throw new Error('Unknown device type');
167+
}
168+
160169
transportState.transport = await transportState.initPromise;
161170
transportState.type = type;
171+
transportState.initialized = true;
162172

163173
return transportState.transport;
164174
}
165175

176+
export function isLedgerTransportInitialized() {
177+
return transportState.initialized;
178+
}
179+
166180
export async function fetchTransactionCount(address) {
167181
const { data: txCount } = await axios.get(
168182
`https://api.kaspa.org/addresses/${address}/transactions-count`,

0 commit comments

Comments
 (0)