@novastera-oss/nitro-metamask 0.6.3 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/NitroMetamask.podspec +12 -3
  2. package/README.md +3 -1
  3. package/android/build.gradle +14 -32
  4. package/android/cargo-ecies.gradle +60 -88
  5. package/android/src/main/aidl/io/metamask/nativesdk/IMessegeService.aidl +8 -0
  6. package/android/src/main/aidl/io/metamask/nativesdk/IMessegeServiceCallback.aidl +8 -0
  7. package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +101 -3
  8. package/android/src/main/java/io/metamask/androidsdk/AnyRequest.kt +8 -0
  9. package/android/src/main/java/io/metamask/androidsdk/ClientMessageServiceCallback.kt +12 -0
  10. package/android/src/main/java/io/metamask/androidsdk/ClientServiceConnection.kt +42 -0
  11. package/android/src/main/java/io/metamask/androidsdk/CommunicationClient.kt +525 -0
  12. package/android/src/main/java/io/metamask/androidsdk/CommunicationClientModule.kt +47 -0
  13. package/android/src/main/java/io/metamask/androidsdk/CommunicationClientModuleInterface.kt +11 -0
  14. package/android/src/main/java/io/metamask/androidsdk/Constants.kt +5 -0
  15. package/android/src/main/java/io/metamask/androidsdk/Crypto.kt +35 -0
  16. package/android/src/main/java/io/metamask/androidsdk/DappMetadata.kt +36 -0
  17. package/android/src/main/java/io/metamask/androidsdk/Encryption.kt +9 -0
  18. package/android/src/main/java/io/metamask/androidsdk/ErrorType.kt +41 -0
  19. package/android/src/main/java/io/metamask/androidsdk/Ethereum.kt +328 -0
  20. package/android/src/main/java/io/metamask/androidsdk/EthereumEventCallback.kt +6 -0
  21. package/android/src/main/java/io/metamask/androidsdk/EthereumMethod.kt +80 -0
  22. package/android/src/main/java/io/metamask/androidsdk/EthereumRequest.kt +7 -0
  23. package/android/src/main/java/io/metamask/androidsdk/EthereumState.kt +7 -0
  24. package/android/src/main/java/io/metamask/androidsdk/KeyExchange.kt +77 -0
  25. package/android/src/main/java/io/metamask/androidsdk/KeyExchangeMessageType.kt +20 -0
  26. package/android/src/main/java/io/metamask/androidsdk/KeyStorage.kt +122 -0
  27. package/android/src/main/java/io/metamask/androidsdk/Logger.kt +18 -0
  28. package/android/src/main/java/io/metamask/androidsdk/Message.kt +3 -0
  29. package/android/src/main/java/io/metamask/androidsdk/MessageType.kt +11 -0
  30. package/android/src/main/java/io/metamask/androidsdk/OriginatorInfo.kt +12 -0
  31. package/android/src/main/java/io/metamask/androidsdk/RequestError.kt +8 -0
  32. package/android/src/main/java/io/metamask/androidsdk/RequestInfo.kt +9 -0
  33. package/android/src/main/java/io/metamask/androidsdk/Result.kt +11 -0
  34. package/android/src/main/java/io/metamask/androidsdk/RpcRequest.kt +7 -0
  35. package/android/src/main/java/io/metamask/androidsdk/SDKInfo.kt +6 -0
  36. package/android/src/main/java/io/metamask/androidsdk/SDKOptions.kt +6 -0
  37. package/android/src/main/java/io/metamask/androidsdk/SecureStorage.kt +9 -0
  38. package/android/src/main/java/io/metamask/androidsdk/SessionConfig.kt +10 -0
  39. package/android/src/main/java/io/metamask/androidsdk/SessionManager.kt +92 -0
  40. package/android/src/main/java/io/metamask/androidsdk/SubmittedRequest.kt +8 -0
  41. package/android/src/main/java/io/metamask/androidsdk/TimeStampGenerator.kt +7 -0
  42. package/android/src/main/jniLibs/arm64-v8a/libecies.so +0 -0
  43. package/android/src/main/jniLibs/armeabi-v7a/libecies.so +0 -0
  44. package/android/src/main/jniLibs/x86/libecies.so +0 -0
  45. package/android/src/main/jniLibs/x86_64/libecies.so +0 -0
  46. package/android/src/test/java/com/margelo/nitro/nitrometamask/CancellationStateMachineTest.kt +128 -0
  47. package/android/src/test/java/com/margelo/nitro/nitrometamask/ChainIdParsingTest.kt +65 -0
  48. package/android/src/test/java/com/margelo/nitro/nitrometamask/ConfigureStateMachineTest.kt +140 -0
  49. package/android/src/test/java/com/margelo/nitro/nitrometamask/ConnectSignJsonTest.kt +76 -0
  50. package/android/src/test/java/com/margelo/nitro/nitrometamask/MetaMaskInstallationCheckTest.kt +42 -0
  51. package/android/src/test/java/com/margelo/nitro/nitrometamask/PersonalSignParamsTest.kt +75 -0
  52. package/ios/Frameworks/Ecies.xcframework/Info.plist +47 -0
  53. package/ios/Frameworks/Ecies.xcframework/ios-arm64/Headers/ecies.h +20 -0
  54. package/ios/Frameworks/Ecies.xcframework/ios-arm64/Headers/module.modulemap +4 -0
  55. package/ios/Frameworks/Ecies.xcframework/ios-arm64/libecies.a +0 -0
  56. package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/Headers/ecies.h +20 -0
  57. package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/Headers/module.modulemap +4 -0
  58. package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/libecies.a +0 -0
  59. package/ios/HybridNitroMetamask.swift +119 -54
  60. package/ios/NitroMetamaskTests/CancellationStateMachineTests.swift +150 -0
  61. package/ios/NitroMetamaskTests/ChainIdParsingTests.swift +117 -0
  62. package/ios/NitroMetamaskTests/ConfigureStateMachineTests.swift +174 -0
  63. package/ios/NitroMetamaskTests/ConnectSignJsonTests.swift +168 -0
  64. package/ios/NitroMetamaskTests/DefaultDappUrlTests.swift +80 -0
  65. package/ios/NitroMetamaskTests/PersonalSignParamsTests.swift +101 -0
  66. package/ios/metamask-ios-sdk/CommunicationLayer/CommClient.swift +43 -0
  67. package/ios/metamask-ios-sdk/CommunicationLayer/CommClientFactory.swift +17 -0
  68. package/ios/metamask-ios-sdk/CommunicationLayer/CommLayer.swift +36 -0
  69. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/Deeplink.swift +26 -0
  70. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/DeeplinkClient.swift +199 -0
  71. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/DeeplinkManager.swift +83 -0
  72. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/String.swift +48 -0
  73. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/URLOpener.swift +19 -0
  74. package/ios/metamask-ios-sdk/CommunicationLayer/SocketClient.swift +27 -0
  75. package/ios/metamask-ios-sdk/Crypto/Crypto.swift +72 -0
  76. package/ios/metamask-ios-sdk/Crypto/Encoding.swift +15 -0
  77. package/ios/metamask-ios-sdk/Crypto/KeyExchange.swift +236 -0
  78. package/ios/metamask-ios-sdk/DeviceInfo/DeviceInfo.swift +11 -0
  79. package/ios/metamask-ios-sdk/Ethereum/AppMetadata.swift +28 -0
  80. package/ios/metamask-ios-sdk/Ethereum/ErrorType.swift +62 -0
  81. package/ios/metamask-ios-sdk/Ethereum/Ethereum.swift +810 -0
  82. package/ios/metamask-ios-sdk/Ethereum/EthereumMethod.swift +111 -0
  83. package/ios/metamask-ios-sdk/Ethereum/EthereumRequest.swift +40 -0
  84. package/ios/metamask-ios-sdk/Ethereum/EthereumWrapper.swift +10 -0
  85. package/ios/metamask-ios-sdk/Ethereum/RPCRequest.swift +14 -0
  86. package/ios/metamask-ios-sdk/Ethereum/RequestError.swift +88 -0
  87. package/ios/metamask-ios-sdk/Ethereum/ResponseMethod.swift +22 -0
  88. package/ios/metamask-ios-sdk/Ethereum/SubmitRequest.swift +26 -0
  89. package/ios/metamask-ios-sdk/Ethereum/TimestampGenerator.swift +16 -0
  90. package/ios/metamask-ios-sdk/Extensions/NSRecursiveLock.swift +14 -0
  91. package/ios/metamask-ios-sdk/Extensions/Notification.swift +10 -0
  92. package/ios/metamask-ios-sdk/Logger/Logging.swift +27 -0
  93. package/ios/metamask-ios-sdk/Models/AddChainParameters.swift +35 -0
  94. package/ios/metamask-ios-sdk/Models/Event.swift +19 -0
  95. package/ios/metamask-ios-sdk/Models/Mappable.swift +40 -0
  96. package/ios/metamask-ios-sdk/Models/NativeCurrency.swift +25 -0
  97. package/ios/metamask-ios-sdk/Models/OriginatorInfo.swift +26 -0
  98. package/ios/metamask-ios-sdk/Models/RequestInfo.swift +18 -0
  99. package/ios/metamask-ios-sdk/Models/SignContract.swift +48 -0
  100. package/ios/metamask-ios-sdk/Models/Typealiases.swift +9 -0
  101. package/ios/metamask-ios-sdk/Persistence/SecureStore.swift +134 -0
  102. package/ios/metamask-ios-sdk/Persistence/SessionConfig.swift +24 -0
  103. package/ios/metamask-ios-sdk/Persistence/SessionManager.swift +56 -0
  104. package/ios/metamask-ios-sdk/SDK/Dependencies.swift +35 -0
  105. package/ios/metamask-ios-sdk/SDK/MetaMaskSDK.swift +215 -0
  106. package/ios/metamask-ios-sdk/SDK/SDKInfo.swift +37 -0
  107. package/ios/metamask-ios-sdk/SDK/SDKOptions.swift +16 -0
  108. package/lib/commonjs/index.js +50 -3
  109. package/lib/commonjs/index.js.map +1 -1
  110. package/lib/module/index.js +49 -3
  111. package/lib/module/index.js.map +1 -1
  112. package/lib/typescript/src/__tests__/parseNitroError.test.d.ts +2 -0
  113. package/lib/typescript/src/__tests__/parseNitroError.test.d.ts.map +1 -0
  114. package/lib/typescript/src/index.d.ts +43 -3
  115. package/lib/typescript/src/index.d.ts.map +1 -1
  116. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +29 -1
  117. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts.map +1 -1
  118. package/package.json +21 -12
  119. package/react-native.config.js +5 -0
  120. package/rust/ecies-jni/Cargo.lock +50 -86
  121. package/rust/ecies-jni/Cargo.toml +1 -1
  122. package/rust/ecies-jni/src/lib.rs +164 -100
  123. package/src/__tests__/parseNitroError.test.ts +35 -0
  124. package/src/index.ts +53 -5
  125. package/src/specs/nitro-metamask.nitro.ts +29 -1
  126. package/scripts/verify-16k-page-alignment.py +0 -117
  127. package/scripts/verify-16k-page-alignment.sh +0 -5
