@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.
Files changed (127) hide show
  1. package/NitroMetamask.podspec +12 -3
  2. package/README.md +3 -1
  3. package/android/build.gradle +14 -32
  4. package/android/cargo-ecies.gradle +60 -88
  5. package/android/src/main/aidl/io/metamask/nativesdk/IMessegeService.aidl +8 -0
  6. package/android/src/main/aidl/io/metamask/nativesdk/IMessegeServiceCallback.aidl +8 -0
  7. package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +101 -3
  8. package/android/src/main/java/io/metamask/androidsdk/AnyRequest.kt +8 -0
  9. package/android/src/main/java/io/metamask/androidsdk/ClientMessageServiceCallback.kt +12 -0
  10. package/android/src/main/java/io/metamask/androidsdk/ClientServiceConnection.kt +42 -0
  11. package/android/src/main/java/io/metamask/androidsdk/CommunicationClient.kt +525 -0
  12. package/android/src/main/java/io/metamask/androidsdk/CommunicationClientModule.kt +47 -0
  13. package/android/src/main/java/io/metamask/androidsdk/CommunicationClientModuleInterface.kt +11 -0
  14. package/android/src/main/java/io/metamask/androidsdk/Constants.kt +5 -0
  15. package/android/src/main/java/io/metamask/androidsdk/Crypto.kt +35 -0
  16. package/android/src/main/java/io/metamask/androidsdk/DappMetadata.kt +36 -0
  17. package/android/src/main/java/io/metamask/androidsdk/Encryption.kt +9 -0
  18. package/android/src/main/java/io/metamask/androidsdk/ErrorType.kt +41 -0
  19. package/android/src/main/java/io/metamask/androidsdk/Ethereum.kt +328 -0
  20. package/android/src/main/java/io/metamask/androidsdk/EthereumEventCallback.kt +6 -0
  21. package/android/src/main/java/io/metamask/androidsdk/EthereumMethod.kt +80 -0
  22. package/android/src/main/java/io/metamask/androidsdk/EthereumRequest.kt +7 -0
  23. package/android/src/main/java/io/metamask/androidsdk/EthereumState.kt +7 -0
  24. package/android/src/main/java/io/metamask/androidsdk/KeyExchange.kt +77 -0
  25. package/android/src/main/java/io/metamask/androidsdk/KeyExchangeMessageType.kt +20 -0
  26. package/android/src/main/java/io/metamask/androidsdk/KeyStorage.kt +122 -0
  27. package/android/src/main/java/io/metamask/androidsdk/Logger.kt +18 -0
  28. package/android/src/main/java/io/metamask/androidsdk/Message.kt +3 -0
  29. package/android/src/main/java/io/metamask/androidsdk/MessageType.kt +11 -0
  30. package/android/src/main/java/io/metamask/androidsdk/OriginatorInfo.kt +12 -0
  31. package/android/src/main/java/io/metamask/androidsdk/RequestError.kt +8 -0
  32. package/android/src/main/java/io/metamask/androidsdk/RequestInfo.kt +9 -0
  33. package/android/src/main/java/io/metamask/androidsdk/Result.kt +11 -0
  34. package/android/src/main/java/io/metamask/androidsdk/RpcRequest.kt +7 -0
  35. package/android/src/main/java/io/metamask/androidsdk/SDKInfo.kt +6 -0
  36. package/android/src/main/java/io/metamask/androidsdk/SDKOptions.kt +6 -0
  37. package/android/src/main/java/io/metamask/androidsdk/SecureStorage.kt +9 -0
  38. package/android/src/main/java/io/metamask/androidsdk/SessionConfig.kt +10 -0
  39. package/android/src/main/java/io/metamask/androidsdk/SessionManager.kt +92 -0
  40. package/android/src/main/java/io/metamask/androidsdk/SubmittedRequest.kt +8 -0
  41. package/android/src/main/java/io/metamask/androidsdk/TimeStampGenerator.kt +7 -0
  42. package/android/src/main/jniLibs/arm64-v8a/libecies.so +0 -0
  43. package/android/src/main/jniLibs/armeabi-v7a/libecies.so +0 -0
  44. package/android/src/main/jniLibs/x86/libecies.so +0 -0
  45. package/android/src/main/jniLibs/x86_64/libecies.so +0 -0
  46. package/android/src/test/java/com/margelo/nitro/nitrometamask/CancellationStateMachineTest.kt +128 -0
  47. package/android/src/test/java/com/margelo/nitro/nitrometamask/ChainIdParsingTest.kt +65 -0
  48. package/android/src/test/java/com/margelo/nitro/nitrometamask/ConfigureStateMachineTest.kt +140 -0
  49. package/android/src/test/java/com/margelo/nitro/nitrometamask/ConnectSignJsonTest.kt +76 -0
  50. package/android/src/test/java/com/margelo/nitro/nitrometamask/MetaMaskInstallationCheckTest.kt +42 -0
  51. package/android/src/test/java/com/margelo/nitro/nitrometamask/PersonalSignParamsTest.kt +75 -0
  52. package/ios/Frameworks/Ecies.xcframework/Info.plist +47 -0
  53. package/ios/Frameworks/Ecies.xcframework/ios-arm64/Headers/ecies.h +20 -0
  54. package/ios/Frameworks/Ecies.xcframework/ios-arm64/Headers/module.modulemap +4 -0
  55. package/ios/Frameworks/Ecies.xcframework/ios-arm64/libecies.a +0 -0
  56. package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/Headers/ecies.h +20 -0
  57. package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/Headers/module.modulemap +4 -0
  58. package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/libecies.a +0 -0
  59. package/ios/HybridNitroMetamask.swift +119 -54
  60. package/ios/NitroMetamaskTests/CancellationStateMachineTests.swift +150 -0
  61. package/ios/NitroMetamaskTests/ChainIdParsingTests.swift +117 -0
  62. package/ios/NitroMetamaskTests/ConfigureStateMachineTests.swift +174 -0
  63. package/ios/NitroMetamaskTests/ConnectSignJsonTests.swift +168 -0
  64. package/ios/NitroMetamaskTests/DefaultDappUrlTests.swift +80 -0
  65. package/ios/NitroMetamaskTests/PersonalSignParamsTests.swift +101 -0
  66. package/ios/metamask-ios-sdk/CommunicationLayer/CommClient.swift +43 -0
  67. package/ios/metamask-ios-sdk/CommunicationLayer/CommClientFactory.swift +17 -0
  68. package/ios/metamask-ios-sdk/CommunicationLayer/CommLayer.swift +36 -0
  69. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/Deeplink.swift +26 -0
  70. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/DeeplinkClient.swift +199 -0
  71. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/DeeplinkManager.swift +83 -0
  72. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/String.swift +48 -0
  73. package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/URLOpener.swift +19 -0
  74. package/ios/metamask-ios-sdk/CommunicationLayer/SocketClient.swift +27 -0
  75. package/ios/metamask-ios-sdk/Crypto/Crypto.swift +72 -0
  76. package/ios/metamask-ios-sdk/Crypto/Encoding.swift +15 -0
  77. package/ios/metamask-ios-sdk/Crypto/KeyExchange.swift +236 -0
  78. package/ios/metamask-ios-sdk/DeviceInfo/DeviceInfo.swift +11 -0
  79. package/ios/metamask-ios-sdk/Ethereum/AppMetadata.swift +28 -0
  80. package/ios/metamask-ios-sdk/Ethereum/ErrorType.swift +62 -0
  81. package/ios/metamask-ios-sdk/Ethereum/Ethereum.swift +810 -0
  82. package/ios/metamask-ios-sdk/Ethereum/EthereumMethod.swift +111 -0
  83. package/ios/metamask-ios-sdk/Ethereum/EthereumRequest.swift +40 -0
  84. package/ios/metamask-ios-sdk/Ethereum/EthereumWrapper.swift +10 -0
  85. package/ios/metamask-ios-sdk/Ethereum/RPCRequest.swift +14 -0
  86. package/ios/metamask-ios-sdk/Ethereum/RequestError.swift +88 -0
  87. package/ios/metamask-ios-sdk/Ethereum/ResponseMethod.swift +22 -0
  88. package/ios/metamask-ios-sdk/Ethereum/SubmitRequest.swift +26 -0
  89. package/ios/metamask-ios-sdk/Ethereum/TimestampGenerator.swift +16 -0
  90. package/ios/metamask-ios-sdk/Extensions/NSRecursiveLock.swift +14 -0
  91. package/ios/metamask-ios-sdk/Extensions/Notification.swift +10 -0
  92. package/ios/metamask-ios-sdk/Logger/Logging.swift +27 -0
  93. package/ios/metamask-ios-sdk/Models/AddChainParameters.swift +35 -0
  94. package/ios/metamask-ios-sdk/Models/Event.swift +19 -0
  95. package/ios/metamask-ios-sdk/Models/Mappable.swift +40 -0
  96. package/ios/metamask-ios-sdk/Models/NativeCurrency.swift +25 -0
  97. package/ios/metamask-ios-sdk/Models/OriginatorInfo.swift +26 -0
  98. package/ios/metamask-ios-sdk/Models/RequestInfo.swift +18 -0
  99. package/ios/metamask-ios-sdk/Models/SignContract.swift +48 -0
  100. package/ios/metamask-ios-sdk/Models/Typealiases.swift +9 -0
  101. package/ios/metamask-ios-sdk/Persistence/SecureStore.swift +134 -0
  102. package/ios/metamask-ios-sdk/Persistence/SessionConfig.swift +24 -0
  103. package/ios/metamask-ios-sdk/Persistence/SessionManager.swift +56 -0
  104. package/ios/metamask-ios-sdk/SDK/Dependencies.swift +35 -0
  105. package/ios/metamask-ios-sdk/SDK/MetaMaskSDK.swift +215 -0
  106. package/ios/metamask-ios-sdk/SDK/SDKInfo.swift +37 -0
  107. package/ios/metamask-ios-sdk/SDK/SDKOptions.swift +16 -0
  108. package/lib/commonjs/index.js +50 -3
  109. package/lib/commonjs/index.js.map +1 -1
  110. package/lib/module/index.js +49 -3
  111. package/lib/module/index.js.map +1 -1
  112. package/lib/typescript/src/__tests__/parseNitroError.test.d.ts +2 -0
  113. package/lib/typescript/src/__tests__/parseNitroError.test.d.ts.map +1 -0
  114. package/lib/typescript/src/index.d.ts +43 -3
  115. package/lib/typescript/src/index.d.ts.map +1 -1
  116. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +29 -1
  117. package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts.map +1 -1
  118. package/package.json +21 -12
  119. package/react-native.config.js +5 -0
  120. package/rust/ecies-jni/Cargo.lock +50 -86
  121. package/rust/ecies-jni/Cargo.toml +1 -1
  122. package/rust/ecies-jni/src/lib.rs +164 -100
  123. package/src/__tests__/parseNitroError.test.ts +35 -0
  124. package/src/index.ts +53 -5
  125. package/src/specs/nitro-metamask.nitro.ts +29 -1
  126. package/scripts/verify-16k-page-alignment.py +0 -117
  127. 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
