Skip to content

Commit f6c1805

Browse files
chore: merge with develop
2 parents a17a5bb + 50d0d83 commit f6c1805

File tree

16 files changed

+747
-124
lines changed

16 files changed

+747
-124
lines changed

Example/myWeb3Wallet/Podfile

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Uncomment the next line to define a global platform for your project
2-
# platform :ios, '9.0'
2+
platform :ios, '15.0'
33

44
target 'myWeb3Wallet' do
55
# Comment the next line if you don't want to use dynamic frameworks
@@ -19,3 +19,12 @@ pod 'web3swift', :git => 'https://github.com/veerChauhan/web3swift.git', :branch
1919
end
2020

2121
end
22+
23+
# set iOS deployment target for every pod to avoid warnings
24+
post_install do |installer|
25+
installer.pods_project.targets.each do |target|
26+
target.build_configurations.each do |config|
27+
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
28+
end
29+
end
30+
end

Example/myWeb3Wallet/myWeb3Wallet.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,7 @@
754754
isa = XCBuildConfiguration;
755755
baseConfigurationReference = D6018FABA256C0117F85A829 /* Pods-myWeb3Wallet-myWeb3WalletUITests.debug.xcconfig */;
756756
buildSettings = {
757-
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
757+
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)";
758758
CODE_SIGN_STYLE = Automatic;
759759
CURRENT_PROJECT_VERSION = 1;
760760
DEVELOPMENT_TEAM = VPS9LBWQ55;
@@ -778,7 +778,7 @@
778778
isa = XCBuildConfiguration;
779779
baseConfigurationReference = 7574A738941D92CC94F93A9E /* Pods-myWeb3Wallet-myWeb3WalletUITests.release.xcconfig */;
780780
buildSettings = {
781-
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
781+
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)";
782782
CODE_SIGN_STYLE = Automatic;
783783
CURRENT_PROJECT_VERSION = 1;
784784
DEVELOPMENT_TEAM = VPS9LBWQ55;
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//
2+
// BIP44.swift
3+
// Created by Alberto Penas Amor on 15/12/22.
4+
//
5+
6+
import Foundation
7+
8+
public protocol BIP44 {
9+
/**
10+
Derive an ``HDNode`` based on the provided path. The function will throw ``BIP44Error.warning`` if it was invoked with throwOnWarning equal to
11+
`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
12+
derived directly using the derive function of ``HDNode``. This function needs to query the blockchain history when throwOnWarning is `true`, so it can throw
13+
network errors.
14+
- Parameter path: valid BIP44 path.
15+
- Parameter throwOnWarning: `true` to use
16+
[Account Discovery](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account-discovery) standard,
17+
otherwise it will dervive the key using the derive function of ``HDNode``.
18+
- Throws: ``BIP44Error.warning`` if the child key shouldn't be used according to
19+
[Account Discovery](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account-discovery) standard.
20+
- Returns: an ``HDNode`` child key for the provided `path` if it can be created, otherwise `nil`
21+
*/
22+
func derive(path: String, throwOnWarning: Bool, transactionChecker: TransactionChecker) async throws -> HDNode?
23+
}
24+
25+
public enum BIP44Error: LocalizedError, Equatable {
26+
/// The selected path doesn't fulfill BIP44 standard, you can derive the root key anyway
27+
case warning
28+
29+
public var errorDescription: String? {
30+
switch self {
31+
case .warning:
32+
return "Couldn't derive key as it doesn't have a previous account with at least one transaction"
33+
}
34+
}
35+
}
36+
37+
public protocol TransactionChecker {
38+
/**
39+
It verifies if the provided address has at least one transaction
40+
- Parameter address: to be queried
41+
- Throws: any error related to query the blockchain provider
42+
- Returns: `true` if the address has at least one transaction, `false` otherwise
43+
*/
44+
func hasTransactions(address: String) async throws -> Bool
45+
}
46+
47+
extension HDNode: BIP44 {
48+
public func derive(path: String, throwOnWarning: Bool = true, transactionChecker: TransactionChecker) async throws -> HDNode? {
49+
guard throwOnWarning else {
50+
return derive(path: path, derivePrivateKey: true)
51+
}
52+
guard let account = path.accountFromPath else {
53+
return nil
54+
}
55+
if account == 0 {
56+
return derive(path: path, derivePrivateKey: true)
57+
} else {
58+
for searchAccount in 0..<account {
59+
let maxUnusedAddressIndexes = 20
60+
var hasTransactions = false
61+
for searchAddressIndex in 0..<maxUnusedAddressIndexes {
62+
if let searchPath = path.newPath(account: searchAccount, addressIndex: searchAddressIndex),
63+
let childNode = derive(path: searchPath, derivePrivateKey: true),
64+
let ethAddress = Utilities.publicToAddress(childNode.publicKey) {
65+
hasTransactions = try await transactionChecker.hasTransactions(address: ethAddress.address)
66+
if hasTransactions {
67+
break
68+
}
69+
}
70+
}
71+
if !hasTransactions {
72+
throw BIP44Error.warning
73+
}
74+
}
75+
return derive(path: path, derivePrivateKey: true)
76+
}
77+
}
78+
}
79+
80+
extension String {
81+
/// Verifies if self matches BIP44 path
82+
var isBip44Path: Bool {
83+
do {
84+
let pattern = "^m/44'/\\d+'/\\d+'/[0-1]/\\d+$"
85+
let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive])
86+
let matches = regex.numberOfMatches(in: self, range: NSRange(location: 0, length: utf16.count))
87+
return matches == 1
88+
} catch {
89+
return false
90+
}
91+
}
92+
93+
/// Returns the account from the path if self contains a well formed BIP44 path
94+
var accountFromPath: Int? {
95+
guard isBip44Path else {
96+
return nil
97+
}
98+
let components = components(separatedBy: "/")
99+
let accountIndex = 3
100+
let rawAccount = components[accountIndex].trimmingCharacters(in: CharacterSet(charactersIn: "'"))
101+
guard let account = Int(rawAccount) else {
102+
return nil
103+
}
104+
return account
105+
}
106+
107+
/**
108+
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`
109+
- Parameter account: the new account to use
110+
- Parameter addressIndex: the new addressIndex to use
111+
- Returns: a valid bip44 path with the provided account, addressIndex and external change or `nil` otherwise
112+
*/
113+
func newPath(account: Int, addressIndex: Int) -> String? {
114+
guard isBip44Path else {
115+
return nil
116+
}
117+
var components = components(separatedBy: "/")
118+
let accountPosition = 3
119+
components[accountPosition] = "\(account)'"
120+
let changePosition = 4
121+
components[changePosition] = "0"
122+
let addressIndexPosition = 5
123+
components[addressIndexPosition] = "\(addressIndex)"
124+
return components.joined(separator: "/")
125+
}
126+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
// EtherscanTransactionChecker.swift
3+
// Created by albertopeam on 28/12/22.
4+
//
5+
6+
import Foundation
7+
8+
public struct EtherscanTransactionChecker: TransactionChecker {
9+
private let urlSession: URLSessionProxy
10+
private let apiKey: String
11+
12+
public init(urlSession: URLSession, apiKey: String) {
13+
self.urlSession = URLSessionProxyImplementation(urlSession: urlSession)
14+
self.apiKey = apiKey
15+
}
16+
17+
internal init(urlSession: URLSessionProxy, apiKey: String) {
18+
self.urlSession = urlSession
19+
self.apiKey = apiKey
20+
}
21+
22+
public func hasTransactions(address: String) async throws -> Bool {
23+
let urlString = "https://api.etherscan.io/api?module=account&action=txlist&address=\(address)&startblock=0&page=1&offset=1&sort=asc&apikey=\(apiKey)"
24+
guard let url = URL(string: urlString) else {
25+
throw EtherscanTransactionCheckerError.invalidUrl(url: urlString)
26+
}
27+
let request = URLRequest(url: url)
28+
let result = try await urlSession.data(for: request)
29+
let response = try JSONDecoder().decode(Response.self, from: result.0)
30+
return !response.result.isEmpty
31+
}
32+
}
33+
34+
extension EtherscanTransactionChecker {
35+
struct Response: Codable {
36+
let result: [Transaction]
37+
}
38+
struct Transaction: Codable {}
39+
}
40+
41+
public enum EtherscanTransactionCheckerError: LocalizedError, Equatable {
42+
case invalidUrl(url: String)
43+
44+
public var errorDescription: String? {
45+
switch self {
46+
case let .invalidUrl(url):
47+
return "Couldn't create URL(string: \(url))"
48+
}
49+
}
50+
}
51+
52+
internal protocol URLSessionProxy {
53+
func data(for request: URLRequest) async throws -> (Data, URLResponse)
54+
}
55+
56+
internal struct URLSessionProxyImplementation: URLSessionProxy {
57+
let urlSession: URLSession
58+
59+
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
60+
try await urlSession.data(for: request)
61+
}
62+
}