@@ -0,0 +1,810 @@
1
+ //
2
+ // Ethereum.swift
3
+ //
4
+
5
+ import UIKit
6
+ import Combine
7
+ import Foundation
8
+
9
+ typealias EthereumPublisher = AnyPublisher<Any, RequestError>
10
+
11
+ protocol EthereumEventsDelegate: AnyObject {
12
+ func chainIdChanged(_ chainId: String)
13
+ func accountChanged(_ account: String)
14
+ }
15
+
16
+ public class Ethereum {
17
+ static let CONNECTION_ID = TimestampGenerator.timestamp()
18
+ static let BATCH_CONNECTION_ID = TimestampGenerator.timestamp()
19
+
20
+ var submittedRequests: [String: SubmittedRequest] = [:]
21
+ private let queue = DispatchQueue(label: "submittedRequests.queue")
22
+
23
+ private var cancellables: Set<AnyCancellable> = []
24
+ private let cancellablesLock = NSRecursiveLock()
25
+
26
+ weak var delegate: EthereumEventsDelegate?
27
+
28
+ var connected: Bool = false
29
+
30
+ /// The active/selected MetaMask account chain
31
+ var chainId: String = ""
32
+
33
+ /// The active/selected MetaMask account address
34
+ var account: String = ""
35
+
36
+ let store: SecureStore
37
+ var appMetadata: AppMetadata?
38
+ var commClient: CommClient
39
+ public var transport: Transport
40
+ var commClientFactory: CommClientFactory
41
+
42
+ private let ACCOUNT_KEY = "ACCOUNT_KEY"
43
+ private let CHAINID_KEY = "CHAIN_ID_KEY"
44
+
45
+ private init(transport: Transport,
46
+ store: SecureStore,
47
+ commClientFactory: CommClientFactory) {
48
+ self.store = store
49
+ self.transport = transport
50
+
51
+ switch transport {
52
+ case .socket:
53
+ self.commClient = commClientFactory.socketClient()
54
+ case .deeplinking(let dappScheme):
55
+ self.commClient = commClientFactory.deeplinkClient(dappScheme: dappScheme)
56
+ }
57
+
58
+ self.commClientFactory = commClientFactory
59
+ self.commClient.handleResponse = handleMessage
60
+ self.commClient.onClientsTerminated = terminateConnection
61
+ fetchCachedSession()
62
+ }
63
+
64
+ public static func shared(transport: Transport,
65
+ store: SecureStore,
66
+ commClientFactory: CommClientFactory) -> Ethereum {
67
+ guard let ethereum = EthereumWrapper.shared.ethereum else {
68
+ let ethereum = Ethereum(
69
+ transport: transport,
70
+ store: store,
71
+ commClientFactory: commClientFactory)
72
+ EthereumWrapper.shared.ethereum = ethereum
73
+ return ethereum
74
+ }
75
+ return ethereum
76
+ }
77
+
78
+ private func fetchCachedSession() {
79
+ if
80
+ let account = store.string(for: ACCOUNT_KEY),
81
+ let chainId = store.string(for: CHAINID_KEY)
82
+ {
83
+ self.account = account
84
+ self.chainId = chainId
85
+ connected = true
86
+ delegate?.accountChanged(account)
87
+ delegate?.chainIdChanged(chainId)
88
+ }
89
+ }
90
+
91
+ @discardableResult
92
+ func updateTransportLayer(_ transport: Transport) -> Ethereum {
93
+ self.transport = transport
94
+
95
+ switch transport {
96
+ case .deeplinking(let dappScheme):
97
+ commClient = commClientFactory.deeplinkClient(dappScheme: dappScheme)
98
+ case .socket:
99
+ commClient = commClientFactory.socketClient()
100
+ commClient.onClientsTerminated = terminateConnection
101
+ }
102
+ commClient.appMetadata = appMetadata
103
+
104
+ fetchCachedSession()
105
+
106
+ commClient.handleResponse = handleMessage
107
+ return self
108
+ }
109
+
110
+ func updateMetadata(_ metadata: AppMetadata) {
111
+ appMetadata = metadata
112
+ commClient.appMetadata = metadata
113
+ }
114
+
115
+ func addRequest(_ submittedRequest: SubmittedRequest, id: String) {
116
+ queue.async { [weak self] in
117
+ self?.submittedRequests[id] = submittedRequest
118
+ }
119
+ }
120
+
121
+ func getAllRequests() -> [String: SubmittedRequest] {
122
+ return queue.sync { [weak self] in
123
+ return self?.submittedRequests ?? [:]
124
+ }
125
+ }
126
+
127
+ func getRequest(id: String) -> SubmittedRequest? {
128
+ return queue.sync { [weak self] in
129
+ return self?.submittedRequests[id]
130
+ }
131
+ }
132
+
133
+ func removeRequest(id: String) {
134
+ queue.async { [weak self] in
135
+ self?.submittedRequests.removeValue(forKey: id)
136
+ }
137
+ }
138
+
139
+ func removeAllRequests() {
140
+ queue.async { [weak self] in
141
+ self?.submittedRequests.removeAll()
142
+ }
143
+ }
144
+
145
+ private func syncCancellables() -> Set<AnyCancellable> {
146
+ cancellablesLock.sync {
147
+ return cancellables
148
+ }
149
+ }
150
+
151
+ // MARK: Session Management
152
+
153
+ @discardableResult
154
+ /// Connect to MetaMask mobile wallet. This method must be called first and once, to establish a connection before any requests can be made
155
+ /// - Returns: A Combine publisher that will emit a connection result or error once a response is received
156
+ func connect() -> EthereumPublisher? {
157
+ commClient.connect(with: nil)
158
+ connected = true
159
+
160
+ if commClient is SocketClient {
161
+ return requestAccounts()
162
+ }
163
+
164
+ let submittedRequest = SubmittedRequest(method: "")
165
+ addRequest(submittedRequest, id: Ethereum.CONNECTION_ID)
166
+ let publisher = getRequest(id: Ethereum.CONNECTION_ID)?.publisher
167
+
168
+ return publisher
169
+ }
170
+
171
+ func performAsyncOperation<T>(_ publisher: EthereumPublisher?, defaultValue: T) async -> Result<T, RequestError> {
172
+ guard let publisher = publisher else {
173
+ return .failure(.genericError)
174
+ }
175
+
176
+ return await withCheckedContinuation { continuation in
177
+ let cancellable = publisher
178
+ .tryMap { output in
179
+ if let resultArray = output as? [Any?] {
180
+ let resultItems = resultArray
181
+ .filter({ !($0 is NSNull) })
182
+ .compactMap({ $0 })
183
+ guard let result = resultItems as? T else {
184
+ return defaultValue
185
+ }
186
+ return result
187
+ }
188
+ guard let result = output as? T else {
189
+ return defaultValue
190
+ }
191
+ return result
192
+ }
193
+ .mapError { error in
194
+ error as? RequestError ?? RequestError.responseError
195
+ }
196
+ .sink(receiveCompletion: { completion in
197
+ switch completion {
198
+ case .finished:
199
+ break
200
+ case .failure(let error):
201
+ continuation.resume(returning: .failure(error))
202
+ }
203
+ }, receiveValue: { result in
204
+ continuation.resume(returning: .success(result))
205
+ })
206
+
207
+ cancellablesLock.sync {
208
+ cancellables.insert(cancellable)
209
+ }
210
+ }
211
+ }
212
+
213
+ func request(_ req: any RPCRequest) async -> Result<String, RequestError> {
214
+ let publisher = performRequest(req)
215
+ return await performAsyncOperation(publisher, defaultValue: String()) as Result<String, RequestError>
216
+ }
217
+
218
+ func request(_ req: any RPCRequest) async -> Result<[String], RequestError> {
219
+ let publisher = performRequest(req)
220
+ return await performAsyncOperation(publisher, defaultValue: [String]()) as Result<[String], RequestError>
221
+ }
222
+
223
+ @discardableResult
224
+ func connect() async -> Result<[String], RequestError> {
225
+ await performAsyncOperation(connect(), defaultValue: []) as Result<[String], RequestError>
226
+ }
227
+
228
+ func connectAndSign(message: String) -> EthereumPublisher? {
229
+ let connectSignRequest = EthereumRequest(
230
+ method: .metaMaskConnectSign,
231
+ params: [message]
232
+ )
233
+ connected = true
234
+
235
+ let requestJson = connectSignRequest.toJsonString() ?? ""
236
+
237
+ if commClient is SocketClient {
238
+ commClient.connect(with: requestJson)
239
+ return performRequest(connectSignRequest)
240
+ }
241
+
242
+ let submittedRequest = SubmittedRequest(method: connectSignRequest.method)
243
+ addRequest(submittedRequest, id: connectSignRequest.id)
244
+ let publisher = getRequest(id: connectSignRequest.id)?.publisher
245
+
246
+ commClient.connect(with: requestJson)
247
+
248
+ return publisher
249
+ }
250
+
251
+ func connectAndSign(message: String) async -> Result<String, RequestError> {
252
+ await performAsyncOperation(connectAndSign(message: message), defaultValue: String()) as Result<String, RequestError>
253
+ }
254
+
255
+ func connectWith<T: CodableData>(_ req: EthereumRequest<T>) -> EthereumPublisher? {
256
+ let params: [EthereumRequest] = [req]
257
+ let connectWithRequest = EthereumRequest(
258
+ method: EthereumMethod.metamaskConnectWith.rawValue,
259
+ params: params
260
+ )
261
+ connected = true
262
+
263
+ switch transport {
264
+ case .socket:
265
+ if let paramsData = req.params as? Data {
266
+ let reqJson = String(data: paramsData, encoding: .utf8)?.trimEscapingChars() ?? ""
267
+ let requestItem: EthereumRequest = EthereumRequest(
268
+ id: req.id,
269
+ method: req.method,
270
+ params: reqJson
271
+ )
272
+
273
+ let connectWithParams = [requestItem]
274
+ let connectRequest = EthereumRequest(
275
+ id: connectWithRequest.id,
276
+ method: connectWithRequest.method,
277
+ params: connectWithParams
278
+ )
279
+ commClient.connect(with: connectRequest.toJsonString())
280
+ return performRequest(connectRequest)
281
+ } else {
282
+ commClient.connect(with: connectWithRequest.toJsonString())
283
+ return performRequest(connectWithRequest)
284
+ }
285
+ case .deeplinking:
286
+ let submittedRequest = SubmittedRequest(method: connectWithRequest.method)
287
+ addRequest(submittedRequest, id: connectWithRequest.id)
288
+ let publisher = getRequest(id: connectWithRequest.id)?.publisher
289
+
290
+ if let paramsData = req.params as? Data {
291
+ do {
292
+ let params = try JSONSerialization.jsonObject(with: paramsData, options: [])
293
+
294
+ let requestDict: [String: Any] = [
295
+ "id": req.id,
296
+ "method": req.method,
297
+ "params": params
298
+ ]
299
+
300
+ let jsonData = try JSONSerialization.data(withJSONObject: requestDict)
301
+ let jsonParams = try JSONSerialization.jsonObject(with: jsonData, options: [])
302
+
303
+ let connectWithParams = [jsonParams]
304
+
305
+ let connectWithDict: [String: Any] = [
306
+ "id": connectWithRequest.id,
307
+ "method": connectWithRequest.method,
308
+ "params": connectWithParams
309
+ ]
310
+
311
+ let connectWithJson = json(from: connectWithDict) ?? ""
312
+
313
+ commClient.connect(with: connectWithJson)
314
+ } catch {
315
+ Logging.error("Ethereum:: error: \(error.localizedDescription)")
316
+ }
317
+ } else {
318
+ let requestJson = connectWithRequest.toJsonString() ?? ""
319
+ commClient.connect(with: requestJson)
320
+ }
321
+ return publisher
322
+ }
323
+ }
324
+
325
+ func connectWith<T: CodableData>(_ req: EthereumRequest<T>) async -> Result<String, RequestError> {
326
+ return await performAsyncOperation(connectWith(req), defaultValue: String()) as Result<String, RequestError>
327
+ }
328
+
329
+ // MARK: Convenience Methods
330
+
331
+ func getChainId() async -> Result<String, RequestError> {
332
+ await ethereumRequest(method: .ethChainId, params: [String]())
333
+ }
334
+
335
+ func getEthAccounts() async -> Result<[String], RequestError> {
336
+ await ethereumRequest(method: .ethAccounts, params: [String]())
337
+ }
338
+
339
+ func getEthGasPrice() async -> Result<String, RequestError> {
340
+ await ethereumRequest(method: .ethGasPrice)
341
+ }
342
+
343
+ func getEthBalance(address: String, block: String) async -> Result<String, RequestError> {
344
+ await ethereumRequest(method: .ethGetBalance, params: [address, block])
345
+ }
346
+
347
+ func getEthBlockNumber() async -> Result<String, RequestError> {
348
+ await ethereumRequest(method: .ethBlockNumber)
349
+ }
350
+
351
+ func getEthEstimateGas() async -> Result<String, RequestError> {
352
+ await ethereumRequest(method: .ethEstimateGas)
353
+ }
354
+
355
+ func getWeb3ClientVersion() async -> Result<String, RequestError> {
356
+ await ethereumRequest(method: .web3ClientVersion, params: [String]())
357
+ }
358
+
359
+ func personalSign(message: String, address: String) async -> Result<String, RequestError> {
360
+ await ethereumRequest(method: .personalSign, params: [address, message])
361
+ }
362
+
363
+ func signTypedDataV4(typedData: String, address: String) async -> Result<String, RequestError> {
364
+ await ethereumRequest(method: .ethSignTypedDataV4, params: [address, typedData])
365
+ }
366
+
367
+ func sendTransaction(from: String, to: String, value: String) async -> Result<String, RequestError> {
368
+ await ethereumRequest(method: .ethSendTransaction, params: [
369
+ [
370
+ "from": from,
371
+ "to": to,
372
+ "value": value
373
+ ]
374
+ ])
375
+ }
376
+
377
+ func sendRawTransaction(signedTransaction: String) async -> Result<String, RequestError> {
378
+ await ethereumRequest(method: .ethSendRawTransaction, params: [signedTransaction])
379
+ }
380
+
381
+ func getBlockTransactionCountByNumber(blockNumber: String) async -> Result<String, RequestError> {
382
+ await ethereumRequest(method: .ethGetBlockTransactionCountByNumber, params: [blockNumber])
383
+ }
384
+
385
+ func getBlockTransactionCountByHash(blockHash: String) async -> Result<String, RequestError> {
386
+ await ethereumRequest(method: .ethGetBlockTransactionCountByHash, params: [blockHash])
387
+ }
388
+
389
+ func getTransactionCount(address: String, tagOrblockNumber: String) async -> Result<String, RequestError> {
390
+ await ethereumRequest(method: .ethGetTransactionCount, params: [address, tagOrblockNumber])
391
+ }
392
+
393
+ func addEthereumChain(chainId: String,
394
+ chainName: String,
395
+ rpcUrls: [String],
396
+ iconUrls: [String]?,
397
+ blockExplorerUrls: [String]?,
398
+ nativeCurrency: NativeCurrency) async -> Result<String, RequestError> {
399
+
400
+ return await ethereumRequest(method: .addEthereumChain, params: [
401
+ AddChainParameters(
402
+ chainId: chainId,
403
+ chainName: chainName,
404
+ rpcUrls: rpcUrls,
405
+ iconUrls: iconUrls,
406
+ blockExplorerUrls: blockExplorerUrls,
407
+ nativeCurrency: nativeCurrency
408
+ )
409
+ ])
410
+ }
411
+
412
+ func switchEthereumChain(chainId: String) async -> Result<String, RequestError> {
413
+ await ethereumRequest(method: .switchEthereumChain, params: [
414
+ ["chainId": chainId]
415
+ ])
416
+ }
417
+
418
+ private func ethereumRequest<T: CodableData>(method: EthereumMethod, params: T = "") async -> Result<String, RequestError> {
419
+ let ethRequest = EthereumRequest(method: method, params: params)
420
+ return await request(ethRequest)
421
+ }
422
+
423
+ private func ethereumRequest<T: CodableData>(method: EthereumMethod, params: T = "") async -> Result<[String], RequestError> {
424
+ let ethRequest = EthereumRequest(method: method, params: params)
425
+ return await request(ethRequest)
426
+ }
427
+
428
+ /// Disconnect dapp
429
+ func disconnect() {
430
+ connected = false
431
+ commClient.disconnect()
432
+ }
433
+
434
+ func clearSession() {
435
+ updateChainId("")
436
+ updateAccount("")
437
+ store.deleteData(for: ACCOUNT_KEY)
438
+ store.deleteData(for: CHAINID_KEY)
439
+ connected = false
440
+ commClient.clearSession()
441
+ }
442
+
443
+ func terminateConnection() {
444
+ let error = RequestError(from: ["message": "The connection request has been rejected"])
445
+ getAllRequests().forEach { key, _ in
446
+ getRequest(id: key)?.error(error)
447
+ }
448
+ removeAllRequests()
449
+ clearSession()
450
+ }
451
+
452
+ // MARK: Request Sending
453
+
454
+ func sendRequest(_ request: any RPCRequest) {
455
+ switch transport {
456
+ case .socket:
457
+ if let paramsData = request.params as? Data {
458
+ do {
459
+ let params = try JSONSerialization.jsonObject(with: paramsData, options: [])
460
+
461
+ let requestDict: [String: Any] = [
462
+ "id": request.id,
463
+ "method": request.method,
464
+ "params": params
465
+ ]
466
+
467
+ let requestJson = json(from: requestDict) ?? ""
468
+
469
+ commClient.sendMessage(requestJson, encrypt: true, options: [:])
470
+ } catch {
471
+ Logging.error("Ethereum:: error: \(error.localizedDescription)")
472
+ }
473
+ } else {
474
+ commClient.sendMessage(request, encrypt: true, options: [:])
475
+ }
476
+
477
+ let authorise = EthereumMethod.requiresAuthorisation(request.methodType)
478
+ let skipAuthorisation = request.methodType == .ethRequestAccounts && !account.isEmpty
479
+
480
+ if authorise && !skipAuthorisation {
481
+ commClient.requestAuthorisation()
482
+ }
483
+
484
+ case .deeplinking:
485
+ if let paramsData = request.params as? Data {
486
+ do {
487
+ let params = try JSONSerialization.jsonObject(with: paramsData, options: [])
488
+
489
+ let requestDict: [String: Any] = [
490
+ "id": request.id,
491
+ "method": request.method,
492
+ "params": params
493
+ ]
494
+
495
+ let requestJson = json(from: requestDict) ?? ""
496
+
497
+ commClient.sendMessage(requestJson, encrypt: true, options: ["account": account, "chainId": chainId])
498
+ } catch {
499
+ Logging.error("Ethereum:: error: \(error.localizedDescription)")
500
+ return
501
+ }
502
+ } else {
503
+ guard let requestJson = request.toJsonString() else {
504
+ Logging.error("Ethereum:: could not convert request to JSON: \(request)")
505
+ return
506
+ }
507
+
508
+ commClient.sendMessage(requestJson, encrypt: true, options: ["account": account, "chainId": chainId])
509
+ }
510
+ }
511
+ }
512
+
513
+ @discardableResult
514
+ private func requestAccounts() -> EthereumPublisher? {
515
+ let requestAccountsRequest = EthereumRequest(
516
+ id: Ethereum.CONNECTION_ID,
517
+ method: .ethRequestAccounts
518
+ )
519
+
520
+ let submittedRequest = SubmittedRequest(method: requestAccountsRequest.method)
521
+ addRequest(submittedRequest, id: requestAccountsRequest.id)
522
+ let publisher = getRequest(id: requestAccountsRequest.id)?.publisher
523
+
524
+ commClient.addRequest { [weak self] in
525
+ self?.sendRequest(requestAccountsRequest)
526
+ }
527
+
528
+ return publisher
529
+ }
530
+
531
+ @discardableResult
532
+ /// Performs and Ethereum remote procedural call (RPC)
533
+ /// - Parameter request: The RPC request. It's `parameters` need to conform to `CodableData`
534
+ /// - Returns: A Combine publisher that will emit a result or error once a response is received
535
+ func performRequest(_ request: any RPCRequest) -> EthereumPublisher? {
536
+ let isConnectMethod = EthereumMethod.isConnectMethod(request.methodType)
537
+
538
+ if !connected && !isConnectMethod && account.isEmpty {
539
+ if request.methodType == .ethRequestAccounts {
540
+ commClient.connect(with: nil)
541
+ connected = true
542
+ return requestAccounts()
543
+ }
544
+
545
+ return RequestError.failWithError(.connectError)
546
+ } else {
547
+ let id = request.id
548
+ let submittedRequest = SubmittedRequest(method: request.method)
549
+ addRequest(submittedRequest, id: id)
550
+
551
+ let publisher = getRequest(id: id)?.publisher
552
+
553
+ if connected || !account.isEmpty {
554
+ connected = true
555
+ sendRequest(request)
556
+ } else {
557
+ commClient.connect(with: nil)
558
+ connected = true
559
+ commClient.addRequest { [weak self] in
560
+ self?.sendRequest(request)
561
+ }
562
+ }
563
+ return publisher
564
+ }
565
+ }
566
+
567
+ private func isRequestParamData<T: CodableData>(_ request: EthereumRequest<T>?) -> Bool {
568
+ if let content = request?.params as? [Any], !content.isEmpty {
569
+ return content.first is Data
570
+ }
571
+ return request?.params is Data
572
+ }
573
+
574
+ func batchRequest<T: CodableData>(_ requests: [EthereumRequest<T>]) async -> Result<[String], RequestError> {
575
+ if (isRequestParamData(requests.first)) {
576
+ var requestDicts: [[String: Any]] = []
577
+
578
+ for request in requests {
579
+ if let paramData = request.params as? Data {
580
+ do {
581
+ let requestParams = try JSONSerialization.jsonObject(with: paramData, options: [])
582
+
583
+ let dict: [String: Any] = [
584
+ "id": request.id,
585
+ "method": request.method,
586
+ "params": requestParams
587
+ ]
588
+ requestDicts.append(dict)
589
+ } catch {
590
+ Logging.error("Ethereum:: error: \(error.localizedDescription)")
591
+ return .failure(RequestError(from: ["message": error.localizedDescription]))
592
+ }
593
+ }
594
+ }
595
+
596
+ do {
597
+ let jsonData = try JSONSerialization.data(withJSONObject: requestDicts)
598
+ let batchReq = EthereumRequest(
599
+ method: EthereumMethod.metamaskBatch.rawValue,
600
+ params: jsonData)
601
+
602
+ return await performAsyncOperation(performRequest(batchReq), defaultValue: [String]()) as Result<[String], RequestError>
603
+ } catch {
604
+ Logging.error("Ethereum:: error: \(error.localizedDescription)")
605
+ return .failure(RequestError(from: ["message": error.localizedDescription]))
606
+ }
607
+ } else {
608
+ let batchRequest = EthereumRequest(
609
+ method: EthereumMethod.metamaskBatch.rawValue,
610
+ params: requests)
611
+
612
+ return await performAsyncOperation(performRequest(batchRequest), defaultValue: [String]()) as Result<[String], RequestError>
613
+ }
614
+ }
615
+
616
+ // MARK: Request Receiving
617
+ private func updateChainId(_ id: String) {
618
+ chainId = id
619
+ delegate?.chainIdChanged(id)
620
+
621
+ guard !id.isEmpty else { return }
622
+ store.save(string: id, key: CHAINID_KEY)
623
+ }
624
+
625
+ private func updateAccount(_ account: String) {
626
+ self.account = account
627
+ delegate?.accountChanged(account)
628
+
629
+ guard !account.isEmpty else { return }
630
+ store.save(string: account, key: ACCOUNT_KEY)
631
+ }
632
+
633
+ func sendResult(_ result: Any, id: String) {
634
+ getRequest(id: id)?.send(result)
635
+ removeRequest(id: id)
636
+ }
637
+
638
+ func sendError(_ error: RequestError, id: String) {
639
+ getRequest(id: id)?.error(error)
640
+ removeRequest(id: id)
641
+
642
+ if error.codeType == .unauthorisedRequest {
643
+ clearSession()
644
+ }
645
+ }
646
+
647
+ func handleMessage(_ message: [String: Any]) {
648
+ if let id = message["id"] {
649
+ if let identifier: Int64 = id as? Int64 {
650
+ let id: String = String(identifier)
651
+ receiveResponse(message, id: id)
652
+ } else if let identifier: String = id as? String {
653
+ receiveResponse(message, id: identifier)
654
+ }
655
+ } else {
656
+ receiveEvent(message)
657
+ }
658
+ }
659
+
660
+ func receiveResponse(_ data: [String: Any], id: String) {
661
+ guard let request = getRequest(id: id) else { return }
662
+
663
+ if let error = data["error"] as? [String: Any] {
664
+ let requestError = RequestError(from: error)
665
+ sendError(requestError, id: id)
666
+
667
+ let accounts = data["accounts"] as? [String] ?? []
668
+
669
+ if let account = accounts.first {
670
+ updateAccount(account)
671
+ sendResult(account, id: id)
672
+ }
673
+
674
+ if let chainId = data["chainId"] as? String {
675
+ updateChainId(chainId)
676
+ sendResult(chainId, id: id)
677
+ }
678
+
679
+ return
680
+ }
681
+
682
+ guard
683
+ let method = EthereumMethod(rawValue: request.method),
684
+ EthereumMethod.isResultMethod(method) else {
685
+ if let result = data["result"] {
686
+ sendResult(result, id: id)
687
+ } else {
688
+ sendResult(data, id: id)
689
+ }
690
+ return
691
+ }
692
+
693
+ switch method {
694
+ case .getMetamaskProviderState:
695
+ let result: [String: Any] = data["result"] as? [String: Any] ?? [:]
696
+ let accounts = result["accounts"] as? [String] ?? []
697
+
698
+ if let account = accounts.first {
699
+ updateAccount(account)
700
+ sendResult(account, id: id)
701
+ }
702
+
703
+ if let chainId = result["chainId"] as? String {
704
+ updateChainId(chainId)
705
+ sendResult(chainId, id: id)
706
+ }
707
+ case .ethRequestAccounts:
708
+ let result: [String] = data["result"] as? [String] ?? []
709
+ if let account = result.first {
710
+ updateAccount(account)
711
+ sendResult(result, id: id)
712
+ } else {
713
+ Logging.error("Ethereum:: Request accounts failure")
714
+ }
715
+ case .ethChainId:
716
+ if let result: String = data["result"] as? String {
717
+ updateChainId(result)
718
+ sendResult(result, id: id)
719
+ }
720
+ case .ethSignTypedDataV4,
721
+ .ethSignTypedDataV3,
722
+ .ethSendTransaction:
723
+ if let result: String = data["result"] as? String {
724
+ sendResult(result, id: id)
725
+ } else {
726
+ Logging.error("Unexpected response \(data)")
727
+ }
728
+ case .metamaskBatch:
729
+ if
730
+ id == Ethereum.BATCH_CONNECTION_ID,
731
+ let result = data["result"] as? [Any],
732
+ result.count == 2,
733
+ let accounts = result.first as? [String],
734
+ let chainId = result[1] as? String {
735
+
736
+ if let account = accounts.first {
737
+ updateAccount(account)
738
+ }
739
+ updateChainId(chainId)
740
+ } else {
741
+ if
742
+ let accounts = data["accounts"] as? [String],
743
+ let account = accounts.first {
744
+ updateAccount(account)
745
+ }
746
+ if let chainId = data["chainId"] as? String {
747
+ updateChainId(chainId)
748
+ }
749
+ if let result = data["result"] {
750
+ sendResult(result, id: id)
751
+ }
752
+ }
753
+ default:
754
+ if let chainId = data["chainId"] as? String {
755
+ updateChainId(chainId)
756
+ }
757
+
758
+ if
759
+ let accounts = data["accounts"] as? [String],
760
+ let selectedAddress = accounts.first {
761
+ updateAccount(selectedAddress)
762
+ }
763
+
764
+ if let result = data["result"] {
765
+ sendResult(result, id: id)
766
+ }
767
+ }
768
+ }
769
+
770
+ func receiveEvent(_ event: [String: Any]) {
771
+ if let error = event["error"] as? [String: Any] {
772
+ Logging.error("Ethereum:: receive error: \(error)")
773
+ let requestError = RequestError(from: error)
774
+ sendError(requestError, id: Ethereum.CONNECTION_ID)
775
+ }
776
+
777
+ guard
778
+ let method = event["method"] as? String,
779
+ let ethMethod = EthereumMethod(rawValue: method)
780
+ else {
781
+ if let chainId = event["chainId"] as? String {
782
+ updateChainId(chainId)
783
+ }
784
+
785
+ if
786
+ let accounts = event["accounts"] as? [String],
787
+ let selectedAddress = accounts.first {
788
+ updateAccount(selectedAddress)
789
+ sendResult(accounts, id: Ethereum.CONNECTION_ID)
790
+ }
791
+ return
792
+ }
793
+
794
+ switch ethMethod {
795
+ case .metaMaskAccountsChanged:
796
+ let accounts: [String] = event["params"] as? [String] ?? []
797
+ if let account = accounts.first {
798
+ updateAccount(account)
799
+ }
800
+ case .metaMaskChainChanged:
801
+ let params: [String: Any] = event["params"] as? [String: Any] ?? [:]
802
+
803
+ if let chainId = params["chainId"] as? String {
804
+ updateChainId(chainId)
805
+ }
806
+ default:
807
+ Logging.error("Unhandled case: \(event)")
808
+ }
809
+ }
810
+ }