@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
@@ -0,0 +1,122 @@
1
+ package io.metamask.androidsdk
2
+
3
+ import android.content.Context
4
+ import android.security.keystore.KeyGenParameterSpec
5
+ import android.security.keystore.KeyProperties
6
+ import android.util.Base64
7
+ import android.util.Base64.decode
8
+ import android.util.Base64.encodeToString
9
+ import kotlinx.coroutines.*
10
+ import java.security.KeyStore
11
+ import javax.crypto.KeyGenerator
12
+ import javax.crypto.SecretKey
13
+
14
+ class KeyStorage(private val context: Context, private val logger: Logger = DefaultLogger): SecureStorage {
15
+
16
+ private val keyStoreAlias = context.packageName
17
+ private val androidKeyStore = "AndroidKeyStore"
18
+
19
+ private lateinit var keyStore: KeyStore
20
+ private lateinit var secretKey: SecretKey
21
+ private val coroutineScope = CoroutineScope(Dispatchers.IO)
22
+
23
+ init {
24
+ loadSecretKey()
25
+ }
26
+
27
+ private val secretKeyEntry: KeyStore.SecretKeyEntry? get() {
28
+ return try {
29
+ keyStore.getEntry(keyStoreAlias, null) as? KeyStore.SecretKeyEntry
30
+ } catch(e: Exception) {
31
+ logger.error("KeyStorage: ${e.message}")
32
+ null
33
+ }
34
+ }
35
+
36
+ private fun encodedValue(value: String): String {
37
+ val bytes = (value + keyStoreAlias).toByteArray()
38
+ val base64 = encodeToString(bytes, Base64.DEFAULT)
39
+ return base64.replace('/', '_').replace('=', '-').lowercase()
40
+ }
41
+
42
+ override fun loadSecretKey() {
43
+ keyStore = KeyStore.getInstance(androidKeyStore)
44
+ keyStore.load(null)
45
+ secretKey = secretKeyEntry?.secretKey ?: generateSecretKey()
46
+ }
47
+
48
+ private fun generateSecretKey(): SecretKey {
49
+ val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, androidKeyStore)
50
+ val keyGenParameterSpec = KeyGenParameterSpec.Builder(
51
+ keyStoreAlias,
52
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
53
+ )
54
+ .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
55
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
56
+ .build()
57
+
58
+ keyGenerator.init(keyGenParameterSpec)
59
+ return keyGenerator.generateKey()
60
+ }
61
+
62
+ override fun clear(file: String) {
63
+ val encodedFileName = encodedValue(file)
64
+
65
+ coroutineScope.launch {
66
+ context.getSharedPreferences(
67
+ encodedFileName,
68
+ Context.MODE_PRIVATE)
69
+ .edit()
70
+ .clear()
71
+ .apply()
72
+ }
73
+ }
74
+
75
+ override fun clearValue(key: String, file: String) {
76
+ val encodedKey = encodedValue(key)
77
+ val encodedFileName = encodedValue(file)
78
+
79
+ coroutineScope.launch {
80
+ context.getSharedPreferences(
81
+ encodedFileName,
82
+ Context.MODE_PRIVATE)
83
+ .edit()
84
+ .putString(encodedKey, null)
85
+ .apply()
86
+ }
87
+ }
88
+
89
+ override fun putValue(value: String, key: String, file: String) {
90
+ val encodedKey = encodedValue(key)
91
+ val encodedFileName = encodedValue(file)
92
+
93
+ val bytes = value.toByteArray()
94
+ val base64 = encodeToString(bytes, Base64.DEFAULT)
95
+
96
+ coroutineScope.launch {
97
+ context.getSharedPreferences(
98
+ encodedFileName,
99
+ Context.MODE_PRIVATE)
100
+ .edit()
101
+ .putString(encodedKey, base64)
102
+ .apply()
103
+ }
104
+ }
105
+
106
+ override suspend fun getValue(key: String, file: String): String? {
107
+ val encodedKey = encodedValue(key)
108
+ val encodedFileName = encodedValue(file)
109
+
110
+ val base64 = context.getSharedPreferences(
111
+ encodedFileName,
112
+ Context.MODE_PRIVATE)
113
+ .getString(encodedKey, null)
114
+
115
+ if (base64 != null) {
116
+ val bytes = decode(base64, Base64.DEFAULT)
117
+ return bytes.toString(Charsets.UTF_8)
118
+ }
119
+
120
+ return null
121
+ }
122
+ }
@@ -0,0 +1,18 @@
1
+ package io.metamask.androidsdk
2
+
3
+ import android.util.Log
4
+
5
+ interface Logger {
6
+ fun log(message: String)
7
+ fun error(message: String)
8
+ }
9
+
10
+ object DefaultLogger : Logger {
11
+ override fun log(message: String) {
12
+ Log.d(TAG, message)
13
+ }
14
+
15
+ override fun error(message: String) {
16
+ Log.e(TAG, message)
17
+ }
18
+ }
@@ -0,0 +1,3 @@
1
+ package io.metamask.androidsdk
2
+
3
+ data class Message(val id: String, val message: String)
@@ -0,0 +1,11 @@
1
+ package io.metamask.androidsdk
2
+
3
+ enum class MessageType(val value: String) {
4
+ ID("id"),
5
+ TYPE("type"),
6
+ DATA("data"),
7
+ ERROR("error"),
8
+ READY("ready"),
9
+ KEYS_EXCHANGED("keys_exchanged"),
10
+ TERMINATE("terminate"),
11
+ }
@@ -0,0 +1,12 @@
1
+ package io.metamask.androidsdk
2
+ import kotlinx.serialization.Serializable
3
+
4
+ @Serializable
5
+ data class OriginatorInfo(
6
+ val title: String?,
7
+ val url: String?,
8
+ val icon: String?,
9
+ val dappId: String?,
10
+ val platform: String,
11
+ val apiVersion: String
12
+ )
@@ -0,0 +1,8 @@
1
+ package io.metamask.androidsdk
2
+
3
+ import kotlinx.serialization.Serializable
4
+
5
+ @Serializable
6
+ data class RequestError(
7
+ val code: Int,
8
+ val message: String)
@@ -0,0 +1,9 @@
1
+ package io.metamask.androidsdk
2
+
3
+ import kotlinx.serialization.Serializable
4
+
5
+ @Serializable
6
+ data class RequestInfo(
7
+ val type: String,
8
+ val originatorInfo: OriginatorInfo
9
+ )
@@ -0,0 +1,11 @@
1
+ package io.metamask.androidsdk
2
+
3
+ sealed class Result {
4
+ sealed class Success: Result() {
5
+ data class Item(val value: String): Success()
6
+ data class Items(val value: List<String>): Success()
7
+ data class ItemMap(val value: Map<String, Any?>): Success()
8
+ }
9
+
10
+ data class Error(val error: RequestError): Result()
11
+ }
@@ -0,0 +1,7 @@
1
+ package io.metamask.androidsdk
2
+
3
+ sealed class RpcRequest {
4
+ abstract val id: String
5
+ abstract val method: String
6
+ abstract val params: Any?
7
+ }
@@ -0,0 +1,6 @@
1
+ package io.metamask.androidsdk
2
+
3
+ object SDKInfo {
4
+ const val VERSION = "0.6.6"
5
+ const val PLATFORM = "android"
6
+ }
@@ -0,0 +1,6 @@
1
+ package io.metamask.androidsdk
2
+
3
+ data class SDKOptions(
4
+ val infuraAPIKey: String?,
5
+ var readonlyRPCMap: Map<String, String>?
6
+ )
@@ -0,0 +1,9 @@
1
+ package io.metamask.androidsdk
2
+
3
+ interface SecureStorage {
4
+ fun loadSecretKey()
5
+ fun clear(file: String)
6
+ fun clearValue(key: String, file: String)
7
+ fun putValue(value: String, key: String, file: String)
8
+ suspend fun getValue(key: String, file: String): String?
9
+ }
@@ -0,0 +1,10 @@
1
+ package io.metamask.androidsdk
2
+
3
+ data class SessionConfig(
4
+ val sessionId: String,
5
+ val expiryDate: Long
6
+ ) {
7
+ fun isValid(): Boolean {
8
+ return System.currentTimeMillis() < expiryDate
9
+ }
10
+ }
@@ -0,0 +1,92 @@
1
+ package io.metamask.androidsdk
2
+
3
+ import com.google.gson.Gson
4
+ import com.google.gson.reflect.TypeToken
5
+ import kotlinx.coroutines.*
6
+ import java.lang.reflect.Type
7
+
8
+ class SessionManager(
9
+ private val store: SecureStorage,
10
+ var sessionDuration: Long = 30 * 24 * 3600, // 30 days default
11
+ private val logger: Logger = DefaultLogger
12
+ ) {
13
+ var sessionId: String = ""
14
+
15
+ var onInitialized: () -> Unit = {}
16
+ private val coroutineScope = CoroutineScope(Dispatchers.IO)
17
+
18
+ companion object {
19
+ const val SESSION_CONFIG_KEY = "SESSION_CONFIG_KEY"
20
+ const val SESSION_CONFIG_FILE = "SESSION_CONFIG_FILE"
21
+ const val SESSION_ACCOUNT_KEY = "SESSION_ACCOUNT_KEY"
22
+ const val SESSION_CHAIN_ID_KEY = "SESSION_CHAIN_ID_KEY"
23
+ const val DEFAULT_SESSION_DURATION: Long = 30 * 24 * 3600 // 30 days default
24
+ }
25
+
26
+ init {
27
+ coroutineScope.launch {
28
+ val id = getSessionConfig().sessionId
29
+ sessionId = id
30
+ onInitialized()
31
+ }
32
+ }
33
+
34
+ fun updateSessionDuration(duration: Long) {
35
+ logger.log("SessionManager:: Session duration extended by: ${duration/3600.0/24.0} days")
36
+ coroutineScope.launch {
37
+ sessionDuration = duration
38
+ val sessionId = getSessionConfig().sessionId
39
+ val expiryDate = System.currentTimeMillis() + sessionDuration * 1000
40
+ val sessionConfig = SessionConfig(sessionId, expiryDate)
41
+ saveSessionConfig(sessionConfig)
42
+ }
43
+ }
44
+
45
+ suspend fun getSessionConfig(reset: Boolean = false): SessionConfig {
46
+ if (reset) {
47
+ store.clearValue(SESSION_CONFIG_KEY, SESSION_CONFIG_FILE)
48
+ return makeNewSessionConfig()
49
+ }
50
+
51
+ val sessionConfigJson = store.getValue(SESSION_CONFIG_KEY, SESSION_CONFIG_FILE)
52
+ ?: return makeNewSessionConfig()
53
+
54
+ val type: Type = object : TypeToken<SessionConfig>() {}.type
55
+
56
+ return try {
57
+ val sessionConfig: SessionConfig = Gson().fromJson(sessionConfigJson, type)
58
+
59
+ if (sessionConfig.isValid()) {
60
+ SessionConfig(sessionConfig.sessionId, System.currentTimeMillis() + sessionDuration * 1000)
61
+ } else {
62
+ makeNewSessionConfig()
63
+ }
64
+ } catch(e: Exception) {
65
+ logger.error("SessionManager: ${e.message}")
66
+ makeNewSessionConfig()
67
+ }
68
+ }
69
+
70
+ fun saveSessionConfig(sessionConfig: SessionConfig) {
71
+ val sessionConfigJson = Gson().toJson(sessionConfig)
72
+ store.putValue(sessionConfigJson, SESSION_CONFIG_KEY, SESSION_CONFIG_FILE)
73
+ }
74
+
75
+ fun clearSession(onComplete: () -> Unit) {
76
+ coroutineScope.launch {
77
+ store.clearValue(SESSION_CONFIG_KEY, SESSION_CONFIG_FILE)
78
+ makeNewSessionConfig()
79
+ sessionId = getSessionConfig().sessionId
80
+ onComplete()
81
+ }
82
+ }
83
+
84
+ fun makeNewSessionConfig(): SessionConfig {
85
+ store.clear(SESSION_CONFIG_FILE)
86
+ val sessionId = TimeStampGenerator.timestamp()
87
+ val expiryDate = System.currentTimeMillis() + sessionDuration * 1000
88
+ val sessionConfig = SessionConfig(sessionId, expiryDate)
89
+ saveSessionConfig(sessionConfig)
90
+ return sessionConfig
91
+ }
92
+ }
@@ -0,0 +1,8 @@
1
+ package io.metamask.androidsdk
2
+
3
+ data class
4
+
5
+ SubmittedRequest(
6
+ val request: RpcRequest,
7
+ val callback: (Result) -> Unit
8
+ )
@@ -0,0 +1,7 @@
1
+ package io.metamask.androidsdk
2
+
3
+ object TimeStampGenerator {
4
+ fun timestamp(): String {
5
+ return System.currentTimeMillis().toString()
6
+ }
7
+ }
@@ -0,0 +1,128 @@
1
+ package com.margelo.nitro.nitrometamask
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertNull
5
+ import org.junit.Assert.assertNotNull
6
+ import org.junit.Test
7
+ import java.util.concurrent.atomic.AtomicInteger
8
+
9
+ /**
10
+ * Unit tests for the cancellation state machine in HybridNitroMetamask.
11
+ *
12
+ * Since `checkAndCancelPendingOperation` and `pendingOperationCancellation` are private,
13
+ * we test the observable behavior through a minimal test double that replicates the
14
+ * exact state machine logic from the production class.
15
+ *
16
+ * Validates: Requirements 11.5, 11.6
17
+ */
18
+ class CancellationStateMachineTest {
19
+
20
+ /**
21
+ * Minimal replica of the cancellation state machine extracted from HybridNitroMetamask.
22
+ * This mirrors the production logic exactly so the tests validate the real algorithm.
23
+ */
24
+ private class CancellationStateMachine {
25
+ @Volatile
26
+ var pendingOperationCancellation: (() -> Unit)? = null
27
+
28
+ fun checkAndCancelPendingOperation() {
29
+ val cancel = pendingOperationCancellation
30
+ if (cancel != null) {
31
+ synchronized(this) {
32
+ if (pendingOperationCancellation != null) {
33
+ pendingOperationCancellation = null
34
+ cancel()
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ @Test
42
+ fun `setting then clearing handler leaves state null`() {
43
+ val sm = CancellationStateMachine()
44
+
45
+ sm.pendingOperationCancellation = { /* no-op */ }
46
+ assertNotNull("Handler should be set", sm.pendingOperationCancellation)
47
+
48
+ sm.pendingOperationCancellation = null
49
+ assertNull("Handler should be null after explicit clear", sm.pendingOperationCancellation)
50
+ }
51
+
52
+ @Test
53
+ fun `checkAndCancelPendingOperation with no pending operation is a no-op`() {
54
+ val sm = CancellationStateMachine()
55
+ val callCount = AtomicInteger(0)
56
+
57
+ // No handler set — should not throw or invoke anything
58
+ sm.checkAndCancelPendingOperation()
59
+
60
+ assertEquals("No handler should have been called", 0, callCount.get())
61
+ assertNull("Handler should remain null", sm.pendingOperationCancellation)
62
+ }
63
+
64
+ @Test
65
+ fun `checkAndCancelPendingOperation invokes handler and clears state`() {
66
+ val sm = CancellationStateMachine()
67
+ val callCount = AtomicInteger(0)
68
+
69
+ sm.pendingOperationCancellation = { callCount.incrementAndGet() }
70
+ sm.checkAndCancelPendingOperation()
71
+
72
+ assertEquals("Handler should have been called exactly once", 1, callCount.get())
73
+ assertNull("Handler should be null after cancellation fires", sm.pendingOperationCancellation)
74
+ }
75
+
76
+ @Test
77
+ fun `checkAndCancelPendingOperation is idempotent — second call is a no-op`() {
78
+ val sm = CancellationStateMachine()
79
+ val callCount = AtomicInteger(0)
80
+
81
+ sm.pendingOperationCancellation = { callCount.incrementAndGet() }
82
+
83
+ sm.checkAndCancelPendingOperation()
84
+ sm.checkAndCancelPendingOperation() // second call — handler already cleared
85
+
86
+ assertEquals("Handler should have been called exactly once", 1, callCount.get())
87
+ assertNull("Handler should remain null after second call", sm.pendingOperationCancellation)
88
+ }
89
+
90
+ @Test
91
+ fun `SDK callback clearing handler prevents cancellation from firing`() {
92
+ // Simulates the race-free path: SDK callback arrives first, clears the handler,
93
+ // then the foreground event fires but finds nothing to cancel.
94
+ val sm = CancellationStateMachine()
95
+ val cancelCallCount = AtomicInteger(0)
96
+
97
+ sm.pendingOperationCancellation = { cancelCallCount.incrementAndGet() }
98
+
99
+ // SDK callback arrives — clears handler (success path)
100
+ sm.pendingOperationCancellation = null
101
+
102
+ // Foreground event fires — should be a no-op
103
+ sm.checkAndCancelPendingOperation()
104
+
105
+ assertEquals("Cancellation should not have fired after SDK cleared the handler", 0, cancelCallCount.get())
106
+ }
107
+
108
+ @Test
109
+ fun `state is clean after cancellation so subsequent operations work correctly`() {
110
+ val sm = CancellationStateMachine()
111
+ val firstCallCount = AtomicInteger(0)
112
+ val secondCallCount = AtomicInteger(0)
113
+
114
+ // First operation
115
+ sm.pendingOperationCancellation = { firstCallCount.incrementAndGet() }
116
+ sm.checkAndCancelPendingOperation()
117
+
118
+ assertEquals("First handler called once", 1, firstCallCount.get())
119
+ assertNull("State clean after first cancellation", sm.pendingOperationCancellation)
120
+
121
+ // Second operation — state should be clean, new handler works correctly
122
+ sm.pendingOperationCancellation = { secondCallCount.incrementAndGet() }
123
+ sm.checkAndCancelPendingOperation()
124
+
125
+ assertEquals("Second handler called once", 1, secondCallCount.get())
126
+ assertNull("State clean after second cancellation", sm.pendingOperationCancellation)
127
+ }
128
+ }
@@ -0,0 +1,65 @@
1
+ package com.margelo.nitro.nitrometamask
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Test
5
+
6
+ /**
7
+ * Unit tests for chainId hex string parsing logic.
8
+ *
9
+ * Mirrors the parsing logic in HybridNitroMetamask.kt so the tests validate
10
+ * the real algorithm without requiring Android SDK dependencies.
11
+ *
12
+ * Validates: Requirements 2.6, 5.5
13
+ *
14
+ * Property 1: ChainId hex parsing round-trip
15
+ * For any valid hex string of the form "0x{N}", parsing it to a Long should
16
+ * produce the correct integer value.
17
+ */
18
+ class ChainIdParsingTest {
19
+
20
+ /**
21
+ * Mirrors the chainId parsing logic from HybridNitroMetamask.kt.
22
+ */
23
+ fun parseChainId(chainIdString: String): Long {
24
+ return if (chainIdString.startsWith("0x") || chainIdString.startsWith("0X")) {
25
+ chainIdString.substring(2).toLong(16)
26
+ } else {
27
+ chainIdString.toLong()
28
+ }
29
+ }
30
+
31
+ @Test
32
+ fun `parse Ethereum mainnet chainId 0x1 returns 1`() {
33
+ assertEquals(1L, parseChainId("0x1"))
34
+ }
35
+
36
+ @Test
37
+ fun `parse Polygon chainId 0x89 returns 137`() {
38
+ assertEquals(137L, parseChainId("0x89"))
39
+ }
40
+
41
+ @Test
42
+ fun `parse chainId 0xa returns 10`() {
43
+ assertEquals(10L, parseChainId("0xa"))
44
+ }
45
+
46
+ @Test
47
+ fun `parse decimal fallback string 1 returns 1`() {
48
+ assertEquals(1L, parseChainId("1"))
49
+ }
50
+
51
+ @Test
52
+ fun `parse uppercase 0X prefix is handled correctly`() {
53
+ assertEquals(1L, parseChainId("0X1"))
54
+ }
55
+
56
+ @Test
57
+ fun `parse larger hex chainId 0x38 returns 56 (BSC)`() {
58
+ assertEquals(56L, parseChainId("0x38"))
59
+ }
60
+
61
+ @Test
62
+ fun `parse hex chainId with multiple digits 0x2105 returns 8453 (Base)`() {
63
+ assertEquals(8453L, parseChainId("0x2105"))
64
+ }
65
+ }