@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.
- package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridMetamaskConnector.kt +105 -46
- package/android/src/main/java/com/margelo/nitro/nitrometamask/NitroMetamaskPackage.kt +4 -1
- package/ios/HybridMetamaskConnector.swift +85 -42
- package/lib/MetamaskConnector.nitro.d.ts +1 -1
- package/nitrogen/generated/android/c++/JConnectResult.hpp +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/ConnectResult.kt +2 -2
- package/nitrogen/generated/ios/swift/ConnectResult.swift +4 -4
- package/nitrogen/generated/shared/c++/ConnectResult.hpp +5 -5
- package/package.json +1 -1
- package/src/MetamaskConnector.nitro.ts +1 -1
|
@@ -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
|
|
11
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
12
|
+
import kotlin.coroutines.resume
|
|
7
13
|
|
|
8
14
|
class HybridMetamaskConnector : HybridMetamaskConnectorSpec() {
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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?
|
|
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
|
|
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()
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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)
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
}
|
|
@@ -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<
|
|
37
|
-
|
|
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
|
|
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>,
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
23
|
-
self.init(std.string(address),
|
|
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:
|
|
33
|
-
return
|
|
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
|
-
|
|
43
|
+
double chainId SWIFT_PRIVATE;
|
|
44
44
|
|
|
45
45
|
public:
|
|
46
46
|
ConnectResult() = default;
|
|
47
|
-
explicit ConnectResult(std::string address,
|
|
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<
|
|
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<
|
|
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<
|
|
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