Skip to content

Commit e0bb4bd

Browse files
committed
Improved Bytewords and BIP-39 entry guidance messages.
1 parent 1370601 commit e0bb4bd

6 files changed

Lines changed: 118 additions & 27 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ _For related Threat Modeling, see the [Seed Tool Manual](https://github.com/Bloc
5656
* Ethereum: The ETH account ID is shown.
5757
* Tezos: The Tezos address is shown.
5858
* When any of the above are shown, a QR code containing the same information is also shown.
59+
* Improved Bytewords and BIP-39 entry guidance messages.
5960

6061
### 1.6 (75)
6162

SeedTool.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.

SeedTool/Extensions/AttributedStringExtensions.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import Foundation
22
import SwiftUI
33

44
extension AttributedString {
5-
init(_ string: String, color: Color, bold: Bool = false, smallStyle: Bool = false) {
5+
init(_ string: String, color: Color, bold: Bool = false, textStyle: UIFont.TextStyle = .body, smallStyle: Bool = false) {
66
var s = AttributedString(string)
77
s.foregroundColor = color
8-
var fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
8+
var fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
99
if bold {
1010
fontDescriptor = fontDescriptor.withSymbolicTraits(.traitBold) ?? fontDescriptor
1111
}
@@ -17,6 +17,14 @@ extension AttributedString {
1717
}
1818
self = s
1919
}
20+
21+
init(error: String) {
22+
self.init(error, color: .red, textStyle: .caption1)
23+
}
24+
25+
init(warning: String) {
26+
self.init(warning, color: .yellowLightSafe, textStyle: .caption1)
27+
}
2028
}
2129

2230
extension Array where Element == AttributedString {

SeedTool/Import/Guidance.swift

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ protocol Guidance: CustomStringConvertible {
9797
static var initialLetters: Int { get }
9898
static var firstAndLastLettersMatch: Bool { get }
9999
var wordGuidances: [WordGuidance] { get }
100+
var summary: AttributedString? { get }
100101
}
101102

102103
extension Guidance {
@@ -111,15 +112,31 @@ extension Guidance {
111112
.joined(separator: " ")
112113
}
113114

114-
var updatedString: String {
115+
var makeUpdatedString: String {
115116
return wordGuidances
116117
.map { $0.bestMatch }
117118
.joined(separator: " ")
118119
}
119120

120-
var guidanceString: AttributedString {
121-
wordGuidances
121+
var makeGuidanceString: AttributedString {
122+
let guidance = wordGuidances
122123
.map { $0.attributedDescription }
123124
.joined(separator: " ")
125+
126+
return [guidance, summary]
127+
.compactMap { $0 }
128+
.joined(separator: "\n")
129+
}
130+
131+
var allValid: Bool {
132+
wordGuidances.allSatisfy { $0.validation == .valid }
133+
}
134+
135+
var anyInvalid: Bool {
136+
wordGuidances.contains { $0.validation == .noMatches }
137+
}
138+
139+
var anyAmbiguous: Bool {
140+
wordGuidances.contains { $0.validation == .multipleMatches }
124141
}
125142
}

SeedTool/Import/ImportBIP39Model.swift

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,34 +28,63 @@ final class ImportBIP39Model: ImportModel {
2828
extension Publisher where Output == String, Failure == Never {
2929
func validateBIP39(seedPublisher: PassthroughSubject<ModelSeed?, Never>, guidancePublisher: PassthroughSubject<AttributedString?, Never>) -> ValidationPublisher {
3030
map { string in
31-
let (updatedString, guidance) = makeGuidance(string)
32-
guidancePublisher.send(guidance)
33-
do {
34-
let seed = try ModelSeed(mnemonic: updatedString)
31+
let guidance = BIP39Guidance(string)
32+
if let guidanceString = guidance.guidanceString {
33+
guidancePublisher.send(guidanceString)
34+
}
35+
switch guidance.result {
36+
case .success(let seed):
3537
seedPublisher.send(seed)
3638
return .valid
37-
} catch {
39+
case .failure(let error):
3840
seedPublisher.send(nil)
3941
return .invalid(error.localizedDescription)
4042
}
4143
}
4244
.dropFirst()
4345
.eraseToAnyPublisher()
4446
}
45-
46-
private func makeGuidance(_ string: String) -> (String, AttributedString) {
47-
let guidance = BIP39Guidance(string)
48-
return (guidance.updatedString, guidance.guidanceString)
49-
}
5047
}
5148

5249
class BIP39Guidance: Guidance {
5350
static let validWords = Wally.bip39AllWords()
5451
static let initialLetters = 4
5552
static var firstAndLastLettersMatch = false
5653
let wordGuidances: [WordGuidance]
54+
private(set) var summary: AttributedString? = nil
55+
private(set) var result: Result<ModelSeed, Error> = .failure(.invalid)
56+
private(set) var guidanceString: AttributedString? = nil
5757

58+
enum Error: LocalizedError {
59+
case invalid
60+
}
61+
5862
init(_ string: String) {
5963
self.wordGuidances = Self.makeWordGuidances(string)
64+
makeSummary()
65+
self.guidanceString = makeGuidanceString
66+
}
67+
68+
func makeSummary() {
69+
guard !wordGuidances.isEmpty else {
70+
return
71+
}
72+
73+
if allValid {
74+
guard [12, 15, 18, 21, 24].contains(wordGuidances.count) else {
75+
summary = AttributedString(warning: "BIP-39 sequences must be of length 12, 15, 18, 21, or 24. Currently: \(wordGuidances.count).")
76+
return
77+
}
78+
79+
do {
80+
result = .success(try ModelSeed(mnemonic: makeUpdatedString))
81+
} catch {
82+
summary = AttributedString(error: "Not a valid BIP-39 word sequence.")
83+
}
84+
} else if anyInvalid {
85+
summary = AttributedString(error: "Some entered words cannot be valid BIP-39 words.")
86+
} else {
87+
summary = AttributedString(warning: "Some entered words might match more than one BIP-39 word.")
88+
}
6089
}
6190
}

SeedTool/Import/ImportByteWordsModel.swift

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,34 +26,70 @@ final class ImportByteWordsModel: ImportModel {
2626
extension Publisher where Output == String, Failure == Never {
2727
func validateByteWords(seedPublisher: PassthroughSubject<ModelSeed?, Never>, guidancePublisher: PassthroughSubject<AttributedString?, Never>) -> ValidationPublisher {
2828
map { string in
29-
let (updatedString, guidance) = makeGuidance(string)
30-
guidancePublisher.send(guidance)
31-
do {
32-
let seed = try ModelSeed(byteWords: updatedString)
29+
let guidance = BytewordsGuidance(string)
30+
if let guidanceString = guidance.guidanceString {
31+
guidancePublisher.send(guidanceString)
32+
}
33+
switch guidance.result {
34+
case .success(let seed):
3335
seedPublisher.send(seed)
3436
return .valid
35-
} catch {
37+
case .failure(let error):
3638
seedPublisher.send(nil)
3739
return .invalid(error.localizedDescription)
3840
}
3941
}
4042
.dropFirst()
4143
.eraseToAnyPublisher()
4244
}
43-
44-
private func makeGuidance(_ string: String) -> (String, AttributedString) {
45-
let guidance = BytewordsGuidance(string)
46-
return (guidance.updatedString, guidance.guidanceString)
47-
}
4845
}
4946

5047
class BytewordsGuidance: Guidance {
5148
static let validWords = Bytewords.allWords
5249
static let initialLetters = 3
5350
static let firstAndLastLettersMatch = true
5451
let wordGuidances: [WordGuidance]
52+
private(set) var summary: AttributedString? = nil
53+
private(set) var result: Result<ModelSeed, Error> = .failure(.invalid)
54+
private(set) var guidanceString: AttributedString? = nil
55+
56+
enum Error: LocalizedError {
57+
case invalid
58+
}
5559

5660
init(_ string: String) {
5761
self.wordGuidances = Self.makeWordGuidances(string)
62+
makeSummary()
63+
self.guidanceString = makeGuidanceString
64+
}
65+
66+
func makeSummary() {
67+
guard !wordGuidances.isEmpty else {
68+
return
69+
}
70+
71+
if allValid {
72+
guard wordGuidances.count >= 20 else {
73+
summary = AttributedString(warning: "You need to enter at least 20 Bytewords. Currently: \(wordGuidances.count).")
74+
return
75+
}
76+
77+
do {
78+
result = .success(try ModelSeed(byteWords: makeUpdatedString))
79+
} catch {
80+
if
81+
let e = error as? BytewordsDecodingError,
82+
e == .invalidChecksum
83+
{
84+
summary = AttributedString(error: "Valid Bytewords, but the checksum doesn’t match.")
85+
} else {
86+
summary = AttributedString(error: "Something's wrong!")
87+
}
88+
}
89+
} else if anyInvalid {
90+
summary = AttributedString(error: "Some entered words cannot be valid Bytewords.")
91+
} else {
92+
summary = AttributedString(warning: "Some entered words might match more than one Byteword.")
93+
}
5894
}
5995
}

0 commit comments

Comments
 (0)