@novastera-oss/nitro-metamask 0.6.3 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NitroMetamask.podspec +12 -3
- package/README.md +3 -1
- package/android/build.gradle +14 -32
- package/android/cargo-ecies.gradle +60 -88
- package/android/src/main/aidl/io/metamask/nativesdk/IMessegeService.aidl +8 -0
- package/android/src/main/aidl/io/metamask/nativesdk/IMessegeServiceCallback.aidl +8 -0
- package/android/src/main/java/com/margelo/nitro/nitrometamask/HybridNitroMetamask.kt +101 -3
- package/android/src/main/java/io/metamask/androidsdk/AnyRequest.kt +8 -0
- package/android/src/main/java/io/metamask/androidsdk/ClientMessageServiceCallback.kt +12 -0
- package/android/src/main/java/io/metamask/androidsdk/ClientServiceConnection.kt +42 -0
- package/android/src/main/java/io/metamask/androidsdk/CommunicationClient.kt +525 -0
- package/android/src/main/java/io/metamask/androidsdk/CommunicationClientModule.kt +47 -0
- package/android/src/main/java/io/metamask/androidsdk/CommunicationClientModuleInterface.kt +11 -0
- package/android/src/main/java/io/metamask/androidsdk/Constants.kt +5 -0
- package/android/src/main/java/io/metamask/androidsdk/Crypto.kt +35 -0
- package/android/src/main/java/io/metamask/androidsdk/DappMetadata.kt +36 -0
- package/android/src/main/java/io/metamask/androidsdk/Encryption.kt +9 -0
- package/android/src/main/java/io/metamask/androidsdk/ErrorType.kt +41 -0
- package/android/src/main/java/io/metamask/androidsdk/Ethereum.kt +328 -0
- package/android/src/main/java/io/metamask/androidsdk/EthereumEventCallback.kt +6 -0
- package/android/src/main/java/io/metamask/androidsdk/EthereumMethod.kt +80 -0
- package/android/src/main/java/io/metamask/androidsdk/EthereumRequest.kt +7 -0
- package/android/src/main/java/io/metamask/androidsdk/EthereumState.kt +7 -0
- package/android/src/main/java/io/metamask/androidsdk/KeyExchange.kt +77 -0
- package/android/src/main/java/io/metamask/androidsdk/KeyExchangeMessageType.kt +20 -0
- package/android/src/main/java/io/metamask/androidsdk/KeyStorage.kt +122 -0
- package/android/src/main/java/io/metamask/androidsdk/Logger.kt +18 -0
- package/android/src/main/java/io/metamask/androidsdk/Message.kt +3 -0
- package/android/src/main/java/io/metamask/androidsdk/MessageType.kt +11 -0
- package/android/src/main/java/io/metamask/androidsdk/OriginatorInfo.kt +12 -0
- package/android/src/main/java/io/metamask/androidsdk/RequestError.kt +8 -0
- package/android/src/main/java/io/metamask/androidsdk/RequestInfo.kt +9 -0
- package/android/src/main/java/io/metamask/androidsdk/Result.kt +11 -0
- package/android/src/main/java/io/metamask/androidsdk/RpcRequest.kt +7 -0
- package/android/src/main/java/io/metamask/androidsdk/SDKInfo.kt +6 -0
- package/android/src/main/java/io/metamask/androidsdk/SDKOptions.kt +6 -0
- package/android/src/main/java/io/metamask/androidsdk/SecureStorage.kt +9 -0
- package/android/src/main/java/io/metamask/androidsdk/SessionConfig.kt +10 -0
- package/android/src/main/java/io/metamask/androidsdk/SessionManager.kt +92 -0
- package/android/src/main/java/io/metamask/androidsdk/SubmittedRequest.kt +8 -0
- package/android/src/main/java/io/metamask/androidsdk/TimeStampGenerator.kt +7 -0
- package/android/src/main/jniLibs/arm64-v8a/libecies.so +0 -0
- package/android/src/main/jniLibs/armeabi-v7a/libecies.so +0 -0
- package/android/src/main/jniLibs/x86/libecies.so +0 -0
- package/android/src/main/jniLibs/x86_64/libecies.so +0 -0
- package/android/src/test/java/com/margelo/nitro/nitrometamask/CancellationStateMachineTest.kt +128 -0
- package/android/src/test/java/com/margelo/nitro/nitrometamask/ChainIdParsingTest.kt +65 -0
- package/android/src/test/java/com/margelo/nitro/nitrometamask/ConfigureStateMachineTest.kt +140 -0
- package/android/src/test/java/com/margelo/nitro/nitrometamask/ConnectSignJsonTest.kt +76 -0
- package/android/src/test/java/com/margelo/nitro/nitrometamask/MetaMaskInstallationCheckTest.kt +42 -0
- package/android/src/test/java/com/margelo/nitro/nitrometamask/PersonalSignParamsTest.kt +75 -0
- package/ios/Frameworks/Ecies.xcframework/Info.plist +47 -0
- package/ios/Frameworks/Ecies.xcframework/ios-arm64/Headers/ecies.h +20 -0
- package/ios/Frameworks/Ecies.xcframework/ios-arm64/Headers/module.modulemap +4 -0
- package/ios/Frameworks/Ecies.xcframework/ios-arm64/libecies.a +0 -0
- package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/Headers/ecies.h +20 -0
- package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/Headers/module.modulemap +4 -0
- package/ios/Frameworks/Ecies.xcframework/ios-arm64-simulator/libecies.a +0 -0
- package/ios/HybridNitroMetamask.swift +119 -54
- package/ios/NitroMetamaskTests/CancellationStateMachineTests.swift +150 -0
- package/ios/NitroMetamaskTests/ChainIdParsingTests.swift +117 -0
- package/ios/NitroMetamaskTests/ConfigureStateMachineTests.swift +174 -0
- package/ios/NitroMetamaskTests/ConnectSignJsonTests.swift +168 -0
- package/ios/NitroMetamaskTests/DefaultDappUrlTests.swift +80 -0
- package/ios/NitroMetamaskTests/PersonalSignParamsTests.swift +101 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/CommClient.swift +43 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/CommClientFactory.swift +17 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/CommLayer.swift +36 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/Deeplink.swift +26 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/DeeplinkClient.swift +199 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/DeeplinkManager.swift +83 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/String.swift +48 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/DeeplinkCommLayer/URLOpener.swift +19 -0
- package/ios/metamask-ios-sdk/CommunicationLayer/SocketClient.swift +27 -0
- package/ios/metamask-ios-sdk/Crypto/Crypto.swift +72 -0
- package/ios/metamask-ios-sdk/Crypto/Encoding.swift +15 -0
- package/ios/metamask-ios-sdk/Crypto/KeyExchange.swift +236 -0
- package/ios/metamask-ios-sdk/DeviceInfo/DeviceInfo.swift +11 -0
- package/ios/metamask-ios-sdk/Ethereum/AppMetadata.swift +28 -0
- package/ios/metamask-ios-sdk/Ethereum/ErrorType.swift +62 -0
- package/ios/metamask-ios-sdk/Ethereum/Ethereum.swift +810 -0
- package/ios/metamask-ios-sdk/Ethereum/EthereumMethod.swift +111 -0
- package/ios/metamask-ios-sdk/Ethereum/EthereumRequest.swift +40 -0
- package/ios/metamask-ios-sdk/Ethereum/EthereumWrapper.swift +10 -0
- package/ios/metamask-ios-sdk/Ethereum/RPCRequest.swift +14 -0
- package/ios/metamask-ios-sdk/Ethereum/RequestError.swift +88 -0
- package/ios/metamask-ios-sdk/Ethereum/ResponseMethod.swift +22 -0
- package/ios/metamask-ios-sdk/Ethereum/SubmitRequest.swift +26 -0
- package/ios/metamask-ios-sdk/Ethereum/TimestampGenerator.swift +16 -0
- package/ios/metamask-ios-sdk/Extensions/NSRecursiveLock.swift +14 -0
- package/ios/metamask-ios-sdk/Extensions/Notification.swift +10 -0
- package/ios/metamask-ios-sdk/Logger/Logging.swift +27 -0
- package/ios/metamask-ios-sdk/Models/AddChainParameters.swift +35 -0
- package/ios/metamask-ios-sdk/Models/Event.swift +19 -0
- package/ios/metamask-ios-sdk/Models/Mappable.swift +40 -0
- package/ios/metamask-ios-sdk/Models/NativeCurrency.swift +25 -0
- package/ios/metamask-ios-sdk/Models/OriginatorInfo.swift +26 -0
- package/ios/metamask-ios-sdk/Models/RequestInfo.swift +18 -0
- package/ios/metamask-ios-sdk/Models/SignContract.swift +48 -0
- package/ios/metamask-ios-sdk/Models/Typealiases.swift +9 -0
- package/ios/metamask-ios-sdk/Persistence/SecureStore.swift +134 -0
- package/ios/metamask-ios-sdk/Persistence/SessionConfig.swift +24 -0
- package/ios/metamask-ios-sdk/Persistence/SessionManager.swift +56 -0
- package/ios/metamask-ios-sdk/SDK/Dependencies.swift +35 -0
- package/ios/metamask-ios-sdk/SDK/MetaMaskSDK.swift +215 -0
- package/ios/metamask-ios-sdk/SDK/SDKInfo.swift +37 -0
- package/ios/metamask-ios-sdk/SDK/SDKOptions.swift +16 -0
- package/lib/commonjs/index.js +50 -3
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +49 -3
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/__tests__/parseNitroError.test.d.ts +2 -0
- package/lib/typescript/src/__tests__/parseNitroError.test.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +43 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts +29 -1
- package/lib/typescript/src/specs/nitro-metamask.nitro.d.ts.map +1 -1
- package/package.json +21 -12
- package/react-native.config.js +5 -0
- package/rust/ecies-jni/Cargo.lock +50 -86
- package/rust/ecies-jni/Cargo.toml +1 -1
- package/rust/ecies-jni/src/lib.rs +164 -100
- package/src/__tests__/parseNitroError.test.ts +35 -0
- package/src/index.ts +53 -5
- package/src/specs/nitro-metamask.nitro.ts +29 -1
- package/scripts/verify-16k-page-alignment.py +0 -117
- package/scripts/verify-16k-page-alignment.sh +0 -5
|
@@ -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,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,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,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,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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|