@novastera-oss/nitro-metamask 0.6.2 → 0.7.0

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.
Files changed (168) hide show
  1. package/NitroMetamask.podspec +12 -3
  2. package/android/build.gradle +10 -13
  3. package/android/cargo-ecies.gradle +59 -78
  4. package/android/src/main/aidl/io/metamask/nativesdk/IMessegeService.aidl +8 -0
  5. package/android/src/main/aidl/io/metamask/nativesdk/IMessegeServiceCallback.aidl +8 -0
  6. package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +101 -3
  7. package/android/src/main/java/io/metamask/androidsdk/AnyRequest.kt +8 -0
  8. package/android/src/main/java/io/metamask/androidsdk/ClientMessageServiceCallback.kt +12 -0
  9. package/android/src/main/java/io/metamask/androidsdk/ClientServiceConnection.kt +42 -0
  10. package/android/src/main/java/io/metamask/androidsdk/CommunicationClient.kt +525 -0
  11. package/android/src/main/java/io/metamask/androidsdk/CommunicationClientModule.kt +47 -0
  12. package/android/src/main/java/io/metamask/androidsdk/CommunicationClientModuleInterface.kt +11 -0
  13. package/android/src/main/java/io/metamask/androidsdk/Constants.kt +5 -0
  14. package/android/src/main/java/io/metamask/androidsdk/Crypto.kt +35 -0
  15. package/android/src/main/java/io/metamask/androidsdk/DappMetadata.kt +36 -0
  16. package/android/src/main/java/io/metamask/androidsdk/Encryption.kt +9 -0
  17. package/android/src/main/java/io/metamask/androidsdk/ErrorType.kt +41 -0
  18. package/android/src/main/java/io/metamask/androidsdk/Ethereum.kt +328 -0
  19. package/android/src/main/java/io/metamask/androidsdk/EthereumEventCallback.kt +6 -0
  20. package/android/src/main/java/io/metamask/androidsdk/EthereumMethod.kt +80 -0
  21. package/android/src/main/java/io/metamask/androidsdk/EthereumRequest.kt +7 -0
  22. package/android/src/main/java/io/metamask/androidsdk/EthereumState.kt +7 -0
  23. package/android/src/main/java/io/metamask/androidsdk/KeyExchange.kt +77 -0
  24. package/android/src/main/java/io/metamask/androidsdk/KeyExchangeMessageType.kt +20 -0
  25. package/android/src/main/java/io/metamask/androidsdk/KeyStorage.kt +122 -0
  26. package/android/src/main/java/io/metamask/androidsdk/Logger.kt +18 -0
  27. package/android/src/main/java/io/metamask/androidsdk/Message.kt +3 -0
  28. package/android/src/main/java/io/metamask/androidsdk/MessageType.kt +11 -0
  29. package/android/src/main/java/io/metamask/androidsdk/OriginatorInfo.kt +12 -0
  30. package/android/src/main/java/io/metamask/androidsdk/RequestError.kt +8 -0
  31. package/android/src/main/java/io/metamask/androidsdk/RequestInfo.kt +9 -0
  32. package/android/src/main/java/io/metamask/androidsdk/Result.kt +11 -0
  33. package/android/src/main/java/io/metamask/androidsdk/RpcRequest.kt +7 -0
  34. package/android/src/main/java/io/metamask/androidsdk/SDKInfo.kt +6 -0
  35. package/android/src/main/java/io/metamask/androidsdk/SDKOptions.kt +6 -0
  36. package/android/src/main/java/io/metamask/androidsdk/SecureStorage.kt +9 -0
  37. package/android/src/main/java/io/metamask/androidsdk/SessionConfig.kt +10 -0
  38. package/android/src/main/java/io/metamask/androidsdk/SessionManager.kt +92 -0
  39. package/android/src/main/java/io/metamask/androidsdk/SubmittedRequest.kt +8 -0
  40. package/android/src/main/java/io/metamask/androidsdk/TimeStampGenerator.kt +7 -0
  41. package/android/src/main/jniLibs/arm64-v8a/libecies.so +0 -0
  42. package/android/src/main/jniLibs/armeabi-v7a/libecies.so +0 -0
  43. package/android/src/main/jniLibs/x86/libecies.so +0 -0
  44. package/android/src/main/jniLibs/x86_64/libecies.so +0 -0
  45. package/android/src/test/java/com/margelo/nitro/nitrometamask/CancellationStateMachineTest.kt +128 -0
  46. package/android/src/test/java/com/margelo/nitro/nitrometamask/ChainIdParsingTest.kt +65 -0
  47. package/android/src/test/java/com/margelo/nitro/nitrometamask/ConfigureStateMachineTest.kt +140 -0
  48. package/android/src/test/java/com/margelo/nitro/nitrometamask/ConnectSignJsonTest.kt +76 -0
  49. package/android/src/test/java/com/margelo/nitro/nitrometamask/MetaMaskInstallationCheckTest.kt +42 -0
  50. package/android/src/test/java/com/margelo/nitro/nitrometamask/PersonalSignParamsTest.kt +75 -0
  51. package/ios/Frameworks/Ecies.xcframework/Info.plist +47 -0
  52. package/ios/Frameworks/Ecies.xcframework/ios-arm64/Headers/ecies.h +20 -0
  53. package/ios/Frameworks/Ecies.xcframework/ios-arm64/Headers/module.modulemap +4 -0
  54. package/ios/Frameworks/Ecies.xcframework/ios-arm64/libecies.a +0 -0
  55. package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/Headers/ecies.h +20 -0
  56. package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/Headers/module.modulemap +4 -0
  57. package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/libecies.a +0 -0
  58. package/ios/HybridNitroMetamask.swift +119 -54
  59. package/ios/NitroMetamaskTests/CancellationStateMachineTests.swift +150 -0
  60. package/ios/NitroMetamaskTests/ChainIdParsingTests.swift +117 -0
  61. package/ios/NitroMetamaskTests/ConfigureStateMachineTests.swift +174 -0
  62. package/ios/NitroMetamaskTests/ConnectSignJsonTests.swift +168 -0
  63. package/ios/NitroMetamaskTests/DefaultDappUrlTests.swift +80 -0
  64. package/ios/NitroMetamaskTests/PersonalSignParamsTests.swift +101 -0
  65. package/ios/metamask-ios-sdk/CommunicationLayer/CommClient.swift +43 -0
  66. package/ios/metamask-ios-sdk/CommunicationLayer/CommClientFactory.swift +17 -0
  67. package/ios/metamask-ios-sdk/CommunicationLayer/CommLayer.swift +36 -0
  68. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/Deeplink.swift +26 -0
  69. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/DeeplinkClient.swift +199 -0
  70. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/DeeplinkManager.swift +83 -0
  71. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/String.swift +48 -0
  72. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/URLOpener.swift +19 -0
  73. package/ios/metamask-ios-sdk/CommunicationLayer/SocketClient.swift +27 -0
  74. package/ios/metamask-ios-sdk/Crypto/Crypto.swift +72 -0
  75. package/ios/metamask-ios-sdk/Crypto/Encoding.swift +15 -0
  76. package/ios/metamask-ios-sdk/Crypto/KeyExchange.swift +236 -0
  77. package/ios/metamask-ios-sdk/DeviceInfo/DeviceInfo.swift +11 -0
  78. package/ios/metamask-ios-sdk/Ethereum/AppMetadata.swift +28 -0
  79. package/ios/metamask-ios-sdk/Ethereum/ErrorType.swift +62 -0
  80. package/ios/metamask-ios-sdk/Ethereum/Ethereum.swift +810 -0
  81. package/ios/metamask-ios-sdk/Ethereum/EthereumMethod.swift +111 -0
  82. package/ios/metamask-ios-sdk/Ethereum/EthereumRequest.swift +40 -0
  83. package/ios/metamask-ios-sdk/Ethereum/EthereumWrapper.swift +10 -0
  84. package/ios/metamask-ios-sdk/Ethereum/RPCRequest.swift +14 -0
  85. package/ios/metamask-ios-sdk/Ethereum/RequestError.swift +88 -0
  86. package/ios/metamask-ios-sdk/Ethereum/ResponseMethod.swift +22 -0
  87. package/ios/metamask-ios-sdk/Ethereum/SubmitRequest.swift +26 -0
  88. package/ios/metamask-ios-sdk/Ethereum/TimestampGenerator.swift +16 -0
  89. package/ios/metamask-ios-sdk/Extensions/NSRecursiveLock.swift +14 -0
  90. package/ios/metamask-ios-sdk/Extensions/Notification.swift +10 -0
  91. package/ios/metamask-ios-sdk/Logger/Logging.swift +27 -0
  92. package/ios/metamask-ios-sdk/Models/AddChainParameters.swift +35 -0
  93. package/ios/metamask-ios-sdk/Models/Event.swift +19 -0
  94. package/ios/metamask-ios-sdk/Models/Mappable.swift +40 -0
  95. package/ios/metamask-ios-sdk/Models/NativeCurrency.swift +25 -0
  96. package/ios/metamask-ios-sdk/Models/OriginatorInfo.swift +26 -0
  97. package/ios/metamask-ios-sdk/Models/RequestInfo.swift +18 -0
  98. package/ios/metamask-ios-sdk/Models/SignContract.swift +48 -0
  99. package/ios/metamask-ios-sdk/Models/Typealiases.swift +9 -0
  100. package/ios/metamask-ios-sdk/Persistence/SecureStore.swift +134 -0
  101. package/ios/metamask-ios-sdk/Persistence/SessionConfig.swift +24 -0
  102. package/ios/metamask-ios-sdk/Persistence/SessionManager.swift +56 -0
  103. package/ios/metamask-ios-sdk/SDK/Dependencies.swift +35 -0
  104. package/ios/metamask-ios-sdk/SDK/MetaMaskSDK.swift +215 -0
  105. package/ios/metamask-ios-sdk/SDK/SDKInfo.swift +37 -0
  106. package/ios/metamask-ios-sdk/SDK/SDKOptions.swift +16 -0
  107. package/lib/commonjs/index.js +50 -3
  108. package/lib/commonjs/index.js.map +1 -1
  109. package/lib/module/index.js +49 -3
  110. package/lib/module/index.js.map +1 -1
  111. package/lib/typescript/src/__tests__/parseNitroError.test.d.ts +2 -0
  112. package/lib/typescript/src/__tests__/parseNitroError.test.d.ts.map +1 -0
  113. package/lib/typescript/src/index.d.ts +43 -3
  114. package/lib/typescript/src/index.d.ts.map +1 -1
  115. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +34 -6
  116. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts.map +1 -1
  117. package/nitro.json +8 -2
  118. package/nitrogen/generated/android/NitroMetamask+autolinking.cmake +1 -1
  119. package/nitrogen/generated/android/NitroMetamask+autolinking.gradle +1 -1
  120. package/nitrogen/generated/android/NitroMetamaskOnLoad.cpp +27 -17
  121. package/nitrogen/generated/android/NitroMetamaskOnLoad.hpp +14 -5
  122. package/nitrogen/generated/android/c++/JConnectResult.hpp +2 -2
  123. package/nitrogen/generated/android/c++/JConnectSignResult.hpp +2 -2
  124. package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.cpp +26 -25
  125. package/nitrogen/generated/android/c++/JHybridNitroMetamaskSpec.hpp +20 -22
  126. package/nitrogen/generated/android/c++/JVariant_NullType_Long.cpp +1 -1
  127. package/nitrogen/generated/android/c++/JVariant_NullType_Long.hpp +4 -4
  128. package/nitrogen/generated/android/c++/JVariant_NullType_String.cpp +1 -1
  129. package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +4 -4
  130. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/ConnectResult.kt +16 -1
  131. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/ConnectSignResult.kt +18 -1
  132. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/HybridNitroMetamaskSpec.kt +16 -19
  133. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/NitroMetamaskOnLoad.kt +1 -1
  134. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/Variant_NullType_Long.kt +15 -12
  135. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrometamask/Variant_NullType_String.kt +15 -12
  136. package/nitrogen/generated/ios/NitroMetamask+autolinking.rb +3 -1
  137. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.cpp +1 -1
  138. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Bridge.hpp +2 -2
  139. package/nitrogen/generated/ios/NitroMetamask-Swift-Cxx-Umbrella.hpp +1 -1
  140. package/nitrogen/generated/ios/NitroMetamaskAutolinking.mm +1 -1
  141. package/nitrogen/generated/ios/NitroMetamaskAutolinking.swift +9 -8
  142. package/nitrogen/generated/ios/c++/HybridNitroMetamaskSpecSwift.cpp +1 -1
  143. package/nitrogen/generated/ios/c++/HybridNitroMetamaskSpecSwift.hpp +7 -1
  144. package/nitrogen/generated/ios/swift/ConnectResult.swift +1 -2
  145. package/nitrogen/generated/ios/swift/ConnectSignResult.swift +1 -2
  146. package/nitrogen/generated/ios/swift/Func_void_ConnectResult.swift +1 -2
  147. package/nitrogen/generated/ios/swift/Func_void_ConnectSignResult.swift +1 -2
  148. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +1 -2
  149. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +1 -2
  150. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__int64_t_.swift +1 -2
  151. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +1 -2
  152. package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec.swift +3 -4
  153. package/nitrogen/generated/ios/swift/HybridNitroMetamaskSpec_cxx.swift +9 -2
  154. package/nitrogen/generated/ios/swift/Variant_NullType_Int64.swift +14 -2
  155. package/nitrogen/generated/ios/swift/Variant_NullType_String.swift +13 -1
  156. package/nitrogen/generated/shared/c++/ConnectResult.hpp +1 -1
  157. package/nitrogen/generated/shared/c++/ConnectSignResult.hpp +1 -1
  158. package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.cpp +1 -1
  159. package/nitrogen/generated/shared/c++/HybridNitroMetamaskSpec.hpp +1 -1
  160. package/package.json +18 -13
  161. package/rust/ecies-jni/Cargo.lock +50 -86
  162. package/rust/ecies-jni/Cargo.toml +1 -1
  163. package/rust/ecies-jni/src/lib.rs +164 -100
  164. package/src/__tests__/parseNitroError.test.ts +35 -0
  165. package/src/index.ts +53 -5
  166. package/src/specs/nitro-metamask.nitro.ts +34 -6
  167. package/scripts/verify-16k-page-alignment.py +0 -117
  168. package/scripts/verify-16k-page-alignment.sh +0 -5
