@novastera-oss/nitro-metamask 0.3.2 → 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 +124 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +583 -0
- package/android/src/main/java/com/{nitrometamask → margelo/nitro/nitrometamask}/MetamaskContextHolder.kt +1 -1
- package/android/src/main/java/com/{nitrometamask → margelo/nitro/nitrometamask}/NitroMetamaskPackage.kt +1 -1
- package/app.plugin.js +121 -0
- package/ios/HybridNitroMetamask.swift +107 -1
- package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +36 -1
- 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 +62 -0
- package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.hpp +4 -0
- 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 +17 -0
- 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 +217 -0
- package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Umbrella.hpp +6 -0
- package/nitrogen/generated/ios/c++/HybridNitroMetamaskSpecSwift.hpp +37 -1
- 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 -0
- package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec_cxx.swift +96 -0
- 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 +4 -0
- package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.hpp +11 -1
- package/package.json +4 -3
- package/react-native.config.js +1 -1
- package/src/specs/nitro-metamask.nitro.ts +37 -1
- package/android/src/main/java/com/nitrometamask/HybridNitroMetamask.kt +0 -146
|
@@ -0,0 +1,583 @@
|
|
|
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.core.NullType
|
|
9
|
+
import com.margelo.nitro.nitrometamask.HybridNitroMetamaskSpec
|
|
10
|
+
import com.margelo.nitro.nitrometamask.ConnectResult
|
|
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
|
|
14
|
+
import io.metamask.androidsdk.Ethereum
|
|
15
|
+
import io.metamask.androidsdk.Result
|
|
16
|
+
import io.metamask.androidsdk.DappMetadata
|
|
17
|
+
import io.metamask.androidsdk.SDKOptions
|
|
18
|
+
import io.metamask.androidsdk.EthereumRequest
|
|
19
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
20
|
+
import kotlin.coroutines.resume
|
|
21
|
+
|
|
22
|
+
class HybridNitroMetamask : HybridNitroMetamaskSpec() {
|
|
23
|
+
// Configurable dapp URL - defaults to novastera.com if not set
|
|
24
|
+
// This is only used for SDK validation - the deep link return is handled via AndroidManifest.xml
|
|
25
|
+
@Volatile
|
|
26
|
+
private var dappUrl: String? = null
|
|
27
|
+
|
|
28
|
+
// Configurable deep link scheme - if not set, will attempt auto-detection
|
|
29
|
+
@Volatile
|
|
30
|
+
private var configuredDeepLinkScheme: String? = null
|
|
31
|
+
|
|
32
|
+
// Ethereum SDK instance - lazy initialization
|
|
33
|
+
@Volatile
|
|
34
|
+
private var ethereumInstance: Ethereum? = null
|
|
35
|
+
|
|
36
|
+
// Track the URL used when creating the current SDK instance
|
|
37
|
+
@Volatile
|
|
38
|
+
private var lastUsedUrl: String? = null
|
|
39
|
+
|
|
40
|
+
// Cache the detected deep link scheme to avoid repeated detection
|
|
41
|
+
@Volatile
|
|
42
|
+
private var cachedDeepLinkScheme: String? = null
|
|
43
|
+
|
|
44
|
+
// Get or create Ethereum SDK instance
|
|
45
|
+
// Important: DappMetadata.url must be a valid HTTP/HTTPS URL (not a deep link scheme)
|
|
46
|
+
// The SDK automatically detects and uses the deep link from AndroidManifest.xml
|
|
47
|
+
// Reference: https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/metamask-android-sdk/src/main/java/io/metamask/androidsdk/DappMetadata.kt
|
|
48
|
+
private val ethereum: Ethereum
|
|
49
|
+
get() {
|
|
50
|
+
val currentUrl = dappUrl ?: "https://novastera.com"
|
|
51
|
+
val existing = ethereumInstance
|
|
52
|
+
val lastUrl = lastUsedUrl
|
|
53
|
+
|
|
54
|
+
// If not initialized or URL changed, recreate SDK
|
|
55
|
+
if (existing == null || lastUrl != currentUrl) {
|
|
56
|
+
synchronized(this) {
|
|
57
|
+
// Double-check after acquiring lock
|
|
58
|
+
val existingAfterLock = ethereumInstance
|
|
59
|
+
val lastUrlAfterLock = lastUsedUrl
|
|
60
|
+
if (existingAfterLock == null || lastUrlAfterLock != currentUrl) {
|
|
61
|
+
val context = MetamaskContextHolder.get()
|
|
62
|
+
|
|
63
|
+
// DappMetadata.url must be a valid HTTP/HTTPS URL for SDK validation
|
|
64
|
+
// This is separate from the deep link scheme which is auto-detected from AndroidManifest.xml
|
|
65
|
+
// The deep link return to your app is handled automatically via the manifest
|
|
66
|
+
val dappMetadata = DappMetadata(
|
|
67
|
+
name = "Nitro MetaMask Connector",
|
|
68
|
+
url = currentUrl
|
|
69
|
+
)
|
|
70
|
+
val sdkOptions = SDKOptions(
|
|
71
|
+
infuraAPIKey = null,
|
|
72
|
+
readonlyRPCMap = null
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
ethereumInstance = Ethereum(context, dappMetadata, sdkOptions)
|
|
76
|
+
lastUsedUrl = currentUrl
|
|
77
|
+
Log.d("NitroMetamask", "Ethereum SDK initialized with DappMetadata.url=$currentUrl. Deep link auto-detected from AndroidManifest.xml")
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return ethereumInstance!!
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
override fun configure(dappUrl: String?, deepLinkScheme: String?) {
|
|
86
|
+
synchronized(this) {
|
|
87
|
+
val urlToUse = dappUrl ?: "https://novastera.com"
|
|
88
|
+
val schemeToUse = deepLinkScheme?.takeIf { it.isNotEmpty() }
|
|
89
|
+
|
|
90
|
+
var changed = false
|
|
91
|
+
if (this.dappUrl != urlToUse) {
|
|
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) {
|
|
103
|
+
// Invalidate existing instance to force recreation with new URL
|
|
104
|
+
ethereumInstance = null
|
|
105
|
+
lastUsedUrl = null
|
|
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
|
+
}
|
|
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)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
override fun connect(): Promise<ConnectResult> {
|
|
248
|
+
// Use Promise.async with coroutines for best practice in Nitro modules
|
|
249
|
+
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
250
|
+
return Promise.async {
|
|
251
|
+
// Convert callback-based connect() to suspend function using suspendCancellableCoroutine
|
|
252
|
+
// This handles cancellation properly when JS GC disposes the promise
|
|
253
|
+
val result = suspendCancellableCoroutine<Result> { continuation ->
|
|
254
|
+
ethereum.connect { callbackResult ->
|
|
255
|
+
if (continuation.isActive) {
|
|
256
|
+
continuation.resume(callbackResult)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
when (result) {
|
|
262
|
+
is Result.Success.Item -> {
|
|
263
|
+
// After successful connection, get account info from SDK
|
|
264
|
+
val address = ethereum.selectedAddress
|
|
265
|
+
?: throw IllegalStateException("MetaMask SDK returned no address after connection")
|
|
266
|
+
val chainIdString = ethereum.chainId
|
|
267
|
+
?: throw IllegalStateException("MetaMask SDK returned no chainId after connection")
|
|
268
|
+
|
|
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)
|
|
271
|
+
val chainId = try {
|
|
272
|
+
if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
|
|
273
|
+
chainIdString.substring(2).toLong(16)
|
|
274
|
+
} else {
|
|
275
|
+
chainIdString.toLong()
|
|
276
|
+
}
|
|
277
|
+
} catch (e: NumberFormatException) {
|
|
278
|
+
throw IllegalStateException("Invalid chainId format: $chainIdString")
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
ConnectResult(
|
|
282
|
+
address = address,
|
|
283
|
+
chainId = chainId
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
is Result.Success.ItemMap -> {
|
|
287
|
+
// Handle ItemMap case (shouldn't happen for connect, but make exhaustive)
|
|
288
|
+
throw IllegalStateException("Unexpected ItemMap result from MetaMask connect")
|
|
289
|
+
}
|
|
290
|
+
is Result.Success.Items -> {
|
|
291
|
+
// Handle Items case (shouldn't happen for connect, but make exhaustive)
|
|
292
|
+
throw IllegalStateException("Unexpected Items result from MetaMask connect")
|
|
293
|
+
}
|
|
294
|
+
is Result.Error -> {
|
|
295
|
+
// Result.Error contains the error directly
|
|
296
|
+
val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask connection failed"
|
|
297
|
+
throw Exception(errorMessage)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
override fun signMessage(message: String): Promise<String> {
|
|
304
|
+
// Use Promise.async with coroutines for best practice in Nitro modules
|
|
305
|
+
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
306
|
+
return Promise.async {
|
|
307
|
+
// Verify connection state before attempting to sign
|
|
308
|
+
// MetaMask SDK requires an active connection to sign messages
|
|
309
|
+
val address = ethereum.selectedAddress
|
|
310
|
+
if (address.isNullOrEmpty()) {
|
|
311
|
+
throw IllegalStateException("No connected account. Please call connect() first to establish a connection with MetaMask.")
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Create EthereumRequest for personal_sign
|
|
315
|
+
// Based on MetaMask Android SDK docs: params are [account, message]
|
|
316
|
+
// Reference: https://github.com/MetaMask/metamask-android-sdk
|
|
317
|
+
// EthereumRequest constructor expects method as String
|
|
318
|
+
val request = EthereumRequest(
|
|
319
|
+
method = "personal_sign",
|
|
320
|
+
params = listOf(address, message)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
// Convert callback-based sendRequest() to suspend function
|
|
324
|
+
// The SDK will automatically handle deep link return to the app
|
|
325
|
+
val result = suspendCancellableCoroutine<Result> { continuation ->
|
|
326
|
+
ethereum.sendRequest(request) { callbackResult ->
|
|
327
|
+
if (continuation.isActive) {
|
|
328
|
+
continuation.resume(callbackResult)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
when (result) {
|
|
334
|
+
is Result.Success.Item -> {
|
|
335
|
+
// Extract signature from response
|
|
336
|
+
// The signature should be a hex-encoded string (0x-prefixed)
|
|
337
|
+
val signature = result.value as? String
|
|
338
|
+
?: throw Exception("Invalid signature response format")
|
|
339
|
+
|
|
340
|
+
// Bring app back to foreground immediately after receiving signature
|
|
341
|
+
// This must be done on the main thread
|
|
342
|
+
bringAppToForeground()
|
|
343
|
+
|
|
344
|
+
signature
|
|
345
|
+
}
|
|
346
|
+
is Result.Success.ItemMap -> {
|
|
347
|
+
// Handle ItemMap case (shouldn't happen for signMessage, but make exhaustive)
|
|
348
|
+
throw IllegalStateException("Unexpected ItemMap result from MetaMask signMessage")
|
|
349
|
+
}
|
|
350
|
+
is Result.Success.Items -> {
|
|
351
|
+
// Handle Items case (shouldn't happen for signMessage, but make exhaustive)
|
|
352
|
+
throw IllegalStateException("Unexpected Items result from MetaMask signMessage")
|
|
353
|
+
}
|
|
354
|
+
is Result.Error -> {
|
|
355
|
+
// Result.Error contains the error directly
|
|
356
|
+
val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask signing failed"
|
|
357
|
+
throw Exception(errorMessage)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
override fun connectSign(nonce: String, exp: Long): Promise<ConnectSignResult> {
|
|
364
|
+
// Use Promise.async with coroutines for best practice in Nitro modules
|
|
365
|
+
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
366
|
+
// Based on MetaMask Android SDK: ethereum.connectSign(message)
|
|
367
|
+
// Reference: https://github.com/MetaMask/metamask-android-sdk
|
|
368
|
+
// The SDK's connectSign method handles connection and signing in one call
|
|
369
|
+
return Promise.async {
|
|
370
|
+
try {
|
|
371
|
+
// Construct JSON message with only nonce and exp
|
|
372
|
+
// We don't include address or chainID - just encrypt nonce and exp
|
|
373
|
+
val message = org.json.JSONObject().apply {
|
|
374
|
+
put("nonce", nonce)
|
|
375
|
+
put("exp", exp)
|
|
376
|
+
}.toString()
|
|
377
|
+
|
|
378
|
+
Log.d("NitroMetamask", "connectSign: Constructed message with nonce and exp: $message")
|
|
379
|
+
|
|
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 ->
|
|
386
|
+
Log.d("NitroMetamask", "connectSign: Received callback result: ${callbackResult.javaClass.simpleName}")
|
|
387
|
+
if (continuation.isActive) {
|
|
388
|
+
continuation.resume(callbackResult)
|
|
389
|
+
} else {
|
|
390
|
+
Log.w("NitroMetamask", "connectSign: Continuation not active, ignoring callback")
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
Log.d("NitroMetamask", "connectSign: Processing result")
|
|
396
|
+
when (result) {
|
|
397
|
+
is Result.Success.Item -> {
|
|
398
|
+
val signature = result.value as? String
|
|
399
|
+
?: throw Exception("Invalid signature response format")
|
|
400
|
+
|
|
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
|
|
476
|
+
}
|
|
477
|
+
else -> null
|
|
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
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
is Result.Success.ItemMap -> {
|
|
520
|
+
throw IllegalStateException("Unexpected ItemMap result from MetaMask connectSign")
|
|
521
|
+
}
|
|
522
|
+
is Result.Success.Items -> {
|
|
523
|
+
throw IllegalStateException("Unexpected Items result from MetaMask connectSign")
|
|
524
|
+
}
|
|
525
|
+
is Result.Error -> {
|
|
526
|
+
val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask connectSign failed"
|
|
527
|
+
Log.e("NitroMetamask", "connectSign: Error from MetaMask SDK: $errorMessage")
|
|
528
|
+
throw Exception(errorMessage)
|
|
529
|
+
}
|
|
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
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|