Skip to content

Commit 3250d72

Browse files
committed
improved doc, code, naming and implement LocalizedError
1 parent 7d38122 commit 3250d72

File tree

2 files changed

+59
-46
lines changed

2 files changed

+59
-46
lines changed

Sources/Web3Core/KeystoreManager/BIP44.swift

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,66 +7,72 @@ import Foundation
77

88
public protocol BIP44 {
99
/**
10-
Derive an `HDNode` based on the provided `path`. The function will throws `BIP44Error.warning` if it was invoked with `warns` as true and the root key doesn't have a previous child with at least one transaction, using false the child node will be derived directly not throwing. This function needs to query the blockchain history when `warns`is true, so it can throw network errors.
11-
- Parameter path: valid BIP32 path.
12-
- Parameter warns: true to be warned about following BIP44 standard, false otherwise.
13-
- Throws: `BIP44Error.warning` if the child key shouldn't be used according to BIP44 standard.
14-
- Returns: an HDNode child key for the provided `path` if it can be created, otherwise nil
10+
Derive an ``HDNode`` based on the provided path. The function will throw ``BIP44Error.warning`` if it was invoked with throwOnError equal to`true` and the root key doesn't have a previous child with at least one transaction. If it is invoked with throwOnError equal to `false` the child node will be derived directly using the derive function of ``HDNode``. This function needs to query the blockchain history when throwOnError is `true`, so it can throw network errors.
11+
- Parameter path: valid BIP44 path.
12+
- Parameter throwOnError: `true` to use [Account Discovery](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account-discovery) standard, otherwise it will dervive the key using the derive function of ``HDNode``.
13+
- Throws: ``BIP44Error.warning`` if the child key shouldn't be used according to [Account Discovery](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account-discovery) standard.
14+
- Returns: an ``HDNode`` child key for the provided `path` if it can be created, otherwise `nil`
1515
*/
16-
func derive(path: String, warns: Bool, transactionChecker: TransactionChecker) async throws -> HDNode?
16+
func derive(path: String, throwOnError: Bool, transactionChecker: TransactionChecker) async throws -> HDNode?
1717
}
1818