@@ -14,19 +14,28 @@ Pod::Spec.new do |s|
14
14
  s.source = { :git => "https://github.com/novastera/nitro-metamask.git", :tag => "#{s.version}" }
15
15
 
16
16
  s.source_files = [
17
- # Implementation (Swift)
18
- "ios/**/*.{swift}",
17
+ # Implementation (Swift) — exclude test directories
18
+ "ios/*.{swift}",
19
+ # Vendored MetaMask iOS SDK source
20
+ "ios/metamask-ios-sdk/**/*.{swift}",
19
21
  # Autolinking/Registration (Objective-C++)
20
22
  "ios/**/*.{m,mm}",
21
23
  # Implementation (C++ objects)
22
24
  "cpp/**/*.{hpp,cpp}",
23
25
  ]
24
26
 
27
+ s.vendored_frameworks = 'ios/Frameworks/Ecies.xcframework'
28
+
29
+ s.frameworks = 'UIKit', 'SwiftUI', 'Combine'
30
+
31
+ s.pod_target_xcconfig = {
32
+ 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "$(PODS_XCFRAMEWORKS_BUILD_DIR)/NitroMetamask"'
33
+ }
34
+
25
35
  load 'nitrogen/generated/ios/NitroMetamask+autolinking.rb'
26
36
  add_nitrogen_files(s)
