Skip to content

feat: add blockstream jade hardware wallet support - #1231

Open
coreyphillips wants to merge 20 commits into
masterfrom
feat/jade-hardware-wallet
Open

coreyphillips wants to merge 20 commits into
masterfrom
feat/jade-hardware-wallet

Conversation

@coreyphillips

@coreyphillips coreyphillips commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

This PR:

  1. Adds Blockstream Jade as a second hardware wallet vendor, over USB and Bluetooth
  2. Generalises the hardware wallet layer so every device call is routed by the vendor of the paired entry
  3. Releases an open Jade Bluetooth link while Bitkit is backgrounded and reconnects silently on return

Requires bitkit-core 0.5.16 (synonymdev/bitkit-core#153), which carries the Jade module and pins jade-client-rs at d52ccd9.

Description

A Jade can now be paired from Connect Hardware over either transport, unlocked with its PIN, and
used exactly like a paired Trezor: watch-only balances, on-device receive address verification, and
on-device signing for both a normal send and a transfer to spending. The protocol, the pinserver
round trip and every deadline live in bitkit-core. This app supplies the byte transport over the
phone's radios and the UI that drives the flows.

The transport covers USB serial through a CP210x bridge on Jade v1 and native USB CDC on Jade Plus,
plus Bluetooth over the Nordic UART Service. Three USB device filter entries were added so Android
offers Bitkit when a Jade is plugged in.

Four things only a physical device revealed, each fixed here:

  • The TX characteristic is indicate-only on this firmware, so the transport subscribes to
    indications when notify is absent instead of failing the connection.
  • A Jade advertises under a new random Bluetooth address after every reboot or pairing reset, so a
    stored entry is recognised by name, which is Jade plus the last six hex digits of its efuse MAC,
    rather than by address.
  • A link left open when the process dies wedges the device's single connection slot until it is
    power-cycled. Links are now closed when the activity finishes, and released after 30 seconds in
    the background so the same thing does not happen when Android kills a backgrounded Bitkit. Coming
    back to the foreground reconnects without a prompt.
  • A bond that went stale across a re-pair used to stall the first write. It now gets a write budget
    wide enough to cover a re-pair, and a message telling the user to forget the Jade in Android's
    Bluetooth settings and pair again.

The vendor-neutral part is a refactor rather than new behaviour. The hardware wallet repository now
merges both vendors' discovery state, routes connect, verify and sign by the vendor stored on the
paired entry, and alternates which vendor gets the Bluetooth half of a scan so repeated searches
stay under Android's scan-rate limit. Watchers, transaction composition and broadcast are vendor
neutral already and stay where they are. Entries saved before this change carry no vendor and are
read as Trezor, so paired Trezors are untouched. Reconnect gets a longer deadline for a Jade,
because that reconnect may be waiting for a PIN to be entered on the device.

Session and identity hardening added during review:

  • Cancelling pairing or receive verification explicitly cancels Jade work and closes the native transport in cleanup that survives caller cancellation.
  • Every failed post-connect step closes both the Android transport and core session.
  • A known USB reconnect checks each attached Jade until its efuse identity matches, but stops immediately if the expected Jade itself fails.
  • The shared repository enforces one active vendor session and checks both vendor states before Bluetooth scans.
  • Equal seeds on different vendors keep separate wallet identities, labels, watchers, and signing routes.
  • Signing on a Jade checks that the connected session belongs to the wallet being spent from, as the Trezor path does.
  • Finishing the activity releases the Jade transport, core session and cached connection together, off the main thread.
  • The receive sheet closes a hardware session only if it used the device, and a USB attach for a vendor with nothing paired no longer drops the other vendor's session.
  • The send sheet can be dismissed while it connects or waits for a Jade PIN; it stays locked while the device signs and while a broadcast is unresolved.

Jade-specific failures get their own copy: PIN entry, wrong PIN, an unreachable pinserver, a device
that is busy, firmware too old, a device that has no wallet yet, a network mismatch, and a PSBT the
device cannot hold.

Two gaps worth naming. The Jade illustration is a placeholder vector until design supplies the real
asset. Signet is not supported by Jade, so that combination throws rather than mapping to a network.

Preview

QA Notes

Verified against a Jade v1 on firmware 1.0.41. There is no Jade emulator in bitkit-docker, so
these are all physical-device checks.

Manual Tests

  • 1. Jade over USB → Connect Hardware → Search → Pair: PIN prompt shows on the device,
    unlock completes, accounts export and the wallet tile appears.
  • 2. regression: USB → Send → pick the Jade source → sign on device → broadcast:
    transaction confirms.
  • 3. Jade over Bluetooth → Connect Hardware → Search → Pair: pairs and unlocks.
  • 4. Bluetooth → Receive → Hardware tab → Verify on Device: the address shown on the Jade
    matches the one in the app.
  • 5. Bluetooth → Send → sign on device → broadcast: broadcast succeeded, txid
    d955bc0c....
  • 6. Bluetooth connected → background Bitkit for about 45 seconds → reopen: reconnects with
    no pairing prompt and no PIN re-entry.

Automated Checks

  • Unit tests added: JadeTransportTest.kt covers USB driver selection, the CP210x and CDC open and
    close sequences, chunk sizing and read and write timeouts; JadeRepoTest.kt covers connect,
    unlock, replug and reconnect, recognising a Bluetooth Jade by name after its address changed, the
    background release and its USB counterpart, signing and address verification; JadeServiceTest.kt
    covers the finalizePsbt alias that used to recurse into itself; HwUsbIdTest.kt covers vendor
    detection from USB ids; KnownDeviceTest.kt covers vendor-aware entry matching, migration of
    pre-Jade entries, wallet identity and equal-seed vendor isolation; HwErrorPresenterTest.kt and HwExceptionExtTest.kt cover
    the Jade error copy and classification. The Bluetooth GATT paths themselves, including the
    indicate-only fallback, are not unit testable and were validated on hardware.
  • Unit tests modified: HwWalletRepoTest.kt, HwConnectViewModelTest.kt, HwSendViewModelTest.kt,
    HwReceiveViewModelTest.kt, TransferViewModelTest.kt, TrezorRepoTest.kt and
    ReceiveInvoiceUtilsTest.kt move onto the vendor-neutral device state and the per-vendor routing.
  • Local: just compile, just test (2626 tests, 0 failures) and just lint all pass.

@coreyphillips
coreyphillips marked this pull request as ready for review September 15, 2026 13:19
@greptile-apps

greptile-apps Bot commented Sep 15, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

The PR is not yet safe to merge because activity teardown can leave a stale Jade session and Jade signing can run against a session that changed after composition.

Findings

  1. P1 Transport Cleanup Leaves Stale Session
  2. P1 Jade Signing Skips Identity Check

Summary

This PR adds Blockstream Jade and Jade Plus support over USB and Bluetooth, generalizes hardware-wallet state and operation routing across vendors, adds Jade-specific pairing, unlocking, verification and signing flows, and introduces lifecycle-based Bluetooth release and reconnect behavior.

  • Adds CP210x, CDC, and Nordic UART transports for Jade.
  • Persists vendor-scoped hardware identities and keeps legacy entries mapped to Trezor.
  • Routes discovery, connection, address verification, transaction composition, and signing by vendor.
  • Adds vendor-specific UI, errors, USB filters, and extensive unit coverage.
  • Two lifecycle/session-boundary defects remain in transport teardown and Jade signing.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    UI[Hardware wallet UI] --> HWR[HwWalletRepo]
    HWR -->|vendor = Trezor| TR[TrezorRepo]
    HWR -->|vendor = Blockstream| JR[JadeRepo]
    JR --> JS[JadeService / bitkit-core]
    JS --> JT[JadeTransport]
    JT --> USB[CP210x or USB CDC]
    JT --> BLE[Nordic UART BLE]
    APP[Process lifecycle] --> HWR
    ACT[MainActivity teardown] -. currently bypasses repository .-> JT
Loading

Reviews (1) · Last reviewed commit: "chore: bump bitkit-core to 0.5.16"

Comment thread app/src/main/java/to/bitkit/ui/MainActivity.kt Outdated
Comment thread app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt Outdated
- Route activity teardown through JadeRepo so the transport, core session and cached connection are cleared together, off the main thread
- Refuse Jade signing when the connected session belongs to a different wallet
# Conflicts:
#	gradle/libs.versions.toml
@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 187fd19 (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at ca919d98b as a funds/signing change. No HIGH, no MEDIUM. Three LOW notes below, all on the lifecycle surface rather than the signing path — none loses funds, none wedges the app.

The signing path holds up. This was the thing I most wanted to break, so here is the trace, because the two vendors are protected by different mechanisms and that is worth writing down:

  • The PSBT is composed app-side from the stored account xpub, so the change output is app-derived and never device-sourced. The only device-supplied input to compose is the master fingerprint, which affects key-origin metadata only.
  • Jade: signPsbt feeds the device's return to finalizePsbt(originalPsbt, signedPsbt). In bitkit-core v0.5.16 src/modules/onchain/psbt.rs, :55-59 rejects original.unsigned_tx != signed.unsigned_tx, :61-73 pins each input's previous output, then finalize_mut + interpreter_check verify the signatures against the pinned inputs. The device can contribute signatures and nothing else.
  • Trezor: does not go through finalizePsbt — it broadcasts serializedTx directly. It is protected instead by trezor-connect-rs 0.4.0, which derives expected_scripts before signing and then runs verify_signed_tx on the device-returned bytes (src/tx_verify.rs:23-73, a port of @trezor/connect's verifyTx): output count, every output amount, every output scriptPubKey against independently derived expectations. Equivalent guarantee, different code path. Worth knowing if anyone later assumes finalizePsbt covers both.
  • One caveat, pre-existing and not this PR: the Trezor path does not pin input outpoints, so a device could in principle substitute another UTXO the same seed controls. That matches upstream Trezor Connect.
  • Every signing caller routes through signFunding/broadcastFunding. The only other signTxFromPsbt/broadcastRawTx user is the pre-existing dev TrezorScreen. Nothing broadcasts a device return without one of these checks.

Also verified clean: amount and address shown on HwSendSignScreen are the same values that build the HwSendRequest, and miningFeeSats comes from the same compose result that produced the PSBT — no recompute after display. A swapped Jade with a matching efuse MAC is caught downstream (different xpubs produce a new walletId, so ensureConnected(oldWalletId) throws; in the locked silent-reconnect path where xpubs aren't re-read, signing fails at interpreter_check). No xpub, PSBT or fingerprint reaches a Logger call. runSuspendCatching is used throughout; plain runCatching appears only around non-suspending calls and once with the correct explicit CancellationException/TimeoutCancellationException rethrow guard.

Trezor regression surface — the part I'd most expect a second-vendor PR to break. TrezorRepo changes are limited to vendor-scoped store reads/writes, the (null-for-Trezor) fingerprint passthrough, and helpers moved to KnownDevice.kt with identical logic plus a vendor equality check. TrezorTransport only extracts requestUsbPermission into UsbPermissionRequester with the same action, flags and timeout. HwWalletStore.saveKnownDevices keeps the other vendor's entries inside one updateData, so concurrent writes from both repos can't drop entries. HwWalletId.derive's default stays "trezor", so existing Trezor wallet ids are stable, and legacy entries deserialize with vendor = TREZOR. Bluetooth discovery now alternates vendors with SCAN_INTERVAL 2s → 4s, so Safe 7 discovery is slower — a deliberate trade against Android's scan-rate limit, not a defect.

Both greptile threads are genuinely fixed at head (2dd3da77e), with tests; I checked rather than taking the claim.

Interaction with #1248 (fix/receive-liquidity-parity): no hidden semantic conflict, but expect a textual one. This PR renames ReceiveTab.TREZOR on the onClickEditInvoice line in ReceiveQrScreen.kt (:383-390) while #1248 rewrites the two lines just below it, and similarly in ReceiveSheet.kt (:156). #1248 adds no new ReceiveTab.TREZOR references, so once the conflict is resolved nothing compiles silently wrong. ReceiveInvoiceEditStateTest.kt hunks are disjoint. Whoever merges second should expect to resolve by hand rather than trusting a clean auto-merge.

journeys/hardware-wallet/README.md honestly scopes Jade as unit-test + manual-only, which is the right call given there's no Jade emulator.

Comment thread app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt
Comment thread app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt
@jvsena42 jvsena42 mentioned this pull request Sep 15, 2026
4 tasks
- Close a hardware session from the receive sheet only after the sheet used the device
- Ignore a transport restore for a vendor with no paired device so it cannot drop the other vendor's session
- Let the send sheet be dismissed while it connects or waits for a PIN, and keep blocking it during device signing and broadcast
- Close the Jade link before the core session so cancelling releases a pending unlock
ovi-reviewer[bot]

This comment was marked as resolved.

coreyphillips and others added 3 commits September 16, 2026 12:38
Order an external disconnect notification against the next connect, which
0.5.17 requires. jade_notify_disconnected matches the path it is given
against the connected session, so a notice still in flight when a
reconnect for that path completes tears the new session down instead of
the old one.

Nothing ordered the two before: observeExternalDisconnects awaited the
notice in one coroutine while retryAutoReconnect connected from another.
ServiceQueue.CORE does not close that gap, since a single thread
dispatcher serialises dispatch rather than suspending work, and
jadeConnect releases the thread while it awaits the handshake. The new
mutex is held for the notice alone, so a long connect or a five minute
unlock never delays one.

No API changes in 0.5.17, only behaviour. Worth knowing:

- A cancel now reports UserCancelled whether core notices the abort flag
  or the closed link first. It used to surface as DeviceDisconnected
  whenever it landed while a read was parked, which is the common case on
  Bluetooth, and isJadeUserCancellation only matches UserCancelled, so
  cancels were being classified as session failures.
- jadeGetVersionInfo reads a cached copy and no longer waits behind an
  operation in flight.
- jadeScan reports NotInitialized when no transport callback is
  registered, which JadeService already makes unreachable.
ovi-reviewer[bot]

This comment was marked as resolved.

ovi-reviewer[bot]

This comment was marked as resolved.

…wallet

# Conflicts:
#	gradle/libs.versions.toml
ovi-reviewer[bot]

This comment was marked as resolved.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ✅ Approve


Reaudit: diff 2 files.

Findings:
N/A

Audit:
Already done in comment.

QA: emu-1 Pixel 10 Pro emulator (sdk_gphone64_arm64) on Android 16 (API 36), to.bitkit.dev devDebug built and installed from the QA clone at this head

  1. ⚠️ hardware absent: No Blockstream Jade v1 over USB on the lane; Search held 58 s logging USB scan found '0' Jade device(s).

    Test 1:
    Jade over USB → Connect Hardware → Search → Pair: PIN prompt shows on the device,
    1.mp4
  2. ⚠️ hardware absent: No Jade over USB; Send reached recipient entry with no hardware source, since routing needs a paired hardwareWalletId.

    Test 2:
    `regression:` USB → Send → pick the Jade source → sign on device → broadcast:
    2.mp4
  3. ⚠️ hardware absent: No Jade advertising over BLE; the scan ran for real, alternating Jade and Trezor cycles every seven seconds.

    Test 3:
    Jade over Bluetooth → Connect Hardware → Search → Pair: pairs and unlocks.
    3.mp4
  4. ⚠️ hardware absent: No Jade over BLE to pair first; Receive showed only the Savings and Spending tabs, no Hardware tab.

    Test 4:
    Bluetooth → Receive → Hardware tab → Verify on Device: the address shown on the Jade
    4.mp4
  5. ⚠️ hardware absent: No Jade over BLE; Send reached recipient entry and logged no Jade session line during the flow.

    Test 5:
    Bluetooth → Send → sign on device → broadcast: broadcast succeeded, txid
    5.mp4
  6. ⚠️ hardware absent: No Jade holding a link; 47 s backgrounded and back brought no pairing prompt and no PIN screen.

    Test 6:
    Bluetooth connected → background Bitkit for about 45 seconds → reopen: reconnects with
    6.mp4

Note

Every item ends at a physical Blockstream Jade v1: the lane has no USB or BLE signer, bitkit-docker ships only the Trezor User Env, and journeys/hardware-wallet/README.md keeps that folder Trezor-only. The vendor-neutral half this PR adds did run on the installed binary, whose usb_device_filter.xml carries the three added ids: both scan cycles, the send routing guard, the unpaired Receive tabs and the background release no-op. The author's Jade v1 runs on firmware 1.0.41 stand for the device halves.

Coverage:
QA: 0 of 6 manual tests passed


Reviewed by claude-opus-5-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer test · retest · audit (author or owner)

@jvsena42

Copy link
Copy Markdown
Member

@coreyphillips conflict

@coreyphillips

Copy link
Copy Markdown
Contributor Author

@jvsena42 Resolved in 44562df, which merges current master. The only conflict was in CustomTabRowWithSpacing.kt: master moved the tab label into TabLabel, and this branch added labelOverrides. The resolution keeps master's layout and passes labelOverrides[tab] ?: tab.uiText as the label text. Compile, unit tests and detekt pass.

ovi-reviewer[bot]

This comment was marked as resolved.

ovi-reviewer[bot]

This comment was marked as resolved.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta since my last pass (bf77458, 7bc62d9, 7e81da2): one LOW observation inline, which I traced statically and haven't verified on hardware.

Checked:

  • Earlier threads. ovi-reviewer's three threads are answered. rejectUnusableDevice now fails closed on a null efuse MAC while tolerating legacy blank ids, loadKnownDevices uses runSuspendCatching, and the BLE prefix is shared. My three earlier LOWs hold at head: engagedWalletId gating, the onTransportRestored early return, and canLeave.
  • disconnectNoticeMutex. No deadlock: the notice path waits only on core lifecycle, and connect takes the mutex before lifecycle. The leftover race window needs a same-path reconnect within milliseconds of a GATT drop, and retryAutoReconnect waits at least 2s.
  • bitkit-core 0.5.14 → 0.5.17. Adds the Blockstream vendor, catalog entries and the finalize_psbt sighash-type check (strictly safer, and Trezor doesn't use it). The backup migration change is rustfmt in tests only, and jade:<hash> ids survive it. No storage changes. The pinserver host is pinned to jadepin.blockstream.com.
  • Sign path. Identity check → check_signable (ALL/DEFAULT sighash only, fingerprint must match) → verify_signed_psbt → core finalize_psbt with interpreter_check. A swapped or re-seeded Jade either fails the wallet-id check or produces signatures that fail interpretation. Change is derived app-side, and a retry reuses the cached signedTx.
  • Upgrade from Trezor-only state. vendor defaults to TREZOR, ignoreUnknownKeys is set, and vendor-scoped saves keep the other vendor's rows.
  • Cancel/Back. Leaving Send during the PIN wait cancels and closes the link before core, NonCancellable. Cancelling the connect sheet resets state. The Receive sheet only disconnects when a device was engaged.
  • Gating. Not gated: Settings, the Home suggestion and USB attach are all reachable in release.
  • iOS twin, synonymdev/bitkit-ios#765. Reviewed clean. The USB case below doesn't apply there.

JadeTransportKind.BLUETOOTH -> path == device.path || advertisesAs(device.name)
// A plugged-in Jade cannot be told from a paired one before connecting, so a paired USB Jade
// claims every serial device; a second one is added through the Add button, which offers it anyway.
JadeTransportKind.SERIAL -> transportType == TransportType.USB

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOW (observation, static trace only): a second USB Jade looks unpairable while any USB Jade is paired, which contradicts this comment.

The SERIAL branch claims every plugged serial device for the paired USB entry, so scan() drops it from nearbyDevices (:196). Connect Hardware then falls back to hasKnownDevice(it.id, advertisedName = it.name) (HwConnectViewModel.kt:333-335). For a USB Jade, id is a path the device was never connected at and name is the USB product string. That string never ends with the efuse suffix, so matches and advertisesAs are both false. The OS attach route is closed as well: hasKnownUsbDevice returns true whenever any USB Jade is paired (JadeRepo.kt:476, AppViewModel.kt:5390), and the restore reconnect then rejects the second device with JadeIdentityMismatchError.

Scenario: Jade A is paired over USB. Plug in Jade B, then Settings → Hardware Wallets → Add → Continue: Searching keeps spinning. Accepting the Android USB chooser shows nothing. The workarounds are pairing B over Bluetooth or removing A first. No funds are involved.

Fix: in the SERIAL branch, treat a serial device as known only when a stored entry matches its path (or serial number when available), and let hasKnownUsbDevice require an exact matches(deviceId). If you've confirmed Add does offer B with two devices, disregard this and fix the trace in the comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, the trace holds and the comment was wrong. Fixed in 187fd19.

Add flow. isSameDevice now lets a paired USB entry claim a serial device only at the path it was last connected at, so Jade B is offered in Settings → Hardware Wallets → Add. A replugged Jade A at a new path is offered too, and pairing it again refreshes its existing entry rather than adding one (a replugged jade refreshes its stored entry instead of adding one). New tests in JadeRepoTest.kt: scan offers a usb jade plugged in away from the paired entry's path, scan does not offer a usb jade at the paired entry's path and pairing a second usb jade keeps the paired one.

Attach route. I kept hasKnownUsbDevice lenient on purpose. Before connecting there is no way to tell A from B, and A's path is renumbered on every plug, so an exact matches(deviceId) would open the Found sheet for the paired Jade each time it is plugged in through the chooser (over Home or Receive, since only Send and a few other sheets block it). So with A paired, attaching B still opens nothing, and B is paired from Add. The KDoc now says this, and a usb attach at a new path counts as known while a usb jade is paired pins it. Happy to switch if you'd rather take the replug prompt.

One related limit I left alone: with two USB Jades paired, the silent restore after an attach only tries the first USB entry, so the other one connects on first use instead. Send and Receive verification go through ensureConnected, which picks the right entry, so nothing is blocked.

ovi-reviewer[bot]

This comment was marked as resolved.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ✅ Approve


Reaudit: diff 2 files.
Counterpart synonymdev/bitkit-ios#765: not compared.

Findings:
N/A

Audit:
Already done in comment.

QA: emu-1 Pixel 10 Pro emulator (sdk_gphone64_arm64) on Android 16, to.bitkit.dev devDebug built and installed from the QA clone at this head

  1. ⚠️ hardware absent: No Blockstream Jade v1 or Jade Plus over USB; Connect Device stayed at Searching for Devices while the exact-head app logged zero USB and BLE Jade devices.

    Test 1:
    Jade over USB → Connect Hardware → Search → Pair: PIN prompt shows on the device,
    1.mp4
  2. ⚠️ hardware absent: No Blockstream Jade v1 or Jade Plus over USB; Send opened with no paired hardware source, so physical signing and broadcast could not run.

    Test 2:
    `regression:` USB → Send → pick the Jade source → sign on device → broadcast:
    2.mp4
  3. ⚠️ hardware absent: No Blockstream Jade v1 or Jade Plus advertising over Bluetooth; the exact-head BLE scan completed with zero Jade devices.

    Test 3:
    Jade over Bluetooth → Connect Hardware → Search → Pair: pairs and unlocks.
    3.mp4
  4. ⚠️ hardware absent: No Blockstream Jade v1 or Jade Plus over Bluetooth; Receive showed Savings and Spending only, with no Hardware tab for on-device address verification.

    Test 4:
    Bluetooth → Receive → Hardware tab → Verify on Device: the address shown on the Jade
    4.mp4
  5. ⚠️ hardware absent: No Blockstream Jade v1 or Jade Plus over Bluetooth; Send opened with no paired Jade source, so physical signing and broadcast could not run.

    Test 5:
    Bluetooth → Send → sign on device → broadcast: broadcast succeeded, txid
    5.mp4
  6. ⚠️ hardware absent: No Blockstream Jade v1 or Jade Plus holding a Bluetooth session; after 46 seconds backgrounded the app reopened without a signer session to reconnect.

    Test 6:
    Bluetooth connected → background Bitkit for about 45 seconds → reopen: reconnects with
    6.mp4

Note

Every item requires a physical Blockstream Jade. The lane has no Jade on USB or Bluetooth, and the repo toolbox has no Jade emulator. The exact-head binary, discovery scans, unpaired Send and Receive paths, and 46-second background/reopen path were exercised; the signer-dependent assertions remain hardware-absent rather than incomplete.

Coverage:
Unit tests: 100% - JadeRepoTest.kt covers both scan outcomes, broad USB attach recognition, and retaining two efuse identities.
QA: 0 of 6 manual tests passed


Reviewed by gpt-5.6-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest · audit (author or owner)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants