From 91c87b73d02af59e2f0fbf56316e97e7f22d10d9 Mon Sep 17 00:00:00 2001 From: coderofstuff <114628839+coderofstuff@users.noreply.github.com> Date: Sun, 22 Dec 2024 00:11:29 -0700 Subject: [PATCH 1/4] Add bluetooth support --- package-lock.json | 37 ++++++++++++++++++++++++------------ package.json | 1 + src/app/page.tsx | 27 ++++++++++++++++++++++---- src/app/wallet/page.tsx | 2 +- src/components/send-form.tsx | 2 +- src/lib/ledger.ts | 10 +++++++++- 6 files changed, 60 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index cc26d7b..5a76858 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@emotion/server": "^11.11.0", "@ledgerhq/errors": "^6.16.0", "@ledgerhq/hw-transport": "^6.30.0", + "@ledgerhq/hw-transport-web-ble": "^6.29.4", "@ledgerhq/hw-transport-webhid": "^6.28.0", "@mantine/core": "^7.1.5", "@mantine/form": "^7.2.2", @@ -3350,32 +3351,44 @@ } }, "node_modules/@ledgerhq/devices": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.2.2.tgz", - "integrity": "sha512-SKahGA4p0mZ3ovypOJ2wa5mUvUkArE3HBrwWKYf+cRs+t/Licp3OJfhj+DHIxP3AfyH2xR6CFFWECYHeKwGsDQ==", + "version": "8.4.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.4.4.tgz", + "integrity": "sha512-sz/ryhe/R687RHtevIE9RlKaV8kkKykUV4k29e7GAVwzHX1gqG+O75cu1NCJUHLbp3eABV5FdvZejqRUlLis9A==", "dependencies": { - "@ledgerhq/errors": "^6.16.3", + "@ledgerhq/errors": "^6.19.1", "@ledgerhq/logs": "^6.12.0", "rxjs": "^7.8.1", "semver": "^7.3.5" } }, "node_modules/@ledgerhq/errors": { - "version": "6.16.3", - "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.16.3.tgz", - "integrity": "sha512-3w7/SJVXOPa9mpzyll7VKoKnGwDD3BzWgN1Nom8byR40DiQvOKjHX+kKQausCedTHVNBn9euzPCNsftZ9+mxfw==" + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.19.1.tgz", + "integrity": "sha512-75yK7Nnit/Gp7gdrJAz0ipp31CCgncRp+evWt6QawQEtQKYEDfGo10QywgrrBBixeRxwnMy1DP6g2oCWRf1bjw==" }, "node_modules/@ledgerhq/hw-transport": { - "version": "6.30.5", - "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.30.5.tgz", - "integrity": "sha512-JMl//7BgPBvWxrWyMu82jj6JEYtsQyOyhYtonWNgtxn6KUZWht3gU4gxmLpeIRr+DiS7e50mW7m3GA+EudZmmA==", + "version": "6.31.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.31.4.tgz", + "integrity": "sha512-6c1ir/cXWJm5dCWdq55NPgCJ3UuKuuxRvf//Xs36Bq9BwkV2YaRQhZITAkads83l07NAdR16hkTWqqpwFMaI6A==", "dependencies": { - "@ledgerhq/devices": "^8.2.2", - "@ledgerhq/errors": "^6.16.3", + "@ledgerhq/devices": "^8.4.4", + "@ledgerhq/errors": "^6.19.1", "@ledgerhq/logs": "^6.12.0", "events": "^3.3.0" } }, + "node_modules/@ledgerhq/hw-transport-web-ble": { + "version": "6.29.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-web-ble/-/hw-transport-web-ble-6.29.4.tgz", + "integrity": "sha512-OJyp6CryvyFlg1L9uifo5hYYdDt+WPw8/0ijBixYhYmGvlRz2W6/F2c5rG/zBQWcNnNydPOLjLJM0vR070RfCw==", + "dependencies": { + "@ledgerhq/devices": "^8.4.4", + "@ledgerhq/errors": "^6.19.1", + "@ledgerhq/hw-transport": "^6.31.4", + "@ledgerhq/logs": "^6.12.0", + "rxjs": "^7.8.1" + } + }, "node_modules/@ledgerhq/hw-transport-webhid": { "version": "6.28.0", "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webhid/-/hw-transport-webhid-6.28.0.tgz", diff --git a/package.json b/package.json index 422503d..f233ba4 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@emotion/server": "^11.11.0", "@ledgerhq/errors": "^6.16.0", "@ledgerhq/hw-transport": "^6.30.0", + "@ledgerhq/hw-transport-web-ble": "^6.29.4", "@ledgerhq/hw-transport-webhid": "^6.28.0", "@mantine/core": "^7.1.5", "@mantine/form": "^7.2.2", diff --git a/src/app/page.tsx b/src/app/page.tsx index 81354f5..5417bab 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -87,7 +87,10 @@ export default function Home() { } } - setIsShowDemo(window.location.hostname !== 'kasvault.io'); + setIsShowDemo( + window.location.hostname === 'preview.kasvault.io' || + window.location.search.includes('demo'), + ); }, []); const smallStyles = width <= 48 * 16 ? { fontSize: '1rem' } : {}; @@ -107,7 +110,23 @@ export default function Home() { (Replaced with bluetooth in the future) - ) : null; + ) : ( + { + getAppData(navigate, 'bluetooth'); + }} + align='center' + > +

+ + Connect with Bluetooth + -> + +

+ Nano X, Stax and Flex +
+ ); return ( @@ -138,11 +157,11 @@ export default function Home() { }} align='center' > -