Sources/Web3Core/Structure/Transaction/TransactionReceipt.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ public struct TransactionReceipt {
1616
public var contractAddress: EthereumAddress?
1717
public var cumulativeGasUsed: BigUInt
1818
public var gasUsed: BigUInt
19+
public var effectiveGasPrice: BigUInt
1920
public var logs: [EventLog]
2021
public var status: TXStatus
2122
public var logsBloom: EthereumBloomFilter?
2223

2324
static func notProcessed(transactionHash: Data) -> TransactionReceipt {
24-
TransactionReceipt(transactionHash: transactionHash, blockHash: Data(), blockNumber: BigUInt(0), transactionIndex: BigUInt(0), contractAddress: nil, cumulativeGasUsed: BigUInt(0), gasUsed: BigUInt(0), logs: [EventLog](), status: .notYetProcessed, logsBloom: nil)
25+
TransactionReceipt(transactionHash: transactionHash, blockHash: Data(), blockNumber: 0, transactionIndex: 0, contractAddress: nil, cumulativeGasUsed: 0, gasUsed: 0, effectiveGasPrice: 0, logs: [], status: .notYetProcessed, logsBloom: nil)
2526
}
2627
}
2728

@@ -45,6 +46,7 @@ extension TransactionReceipt: Decodable {
4546
case logs
4647
case logsBloom
4748
case status
49+
case effectiveGasPrice
4850
}
4951