27
37
 
28
38
  s.dependency 'React-jsi'
29
39
  s.dependency 'React-callinvoker'
30
- s.dependency 'metamask-ios-sdk', '~> 0.8.10'
31
40
  install_modules_dependencies(s)
32
41
  end
@@ -98,6 +98,7 @@ android {
98
98
  buildFeatures {
99
99
  buildConfig true
100
100
  prefab true
101
+ aidl true
101
102
  }
102
103
 
103
104
  buildTypes {
@@ -117,7 +118,10 @@ android {
117
118
 
118
119
  sourceSets {
119
120
  main {
120
- jniLibs.srcDirs += ["${buildDir}/intermediates/rust-ecies-jni"]
121
+ // Local/CI build: libecies.so built from rust/ecies-jni via cargoNdkBuildEcies
122
+ // npm consumers: libecies.so shipped as prebuilt in android/src/main/jniLibs
123
+ jniLibs.srcDirs += ["${buildDir}/intermediates/rust-ecies-jni", "src/main/jniLibs"]
124
+ aidl.srcDirs += ["src/main/aidl"]
121
125
  if (isNewArchitectureEnabled()) {
122
126
  java.srcDirs += [
123
127
  // React Codegen files
@@ -133,13 +137,6 @@ repositories {
133
137
  google()
134
138
  }
135
139
 
136
- // Hard block old prebuilt ECIES .so from any transitive edge.
137
- // NitroMetamask always provides libecies.so from rust/ecies-jni (16 KB page-size compatible).
138
- configurations.configureEach {
139
- exclude group: "io.metamask.ecies", module: "ecies"
140
- }
141
-
142
-
143
140
  dependencies {
144
141
  // For < 0.71, this will be from the local maven repo
145
142
  // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
@@ -153,11 +150,11 @@ dependencies {
153
150
  // Required for Promise.async with coroutines in Nitro modules
154
151
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
155
152
 
156
- // MetaMask Android SDK exclude prebuilt io.metamask.ecies (4 KB ELF); we ship io.metamask.ecies.Ecies + libecies.so from rust/ecies-jni
157
- // See: https://github.com/MetaMask/metamask-android-sdk/issues/157
158
- implementation("io.metamask.androidsdk:metamask-android-sdk:0.6.6") {
159
- exclude group: "io.metamask.ecies", module: "ecies"
160
- }
153
+ // Gsonused by vendored MetaMask SDK source for JSON serialization
154
+ implementation "com.google.code.gson:gson:2.11.0"
155
+
156
+ // ProcessLifecycleOwner required for user cancellation detection (Requirement 11.4)
157
+ implementation "androidx.lifecycle:lifecycle-process:2.10.0"
161
158
  }
162
159
 
163
160
  apply from: "./cargo-ecies.gradle"
@@ -1,114 +1,95 @@
1
- def eciesRustDir = file("${projectDir}/../rust/ecies-jni")
1
+ // ─── ECIES native library build ───────────────────────────────────────────────
2
+ //
3
+ // Strategy:
4
+ // Local dev → builds only the host ABI (fast, no cross-compilation needed)
5
+ // CI publish → builds all 4 ABIs via -PreactNativeArchitectures=arm64-v8a,...
6
+ //
7
+ // The built .so files land in:
8
+ // android/build/intermediates/rust-ecies-jni/<abi>/libecies.so
9
+ // which is already on jniLibs.srcDirs in build.gradle.
10
+ // ──────────────────────────────────────────────────────────────────────────────
11
+
12
+ def eciesRustDir = file("${projectDir}/../rust/ecies-jni")
2
13
  def eciesCargoToml = file("${projectDir}/../rust/ecies-jni/Cargo.toml")
3
- def eciesJniOut = file("${buildDir}/intermediates/rust-ecies-jni")
4
- def eciesPrebuiltDir = file("${projectDir}/src/main/jniLibs")
5
- def buildEciesFromSource = (project.findProperty("NitroMetamask_buildEciesFromSource") ?: "false").toString().toBoolean()
14
+ def eciesJniOut = file("${buildDir}/intermediates/rust-ecies-jni")
6
15
 
7
- def reactNativeArchitectures() {
8
- def value = rootProject.getProperties().get("reactNativeArchitectures")
9
- return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
16
+ // Detect host ABI for local builds
17
+ def hostAbi = { ->
18
+ def arch = System.getProperty("os.arch") ?: ""
19
+ if (arch.contains("aarch64") || arch.contains("arm64")) return "arm64-v8a"
20
+ if (arch.contains("x86_64") || arch.contains("amd64")) return "x86_64"
21
+ return "arm64-v8a" // safe default for Apple Silicon Macs
10
22
  }
11
23
 
12
- def hasCargoExecutable = { ->
13
- def path = System.getenv("PATH") ?: ""
14
- if (path.isEmpty()) {
15
- return false
16
- }
24
+ // Resolve which ABIs to build:
25
+ // - If -PreactNativeArchitectures is set (CI), use that list
26
+ // - Otherwise build only the host ABI (local dev)
27
+ def resolveAbis = { ->
28
+ def explicit = rootProject.getProperties().get("reactNativeArchitectures")
29
+ if (explicit) return explicit.split(",").toList()
30
+ return [hostAbi()]
31
+ }
17
32
 
18
- def names = ["cargo"]
33
+ // Find cargo/cargo-ndk — searches PATH then ~/.cargo/bin (rustup default)
34
+ def findBin = { String name ->
19
35
  def isWindows = (System.getProperty("os.name") ?: "").toLowerCase().contains("win")
20
- if (isWindows) {
21
- names = ["cargo.exe", "cargo.bat", "cargo.cmd"]
22
- }
23
-
24
- for (dirPath in path.split(java.io.File.pathSeparator)) {
25
- for (name in names) {
26
- def candidate = new File(dirPath, name)
27
- if (candidate.isFile() && candidate.canExecute()) {
28
- return true
29
- }
30
- }
36
+ def exeName = isWindows ? "${name}.exe" : name
37
+ def path = System.getenv("PATH") ?: ""
38
+ for (dir in path.split(java.io.File.pathSeparator)) {
39
+ def f = new File(dir, exeName)
40
+ if (f.isFile() && f.canExecute()) return f.absolutePath
31
41
  }
32
-
33
- return false
34
- }
35
-
36
- tasks.register("verifyPrebuiltEciesLibs") {
37
- group = "verification"
38
- description = "Verify prebuilt libecies.so exists for all configured ABIs"
39
-
40
- doLast {
41
- def missing = []
42
- reactNativeArchitectures().each { abi ->
43
- def soFile = file("${eciesPrebuiltDir}/${abi}/libecies.so")
44
- if (!soFile.isFile()) {
45
- missing.add("${abi}/libecies.so")
46
- }
47
- }
48
-
49
- if (!missing.isEmpty()) {
50
- throw new GradleException(
51
- "Missing prebuilt ECIES native libraries under android/src/main/jniLibs: ${missing.join(', ')}. " +
52
- "Consumers should use prebuilt libs (no Cargo required). " +
53
- "Maintainers can regenerate with -PNitroMetamask_buildEciesFromSource=true."
54
- )
55
- }
42
+ def home = System.getProperty("user.home") ?: System.getenv("HOME") ?: ""
43
+ if (home) {
44
+ def f = new File("${home}/.cargo/bin/${exeName}")
45
+ if (f.isFile() && f.canExecute()) return f.absolutePath
56
46
  }
47
+ return null
57
48
  }
58
49
 
59
50
  tasks.register("cargoNdkBuildEcies", Exec) {
60
- group = "build"
61
- description = "Build libecies.so via Rust (maintainer-only)"
62
- enabled = buildEciesFromSource
63
- onlyIf {
64
- buildEciesFromSource && eciesRustDir.isDirectory() && eciesCargoToml.isFile()
65
- }
66
- workingDir eciesRustDir
51
+ group = "build"
52
+ description = "Build libecies.so from Rust source (local: host ABI only, CI: all ABIs)"
53
+ onlyIf { eciesRustDir.isDirectory() && eciesCargoToml.isFile() }
54
+ workingDir eciesRustDir
67
55
  standardOutput System.out
68
- errorOutput System.err
56
+ errorOutput System.err
69
57
  outputs.dir(eciesJniOut)
70
58
  }
71
59
 
72
- // Resolve NDK + ABIs at configuration time (afterEvaluate). Task actions must NOT touch
73
- // Task.project — configuration cache forbids it (see Gradle 9 config cache requirements).
74
60
  afterEvaluate {
75
61
  def ndkProp = project.android.ndkDirectory
76
- def ndkDir = (ndkProp instanceof File) ? ndkProp : ndkProp.get().asFile
62
+ def ndkDir = (ndkProp instanceof File) ? ndkProp : ndkProp.get().asFile
77
63
  def ndkPath = ndkDir.absolutePath
78
- def ndkVer = project.android.ndkVersion
79
- def abis = reactNativeArchitectures()
64
+ def ndkVer = project.android.ndkVersion
65
+ def abis = resolveAbis()
80
66
  def outPath = eciesJniOut.absolutePath
67
+
81
68
  tasks.named("cargoNdkBuildEcies", Exec).configure {
82
69
  doFirst {
83
- if (!hasCargoExecutable()) {
70
+ def cargo = findBin("cargo")
71
+ if (!cargo) {
84
72
  throw new GradleException(
85
- "Cargo is not available. This build mode is maintainer-only. " +
86
- "Run without -PNitroMetamask_buildEciesFromSource to use bundled prebuilt libraries."
73
+ "cargo not found. Install Rust: https://rustup.rs\n" +
74
+ "Then run: cargo install cargo-ndk --locked && rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android"
87
75
  )
88
76
  }
89
-
90
- eciesJniOut.mkdirs()
91
77
  if (!ndkDir.exists()) {
92
- throw new GradleException("ANDROID NDK not found; ndkVersion is ${ndkVer}")
78
+ throw new GradleException("Android NDK not found (ndkVersion=${ndkVer}). Install it via Android Studio SDK Manager.")
93
79
  }
80
+ eciesJniOut.mkdirs()
94
81
  environment "ANDROID_NDK_HOME", ndkPath
95
82
  environment "ANDROID_NDK_ROOT", ndkPath
96
83
  environment "RUSTFLAGS", "-C link-arg=-Wl,-z,max-page-size=16384"
97
- def args = ["cargo", "ndk"]
98
- abis.each { abi ->
99
- args.add("-t")
100
- args.add(abi)
101
- }
102
- args.addAll(["-o", outPath, "build", "--release"])
84
+ def args = [cargo, "ndk"]
85
+ abis.each { abi -> args += ["-t", abi] }
86
+ args += ["-o", outPath, "build", "--release"]
103
87
  commandLine args
88
+ logger.lifecycle("Building libecies.so for ABIs: ${abis.join(', ')}")
104
89
  }
105
90
  }
106
91
 
107
92
  tasks.matching { it.name.startsWith("preBuild") }.configureEach {
108
- if (buildEciesFromSource) {
109
- dependsOn("cargoNdkBuildEcies")
110
- } else {
111
- dependsOn("verifyPrebuiltEciesLibs")
112
- }
93
+ dependsOn("cargoNdkBuildEcies")
113
94
  }
114
95
  }
@@ -0,0 +1,8 @@
1
+ // IMessegeService.aidl
2
+ package io.metamask.nativesdk;
3
+ import io.metamask.nativesdk.IMessegeServiceCallback;
4
+
5
+ interface IMessegeService {
6
+ void registerCallback(in IMessegeServiceCallback callback);
7
+ void sendMessage(inout Bundle message);
8
+ }
@@ -0,0 +1,8 @@
1
+ // IMessegeServiceCallback.aidl
2
+ package io.metamask.nativesdk;
3
+
4
+ // Declare any non-default types here with import statements
5
+
6
+ interface IMessegeServiceCallback {
7
+ void onMessageReceived(inout Bundle response);
8
+ }
@@ -3,7 +3,12 @@ package com.margelo.nitro.nitrometamask
3
3
  import android.content.Intent
4
4
  import android.content.pm.PackageManager
5
5
  import android.net.Uri
6
+ import android.os.Handler
7
+ import android.os.Looper
6
8
  import android.util.Log
9
+ import androidx.lifecycle.DefaultLifecycleObserver
10
+ import androidx.lifecycle.LifecycleOwner
11
+ import androidx.lifecycle.ProcessLifecycleOwner
7
12
  import com.margelo.nitro.core.Promise
8
13
  import com.margelo.nitro.core.NullType
9
14
  import com.margelo.nitro.nitrometamask.HybridNitroMetamaskSpec
@@ -16,8 +21,10 @@ import io.metamask.androidsdk.Result
16
21
  import io.metamask.androidsdk.DappMetadata
17
22
  import io.metamask.androidsdk.SDKOptions
18
23
  import io.metamask.androidsdk.EthereumRequest
24
+ import kotlinx.coroutines.CancellableContinuation
19
25
  import kotlinx.coroutines.suspendCancellableCoroutine
20
26
  import kotlin.coroutines.resume
27
+ import kotlin.coroutines.resumeWithException
21
28
 
22
29
  class HybridNitroMetamask : HybridNitroMetamaskSpec() {
23
30
  // Configurable dapp URL - defaults to novastera.com if not set
@@ -40,7 +47,41 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
40
47
  // Cache the detected deep link scheme to avoid repeated detection
41
48
  @Volatile
42
49
  private var cachedDeepLinkScheme: String? = null
43
-
50
+
51
+ // Pending operation cancellation handler — set when an operation is in-flight,
52
+ // cleared when the SDK callback arrives or when the foreground observer fires.
53
+ @Volatile
54
+ private var pendingOperationCancellation: (() -> Unit)? = null
55
+
56
+ init {
57
+ // addObserver must be called on the main thread — post to main looper
58
+ // since Nitro constructs hybrid objects on the JS thread.
59
+ Handler(Looper.getMainLooper()).post {
60
+ ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
61
+ override fun onStart(owner: LifecycleOwner) {
62
+ checkAndCancelPendingOperation()
63
+ }
64
+ })
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Atomically clears the pending cancellation handler and invokes it.
70
+ * The double-checked pattern ensures only one caller (SDK callback vs. lifecycle event)
71
+ * wins the race (Requirement 11.3).
72
+ */
73
+ private fun checkAndCancelPendingOperation() {
74
+ val cancel = pendingOperationCancellation
75
+ if (cancel != null) {
76
+ synchronized(this) {
77
+ if (pendingOperationCancellation != null) {
78
+ pendingOperationCancellation = null
79
+ cancel()
80
+ }
81
+ }
82
+ }
83
+ }
84
+
44
85
  // Get or create Ethereum SDK instance
45
86
  // Important: DappMetadata.url must be a valid HTTP/HTTPS URL (not a deep link scheme)
46
87
  // The SDK automatically detects and uses the deep link from AndroidManifest.xml
@@ -112,6 +153,21 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
112
153
  }
113
154
  }
114
155
 
156
+ /**
157
+ * Check if MetaMask is installed on the device.
158
+ * Queries PackageManager for the io.metamask package.
159
+ * Returns true if found, false otherwise.
160
+ */
161
+ private fun isMetaMaskInstalled(): Boolean {
162
+ return try {
163
+ val context = MetamaskContextHolder.get()
164
+ context.packageManager.getPackageInfo("io.metamask", 0)
165
+ true
166
+ } catch (e: PackageManager.NameNotFoundException) {
167
+ false
168
+ }
169
+ }
170
+
115
171
  /**
116
172
  * Get the deep link scheme - uses configured value first, then attempts auto-detection.
117
173
  * Directly reads intent filters from PackageManager to find the scheme with host="mmsdk"
@@ -248,15 +304,29 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
248
304
  // Use Promise.async with coroutines for best practice in Nitro modules
249
305
  // Reference: https://nitro.margelo.com/docs/types/promises
250
306
  return Promise.async {
307
+ if (!isMetaMaskInstalled()) {
308
+ throw Exception("[2] MetaMask is not installed")
309
+ }
251
310
  // Convert callback-based connect() to suspend function using suspendCancellableCoroutine
252
311
  // This handles cancellation properly when JS GC disposes the promise
312
+ var localContinuation: CancellableContinuation<Result>? = null
313
+ pendingOperationCancellation = {
314
+ localContinuation?.let { cont ->
315
+ if (cont.isActive) cont.resumeWithException(
316
+ Exception("MetaMask operation cancelled: user returned to app without completing the request")
317
+ )
318
+ }
319
+ }
253
320
  val result = suspendCancellableCoroutine<Result> { continuation ->
321
+ localContinuation = continuation
254
322
  ethereum.connect { callbackResult ->
323
+ pendingOperationCancellation = null
255
324
  if (continuation.isActive) {
256
325
  continuation.resume(callbackResult)
257
326
  }
258
327
  }
259
328
  }
329
+ pendingOperationCancellation = null
260
330
 
261
331
  when (result) {
262
332
  is Result.Success.Item -> {
@@ -304,6 +374,9 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
304
374
  // Use Promise.async with coroutines for best practice in Nitro modules
305
375
  // Reference: https://nitro.margelo.com/docs/types/promises
306
376
  return Promise.async {
377
+ if (!isMetaMaskInstalled()) {
378
+ throw Exception("[2] MetaMask is not installed")
379
+ }
307
380
  // Verify connection state before attempting to sign
308
381
  // MetaMask SDK requires an active connection to sign messages
309
382
  val address = ethereum.selectedAddress
@@ -322,13 +395,24 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
322
395
 
323
396
  // Convert callback-based sendRequest() to suspend function
324
397
  // The SDK will automatically handle deep link return to the app
398
+ var localContinuation: CancellableContinuation<Result>? = null
399
+ pendingOperationCancellation = {
400
+ localContinuation?.let { cont ->
401
+ if (cont.isActive) cont.resumeWithException(
402
+ Exception("MetaMask operation cancelled: user returned to app without completing the request")
403
+ )
404
+ }
405
+ }
325
406
  val result = suspendCancellableCoroutine<Result> { continuation ->
407
+ localContinuation = continuation
326
408
  ethereum.sendRequest(request) { callbackResult ->
409
+ pendingOperationCancellation = null
327
410
  if (continuation.isActive) {
328
411
  continuation.resume(callbackResult)
329
412
  }
330
413
  }
331
414
  }
415
+ pendingOperationCancellation = null
332
416
 
333
417
  when (result) {
334
418
  is Result.Success.Item -> {
@@ -368,6 +452,9 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
368
452
  // The SDK's connectSign method handles connection and signing in one call
369
453
  return Promise.async {
370
454
  try {
455
+ if (!isMetaMaskInstalled()) {
456
+ throw Exception("[2] MetaMask is not installed")
457
+ }
371
458
  // Construct JSON message with only nonce and exp
372
459
  // We don't include address or chainID - just encrypt nonce and exp
373
460
  val message = org.json.JSONObject().apply {
@@ -380,9 +467,19 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
380
467
  // Use the SDK's connectSign method - it will connect if needed and sign the message
381
468
  // This is the recommended approach per MetaMask Android SDK documentation
382
469
  // The SDK will handle bringing the app back to foreground via deep linking
470
+ var localContinuation: CancellableContinuation<Result>? = null
471
+ pendingOperationCancellation = {
472
+ localContinuation?.let { cont ->
473
+ if (cont.isActive) cont.resumeWithException(
474
+ Exception("MetaMask operation cancelled: user returned to app without completing the request")
475
+ )
476
+ }
477
+ }
383
478
  val result = suspendCancellableCoroutine<Result> { continuation ->
479
+ localContinuation = continuation
384
480
  Log.d("NitroMetamask", "connectSign: Calling ethereum.connectSign with message")
385
481
  ethereum.connectSign(message) { callbackResult ->
482
+ pendingOperationCancellation = null
386
483
  Log.d("NitroMetamask", "connectSign: Received callback result: ${callbackResult.javaClass.simpleName}")
387
484
  if (continuation.isActive) {
388
485
  continuation.resume(callbackResult)
@@ -391,6 +488,7 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
391
488
  }
392
489
  }
393
490
  }
491
+ pendingOperationCancellation = null
394
492
 
395
493
  Log.d("NitroMetamask", "connectSign: Processing result")
396
494
  when (result) {
@@ -548,7 +646,7 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
548
646
  val state = ethereum.ethereumState.value
549
647
  val address = state?.selectedAddress?.takeIf { it.isNotEmpty() } ?: ethereum.selectedAddress
550
648
  Log.d("NitroMetamask", "getAddress: ethereumState.value?.selectedAddress = ${state?.selectedAddress}, ethereum.selectedAddress = ${ethereum.selectedAddress}, final = $address")
551
- if (address == null || address.isEmpty()) {
649
+ if (address.isEmpty()) {
552
650
  Log.w("NitroMetamask", "getAddress: Address is null or empty")
553
651
  // Use NullType.NULL singleton as per Nitro documentation: https://nitro.margelo.com/docs/types/nulls
554
652
  Variant_NullType_String.First(NullType.NULL)
@@ -566,7 +664,7 @@ class HybridNitroMetamask : HybridNitroMetamaskSpec() {
566
664
  val state = ethereum.ethereumState.value
567
665
  val chainIdString = state?.chainId?.takeIf { it.isNotEmpty() } ?: ethereum.chainId
568
666
  Log.d("NitroMetamask", "getChainId: ethereumState.value?.chainId = ${state?.chainId}, ethereum.chainId = ${ethereum.chainId}, final = $chainIdString")
569
- if (chainIdString == null || chainIdString.isEmpty()) {
667
+ if (chainIdString.isEmpty()) {
570
668
  Log.w("NitroMetamask", "getChainId: ChainId is null or empty")
571
669
  // Use NullType.NULL singleton as per Nitro documentation: https://nitro.margelo.com/docs/types/nulls
572
670
  Variant_NullType_Long.First(NullType.NULL)
@@ -0,0 +1,8 @@
1
+ package io.metamask.androidsdk
2
+
3
+ data class AnyRequest(
4
+ override var id: String = TimeStampGenerator.timestamp(),
5
+ override val method: String,
6
+ override val params: Any?
7
+ ) : RpcRequest()
8
+
@@ -0,0 +1,12 @@
1
+ package io.metamask.androidsdk
2
+
3
+ import android.os.Bundle
4
+ import io.metamask.nativesdk.IMessegeServiceCallback
5
+
6
+ open class ClientMessageServiceCallback(
7
+ var onMessage: ((Bundle) -> Unit)? = null
8
+ ) : IMessegeServiceCallback.Stub() {
9
+ override fun onMessageReceived(bundle: Bundle) {
10
+ onMessage?.invoke(bundle)
11
+ }
12
+ }
@@ -0,0 +1,42 @@
1
+ package io.metamask.androidsdk
2
+
3
+ import android.content.ComponentName
4
+ import android.content.ServiceConnection
5
+ import android.os.Bundle
6
+ import android.os.IBinder
7
+ import io.metamask.nativesdk.IMessegeService
8
+ import io.metamask.nativesdk.IMessegeServiceCallback
9
+
10
+ open class ClientServiceConnection(
11
+ var onConnected: (() -> Unit)? = null,
12
+ var onDisconnected: ((ComponentName?) -> Unit)? = null,
13
+ var onBindingDied: ((ComponentName?) -> Unit)? = null,
14
+ var onNullBinding: ((ComponentName?) -> Unit)? = null
15
+ ) : ServiceConnection {
16
+ private var messageService: IMessegeService? = null
17
+
18
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
19
+ messageService = IMessegeService.Stub.asInterface(service)
20
+ onConnected?.invoke()
21
+ }
22
+
23
+ override fun onServiceDisconnected(name: ComponentName?) {
24
+ onDisconnected?.invoke(name)
25
+ }
26
+
27
+ override fun onBindingDied(name: ComponentName?) {
28
+ onBindingDied?.invoke(name)
29
+ }
30
+
31
+ override fun onNullBinding(name: ComponentName?) {
32
+ onNullBinding?.invoke(name)
33
+ }
34
+
35
+ open fun registerCallback(callback: IMessegeServiceCallback) {
36
+ messageService?.registerCallback(callback)
37
+ }
38
+
39
+ open fun sendMessage(bundle: Bundle) {
40
+ messageService?.sendMessage(bundle)
41
+ }
42
+ }