@novastera-oss/nitro-metamask 0.3.3 → 0.4.1
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/README.md +45 -1
- package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +349 -175
- package/app.plugin.js +121 -0
- package/ios/HybridNitroMetamask.swift +71 -110
- package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +31 -6
- package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroMetamask+autolinking.cmake +2 -0
- package/nitrogen/generated/android/c++/JConnectResult.hpp +3 -3
- package/nitrogen/generated/android/c++/JConnectSignResult.hpp +65 -0
- package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.cpp +48 -7
- package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.hpp +4 -2
- package/nitrogen/generated/android/c++/JVariant_NullType_Long.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Long.hpp +69 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_String.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/ConnectResult.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/ConnectSignResult.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/HybridNitroMetamaskSpec.kt +11 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/Variant_NullType_Long.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/Variant_NullType_String.kt +59 -0
- package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.cpp +24 -0
- package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.hpp +192 -0
- package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Umbrella.hpp +5 -0
- package/nitrogen/generated/ios/c++/HybridNitroMetamaskSpecSwift.hpp +24 -3
- package/nitrogen/generated/ios/swift/ConnectResult.swift +2 -2
- package/nitrogen/generated/ios/swift/ConnectSignResult.swift +40 -0
- package/nitrogen/generated/ios/swift/Func_void_ConnectSignResult.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__int64_t_.swift +59 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec.swift +4 -2
- package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec_cxx.swift +67 -8
- package/nitrogen/generated/ios/swift/Variant_NullType_Int64.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_String.swift +18 -0
- package/nitrogen/generated/shared/c++/ConnectResult.hpp +5 -5
- package/nitrogen/generated/shared/c++/ConnectSignResult.hpp +91 -0
- package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.hpp +9 -2
- package/package.json +3 -2
- package/src/specs/nitro-metamask.nitro.ts +32 -6
|
@@ -5,9 +5,12 @@ import android.content.pm.PackageManager
|
|
|
5
5
|
import android.net.Uri
|
|
6
6
|
import android.util.Log
|
|
7
7
|
import com.margelo.nitro.core.Promise
|
|
8
|
+
import com.margelo.nitro.core.NullType
|
|
8
9
|
import com.margelo.nitro.nitrometamask.HybridNitroMetamaskSpec
|
|
9
10
|
import com.margelo.nitro.nitrometamask.ConnectResult
|
|
10
11
|
import com.margelo.nitro.nitrometamask.MetamaskContextHolder
|
|
12
|
+
import com.margelo.nitro.nitrometamask.Variant_NullType_String
|
|
13
|
+
import com.margelo.nitro.nitrometamask.Variant_NullType_Long
|
|
11
14
|
import io.metamask.androidsdk.Ethereum
|
|
12
15
|
import io.metamask.androidsdk.Result
|
|
13
16
|
import io.metamask.androidsdk.DappMetadata
|
|
@@ -22,6 +25,10 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
|
|
|
22
25
|
@Volatile
|
|
23
26
|
private var dappUrl: String? = null
|
|
24
27
|
|
|
28
|
+
// Configurable deep link scheme - if not set, will attempt auto-detection
|
|
29
|
+
@Volatile
|
|
30
|
+
private var configuredDeepLinkScheme: String? = null
|
|
31
|
+
|
|
25
32
|
// Ethereum SDK instance - lazy initialization
|
|
26
33
|
@Volatile
|
|
27
34
|
private var ethereumInstance: Ethereum? = null
|
|
@@ -30,6 +37,10 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
|
|
|
30
37
|
@Volatile
|
|
31
38
|
private var lastUsedUrl: String? = null
|
|
32
39
|
|
|
40
|
+
// Cache the detected deep link scheme to avoid repeated detection
|
|
41
|
+
@Volatile
|
|
42
|
+
private var cachedDeepLinkScheme: String? = null
|
|
43
|
+
|
|
33
44
|
// Get or create Ethereum SDK instance
|
|
34
45
|
// Important: DappMetadata.url must be a valid HTTP/HTTPS URL (not a deep link scheme)
|
|
35
46
|
// The SDK automatically detects and uses the deep link from AndroidManifest.xml
|
|
@@ -71,16 +82,165 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
|
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
|
|
74
|
-
override fun configure(dappUrl: String?) {
|
|
85
|
+
override fun configure(dappUrl: String?, deepLinkScheme: String?) {
|
|
75
86
|
synchronized(this) {
|
|
76
87
|
val urlToUse = dappUrl ?: "https://novastera.com"
|
|
88
|
+
val schemeToUse = deepLinkScheme?.takeIf { it.isNotEmpty() }
|
|
89
|
+
|
|
90
|
+
var changed = false
|
|
77
91
|
if (this.dappUrl != urlToUse) {
|
|
78
92
|
this.dappUrl = urlToUse
|
|
93
|
+
changed = true
|
|
94
|
+
}
|
|
95
|
+
if (this.configuredDeepLinkScheme != schemeToUse) {
|
|
96
|
+
this.configuredDeepLinkScheme = schemeToUse
|
|
97
|
+
// Clear cached detection when manually configured
|
|
98
|
+
cachedDeepLinkScheme = null
|
|
99
|
+
changed = true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (changed) {
|
|
79
103
|
// Invalidate existing instance to force recreation with new URL
|
|
80
104
|
ethereumInstance = null
|
|
81
105
|
lastUsedUrl = null
|
|
82
|
-
|
|
106
|
+
if (schemeToUse != null) {
|
|
107
|
+
Log.d("NitroMetamask", "configure: Dapp URL set to $urlToUse, deep link scheme set to $schemeToUse")
|
|
108
|
+
} else {
|
|
109
|
+
Log.d("NitroMetamask", "configure: Dapp URL set to $urlToUse. Deep link scheme will be auto-detected from AndroidManifest.xml")
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the deep link scheme - uses configured value first, then attempts auto-detection.
|
|
117
|
+
* Directly reads intent filters from PackageManager to find the scheme with host="mmsdk"
|
|
118
|
+
* Returns the scheme if found, null otherwise
|
|
119
|
+
*
|
|
120
|
+
* The scheme is cached after first detection to avoid repeated queries.
|
|
121
|
+
*/
|
|
122
|
+
private fun getDeepLinkScheme(context: android.content.Context): String? {
|
|
123
|
+
// Use configured scheme if available
|
|
124
|
+
configuredDeepLinkScheme?.let { return it }
|
|
125
|
+
|
|
126
|
+
// Return cached detected scheme if available
|
|
127
|
+
cachedDeepLinkScheme?.let { return it }
|
|
128
|
+
|
|
129
|
+
return try {
|
|
130
|
+
val packageManager = context.packageManager
|
|
131
|
+
val packageName = context.packageName
|
|
132
|
+
|
|
133
|
+
// Query for activities that can handle VIEW intents with BROWSABLE category
|
|
134
|
+
val viewIntent = Intent(Intent.ACTION_VIEW).apply {
|
|
135
|
+
addCategory(android.content.Intent.CATEGORY_DEFAULT)
|
|
136
|
+
addCategory(android.content.Intent.CATEGORY_BROWSABLE)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
val resolveList = packageManager.queryIntentActivities(viewIntent, PackageManager.MATCH_DEFAULT_ONLY)
|
|
140
|
+
|
|
141
|
+
// Look for activities in our package
|
|
142
|
+
for (resolveInfo in resolveList) {
|
|
143
|
+
if (resolveInfo.activityInfo?.packageName == packageName) {
|
|
144
|
+
val filter = resolveInfo.filter ?: continue
|
|
145
|
+
|
|
146
|
+
// Check if this filter has the required actions and categories
|
|
147
|
+
if (!filter.hasAction(Intent.ACTION_VIEW)) continue
|
|
148
|
+
if (!filter.hasCategory(android.content.Intent.CATEGORY_DEFAULT)) continue
|
|
149
|
+
if (!filter.hasCategory(android.content.Intent.CATEGORY_BROWSABLE)) continue
|
|
150
|
+
|
|
151
|
+
// Get all data schemes from this filter
|
|
152
|
+
val schemeCount = filter.countDataSchemes()
|
|
153
|
+
for (schemeIdx in 0 until schemeCount) {
|
|
154
|
+
val scheme = filter.getDataScheme(schemeIdx)
|
|
155
|
+
if (scheme != null) {
|
|
156
|
+
// Check if this scheme has mmsdk host in any authority
|
|
157
|
+
val authorityCount = filter.countDataAuthorities()
|
|
158
|
+
var hasMmsdkHost = false
|
|
159
|
+
for (authIdx in 0 until authorityCount) {
|
|
160
|
+
val authority = filter.getDataAuthority(authIdx)
|
|
161
|
+
if (authority != null && authority.host == "mmsdk") {
|
|
162
|
+
hasMmsdkHost = true
|
|
163
|
+
break
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (hasMmsdkHost) {
|
|
168
|
+
// Verify this scheme with mmsdk host resolves to our package
|
|
169
|
+
val testUri = Uri.parse("$scheme://mmsdk")
|
|
170
|
+
val testIntent = Intent(Intent.ACTION_VIEW, testUri).apply {
|
|
171
|
+
addCategory(android.content.Intent.CATEGORY_DEFAULT)
|
|
172
|
+
addCategory(android.content.Intent.CATEGORY_BROWSABLE)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Verify this intent resolves to our package
|
|
176
|
+
val testResolveList = packageManager.queryIntentActivities(testIntent, PackageManager.MATCH_DEFAULT_ONLY)
|
|
177
|
+
for (testResolveInfo in testResolveList) {
|
|
178
|
+
if (testResolveInfo.activityInfo?.packageName == packageName) {
|
|
179
|
+
// Cache the detected scheme
|
|
180
|
+
cachedDeepLinkScheme = scheme
|
|
181
|
+
Log.d("NitroMetamask", "Detected deep link scheme: $scheme from activity ${resolveInfo.activityInfo?.name}")
|
|
182
|
+
return scheme
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
Log.w("NitroMetamask", "Scheme $scheme with mmsdk host found but does not resolve to package $packageName")
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
83
190
|
}
|
|
191
|
+
|
|
192
|
+
Log.w("NitroMetamask", "Could not detect deep link scheme from AndroidManifest.xml. Searched ${resolveList.size} activities in package $packageName")
|
|
193
|
+
null
|
|
194
|
+
} catch (e: Exception) {
|
|
195
|
+
Log.w("NitroMetamask", "Error detecting deep link scheme: ${e.message}", e)
|
|
196
|
+
null
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Bring app back to foreground after MetaMask operations.
|
|
202
|
+
* Uses the deep link scheme detected from AndroidManifest.xml to trigger the return.
|
|
203
|
+
* This works by launching the same deep link that MetaMask app would use.
|
|
204
|
+
*
|
|
205
|
+
* Note: Deep links work from background, but getLaunchIntentForPackage() is blocked.
|
|
206
|
+
* So we only use deep link, never fallback to launch intent.
|
|
207
|
+
*/
|
|
208
|
+
private fun bringAppToForeground() {
|
|
209
|
+
try {
|
|
210
|
+
val context = MetamaskContextHolder.get()
|
|
211
|
+
// Must run on main thread - use Handler to ensure we're on main thread
|
|
212
|
+
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
|
213
|
+
try {
|
|
214
|
+
val deepLinkScheme = getDeepLinkScheme(context)
|
|
215
|
+
if (deepLinkScheme != null) {
|
|
216
|
+
// Use the configured or detected deep link scheme to bring app to foreground
|
|
217
|
+
// This is the same deep link that MetaMask app would trigger
|
|
218
|
+
// Deep links work from background (unlike getLaunchIntentForPackage)
|
|
219
|
+
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
220
|
+
data = Uri.parse("$deepLinkScheme://mmsdk")
|
|
221
|
+
addFlags(
|
|
222
|
+
Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
223
|
+
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
|
224
|
+
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
225
|
+
)
|
|
226
|
+
setPackage(context.packageName)
|
|
227
|
+
}
|
|
228
|
+
context.startActivity(intent)
|
|
229
|
+
Log.d("NitroMetamask", "Brought app to foreground using deep link: $deepLinkScheme://mmsdk")
|
|
230
|
+
} else {
|
|
231
|
+
// Cannot use getLaunchIntentForPackage() - Android blocks it from background
|
|
232
|
+
// MetaMask should handle the return via deep link automatically
|
|
233
|
+
Log.w("NitroMetamask", "Could not determine deep link scheme. Please configure it via configure(dappUrl, deepLinkScheme) or ensure AndroidManifest.xml has the correct intent filter.")
|
|
234
|
+
}
|
|
235
|
+
} catch (e: Exception) {
|
|
236
|
+
// Silently fail - better than crashing
|
|
237
|
+
// This is a defensive mechanism, not critical
|
|
238
|
+
Log.e("NitroMetamask", "Failed to bring app to foreground: ${e.message}", e)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch (e: Exception) {
|
|
242
|
+
// Silently fail - this is a defensive mechanism, not critical
|
|
243
|
+
Log.e("NitroMetamask", "Error scheduling bringAppToForeground: ${e.message}", e)
|
|
84
244
|
}
|
|
85
245
|
}
|
|
86
246
|
|
|
@@ -106,15 +266,14 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
|
|
|
106
266
|
val chainIdString = ethereum.chainId
|
|
107
267
|
?: throw IllegalStateException("MetaMask SDK returned no chainId after connection")
|
|
108
268
|
|
|
109
|
-
// Parse chainId from hex string (e.g., "0x1") or decimal string to
|
|
110
|
-
//
|
|
269
|
+
// Parse chainId from hex string (e.g., "0x1") or decimal string to Long
|
|
270
|
+
// chainId is an integer, so we use Long (bigint in TS maps to Long in Kotlin)
|
|
111
271
|
val chainId = try {
|
|
112
|
-
|
|
113
|
-
chainIdString.substring(2).toLong(16)
|
|
272
|
+
if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
|
|
273
|
+
chainIdString.substring(2).toLong(16)
|
|
114
274
|
} else {
|
|
115
|
-
chainIdString.toLong()
|
|
275
|
+
chainIdString.toLong()
|
|
116
276
|
}
|
|
117
|
-
chainIdInt.toDouble()
|
|
118
277
|
} catch (e: NumberFormatException) {
|
|
119
278
|
throw IllegalStateException("Invalid chainId format: $chainIdString")
|
|
120
279
|
}
|
|
@@ -178,22 +337,9 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
|
|
|
178
337
|
val signature = result.value as? String
|
|
179
338
|
?: throw Exception("Invalid signature response format")
|
|
180
339
|
|
|
181
|
-
// Bring app to foreground after receiving
|
|
340
|
+
// Bring app back to foreground immediately after receiving signature
|
|
182
341
|
// This must be done on the main thread
|
|
183
|
-
|
|
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
|
-
}
|
|
342
|
+
bringAppToForeground()
|
|
197
343
|
|
|
198
344
|
signature
|
|
199
345
|
}
|
|
@@ -214,148 +360,29 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
|
|
|
214
360
|
}
|
|
215
361
|
}
|
|
216
362
|
|
|
217
|
-
override fun connectSign(nonce: String, exp: Long): Promise<
|
|
363
|
+
override fun connectSign(nonce: String, exp: Long): Promise<ConnectSignResult> {
|
|
218
364
|
// Use Promise.async with coroutines for best practice in Nitro modules
|
|
219
365
|
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
220
366
|
// Based on MetaMask Android SDK: ethereum.connectSign(message)
|
|
221
367
|
// Reference: https://github.com/MetaMask/metamask-android-sdk
|
|
222
|
-
//
|
|
368
|
+
// The SDK's connectSign method handles connection and signing in one call
|
|
223
369
|
return Promise.async {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
370
|
+
try {
|
|
371
|
+
// Construct JSON message with only nonce and exp
|
|
372
|
+
// We don't include address or chainID - just encrypt nonce and exp
|
|
341
373
|
val message = org.json.JSONObject().apply {
|
|
342
|
-
put("address", address)
|
|
343
|
-
put("chainID", chainId)
|
|
344
374
|
put("nonce", nonce)
|
|
345
375
|
put("exp", exp)
|
|
346
376
|
}.toString()
|
|
347
377
|
|
|
348
|
-
|
|
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
|
-
)
|
|
378
|
+
Log.d("NitroMetamask", "connectSign: Constructed message with nonce and exp: $message")
|
|
354
379
|
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
380
|
+
// Use the SDK's connectSign method - it will connect if needed and sign the message
|
|
381
|
+
// This is the recommended approach per MetaMask Android SDK documentation
|
|
382
|
+
// The SDK will handle bringing the app back to foreground via deep linking
|
|
383
|
+
val result = suspendCancellableCoroutine<Result> { continuation ->
|
|
384
|
+
Log.d("NitroMetamask", "connectSign: Calling ethereum.connectSign with message")
|
|
385
|
+
ethereum.connectSign(message) { callbackResult ->
|
|
359
386
|
Log.d("NitroMetamask", "connectSign: Received callback result: ${callbackResult.javaClass.simpleName}")
|
|
360
387
|
if (continuation.isActive) {
|
|
361
388
|
continuation.resume(callbackResult)
|
|
@@ -365,44 +392,191 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
|
|
|
365
392
|
}
|
|
366
393
|
}
|
|
367
394
|
|
|
368
|
-
Log.d("NitroMetamask", "connectSign: Processing
|
|
369
|
-
when (
|
|
395
|
+
Log.d("NitroMetamask", "connectSign: Processing result")
|
|
396
|
+
when (result) {
|
|
370
397
|
is Result.Success.Item -> {
|
|
371
|
-
val signature =
|
|
398
|
+
val signature = result.value as? String
|
|
372
399
|
?: throw Exception("Invalid signature response format")
|
|
373
400
|
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
401
|
+
// After connectSign, the SDK state might not be immediately updated
|
|
402
|
+
// Try to explicitly fetch the account and chainId to ensure they're available
|
|
403
|
+
// This will trigger the SDK to update its state if needed
|
|
404
|
+
try {
|
|
405
|
+
val addressResult = suspendCancellableCoroutine<Result> { continuation ->
|
|
406
|
+
ethereum.getEthAccounts { callbackResult ->
|
|
407
|
+
if (continuation.isActive) {
|
|
408
|
+
continuation.resume(callbackResult)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
val chainIdResult = suspendCancellableCoroutine<Result> { continuation ->
|
|
413
|
+
ethereum.getChainId { callbackResult ->
|
|
414
|
+
if (continuation.isActive) {
|
|
415
|
+
continuation.resume(callbackResult)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// getEthAccounts returns an array of addresses, we need to extract the first one
|
|
421
|
+
// The SDK may return Result.Success.Item (JSON string) or Result.Success.Items (List)
|
|
422
|
+
Log.d("NitroMetamask", "connectSign: addressResult type: ${addressResult.javaClass.simpleName}")
|
|
423
|
+
val address = when (addressResult) {
|
|
424
|
+
is Result.Success.Item -> {
|
|
425
|
+
val value = addressResult.value
|
|
426
|
+
Log.d("NitroMetamask", "connectSign: addressResult.Item value type: ${value.javaClass.simpleName}, value: $value")
|
|
427
|
+
// Check if it's a JSON array string that needs parsing
|
|
428
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
429
|
+
try {
|
|
430
|
+
val jsonArray = org.json.JSONArray(value)
|
|
431
|
+
val firstAddr = if (jsonArray.length() > 0) jsonArray.getString(0) else null
|
|
432
|
+
if (firstAddr != null && firstAddr.isNotEmpty()) {
|
|
433
|
+
ethereum.updateAccount(firstAddr)
|
|
434
|
+
Log.d("NitroMetamask", "connectSign: Extracted address from JSON array: $firstAddr")
|
|
435
|
+
}
|
|
436
|
+
firstAddr
|
|
437
|
+
} catch (e: Exception) {
|
|
438
|
+
Log.w("NitroMetamask", "connectSign: Failed to parse address array: ${e.message}")
|
|
439
|
+
// If it's not a JSON array, treat it as a single address
|
|
440
|
+
if (value.isNotEmpty()) {
|
|
441
|
+
ethereum.updateAccount(value)
|
|
442
|
+
}
|
|
443
|
+
value
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
// Single address string
|
|
447
|
+
if (value.isNotEmpty()) {
|
|
448
|
+
ethereum.updateAccount(value)
|
|
449
|
+
}
|
|
450
|
+
value
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
is Result.Success.Items -> {
|
|
454
|
+
// Array of addresses - get the first one
|
|
455
|
+
Log.d("NitroMetamask", "connectSign: addressResult.Items value size: ${addressResult.value.size}")
|
|
456
|
+
val firstAddr = addressResult.value.firstOrNull()
|
|
457
|
+
if (firstAddr != null && firstAddr.isNotEmpty()) {
|
|
458
|
+
ethereum.updateAccount(firstAddr)
|
|
459
|
+
Log.d("NitroMetamask", "connectSign: Extracted address from Items: $firstAddr")
|
|
460
|
+
}
|
|
461
|
+
firstAddr
|
|
462
|
+
}
|
|
463
|
+
else -> {
|
|
464
|
+
Log.w("NitroMetamask", "connectSign: Unexpected addressResult type: ${addressResult.javaClass.simpleName}")
|
|
465
|
+
null
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
val chainIdStr = when (chainIdResult) {
|
|
469
|
+
is Result.Success.Item -> {
|
|
470
|
+
val chainIdValue = chainIdResult.value as? String
|
|
471
|
+
// Update the SDK state with the chainId
|
|
472
|
+
if (chainIdValue != null && chainIdValue.isNotEmpty()) {
|
|
473
|
+
ethereum.updateChainId(chainIdValue)
|
|
474
|
+
}
|
|
475
|
+
chainIdValue
|
|
383
476
|
}
|
|
384
|
-
|
|
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}")
|
|
477
|
+
else -> null
|
|
388
478
|
}
|
|
479
|
+
|
|
480
|
+
// Parse chainId from hex string (e.g., "0x1") to Long
|
|
481
|
+
val chainId = chainIdStr?.let { chainId ->
|
|
482
|
+
try {
|
|
483
|
+
if (chainId.startsWith("0x") || chainId.startsWith("0X")) {
|
|
484
|
+
chainId.substring(2).toLong(16)
|
|
485
|
+
} else {
|
|
486
|
+
chainId.toLong()
|
|
487
|
+
}
|
|
488
|
+
} catch (e: NumberFormatException) {
|
|
489
|
+
Log.w("NitroMetamask", "Invalid chainId format: $chainId", e)
|
|
490
|
+
null
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Bring app back to foreground immediately after receiving signature
|
|
495
|
+
// This must be done on the main thread
|
|
496
|
+
bringAppToForeground()
|
|
497
|
+
|
|
498
|
+
// Validate that we have all required values
|
|
499
|
+
if (address == null || address.isEmpty()) {
|
|
500
|
+
throw IllegalStateException("Failed to retrieve address after connectSign. The signature was received but the address could not be determined.")
|
|
501
|
+
}
|
|
502
|
+
if (chainId == null) {
|
|
503
|
+
throw IllegalStateException("Failed to retrieve chainId after connectSign. The signature was received but the chainId could not be determined.")
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
Log.d("NitroMetamask", "connectSign: Returning ConnectSignResult with signature, address=$address, chainId=$chainId")
|
|
507
|
+
|
|
508
|
+
// Return ConnectSignResult with signature, address, and chainId
|
|
509
|
+
ConnectSignResult(
|
|
510
|
+
signature = signature,
|
|
511
|
+
address = address,
|
|
512
|
+
chainId = chainId
|
|
513
|
+
)
|
|
514
|
+
} catch (e: Exception) {
|
|
515
|
+
Log.e("NitroMetamask", "connectSign: Error fetching address/chainId: ${e.message}", e)
|
|
516
|
+
throw e
|
|
389
517
|
}
|
|
390
|
-
|
|
391
|
-
signature
|
|
392
518
|
}
|
|
393
519
|
is Result.Success.ItemMap -> {
|
|
394
|
-
// Handle ItemMap case (shouldn't happen for personal_sign, but make exhaustive)
|
|
395
520
|
throw IllegalStateException("Unexpected ItemMap result from MetaMask connectSign")
|
|
396
521
|
}
|
|
397
522
|
is Result.Success.Items -> {
|
|
398
|
-
// Handle Items case (shouldn't happen for personal_sign, but make exhaustive)
|
|
399
523
|
throw IllegalStateException("Unexpected Items result from MetaMask connectSign")
|
|
400
524
|
}
|
|
401
525
|
is Result.Error -> {
|
|
402
|
-
val errorMessage =
|
|
526
|
+
val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask connectSign failed"
|
|
527
|
+
Log.e("NitroMetamask", "connectSign: Error from MetaMask SDK: $errorMessage")
|
|
403
528
|
throw Exception(errorMessage)
|
|
404
529
|
}
|
|
405
530
|
}
|
|
531
|
+
} catch (e: Exception) {
|
|
532
|
+
Log.e("NitroMetamask", "connectSign: Unexpected error", e)
|
|
533
|
+
throw e
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
override fun getAddress(): Promise<Variant_NullType_String> {
|
|
539
|
+
return Promise.async {
|
|
540
|
+
// Read from ethereumState.value directly to get the most up-to-date value
|
|
541
|
+
// The SDK uses LiveData, so the cached properties might not be updated immediately
|
|
542
|
+
val state = ethereum.ethereumState.value
|
|
543
|
+
val address = state?.selectedAddress?.takeIf { it.isNotEmpty() } ?: ethereum.selectedAddress
|
|
544
|
+
Log.d("NitroMetamask", "getAddress: ethereumState.value?.selectedAddress = ${state?.selectedAddress}, ethereum.selectedAddress = ${ethereum.selectedAddress}, final = $address")
|
|
545
|
+
if (address == null || address.isEmpty()) {
|
|
546
|
+
Log.w("NitroMetamask", "getAddress: Address is null or empty")
|
|
547
|
+
// Use NullType.NULL singleton as per Nitro documentation: https://nitro.margelo.com/docs/types/nulls
|
|
548
|
+
Variant_NullType_String.First(NullType.NULL)
|
|
549
|
+
} else {
|
|
550
|
+
Log.d("NitroMetamask", "getAddress: Returning address: $address")
|
|
551
|
+
Variant_NullType_String.create(address)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
override fun getChainId(): Promise<Variant_NullType_Long> {
|
|
557
|
+
return Promise.async {
|
|
558
|
+
// Read from ethereumState.value directly to get the most up-to-date value
|
|
559
|
+
// The SDK uses LiveData, so the cached properties might not be updated immediately
|
|
560
|
+
val state = ethereum.ethereumState.value
|
|
561
|
+
val chainIdString = state?.chainId?.takeIf { it.isNotEmpty() } ?: ethereum.chainId
|
|
562
|
+
Log.d("NitroMetamask", "getChainId: ethereumState.value?.chainId = ${state?.chainId}, ethereum.chainId = ${ethereum.chainId}, final = $chainIdString")
|
|
563
|
+
if (chainIdString == null || chainIdString.isEmpty()) {
|
|
564
|
+
Log.w("NitroMetamask", "getChainId: ChainId is null or empty")
|
|
565
|
+
// Use NullType.NULL singleton as per Nitro documentation: https://nitro.margelo.com/docs/types/nulls
|
|
566
|
+
Variant_NullType_Long.First(NullType.NULL)
|
|
567
|
+
} else {
|
|
568
|
+
try {
|
|
569
|
+
val chainIdLong = if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
|
|
570
|
+
chainIdString.substring(2).toLong(16)
|
|
571
|
+
} else {
|
|
572
|
+
chainIdString.toLong()
|
|
573
|
+
}
|
|
574
|
+
Log.d("NitroMetamask", "getChainId: Returning chainId: $chainIdLong")
|
|
575
|
+
Variant_NullType_Long.create(chainIdLong)
|
|
576
|
+
} catch (e: NumberFormatException) {
|
|
577
|
+
Log.w("NitroMetamask", "Invalid chainId format: $chainIdString", e)
|
|
578
|
+
Variant_NullType_Long.First(NullType.NULL)
|
|
579
|
+
}
|
|
406
580
|
}
|
|
407
581
|
}
|
|
408
582
|
}
|