Skip to content

Commit c01f6a1

Browse files
fix: ParsingError and AbstractKeystoreError are now extensions of LocalizedError type;
- updated ParsingError and AbstractKeystoreError with description; - minor refactoring; - added 2 more EIP712 tests;
1 parent b8e55b2 commit c01f6a1

File tree

9 files changed

+163
-108
lines changed

9 files changed

+163
-108
lines changed

Sources/Web3Core/EthereumABI/ABIParsing.swift

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,41 @@ import Foundation
77

88
extension ABI {
99

10-
public enum ParsingError: Swift.Error {
10+
public enum ParsingError: LocalizedError {
1111
case invalidJsonFile
12-
case elementTypeInvalid
12+
case elementTypeInvalid(_ desc: String? = nil)
1313
case elementNameInvalid
1414
case functionInputInvalid
1515
case functionOutputInvalid
1616
case eventInputInvalid
1717
case parameterTypeInvalid
1818
case parameterTypeNotFound
1919
case abiInvalid
20+
21+
public var errorDescription: String? {
22+
var errorMessage: [String?]
23+
switch self {
24+
case .invalidJsonFile:
25+
errorMessage = ["invalidJsonFile"]
26+
case .elementTypeInvalid(let desc):
27+
errorMessage = ["elementTypeInvalid", desc]
28+
case .elementNameInvalid:
29+
errorMessage = ["elementNameInvalid"]
30+
case .functionInputInvalid:
31+
errorMessage = ["functionInputInvalid"]
32+
case .functionOutputInvalid:
33+
errorMessage = ["functionOutputInvalid"]
34+
case .eventInputInvalid:
35+
errorMessage = ["eventInputInvalid"]
36+
case .parameterTypeInvalid:
37+
errorMessage = ["parameterTypeInvalid"]
38+
case .parameterTypeNotFound:
39+
errorMessage = ["parameterTypeNotFound"]
40+
case .abiInvalid:
41+
errorMessage = ["abiInvalid"]
42+
}
43+
return errorMessage.compactMap { $0 }.joined(separator: " ")
44+
}
2045
}
2146

2247
enum TypeParsingExpressions {
@@ -39,7 +64,7 @@ extension ABI.Record {
3964
public func parse() throws -> ABI.Element {
4065
let typeString = self.type ?? "function"
4166
guard let type = ABI.ElementType(rawValue: typeString) else {
42-
throw ABI.ParsingError.elementTypeInvalid
67+
throw ABI.ParsingError.elementTypeInvalid("Invalid ABI type \(typeString).")
4368
}
4469
return try parseToElement(from: self, type: type)
4570
}

Sources/Web3Core/EthereumABI/ABITypeParser.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public struct ABITypeParser {
4646

4747
public static func parseTypeString(_ string: String) throws -> ABI.Element.ParameterType {
4848
let (type, tail) = recursiveParseType(string)
49-
guard let t = type, tail == nil else {throw ABI.ParsingError.elementTypeInvalid}
49+
guard let t = type, tail == nil else { throw ABI.ParsingError.elementTypeInvalid("Invalid ABI type \(string).") }
5050
return t
5151
}
5252

Sources/Web3Core/KeystoreManager/AbstractKeystore.swift

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,30 @@ public protocol AbstractKeystore {
1111
func UNSAFE_getPrivateKeyData(password: String, account: EthereumAddress) throws -> Data
1212
}
1313

14-
public enum AbstractKeystoreError: Error {
15-
case noEntropyError
16-
case keyDerivationError
17-
case aesError
18-
case invalidAccountError
14+
public enum AbstractKeystoreError: LocalizedError {
15+
case noEntropyError(_ additionalDescription: String? = nil)
16+
case keyDerivationError(_ additionalDescription: String? = nil)
17+
case aesError(_ additionalDescription: String? = nil)
18+
case invalidAccountError(_ additionalDescription: String? = nil)
1919
case invalidPasswordError
20-
case encryptionError(String)
20+
case encryptionError(_ additionalDescription: String? = nil)
21+
22+
public var errorDescription: String? {
23+
var errorMessage: [String?]
24+
switch self {
25+
case .noEntropyError(let additionalDescription):
26+
errorMessage = ["Entropy error (e.g. failed to generate a random array of bytes).", additionalDescription]
27+
case .keyDerivationError(let additionalDescription):
28+
errorMessage = ["Key derivation error.", additionalDescription]
29+
case .aesError(let additionalDescription):
30+
errorMessage = ["AES error.", additionalDescription]
31+
case .invalidAccountError(let additionalDescription):
32+
errorMessage = ["Invalid account error.", additionalDescription]
33+
case .invalidPasswordError:
34+
errorMessage = ["Invalid password error."]
35+
case .encryptionError(let additionalDescription):
36+
errorMessage = ["Encryption error.", additionalDescription]
37+
}
38+
return errorMessage.compactMap { $0 }.joined(separator: " ")
39+
}
2140
}

Sources/Web3Core/KeystoreManager/BIP32Keystore.swift

Lines changed: 57 additions & 58 deletions
Large diffs are not rendered by default.

Sources/Web3Core/KeystoreManager/BIP39.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,13 @@ public class BIP39 {
9595
}
9696

9797
private static func entropyOf(size: Int) throws -> Data {
98+
let isCorrectSize = size >= 128 && size <= 256 && size.isMultiple(of: 32)
99+
let randomBytesCount = size / 8
98100
guard
99-
size >= 128 && size <= 256 && size.isMultiple(of: 32),
100-
let entropy = Data.randomBytes(length: size/8)
101+
isCorrectSize,
102+
let entropy = Data.randomBytes(length: randomBytesCount)
101103
else {
102-
throw AbstractKeystoreError.noEntropyError
104+
throw AbstractKeystoreError.noEntropyError("BIP39. \(!isCorrectSize ? "Requested entropy of wrong bits size \(size)." : "Failed to generated \(randomBytesCount) of random bytes.")")
103105
}
104106
return entropy
105107
}

Sources/Web3Core/KeystoreManager/EthereumKeystoreV3.swift

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ public class EthereumKeystoreV3: AbstractKeystore {
2323
}
2424

2525
public func UNSAFE_getPrivateKeyData(password: String, account: EthereumAddress) throws -> Data {
26-
if self.addresses?.count == 1 && account == self.addresses?.last {
27-
guard let privateKey = try? self.getKeyData(password) else {
26+
if account == addresses?.last {
27+
guard let privateKey = try? getKeyData(password) else {
2828
throw AbstractKeystoreError.invalidPasswordError
2929
}
3030
return privateKey
3131
}
32-
throw AbstractKeystoreError.invalidAccountError
32+
throw AbstractKeystoreError.invalidAccountError("EthereumKeystoreV3. Cannot get private key: keystore doesn't contain information about given address \(account.address).")
3333
}
3434

3535
// Class
@@ -77,7 +77,7 @@ public class EthereumKeystoreV3: AbstractKeystore {
7777
defer {
7878
Data.zero(&newPrivateKey)
7979
}
80-
try encryptDataToStorage(password, keyData: newPrivateKey, aesMode: aesMode)
80+
try encryptDataToStorage(password, privateKey: newPrivateKey, aesMode: aesMode)
8181
}
8282

8383
public init?(privateKey: Data, password: String, aesMode: String = "aes-128-cbc") throws {
@@ -87,68 +87,60 @@ public class EthereumKeystoreV3: AbstractKeystore {
8787
guard SECP256K1.verifyPrivateKey(privateKey: privateKey) else {
8888
return nil
8989
}
90-
try encryptDataToStorage(password, keyData: privateKey, aesMode: aesMode)
90+
try encryptDataToStorage(password, privateKey: privateKey, aesMode: aesMode)
9191
}
9292

93-
fileprivate func encryptDataToStorage(_ password: String, keyData: Data?, dkLen: Int = 32, N: Int = 4096, R: Int = 6, P: Int = 1, aesMode: String = "aes-128-cbc") throws {
94-
if keyData == nil {
95-
throw AbstractKeystoreError.encryptionError("Encryption without key data")
93+
fileprivate func encryptDataToStorage(_ password: String, privateKey: Data, dkLen: Int = 32, N: Int = 4096, R: Int = 6, P: Int = 1, aesMode: String = "aes-128-cbc") throws {
94+
if privateKey.count != 32 {
95+
throw AbstractKeystoreError.encryptionError("EthereumKeystoreV3. Attempted encryption with private key of length != 32. Given private key length is \(privateKey.count).")
9696
}
9797
let saltLen = 32
9898
guard let saltData = Data.randomBytes(length: saltLen) else {
99-
throw AbstractKeystoreError.noEntropyError
99+
throw AbstractKeystoreError.noEntropyError("EthereumKeystoreV3. Failed to generate random bytes: `Data.randomBytes(length: \(saltLen))`.")
100100
}
101101
guard let derivedKey = scrypt(password: password, salt: saltData, length: dkLen, N: N, R: R, P: P) else {
102-
throw AbstractKeystoreError.keyDerivationError
102+
throw AbstractKeystoreError.keyDerivationError("EthereumKeystoreV3. Scrypt function failed.")
103103
}
104104
let last16bytes = Data(derivedKey[(derivedKey.count - 16)...(derivedKey.count - 1)])
105105
let encryptionKey = Data(derivedKey[0...15])
106106
guard let IV = Data.randomBytes(length: 16) else {
107-
throw AbstractKeystoreError.noEntropyError
107+
throw AbstractKeystoreError.noEntropyError("EthereumKeystoreV3. Failed to generate random bytes: `Data.randomBytes(length: 16)`.")
108108
}
109-
var aesCipher: AES?
110-
switch aesMode {
109+
var aesCipher: AES
110+
switch aesMode.lowercased() {
111111
case "aes-128-cbc":
112-
aesCipher = try? AES(key: encryptionKey.bytes, blockMode: CBC(iv: IV.bytes), padding: .noPadding)
112+
aesCipher = try AES(key: encryptionKey.bytes, blockMode: CBC(iv: IV.bytes), padding: .noPadding)
113113
case "aes-128-ctr":
114-
aesCipher = try? AES(key: encryptionKey.bytes, blockMode: CTR(iv: IV.bytes), padding: .noPadding)
114+
aesCipher = try AES(key: encryptionKey.bytes, blockMode: CTR(iv: IV.bytes), padding: .noPadding)
115115
default:
116-
aesCipher = nil
116+
throw AbstractKeystoreError.aesError("EthereumKeystoreV3. AES error: given AES mode can be one of 'aes-128-cbc' or 'aes-128-ctr'. Instead '\(aesMode)' was given.")
117117
}
118-
if aesCipher == nil {
119-
throw AbstractKeystoreError.aesError
120-
}
121-
guard let encryptedKey = try aesCipher?.encrypt(keyData!.bytes) else {
122-
throw AbstractKeystoreError.aesError
123-
}
124-
let encryptedKeyData = Data(encryptedKey)
125-
var dataForMAC = Data()
126-
dataForMAC.append(last16bytes)
127-
dataForMAC.append(encryptedKeyData)
118+
119+
let encryptedKeyData = Data(try aesCipher.encrypt(privateKey.bytes))
120+
let dataForMAC = last16bytes + encryptedKeyData
128121
let mac = dataForMAC.sha3(.keccak256)
129122
let kdfparams = KdfParamsV3(salt: saltData.toHexString(), dklen: dkLen, n: N, p: P, r: R, c: nil, prf: nil)
130123
let cipherparams = CipherParamsV3(iv: IV.toHexString())
131124
let crypto = CryptoParamsV3(ciphertext: encryptedKeyData.toHexString(), cipher: aesMode, cipherparams: cipherparams, kdf: "scrypt", kdfparams: kdfparams, mac: mac.toHexString(), version: nil)
132-
guard let pubKey = Utilities.privateToPublic(keyData!) else {
133-
throw AbstractKeystoreError.keyDerivationError
125+
guard let publicKey = Utilities.privateToPublic(privateKey) else {
126+
throw AbstractKeystoreError.keyDerivationError("EthereumKeystoreV3. Failed to derive public key from given private key. `Utilities.privateToPublic(privateKey)` returned `nil`.")
134127
}
135-
guard let addr = Utilities.publicToAddress(pubKey) else {
136-
throw AbstractKeystoreError.keyDerivationError
128+
guard let addr = Utilities.publicToAddress(publicKey) else {
129+
throw AbstractKeystoreError.keyDerivationError("EthereumKeystoreV3. Failed to derive address from derived public key. `Utilities.publicToAddress(publicKey)` returned `nil`.")
137130
}
138131
self.address = addr
139132
let keystoreparams = KeystoreParamsV3(address: addr.address.lowercased(), crypto: crypto, id: UUID().uuidString.lowercased(), version: 3)
140133
self.keystoreParams = keystoreparams
141134
}
142135

143136
public func regenerate(oldPassword: String, newPassword: String, dkLen: Int = 32, N: Int = 4096, R: Int = 6, P: Int = 1) throws {
144-
var keyData = try self.getKeyData(oldPassword)
145-
if keyData == nil {
146-
throw AbstractKeystoreError.encryptionError("Failed to decrypt a keystore")
137+
guard var privateKey = try getKeyData(oldPassword) else {
138+
throw AbstractKeystoreError.encryptionError("EthereumKeystoreV3. Failed to decrypt a keystore")
147139
}
148140
defer {
149-
Data.zero(&keyData!)
141+
Data.zero(&privateKey)
150142
}
151-
try self.encryptDataToStorage(newPassword, keyData: keyData!, aesMode: self.keystoreParams!.crypto.cipher)
143+
try self.encryptDataToStorage(newPassword, privateKey: privateKey, aesMode: self.keystoreParams!.crypto.cipher)
152144
}
153145

154146
fileprivate func getKeyData(_ password: String) throws -> Data? {

Sources/Web3Core/KeystoreManager/KeystoreManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class KeystoreManager: AbstractKeystore {
4343

4444
public func UNSAFE_getPrivateKeyData(password: String, account: EthereumAddress) throws -> Data {
4545
guard let keystore = walletForAddress(account) else {
46-
throw AbstractKeystoreError.invalidAccountError
46+
throw AbstractKeystoreError.invalidAccountError("KeystoreManager: no keystore/wallet found for given address. Address `\(account.address)`.")
4747
}
4848
return try keystore.UNSAFE_getPrivateKeyData(password: password, account: account)
4949
}

Sources/Web3Core/Transaction/CodableTransaction.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public struct CodableTransaction {
156156
let result = self.attemptSignature(privateKey: privateKey, useExtraEntropy: useExtraEntropy)
157157
if result { return }
158158
}
159-
throw AbstractKeystoreError.invalidAccountError
159+
throw AbstractKeystoreError.invalidAccountError("Failed to sign transaction with given private key.")
160160
}
161161

162162
// actual signing algorithm implementation

Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,24 @@ class EIP712TypedDataPayloadTests: XCTestCase {
142142
}
143143
}
144144

145+
func testEIP712ParserWithCustomTypeArrays() throws {
146+
let problematicTypeExample = """
147+
{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"OrderComponents":[{"name":"offerer","type":"address"},{"name":"zone","type":"address"},{"name":"offer","type":"OfferItem[]"},{"name":"consideration","type":"ConsiderationItem[]"},{"name":"orderType","type":"uint8"},{"name":"startTime","type":"uint256"},{"name":"endTime","type":"uint256"},{"name":"zoneHash","type":"bytes32"},{"name":"salt","type":"uint256"},{"name":"conduitKey","type":"bytes32"},{"name":"counter","type":"uint256"}],"OfferItem":[{"name":"itemType","type":"uint8"},{"name":"token","type":"address"},{"name":"identifierOrCriteria","type":"uint256"},{"name":"startAmount","type":"uint256"},{"name":"endAmount","type":"uint256"}],"ConsiderationItem":[{"name":"itemType","type":"uint8"},{"name":"token","type":"address"},{"name":"identifierOrCriteria","type":"uint256"},{"name":"startAmount","type":"uint256"},{"name":"endAmount","type":"uint256"},{"name":"recipient","type":"address"}]},"primaryType":"OrderComponents","domain":{"name":"Seaport","version":"1.5","chainId":"5","verifyingContract":"0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC"},"message":{"offerer":"0xD0727E8a578DE9Dd19BcED635B1aa43576E638bC","offer":[{"itemType":"2","token":"0xE84a7676aAe742770A179dd7431073429a88c7B8","identifierOrCriteria":"44","startAmount":"1","endAmount":"1"}],"consideration":[{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"950000000000000000","endAmount":"950000000000000000","recipient":"0xD0727E8a578DE9Dd19BcED635B1aa43576E638bC"},{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"25000000000000000","endAmount":"25000000000000000","recipient":"0x0000a26b00c1F0DF003000390027140000fAa719"},{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"25000000000000000","endAmount":"25000000000000000","recipient":"0xbDEf201FB5BE36579b6B66971d40A6e162b92B80"}],"startTime":"1698665491","endTime":"1701343891","orderType":"0","zone":"0x004C00500000aD104D7DBd00e3ae0A5C00560C00","zoneHash":"0x0000000000000000000000000000000000000000000000000000000000000000","salt":"24446860302761739304752683030156737591518664810215442929808784621098726351597","conduitKey":"0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000","totalOriginalConsiderationItems":"3","counter":"0"}}
148+
"""
149+
XCTAssertNoThrow(try EIP712Parser.parse(problematicTypeExample))
150+
}
151+
152+
func testEIP712SignHashWithCustomTypeArrays() throws {
153+
let problematicTypeExample = """
154+
{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"OrderComponents":[{"name":"offerer","type":"address"},{"name":"zone","type":"address"},{"name":"offer","type":"OfferItem[]"},{"name":"consideration","type":"ConsiderationItem[]"},{"name":"orderType","type":"uint8"},{"name":"startTime","type":"uint256"},{"name":"endTime","type":"uint256"},{"name":"zoneHash","type":"bytes32"},{"name":"salt","type":"uint256"},{"name":"conduitKey","type":"bytes32"},{"name":"counter","type":"uint256"}],"OfferItem":[{"name":"itemType","type":"uint8"},{"name":"token","type":"address"},{"name":"identifierOrCriteria","type":"uint256"},{"name":"startAmount","type":"uint256"},{"name":"endAmount","type":"uint256"}],"ConsiderationItem":[{"name":"itemType","type":"uint8"},{"name":"token","type":"address"},{"name":"identifierOrCriteria","type":"uint256"},{"name":"startAmount","type":"uint256"},{"name":"endAmount","type":"uint256"},{"name":"recipient","type":"address"}]},"primaryType":"OrderComponents","domain":{"name":"Seaport","version":"1.5","chainId":"5","verifyingContract":"0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC"},"message":{"offerer":"0xD0727E8a578DE9Dd19BcED635B1aa43576E638bC","offer":[{"itemType":"2","token":"0xE84a7676aAe742770A179dd7431073429a88c7B8","identifierOrCriteria":"44","startAmount":"1","endAmount":"1"}],"consideration":[{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"950000000000000000","endAmount":"950000000000000000","recipient":"0xD0727E8a578DE9Dd19BcED635B1aa43576E638bC"},{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"25000000000000000","endAmount":"25000000000000000","recipient":"0x0000a26b00c1F0DF003000390027140000fAa719"},{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"25000000000000000","endAmount":"25000000000000000","recipient":"0xbDEf201FB5BE36579b6B66971d40A6e162b92B80"}],"startTime":"1698665491","endTime":"1701343891","orderType":"0","zone":"0x004C00500000aD104D7DBd00e3ae0A5C00560C00","zoneHash":"0x0000000000000000000000000000000000000000000000000000000000000000","salt":"24446860302761739304752683030156737591518664810215442929808784621098726351597","conduitKey":"0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000","totalOriginalConsiderationItems":"3","counter":"0"}}
155+
"""
156+
let eip712Payload = try EIP712Parser.parse(problematicTypeExample)
157+
XCTAssertEqual(try eip712Payload.encodeType("OrderComponents"), "OrderComponents(address offerer,address zone,OfferItem[] offer,ConsiderationItem[] consideration,uint8 orderType,uint256 startTime,uint256 endTime,bytes32 zoneHash,uint256 salt,bytes32 conduitKey,uint256 counter)OfferItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount)ConsiderationItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount,address recipient)")
158+
XCTAssertEqual(try eip712Payload.encodeType("OfferItem"), "OfferItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount)")
159+
XCTAssertEqual(try eip712Payload.encodeType("ConsiderationItem"), "ConsiderationItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount,address recipient)")
160+
XCTAssertNoThrow(try eip712Payload.signHash())
161+
}
162+
145163
func testEIP712EncodeType() throws {
146164
let parsedEip712TypedData = try EIP712Parser.parse(EIP712TestData.testTypedDataPayload)
147165
try XCTAssertEqual(parsedEip712TypedData.encodeType("EIP712Domain"), "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")

0 commit comments

Comments
 (0)