Skip to content

Feature BIP44(2/2) #734

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Sources/Web3Core/KeystoreManager/BIP44.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import Foundation

public protocol BIP44 {
/**
Derive an ``HDNode`` based on the provided path. The function will throw ``BIP44Error.warning`` if it was invoked with throwOnWarning 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 throwOnWarning is `true`, so it can throw
Derive an ``HDNode`` based on the provided path. The function will throw ``BIP44Error.warning`` if it was invoked with `throwOnWarning` equal to
`true` and the root key doesn't have a previous child with at least one transaction. If it is invoked with `throwOnWarning` 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 `throwOnWarning` is `true`, so it can throw
network errors.
- Parameter path: valid BIP44 path.
- Parameter throwOnWarning: `true` to use
Expand Down Expand Up @@ -41,7 +41,7 @@ public protocol TransactionChecker {
- Throws: any error related to query the blockchain provider
- Returns: `true` if the address has at least one transaction, `false` otherwise
*/
func hasTransactions(address: String) async throws -> Bool
func hasTransactions(ethereumAddress: EthereumAddress) async throws -> Bool
}

extension HDNode: BIP44 {
Expand All @@ -62,7 +62,7 @@ extension HDNode: BIP44 {
if let searchPath = path.newPath(account: searchAccount, addressIndex: searchAddressIndex),
let childNode = derive(path: searchPath, derivePrivateKey: true),
let ethAddress = Utilities.publicToAddress(childNode.publicKey) {
hasTransactions = try await transactionChecker.hasTransactions(address: ethAddress.address)
hasTransactions = try await transactionChecker.hasTransactions(ethereumAddress: ethAddress)
if hasTransactions {
break
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Foundation
public struct EtherscanTransactionChecker: TransactionChecker {
private let urlSession: URLSessionProxy
private let apiKey: String
private let successRange = 200..<300

public init(urlSession: URLSession, apiKey: String) {
self.urlSession = URLSessionProxyImplementation(urlSession: urlSession)
Expand All @@ -19,13 +20,16 @@ public struct EtherscanTransactionChecker: TransactionChecker {
self.apiKey = apiKey
}

public func hasTransactions(address: String) async throws -> Bool {
let urlString = "https://api.etherscan.io/api?module=account&action=txlist&address=\(address)&startblock=0&page=1&offset=1&sort=asc&apikey=\(apiKey)"
public func hasTransactions(ethereumAddress: EthereumAddress) async throws -> Bool {
let urlString = "https://api.etherscan.io/api?module=account&action=txlist&address=\(ethereumAddress.address)&startblock=0&page=1&offset=1&sort=asc&apikey=\(apiKey)"
guard let url = URL(string: urlString) else {
throw EtherscanTransactionCheckerError.invalidUrl(url: urlString)
}
let request = URLRequest(url: url)
let result = try await urlSession.data(for: request)
if let httpResponse = result.1 as? HTTPURLResponse, !successRange.contains(httpResponse.statusCode) {
throw EtherscanTransactionCheckerError.network(statusCode: httpResponse.statusCode)
}
let response = try JSONDecoder().decode(Response.self, from: result.0)
return !response.result.isEmpty
}
Expand All @@ -40,11 +44,14 @@ extension EtherscanTransactionChecker {

public enum EtherscanTransactionCheckerError: LocalizedError, Equatable {
case invalidUrl(url: String)
case network(statusCode: Int)

public var errorDescription: String? {
switch self {
case let .invalidUrl(url):
return "Couldn't create URL(string: \(url))"
case let .network(statusCode):
return "Network error, statusCode: \(statusCode)"
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/web3swiftTests/localTests/BIP44Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ private final class MockTransactionChecker: TransactionChecker {
var addresses: [String] = .init()
var results: [Bool] = .init()

func hasTransactions(address: String) async throws -> Bool {
addresses.append(address)
func hasTransactions(ethereumAddress: EthereumAddress) async throws -> Bool {
addresses.append(ethereumAddress.address)
return results.removeFirst()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@ import XCTest
final class EtherscanTransactionCheckerTests: XCTestCase {
private var testApiKey: String { "4HVPVMV1PN6NGZDFXZIYKEZRP53IA41KVC" }
private var vitaliksAddress: String { "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B" }
private var emptyAddress: String { "0x1BeY3KhtHpfATH5Yqxz9d8Z1XbqZFSXtK7" }
private var emptyAddress: String { "0x3a0cd085155dc74cdddf3196f23c8cec9b217dd8" }

func testHasTransactions() async throws {
let sut = EtherscanTransactionChecker(urlSession: URLSession.shared, apiKey: testApiKey)

let result = try await sut.hasTransactions(address: vitaliksAddress)
let result = try await sut.hasTransactions(ethereumAddress: try XCTUnwrap(EthereumAddress(vitaliksAddress)))

XCTAssertTrue(result)
}

func testHasNotTransactions() async throws {
let sut = EtherscanTransactionChecker(urlSession: URLSession.shared, apiKey: testApiKey)

let result = try await sut.hasTransactions(address: emptyAddress)
let ethAddr = try XCTUnwrap(EthereumAddress(emptyAddress))
let result = try await sut.hasTransactions(ethereumAddress: ethAddr)

XCTAssertFalse(result)
}
Expand All @@ -33,31 +34,31 @@ final class EtherscanTransactionCheckerTests: XCTestCase {
urlSessionMock.response = (Data(), try XCTUnwrap(HTTPURLResponse(url: try XCTUnwrap(URL(string: "https://")), statusCode: 500, httpVersion: nil, headerFields: nil)))
let sut = EtherscanTransactionChecker(urlSession: urlSessionMock, apiKey: testApiKey)

_ = try await sut.hasTransactions(address: vitaliksAddress)
_ = try await sut.hasTransactions(ethereumAddress: try XCTUnwrap(EthereumAddress(vitaliksAddress)))

XCTFail("Network must throw an error")
} catch {
XCTAssertTrue(true)
} catch let EtherscanTransactionCheckerError.network(statusCode) {
XCTAssertEqual(statusCode, 500)
}
}

func testInitURLError() async throws {
do {
let sut = EtherscanTransactionChecker(urlSession: URLSessionMock(), apiKey: testApiKey)
let sut = EtherscanTransactionChecker(urlSession: URLSessionMock(), apiKey: " ")

_ = try await sut.hasTransactions(address: " ")
_ = try await sut.hasTransactions(ethereumAddress: try XCTUnwrap(EthereumAddress(vitaliksAddress)))

XCTFail("URL init must throw an error")
} catch {
XCTAssertTrue(error is EtherscanTransactionCheckerError)
} catch EtherscanTransactionCheckerError.invalidUrl {
XCTAssertTrue(true)
}
}

func testWrongApiKey() async throws {
do {
let sut = EtherscanTransactionChecker(urlSession: URLSession.shared, apiKey: "")
let sut = EtherscanTransactionChecker(urlSession: URLSession.shared, apiKey: "-")

_ = try await sut.hasTransactions(address: "")
_ = try await sut.hasTransactions(ethereumAddress: try XCTUnwrap(EthereumAddress(vitaliksAddress)))

XCTFail("API not returns a valid response")
} catch DecodingError.typeMismatch {
Expand Down