@novastera-oss/nitro-metamask 0.6.3 → 0.7.2
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/NitroMetamask.podspec +12 -3
- package/README.md +3 -1
- package/android/build.gradle +14 -32
- package/android/cargo-ecies.gradle +60 -88
- package/android/src/main/aidl/io/metamask/nativesdk/IMessegeService.aidl +8 -0
- package/android/src/main/aidl/io/metamask/nativesdk/IMessegeServiceCallback.aidl +8 -0
- package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +101 -3
- package/android/src/main/java/io/metamask/androidsdk/AnyRequest.kt +8 -0
- package/android/src/main/java/io/metamask/androidsdk/ClientMessageServiceCallback.kt +12 -0
- package/android/src/main/java/io/metamask/androidsdk/ClientServiceConnection.kt +42 -0
- package/android/src/main/java/io/metamask/androidsdk/CommunicationClient.kt +525 -0
- package/android/src/main/java/io/metamask/androidsdk/CommunicationClientModule.kt +47 -0
- package/android/src/main/java/io/metamask/androidsdk/CommunicationClientModuleInterface.kt +11 -0
- package/android/src/main/java/io/metamask/androidsdk/Constants.kt +5 -0
- package/android/src/main/java/io/metamask/androidsdk/Crypto.kt +35 -0
- package/android/src/main/java/io/metamask/androidsdk/DappMetadata.kt +36 -0
- package/android/src/main/java/io/metamask/androidsdk/Encryption.kt +9 -0
- package/android/src/main/java/io/metamask/androidsdk/ErrorType.kt +41 -0
- package/android/src/main/java/io/metamask/androidsdk/Ethereum.kt +328 -0
- package/android/src/main/java/io/metamask/androidsdk/EthereumEventCallback.kt +6 -0
- package/android/src/main/java/io/metamask/androidsdk/EthereumMethod.kt +80 -0
- package/android/src/main/java/io/metamask/androidsdk/EthereumRequest.kt +7 -0
- package/android/src/main/java/io/metamask/androidsdk/EthereumState.kt +7 -0
- package/android/src/main/java/io/metamask/androidsdk/KeyExchange.kt +77 -0
- package/android/src/main/java/io/metamask/androidsdk/KeyExchangeMessageType.kt +20 -0
- package/android/src/main/java/io/metamask/androidsdk/KeyStorage.kt +122 -0
- package/android/src/main/java/io/metamask/androidsdk/Logger.kt +18 -0
- package/android/src/main/java/io/metamask/androidsdk/Message.kt +3 -0
- package/android/src/main/java/io/metamask/androidsdk/MessageType.kt +11 -0
- package/android/src/main/java/io/metamask/androidsdk/OriginatorInfo.kt +12 -0
- package/android/src/main/java/io/metamask/androidsdk/RequestError.kt +8 -0
- package/android/src/main/java/io/metamask/androidsdk/RequestInfo.kt +9 -0
- package/android/src/main/java/io/metamask/androidsdk/Result.kt +11 -0
- package/android/src/main/java/io/metamask/androidsdk/RpcRequest.kt +7 -0
- package/android/src/main/java/io/metamask/androidsdk/SDKInfo.kt +6 -0
- package/android/src/main/java/io/metamask/androidsdk/SDKOptions.kt +6 -0
- package/android/src/main/java/io/metamask/androidsdk/SecureStorage.kt +9 -0
- package/android/src/main/java/io/metamask/androidsdk/SessionConfig.kt +10 -0
- package/android/src/main/java/io/metamask/androidsdk/SessionManager.kt +92 -0
- package/android/src/main/java/io/metamask/androidsdk/SubmittedRequest.kt +8 -0
- package/android/src/main/java/io/metamask/androidsdk/TimeStampGenerator.kt +7 -0
- package/android/src/main/jniLibs/arm64-v8a/libecies.so +0 -0
- package/android/src/main/jniLibs/armeabi-v7a/libecies.so +0 -0
- package/android/src/main/jniLibs/x86/libecies.so +0 -0
- package/android/src/main/jniLibs/x86_64/libecies.so +0 -0
- package/android/src/test/java/com/margelo/nitro/nitrometamask/CancellationStateMachineTest.kt +128 -0
- package/android/src/test/java/com/margelo/nitro/nitrometamask/ChainIdParsingTest.kt +65 -0
- package/android/src/test/java/com/margelo/nitro/nitrometamask/ConfigureStateMachineTest.kt +140 -0
- package/android/src/test/java/com/margelo/nitro/nitrometamask/ConnectSignJsonTest.kt +76 -0
- package/android/src/test/java/com/margelo/nitro/nitrometamask/MetaMaskInstallationCheckTest.kt +42 -0
- package/android/src/test/java/com/margelo/nitro/nitrometamask/PersonalSignParamsTest.kt +75 -0
- package/ios/Frameworks/Ecies.xcframework/Info.plist +47 -0
- package/ios/Frameworks/Ecies.xcframework/ios-arm64/Headers/ecies.h +20 -0
- package/ios/Frameworks/Ecies.xcframework/ios-arm64/Headers/module.modulemap +4 -0
- package/ios/Frameworks/Ecies.xcframework/ios-arm64/libecies.a +0 -0
- package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/Headers/ecies.h +20 -0
- package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/Headers/module.modulemap +4 -0
- package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/libecies.a +0 -0
- package/ios/HybridNitroMetamask.swift +119 -54
- package/ios/NitroMetamaskTests/CancellationStateMachineTests.swift +150 -0
- package/ios/NitroMetamaskTests/ChainIdParsingTests.swift +117 -0
- package/ios/NitroMetamaskTests/ConfigureStateMachineTests.swift +174 -0
- package/ios/NitroMetamaskTests/ConnectSignJsonTests.swift +168 -0
- package/ios/NitroMetamaskTests/DefaultDappUrlTests.swift +80 -0
- package/ios/NitroMetamaskTests/PersonalSignParamsTests.swift +101 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/CommClient.swift +43 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/CommClientFactory.swift +17 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/CommLayer.swift +36 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/Deeplink.swift +26 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/DeeplinkClient.swift +199 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/DeeplinkManager.swift +83 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/String.swift +48 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/URLOpener.swift +19 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/SocketClient.swift +27 -0
- package/ios/metamask-ios-sdk/Crypto/Crypto.swift +72 -0
- package/ios/metamask-ios-sdk/Crypto/Encoding.swift +15 -0
- package/ios/metamask-ios-sdk/Crypto/KeyExchange.swift +236 -0
- package/ios/metamask-ios-sdk/DeviceInfo/DeviceInfo.swift +11 -0
- package/ios/metamask-ios-sdk/Ethereum/AppMetadata.swift +28 -0
- package/ios/metamask-ios-sdk/Ethereum/ErrorType.swift +62 -0
- package/ios/metamask-ios-sdk/Ethereum/Ethereum.swift +810 -0
- package/ios/metamask-ios-sdk/Ethereum/EthereumMethod.swift +111 -0
- package/ios/metamask-ios-sdk/Ethereum/EthereumRequest.swift +40 -0
- package/ios/metamask-ios-sdk/Ethereum/EthereumWrapper.swift +10 -0
- package/ios/metamask-ios-sdk/Ethereum/RPCRequest.swift +14 -0
- package/ios/metamask-ios-sdk/Ethereum/RequestError.swift +88 -0
- package/ios/metamask-ios-sdk/Ethereum/ResponseMethod.swift +22 -0
- package/ios/metamask-ios-sdk/Ethereum/SubmitRequest.swift +26 -0
- package/ios/metamask-ios-sdk/Ethereum/TimestampGenerator.swift +16 -0
- package/ios/metamask-ios-sdk/Extensions/NSRecursiveLock.swift +14 -0
- package/ios/metamask-ios-sdk/Extensions/Notification.swift +10 -0
- package/ios/metamask-ios-sdk/Logger/Logging.swift +27 -0
- package/ios/metamask-ios-sdk/Models/AddChainParameters.swift +35 -0
- package/ios/metamask-ios-sdk/Models/Event.swift +19 -0
- package/ios/metamask-ios-sdk/Models/Mappable.swift +40 -0
- package/ios/metamask-ios-sdk/Models/NativeCurrency.swift +25 -0
- package/ios/metamask-ios-sdk/Models/OriginatorInfo.swift +26 -0
- package/ios/metamask-ios-sdk/Models/RequestInfo.swift +18 -0
- package/ios/metamask-ios-sdk/Models/SignContract.swift +48 -0
- package/ios/metamask-ios-sdk/Models/Typealiases.swift +9 -0
- package/ios/metamask-ios-sdk/Persistence/SecureStore.swift +134 -0
- package/ios/metamask-ios-sdk/Persistence/SessionConfig.swift +24 -0
- package/ios/metamask-ios-sdk/Persistence/SessionManager.swift +56 -0
- package/ios/metamask-ios-sdk/SDK/Dependencies.swift +35 -0
- package/ios/metamask-ios-sdk/SDK/MetaMaskSDK.swift +215 -0
- package/ios/metamask-ios-sdk/SDK/SDKInfo.swift +37 -0
- package/ios/metamask-ios-sdk/SDK/SDKOptions.swift +16 -0
- package/lib/commonjs/index.js +50 -3
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +49 -3
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/__tests__/parseNitroError.test.d.ts +2 -0
- package/lib/typescript/src/__tests__/parseNitroError.test.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +43 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +29 -1
- package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts.map +1 -1
- package/package.json +21 -12
- package/react-native.config.js +5 -0
- package/rust/ecies-jni/Cargo.lock +50 -86
- package/rust/ecies-jni/Cargo.toml +1 -1
- package/rust/ecies-jni/src/lib.rs +164 -100
- package/src/__tests__/parseNitroError.test.ts +35 -0
- package/src/index.ts +53 -5
- package/src/specs/nitro-metamask.nitro.ts +29 -1
- package/scripts/verify-16k-page-alignment.py +0 -117
- package/scripts/verify-16k-page-alignment.sh +0 -5
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import NitroModules
|
|
2
|
-
import metamask_ios_sdk
|
|
3
2
|
import Foundation
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
// Posted by the app's AppDelegate when a MetaMask deep link is received.
|
|
6
|
+
// This avoids importing NitroMetamask (a C++ module) from the app target.
|
|
7
|
+
extension Notification.Name {
|
|
8
|
+
static let metamaskDeepLink = Notification.Name("io.metamask.deeplink")
|
|
9
|
+
}
|
|
4
10
|
|
|
5
11
|
final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
6
12
|
// SDK instance - can be recreated when configure() is called
|
|
@@ -9,10 +15,47 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
|
9
15
|
private var lastUsedUrl: String? = nil
|
|
10
16
|
private var lastUsedScheme: String? = nil
|
|
11
17
|
|
|
18
|
+
// Pending cancellation handler — set while an SDK operation is in-flight,
|
|
19
|
+
// cleared when the SDK responds or when the user returns to the app.
|
|
20
|
+
private var pendingCancellationHandler: (() -> Void)?
|
|
21
|
+
|
|
22
|
+
override init() {
|
|
23
|
+
super.init()
|
|
24
|
+
NotificationCenter.default.addObserver(
|
|
25
|
+
self,
|
|
26
|
+
selector: #selector(appDidBecomeActive),
|
|
27
|
+
name: UIApplication.didBecomeActiveNotification,
|
|
28
|
+
object: nil
|
|
29
|
+
)
|
|
30
|
+
NotificationCenter.default.addObserver(
|
|
31
|
+
self,
|
|
32
|
+
selector: #selector(handleDeepLinkNotification(_:)),
|
|
33
|
+
name: .metamaskDeepLink,
|
|
34
|
+
object: nil
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
deinit {
|
|
39
|
+
NotificationCenter.default.removeObserver(self)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@objc private func appDidBecomeActive() {
|
|
43
|
+
if let handler = pendingCancellationHandler {
|
|
44
|
+
pendingCancellationHandler = nil
|
|
45
|
+
handler()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@objc private func handleDeepLinkNotification(_ notification: Notification) {
|
|
50
|
+
if let url = notification.userInfo?["url"] as? URL {
|
|
51
|
+
sdk.handleUrl(url)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
12
55
|
// Get or create MetaMask SDK instance
|
|
13
56
|
// Aligned with Android: SDK is recreated when configure() changes values
|
|
14
57
|
private var sdk: MetaMaskSDK {
|
|
15
|
-
let currentUrl = dappUrl ?? "https://
|
|
58
|
+
let currentUrl = dappUrl ?? "https://novastera.com"
|
|
16
59
|
let currentScheme = deepLinkScheme ?? getDefaultDappScheme()
|
|
17
60
|
|
|
18
61
|
// Check if we need to recreate the SDK
|
|
@@ -22,14 +65,6 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
|
22
65
|
return existing
|
|
23
66
|
}
|
|
24
67
|
|
|
25
|
-
// Check if there's a shared instance we should use (only if it matches our config)
|
|
26
|
-
if let existing = MetaMaskSDK.sharedInstance,
|
|
27
|
-
lastUsedUrl == currentUrl,
|
|
28
|
-
lastUsedScheme == currentScheme {
|
|
29
|
-
sdkInstance = existing
|
|
30
|
-
return existing
|
|
31
|
-
}
|
|
32
|
-
|
|
33
68
|
// Create new SDK instance with current configuration
|
|
34
69
|
let appMetadata = AppMetadata(
|
|
35
70
|
name: "NitroMetamask",
|
|
@@ -60,7 +95,7 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
|
60
95
|
private var deepLinkScheme: String? = nil
|
|
61
96
|
|
|
62
97
|
func configure(dappUrl: String?, deepLinkScheme: String?) {
|
|
63
|
-
let urlToUse = dappUrl ?? "https://
|
|
98
|
+
let urlToUse = dappUrl ?? "https://novastera.com"
|
|
64
99
|
let schemeToUse = deepLinkScheme ?? getDefaultDappScheme()
|
|
65
100
|
|
|
66
101
|
var changed = false
|
|
@@ -99,33 +134,47 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
|
99
134
|
}
|
|
100
135
|
|
|
101
136
|
func connect() -> Promise<ConnectResult> {
|
|
102
|
-
// Use Promise.async with Swift async/await for best practice in Nitro modules
|
|
103
|
-
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
104
137
|
return Promise.async {
|
|
105
138
|
NSLog("NitroMetamask: connect() called")
|
|
106
139
|
|
|
107
140
|
// Check if MetaMask is installed before attempting to connect
|
|
108
141
|
if !self.sdk.isMetaMaskInstalled {
|
|
109
|
-
|
|
110
|
-
NSLog("NitroMetamask: MetaMask not installed - \(errorMessage)")
|
|
142
|
+
NSLog("NitroMetamask: MetaMask not installed")
|
|
111
143
|
throw NSError(
|
|
112
144
|
domain: "MetamaskConnector",
|
|
113
|
-
code:
|
|
114
|
-
userInfo: [NSLocalizedDescriptionKey:
|
|
145
|
+
code: 2,
|
|
146
|
+
userInfo: [NSLocalizedDescriptionKey: "[2] MetaMask is not installed"]
|
|
115
147
|
)
|
|
116
148
|
}
|
|
117
149
|
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
150
|
+
// Bridge the async SDK call with a cancellation handler using CheckedContinuation.
|
|
151
|
+
// The cancellation handler fires when the user returns to the app (appDidBecomeActive)
|
|
152
|
+
// without completing the MetaMask request.
|
|
153
|
+
let connectResult: Result<[String], RequestError> = try await withCheckedThrowingContinuation { continuation in
|
|
154
|
+
var resumed = false
|
|
155
|
+
self.pendingCancellationHandler = {
|
|
156
|
+
guard !resumed else { return }
|
|
157
|
+
resumed = true
|
|
158
|
+
self.pendingCancellationHandler = nil
|
|
159
|
+
continuation.resume(throwing: NSError(
|
|
160
|
+
domain: "MetamaskConnector",
|
|
161
|
+
code: -3,
|
|
162
|
+
userInfo: [NSLocalizedDescriptionKey: "MetaMask operation cancelled: user returned to app without completing the request"]
|
|
163
|
+
))
|
|
164
|
+
}
|
|
165
|
+
Task {
|
|
166
|
+
let result = await self.sdk.connect()
|
|
167
|
+
guard !resumed else { return }
|
|
168
|
+
resumed = true
|
|
169
|
+
self.pendingCancellationHandler = nil
|
|
170
|
+
continuation.resume(returning: result)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
121
173
|
|
|
122
174
|
NSLog("NitroMetamask: connect() result: \(connectResult)")
|
|
123
175
|
|
|
124
176
|
switch connectResult {
|
|
125
177
|
case .success:
|
|
126
|
-
// After successful connection, get account info from SDK
|
|
127
|
-
// Note: sdk.account is a String (not optional), check if empty
|
|
128
|
-
// Reference: https://raw.githubusercontent.com/MetaMask/metamask-ios-sdk/924d91bb3e98a5383c3082d6d5ba3ddac9e1c565/README.md
|
|
129
178
|
let address = self.sdk.account
|
|
130
179
|
guard !address.isEmpty else {
|
|
131
180
|
throw NSError(
|
|
@@ -135,8 +184,6 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
|
135
184
|
)
|
|
136
185
|
}
|
|
137
186
|
|
|
138
|
-
// Parse chainId from hex string (e.g., "0x1") to number
|
|
139
|
-
// Nitro requires chainId to be a number, not a string, for type safety
|
|
140
187
|
let chainIdHex = self.sdk.chainId
|
|
141
188
|
guard !chainIdHex.isEmpty,
|
|
142
189
|
let chainIdInt = Int(chainIdHex.replacingOccurrences(of: "0x", with: ""), radix: 16) else {
|
|
@@ -160,12 +207,7 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
|
160
207
|
}
|
|
161
208
|
|
|
162
209
|
func signMessage(message: String) -> Promise<String> {
|
|
163
|
-
// Use Promise.async with Swift async/await for best practice in Nitro modules
|
|
164
|
-
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
165
210
|
return Promise.async {
|
|
166
|
-
// Use explicit sign() method (requires connection first via connect())
|
|
167
|
-
// This is more explicit and predictable than connectAndSign() which forces connection
|
|
168
|
-
// Nitro encourages explicit object state, not convenience shortcuts
|
|
169
211
|
let account = self.sdk.account
|
|
170
212
|
guard !account.isEmpty else {
|
|
171
213
|
throw NSError(
|
|
@@ -175,23 +217,38 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
|
175
217
|
)
|
|
176
218
|
}
|
|
177
219
|
|
|
178
|
-
// Create EthereumRequest for personal_sign
|
|
179
|
-
// Based on MetaMask iOS SDK docs: params are [account, message]
|
|
180
|
-
// Reference: https://raw.githubusercontent.com/MetaMask/metamask-ios-sdk/924d91bb3e98a5383c3082d6d5ba3ddac9e1c565/README.md
|
|
181
220
|
let params: [String] = [account, message]
|
|
182
221
|
let request = EthereumRequest(
|
|
183
222
|
method: .personalSign,
|
|
184
223
|
params: params
|
|
185
224
|
)
|
|
186
225
|
|
|
187
|
-
// Make the request using the SDK's async request method
|
|
188
|
-
// request() returns Result<String, RequestError>
|
|
189
226
|
NSLog("NitroMetamask: signMessage() calling request")
|
|
190
|
-
|
|
227
|
+
|
|
228
|
+
// Bridge the async SDK call with a cancellation handler.
|
|
229
|
+
let result: Result<String, RequestError> = try await withCheckedThrowingContinuation { continuation in
|
|
230
|
+
var resumed = false
|
|
231
|
+
self.pendingCancellationHandler = {
|
|
232
|
+
guard !resumed else { return }
|
|
233
|
+
resumed = true
|
|
234
|
+
self.pendingCancellationHandler = nil
|
|
235
|
+
continuation.resume(throwing: NSError(
|
|
236
|
+
domain: "MetamaskConnector",
|
|
237
|
+
code: -3,
|
|
238
|
+
userInfo: [NSLocalizedDescriptionKey: "MetaMask operation cancelled: user returned to app without completing the request"]
|
|
239
|
+
))
|
|
240
|
+
}
|
|
241
|
+
Task {
|
|
242
|
+
let r = await self.sdk.request(request)
|
|
243
|
+
guard !resumed else { return }
|
|
244
|
+
resumed = true
|
|
245
|
+
self.pendingCancellationHandler = nil
|
|
246
|
+
continuation.resume(returning: r)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
191
249
|
|
|
192
250
|
NSLog("NitroMetamask: signMessage() result: \(result)")
|
|
193
251
|
|
|
194
|
-
// Extract signature from response
|
|
195
252
|
switch result {
|
|
196
253
|
case .success(let signature):
|
|
197
254
|
return signature
|
|
@@ -203,13 +260,8 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
|
203
260
|
}
|
|
204
261
|
|
|
205
262
|
func connectSign(nonce: String, exp: Int64) -> Promise<ConnectSignResult> {
|
|
206
|
-
// Use Promise.async with Swift async/await for best practice in Nitro modules
|
|
207
|
-
// Reference: https://nitro.margelo.com/docs/types/promises
|
|
208
|
-
// Based on MetaMask iOS SDK: connectAndSign(message:) convenience method
|
|
209
|
-
// Reference: https://github.com/MetaMask/metamask-ios-sdk
|
|
210
263
|
return Promise.async {
|
|
211
264
|
// Construct JSON message with only nonce and exp
|
|
212
|
-
// We don't include address or chainID - just encrypt nonce and exp
|
|
213
265
|
let messageDict: [String: Any] = [
|
|
214
266
|
"nonce": nonce,
|
|
215
267
|
"exp": exp
|
|
@@ -226,28 +278,44 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
|
226
278
|
|
|
227
279
|
NSLog("NitroMetamask: connectSign: Constructed message with nonce and exp: \(message)")
|
|
228
280
|
|
|
229
|
-
// Use the SDK's connectAndSign convenience method - it will connect if needed and sign the message
|
|
230
|
-
// This is the recommended approach per MetaMask iOS SDK documentation
|
|
231
|
-
// Reference: https://github.com/MetaMask/metamask-ios-sdk
|
|
232
281
|
// Check if MetaMask is installed before attempting to connect and sign
|
|
233
282
|
if !self.sdk.isMetaMaskInstalled {
|
|
234
|
-
|
|
235
|
-
NSLog("NitroMetamask: MetaMask not installed - \(errorMessage)")
|
|
283
|
+
NSLog("NitroMetamask: MetaMask not installed")
|
|
236
284
|
throw NSError(
|
|
237
285
|
domain: "MetamaskConnector",
|
|
238
|
-
code:
|
|
239
|
-
userInfo: [NSLocalizedDescriptionKey:
|
|
286
|
+
code: 2,
|
|
287
|
+
userInfo: [NSLocalizedDescriptionKey: "[2] MetaMask is not installed"]
|
|
240
288
|
)
|
|
241
289
|
}
|
|
242
290
|
|
|
243
291
|
NSLog("NitroMetamask: connectSign() calling connectAndSign")
|
|
244
|
-
|
|
292
|
+
|
|
293
|
+
// Bridge the async SDK call with a cancellation handler.
|
|
294
|
+
let connectSignResult: Result<String, RequestError> = try await withCheckedThrowingContinuation { continuation in
|
|
295
|
+
var resumed = false
|
|
296
|
+
self.pendingCancellationHandler = {
|
|
297
|
+
guard !resumed else { return }
|
|
298
|
+
resumed = true
|
|
299
|
+
self.pendingCancellationHandler = nil
|
|
300
|
+
continuation.resume(throwing: NSError(
|
|
301
|
+
domain: "MetamaskConnector",
|
|
302
|
+
code: -3,
|
|
303
|
+
userInfo: [NSLocalizedDescriptionKey: "MetaMask operation cancelled: user returned to app without completing the request"]
|
|
304
|
+
))
|
|
305
|
+
}
|
|
306
|
+
Task {
|
|
307
|
+
let result = await self.sdk.connectAndSign(message: message)
|
|
308
|
+
guard !resumed else { return }
|
|
309
|
+
resumed = true
|
|
310
|
+
self.pendingCancellationHandler = nil
|
|
311
|
+
continuation.resume(returning: result)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
245
314
|
|
|
246
315
|
NSLog("NitroMetamask: connectSign() result: \(connectSignResult)")
|
|
247
316
|
|
|
248
317
|
switch connectSignResult {
|
|
249
318
|
case .success(let signature):
|
|
250
|
-
// After connectSign completes, get the address and chainId from the SDK
|
|
251
319
|
let address = self.sdk.account
|
|
252
320
|
guard !address.isEmpty else {
|
|
253
321
|
throw NSError(
|
|
@@ -266,7 +334,6 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
|
266
334
|
)
|
|
267
335
|
}
|
|
268
336
|
|
|
269
|
-
// Parse chainId from hex string (e.g., "0x1") to Int64
|
|
270
337
|
guard let chainId = Int64(chainIdHex.replacingOccurrences(of: "0x", with: ""), radix: 16) else {
|
|
271
338
|
throw NSError(
|
|
272
339
|
domain: "MetamaskConnector",
|
|
@@ -276,8 +343,6 @@ final class HybridNitroMetamask: HybridNitroMetamaskSpec {
|
|
|
276
343
|
}
|
|
277
344
|
|
|
278
345
|
NSLog("NitroMetamask: connectSign: Signature received successfully, address=\(address), chainId=\(chainId)")
|
|
279
|
-
|
|
280
|
-
// Return ConnectSignResult with signature, address, and chainId
|
|
281
346
|
return ConnectSignResult(signature: signature, address: address, chainId: chainId)
|
|
282
347
|
|
|
283
348
|
case .failure(let error):
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
// CancellationStateMachineTests
|
|
4
|
+
//
|
|
5
|
+
// Validates: Requirements 11.5, 11.6
|
|
6
|
+
// "WHEN no operation is pending, THE Nitro_Module SHALL ignore app foreground transitions."
|
|
7
|
+
// "WHEN a pending operation is cancelled via foreground detection, THE Nitro_Module SHALL
|
|
8
|
+
// clean up the pending operation state so subsequent calls work correctly."
|
|
9
|
+
//
|
|
10
|
+
// These tests verify the cancellation state machine logic in isolation using a minimal
|
|
11
|
+
// test double that mirrors the pendingCancellationHandler pattern in HybridNitroMetamask.
|
|
12
|
+
// Because HybridNitroMetamask depends on the full MetaMask iOS SDK and Nitro runtime,
|
|
13
|
+
// the state machine is extracted into a testable helper class below.
|
|
14
|
+
|
|
15
|
+
// MARK: - Minimal test double for the cancellation state machine
|
|
16
|
+
|
|
17
|
+
/// CancellationStateMachine mirrors the pendingCancellationHandler logic in
|
|
18
|
+
/// HybridNitroMetamask without any SDK or Nitro dependencies.
|
|
19
|
+
final class CancellationStateMachine {
|
|
20
|
+
var pendingCancellationHandler: (() -> Void)?
|
|
21
|
+
|
|
22
|
+
/// Called when the app returns to the foreground (mirrors appDidBecomeActive).
|
|
23
|
+
func appDidBecomeActive() {
|
|
24
|
+
if let handler = pendingCancellationHandler {
|
|
25
|
+
pendingCancellationHandler = nil
|
|
26
|
+
handler()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// MARK: - Tests
|
|
32
|
+
|
|
33
|
+
final class CancellationStateMachineTests: XCTestCase {
|
|
34
|
+
|
|
35
|
+
// MARK: - Handler lifecycle
|
|
36
|
+
|
|
37
|
+
/// After setting and then clearing the handler (simulating a successful SDK response),
|
|
38
|
+
/// the state must be nil — no stale handler remains.
|
|
39
|
+
///
|
|
40
|
+
/// Validates: Requirement 11.6
|
|
41
|
+
func testHandlerIsNilAfterSuccessfulOperation() {
|
|
42
|
+
let sm = CancellationStateMachine()
|
|
43
|
+
|
|
44
|
+
// Simulate: operation starts — set handler
|
|
45
|
+
sm.pendingCancellationHandler = { /* would reject continuation */ }
|
|
46
|
+
XCTAssertNotNil(sm.pendingCancellationHandler, "Handler should be set while operation is in-flight")
|
|
47
|
+
|
|
48
|
+
// Simulate: SDK responds successfully — clear handler
|
|
49
|
+
sm.pendingCancellationHandler = nil
|
|
50
|
+
XCTAssertNil(sm.pendingCancellationHandler, "Handler must be nil after a successful operation")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// appDidBecomeActive with no pending handler must be a no-op — it must not crash
|
|
54
|
+
/// and must leave state unchanged.
|
|
55
|
+
///
|
|
56
|
+
/// Validates: Requirement 11.5
|
|
57
|
+
func testAppDidBecomeActiveWithNoPendingHandlerIsNoOp() {
|
|
58
|
+
let sm = CancellationStateMachine()
|
|
59
|
+
XCTAssertNil(sm.pendingCancellationHandler, "Precondition: no pending handler")
|
|
60
|
+
|
|
61
|
+
// Must not crash and must leave state as nil
|
|
62
|
+
sm.appDidBecomeActive()
|
|
63
|
+
|
|
64
|
+
XCTAssertNil(sm.pendingCancellationHandler, "State must remain nil after no-op foreground transition")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// appDidBecomeActive fires the handler and clears it (nil-before-invoke pattern).
|
|
68
|
+
///
|
|
69
|
+
/// Validates: Requirements 11.2, 11.6
|
|
70
|
+
func testAppDidBecomeActiveFiresAndClearsHandler() {
|
|
71
|
+
let sm = CancellationStateMachine()
|
|
72
|
+
var handlerFired = false
|
|
73
|
+
|
|
74
|
+
sm.pendingCancellationHandler = {
|
|
75
|
+
handlerFired = true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
sm.appDidBecomeActive()
|
|
79
|
+
|
|
80
|
+
XCTAssertTrue(handlerFired, "Handler must be invoked when app becomes active with a pending operation")
|
|
81
|
+
XCTAssertNil(sm.pendingCancellationHandler, "Handler must be cleared before/after invocation")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// A second appDidBecomeActive after the first has already fired must be a no-op.
|
|
85
|
+
/// This guards against double-rejection if the notification fires more than once.
|
|
86
|
+
///
|
|
87
|
+
/// Validates: Requirement 11.5
|
|
88
|
+
func testSecondAppDidBecomeActiveIsNoOp() {
|
|
89
|
+
let sm = CancellationStateMachine()
|
|
90
|
+
var callCount = 0
|
|
91
|
+
|
|
92
|
+
sm.pendingCancellationHandler = {
|
|
93
|
+
callCount += 1
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
sm.appDidBecomeActive() // first — fires handler
|
|
97
|
+
sm.appDidBecomeActive() // second — must be no-op
|
|
98
|
+
|
|
99
|
+
XCTAssertEqual(callCount, 1, "Handler must be invoked exactly once even if appDidBecomeActive fires multiple times")
|
|
100
|
+
XCTAssertNil(sm.pendingCancellationHandler, "Handler must remain nil after second foreground transition")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// After a cancellation fires, the state machine must be clean so a subsequent
|
|
104
|
+
/// operation can set a new handler without interference.
|
|
105
|
+
///
|
|
106
|
+
/// Validates: Requirement 11.6
|
|
107
|
+
func testStateIsCleanAfterCancellationForSubsequentOperation() {
|
|
108
|
+
let sm = CancellationStateMachine()
|
|
109
|
+
var firstFired = false
|
|
110
|
+
var secondFired = false
|
|
111
|
+
|
|
112
|
+
// First operation
|
|
113
|
+
sm.pendingCancellationHandler = { firstFired = true }
|
|
114
|
+
sm.appDidBecomeActive() // cancels first operation
|
|
115
|
+
|
|
116
|
+
XCTAssertTrue(firstFired, "First handler must fire")
|
|
117
|
+
XCTAssertNil(sm.pendingCancellationHandler, "State must be clean after first cancellation")
|
|
118
|
+
|
|
119
|
+
// Second operation — must work correctly
|
|
120
|
+
sm.pendingCancellationHandler = { secondFired = true }
|
|
121
|
+
sm.appDidBecomeActive() // cancels second operation
|
|
122
|
+
|
|
123
|
+
XCTAssertTrue(secondFired, "Second handler must fire independently")
|
|
124
|
+
XCTAssertNil(sm.pendingCancellationHandler, "State must be clean after second cancellation")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// MARK: - Race-condition guard (nil-before-invoke)
|
|
128
|
+
|
|
129
|
+
/// The nil-before-invoke pattern ensures the handler is cleared before being called,
|
|
130
|
+
/// preventing re-entrant invocation if the handler itself triggers appDidBecomeActive.
|
|
131
|
+
///
|
|
132
|
+
/// Validates: Requirement 11.6
|
|
133
|
+
func testHandlerIsNilWhenInvokedNilBeforeInvokePattern() {
|
|
134
|
+
let sm = CancellationStateMachine()
|
|
135
|
+
var observedHandlerDuringInvocation: (() -> Void)? = { /* sentinel */ }
|
|
136
|
+
|
|
137
|
+
sm.pendingCancellationHandler = {
|
|
138
|
+
// Capture the state of pendingCancellationHandler at the moment the handler runs.
|
|
139
|
+
// It must already be nil (nil-before-invoke).
|
|
140
|
+
observedHandlerDuringInvocation = sm.pendingCancellationHandler
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
sm.appDidBecomeActive()
|
|
144
|
+
|
|
145
|
+
XCTAssertNil(
|
|
146
|
+
observedHandlerDuringInvocation,
|
|
147
|
+
"pendingCancellationHandler must be nil before the handler is invoked (nil-before-invoke pattern)"
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
// ChainIdParsingTests
|
|
4
|
+
//
|
|
5
|
+
// Property 1: ChainId hex parsing round-trip
|
|
6
|
+
// Validates: Requirements 2.6, 5.5
|
|
7
|
+
//
|
|
8
|
+
// "WHEN connect() succeeds, THE Nitro_Module SHALL return a chainId parsed from
|
|
9
|
+
// the hex string returned by the SDK (e.g., "0x1" → 1n)."
|
|
10
|
+
// "WHEN getChainId() is called and a wallet is connected, THE Nitro_Module SHALL
|
|
11
|
+
// resolve the promise with the chain ID as a bigint parsed from the SDK's hex string."
|
|
12
|
+
//
|
|
13
|
+
// These tests verify the hex-to-Int64 parsing logic used in HybridNitroMetamask.swift.
|
|
14
|
+
// The helper below mirrors the parsing logic exactly so tests remain independent of
|
|
15
|
+
// the full SDK and Nitro runtime.
|
|
16
|
+
|
|
17
|
+
// MARK: - Parsing helper (mirrors HybridNitroMetamask.swift)
|
|
18
|
+
|
|
19
|
+
/// Mirrors the chainId parsing logic used in HybridNitroMetamask.swift:
|
|
20
|
+
///
|
|
21
|
+
/// Int64(chainIdHex.replacingOccurrences(of: "0x", with: ""), radix: 16)
|
|
22
|
+
///
|
|
23
|
+
/// Both lowercase "0x" and uppercase "0X" prefixes are stripped.
|
|
24
|
+
func parseChainId(_ hex: String) -> Int64? {
|
|
25
|
+
let stripped = hex
|
|
26
|
+
.replacingOccurrences(of: "0x", with: "")
|
|
27
|
+
.replacingOccurrences(of: "0X", with: "")
|
|
28
|
+
return Int64(stripped, radix: 16)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// MARK: - Tests
|
|
32
|
+
|
|
33
|
+
final class ChainIdParsingTests: XCTestCase {
|
|
34
|
+
|
|
35
|
+
// MARK: - Known values
|
|
36
|
+
|
|
37
|
+
/// "0x1" is Ethereum Mainnet — the most common chain.
|
|
38
|
+
func testMainnet_0x1_parsesTo1() {
|
|
39
|
+
XCTAssertEqual(parseChainId("0x1"), 1)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// "0x89" is Polygon Mainnet (137 decimal).
|
|
43
|
+
func testPolygon_0x89_parsesTo137() {
|
|
44
|
+
XCTAssertEqual(parseChainId("0x89"), 137)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// "0xa" is Optimism Mainnet (10 decimal).
|
|
48
|
+
func testOptimism_0xa_parsesTo10() {
|
|
49
|
+
XCTAssertEqual(parseChainId("0xa"), 10)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// "0x38" is BNB Smart Chain (56 decimal).
|
|
53
|
+
func testBNBChain_0x38_parsesTo56() {
|
|
54
|
+
XCTAssertEqual(parseChainId("0x38"), 56)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// "0xa4b1" is Arbitrum One (42161 decimal).
|
|
58
|
+
func testArbitrum_0xa4b1_parsesTo42161() {
|
|
59
|
+
XCTAssertEqual(parseChainId("0xa4b1"), 42161)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// MARK: - Case insensitivity
|
|
63
|
+
|
|
64
|
+
/// Uppercase "0X" prefix must be stripped correctly.
|
|
65
|
+
func testUppercasePrefix_0X1_parsesTo1() {
|
|
66
|
+
XCTAssertEqual(parseChainId("0X1"), 1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Mixed-case hex digits must parse correctly.
|
|
70
|
+
func testMixedCase_0xA4B1_parsesTo42161() {
|
|
71
|
+
XCTAssertEqual(parseChainId("0xA4B1"), 42161)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// MARK: - Round-trip property
|
|
75
|
+
|
|
76
|
+
/// For any parsed value, converting back to hex and re-parsing must yield the same Int64.
|
|
77
|
+
/// This is the core round-trip property.
|
|
78
|
+
func testRoundTrip_parseThenFormat_yieldsOriginalValue() {
|
|
79
|
+
let testCases: [(hex: String, expected: Int64)] = [
|
|
80
|
+
("0x1", 1),
|
|
81
|
+
("0x89", 137),
|
|
82
|
+
("0xa", 10),
|
|
83
|
+
("0x38", 56),
|
|
84
|
+
("0xa4b1", 42161),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
for (hex, expected) in testCases {
|
|
88
|
+
guard let parsed = parseChainId(hex) else {
|
|
89
|
+
XCTFail("parseChainId(\"\(hex)\") returned nil — expected \(expected)")
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
XCTAssertEqual(parsed, expected, "parseChainId(\"\(hex)\") should equal \(expected)")
|
|
93
|
+
|
|
94
|
+
// Round-trip: format back to hex and re-parse
|
|
95
|
+
let reformatted = "0x" + String(parsed, radix: 16)
|
|
96
|
+
let reparsed = parseChainId(reformatted)
|
|
97
|
+
XCTAssertEqual(reparsed, parsed, "Round-trip failed for \"\(hex)\": reformatted=\"\(reformatted)\"")
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// MARK: - Invalid inputs
|
|
102
|
+
|
|
103
|
+
/// An empty string must return nil (not crash).
|
|
104
|
+
func testEmptyString_returnsNil() {
|
|
105
|
+
XCTAssertNil(parseChainId(""))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// A bare "0x" with no digits must return nil.
|
|
109
|
+
func testBarePrefix_0x_returnsNil() {
|
|
110
|
+
XCTAssertNil(parseChainId("0x"))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// A non-hex string must return nil.
|
|
114
|
+
func testNonHex_returnsNil() {
|
|
115
|
+
XCTAssertNil(parseChainId("0xGGGG"))
|
|
116
|
+
}
|
|
117
|
+
}
|