package/README.md CHANGED
@@ -63,9 +63,11 @@ No extra Android configuration is required for consumers of this package.
63
63
  For app developers, the expected upgrade path is:
64
64
 
65
65
  1. Update to the new npm version.
66
- 2. Clean install + clean Android build.
66
+ 2. Clean install + clean Android build (if you ever hit CMake / `react_codegen_*` / missing `codegen/jni` errors for this package, delete `android/app/.cxx` and `android/app/build`, then rebuild so autolinking regenerates).
67
67
  3. Rebuild release AAB/APK.
68
68
 
69
+ **New Architecture:** this library is intended for apps with `newArchEnabled=true` (Nitro + Nitrogen); it does not register React Native’s Codegen `react_codegen_*` target.
70
+
69
71
  No manual Gradle dependency override should be needed in the consuming app.
70
72
 
71
73
  ### iOS Configuration
@@ -18,15 +18,14 @@ def isNewArchitectureEnabled() {
18
18
  return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
19
19
  }
20
20
 
21
+ // Nitro hybrid module: native bindings come from nitrogen/generated (NitroMetamask+autolinking.gradle + CMake).
22
+ // Do not apply com.facebook.react / react { libraryName } here — that registers RN New-Arch Codegen (react_codegen_*)
23
+ // and breaks consumers (CMake expects android/build/generated/source/codegen/jni/ which this package never ships).
21
24
  apply plugin: "com.android.library"
