@novastera-oss/nitro-metamask 0.2.2 → 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,64 +1,123 @@
1
1
  package com.margelo.nitro.nitrometamask
2
2
 
3
+ import com.margelo.nitro.core.Promise
4
+ import com.margelo.nitro.core.NitroRuntime
3
5
  import io.metamask.androidsdk.Ethereum
6
+ import io.metamask.androidsdk.Result
7
+ import io.metamask.androidsdk.DappMetadata
8
+ import io.metamask.androidsdk.SDKOptions
4
9
  import io.metamask.androidsdk.EthereumRequest
5
10
  import io.metamask.androidsdk.EthereumMethod
6
- import io.metamask.androidsdk.Result
11
+ import kotlinx.coroutines.suspendCancellableCoroutine
12
+ import kotlin.coroutines.resume
7
13
 
8
14
  class HybridMetamaskConnector : HybridMetamaskConnectorSpec() {
9
- private val ethereum by lazy {
10
- Ethereum.getInstance()
15
+ // Initialize Ethereum SDK with Context, DappMetadata, and SDKOptions
16
+ // Based on: https://github.com/MetaMask/metamask-android-sdk
17
+ // Using NitroRuntime for proper Nitro context access (survives reloads, no static leaks)
18
+ private val ethereum: Ethereum by lazy {
19
+ val context = NitroRuntime.applicationContext
20
+ ?: throw IllegalStateException("Nitro application context not available")
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)
11
34
  }
12
35
 
13
- override suspend fun connect(): ConnectResult {
14
- val result = ethereum.connect()
15
- return when (result) {
16
- is Result.Success.Item -> {
17
- // result.value contains the connection result
18
- // Based on SDK docs, this should contain address and chainId
19
- val connectionResult = result.value
20
- // Extract address and chainId from the result
21
- // The SDK returns account info in result.value
22
- val address = ethereum.selectedAddress
23
- ?: throw IllegalStateException("MetaMask SDK returned no address")
24
- val chainId = ethereum.chainId
25
- ?: throw IllegalStateException("MetaMask SDK returned no chainId")
26
-
27
- ConnectResult(
28
- address = address,
29
- chainId = chainId.toString()
30
- )
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
+ }
31
48
  }
32
- is Result.Error -> {
33
- throw Exception(result.error.message ?: "Failed to connect to MetaMask")
34
- }
35
- else -> {
36
- throw IllegalStateException("Unexpected result type from MetaMask connect")
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
+ 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
+
69
+ ConnectResult(
70
+ address = address,
71
+ chainId = chainId
72
+ )
73
+ }
74
+ is Result.Error -> {
75
+ throw Exception(result.error.message ?: "Failed to connect to MetaMask")
76
+ }
77
+ else -> {
78
+ throw IllegalStateException("Unexpected result type from MetaMask connect")
79
+ }
37
80
  }
38
81
  }
39
82
  }
40
83
 
