@novastera-oss/nitro-metamask 0.2.3 → 0.2.4

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.
@@ -1,35 +1,34 @@
1
1
  package com.margelo.nitro.nitrometamask
2
2
 
3
- import com.margelo.nitro.Promise
3
+ import com.margelo.nitro.core.Promise
4
+ import com.margelo.nitro.core.NitroRuntime
4
5
  import io.metamask.androidsdk.Ethereum
5
6
  import io.metamask.androidsdk.Result
6
7
  import io.metamask.androidsdk.DappMetadata
7
8
  import io.metamask.androidsdk.SDKOptions
8
- import com.facebook.react.bridge.ReactApplicationContext
9
- import kotlinx.coroutines.suspendCoroutine
9
+ import io.metamask.androidsdk.EthereumRequest
10
+ import io.metamask.androidsdk.EthereumMethod
11
+ import kotlinx.coroutines.suspendCancellableCoroutine
12
+ import kotlin.coroutines.resume
10
13
 
11
14
  class HybridMetamaskConnector : HybridMetamaskConnectorSpec() {
12
- companion object {
13
- @Volatile
14
- private var reactContext: ReactApplicationContext? = null
15
-
16
- fun setReactContext(context: ReactApplicationContext) {
17
- reactContext = context
18
- }
19
- }
20
-
21
15
  // Initialize Ethereum SDK with Context, DappMetadata, and SDKOptions
22
16
  // Based on: https://github.com/MetaMask/metamask-android-sdk
23
- // The SDK requires a Context for initialization
17
+ // Using NitroRuntime for proper Nitro context access (survives reloads, no static leaks)
24
18
  private val ethereum: Ethereum by lazy {
25
- val context = reactContext?.applicationContext
26
- ?: throw IllegalStateException("ReactApplicationContext not initialized. Make sure NitroMetamaskPackage is properly registered.")
19
+ val context = NitroRuntime.applicationContext
20
+ ?: throw IllegalStateException("Nitro application context not available")
27
21
 
28
22
  val dappMetadata = DappMetadata(
29
23
  name = "Nitro MetaMask Connector",
30
24
  url = "https://novastera.com"
31
25
  )
32
- val sdkOptions = SDKOptions()
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
+ )
33
32
 
34
33
  Ethereum(context, dappMetadata, sdkOptions)
35
34
  }