5052
public init(from decoder: Decoder) throws {
@@ -64,6 +66,8 @@ extension TransactionReceipt: Decodable {
6466

6567
self.gasUsed = try container.decodeHex(BigUInt.self, forKey: .gasUsed)
6668

69+
self.effectiveGasPrice = (try? container.decodeHex(BigUInt.self, forKey: .effectiveGasPrice)) ?? 0
70+
6771
let status = try? container.decodeHex(BigUInt.self, forKey: .status)
6872
switch status {
6973
case nil: self.status = .notYetProcessed
@@ -72,6 +76,10 @@ extension TransactionReceipt: Decodable {
7276
}
7377

7478
self.logs = try container.decode([EventLog].self, forKey: .logs)
79+
80+
if let hexBytes = try? container.decodeHex(Data.self, forKey: .logsBloom) {
81+
self.logsBloom = EthereumBloomFilter(hexBytes)
82+
}
7583
}
7684
}
7785

Sources/web3swift/Utils/ENS/ENS.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,7 @@ public class ENS {
7474
self.reverseRegistrar = reverseRegistrar
7575
}
7676

77-
// FIXME: Rewrite this to CodableTransaction
78-
lazy var defaultOptions: CodableTransaction = {
77+
lazy var defaultTransaction: CodableTransaction = {
7978
return CodableTransaction.emptyTransaction
8079
}()
8180

@@ -107,7 +106,7 @@ public class ENS {
107106
guard isAddrSupports else {
108107
throw Web3Error.processingError(desc: "Address isn't supported")
109108
}
110-
var options = options ?? defaultOptions
109+
var options = options ?? defaultTransaction
111110
options.to = resolver.resolverContractAddress
112111
guard let result = try? await resolver.setAddress(forNode: node, address: address, options: options, password: password) else {
113112
throw Web3Error.processingError(desc: "Can't get result")
@@ -142,7 +141,7 @@ public class ENS {
142141
guard isNameSupports else {
143142
throw Web3Error.processingError(desc: "Name isn't supported")
144143
}
145-
var options = options ?? defaultOptions
144+
var options = options ?? defaultTransaction
146145
options.to = resolver.resolverContractAddress
147146
guard let result = try? await resolver.setCanonicalName(forNode: node, name: name, options: options, password: password) else {
148147
throw Web3Error.processingError(desc: "Can't get result")
@@ -178,7 +177,7 @@ public class ENS {
178177
guard isContentSupports else {
179178
throw Web3Error.processingError(desc: "Content isn't supported")
180179
}
181-
var options = options ?? defaultOptions
180+
var options = options ?? defaultTransaction
182181
options.to = resolver.resolverContractAddress
183182
guard let result = try? await resolver.setContentHash(forNode: node, hash: hash, options: options, password: password) else {
184183
throw Web3Error.processingError(desc: "Can't get result")
@@ -213,7 +212,7 @@ public class ENS {
213212
guard isABISupports else {
214213
throw Web3Error.processingError(desc: "ABI isn't supported")
215214
}
216-
var options = options ?? defaultOptions
215+
var options = options ?? defaultTransaction
217216
options.to = resolver.resolverContractAddress
218217
guard let result = try? await resolver.setContractABI(forNode: node, contentType: contentType, data: data, options: options, password: password) else {
219218
throw Web3Error.processingError(desc: "Can't get result")
@@ -248,7 +247,7 @@ public class ENS {
248247
guard isPKSupports else {
249248
throw Web3Error.processingError(desc: "Public Key isn't supported")
250249
}
251-
var options = options ?? defaultOptions
250+
var options = options ?? defaultTransaction
252251
options.to = resolver.resolverContractAddress
253252
guard let result = try? await resolver.setPublicKey(forNode: node, publicKey: publicKey, options: options, password: password) else {
254253
throw Web3Error.processingError(desc: "Can't get result")
@@ -283,7 +282,7 @@ public class ENS {
283282
guard isTextSupports else {
284283
throw Web3Error.processingError(desc: "Text isn't supported")
285284
}
286-
var options = options ?? defaultOptions
285+
var options = options ?? defaultTransaction
287286
options.to = resolver.resolverContractAddress
288287
guard let result = try? await resolver.setTextData(forNode: node, key: key, value: value, options: options, password: password) else {
289288
throw Web3Error.processingError(desc: "Can't get result")

0 commit comments

Comments
 (0)