@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,525 @@
1
+ package io.metamask.androidsdk
2
+
3
+ import android.content.ComponentName
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.content.pm.PackageManager
7
+ import android.os.Build
8
+ import android.os.Bundle
9
+ import com.google.gson.Gson
10
+ import com.google.gson.JsonSyntaxException
11
+ import com.google.gson.reflect.TypeToken
12
+ import kotlinx.serialization.Serializable
13
+ import org.json.JSONObject
14
+ import java.lang.ref.WeakReference
15
+
16
+ class CommunicationClient(
17
+ context: Context,
18
+ callback: EthereumEventCallback?,
19
+ private val sessionManager: SessionManager,
20
+ private val keyExchange: KeyExchange,
21
+ private val serviceConnection: ClientServiceConnection,
22
+ private val messageServiceCallback: ClientMessageServiceCallback,
23
+ private val logger: Logger = DefaultLogger) {
24
+
25
+ var sessionId: String = ""
26
+
27
+ var dappMetadata: DappMetadata? = null
28
+ var isServiceConnected = false
29
+ private set
30
+
31
+ private val appContextRef: WeakReference<Context> = WeakReference(context)
32
+ var ethereumEventCallbackRef: WeakReference<EthereumEventCallback> = WeakReference(callback)
33
+
34
+ var requestJobs: MutableList<() -> Unit> = mutableListOf()
35
+ private set
36
+
37
+ var submittedRequests: MutableMap<String, SubmittedRequest> = mutableMapOf()
38
+ private set
39
+
40
+ var queuedRequests: MutableMap<String, SubmittedRequest> = mutableMapOf()
41
+ private set
42
+
43
+ private var isMetaMaskReady = false
44
+ private var sentOriginatorInfo = false
45
+
46
+ var requestedBindService = false
47
+ private set
48
+
49
+ var enableDebug: Boolean = false
50
+
51
+ init {
52
+ sessionId = sessionManager.sessionId
53
+ // in case not yet initialised in SessionManager
54
+ sessionManager.onInitialized = {
55
+ sessionId = sessionManager.sessionId
56
+ }
57
+ setupServiceConnection()
58
+ setupMessageServiceCallback()
59
+ }
60
+
61
+ fun resetState() {
62
+ sentOriginatorInfo = false
63
+ submittedRequests.clear()
64
+ queuedRequests.clear()
65
+ requestJobs.clear()
66
+ }
67
+
68
+ private fun setupServiceConnection() {
69
+ serviceConnection.onConnected = {
70
+ logger.log("CommunicationClient:: Service connected")
71
+ isServiceConnected = true
72
+ serviceConnection.registerCallback(messageServiceCallback)
73
+ initiateKeyExchange()
74
+ }
75
+
76
+ serviceConnection.onDisconnected = { name ->
77
+ isServiceConnected = false
78
+ logger.error("CommunicationClient:: Service disconnected $name")
79
+ }
80
+
81
+ serviceConnection.onBindingDied = { name ->
82
+ logger.error("CommunicationClient:: binding died: $name")
83
+ }
84
+
85
+ serviceConnection.onNullBinding = { name ->
86
+ logger.error("CommunicationClient:: null binding: $name")
87
+ }
88
+ }
89
+
90
+ private fun setupMessageServiceCallback() {
91
+ messageServiceCallback.onMessage = { bundle ->
92
+ val keyExchange = bundle.getString(KEY_EXCHANGE)
93
+ val message = bundle.getString(MESSAGE)
94
+
95
+ if (keyExchange != null) {
96
+ handleKeyExchange(keyExchange)
97
+ } else if (message != null) {
98
+ handleMessage(message)
99
+ }
100
+ }
101
+ }
102
+
103
+ fun updateSessionDuration(duration: Long) {
104
+ sessionManager.updateSessionDuration(duration)
105
+ }
106
+
107
+ fun clearSession(onComplete: () -> Unit) {
108
+ sessionManager.clearSession {
109
+ sessionId = sessionManager.sessionId
110
+ keyExchange.reset()
111
+ onComplete()
112
+ }
113
+ sentOriginatorInfo = false
114
+ }
115
+
116
+ fun handleMessage(message: String) {
117
+ val jsonString = keyExchange.decrypt(message)
118
+ val json = JSONObject(jsonString)
119
+
120
+ when (json.optString(MessageType.TYPE.value)) {
121
+ MessageType.TERMINATE.value -> {
122
+ logger.log("CommunicationClient:: Connection terminated by MetaMask")
123
+ unbindService()
124
+ keyExchange.reset()
125
+ }
126
+ MessageType.KEYS_EXCHANGED.value -> {
127
+ logger.log("CommunicationClient:: Keys exchanged")
128
+ keyExchange.complete()
129
+ sendOriginatorInfo()
130
+ }
131
+ MessageType.READY.value -> {
132
+ logger.log("CommunicationClient:: Connection ready")
133
+ isMetaMaskReady = true
134
+ resumeRequestJobs()
135
+ }
136
+ else -> {
137
+ val data = json.optString(MessageType.DATA.value)
138
+
139
+ if (data.isNotEmpty()) {
140
+ val dataJson = JSONObject(data)
141
+ val id = dataJson.optString(MessageType.ID.value)
142
+
143
+ if (id.isNotEmpty()) {
144
+ handleResponse(id, dataJson)
145
+ } else if (dataJson.optString(MessageType.ERROR.value).isNotEmpty()) {
146
+ handleError(dataJson.optString(MessageType.ERROR.value), "")
147
+ sentOriginatorInfo = false // connection request rejected
148
+ } else {
149
+ handleEvent(dataJson)
150
+ }
151
+ } else {
152
+ logger.log("CommunicationClient:: Received error $json")
153
+ val id = json.optString("id")
154
+ val error = json.optString(MessageType.ERROR.value)
155
+ handleError(error, id)
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ fun resumeRequestJobs() {
162
+ logger.log("CommunicationClient:: Resuming jobs")
163
+
164
+ while (requestJobs.isNotEmpty()) {
165
+ val job = requestJobs.removeFirstOrNull()
166
+ job?.invoke()
167
+ }
168
+ }
169
+
170
+ fun queueRequestJob(job: () -> Unit) {
171
+ requestJobs.add(job)
172
+ logger.log("CommunicationClient:: Queued job")
173
+ }
174
+
175
+ fun clearPendingRequests() {
176
+ queuedRequests = mutableMapOf()
177
+ requestJobs = mutableListOf()
178
+ submittedRequests = mutableMapOf()
179
+ }
180
+
181
+ fun handleResponse(id: String, data: JSONObject) {
182
+ val submittedRequest = submittedRequests[id]?.request ?: return
183
+
184
+ val error = data.optString("error")
185
+
186
+ if (handleError(error, id)) {
187
+ return
188
+ }
189
+
190
+ val isResultMethod = EthereumMethod.isResultMethod(submittedRequest.method)
191
+
192
+ if (!isResultMethod) {
193
+ val resultJson = data.optString("result")
194
+
195
+ if (resultJson.isNotEmpty()) {
196
+ val resultMap: Map<String, Any?>? = try {
197
+ Gson().fromJson(resultJson, object : TypeToken<Map<String, Any?>>() {}.type)
198
+ } catch (e: JsonSyntaxException) {
199
+ null
200
+ }
201
+
202
+ if (resultMap != null) {
203
+ submittedRequests[id]?.callback?.invoke(Result.Success.ItemMap(resultMap))
204
+ completeRequest(id, Result.Success.ItemMap(resultMap))
205
+ } else {
206
+ val accounts: List<String>? = try {
207
+ Gson().fromJson(resultJson, object : TypeToken<List<String>>() {}.type)
208
+ } catch (e: JsonSyntaxException) {
209
+ null
210
+ }
211
+ val account = accounts?.firstOrNull()
212
+ if (account != null) {
213
+ submittedRequests[id]?.callback?.invoke(Result.Success.Item(account))
214
+ completeRequest(id, Result.Success.Item(account))
215
+ } else {
216
+ submittedRequests[id]?.callback?.invoke(Result.Success.Item(resultJson))
217
+ completeRequest(id, Result.Success.Item(resultJson))
218
+ }
219
+ }
220
+ } else {
221
+ val result: Map<String, Serializable> = Gson().fromJson(data.toString(), object : TypeToken<Map<String, Serializable>>() {}.type)
222
+ completeRequest(id, Result.Success.ItemMap(result))
223
+ }
224
+ return
225
+ }
226
+
227
+ when(submittedRequest.method) {
228
+ EthereumMethod.GET_METAMASK_PROVIDER_STATE.value -> {
229
+ val result = data.optString("result")
230
+ val resultJson = JSONObject(result)
231
+ val accountsJson = resultJson.optString("accounts")
232
+ val accounts: List<String> = Gson().fromJson(accountsJson, object : TypeToken<List<String>>() {}.type)
233
+
234
+ val account = accounts.firstOrNull()
235
+
236
+ if (account != null) {
237
+ updateAccount(account)
238
+ completeRequest(id, Result.Success.Item(account))
239
+ }
240
+
241
+ val chainId = resultJson.optString("chainId")
242
+
243
+ if (chainId.isNotEmpty()) {
244
+ updateChainId(chainId)
245
+ completeRequest(id, Result.Success.Item(chainId))
246
+ }
247
+ }
248
+ EthereumMethod.ETH_REQUEST_ACCOUNTS.value -> {
249
+ val result = data.optString("result")
250
+ val accounts: List<String> = Gson().fromJson(result, object : TypeToken<List<String>>() {}.type)
251
+ val selectedAccount = accounts.getOrNull(0)
252
+
253
+ if (selectedAccount != null) {
254
+ updateAccount(selectedAccount)
255
+ }
256
+
257
+ completeRequest(id, Result.Success.Items(accounts))
258
+ }
259
+ EthereumMethod.ETH_CHAIN_ID.value -> {
260
+ val chainId = data.optString("result")
261
+
262
+ if (chainId.isNotEmpty()) {
263
+ updateChainId(chainId)
264
+ completeRequest(id, Result.Success.Item(chainId))
265
+ }
266
+ }
267
+ EthereumMethod.ETH_SIGN_TYPED_DATA_V3.value,
268
+ EthereumMethod.ETH_SIGN_TYPED_DATA_V4.value,
269
+ EthereumMethod.ETH_SEND_TRANSACTION.value -> {
270
+ val result = data.optString("result")
271
+
272
+ if (result.isNotEmpty()) {
273
+ completeRequest(id, Result.Success.Item(result))
274
+ } else {
275
+ logger.error("CommunicationClient:: Unexpected response: $data")
276
+ }
277
+ }
278
+ EthereumMethod.METAMASK_BATCH.value -> {
279
+ val result = data.optString("result")
280
+ val results: List<String?> = Gson().fromJson(result, object : TypeToken<List<String?>>() {}.type)
281
+ val sanitisedResults = results.filterNotNull()
282
+ completeRequest(id, Result.Success.Items(sanitisedResults))
283
+ }
284
+ else -> {
285
+ val result = data.optString("result")
286
+ completeRequest(id, Result.Success.Item(result))
287
+ }
288
+ }
289
+ }
290
+
291
+ fun handleError(error: String, id: String): Boolean {
292
+ if (error.isEmpty()) {
293
+ return false
294
+ }
295
+
296
+ val requestId: String = id.ifEmpty {
297
+ queuedRequests.entries.find { it.value.request.method == EthereumMethod.ETH_REQUEST_ACCOUNTS.value }?.key ?: ""
298
+ }
299
+
300
+ val errorMap: Map<String, Any?> = Gson().fromJson(error, object : TypeToken<Map<String, Any?>>() {}.type)
301
+ val errorCode = errorMap["code"] as? Double ?: -1
302
+ val code = errorCode.toInt()
303
+ val message = errorMap["message"] as? String ?: ErrorType.message(code)
304
+ logger.error("CommunicationClient:: Got error $error")
305
+ completeRequest(requestId, Result.Error(RequestError(code, message)))
306
+ return true
307
+ }
308
+
309
+ fun completeRequest(id: String, result: Result) {
310
+ if (queuedRequests[id] != null) {
311
+ queuedRequests[id]?.callback?.invoke(result)
312
+ queuedRequests.remove(id)
313
+ }
314
+ submittedRequests[id]?.callback?.invoke(result)
315
+ submittedRequests.remove(id)
316
+ }
317
+
318
+ fun handleEvent(event: JSONObject) {
319
+ when (event.optString("method")) {
320
+ EthereumMethod.METAMASK_ACCOUNTS_CHANGED.value -> {
321
+ val accountsJson = event.optString("params")
322
+ val accounts: List<String> = Gson().fromJson(accountsJson, object : TypeToken<List<String>>() {}.type)
323
+ accounts.getOrNull(0)?.let { account ->
324
+ logger.log("CommunicationClient:: Event Updated to account $account")
325
+ updateAccount(account)
326
+ }
327
+ }
328
+ EthereumMethod.METAMASK_CHAIN_CHANGED.value -> {
329
+ val paramsJson = event.optJSONObject("params")
330
+ val chainId = paramsJson?.optString("chainId")
331
+
332
+ if (!chainId.isNullOrEmpty()) {
333
+ updateChainId(chainId)
334
+ }
335
+ }
336
+ else -> {
337
+ logger.error("CommunicationClient:: Unexpected event: $event")
338
+ }
339
+ }
340
+ }
341
+
342
+ fun updateAccount(account: String) {
343
+ val callback = ethereumEventCallbackRef.get()
344
+ callback?.updateAccount(account)
345
+ }
346
+
347
+ fun updateChainId(chainId: String) {
348
+ val callback = ethereumEventCallbackRef.get()
349
+ callback?.updateChainId(chainId)
350
+ }
351
+
352
+ fun handleKeyExchange(message: String) {
353
+ val json = JSONObject(message)
354
+
355
+ val keyExchangeStep = json.optString(KeyExchange.TYPE, KeyExchangeMessageType.KEY_HANDSHAKE_SYN.name)
356
+ val type = KeyExchangeMessageType.valueOf(keyExchangeStep)
357
+ val theirPublicKey = json.optString(KeyExchange.PUBLIC_KEY)
358
+ val keyExchangeMessage = KeyExchangeMessage(type.name, theirPublicKey)
359
+ val nextStep = keyExchange.nextKeyExchangeMessage(keyExchangeMessage)
360
+
361
+ if (type == KeyExchangeMessageType.KEY_HANDSHAKE_ACK) {
362
+ keyExchange.complete()
363
+ }
364
+
365
+ if (nextStep != null) {
366
+ val exchangeMessage = JSONObject().apply {
367
+ put(KeyExchange.PUBLIC_KEY, nextStep.publicKey)
368
+ put(KeyExchange.TYPE, nextStep.type)
369
+ }.toString()
370
+
371
+ logger.log("Sending key exchange ${nextStep.type}")
372
+ sendKeyExchangeMesage(exchangeMessage)
373
+ }
374
+ }
375
+
376
+ fun sendMessage(message: String) {
377
+ val bundle = Bundle().apply {
378
+ putString(MESSAGE, message)
379
+ }
380
+
381
+ if (keyExchange.keysExchanged()) {
382
+ serviceConnection.sendMessage(bundle)
383
+ } else {
384
+ logger.log("CommunicationClient::sendMessage keys not exchanged, queueing job")
385
+ queueRequestJob { serviceConnection.sendMessage(bundle) }
386
+ }
387
+ }
388
+
389
+ fun sendRequest(request: RpcRequest, callback: (Result) -> Unit) {
390
+ if (request.method == EthereumMethod.GET_METAMASK_PROVIDER_STATE.value) {
391
+ clearPendingRequests()
392
+ }
393
+
394
+ if (!isServiceConnected) {
395
+ queuedRequests[request.id] = SubmittedRequest(request, callback)
396
+ queueRequestJob { processRequest(request, callback) }
397
+ if (!requestedBindService) {
398
+ logger.log("CommunicationClient:: sendRequest - not yet connected to metamask, binding service first")
399
+ bindService()
400
+ } else {
401
+ logger.log("CommunicationClient:: sendRequest - not yet connected to metamask, waiting for service to bind")
402
+ }
403
+ } else if (!keyExchange.keysExchanged()) {
404
+ logger.log("CommunicationClient:: sendRequest - keys not yet exchanged")
405
+ queuedRequests[request.id] = SubmittedRequest(request, callback)
406
+ queueRequestJob { processRequest(request, callback) }
407
+ initiateKeyExchange()
408
+ } else {
409
+ if (isMetaMaskReady) {
410
+ processRequest(request, callback)
411
+ } else {
412
+ logger.log("CommunicationClient::sendRequest - wallet is not ready, queueing request")
413
+ queueRequestJob { processRequest(request, callback) }
414
+ sendOriginatorInfo()
415
+ }
416
+ }
417
+ }
418
+
419
+ fun processRequest(request: RpcRequest, callback: (Result) -> Unit) {
420
+ logger.log("CommunicationClient:: sending request $request")
421
+ if (queuedRequests[request.id] != null) {
422
+ queuedRequests.remove(request.id)
423
+ }
424
+
425
+ val requestJson = Gson().toJson(request)
426
+
427
+ val payload = keyExchange.encrypt(requestJson)
428
+ val message = Message(sessionId, payload)
429
+ val messageJson = Gson().toJson(message)
430
+
431
+ submittedRequests[request.id] = SubmittedRequest(request, callback)
432
+ sendMessage(messageJson)
433
+ }
434
+
435
+ fun sendOriginatorInfo() {
436
+ if (sentOriginatorInfo) { return }
437
+ sentOriginatorInfo = true
438
+
439
+ val originatorInfo = OriginatorInfo(
440
+ title = dappMetadata?.name,
441
+ url = dappMetadata?.url,
442
+ icon = dappMetadata?.iconUrl ?: dappMetadata?.base64Icon,
443
+ dappId = appContextRef.get()?.packageName,
444
+ platform = SDKInfo.PLATFORM,
445
+ apiVersion = SDKInfo.VERSION)
446
+ val requestInfo = RequestInfo("originator_info", originatorInfo)
447
+ val requestInfoJson = Gson().toJson(requestInfo)
448
+
449
+ logger.log("CommunicationClient:: Sending originator info: $requestInfoJson")
450
+ logger.log("CommunicationClient:: SessionId $sessionId")
451
+
452
+ val payload = keyExchange.encrypt(requestInfoJson)
453
+
454
+ val message = Message(sessionId, payload)
455
+ val messageJson = Gson().toJson(message)
456
+
457
+ sendMessage(messageJson)
458
+ }
459
+
460
+ fun isQA(): Boolean {
461
+ if (Build.VERSION.SDK_INT < 33 ) { // i.e Build.VERSION_CODES.TIRAMISU
462
+ return false
463
+ }
464
+
465
+ val packageManager = appContextRef.get()?.packageManager
466
+
467
+ return try {
468
+ packageManager?.getPackageInfo("io.metamask.qa", PackageManager.PackageInfoFlags.of(0))
469
+ true
470
+ } catch (e: PackageManager.NameNotFoundException) {
471
+ false
472
+ }
473
+ }
474
+
475
+ fun bindService() {
476
+ logger.log("CommunicationClient:: Binding service")
477
+ requestedBindService = true
478
+
479
+ val serviceIntent = Intent()
480
+ .setComponent(
481
+ ComponentName(
482
+ if (isQA()) "io.metamask.qa" else "io.metamask",
483
+ "io.metamask.nativesdk.MessageService"
484
+ )
485
+ )
486
+
487
+ if (appContextRef.get() != null) {
488
+ appContextRef.get()?.bindService(
489
+ serviceIntent,
490
+ serviceConnection,
491
+ Context.BIND_AUTO_CREATE)
492
+ } else {
493
+ logger.error("App context null")
494
+ }
495
+ }
496
+
497
+ fun unbindService() {
498
+ requestedBindService = false
499
+
500
+ if (isServiceConnected) {
501
+ logger.log("CommunicationClient:: unbindService")
502
+ appContextRef.get()?.unbindService(serviceConnection)
503
+ isServiceConnected = false
504
+ }
505
+ }
506
+
507
+ fun initiateKeyExchange() {
508
+ logger.log("CommunicationClient:: Initiating key exchange")
509
+
510
+ val keyExchange = JSONObject().apply {
511
+ put(KeyExchange.PUBLIC_KEY, keyExchange.publicKey)
512
+ put(KeyExchange.TYPE, KeyExchangeMessageType.KEY_HANDSHAKE_SYN.name)
513
+ }
514
+
515
+ logger.log("Sending key exchange ${KeyExchangeMessageType.KEY_HANDSHAKE_SYN}")
516
+ sendKeyExchangeMesage(keyExchange.toString())
517
+ }
518
+
519
+ fun sendKeyExchangeMesage(message: String) {
520
+ val bundle = Bundle().apply {
521
+ putString(KEY_EXCHANGE, message)
522
+ }
523
+ serviceConnection.sendMessage(bundle)
524
+ }
525
+ }
@@ -0,0 +1,47 @@
1
+ package io.metamask.androidsdk
2
+
3
+ import android.content.Context
4
+
5
+ open class CommunicationClientModule(private val context: Context): CommunicationClientModuleInterface {
6
+ override fun provideKeyStorage(): SecureStorage {
7
+ return KeyStorage(context)
8
+ }
9
+
10
+ override fun provideSessionManager(keyStorage: SecureStorage): SessionManager {
11
+ return SessionManager(keyStorage)
12
+ }
13
+
14
+ override fun provideKeyExchange(): KeyExchange {
15
+ return KeyExchange()
16
+ }
17
+
18
+ override fun provideLogger(): Logger {
19
+ return DefaultLogger
20
+ }
21
+
22
+ override fun provideClientServiceConnection(): ClientServiceConnection {
23
+ return ClientServiceConnection()
24
+ }
25
+
26
+ override fun provideClientMessageServiceCallback(): ClientMessageServiceCallback {
27
+ return ClientMessageServiceCallback()
28
+ }
29
+
30
+ override fun provideCommunicationClient(callback: EthereumEventCallback?): CommunicationClient {
31
+ val keyStorage = provideKeyStorage()
32
+ val sessionManager = provideSessionManager(keyStorage)
33
+ val keyExchange = provideKeyExchange()
34
+ val serviceConnection = provideClientServiceConnection()
35
+ val messageServiceCallback = provideClientMessageServiceCallback()
36
+ val logger = provideLogger()
37
+
38
+ return CommunicationClient(
39
+ context,
40
+ callback,
41
+ sessionManager,
42
+ keyExchange,
43
+ serviceConnection,
44
+ messageServiceCallback,
45
+ logger)
46
+ }
47
+ }
@@ -0,0 +1,11 @@
1
+ package io.metamask.androidsdk
2
+
3
+ interface CommunicationClientModuleInterface {
4
+ fun provideKeyStorage(): SecureStorage
5
+ fun provideSessionManager(keyStorage: SecureStorage): SessionManager
6
+ fun provideKeyExchange(): KeyExchange
7
+ fun provideLogger(): Logger
8
+ fun provideClientServiceConnection(): ClientServiceConnection
9
+ fun provideClientMessageServiceCallback(): ClientMessageServiceCallback
10
+ fun provideCommunicationClient(callback: EthereumEventCallback?): CommunicationClient
11
+ }
@@ -0,0 +1,5 @@
1
+ package io.metamask.androidsdk
2
+
3
+ const val TAG = "MM_ANDROID_SDK"
4
+ const val MESSAGE = "message"
5
+ const val KEY_EXCHANGE = "key_exchange"
@@ -0,0 +1,35 @@
1
+ package io.metamask.androidsdk
2
+
3
+ import io.metamask.ecies.Ecies
4
+ import kotlinx.coroutines.CoroutineScope
5
+ import kotlinx.coroutines.Dispatchers
6
+ import kotlinx.coroutines.launch
7
+
8
+ class Crypto : Encryption {
9
+ private var ecies: Ecies? = null
10
+ override var onInitialized: () -> Unit = {}
11
+ private val coroutineScope = CoroutineScope(Dispatchers.IO)
12
+
13
+ init {
14
+ coroutineScope.launch {
15
+ ecies = Ecies()
16
+ onInitialized()
17
+ }
18
+ }
19
+
20
+ override fun generatePrivateKey(): String {
21
+ return ecies?.privateKey() ?: ""
22
+ }
23
+
24
+ override fun publicKey(privateKey: String): String {
25
+ return ecies?.publicKeyFrom(privateKey) ?: ""
26
+ }
27
+
28
+ override fun encrypt(publicKey: String, message: String): String {
29
+ return ecies?.encrypt(publicKey, message) ?: ""
30
+ }
31
+
32
+ override fun decrypt(privateKey: String, message: String): String {
33
+ return ecies?.decrypt(privateKey, message) ?: ""
34
+ }
35
+ }
@@ -0,0 +1,36 @@
1
+ package io.metamask.androidsdk
2
+ import kotlinx.serialization.Serializable
3
+ import java.net.MalformedURLException
4
+ import java.net.URL
5
+
6
+ @Serializable
7
+ data class DappMetadata(
8
+ val name: String,
9
+ val url: String,
10
+ val iconUrl: String? = null,
11
+ val base64Icon: String? = null
12
+ ) {
13
+ fun hasValidUrl(): Boolean {
14
+ return try {
15
+ val url = URL(url)
16
+ url.protocol != null && url.host != null
17
+ } catch (e: MalformedURLException) {
18
+ false
19
+ }
20
+ }
21
+
22
+ fun hasValidName(): Boolean {
23
+ return name.isNotEmpty()
24
+ }
25
+
26
+ val validationError: RequestError?
27
+ get() {
28
+ if (!hasValidUrl()) {
29
+ return RequestError(-101, "Please use a valid Dapp url")
30
+ }
31
+ if (!hasValidName()) {
32
+ return RequestError(-102, "Please use a valid Dapp name")
33
+ }
34
+ return null
35
+ }
36
+ }
@@ -0,0 +1,9 @@
1
+ package io.metamask.androidsdk
2
+
3
+ interface Encryption {
4
+ var onInitialized: () -> Unit
5
+ fun generatePrivateKey(): String
6
+ fun publicKey(privateKey: String): String
7
+ fun encrypt(publicKey: String, message: String): String
8
+ fun decrypt(privateKey: String, message: String): String
9
+ }