22
25
  apply plugin: 'org.jetbrains.kotlin.android'
23
26
  apply from: '../nitrogen/generated/android/NitroMetamask+autolinking.gradle'
24
27
  apply from: "./fix-prefab.gradle"
25
28
 
26
- if (isNewArchitectureEnabled()) {
27
- apply plugin: "com.facebook.react"
28
- }
29
-
30
29
  def getExtOrDefault(name) {
31
30
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["NitroMetamask_" + name]
32
31
  }
@@ -98,6 +97,7 @@ android {
98
97
  buildFeatures {
99
98
  buildConfig true
100
99
  prefab true
100
+ aidl true
101
101
  }
102
102
 
103
103
  buildTypes {
@@ -117,13 +117,10 @@ android {
117
117
 
118
118
  sourceSets {
119
119
  main {
120
- jniLibs.srcDirs += ["${buildDir}/intermediates/rust-ecies-jni"]
121
- if (isNewArchitectureEnabled()) {
122
- java.srcDirs += [
123
- // React Codegen files
124
- "${project.buildDir}/generated/source/codegen/java"
125
- ]
126
- }
120
+ // Local/CI build: libecies.so built from rust/ecies-jni via cargoNdkBuildEcies
121
+ // npm consumers: libecies.so shipped as prebuilt in android/src/main/jniLibs
122
+ jniLibs.srcDirs += ["${buildDir}/intermediates/rust-ecies-jni", "src/main/jniLibs"]
123
+ aidl.srcDirs += ["src/main/aidl"]
127
124
  }
128
125
  }
129
126
  }
@@ -133,13 +130,6 @@ repositories {
133
130
  google()
134
131
  }
135
132
 
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
133
  dependencies {
144
134
  // For < 0.71, this will be from the local maven repo
145
135
  // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
@@ -153,19 +143,11 @@ dependencies {
153
143
  // Required for Promise.async with coroutines in Nitro modules
154
144
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
155
145
 
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
- }
161
- }
146
+ // Gsonused by vendored MetaMask SDK source for JSON serialization
147
+ implementation "com.google.code.gson:gson:2.11.0"
162
148
 
163
- apply from: "./cargo-ecies.gradle"
149
+ // ProcessLifecycleOwner — required for user cancellation detection (Requirement 11.4)
150
+ implementation "androidx.lifecycle:lifecycle-process:2.10.0"
151
+ }
164
152
 
165
- if (isNewArchitectureEnabled()) {
166
- react {
167
- jsRootDir = file("../src/")
168
- libraryName = "NitroMetamask"
169
- codegenJavaPackageName = "com.margelo.nitro.nitrometamask"
170
- }
171
- }
153
+ apply from: "./cargo-ecies.gradle"
@@ -1,123 +1,95 @@
1
- def eciesRustDir = file("${projectDir}/../rust/ecies-jni")
2
- 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()
6
-
7
- def configuredAbis = ((rootProject.getProperties().get("reactNativeArchitectures"))?.split(",")
8
- ?: ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]) as List<String>
9
-
10
- def hasCargoExecutable = { ->
11
- def path = System.getenv("PATH") ?: ""
12
- if (path.isEmpty()) {
13
- return false
14
- }
15
-
16
- def names = ["cargo"]
17
- def isWindows = (System.getProperty("os.name") ?: "").toLowerCase().contains("win")
18
- if (isWindows) {
19
- names = ["cargo.exe", "cargo.bat", "cargo.cmd"]
20
- }
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
+ // ──────────────────────────────────────────────────────────────────────────────
21
11
 
22
- for (dirPath in path.split(java.io.File.pathSeparator)) {
23
- for (name in names) {
24
- def candidate = new File(dirPath, name)
25
- if (candidate.isFile() && candidate.canExecute()) {
26
- return true
27
- }
28
- }
29
- }
12
+ def eciesRustDir = file("${projectDir}/../rust/ecies-jni")
13
+ def eciesCargoToml = file("${projectDir}/../rust/ecies-jni/Cargo.toml")
14
+ def eciesJniOut = file("${buildDir}/intermediates/rust-ecies-jni")
30
15
 
31
- return false
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
32
22
  }