@@ -38,10 +37,13 @@ class HybridMetamaskConnector : HybridMetamaskConnectorSpec() {
38
37
  // Use Promise.async with coroutines for best practice in Nitro modules
39
38
  // Reference: https://nitro.margelo.com/docs/types/promises
40
39
  return Promise.async {
41
- // Convert callback-based connect() to suspend function using suspendCoroutine
42
- val result = suspendCoroutine<Result> { continuation ->
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
43
  ethereum.connect { callbackResult ->
44
- continuation.resume(callbackResult)
44
+ if (continuation.isActive) {
45
+ continuation.resume(callbackResult)
46
+ }
45
47
  }
46
48
  }
47
49
 
@@ -50,12 +52,23 @@ class HybridMetamaskConnector : HybridMetamaskConnectorSpec() {
50
52
  // After successful connection, get account info from SDK
51
53
  val address = ethereum.selectedAddress
52
54
  ?: throw IllegalStateException("MetaMask SDK returned no address after connection")
53
- val chainId = ethereum.chainId
55
+ val chainIdString = ethereum.chainId
54
56
  ?: throw IllegalStateException("MetaMask SDK returned no chainId after connection")
55
57
 
58
+ // Parse chainId from hex string (e.g., "0x1") or decimal string to number
59
+ val chainId = try {
60
+ if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
61
+ chainIdString.substring(2).toLong(16).toInt()
62
+ } else {
63
+ chainIdString.toLong().toInt()
64
+ }
65
+ } catch (e: NumberFormatException) {
66
+ throw IllegalStateException("Invalid chainId format: $chainIdString")
67
+ }
68
+
56
69
  ConnectResult(
57
70
  address = address,
58
- chainId = chainId.toString()
71
+ chainId = chainId
59
72
  )
60
73
  }
61
74
  is Result.Error -> {
@@ -72,10 +85,28 @@ class HybridMetamaskConnector : HybridMetamaskConnectorSpec() {
72
85
  // Use Promise.async with coroutines for best practice in Nitro modules
73
86
  // Reference: https://nitro.margelo.com/docs/types/promises
74
87
  return Promise.async {
75
- // Use the convenience method connectSign() which connects and signs in one call
76
- // Based on SDK docs: ethereum.connectSign(message) returns Result synchronously
77
- // Reference: https://github.com/MetaMask/metamask-android-sdk
78
- when (val result = ethereum.connectSign(message)) {
88
+ // Use direct signMessage() method (requires connection first via connect())
89
+ // This is more explicit and predictable than connectSign() which forces connection
90
+ // Based on SDK docs: ethereum.signMessage() requires address and message
91
+ val address = ethereum.selectedAddress
92
+ ?: throw IllegalStateException("No connected account. Call connect() first.")
93
+
94
+ // Create EthereumRequest for personal_sign
95
+ val request = EthereumRequest(
96
+ method = EthereumMethod.PERSONAL_SIGN.value,
97
+ params = listOf(address, message)
98
+ )
99
+
100
+ // Convert callback-based sendRequest() to suspend function
101
+ val result = suspendCancellableCoroutine<Result> { continuation ->
102
+ ethereum.sendRequest(request) { callbackResult ->
103
+ if (continuation.isActive) {
104
+ continuation.resume(callbackResult)
105
+ }
106
+ }
107
+ }
108
+
109
+ when (result) {
79
110
  is Result.Success.Item -> {
80
111
  // Extract signature from result
81
112
  result.value as? String
@@ -7,8 +7,7 @@ import com.facebook.react.BaseReactPackage
7
7
 
8
8
  class NitroMetamaskPackage : BaseReactPackage() {
9
9
  override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
10
- // Store the ReactApplicationContext for use in HybridMetamaskConnector
11
- HybridMetamaskConnector.setReactContext(reactContext)
10
+ // No need to store ReactApplicationContext - HybridMetamaskConnector uses NitroRuntime
12
11
  return null
13
12
  }
14
13
 
@@ -1,8 +1,8 @@
1
- import Foundation
2
- import MetaMaskSDK
3
1
  import NitroModules
2
+ import MetaMaskSDK
3
+ import Foundation
4
4
 
5
- class HybridMetamaskConnector: HybridMetamaskConnectorSpec {
5
+ final class HybridMetamaskConnector: HybridMetamaskConnectorSpec {
6
6
  private let sdk = MetaMaskSDK.shared
7
7
 
8
8
  func connect() -> Promise<ConnectResult> {
@@ -14,17 +14,37 @@ class HybridMetamaskConnector: HybridMetamaskConnectorSpec {
14
14
  let connectResult = try await self.sdk.connect()
15
15
 
16
16
  switch connectResult {
17
- case .success(let value):
17
+ case .success:
18
18
  // After successful connection, get account info from SDK
19
19
  // Note: sdk.account is a String (address), not an object
20
20
  // Reference: https://raw.githubusercontent.com/MetaMask/metamask-ios-sdk/924d91bb3e98a5383c3082d6d5ba3ddac9e1c565/README.md
21
21
  guard let address = self.sdk.account, !address.isEmpty else {
22
- throw NSError(domain: "MetamaskConnector", code: -1, userInfo: [NSLocalizedDescriptionKey: "MetaMask SDK returned no address after connection"])
22
+ throw NSError(
23
+ domain: "MetamaskConnector",
24
+ code: -1,
25
+ userInfo: [NSLocalizedDescriptionKey: "MetaMask SDK returned no address after connection"]
26
+ )
23
27
  }
24
- guard let chainId = self.sdk.chainId, !chainId.isEmpty else {
25
- throw NSError(domain: "MetamaskConnector", code: -1, userInfo: [NSLocalizedDescriptionKey: "MetaMask SDK returned no chainId after connection"])
28
+
29
+ // Parse chainId from hex string (e.g., "0x1") to number
30
+ // Nitro requires chainId to be a number, not a string, for type safety
31
+ guard
32
+ let chainIdHex = self.sdk.chainId,
33
+ !chainIdHex.isEmpty,
34
+ let chainIdInt = Int(chainIdHex.replacingOccurrences(of: "0x", with: ""), radix: 16)
35
+ else {
36
+ throw NSError(
37
+ domain: "MetamaskConnector",
38
+ code: -1,
39
+ userInfo: [NSLocalizedDescriptionKey: "Invalid chainId format"]
40
+ )
26
41
  }
27
- return ConnectResult(address: address, chainId: chainId)
42
+
43
+ return ConnectResult(
44
+ address: address,
45
+ chainId: Double(chainIdInt)
46
+ )
47
+
28
48
  case .failure(let error):
29
49
  throw error
30
50
  }
@@ -35,17 +55,41 @@ class HybridMetamaskConnector: HybridMetamaskConnectorSpec {
35
55
  // Use Promise.async with Swift async/await for best practice in Nitro modules
36
56
  // Reference: https://nitro.margelo.com/docs/types/promises
37
57
  return Promise.async {
38
- // Use the convenience method connectAndSign() which connects and signs in one call
39
- // This is equivalent to Android's connectSign() method
58
+ // Use explicit sign() method (requires connection first via connect())
59
+ // This is more explicit and predictable than connectAndSign() which forces connection
60
+ // Nitro encourages explicit object state, not convenience shortcuts
61
+ guard let account = self.sdk.account, !account.isEmpty else {
62
+ throw NSError(
63
+ domain: "MetamaskConnector",
64
+ code: -1,
65
+ userInfo: [NSLocalizedDescriptionKey: "No connected account. Call connect() first."]
66
+ )
67
+ }
68
+
69
+ // Create EthereumRequest for personal_sign
70
+ // Based on MetaMask iOS SDK docs: params are [account, message]
40
71
  // Reference: https://raw.githubusercontent.com/MetaMask/metamask-ios-sdk/924d91bb3e98a5383c3082d6d5ba3ddac9e1c565/README.md
41
- // Example: https://raw.githubusercontent.com/MetaMask/metamask-ios-sdk/924d91bb3e98a5383c3082d6d5ba3ddac9e1c565/Example/metamask-ios-sdk/SignView.swift
42
- let connectSignResult = try await self.sdk.connectAndSign(message: message)
72
+ let params: [String] = [account, message]
73
+ let request = EthereumRequest(
74
+ method: .personalSign,
75
+ params: params
76
+ )
77
+
78
+ // Make the request using the SDK's async request method
79
+ let result = try await self.sdk.request(request)
43
80
 
44
- switch connectSignResult {
45
- case .success(let signature):
81
+ // Extract signature from response
82
+ // The signature should be a hex-encoded string (0x-prefixed)
83
+ if let signature = result as? String {
46
84
  return signature
47
- case .failure(let error):
48
- throw error
85
+ } else if let dict = result as? [String: Any], let sig = dict["signature"] as? String ?? dict["result"] as? String {
86
+ return sig
87
+ } else {
88
+ throw NSError(
89
+ domain: "MetamaskConnector",
90
+ code: -1,
91
+ userInfo: [NSLocalizedDescriptionKey: "Invalid signature response format"]
92
+ )
49
93
  }
50
94
  }
51
95
  }
@@ -1,7 +1,7 @@
1
1
  import type { HybridObject } from 'react-native-nitro-modules';
2
2
  export interface ConnectResult {
3
3
  address: string;
4
- chainId: string;
4
+ chainId: number;
5
5
  }
6
6
  export interface MetamaskConnector extends HybridObject<{
7
7
  ios: 'swift';
@@ -33,11 +33,11 @@ namespace margelo::nitro::nitrometamask {
33
33
  static const auto clazz = javaClassStatic();
34
34
  static const auto fieldAddress = clazz->getField<jni::JString>("address");
35
35
  jni::local_ref<jni::JString> address = this->getFieldValue(fieldAddress);
36
- static const auto fieldChainId = clazz->getField<jni::JString>("chainId");
37
- jni::local_ref<jni::JString> chainId = this->getFieldValue(fieldChainId);
36
+ static const auto fieldChainId = clazz->getField<double>("chainId");
37
+ double chainId = this->getFieldValue(fieldChainId);
38
38
  return ConnectResult(
39
39
  address->toStdString(),
40
- chainId->toStdString()
40
+ chainId
41
41
  );
42
42
  }
43
43
 
@@ -47,13 +47,13 @@ namespace margelo::nitro::nitrometamask {
47
47
  */
48
48
  [[maybe_unused]]
49
49
  static jni::local_ref<JConnectResult::javaobject> fromCpp(const ConnectResult& value) {
50
- using JSignature = JConnectResult(jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>);
50
+ using JSignature = JConnectResult(jni::alias_ref<jni::JString>, double);
51
51
  static const auto clazz = javaClassStatic();
52
52
  static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
53
53
  return create(
54
54
  clazz,
55
55
  jni::make_jstring(value.address),
56
- jni::make_jstring(value.chainId)
56
+ value.chainId
57
57
  );
58
58
  }
59
59
  };
@@ -22,7 +22,7 @@ data class ConnectResult(
22
22
  val address: String,
23
23
  @DoNotStrip
24
24
  @Keep
25
- val chainId: String
25
+ val chainId: Double
26
26
  ) {
27
27
  /* primary constructor */
28
28
 
@@ -34,7 +34,7 @@ data class ConnectResult(
34
34
  @Keep
35
35
  @Suppress("unused")
36
36
  @JvmStatic
37
- private fun fromCpp(address: String, chainId: String): ConnectResult {
37
+ private fun fromCpp(address: String, chainId: Double): ConnectResult {
38
38
  return ConnectResult(address, chainId)
39
39
  }
40
40
  }
@@ -19,8 +19,8 @@ public extension ConnectResult {
19
19
  /**
20
20
  * Create a new instance of `ConnectResult`.
21
21
  */
22
- init(address: String, chainId: String) {
23
- self.init(std.string(address), std.string(chainId))
22
+ init(address: String, chainId: Double) {
23
+ self.init(std.string(address), chainId)
24
24
  }
25
25
 
26
26
  @inline(__always)
@@ -29,7 +29,7 @@ public extension ConnectResult {
29
29
  }
30
30
 
31
31
  @inline(__always)
32
- var chainId: String {
33
- return String(self.__chainId)
32
+ var chainId: Double {
33
+ return self.__chainId
34
34
  }
35
35
  }
@@ -40,11 +40,11 @@ namespace margelo::nitro::nitrometamask {
40
40
  struct ConnectResult final {
41
41
  public:
42
42
  std::string address SWIFT_PRIVATE;
43
- std::string chainId SWIFT_PRIVATE;
43
+ double chainId SWIFT_PRIVATE;
44
44
 
45
45
  public:
46
46
  ConnectResult() = default;
47
- explicit ConnectResult(std::string address, std::string chainId): address(address), chainId(chainId) {}
47
+ explicit ConnectResult(std::string address, double chainId): address(address), chainId(chainId) {}
48
48
 
49
49
  public:
50
50
  friend bool operator==(const ConnectResult& lhs, const ConnectResult& rhs) = default;
@@ -61,13 +61,13 @@ namespace margelo::nitro {
61
61
  jsi::Object obj = arg.asObject(runtime);
62
62
  return margelo::nitro::nitrometamask::ConnectResult(
63
63
  JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "address"))),
64
- JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chainId")))
64
+ JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chainId")))
65
65
  );
66
66
  }
67
67
  static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitrometamask::ConnectResult& arg) {
68
68
  jsi::Object obj(runtime);
69
69
  obj.setProperty(runtime, PropNameIDCache::get(runtime, "address"), JSIConverter<std::string>::toJSI(runtime, arg.address));
70
- obj.setProperty(runtime, PropNameIDCache::get(runtime, "chainId"), JSIConverter<std::string>::toJSI(runtime, arg.chainId));
70
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "chainId"), JSIConverter<double>::toJSI(runtime, arg.chainId));
71
71
  return obj;
72
72
  }
73
73
  static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
@@ -79,7 +79,7 @@ namespace margelo::nitro {
79
79
  return false;
80
80
  }
81
81
  if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "address")))) return false;
82
- if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chainId")))) return false;
82
+ if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chainId")))) return false;
83
83
  return true;
84
84
  }
85
85
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@novastera-oss/nitro-metamask",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "nitro-metamask",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",
@@ -2,7 +2,7 @@ import type { HybridObject } from 'react-native-nitro-modules'
2
2
 
3
3
  export interface ConnectResult {
4
4
  address: string
5
- chainId: string
5
+ chainId: number
6
6
  }
7
7
 
8
8
  export interface MetamaskConnector