+

Connect with USB -> -

+ All Ledger devices
diff --git a/src/app/wallet/page.tsx b/src/app/wallet/page.tsx index 9b962a5..3fe4787 100644 --- a/src/app/wallet/page.tsx +++ b/src/app/wallet/page.tsx @@ -426,7 +426,7 @@ export default function Dashboard() { return; } - if (deviceType === 'usb') { + if (deviceType === 'usb' || deviceType === 'bluetooth') { loadOrScanAddressBatch(bip32base, setAddresses, setRawAddresses, userSettings).finally( () => { setEnableGenerate(true); diff --git a/src/components/send-form.tsx b/src/components/send-form.tsx index f3dd7bb..adc343f 100644 --- a/src/components/send-form.tsx +++ b/src/components/send-form.tsx @@ -181,7 +181,7 @@ export default function SendForm(props: SendFormProps) { if (deviceType == 'demo') { simulateConfirmation(notifId); - } else if (deviceType == 'usb') { + } else if (deviceType == 'usb' || deviceType == 'bluetooth') { try { const { tx } = createTransaction( kasToSompi(Number(form.values.amount)), diff --git a/src/lib/ledger.ts b/src/lib/ledger.ts index 1a16faa..d555e59 100644 --- a/src/lib/ledger.ts +++ b/src/lib/ledger.ts @@ -1,4 +1,5 @@ import TransportWebHID from '@ledgerhq/hw-transport-webhid'; +import BluetoothTransport from '@ledgerhq/hw-transport-web-ble'; import axios from 'axios'; import axiosRetry from 'axios-retry'; @@ -156,7 +157,14 @@ export async function initTransport(type = 'usb') { return await transportState.initPromise; } - transportState.initPromise = TransportWebHID.create(); + if (type === 'usb') { + transportState.initPromise = TransportWebHID.create(); + } else if (type === 'bluetooth') { + transportState.initPromise = BluetoothTransport.create(); + } else { + throw new Error('Unknown device type'); + } + transportState.transport = await transportState.initPromise; transportState.type = type; From b96a7c4b78657026c9937efacd6ec114e5969dda Mon Sep 17 00:00:00 2001 From: coderofstuff <114628839+coderofstuff@users.noreply.github.com> Date: Sun, 22 Dec 2024 00:20:22 -0700 Subject: [PATCH 2/4] Add temp page whitelist for bluetooth --- src/app/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/page.tsx b/src/app/page.tsx index 5417bab..77d2d37 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -67,6 +67,7 @@ const WHITELIST = [ 'preview.kasvault.io', 'privatepreview.kasvault.io', 'kasvault.vercel.app', + 'bluetooth.kasvault.io', ]; export default function Home() { From 64601d3ec508271cec4ff55a76c25e8adc4cfd3a Mon Sep 17 00:00:00 2001 From: coderofstuff <114628839+coderofstuff@users.noreply.github.com> Date: Mon, 12 May 2025 15:49:35 -0600 Subject: [PATCH 3/4] Add Ledger transport Initialized helper --- src/lib/ledger.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/ledger.ts b/src/lib/ledger.ts index d555e59..3421806 100644 --- a/src/lib/ledger.ts +++ b/src/lib/ledger.ts @@ -20,6 +20,7 @@ let transportState = { transport: null, initPromise: null, type: null, + initialized: false, }; const kaspaState = { @@ -167,10 +168,15 @@ export async function initTransport(type = 'usb') { transportState.transport = await transportState.initPromise; transportState.type = type; + transportState.initialized = true; return transportState.transport; } +export function isLedgerTransportInitialized() { + return transportState.initialized; +} + export async function fetchTransactionCount(address) { const { data: txCount } = await axios.get( `https://api.kaspa.org/addresses/${address}/transactions-count`, From da3f025d148f4edbe61b240f96a9e00409c01471 Mon Sep 17 00:00:00 2001 From: coderofstuff <114628839+coderofstuff@users.noreply.github.com> Date: Mon, 12 May 2025 16:16:42 -0600 Subject: [PATCH 4/4] Add modal for device connection --- src/app/wallet/page.tsx | 92 ++++++++++++++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 20 deletions(-) diff --git a/src/app/wallet/page.tsx b/src/app/wallet/page.tsx index 3fe4787..17b684c 100644 --- a/src/app/wallet/page.tsx +++ b/src/app/wallet/page.tsx @@ -6,9 +6,10 @@ import { fetchAddressDetails, initTransport, fetchAddressBalance, + isLedgerTransportInitialized, } from '../../lib/ledger'; import { useState, useEffect } from 'react'; -import { Stack, Tabs, Breadcrumbs, Anchor, Button, Center } from '@mantine/core'; +import { Stack, Tabs, Breadcrumbs, Anchor, Button, Center, Modal, Text } from '@mantine/core'; import Header from '../../components/header'; import AddressesTab from './addresses-tab'; import OverviewTab from './overview-tab'; @@ -279,6 +280,7 @@ export default function Dashboard() { const [enableGenerate, setEnableGenerate] = useState(false); const [mempoolEntryToReplace, setMempoolEntryToReplace] = useState(null); const [pendingTxId, setPendingTxId] = useState(null); + const [showConnectModal, setShowConnectModal] = useState(false); const { ref: containerRef, width: containerWidth, height: containerHeight } = useElementSize(); @@ -376,27 +378,32 @@ export default function Dashboard() { let unloaded = false; - initTransport(deviceType) - .then(() => { - if (!unloaded) { - setTransportInitialized(true); - - return getXPubFromLedger().then((xpub) => - setBIP32Base(new KaspaBIP32(xpub.compressedPublicKey, xpub.chainCode)), - ); - } + if (!isLedgerTransportInitialized()) { + setShowConnectModal(true); + } else { + initTransport(deviceType) + .then(() => { + if (!unloaded) { + setTransportInitialized(true); + + return getXPubFromLedger().then((xpub) => + setBIP32Base(new KaspaBIP32(xpub.compressedPublicKey, xpub.chainCode)), + ); + } - return null; - }) - .catch((e) => { - notifications.show({ - title: 'Error', - color: 'red', - message: 'Please make sure your device is unlocked and the Kaspa app is open', - autoClose: false, + return null; + }) + .catch((e) => { + notifications.show({ + title: 'Error', + color: 'red', + message: + 'Please make sure your device is unlocked and the Kaspa app is open', + autoClose: false, + }); + console.error(e); }); - console.error(e); - }); + } return () => { unloaded = true; @@ -455,6 +462,51 @@ export default function Dashboard() { {breadcrumbs} + setShowConnectModal(false)} + title={'Connect Ledger Device via ' + deviceType} + > + + Please connect your Ledger device and open the Kaspa app. + + + +