33
23
 
34
- def hasPrebuiltEciesForConfiguredAbis = { ->
35
- def missing = []
36
- configuredAbis.each { abi ->
37
- def soFile = new File(eciesPrebuiltDir, "${abi}/libecies.so")
38
- if (!soFile.isFile()) {
39
- missing.add("${abi}/libecies.so")
40
- }
41
- }
42
- return [available: missing.isEmpty(), missing: missing]
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()]
43
31
  }
44
32
 
45
- tasks.register("verifyPrebuiltEciesLibs") {
46
- group = "verification"
47
- description = "Verify prebuilt libecies.so exists for all configured ABIs"
48
-
49
- doLast {
50
- def result = hasPrebuiltEciesForConfiguredAbis()
51
- def missing = result.missing
52
-
53
- if (!missing.isEmpty()) {
54
- throw new GradleException(
55
- "Missing prebuilt ECIES native libraries under android/src/main/jniLibs: ${missing.join(', ')}. " +
56
- "If this is a source checkout, install Rust + cargo-ndk and build with " +
57
- "-PNitroMetamask_buildEciesFromSource=true."
58
- )
59
- }
33
+ // Find cargo/cargo-ndk — searches PATH then ~/.cargo/bin (rustup default)
34
+ def findBin = { String name ->
35
+ def isWindows = (System.getProperty("os.name") ?: "").toLowerCase().contains("win")
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
41
+ }
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
60
46
  }
47
+ return null
61
48
  }