41
- override suspend fun signMessage(message: String): String {
42
- val address = ethereum.selectedAddress
43
- ?: throw IllegalStateException("No connected account. Call connect() first.")
44
-
45
- // Based on SDK documentation, personal_sign params are: [address, message]
46
- // The SDK handles message encoding internally
47
- val request = EthereumRequest(
48
- method = EthereumMethod.PERSONAL_SIGN.value,
49
- params = listOf(address, message)
50
- )
51
-
52
- val result = ethereum.sendRequest(request)
53
- return when (result) {
54
- is Result.Success.Item -> {
55
- result.value as? String ?: throw IllegalStateException("Invalid signature response")
56
- }
57
- is Result.Error -> {
58
- throw Exception(result.error.message ?: "Failed to sign message")
84
+ override fun signMessage(message: String): Promise<String> {
85
+ // Use Promise.async with coroutines for best practice in Nitro modules
86
+ // Reference: https://nitro.margelo.com/docs/types/promises
87
+ return Promise.async {
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
+ }
59
107
  }
60
- else -> {
61
- throw IllegalStateException("Unexpected result type from MetaMask signMessage")
108
+
109
+ when (result) {
110
+ is Result.Success.Item -> {
111
+ // Extract signature from result
112
+ result.value as? String
113
+ ?: throw IllegalStateException("Invalid signature response format")
114
+ }
115
+ is Result.Error -> {
116
+ throw Exception(result.error.message ?: "Failed to sign message")
117
+ }
118
+ else -> {
119
+ throw IllegalStateException("Unexpected result type from MetaMask signMessage")
120
+ }
62
121
  }
63
122
  }
64
123
  }
@@ -6,7 +6,10 @@ import com.facebook.react.module.model.ReactModuleInfoProvider
6
6
  import com.facebook.react.BaseReactPackage
7
7
 
8
8
  class NitroMetamaskPackage : BaseReactPackage() {
9
- override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
9
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
10
+ // No need to store ReactApplicationContext - HybridMetamaskConnector uses NitroRuntime
11
+ return null
12
+ }
10
13
 
11
14
  override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { HashMap() }
12
15
 
@@ -1,53 +1,96 @@
1
- import Foundation
1
+ import NitroModules
2
2
  import MetaMaskSDK
3
+ import Foundation
3
4
 
4
- class HybridMetamaskConnector: HybridMetamaskConnectorSpec {
5
+ final class HybridMetamaskConnector: HybridMetamaskConnectorSpec {
5
6
  private let sdk = MetaMaskSDK.shared
6
7
 
7
- func connect() async throws -> ConnectResult {
8
- // Based on MetaMask iOS SDK docs: let connectResult = await metamaskSDK.connect()
9
- let connectResult = await sdk.connect()
10
-
11
- switch connectResult {
12
- case .success(let value):
13
- // After successful connection, get account info from SDK
14
- guard let account = sdk.account else {
15
- throw NSError(domain: "MetamaskConnector", code: -1, userInfo: [NSLocalizedDescriptionKey: "MetaMask SDK returned no account"])
8
+ func connect() -> Promise<ConnectResult> {
9
+ // Use Promise.async with Swift async/await for best practice in Nitro modules
10
+ // Reference: https://nitro.margelo.com/docs/types/promises
11
+ return Promise.async {
12
+ // Based on MetaMask iOS SDK docs: let connectResult = await metamaskSDK.connect()
13
+ // Reference: https://github.com/MetaMask/metamask-ios-sdk
14
+ let connectResult = try await self.sdk.connect()
15
+
16
+ switch connectResult {
17
+ case .success:
18
+ // After successful connection, get account info from SDK
19
+ // Note: sdk.account is a String (address), not an object
20
+ // Reference: https://raw.githubusercontent.com/MetaMask/metamask-ios-sdk/924d91bb3e98a5383c3082d6d5ba3ddac9e1c565/README.md
21
+ guard let address = self.sdk.account, !address.isEmpty else {
22
+ throw NSError(
23
+ domain: "MetamaskConnector",
24
+ code: -1,
25
+ userInfo: [NSLocalizedDescriptionKey: "MetaMask SDK returned no address after connection"]
26
+ )
27
+ }
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
+ )
41
+ }
42
+
43
+ return ConnectResult(
44
+ address: address,
45
+ chainId: Double(chainIdInt)
46
+ )
47
+
48
+ case .failure(let error):
49
+ throw error
16
50
  }
17
- return ConnectResult(address: account.address, chainId: "\(account.chainId)")
18
- case .failure(let error):
19
- throw error
20
51
  }
21
52
  }
22
53
 
23
- func signMessage(message: String) async throws -> String {
24
- // Get the connected account address
25
- guard let account = sdk.account else {
26
- throw NSError(domain: "MetamaskConnector", code: -1, userInfo: [NSLocalizedDescriptionKey: "No connected account. Call connect() first."])
27
- }
28
-
29
- // Based on MetaMask iOS SDK docs, personal_sign params are: [account, message]
30
- // The SDK handles message encoding internally
31
- // Reference: https://github.com/MetaMask/metamask-ios-sdk
32
- let params: [String] = [account.address, message]
33
-
34
- // Create EthereumRequest for personal_sign JSON-RPC method
35
- let request = EthereumRequest(
36
- method: .personalSign,
37
- params: params
38
- )
39
-
40
- // Make the request using the SDK's async request method
41
- let result = try await sdk.request(request)
42
-
43
- // Extract signature from response
44
- // The signature should be a hex-encoded string (0x-prefixed)
45
- if let signature = result as? String {
46
- return signature
47
- } else if let dict = result as? [String: Any], let sig = dict["signature"] as? String ?? dict["result"] as? String {
48
- return sig
49
- } else {
50
- throw NSError(domain: "MetamaskConnector", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid signature response format"])
54
+ func signMessage(message: String) -> Promise<String> {
55
+ // Use Promise.async with Swift async/await for best practice in Nitro modules
56
+ // Reference: https://nitro.margelo.com/docs/types/promises
57
+ return Promise.async {
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]
71
+ // Reference: https://raw.githubusercontent.com/MetaMask/metamask-ios-sdk/924d91bb3e98a5383c3082d6d5ba3ddac9e1c565/README.md
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)
80
+
81
+ // Extract signature from response
82
+ // The signature should be a hex-encoded string (0x-prefixed)
83
+ if let signature = result as? String {
84
+ return signature
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
+ )
93
+ }
51
94
  }
52
95
  }
53
96
  }
@@ -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.2",
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