19-
public enum BIP44Error: Error, Equatable {
19+
public enum BIP44Error: Error, Equatable, LocalizedError {
2020
/// The selected path doesn't fulfill BIP44 standard, you can derive the root key anyway
2121
case warning
22+
23+
public var errorDescription: String? {
24+
switch self {
25+
case .warning:
26+
return "Couldn't derive key as it doesn't have a previous account with at least one transaction"
27+
}
28+
}
2229
}
2330

2431
public protocol TransactionChecker {
2532
/**
26-
It verifies if the provided `address` has at least one transaction
33+
It verifies if the provided address has at least one transaction
2734
- Parameter address: to be queried
2835
- Throws: any error related to query the blockchain provider
29-
- Returns: true if the `address` has at least one transaction, false otherwise
36+
- Returns: `true` if the address has at least one transaction, `false` otherwise
3037
*/
3138
func hasTransactions(address: String) async throws -> Bool
3239
}
3340

3441
extension HDNode: BIP44 {
35-
public func derive(path: String, warns: Bool = true, transactionChecker: TransactionChecker) async throws -> HDNode? {
36-
if warns {
37-
guard let account = path.accountFromPath else {
38-
return nil
39-
}
40-
if account == 0 {
41-
return derive(path: path, derivePrivateKey: true)
42-
} else {
43-
for searchAccount in 0..<account {
44-
let maxUnusedAddressIndexes = 20
45-
var hasTransactions = false
46-
for searchAddressIndex in 0..<maxUnusedAddressIndexes {
47-
if let searchPath = path.newPath(account: searchAccount, addressIndex: searchAddressIndex),
48-
let childNode = derive(path: searchPath, derivePrivateKey: true),
49-
let ethAddress = Utilities.publicToAddress(childNode.publicKey) {
50-
hasTransactions = try await transactionChecker.hasTransactions(address: ethAddress.address)
51-
if hasTransactions {
52-
break
53-
}
42+
public func derive(path: String, throwOnError: Bool = true, transactionChecker: TransactionChecker) async throws -> HDNode? {
43+
guard throwOnError else {
44+
return derive(path: path, derivePrivateKey: true)
45+
}
46+
guard let account = path.accountFromPath else {
47+
return nil
48+
}
49+
if account == 0 {
50+
return derive(path: path, derivePrivateKey: true)
51+
} else {
52+
for searchAccount in 0..<account {
53+
let maxUnusedAddressIndexes = 20
54+
var hasTransactions = false
55+
for searchAddressIndex in 0..<maxUnusedAddressIndexes {
56+
if let searchPath = path.newPath(account: searchAccount, addressIndex: searchAddressIndex),
57+
let childNode = derive(path: searchPath, derivePrivateKey: true),
58+
let ethAddress = Utilities.publicToAddress(childNode.publicKey) {
59+
hasTransactions = try await transactionChecker.hasTransactions(address: ethAddress.address)
60+
if hasTransactions {
61+
break
5462
}
5563
}
56-
if !hasTransactions {
57-
throw BIP44Error.warning
58-
}
5964
}
60-
return derive(path: path, derivePrivateKey: true)
65+
if !hasTransactions {
66+
throw BIP44Error.warning
67+
}
6168
}
62-
} else {
6369
return derive(path: path, derivePrivateKey: true)
6470
}
6571
}
6672
}
6773

6874
extension String {
69-
/// Verifies if current value matches BIP44 path standard
75+
/// Verifies if self matches BIP44 path
7076
var isBip44Path: Bool {
7177
do {
7278
let pattern = "^m/44'/\\d+'/\\d+'/[0-1]/\\d+$"
@@ -78,7 +84,7 @@ extension String {
7884
}
7985
}
8086

81-
/// Returns the account from the path if the string contains a well formed BIP44 path
87+
/// Returns the account from the path if self contains a well formed BIP44 path
8288
var accountFromPath: Int? {
8389
guard isBip44Path else {
8490
return nil
@@ -93,10 +99,10 @@ extension String {
9399
}
94100

95101
/**
96-
Transforms a bip44 path into a new one changing `account` & `index`. The resulting one will have the change value equal to `0` to represent external chain. Format `m/44'/coin_type'/account'/change/address_index`
97-
- Parameter account: the new `account` to use
98-
- Parameter addressIndex: the new `addressIndex` to use
99-
- Returns: a valid bip44 path with the provided `account`, `addressIndex`and external `change` or nil otherwise
102+
Transforms a bip44 path into a new one changing account & index. The resulting one will have the change value equal to `0` to represent the external chain. The format will be `m/44'/coin_type'/account'/change/address_index`
103+
- Parameter account: the new account to use
104+
- Parameter addressIndex: the new addressIndex to use
105+
- Returns: a valid bip44 path with the provided account, addressIndex and external change or `nil` otherwise
100106
*/
101107
func newPath(account: Int, addressIndex: Int) -> String? {
102108
guard isBip44Path else {

Tests/web3swiftTests/localTests/BIP44Tests.swift

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ final class BIP44Tests: XCTestCase {
6060
func testDeriveNoWarn() async throws {
6161
let rootNode = try rootNode()
6262

63-
let childNode = try await rootNode.derive(path: "m/44'/60'/8096'/0/1", warns: false, transactionChecker: mockTransactionChecker)
63+
let childNode = try await rootNode.derive(path: "m/44'/60'/8096'/0/1", throwOnError: false, transactionChecker: mockTransactionChecker)
6464

6565
XCTAssertEqual(try XCTUnwrap(childNode).publicKey.toHexString(), "035785d4918449c87892371c0f9ccf6e4eda40a7fb0f773f1254c064d3bba64026")
6666
XCTAssertEqual(mockTransactionChecker.addresses.count, 0)
@@ -69,7 +69,7 @@ final class BIP44Tests: XCTestCase {
6969
func testDeriveInvalidPath() async throws {
7070
let rootNode = try rootNode()
7171

72-
let childNode = try? await rootNode.derive(path: "", warns: true, transactionChecker: mockTransactionChecker)
72+
let childNode = try? await rootNode.derive(path: "", throwOnError: true, transactionChecker: mockTransactionChecker)
7373

7474
XCTAssertNil(childNode)
7575
XCTAssertEqual(mockTransactionChecker.addresses.count, 0)
@@ -80,7 +80,7 @@ final class BIP44Tests: XCTestCase {
8080
func testAccountZeroCanBeDerivedAlways() async throws {
8181
let rootNode = try rootNode()
8282

83-
let childNode = try await rootNode.derive(path: "m/44'/60'/0'/0/255", warns: true, transactionChecker: mockTransactionChecker)
83+
let childNode = try await rootNode.derive(path: "m/44'/60'/0'/0/255", throwOnError: true, transactionChecker: mockTransactionChecker)
8484

8585
XCTAssertEqual(try XCTUnwrap(childNode).publicKey.toHexString(), "0262fba1af8f149258123265318114066decf50d16c1222a9d657b7de2296c2734")
8686
XCTAssertEqual(mockTransactionChecker.addresses.count, 0)
@@ -94,7 +94,7 @@ final class BIP44Tests: XCTestCase {
9494
results.append(true)
9595
mockTransactionChecker.results = results
9696

97-
_ = try await rootNode.derive(path: path, warns: true, transactionChecker: mockTransactionChecker)
97+
_ = try await rootNode.derive(path: path, throwOnError: true, transactionChecker: mockTransactionChecker)
9898

9999
XCTFail("Child must not be created usign warns true for the path: \(path)")
100100
} catch BIP44Error.warning {
@@ -111,7 +111,7 @@ final class BIP44Tests: XCTestCase {
111111
results.append(true)
112112
mockTransactionChecker.results = results
113113

114-
let childNode = try await rootNode.derive(path: path, warns: true, transactionChecker: mockTransactionChecker)
114+
let childNode = try await rootNode.derive(path: path, throwOnError: true, transactionChecker: mockTransactionChecker)
115115

116116
XCTAssertEqual(try XCTUnwrap(childNode).publicKey.toHexString(), "036cd8f1bad46fa7caf7a80d48528b90db2a3b7a5c9a18d74d61b286e03850abf4")
117117
XCTAssertEqual(mockTransactionChecker.addresses, accountZeroScannedAddresses)
@@ -129,7 +129,7 @@ final class BIP44Tests: XCTestCase {
129129
results.append(contentsOf: false.times(n: 20))
130130
mockTransactionChecker.results = results
131131

132-
_ = try await rootNode.derive(path: path, warns: true, transactionChecker: mockTransactionChecker)
132+
_ = try await rootNode.derive(path: path, throwOnError: true, transactionChecker: mockTransactionChecker)
133133

134134
XCTFail("Child must not be created usign warns true for the path: \(path)")
135135
} catch BIP44Error.warning {
@@ -149,7 +149,7 @@ final class BIP44Tests: XCTestCase {
149149
results.append(true)
150150
mockTransactionChecker.results = results
151151

152-
let childNode = try await rootNode.derive(path: path, warns: true, transactionChecker: mockTransactionChecker)
152+
let childNode = try await rootNode.derive(path: path, throwOnError: true, transactionChecker: mockTransactionChecker)
153153

154154
XCTAssertEqual(try XCTUnwrap(childNode).publicKey.toHexString(), "0282134e44d4c040a4b4c1a780d8302955096cf1d5e738b161c83f0ce1b863c14e")
155155
XCTAssertEqual(mockTransactionChecker.addresses, accountZeroScannedAddresses)
@@ -167,6 +167,13 @@ final class BIP44Tests: XCTestCase {
167167
}
168168
}
169169

170+
class BIP44ErrorTests: XCTestCase {
171+
func testLocalizedDescription() {
172+
let error = BIP44Error.warning
173+
XCTAssertEqual(error.localizedDescription, "Couldn't derive key as it doesn't have a previous account with at least one transaction")
174+
}
175+
}
176+
170177
extension Bool {
171178
func times(n: Int) -> [Bool] {
172179
var array: [Bool] = .init()

0 commit comments

Comments
 (0)