62
49
 
63
50
  tasks.register("cargoNdkBuildEcies", Exec) {
64
- group = "build"
65
- description = "Build libecies.so via Rust (maintainer-only)"
66
- onlyIf {
67
- def prebuiltStatus = hasPrebuiltEciesForConfiguredAbis()
68
- (buildEciesFromSource || !prebuiltStatus.available) &&
69
- eciesRustDir.isDirectory() && eciesCargoToml.isFile()
70
- }
71
- 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
72
55
  standardOutput System.out
73
- errorOutput System.err
56
+ errorOutput System.err
74
57
  outputs.dir(eciesJniOut)
75
58
  }
76
59
 
77
- // Resolve NDK + ABIs at configuration time (afterEvaluate). Task actions must NOT touch
78
- // Task.project — configuration cache forbids it (see Gradle 9 config cache requirements).
79
60
  afterEvaluate {
80
61
  def ndkProp = project.android.ndkDirectory
81
- def ndkDir = (ndkProp instanceof File) ? ndkProp : ndkProp.get().asFile
62
+ def ndkDir = (ndkProp instanceof File) ? ndkProp : ndkProp.get().asFile
82
63
  def ndkPath = ndkDir.absolutePath
83
- def ndkVer = project.android.ndkVersion
84
- def abis = configuredAbis
64
+ def ndkVer = project.android.ndkVersion
65
+ def abis = resolveAbis()
85
66
  def outPath = eciesJniOut.absolutePath
67
+
86
68
  tasks.named("cargoNdkBuildEcies", Exec).configure {
87
69
  doFirst {
88
- if (!hasCargoExecutable()) {
70
+ def cargo = findBin("cargo")
71
+ if (!cargo) {
89
72
  throw new GradleException(
90
- "Cargo is not available. This build mode is maintainer-only. " +
91
- "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"
92
75
  )
93
76
  }
94
-
95
- eciesJniOut.mkdirs()
96
77
  if (!ndkDir.exists()) {
97
- 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.")
98
79
  }
80
+ eciesJniOut.mkdirs()
99
81
  environment "ANDROID_NDK_HOME", ndkPath
100
82
  environment "ANDROID_NDK_ROOT", ndkPath
101
83
  environment "RUSTFLAGS", "-C link-arg=-Wl,-z,max-page-size=16384"
102
- def args = ["cargo", "ndk"]
103
- abis.each { abi ->
104
- args.add("-t")
105
- args.add(abi)
106
- }
107
- args.addAll(["-o", outPath, "build", "--release"])
84
+ def args = [cargo, "ndk"]
85
+ abis.each { abi -> args += ["-t", abi] }
86
+ args += ["-o", outPath, "build", "--release"]
108
87
  commandLine args
88
+ logger.lifecycle("Building libecies.so for ABIs: ${abis.join(', ')}")
109
89
  }
110
90
  }
111
91
 
112
- def prebuiltStatus = hasPrebuiltEciesForConfiguredAbis()
113
- def canBuildFromSource = hasCargoExecutable() && eciesRustDir.isDirectory() && eciesCargoToml.isFile()
114
- def shouldBuildFromSource = buildEciesFromSource || (!prebuiltStatus.available && canBuildFromSource)
115
-
116
92
  tasks.matching { it.name.startsWith("preBuild") }.configureEach {
117
- if (shouldBuildFromSource) {
118
- dependsOn("cargoNdkBuildEcies")
119
- } else {
120
- dependsOn("verifyPrebuiltEciesLibs")
121
- }
93
+ dependsOn("cargoNdkBuildEcies")
122
94
  }
123
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
+ }