Skip to content

Commit e511a0d

Browse files
authored
Merge pull request #574 from synonymdev/feat/trezor-hidden-wallet-and-watcher
feat(trezor): add hidden-wallet passphrase selection and on-chain watcher
2 parents 85d5063 + e1a50ba commit e511a0d

17 files changed

Lines changed: 1625 additions & 146 deletions

Bitkit.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1201,7 +1201,7 @@
12011201
repositoryURL = "https://github.com/synonymdev/bitkit-core";
12021202
requirement = {
12031203
kind = exactVersion;
1204-
version = 0.1.64;
1204+
version = 0.1.66;
12051205
};
12061206
};
12071207
96E20CD22CB6D91A00C24149 /* XCRemoteSwiftPackageReference "CodeScanner" */ = {

Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import SwiftUI
2+
3+
struct TrezorAccountTypeSelector: View {
4+
@Binding var selection: TrezorAccountTypeSelection
5+
var title: String = "Account Type"
6+
7+
var body: some View {
8+
VStack(alignment: .leading, spacing: 8) {
9+
CaptionMText(title)
10+
11+
SegmentedControl(selectedTab: $selection, tabs: TrezorAccountTypeSelection.allCases)
12+
13+
FootnoteText(selection.subtitle)
14+
}
15+
.frame(maxWidth: .infinity, alignment: .leading)
16+
.accessibilityIdentifier("TrezorAccountTypeSelector")
17+
}
18+
}

Bitkit/Components/Trezor/TrezorPinPad.swift

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ struct TrezorPinPad: View {
66
/// Current PIN being entered
77
@Binding var pin: String
88

9-
/// Maximum PIN length
10-
var maxLength: Int = 9
9+
/// Maximum PIN length. Trezor PINs can be up to 50 digits.
10+
var maxLength: Int = 50
11+
12+
/// Number of entered-digit dots to render per row before wrapping.
13+
private let dotsPerRow = 9
1114

1215
/// PIN pad layout (positions map to device keypad)
1316
/// The Trezor shows scrambled numbers, we show only position dots
@@ -19,12 +22,30 @@ struct TrezorPinPad: View {
1922

2023
var body: some View {
2124
VStack(spacing: 16) {
22-
// PIN display
23-
HStack(spacing: 12) {
24-
ForEach(0 ..< maxLength, id: \.self) { index in
25-
Circle()
26-
.fill(index < pin.count ? Color.white : Color.white.opacity(0.3))
27-
.frame(width: 12, height: 12)
25+
// PIN display — one dot per entered digit, wrapping across rows so long
26+
// PINs (Trezor allows up to 50 digits) don't overflow a single line.
27+
VStack(spacing: 8) {
28+
if pin.isEmpty {
29+
// Placeholder row so the layout doesn't collapse before entry.
30+
HStack(spacing: 12) {
31+
ForEach(0 ..< dotsPerRow, id: \.self) { _ in
32+
Circle()
33+
.fill(Color.white.opacity(0.3))
34+
.frame(width: 12, height: 12)
35+
}
36+
}
37+
} else {
38+
let rowCount = (pin.count + dotsPerRow - 1) / dotsPerRow
39+
ForEach(0 ..< rowCount, id: \.self) { row in
40+
let dotsInRow = min(dotsPerRow, pin.count - row * dotsPerRow)
41+
HStack(spacing: 12) {
42+
ForEach(0 ..< dotsInRow, id: \.self) { _ in
43+
Circle()
44+
.fill(Color.white)
45+
.frame(width: 12, height: 12)
46+
}
47+
}
48+
}
2849
}
2950
}
3051
.padding(.bottom, 24)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import BitkitCore
2+
import Foundation
3+
4+
/// Bridges bitkit-core's `EventListener` callback (invoked on a background thread by the
5+
/// Rust watcher loop) onto the main actor so the ViewModel can update `@Observable` state.
6+
///
7+
/// Mirrors bitkit-android's `eventBridge` in `TrezorRepo`.
8+
final class TrezorEventListener: EventListener, @unchecked Sendable {
9+
/// Forwards `(watcherId, event)` to a consumer on the main actor.
10+
private let onEventHandler: @MainActor (String, WatcherEvent) -> Void
11+
12+
init(onEvent: @escaping @MainActor (String, WatcherEvent) -> Void) {
13+
onEventHandler = onEvent
14+
}
15+
16+
func onEvent(watcherId: String, event: WatcherEvent) {
17+
let handler = onEventHandler
18+
Task { @MainActor in
19+
TrezorDebugLog.shared.log("[WATCHER] [\(watcherId)] \(event.logLabel)")
20+
handler(watcherId, event)
21+
}
22+
}
23+
}
24+
25+
extension WatcherEvent {
26+
/// Short label for the debug log.
27+
var logLabel: String {
28+
switch self {
29+
case .transactionsChanged:
30+
return "transactionsChanged"
31+
case let .error(message):
32+
return "error: \(message)"
33+
case let .disconnected(message):
34+
return "disconnected: \(message)"
35+
case .reconnected:
36+
return "reconnected"
37+
}
38+
}
39+
}

Bitkit/Services/Trezor/TrezorService.swift

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import BitkitCore
22
import Foundation
33

4+
/// Watcher-related service calls, extracted as a protocol so unit tests can
5+
/// substitute a mock (mirrors bitkit-android's mocked TrezorRepo in TrezorViewModelTest).
6+
protocol TrezorWatcherServicing {
7+
func startWatcher(params: WatcherParams, listener: EventListener) async throws
8+
func stopWatcher(watcherId: String) throws
9+
func stopAllWatchers()
10+
}
11+
12+
extension TrezorService: TrezorWatcherServicing {}
13+
414
/// Service layer wrapper for Trezor FFI functions
515
/// All operations run on ServiceQueue.background(.core) to ensure thread safety
616
class TrezorService {
@@ -65,13 +75,17 @@ class TrezorService {
6575

6676
// MARK: - Connection Management
6777

68-
/// Connect to a Trezor device by its ID
69-
/// - Parameter deviceId: The device identifier (path)
78+
/// Connect to a Trezor device by its ID, opening the wallet given by `selection`.
79+
/// On THP devices (Safe 5/7) the passphrase is bound to the session at creation, so
80+
/// it is supplied per-connect rather than cached between calls.
81+
/// - Parameters:
82+
/// - deviceId: The device identifier (path)
83+
/// - selection: Which wallet to open (standard / hidden / on-device passphrase)
7084
/// - Returns: Device features after successful connection
71-
func connect(deviceId: String) async throws -> TrezorFeatures {
85+
func connect(deviceId: String, selection: WalletSelection) async throws -> TrezorFeatures {
7286
try await ServiceQueue.background(.core) { [self] in
7387
ensureCallbacksRegistered()
74-
return try await trezorConnect(deviceId: deviceId, selection: .standard)
88+
return try await trezorConnect(deviceId: deviceId, selection: selection)
7589
}
7690
}
7791

@@ -268,6 +282,27 @@ class TrezorService {
268282
}
269283
}
270284

285+
// MARK: - Event Watcher (No Device Required)
286+
287+
/// Start watching an extended public key for on-chain transaction activity.
288+
/// Events are delivered to `listener` until the watcher is stopped.
289+
/// Does NOT require a connected Trezor device — it subscribes to Electrum directly.
290+
func startWatcher(params: WatcherParams, listener: EventListener) async throws {
291+
try await ServiceQueue.background(.core) {
292+
try await onchainStartWatcher(params: params, listener: listener)
293+
}
294+
}
295+
296+
/// Stop a specific watcher by its id.
297+
func stopWatcher(watcherId: String) throws {
298+
try onchainStopWatcher(watcherId: watcherId)
299+
}
300+
301+
/// Stop all active watchers.
302+
func stopAllWatchers() {
303+
onchainStopAllWatchers()
304+
}
305+
271306
// MARK: - Helpers
272307

273308
/// Convert TrezorCoinType to the Network enum used by onchain FFI functions

0 commit comments

Comments
 (0)