@novastera-oss/nitro-metamask 0.2.7 → 0.3.3
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.
- package/LICENSE +21 -201
- package/NitroMetamask.podspec +1 -1
- package/README.md +75 -228
- package/android/CMakeLists.txt +3 -0
- package/android/build.gradle +24 -18
- package/android/fix-prefab.gradle +1 -1
- package/android/gradle.properties +2 -2
- package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +409 -0
- package/android/src/main/java/com/margelo/nitro/nitrometamask/MetamaskContextHolder.kt +4 -25
- package/android/src/main/java/com/margelo/nitro/nitrometamask/NitroMetamaskPackage.kt +36 -7
- package/ios/Bridge.h +2 -2
- package/ios/HybridNitroMetamask.swift +241 -0
- package/lib/commonjs/index.js +20 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/nitro-metamask.nitro.js +6 -0
- package/lib/commonjs/specs/nitro-metamask.nitro.js.map +1 -0
- package/lib/module/index.js +16 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/specs/nitro-metamask.nitro.js +4 -0
- package/lib/module/specs/nitro-metamask.nitro.js.map +1 -0
- package/lib/typescript/src/index.d.ts +15 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +23 -0
- package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts.map +1 -0
- package/nitro.json +4 -4
- package/nitrogen/generated/android/NitroMetamask+autolinking.cmake +2 -2
- package/nitrogen/generated/android/NitroMetamaskOnLoad.cpp +4 -4
- package/nitrogen/generated/android/c++/{JHybridMetamaskConnectorSpec.cpp → JHybridNitroMetamaskSpec.cpp} +31 -10
- package/nitrogen/generated/android/c++/{JHybridMetamaskConnectorSpec.hpp → JHybridNitroMetamaskSpec.hpp} +12 -10
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/{HybridMetamaskConnectorSpec.kt → HybridNitroMetamaskSpec.kt} +14 -6
- package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.cpp +9 -9
- package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.hpp +38 -13
- package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Umbrella.hpp +6 -5
- package/nitrogen/generated/ios/NitroMetamaskAutolinking.mm +3 -3
- package/nitrogen/generated/ios/NitroMetamaskAutolinking.swift +6 -6
- package/nitrogen/generated/ios/c++/{HybridMetamaskConnectorSpecSwift.cpp → HybridNitroMetamaskSpecSwift.cpp} +2 -2
- package/nitrogen/generated/ios/c++/{HybridMetamaskConnectorSpecSwift.hpp → HybridNitroMetamaskSpecSwift.hpp} +28 -13
- package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec.swift +59 -0
- package/nitrogen/generated/ios/swift/{HybridMetamaskConnectorSpec_cxx.swift → HybridNitroMetamaskSpec_cxx.swift} +55 -18
- package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/{HybridMetamaskConnectorSpec.hpp → HybridNitroMetamaskSpec.hpp} +13 -10
- package/package.json +55 -39
- package/react-native.config.js +4 -1
- package/src/index.ts +14 -5
- package/src/specs/nitro-metamask.nitro.ts +21 -0
- package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridMetamaskConnector.kt +0 -126
- package/ios/HybridMetamaskConnector.swift +0 -97
- package/lib/MetamaskConnector.nitro.d.ts +0 -12
- package/lib/MetamaskConnector.nitro.js +0 -1
- package/lib/index.d.ts +0 -3
- package/lib/index.js +0 -2
- package/lib/specs/Example.nitro.d.ts +0 -0
- package/lib/specs/Example.nitro.js +0 -2
- package/nitrogen/generated/ios/swift/HybridMetamaskConnectorSpec.swift +0 -57
- package/nitrogen/generated/shared/c++/HybridMetamaskConnectorSpec.cpp +0 -22
- package/src/MetamaskConnector.nitro.ts +0 -13
- package/src/specs/Example.nitro.ts +0 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrometamask
|
|
2
|
+
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import com.margelo.nitro.core.Promise
|
|
8
|
+
import com.margelo.nitro.nitrometamask.HybridNitroMetamaskSpec
|
|
9
|
+
import com.margelo.nitro.nitrometamask.ConnectResult
|
|
10
|
+
import com.margelo.nitro.nitrometamask.MetamaskContextHolder
|
|
11
|
+
import io.metamask.androidsdk.Ethereum
|
|
12
|
+
import io.metamask.androidsdk.Result
|
|
13
|
+
import io.metamask.androidsdk.DappMetadata
|
|
14
|
+
import io.metamask.androidsdk.SDKOptions
|
|
15
|
+
import io.metamask.androidsdk.EthereumRequest
|
|
16
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
17
|
+
import kotlin.coroutines.resume
|
|
18
|
+
|
|
19
|
+
class HybridNitroMetamask : HybridNitroMetamaskSpec() {
|
|
20
|
+
// Configurable dapp URL - defaults to novastera.com if not set
|
|
21
|
+
// This is only used for SDK validation - the deep link return is handled via AndroidManifest.xml
|
|
22
|
+
@Volatile
|
|
23
|
+
private var dappUrl: String? = null
|
|
24
|
+
|
|
25
|
+
// Ethereum SDK instance - lazy initialization
|
|
26
|
+
@Volatile
|
|
27
|
+
private var ethereumInstance: Ethereum? = null
|
|
28
|
+
|
|
29
|
+
// Track the URL used when creating the current SDK instance
|
|
30
|
+
@Volatile
|
|
31
|
+
private var lastUsedUrl: String? = null
|
|
32
|
+
|
|
33
|
+
// Get or create Ethereum SDK instance
|
|
34
|
+
// Important: DappMetadata.url must be a valid HTTP/HTTPS URL (not a deep link scheme)
|
|
35
|
+
// The SDK automatically detects and uses the deep link from AndroidManifest.xml
|
|
36
|
+
// Reference: https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/metamask-android-sdk/src/main/java/io/metamask/androidsdk/DappMetadata.kt
|
|
37
|
+
private val ethereum: Ethereum
|
|
38
|
+
get() {
|
|
39
|
+
val currentUrl = dappUrl ?: "https://novastera.com"
|
|
40
|
+
val existing = ethereumInstance
|
|
41
|
+
val lastUrl = lastUsedUrl
|
|
42
|
+
|
|
43
|
+
// If not initialized or URL changed, recreate SDK
|
|
44
|
+
if (existing == null || lastUrl != currentUrl) {
|
|
45
|
+
synchronized(this) {
|
|
46
|
+
// Double-check after acquiring lock
|
|
47
|
+
val existingAfterLock = ethereumInstance
|
|
48
|
+
val lastUrlAfterLock = lastUsedUrl
|
|
49
|
+
if (existingAfterLock == null || lastUrlAfterLock != currentUrl) {
|
|
50
|
+
val context = MetamaskContextHolder.get()
|
|
51
|
+
|
|
52
|
+
// DappMetadata.url must be a valid HTTP/HTTPS URL for SDK validation
|
|
53
|
+
// This is separate from the deep link scheme which is auto-detected from AndroidManifest.xml
|
|
54
|
+
// The deep link return to your app is handled automatically via the manifest
|
|
55
|
+
val dappMetadata = DappMetadata(
|
|
56
|
+
name = "Nitro MetaMask Connector",
|
|
57
|
+
url = currentUrl
|
|
58
|
+
)
|
|
59
|
+
val sdkOptions = SDKOptions(
|
|
60
|
+
infuraAPIKey = null,
|
|
61
|
+
readonlyRPCMap = null
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
ethereumInstance = Ethereum(context, dappMetadata, sdkOptions)
|
|
65
|
+
lastUsedUrl = currentUrl
|
|
66
|
+
Log.d("NitroMetamask", "Ethereum SDK initialized with DappMetadata.url=$currentUrl. Deep link auto-detected from AndroidManifest.xml")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return ethereumInstance!!
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
override fun configure(dappUrl: String?) {
|
|
75
|
+
synchronized(this) {
|
|
76
|
+
val urlToUse = dappUrl ?: "https://novastera.com"
|
|
77
|
+
if (this.dappUrl != urlToUse) {
|
|
78
|
+
this.dappUrl = urlToUse
|
|
79
|
+
// Invalidate existing instance to force recreation with new URL
|
|
80
|
+
ethereumInstance = null
|
|
81
|
+
lastUsedUrl = null
|
|
82
|
+
Log.d("NitroMetamask", "configure: Dapp URL set to $urlToUse. Deep link return is handled automatically via AndroidManifest.xml")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override fun connect(): Promise<ConnectResult> {
|
|
88
|
+
// Use Promise.async with coroutines for best practice in Nitro modules
|
|
89
|
+
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
90
|
+
return Promise.async {
|
|
91
|
+
// Convert callback-based connect() to suspend function using suspendCancellableCoroutine
|
|
92
|
+
// This handles cancellation properly when JS GC disposes the promise
|
|
93
|
+
val result = suspendCancellableCoroutine<Result> { continuation ->
|
|
94
|
+
ethereum.connect { callbackResult ->
|
|
95
|
+
if (continuation.isActive) {
|
|
96
|
+
continuation.resume(callbackResult)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
when (result) {
|
|
102
|
+
is Result.Success.Item -> {
|
|
103
|
+
// After successful connection, get account info from SDK
|
|
104
|
+
val address = ethereum.selectedAddress
|
|
105
|
+
?: throw IllegalStateException("MetaMask SDK returned no address after connection")
|
|
106
|
+
val chainIdString = ethereum.chainId
|
|
107
|
+
?: throw IllegalStateException("MetaMask SDK returned no chainId after connection")
|
|
108
|
+
|
|
109
|
+
// Parse chainId from hex string (e.g., "0x1") or decimal string to number
|
|
110
|
+
// Nitro requires chainId to be Double (number in TS maps to Double in Kotlin)
|
|
111
|
+
val chainId = try {
|
|
112
|
+
val chainIdInt = if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
|
|
113
|
+
chainIdString.substring(2).toLong(16).toInt()
|
|
114
|
+
} else {
|
|
115
|
+
chainIdString.toLong().toInt()
|
|
116
|
+
}
|
|
117
|
+
chainIdInt.toDouble()
|
|
118
|
+
} catch (e: NumberFormatException) {
|
|
119
|
+
throw IllegalStateException("Invalid chainId format: $chainIdString")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ConnectResult(
|
|
123
|
+
address = address,
|
|
124
|
+
chainId = chainId
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
is Result.Success.ItemMap -> {
|
|
128
|
+
// Handle ItemMap case (shouldn't happen for connect, but make exhaustive)
|
|
129
|
+
throw IllegalStateException("Unexpected ItemMap result from MetaMask connect")
|
|
130
|
+
}
|
|
131
|
+
is Result.Success.Items -> {
|
|
132
|
+
// Handle Items case (shouldn't happen for connect, but make exhaustive)
|
|
133
|
+
throw IllegalStateException("Unexpected Items result from MetaMask connect")
|
|
134
|
+
}
|
|
135
|
+
is Result.Error -> {
|
|
136
|
+
// Result.Error contains the error directly
|
|
137
|
+
val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask connection failed"
|
|
138
|
+
throw Exception(errorMessage)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
override fun signMessage(message: String): Promise<String> {
|
|
145
|
+
// Use Promise.async with coroutines for best practice in Nitro modules
|
|
146
|
+
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
147
|
+
return Promise.async {
|
|
148
|
+
// Verify connection state before attempting to sign
|
|
149
|
+
// MetaMask SDK requires an active connection to sign messages
|
|
150
|
+
val address = ethereum.selectedAddress
|
|
151
|
+
if (address.isNullOrEmpty()) {
|
|
152
|
+
throw IllegalStateException("No connected account. Please call connect() first to establish a connection with MetaMask.")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Create EthereumRequest for personal_sign
|
|
156
|
+
// Based on MetaMask Android SDK docs: params are [account, message]
|
|
157
|
+
// Reference: https://github.com/MetaMask/metamask-android-sdk
|
|
158
|
+
// EthereumRequest constructor expects method as String
|
|
159
|
+
val request = EthereumRequest(
|
|
160
|
+
method = "personal_sign",
|
|
161
|
+
params = listOf(address, message)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
// Convert callback-based sendRequest() to suspend function
|
|
165
|
+
// The SDK will automatically handle deep link return to the app
|
|
166
|
+
val result = suspendCancellableCoroutine<Result> { continuation ->
|
|
167
|
+
ethereum.sendRequest(request) { callbackResult ->
|
|
168
|
+
if (continuation.isActive) {
|
|
169
|
+
continuation.resume(callbackResult)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
when (result) {
|
|
175
|
+
is Result.Success.Item -> {
|
|
176
|
+
// Extract signature from response
|
|
177
|
+
// The signature should be a hex-encoded string (0x-prefixed)
|
|
178
|
+
val signature = result.value as? String
|
|
179
|
+
?: throw Exception("Invalid signature response format")
|
|
180
|
+
|
|
181
|
+
// Bring app to foreground after receiving the result
|
|
182
|
+
// This must be done on the main thread
|
|
183
|
+
val context = MetamaskContextHolder.get()
|
|
184
|
+
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
|
185
|
+
try {
|
|
186
|
+
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
187
|
+
data = Uri.parse("nitrometamask://mmsdk")
|
|
188
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
189
|
+
setPackage(context.packageName)
|
|
190
|
+
}
|
|
191
|
+
context.startActivity(intent)
|
|
192
|
+
Log.d("NitroMetamask", "Brought app to foreground after signing")
|
|
193
|
+
} catch (e: Exception) {
|
|
194
|
+
Log.w("NitroMetamask", "Failed to bring app to foreground: ${e.message}")
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
signature
|
|
199
|
+
}
|
|
200
|
+
is Result.Success.ItemMap -> {
|
|
201
|
+
// Handle ItemMap case (shouldn't happen for signMessage, but make exhaustive)
|
|
202
|
+
throw IllegalStateException("Unexpected ItemMap result from MetaMask signMessage")
|
|
203
|
+
}
|
|
204
|
+
is Result.Success.Items -> {
|
|
205
|
+
// Handle Items case (shouldn't happen for signMessage, but make exhaustive)
|
|
206
|
+
throw IllegalStateException("Unexpected Items result from MetaMask signMessage")
|
|
207
|
+
}
|
|
208
|
+
is Result.Error -> {
|
|
209
|
+
// Result.Error contains the error directly
|
|
210
|
+
val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask signing failed"
|
|
211
|
+
throw Exception(errorMessage)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
override fun connectSign(nonce: String, exp: Long): Promise<String> {
|
|
218
|
+
// Use Promise.async with coroutines for best practice in Nitro modules
|
|
219
|
+
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
220
|
+
// Based on MetaMask Android SDK: ethereum.connectSign(message)
|
|
221
|
+
// Reference: https://github.com/MetaMask/metamask-android-sdk
|
|
222
|
+
// This convenience method connects (if needed) and signs a JSON message
|
|
223
|
+
return Promise.async {
|
|
224
|
+
// First, ensure we're connected to get address and chainId
|
|
225
|
+
val address = ethereum.selectedAddress
|
|
226
|
+
val chainIdString = ethereum.chainId
|
|
227
|
+
|
|
228
|
+
// If not connected, connect first
|
|
229
|
+
if (address.isNullOrEmpty() || chainIdString.isNullOrEmpty()) {
|
|
230
|
+
val connectResult = suspendCancellableCoroutine<Result> { continuation ->
|
|
231
|
+
ethereum.connect { callbackResult ->
|
|
232
|
+
if (continuation.isActive) {
|
|
233
|
+
continuation.resume(callbackResult)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
when (connectResult) {
|
|
239
|
+
is Result.Success.Item -> {
|
|
240
|
+
// Connection successful, get address and chainId
|
|
241
|
+
val connectedAddress = ethereum.selectedAddress
|
|
242
|
+
?: throw IllegalStateException("MetaMask SDK returned no address after connection")
|
|
243
|
+
val connectedChainId = ethereum.chainId
|
|
244
|
+
?: throw IllegalStateException("MetaMask SDK returned no chainId after connection")
|
|
245
|
+
|
|
246
|
+
// Parse chainId to number
|
|
247
|
+
val chainId = try {
|
|
248
|
+
val chainIdInt = if (connectedChainId.startsWith("0x") || connectedChainId.startsWith("0X")) {
|
|
249
|
+
connectedChainId.substring(2).toLong(16).toInt()
|
|
250
|
+
} else {
|
|
251
|
+
connectedChainId.toLong().toInt()
|
|
252
|
+
}
|
|
253
|
+
chainIdInt
|
|
254
|
+
} catch (e: NumberFormatException) {
|
|
255
|
+
throw IllegalStateException("Invalid chainId format: $connectedChainId")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Construct JSON message
|
|
259
|
+
val message = org.json.JSONObject().apply {
|
|
260
|
+
put("address", connectedAddress)
|
|
261
|
+
put("chainID", chainId)
|
|
262
|
+
put("nonce", nonce)
|
|
263
|
+
put("exp", exp)
|
|
264
|
+
}.toString()
|
|
265
|
+
|
|
266
|
+
// Sign the message using sendRequest with personal_sign (same as signMessage)
|
|
267
|
+
// This ensures proper deep link handling for returning to the app
|
|
268
|
+
val request = EthereumRequest(
|
|
269
|
+
method = "personal_sign",
|
|
270
|
+
params = listOf(connectedAddress, message)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
val signResult = suspendCancellableCoroutine<Result> { signContinuation ->
|
|
274
|
+
ethereum.sendRequest(request) { callbackResult ->
|
|
275
|
+
if (signContinuation.isActive) {
|
|
276
|
+
signContinuation.resume(callbackResult)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
when (signResult) {
|
|
282
|
+
is Result.Success.Item -> {
|
|
283
|
+
val signature = signResult.value as? String
|
|
284
|
+
?: throw Exception("Invalid signature response format")
|
|
285
|
+
|
|
286
|
+
// Bring app to foreground after receiving the result
|
|
287
|
+
// This must be done on the main thread
|
|
288
|
+
val context = MetamaskContextHolder.get()
|
|
289
|
+
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
|
290
|
+
try {
|
|
291
|
+
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
292
|
+
data = Uri.parse("nitrometamask://mmsdk")
|
|
293
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
294
|
+
setPackage(context.packageName)
|
|
295
|
+
}
|
|
296
|
+
context.startActivity(intent)
|
|
297
|
+
Log.d("NitroMetamask", "Brought app to foreground after connectSign")
|
|
298
|
+
} catch (e: Exception) {
|
|
299
|
+
Log.w("NitroMetamask", "Failed to bring app to foreground: ${e.message}")
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
signature
|
|
304
|
+
}
|
|
305
|
+
is Result.Success.ItemMap -> {
|
|
306
|
+
// Handle ItemMap case (shouldn't happen for personal_sign, but make exhaustive)
|
|
307
|
+
throw IllegalStateException("Unexpected ItemMap result from MetaMask connectSign")
|
|
308
|
+
}
|
|
309
|
+
is Result.Success.Items -> {
|
|
310
|
+
// Handle Items case (shouldn't happen for personal_sign, but make exhaustive)
|
|
311
|
+
throw IllegalStateException("Unexpected Items result from MetaMask connectSign")
|
|
312
|
+
}
|
|
313
|
+
is Result.Error -> {
|
|
314
|
+
val errorMessage = signResult.error?.message ?: signResult.error?.toString() ?: "MetaMask signing failed"
|
|
315
|
+
throw Exception(errorMessage)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
is Result.Error -> {
|
|
320
|
+
val errorMessage = connectResult.error?.message ?: connectResult.error?.toString() ?: "MetaMask connection failed"
|
|
321
|
+
throw Exception(errorMessage)
|
|
322
|
+
}
|
|
323
|
+
else -> {
|
|
324
|
+
throw IllegalStateException("Unexpected result type from MetaMask connect")
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
// Already connected, construct message and sign
|
|
329
|
+
val chainId = try {
|
|
330
|
+
val chainIdInt = if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
|
|
331
|
+
chainIdString.substring(2).toLong(16).toInt()
|
|
332
|
+
} else {
|
|
333
|
+
chainIdString.toLong().toInt()
|
|
334
|
+
}
|
|
335
|
+
chainIdInt
|
|
336
|
+
} catch (e: NumberFormatException) {
|
|
337
|
+
throw IllegalStateException("Invalid chainId format: $chainIdString")
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Construct JSON message
|
|
341
|
+
val message = org.json.JSONObject().apply {
|
|
342
|
+
put("address", address)
|
|
343
|
+
put("chainID", chainId)
|
|
344
|
+
put("nonce", nonce)
|
|
345
|
+
put("exp", exp)
|
|
346
|
+
}.toString()
|
|
347
|
+
|
|
348
|
+
// Sign the message using sendRequest with personal_sign (same as signMessage)
|
|
349
|
+
// This ensures proper deep link handling for returning to the app
|
|
350
|
+
val request = EthereumRequest(
|
|
351
|
+
method = "personal_sign",
|
|
352
|
+
params = listOf(address, message)
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
// The SDK will automatically handle deep link return to the app
|
|
356
|
+
val signResult = suspendCancellableCoroutine<Result> { continuation ->
|
|
357
|
+
Log.d("NitroMetamask", "connectSign: Sending personal_sign request")
|
|
358
|
+
ethereum.sendRequest(request) { callbackResult ->
|
|
359
|
+
Log.d("NitroMetamask", "connectSign: Received callback result: ${callbackResult.javaClass.simpleName}")
|
|
360
|
+
if (continuation.isActive) {
|
|
361
|
+
continuation.resume(callbackResult)
|
|
362
|
+
} else {
|
|
363
|
+
Log.w("NitroMetamask", "connectSign: Continuation not active, ignoring callback")
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
Log.d("NitroMetamask", "connectSign: Processing signResult")
|
|
369
|
+
when (signResult) {
|
|
370
|
+
is Result.Success.Item -> {
|
|
371
|
+
val signature = signResult.value as? String
|
|
372
|
+
?: throw Exception("Invalid signature response format")
|
|
373
|
+
|
|
374
|
+
// Bring app to foreground after receiving the result
|
|
375
|
+
// This must be done on the main thread
|
|
376
|
+
val context = MetamaskContextHolder.get()
|
|
377
|
+
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
|
378
|
+
try {
|
|
379
|
+
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
380
|
+
data = Uri.parse("nitrometamask://mmsdk")
|
|
381
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
382
|
+
setPackage(context.packageName)
|
|
383
|
+
}
|
|
384
|
+
context.startActivity(intent)
|
|
385
|
+
Log.d("NitroMetamask", "Brought app to foreground after connectSign (already connected)")
|
|
386
|
+
} catch (e: Exception) {
|
|
387
|
+
Log.w("NitroMetamask", "Failed to bring app to foreground: ${e.message}")
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
signature
|
|
392
|
+
}
|
|
393
|
+
is Result.Success.ItemMap -> {
|
|
394
|
+
// Handle ItemMap case (shouldn't happen for personal_sign, but make exhaustive)
|
|
395
|
+
throw IllegalStateException("Unexpected ItemMap result from MetaMask connectSign")
|
|
396
|
+
}
|
|
397
|
+
is Result.Success.Items -> {
|
|
398
|
+
// Handle Items case (shouldn't happen for personal_sign, but make exhaustive)
|
|
399
|
+
throw IllegalStateException("Unexpected Items result from MetaMask connectSign")
|
|
400
|
+
}
|
|
401
|
+
is Result.Error -> {
|
|
402
|
+
val errorMessage = signResult.error?.message ?: signResult.error?.toString() ?: "MetaMask signing failed"
|
|
403
|
+
throw Exception(errorMessage)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -2,35 +2,14 @@ package com.margelo.nitro.nitrometamask
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Context holder for MetaMask SDK initialization.
|
|
7
|
-
*
|
|
8
|
-
* Nitro does not provide Android Context access, so we must manage it ourselves.
|
|
9
|
-
* This pattern is used by all Nitro modules that need Context (VisionCamera, MMKV, etc.)
|
|
10
|
-
*
|
|
11
|
-
* The context is initialized from NitroMetamaskPackage when React Native loads the module.
|
|
12
|
-
*/
|
|
13
5
|
object MetamaskContextHolder {
|
|
14
|
-
|
|
15
|
-
private var appContext: Context? = null
|
|
6
|
+
private var context: Context? = null
|
|
16
7
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* This should be called once from NitroMetamaskPackage.getModule()
|
|
20
|
-
*/
|
|
21
|
-
fun initialize(context: Context) {
|
|
22
|
-
appContext = context.applicationContext
|
|
8
|
+
fun initialize(ctx: Context) {
|
|
9
|
+
context = ctx.applicationContext
|
|
23
10
|
}
|
|
24
11
|
|
|
25
|
-
/**
|
|
26
|
-
* Get the application context.
|
|
27
|
-
* Throws if not initialized - this ensures we fail fast if the package wasn't loaded correctly.
|
|
28
|
-
*/
|
|
29
12
|
fun get(): Context {
|
|
30
|
-
return
|
|
31
|
-
?: throw IllegalStateException(
|
|
32
|
-
"MetamaskContextHolder not initialized. " +
|
|
33
|
-
"Make sure NitroMetamaskPackage is properly registered in your React Native app."
|
|
34
|
-
)
|
|
13
|
+
return context ?: throw IllegalStateException("Context not initialized")
|
|
35
14
|
}
|
|
36
15
|
}
|
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
package com.margelo.nitro.nitrometamask
|
|
2
2
|
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
3
4
|
import com.facebook.react.bridge.NativeModule
|
|
4
5
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
5
7
|
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
6
|
-
import com.
|
|
8
|
+
import com.margelo.nitro.nitrometamask.NitroMetamaskOnLoad
|
|
7
9
|
|
|
8
10
|
class NitroMetamaskPackage : BaseReactPackage() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
@Volatile
|
|
12
|
+
private var contextInitialized = false
|
|
13
|
+
|
|
14
|
+
override fun getModule(
|
|
15
|
+
name: String,
|
|
16
|
+
reactContext: ReactApplicationContext
|
|
17
|
+
): NativeModule? {
|
|
18
|
+
// Initialize context on first call (thread-safe)
|
|
19
|
+
if (!contextInitialized) {
|
|
20
|
+
synchronized(this) {
|
|
21
|
+
if (!contextInitialized) {
|
|
22
|
+
MetamaskContextHolder.initialize(reactContext)
|
|
23
|
+
contextInitialized = true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
14
27
|
return null
|
|
15
28
|
}
|
|
16
29
|
|
|
17
|
-
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider
|
|
30
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
31
|
+
// Register a dummy module name to ensure getModule() is called
|
|
32
|
+
// This guarantees context initialization
|
|
33
|
+
return ReactModuleInfoProvider {
|
|
34
|
+
mapOf(
|
|
35
|
+
"NitroMetamaskPackage" to ReactModuleInfo(
|
|
36
|
+
"NitroMetamaskPackage",
|
|
37
|
+
"NitroMetamaskPackage",
|
|
38
|
+
false, // canOverrideExistingModule
|
|
39
|
+
true, // needsEagerInit
|
|
40
|
+
true, // hasConstants
|
|
41
|
+
false, // isCxxModule
|
|
42
|
+
true // isTurboModule
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
18
47
|
|
|
19
48
|
companion object {
|
|
20
49
|
init {
|