@novastera-oss/nitro-metamask 0.3.2 → 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/README.md +80 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +409 -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/ios/HybridNitroMetamask.swift +145 -0
- package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +10 -0
- package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.cpp +21 -0
- package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/HybridNitroMetamaskSpec.kt +8 -0
- package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.hpp +25 -0
- package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Umbrella.hpp +1 -0
- package/nitrogen/generated/ios/c++/HybridNitroMetamaskSpecSwift.hpp +16 -1
- package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec_cxx.swift +37 -0
- package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.hpp +4 -1
- package/package.json +2 -2
- package/react-native.config.js +1 -1
- package/src/specs/nitro-metamask.nitro.ts +10 -0
- package/android/src/main/java/com/nitrometamask/HybridNitroMetamask.kt +0 -146
package/README.md
CHANGED
|
@@ -20,6 +20,86 @@ Novastera authentication with native mobile libraries
|
|
|
20
20
|
npm install @novastera-oss/nitro-metamask react-native-nitro-modules
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
### Android Configuration
|
|
24
|
+
|
|
25
|
+
**Required:** Add a deep link intent filter to enable MetaMask to return to your app after connecting or signing.
|
|
26
|
+
|
|
27
|
+
**File:** `android/app/src/main/AndroidManifest.xml`
|
|
28
|
+
|
|
29
|
+
Add this inside your `MainActivity` `<activity>` tag:
|
|
30
|
+
|
|
31
|
+
```xml
|
|
32
|
+
<activity
|
|
33
|
+
android:name=".MainActivity"
|
|
34
|
+
android:launchMode="singleTask"
|
|
35
|
+
...>
|
|
36
|
+
<!-- Existing intent filters -->
|
|
37
|
+
|
|
38
|
+
<!-- Deep link intent filter for MetaMask callback -->
|
|
39
|
+
<intent-filter>
|
|
40
|
+
<action android:name="android.intent.action.VIEW" />
|
|
41
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
42
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
43
|
+
<data android:scheme="nitrometamask" android:host="mmsdk" />
|
|
44
|
+
</intent-filter>
|
|
45
|
+
</activity>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Important:**
|
|
49
|
+
- Ensure `android:launchMode="singleTask"` is set on your MainActivity (recommended for deep linking)
|
|
50
|
+
- This allows MetaMask to return to your app after the user approves the connection or signature
|
|
51
|
+
|
|
52
|
+
### iOS Configuration
|
|
53
|
+
|
|
54
|
+
For iOS, the MetaMask SDK handles deep linking automatically. No additional configuration is required in `Info.plist`.
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { NitroMetamask } from '@novastera-oss/nitro-metamask';
|
|
60
|
+
|
|
61
|
+
// Optional: Configure dapp URL (only needed if you have a website)
|
|
62
|
+
// If not called, defaults to "https://novastera.com"
|
|
63
|
+
// This URL is ONLY used for SDK validation - the deep link return is handled automatically
|
|
64
|
+
NitroMetamask.configure('https://yourdomain.com'); // Optional
|
|
65
|
+
|
|
66
|
+
// Connect to MetaMask
|
|
67
|
+
const connectResult = await NitroMetamask.connect();
|
|
68
|
+
console.log('Connected:', connectResult.address, 'Chain:', connectResult.chainId);
|
|
69
|
+
|
|
70
|
+
// Sign a message (requires connection first)
|
|
71
|
+
const signature = await NitroMetamask.signMessage('Hello from my app!');
|
|
72
|
+
|
|
73
|
+
// Connect and sign in one call (convenience method)
|
|
74
|
+
// This constructs a JSON message with address, chainID, nonce, and exp
|
|
75
|
+
const nonce = 'random-nonce-123';
|
|
76
|
+
const exp = BigInt(Date.now() + 42000); // 42 seconds from now (use BigInt for timestamp)
|
|
77
|
+
const signature = await NitroMetamask.connectSign(nonce, exp);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### How Deep Linking Works
|
|
81
|
+
|
|
82
|
+
**Important:** The MetaMask SDK requires a valid HTTP/HTTPS URL for `DappMetadata.url` validation, but this is **separate** from the deep link that returns to your app.
|
|
83
|
+
|
|
84
|
+
**Two separate things:**
|
|
85
|
+
1. **DappMetadata.url** (optional, configurable):
|
|
86
|
+
- Used only for SDK validation - the SDK checks it's a valid HTTP/HTTPS URL
|
|
87
|
+
- Defaults to `"https://novastera.com"` if not configured
|
|
88
|
+
- You can call `NitroMetamask.configure('https://yourdomain.com')` if you have a website
|
|
89
|
+
- If you don't have a website, the default works fine - it's just for validation
|
|
90
|
+
- [Reference](https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/metamask-android-sdk/src/main/java/io/metamask/androidsdk/DappMetadata.kt)
|
|
91
|
+
|
|
92
|
+
2. **Deep Link Return** (automatic):
|
|
93
|
+
- Automatically detected from your `AndroidManifest.xml` intent filter
|
|
94
|
+
- The SDK reads `<data android:scheme="..." android:host="mmsdk" />` and uses it to return to your app
|
|
95
|
+
- This is what actually makes MetaMask return to your app after operations
|
|
96
|
+
- No configuration needed - it's handled automatically
|
|
97
|
+
|
|
98
|
+
**Summary:**
|
|
99
|
+
- The `configure()` URL is just for SDK validation (you can use the default if you don't have a website)
|
|
100
|
+
- The deep link return is handled automatically via your `AndroidManifest.xml`
|
|
101
|
+
- Your app will return correctly as long as the manifest is configured properly
|
|
102
|
+
|
|
23
103
|
## Credits
|
|
24
104
|
|
|
25
105
|
Bootstrapped with [create-nitro-module](https://github.com/patrickkabwe/create-nitro-module).
|
package/android/build.gradle
CHANGED
|
@@ -36,7 +36,7 @@ def getExtOrIntegerDefault(name) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
android {
|
|
39
|
-
namespace "com.nitrometamask"
|
|
39
|
+
namespace "com.margelo.nitro.nitrometamask"
|
|
40
40
|
|
|
41
41
|
ndkVersion getExtOrDefault("ndkVersion")
|
|
42
42
|
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
@@ -152,6 +152,6 @@ if (isNewArchitectureEnabled()) {
|
|
|
152
152
|
react {
|
|
153
153
|
jsRootDir = file("../src/")
|
|
154
154
|
libraryName = "NitroMetamask"
|
|
155
|
-
codegenJavaPackageName = "com.nitrometamask"
|
|
155
|
+
codegenJavaPackageName = "com.margelo.nitro.nitrometamask"
|
|
156
156
|
}
|
|
157
157
|
}
|
|
@@ -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
|
+
}
|
|
@@ -4,6 +4,17 @@ import Foundation
|
|
|
4
4
|
|
|
5
5
|
final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
6
6
|
private let sdk = MetaMaskSDK.shared
|
|
7
|
+
|
|
8
|
+
// Configurable dapp URL - stored for consistency with Android
|
|
9
|
+
// iOS SDK handles deep linking automatically via Info.plist
|
|
10
|
+
private var dappUrl: String? = nil
|
|
11
|
+
|
|
12
|
+
func configure(dappUrl: String?) {
|
|
13
|
+
// iOS SDK handles deep linking automatically via Info.plist
|
|
14
|
+
// Store the URL for consistency with Android implementation
|
|
15
|
+
self.dappUrl = dappUrl
|
|
16
|
+
NSLog("NitroMetamask: configure: Dapp URL set to \(dappUrl ?? "default"). Deep link handled automatically via Info.plist")
|
|
17
|
+
}
|
|
7
18
|
|
|
8
19
|
func connect() -> Promise<ConnectResult> {
|
|
9
20
|
// Use Promise.async with Swift async/await for best practice in Nitro modules
|
|
@@ -93,4 +104,138 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
106
|
}
|
|
107
|
+
|
|
108
|
+
func connectSign(nonce: String, exp: Int64) -> Promise<String> {
|
|
109
|
+
// Use Promise.async with Swift async/await for best practice in Nitro modules
|
|
110
|
+
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
111
|
+
// Based on MetaMask iOS SDK: connect and sign in one call
|
|
112
|
+
// Reference: https://github.com/MetaMask/metamask-ios-sdk
|
|
113
|
+
return Promise.async {
|
|
114
|
+
// First, ensure we're connected
|
|
115
|
+
var address = self.sdk.account
|
|
116
|
+
var chainIdHex = self.sdk.chainId
|
|
117
|
+
|
|
118
|
+
// If not connected, connect first
|
|
119
|
+
if address == nil || address?.isEmpty == true || chainIdHex == nil || chainIdHex?.isEmpty == true {
|
|
120
|
+
let connectResult = try await self.sdk.connect()
|
|
121
|
+
|
|
122
|
+
switch connectResult {
|
|
123
|
+
case .success:
|
|
124
|
+
address = self.sdk.account
|
|
125
|
+
chainIdHex = self.sdk.chainId
|
|
126
|
+
|
|
127
|
+
guard let account = address, !account.isEmpty else {
|
|
128
|
+
throw NSError(
|
|
129
|
+
domain: "MetamaskConnector",
|
|
130
|
+
code: -1,
|
|
131
|
+
userInfo: [NSLocalizedDescriptionKey: "MetaMask SDK returned no address after connection"]
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
guard let chainId = chainIdHex, !chainId.isEmpty,
|
|
136
|
+
let chainIdInt = Int(chainId.replacingOccurrences(of: "0x", with: ""), radix: 16) else {
|
|
137
|
+
throw NSError(
|
|
138
|
+
domain: "MetamaskConnector",
|
|
139
|
+
code: -1,
|
|
140
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid chainId format"]
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Construct JSON message
|
|
145
|
+
let messageDict: [String: Any] = [
|
|
146
|
+
"address": account,
|
|
147
|
+
"chainID": chainIdInt,
|
|
148
|
+
"nonce": nonce,
|
|
149
|
+
"exp": exp
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
guard let jsonData = try? JSONSerialization.data(withJSONObject: messageDict),
|
|
153
|
+
let message = String(data: jsonData, encoding: .utf8) else {
|
|
154
|
+
throw NSError(
|
|
155
|
+
domain: "MetamaskConnector",
|
|
156
|
+
code: -1,
|
|
157
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to create JSON message"]
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Create EthereumRequest for personal_sign
|
|
162
|
+
let params: [String] = [account, message]
|
|
163
|
+
let request = EthereumRequest(
|
|
164
|
+
method: .personalSign,
|
|
165
|
+
params: params
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
// Make the request using the SDK's async request method
|
|
169
|
+
let result = try await self.sdk.request(request)
|
|
170
|
+
|
|
171
|
+
// Extract signature from response
|
|
172
|
+
if let signature = result as? String {
|
|
173
|
+
return signature
|
|
174
|
+
} else if let dict = result as? [String: Any], let sig = dict["signature"] as? String ?? dict["result"] as? String {
|
|
175
|
+
return sig
|
|
176
|
+
} else {
|
|
177
|
+
throw NSError(
|
|
178
|
+
domain: "MetamaskConnector",
|
|
179
|
+
code: -1,
|
|
180
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid signature response format"]
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case .failure(let error):
|
|
185
|
+
throw error
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
// Already connected, construct message and sign
|
|
189
|
+
guard let account = address, !account.isEmpty,
|
|
190
|
+
let chainId = chainIdHex, !chainId.isEmpty,
|
|
191
|
+
let chainIdInt = Int(chainId.replacingOccurrences(of: "0x", with: ""), radix: 16) else {
|
|
192
|
+
throw NSError(
|
|
193
|
+
domain: "MetamaskConnector",
|
|
194
|
+
code: -1,
|
|
195
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid connection state"]
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Construct JSON message
|
|
200
|
+
let messageDict: [String: Any] = [
|
|
201
|
+
"address": account,
|
|
202
|
+
"chainID": chainIdInt,
|
|
203
|
+
"nonce": nonce,
|
|
204
|
+
"exp": Int64(exp)
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
guard let jsonData = try? JSONSerialization.data(withJSONObject: messageDict),
|
|
208
|
+
let message = String(data: jsonData, encoding: .utf8) else {
|
|
209
|
+
throw NSError(
|
|
210
|
+
domain: "MetamaskConnector",
|
|
211
|
+
code: -1,
|
|
212
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to create JSON message"]
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Create EthereumRequest for personal_sign
|
|
217
|
+
let params: [String] = [account, message]
|
|
218
|
+
let request = EthereumRequest(
|
|
219
|
+
method: .personalSign,
|
|
220
|
+
params: params
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
// Make the request using the SDK's async request method
|
|
224
|
+
let result = try await self.sdk.request(request)
|
|
225
|
+
|
|
226
|
+
// Extract signature from response
|
|
227
|
+
if let signature = result as? String {
|
|
228
|
+
return signature
|
|
229
|
+
} else if let dict = result as? [String: Any], let sig = dict["signature"] as? String ?? dict["result"] as? String {
|
|
230
|
+
return sig
|
|
231
|
+
} else {
|
|
232
|
+
throw NSError(
|
|
233
|
+
domain: "MetamaskConnector",
|
|
234
|
+
code: -1,
|
|
235
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid signature response format"]
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
96
241
|
}
|
|
@@ -7,7 +7,17 @@ export interface NitroMetamask extends HybridObject<{
|
|
|
7
7
|
ios: 'swift';
|
|
8
8
|
android: 'kotlin';
|
|
9
9
|
}> {
|
|
10
|
+
/**
|
|
11
|
+
* Configure the dapp URL for MetaMask SDK validation.
|
|
12
|
+
* This URL is only used for SDK validation - the deep link return is handled automatically via AndroidManifest.xml.
|
|
13
|
+
*
|
|
14
|
+
* @param dappUrl - A valid HTTP/HTTPS URL (e.g., "https://yourdomain.com").
|
|
15
|
+
* If not provided, defaults to "https://novastera.com".
|
|
16
|
+
* This is separate from the deep link scheme which is auto-detected from your manifest.
|
|
17
|
+
*/
|
|
18
|
+
configure(dappUrl?: string): void;
|
|
10
19
|
connect(): Promise<ConnectResult>;
|
|
11
20
|
signMessage(message: string): Promise<string>;
|
|
21
|
+
connectSign(nonce: string, exp: bigint): Promise<string>;
|
|
12
22
|
}
|
|
13
23
|
//# sourceMappingURL=nitro-metamask.nitro.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nitro-metamask.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/nitro-metamask.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAE9D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACtF,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,CAAA;IACjC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"nitro-metamask.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/nitro-metamask.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAE9D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACtF;;;;;;;OAOG;IACH,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,CAAA;IACjC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC7C,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACzD"}
|
|
@@ -15,6 +15,7 @@ namespace margelo::nitro::nitrometamask { struct ConnectResult; }
|
|
|
15
15
|
#include <NitroModules/JPromise.hpp>
|
|
16
16
|
#include "JConnectResult.hpp"
|
|
17
17
|
#include <string>
|
|
18
|
+
#include <optional>
|
|
18
19
|
|
|
19
20
|
namespace margelo::nitro::nitrometamask {
|
|
20
21
|
|
|
@@ -48,6 +49,10 @@ namespace margelo::nitro::nitrometamask {
|
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
// Methods
|
|
52
|
+
void JHybridNitroMetamaskSpec::configure(const std::optional<std::string>& dappUrl) {
|
|
53
|
+
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* dappUrl */)>("configure");
|
|
54
|
+
method(_javaPart, dappUrl.has_value() ? jni::make_jstring(dappUrl.value()) : nullptr);
|
|
55
|
+
}
|
|
51
56
|
std::shared_ptr<Promise<ConnectResult>> JHybridNitroMetamaskSpec::connect() {
|
|
52
57
|
static const auto method = javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>()>("connect");
|
|
53
58
|
auto __result = method(_javaPart);
|
|
@@ -80,5 +85,21 @@ namespace margelo::nitro::nitrometamask {
|
|
|
80
85
|
return __promise;
|
|
81
86
|
}();
|
|
82
87
|
}
|
|
88
|
+
std::shared_ptr<Promise<std::string>> JHybridNitroMetamaskSpec::connectSign(const std::string& nonce, int64_t exp) {
|
|
89
|
+
static const auto method = javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>(jni::alias_ref<jni::JString> /* nonce */, int64_t /* exp */)>("connectSign");
|
|
90
|
+
auto __result = method(_javaPart, jni::make_jstring(nonce), exp);
|
|
91
|
+
return [&]() {
|
|
92
|
+
auto __promise = Promise<std::string>::create();
|
|
93
|
+
__result->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& __boxedResult) {
|
|
94
|
+
auto __result = jni::static_ref_cast<jni::JString>(__boxedResult);
|
|
95
|
+
__promise->resolve(__result->toStdString());
|
|
96
|
+
});
|
|
97
|
+
__result->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JThrowable>& __throwable) {
|
|
98
|
+
jni::JniException __jniError(__throwable);
|
|
99
|
+
__promise->reject(std::make_exception_ptr(__jniError));
|
|
100
|
+
});
|
|
101
|
+
return __promise;
|
|
102
|
+
}();
|
|
103
|
+
}
|
|
83
104
|
|
|
84
105
|
} // namespace margelo::nitro::nitrometamask
|
|
@@ -54,8 +54,10 @@ namespace margelo::nitro::nitrometamask {
|
|
|
54
54
|
|
|
55
55
|
public:
|
|
56
56
|
// Methods
|
|
57
|
+
void configure(const std::optional<std::string>& dappUrl) override;
|
|
57
58
|
std::shared_ptr<Promise<ConnectResult>> connect() override;
|
|
58
59
|
std::shared_ptr<Promise<std::string>> signMessage(const std::string& message) override;
|
|
60
|
+
std::shared_ptr<Promise<std::string>> connectSign(const std::string& nonce, int64_t exp) override;
|
|
59
61
|
|
|
60
62
|
private:
|
|
61
63
|
friend HybridBase;
|
package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/HybridNitroMetamaskSpec.kt
CHANGED
|
@@ -46,6 +46,10 @@ abstract class HybridNitroMetamaskSpec: HybridObject() {
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
// Methods
|
|
49
|
+
@DoNotStrip
|
|
50
|
+
@Keep
|
|
51
|
+
abstract fun configure(dappUrl: String?): Unit
|
|
52
|
+
|
|
49
53
|
@DoNotStrip
|
|
50
54
|
@Keep
|
|
51
55
|
abstract fun connect(): Promise<ConnectResult>
|
|
@@ -53,6 +57,10 @@ abstract class HybridNitroMetamaskSpec: HybridObject() {
|
|
|
53
57
|
@DoNotStrip
|
|
54
58
|
@Keep
|
|
55
59
|
abstract fun signMessage(message: String): Promise<String>
|
|
60
|
+
|
|
61
|
+
@DoNotStrip
|
|
62
|
+
@Keep
|
|
63
|
+
abstract fun connectSign(nonce: String, exp: Long): Promise<String>
|
|
56
64
|
|
|
57
65
|
private external fun initHybrid(): HybridData
|
|
58
66
|
|
|
@@ -26,6 +26,7 @@ namespace NitroMetamask { class HybridNitroMetamaskSpec_cxx; }
|
|
|
26
26
|
#include <exception>
|
|
27
27
|
#include <functional>
|
|
28
28
|
#include <memory>
|
|
29
|
+
#include <optional>
|
|
29
30
|
#include <string>
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -34,6 +35,21 @@ namespace NitroMetamask { class HybridNitroMetamaskSpec_cxx; }
|
|
|
34
35
|
*/
|
|
35
36
|
namespace margelo::nitro::nitrometamask::bridge::swift {
|
|
36
37
|
|
|
38
|
+
// pragma MARK: std::optional<std::string>
|
|
39
|
+
/**
|
|
40
|
+
* Specialized version of `std::optional<std::string>`.
|
|
41
|
+
*/
|
|
42
|
+
using std__optional_std__string_ = std::optional<std::string>;
|
|
43
|
+
inline std::optional<std::string> create_std__optional_std__string_(const std::string& value) noexcept {
|
|
44
|
+
return std::optional<std::string>(value);
|
|
45
|
+
}
|
|
46
|
+
inline bool has_value_std__optional_std__string_(const std::optional<std::string>& optional) noexcept {
|
|
47
|
+
return optional.has_value();
|
|
48
|
+
}
|
|
49
|
+
inline std::string get_std__optional_std__string_(const std::optional<std::string>& optional) noexcept {
|
|
50
|
+
return *optional;
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
// pragma MARK: std::shared_ptr<Promise<ConnectResult>>
|
|
38
54
|
/**
|
|
39
55
|
* Specialized version of `std::shared_ptr<Promise<ConnectResult>>`.
|
|
@@ -136,6 +152,15 @@ namespace margelo::nitro::nitrometamask::bridge::swift {
|
|
|
136
152
|
using std__weak_ptr_HybridNitroMetamaskSpec_ = std::weak_ptr<HybridNitroMetamaskSpec>;
|
|
137
153
|
inline std__weak_ptr_HybridNitroMetamaskSpec_ weakify_std__shared_ptr_HybridNitroMetamaskSpec_(const std::shared_ptr<HybridNitroMetamaskSpec>& strong) noexcept { return strong; }
|
|
138
154
|
|
|
155
|
+
// pragma MARK: Result<void>
|
|
156
|
+
using Result_void_ = Result<void>;
|
|
157
|
+
inline Result_void_ create_Result_void_() noexcept {
|
|
158
|
+
return Result<void>::withValue();
|
|
159
|
+
}
|
|
160
|
+
inline Result_void_ create_Result_void_(const std::exception_ptr& error) noexcept {
|
|
161
|
+
return Result<void>::withError(error);
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
// pragma MARK: Result<std::shared_ptr<Promise<ConnectResult>>>
|
|
140
165
|
using Result_std__shared_ptr_Promise_ConnectResult___ = Result<std::shared_ptr<Promise<ConnectResult>>>;
|
|
141
166
|
inline Result_std__shared_ptr_Promise_ConnectResult___ create_Result_std__shared_ptr_Promise_ConnectResult___(const std::shared_ptr<Promise<ConnectResult>>& value) noexcept {
|
|
@@ -15,9 +15,10 @@ namespace NitroMetamask { class HybridNitroMetamaskSpec_cxx; }
|
|
|
15
15
|
// Forward declaration of `ConnectResult` to properly resolve imports.
|
|
16
16
|
namespace margelo::nitro::nitrometamask { struct ConnectResult; }
|
|
17
17
|
|
|
18
|
+
#include <string>
|
|
19
|
+
#include <optional>
|
|
18
20
|
#include "ConnectResult.hpp"
|
|
19
21
|
#include <NitroModules/Promise.hpp>
|
|
20
|
-
#include <string>
|
|
21
22
|
|
|
22
23
|
#include "NitroMetamask-Swift-Cxx-Umbrella.hpp"
|
|
23
24
|
|
|
@@ -63,6 +64,12 @@ namespace margelo::nitro::nitrometamask {
|
|
|
63
64
|
|
|
64
65
|
public:
|
|
65
66
|
// Methods
|
|
67
|
+
inline void configure(const std::optional<std::string>& dappUrl) override {
|
|
68
|
+
auto __result = _swiftPart.configure(dappUrl);
|
|
69
|
+
if (__result.hasError()) [[unlikely]] {
|
|
70
|
+
std::rethrow_exception(__result.error());
|
|
71
|
+
}
|
|
72
|
+
}
|
|
66
73
|
inline std::shared_ptr<Promise<ConnectResult>> connect() override {
|
|
67
74
|
auto __result = _swiftPart.connect();
|
|
68
75
|
if (__result.hasError()) [[unlikely]] {
|
|
@@ -79,6 +86,14 @@ namespace margelo::nitro::nitrometamask {
|
|
|
79
86
|
auto __value = std::move(__result.value());
|
|
80
87
|
return __value;
|
|
81
88
|
}
|
|
89
|
+
inline std::shared_ptr<Promise<std::string>> connectSign(const std::string& nonce, int64_t exp) override {
|
|
90
|
+
auto __result = _swiftPart.connectSign(nonce, std::forward<decltype(exp)>(exp));
|
|
91
|
+
if (__result.hasError()) [[unlikely]] {
|
|
92
|
+
std::rethrow_exception(__result.error());
|
|
93
|
+
}
|
|
94
|
+
auto __value = std::move(__result.value());
|
|
95
|
+
return __value;
|
|
96
|
+
}
|
|
82
97
|
|
|
83
98
|
private:
|
|
84
99
|
NitroMetamask::HybridNitroMetamaskSpec_cxx _swiftPart;
|
|
@@ -14,8 +14,10 @@ public protocol HybridNitroMetamaskSpec_protocol: HybridObject {
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
// Methods
|
|
17
|
+
func configure(dappUrl: String?) throws -> Void
|
|
17
18
|
func connect() throws -> Promise<ConnectResult>
|
|
18
19
|
func signMessage(message: String) throws -> Promise<String>
|
|
20
|
+
func connectSign(nonce: String, exp: Int64) throws -> Promise<String>
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
public extension HybridNitroMetamaskSpec_protocol {
|
|
@@ -117,6 +117,24 @@ open class HybridNitroMetamaskSpec_cxx {
|
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
// Methods
|
|
120
|
+
@inline(__always)
|
|
121
|
+
public final func configure(dappUrl: bridge.std__optional_std__string_) -> bridge.Result_void_ {
|
|
122
|
+
do {
|
|
123
|
+
try self.__implementation.configure(dappUrl: { () -> String? in
|
|
124
|
+
if bridge.has_value_std__optional_std__string_(dappUrl) {
|
|
125
|
+
let __unwrapped = bridge.get_std__optional_std__string_(dappUrl)
|
|
126
|
+
return String(__unwrapped)
|
|
127
|
+
} else {
|
|
128
|
+
return nil
|
|
129
|
+
}
|
|
130
|
+
}())
|
|
131
|
+
return bridge.create_Result_void_()
|
|
132
|
+
} catch (let __error) {
|
|
133
|
+
let __exceptionPtr = __error.toCpp()
|
|
134
|
+
return bridge.create_Result_void_(__exceptionPtr)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
120
138
|
@inline(__always)
|
|
121
139
|
public final func connect() -> bridge.Result_std__shared_ptr_Promise_ConnectResult___ {
|
|
122
140
|
do {
|
|
@@ -154,4 +172,23 @@ open class HybridNitroMetamaskSpec_cxx {
|
|
|
154
172
|
return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr)
|
|
155
173
|
}
|
|
156
174
|
}
|
|
175
|
+
|
|
176
|
+
@inline(__always)
|
|
177
|
+
public final func connectSign(nonce: std.string, exp: Int64) -> bridge.Result_std__shared_ptr_Promise_std__string___ {
|
|
178
|
+
do {
|
|
179
|
+
let __result = try self.__implementation.connectSign(nonce: String(nonce), exp: exp)
|
|
180
|
+
let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in
|
|
181
|
+
let __promise = bridge.create_std__shared_ptr_Promise_std__string__()
|
|
182
|
+
let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise)
|
|
183
|
+
__result
|
|
184
|
+
.then({ __result in __promiseHolder.resolve(std.string(__result)) })
|
|
185
|
+
.catch({ __error in __promiseHolder.reject(__error.toCpp()) })
|
|
186
|
+
return __promise
|
|
187
|
+
}()
|
|
188
|
+
return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp)
|
|
189
|
+
} catch (let __error) {
|
|
190
|
+
let __exceptionPtr = __error.toCpp()
|
|
191
|
+
return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
157
194
|
}
|
|
@@ -14,8 +14,10 @@ namespace margelo::nitro::nitrometamask {
|
|
|
14
14
|
HybridObject::loadHybridMethods();
|
|
15
15
|
// load custom methods/properties
|
|
16
16
|
registerHybrids(this, [](Prototype& prototype) {
|
|
17
|
+
prototype.registerHybridMethod("configure", &HybridNitroMetamaskSpec::configure);
|
|
17
18
|
prototype.registerHybridMethod("connect", &HybridNitroMetamaskSpec::connect);
|
|
18
19
|
prototype.registerHybridMethod("signMessage", &HybridNitroMetamaskSpec::signMessage);
|
|
20
|
+
prototype.registerHybridMethod("connectSign", &HybridNitroMetamaskSpec::connectSign);
|
|
19
21
|
});
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
// Forward declaration of `ConnectResult` to properly resolve imports.
|
|
17
17
|
namespace margelo::nitro::nitrometamask { struct ConnectResult; }
|
|
18
18
|
|
|
19
|
+
#include <string>
|
|
20
|
+
#include <optional>
|
|
19
21
|
#include "ConnectResult.hpp"
|
|
20
22
|
#include <NitroModules/Promise.hpp>
|
|
21
|
-
#include <string>
|
|
22
23
|
|
|
23
24
|
namespace margelo::nitro::nitrometamask {
|
|
24
25
|
|
|
@@ -51,8 +52,10 @@ namespace margelo::nitro::nitrometamask {
|
|
|
51
52
|
|
|
52
53
|
public:
|
|
53
54
|
// Methods
|
|
55
|
+
virtual void configure(const std::optional<std::string>& dappUrl) = 0;
|
|
54
56
|
virtual std::shared_ptr<Promise<ConnectResult>> connect() = 0;
|
|
55
57
|
virtual std::shared_ptr<Promise<std::string>> signMessage(const std::string& message) = 0;
|
|
58
|
+
virtual std::shared_ptr<Promise<std::string>> connectSign(const std::string& nonce, int64_t exp) = 0;
|
|
56
59
|
|
|
57
60
|
protected:
|
|
58
61
|
// Hybrid Setup
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novastera-oss/nitro-metamask",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Novastera metamask authentication with native mobile libraries",
|
|
5
5
|
"main": "./lib/commonjs/index.js",
|
|
6
6
|
"module": "./lib/module/index.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"clean": "git clean -dfX",
|
|
13
13
|
"release": "semantic-release",
|
|
14
14
|
"build": "npm run typecheck && bob build",
|
|
15
|
-
"codegen": "nitrogen --logLevel=\"debug\" && npm run build
|
|
15
|
+
"codegen": "nitrogen --logLevel=\"debug\" && npm run build"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"react-native",
|
package/react-native.config.js
CHANGED
|
@@ -11,7 +11,7 @@ module.exports = {
|
|
|
11
11
|
* @type {import('@react-native-community/cli-types').AndroidDependencyParams}
|
|
12
12
|
*/
|
|
13
13
|
android: {
|
|
14
|
-
packageImportPath: 'import com.nitrometamask.NitroMetamaskPackage;',
|
|
14
|
+
packageImportPath: 'import com.margelo.nitro.nitrometamask.NitroMetamaskPackage;',
|
|
15
15
|
packageInstance: 'new NitroMetamaskPackage()',
|
|
16
16
|
},
|
|
17
17
|
},
|
|
@@ -6,6 +6,16 @@ export interface ConnectResult {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export interface NitroMetamask extends HybridObject<{ ios: 'swift', android: 'kotlin' }> {
|
|
9
|
+
/**
|
|
10
|
+
* Configure the dapp URL for MetaMask SDK validation.
|
|
11
|
+
* This URL is only used for SDK validation - the deep link return is handled automatically via AndroidManifest.xml.
|
|
12
|
+
*
|
|
13
|
+
* @param dappUrl - A valid HTTP/HTTPS URL (e.g., "https://yourdomain.com").
|
|
14
|
+
* If not provided, defaults to "https://novastera.com".
|
|
15
|
+
* This is separate from the deep link scheme which is auto-detected from your manifest.
|
|
16
|
+
*/
|
|
17
|
+
configure(dappUrl?: string): void
|
|
9
18
|
connect(): Promise<ConnectResult>
|
|
10
19
|
signMessage(message: string): Promise<string>
|
|
20
|
+
connectSign(nonce: string, exp: bigint): Promise<string>
|
|
11
21
|
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
package com.nitrometamask
|
|
2
|
-
|
|
3
|
-
import com.margelo.nitro.core.Promise
|
|
4
|
-
import com.margelo.nitro.nitrometamask.HybridNitroMetamaskSpec
|
|
5
|
-
import com.margelo.nitro.nitrometamask.ConnectResult
|
|
6
|
-
import io.metamask.androidsdk.Ethereum
|
|
7
|
-
import io.metamask.androidsdk.Result
|
|
8
|
-
import io.metamask.androidsdk.DappMetadata
|
|
9
|
-
import io.metamask.androidsdk.SDKOptions
|
|
10
|
-
import io.metamask.androidsdk.EthereumRequest
|
|
11
|
-
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
12
|
-
import kotlin.coroutines.resume
|
|
13
|
-
|
|
14
|
-
class HybridNitroMetamask : HybridNitroMetamaskSpec() {
|
|
15
|
-
// Initialize Ethereum SDK with Context, DappMetadata, and SDKOptions
|
|
16
|
-
// Based on: https://github.com/MetaMask/metamask-android-sdk
|
|
17
|
-
// Using MetamaskContextHolder for Context access (Nitro doesn't provide Context APIs)
|
|
18
|
-
// This pattern matches how other Nitro modules handle Context (VisionCamera, MMKV, etc.)
|
|
19
|
-
private val ethereum: Ethereum by lazy {
|
|
20
|
-
val context = MetamaskContextHolder.get()
|
|
21
|
-
|
|
22
|
-
val dappMetadata = DappMetadata(
|
|
23
|
-
name = "Nitro MetaMask Connector",
|
|
24
|
-
url = "https://novastera.com"
|
|
25
|
-
)
|
|
26
|
-
// SDKOptions constructor requires infuraAPIKey and readonlyRPCMap parameters
|
|
27
|
-
// They can be null for basic usage without Infura or custom RPC
|
|
28
|
-
val sdkOptions = SDKOptions(
|
|
29
|
-
infuraAPIKey = null,
|
|
30
|
-
readonlyRPCMap = null
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
Ethereum(context, dappMetadata, sdkOptions)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
override fun connect(): Promise<ConnectResult> {
|
|
37
|
-
// Use Promise.async with coroutines for best practice in Nitro modules
|
|
38
|
-
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
39
|
-
return Promise.async {
|
|
40
|
-
// Convert callback-based connect() to suspend function using suspendCancellableCoroutine
|
|
41
|
-
// This handles cancellation properly when JS GC disposes the promise
|
|
42
|
-
val result = suspendCancellableCoroutine<Result> { continuation ->
|
|
43
|
-
ethereum.connect { callbackResult ->
|
|
44
|
-
if (continuation.isActive) {
|
|
45
|
-
continuation.resume(callbackResult)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
when (result) {
|
|
51
|
-
is Result.Success.Item -> {
|
|
52
|
-
// After successful connection, get account info from SDK
|
|
53
|
-
val address = ethereum.selectedAddress
|
|
54
|
-
?: throw IllegalStateException("MetaMask SDK returned no address after connection")
|
|
55
|
-
val chainIdString = ethereum.chainId
|
|
56
|
-
?: throw IllegalStateException("MetaMask SDK returned no chainId after connection")
|
|
57
|
-
|
|
58
|
-
// Parse chainId from hex string (e.g., "0x1") or decimal string to number
|
|
59
|
-
// Nitro requires chainId to be Double (number in TS maps to Double in Kotlin)
|
|
60
|
-
val chainId = try {
|
|
61
|
-
val chainIdInt = if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
|
|
62
|
-
chainIdString.substring(2).toLong(16).toInt()
|
|
63
|
-
} else {
|
|
64
|
-
chainIdString.toLong().toInt()
|
|
65
|
-
}
|
|
66
|
-
chainIdInt.toDouble()
|
|
67
|
-
} catch (e: NumberFormatException) {
|
|
68
|
-
throw IllegalStateException("Invalid chainId format: $chainIdString")
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
ConnectResult(
|
|
72
|
-
address = address,
|
|
73
|
-
chainId = chainId
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
is Result.Success.ItemMap -> {
|
|
77
|
-
// Handle ItemMap case (shouldn't happen for connect, but make exhaustive)
|
|
78
|
-
throw IllegalStateException("Unexpected ItemMap result from MetaMask connect")
|
|
79
|
-
}
|
|
80
|
-
is Result.Success.Items -> {
|
|
81
|
-
// Handle Items case (shouldn't happen for connect, but make exhaustive)
|
|
82
|
-
throw IllegalStateException("Unexpected Items result from MetaMask connect")
|
|
83
|
-
}
|
|
84
|
-
is Result.Error -> {
|
|
85
|
-
// Result.Error contains the error directly
|
|
86
|
-
val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask connection failed"
|
|
87
|
-
throw Exception(errorMessage)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
override fun signMessage(message: String): Promise<String> {
|
|
94
|
-
// Use Promise.async with coroutines for best practice in Nitro modules
|
|
95
|
-
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
96
|
-
return Promise.async {
|
|
97
|
-
// Verify connection state before attempting to sign
|
|
98
|
-
// MetaMask SDK requires an active connection to sign messages
|
|
99
|
-
val address = ethereum.selectedAddress
|
|
100
|
-
if (address.isNullOrEmpty()) {
|
|
101
|
-
throw IllegalStateException("No connected account. Please call connect() first to establish a connection with MetaMask.")
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Create EthereumRequest for personal_sign
|
|
105
|
-
// Based on MetaMask Android SDK docs: params are [account, message]
|
|
106
|
-
// Reference: https://github.com/MetaMask/metamask-android-sdk
|
|
107
|
-
// EthereumRequest constructor expects method as String
|
|
108
|
-
val request = EthereumRequest(
|
|
109
|
-
method = "personal_sign",
|
|
110
|
-
params = listOf(address, message)
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
// Convert callback-based sendRequest() to suspend function
|
|
114
|
-
val result = suspendCancellableCoroutine<Result> { continuation ->
|
|
115
|
-
ethereum.sendRequest(request) { callbackResult ->
|
|
116
|
-
if (continuation.isActive) {
|
|
117
|
-
continuation.resume(callbackResult)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
when (result) {
|
|
123
|
-
is Result.Success.Item -> {
|
|
124
|
-
// Extract signature from response
|
|
125
|
-
// The signature should be a hex-encoded string (0x-prefixed)
|
|
126
|
-
val signature = result.value as? String
|
|
127
|
-
?: throw Exception("Invalid signature response format")
|
|
128
|
-
signature
|
|
129
|
-
}
|
|
130
|
-
is Result.Success.ItemMap -> {
|
|
131
|
-
// Handle ItemMap case (shouldn't happen for signMessage, but make exhaustive)
|
|
132
|
-
throw IllegalStateException("Unexpected ItemMap result from MetaMask signMessage")
|
|
133
|
-
}
|
|
134
|
-
is Result.Success.Items -> {
|
|
135
|
-
// Handle Items case (shouldn't happen for signMessage, but make exhaustive)
|
|
136
|
-
throw IllegalStateException("Unexpected Items result from MetaMask signMessage")
|
|
137
|
-
}
|
|
138
|
-
is Result.Error -> {
|
|
139
|
-
// Result.Error contains the error directly
|
|
140
|
-
val errorMessage = result.error?.message ?: result.error?.toString() ?: "MetaMask signing failed"
|
|
141
|
-
throw Exception(errorMessage)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|