diff --git a/build.gradle b/build.gradle index bc5e50442c..aba2915485 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ buildscript { classpath plugin.kotlin.gradle classpath plugin.square.exhaustive classpath plugin.square.sqlDelight + classpath plugin.kotlin.serialization // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 403b0e30e2..c3056fffb1 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -22,7 +22,6 @@ ext.versions = [ 'okhttp': '4.9.1', 'sqlDelight': '1.5.0', 'toplAndroid': '2.1.2', - 'bip39': '1.0.2', ] /** @@ -100,6 +99,9 @@ ext.deps = [ coroutinesCore: "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}", reflect: "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin}", ], + kotlinx: [ + serialization: "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2", + ], matthewNelson: [ toplService: "io.matthewnelson.topl-android:topl-service:${versions.toplAndroid}", toplServiceBase: "io.matthewnelson.topl-android:topl-service-base:${versions.toplAndroid}", @@ -121,22 +123,25 @@ ext.deps = [ ], viewBindingDelegateNoReflect: "com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.4.7", giphy: [ - sdk: "com.giphy.sdk:ui:2.1.2" + sdk: "com.giphy.sdk:ui:2.1.2" ], jitsi: [ - sdk: "org.jitsi.react:jitsi-meet-sdk:7.0.0" + sdk: "org.jitsi.react:jitsi-meet-sdk:7.0.0" ], lottie: [ - sdk: "com.airbnb.android:lottie:4.0.0" - ], - bip39: [ - sdk: "cash.z.ecc.android:kotlin-bip39:${versions.bip39}" + sdk: "com.airbnb.android:lottie:4.0.0" ], jna: [ sdk: "net.java.dev.jna:jna:5.8.0@aar" ], facebook: [ shimmer: "com.facebook.shimmer:shimmer:0.5.0" + ], + paho: [ + sdk: "org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.1" + ], + msgpack: [ + sdk: "com.ensarsarajcic.kotlinx:serialization-msgpack:0.5.0" ] ] @@ -172,7 +177,7 @@ ext.plugin = [ square: [ exhaustive: "app.cash.exhaustive:exhaustive-gradle:0.2.0", sqlDelight: "com.squareup.sqldelight:gradle-plugin:${versions.sqlDelight}" - ], + ] ] /** @@ -224,5 +229,5 @@ ext.kaptDeps = [ ], square: [ moshiCodegen: "com.squareup.moshi:moshi-kotlin-codegen:${versions.moshi}", - ], + ] ] diff --git a/settings.gradle b/settings.gradle index 3b70d2444c..8d6e547b3d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -131,6 +131,8 @@ include ':sphinx:activity:features:connectivity-helper' include ':sphinx:activity:concepts:concept-view-model-coordinator' include ':sphinx:activity:features:feature-view-model-coordinator' include ':sphinx:activity:features:video-player-controller' +include ':sphinx:activity:concepts:concept-signer-manager' +include ':sphinx:activity:features:signer-manager' // Screens include ':sphinx:screens:add-sats:add-sats' @@ -342,3 +344,7 @@ include ':sphinx:screens-detail:delete-podcast-detail:delete-podcast-detail' include ':sphinx:screens-detail:delete-chat-media:delete-chat-media' include ':sphinx:screens-detail:delete-chat-media-detail:delete-chat-media-detail' include ':sphinx:screens:threads:threads' +include ':sphinx:activity:concepts:concept-signer-manager' +include ':sphinx:activity:features:signer-manager' +include ':sphinx:application:common:menus:menu-bottom-signer' +include ':sphinx:application:common:menus:menu-bottom-phone-signer-method' diff --git a/sphinx/activity/concepts/concept-signer-manager/.gitignore b/sphinx/activity/concepts/concept-signer-manager/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/sphinx/activity/concepts/concept-signer-manager/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sphinx/activity/concepts/concept-signer-manager/build.gradle b/sphinx/activity/concepts/concept-signer-manager/build.gradle new file mode 100644 index 0000000000..8e4fdef491 --- /dev/null +++ b/sphinx/activity/concepts/concept-signer-manager/build.gradle @@ -0,0 +1,4 @@ +plugins { + id 'java-library' + id 'kotlin' +} \ No newline at end of file diff --git a/sphinx/activity/concepts/concept-signer-manager/src/main/java/chat/sphinx/concept_signer_manager/SignerCallback.kt b/sphinx/activity/concepts/concept-signer-manager/src/main/java/chat/sphinx/concept_signer_manager/SignerCallback.kt new file mode 100644 index 0000000000..62fffe046b --- /dev/null +++ b/sphinx/activity/concepts/concept-signer-manager/src/main/java/chat/sphinx/concept_signer_manager/SignerCallback.kt @@ -0,0 +1,24 @@ +package chat.sphinx.concept_signer_manager + +interface SignerHardwareCallback { + fun checkNetwork(callback: (Boolean) -> Unit) + fun signingDeviceNetwork(callback: (String) -> Unit) + fun signingDevicePassword(networkName: String, callback: (String) -> Unit) + fun signingDeviceLightningNodeUrl(callback: (String) -> Unit) + fun signingDeviceCheckBitcoinNetwork(network: (String) -> Unit, linkSigningDevice: (Boolean) -> Unit) + fun failedToSetupSigningDevice(message: String) + fun sendingSeedToHardware() + fun signingDeviceSuccessfullySet() + fun showMnemonicToUser(message: String, callback: (Boolean) -> Unit) +} + +interface SignerPhoneCallback { + fun showMnemonicToUser(message: String, callback: (Boolean) -> Unit) + fun phoneSignerSuccessfullySet() + fun phoneSignerSetupError() +} + +interface CheckAdminCallback { + fun checkAdminFailed() + fun checkAdminSucceeded() +} \ No newline at end of file diff --git a/sphinx/activity/concepts/concept-signer-manager/src/main/java/chat/sphinx/concept_signer_manager/SignerManager.kt b/sphinx/activity/concepts/concept-signer-manager/src/main/java/chat/sphinx/concept_signer_manager/SignerManager.kt new file mode 100644 index 0000000000..38bd4534ed --- /dev/null +++ b/sphinx/activity/concepts/concept-signer-manager/src/main/java/chat/sphinx/concept_signer_manager/SignerManager.kt @@ -0,0 +1,22 @@ +package chat.sphinx.concept_signer_manager + +abstract class SignerManager { + abstract fun setupPhoneSigner(mnemonicWords: String?, signerPhoneCallback: SignerPhoneCallback) + abstract fun setupSignerHardware(signerHardwareCallback: SignerHardwareCallback) + + abstract fun reconnectMQTT(signerPhoneCallback: SignerPhoneCallback) + + abstract fun setWalletDataHandler(walletDataHandlerInstance: Any) + abstract fun setMoshi(moshiInstance: Any) + abstract fun setNetworkQueryCrypter(networkQueryCrypterInstance: Any) + abstract fun setNetworkQueryContact(networkQueryContactInstance: Any) + + abstract fun setSeedFromGlyph(mqtt: String, network: String, relay: String) + + abstract fun isPhoneSignerSettingUp() : Boolean + abstract fun persistMnemonic() + abstract suspend fun getPublicKeyAndRelayUrl(): Pair? + abstract suspend fun checkHasAdmin(checkAdminCallback: CheckAdminCallback) + + abstract fun reset() +} \ No newline at end of file diff --git a/sphinx/activity/features/signer-manager/.gitignore b/sphinx/activity/features/signer-manager/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/sphinx/activity/features/signer-manager/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sphinx/activity/features/signer-manager/build.gradle b/sphinx/activity/features/signer-manager/build.gradle new file mode 100644 index 0000000000..f85b87f0be --- /dev/null +++ b/sphinx/activity/features/signer-manager/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'app.cash.exhaustive' + id 'com.android.library' + id 'kotlin-android' + id 'kotlinx-serialization' +} + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.compileSdk + versionCode VERSION_CODE.toInteger() + versionName VERSION_NAME + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments disableAnalytics: 'true' + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + api project(path: ':kotlin:concepts:concept-coroutines') + + implementation project(path: ':kotlin:encoders:base64') + + // Sphinx + api project(path: ':sphinx:activity:concepts:concept-signer-manager') + + implementation project(path: ':sphinx:screens-detail:common:detail-resources') + implementation project(path: ':sphinx:application:common:resources') + implementation project(path: ':sphinx:application:data:concepts:repositories:concept-repository-lightning') + implementation project(path: ':sphinx:application:data:concepts:concept-wallet') + implementation project(path: ':sphinx:application:common:wrappers:wrapper-message-media') + + implementation project(path: ':sphinx:application:data:concepts:concept-background-login') + implementation project(path: ':sphinx:application:data:concepts:repositories:concept-repository-media') + implementation project(path: ':sphinx:application:data:concepts:repositories:concept-repository-contact') + implementation project(path: ':sphinx:application:network:concepts:queries:concept-network-query-crypter') + implementation project(path: ':sphinx:application:network:concepts:queries:concept-network-query-contact') + implementation project(path: ':sphinx:application:data:concepts:concept-relay') + + implementation deps.jncryptor + + implementation deps.androidx.lifecycle.hilt + implementation deps.jna.sdk + implementation deps.paho.sdk + implementation deps.kotlinx.serialization + implementation deps.msgpack.sdk + implementation deps.square.moshi + + + testImplementation project(path: ':kotlin:test:test-concept-coroutines') +} diff --git a/sphinx/activity/features/signer-manager/consumer-rules.pro b/sphinx/activity/features/signer-manager/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sphinx/activity/features/signer-manager/proguard-rules.pro b/sphinx/activity/features/signer-manager/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/sphinx/activity/features/signer-manager/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sphinx/activity/features/signer-manager/src/main/AndroidManifest.xml b/sphinx/activity/features/signer-manager/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c4eff15dd3 --- /dev/null +++ b/sphinx/activity/features/signer-manager/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/sphinx/activity/features/signer-manager/src/main/java/chat/sphinx/signer_manager/SignerManagerImpl.kt b/sphinx/activity/features/signer-manager/src/main/java/chat/sphinx/signer_manager/SignerManagerImpl.kt new file mode 100644 index 0000000000..90c51d417d --- /dev/null +++ b/sphinx/activity/features/signer-manager/src/main/java/chat/sphinx/signer_manager/SignerManagerImpl.kt @@ -0,0 +1,836 @@ +package chat.sphinx.signer_manager + +import android.content.Context +import android.content.SharedPreferences +import android.util.Base64 +import chat.sphinx.concept_network_query_contact.NetworkQueryContact +import chat.sphinx.concept_network_query_crypter.NetworkQueryCrypter +import chat.sphinx.concept_network_query_crypter.model.SendSeedDto +import chat.sphinx.concept_signer_manager.CheckAdminCallback +import chat.sphinx.concept_signer_manager.SignerHardwareCallback +import chat.sphinx.concept_signer_manager.SignerManager +import chat.sphinx.concept_signer_manager.SignerPhoneCallback +import chat.sphinx.concept_wallet.WalletDataHandler +import chat.sphinx.kotlin_response.LoadResponse +import chat.sphinx.kotlin_response.Response +import chat.sphinx.wrapper_common.SignerTopics +import chat.sphinx.wrapper_lightning.WalletMnemonic +import chat.sphinx.wrapper_lightning.toWalletMnemonic +import chat.sphinx.wrapper_relay.toRelayUrl +import chat.sphinx.wrapper_relay.withProtocol +import com.ensarsarajcic.kotlinx.serialization.msgpack.MsgPack +import com.ensarsarajcic.kotlinx.serialization.msgpack.MsgPackDynamicSerializer +import com.squareup.moshi.Moshi +import io.matthewnelson.concept_coroutines.CoroutineDispatchers +import io.matthewnelson.crypto_common.extensions.toHex +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import org.eclipse.paho.client.mqttv3.* +import org.json.JSONException +import org.json.JSONObject +import uniffi.sphinxrs.Keys +import uniffi.sphinxrs.VlsResponse +import uniffi.sphinxrs.deriveSharedSecret +import uniffi.sphinxrs.encrypt +import uniffi.sphinxrs.makeAuthToken +import uniffi.sphinxrs.mnemonicFromEntropy +import uniffi.sphinxrs.mnemonicToSeed +import uniffi.sphinxrs.nodeKeys +import uniffi.sphinxrs.pubkeyFromSecretKey +import java.security.SecureRandom +import java.util.Date +import kotlin.coroutines.CoroutineContext + +class SignerManagerImpl( + context: Context, + private val dispatchers: CoroutineDispatchers +): SignerManager(), CoroutineScope { + + private lateinit var moshi: Moshi + private var walletDataHandler: WalletDataHandler? = null + private var networkQueryCrypter: NetworkQueryCrypter? = null + private var networkQueryContact: NetworkQueryContact? = null + + private var seedDto = SendSeedDto() + private var walletMnemonic: WalletMnemonic? = null + + private var mqttClient: MqttClient? = null + + override fun setWalletDataHandler(walletDataHandlerInstance: Any) { + (walletDataHandlerInstance as WalletDataHandler).let { + walletDataHandler = it + } + } + + override fun setMoshi(moshiInstance: Any) { + (moshiInstance as Moshi).let { + moshi = it + } + } + + override fun setNetworkQueryCrypter(networkQueryCrypterInstance: Any) { + (networkQueryCrypterInstance as NetworkQueryCrypter).let { + networkQueryCrypter = it + } + } + + override fun setNetworkQueryContact(networkQueryContactInstance: Any) { + (networkQueryContactInstance as NetworkQueryContact).let { + networkQueryContact = it + } + } + + override fun setSeedFromGlyph( + mqtt: String, + network: String, + relay: String + ) { + seedDto.lightningNodeUrl = mqtt + seedDto.network = network + seedDto.relayUrl = relay.withProtocol + } + + private val job = SupervisorJob() + override val coroutineContext: CoroutineContext + get() = job + dispatchers.mainImmediate + + companion object { + const val VLS_ERROR = "Error: VLS Failed: invalid sequence" + + const val SIGNING_DEVICE_SHARED_PREFERENCES = "signer_settings" + + const val SIGNER_CLIENT_ID_KEY = "signer_client_id" + const val SIGNER_LSS_NONCE_KEY = "signer_lss_nonce" + const val SIGNER_MUTATIONS_KEY = "signer_mutations" + const val SIGNER_SEQUENCE_KEY = "signer_sequence" + const val SIGNER_LIGHTNING_NODE_URL_KEY = "signer-lightning-node-url" + const val SIGNER_NETWORK_KEY = "signer-network" + const val SIGNING_DEVICE_SETUP_KEY = "signing-device-setup" + const val PHONE_SIGNER_SETUP_KEY = "phone-signer-setup" + } + + private val appContext: Context = context.applicationContext + + private val signerSharedPreferences: SharedPreferences = + appContext.getSharedPreferences(SIGNING_DEVICE_SHARED_PREFERENCES, Context.MODE_PRIVATE) + + private var setupSignerHardwareJob: Job? = null + private var setupPhoneSignerJob: Job? = null + + + override fun isPhoneSignerSettingUp() : Boolean { + return mqttClient != null + } + + override fun setupSignerHardware(signerHardwareCallback: SignerHardwareCallback) { + if (setupSignerHardwareJob?.isActive == true) return + + resetSeedDto() + resetMQTT() + + setupSignerHardwareJob = launch { + signerHardwareCallback.checkNetwork { + signerHardwareCallback.signingDeviceNetwork { networkName -> + seedDto.ssid = networkName + + signerHardwareCallback.signingDevicePassword(networkName) { networkPass -> + seedDto.pass = networkPass + + signerHardwareCallback.signingDeviceLightningNodeUrl { lightningNodeUrl -> + seedDto.lightningNodeUrl = lightningNodeUrl + + signerHardwareCallback.signingDeviceCheckBitcoinNetwork( + network = { seedDto.network = it }, + linkSigningDevice = { callback -> + if (callback) { + linkSigningDevice(signerHardwareCallback) + } + } + ) + } + } + } + } + } + } + + private fun linkSigningDevice(signerHardwareCallback: SignerHardwareCallback) { + launch { + val secKey = ByteArray(32) + SecureRandom().nextBytes(secKey) + + val sk1 = secKey.toHex() + val pk1 = pubkeyFromSecretKey(sk1) + + var pk2: String? = null + + if (pk1 == null) { + signerHardwareCallback.failedToSetupSigningDevice("error generating secret key") + resetSeedDto() + return@launch + } + + seedDto.pubkey = pk1 + + if ( + seedDto.lightningNodeUrl == null || + seedDto.lightningNodeUrl?.isEmpty() == true + ) { + resetSeedDto() + signerHardwareCallback.failedToSetupSigningDevice("lightning node URL can't be empty") + return@launch + } + + networkQueryCrypter?.getCrypterPubKey()?.collect { loadResponse -> + when (loadResponse) { + is LoadResponse.Loading -> {} + is Response.Error -> { + resetSeedDto() + signerHardwareCallback.failedToSetupSigningDevice("error getting public key from hardware") + } + + is Response.Success -> { + pk2 = loadResponse.value.pubkey + } + } + } + + pk2?.let { nnPk2 -> + val sec1 = deriveSharedSecret(nnPk2, sk1) + val seedAndMnemonic = generateAndPersistMnemonic(null, null) + + seedAndMnemonic.second?.let { mnemonic -> + signerHardwareCallback.showMnemonicToUser(mnemonic.value) { callback -> + if (callback) { + seedAndMnemonic.first?.let { seed -> + encryptAndSendSeed(seed, sec1, signerHardwareCallback) + } + } + } + } + } + } + } + + private fun encryptAndSendSeed( + seed: String, + sec1: String, + signerHardwareCallback: SignerHardwareCallback + ) { + launch { + val nonce = ByteArray(12) + SecureRandom().nextBytes(nonce) + + encrypt(seed, sec1, nonce.toHex()).let { cipher -> + if (cipher.isNotEmpty()) { + seedDto.seed = cipher + + signerHardwareCallback.sendingSeedToHardware() + + networkQueryCrypter?.sendEncryptedSeed(seedDto)?.collect { loadResponse -> + when (loadResponse) { + is LoadResponse.Loading -> {} + is Response.Error -> { + resetSeedDto() + signerHardwareCallback.failedToSetupSigningDevice("error sending seed to hardware") + } + + is Response.Success -> { + setSigningDeviceSetupDone(signerHardwareCallback) + } + } + } + } + } + } + } + + private fun setSigningDeviceSetupDone( + signerHardwareCallback: SignerHardwareCallback + ) { + signerSharedPreferences.edit().putBoolean(SIGNING_DEVICE_SETUP_KEY, true) + .let { editor -> + if (!editor.commit()) { + editor.apply() + } + signerHardwareCallback.signingDeviceSuccessfullySet() + } + } + + override fun reset() { + resetMQTT() + resetSeedDto() + } + + private fun resetSeedDto() { + seedDto.seed = null + seedDto.network = null + seedDto.pass = null + seedDto.ssid = null + } + + private fun resetMQTT() { + if (mqttClient?.isConnected == true) { + mqttClient?.disconnect() + } + mqttClient = null + } + + override fun setupPhoneSigner( + mnemonicWords: String?, + signerPhoneCallback: SignerPhoneCallback + ) { + + if (setupPhoneSignerJob?.isActive == true) return + + setupPhoneSignerJob = launch { + val (seed, _) = generateAndPersistMnemonic(mnemonicWords, signerPhoneCallback) + + if (seedDto.network == null) { + signerPhoneCallback.phoneSignerSetupError() + return@launch + } + + val keys: Keys? = try { + nodeKeys(net = seedDto.network!!, seed = seed!!) + } catch (e: Exception) { + println(e.message) + null + } + + keys?.let { nnKeys -> + val password: String? = try { + makeAuthToken(ts = (Date().time / 1000).toUInt(), secret = nnKeys.secret) + } catch (e: Exception) { + println(e.message) + null + } + + if (password != null) { + connectToMQTTWith(keys, password, signerPhoneCallback) + } else { + signerPhoneCallback.phoneSignerSetupError() + } + } ?: run { + signerPhoneCallback.phoneSignerSetupError() + } + } + } + + private fun connectToMQTTWith(keys: Keys, password: String, signerPhoneCallback: SignerPhoneCallback) { + seedDto.lightningNodeUrl?.let { lightningNodeUrl -> + val serverURI = lightningNodeUrl.toMQTTUrl() + val clientId = retrieveOrGenerateClientId() + + mqttClient = try { + MqttClient(serverURI, clientId, null) + } catch (e: MqttException) { + reset() + signerPhoneCallback.phoneSignerSetupError() + return + } + + val options = MqttConnectOptions().apply { + this.userName = keys.pubkey + this.password = password.toCharArray() + this.keepAliveInterval = 60 + } + + try { + mqttClient?.connect(options) + + if (mqttClient?.isConnected == true) { + + val topics = arrayOf( + "${clientId}/${SignerTopics.VLS}", + "${clientId}/${SignerTopics.INIT_1_MSG}", + "${clientId}/${SignerTopics.INIT_2_MSG}", + "${clientId}/${SignerTopics.LSS_MSG}" + ) + val qos = IntArray(topics.size) { 1 } + + mqttClient?.subscribe(topics, qos) + + val topic = "${clientId}/${SignerTopics.HELLO}" + val message = MqttMessage() + + mqttClient?.publish(topic, message) + + } + + mqttClient?.setCallback(object : MqttCallback { + + override fun connectionLost(cause: Throwable?) { + restartMQTT() + } + + override fun messageArrived(topic: String?, message: MqttMessage?) { + val payload = message?.payload ?: byteArrayOf() + val modifiedTopic = topic?.replace("${clientId}/", "") ?: "" + + if (topic?.contains("vls") == true) { + println("MQTT message arrived $topic") + } + + processMessage(modifiedTopic, payload) + } + + override fun deliveryComplete(token: IMqttDeliveryToken?) {} + }) + + persistServerUrlAndNetwork() + + signerPhoneCallback.phoneSignerSuccessfullySet() + + } catch (e: MqttException) { + e.printStackTrace() + reset() + signerPhoneCallback.phoneSignerSetupError() + } + } ?: run { + reset() + signerPhoneCallback.phoneSignerSetupError() + } + } + + private fun processMessage(topic: String, payload: ByteArray) { + launch { + val (args, state) = argsAndState() + + val ret: VlsResponse? = + try { + uniffi.sphinxrs.run( + topic, + args, + state, + payload, + retrieveSequence(), + ) + } catch (e: Exception) { + if (e.localizedMessage?.contains(VLS_ERROR) == true) { + restartMQTT() + } + null + } + + ret?.let { + storeMutations(it.state) + + val clientId = retrieveOrGenerateClientId() + mqttClient?.publish("${clientId}/${it.topic}", MqttMessage(it.bytes)) + + if (topic.contains(SignerTopics.VLS)) { + storeAndIncrementSequence(ret.sequence) + } + } + } + } + + private suspend fun argsAndState(): Pair { + val args = makeArgs() + val stringArgs = argsToJson(args) ?: "" + val mutationsState: Map = retrieveMutations() + + val state = MsgPack.encodeToByteArray(MsgPackDynamicSerializer, mutationsState) + + return Pair(stringArgs, state) + } + + private fun restartMQTT() { + val editor = signerSharedPreferences.edit() + + editor.putString(SIGNER_MUTATIONS_KEY, "") + editor.putInt(SIGNER_SEQUENCE_KEY, 0) + editor.apply() + + val clientId = retrieveOrGenerateClientId() + val topic = "${clientId}/${SignerTopics.HELLO}" + val message = MqttMessage() + + mqttClient?.publish(topic, message) + } + + + private fun storeMutations(inc: ByteArray) { + launch { + try { + val decoded = MsgPack.decodeFromByteArray(MsgPackDynamicSerializer, inc) + + (decoded as? MutableMap)?.let { + storeMutationsOnSharedPreferences(it) + } + + } catch (e: Exception) { } + } + } + + private suspend fun makeArgs(): Map { + val seedBytes = getStoredMnemonicAndSeed().first?.hexToByArray() + val lssNonce = retrieveOrGenerateLssNonce() + + val defaultPolicy = mapOf( + "msat_per_interval" to 21000000000L, + "interval" to "daily", + "htlc_limit_msat" to 1000000000L + ) + + val args = mapOf( + "seed" to seedBytes, + "network" to (seedDto.network ?: "regtest"), + "policy" to defaultPolicy, + "allowlist" to emptyList(), + "timestamp" to Date().time / 1000L, + "lss_nonce" to lssNonce + ) + + return args + } + + private fun retrieveOrGenerateClientId(): String { + signerSharedPreferences.getString(SIGNER_CLIENT_ID_KEY, null)?.let { + return it + } + + val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') + val newClientId = (1..20).map { allowedChars.random() }.joinToString("") + + val editor = signerSharedPreferences.edit() + editor.putString(SIGNER_CLIENT_ID_KEY, newClientId) + editor.apply() + + return newClientId + } + + @OptIn(ExperimentalUnsignedTypes::class) + private fun retrieveOrGenerateLssNonce(): List { + val storedLssNonceString = signerSharedPreferences.getString(SIGNER_LSS_NONCE_KEY, "") + val storedLssNonce = storedLssNonceString?.split(",")?.mapNotNull { it.toIntOrNull() } + + val result = if (!storedLssNonce.isNullOrEmpty()) { + storedLssNonce + } else { + val editor = signerSharedPreferences.edit() + val randomBytes = generateRandomBytes(32) + val randomBytesString = randomBytes.joinToString(",") { it.toString() } + + editor.putString(SIGNER_LSS_NONCE_KEY, randomBytesString) + editor.apply() + + randomBytes.map { it.toInt() } + } + + return result + } + + private fun storeMutationsOnSharedPreferences(newMutations: MutableMap) { + val existingMutations = retrieveMutations() + existingMutations.putAll(newMutations) + + val encodedString = encodeMapToBase64(existingMutations) + val editor = signerSharedPreferences.edit() + + editor.putString(SIGNER_MUTATIONS_KEY, encodedString) + editor.apply() + } + + private fun retrieveMutations(): MutableMap { + val encodedString = signerSharedPreferences.getString(SIGNER_MUTATIONS_KEY, null) + + val result = encodedString?.let { + decodeBase64ToMap(it) + } ?: mutableMapOf() + + return result + } + + private fun storeAndIncrementSequence(sequence: UShort) { + val newSequence = sequence.toInt().plus(1) + val editor = signerSharedPreferences.edit() + + editor.putInt(SIGNER_SEQUENCE_KEY, newSequence) + editor.apply() + } + + private fun retrieveSequence(): UShort? { + val sequence = signerSharedPreferences.getInt(SIGNER_SEQUENCE_KEY, 0) + + if (sequence == 0) { + return null + } + + return sequence.toUShort() + } + + private fun encodeMapToBase64(map: MutableMap): String { + val encodedMap = mutableMapOf() + + for ((key, value) in map) { + encodedMap[key] = Base64.encodeToString(value, Base64.DEFAULT) + } + + val result = (encodedMap as Map<*, *>?)?.let { JSONObject(it).toString() } ?: "" + + return result + } + + private fun decodeBase64ToMap(encodedString: String): MutableMap { + if (encodedString.isEmpty()) { + return mutableMapOf() + } + + val decodedMap = mutableMapOf() + + try { + val jsonObject = JSONObject(encodedString) + val keys = jsonObject.keys() + + while (keys.hasNext()) { + val key = keys.next() + val encodedValue = jsonObject.getString(key) + val decodedValue = Base64.decode(encodedValue, Base64.DEFAULT) + decodedMap[key] = decodedValue + } + } catch (e: JSONException) { } + + return decodedMap + } + + private fun argsToJson(map: Map): String? { + val adapter = moshi.adapter(Map::class.java) + return adapter.toJson(map) + } + + private fun generateRandomBytes(size: Int): UByteArray { + val random = SecureRandom() + val bytes = ByteArray(size) + random.nextBytes(bytes) + val uByteArray = UByteArray(size) + + for (i in bytes.indices) { + uByteArray[i] = bytes[i].toUByte() + } + + return uByteArray + } + + @OptIn(ExperimentalUnsignedTypes::class) + private suspend fun generateAndPersistMnemonic( + mnemonicWords: String?, + signerPhoneCallback: SignerPhoneCallback? + ): Pair { + var seed: String? = null + + coroutineScope { + launch { + walletMnemonic = run { + try { + mnemonicWords?.toWalletMnemonic()?.let { nnWalletMnemonic -> + nnWalletMnemonic + } ?: run { + val randomBytes = generateRandomBytes(16) + val randomBytesString = randomBytes.joinToString("") { it.toString(16).padStart(2, '0') } + val words = mnemonicFromEntropy(randomBytesString) + + words.toWalletMnemonic()?.let { nnWalletMnemonic -> + signerPhoneCallback?.showMnemonicToUser(words) {} + nnWalletMnemonic + } + } + } catch (e: Exception) { + null + } + } + + walletMnemonic?.value?.let { words -> + try { + seed = mnemonicToSeed(words) + } catch (e: Exception) {} + } + }.join() + } + return Pair(seed, walletMnemonic) + } + + private suspend fun getStoredMnemonicAndSeed(): Pair { + var seed: String? = null + + (walletDataHandler?.retrieveWalletMnemonic() ?: walletMnemonic)?.value?.let { words -> + try { + seed = mnemonicToSeed(words) + } catch (e: Exception) {} + } + + return Pair(seed, walletMnemonic) + } + + override suspend fun getPublicKeyAndRelayUrl(): Pair? { + var keys: Keys? = null + + getStoredMnemonicAndSeed().first?.let { seed -> + seedDto.network?.let { nnNetwork -> + keys = try { + nodeKeys(net = nnNetwork, seed = seed) + } catch (e: Exception) { + null + } + } + } + seedDto.relayUrl?.let { relayUrl -> + keys?.pubkey?.let { publicKey -> + return Pair(publicKey, relayUrl) + } + } + + return null + } + + private var hasAdminRetries = 0 + override suspend fun checkHasAdmin( + checkAdminCallback: CheckAdminCallback + ) { + seedDto.relayUrl?.toRelayUrl()?.let { relayUrl -> + if (hasAdminRetries < 50) { + hasAdminRetries += 1 + + networkQueryContact?.hasAdmin( + relayUrl + )?.collect { loadResponse -> + when (loadResponse) { + is LoadResponse.Loading -> {} + is Response.Error -> { + checkHasAdmin(checkAdminCallback) + } + + is Response.Success -> { + checkAdminCallback.checkAdminSucceeded() + } + } + } + } else { + hasAdminRetries = 0 + checkAdminCallback.checkAdminFailed() + } + } ?: run { + hasAdminRetries = 0 + checkAdminCallback.checkAdminFailed() + } + } + + override fun persistMnemonic() { + walletMnemonic?.let { + launch { + val success = walletDataHandler?.persistWalletMnemonic(it) ?: false + + if (success) { + walletMnemonic = null + setPhoneSignerSetupDone() + } + } + } + } + + private fun setPhoneSignerSetupDone() { + signerSharedPreferences.edit().putBoolean(PHONE_SIGNER_SETUP_KEY, true) + .let { editor -> + if (!editor.commit()) { + editor.apply() + } + } + } + + private fun persistServerUrlAndNetwork() { + val serverUrl = seedDto.lightningNodeUrl + val network = seedDto.network + + signerSharedPreferences.edit().putString(SIGNER_LIGHTNING_NODE_URL_KEY, serverUrl) + .let { editor -> + if (!editor.commit()) { + editor.apply() + } + } + + signerSharedPreferences.edit().putString(SIGNER_NETWORK_KEY, network) + .let { editor -> + if (!editor.commit()) { + editor.apply() + } + } + } + + override fun reconnectMQTT(signerPhoneCallback: SignerPhoneCallback) { + if (!signerSharedPreferences.getBoolean(PHONE_SIGNER_SETUP_KEY, false)) { + return + } + + val mqttUrl = signerSharedPreferences.getString(SIGNER_LIGHTNING_NODE_URL_KEY, null) + val network = signerSharedPreferences.getString(SIGNER_NETWORK_KEY, null) + + if (mqttUrl == null || mqttUrl.isEmpty() || network == null || network.isEmpty()) { + signerPhoneCallback.phoneSignerSetupError() + return + } + + seedDto.lightningNodeUrl = mqttUrl + seedDto.network = network + + launch { + getStoredMnemonicAndSeed().first?.let { nnSeed -> + + val keys: Keys? = try { + nodeKeys(net = seedDto.network!!, seed = nnSeed) + } catch (e: Exception) { + println(e.message) + null + } + + keys?.let { nnKeys -> + val password: String? = try { + makeAuthToken(ts = (Date().time / 1000).toUInt(), secret = nnKeys.secret) + } catch (e: Exception) { + println(e.message) + null + } + + if (password != null) { + connectToMQTTWith(keys, password, signerPhoneCallback) + } else { + signerPhoneCallback.phoneSignerSetupError() + } + } ?: run { + signerPhoneCallback.phoneSignerSetupError() + } + } ?: run { + signerPhoneCallback.phoneSignerSetupError() + } + } + } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun String.hexToByArray(): ByteArray { + val byteIterator = chunkedSequence(2) + .map { it.toInt(16).toByte() } + .iterator() + + return ByteArray(length / 2) { byteIterator.next() } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun String.toMQTTUrl(): String { + if (this.contains("https://")) { + return this.replace("https://", "ssl://") + } + if (this.contains("http://")) { + return this.replace("http://", "tcp://") + } + + return if (this.endsWith(":8883")) { + "ssl://$this" + } else { + "tcp://$this" + } +} diff --git a/sphinx/activity/main/activitymain/build.gradle b/sphinx/activity/main/activitymain/build.gradle index 44f121e70e..275e8b6147 100644 --- a/sphinx/activity/main/activitymain/build.gradle +++ b/sphinx/activity/main/activitymain/build.gradle @@ -37,6 +37,7 @@ dependencies { // Sphinx implementation project(path: ':sphinx:activity:insetter-activity') api project(path: ':sphinx:activity:features:user-colors-helper') + api project(path: ':sphinx:activity:features:signer-manager') api project(path: ':sphinx:activity:features:connectivity-helper') api project(path: ':sphinx:activity:hilt-qualifiers') diff --git a/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/di/ActivityModule.kt b/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/di/ActivityModule.kt index 32f05a496b..27ee7bda79 100644 --- a/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/di/ActivityModule.kt +++ b/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/di/ActivityModule.kt @@ -2,8 +2,10 @@ package chat.sphinx.activitymain.di import android.content.Context import chat.sphinx.concept_connectivity_helper.ConnectivityHelper +import chat.sphinx.concept_signer_manager.SignerManager import chat.sphinx.concept_user_colors_helper.UserColorsHelper import chat.sphinx.connectivity_helper.ConnectivityHelperImpl +import chat.sphinx.signer_manager.SignerManagerImpl import chat.sphinx.user_colors_helper.UserColorsHelperImpl import dagger.Module import dagger.Provides @@ -12,6 +14,7 @@ import dagger.hilt.android.components.ActivityComponent import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.scopes.ActivityScoped import io.matthewnelson.concept_coroutines.CoroutineDispatchers +import javax.inject.Singleton @Module @InstallIn(ActivityComponent::class) @@ -43,4 +46,19 @@ object ActivityModule { connectivityHelperImpl: ConnectivityHelperImpl ): ConnectivityHelper = connectivityHelperImpl + + @Provides + @ActivityScoped + fun provideSignerManagerImpl( + @ApplicationContext appContext: Context, + dispatchers: CoroutineDispatchers, + ): SignerManagerImpl = + SignerManagerImpl(appContext, dispatchers) + + @Provides + fun provideSignerManager( + signerManagerImpl: SignerManagerImpl + ): SignerManager = + signerManagerImpl + } diff --git a/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/navigation/navigators/primary/OnBoardConnectNavigatorImpl.kt b/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/navigation/navigators/primary/OnBoardConnectNavigatorImpl.kt index b2bb6c915d..a043a68b41 100644 --- a/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/navigation/navigators/primary/OnBoardConnectNavigatorImpl.kt +++ b/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/navigation/navigators/primary/OnBoardConnectNavigatorImpl.kt @@ -12,7 +12,7 @@ internal class OnBoardConnectNavigatorImpl @Inject constructor( ): OnBoardConnectNavigator(navigationDriver) { override suspend fun toOnBoardConnectingScreen( - code: String, + code: String?, ) { navigationDriver.submitNavigationRequest( ToOnBoardConnectingScreen( diff --git a/sphinx/application/common/menus/menu-bottom-phone-signer-method/.gitignore b/sphinx/application/common/menus/menu-bottom-phone-signer-method/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-phone-signer-method/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-phone-signer-method/build.gradle b/sphinx/application/common/menus/menu-bottom-phone-signer-method/build.gradle new file mode 100644 index 0000000000..c795ff9a1d --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-phone-signer-method/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'app.cash.exhaustive' + id 'com.android.library' + id 'kotlin-android' +} + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + buildFeatures.viewBinding = true + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.compileSdk + versionCode VERSION_CODE.toInteger() + versionName VERSION_NAME + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments disableAnalytics: 'true' + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + api project(path: ':sphinx:application:common:menus:menu-bottom') + implementation project(path: ':sphinx:application:common:wrappers:wrapper-common') +} diff --git a/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/AndroidManifest.xml b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2003a09268 --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/java/chat/sphinx/menu_bottom_phone_signer_method/PhoneSignerMethodMenu.kt b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/java/chat/sphinx/menu_bottom_phone_signer_method/PhoneSignerMethodMenu.kt new file mode 100644 index 0000000000..c00369e402 --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/java/chat/sphinx/menu_bottom_phone_signer_method/PhoneSignerMethodMenu.kt @@ -0,0 +1,59 @@ +package chat.sphinx.menu_bottom_phone_signer_method + +import androidx.annotation.StringRes +import androidx.lifecycle.LifecycleOwner +import chat.sphinx.menu_bottom.databinding.LayoutMenuBottomBinding +import chat.sphinx.menu_bottom.model.MenuBottomOption +import chat.sphinx.menu_bottom.ui.BottomMenu +import chat.sphinx.menu_bottom.ui.MenuBottomViewState +import io.matthewnelson.android_feature_viewmodel.util.OnStopSupervisor + +class PhoneSignerMethodMenu( + onStopSupervisor: OnStopSupervisor, + private val phoneSignerMethodMenuViewModel: PhoneSignerMethodMenuViewModel, +) : BottomMenu( + phoneSignerMethodMenuViewModel.dispatchers, + onStopSupervisor, + phoneSignerMethodMenuViewModel.phoneSignerMethodMenuHandler.viewStateContainer, +) { + + fun initialize( + @StringRes + headerText: Int, + + binding: LayoutMenuBottomBinding, + lifecycleOwner: LifecycleOwner + ) { + super.newBuilder(binding, lifecycleOwner) + .setHeaderText(headerText) + .setHeaderSubText(R.string.bottom_menu_phone_signer_method_sub_header_text) + .setOptions( + setOf( + MenuBottomOption( + text = R.string.bottom_menu_phone_signer_method_generate, + textColor = R.color.primaryBlueFontColor, + onClick = { + phoneSignerMethodMenuViewModel.generateSeed() + viewStateContainer.updateViewState(MenuBottomViewState.Closed) + } + ), + MenuBottomOption( + text = R.string.bottom_menu_phone_signer_method_import, + textColor = R.color.primaryBlueFontColor, + onClick = { + phoneSignerMethodMenuViewModel.importSeed() + viewStateContainer.updateViewState(MenuBottomViewState.Closed) + } + ) + ) + ) + .build() + } + + override fun newBuilder( + binding: LayoutMenuBottomBinding, + lifecycleOwner: LifecycleOwner + ): Builder { + throw IllegalStateException("Use the BottomSignerMenu.initialize method") + } +} \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/java/chat/sphinx/menu_bottom_phone_signer_method/PhoneSignerMethodMenuViewModel.kt b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/java/chat/sphinx/menu_bottom_phone_signer_method/PhoneSignerMethodMenuViewModel.kt new file mode 100644 index 0000000000..35168dc9c2 --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/java/chat/sphinx/menu_bottom_phone_signer_method/PhoneSignerMethodMenuViewModel.kt @@ -0,0 +1,18 @@ +package chat.sphinx.menu_bottom_phone_signer_method + +import chat.sphinx.menu_bottom.ui.MenuBottomViewState +import io.matthewnelson.concept_coroutines.CoroutineDispatchers +import io.matthewnelson.concept_views.viewstate.ViewStateContainer + +interface PhoneSignerMethodMenuViewModel { + val phoneSignerMethodMenuHandler: PhoneSignerMethodMenuHandler + val dispatchers: CoroutineDispatchers + + fun generateSeed() + fun importSeed() +} +class PhoneSignerMethodMenuHandler { + val viewStateContainer: ViewStateContainer by lazy { + ViewStateContainer(MenuBottomViewState.Closed) + } +} \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values-b+fil/strings.xml b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values-b+fil/strings.xml new file mode 100644 index 0000000000..f492df3db7 --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values-b+fil/strings.xml @@ -0,0 +1,9 @@ + + + Choose Your Seed Method + Would you like to generate a new seed phrase or import an existing one? + + + Generate + Import + \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values-es/strings.xml b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..d2ef708058 --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values-es/strings.xml @@ -0,0 +1,9 @@ + + + Elige un método para el seed + ¿Desea generar una nueva seed o importar una existente? + + + Generar + Importar + \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values-ja/strings.xml b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..ff4b8ec1b4 --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values-ja/strings.xml @@ -0,0 +1,8 @@ + + + Choose Your Seed Method + Would you like to generate a new seed phrase or import an existing one? + + Generate + Import + \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values-zh/strings.xml b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values-zh/strings.xml new file mode 100644 index 0000000000..f492df3db7 --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values-zh/strings.xml @@ -0,0 +1,9 @@ + + + Choose Your Seed Method + Would you like to generate a new seed phrase or import an existing one? + + + Generate + Import + \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values/strings.xml b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values/strings.xml new file mode 100644 index 0000000000..ff4b8ec1b4 --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-phone-signer-method/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + + Choose Your Seed Method + Would you like to generate a new seed phrase or import an existing one? + + Generate + Import + \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-signer/.gitignore b/sphinx/application/common/menus/menu-bottom-signer/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-signer/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-signer/build.gradle b/sphinx/application/common/menus/menu-bottom-signer/build.gradle new file mode 100644 index 0000000000..c795ff9a1d --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-signer/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'app.cash.exhaustive' + id 'com.android.library' + id 'kotlin-android' +} + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + buildFeatures.viewBinding = true + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.compileSdk + versionCode VERSION_CODE.toInteger() + versionName VERSION_NAME + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments disableAnalytics: 'true' + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + api project(path: ':sphinx:application:common:menus:menu-bottom') + implementation project(path: ':sphinx:application:common:wrappers:wrapper-common') +} diff --git a/sphinx/application/common/menus/menu-bottom-signer/src/main/AndroidManifest.xml b/sphinx/application/common/menus/menu-bottom-signer/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..47b0d0fb36 --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-signer/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/sphinx/application/common/menus/menu-bottom-signer/src/main/java/chat/sphinx/menu_bottom_signer/BottomSignerMenu.kt b/sphinx/application/common/menus/menu-bottom-signer/src/main/java/chat/sphinx/menu_bottom_signer/BottomSignerMenu.kt new file mode 100644 index 0000000000..5c57281d88 --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-signer/src/main/java/chat/sphinx/menu_bottom_signer/BottomSignerMenu.kt @@ -0,0 +1,62 @@ +package chat.sphinx.menu_bottom_signer + +import androidx.annotation.StringRes +import androidx.lifecycle.LifecycleOwner +import chat.sphinx.menu_bottom.databinding.LayoutMenuBottomBinding +import chat.sphinx.menu_bottom.model.MenuBottomDismiss +import chat.sphinx.menu_bottom.model.MenuBottomOption +import chat.sphinx.menu_bottom.ui.BottomMenu +import chat.sphinx.menu_bottom.ui.MenuBottomViewState +import chat.sphinx.resources.getString +import chat.sphinx.wrapper_common.lightning.LightningNodePubKey +import chat.sphinx.wrapper_common.lightning.LightningRouteHint +import io.matthewnelson.android_feature_viewmodel.util.OnStopSupervisor + +class BottomSignerMenu( + onStopSupervisor: OnStopSupervisor, + private val signerMenuViewModel: SignerMenuViewModel, +) : BottomMenu( + signerMenuViewModel.dispatchers, + onStopSupervisor, + signerMenuViewModel.signerMenuHandler.viewStateContainer, +) { + + fun initialize( + @StringRes + headerText: Int, + + binding: LayoutMenuBottomBinding, + lifecycleOwner: LifecycleOwner + ) { + super.newBuilder(binding, lifecycleOwner) + .setHeaderText(headerText) + .setOptions( + setOf( + MenuBottomOption( + text = R.string.bottom_menu_signer_option_set_hardware, + textColor = R.color.primaryBlueFontColor, + onClick = { + signerMenuViewModel.setupHardwareSigner() + viewStateContainer.updateViewState(MenuBottomViewState.Closed) + } + ), + MenuBottomOption( + text = R.string.bottom_menu_signer_option_set_phone, + textColor = R.color.primaryBlueFontColor, + onClick = { + signerMenuViewModel.setupPhoneSigner() + viewStateContainer.updateViewState(MenuBottomViewState.Closed) + } + ) + ) + ) + .build() + } + + override fun newBuilder( + binding: LayoutMenuBottomBinding, + lifecycleOwner: LifecycleOwner + ): Builder { + throw IllegalStateException("Use the BottomSignerMenu.initialize method") + } +} \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-signer/src/main/java/chat/sphinx/menu_bottom_signer/SignerMenuViewModel.kt b/sphinx/application/common/menus/menu-bottom-signer/src/main/java/chat/sphinx/menu_bottom_signer/SignerMenuViewModel.kt new file mode 100644 index 0000000000..caef4983fb --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-signer/src/main/java/chat/sphinx/menu_bottom_signer/SignerMenuViewModel.kt @@ -0,0 +1,18 @@ +package chat.sphinx.menu_bottom_signer + +import chat.sphinx.menu_bottom.ui.MenuBottomViewState +import io.matthewnelson.concept_coroutines.CoroutineDispatchers +import io.matthewnelson.concept_views.viewstate.ViewStateContainer + +interface SignerMenuViewModel { + val signerMenuHandler: SignerMenuHandler + val dispatchers: CoroutineDispatchers + + fun setupHardwareSigner() + fun setupPhoneSigner() +} +class SignerMenuHandler { + val viewStateContainer: ViewStateContainer by lazy { + ViewStateContainer(MenuBottomViewState.Closed) + } +} \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values-b+fil/strings.xml b/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values-b+fil/strings.xml new file mode 100644 index 0000000000..743675d74a --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values-b+fil/strings.xml @@ -0,0 +1,7 @@ + + + Signer Options + + Set up hardware device + Set up phone signer + \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values-es/strings.xml b/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..743675d74a --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values-es/strings.xml @@ -0,0 +1,7 @@ + + + Signer Options + + Set up hardware device + Set up phone signer + \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values-ja/strings.xml b/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..743675d74a --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values-ja/strings.xml @@ -0,0 +1,7 @@ + + + Signer Options + + Set up hardware device + Set up phone signer + \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values-zh/strings.xml b/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values-zh/strings.xml new file mode 100644 index 0000000000..743675d74a --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values-zh/strings.xml @@ -0,0 +1,7 @@ + + + Signer Options + + Set up hardware device + Set up phone signer + \ No newline at end of file diff --git a/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values/strings.xml b/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values/strings.xml new file mode 100644 index 0000000000..5cc88c7fab --- /dev/null +++ b/sphinx/application/common/menus/menu-bottom-signer/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + Setup hardware device or phone signer + + Hardware + Phone Signer + \ No newline at end of file diff --git a/sphinx/application/common/resources/build.gradle b/sphinx/application/common/resources/build.gradle index 9a0fe15824..b8ecb14f6d 100644 --- a/sphinx/application/common/resources/build.gradle +++ b/sphinx/application/common/resources/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.library' id 'kotlin-android' + id 'kotlinx-serialization' } android { @@ -38,8 +39,8 @@ dependencies { implementation deps.androidx.annotation implementation deps.androidx.appCompat implementation deps.androidx.constraintLayout - implementation deps.bip39.sdk implementation deps.jna.sdk + implementation deps.kotlinx.serialization implementation deps.lottie.sdk } diff --git a/sphinx/application/common/resources/src/main/assets/words-list-constants.json b/sphinx/application/common/resources/src/main/assets/words-list-constants.json new file mode 100644 index 0000000000..6055940d81 --- /dev/null +++ b/sphinx/application/common/resources/src/main/assets/words-list-constants.json @@ -0,0 +1,10 @@ +{ + "ENGLISH": ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", "always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", "convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", "job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize", "problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property", "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle", "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", "relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", "result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo"], + "JAPANESE": ["あいこくしん", "あいさつ", "あいだ", "あおぞら", "あかちゃん", "あきる", "あけがた", "あける", "あこがれる", "あさい", "あさひ", "あしあと", "あじわう", "あずかる", "あずき", "あそぶ", "あたえる", "あたためる", "あたりまえ", "あたる", "あつい", "あつかう", "あっしゅく", "あつまり", "あつめる", "あてな", "あてはまる", "あひる", "あぶら", "あぶる", "あふれる", "あまい", "あまど", "あまやかす", "あまり", "あみもの", "あめりか", "あやまる", "あゆむ", "あらいぐま", "あらし", "あらすじ", "あらためる", "あらゆる", "あらわす", "ありがとう", "あわせる", "あわてる", "あんい", "あんがい", "あんこ", "あんぜん", "あんてい", "あんない", "あんまり", "いいだす", "いおん", "いがい", "いがく", "いきおい", "いきなり", "いきもの", "いきる", "いくじ", "いくぶん", "いけばな", "いけん", "いこう", "いこく", "いこつ", "いさましい", "いさん", "いしき", "いじゅう", "いじょう", "いじわる", "いずみ", "いずれ", "いせい", "いせえび", "いせかい", "いせき", "いぜん", "いそうろう", "いそがしい", "いだい", "いだく", "いたずら", "いたみ", "いたりあ", "いちおう", "いちじ", "いちど", "いちば", "いちぶ", "いちりゅう", "いつか", "いっしゅん", "いっせい", "いっそう", "いったん", "いっち", "いってい", "いっぽう", "いてざ", "いてん", "いどう", "いとこ", "いない", "いなか", "いねむり", "いのち", "いのる", "いはつ", "いばる", "いはん", "いびき", "いひん", "いふく", "いへん", "いほう", "いみん", "いもうと", "いもたれ", "いもり", "いやがる", "いやす", "いよかん", "いよく", "いらい", "いらすと", "いりぐち", "いりょう", "いれい", "いれもの", "いれる", "いろえんぴつ", "いわい", "いわう", "いわかん", "いわば", "いわゆる", "いんげんまめ", "いんさつ", "いんしょう", "いんよう", "うえき", "うえる", "うおざ", "うがい", "うかぶ", "うかべる", "うきわ", "うくらいな", "うくれれ", "うけたまわる", "うけつけ", "うけとる", "うけもつ", "うける", "うごかす", "うごく", "うこん", "うさぎ", "うしなう", "うしろがみ", "うすい", "うすぎ", "うすぐらい", "うすめる", "うせつ", "うちあわせ", "うちがわ", "うちき", "うちゅう", "うっかり", "うつくしい", "うったえる", "うつる", "うどん", "うなぎ", "うなじ", "うなずく", "うなる", "うねる", "うのう", "うぶげ", "うぶごえ", "うまれる", "うめる", "うもう", "うやまう", "うよく", "うらがえす", "うらぐち", "うらない", "うりあげ", "うりきれ", "うるさい", "うれしい", "うれゆき", "うれる", "うろこ", "うわき", "うわさ", "うんこう", "うんちん", "うんてん", "うんどう", "えいえん", "えいが", "えいきょう", "えいご", "えいせい", "えいぶん", "えいよう", "えいわ", "えおり", "えがお", "えがく", "えきたい", "えくせる", "えしゃく", "えすて", "えつらん", "えのぐ", "えほうまき", "えほん", "えまき", "えもじ", "えもの", "えらい", "えらぶ", "えりあ", "えんえん", "えんかい", "えんぎ", "えんげき", "えんしゅう", "えんぜつ", "えんそく", "えんちょう", "えんとつ", "おいかける", "おいこす", "おいしい", "おいつく", "おうえん", "おうさま", "おうじ", "おうせつ", "おうたい", "おうふく", "おうべい", "おうよう", "おえる", "おおい", "おおう", "おおどおり", "おおや", "おおよそ", "おかえり", "おかず", "おがむ", "おかわり", "おぎなう", "おきる", "おくさま", "おくじょう", "おくりがな", "おくる", "おくれる", "おこす", "おこなう", "おこる", "おさえる", "おさない", "おさめる", "おしいれ", "おしえる", "おじぎ", "おじさん", "おしゃれ", "おそらく", "おそわる", "おたがい", "おたく", "おだやか", "おちつく", "おっと", "おつり", "おでかけ", "おとしもの", "おとなしい", "おどり", "おどろかす", "おばさん", "おまいり", "おめでとう", "おもいで", "おもう", "おもたい", "おもちゃ", "おやつ", "おやゆび", "およぼす", "おらんだ", "おろす", "おんがく", "おんけい", "おんしゃ", "おんせん", "おんだん", "おんちゅう", "おんどけい", "かあつ", "かいが", "がいき", "がいけん", "がいこう", "かいさつ", "かいしゃ", "かいすいよく", "かいぜん", "かいぞうど", "かいつう", "かいてん", "かいとう", "かいふく", "がいへき", "かいほう", "かいよう", "がいらい", "かいわ", "かえる", "かおり", "かかえる", "かがく", "かがし", "かがみ", "かくご", "かくとく", "かざる", "がぞう", "かたい", "かたち", "がちょう", "がっきゅう", "がっこう", "がっさん", "がっしょう", "かなざわし", "かのう", "がはく", "かぶか", "かほう", "かほご", "かまう", "かまぼこ", "かめれおん", "かゆい", "かようび", "からい", "かるい", "かろう", "かわく", "かわら", "がんか", "かんけい", "かんこう", "かんしゃ", "かんそう", "かんたん", "かんち", "がんばる", "きあい", "きあつ", "きいろ", "ぎいん", "きうい", "きうん", "きえる", "きおう", "きおく", "きおち", "きおん", "きかい", "きかく", "きかんしゃ", "ききて", "きくばり", "きくらげ", "きけんせい", "きこう", "きこえる", "きこく", "きさい", "きさく", "きさま", "きさらぎ", "ぎじかがく", "ぎしき", "ぎじたいけん", "ぎじにってい", "ぎじゅつしゃ", "きすう", "きせい", "きせき", "きせつ", "きそう", "きぞく", "きぞん", "きたえる", "きちょう", "きつえん", "ぎっちり", "きつつき", "きつね", "きてい", "きどう", "きどく", "きない", "きなが", "きなこ", "きぬごし", "きねん", "きのう", "きのした", "きはく", "きびしい", "きひん", "きふく", "きぶん", "きぼう", "きほん", "きまる", "きみつ", "きむずかしい", "きめる", "きもだめし", "きもち", "きもの", "きゃく", "きやく", "ぎゅうにく", "きよう", "きょうりゅう", "きらい", "きらく", "きりん", "きれい", "きれつ", "きろく", "ぎろん", "きわめる", "ぎんいろ", "きんかくじ", "きんじょ", "きんようび", "ぐあい", "くいず", "くうかん", "くうき", "くうぐん", "くうこう", "ぐうせい", "くうそう", "ぐうたら", "くうふく", "くうぼ", "くかん", "くきょう", "くげん", "ぐこう", "くさい", "くさき", "くさばな", "くさる", "くしゃみ", "くしょう", "くすのき", "くすりゆび", "くせげ", "くせん", "ぐたいてき", "くださる", "くたびれる", "くちこみ", "くちさき", "くつした", "ぐっすり", "くつろぐ", "くとうてん", "くどく", "くなん", "くねくね", "くのう", "くふう", "くみあわせ", "くみたてる", "くめる", "くやくしょ", "くらす", "くらべる", "くるま", "くれる", "くろう", "くわしい", "ぐんかん", "ぐんしょく", "ぐんたい", "ぐんて", "けあな", "けいかく", "けいけん", "けいこ", "けいさつ", "げいじゅつ", "けいたい", "げいのうじん", "けいれき", "けいろ", "けおとす", "けおりもの", "げきか", "げきげん", "げきだん", "げきちん", "げきとつ", "げきは", "げきやく", "げこう", "げこくじょう", "げざい", "けさき", "げざん", "けしき", "けしごむ", "けしょう", "げすと", "けたば", "けちゃっぷ", "けちらす", "けつあつ", "けつい", "けつえき", "けっこん", "けつじょ", "けっせき", "けってい", "けつまつ", "げつようび", "げつれい", "けつろん", "げどく", "けとばす", "けとる", "けなげ", "けなす", "けなみ", "けぬき", "げねつ", "けねん", "けはい", "げひん", "けぶかい", "げぼく", "けまり", "けみかる", "けむし", "けむり", "けもの", "けらい", "けろけろ", "けわしい", "けんい", "けんえつ", "けんお", "けんか", "げんき", "けんげん", "けんこう", "けんさく", "けんしゅう", "けんすう", "げんそう", "けんちく", "けんてい", "けんとう", "けんない", "けんにん", "げんぶつ", "けんま", "けんみん", "けんめい", "けんらん", "けんり", "こあくま", "こいぬ", "こいびと", "ごうい", "こうえん", "こうおん", "こうかん", "ごうきゅう", "ごうけい", "こうこう", "こうさい", "こうじ", "こうすい", "ごうせい", "こうそく", "こうたい", "こうちゃ", "こうつう", "こうてい", "こうどう", "こうない", "こうはい", "ごうほう", "ごうまん", "こうもく", "こうりつ", "こえる", "こおり", "ごかい", "ごがつ", "ごかん", "こくご", "こくさい", "こくとう", "こくない", "こくはく", "こぐま", "こけい", "こける", "ここのか", "こころ", "こさめ", "こしつ", "こすう", "こせい", "こせき", "こぜん", "こそだて", "こたい", "こたえる", "こたつ", "こちょう", "こっか", "こつこつ", "こつばん", "こつぶ", "こてい", "こてん", "ことがら", "ことし", "ことば", "ことり", "こなごな", "こねこね", "このまま", "このみ", "このよ", "ごはん", "こひつじ", "こふう", "こふん", "こぼれる", "ごまあぶら", "こまかい", "ごますり", "こまつな", "こまる", "こむぎこ", "こもじ", "こもち", "こもの", "こもん", "こやく", "こやま", "こゆう", "こゆび", "こよい", "こよう", "こりる", "これくしょん", "ころっけ", "こわもて", "こわれる", "こんいん", "こんかい", "こんき", "こんしゅう", "こんすい", "こんだて", "こんとん", "こんなん", "こんびに", "こんぽん", "こんまけ", "こんや", "こんれい", "こんわく", "ざいえき", "さいかい", "さいきん", "ざいげん", "ざいこ", "さいしょ", "さいせい", "ざいたく", "ざいちゅう", "さいてき", "ざいりょう", "さうな", "さかいし", "さがす", "さかな", "さかみち", "さがる", "さぎょう", "さくし", "さくひん", "さくら", "さこく", "さこつ", "さずかる", "ざせき", "さたん", "さつえい", "ざつおん", "ざっか", "ざつがく", "さっきょく", "ざっし", "さつじん", "ざっそう", "さつたば", "さつまいも", "さてい", "さといも", "さとう", "さとおや", "さとし", "さとる", "さのう", "さばく", "さびしい", "さべつ", "さほう", "さほど", "さます", "さみしい", "さみだれ", "さむけ", "さめる", "さやえんどう", "さゆう", "さよう", "さよく", "さらだ", "ざるそば", "さわやか", "さわる", "さんいん", "さんか", "さんきゃく", "さんこう", "さんさい", "ざんしょ", "さんすう", "さんせい", "さんそ", "さんち", "さんま", "さんみ", "さんらん", "しあい", "しあげ", "しあさって", "しあわせ", "しいく", "しいん", "しうち", "しえい", "しおけ", "しかい", "しかく", "じかん", "しごと", "しすう", "じだい", "したうけ", "したぎ", "したて", "したみ", "しちょう", "しちりん", "しっかり", "しつじ", "しつもん", "してい", "してき", "してつ", "じてん", "じどう", "しなぎれ", "しなもの", "しなん", "しねま", "しねん", "しのぐ", "しのぶ", "しはい", "しばかり", "しはつ", "しはらい", "しはん", "しひょう", "しふく", "じぶん", "しへい", "しほう", "しほん", "しまう", "しまる", "しみん", "しむける", "じむしょ", "しめい", "しめる", "しもん", "しゃいん", "しゃうん", "しゃおん", "じゃがいも", "しやくしょ", "しゃくほう", "しゃけん", "しゃこ", "しゃざい", "しゃしん", "しゃせん", "しゃそう", "しゃたい", "しゃちょう", "しゃっきん", "じゃま", "しゃりん", "しゃれい", "じゆう", "じゅうしょ", "しゅくはく", "じゅしん", "しゅっせき", "しゅみ", "しゅらば", "じゅんばん", "しょうかい", "しょくたく", "しょっけん", "しょどう", "しょもつ", "しらせる", "しらべる", "しんか", "しんこう", "じんじゃ", "しんせいじ", "しんちく", "しんりん", "すあげ", "すあし", "すあな", "ずあん", "すいえい", "すいか", "すいとう", "ずいぶん", "すいようび", "すうがく", "すうじつ", "すうせん", "すおどり", "すきま", "すくう", "すくない", "すける", "すごい", "すこし", "ずさん", "すずしい", "すすむ", "すすめる", "すっかり", "ずっしり", "ずっと", "すてき", "すてる", "すねる", "すのこ", "すはだ", "すばらしい", "ずひょう", "ずぶぬれ", "すぶり", "すふれ", "すべて", "すべる", "ずほう", "すぼん", "すまい", "すめし", "すもう", "すやき", "すらすら", "するめ", "すれちがう", "すろっと", "すわる", "すんぜん", "すんぽう", "せあぶら", "せいかつ", "せいげん", "せいじ", "せいよう", "せおう", "せかいかん", "せきにん", "せきむ", "せきゆ", "せきらんうん", "せけん", "せこう", "せすじ", "せたい", "せたけ", "せっかく", "せっきゃく", "ぜっく", "せっけん", "せっこつ", "せっさたくま", "せつぞく", "せつだん", "せつでん", "せっぱん", "せつび", "せつぶん", "せつめい", "せつりつ", "せなか", "せのび", "せはば", "せびろ", "せぼね", "せまい", "せまる", "せめる", "せもたれ", "せりふ", "ぜんあく", "せんい", "せんえい", "せんか", "せんきょ", "せんく", "せんげん", "ぜんご", "せんさい", "せんしゅ", "せんすい", "せんせい", "せんぞ", "せんたく", "せんちょう", "せんてい", "せんとう", "せんぬき", "せんねん", "せんぱい", "ぜんぶ", "ぜんぽう", "せんむ", "せんめんじょ", "せんもん", "せんやく", "せんゆう", "せんよう", "ぜんら", "ぜんりゃく", "せんれい", "せんろ", "そあく", "そいとげる", "そいね", "そうがんきょう", "そうき", "そうご", "そうしん", "そうだん", "そうなん", "そうび", "そうめん", "そうり", "そえもの", "そえん", "そがい", "そげき", "そこう", "そこそこ", "そざい", "そしな", "そせい", "そせん", "そそぐ", "そだてる", "そつう", "そつえん", "そっかん", "そつぎょう", "そっけつ", "そっこう", "そっせん", "そっと", "そとがわ", "そとづら", "そなえる", "そなた", "そふぼ", "そぼく", "そぼろ", "そまつ", "そまる", "そむく", "そむりえ", "そめる", "そもそも", "そよかぜ", "そらまめ", "そろう", "そんかい", "そんけい", "そんざい", "そんしつ", "そんぞく", "そんちょう", "ぞんび", "ぞんぶん", "そんみん", "たあい", "たいいん", "たいうん", "たいえき", "たいおう", "だいがく", "たいき", "たいぐう", "たいけん", "たいこ", "たいざい", "だいじょうぶ", "だいすき", "たいせつ", "たいそう", "だいたい", "たいちょう", "たいてい", "だいどころ", "たいない", "たいねつ", "たいのう", "たいはん", "だいひょう", "たいふう", "たいへん", "たいほ", "たいまつばな", "たいみんぐ", "たいむ", "たいめん", "たいやき", "たいよう", "たいら", "たいりょく", "たいる", "たいわん", "たうえ", "たえる", "たおす", "たおる", "たおれる", "たかい", "たかね", "たきび", "たくさん", "たこく", "たこやき", "たさい", "たしざん", "だじゃれ", "たすける", "たずさわる", "たそがれ", "たたかう", "たたく", "ただしい", "たたみ", "たちばな", "だっかい", "だっきゃく", "だっこ", "だっしゅつ", "だったい", "たてる", "たとえる", "たなばた", "たにん", "たぬき", "たのしみ", "たはつ", "たぶん", "たべる", "たぼう", "たまご", "たまる", "だむる", "ためいき", "ためす", "ためる", "たもつ", "たやすい", "たよる", "たらす", "たりきほんがん", "たりょう", "たりる", "たると", "たれる", "たれんと", "たろっと", "たわむれる", "だんあつ", "たんい", "たんおん", "たんか", "たんき", "たんけん", "たんご", "たんさん", "たんじょうび", "だんせい", "たんそく", "たんたい", "だんち", "たんてい", "たんとう", "だんな", "たんにん", "だんねつ", "たんのう", "たんぴん", "だんぼう", "たんまつ", "たんめい", "だんれつ", "だんろ", "だんわ", "ちあい", "ちあん", "ちいき", "ちいさい", "ちえん", "ちかい", "ちから", "ちきゅう", "ちきん", "ちけいず", "ちけん", "ちこく", "ちさい", "ちしき", "ちしりょう", "ちせい", "ちそう", "ちたい", "ちたん", "ちちおや", "ちつじょ", "ちてき", "ちてん", "ちぬき", "ちぬり", "ちのう", "ちひょう", "ちへいせん", "ちほう", "ちまた", "ちみつ", "ちみどろ", "ちめいど", "ちゃんこなべ", "ちゅうい", "ちゆりょく", "ちょうし", "ちょさくけん", "ちらし", "ちらみ", "ちりがみ", "ちりょう", "ちるど", "ちわわ", "ちんたい", "ちんもく", "ついか", "ついたち", "つうか", "つうじょう", "つうはん", "つうわ", "つかう", "つかれる", "つくね", "つくる", "つけね", "つける", "つごう", "つたえる", "つづく", "つつじ", "つつむ", "つとめる", "つながる", "つなみ", "つねづね", "つのる", "つぶす", "つまらない", "つまる", "つみき", "つめたい", "つもり", "つもる", "つよい", "つるぼ", "つるみく", "つわもの", "つわり", "てあし", "てあて", "てあみ", "ていおん", "ていか", "ていき", "ていけい", "ていこく", "ていさつ", "ていし", "ていせい", "ていたい", "ていど", "ていねい", "ていひょう", "ていへん", "ていぼう", "てうち", "ておくれ", "てきとう", "てくび", "でこぼこ", "てさぎょう", "てさげ", "てすり", "てそう", "てちがい", "てちょう", "てつがく", "てつづき", "でっぱ", "てつぼう", "てつや", "でぬかえ", "てぬき", "てぬぐい", "てのひら", "てはい", "てぶくろ", "てふだ", "てほどき", "てほん", "てまえ", "てまきずし", "てみじか", "てみやげ", "てらす", "てれび", "てわけ", "てわたし", "でんあつ", "てんいん", "てんかい", "てんき", "てんぐ", "てんけん", "てんごく", "てんさい", "てんし", "てんすう", "でんち", "てんてき", "てんとう", "てんない", "てんぷら", "てんぼうだい", "てんめつ", "てんらんかい", "でんりょく", "でんわ", "どあい", "といれ", "どうかん", "とうきゅう", "どうぐ", "とうし", "とうむぎ", "とおい", "とおか", "とおく", "とおす", "とおる", "とかい", "とかす", "ときおり", "ときどき", "とくい", "とくしゅう", "とくてん", "とくに", "とくべつ", "とけい", "とける", "とこや", "とさか", "としょかん", "とそう", "とたん", "とちゅう", "とっきゅう", "とっくん", "とつぜん", "とつにゅう", "とどける", "ととのえる", "とない", "となえる", "となり", "とのさま", "とばす", "どぶがわ", "とほう", "とまる", "とめる", "ともだち", "ともる", "どようび", "とらえる", "とんかつ", "どんぶり", "ないかく", "ないこう", "ないしょ", "ないす", "ないせん", "ないそう", "なおす", "ながい", "なくす", "なげる", "なこうど", "なさけ", "なたでここ", "なっとう", "なつやすみ", "ななおし", "なにごと", "なにもの", "なにわ", "なのか", "なふだ", "なまいき", "なまえ", "なまみ", "なみだ", "なめらか", "なめる", "なやむ", "ならう", "ならび", "ならぶ", "なれる", "なわとび", "なわばり", "にあう", "にいがた", "にうけ", "におい", "にかい", "にがて", "にきび", "にくしみ", "にくまん", "にげる", "にさんかたんそ", "にしき", "にせもの", "にちじょう", "にちようび", "にっか", "にっき", "にっけい", "にっこう", "にっさん", "にっしょく", "にっすう", "にっせき", "にってい", "になう", "にほん", "にまめ", "にもつ", "にやり", "にゅういん", "にりんしゃ", "にわとり", "にんい", "にんか", "にんき", "にんげん", "にんしき", "にんずう", "にんそう", "にんたい", "にんち", "にんてい", "にんにく", "にんぷ", "にんまり", "にんむ", "にんめい", "にんよう", "ぬいくぎ", "ぬかす", "ぬぐいとる", "ぬぐう", "ぬくもり", "ぬすむ", "ぬまえび", "ぬめり", "ぬらす", "ぬんちゃく", "ねあげ", "ねいき", "ねいる", "ねいろ", "ねぐせ", "ねくたい", "ねくら", "ねこぜ", "ねこむ", "ねさげ", "ねすごす", "ねそべる", "ねだん", "ねつい", "ねっしん", "ねつぞう", "ねったいぎょ", "ねぶそく", "ねふだ", "ねぼう", "ねほりはほり", "ねまき", "ねまわし", "ねみみ", "ねむい", "ねむたい", "ねもと", "ねらう", "ねわざ", "ねんいり", "ねんおし", "ねんかん", "ねんきん", "ねんぐ", "ねんざ", "ねんし", "ねんちゃく", "ねんど", "ねんぴ", "ねんぶつ", "ねんまつ", "ねんりょう", "ねんれい", "のいず", "のおづま", "のがす", "のきなみ", "のこぎり", "のこす", "のこる", "のせる", "のぞく", "のぞむ", "のたまう", "のちほど", "のっく", "のばす", "のはら", "のべる", "のぼる", "のみもの", "のやま", "のらいぬ", "のらねこ", "のりもの", "のりゆき", "のれん", "のんき", "ばあい", "はあく", "ばあさん", "ばいか", "ばいく", "はいけん", "はいご", "はいしん", "はいすい", "はいせん", "はいそう", "はいち", "ばいばい", "はいれつ", "はえる", "はおる", "はかい", "ばかり", "はかる", "はくしゅ", "はけん", "はこぶ", "はさみ", "はさん", "はしご", "ばしょ", "はしる", "はせる", "ぱそこん", "はそん", "はたん", "はちみつ", "はつおん", "はっかく", "はづき", "はっきり", "はっくつ", "はっけん", "はっこう", "はっさん", "はっしん", "はったつ", "はっちゅう", "はってん", "はっぴょう", "はっぽう", "はなす", "はなび", "はにかむ", "はぶらし", "はみがき", "はむかう", "はめつ", "はやい", "はやし", "はらう", "はろうぃん", "はわい", "はんい", "はんえい", "はんおん", "はんかく", "はんきょう", "ばんぐみ", "はんこ", "はんしゃ", "はんすう", "はんだん", "ぱんち", "ぱんつ", "はんてい", "はんとし", "はんのう", "はんぱ", "はんぶん", "はんぺん", "はんぼうき", "はんめい", "はんらん", "はんろん", "ひいき", "ひうん", "ひえる", "ひかく", "ひかり", "ひかる", "ひかん", "ひくい", "ひけつ", "ひこうき", "ひこく", "ひさい", "ひさしぶり", "ひさん", "びじゅつかん", "ひしょ", "ひそか", "ひそむ", "ひたむき", "ひだり", "ひたる", "ひつぎ", "ひっこし", "ひっし", "ひつじゅひん", "ひっす", "ひつぜん", "ぴったり", "ぴっちり", "ひつよう", "ひてい", "ひとごみ", "ひなまつり", "ひなん", "ひねる", "ひはん", "ひびく", "ひひょう", "ひほう", "ひまわり", "ひまん", "ひみつ", "ひめい", "ひめじし", "ひやけ", "ひやす", "ひよう", "びょうき", "ひらがな", "ひらく", "ひりつ", "ひりょう", "ひるま", "ひるやすみ", "ひれい", "ひろい", "ひろう", "ひろき", "ひろゆき", "ひんかく", "ひんけつ", "ひんこん", "ひんしゅ", "ひんそう", "ぴんち", "ひんぱん", "びんぼう", "ふあん", "ふいうち", "ふうけい", "ふうせん", "ぷうたろう", "ふうとう", "ふうふ", "ふえる", "ふおん", "ふかい", "ふきん", "ふくざつ", "ふくぶくろ", "ふこう", "ふさい", "ふしぎ", "ふじみ", "ふすま", "ふせい", "ふせぐ", "ふそく", "ぶたにく", "ふたん", "ふちょう", "ふつう", "ふつか", "ふっかつ", "ふっき", "ふっこく", "ぶどう", "ふとる", "ふとん", "ふのう", "ふはい", "ふひょう", "ふへん", "ふまん", "ふみん", "ふめつ", "ふめん", "ふよう", "ふりこ", "ふりる", "ふるい", "ふんいき", "ぶんがく", "ぶんぐ", "ふんしつ", "ぶんせき", "ふんそう", "ぶんぽう", "へいあん", "へいおん", "へいがい", "へいき", "へいげん", "へいこう", "へいさ", "へいしゃ", "へいせつ", "へいそ", "へいたく", "へいてん", "へいねつ", "へいわ", "へきが", "へこむ", "べにいろ", "べにしょうが", "へらす", "へんかん", "べんきょう", "べんごし", "へんさい", "へんたい", "べんり", "ほあん", "ほいく", "ぼうぎょ", "ほうこく", "ほうそう", "ほうほう", "ほうもん", "ほうりつ", "ほえる", "ほおん", "ほかん", "ほきょう", "ぼきん", "ほくろ", "ほけつ", "ほけん", "ほこう", "ほこる", "ほしい", "ほしつ", "ほしゅ", "ほしょう", "ほせい", "ほそい", "ほそく", "ほたて", "ほたる", "ぽちぶくろ", "ほっきょく", "ほっさ", "ほったん", "ほとんど", "ほめる", "ほんい", "ほんき", "ほんけ", "ほんしつ", "ほんやく", "まいにち", "まかい", "まかせる", "まがる", "まける", "まこと", "まさつ", "まじめ", "ますく", "まぜる", "まつり", "まとめ", "まなぶ", "まぬけ", "まねく", "まほう", "まもる", "まゆげ", "まよう", "まろやか", "まわす", "まわり", "まわる", "まんが", "まんきつ", "まんぞく", "まんなか", "みいら", "みうち", "みえる", "みがく", "みかた", "みかん", "みけん", "みこん", "みじかい", "みすい", "みすえる", "みせる", "みっか", "みつかる", "みつける", "みてい", "みとめる", "みなと", "みなみかさい", "みねらる", "みのう", "みのがす", "みほん", "みもと", "みやげ", "みらい", "みりょく", "みわく", "みんか", "みんぞく", "むいか", "むえき", "むえん", "むかい", "むかう", "むかえ", "むかし", "むぎちゃ", "むける", "むげん", "むさぼる", "むしあつい", "むしば", "むじゅん", "むしろ", "むすう", "むすこ", "むすぶ", "むすめ", "むせる", "むせん", "むちゅう", "むなしい", "むのう", "むやみ", "むよう", "むらさき", "むりょう", "むろん", "めいあん", "めいうん", "めいえん", "めいかく", "めいきょく", "めいさい", "めいし", "めいそう", "めいぶつ", "めいれい", "めいわく", "めぐまれる", "めざす", "めした", "めずらしい", "めだつ", "めまい", "めやす", "めんきょ", "めんせき", "めんどう", "もうしあげる", "もうどうけん", "もえる", "もくし", "もくてき", "もくようび", "もちろん", "もどる", "もらう", "もんく", "もんだい", "やおや", "やける", "やさい", "やさしい", "やすい", "やすたろう", "やすみ", "やせる", "やそう", "やたい", "やちん", "やっと", "やっぱり", "やぶる", "やめる", "ややこしい", "やよい", "やわらかい", "ゆうき", "ゆうびんきょく", "ゆうべ", "ゆうめい", "ゆけつ", "ゆしゅつ", "ゆせん", "ゆそう", "ゆたか", "ゆちゃく", "ゆでる", "ゆにゅう", "ゆびわ", "ゆらい", "ゆれる", "ようい", "ようか", "ようきゅう", "ようじ", "ようす", "ようちえん", "よかぜ", "よかん", "よきん", "よくせい", "よくぼう", "よけい", "よごれる", "よさん", "よしゅう", "よそう", "よそく", "よっか", "よてい", "よどがわく", "よねつ", "よやく", "よゆう", "よろこぶ", "よろしい", "らいう", "らくがき", "らくご", "らくさつ", "らくだ", "らしんばん", "らせん", "らぞく", "らたい", "らっか", "られつ", "りえき", "りかい", "りきさく", "りきせつ", "りくぐん", "りくつ", "りけん", "りこう", "りせい", "りそう", "りそく", "りてん", "りねん", "りゆう", "りゅうがく", "りよう", "りょうり", "りょかん", "りょくちゃ", "りょこう", "りりく", "りれき", "りろん", "りんご", "るいけい", "るいさい", "るいじ", "るいせき", "るすばん", "るりがわら", "れいかん", "れいぎ", "れいせい", "れいぞうこ", "れいとう", "れいぼう", "れきし", "れきだい", "れんあい", "れんけい", "れんこん", "れんさい", "れんしゅう", "れんぞく", "れんらく", "ろうか", "ろうご", "ろうじん", "ろうそく", "ろくが", "ろこつ", "ろじうら", "ろしゅつ", "ろせん", "ろてん", "ろめん", "ろれつ", "ろんぎ", "ろんぱ", "ろんぶん", "ろんり", "わかす", "わかめ", "わかやま", "わかれる", "わしつ", "わじまし", "わすれもの", "わらう", "われる"], + "KOREAN": ["가격", "가끔", "가난", "가능", "가득", "가르침", "가뭄", "가방", "가상", "가슴", "가운데", "가을", "가이드", "가입", "가장", "가정", "가족", "가죽", "각오", "각자", "간격", "간부", "간섭", "간장", "간접", "간판", "갈등", "갈비", "갈색", "갈증", "감각", "감기", "감소", "감수성", "감자", "감정", "갑자기", "강남", "강당", "강도", "강력히", "강변", "강북", "강사", "강수량", "강아지", "강원도", "강의", "강제", "강조", "같이", "개구리", "개나리", "개방", "개별", "개선", "개성", "개인", "객관적", "거실", "거액", "거울", "거짓", "거품", "걱정", "건강", "건물", "건설", "건조", "건축", "걸음", "검사", "검토", "게시판", "게임", "겨울", "견해", "결과", "결국", "결론", "결석", "결승", "결심", "결정", "결혼", "경계", "경고", "경기", "경력", "경복궁", "경비", "경상도", "경영", "경우", "경쟁", "경제", "경주", "경찰", "경치", "경향", "경험", "계곡", "계단", "계란", "계산", "계속", "계약", "계절", "계층", "계획", "고객", "고구려", "고궁", "고급", "고등학생", "고무신", "고민", "고양이", "고장", "고전", "고집", "고춧가루", "고통", "고향", "곡식", "골목", "골짜기", "골프", "공간", "공개", "공격", "공군", "공급", "공기", "공동", "공무원", "공부", "공사", "공식", "공업", "공연", "공원", "공장", "공짜", "공책", "공통", "공포", "공항", "공휴일", "과목", "과일", "과장", "과정", "과학", "관객", "관계", "관광", "관념", "관람", "관련", "관리", "관습", "관심", "관점", "관찰", "광경", "광고", "광장", "광주", "괴로움", "굉장히", "교과서", "교문", "교복", "교실", "교양", "교육", "교장", "교직", "교통", "교환", "교훈", "구경", "구름", "구멍", "구별", "구분", "구석", "구성", "구속", "구역", "구입", "구청", "구체적", "국가", "국기", "국내", "국립", "국물", "국민", "국수", "국어", "국왕", "국적", "국제", "국회", "군대", "군사", "군인", "궁극적", "권리", "권위", "권투", "귀국", "귀신", "규정", "규칙", "균형", "그날", "그냥", "그늘", "그러나", "그룹", "그릇", "그림", "그제서야", "그토록", "극복", "극히", "근거", "근교", "근래", "근로", "근무", "근본", "근원", "근육", "근처", "글씨", "글자", "금강산", "금고", "금년", "금메달", "금액", "금연", "금요일", "금지", "긍정적", "기간", "기관", "기념", "기능", "기독교", "기둥", "기록", "기름", "기법", "기본", "기분", "기쁨", "기숙사", "기술", "기억", "기업", "기온", "기운", "기원", "기적", "기준", "기침", "기혼", "기획", "긴급", "긴장", "길이", "김밥", "김치", "김포공항", "깍두기", "깜빡", "깨달음", "깨소금", "껍질", "꼭대기", "꽃잎", "나들이", "나란히", "나머지", "나물", "나침반", "나흘", "낙엽", "난방", "날개", "날씨", "날짜", "남녀", "남대문", "남매", "남산", "남자", "남편", "남학생", "낭비", "낱말", "내년", "내용", "내일", "냄비", "냄새", "냇물", "냉동", "냉면", "냉방", "냉장고", "넥타이", "넷째", "노동", "노란색", "노력", "노인", "녹음", "녹차", "녹화", "논리", "논문", "논쟁", "놀이", "농구", "농담", "농민", "농부", "농업", "농장", "농촌", "높이", "눈동자", "눈물", "눈썹", "뉴욕", "느낌", "늑대", "능동적", "능력", "다방", "다양성", "다음", "다이어트", "다행", "단계", "단골", "단독", "단맛", "단순", "단어", "단위", "단점", "단체", "단추", "단편", "단풍", "달걀", "달러", "달력", "달리", "닭고기", "담당", "담배", "담요", "담임", "답변", "답장", "당근", "당분간", "당연히", "당장", "대규모", "대낮", "대단히", "대답", "대도시", "대략", "대량", "대륙", "대문", "대부분", "대신", "대응", "대장", "대전", "대접", "대중", "대책", "대출", "대충", "대통령", "대학", "대한민국", "대합실", "대형", "덩어리", "데이트", "도대체", "도덕", "도둑", "도망", "도서관", "도심", "도움", "도입", "도자기", "도저히", "도전", "도중", "도착", "독감", "독립", "독서", "독일", "독창적", "동화책", "뒷모습", "뒷산", "딸아이", "마누라", "마늘", "마당", "마라톤", "마련", "마무리", "마사지", "마약", "마요네즈", "마을", "마음", "마이크", "마중", "마지막", "마찬가지", "마찰", "마흔", "막걸리", "막내", "막상", "만남", "만두", "만세", "만약", "만일", "만점", "만족", "만화", "많이", "말기", "말씀", "말투", "맘대로", "망원경", "매년", "매달", "매력", "매번", "매스컴", "매일", "매장", "맥주", "먹이", "먼저", "먼지", "멀리", "메일", "며느리", "며칠", "면담", "멸치", "명단", "명령", "명예", "명의", "명절", "명칭", "명함", "모금", "모니터", "모델", "모든", "모범", "모습", "모양", "모임", "모조리", "모집", "모퉁이", "목걸이", "목록", "목사", "목소리", "목숨", "목적", "목표", "몰래", "몸매", "몸무게", "몸살", "몸속", "몸짓", "몸통", "몹시", "무관심", "무궁화", "무더위", "무덤", "무릎", "무슨", "무엇", "무역", "무용", "무조건", "무지개", "무척", "문구", "문득", "문법", "문서", "문제", "문학", "문화", "물가", "물건", "물결", "물고기", "물론", "물리학", "물음", "물질", "물체", "미국", "미디어", "미사일", "미술", "미역", "미용실", "미움", "미인", "미팅", "미혼", "민간", "민족", "민주", "믿음", "밀가루", "밀리미터", "밑바닥", "바가지", "바구니", "바나나", "바늘", "바닥", "바닷가", "바람", "바이러스", "바탕", "박물관", "박사", "박수", "반대", "반드시", "반말", "반발", "반성", "반응", "반장", "반죽", "반지", "반찬", "받침", "발가락", "발걸음", "발견", "발달", "발레", "발목", "발바닥", "발생", "발음", "발자국", "발전", "발톱", "발표", "밤하늘", "밥그릇", "밥맛", "밥상", "밥솥", "방금", "방면", "방문", "방바닥", "방법", "방송", "방식", "방안", "방울", "방지", "방학", "방해", "방향", "배경", "배꼽", "배달", "배드민턴", "백두산", "백색", "백성", "백인", "백제", "백화점", "버릇", "버섯", "버튼", "번개", "번역", "번지", "번호", "벌금", "벌레", "벌써", "범위", "범인", "범죄", "법률", "법원", "법적", "법칙", "베이징", "벨트", "변경", "변동", "변명", "변신", "변호사", "변화", "별도", "별명", "별일", "병실", "병아리", "병원", "보관", "보너스", "보라색", "보람", "보름", "보상", "보안", "보자기", "보장", "보전", "보존", "보통", "보편적", "보험", "복도", "복사", "복숭아", "복습", "볶음", "본격적", "본래", "본부", "본사", "본성", "본인", "본질", "볼펜", "봉사", "봉지", "봉투", "부근", "부끄러움", "부담", "부동산", "부문", "부분", "부산", "부상", "부엌", "부인", "부작용", "부장", "부정", "부족", "부지런히", "부친", "부탁", "부품", "부회장", "북부", "북한", "분노", "분량", "분리", "분명", "분석", "분야", "분위기", "분필", "분홍색", "불고기", "불과", "불교", "불꽃", "불만", "불법", "불빛", "불안", "불이익", "불행", "브랜드", "비극", "비난", "비닐", "비둘기", "비디오", "비로소", "비만", "비명", "비밀", "비바람", "비빔밥", "비상", "비용", "비율", "비중", "비타민", "비판", "빌딩", "빗물", "빗방울", "빗줄기", "빛깔", "빨간색", "빨래", "빨리", "사건", "사계절", "사나이", "사냥", "사람", "사랑", "사립", "사모님", "사물", "사방", "사상", "사생활", "사설", "사슴", "사실", "사업", "사용", "사월", "사장", "사전", "사진", "사촌", "사춘기", "사탕", "사투리", "사흘", "산길", "산부인과", "산업", "산책", "살림", "살인", "살짝", "삼계탕", "삼국", "삼십", "삼월", "삼촌", "상관", "상금", "상대", "상류", "상반기", "상상", "상식", "상업", "상인", "상자", "상점", "상처", "상추", "상태", "상표", "상품", "상황", "새벽", "색깔", "색연필", "생각", "생명", "생물", "생방송", "생산", "생선", "생신", "생일", "생활", "서랍", "서른", "서명", "서민", "서비스", "서양", "서울", "서적", "서점", "서쪽", "서클", "석사", "석유", "선거", "선물", "선배", "선생", "선수", "선원", "선장", "선전", "선택", "선풍기", "설거지", "설날", "설렁탕", "설명", "설문", "설사", "설악산", "설치", "설탕", "섭씨", "성공", "성당", "성명", "성별", "성인", "성장", "성적", "성질", "성함", "세금", "세미나", "세상", "세월", "세종대왕", "세탁", "센터", "센티미터", "셋째", "소규모", "소극적", "소금", "소나기", "소년", "소득", "소망", "소문", "소설", "소속", "소아과", "소용", "소원", "소음", "소중히", "소지품", "소질", "소풍", "소형", "속담", "속도", "속옷", "손가락", "손길", "손녀", "손님", "손등", "손목", "손뼉", "손실", "손질", "손톱", "손해", "솔직히", "솜씨", "송아지", "송이", "송편", "쇠고기", "쇼핑", "수건", "수년", "수단", "수돗물", "수동적", "수면", "수명", "수박", "수상", "수석", "수술", "수시로", "수업", "수염", "수영", "수입", "수준", "수집", "수출", "수컷", "수필", "수학", "수험생", "수화기", "숙녀", "숙소", "숙제", "순간", "순서", "순수", "순식간", "순위", "숟가락", "술병", "술집", "숫자", "스님", "스물", "스스로", "스승", "스웨터", "스위치", "스케이트", "스튜디오", "스트레스", "스포츠", "슬쩍", "슬픔", "습관", "습기", "승객", "승리", "승부", "승용차", "승진", "시각", "시간", "시골", "시금치", "시나리오", "시댁", "시리즈", "시멘트", "시민", "시부모", "시선", "시설", "시스템", "시아버지", "시어머니", "시월", "시인", "시일", "시작", "시장", "시절", "시점", "시중", "시즌", "시집", "시청", "시합", "시험", "식구", "식기", "식당", "식량", "식료품", "식물", "식빵", "식사", "식생활", "식초", "식탁", "식품", "신고", "신규", "신념", "신문", "신발", "신비", "신사", "신세", "신용", "신제품", "신청", "신체", "신화", "실감", "실내", "실력", "실례", "실망", "실수", "실습", "실시", "실장", "실정", "실질적", "실천", "실체", "실컷", "실태", "실패", "실험", "실현", "심리", "심부름", "심사", "심장", "심정", "심판", "쌍둥이", "씨름", "씨앗", "아가씨", "아나운서", "아드님", "아들", "아쉬움", "아스팔트", "아시아", "아울러", "아저씨", "아줌마", "아직", "아침", "아파트", "아프리카", "아픔", "아홉", "아흔", "악기", "악몽", "악수", "안개", "안경", "안과", "안내", "안녕", "안동", "안방", "안부", "안주", "알루미늄", "알코올", "암시", "암컷", "압력", "앞날", "앞문", "애인", "애정", "액수", "앨범", "야간", "야단", "야옹", "약간", "약국", "약속", "약수", "약점", "약품", "약혼녀", "양념", "양력", "양말", "양배추", "양주", "양파", "어둠", "어려움", "어른", "어젯밤", "어쨌든", "어쩌다가", "어쩐지", "언니", "언덕", "언론", "언어", "얼굴", "얼른", "얼음", "얼핏", "엄마", "업무", "업종", "업체", "엉덩이", "엉망", "엉터리", "엊그제", "에너지", "에어컨", "엔진", "여건", "여고생", "여관", "여군", "여권", "여대생", "여덟", "여동생", "여든", "여론", "여름", "여섯", "여성", "여왕", "여인", "여전히", "여직원", "여학생", "여행", "역사", "역시", "역할", "연결", "연구", "연극", "연기", "연락", "연설", "연세", "연속", "연습", "연애", "연예인", "연인", "연장", "연주", "연출", "연필", "연합", "연휴", "열기", "열매", "열쇠", "열심히", "열정", "열차", "열흘", "염려", "엽서", "영국", "영남", "영상", "영양", "영역", "영웅", "영원히", "영하", "영향", "영혼", "영화", "옆구리", "옆방", "옆집", "예감", "예금", "예방", "예산", "예상", "예선", "예술", "예습", "예식장", "예약", "예전", "예절", "예정", "예컨대", "옛날", "오늘", "오락", "오랫동안", "오렌지", "오로지", "오른발", "오븐", "오십", "오염", "오월", "오전", "오직", "오징어", "오페라", "오피스텔", "오히려", "옥상", "옥수수", "온갖", "온라인", "온몸", "온종일", "온통", "올가을", "올림픽", "올해", "옷차림", "와이셔츠", "와인", "완성", "완전", "왕비", "왕자", "왜냐하면", "왠지", "외갓집", "외국", "외로움", "외삼촌", "외출", "외침", "외할머니", "왼발", "왼손", "왼쪽", "요금", "요일", "요즘", "요청", "용기", "용서", "용어", "우산", "우선", "우승", "우연히", "우정", "우체국", "우편", "운동", "운명", "운반", "운전", "운행", "울산", "울음", "움직임", "웃어른", "웃음", "워낙", "원고", "원래", "원서", "원숭이", "원인", "원장", "원피스", "월급", "월드컵", "월세", "월요일", "웨이터", "위반", "위법", "위성", "위원", "위험", "위협", "윗사람", "유난히", "유럽", "유명", "유물", "유산", "유적", "유치원", "유학", "유행", "유형", "육군", "육상", "육십", "육체", "은행", "음력", "음료", "음반", "음성", "음식", "음악", "음주", "의견", "의논", "의문", "의복", "의식", "의심", "의외로", "의욕", "의원", "의학", "이것", "이곳", "이념", "이놈", "이달", "이대로", "이동", "이렇게", "이력서", "이론적", "이름", "이민", "이발소", "이별", "이불", "이빨", "이상", "이성", "이슬", "이야기", "이용", "이웃", "이월", "이윽고", "이익", "이전", "이중", "이튿날", "이틀", "이혼", "인간", "인격", "인공", "인구", "인근", "인기", "인도", "인류", "인물", "인생", "인쇄", "인연", "인원", "인재", "인종", "인천", "인체", "인터넷", "인하", "인형", "일곱", "일기", "일단", "일대", "일등", "일반", "일본", "일부", "일상", "일생", "일손", "일요일", "일월", "일정", "일종", "일주일", "일찍", "일체", "일치", "일행", "일회용", "임금", "임무", "입대", "입력", "입맛", "입사", "입술", "입시", "입원", "입장", "입학", "자가용", "자격", "자극", "자동", "자랑", "자부심", "자식", "자신", "자연", "자원", "자율", "자전거", "자정", "자존심", "자판", "작가", "작년", "작성", "작업", "작용", "작은딸", "작품", "잔디", "잔뜩", "잔치", "잘못", "잠깐", "잠수함", "잠시", "잠옷", "잠자리", "잡지", "장관", "장군", "장기간", "장래", "장례", "장르", "장마", "장면", "장모", "장미", "장비", "장사", "장소", "장식", "장애인", "장인", "장점", "장차", "장학금", "재능", "재빨리", "재산", "재생", "재작년", "재정", "재채기", "재판", "재학", "재활용", "저것", "저고리", "저곳", "저녁", "저런", "저렇게", "저번", "저울", "저절로", "저축", "적극", "적당히", "적성", "적용", "적응", "전개", "전공", "전기", "전달", "전라도", "전망", "전문", "전반", "전부", "전세", "전시", "전용", "전자", "전쟁", "전주", "전철", "전체", "전통", "전혀", "전후", "절대", "절망", "절반", "절약", "절차", "점검", "점수", "점심", "점원", "점점", "점차", "접근", "접시", "접촉", "젓가락", "정거장", "정도", "정류장", "정리", "정말", "정면", "정문", "정반대", "정보", "정부", "정비", "정상", "정성", "정오", "정원", "정장", "정지", "정치", "정확히", "제공", "제과점", "제대로", "제목", "제발", "제법", "제삿날", "제안", "제일", "제작", "제주도", "제출", "제품", "제한", "조각", "조건", "조금", "조깅", "조명", "조미료", "조상", "조선", "조용히", "조절", "조정", "조직", "존댓말", "존재", "졸업", "졸음", "종교", "종로", "종류", "종소리", "종업원", "종종", "종합", "좌석", "죄인", "주관적", "주름", "주말", "주머니", "주먹", "주문", "주민", "주방", "주변", "주식", "주인", "주일", "주장", "주전자", "주택", "준비", "줄거리", "줄기", "줄무늬", "중간", "중계방송", "중국", "중년", "중단", "중독", "중반", "중부", "중세", "중소기업", "중순", "중앙", "중요", "중학교", "즉석", "즉시", "즐거움", "증가", "증거", "증권", "증상", "증세", "지각", "지갑", "지경", "지극히", "지금", "지급", "지능", "지름길", "지리산", "지방", "지붕", "지식", "지역", "지우개", "지원", "지적", "지점", "지진", "지출", "직선", "직업", "직원", "직장", "진급", "진동", "진로", "진료", "진리", "진짜", "진찰", "진출", "진통", "진행", "질문", "질병", "질서", "짐작", "집단", "집안", "집중", "짜증", "찌꺼기", "차남", "차라리", "차량", "차림", "차별", "차선", "차츰", "착각", "찬물", "찬성", "참가", "참기름", "참새", "참석", "참여", "참외", "참조", "찻잔", "창가", "창고", "창구", "창문", "창밖", "창작", "창조", "채널", "채점", "책가방", "책방", "책상", "책임", "챔피언", "처벌", "처음", "천국", "천둥", "천장", "천재", "천천히", "철도", "철저히", "철학", "첫날", "첫째", "청년", "청바지", "청소", "청춘", "체계", "체력", "체온", "체육", "체중", "체험", "초등학생", "초반", "초밥", "초상화", "초순", "초여름", "초원", "초저녁", "초점", "초청", "초콜릿", "촛불", "총각", "총리", "총장", "촬영", "최근", "최상", "최선", "최신", "최악", "최종", "추석", "추억", "추진", "추천", "추측", "축구", "축소", "축제", "축하", "출근", "출발", "출산", "출신", "출연", "출입", "출장", "출판", "충격", "충고", "충돌", "충분히", "충청도", "취업", "취직", "취향", "치약", "친구", "친척", "칠십", "칠월", "칠판", "침대", "침묵", "침실", "칫솔", "칭찬", "카메라", "카운터", "칼국수", "캐릭터", "캠퍼스", "캠페인", "커튼", "컨디션", "컬러", "컴퓨터", "코끼리", "코미디", "콘서트", "콜라", "콤플렉스", "콩나물", "쾌감", "쿠데타", "크림", "큰길", "큰딸", "큰소리", "큰아들", "큰어머니", "큰일", "큰절", "클래식", "클럽", "킬로", "타입", "타자기", "탁구", "탁자", "탄생", "태권도", "태양", "태풍", "택시", "탤런트", "터널", "터미널", "테니스", "테스트", "테이블", "텔레비전", "토론", "토마토", "토요일", "통계", "통과", "통로", "통신", "통역", "통일", "통장", "통제", "통증", "통합", "통화", "퇴근", "퇴원", "퇴직금", "튀김", "트럭", "특급", "특별", "특성", "특수", "특징", "특히", "튼튼히", "티셔츠", "파란색", "파일", "파출소", "판결", "판단", "판매", "판사", "팔십", "팔월", "팝송", "패션", "팩스", "팩시밀리", "팬티", "퍼센트", "페인트", "편견", "편의", "편지", "편히", "평가", "평균", "평생", "평소", "평양", "평일", "평화", "포스터", "포인트", "포장", "포함", "표면", "표정", "표준", "표현", "품목", "품질", "풍경", "풍속", "풍습", "프랑스", "프린터", "플라스틱", "피곤", "피망", "피아노", "필름", "필수", "필요", "필자", "필통", "핑계", "하느님", "하늘", "하드웨어", "하룻밤", "하반기", "하숙집", "하순", "하여튼", "하지만", "하천", "하품", "하필", "학과", "학교", "학급", "학기", "학년", "학력", "학번", "학부모", "학비", "학생", "학술", "학습", "학용품", "학원", "학위", "학자", "학점", "한계", "한글", "한꺼번에", "한낮", "한눈", "한동안", "한때", "한라산", "한마디", "한문", "한번", "한복", "한식", "한여름", "한쪽", "할머니", "할아버지", "할인", "함께", "함부로", "합격", "합리적", "항공", "항구", "항상", "항의", "해결", "해군", "해답", "해당", "해물", "해석", "해설", "해수욕장", "해안", "핵심", "핸드백", "햄버거", "햇볕", "햇살", "행동", "행복", "행사", "행운", "행위", "향기", "향상", "향수", "허락", "허용", "헬기", "현관", "현금", "현대", "현상", "현실", "현장", "현재", "현지", "혈액", "협력", "형부", "형사", "형수", "형식", "형제", "형태", "형편", "혜택", "호기심", "호남", "호랑이", "호박", "호텔", "호흡", "혹시", "홀로", "홈페이지", "홍보", "홍수", "홍차", "화면", "화분", "화살", "화요일", "화장", "화학", "확보", "확인", "확장", "확정", "환갑", "환경", "환영", "환율", "환자", "활기", "활동", "활발히", "활용", "활짝", "회견", "회관", "회복", "회색", "회원", "회장", "회전", "횟수", "횡단보도", "효율적", "후반", "후춧가루", "훈련", "훨씬", "휴식", "휴일", "흉내", "흐름", "흑백", "흑인", "흔적", "흔히", "흥미", "흥분", "희곡", "희망", "희생", "흰색", "힘껏"], + "SPANISH": ["ábaco", "abdomen", "abeja", "abierto", "abogado", "abono", "aborto", "abrazo", "abrir", "abuelo", "abuso", "acabar", "academia", "acceso", "acción", "aceite", "acelga", "acento", "aceptar", "ácido", "aclarar", "acné", "acoger", "acoso", "activo", "acto", "actriz", "actuar", "acudir", "acuerdo", "acusar", "adicto", "admitir", "adoptar", "adorno", "aduana", "adulto", "aéreo", "afectar", "afición", "afinar", "afirmar", "ágil", "agitar", "agonía", "agosto", "agotar", "agregar", "agrio", "agua", "agudo", "águila", "aguja", "ahogo", "ahorro", "aire", "aislar", "ajedrez", "ajeno", "ajuste", "alacrán", "alambre", "alarma", "alba", "álbum", "alcalde", "aldea", "alegre", "alejar", "alerta", "aleta", "alfiler", "alga", "algodón", "aliado", "aliento", "alivio", "alma", "almeja", "almíbar", "altar", "alteza", "altivo", "alto", "altura", "alumno", "alzar", "amable", "amante", "amapola", "amargo", "amasar", "ámbar", "ámbito", "ameno", "amigo", "amistad", "amor", "amparo", "amplio", "ancho", "anciano", "ancla", "andar", "andén", "anemia", "ángulo", "anillo", "ánimo", "anís", "anotar", "antena", "antiguo", "antojo", "anual", "anular", "anuncio", "añadir", "añejo", "año", "apagar", "aparato", "apetito", "apio", "aplicar", "apodo", "aporte", "apoyo", "aprender", "aprobar", "apuesta", "apuro", "arado", "araña", "arar", "árbitro", "árbol", "arbusto", "archivo", "arco", "arder", "ardilla", "arduo", "área", "árido", "aries", "armonía", "arnés", "aroma", "arpa", "arpón", "arreglo", "arroz", "arruga", "arte", "artista", "asa", "asado", "asalto", "ascenso", "asegurar", "aseo", "asesor", "asiento", "asilo", "asistir", "asno", "asombro", "áspero", "astilla", "astro", "astuto", "asumir", "asunto", "atajo", "ataque", "atar", "atento", "ateo", "ático", "atleta", "átomo", "atraer", "atroz", "atún", "audaz", "audio", "auge", "aula", "aumento", "ausente", "autor", "aval", "avance", "avaro", "ave", "avellana", "avena", "avestruz", "avión", "aviso", "ayer", "ayuda", "ayuno", "azafrán", "azar", "azote", "azúcar", "azufre", "azul", "baba", "babor", "bache", "bahía", "baile", "bajar", "balanza", "balcón", "balde", "bambú", "banco", "banda", "baño", "barba", "barco", "barniz", "barro", "báscula", "bastón", "basura", "batalla", "batería", "batir", "batuta", "baúl", "bazar", "bebé", "bebida", "bello", "besar", "beso", "bestia", "bicho", "bien", "bingo", "blanco", "bloque", "blusa", "boa", "bobina", "bobo", "boca", "bocina", "boda", "bodega", "boina", "bola", "bolero", "bolsa", "bomba", "bondad", "bonito", "bono", "bonsái", "borde", "borrar", "bosque", "bote", "botín", "bóveda", "bozal", "bravo", "brazo", "brecha", "breve", "brillo", "brinco", "brisa", "broca", "broma", "bronce", "brote", "bruja", "brusco", "bruto", "buceo", "bucle", "bueno", "buey", "bufanda", "bufón", "búho", "buitre", "bulto", "burbuja", "burla", "burro", "buscar", "butaca", "buzón", "caballo", "cabeza", "cabina", "cabra", "cacao", "cadáver", "cadena", "caer", "café", "caída", "caimán", "caja", "cajón", "cal", "calamar", "calcio", "caldo", "calidad", "calle", "calma", "calor", "calvo", "cama", "cambio", "camello", "camino", "campo", "cáncer", "candil", "canela", "canguro", "canica", "canto", "caña", "cañón", "caoba", "caos", "capaz", "capitán", "capote", "captar", "capucha", "cara", "carbón", "cárcel", "careta", "carga", "cariño", "carne", "carpeta", "carro", "carta", "casa", "casco", "casero", "caspa", "castor", "catorce", "catre", "caudal", "causa", "cazo", "cebolla", "ceder", "cedro", "celda", "célebre", "celoso", "célula", "cemento", "ceniza", "centro", "cerca", "cerdo", "cereza", "cero", "cerrar", "certeza", "césped", "cetro", "chacal", "chaleco", "champú", "chancla", "chapa", "charla", "chico", "chiste", "chivo", "choque", "choza", "chuleta", "chupar", "ciclón", "ciego", "cielo", "cien", "cierto", "cifra", "cigarro", "cima", "cinco", "cine", "cinta", "ciprés", "circo", "ciruela", "cisne", "cita", "ciudad", "clamor", "clan", "claro", "clase", "clave", "cliente", "clima", "clínica", "cobre", "cocción", "cochino", "cocina", "coco", "código", "codo", "cofre", "coger", "cohete", "cojín", "cojo", "cola", "colcha", "colegio", "colgar", "colina", "collar", "colmo", "columna", "combate", "comer", "comida", "cómodo", "compra", "conde", "conejo", "conga", "conocer", "consejo", "contar", "copa", "copia", "corazón", "corbata", "corcho", "cordón", "corona", "correr", "coser", "cosmos", "costa", "cráneo", "cráter", "crear", "crecer", "creído", "crema", "cría", "crimen", "cripta", "crisis", "cromo", "crónica", "croqueta", "crudo", "cruz", "cuadro", "cuarto", "cuatro", "cubo", "cubrir", "cuchara", "cuello", "cuento", "cuerda", "cuesta", "cueva", "cuidar", "culebra", "culpa", "culto", "cumbre", "cumplir", "cuna", "cuneta", "cuota", "cupón", "cúpula", "curar", "curioso", "curso", "curva", "cutis", "dama", "danza", "dar", "dardo", "dátil", "deber", "débil", "década", "decir", "dedo", "defensa", "definir", "dejar", "delfín", "delgado", "delito", "demora", "denso", "dental", "deporte", "derecho", "derrota", "desayuno", "deseo", "desfile", "desnudo", "destino", "desvío", "detalle", "detener", "deuda", "día", "diablo", "diadema", "diamante", "diana", "diario", "dibujo", "dictar", "diente", "dieta", "diez", "difícil", "digno", "dilema", "diluir", "dinero", "directo", "dirigir", "disco", "diseño", "disfraz", "diva", "divino", "doble", "doce", "dolor", "domingo", "don", "donar", "dorado", "dormir", "dorso", "dos", "dosis", "dragón", "droga", "ducha", "duda", "duelo", "dueño", "dulce", "dúo", "duque", "durar", "dureza", "duro", "ébano", "ebrio", "echar", "eco", "ecuador", "edad", "edición", "edificio", "editor", "educar", "efecto", "eficaz", "eje", "ejemplo", "elefante", "elegir", "elemento", "elevar", "elipse", "élite", "elixir", "elogio", "eludir", "embudo", "emitir", "emoción", "empate", "empeño", "empleo", "empresa", "enano", "encargo", "enchufe", "encía", "enemigo", "enero", "enfado", "enfermo", "engaño", "enigma", "enlace", "enorme", "enredo", "ensayo", "enseñar", "entero", "entrar", "envase", "envío", "época", "equipo", "erizo", "escala", "escena", "escolar", "escribir", "escudo", "esencia", "esfera", "esfuerzo", "espada", "espejo", "espía", "esposa", "espuma", "esquí", "estar", "este", "estilo", "estufa", "etapa", "eterno", "ética", "etnia", "evadir", "evaluar", "evento", "evitar", "exacto", "examen", "exceso", "excusa", "exento", "exigir", "exilio", "existir", "éxito", "experto", "explicar", "exponer", "extremo", "fábrica", "fábula", "fachada", "fácil", "factor", "faena", "faja", "falda", "fallo", "falso", "faltar", "fama", "familia", "famoso", "faraón", "farmacia", "farol", "farsa", "fase", "fatiga", "fauna", "favor", "fax", "febrero", "fecha", "feliz", "feo", "feria", "feroz", "fértil", "fervor", "festín", "fiable", "fianza", "fiar", "fibra", "ficción", "ficha", "fideo", "fiebre", "fiel", "fiera", "fiesta", "figura", "fijar", "fijo", "fila", "filete", "filial", "filtro", "fin", "finca", "fingir", "finito", "firma", "flaco", "flauta", "flecha", "flor", "flota", "fluir", "flujo", "flúor", "fobia", "foca", "fogata", "fogón", "folio", "folleto", "fondo", "forma", "forro", "fortuna", "forzar", "fosa", "foto", "fracaso", "frágil", "franja", "frase", "fraude", "freír", "freno", "fresa", "frío", "frito", "fruta", "fuego", "fuente", "fuerza", "fuga", "fumar", "función", "funda", "furgón", "furia", "fusil", "fútbol", "futuro", "gacela", "gafas", "gaita", "gajo", "gala", "galería", "gallo", "gamba", "ganar", "gancho", "ganga", "ganso", "garaje", "garza", "gasolina", "gastar", "gato", "gavilán", "gemelo", "gemir", "gen", "género", "genio", "gente", "geranio", "gerente", "germen", "gesto", "gigante", "gimnasio", "girar", "giro", "glaciar", "globo", "gloria", "gol", "golfo", "goloso", "golpe", "goma", "gordo", "gorila", "gorra", "gota", "goteo", "gozar", "grada", "gráfico", "grano", "grasa", "gratis", "grave", "grieta", "grillo", "gripe", "gris", "grito", "grosor", "grúa", "grueso", "grumo", "grupo", "guante", "guapo", "guardia", "guerra", "guía", "guiño", "guion", "guiso", "guitarra", "gusano", "gustar", "haber", "hábil", "hablar", "hacer", "hacha", "hada", "hallar", "hamaca", "harina", "haz", "hazaña", "hebilla", "hebra", "hecho", "helado", "helio", "hembra", "herir", "hermano", "héroe", "hervir", "hielo", "hierro", "hígado", "higiene", "hijo", "himno", "historia", "hocico", "hogar", "hoguera", "hoja", "hombre", "hongo", "honor", "honra", "hora", "hormiga", "horno", "hostil", "hoyo", "hueco", "huelga", "huerta", "hueso", "huevo", "huida", "huir", "humano", "húmedo", "humilde", "humo", "hundir", "huracán", "hurto", "icono", "ideal", "idioma", "ídolo", "iglesia", "iglú", "igual", "ilegal", "ilusión", "imagen", "imán", "imitar", "impar", "imperio", "imponer", "impulso", "incapaz", "índice", "inerte", "infiel", "informe", "ingenio", "inicio", "inmenso", "inmune", "innato", "insecto", "instante", "interés", "íntimo", "intuir", "inútil", "invierno", "ira", "iris", "ironía", "isla", "islote", "jabalí", "jabón", "jamón", "jarabe", "jardín", "jarra", "jaula", "jazmín", "jefe", "jeringa", "jinete", "jornada", "joroba", "joven", "joya", "juerga", "jueves", "juez", "jugador", "jugo", "juguete", "juicio", "junco", "jungla", "junio", "juntar", "júpiter", "jurar", "justo", "juvenil", "juzgar", "kilo", "koala", "labio", "lacio", "lacra", "lado", "ladrón", "lagarto", "lágrima", "laguna", "laico", "lamer", "lámina", "lámpara", "lana", "lancha", "langosta", "lanza", "lápiz", "largo", "larva", "lástima", "lata", "látex", "latir", "laurel", "lavar", "lazo", "leal", "lección", "leche", "lector", "leer", "legión", "legumbre", "lejano", "lengua", "lento", "leña", "león", "leopardo", "lesión", "letal", "letra", "leve", "leyenda", "libertad", "libro", "licor", "líder", "lidiar", "lienzo", "liga", "ligero", "lima", "límite", "limón", "limpio", "lince", "lindo", "línea", "lingote", "lino", "linterna", "líquido", "liso", "lista", "litera", "litio", "litro", "llaga", "llama", "llanto", "llave", "llegar", "llenar", "llevar", "llorar", "llover", "lluvia", "lobo", "loción", "loco", "locura", "lógica", "logro", "lombriz", "lomo", "lonja", "lote", "lucha", "lucir", "lugar", "lujo", "luna", "lunes", "lupa", "lustro", "luto", "luz", "maceta", "macho", "madera", "madre", "maduro", "maestro", "mafia", "magia", "mago", "maíz", "maldad", "maleta", "malla", "malo", "mamá", "mambo", "mamut", "manco", "mando", "manejar", "manga", "maniquí", "manjar", "mano", "manso", "manta", "mañana", "mapa", "máquina", "mar", "marco", "marea", "marfil", "margen", "marido", "mármol", "marrón", "martes", "marzo", "masa", "máscara", "masivo", "matar", "materia", "matiz", "matriz", "máximo", "mayor", "mazorca", "mecha", "medalla", "medio", "médula", "mejilla", "mejor", "melena", "melón", "memoria", "menor", "mensaje", "mente", "menú", "mercado", "merengue", "mérito", "mes", "mesón", "meta", "meter", "método", "metro", "mezcla", "miedo", "miel", "miembro", "miga", "mil", "milagro", "militar", "millón", "mimo", "mina", "minero", "mínimo", "minuto", "miope", "mirar", "misa", "miseria", "misil", "mismo", "mitad", "mito", "mochila", "moción", "moda", "modelo", "moho", "mojar", "molde", "moler", "molino", "momento", "momia", "monarca", "moneda", "monja", "monto", "moño", "morada", "morder", "moreno", "morir", "morro", "morsa", "mortal", "mosca", "mostrar", "motivo", "mover", "móvil", "mozo", "mucho", "mudar", "mueble", "muela", "muerte", "muestra", "mugre", "mujer", "mula", "muleta", "multa", "mundo", "muñeca", "mural", "muro", "músculo", "museo", "musgo", "música", "muslo", "nácar", "nación", "nadar", "naipe", "naranja", "nariz", "narrar", "nasal", "natal", "nativo", "natural", "náusea", "naval", "nave", "navidad", "necio", "néctar", "negar", "negocio", "negro", "neón", "nervio", "neto", "neutro", "nevar", "nevera", "nicho", "nido", "niebla", "nieto", "niñez", "niño", "nítido", "nivel", "nobleza", "noche", "nómina", "noria", "norma", "norte", "nota", "noticia", "novato", "novela", "novio", "nube", "nuca", "núcleo", "nudillo", "nudo", "nuera", "nueve", "nuez", "nulo", "número", "nutria", "oasis", "obeso", "obispo", "objeto", "obra", "obrero", "observar", "obtener", "obvio", "oca", "ocaso", "océano", "ochenta", "ocho", "ocio", "ocre", "octavo", "octubre", "oculto", "ocupar", "ocurrir", "odiar", "odio", "odisea", "oeste", "ofensa", "oferta", "oficio", "ofrecer", "ogro", "oído", "oír", "ojo", "ola", "oleada", "olfato", "olivo", "olla", "olmo", "olor", "olvido", "ombligo", "onda", "onza", "opaco", "opción", "ópera", "opinar", "oponer", "optar", "óptica", "opuesto", "oración", "orador", "oral", "órbita", "orca", "orden", "oreja", "órgano", "orgía", "orgullo", "oriente", "origen", "orilla", "oro", "orquesta", "oruga", "osadía", "oscuro", "osezno", "oso", "ostra", "otoño", "otro", "oveja", "óvulo", "óxido", "oxígeno", "oyente", "ozono", "pacto", "padre", "paella", "página", "pago", "país", "pájaro", "palabra", "palco", "paleta", "pálido", "palma", "paloma", "palpar", "pan", "panal", "pánico", "pantera", "pañuelo", "papá", "papel", "papilla", "paquete", "parar", "parcela", "pared", "parir", "paro", "párpado", "parque", "párrafo", "parte", "pasar", "paseo", "pasión", "paso", "pasta", "pata", "patio", "patria", "pausa", "pauta", "pavo", "payaso", "peatón", "pecado", "pecera", "pecho", "pedal", "pedir", "pegar", "peine", "pelar", "peldaño", "pelea", "peligro", "pellejo", "pelo", "peluca", "pena", "pensar", "peñón", "peón", "peor", "pepino", "pequeño", "pera", "percha", "perder", "pereza", "perfil", "perico", "perla", "permiso", "perro", "persona", "pesa", "pesca", "pésimo", "pestaña", "pétalo", "petróleo", "pez", "pezuña", "picar", "pichón", "pie", "piedra", "pierna", "pieza", "pijama", "pilar", "piloto", "pimienta", "pino", "pintor", "pinza", "piña", "piojo", "pipa", "pirata", "pisar", "piscina", "piso", "pista", "pitón", "pizca", "placa", "plan", "plata", "playa", "plaza", "pleito", "pleno", "plomo", "pluma", "plural", "pobre", "poco", "poder", "podio", "poema", "poesía", "poeta", "polen", "policía", "pollo", "polvo", "pomada", "pomelo", "pomo", "pompa", "poner", "porción", "portal", "posada", "poseer", "posible", "poste", "potencia", "potro", "pozo", "prado", "precoz", "pregunta", "premio", "prensa", "preso", "previo", "primo", "príncipe", "prisión", "privar", "proa", "probar", "proceso", "producto", "proeza", "profesor", "programa", "prole", "promesa", "pronto", "propio", "próximo", "prueba", "público", "puchero", "pudor", "pueblo", "puerta", "puesto", "pulga", "pulir", "pulmón", "pulpo", "pulso", "puma", "punto", "puñal", "puño", "pupa", "pupila", "puré", "quedar", "queja", "quemar", "querer", "queso", "quieto", "química", "quince", "quitar", "rábano", "rabia", "rabo", "ración", "radical", "raíz", "rama", "rampa", "rancho", "rango", "rapaz", "rápido", "rapto", "rasgo", "raspa", "rato", "rayo", "raza", "razón", "reacción", "realidad", "rebaño", "rebote", "recaer", "receta", "rechazo", "recoger", "recreo", "recto", "recurso", "red", "redondo", "reducir", "reflejo", "reforma", "refrán", "refugio", "regalo", "regir", "regla", "regreso", "rehén", "reino", "reír", "reja", "relato", "relevo", "relieve", "relleno", "reloj", "remar", "remedio", "remo", "rencor", "rendir", "renta", "reparto", "repetir", "reposo", "reptil", "res", "rescate", "resina", "respeto", "resto", "resumen", "retiro", "retorno", "retrato", "reunir", "revés", "revista", "rey", "rezar", "rico", "riego", "rienda", "riesgo", "rifa", "rígido", "rigor", "rincón", "riñón", "río", "riqueza", "risa", "ritmo", "rito", "rizo", "roble", "roce", "rociar", "rodar", "rodeo", "rodilla", "roer", "rojizo", "rojo", "romero", "romper", "ron", "ronco", "ronda", "ropa", "ropero", "rosa", "rosca", "rostro", "rotar", "rubí", "rubor", "rudo", "rueda", "rugir", "ruido", "ruina", "ruleta", "rulo", "rumbo", "rumor", "ruptura", "ruta", "rutina", "sábado", "saber", "sabio", "sable", "sacar", "sagaz", "sagrado", "sala", "saldo", "salero", "salir", "salmón", "salón", "salsa", "salto", "salud", "salvar", "samba", "sanción", "sandía", "sanear", "sangre", "sanidad", "sano", "santo", "sapo", "saque", "sardina", "sartén", "sastre", "satán", "sauna", "saxofón", "sección", "seco", "secreto", "secta", "sed", "seguir", "seis", "sello", "selva", "semana", "semilla", "senda", "sensor", "señal", "señor", "separar", "sepia", "sequía", "ser", "serie", "sermón", "servir", "sesenta", "sesión", "seta", "setenta", "severo", "sexo", "sexto", "sidra", "siesta", "siete", "siglo", "signo", "sílaba", "silbar", "silencio", "silla", "símbolo", "simio", "sirena", "sistema", "sitio", "situar", "sobre", "socio", "sodio", "sol", "solapa", "soldado", "soledad", "sólido", "soltar", "solución", "sombra", "sondeo", "sonido", "sonoro", "sonrisa", "sopa", "soplar", "soporte", "sordo", "sorpresa", "sorteo", "sostén", "sótano", "suave", "subir", "suceso", "sudor", "suegra", "suelo", "sueño", "suerte", "sufrir", "sujeto", "sultán", "sumar", "superar", "suplir", "suponer", "supremo", "sur", "surco", "sureño", "surgir", "susto", "sutil", "tabaco", "tabique", "tabla", "tabú", "taco", "tacto", "tajo", "talar", "talco", "talento", "talla", "talón", "tamaño", "tambor", "tango", "tanque", "tapa", "tapete", "tapia", "tapón", "taquilla", "tarde", "tarea", "tarifa", "tarjeta", "tarot", "tarro", "tarta", "tatuaje", "tauro", "taza", "tazón", "teatro", "techo", "tecla", "técnica", "tejado", "tejer", "tejido", "tela", "teléfono", "tema", "temor", "templo", "tenaz", "tender", "tener", "tenis", "tenso", "teoría", "terapia", "terco", "término", "ternura", "terror", "tesis", "tesoro", "testigo", "tetera", "texto", "tez", "tibio", "tiburón", "tiempo", "tienda", "tierra", "tieso", "tigre", "tijera", "tilde", "timbre", "tímido", "timo", "tinta", "tío", "típico", "tipo", "tira", "tirón", "titán", "títere", "título", "tiza", "toalla", "tobillo", "tocar", "tocino", "todo", "toga", "toldo", "tomar", "tono", "tonto", "topar", "tope", "toque", "tórax", "torero", "tormenta", "torneo", "toro", "torpedo", "torre", "torso", "tortuga", "tos", "tosco", "toser", "tóxico", "trabajo", "tractor", "traer", "tráfico", "trago", "traje", "tramo", "trance", "trato", "trauma", "trazar", "trébol", "tregua", "treinta", "tren", "trepar", "tres", "tribu", "trigo", "tripa", "triste", "triunfo", "trofeo", "trompa", "tronco", "tropa", "trote", "trozo", "truco", "trueno", "trufa", "tubería", "tubo", "tuerto", "tumba", "tumor", "túnel", "túnica", "turbina", "turismo", "turno", "tutor", "ubicar", "úlcera", "umbral", "unidad", "unir", "universo", "uno", "untar", "uña", "urbano", "urbe", "urgente", "urna", "usar", "usuario", "útil", "utopía", "uva", "vaca", "vacío", "vacuna", "vagar", "vago", "vaina", "vajilla", "vale", "válido", "valle", "valor", "válvula", "vampiro", "vara", "variar", "varón", "vaso", "vecino", "vector", "vehículo", "veinte", "vejez", "vela", "velero", "veloz", "vena", "vencer", "venda", "veneno", "vengar", "venir", "venta", "venus", "ver", "verano", "verbo", "verde", "vereda", "verja", "verso", "verter", "vía", "viaje", "vibrar", "vicio", "víctima", "vida", "vídeo", "vidrio", "viejo", "viernes", "vigor", "vil", "villa", "vinagre", "vino", "viñedo", "violín", "viral", "virgo", "virtud", "visor", "víspera", "vista", "vitamina", "viudo", "vivaz", "vivero", "vivir", "vivo", "volcán", "volumen", "volver", "voraz", "votar", "voto", "voz", "vuelo", "vulgar", "yacer", "yate", "yegua", "yema", "yerno", "yeso", "yodo", "yoga", "yogur", "zafiro", "zanja", "zapato", "zarza", "zona", "zorro", "zumo", "zurdo"], + "SIMPLIFIED_CHINESE": ["的", "一", "是", "在", "不", "了", "有", "和", "人", "这", "中", "大", "为", "上", "个", "国", "我", "以", "要", "他", "时", "来", "用", "们", "生", "到", "作", "地", "于", "出", "就", "分", "对", "成", "会", "可", "主", "发", "年", "动", "同", "工", "也", "能", "下", "过", "子", "说", "产", "种", "面", "而", "方", "后", "多", "定", "行", "学", "法", "所", "民", "得", "经", "十", "三", "之", "进", "着", "等", "部", "度", "家", "电", "力", "里", "如", "水", "化", "高", "自", "二", "理", "起", "小", "物", "现", "实", "加", "量", "都", "两", "体", "制", "机", "当", "使", "点", "从", "业", "本", "去", "把", "性", "好", "应", "开", "它", "合", "还", "因", "由", "其", "些", "然", "前", "外", "天", "政", "四", "日", "那", "社", "义", "事", "平", "形", "相", "全", "表", "间", "样", "与", "关", "各", "重", "新", "线", "内", "数", "正", "心", "反", "你", "明", "看", "原", "又", "么", "利", "比", "或", "但", "质", "气", "第", "向", "道", "命", "此", "变", "条", "只", "没", "结", "解", "问", "意", "建", "月", "公", "无", "系", "军", "很", "情", "者", "最", "立", "代", "想", "已", "通", "并", "提", "直", "题", "党", "程", "展", "五", "果", "料", "象", "员", "革", "位", "入", "常", "文", "总", "次", "品", "式", "活", "设", "及", "管", "特", "件", "长", "求", "老", "头", "基", "资", "边", "流", "路", "级", "少", "图", "山", "统", "接", "知", "较", "将", "组", "见", "计", "别", "她", "手", "角", "期", "根", "论", "运", "农", "指", "几", "九", "区", "强", "放", "决", "西", "被", "干", "做", "必", "战", "先", "回", "则", "任", "取", "据", "处", "队", "南", "给", "色", "光", "门", "即", "保", "治", "北", "造", "百", "规", "热", "领", "七", "海", "口", "东", "导", "器", "压", "志", "世", "金", "增", "争", "济", "阶", "油", "思", "术", "极", "交", "受", "联", "什", "认", "六", "共", "权", "收", "证", "改", "清", "美", "再", "采", "转", "更", "单", "风", "切", "打", "白", "教", "速", "花", "带", "安", "场", "身", "车", "例", "真", "务", "具", "万", "每", "目", "至", "达", "走", "积", "示", "议", "声", "报", "斗", "完", "类", "八", "离", "华", "名", "确", "才", "科", "张", "信", "马", "节", "话", "米", "整", "空", "元", "况", "今", "集", "温", "传", "土", "许", "步", "群", "广", "石", "记", "需", "段", "研", "界", "拉", "林", "律", "叫", "且", "究", "观", "越", "织", "装", "影", "算", "低", "持", "音", "众", "书", "布", "复", "容", "儿", "须", "际", "商", "非", "验", "连", "断", "深", "难", "近", "矿", "千", "周", "委", "素", "技", "备", "半", "办", "青", "省", "列", "习", "响", "约", "支", "般", "史", "感", "劳", "便", "团", "往", "酸", "历", "市", "克", "何", "除", "消", "构", "府", "称", "太", "准", "精", "值", "号", "率", "族", "维", "划", "选", "标", "写", "存", "候", "毛", "亲", "快", "效", "斯", "院", "查", "江", "型", "眼", "王", "按", "格", "养", "易", "置", "派", "层", "片", "始", "却", "专", "状", "育", "厂", "京", "识", "适", "属", "圆", "包", "火", "住", "调", "满", "县", "局", "照", "参", "红", "细", "引", "听", "该", "铁", "价", "严", "首", "底", "液", "官", "德", "随", "病", "苏", "失", "尔", "死", "讲", "配", "女", "黄", "推", "显", "谈", "罪", "神", "艺", "呢", "席", "含", "企", "望", "密", "批", "营", "项", "防", "举", "球", "英", "氧", "势", "告", "李", "台", "落", "木", "帮", "轮", "破", "亚", "师", "围", "注", "远", "字", "材", "排", "供", "河", "态", "封", "另", "施", "减", "树", "溶", "怎", "止", "案", "言", "士", "均", "武", "固", "叶", "鱼", "波", "视", "仅", "费", "紧", "爱", "左", "章", "早", "朝", "害", "续", "轻", "服", "试", "食", "充", "兵", "源", "判", "护", "司", "足", "某", "练", "差", "致", "板", "田", "降", "黑", "犯", "负", "击", "范", "继", "兴", "似", "余", "坚", "曲", "输", "修", "故", "城", "夫", "够", "送", "笔", "船", "占", "右", "财", "吃", "富", "春", "职", "觉", "汉", "画", "功", "巴", "跟", "虽", "杂", "飞", "检", "吸", "助", "升", "阳", "互", "初", "创", "抗", "考", "投", "坏", "策", "古", "径", "换", "未", "跑", "留", "钢", "曾", "端", "责", "站", "简", "述", "钱", "副", "尽", "帝", "射", "草", "冲", "承", "独", "令", "限", "阿", "宣", "环", "双", "请", "超", "微", "让", "控", "州", "良", "轴", "找", "否", "纪", "益", "依", "优", "顶", "础", "载", "倒", "房", "突", "坐", "粉", "敌", "略", "客", "袁", "冷", "胜", "绝", "析", "块", "剂", "测", "丝", "协", "诉", "念", "陈", "仍", "罗", "盐", "友", "洋", "错", "苦", "夜", "刑", "移", "频", "逐", "靠", "混", "母", "短", "皮", "终", "聚", "汽", "村", "云", "哪", "既", "距", "卫", "停", "烈", "央", "察", "烧", "迅", "境", "若", "印", "洲", "刻", "括", "激", "孔", "搞", "甚", "室", "待", "核", "校", "散", "侵", "吧", "甲", "游", "久", "菜", "味", "旧", "模", "湖", "货", "损", "预", "阻", "毫", "普", "稳", "乙", "妈", "植", "息", "扩", "银", "语", "挥", "酒", "守", "拿", "序", "纸", "医", "缺", "雨", "吗", "针", "刘", "啊", "急", "唱", "误", "训", "愿", "审", "附", "获", "茶", "鲜", "粮", "斤", "孩", "脱", "硫", "肥", "善", "龙", "演", "父", "渐", "血", "欢", "械", "掌", "歌", "沙", "刚", "攻", "谓", "盾", "讨", "晚", "粒", "乱", "燃", "矛", "乎", "杀", "药", "宁", "鲁", "贵", "钟", "煤", "读", "班", "伯", "香", "介", "迫", "句", "丰", "培", "握", "兰", "担", "弦", "蛋", "沉", "假", "穿", "执", "答", "乐", "谁", "顺", "烟", "缩", "征", "脸", "喜", "松", "脚", "困", "异", "免", "背", "星", "福", "买", "染", "井", "概", "慢", "怕", "磁", "倍", "祖", "皇", "促", "静", "补", "评", "翻", "肉", "践", "尼", "衣", "宽", "扬", "棉", "希", "伤", "操", "垂", "秋", "宜", "氢", "套", "督", "振", "架", "亮", "末", "宪", "庆", "编", "牛", "触", "映", "雷", "销", "诗", "座", "居", "抓", "裂", "胞", "呼", "娘", "景", "威", "绿", "晶", "厚", "盟", "衡", "鸡", "孙", "延", "危", "胶", "屋", "乡", "临", "陆", "顾", "掉", "呀", "灯", "岁", "措", "束", "耐", "剧", "玉", "赵", "跳", "哥", "季", "课", "凯", "胡", "额", "款", "绍", "卷", "齐", "伟", "蒸", "殖", "永", "宗", "苗", "川", "炉", "岩", "弱", "零", "杨", "奏", "沿", "露", "杆", "探", "滑", "镇", "饭", "浓", "航", "怀", "赶", "库", "夺", "伊", "灵", "税", "途", "灭", "赛", "归", "召", "鼓", "播", "盘", "裁", "险", "康", "唯", "录", "菌", "纯", "借", "糖", "盖", "横", "符", "私", "努", "堂", "域", "枪", "润", "幅", "哈", "竟", "熟", "虫", "泽", "脑", "壤", "碳", "欧", "遍", "侧", "寨", "敢", "彻", "虑", "斜", "薄", "庭", "纳", "弹", "饲", "伸", "折", "麦", "湿", "暗", "荷", "瓦", "塞", "床", "筑", "恶", "户", "访", "塔", "奇", "透", "梁", "刀", "旋", "迹", "卡", "氯", "遇", "份", "毒", "泥", "退", "洗", "摆", "灰", "彩", "卖", "耗", "夏", "择", "忙", "铜", "献", "硬", "予", "繁", "圈", "雪", "函", "亦", "抽", "篇", "阵", "阴", "丁", "尺", "追", "堆", "雄", "迎", "泛", "爸", "楼", "避", "谋", "吨", "野", "猪", "旗", "累", "偏", "典", "馆", "索", "秦", "脂", "潮", "爷", "豆", "忽", "托", "惊", "塑", "遗", "愈", "朱", "替", "纤", "粗", "倾", "尚", "痛", "楚", "谢", "奋", "购", "磨", "君", "池", "旁", "碎", "骨", "监", "捕", "弟", "暴", "割", "贯", "殊", "释", "词", "亡", "壁", "顿", "宝", "午", "尘", "闻", "揭", "炮", "残", "冬", "桥", "妇", "警", "综", "招", "吴", "付", "浮", "遭", "徐", "您", "摇", "谷", "赞", "箱", "隔", "订", "男", "吹", "园", "纷", "唐", "败", "宋", "玻", "巨", "耕", "坦", "荣", "闭", "湾", "键", "凡", "驻", "锅", "救", "恩", "剥", "凝", "碱", "齿", "截", "炼", "麻", "纺", "禁", "废", "盛", "版", "缓", "净", "睛", "昌", "婚", "涉", "筒", "嘴", "插", "岸", "朗", "庄", "街", "藏", "姑", "贸", "腐", "奴", "啦", "惯", "乘", "伙", "恢", "匀", "纱", "扎", "辩", "耳", "彪", "臣", "亿", "璃", "抵", "脉", "秀", "萨", "俄", "网", "舞", "店", "喷", "纵", "寸", "汗", "挂", "洪", "贺", "闪", "柬", "爆", "烯", "津", "稻", "墙", "软", "勇", "像", "滚", "厘", "蒙", "芳", "肯", "坡", "柱", "荡", "腿", "仪", "旅", "尾", "轧", "冰", "贡", "登", "黎", "削", "钻", "勒", "逃", "障", "氨", "郭", "峰", "币", "港", "伏", "轨", "亩", "毕", "擦", "莫", "刺", "浪", "秘", "援", "株", "健", "售", "股", "岛", "甘", "泡", "睡", "童", "铸", "汤", "阀", "休", "汇", "舍", "牧", "绕", "炸", "哲", "磷", "绩", "朋", "淡", "尖", "启", "陷", "柴", "呈", "徒", "颜", "泪", "稍", "忘", "泵", "蓝", "拖", "洞", "授", "镜", "辛", "壮", "锋", "贫", "虚", "弯", "摩", "泰", "幼", "廷", "尊", "窗", "纲", "弄", "隶", "疑", "氏", "宫", "姐", "震", "瑞", "怪", "尤", "琴", "循", "描", "膜", "违", "夹", "腰", "缘", "珠", "穷", "森", "枝", "竹", "沟", "催", "绳", "忆", "邦", "剩", "幸", "浆", "栏", "拥", "牙", "贮", "礼", "滤", "钠", "纹", "罢", "拍", "咱", "喊", "袖", "埃", "勤", "罚", "焦", "潜", "伍", "墨", "欲", "缝", "姓", "刊", "饱", "仿", "奖", "铝", "鬼", "丽", "跨", "默", "挖", "链", "扫", "喝", "袋", "炭", "污", "幕", "诸", "弧", "励", "梅", "奶", "洁", "灾", "舟", "鉴", "苯", "讼", "抱", "毁", "懂", "寒", "智", "埔", "寄", "届", "跃", "渡", "挑", "丹", "艰", "贝", "碰", "拔", "爹", "戴", "码", "梦", "芽", "熔", "赤", "渔", "哭", "敬", "颗", "奔", "铅", "仲", "虎", "稀", "妹", "乏", "珍", "申", "桌", "遵", "允", "隆", "螺", "仓", "魏", "锐", "晓", "氮", "兼", "隐", "碍", "赫", "拨", "忠", "肃", "缸", "牵", "抢", "博", "巧", "壳", "兄", "杜", "讯", "诚", "碧", "祥", "柯", "页", "巡", "矩", "悲", "灌", "龄", "伦", "票", "寻", "桂", "铺", "圣", "恐", "恰", "郑", "趣", "抬", "荒", "腾", "贴", "柔", "滴", "猛", "阔", "辆", "妻", "填", "撤", "储", "签", "闹", "扰", "紫", "砂", "递", "戏", "吊", "陶", "伐", "喂", "疗", "瓶", "婆", "抚", "臂", "摸", "忍", "虾", "蜡", "邻", "胸", "巩", "挤", "偶", "弃", "槽", "劲", "乳", "邓", "吉", "仁", "烂", "砖", "租", "乌", "舰", "伴", "瓜", "浅", "丙", "暂", "燥", "橡", "柳", "迷", "暖", "牌", "秧", "胆", "详", "簧", "踏", "瓷", "谱", "呆", "宾", "糊", "洛", "辉", "愤", "竞", "隙", "怒", "粘", "乃", "绪", "肩", "籍", "敏", "涂", "熙", "皆", "侦", "悬", "掘", "享", "纠", "醒", "狂", "锁", "淀", "恨", "牲", "霸", "爬", "赏", "逆", "玩", "陵", "祝", "秒", "浙", "貌", "役", "彼", "悉", "鸭", "趋", "凤", "晨", "畜", "辈", "秩", "卵", "署", "梯", "炎", "滩", "棋", "驱", "筛", "峡", "冒", "啥", "寿", "译", "浸", "泉", "帽", "迟", "硅", "疆", "贷", "漏", "稿", "冠", "嫩", "胁", "芯", "牢", "叛", "蚀", "奥", "鸣", "岭", "羊", "凭", "串", "塘", "绘", "酵", "融", "盆", "锡", "庙", "筹", "冻", "辅", "摄", "袭", "筋", "拒", "僚", "旱", "钾", "鸟", "漆", "沈", "眉", "疏", "添", "棒", "穗", "硝", "韩", "逼", "扭", "侨", "凉", "挺", "碗", "栽", "炒", "杯", "患", "馏", "劝", "豪", "辽", "勃", "鸿", "旦", "吏", "拜", "狗", "埋", "辊", "掩", "饮", "搬", "骂", "辞", "勾", "扣", "估", "蒋", "绒", "雾", "丈", "朵", "姆", "拟", "宇", "辑", "陕", "雕", "偿", "蓄", "崇", "剪", "倡", "厅", "咬", "驶", "薯", "刷", "斥", "番", "赋", "奉", "佛", "浇", "漫", "曼", "扇", "钙", "桃", "扶", "仔", "返", "俗", "亏", "腔", "鞋", "棱", "覆", "框", "悄", "叔", "撞", "骗", "勘", "旺", "沸", "孤", "吐", "孟", "渠", "屈", "疾", "妙", "惜", "仰", "狠", "胀", "谐", "抛", "霉", "桑", "岗", "嘛", "衰", "盗", "渗", "脏", "赖", "涌", "甜", "曹", "阅", "肌", "哩", "厉", "烃", "纬", "毅", "昨", "伪", "症", "煮", "叹", "钉", "搭", "茎", "笼", "酷", "偷", "弓", "锥", "恒", "杰", "坑", "鼻", "翼", "纶", "叙", "狱", "逮", "罐", "络", "棚", "抑", "膨", "蔬", "寺", "骤", "穆", "冶", "枯", "册", "尸", "凸", "绅", "坯", "牺", "焰", "轰", "欣", "晋", "瘦", "御", "锭", "锦", "丧", "旬", "锻", "垄", "搜", "扑", "邀", "亭", "酯", "迈", "舒", "脆", "酶", "闲", "忧", "酚", "顽", "羽", "涨", "卸", "仗", "陪", "辟", "惩", "杭", "姚", "肚", "捉", "飘", "漂", "昆", "欺", "吾", "郎", "烷", "汁", "呵", "饰", "萧", "雅", "邮", "迁", "燕", "撒", "姻", "赴", "宴", "烦", "债", "帐", "斑", "铃", "旨", "醇", "董", "饼", "雏", "姿", "拌", "傅", "腹", "妥", "揉", "贤", "拆", "歪", "葡", "胺", "丢", "浩", "徽", "昂", "垫", "挡", "览", "贪", "慰", "缴", "汪", "慌", "冯", "诺", "姜", "谊", "凶", "劣", "诬", "耀", "昏", "躺", "盈", "骑", "乔", "溪", "丛", "卢", "抹", "闷", "咨", "刮", "驾", "缆", "悟", "摘", "铒", "掷", "颇", "幻", "柄", "惠", "惨", "佳", "仇", "腊", "窝", "涤", "剑", "瞧", "堡", "泼", "葱", "罩", "霍", "捞", "胎", "苍", "滨", "俩", "捅", "湘", "砍", "霞", "邵", "萄", "疯", "淮", "遂", "熊", "粪", "烘", "宿", "档", "戈", "驳", "嫂", "裕", "徙", "箭", "捐", "肠", "撑", "晒", "辨", "殿", "莲", "摊", "搅", "酱", "屏", "疫", "哀", "蔡", "堵", "沫", "皱", "畅", "叠", "阁", "莱", "敲", "辖", "钩", "痕", "坝", "巷", "饿", "祸", "丘", "玄", "溜", "曰", "逻", "彭", "尝", "卿", "妨", "艇", "吞", "韦", "怨", "矮", "歇"], + "TRADITIONAL_CHINESE": ["的", "一", "是", "在", "不", "了", "有", "和", "人", "這", "中", "大", "為", "上", "個", "國", "我", "以", "要", "他", "時", "來", "用", "們", "生", "到", "作", "地", "於", "出", "就", "分", "對", "成", "會", "可", "主", "發", "年", "動", "同", "工", "也", "能", "下", "過", "子", "說", "產", "種", "面", "而", "方", "後", "多", "定", "行", "學", "法", "所", "民", "得", "經", "十", "三", "之", "進", "著", "等", "部", "度", "家", "電", "力", "裡", "如", "水", "化", "高", "自", "二", "理", "起", "小", "物", "現", "實", "加", "量", "都", "兩", "體", "制", "機", "當", "使", "點", "從", "業", "本", "去", "把", "性", "好", "應", "開", "它", "合", "還", "因", "由", "其", "些", "然", "前", "外", "天", "政", "四", "日", "那", "社", "義", "事", "平", "形", "相", "全", "表", "間", "樣", "與", "關", "各", "重", "新", "線", "內", "數", "正", "心", "反", "你", "明", "看", "原", "又", "麼", "利", "比", "或", "但", "質", "氣", "第", "向", "道", "命", "此", "變", "條", "只", "沒", "結", "解", "問", "意", "建", "月", "公", "無", "系", "軍", "很", "情", "者", "最", "立", "代", "想", "已", "通", "並", "提", "直", "題", "黨", "程", "展", "五", "果", "料", "象", "員", "革", "位", "入", "常", "文", "總", "次", "品", "式", "活", "設", "及", "管", "特", "件", "長", "求", "老", "頭", "基", "資", "邊", "流", "路", "級", "少", "圖", "山", "統", "接", "知", "較", "將", "組", "見", "計", "別", "她", "手", "角", "期", "根", "論", "運", "農", "指", "幾", "九", "區", "強", "放", "決", "西", "被", "幹", "做", "必", "戰", "先", "回", "則", "任", "取", "據", "處", "隊", "南", "給", "色", "光", "門", "即", "保", "治", "北", "造", "百", "規", "熱", "領", "七", "海", "口", "東", "導", "器", "壓", "志", "世", "金", "增", "爭", "濟", "階", "油", "思", "術", "極", "交", "受", "聯", "什", "認", "六", "共", "權", "收", "證", "改", "清", "美", "再", "採", "轉", "更", "單", "風", "切", "打", "白", "教", "速", "花", "帶", "安", "場", "身", "車", "例", "真", "務", "具", "萬", "每", "目", "至", "達", "走", "積", "示", "議", "聲", "報", "鬥", "完", "類", "八", "離", "華", "名", "確", "才", "科", "張", "信", "馬", "節", "話", "米", "整", "空", "元", "況", "今", "集", "溫", "傳", "土", "許", "步", "群", "廣", "石", "記", "需", "段", "研", "界", "拉", "林", "律", "叫", "且", "究", "觀", "越", "織", "裝", "影", "算", "低", "持", "音", "眾", "書", "布", "复", "容", "兒", "須", "際", "商", "非", "驗", "連", "斷", "深", "難", "近", "礦", "千", "週", "委", "素", "技", "備", "半", "辦", "青", "省", "列", "習", "響", "約", "支", "般", "史", "感", "勞", "便", "團", "往", "酸", "歷", "市", "克", "何", "除", "消", "構", "府", "稱", "太", "準", "精", "值", "號", "率", "族", "維", "劃", "選", "標", "寫", "存", "候", "毛", "親", "快", "效", "斯", "院", "查", "江", "型", "眼", "王", "按", "格", "養", "易", "置", "派", "層", "片", "始", "卻", "專", "狀", "育", "廠", "京", "識", "適", "屬", "圓", "包", "火", "住", "調", "滿", "縣", "局", "照", "參", "紅", "細", "引", "聽", "該", "鐵", "價", "嚴", "首", "底", "液", "官", "德", "隨", "病", "蘇", "失", "爾", "死", "講", "配", "女", "黃", "推", "顯", "談", "罪", "神", "藝", "呢", "席", "含", "企", "望", "密", "批", "營", "項", "防", "舉", "球", "英", "氧", "勢", "告", "李", "台", "落", "木", "幫", "輪", "破", "亞", "師", "圍", "注", "遠", "字", "材", "排", "供", "河", "態", "封", "另", "施", "減", "樹", "溶", "怎", "止", "案", "言", "士", "均", "武", "固", "葉", "魚", "波", "視", "僅", "費", "緊", "愛", "左", "章", "早", "朝", "害", "續", "輕", "服", "試", "食", "充", "兵", "源", "判", "護", "司", "足", "某", "練", "差", "致", "板", "田", "降", "黑", "犯", "負", "擊", "范", "繼", "興", "似", "餘", "堅", "曲", "輸", "修", "故", "城", "夫", "夠", "送", "筆", "船", "佔", "右", "財", "吃", "富", "春", "職", "覺", "漢", "畫", "功", "巴", "跟", "雖", "雜", "飛", "檢", "吸", "助", "昇", "陽", "互", "初", "創", "抗", "考", "投", "壞", "策", "古", "徑", "換", "未", "跑", "留", "鋼", "曾", "端", "責", "站", "簡", "述", "錢", "副", "盡", "帝", "射", "草", "衝", "承", "獨", "令", "限", "阿", "宣", "環", "雙", "請", "超", "微", "讓", "控", "州", "良", "軸", "找", "否", "紀", "益", "依", "優", "頂", "礎", "載", "倒", "房", "突", "坐", "粉", "敵", "略", "客", "袁", "冷", "勝", "絕", "析", "塊", "劑", "測", "絲", "協", "訴", "念", "陳", "仍", "羅", "鹽", "友", "洋", "錯", "苦", "夜", "刑", "移", "頻", "逐", "靠", "混", "母", "短", "皮", "終", "聚", "汽", "村", "雲", "哪", "既", "距", "衛", "停", "烈", "央", "察", "燒", "迅", "境", "若", "印", "洲", "刻", "括", "激", "孔", "搞", "甚", "室", "待", "核", "校", "散", "侵", "吧", "甲", "遊", "久", "菜", "味", "舊", "模", "湖", "貨", "損", "預", "阻", "毫", "普", "穩", "乙", "媽", "植", "息", "擴", "銀", "語", "揮", "酒", "守", "拿", "序", "紙", "醫", "缺", "雨", "嗎", "針", "劉", "啊", "急", "唱", "誤", "訓", "願", "審", "附", "獲", "茶", "鮮", "糧", "斤", "孩", "脫", "硫", "肥", "善", "龍", "演", "父", "漸", "血", "歡", "械", "掌", "歌", "沙", "剛", "攻", "謂", "盾", "討", "晚", "粒", "亂", "燃", "矛", "乎", "殺", "藥", "寧", "魯", "貴", "鐘", "煤", "讀", "班", "伯", "香", "介", "迫", "句", "豐", "培", "握", "蘭", "擔", "弦", "蛋", "沉", "假", "穿", "執", "答", "樂", "誰", "順", "煙", "縮", "徵", "臉", "喜", "松", "腳", "困", "異", "免", "背", "星", "福", "買", "染", "井", "概", "慢", "怕", "磁", "倍", "祖", "皇", "促", "靜", "補", "評", "翻", "肉", "踐", "尼", "衣", "寬", "揚", "棉", "希", "傷", "操", "垂", "秋", "宜", "氫", "套", "督", "振", "架", "亮", "末", "憲", "慶", "編", "牛", "觸", "映", "雷", "銷", "詩", "座", "居", "抓", "裂", "胞", "呼", "娘", "景", "威", "綠", "晶", "厚", "盟", "衡", "雞", "孫", "延", "危", "膠", "屋", "鄉", "臨", "陸", "顧", "掉", "呀", "燈", "歲", "措", "束", "耐", "劇", "玉", "趙", "跳", "哥", "季", "課", "凱", "胡", "額", "款", "紹", "卷", "齊", "偉", "蒸", "殖", "永", "宗", "苗", "川", "爐", "岩", "弱", "零", "楊", "奏", "沿", "露", "桿", "探", "滑", "鎮", "飯", "濃", "航", "懷", "趕", "庫", "奪", "伊", "靈", "稅", "途", "滅", "賽", "歸", "召", "鼓", "播", "盤", "裁", "險", "康", "唯", "錄", "菌", "純", "借", "糖", "蓋", "橫", "符", "私", "努", "堂", "域", "槍", "潤", "幅", "哈", "竟", "熟", "蟲", "澤", "腦", "壤", "碳", "歐", "遍", "側", "寨", "敢", "徹", "慮", "斜", "薄", "庭", "納", "彈", "飼", "伸", "折", "麥", "濕", "暗", "荷", "瓦", "塞", "床", "築", "惡", "戶", "訪", "塔", "奇", "透", "梁", "刀", "旋", "跡", "卡", "氯", "遇", "份", "毒", "泥", "退", "洗", "擺", "灰", "彩", "賣", "耗", "夏", "擇", "忙", "銅", "獻", "硬", "予", "繁", "圈", "雪", "函", "亦", "抽", "篇", "陣", "陰", "丁", "尺", "追", "堆", "雄", "迎", "泛", "爸", "樓", "避", "謀", "噸", "野", "豬", "旗", "累", "偏", "典", "館", "索", "秦", "脂", "潮", "爺", "豆", "忽", "托", "驚", "塑", "遺", "愈", "朱", "替", "纖", "粗", "傾", "尚", "痛", "楚", "謝", "奮", "購", "磨", "君", "池", "旁", "碎", "骨", "監", "捕", "弟", "暴", "割", "貫", "殊", "釋", "詞", "亡", "壁", "頓", "寶", "午", "塵", "聞", "揭", "炮", "殘", "冬", "橋", "婦", "警", "綜", "招", "吳", "付", "浮", "遭", "徐", "您", "搖", "谷", "贊", "箱", "隔", "訂", "男", "吹", "園", "紛", "唐", "敗", "宋", "玻", "巨", "耕", "坦", "榮", "閉", "灣", "鍵", "凡", "駐", "鍋", "救", "恩", "剝", "凝", "鹼", "齒", "截", "煉", "麻", "紡", "禁", "廢", "盛", "版", "緩", "淨", "睛", "昌", "婚", "涉", "筒", "嘴", "插", "岸", "朗", "莊", "街", "藏", "姑", "貿", "腐", "奴", "啦", "慣", "乘", "夥", "恢", "勻", "紗", "扎", "辯", "耳", "彪", "臣", "億", "璃", "抵", "脈", "秀", "薩", "俄", "網", "舞", "店", "噴", "縱", "寸", "汗", "掛", "洪", "賀", "閃", "柬", "爆", "烯", "津", "稻", "牆", "軟", "勇", "像", "滾", "厘", "蒙", "芳", "肯", "坡", "柱", "盪", "腿", "儀", "旅", "尾", "軋", "冰", "貢", "登", "黎", "削", "鑽", "勒", "逃", "障", "氨", "郭", "峰", "幣", "港", "伏", "軌", "畝", "畢", "擦", "莫", "刺", "浪", "秘", "援", "株", "健", "售", "股", "島", "甘", "泡", "睡", "童", "鑄", "湯", "閥", "休", "匯", "舍", "牧", "繞", "炸", "哲", "磷", "績", "朋", "淡", "尖", "啟", "陷", "柴", "呈", "徒", "顏", "淚", "稍", "忘", "泵", "藍", "拖", "洞", "授", "鏡", "辛", "壯", "鋒", "貧", "虛", "彎", "摩", "泰", "幼", "廷", "尊", "窗", "綱", "弄", "隸", "疑", "氏", "宮", "姐", "震", "瑞", "怪", "尤", "琴", "循", "描", "膜", "違", "夾", "腰", "緣", "珠", "窮", "森", "枝", "竹", "溝", "催", "繩", "憶", "邦", "剩", "幸", "漿", "欄", "擁", "牙", "貯", "禮", "濾", "鈉", "紋", "罷", "拍", "咱", "喊", "袖", "埃", "勤", "罰", "焦", "潛", "伍", "墨", "欲", "縫", "姓", "刊", "飽", "仿", "獎", "鋁", "鬼", "麗", "跨", "默", "挖", "鏈", "掃", "喝", "袋", "炭", "污", "幕", "諸", "弧", "勵", "梅", "奶", "潔", "災", "舟", "鑑", "苯", "訟", "抱", "毀", "懂", "寒", "智", "埔", "寄", "屆", "躍", "渡", "挑", "丹", "艱", "貝", "碰", "拔", "爹", "戴", "碼", "夢", "芽", "熔", "赤", "漁", "哭", "敬", "顆", "奔", "鉛", "仲", "虎", "稀", "妹", "乏", "珍", "申", "桌", "遵", "允", "隆", "螺", "倉", "魏", "銳", "曉", "氮", "兼", "隱", "礙", "赫", "撥", "忠", "肅", "缸", "牽", "搶", "博", "巧", "殼", "兄", "杜", "訊", "誠", "碧", "祥", "柯", "頁", "巡", "矩", "悲", "灌", "齡", "倫", "票", "尋", "桂", "鋪", "聖", "恐", "恰", "鄭", "趣", "抬", "荒", "騰", "貼", "柔", "滴", "猛", "闊", "輛", "妻", "填", "撤", "儲", "簽", "鬧", "擾", "紫", "砂", "遞", "戲", "吊", "陶", "伐", "餵", "療", "瓶", "婆", "撫", "臂", "摸", "忍", "蝦", "蠟", "鄰", "胸", "鞏", "擠", "偶", "棄", "槽", "勁", "乳", "鄧", "吉", "仁", "爛", "磚", "租", "烏", "艦", "伴", "瓜", "淺", "丙", "暫", "燥", "橡", "柳", "迷", "暖", "牌", "秧", "膽", "詳", "簧", "踏", "瓷", "譜", "呆", "賓", "糊", "洛", "輝", "憤", "競", "隙", "怒", "粘", "乃", "緒", "肩", "籍", "敏", "塗", "熙", "皆", "偵", "懸", "掘", "享", "糾", "醒", "狂", "鎖", "淀", "恨", "牲", "霸", "爬", "賞", "逆", "玩", "陵", "祝", "秒", "浙", "貌", "役", "彼", "悉", "鴨", "趨", "鳳", "晨", "畜", "輩", "秩", "卵", "署", "梯", "炎", "灘", "棋", "驅", "篩", "峽", "冒", "啥", "壽", "譯", "浸", "泉", "帽", "遲", "矽", "疆", "貸", "漏", "稿", "冠", "嫩", "脅", "芯", "牢", "叛", "蝕", "奧", "鳴", "嶺", "羊", "憑", "串", "塘", "繪", "酵", "融", "盆", "錫", "廟", "籌", "凍", "輔", "攝", "襲", "筋", "拒", "僚", "旱", "鉀", "鳥", "漆", "沈", "眉", "疏", "添", "棒", "穗", "硝", "韓", "逼", "扭", "僑", "涼", "挺", "碗", "栽", "炒", "杯", "患", "餾", "勸", "豪", "遼", "勃", "鴻", "旦", "吏", "拜", "狗", "埋", "輥", "掩", "飲", "搬", "罵", "辭", "勾", "扣", "估", "蔣", "絨", "霧", "丈", "朵", "姆", "擬", "宇", "輯", "陝", "雕", "償", "蓄", "崇", "剪", "倡", "廳", "咬", "駛", "薯", "刷", "斥", "番", "賦", "奉", "佛", "澆", "漫", "曼", "扇", "鈣", "桃", "扶", "仔", "返", "俗", "虧", "腔", "鞋", "棱", "覆", "框", "悄", "叔", "撞", "騙", "勘", "旺", "沸", "孤", "吐", "孟", "渠", "屈", "疾", "妙", "惜", "仰", "狠", "脹", "諧", "拋", "黴", "桑", "崗", "嘛", "衰", "盜", "滲", "臟", "賴", "湧", "甜", "曹", "閱", "肌", "哩", "厲", "烴", "緯", "毅", "昨", "偽", "症", "煮", "嘆", "釘", "搭", "莖", "籠", "酷", "偷", "弓", "錐", "恆", "傑", "坑", "鼻", "翼", "綸", "敘", "獄", "逮", "罐", "絡", "棚", "抑", "膨", "蔬", "寺", "驟", "穆", "冶", "枯", "冊", "屍", "凸", "紳", "坯", "犧", "焰", "轟", "欣", "晉", "瘦", "禦", "錠", "錦", "喪", "旬", "鍛", "壟", "搜", "撲", "邀", "亭", "酯", "邁", "舒", "脆", "酶", "閒", "憂", "酚", "頑", "羽", "漲", "卸", "仗", "陪", "闢", "懲", "杭", "姚", "肚", "捉", "飄", "漂", "昆", "欺", "吾", "郎", "烷", "汁", "呵", "飾", "蕭", "雅", "郵", "遷", "燕", "撒", "姻", "赴", "宴", "煩", "債", "帳", "斑", "鈴", "旨", "醇", "董", "餅", "雛", "姿", "拌", "傅", "腹", "妥", "揉", "賢", "拆", "歪", "葡", "胺", "丟", "浩", "徽", "昂", "墊", "擋", "覽", "貪", "慰", "繳", "汪", "慌", "馮", "諾", "姜", "誼", "兇", "劣", "誣", "耀", "昏", "躺", "盈", "騎", "喬", "溪", "叢", "盧", "抹", "悶", "諮", "刮", "駕", "纜", "悟", "摘", "鉺", "擲", "頗", "幻", "柄", "惠", "慘", "佳", "仇", "臘", "窩", "滌", "劍", "瞧", "堡", "潑", "蔥", "罩", "霍", "撈", "胎", "蒼", "濱", "倆", "捅", "湘", "砍", "霞", "邵", "萄", "瘋", "淮", "遂", "熊", "糞", "烘", "宿", "檔", "戈", "駁", "嫂", "裕", "徙", "箭", "捐", "腸", "撐", "曬", "辨", "殿", "蓮", "攤", "攪", "醬", "屏", "疫", "哀", "蔡", "堵", "沫", "皺", "暢", "疊", "閣", "萊", "敲", "轄", "鉤", "痕", "壩", "巷", "餓", "禍", "丘", "玄", "溜", "曰", "邏", "彭", "嘗", "卿", "妨", "艇", "吞", "韋", "怨", "矮", "歇"], + "FRENCH": ["abaisser", "abandon", "abdiquer", "abeille", "abolir", "aborder", "aboutir", "aboyer", "abrasif", "abreuver", "abriter", "abroger", "abrupt", "absence", "absolu", "absurde", "abusif", "abyssal", "académie", "acajou", "acarien", "accabler", "accepter", "acclamer", "accolade", "accroche", "accuser", "acerbe", "achat", "acheter", "aciduler", "acier", "acompte", "acquérir", "acronyme", "acteur", "actif", "actuel", "adepte", "adéquat", "adhésif", "adjectif", "adjuger", "admettre", "admirer", "adopter", "adorer", "adoucir", "adresse", "adroit", "adulte", "adverbe", "aérer", "aéronef", "affaire", "affecter", "affiche", "affreux", "affubler", "agacer", "agencer", "agile", "agiter", "agrafer", "agréable", "agrume", "aider", "aiguille", "ailier", "aimable", "aisance", "ajouter", "ajuster", "alarmer", "alchimie", "alerte", "algèbre", "algue", "aliéner", "aliment", "alléger", "alliage", "allouer", "allumer", "alourdir", "alpaga", "altesse", "alvéole", "amateur", "ambigu", "ambre", "aménager", "amertume", "amidon", "amiral", "amorcer", "amour", "amovible", "amphibie", "ampleur", "amusant", "analyse", "anaphore", "anarchie", "anatomie", "ancien", "anéantir", "angle", "angoisse", "anguleux", "animal", "annexer", "annonce", "annuel", "anodin", "anomalie", "anonyme", "anormal", "antenne", "antidote", "anxieux", "apaiser", "apéritif", "aplanir", "apologie", "appareil", "appeler", "apporter", "appuyer", "aquarium", "aqueduc", "arbitre", "arbuste", "ardeur", "ardoise", "argent", "arlequin", "armature", "armement", "armoire", "armure", "arpenter", "arracher", "arriver", "arroser", "arsenic", "artériel", "article", "aspect", "asphalte", "aspirer", "assaut", "asservir", "assiette", "associer", "assurer", "asticot", "astre", "astuce", "atelier", "atome", "atrium", "atroce", "attaque", "attentif", "attirer", "attraper", "aubaine", "auberge", "audace", "audible", "augurer", "aurore", "automne", "autruche", "avaler", "avancer", "avarice", "avenir", "averse", "aveugle", "aviateur", "avide", "avion", "aviser", "avoine", "avouer", "avril", "axial", "axiome", "badge", "bafouer", "bagage", "baguette", "baignade", "balancer", "balcon", "baleine", "balisage", "bambin", "bancaire", "bandage", "banlieue", "bannière", "banquier", "barbier", "baril", "baron", "barque", "barrage", "bassin", "bastion", "bataille", "bateau", "batterie", "baudrier", "bavarder", "belette", "bélier", "belote", "bénéfice", "berceau", "berger", "berline", "bermuda", "besace", "besogne", "bétail", "beurre", "biberon", "bicycle", "bidule", "bijou", "bilan", "bilingue", "billard", "binaire", "biologie", "biopsie", "biotype", "biscuit", "bison", "bistouri", "bitume", "bizarre", "blafard", "blague", "blanchir", "blessant", "blinder", "blond", "bloquer", "blouson", "bobard", "bobine", "boire", "boiser", "bolide", "bonbon", "bondir", "bonheur", "bonifier", "bonus", "bordure", "borne", "botte", "boucle", "boueux", "bougie", "boulon", "bouquin", "bourse", "boussole", "boutique", "boxeur", "branche", "brasier", "brave", "brebis", "brèche", "breuvage", "bricoler", "brigade", "brillant", "brioche", "brique", "brochure", "broder", "bronzer", "brousse", "broyeur", "brume", "brusque", "brutal", "bruyant", "buffle", "buisson", "bulletin", "bureau", "burin", "bustier", "butiner", "butoir", "buvable", "buvette", "cabanon", "cabine", "cachette", "cadeau", "cadre", "caféine", "caillou", "caisson", "calculer", "calepin", "calibre", "calmer", "calomnie", "calvaire", "camarade", "caméra", "camion", "campagne", "canal", "caneton", "canon", "cantine", "canular", "capable", "caporal", "caprice", "capsule", "capter", "capuche", "carabine", "carbone", "caresser", "caribou", "carnage", "carotte", "carreau", "carton", "cascade", "casier", "casque", "cassure", "causer", "caution", "cavalier", "caverne", "caviar", "cédille", "ceinture", "céleste", "cellule", "cendrier", "censurer", "central", "cercle", "cérébral", "cerise", "cerner", "cerveau", "cesser", "chagrin", "chaise", "chaleur", "chambre", "chance", "chapitre", "charbon", "chasseur", "chaton", "chausson", "chavirer", "chemise", "chenille", "chéquier", "chercher", "cheval", "chien", "chiffre", "chignon", "chimère", "chiot", "chlorure", "chocolat", "choisir", "chose", "chouette", "chrome", "chute", "cigare", "cigogne", "cimenter", "cinéma", "cintrer", "circuler", "cirer", "cirque", "citerne", "citoyen", "citron", "civil", "clairon", "clameur", "claquer", "classe", "clavier", "client", "cligner", "climat", "clivage", "cloche", "clonage", "cloporte", "cobalt", "cobra", "cocasse", "cocotier", "coder", "codifier", "coffre", "cogner", "cohésion", "coiffer", "coincer", "colère", "colibri", "colline", "colmater", "colonel", "combat", "comédie", "commande", "compact", "concert", "conduire", "confier", "congeler", "connoter", "consonne", "contact", "convexe", "copain", "copie", "corail", "corbeau", "cordage", "corniche", "corpus", "correct", "cortège", "cosmique", "costume", "coton", "coude", "coupure", "courage", "couteau", "couvrir", "coyote", "crabe", "crainte", "cravate", "crayon", "créature", "créditer", "crémeux", "creuser", "crevette", "cribler", "crier", "cristal", "critère", "croire", "croquer", "crotale", "crucial", "cruel", "crypter", "cubique", "cueillir", "cuillère", "cuisine", "cuivre", "culminer", "cultiver", "cumuler", "cupide", "curatif", "curseur", "cyanure", "cycle", "cylindre", "cynique", "daigner", "damier", "danger", "danseur", "dauphin", "débattre", "débiter", "déborder", "débrider", "débutant", "décaler", "décembre", "déchirer", "décider", "déclarer", "décorer", "décrire", "décupler", "dédale", "déductif", "déesse", "défensif", "défiler", "défrayer", "dégager", "dégivrer", "déglutir", "dégrafer", "déjeuner", "délice", "déloger", "demander", "demeurer", "démolir", "dénicher", "dénouer", "dentelle", "dénuder", "départ", "dépenser", "déphaser", "déplacer", "déposer", "déranger", "dérober", "désastre", "descente", "désert", "désigner", "désobéir", "dessiner", "destrier", "détacher", "détester", "détourer", "détresse", "devancer", "devenir", "deviner", "devoir", "diable", "dialogue", "diamant", "dicter", "différer", "digérer", "digital", "digne", "diluer", "dimanche", "diminuer", "dioxyde", "directif", "diriger", "discuter", "disposer", "dissiper", "distance", "divertir", "diviser", "docile", "docteur", "dogme", "doigt", "domaine", "domicile", "dompter", "donateur", "donjon", "donner", "dopamine", "dortoir", "dorure", "dosage", "doseur", "dossier", "dotation", "douanier", "double", "douceur", "douter", "doyen", "dragon", "draper", "dresser", "dribbler", "droiture", "duperie", "duplexe", "durable", "durcir", "dynastie", "éblouir", "écarter", "écharpe", "échelle", "éclairer", "éclipse", "éclore", "écluse", "école", "économie", "écorce", "écouter", "écraser", "écrémer", "écrivain", "écrou", "écume", "écureuil", "édifier", "éduquer", "effacer", "effectif", "effigie", "effort", "effrayer", "effusion", "égaliser", "égarer", "éjecter", "élaborer", "élargir", "électron", "élégant", "éléphant", "élève", "éligible", "élitisme", "éloge", "élucider", "éluder", "emballer", "embellir", "embryon", "émeraude", "émission", "emmener", "émotion", "émouvoir", "empereur", "employer", "emporter", "emprise", "émulsion", "encadrer", "enchère", "enclave", "encoche", "endiguer", "endosser", "endroit", "enduire", "énergie", "enfance", "enfermer", "enfouir", "engager", "engin", "englober", "énigme", "enjamber", "enjeu", "enlever", "ennemi", "ennuyeux", "enrichir", "enrobage", "enseigne", "entasser", "entendre", "entier", "entourer", "entraver", "énumérer", "envahir", "enviable", "envoyer", "enzyme", "éolien", "épaissir", "épargne", "épatant", "épaule", "épicerie", "épidémie", "épier", "épilogue", "épine", "épisode", "épitaphe", "époque", "épreuve", "éprouver", "épuisant", "équerre", "équipe", "ériger", "érosion", "erreur", "éruption", "escalier", "espadon", "espèce", "espiègle", "espoir", "esprit", "esquiver", "essayer", "essence", "essieu", "essorer", "estime", "estomac", "estrade", "étagère", "étaler", "étanche", "étatique", "éteindre", "étendoir", "éternel", "éthanol", "éthique", "ethnie", "étirer", "étoffer", "étoile", "étonnant", "étourdir", "étrange", "étroit", "étude", "euphorie", "évaluer", "évasion", "éventail", "évidence", "éviter", "évolutif", "évoquer", "exact", "exagérer", "exaucer", "exceller", "excitant", "exclusif", "excuse", "exécuter", "exemple", "exercer", "exhaler", "exhorter", "exigence", "exiler", "exister", "exotique", "expédier", "explorer", "exposer", "exprimer", "exquis", "extensif", "extraire", "exulter", "fable", "fabuleux", "facette", "facile", "facture", "faiblir", "falaise", "fameux", "famille", "farceur", "farfelu", "farine", "farouche", "fasciner", "fatal", "fatigue", "faucon", "fautif", "faveur", "favori", "fébrile", "féconder", "fédérer", "félin", "femme", "fémur", "fendoir", "féodal", "fermer", "féroce", "ferveur", "festival", "feuille", "feutre", "février", "fiasco", "ficeler", "fictif", "fidèle", "figure", "filature", "filetage", "filière", "filleul", "filmer", "filou", "filtrer", "financer", "finir", "fiole", "firme", "fissure", "fixer", "flairer", "flamme", "flasque", "flatteur", "fléau", "flèche", "fleur", "flexion", "flocon", "flore", "fluctuer", "fluide", "fluvial", "folie", "fonderie", "fongible", "fontaine", "forcer", "forgeron", "formuler", "fortune", "fossile", "foudre", "fougère", "fouiller", "foulure", "fourmi", "fragile", "fraise", "franchir", "frapper", "frayeur", "frégate", "freiner", "frelon", "frémir", "frénésie", "frère", "friable", "friction", "frisson", "frivole", "froid", "fromage", "frontal", "frotter", "fruit", "fugitif", "fuite", "fureur", "furieux", "furtif", "fusion", "futur", "gagner", "galaxie", "galerie", "gambader", "garantir", "gardien", "garnir", "garrigue", "gazelle", "gazon", "géant", "gélatine", "gélule", "gendarme", "général", "génie", "genou", "gentil", "géologie", "géomètre", "géranium", "germe", "gestuel", "geyser", "gibier", "gicler", "girafe", "givre", "glace", "glaive", "glisser", "globe", "gloire", "glorieux", "golfeur", "gomme", "gonfler", "gorge", "gorille", "goudron", "gouffre", "goulot", "goupille", "gourmand", "goutte", "graduel", "graffiti", "graine", "grand", "grappin", "gratuit", "gravir", "grenat", "griffure", "griller", "grimper", "grogner", "gronder", "grotte", "groupe", "gruger", "grutier", "gruyère", "guépard", "guerrier", "guide", "guimauve", "guitare", "gustatif", "gymnaste", "gyrostat", "habitude", "hachoir", "halte", "hameau", "hangar", "hanneton", "haricot", "harmonie", "harpon", "hasard", "hélium", "hématome", "herbe", "hérisson", "hermine", "héron", "hésiter", "heureux", "hiberner", "hibou", "hilarant", "histoire", "hiver", "homard", "hommage", "homogène", "honneur", "honorer", "honteux", "horde", "horizon", "horloge", "hormone", "horrible", "houleux", "housse", "hublot", "huileux", "humain", "humble", "humide", "humour", "hurler", "hydromel", "hygiène", "hymne", "hypnose", "idylle", "ignorer", "iguane", "illicite", "illusion", "image", "imbiber", "imiter", "immense", "immobile", "immuable", "impact", "impérial", "implorer", "imposer", "imprimer", "imputer", "incarner", "incendie", "incident", "incliner", "incolore", "indexer", "indice", "inductif", "inédit", "ineptie", "inexact", "infini", "infliger", "informer", "infusion", "ingérer", "inhaler", "inhiber", "injecter", "injure", "innocent", "inoculer", "inonder", "inscrire", "insecte", "insigne", "insolite", "inspirer", "instinct", "insulter", "intact", "intense", "intime", "intrigue", "intuitif", "inutile", "invasion", "inventer", "inviter", "invoquer", "ironique", "irradier", "irréel", "irriter", "isoler", "ivoire", "ivresse", "jaguar", "jaillir", "jambe", "janvier", "jardin", "jauger", "jaune", "javelot", "jetable", "jeton", "jeudi", "jeunesse", "joindre", "joncher", "jongler", "joueur", "jouissif", "journal", "jovial", "joyau", "joyeux", "jubiler", "jugement", "junior", "jupon", "juriste", "justice", "juteux", "juvénile", "kayak", "kimono", "kiosque", "label", "labial", "labourer", "lacérer", "lactose", "lagune", "laine", "laisser", "laitier", "lambeau", "lamelle", "lampe", "lanceur", "langage", "lanterne", "lapin", "largeur", "larme", "laurier", "lavabo", "lavoir", "lecture", "légal", "léger", "légume", "lessive", "lettre", "levier", "lexique", "lézard", "liasse", "libérer", "libre", "licence", "licorne", "liège", "lièvre", "ligature", "ligoter", "ligue", "limer", "limite", "limonade", "limpide", "linéaire", "lingot", "lionceau", "liquide", "lisière", "lister", "lithium", "litige", "littoral", "livreur", "logique", "lointain", "loisir", "lombric", "loterie", "louer", "lourd", "loutre", "louve", "loyal", "lubie", "lucide", "lucratif", "lueur", "lugubre", "luisant", "lumière", "lunaire", "lundi", "luron", "lutter", "luxueux", "machine", "magasin", "magenta", "magique", "maigre", "maillon", "maintien", "mairie", "maison", "majorer", "malaxer", "maléfice", "malheur", "malice", "mallette", "mammouth", "mandater", "maniable", "manquant", "manteau", "manuel", "marathon", "marbre", "marchand", "mardi", "maritime", "marqueur", "marron", "marteler", "mascotte", "massif", "matériel", "matière", "matraque", "maudire", "maussade", "mauve", "maximal", "méchant", "méconnu", "médaille", "médecin", "méditer", "méduse", "meilleur", "mélange", "mélodie", "membre", "mémoire", "menacer", "mener", "menhir", "mensonge", "mentor", "mercredi", "mérite", "merle", "messager", "mesure", "métal", "météore", "méthode", "métier", "meuble", "miauler", "microbe", "miette", "mignon", "migrer", "milieu", "million", "mimique", "mince", "minéral", "minimal", "minorer", "minute", "miracle", "miroiter", "missile", "mixte", "mobile", "moderne", "moelleux", "mondial", "moniteur", "monnaie", "monotone", "monstre", "montagne", "monument", "moqueur", "morceau", "morsure", "mortier", "moteur", "motif", "mouche", "moufle", "moulin", "mousson", "mouton", "mouvant", "multiple", "munition", "muraille", "murène", "murmure", "muscle", "muséum", "musicien", "mutation", "muter", "mutuel", "myriade", "myrtille", "mystère", "mythique", "nageur", "nappe", "narquois", "narrer", "natation", "nation", "nature", "naufrage", "nautique", "navire", "nébuleux", "nectar", "néfaste", "négation", "négliger", "négocier", "neige", "nerveux", "nettoyer", "neurone", "neutron", "neveu", "niche", "nickel", "nitrate", "niveau", "noble", "nocif", "nocturne", "noirceur", "noisette", "nomade", "nombreux", "nommer", "normatif", "notable", "notifier", "notoire", "nourrir", "nouveau", "novateur", "novembre", "novice", "nuage", "nuancer", "nuire", "nuisible", "numéro", "nuptial", "nuque", "nutritif", "obéir", "objectif", "obliger", "obscur", "observer", "obstacle", "obtenir", "obturer", "occasion", "occuper", "océan", "octobre", "octroyer", "octupler", "oculaire", "odeur", "odorant", "offenser", "officier", "offrir", "ogive", "oiseau", "oisillon", "olfactif", "olivier", "ombrage", "omettre", "onctueux", "onduler", "onéreux", "onirique", "opale", "opaque", "opérer", "opinion", "opportun", "opprimer", "opter", "optique", "orageux", "orange", "orbite", "ordonner", "oreille", "organe", "orgueil", "orifice", "ornement", "orque", "ortie", "osciller", "osmose", "ossature", "otarie", "ouragan", "ourson", "outil", "outrager", "ouvrage", "ovation", "oxyde", "oxygène", "ozone", "paisible", "palace", "palmarès", "palourde", "palper", "panache", "panda", "pangolin", "paniquer", "panneau", "panorama", "pantalon", "papaye", "papier", "papoter", "papyrus", "paradoxe", "parcelle", "paresse", "parfumer", "parler", "parole", "parrain", "parsemer", "partager", "parure", "parvenir", "passion", "pastèque", "paternel", "patience", "patron", "pavillon", "pavoiser", "payer", "paysage", "peigne", "peintre", "pelage", "pélican", "pelle", "pelouse", "peluche", "pendule", "pénétrer", "pénible", "pensif", "pénurie", "pépite", "péplum", "perdrix", "perforer", "période", "permuter", "perplexe", "persil", "perte", "peser", "pétale", "petit", "pétrir", "peuple", "pharaon", "phobie", "phoque", "photon", "phrase", "physique", "piano", "pictural", "pièce", "pierre", "pieuvre", "pilote", "pinceau", "pipette", "piquer", "pirogue", "piscine", "piston", "pivoter", "pixel", "pizza", "placard", "plafond", "plaisir", "planer", "plaque", "plastron", "plateau", "pleurer", "plexus", "pliage", "plomb", "plonger", "pluie", "plumage", "pochette", "poésie", "poète", "pointe", "poirier", "poisson", "poivre", "polaire", "policier", "pollen", "polygone", "pommade", "pompier", "ponctuel", "pondérer", "poney", "portique", "position", "posséder", "posture", "potager", "poteau", "potion", "pouce", "poulain", "poumon", "pourpre", "poussin", "pouvoir", "prairie", "pratique", "précieux", "prédire", "préfixe", "prélude", "prénom", "présence", "prétexte", "prévoir", "primitif", "prince", "prison", "priver", "problème", "procéder", "prodige", "profond", "progrès", "proie", "projeter", "prologue", "promener", "propre", "prospère", "protéger", "prouesse", "proverbe", "prudence", "pruneau", "psychose", "public", "puceron", "puiser", "pulpe", "pulsar", "punaise", "punitif", "pupitre", "purifier", "puzzle", "pyramide", "quasar", "querelle", "question", "quiétude", "quitter", "quotient", "racine", "raconter", "radieux", "ragondin", "raideur", "raisin", "ralentir", "rallonge", "ramasser", "rapide", "rasage", "ratisser", "ravager", "ravin", "rayonner", "réactif", "réagir", "réaliser", "réanimer", "recevoir", "réciter", "réclamer", "récolter", "recruter", "reculer", "recycler", "rédiger", "redouter", "refaire", "réflexe", "réformer", "refrain", "refuge", "régalien", "région", "réglage", "régulier", "réitérer", "rejeter", "rejouer", "relatif", "relever", "relief", "remarque", "remède", "remise", "remonter", "remplir", "remuer", "renard", "renfort", "renifler", "renoncer", "rentrer", "renvoi", "replier", "reporter", "reprise", "reptile", "requin", "réserve", "résineux", "résoudre", "respect", "rester", "résultat", "rétablir", "retenir", "réticule", "retomber", "retracer", "réunion", "réussir", "revanche", "revivre", "révolte", "révulsif", "richesse", "rideau", "rieur", "rigide", "rigoler", "rincer", "riposter", "risible", "risque", "rituel", "rival", "rivière", "rocheux", "romance", "rompre", "ronce", "rondin", "roseau", "rosier", "rotatif", "rotor", "rotule", "rouge", "rouille", "rouleau", "routine", "royaume", "ruban", "rubis", "ruche", "ruelle", "rugueux", "ruiner", "ruisseau", "ruser", "rustique", "rythme", "sabler", "saboter", "sabre", "sacoche", "safari", "sagesse", "saisir", "salade", "salive", "salon", "saluer", "samedi", "sanction", "sanglier", "sarcasme", "sardine", "saturer", "saugrenu", "saumon", "sauter", "sauvage", "savant", "savonner", "scalpel", "scandale", "scélérat", "scénario", "sceptre", "schéma", "science", "scinder", "score", "scrutin", "sculpter", "séance", "sécable", "sécher", "secouer", "sécréter", "sédatif", "séduire", "seigneur", "séjour", "sélectif", "semaine", "sembler", "semence", "séminal", "sénateur", "sensible", "sentence", "séparer", "séquence", "serein", "sergent", "sérieux", "serrure", "sérum", "service", "sésame", "sévir", "sevrage", "sextuple", "sidéral", "siècle", "siéger", "siffler", "sigle", "signal", "silence", "silicium", "simple", "sincère", "sinistre", "siphon", "sirop", "sismique", "situer", "skier", "social", "socle", "sodium", "soigneux", "soldat", "soleil", "solitude", "soluble", "sombre", "sommeil", "somnoler", "sonde", "songeur", "sonnette", "sonore", "sorcier", "sortir", "sosie", "sottise", "soucieux", "soudure", "souffle", "soulever", "soupape", "source", "soutirer", "souvenir", "spacieux", "spatial", "spécial", "sphère", "spiral", "stable", "station", "sternum", "stimulus", "stipuler", "strict", "studieux", "stupeur", "styliste", "sublime", "substrat", "subtil", "subvenir", "succès", "sucre", "suffixe", "suggérer", "suiveur", "sulfate", "superbe", "supplier", "surface", "suricate", "surmener", "surprise", "sursaut", "survie", "suspect", "syllabe", "symbole", "symétrie", "synapse", "syntaxe", "système", "tabac", "tablier", "tactile", "tailler", "talent", "talisman", "talonner", "tambour", "tamiser", "tangible", "tapis", "taquiner", "tarder", "tarif", "tartine", "tasse", "tatami", "tatouage", "taupe", "taureau", "taxer", "témoin", "temporel", "tenaille", "tendre", "teneur", "tenir", "tension", "terminer", "terne", "terrible", "tétine", "texte", "thème", "théorie", "thérapie", "thorax", "tibia", "tiède", "timide", "tirelire", "tiroir", "tissu", "titane", "titre", "tituber", "toboggan", "tolérant", "tomate", "tonique", "tonneau", "toponyme", "torche", "tordre", "tornade", "torpille", "torrent", "torse", "tortue", "totem", "toucher", "tournage", "tousser", "toxine", "traction", "trafic", "tragique", "trahir", "train", "trancher", "travail", "trèfle", "tremper", "trésor", "treuil", "triage", "tribunal", "tricoter", "trilogie", "triomphe", "tripler", "triturer", "trivial", "trombone", "tronc", "tropical", "troupeau", "tuile", "tulipe", "tumulte", "tunnel", "turbine", "tuteur", "tutoyer", "tuyau", "tympan", "typhon", "typique", "tyran", "ubuesque", "ultime", "ultrason", "unanime", "unifier", "union", "unique", "unitaire", "univers", "uranium", "urbain", "urticant", "usage", "usine", "usuel", "usure", "utile", "utopie", "vacarme", "vaccin", "vagabond", "vague", "vaillant", "vaincre", "vaisseau", "valable", "valise", "vallon", "valve", "vampire", "vanille", "vapeur", "varier", "vaseux", "vassal", "vaste", "vecteur", "vedette", "végétal", "véhicule", "veinard", "véloce", "vendredi", "vénérer", "venger", "venimeux", "ventouse", "verdure", "vérin", "vernir", "verrou", "verser", "vertu", "veston", "vétéran", "vétuste", "vexant", "vexer", "viaduc", "viande", "victoire", "vidange", "vidéo", "vignette", "vigueur", "vilain", "village", "vinaigre", "violon", "vipère", "virement", "virtuose", "virus", "visage", "viseur", "vision", "visqueux", "visuel", "vital", "vitesse", "viticole", "vitrine", "vivace", "vivipare", "vocation", "voguer", "voile", "voisin", "voiture", "volaille", "volcan", "voltiger", "volume", "vorace", "vortex", "voter", "vouloir", "voyage", "voyelle", "wagon", "xénon", "yacht", "zèbre", "zénith", "zeste", "zoologie"], + "ITALIAN": ["abaco", "abbaglio", "abbinato", "abete", "abisso", "abolire", "abrasivo", "abrogato", "accadere", "accenno", "accusato", "acetone", "achille", "acido", "acqua", "acre", "acrilico", "acrobata", "acuto", "adagio", "addebito", "addome", "adeguato", "aderire", "adipe", "adottare", "adulare", "affabile", "affetto", "affisso", "affranto", "aforisma", "afoso", "africano", "agave", "agente", "agevole", "aggancio", "agire", "agitare", "agonismo", "agricolo", "agrumeto", "aguzzo", "alabarda", "alato", "albatro", "alberato", "albo", "albume", "alce", "alcolico", "alettone", "alfa", "algebra", "aliante", "alibi", "alimento", "allagato", "allegro", "allievo", "allodola", "allusivo", "almeno", "alogeno", "alpaca", "alpestre", "altalena", "alterno", "alticcio", "altrove", "alunno", "alveolo", "alzare", "amalgama", "amanita", "amarena", "ambito", "ambrato", "ameba", "america", "ametista", "amico", "ammasso", "ammenda", "ammirare", "ammonito", "amore", "ampio", "ampliare", "amuleto", "anacardo", "anagrafe", "analista", "anarchia", "anatra", "anca", "ancella", "ancora", "andare", "andrea", "anello", "angelo", "angolare", "angusto", "anima", "annegare", "annidato", "anno", "annuncio", "anonimo", "anticipo", "anzi", "apatico", "apertura", "apode", "apparire", "appetito", "appoggio", "approdo", "appunto", "aprile", "arabica", "arachide", "aragosta", "araldica", "arancio", "aratura", "arazzo", "arbitro", "archivio", "ardito", "arenile", "argento", "argine", "arguto", "aria", "armonia", "arnese", "arredato", "arringa", "arrosto", "arsenico", "arso", "artefice", "arzillo", "asciutto", "ascolto", "asepsi", "asettico", "asfalto", "asino", "asola", "aspirato", "aspro", "assaggio", "asse", "assoluto", "assurdo", "asta", "astenuto", "astice", "astratto", "atavico", "ateismo", "atomico", "atono", "attesa", "attivare", "attorno", "attrito", "attuale", "ausilio", "austria", "autista", "autonomo", "autunno", "avanzato", "avere", "avvenire", "avviso", "avvolgere", "azione", "azoto", "azzimo", "azzurro", "babele", "baccano", "bacino", "baco", "badessa", "badilata", "bagnato", "baita", "balcone", "baldo", "balena", "ballata", "balzano", "bambino", "bandire", "baraonda", "barbaro", "barca", "baritono", "barlume", "barocco", "basilico", "basso", "batosta", "battuto", "baule", "bava", "bavosa", "becco", "beffa", "belgio", "belva", "benda", "benevole", "benigno", "benzina", "bere", "berlina", "beta", "bibita", "bici", "bidone", "bifido", "biga", "bilancia", "bimbo", "binocolo", "biologo", "bipede", "bipolare", "birbante", "birra", "biscotto", "bisesto", "bisnonno", "bisonte", "bisturi", "bizzarro", "blando", "blatta", "bollito", "bonifico", "bordo", "bosco", "botanico", "bottino", "bozzolo", "braccio", "bradipo", "brama", "branca", "bravura", "bretella", "brevetto", "brezza", "briglia", "brillante", "brindare", "broccolo", "brodo", "bronzina", "brullo", "bruno", "bubbone", "buca", "budino", "buffone", "buio", "bulbo", "buono", "burlone", "burrasca", "bussola", "busta", "cadetto", "caduco", "calamaro", "calcolo", "calesse", "calibro", "calmo", "caloria", "cambusa", "camerata", "camicia", "cammino", "camola", "campale", "canapa", "candela", "cane", "canino", "canotto", "cantina", "capace", "capello", "capitolo", "capogiro", "cappero", "capra", "capsula", "carapace", "carcassa", "cardo", "carisma", "carovana", "carretto", "cartolina", "casaccio", "cascata", "caserma", "caso", "cassone", "castello", "casuale", "catasta", "catena", "catrame", "cauto", "cavillo", "cedibile", "cedrata", "cefalo", "celebre", "cellulare", "cena", "cenone", "centesimo", "ceramica", "cercare", "certo", "cerume", "cervello", "cesoia", "cespo", "ceto", "chela", "chiaro", "chicca", "chiedere", "chimera", "china", "chirurgo", "chitarra", "ciao", "ciclismo", "cifrare", "cigno", "cilindro", "ciottolo", "circa", "cirrosi", "citrico", "cittadino", "ciuffo", "civetta", "civile", "classico", "clinica", "cloro", "cocco", "codardo", "codice", "coerente", "cognome", "collare", "colmato", "colore", "colposo", "coltivato", "colza", "coma", "cometa", "commando", "comodo", "computer", "comune", "conciso", "condurre", "conferma", "congelare", "coniuge", "connesso", "conoscere", "consumo", "continuo", "convegno", "coperto", "copione", "coppia", "copricapo", "corazza", "cordata", "coricato", "cornice", "corolla", "corpo", "corredo", "corsia", "cortese", "cosmico", "costante", "cottura", "covato", "cratere", "cravatta", "creato", "credere", "cremoso", "crescita", "creta", "criceto", "crinale", "crisi", "critico", "croce", "cronaca", "crostata", "cruciale", "crusca", "cucire", "cuculo", "cugino", "cullato", "cupola", "curatore", "cursore", "curvo", "cuscino", "custode", "dado", "daino", "dalmata", "damerino", "daniela", "dannoso", "danzare", "datato", "davanti", "davvero", "debutto", "decennio", "deciso", "declino", "decollo", "decreto", "dedicato", "definito", "deforme", "degno", "delegare", "delfino", "delirio", "delta", "demenza", "denotato", "dentro", "deposito", "derapata", "derivare", "deroga", "descritto", "deserto", "desiderio", "desumere", "detersivo", "devoto", "diametro", "dicembre", "diedro", "difeso", "diffuso", "digerire", "digitale", "diluvio", "dinamico", "dinnanzi", "dipinto", "diploma", "dipolo", "diradare", "dire", "dirotto", "dirupo", "disagio", "discreto", "disfare", "disgelo", "disposto", "distanza", "disumano", "dito", "divano", "divelto", "dividere", "divorato", "doblone", "docente", "doganale", "dogma", "dolce", "domato", "domenica", "dominare", "dondolo", "dono", "dormire", "dote", "dottore", "dovuto", "dozzina", "drago", "druido", "dubbio", "dubitare", "ducale", "duna", "duomo", "duplice", "duraturo", "ebano", "eccesso", "ecco", "eclissi", "economia", "edera", "edicola", "edile", "editoria", "educare", "egemonia", "egli", "egoismo", "egregio", "elaborato", "elargire", "elegante", "elencato", "eletto", "elevare", "elfico", "elica", "elmo", "elsa", "eluso", "emanato", "emblema", "emesso", "emiro", "emotivo", "emozione", "empirico", "emulo", "endemico", "enduro", "energia", "enfasi", "enoteca", "entrare", "enzima", "epatite", "epilogo", "episodio", "epocale", "eppure", "equatore", "erario", "erba", "erboso", "erede", "eremita", "erigere", "ermetico", "eroe", "erosivo", "errante", "esagono", "esame", "esanime", "esaudire", "esca", "esempio", "esercito", "esibito", "esigente", "esistere", "esito", "esofago", "esortato", "esoso", "espanso", "espresso", "essenza", "esso", "esteso", "estimare", "estonia", "estroso", "esultare", "etilico", "etnico", "etrusco", "etto", "euclideo", "europa", "evaso", "evidenza", "evitato", "evoluto", "evviva", "fabbrica", "faccenda", "fachiro", "falco", "famiglia", "fanale", "fanfara", "fango", "fantasma", "fare", "farfalla", "farinoso", "farmaco", "fascia", "fastoso", "fasullo", "faticare", "fato", "favoloso", "febbre", "fecola", "fede", "fegato", "felpa", "feltro", "femmina", "fendere", "fenomeno", "fermento", "ferro", "fertile", "fessura", "festivo", "fetta", "feudo", "fiaba", "fiducia", "fifa", "figurato", "filo", "finanza", "finestra", "finire", "fiore", "fiscale", "fisico", "fiume", "flacone", "flamenco", "flebo", "flemma", "florido", "fluente", "fluoro", "fobico", "focaccia", "focoso", "foderato", "foglio", "folata", "folclore", "folgore", "fondente", "fonetico", "fonia", "fontana", "forbito", "forchetta", "foresta", "formica", "fornaio", "foro", "fortezza", "forzare", "fosfato", "fosso", "fracasso", "frana", "frassino", "fratello", "freccetta", "frenata", "fresco", "frigo", "frollino", "fronde", "frugale", "frutta", "fucilata", "fucsia", "fuggente", "fulmine", "fulvo", "fumante", "fumetto", "fumoso", "fune", "funzione", "fuoco", "furbo", "furgone", "furore", "fuso", "futile", "gabbiano", "gaffe", "galateo", "gallina", "galoppo", "gambero", "gamma", "garanzia", "garbo", "garofano", "garzone", "gasdotto", "gasolio", "gastrico", "gatto", "gaudio", "gazebo", "gazzella", "geco", "gelatina", "gelso", "gemello", "gemmato", "gene", "genitore", "gennaio", "genotipo", "gergo", "ghepardo", "ghiaccio", "ghisa", "giallo", "gilda", "ginepro", "giocare", "gioiello", "giorno", "giove", "girato", "girone", "gittata", "giudizio", "giurato", "giusto", "globulo", "glutine", "gnomo", "gobba", "golf", "gomito", "gommone", "gonfio", "gonna", "governo", "gracile", "grado", "grafico", "grammo", "grande", "grattare", "gravoso", "grazia", "greca", "gregge", "grifone", "grigio", "grinza", "grotta", "gruppo", "guadagno", "guaio", "guanto", "guardare", "gufo", "guidare", "ibernato", "icona", "identico", "idillio", "idolo", "idra", "idrico", "idrogeno", "igiene", "ignaro", "ignorato", "ilare", "illeso", "illogico", "illudere", "imballo", "imbevuto", "imbocco", "imbuto", "immane", "immerso", "immolato", "impacco", "impeto", "impiego", "importo", "impronta", "inalare", "inarcare", "inattivo", "incanto", "incendio", "inchino", "incisivo", "incluso", "incontro", "incrocio", "incubo", "indagine", "india", "indole", "inedito", "infatti", "infilare", "inflitto", "ingaggio", "ingegno", "inglese", "ingordo", "ingrosso", "innesco", "inodore", "inoltrare", "inondato", "insano", "insetto", "insieme", "insonnia", "insulina", "intasato", "intero", "intonaco", "intuito", "inumidire", "invalido", "invece", "invito", "iperbole", "ipnotico", "ipotesi", "ippica", "iride", "irlanda", "ironico", "irrigato", "irrorare", "isolato", "isotopo", "isterico", "istituto", "istrice", "italia", "iterare", "labbro", "labirinto", "lacca", "lacerato", "lacrima", "lacuna", "laddove", "lago", "lampo", "lancetta", "lanterna", "lardoso", "larga", "laringe", "lastra", "latenza", "latino", "lattuga", "lavagna", "lavoro", "legale", "leggero", "lembo", "lentezza", "lenza", "leone", "lepre", "lesivo", "lessato", "lesto", "letterale", "leva", "levigato", "libero", "lido", "lievito", "lilla", "limatura", "limitare", "limpido", "lineare", "lingua", "liquido", "lira", "lirica", "lisca", "lite", "litigio", "livrea", "locanda", "lode", "logica", "lombare", "londra", "longevo", "loquace", "lorenzo", "loto", "lotteria", "luce", "lucidato", "lumaca", "luminoso", "lungo", "lupo", "luppolo", "lusinga", "lusso", "lutto", "macabro", "macchina", "macero", "macinato", "madama", "magico", "maglia", "magnete", "magro", "maiolica", "malafede", "malgrado", "malinteso", "malsano", "malto", "malumore", "mana", "mancia", "mandorla", "mangiare", "manifesto", "mannaro", "manovra", "mansarda", "mantide", "manubrio", "mappa", "maratona", "marcire", "maretta", "marmo", "marsupio", "maschera", "massaia", "mastino", "materasso", "matricola", "mattone", "maturo", "mazurca", "meandro", "meccanico", "mecenate", "medesimo", "meditare", "mega", "melassa", "melis", "melodia", "meninge", "meno", "mensola", "mercurio", "merenda", "merlo", "meschino", "mese", "messere", "mestolo", "metallo", "metodo", "mettere", "miagolare", "mica", "micelio", "michele", "microbo", "midollo", "miele", "migliore", "milano", "milite", "mimosa", "minerale", "mini", "minore", "mirino", "mirtillo", "miscela", "missiva", "misto", "misurare", "mitezza", "mitigare", "mitra", "mittente", "mnemonico", "modello", "modifica", "modulo", "mogano", "mogio", "mole", "molosso", "monastero", "monco", "mondina", "monetario", "monile", "monotono", "monsone", "montato", "monviso", "mora", "mordere", "morsicato", "mostro", "motivato", "motosega", "motto", "movenza", "movimento", "mozzo", "mucca", "mucosa", "muffa", "mughetto", "mugnaio", "mulatto", "mulinello", "multiplo", "mummia", "munto", "muovere", "murale", "musa", "muscolo", "musica", "mutevole", "muto", "nababbo", "nafta", "nanometro", "narciso", "narice", "narrato", "nascere", "nastrare", "naturale", "nautica", "naviglio", "nebulosa", "necrosi", "negativo", "negozio", "nemmeno", "neofita", "neretto", "nervo", "nessuno", "nettuno", "neutrale", "neve", "nevrotico", "nicchia", "ninfa", "nitido", "nobile", "nocivo", "nodo", "nome", "nomina", "nordico", "normale", "norvegese", "nostrano", "notare", "notizia", "notturno", "novella", "nucleo", "nulla", "numero", "nuovo", "nutrire", "nuvola", "nuziale", "oasi", "obbedire", "obbligo", "obelisco", "oblio", "obolo", "obsoleto", "occasione", "occhio", "occidente", "occorrere", "occultare", "ocra", "oculato", "odierno", "odorare", "offerta", "offrire", "offuscato", "oggetto", "oggi", "ognuno", "olandese", "olfatto", "oliato", "oliva", "ologramma", "oltre", "omaggio", "ombelico", "ombra", "omega", "omissione", "ondoso", "onere", "onice", "onnivoro", "onorevole", "onta", "operato", "opinione", "opposto", "oracolo", "orafo", "ordine", "orecchino", "orefice", "orfano", "organico", "origine", "orizzonte", "orma", "ormeggio", "ornativo", "orologio", "orrendo", "orribile", "ortensia", "ortica", "orzata", "orzo", "osare", "oscurare", "osmosi", "ospedale", "ospite", "ossa", "ossidare", "ostacolo", "oste", "otite", "otre", "ottagono", "ottimo", "ottobre", "ovale", "ovest", "ovino", "oviparo", "ovocito", "ovunque", "ovviare", "ozio", "pacchetto", "pace", "pacifico", "padella", "padrone", "paese", "paga", "pagina", "palazzina", "palesare", "pallido", "palo", "palude", "pandoro", "pannello", "paolo", "paonazzo", "paprica", "parabola", "parcella", "parere", "pargolo", "pari", "parlato", "parola", "partire", "parvenza", "parziale", "passivo", "pasticca", "patacca", "patologia", "pattume", "pavone", "peccato", "pedalare", "pedonale", "peggio", "peloso", "penare", "pendice", "penisola", "pennuto", "penombra", "pensare", "pentola", "pepe", "pepita", "perbene", "percorso", "perdonato", "perforare", "pergamena", "periodo", "permesso", "perno", "perplesso", "persuaso", "pertugio", "pervaso", "pesatore", "pesista", "peso", "pestifero", "petalo", "pettine", "petulante", "pezzo", "piacere", "pianta", "piattino", "piccino", "picozza", "piega", "pietra", "piffero", "pigiama", "pigolio", "pigro", "pila", "pilifero", "pillola", "pilota", "pimpante", "pineta", "pinna", "pinolo", "pioggia", "piombo", "piramide", "piretico", "pirite", "pirolisi", "pitone", "pizzico", "placebo", "planare", "plasma", "platano", "plenario", "pochezza", "poderoso", "podismo", "poesia", "poggiare", "polenta", "poligono", "pollice", "polmonite", "polpetta", "polso", "poltrona", "polvere", "pomice", "pomodoro", "ponte", "popoloso", "porfido", "poroso", "porpora", "porre", "portata", "posa", "positivo", "possesso", "postulato", "potassio", "potere", "pranzo", "prassi", "pratica", "precluso", "predica", "prefisso", "pregiato", "prelievo", "premere", "prenotare", "preparato", "presenza", "pretesto", "prevalso", "prima", "principe", "privato", "problema", "procura", "produrre", "profumo", "progetto", "prolunga", "promessa", "pronome", "proposta", "proroga", "proteso", "prova", "prudente", "prugna", "prurito", "psiche", "pubblico", "pudica", "pugilato", "pugno", "pulce", "pulito", "pulsante", "puntare", "pupazzo", "pupilla", "puro", "quadro", "qualcosa", "quasi", "querela", "quota", "raccolto", "raddoppio", "radicale", "radunato", "raffica", "ragazzo", "ragione", "ragno", "ramarro", "ramingo", "ramo", "randagio", "rantolare", "rapato", "rapina", "rappreso", "rasatura", "raschiato", "rasente", "rassegna", "rastrello", "rata", "ravveduto", "reale", "recepire", "recinto", "recluta", "recondito", "recupero", "reddito", "redimere", "regalato", "registro", "regola", "regresso", "relazione", "remare", "remoto", "renna", "replica", "reprimere", "reputare", "resa", "residente", "responso", "restauro", "rete", "retina", "retorica", "rettifica", "revocato", "riassunto", "ribadire", "ribelle", "ribrezzo", "ricarica", "ricco", "ricevere", "riciclato", "ricordo", "ricreduto", "ridicolo", "ridurre", "rifasare", "riflesso", "riforma", "rifugio", "rigare", "rigettato", "righello", "rilassato", "rilevato", "rimanere", "rimbalzo", "rimedio", "rimorchio", "rinascita", "rincaro", "rinforzo", "rinnovo", "rinomato", "rinsavito", "rintocco", "rinuncia", "rinvenire", "riparato", "ripetuto", "ripieno", "riportare", "ripresa", "ripulire", "risata", "rischio", "riserva", "risibile", "riso", "rispetto", "ristoro", "risultato", "risvolto", "ritardo", "ritegno", "ritmico", "ritrovo", "riunione", "riva", "riverso", "rivincita", "rivolto", "rizoma", "roba", "robotico", "robusto", "roccia", "roco", "rodaggio", "rodere", "roditore", "rogito", "rollio", "romantico", "rompere", "ronzio", "rosolare", "rospo", "rotante", "rotondo", "rotula", "rovescio", "rubizzo", "rubrica", "ruga", "rullino", "rumine", "rumoroso", "ruolo", "rupe", "russare", "rustico", "sabato", "sabbiare", "sabotato", "sagoma", "salasso", "saldatura", "salgemma", "salivare", "salmone", "salone", "saltare", "saluto", "salvo", "sapere", "sapido", "saporito", "saraceno", "sarcasmo", "sarto", "sassoso", "satellite", "satira", "satollo", "saturno", "savana", "savio", "saziato", "sbadiglio", "sbalzo", "sbancato", "sbarra", "sbattere", "sbavare", "sbendare", "sbirciare", "sbloccato", "sbocciato", "sbrinare", "sbruffone", "sbuffare", "scabroso", "scadenza", "scala", "scambiare", "scandalo", "scapola", "scarso", "scatenare", "scavato", "scelto", "scenico", "scettro", "scheda", "schiena", "sciarpa", "scienza", "scindere", "scippo", "sciroppo", "scivolo", "sclerare", "scodella", "scolpito", "scomparto", "sconforto", "scoprire", "scorta", "scossone", "scozzese", "scriba", "scrollare", "scrutinio", "scuderia", "scultore", "scuola", "scuro", "scusare", "sdebitare", "sdoganare", "seccatura", "secondo", "sedano", "seggiola", "segnalato", "segregato", "seguito", "selciato", "selettivo", "sella", "selvaggio", "semaforo", "sembrare", "seme", "seminato", "sempre", "senso", "sentire", "sepolto", "sequenza", "serata", "serbato", "sereno", "serio", "serpente", "serraglio", "servire", "sestina", "setola", "settimana", "sfacelo", "sfaldare", "sfamato", "sfarzoso", "sfaticato", "sfera", "sfida", "sfilato", "sfinge", "sfocato", "sfoderare", "sfogo", "sfoltire", "sforzato", "sfratto", "sfruttato", "sfuggito", "sfumare", "sfuso", "sgabello", "sgarbato", "sgonfiare", "sgorbio", "sgrassato", "sguardo", "sibilo", "siccome", "sierra", "sigla", "signore", "silenzio", "sillaba", "simbolo", "simpatico", "simulato", "sinfonia", "singolo", "sinistro", "sino", "sintesi", "sinusoide", "sipario", "sisma", "sistole", "situato", "slitta", "slogatura", "sloveno", "smarrito", "smemorato", "smentito", "smeraldo", "smilzo", "smontare", "smottato", "smussato", "snellire", "snervato", "snodo", "sobbalzo", "sobrio", "soccorso", "sociale", "sodale", "soffitto", "sogno", "soldato", "solenne", "solido", "sollazzo", "solo", "solubile", "solvente", "somatico", "somma", "sonda", "sonetto", "sonnifero", "sopire", "soppeso", "sopra", "sorgere", "sorpasso", "sorriso", "sorso", "sorteggio", "sorvolato", "sospiro", "sosta", "sottile", "spada", "spalla", "spargere", "spatola", "spavento", "spazzola", "specie", "spedire", "spegnere", "spelatura", "speranza", "spessore", "spettrale", "spezzato", "spia", "spigoloso", "spillato", "spinoso", "spirale", "splendido", "sportivo", "sposo", "spranga", "sprecare", "spronato", "spruzzo", "spuntino", "squillo", "sradicare", "srotolato", "stabile", "stacco", "staffa", "stagnare", "stampato", "stantio", "starnuto", "stasera", "statuto", "stelo", "steppa", "sterzo", "stiletto", "stima", "stirpe", "stivale", "stizzoso", "stonato", "storico", "strappo", "stregato", "stridulo", "strozzare", "strutto", "stuccare", "stufo", "stupendo", "subentro", "succoso", "sudore", "suggerito", "sugo", "sultano", "suonare", "superbo", "supporto", "surgelato", "surrogato", "sussurro", "sutura", "svagare", "svedese", "sveglio", "svelare", "svenuto", "svezia", "sviluppo", "svista", "svizzera", "svolta", "svuotare", "tabacco", "tabulato", "tacciare", "taciturno", "tale", "talismano", "tampone", "tannino", "tara", "tardivo", "targato", "tariffa", "tarpare", "tartaruga", "tasto", "tattico", "taverna", "tavolata", "tazza", "teca", "tecnico", "telefono", "temerario", "tempo", "temuto", "tendone", "tenero", "tensione", "tentacolo", "teorema", "terme", "terrazzo", "terzetto", "tesi", "tesserato", "testato", "tetro", "tettoia", "tifare", "tigella", "timbro", "tinto", "tipico", "tipografo", "tiraggio", "tiro", "titanio", "titolo", "titubante", "tizio", "tizzone", "toccare", "tollerare", "tolto", "tombola", "tomo", "tonfo", "tonsilla", "topazio", "topologia", "toppa", "torba", "tornare", "torrone", "tortora", "toscano", "tossire", "tostatura", "totano", "trabocco", "trachea", "trafila", "tragedia", "tralcio", "tramonto", "transito", "trapano", "trarre", "trasloco", "trattato", "trave", "treccia", "tremolio", "trespolo", "tributo", "tricheco", "trifoglio", "trillo", "trincea", "trio", "tristezza", "triturato", "trivella", "tromba", "trono", "troppo", "trottola", "trovare", "truccato", "tubatura", "tuffato", "tulipano", "tumulto", "tunisia", "turbare", "turchino", "tuta", "tutela", "ubicato", "uccello", "uccisore", "udire", "uditivo", "uffa", "ufficio", "uguale", "ulisse", "ultimato", "umano", "umile", "umorismo", "uncinetto", "ungere", "ungherese", "unicorno", "unificato", "unisono", "unitario", "unte", "uovo", "upupa", "uragano", "urgenza", "urlo", "usanza", "usato", "uscito", "usignolo", "usuraio", "utensile", "utilizzo", "utopia", "vacante", "vaccinato", "vagabondo", "vagliato", "valanga", "valgo", "valico", "valletta", "valoroso", "valutare", "valvola", "vampata", "vangare", "vanitoso", "vano", "vantaggio", "vanvera", "vapore", "varano", "varcato", "variante", "vasca", "vedetta", "vedova", "veduto", "vegetale", "veicolo", "velcro", "velina", "velluto", "veloce", "venato", "vendemmia", "vento", "verace", "verbale", "vergogna", "verifica", "vero", "verruca", "verticale", "vescica", "vessillo", "vestale", "veterano", "vetrina", "vetusto", "viandante", "vibrante", "vicenda", "vichingo", "vicinanza", "vidimare", "vigilia", "vigneto", "vigore", "vile", "villano", "vimini", "vincitore", "viola", "vipera", "virgola", "virologo", "virulento", "viscoso", "visione", "vispo", "vissuto", "visura", "vita", "vitello", "vittima", "vivanda", "vivido", "viziare", "voce", "voga", "volatile", "volere", "volpe", "voragine", "vulcano", "zampogna", "zanna", "zappato", "zattera", "zavorra", "zefiro", "zelante", "zelo", "zenzero", "zerbino", "zibetto", "zinco", "zircone", "zitto", "zolla", "zotico", "zucchero", "zufolo", "zulu", "zuppa"] +} \ No newline at end of file diff --git a/sphinx/application/common/resources/src/main/java/chat/sphinx/resources/MnemonicLanguagesUtils.kt b/sphinx/application/common/resources/src/main/java/chat/sphinx/resources/MnemonicLanguagesUtils.kt new file mode 100644 index 0000000000..016ceed792 --- /dev/null +++ b/sphinx/application/common/resources/src/main/java/chat/sphinx/resources/MnemonicLanguagesUtils.kt @@ -0,0 +1,86 @@ +package chat.sphinx.resources + +import android.content.Context +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import java.lang.Exception + +@Serializable +data class WordLists( + val ENGLISH: List, + val JAPANESE: List, + val KOREAN: List, + val SPANISH: List, + val SIMPLIFIED_CHINESE: List, + val TRADITIONAL_CHINESE: List, + val FRENCH: List, + val ITALIAN: List +) + +class MnemonicLanguagesUtils(private val context: Context) { + + enum class Language { + ENGLISH, + JAPANESE, + KOREAN, + SPANISH, + SIMPLIFIED_CHINESE, + TRADITIONAL_CHINESE, + FRENCH, + ITALIAN; + } + + private fun loadJsonFromAsset(filename: String): String { + val bufferedReader = context.assets.open(filename).bufferedReader() + return bufferedReader.use { it.readText() } + } + + private fun getWordsForLanguage(language: Language): List { + try { + val jsonContent = loadJsonFromAsset("words-list-constants.json") + val wordLists = Json.decodeFromString(jsonContent) + + return when (language) { + Language.ENGLISH -> wordLists.ENGLISH + Language.JAPANESE -> wordLists.JAPANESE + Language.KOREAN -> wordLists.KOREAN + Language.SPANISH -> wordLists.SPANISH + Language.SIMPLIFIED_CHINESE -> wordLists.SIMPLIFIED_CHINESE + Language.TRADITIONAL_CHINESE -> wordLists.TRADITIONAL_CHINESE + Language.FRENCH -> wordLists.FRENCH + Language.ITALIAN -> wordLists.ITALIAN + } + } + catch (e: Exception) { + return emptyList() + } + } + + private fun findLanguage(word: String): Language? { + for (language in Language.values()) { + if (getWordsForLanguage(language).contains(word)) { + return language + } + } + return null + } + + fun validateWords(words: List): Boolean { + val language = findLanguage(words[0]) + + if (language != null) { + val wordsList = getWordsForLanguage(language) + + for (word in words) { + if (!wordsList.contains(word)) { + return false + } + } + } else { + return false + } + + return true + } +} diff --git a/sphinx/application/common/resources/src/main/java/chat/sphinx/resources/sphinxrs.kt b/sphinx/application/common/resources/src/main/java/chat/sphinx/resources/sphinxrs.kt index cd4e56d095..1a4c6626ef 100644 --- a/sphinx/application/common/resources/src/main/java/chat/sphinx/resources/sphinxrs.kt +++ b/sphinx/application/common/resources/src/main/java/chat/sphinx/resources/sphinxrs.kt @@ -18,12 +18,15 @@ package uniffi.sphinxrs; // helpers directly inline like we're doing here. import com.sun.jna.Library +import com.sun.jna.IntegerType import com.sun.jna.Native import com.sun.jna.Pointer import com.sun.jna.Structure -import com.sun.jna.ptr.ByReference +import com.sun.jna.Callback +import com.sun.jna.ptr.* import java.nio.ByteBuffer import java.nio.ByteOrder +import java.util.concurrent.ConcurrentHashMap // This is a helper for safely working with byte buffers returned from the Rust code. // A rust-owned buffer is represented by its capacity, its current length, and a @@ -35,20 +38,20 @@ open class RustBuffer : Structure() { @JvmField var len: Int = 0 @JvmField var data: Pointer? = null - class ByValue : RustBuffer(), Structure.ByValue - class ByReference : RustBuffer(), Structure.ByReference + class ByValue: RustBuffer(), Structure.ByValue + class ByReference: RustBuffer(), Structure.ByReference companion object { internal fun alloc(size: Int = 0) = rustCall() { status -> - _UniFFILib.INSTANCE.ffi_sphinxrs_b6a8_rustbuffer_alloc(size, status).also { + _UniFFILib.INSTANCE.ffi_sphinxrs_rustbuffer_alloc(size, status).also { if(it.data == null) { - throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=${size})") - } + throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=${size})") + } } } internal fun free(buf: RustBuffer.ByValue) = rustCall() { status -> - _UniFFILib.INSTANCE.ffi_sphinxrs_b6a8_rustbuffer_free(buf, status) + _UniFFILib.INSTANCE.ffi_sphinxrs_rustbuffer_free(buf, status) } } @@ -76,6 +79,19 @@ class RustBufferByReference : ByReference(16) { pointer.setInt(4, value.len) pointer.setPointer(8, value.data) } + + /** + * Get a `RustBuffer.ByValue` from this reference. + */ + fun getValue(): RustBuffer.ByValue { + val pointer = getPointer() + val value = RustBuffer.ByValue() + value.writeField("capacity", pointer.getInt(0)) + value.writeField("len", pointer.getInt(4)) + value.writeField("data", pointer.getPointer(8)) + + return value + } } // This is a helper for safely passing byte references into the rust code. @@ -146,11 +162,11 @@ public interface FfiConverter { fun liftFromRustBuffer(rbuf: RustBuffer.ByValue): KotlinType { val byteBuf = rbuf.asByteBuffer()!! try { - val item = read(byteBuf) - if (byteBuf.hasRemaining()) { - throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!") - } - return item + val item = read(byteBuf) + if (byteBuf.hasRemaining()) { + throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!") + } + return item } finally { RustBuffer.free(rbuf) } @@ -167,19 +183,21 @@ public interface FfiConverterRustBuffer: FfiConverter { private inline fun rustCallWithError(errorHandler: CallStatusErrorHandler, callback: (RustCallStatus) -> U): U { var status = RustCallStatus(); val return_value = callback(status) + checkCallStatus(errorHandler, status) + return return_value +} + +// Check RustCallStatus and throw an error if the call wasn't successful +private fun checkCallStatus(errorHandler: CallStatusErrorHandler, status: RustCallStatus) { if (status.isSuccess()) { - return return_value + return } else if (status.isError()) { throw errorHandler.lift(status.error_buf) } else if (status.isPanic()) { @@ -229,6 +253,86 @@ private inline fun rustCall(callback: (RustCallStatus) -> U): U { return rustCallWithError(NullCallStatusErrorHandler, callback); } +// IntegerType that matches Rust's `usize` / C's `size_t` +public class USize(value: Long = 0) : IntegerType(Native.SIZE_T_SIZE, value, true) { + // This is needed to fill in the gaps of IntegerType's implementation of Number for Kotlin. + override fun toByte() = toInt().toByte() + override fun toChar() = toInt().toChar() + override fun toShort() = toInt().toShort() + + fun writeToBuffer(buf: ByteBuffer) { + // Make sure we always write usize integers using native byte-order, since they may be + // casted to pointer values + buf.order(ByteOrder.nativeOrder()) + try { + when (Native.SIZE_T_SIZE) { + 4 -> buf.putInt(toInt()) + 8 -> buf.putLong(toLong()) + else -> throw RuntimeException("Invalid SIZE_T_SIZE: ${Native.SIZE_T_SIZE}") + } + } finally { + buf.order(ByteOrder.BIG_ENDIAN) + } + } + + companion object { + val size: Int + get() = Native.SIZE_T_SIZE + + fun readFromBuffer(buf: ByteBuffer) : USize { + // Make sure we always read usize integers using native byte-order, since they may be + // casted from pointer values + buf.order(ByteOrder.nativeOrder()) + try { + return when (Native.SIZE_T_SIZE) { + 4 -> USize(buf.getInt().toLong()) + 8 -> USize(buf.getLong()) + else -> throw RuntimeException("Invalid SIZE_T_SIZE: ${Native.SIZE_T_SIZE}") + } + } finally { + buf.order(ByteOrder.BIG_ENDIAN) + } + } + } +} + + +// Map handles to objects +// +// This is used when the Rust code expects an opaque pointer to represent some foreign object. +// Normally we would pass a pointer to the object, but JNA doesn't support getting a pointer from an +// object reference , nor does it support leaking a reference to Rust. +// +// Instead, this class maps USize values to objects so that we can pass a pointer-sized type to +// Rust when it needs an opaque pointer. +// +// TODO: refactor callbacks to use this class +internal class UniFfiHandleMap { + private val map = ConcurrentHashMap() + // Use AtomicInteger for our counter, since we may be on a 32-bit system. 4 billion possible + // values seems like enough. If somehow we generate 4 billion handles, then this will wrap + // around back to zero and we can assume the first handle generated will have been dropped by + // then. + private val counter = java.util.concurrent.atomic.AtomicInteger(0) + + val size: Int + get() = map.size + + fun insert(obj: T): USize { + val handle = USize(counter.getAndAdd(1).toLong()) + map.put(handle, obj) + return handle + } + + fun get(handle: USize): T? { + return map.get(handle) + } + + fun remove(handle: USize) { + map.remove(handle) + } +} + // Contains loading, initialization code, // and the FFI Function declarations in a com.sun.jna.Library. @Synchronized @@ -253,131 +357,173 @@ internal interface _UniFFILib : Library { companion object { internal val INSTANCE: _UniFFILib by lazy { loadIndirect<_UniFFILib>(componentName = "sphinxrs") - + .also { lib: _UniFFILib -> + uniffiCheckContractApiVersion(lib) + uniffiCheckApiChecksums(lib) + } } } - fun sphinxrs_b6a8_pubkey_from_secret_key(`mySecretKey`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_pubkey_from_secret_key(`mySecretKey`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_derive_shared_secret(`theirPubkey`: RustBuffer.ByValue,`mySecretKey`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_derive_shared_secret(`theirPubkey`: RustBuffer.ByValue,`mySecretKey`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_encrypt(`plaintext`: RustBuffer.ByValue,`secret`: RustBuffer.ByValue,`nonce`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_encrypt(`plaintext`: RustBuffer.ByValue,`secret`: RustBuffer.ByValue,`nonce`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_decrypt(`ciphertext`: RustBuffer.ByValue,`secret`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_decrypt(`ciphertext`: RustBuffer.ByValue,`secret`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_node_keys(`net`: RustBuffer.ByValue,`seed`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_node_keys(`net`: RustBuffer.ByValue,`seed`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_mnemonic_from_entropy(`seed`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_mnemonic_from_entropy(`entropy`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_entropy_from_mnemonic(`mnemonic`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_entropy_from_mnemonic(`mnemonic`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_get_nonce_request(`secret`: RustBuffer.ByValue,`nonce`: Long, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_mnemonic_to_seed(`mnemonic`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_get_nonce_response(`bytes`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus - ): Long - - fun sphinxrs_b6a8_reset_wifi_request(`secret`: RustBuffer.ByValue,`nonce`: Long, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_entropy_to_seed(`entropy`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_reset_wifi_response(`bytes`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus - ): Unit - - fun sphinxrs_b6a8_reset_keys_request(`secret`: RustBuffer.ByValue,`nonce`: Long, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_build_request(`msg`: RustBuffer.ByValue,`secret`: RustBuffer.ByValue,`nonce`: Long,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_reset_keys_response(`bytes`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus - ): Unit - - fun sphinxrs_b6a8_reset_all_request(`secret`: RustBuffer.ByValue,`nonce`: Long, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_parse_response(`res`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_reset_all_response(`bytes`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus - ): Unit - - fun sphinxrs_b6a8_get_policy_request(`secret`: RustBuffer.ByValue,`nonce`: Long, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_make_auth_token(`ts`: Int,`secret`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_get_policy_response(`bytes`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus + fun uniffi_sphinxrs_fn_func_run(`topic`: RustBuffer.ByValue,`args`: RustBuffer.ByValue,`state`: RustBuffer.ByValue,`msg1`: RustBuffer.ByValue,`expectedSequence`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_update_policy_request(`secret`: RustBuffer.ByValue,`nonce`: Long,`policy`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus + fun ffi_sphinxrs_rustbuffer_alloc(`size`: Int,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_update_policy_response(`bytes`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus + fun ffi_sphinxrs_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - - fun sphinxrs_b6a8_get_allowlist_request(`secret`: RustBuffer.ByValue,`nonce`: Long, - _uniffi_out_err: RustCallStatus + fun ffi_sphinxrs_rustbuffer_free(`buf`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): Unit + fun ffi_sphinxrs_rustbuffer_reserve(`buf`: RustBuffer.ByValue,`additional`: Int,_uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue + fun uniffi_sphinxrs_checksum_func_pubkey_from_secret_key( + ): Short + fun uniffi_sphinxrs_checksum_func_derive_shared_secret( + ): Short + fun uniffi_sphinxrs_checksum_func_encrypt( + ): Short + fun uniffi_sphinxrs_checksum_func_decrypt( + ): Short + fun uniffi_sphinxrs_checksum_func_node_keys( + ): Short + fun uniffi_sphinxrs_checksum_func_mnemonic_from_entropy( + ): Short + fun uniffi_sphinxrs_checksum_func_entropy_from_mnemonic( + ): Short + fun uniffi_sphinxrs_checksum_func_mnemonic_to_seed( + ): Short + fun uniffi_sphinxrs_checksum_func_entropy_to_seed( + ): Short + fun uniffi_sphinxrs_checksum_func_build_request( + ): Short + fun uniffi_sphinxrs_checksum_func_parse_response( + ): Short + fun uniffi_sphinxrs_checksum_func_make_auth_token( + ): Short + fun uniffi_sphinxrs_checksum_func_run( + ): Short + fun ffi_sphinxrs_uniffi_contract_version( + ): Int + +} - fun sphinxrs_b6a8_get_allowlist_response(`bytes`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus - ): RustBuffer.ByValue +private fun uniffiCheckContractApiVersion(lib: _UniFFILib) { + // Get the bindings contract version from our ComponentInterface + val bindings_contract_version = 22 + // Get the scaffolding contract version by calling the into the dylib + val scaffolding_contract_version = lib.ffi_sphinxrs_uniffi_contract_version() + if (bindings_contract_version != scaffolding_contract_version) { + throw RuntimeException("UniFFI contract version mismatch: try cleaning and rebuilding your project") + } +} - fun sphinxrs_b6a8_update_allowlist_request(`secret`: RustBuffer.ByValue,`nonce`: Long,`allowlist`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus - ): RustBuffer.ByValue +@Suppress("UNUSED_PARAMETER") +private fun uniffiCheckApiChecksums(lib: _UniFFILib) { + if (lib.uniffi_sphinxrs_checksum_func_pubkey_from_secret_key() != 14435.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_sphinxrs_checksum_func_derive_shared_secret() != 20125.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_sphinxrs_checksum_func_encrypt() != 43446.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_sphinxrs_checksum_func_decrypt() != 47725.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_sphinxrs_checksum_func_node_keys() != 21192.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_sphinxrs_checksum_func_mnemonic_from_entropy() != 32309.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_sphinxrs_checksum_func_entropy_from_mnemonic() != 33294.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_sphinxrs_checksum_func_mnemonic_to_seed() != 23084.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_sphinxrs_checksum_func_entropy_to_seed() != 33710.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_sphinxrs_checksum_func_build_request() != 31264.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_sphinxrs_checksum_func_parse_response() != 12980.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_sphinxrs_checksum_func_make_auth_token() != 13236.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_sphinxrs_checksum_func_run() != 47350.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } +} - fun sphinxrs_b6a8_update_allowlist_response(`bytes`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus - ): RustBuffer.ByValue +// Public interface members begin here. - fun sphinxrs_b6a8_ota_request(`secret`: RustBuffer.ByValue,`nonce`: Long,`version`: Long,`url`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus - ): RustBuffer.ByValue - fun sphinxrs_b6a8_ota_response(`bytes`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus - ): Long +public object FfiConverterUShort: FfiConverter { + override fun lift(value: Short): UShort { + return value.toUShort() + } - fun ffi_sphinxrs_b6a8_rustbuffer_alloc(`size`: Int, - _uniffi_out_err: RustCallStatus - ): RustBuffer.ByValue + override fun read(buf: ByteBuffer): UShort { + return lift(buf.getShort()) + } - fun ffi_sphinxrs_b6a8_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue, - _uniffi_out_err: RustCallStatus - ): RustBuffer.ByValue + override fun lower(value: UShort): Short { + return value.toShort() + } - fun ffi_sphinxrs_b6a8_rustbuffer_free(`buf`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus - ): Unit + override fun allocationSize(value: UShort) = 2 - fun ffi_sphinxrs_b6a8_rustbuffer_reserve(`buf`: RustBuffer.ByValue,`additional`: Int, - _uniffi_out_err: RustCallStatus - ): RustBuffer.ByValue + override fun write(value: UShort, buf: ByteBuffer) { + buf.putShort(value.toShort()) + } +} +public object FfiConverterUInt: FfiConverter { + override fun lift(value: Int): UInt { + return value.toUInt() + } -} + override fun read(buf: ByteBuffer): UInt { + return lift(buf.getInt()) + } -// Public interface members begin here. + override fun lower(value: UInt): Int { + return value.toInt() + } + + override fun allocationSize(value: UInt) = 4 + override fun write(value: UInt, buf: ByteBuffer) { + buf.putInt(value.toInt()) + } +} public object FfiConverterULong: FfiConverter { override fun lift(value: Long): ULong { @@ -445,14 +591,30 @@ public object FfiConverterString: FfiConverter { } } +public object FfiConverterByteArray: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ByteArray { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr + } + override fun allocationSize(value: ByteArray): Int { + return 4 + value.size + } + override fun write(value: ByteArray, buf: ByteBuffer) { + buf.putInt(value.size) + buf.put(value) + } +} + data class Keys ( - var `secret`: String, + var `secret`: String, var `pubkey`: String ) { - + } public object FfiConverterTypeKeys: FfiConverterRustBuffer { @@ -465,45 +627,53 @@ public object FfiConverterTypeKeys: FfiConverterRustBuffer { override fun allocationSize(value: Keys) = ( FfiConverterString.allocationSize(value.`secret`) + - FfiConverterString.allocationSize(value.`pubkey`) - ) + FfiConverterString.allocationSize(value.`pubkey`) + ) override fun write(value: Keys, buf: ByteBuffer) { - FfiConverterString.write(value.`secret`, buf) - FfiConverterString.write(value.`pubkey`, buf) + FfiConverterString.write(value.`secret`, buf) + FfiConverterString.write(value.`pubkey`, buf) } } -data class Policy ( - var `satLimit`: ULong, - var `interval`: String, - var `htlcLimit`: ULong +data class VlsResponse ( + var `topic`: String, + var `bytes`: ByteArray, + var `sequence`: UShort, + var `cmd`: String, + var `state`: ByteArray ) { - + } -public object FfiConverterTypePolicy: FfiConverterRustBuffer { - override fun read(buf: ByteBuffer): Policy { - return Policy( - FfiConverterULong.read(buf), +public object FfiConverterTypeVlsResponse: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): VlsResponse { + return VlsResponse( FfiConverterString.read(buf), - FfiConverterULong.read(buf), + FfiConverterByteArray.read(buf), + FfiConverterUShort.read(buf), + FfiConverterString.read(buf), + FfiConverterByteArray.read(buf), ) } - override fun allocationSize(value: Policy) = ( - FfiConverterULong.allocationSize(value.`satLimit`) + - FfiConverterString.allocationSize(value.`interval`) + - FfiConverterULong.allocationSize(value.`htlcLimit`) - ) - - override fun write(value: Policy, buf: ByteBuffer) { - FfiConverterULong.write(value.`satLimit`, buf) - FfiConverterString.write(value.`interval`, buf) - FfiConverterULong.write(value.`htlcLimit`, buf) + override fun allocationSize(value: VlsResponse) = ( + FfiConverterString.allocationSize(value.`topic`) + + FfiConverterByteArray.allocationSize(value.`bytes`) + + FfiConverterUShort.allocationSize(value.`sequence`) + + FfiConverterString.allocationSize(value.`cmd`) + + FfiConverterByteArray.allocationSize(value.`state`) + ) + + override fun write(value: VlsResponse, buf: ByteBuffer) { + FfiConverterString.write(value.`topic`, buf) + FfiConverterByteArray.write(value.`bytes`, buf) + FfiConverterUShort.write(value.`sequence`, buf) + FfiConverterString.write(value.`cmd`, buf) + FfiConverterByteArray.write(value.`state`, buf) } } @@ -511,55 +681,394 @@ public object FfiConverterTypePolicy: FfiConverterRustBuffer { -sealed class SphinxException(message: String): Exception(message) { +sealed class SphinxException: Exception() { // Each variant is a nested class - // Flat enums carries a string error message, so no special implementation is necessary. - class DerivePublicKey(message: String) : SphinxException(message) - class DeriveSharedSecret(message: String) : SphinxException(message) - class Encrypt(message: String) : SphinxException(message) - class Decrypt(message: String) : SphinxException(message) - class BadPubkey(message: String) : SphinxException(message) - class BadSecret(message: String) : SphinxException(message) - class BadNonce(message: String) : SphinxException(message) - class BadCiper(message: String) : SphinxException(message) - class InvalidNetwork(message: String) : SphinxException(message) - class BadRequest(message: String) : SphinxException(message) - class BadResponse(message: String) : SphinxException(message) - + + class DerivePublicKey( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class DeriveSharedSecret( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class Encrypt( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class Decrypt( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class BadPubkey( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class BadSecret( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class BadNonce( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class BadCiper( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class InvalidNetwork( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class BadRequest( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class BadResponse( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class BadTopic( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class BadArgs( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class BadState( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class BadVelocity( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class InitFailed( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class LssFailed( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + + class VlsFailed( + val `r`: String + ) : SphinxException() { + override val message + get() = "r=${ `r` }" + } + companion object ErrorHandler : CallStatusErrorHandler { override fun lift(error_buf: RustBuffer.ByValue): SphinxException = FfiConverterTypeSphinxError.lift(error_buf) } + + } public object FfiConverterTypeSphinxError : FfiConverterRustBuffer { override fun read(buf: ByteBuffer): SphinxException { + return when(buf.getInt()) { - 1 -> SphinxException.DerivePublicKey(FfiConverterString.read(buf)) - 2 -> SphinxException.DeriveSharedSecret(FfiConverterString.read(buf)) - 3 -> SphinxException.Encrypt(FfiConverterString.read(buf)) - 4 -> SphinxException.Decrypt(FfiConverterString.read(buf)) - 5 -> SphinxException.BadPubkey(FfiConverterString.read(buf)) - 6 -> SphinxException.BadSecret(FfiConverterString.read(buf)) - 7 -> SphinxException.BadNonce(FfiConverterString.read(buf)) - 8 -> SphinxException.BadCiper(FfiConverterString.read(buf)) - 9 -> SphinxException.InvalidNetwork(FfiConverterString.read(buf)) - 10 -> SphinxException.BadRequest(FfiConverterString.read(buf)) - 11 -> SphinxException.BadResponse(FfiConverterString.read(buf)) + 1 -> SphinxException.DerivePublicKey( + FfiConverterString.read(buf), + ) + 2 -> SphinxException.DeriveSharedSecret( + FfiConverterString.read(buf), + ) + 3 -> SphinxException.Encrypt( + FfiConverterString.read(buf), + ) + 4 -> SphinxException.Decrypt( + FfiConverterString.read(buf), + ) + 5 -> SphinxException.BadPubkey( + FfiConverterString.read(buf), + ) + 6 -> SphinxException.BadSecret( + FfiConverterString.read(buf), + ) + 7 -> SphinxException.BadNonce( + FfiConverterString.read(buf), + ) + 8 -> SphinxException.BadCiper( + FfiConverterString.read(buf), + ) + 9 -> SphinxException.InvalidNetwork( + FfiConverterString.read(buf), + ) + 10 -> SphinxException.BadRequest( + FfiConverterString.read(buf), + ) + 11 -> SphinxException.BadResponse( + FfiConverterString.read(buf), + ) + 12 -> SphinxException.BadTopic( + FfiConverterString.read(buf), + ) + 13 -> SphinxException.BadArgs( + FfiConverterString.read(buf), + ) + 14 -> SphinxException.BadState( + FfiConverterString.read(buf), + ) + 15 -> SphinxException.BadVelocity( + FfiConverterString.read(buf), + ) + 16 -> SphinxException.InitFailed( + FfiConverterString.read(buf), + ) + 17 -> SphinxException.LssFailed( + FfiConverterString.read(buf), + ) + 18 -> SphinxException.VlsFailed( + FfiConverterString.read(buf), + ) else -> throw RuntimeException("invalid error enum value, something is very wrong!!") } - } - @Suppress("UNUSED_PARAMETER") override fun allocationSize(value: SphinxException): Int { - throw RuntimeException("Writing Errors is not supported") + return when(value) { + is SphinxException.DerivePublicKey -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.DeriveSharedSecret -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.Encrypt -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.Decrypt -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.BadPubkey -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.BadSecret -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.BadNonce -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.BadCiper -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.InvalidNetwork -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.BadRequest -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.BadResponse -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.BadTopic -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.BadArgs -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.BadState -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.BadVelocity -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.InitFailed -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.LssFailed -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + is SphinxException.VlsFailed -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4 + + FfiConverterString.allocationSize(value.`r`) + ) + } } - @Suppress("UNUSED_PARAMETER") override fun write(value: SphinxException, buf: ByteBuffer) { - throw RuntimeException("Writing Errors is not supported") + when(value) { + is SphinxException.DerivePublicKey -> { + buf.putInt(1) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.DeriveSharedSecret -> { + buf.putInt(2) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.Encrypt -> { + buf.putInt(3) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.Decrypt -> { + buf.putInt(4) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.BadPubkey -> { + buf.putInt(5) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.BadSecret -> { + buf.putInt(6) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.BadNonce -> { + buf.putInt(7) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.BadCiper -> { + buf.putInt(8) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.InvalidNetwork -> { + buf.putInt(9) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.BadRequest -> { + buf.putInt(10) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.BadResponse -> { + buf.putInt(11) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.BadTopic -> { + buf.putInt(12) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.BadArgs -> { + buf.putInt(13) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.BadState -> { + buf.putInt(14) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.BadVelocity -> { + buf.putInt(15) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.InitFailed -> { + buf.putInt(16) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.LssFailed -> { + buf.putInt(17) + FfiConverterString.write(value.`r`, buf) + Unit + } + is SphinxException.VlsFailed -> { + buf.putInt(18) + FfiConverterString.write(value.`r`, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } } @@ -567,24 +1076,28 @@ public object FfiConverterTypeSphinxError : FfiConverterRustBuffer> { - override fun read(buf: ByteBuffer): List { - val len = buf.getInt() - return List(len) { - FfiConverterString.read(buf) +public object FfiConverterOptionalUShort: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): UShort? { + if (buf.get().toInt() == 0) { + return null } + return FfiConverterUShort.read(buf) } - override fun allocationSize(value: List): Int { - val sizeForLength = 4 - val sizeForItems = value.map { FfiConverterString.allocationSize(it) }.sum() - return sizeForLength + sizeForItems + override fun allocationSize(value: UShort?): Int { + if (value == null) { + return 1 + } else { + return 1 + FfiConverterUShort.allocationSize(value) + } } - override fun write(value: List, buf: ByteBuffer) { - buf.putInt(value.size) - value.forEach { - FfiConverterString.write(it, buf) + override fun write(value: UShort?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterUShort.write(value, buf) } } } @@ -592,238 +1105,117 @@ public object FfiConverterSequenceString: FfiConverterRustBuffer> { fun `pubkeyFromSecretKey`(`mySecretKey`: String): String { return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_pubkey_from_secret_key(FfiConverterString.lower(`mySecretKey`), _status) - }) + rustCallWithError(SphinxException) { _status -> + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_pubkey_from_secret_key(FfiConverterString.lower(`mySecretKey`),_status) +}) } - @Throws(SphinxException::class) fun `deriveSharedSecret`(`theirPubkey`: String, `mySecretKey`: String): String { return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_derive_shared_secret(FfiConverterString.lower(`theirPubkey`), FfiConverterString.lower(`mySecretKey`), _status) - }) + rustCallWithError(SphinxException) { _status -> + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_derive_shared_secret(FfiConverterString.lower(`theirPubkey`),FfiConverterString.lower(`mySecretKey`),_status) +}) } - @Throws(SphinxException::class) fun `encrypt`(`plaintext`: String, `secret`: String, `nonce`: String): String { return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_encrypt(FfiConverterString.lower(`plaintext`), FfiConverterString.lower(`secret`), FfiConverterString.lower(`nonce`), _status) - }) + rustCallWithError(SphinxException) { _status -> + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_encrypt(FfiConverterString.lower(`plaintext`),FfiConverterString.lower(`secret`),FfiConverterString.lower(`nonce`),_status) +}) } - @Throws(SphinxException::class) fun `decrypt`(`ciphertext`: String, `secret`: String): String { return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_decrypt(FfiConverterString.lower(`ciphertext`), FfiConverterString.lower(`secret`), _status) - }) + rustCallWithError(SphinxException) { _status -> + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_decrypt(FfiConverterString.lower(`ciphertext`),FfiConverterString.lower(`secret`),_status) +}) } - @Throws(SphinxException::class) fun `nodeKeys`(`net`: String, `seed`: String): Keys { return FfiConverterTypeKeys.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_node_keys(FfiConverterString.lower(`net`), FfiConverterString.lower(`seed`), _status) - }) + rustCallWithError(SphinxException) { _status -> + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_node_keys(FfiConverterString.lower(`net`),FfiConverterString.lower(`seed`),_status) +}) } - @Throws(SphinxException::class) -fun `mnemonicFromEntropy`(`seed`: String): String { +fun `mnemonicFromEntropy`(`entropy`: String): String { return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_mnemonic_from_entropy(FfiConverterString.lower(`seed`), _status) - }) + rustCallWithError(SphinxException) { _status -> + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_mnemonic_from_entropy(FfiConverterString.lower(`entropy`),_status) +}) } - @Throws(SphinxException::class) fun `entropyFromMnemonic`(`mnemonic`: String): String { return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_entropy_from_mnemonic(FfiConverterString.lower(`mnemonic`), _status) - }) -} - - -@Throws(SphinxException::class) - -fun `getNonceRequest`(`secret`: String, `nonce`: ULong): String { - return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_get_nonce_request(FfiConverterString.lower(`secret`), FfiConverterULong.lower(`nonce`), _status) - }) -} - - -@Throws(SphinxException::class) - -fun `getNonceResponse`(`bytes`: String): ULong { - return FfiConverterULong.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_get_nonce_response(FfiConverterString.lower(`bytes`), _status) - }) -} - - -@Throws(SphinxException::class) - -fun `resetWifiRequest`(`secret`: String, `nonce`: ULong): String { - return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_reset_wifi_request(FfiConverterString.lower(`secret`), FfiConverterULong.lower(`nonce`), _status) - }) -} - - -@Throws(SphinxException::class)fun `resetWifiResponse`(`bytes`: String) = - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_reset_wifi_response(FfiConverterString.lower(`bytes`), _status) - } - -@Throws(SphinxException::class) - -fun `resetKeysRequest`(`secret`: String, `nonce`: ULong): String { - return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_reset_keys_request(FfiConverterString.lower(`secret`), FfiConverterULong.lower(`nonce`), _status) - }) + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_entropy_from_mnemonic(FfiConverterString.lower(`mnemonic`),_status) +}) } - -@Throws(SphinxException::class)fun `resetKeysResponse`(`bytes`: String) = - - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_reset_keys_response(FfiConverterString.lower(`bytes`), _status) - } - @Throws(SphinxException::class) -fun `resetAllRequest`(`secret`: String, `nonce`: ULong): String { +fun `mnemonicToSeed`(`mnemonic`: String): String { return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_reset_all_request(FfiConverterString.lower(`secret`), FfiConverterULong.lower(`nonce`), _status) - }) -} - - -@Throws(SphinxException::class)fun `resetAllResponse`(`bytes`: String) = - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_reset_all_response(FfiConverterString.lower(`bytes`), _status) - } - -@Throws(SphinxException::class) - -fun `getPolicyRequest`(`secret`: String, `nonce`: ULong): String { - return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_get_policy_request(FfiConverterString.lower(`secret`), FfiConverterULong.lower(`nonce`), _status) - }) -} - - -@Throws(SphinxException::class) - -fun `getPolicyResponse`(`bytes`: String): Policy { - return FfiConverterTypePolicy.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_get_policy_response(FfiConverterString.lower(`bytes`), _status) - }) + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_mnemonic_to_seed(FfiConverterString.lower(`mnemonic`),_status) +}) } - @Throws(SphinxException::class) -fun `updatePolicyRequest`(`secret`: String, `nonce`: ULong, `policy`: Policy): String { +fun `entropyToSeed`(`entropy`: String): String { return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_update_policy_request(FfiConverterString.lower(`secret`), FfiConverterULong.lower(`nonce`), FfiConverterTypePolicy.lower(`policy`), _status) - }) -} - - -@Throws(SphinxException::class) - -fun `updatePolicyResponse`(`bytes`: String): Policy { - return FfiConverterTypePolicy.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_update_policy_response(FfiConverterString.lower(`bytes`), _status) - }) + rustCallWithError(SphinxException) { _status -> + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_entropy_to_seed(FfiConverterString.lower(`entropy`),_status) +}) } - @Throws(SphinxException::class) -fun `getAllowlistRequest`(`secret`: String, `nonce`: ULong): String { +fun `buildRequest`(`msg`: String, `secret`: String, `nonce`: ULong): String { return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_get_allowlist_request(FfiConverterString.lower(`secret`), FfiConverterULong.lower(`nonce`), _status) - }) -} - - -@Throws(SphinxException::class) - -fun `getAllowlistResponse`(`bytes`: String): List { - return FfiConverterSequenceString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_get_allowlist_response(FfiConverterString.lower(`bytes`), _status) - }) + rustCallWithError(SphinxException) { _status -> + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_build_request(FfiConverterString.lower(`msg`),FfiConverterString.lower(`secret`),FfiConverterULong.lower(`nonce`),_status) +}) } - @Throws(SphinxException::class) -fun `updateAllowlistRequest`(`secret`: String, `nonce`: ULong, `allowlist`: List): String { +fun `parseResponse`(`res`: String): String { return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_update_allowlist_request(FfiConverterString.lower(`secret`), FfiConverterULong.lower(`nonce`), FfiConverterSequenceString.lower(`allowlist`), _status) - }) -} - - -@Throws(SphinxException::class) - -fun `updateAllowlistResponse`(`bytes`: String): List { - return FfiConverterSequenceString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_update_allowlist_response(FfiConverterString.lower(`bytes`), _status) - }) + rustCallWithError(SphinxException) { _status -> + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_parse_response(FfiConverterString.lower(`res`),_status) +}) } - @Throws(SphinxException::class) -fun `otaRequest`(`secret`: String, `nonce`: ULong, `version`: ULong, `url`: String): String { +fun `makeAuthToken`(`ts`: UInt, `secret`: String): String { return FfiConverterString.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_ota_request(FfiConverterString.lower(`secret`), FfiConverterULong.lower(`nonce`), FfiConverterULong.lower(`version`), FfiConverterString.lower(`url`), _status) - }) + rustCallWithError(SphinxException) { _status -> + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_make_auth_token(FfiConverterUInt.lower(`ts`),FfiConverterString.lower(`secret`),_status) +}) } - @Throws(SphinxException::class) -fun `otaResponse`(`bytes`: String): ULong { - return FfiConverterULong.lift( - rustCallWithError(SphinxException) { _status -> - _UniFFILib.INSTANCE.sphinxrs_b6a8_ota_response(FfiConverterString.lower(`bytes`), _status) - }) +fun `run`(`topic`: String, `args`: String, `state`: ByteArray, `msg1`: ByteArray, `expectedSequence`: UShort?): VlsResponse { + return FfiConverterTypeVlsResponse.lift( + rustCallWithError(SphinxException) { _status -> + _UniFFILib.INSTANCE.uniffi_sphinxrs_fn_func_run(FfiConverterString.lower(`topic`),FfiConverterString.lower(`args`),FfiConverterByteArray.lower(`state`),FfiConverterByteArray.lower(`msg1`),FfiConverterOptionalUShort.lower(`expectedSequence`),_status) +}) } - diff --git a/sphinx/application/common/resources/src/main/res/drawable/background_mnemonic_text.xml b/sphinx/application/common/resources/src/main/res/drawable/background_mnemonic_text.xml new file mode 100644 index 0000000000..2b94376bc6 --- /dev/null +++ b/sphinx/application/common/resources/src/main/res/drawable/background_mnemonic_text.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/sphinx/application/common/resources/src/main/res/drawable/background_mnemonic_words.xml b/sphinx/application/common/resources/src/main/res/drawable/background_mnemonic_words.xml new file mode 100644 index 0000000000..f78c041e1d --- /dev/null +++ b/sphinx/application/common/resources/src/main/res/drawable/background_mnemonic_words.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/sphinx/application/common/resources/src/main/res/values-b+fil/strings.xml b/sphinx/application/common/resources/src/main/res/values-b+fil/strings.xml index c0f7d03fd7..74fc3b4a67 100644 --- a/sphinx/application/common/resources/src/main/res/values-b+fil/strings.xml +++ b/sphinx/application/common/resources/src/main/res/values-b+fil/strings.xml @@ -220,6 +220,15 @@ Piliin ang Bitcoin Network %s replies more replies + Enter your mnemonic words + Confirm + Cancel + Loading, please wait... + Your seed phrase must be exactly 12 or 24 words + It appears you\'ve typed an invalid word + Error connecting with MQTT + Phone connected MQTT + \ No newline at end of file diff --git a/sphinx/application/common/resources/src/main/res/values-es/strings.xml b/sphinx/application/common/resources/src/main/res/values-es/strings.xml index 2290e5e72e..88c718832e 100644 --- a/sphinx/application/common/resources/src/main/res/values-es/strings.xml +++ b/sphinx/application/common/resources/src/main/res/values-es/strings.xml @@ -151,4 +151,13 @@ Seleccioná la red de Bitcoin %s respuestas respuestas más + Ingrese sus palabras mnemotécnicas + Aceptar + Cancelar + Cargando... + Tu frase de recuperación debe tener exactamente 12 o 24 palabras + Parece que has ingresado una palabra no válida + Error al concterse con MQTT + El dispositivo está conectado a MQTT + \ No newline at end of file diff --git a/sphinx/application/common/resources/src/main/res/values-ja/strings.xml b/sphinx/application/common/resources/src/main/res/values-ja/strings.xml index 3589078134..40eb7fd426 100644 --- a/sphinx/application/common/resources/src/main/res/values-ja/strings.xml +++ b/sphinx/application/common/resources/src/main/res/values-ja/strings.xml @@ -149,5 +149,13 @@ Select Bitcoin Network %s replies more replies + Enter your mnemonic words + Confirm + Cancel + Loading, please wait... + Your seed phrase must be exactly 12 or 24 words + It appears you\'ve typed an invalid word + Error connecting with MQTT + Phone connected MQTT \ No newline at end of file diff --git a/sphinx/application/common/resources/src/main/res/values-zh/strings.xml b/sphinx/application/common/resources/src/main/res/values-zh/strings.xml index 0087146f31..c3514a0238 100644 --- a/sphinx/application/common/resources/src/main/res/values-zh/strings.xml +++ b/sphinx/application/common/resources/src/main/res/values-zh/strings.xml @@ -148,5 +148,13 @@ Select Bitcoin Network %s replies more replies + Enter your mnemonic words + Confirm + Cancel + Loading, please wait… + Your seed phrase must be exactly 12 or 24 words + It appears you\'ve typed an invalid word + Error connecting with MQTT + Phone connected MQTT diff --git a/sphinx/application/common/resources/src/main/res/values/strings.xml b/sphinx/application/common/resources/src/main/res/values/strings.xml index bf79311670..d4661b98ce 100644 --- a/sphinx/application/common/resources/src/main/res/values/strings.xml +++ b/sphinx/application/common/resources/src/main/res/values/strings.xml @@ -229,7 +229,6 @@ Saving this limit\nwill delete %s of your oldest content All files - Sats per minute %s left @@ -271,4 +270,13 @@ more replies Thread + Enter your mnemonic words + Confirm + Cancel + Loading, please wait... + Your seed phrase must be exactly 12 or 24 words + It appears you\'ve typed an invalid word + Error connecting with MQTT + MQTT connected + diff --git a/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/SignerTopics.kt b/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/SignerTopics.kt new file mode 100644 index 0000000000..b656be631b --- /dev/null +++ b/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/SignerTopics.kt @@ -0,0 +1,19 @@ +package chat.sphinx.wrapper_common + +object SignerTopics { + const val VLS = "vls" + const val VLS_RES = "vls-res" + const val CONTROL = "control" + const val CONTROL_RES = "control-res" + const val PROXY = "proxy" + const val PROXY_RES = "proxy-res" + const val ERROR = "error" + const val INIT_1_MSG = "init-1-msg" + const val INIT_1_RES = "init-1-res" + const val INIT_2_MSG = "init-2-msg" + const val INIT_2_RES = "init-2-res" + const val LSS_MSG = "lss-msg" + const val LSS_RES = "lss-res" + const val HELLO = "hello" + const val BYE = "bye" +} \ No newline at end of file diff --git a/sphinx/application/common/wrappers/wrapper-lightning/src/main/java/chat/sphinx/wrapper_lightning/WalletMnemonic.kt b/sphinx/application/common/wrappers/wrapper-lightning/src/main/java/chat/sphinx/wrapper_lightning/WalletMnemonic.kt index ebcb39c909..369ec008ea 100644 --- a/sphinx/application/common/wrappers/wrapper-lightning/src/main/java/chat/sphinx/wrapper_lightning/WalletMnemonic.kt +++ b/sphinx/application/common/wrappers/wrapper-lightning/src/main/java/chat/sphinx/wrapper_lightning/WalletMnemonic.kt @@ -10,7 +10,8 @@ inline fun String.toWalletMnemonic(): WalletMnemonic? = inline val String.isValidWalletMnemonic: Boolean get() { - return this.split(" ").size == 12 + val wordCount = this.split(" ").size + return wordCount == 12 || wordCount == 24 } @JvmInline diff --git a/sphinx/application/common/wrappers/wrapper-relay/src/main/java/chat/sphinx/wrapper_relay/RelayUrl.kt b/sphinx/application/common/wrappers/wrapper-relay/src/main/java/chat/sphinx/wrapper_relay/RelayUrl.kt index 27d217acc3..e533eef419 100644 --- a/sphinx/application/common/wrappers/wrapper-relay/src/main/java/chat/sphinx/wrapper_relay/RelayUrl.kt +++ b/sphinx/application/common/wrappers/wrapper-relay/src/main/java/chat/sphinx/wrapper_relay/RelayUrl.kt @@ -16,6 +16,15 @@ inline val String.isOnionAddress: Boolean .replaceFirst("https://", "") .matches("([a-z2-7]{56}).onion.*".toRegex()) +inline val String.withProtocol: String + get() { + return if (!this.contains("http")) { + "https://$this" + } else { + this + } + } + inline val String.isValidRelayUrl: Boolean get() = toRelayUrl() != null diff --git a/sphinx/application/network/concepts/queries/concept-network-query-contact/src/main/java/chat/sphinx/concept_network_query_contact/NetworkQueryContact.kt b/sphinx/application/network/concepts/queries/concept-network-query-contact/src/main/java/chat/sphinx/concept_network_query_contact/NetworkQueryContact.kt index 1e3673a623..52a3d855f7 100644 --- a/sphinx/application/network/concepts/queries/concept-network-query-contact/src/main/java/chat/sphinx/concept_network_query_contact/NetworkQueryContact.kt +++ b/sphinx/application/network/concepts/queries/concept-network-query-contact/src/main/java/chat/sphinx/concept_network_query_contact/NetworkQueryContact.kt @@ -97,6 +97,10 @@ abstract class NetworkQueryContact { relayData: Triple, RequestSignature?, RelayUrl>? = null ): Flow> + abstract fun hasAdmin( + url: RelayUrl + ): Flow> + // app.post('/contacts/:id/keys', contacts.exchangeKeys) // app.post('/contacts', contacts.createContact) } diff --git a/sphinx/application/network/concepts/queries/concept-network-query-crypter/src/main/java/chat/sphinx/concept_network_query_crypter/model/SendSeedDto.kt b/sphinx/application/network/concepts/queries/concept-network-query-crypter/src/main/java/chat/sphinx/concept_network_query_crypter/model/SendSeedDto.kt index 95db9c5ab2..c86949d65e 100644 --- a/sphinx/application/network/concepts/queries/concept-network-query-crypter/src/main/java/chat/sphinx/concept_network_query_crypter/model/SendSeedDto.kt +++ b/sphinx/application/network/concepts/queries/concept-network-query-crypter/src/main/java/chat/sphinx/concept_network_query_crypter/model/SendSeedDto.kt @@ -6,5 +6,6 @@ class SendSeedDto { var pass: String? = null var lightningNodeUrl: String? = null var pubkey: String? = null - var network: String = "regtest" + var network: String? = null + var relayUrl: String? = null } \ No newline at end of file diff --git a/sphinx/application/network/features/queries/feature-network-query-contact/src/main/java/chat/sphinx/feature_network_query_contact/NetworkQueryContactImpl.kt b/sphinx/application/network/features/queries/feature-network-query-contact/src/main/java/chat/sphinx/feature_network_query_contact/NetworkQueryContactImpl.kt index d70dfbaa3e..3e0ae45f23 100644 --- a/sphinx/application/network/features/queries/feature-network-query-contact/src/main/java/chat/sphinx/feature_network_query_contact/NetworkQueryContactImpl.kt +++ b/sphinx/application/network/features/queries/feature-network-query-contact/src/main/java/chat/sphinx/feature_network_query_contact/NetworkQueryContactImpl.kt @@ -32,6 +32,8 @@ class NetworkQueryContactImpl( private const val ENDPOINT_GENERATE_TOKEN = "/contacts/tokens" private const val ENDPOINT_KEYS_EXCHANGE = "/contacts/%d/keys" private const val ENDPOINT_GENERATE_GITHUB_PAT = "/bot/git" + private const val ENDPOINT_HAS_ADMIN = "/has_admin" + private const val ENDPOINT_CREATE_INVITE = "/invites" @@ -252,4 +254,13 @@ class NetworkQueryContactImpl( requestBody = mapOf(Pair("", "")), relayData = relayData ) + + override fun hasAdmin( + url: RelayUrl + ): Flow> = + networkRelayCall.get( + url = "${url.value}$ENDPOINT_HAS_ADMIN", + responseJsonClass = HasAdminRelayResponse::class.java, + ) + } diff --git a/sphinx/application/network/features/queries/feature-network-query-contact/src/main/java/chat/sphinx/feature_network_query_contact/model/HasAdminRelayResponse.kt b/sphinx/application/network/features/queries/feature-network-query-contact/src/main/java/chat/sphinx/feature_network_query_contact/model/HasAdminRelayResponse.kt new file mode 100644 index 0000000000..f951face57 --- /dev/null +++ b/sphinx/application/network/features/queries/feature-network-query-contact/src/main/java/chat/sphinx/feature_network_query_contact/model/HasAdminRelayResponse.kt @@ -0,0 +1,11 @@ +package chat.sphinx.feature_network_query_contact.model + +import chat.sphinx.concept_network_relay_call.RelayResponse +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +class HasAdminRelayResponse( + override val success: Boolean, + override val response: Any?, + override val error: String? +) :RelayResponse() diff --git a/sphinx/application/sphinx/.gitignore b/sphinx/application/sphinx/.gitignore index 4e1e4d00cf..796b96d1c4 100644 --- a/sphinx/application/sphinx/.gitignore +++ b/sphinx/application/sphinx/.gitignore @@ -1,18 +1 @@ /build -/src/main/jniLibs/arm64-v8a/* -/src/main/jniLibs/armeabi/* -/src/main/jniLibs/armeabi-v7a/* -/src/main/jniLibs/x86/* -/src/main/jniLibs/x86_64/* - -!/src/main/jniLibs/arm64-v8a/libcrypter.so -!/src/main/jniLibs/armeabi/libcrypter.so -!/src/main/jniLibs/armeabi-v7a/libcrypter.so -!/src/main/jniLibs/x86/libcrypter.so -!/src/main/jniLibs/x86_64/libcrypter.so - -!/src/main/jniLibs/arm64-v8a/libsphinxrs.so -!/src/main/jniLibs/armeabi/libsphinxrs.so -!/src/main/jniLibs/armeabi-v7a/libsphinxrs.so -!/src/main/jniLibs/x86/libsphinxrs.so -!/src/main/jniLibs/x86_64/libsphinxrs.so diff --git a/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libobfs4proxy.so b/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libobfs4proxy.so new file mode 100644 index 0000000000..35319201ec Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libobfs4proxy.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libpdnsd.so b/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libpdnsd.so new file mode 100644 index 0000000000..9c23b87b15 Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libpdnsd.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libsphinxrs.so b/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libsphinxrs.so index 9db909365d..d10c532c7a 100755 Binary files a/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libsphinxrs.so and b/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libsphinxrs.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libtor.so b/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libtor.so new file mode 100644 index 0000000000..c5928ad656 Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libtor.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libtun2socks.so b/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libtun2socks.so new file mode 100644 index 0000000000..69476cc9ae Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/arm64-v8a/libtun2socks.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libobfs4proxy.so b/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libobfs4proxy.so new file mode 100644 index 0000000000..f9085aab9f Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libobfs4proxy.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libpdnsd.so b/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libpdnsd.so new file mode 100644 index 0000000000..af435fe114 Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libpdnsd.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libsphinxrs.so b/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libsphinxrs.so index 26f6ce174c..445e5079f4 100755 Binary files a/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libsphinxrs.so and b/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libsphinxrs.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libtor.so b/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libtor.so new file mode 100644 index 0000000000..e689bc4991 Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libtor.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libtun2socks.so b/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libtun2socks.so new file mode 100644 index 0000000000..3d1b0acd7f Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/armeabi-v7a/libtun2socks.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/armeabi/libobfs4proxy.so b/sphinx/application/sphinx/src/main/jniLibs/armeabi/libobfs4proxy.so new file mode 100644 index 0000000000..c6901405d3 Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/armeabi/libobfs4proxy.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/armeabi/libsphinxrs.so b/sphinx/application/sphinx/src/main/jniLibs/armeabi/libsphinxrs.so index 5b9b4b5f8f..5222f902cf 100755 Binary files a/sphinx/application/sphinx/src/main/jniLibs/armeabi/libsphinxrs.so and b/sphinx/application/sphinx/src/main/jniLibs/armeabi/libsphinxrs.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/x86/libobfs4proxy.so b/sphinx/application/sphinx/src/main/jniLibs/x86/libobfs4proxy.so new file mode 100644 index 0000000000..7b3041a614 Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/x86/libobfs4proxy.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/x86/libpdnsd.so b/sphinx/application/sphinx/src/main/jniLibs/x86/libpdnsd.so new file mode 100644 index 0000000000..4799d46b1f Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/x86/libpdnsd.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/x86/libsphinxrs.so b/sphinx/application/sphinx/src/main/jniLibs/x86/libsphinxrs.so index 90d5b86746..35d60981a9 100755 Binary files a/sphinx/application/sphinx/src/main/jniLibs/x86/libsphinxrs.so and b/sphinx/application/sphinx/src/main/jniLibs/x86/libsphinxrs.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/x86/libtor.so b/sphinx/application/sphinx/src/main/jniLibs/x86/libtor.so new file mode 100644 index 0000000000..3ac3fba705 Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/x86/libtor.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/x86/libtun2socks.so b/sphinx/application/sphinx/src/main/jniLibs/x86/libtun2socks.so new file mode 100644 index 0000000000..1bce0e5066 Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/x86/libtun2socks.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/x86_64/libobfs4proxy.so b/sphinx/application/sphinx/src/main/jniLibs/x86_64/libobfs4proxy.so new file mode 100644 index 0000000000..fc7cbbf91e Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/x86_64/libobfs4proxy.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/x86_64/libpdnsd.so b/sphinx/application/sphinx/src/main/jniLibs/x86_64/libpdnsd.so new file mode 100644 index 0000000000..5b0501ea6f Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/x86_64/libpdnsd.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/x86_64/libsphinxrs.so b/sphinx/application/sphinx/src/main/jniLibs/x86_64/libsphinxrs.so index 300d385c9d..8369fdaca9 100755 Binary files a/sphinx/application/sphinx/src/main/jniLibs/x86_64/libsphinxrs.so and b/sphinx/application/sphinx/src/main/jniLibs/x86_64/libsphinxrs.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/x86_64/libtor.so b/sphinx/application/sphinx/src/main/jniLibs/x86_64/libtor.so new file mode 100644 index 0000000000..d7eb3e88ee Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/x86_64/libtor.so differ diff --git a/sphinx/application/sphinx/src/main/jniLibs/x86_64/libtun2socks.so b/sphinx/application/sphinx/src/main/jniLibs/x86_64/libtun2socks.so new file mode 100644 index 0000000000..8072c62d28 Binary files /dev/null and b/sphinx/application/sphinx/src/main/jniLibs/x86_64/libtun2socks.so differ diff --git a/sphinx/screens-detail/manage-storage/manage-storage/src/main/res/layout/layout_change_storage_limit_detail.xml b/sphinx/screens-detail/manage-storage/manage-storage/src/main/res/layout/layout_change_storage_limit_detail.xml index 15d0d9316f..4a3c38c91f 100644 --- a/sphinx/screens-detail/manage-storage/manage-storage/src/main/res/layout/layout_change_storage_limit_detail.xml +++ b/sphinx/screens-detail/manage-storage/manage-storage/src/main/res/layout/layout_change_storage_limit_detail.xml @@ -168,7 +168,9 @@ android:text="@string/manage_storage_save" android:textAllCaps="false" android:textColor="@color/text" - android:textSize="@dimen/chat_footer_episode_title_text_size" /> + android:textSize="@dimen/chat_footer_episode_title_text_size" + tools:layout_editor_absoluteX="0dp" + tools:layout_editor_absoluteY="0dp" /> diff --git a/sphinx/screens/dashboard/dashboard/build.gradle b/sphinx/screens/dashboard/dashboard/build.gradle index e58c200eb6..691a02d9e0 100644 --- a/sphinx/screens/dashboard/dashboard/build.gradle +++ b/sphinx/screens/dashboard/dashboard/build.gradle @@ -69,7 +69,7 @@ dependencies { implementation project(path: ':sphinx:screens-detail:tribes-discover:tribes-discover-view-model-coordinator') implementation project(path: ':sphinx:service:concepts:concept-service-notification') implementation project(path: ':sphinx:application:common:logger') - + implementation project(path: ':sphinx:activity:concepts:concept-signer-manager') api project(path: ':sphinx:service:concepts:concept-service-media-player') @@ -77,7 +77,6 @@ dependencies { implementation deps.androidx.recyclerView implementation deps.androidx.lifecycle.hilt implementation deps.google.hilt - implementation deps.bip39.sdk implementation deps.jna.sdk implementation deps.lottie.sdk implementation deps.square.moshi diff --git a/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/ChatListSideEffect.kt b/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/ChatListSideEffect.kt index f82e97f685..0deb9b0f3c 100644 --- a/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/ChatListSideEffect.kt +++ b/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/ChatListSideEffect.kt @@ -1,18 +1,9 @@ package chat.sphinx.dashboard.ui import android.app.AlertDialog -import android.content.ClipData -import android.content.ClipboardManager import android.content.Context -import android.view.ViewGroup -import android.view.inputmethod.InputMethodManager -import android.widget.EditText -import android.widget.FrameLayout import chat.sphinx.dashboard.R -import chat.sphinx.dashboard.ui.DashboardViewModel.Companion.BITCOIN_NETWORK_MAIN_NET -import chat.sphinx.dashboard.ui.DashboardViewModel.Companion.BITCOIN_NETWORK_REG_TEST import chat.sphinx.resources.SphinxToastUtils -import chat.sphinx.wrapper_chat.Chat import io.matthewnelson.android_feature_toast_utils.show import io.matthewnelson.concept_views.sideeffect.SideEffect import java.util.Locale @@ -81,157 +72,6 @@ sealed class ChatListSideEffect: SideEffect() { } } - class CheckNetwork( - private val callback: () -> Unit, - ): ChatListSideEffect() { - override suspend fun execute(value: Context) { - val builder = AlertDialog.Builder(value, R.style.AlertDialogTheme) - builder.setTitle(value.getString(R.string.network_check_title)) - builder.setMessage(value.getString(R.string.network_check_message)) - builder.setNegativeButton(R.string.no) { _, _ -> } - builder.setPositiveButton(R.string.yes) { _, _ -> - callback.invoke() - } - builder.show() - } - } - - class FailedToSetupSigningDevice( - private val errorMessage: String - ): ChatListSideEffect() { - override suspend fun execute(value: Context) { - SphinxToastUtils(true).show( - value, - value.getString(R.string.error_setting_up_signing_device, errorMessage) - ) - } - } - - class SigningDeviceInfo( - private val title: String, - private val message: String, - private val defaultValue: String? = null, - private val inputType: Int? = null, - private val callback: (String?) -> Unit, - ): ChatListSideEffect() { - - override suspend fun execute(value: Context) { - - val inputEditTextField = EditText(value) - inputEditTextField.isSingleLine = true - inputType?.let { - inputEditTextField.inputType = it - } - inputEditTextField.setOnFocusChangeListener { _, _ -> - inputEditTextField.post { - val inputMethodManager: InputMethodManager = - value.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.showSoftInput(inputEditTextField, InputMethodManager.SHOW_IMPLICIT) - } - } - inputEditTextField.requestFocus() - inputEditTextField.setText(defaultValue) - - val container = FrameLayout(value) - val params: FrameLayout.LayoutParams = FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - params.leftMargin = value.resources.getDimensionPixelSize(R.dimen.default_layout_margin) - params.rightMargin = value.resources.getDimensionPixelSize(R.dimen.default_layout_margin) - inputEditTextField.layoutParams = params - container.addView(inputEditTextField) - - val builder = AlertDialog.Builder(value, R.style.AlertDialogTheme) - builder.setTitle(title) - builder.setMessage(message) - builder.setView(container) - builder.setPositiveButton(android.R.string.ok) { _, _ -> - val editTextInput = inputEditTextField.text.toString() - - callback.invoke( - if (editTextInput.isEmpty()) { - null - } else { - editTextInput - } - ) - } - builder.setNegativeButton(android.R.string.cancel) { _, _ -> } - - builder.show() - } - } - - class CheckBitcoinNetwork( - private val regTestCallback: () -> Unit, - private val mainNetCallback: () -> Unit, - private val callback: () -> Unit, - ): ChatListSideEffect() { - override suspend fun execute(value: Context) { - val builder = AlertDialog.Builder(value, R.style.AlertDialogTheme) - builder.setTitle(value.getString(R.string.select_bitcoin_network)) - - val items = arrayOf(BITCOIN_NETWORK_REG_TEST.toCapitalized(), BITCOIN_NETWORK_MAIN_NET.toCapitalized()) - builder.setSingleChoiceItems(items, 0 - ) { _, p1 -> - when (p1) { - 0 -> { - regTestCallback.invoke() - } - 1 -> { - mainNetCallback.invoke() - } - else -> {} - } - } - builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> - dialog.dismiss() - } - builder.setPositiveButton(android.R.string.ok) { _, _ -> - callback.invoke() - } - builder.show() - } - } - - class ShowMnemonicToUser( - private val mnemonic: String, - private val callback: () -> Unit, - ): ChatListSideEffect() { - override suspend fun execute(value: Context) { - val builder = AlertDialog.Builder(value, R.style.AlertDialogTheme) - builder.setTitle(value.getString(R.string.store_mnemonic)) - builder.setMessage(mnemonic) - builder.setNeutralButton(android.R.string.copy) { _, _ -> - (value.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.let { manager -> - val clipData = ClipData.newPlainText("mnemonic", mnemonic) - manager.setPrimaryClip(clipData) - - SphinxToastUtils().show(value, R.string.mnemonic_copied_to_clipboard) - } - callback.invoke() - } - builder.setPositiveButton(android.R.string.ok) { _, _ -> - callback.invoke() - } - builder.show() - } - } - - object SendingSeedToHardware: ChatListSideEffect() { - override suspend fun execute(value: Context) { - SphinxToastUtils().show(value, R.string.sending_seed) - } - } - - object SigningDeviceSuccessfullySet: ChatListSideEffect() { - override suspend fun execute(value: Context) { - SphinxToastUtils(true).show(value, R.string.signing_device_successfully_set) - } - } - - @Suppress("NOTHING_TO_INLINE") inline fun String.toCapitalized(): String { return this.replaceFirstChar { diff --git a/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardFragment.kt b/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardFragment.kt index 9e776b59ce..57c6d77167 100644 --- a/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardFragment.kt +++ b/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardFragment.kt @@ -24,6 +24,7 @@ import chat.sphinx.concept_image_loader.Disposable import chat.sphinx.concept_image_loader.ImageLoader import chat.sphinx.concept_image_loader.ImageLoaderOptions import chat.sphinx.concept_image_loader.Transformation +import chat.sphinx.concept_signer_manager.SignerManager import chat.sphinx.dashboard.R import chat.sphinx.dashboard.databinding.FragmentDashboardBinding import chat.sphinx.dashboard.ui.viewstates.* @@ -67,6 +68,10 @@ internal class DashboardFragment : MotionLayoutFragment< @Suppress("ProtectedInFinal") protected lateinit var imageLoader: ImageLoader + @Inject + @Suppress("ProtectedInFinal") + protected lateinit var signerManager: SignerManager + override val viewModel: DashboardViewModel by viewModels() private val dashboardPodcastViewModel: DashboardPodcastViewModel by viewModels() @@ -98,6 +103,7 @@ internal class DashboardFragment : MotionLayoutFragment< setupPopups() setupRestorePopup() setupBottomMenuScanner() + setupSignerManager() } override fun onPause() { @@ -122,6 +128,10 @@ internal class DashboardFragment : MotionLayoutFragment< viewModel.networkRefresh(true) } + private fun setupSignerManager(){ + viewModel.setSignerManager(signerManager) + } + fun closeDrawerIfOpen(): Boolean { if (viewModel.currentViewState is DashboardMotionViewState.DrawerOpenNavBarHidden) { viewModel.updateViewState(DashboardMotionViewState.DrawerCloseNavBarHidden) diff --git a/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardViewModel.kt b/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardViewModel.kt index c96f9855c2..769ae568a1 100644 --- a/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardViewModel.kt +++ b/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardViewModel.kt @@ -4,16 +4,11 @@ import android.app.Application import android.content.Context import android.content.Intent import android.net.Uri -import android.text.InputType import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import app.cash.exhaustive.Exhaustive -import cash.z.ecc.android.bip39.Mnemonics -import cash.z.ecc.android.bip39.toEntropy -import cash.z.ecc.android.bip39.toSeed import chat.sphinx.concept_background_login.BackgroundLoginHandler import chat.sphinx.concept_network_query_crypter.NetworkQueryCrypter -import chat.sphinx.concept_network_query_crypter.model.SendSeedDto import chat.sphinx.concept_network_query_lightning.NetworkQueryLightning import chat.sphinx.concept_network_query_lightning.model.invoice.PayRequestDto import chat.sphinx.concept_network_query_lightning.model.invoice.PostRequestPaymentDto @@ -32,6 +27,8 @@ import chat.sphinx.concept_repository_dashboard_android.RepositoryDashboardAndro import chat.sphinx.concept_repository_feed.FeedRepository import chat.sphinx.concept_service_media.MediaPlayerServiceController import chat.sphinx.concept_service_notification.PushNotificationRegistrar +import chat.sphinx.concept_signer_manager.SignerManager +import chat.sphinx.concept_signer_manager.SignerPhoneCallback import chat.sphinx.concept_socket_io.SocketIOManager import chat.sphinx.concept_socket_io.SocketIOState import chat.sphinx.concept_view_model_coordinator.ViewModelCoordinator @@ -63,9 +60,8 @@ import chat.sphinx.wrapper_common.tribe.toTribeJoinLink import chat.sphinx.wrapper_contact.* import chat.sphinx.wrapper_feed.* import chat.sphinx.wrapper_lightning.NodeBalance -import chat.sphinx.wrapper_lightning.WalletMnemonic -import chat.sphinx.wrapper_lightning.toWalletMnemonic import chat.sphinx.wrapper_relay.RelayUrl +import com.squareup.moshi.Moshi import dagger.hilt.android.lifecycle.HiltViewModel import io.matthewnelson.android_feature_navigation.util.navArgs import io.matthewnelson.android_feature_viewmodel.MotionLayoutViewModel @@ -74,13 +70,8 @@ import io.matthewnelson.android_feature_viewmodel.updateViewState import io.matthewnelson.build_config.BuildConfigVersionCode import io.matthewnelson.concept_coroutines.CoroutineDispatchers import io.matthewnelson.concept_views.viewstate.ViewStateContainer -import io.matthewnelson.crypto_common.extensions.toHex import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import uniffi.sphinxrs.deriveSharedSecret -import uniffi.sphinxrs.encrypt -import uniffi.sphinxrs.pubkeyFromSecretKey -import java.security.SecureRandom import javax.inject.Inject @HiltViewModel @@ -117,6 +108,7 @@ internal class DashboardViewModel @Inject constructor( private val mediaPlayerServiceController: MediaPlayerServiceController, private val scannerCoordinator: ViewModelCoordinator, + private val moshi: Moshi, private val LOG: SphinxLogger, private val socketIOManager: SocketIOManager, @@ -126,7 +118,9 @@ internal class DashboardViewModel @Inject constructor( ChatListSideEffect, DashboardMotionViewState >(dispatchers, DashboardMotionViewState.DrawerCloseNavBarVisible), - ScannerMenuViewModel{ + ScannerMenuViewModel, + SignerPhoneCallback +{ private val args: DashboardFragmentArgs by handler.navArgs() @@ -142,13 +136,7 @@ internal class DashboardViewModel @Inject constructor( MutableStateFlow(null) } - companion object { - const val SIGNING_DEVICE_SHARED_PREFERENCES = "general_settings" - const val SIGNING_DEVICE_SETUP_KEY = "signing-device-setup" - - const val BITCOIN_NETWORK_REG_TEST = "regtest" - const val BITCOIN_NETWORK_MAIN_NET = "mainnet" - } + private lateinit var signerManager: SignerManager init { if (args.updateBackgroundLoginTime) { @@ -194,8 +182,6 @@ internal class DashboardViewModel @Inject constructor( handleRedeemSatsLink(redeemSatsLink) } ?: deepLink?.toFeedItemLink()?.let { feedItemLink -> handleFeedItemLink(feedItemLink) - } ?: deepLink?.toLightningNodeLink()?.let { lightningNodeLink -> - handleLightningNodeLink(lightningNodeLink) } } } @@ -210,6 +196,44 @@ internal class DashboardViewModel @Inject constructor( feedRepository.setRecommendationsToggle(feedRecommendationsToggle) } + fun setSignerManager(signerManager: SignerManager) { + signerManager.setWalletDataHandler(walletDataHandler) + signerManager.setMoshi(moshi) + + this.signerManager = signerManager + + reconnectMQTT() + } + + private fun reconnectMQTT(){ + signerManager.reconnectMQTT(this) + } + + override fun showMnemonicToUser(message: String, callback: (Boolean) -> Unit) { + return + } + + override fun phoneSignerSuccessfullySet() { + viewModelScope.launch(mainImmediate) { + submitSideEffect( + ChatListSideEffect.Notify( + app.getString(R.string.signer_phone_connected_to_mqtt) + ) + ) + + } + } + + override fun phoneSignerSetupError() { + viewModelScope.launch(mainImmediate) { + submitSideEffect( + ChatListSideEffect.Notify( + app.getString(R.string.signer_phone_error_mqtt) + ) + ) + } + } + fun toScanner(isPayment: Boolean) { viewModelScope.launch(mainImmediate) { val response = scannerCoordinator.submitRequest( @@ -297,8 +321,6 @@ internal class DashboardViewModel @Inject constructor( ) } } catch (e: Exception) {} - } ?: code.toLightningNodeLink()?.let { lightningNodeLink -> - handleLightningNodeLink(lightningNodeLink) } } } @@ -487,213 +509,6 @@ internal class DashboardViewModel @Inject constructor( } } - private var seedDto = SendSeedDto() - - private fun resetSeedDto() { - seedDto = SendSeedDto() - } - - private var setupSigningDeviceJob: Job? = null - private fun handleLightningNodeLink(link: LightningNodeLink) { - if (setupSigningDeviceJob?.isActive == true) return - - setupSigningDeviceJob = viewModelScope.launch(mainImmediate) { - submitSideEffect(ChatListSideEffect.CheckNetwork { - viewModelScope.launch(mainImmediate) { - submitSideEffect(ChatListSideEffect.SigningDeviceInfo( - app.getString(R.string.network_name_title), - app.getString(R.string.network_name_message) - ) { networkName -> - viewModelScope.launch(mainImmediate) { - if (networkName == null) { - submitSideEffect(ChatListSideEffect.FailedToSetupSigningDevice("Network can not be empty")) - return@launch - } - - seedDto.ssid = networkName - - submitSideEffect(ChatListSideEffect.SigningDeviceInfo( - app.getString(R.string.network_password_title), - app.getString( - R.string.network_password_message, - networkName ?: "-" - ), - inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - ) { networkPass -> - viewModelScope.launch(mainImmediate) { - if (networkPass == null) { - submitSideEffect(ChatListSideEffect.FailedToSetupSigningDevice("Network password can not be empty")) - return@launch - } - - seedDto.pass = networkPass - seedDto.lightningNodeUrl = link.lightningMqtt - - if (link.lightningNetwork.isBitcoinNetwork()) { - seedDto.network = BITCOIN_NETWORK_MAIN_NET - linkSigningDevice() - } - else { - submitSideEffect(ChatListSideEffect.CheckBitcoinNetwork( - regTestCallback = { - seedDto.network = BITCOIN_NETWORK_REG_TEST - }, mainNetCallback = { - seedDto.network = BITCOIN_NETWORK_MAIN_NET - }, callback = { - viewModelScope.launch(mainImmediate) { - linkSigningDevice() - } - } - )) - } - } - }) - } - }) - } - }) - } - } - - private suspend fun linkSigningDevice() { - val secKey = ByteArray(32) - SecureRandom().nextBytes(secKey) - - val sk1 = secKey.toHex() - val pk1 = pubkeyFromSecretKey(sk1) - - var pk2 : String? = null - - if (pk1 == null) { - submitSideEffect(ChatListSideEffect.FailedToSetupSigningDevice("error generating secret key")) - resetSeedDto() - return - } - - seedDto.pubkey = pk1 - - if ( - seedDto.lightningNodeUrl == null || - seedDto.lightningNodeUrl?.isEmpty() == true - ) { - resetSeedDto() - submitSideEffect(ChatListSideEffect.FailedToSetupSigningDevice("lightning node URL can't be empty")) - return - } - - networkQueryCrypter.getCrypterPubKey().collect { loadResponse -> - when (loadResponse) { - is LoadResponse.Loading -> {} - is Response.Error -> { - resetSeedDto() - submitSideEffect(ChatListSideEffect.FailedToSetupSigningDevice("error getting public key from hardware")) - } - is Response.Success -> { - pk2 = loadResponse.value.pubkey - } - } - } - - pk2?.let { nnPk2 -> - val sec1 = deriveSharedSecret(nnPk2, sk1) - val seedAndMnemonic = generateAndPersistMnemonic() - - seedAndMnemonic.second?.let { mnemonic -> - submitSideEffect(ChatListSideEffect.ShowMnemonicToUser( - mnemonic.value - ) { - seedAndMnemonic.first?.let { seed -> - viewModelScope.launch(mainImmediate) { - encryptAndSendSeed(seed, sec1) - } - } - }) - } - } - } - - private suspend fun generateAndPersistMnemonic() : Pair { - var walletMnemonic: WalletMnemonic? = null - var seed: String? = null - - viewModelScope.launch(mainImmediate) { - walletMnemonic = walletDataHandler.retrieveWalletMnemonic() ?: run { - val entropy = (Mnemonics.WordCount.COUNT_12).toEntropy() - - Mnemonics.MnemonicCode(entropy).use { mnemonicCode -> - val wordsArray:MutableList = mutableListOf() - mnemonicCode.words.forEach { word -> - wordsArray.add(word.joinToString("")) - } - val words = wordsArray.joinToString(" ") - - words.toWalletMnemonic()?.let { walletMnemonic -> - if (walletDataHandler.persistWalletMnemonic(walletMnemonic)) { - LOG.d("MNEMONIC WORDS SAVED" , words) - LOG.d("MNEMONIC WORDS SAVED" , words) - } - walletMnemonic - } - } - } - - walletMnemonic?.value?.toCharArray()?.let { words -> - val mnemonic = Mnemonics.MnemonicCode(words) - - val seedData = mnemonic.toSeed().take(32).toByteArray() - seed = seedData.toHex() - } - }.join() - - return Pair(seed, walletMnemonic) - } - - private suspend fun encryptAndSendSeed( - seed: String, - sec1: String - ) { - val nonce = ByteArray(12) - SecureRandom().nextBytes(nonce) - - encrypt(seed, sec1, nonce.toHex()).let { cipher -> - if (cipher.isNotEmpty()) { - seedDto.seed = cipher - - submitSideEffect(ChatListSideEffect.SendingSeedToHardware) - - networkQueryCrypter.sendEncryptedSeed(seedDto).collect { loadResponse -> - when (loadResponse) { - is LoadResponse.Loading -> {} - is Response.Error -> { - resetSeedDto() - submitSideEffect(ChatListSideEffect.FailedToSetupSigningDevice("error sending seed to hardware")) - } - - is Response.Success -> { - submitSideEffect(ChatListSideEffect.SigningDeviceSuccessfullySet) - setSigningDeviceSetupDone() - } - } - } - } - } - } - - private suspend fun setSigningDeviceSetupDone() { - val appContext: Context = app.applicationContext - val sharedPreferences = appContext.getSharedPreferences(SIGNING_DEVICE_SHARED_PREFERENCES, Context.MODE_PRIVATE) - - withContext(dispatchers.io) { - sharedPreferences.edit().putBoolean(SIGNING_DEVICE_SETUP_KEY, true) - .let { editor -> - if (!editor.commit()) { - editor.apply() - } - } - } - } - - private suspend fun goToFeedDetailView(feed: Feed) { when { feed.isPodcast -> { diff --git a/sphinx/screens/onboard/onboard-common/src/main/java/chat/sphinx/onboard_common/model/RedemptionCode.kt b/sphinx/screens/onboard/onboard-common/src/main/java/chat/sphinx/onboard_common/model/RedemptionCode.kt index 5f41a473b3..8da981a759 100644 --- a/sphinx/screens/onboard/onboard-common/src/main/java/chat/sphinx/onboard_common/model/RedemptionCode.kt +++ b/sphinx/screens/onboard/onboard-common/src/main/java/chat/sphinx/onboard_common/model/RedemptionCode.kt @@ -51,6 +51,21 @@ sealed class RedemptionCode { } } } + + if (code.startsWith(Glyph.REGEX)) { + val parameters = code.substringAfter(Glyph.REGEX).split("&").mapNotNull { code -> + code.split("=").takeIf { it.size == 2 }?.let { pair -> + pair[0] to pair[1] + } + }.toMap() + + val mqtt = parameters[Glyph.PARAM_MQTT] ?: return null + val network = parameters[Glyph.PARAM_NETWORK] ?: return null + val relay = parameters[Glyph.PARAM_RELAY] ?: return null + + return Glyph(mqtt, network, relay) + } + return null } } @@ -161,4 +176,24 @@ sealed class RedemptionCode { SwarmClaim(ip, token) } } + + @Suppress("DataClassPrivateConstructor") + data class Glyph private constructor( + val mqtt: String, + val network: String, + val relay: String, + ) : RedemptionCode() { + + companion object { + const val REGEX = "sphinx.chat://?action=glyph" + const val PARAM_MQTT = "mqtt" + const val PARAM_NETWORK = "network" + const val PARAM_RELAY = "relay" + + @JvmSynthetic + internal operator fun invoke(mqtt: String, network: String, relay: String): Glyph = + Glyph(mqtt, network, relay) + } + } + } diff --git a/sphinx/screens/onboard/onboard-connect/build.gradle b/sphinx/screens/onboard/onboard-connect/build.gradle index 0973e35696..8d3872152a 100644 --- a/sphinx/screens/onboard/onboard-connect/build.gradle +++ b/sphinx/screens/onboard/onboard-connect/build.gradle @@ -36,12 +36,18 @@ dependencies { implementation deps.androidx.lifecycle.hilt implementation deps.google.hilt + implementation deps.square.moshi // Sphinx implementation project(path: ':sphinx:activity:insetter-activity') implementation project(path: ':sphinx:screens-detail:scanner:scanner-view-model-coordinator') + implementation project(path: ':sphinx:activity:concepts:concept-signer-manager') + implementation project(path: ':sphinx:application:data:concepts:concept-wallet') + implementation project(path: ':sphinx:application:network:concepts:queries:concept-network-query-crypter') api project(path: ':sphinx:screens:onboard:onboard-common') api project(path: ':sphinx:screens:onboard:onboard-resources') + api project(path: ':sphinx:application:common:menus:menu-bottom-signer') + api project(path: ':sphinx:application:common:menus:menu-bottom-phone-signer-method') kapt kaptDeps.google.hilt diff --git a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/navigation/OnBoardConnectNavigator.kt b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/navigation/OnBoardConnectNavigator.kt index 59b2b53952..03ee420f24 100644 --- a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/navigation/OnBoardConnectNavigator.kt +++ b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/navigation/OnBoardConnectNavigator.kt @@ -10,7 +10,7 @@ abstract class OnBoardConnectNavigator( ): Navigator(navigationDriver) { abstract suspend fun toOnBoardConnectingScreen( - code: String, + code: String?, ) suspend fun popBackStack() { diff --git a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectFragment.kt b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectFragment.kt index 642e12e80a..a84c10fa83 100644 --- a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectFragment.kt +++ b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectFragment.kt @@ -7,22 +7,35 @@ import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.View +import android.view.inputmethod.InputMethodManager +import androidx.activity.OnBackPressedCallback import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity import androidx.fragment.app.viewModels +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import by.kirich1409.viewbindingdelegate.viewBinding +import chat.sphinx.concept_signer_manager.SignerManager import chat.sphinx.insetter_activity.InsetterActivity import chat.sphinx.insetter_activity.addNavigationBarPadding import chat.sphinx.insetter_activity.addStatusBarPadding +import chat.sphinx.menu_bottom_phone_signer_method.PhoneSignerMethodMenu +import chat.sphinx.menu_bottom_signer.BottomSignerMenu import chat.sphinx.onboard_connect.R import chat.sphinx.onboard_connect.databinding.FragmentOnBoardConnectBinding +import chat.sphinx.onboard_connect.viewstate.MnemonicDialogViewState +import chat.sphinx.onboard_connect.viewstate.MnemonicWordsViewState +import chat.sphinx.onboard_connect.viewstate.OnBoardConnectSubmitButtonViewState +import chat.sphinx.onboard_connect.viewstate.OnBoardConnectViewState import dagger.hilt.android.AndroidEntryPoint import io.matthewnelson.android_feature_screens.ui.sideeffect.SideEffectFragment import io.matthewnelson.android_feature_screens.util.gone import io.matthewnelson.android_feature_screens.util.visible import io.matthewnelson.concept_views.viewstate.collect +import io.matthewnelson.concept_views.viewstate.value import kotlinx.coroutines.launch import javax.annotation.meta.Exhaustive +import javax.inject.Inject @AndroidEntryPoint internal class OnBoardConnectFragment: SideEffectFragment< @@ -36,11 +49,31 @@ internal class OnBoardConnectFragment: SideEffectFragment< override val viewModel: OnBoardConnectViewModel by viewModels() override val binding: FragmentOnBoardConnectBinding by viewBinding(FragmentOnBoardConnectBinding::bind) + @Inject + @Suppress("ProtectedInFinal") + protected lateinit var signerManager: SignerManager + + private val bottomMenuSigner: BottomSignerMenu by lazy(LazyThreadSafetyMode.NONE) { + BottomSignerMenu( + onStopSupervisor, + viewModel + ) + } + + private val phoneSignerMethodMenu: PhoneSignerMethodMenu by lazy(LazyThreadSafetyMode.NONE) { + PhoneSignerMethodMenu( + onStopSupervisor, + viewModel + ) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + BackPressHandler(viewLifecycleOwner, requireActivity()) setupHeaderAndFooter() setupEditText() + setupSignerManager() binding.apply { imageButtonScanner.setOnClickListener { @@ -57,8 +90,44 @@ internal class OnBoardConnectFragment: SideEffectFragment< viewModel.continueToConnectingScreen( binding.editTextCodeInput.text.toString() ) + + hideKeyboardFrom(buttonSubmit.context, buttonSubmit) + } + + includeLayoutMnemonicWords.includeLayoutMnemonicWordsDetail.apply { + buttonCancel.setOnClickListener { + viewModel.mnemonicWordsViewStateContainer.updateViewState(MnemonicWordsViewState.Closed) + } + + buttonConfirm.setOnClickListener { + val words = editTextMnemonic.text.toString() + viewModel.validateSeed(words) + + hideKeyboardFrom(buttonConfirm.context, buttonConfirm) + } } } + + bottomMenuSigner.initialize( + R.string.bottom_menu_signer_header_text, + binding.includeLayoutMenuBottomSigner, + viewLifecycleOwner + ) + + phoneSignerMethodMenu.initialize( + R.string.bottom_menu_phone_signer_method_header_text, + binding.includeLayoutMenuBottomPhoneSignerMethod, + viewLifecycleOwner + ) + } + + private fun setupSignerManager(){ + viewModel.setSignerManager(signerManager) + } + + private fun hideKeyboardFrom(context: Context, view: View) { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(view.windowToken, 0) } private fun setupHeaderAndFooter() { @@ -67,6 +136,35 @@ internal class OnBoardConnectFragment: SideEffectFragment< .addNavigationBarPadding(binding.layoutConstraintOnBoardConnect) } + private inner class BackPressHandler( + owner: LifecycleOwner, + activity: FragmentActivity, + ): OnBackPressedCallback(true) { + + init { + activity.apply { + onBackPressedDispatcher.addCallback( + owner, + this@BackPressHandler, + ) + } + } + + override fun handleOnBackPressed() { + when { + (viewModel.mnemonicWordsViewStateContainer.value is MnemonicWordsViewState.Open) -> { + viewModel.mnemonicWordsViewStateContainer.updateViewState(MnemonicWordsViewState.Closed) + } + else -> { + lifecycleScope.launch(viewModel.mainImmediate) { + viewModel.navigator.popBackStack() + } + } + } + } + } + + private fun setupEditText() { binding.editTextCodeInput.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} @@ -150,5 +248,36 @@ internal class OnBoardConnectFragment: SideEffectFragment< } } } + + onStopSupervisor.scope.launch(viewModel.mainImmediate) { + viewModel.mnemonicWordsViewStateContainer.collect { viewState -> + binding.includeLayoutMnemonicWords.apply { + when (viewState) { + is MnemonicWordsViewState.Open -> {} + is MnemonicWordsViewState.Closed -> {} + } + root.setTransitionDuration(300) + viewState.transitionToEndSet(root) + } + } + } + + onStopSupervisor.scope.launch(viewModel.mainImmediate) { + viewModel.mnemonicDialogViewStateContainer.collect { viewState -> + binding.includeLayoutMnemonicWords.includeLayoutMnemonicWordsDetail.apply { + when (viewState) { + is MnemonicDialogViewState.Idle -> { + layoutConstraintEnterWordsContainer.visible + layoutConstraintLoadingContainer.gone + } + is MnemonicDialogViewState.Loading -> { + layoutConstraintLoadingContainer.visible + layoutConstraintEnterWordsContainer.gone + } + } + } + } + } + } } \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectSideEffect.kt b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectSideEffect.kt index 27a55fef86..bada18144c 100644 --- a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectSideEffect.kt +++ b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectSideEffect.kt @@ -1,10 +1,22 @@ package chat.sphinx.onboard_connect.ui +import android.app.AlertDialog +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import android.widget.FrameLayout +import chat.sphinx.onboard_connect.R +import chat.sphinx.onboard_connect.ui.OnBoardConnectViewModel.Companion.BITCOIN_NETWORK_MAIN_NET +import chat.sphinx.onboard_connect.ui.OnBoardConnectViewModel.Companion.BITCOIN_NETWORK_REG_TEST import chat.sphinx.resources.SphinxToastUtils import chat.sphinx.scanner_view_model_coordinator.response.ScannerResponse +import chat.sphinx.wrapper_relay.RelayUrl import io.matthewnelson.android_feature_toast_utils.show import io.matthewnelson.concept_views.sideeffect.SideEffect +import java.util.Locale internal sealed class OnBoardConnectSideEffect: SideEffect() { @@ -20,4 +32,163 @@ internal sealed class OnBoardConnectSideEffect: SideEffect() { SphinxToastUtils(toastLengthLong = notificationLengthLong).show(value, msg) } } -} \ No newline at end of file + + class SigningDeviceInfo( + private val title: String, + private val message: String, + private val defaultValue: String? = null, + private val inputType: Int? = null, + private val callback: (String?) -> Unit, + ): OnBoardConnectSideEffect() { + + override suspend fun execute(value: Context) { + + val inputEditTextField = EditText(value) + inputEditTextField.isSingleLine = true + inputType?.let { + inputEditTextField.inputType = it + } + inputEditTextField.setOnFocusChangeListener { _, _ -> + inputEditTextField.post { + val inputMethodManager: InputMethodManager = + value.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.showSoftInput(inputEditTextField, InputMethodManager.SHOW_IMPLICIT) + } + } + inputEditTextField.requestFocus() + inputEditTextField.setText(defaultValue) + + val container = FrameLayout(value) + val params: FrameLayout.LayoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + params.leftMargin = value.resources.getDimensionPixelSize(R.dimen.default_layout_margin) + params.rightMargin = value.resources.getDimensionPixelSize(R.dimen.default_layout_margin) + inputEditTextField.layoutParams = params + container.addView(inputEditTextField) + + val builder = AlertDialog.Builder(value, R.style.AlertDialogTheme) + builder.setTitle(title) + builder.setMessage(message) + builder.setView(container) + builder.setPositiveButton(android.R.string.ok) { _, _ -> + val editTextInput = inputEditTextField.text.toString() + + callback.invoke( + if (editTextInput.isEmpty()) { + null + } else { + editTextInput + } + ) + } + builder.setNegativeButton(android.R.string.cancel) { _, _ -> } + + builder.show() + } + } + + class CheckNetwork( + private val callback: () -> Unit, + ): OnBoardConnectSideEffect() { + override suspend fun execute(value: Context) { + val builder = AlertDialog.Builder(value, R.style.AlertDialogTheme) + builder.setTitle(value.getString(R.string.network_check_title)) + builder.setMessage(value.getString(R.string.network_check_message)) + builder.setNegativeButton(R.string.no) { _, _ -> } + builder.setPositiveButton(R.string.yes) { _, _ -> + callback.invoke() + } + builder.show() + } + } + + class ShowMnemonicToUser( + private val mnemonic: String, + private val callback: () -> Unit, + ): OnBoardConnectSideEffect() { + override suspend fun execute(value: Context) { + val builder = AlertDialog.Builder(value, R.style.AlertDialogTheme) + builder.setTitle(value.getString(R.string.store_mnemonic)) + builder.setMessage(mnemonic) + builder.setNeutralButton(android.R.string.copy) { _, _ -> + (value.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.let { manager -> + val clipData = ClipData.newPlainText("mnemonic", mnemonic) + manager.setPrimaryClip(clipData) + + SphinxToastUtils().show(value, R.string.mnemonic_copied_to_clipboard) + } + callback.invoke() + } + builder.setPositiveButton(android.R.string.ok) { _, _ -> + callback.invoke() + } + builder.show() + } + } + + class CheckBitcoinNetwork( + private val regTestCallback: () -> Unit, + private val mainNetCallback: () -> Unit, + private val callback: () -> Unit, + ): OnBoardConnectSideEffect() { + override suspend fun execute(value: Context) { + val builder = AlertDialog.Builder(value, R.style.AlertDialogTheme) + builder.setTitle(value.getString(R.string.select_bitcoin_network)) + + val items = arrayOf(BITCOIN_NETWORK_REG_TEST.toCapitalized(), BITCOIN_NETWORK_MAIN_NET.toCapitalized()) + builder.setSingleChoiceItems(items, 0 + ) { _, p1 -> + when (p1) { + 0 -> { + regTestCallback.invoke() + } + 1 -> { + mainNetCallback.invoke() + } + else -> {} + } + } + builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> + dialog.dismiss() + } + builder.setPositiveButton(android.R.string.ok) { _, _ -> + callback.invoke() + } + builder.show() + } + } + + object SendingSeedToHardware: OnBoardConnectSideEffect() { + override suspend fun execute(value: Context) { + SphinxToastUtils().show(value, R.string.sending_seed) + } + } + + object SigningDeviceSuccessfullySet: OnBoardConnectSideEffect() { + override suspend fun execute(value: Context) { + SphinxToastUtils(true).show(value, R.string.signing_device_successfully_set) + } + } + + class FailedToSetupSigningDevice( + private val errorMessage: String + ): OnBoardConnectSideEffect() { + override suspend fun execute(value: Context) { + SphinxToastUtils(true).show( + value, + value.getString(R.string.error_setting_up_signing_device, errorMessage) + ) + } + } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun String.toCapitalized(): String { + return this.replaceFirstChar { + if (it.isLowerCase()) it.titlecase( + Locale.ROOT + ) else it.toString() + } +} diff --git a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectViewModel.kt b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectViewModel.kt index 6356d407d9..f8df1573a3 100644 --- a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectViewModel.kt +++ b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectViewModel.kt @@ -1,12 +1,23 @@ package chat.sphinx.onboard_connect.ui +import android.app.Application import android.content.Context +import android.text.InputType import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import chat.sphinx.concept_network_query_crypter.NetworkQueryCrypter +import chat.sphinx.concept_signer_manager.SignerHardwareCallback +import chat.sphinx.concept_signer_manager.SignerManager +import chat.sphinx.concept_signer_manager.SignerPhoneCallback import chat.sphinx.concept_view_model_coordinator.ViewModelCoordinator +import chat.sphinx.concept_wallet.WalletDataHandler import chat.sphinx.kotlin_response.Response +import chat.sphinx.menu_bottom.ui.MenuBottomViewState +import chat.sphinx.menu_bottom_phone_signer_method.PhoneSignerMethodMenuHandler +import chat.sphinx.menu_bottom_phone_signer_method.PhoneSignerMethodMenuViewModel +import chat.sphinx.menu_bottom_signer.SignerMenuHandler +import chat.sphinx.menu_bottom_signer.SignerMenuViewModel import chat.sphinx.onboard_connect.navigation.OnBoardConnectNavigator -import chat.sphinx.scanner_view_model_coordinator.request.ScannerFilter import chat.sphinx.scanner_view_model_coordinator.request.ScannerRequest import chat.sphinx.scanner_view_model_coordinator.response.ScannerResponse import chat.sphinx.wrapper_invite.toValidInviteStringOrNull @@ -18,6 +29,13 @@ import io.matthewnelson.android_feature_viewmodel.updateViewState import io.matthewnelson.concept_coroutines.CoroutineDispatchers import io.matthewnelson.concept_views.viewstate.ViewStateContainer import chat.sphinx.onboard_common.model.RedemptionCode +import chat.sphinx.onboard_connect.R +import chat.sphinx.onboard_connect.viewstate.MnemonicDialogViewState +import chat.sphinx.onboard_connect.viewstate.MnemonicWordsViewState +import chat.sphinx.onboard_connect.viewstate.OnBoardConnectSubmitButtonViewState +import chat.sphinx.onboard_connect.viewstate.OnBoardConnectViewState +import chat.sphinx.resources.MnemonicLanguagesUtils +import com.squareup.moshi.Moshi import io.matthewnelson.android_feature_viewmodel.currentViewState import io.matthewnelson.concept_views.viewstate.value import kotlinx.coroutines.launch @@ -32,20 +50,42 @@ internal class OnBoardConnectViewModel @Inject constructor( dispatchers: CoroutineDispatchers, handle: SavedStateHandle, private val scannerCoordinator: ViewModelCoordinator, - val navigator: OnBoardConnectNavigator, -): SideEffectViewModel< + private val walletDataHandler: WalletDataHandler, + private val networkQueryCrypter: NetworkQueryCrypter, + private val app: Application, + val moshi: Moshi, + val navigator: OnBoardConnectNavigator + ): SideEffectViewModel< Context, OnBoardConnectSideEffect, OnBoardConnectViewState - >(dispatchers, OnBoardConnectViewState.Idle) + >(dispatchers, OnBoardConnectViewState.Idle), + SignerMenuViewModel, + PhoneSignerMethodMenuViewModel, + SignerHardwareCallback, + SignerPhoneCallback { private val args: OnBoardConnectFragmentArgs by handle.navArgs() + private lateinit var signerManager: SignerManager + + companion object { + const val BITCOIN_NETWORK_REG_TEST = "regtest" + const val BITCOIN_NETWORK_MAIN_NET = "mainnet" + } val submitButtonViewStateContainer: ViewStateContainer by lazy { ViewStateContainer(OnBoardConnectSubmitButtonViewState.Disabled) } + val mnemonicWordsViewStateContainer: ViewStateContainer by lazy { + ViewStateContainer(MnemonicWordsViewState.Closed) + } + + val mnemonicDialogViewStateContainer: ViewStateContainer by lazy { + ViewStateContainer(MnemonicDialogViewState.Idle) + } + init { updateViewState( if (args.newUser) { @@ -77,6 +117,16 @@ internal class OnBoardConnectViewModel @Inject constructor( redemptionCode is RedemptionCode.SwarmClaim) { isValid = true } + if (redemptionCode != null && + redemptionCode is RedemptionCode.Glyph) { + signerManager.setSeedFromGlyph( + redemptionCode.mqtt, + redemptionCode.network, + redemptionCode.relay + ) + isValid = true + } + } else if (vs is OnBoardConnectViewState.ExistingUser) { if (redemptionCode != null && redemptionCode is RedemptionCode.AccountRestoration) { @@ -102,11 +152,22 @@ internal class OnBoardConnectViewModel @Inject constructor( submitSideEffect(OnBoardConnectSideEffect.FromScanner(response.value)) val code = response.value.value + val redemptionCode = RedemptionCode.decode(code) validateCode(code) if (submitButtonViewStateContainer.value is OnBoardConnectSubmitButtonViewState.Enabled) { - viewModelScope.launch(mainImmediate) { - navigator.toOnBoardConnectingScreen(code) + + if (redemptionCode is RedemptionCode.Glyph) { + signerManager.setSeedFromGlyph( + redemptionCode.mqtt, + redemptionCode.network, + redemptionCode.relay + ) + signerMenuHandler.viewStateContainer.updateViewState(MenuBottomViewState.Open) + } else { + viewModelScope.launch(mainImmediate) { + navigator.toOnBoardConnectingScreen(code) + } } } } @@ -115,10 +176,21 @@ internal class OnBoardConnectViewModel @Inject constructor( fun continueToConnectingScreen(code: String) { val submitButtonVS = submitButtonViewStateContainer.value + val redemptionCode = RedemptionCode.decode(code) if (submitButtonVS is OnBoardConnectSubmitButtonViewState.Enabled) { - viewModelScope.launch(mainImmediate) { - navigator.toOnBoardConnectingScreen(code) + + if (redemptionCode is RedemptionCode.Glyph) { + signerManager.setSeedFromGlyph( + redemptionCode.mqtt, + redemptionCode.network, + redemptionCode.relay + ) + signerMenuHandler.viewStateContainer.updateViewState(MenuBottomViewState.Open) + } else { + viewModelScope.launch(mainImmediate) { + navigator.toOnBoardConnectingScreen(code) + } } } else { viewModelScope.launch(mainImmediate) { @@ -134,4 +206,195 @@ internal class OnBoardConnectViewModel @Inject constructor( } } } + + fun setSignerManager(signerManager: SignerManager) { + signerManager.setWalletDataHandler(walletDataHandler) + signerManager.setMoshi(moshi) + signerManager.setNetworkQueryCrypter(networkQueryCrypter) + + this.signerManager = signerManager + } + + override val signerMenuHandler: SignerMenuHandler by lazy { + SignerMenuHandler() + } + + override val phoneSignerMethodMenuHandler: PhoneSignerMethodMenuHandler by lazy { + PhoneSignerMethodMenuHandler() + } + + ///Signing Hardware device + override fun setupHardwareSigner() { + signerManager.setupSignerHardware(this) + } + + override fun checkNetwork(callback: (Boolean) -> Unit) { + viewModelScope.launch(mainImmediate) { + submitSideEffect(OnBoardConnectSideEffect.CheckNetwork { + callback.invoke(true) + }) + } + } + + override fun signingDeviceNetwork( + callback: (String) -> Unit + ) { + viewModelScope.launch(mainImmediate) { + submitSideEffect(OnBoardConnectSideEffect.SigningDeviceInfo( + app.getString(R.string.network_name_title), + app.getString(R.string.network_name_message) + ) { networkName -> + if (networkName == null) { + viewModelScope.launch(mainImmediate) { + submitSideEffect(OnBoardConnectSideEffect.FailedToSetupSigningDevice("Network can not be empty")) + return@launch + } + } else { + callback.invoke(networkName) + } + }) + } + } + + override fun signingDevicePassword(networkName: String, callback: (String) -> Unit) { + viewModelScope.launch(mainImmediate) { + submitSideEffect(OnBoardConnectSideEffect.SigningDeviceInfo( + app.getString(R.string.network_password_title), + app.getString( + R.string.network_password_message, + networkName ?: "-" + ), + inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + ) { networkPass -> + viewModelScope.launch(mainImmediate) { + if (networkPass == null) { + submitSideEffect(OnBoardConnectSideEffect.FailedToSetupSigningDevice("Network password can not be empty")) + return@launch + } else { + callback.invoke(networkPass) + } + } + }) + } + } + + override fun signingDeviceLightningNodeUrl(callback: (String) -> Unit) { + viewModelScope.launch(mainImmediate) { + submitSideEffect(OnBoardConnectSideEffect.SigningDeviceInfo( + app.getString(R.string.lightning_node_url_title), + app.getString(R.string.lightning_node_url_message), + ) { lightningNodeUrl -> + viewModelScope.launch(mainImmediate) { + if (lightningNodeUrl == null) { + submitSideEffect(OnBoardConnectSideEffect.FailedToSetupSigningDevice("Lightning node URL can not be empty")) + return@launch + } + else { + callback.invoke(lightningNodeUrl) + } + } + }) + } + } + + override fun signingDeviceCheckBitcoinNetwork(network: (String) -> Unit, linkSigningDevice: (Boolean) -> Unit) { + viewModelScope.launch(mainImmediate) { + submitSideEffect(OnBoardConnectSideEffect.CheckBitcoinNetwork( + regTestCallback = { + network.invoke(BITCOIN_NETWORK_REG_TEST) + }, mainNetCallback = { + network.invoke(BITCOIN_NETWORK_MAIN_NET) + }, callback = { + viewModelScope.launch(mainImmediate) { + linkSigningDevice.invoke(true) + } + } + )) + } + } + + override fun failedToSetupSigningDevice(message: String) { + viewModelScope.launch(mainImmediate) { + submitSideEffect(OnBoardConnectSideEffect.FailedToSetupSigningDevice(message)) + } + } + + override fun showMnemonicToUser(message: String, callback: (Boolean) -> Unit) { + viewModelScope.launch(mainImmediate) { + submitSideEffect(OnBoardConnectSideEffect.ShowMnemonicToUser(message) { + callback.invoke(true) + }) + } + } + + override fun sendingSeedToHardware() { + viewModelScope.launch(mainImmediate) { + submitSideEffect(OnBoardConnectSideEffect.SendingSeedToHardware) + } + } + + override fun signingDeviceSuccessfullySet() { + viewModelScope.launch(mainImmediate) { + submitSideEffect(OnBoardConnectSideEffect.SigningDeviceSuccessfullySet) + } + } + + ///Phone Signer setup + override fun setupPhoneSigner() { + phoneSignerMethodMenuHandler.viewStateContainer.updateViewState(MenuBottomViewState.Open) + } + + override fun generateSeed() { + signerManager.setupPhoneSigner(null, this) + } + + override fun importSeed() { + mnemonicWordsViewStateContainer.updateViewState(MnemonicWordsViewState.Open) + } + + fun validateSeed(seed: String) { + viewModelScope.launch(mainImmediate) { + val mnemonicUtils = MnemonicLanguagesUtils(app.applicationContext) + val words = seed.lowercase().trim().split(" ") + + if (words.size != 12 && words.size != 24) { + submitSideEffect( + OnBoardConnectSideEffect.Notify( + app.getString(R.string.mnemonic_incorrect_length) + ) + ) + } + + if (mnemonicUtils.validateWords(words)) { + signerManager.setupPhoneSigner( + seed.lowercase().trim(), + this@OnBoardConnectViewModel + ) + mnemonicDialogViewStateContainer.updateViewState(MnemonicDialogViewState.Loading) + } else { + submitSideEffect( + OnBoardConnectSideEffect.Notify( + app.getString(R.string.mnemonic_invalid_word) + ) + ) + } + } + } + + override fun phoneSignerSuccessfullySet() { + viewModelScope.launch(mainImmediate) { + navigator.toOnBoardConnectingScreen(null) + } + } + + override fun phoneSignerSetupError() { + viewModelScope.launch(mainImmediate) { + submitSideEffect( + OnBoardConnectSideEffect.Notify( + app.getString(R.string.phone_signer_error) + ) + ) + } + } + } \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/viewstate/MnemonicDialogViewState.kt b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/viewstate/MnemonicDialogViewState.kt new file mode 100644 index 0000000000..55704a34c2 --- /dev/null +++ b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/viewstate/MnemonicDialogViewState.kt @@ -0,0 +1,8 @@ +package chat.sphinx.onboard_connect.viewstate + +import io.matthewnelson.concept_views.viewstate.ViewState + +internal sealed class MnemonicDialogViewState: ViewState() { + object Idle: MnemonicDialogViewState() + object Loading: MnemonicDialogViewState() +} \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/viewstate/MnemonicWordsViewState.kt b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/viewstate/MnemonicWordsViewState.kt new file mode 100644 index 0000000000..3c0321a600 --- /dev/null +++ b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/viewstate/MnemonicWordsViewState.kt @@ -0,0 +1,30 @@ +package chat.sphinx.onboard_connect.viewstate + +import androidx.constraintlayout.motion.widget.MotionLayout +import chat.sphinx.onboard_connect.R +import io.matthewnelson.android_concept_views.MotionLayoutViewState + + +sealed class MnemonicWordsViewState: MotionLayoutViewState() { + + object Closed: MnemonicWordsViewState() { + override val startSetId: Int + get() = R.id.motion_scene_mnemonic_words_open + override val endSetId: Int? + get() = R.id.motion_scene_mnemonic_words_closed + + override fun restoreMotionScene(motionLayout: MotionLayout) {} + } + + object Open: MnemonicWordsViewState() { + override val startSetId: Int + get() = R.id.motion_scene_mnemonic_words_closed + override val endSetId: Int? + get() = R.id.motion_scene_mnemonic_words_open + + override fun restoreMotionScene(motionLayout: MotionLayout) { + motionLayout.setTransition(R.id.transition_mnemonic_words_closed_to_open) + motionLayout.setProgress(1F, 1F) + } + } +} \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectSubmitButtonViewState.kt b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/viewstate/OnBoardConnectSubmitButtonViewState.kt similarity index 86% rename from sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectSubmitButtonViewState.kt rename to sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/viewstate/OnBoardConnectSubmitButtonViewState.kt index 6e3c505ec1..bbaca1963b 100644 --- a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectSubmitButtonViewState.kt +++ b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/viewstate/OnBoardConnectSubmitButtonViewState.kt @@ -1,4 +1,4 @@ -package chat.sphinx.onboard_connect.ui +package chat.sphinx.onboard_connect.viewstate import io.matthewnelson.concept_views.viewstate.ViewState diff --git a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectViewState.kt b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/viewstate/OnBoardConnectViewState.kt similarity index 86% rename from sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectViewState.kt rename to sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/viewstate/OnBoardConnectViewState.kt index 450ce75390..dd76bdbf5e 100644 --- a/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/ui/OnBoardConnectViewState.kt +++ b/sphinx/screens/onboard/onboard-connect/src/main/java/chat/sphinx/onboard_connect/viewstate/OnBoardConnectViewState.kt @@ -1,4 +1,4 @@ -package chat.sphinx.onboard_connect.ui +package chat.sphinx.onboard_connect.viewstate import io.matthewnelson.concept_views.viewstate.ViewState diff --git a/sphinx/screens/onboard/onboard-connect/src/main/res/layout/fragment_on_board_connect.xml b/sphinx/screens/onboard/onboard-connect/src/main/res/layout/fragment_on_board_connect.xml index 09c0b66248..02bf4c602e 100644 --- a/sphinx/screens/onboard/onboard-connect/src/main/res/layout/fragment_on_board_connect.xml +++ b/sphinx/screens/onboard/onboard-connect/src/main/res/layout/fragment_on_board_connect.xml @@ -102,5 +102,20 @@ + + + + + + \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connect/src/main/res/layout/layout_mnemonic_words.xml b/sphinx/screens/onboard/onboard-connect/src/main/res/layout/layout_mnemonic_words.xml new file mode 100644 index 0000000000..18933558d6 --- /dev/null +++ b/sphinx/screens/onboard/onboard-connect/src/main/res/layout/layout_mnemonic_words.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connect/src/main/res/layout/layout_mnemonic_words_detail.xml b/sphinx/screens/onboard/onboard-connect/src/main/res/layout/layout_mnemonic_words_detail.xml new file mode 100644 index 0000000000..938af3358c --- /dev/null +++ b/sphinx/screens/onboard/onboard-connect/src/main/res/layout/layout_mnemonic_words_detail.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connect/src/main/res/values-b+fil/strings.xml b/sphinx/screens/onboard/onboard-connect/src/main/res/values-b+fil/strings.xml new file mode 100644 index 0000000000..dd7b6da656 --- /dev/null +++ b/sphinx/screens/onboard/onboard-connect/src/main/res/values-b+fil/strings.xml @@ -0,0 +1,22 @@ + + + Mga Susi sa Pag-export + "Enter your PIN to encrypt your keys. You will need it when restoring account on other device." + Kokopyahin ang iyong mga susi sa clipboard. I-save ang string na ito at tandaan ang iyong pin bago burahin ang app o mawawala ang lahat ng data. + I-export ang mga key na kinopya sa clipboard + Wrong PIN provided + Backup keys failed + Failed to process image + Image uploaded successfully + Invalid Meeting Server Url + New Relay URL is invalid + Updating to an onion address is currently not supported. + Testing new Relay URL… + New URL test failed, restoring previous setting… + Relay URL successfully updated + Github Personal Access Token + Bumuo ng Personal Access Token sa github gamit ang repo scope + Matagumpay na naitakda ang Github Personal Access Token + Github Personal Access Token could not be set + Something went wrong while setting up the phone signer. Please try again later + \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connect/src/main/res/values-es/strings.xml b/sphinx/screens/onboard/onboard-connect/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..cd35188a60 --- /dev/null +++ b/sphinx/screens/onboard/onboard-connect/src/main/res/values-es/strings.xml @@ -0,0 +1,24 @@ + + + Exportar Claves + "Ingresa tu PIN para encriptar las claves. ¡Recuérdalo! Lo necesitarás para restaurar tu cuenta en otro dispositivo" + "Tus claves se copiarán al portapapeles. Guarda las claves y recuerda tu PIN antes de borrar la aplicación o toda la información se perderá" + Claves copiadas al portapapeles + PIN incorrecto + Guardado de claves fallido + No se ha podido procesar la imagen + Imagen cargada con éxito + Url de Meeting Server inválida + + La nueva URL de Relay es inválida + Actualización a una dirección de tipo onion no soportado actualmente + Verificando la nueva URL de Relay… + La verificación de la nueva URL de Relay ha fallado, restaurando configuración previa… + La URL de Relay ha sido actualizada con éxito + + Github Personal Access Token + Crea un Personal Access Token en Github con permisos para el repositorio + El Personal Access Token de Github ha sido establecido con éxito + No se pudo establecer el Github Personal Access Token + Ocurrió un error configurado el phone signer. Por favor volvé a intentarlo más tarde. + \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connect/src/main/res/values-ja/strings.xml b/sphinx/screens/onboard/onboard-connect/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..15fd97301b --- /dev/null +++ b/sphinx/screens/onboard/onboard-connect/src/main/res/values-ja/strings.xml @@ -0,0 +1,24 @@ + + + バックアップキーをエクスポート + バックアップキーを暗号化するPINコードを入力してください。このPINコードは別の端末でアカウントを復元する時に必要です。 + バックアップキーをクリップボードにコピーします。アプリを削除する前に、この文字列とPINコードを保存してください。一方でも失くすとアプリ再インストール後にデータを復元できません。 + クリップボードにコピーしたバックアップキーをエクスポート + PINコードが違います + バックアップキーの生成と暗号化に失敗しました + 写真をアップロードできませんでした + 写真をアップロードしました + Invalid Meeting Server Url + + 新しいRelay URLは無効です + .onionアドレスへの変更はまだできません + 新しいRelay URLをテストしています + 新しいURLのテストに失敗しました。既存設定を復元しています。 + Relay URLを更新しました + + Github Personal Access Token + Generate a Personal Access Token on github with the repo scope + Github Personal Access Token successfully set + Github Personal Access Token coult not be set + Something went wrong while setting up the phone signer. Please try again later + \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connect/src/main/res/values-zh/strings.xml b/sphinx/screens/onboard/onboard-connect/src/main/res/values-zh/strings.xml new file mode 100644 index 0000000000..868db8b32f --- /dev/null +++ b/sphinx/screens/onboard/onboard-connect/src/main/res/values-zh/strings.xml @@ -0,0 +1,24 @@ + + + 匯出私鏈 + "請輸入加密私鏈PIN. 您備份恢復將需要它." + 私鏈已複製到剪貼板. 請保留 & 警告! 請刪除APP之前記住私鏈PIN 免得資料損失. + 私鏈已複製到剪貼板 + 密碼錯誤 + 備份私鏈失敗 + 圖片上傳失敗 + 圖片上傳成功 + Invalid Meeting Server Url + + 更新 Relay URL 失敗 + 目前不支持轉換洋蔥住址(TOR). + 測試 新 Relay URL… + 新 URL 測試失敗, 恢復設定… + Relay URL 更新成功 + + Github Personal Access Token + Generate a Personal Access Token on github with the repo scope + Github Personal Access Token successfully set + Github Personal Access Token coult not be set + Something went wrong while setting up the phone signer. Please try again later + \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connect/src/main/res/values/strings.xml b/sphinx/screens/onboard/onboard-connect/src/main/res/values/strings.xml new file mode 100644 index 0000000000..01b206860b --- /dev/null +++ b/sphinx/screens/onboard/onboard-connect/src/main/res/values/strings.xml @@ -0,0 +1,22 @@ + + + Export Keys + "Enter your PIN to encrypt your keys. You will need it when restoring account on other device." + Your keys will be copied to the clipboard. Save this string & remember your pin before deleting the app or all data will be lost. + Export keys copied to clipboard + Wrong PIN provided + Backup keys failed + Failed to process image + Image uploaded successfully + New Relay URL is invalid + Testing new Relay URL… + Updating to an onion address is currently not supported. + Relay URL successfully updated + New URL test failed, restoring previous setting… + Invalid Meeting Server Url + Github Personal Access Token + Generate a Personal Access Token on github with the repo scope + Github Personal Access Token successfully set + Github Personal Access Token could not be set + Something went wrong while setting up the phone signer. Please try again later + diff --git a/sphinx/screens/onboard/onboard-connect/src/main/res/xml/motion_mnemonic_words.xml b/sphinx/screens/onboard/onboard-connect/src/main/res/xml/motion_mnemonic_words.xml new file mode 100644 index 0000000000..52a9ccaff3 --- /dev/null +++ b/sphinx/screens/onboard/onboard-connect/src/main/res/xml/motion_mnemonic_words.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sphinx/screens/onboard/onboard-connecting/build.gradle b/sphinx/screens/onboard/onboard-connecting/build.gradle index d53d1fdc6b..3a729d1525 100644 --- a/sphinx/screens/onboard/onboard-connecting/build.gradle +++ b/sphinx/screens/onboard/onboard-connecting/build.gradle @@ -36,12 +36,15 @@ dependencies { implementation deps.androidx.lifecycle.hilt implementation deps.google.hilt + implementation deps.square.moshi // Sphinx api project(path: ':sphinx:screens:onboard:onboard-common') api project(path: ':sphinx:screens:onboard:onboard-resources') api project(path: ':sphinx:application:data:concepts:concept-image-loader') api project(path: ':sphinx:application:data:concepts:crypto:concept-crypto-rsa') + implementation project(path: ':sphinx:application:data:concepts:concept-wallet') + implementation project(path: ':sphinx:activity:concepts:concept-signer-manager') implementation project(path: ':sphinx:activity:insetter-activity') implementation project(path: ':sphinx:screens:onboard:key-restore') diff --git a/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/model/Glyph.kt b/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/model/Glyph.kt new file mode 100644 index 0000000000..cbec396db7 --- /dev/null +++ b/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/model/Glyph.kt @@ -0,0 +1,7 @@ +package chat.sphinx.onboard_connecting.model + +data class Glyph( + val mqtt: String, + val network: String, + val relay: String, +) diff --git a/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/navigation/ToOnBoardConnectingScreen.kt b/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/navigation/ToOnBoardConnectingScreen.kt index 4139243660..7070cd7fd2 100644 --- a/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/navigation/ToOnBoardConnectingScreen.kt +++ b/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/navigation/ToOnBoardConnectingScreen.kt @@ -1,6 +1,5 @@ package chat.sphinx.onboard_connecting.navigation -import androidx.annotation.IdRes import androidx.navigation.NavController import chat.sphinx.onboard_connecting.R import chat.sphinx.onboard_connecting.ui.OnBoardConnectingFragmentArgs @@ -8,23 +7,19 @@ import io.matthewnelson.android_feature_navigation.DefaultNavOptions import io.matthewnelson.concept_navigation.NavigationRequest class ToOnBoardConnectingScreen( - private val code: String, + private val code: String?, ): NavigationRequest() { override fun navigate(controller: NavController) { - OnBoardConnectingFragmentArgs.Builder( - code, - ).build().toBundle()?.let { args -> + val args = OnBoardConnectingFragmentArgs.Builder() + args.argCode = code - controller.navigate( - R.id.on_board_connecting_nav_graph, - args, - DefaultNavOptions.defaultAnims - .build() - ) - - } + controller.navigate( + R.id.on_board_connecting_nav_graph, + args.build().toBundle(), + DefaultNavOptions.defaultAnims.build() + ) } } \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/ui/OnBoardConnectingFragment.kt b/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/ui/OnBoardConnectingFragment.kt index cf5f24bf00..909ddbda1f 100644 --- a/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/ui/OnBoardConnectingFragment.kt +++ b/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/ui/OnBoardConnectingFragment.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.lifecycleScope import by.kirich1409.viewbindingdelegate.viewBinding import chat.sphinx.authentication_resources.databinding.LayoutAuthenticationBinding import chat.sphinx.concept_image_loader.ImageLoader +import chat.sphinx.concept_signer_manager.SignerManager import chat.sphinx.insetter_activity.InsetterActivity import chat.sphinx.insetter_activity.addNavigationBarPadding import chat.sphinx.insetter_activity.addStatusBarPadding @@ -48,6 +49,10 @@ internal class OnBoardConnectingFragment: MotionLayoutFragment< @Inject lateinit var imageLoaderInj: ImageLoader + @Inject + @Suppress("ProtectedInFinal") + protected lateinit var signerManager: SignerManager + private val imageLoader: ImageLoader get() = imageLoaderInj @@ -57,6 +62,7 @@ internal class OnBoardConnectingFragment: MotionLayoutFragment< OnBackPress(binding.root.context).addCallback(viewLifecycleOwner, requireActivity()) setupHeaderAndFooter() + setupSignerManager() lifecycleScope.launch { imageLoader.load( @@ -66,6 +72,10 @@ internal class OnBoardConnectingFragment: MotionLayoutFragment< } } + private fun setupSignerManager(){ + viewModel.setSignerManager(signerManager) + } + private fun setupHeaderAndFooter() { (requireActivity() as InsetterActivity) .addNavigationBarPadding(binding.layoutMotionOnBoardConnecting) diff --git a/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/ui/OnBoardConnectingSideEffect.kt b/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/ui/OnBoardConnectingSideEffect.kt index cad2a826e8..a92e4a1eb7 100644 --- a/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/ui/OnBoardConnectingSideEffect.kt +++ b/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/ui/OnBoardConnectingSideEffect.kt @@ -52,4 +52,10 @@ internal sealed class OnBoardConnectingSideEffect: SideEffect() { } } + object CheckAdminFailed: OnBoardConnectingSideEffect() { + override suspend fun execute(value: Context) { + SphinxToastUtils().show(value, R.string.side_effect_check_admin_failed) + } + } + } \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/ui/OnBoardConnectingViewModel.kt b/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/ui/OnBoardConnectingViewModel.kt index b4044b4d52..95f32ee5cd 100644 --- a/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/ui/OnBoardConnectingViewModel.kt +++ b/sphinx/screens/onboard/onboard-connecting/src/main/java/chat/sphinx/onboard_connecting/ui/OnBoardConnectingViewModel.kt @@ -13,6 +13,9 @@ import chat.sphinx.concept_network_query_relay_keys.NetworkQueryRelayKeys import chat.sphinx.concept_network_query_relay_keys.model.PostHMacKeyDto import chat.sphinx.concept_network_tor.TorManager import chat.sphinx.concept_relay.RelayDataHandler +import chat.sphinx.concept_signer_manager.CheckAdminCallback +import chat.sphinx.concept_signer_manager.SignerManager +import chat.sphinx.concept_wallet.WalletDataHandler import chat.sphinx.key_restore.KeyRestore import chat.sphinx.key_restore.KeyRestoreResponse import chat.sphinx.kotlin_response.LoadResponse @@ -29,6 +32,7 @@ import chat.sphinx.wrapper_invite.toValidInviteStringOrNull import chat.sphinx.wrapper_relay.* import chat.sphinx.wrapper_rsa.RsaPrivateKey import chat.sphinx.wrapper_rsa.RsaPublicKey +import com.squareup.moshi.Moshi import dagger.hilt.android.lifecycle.HiltViewModel import io.matthewnelson.android_feature_navigation.util.navArgs import io.matthewnelson.android_feature_viewmodel.MotionLayoutViewModel @@ -47,46 +51,59 @@ import javax.inject.Inject internal inline val OnBoardConnectingFragmentArgs.restoreCode: RedemptionCode.AccountRestoration? get() { - val redemptionCode = RedemptionCode.decode(argCode) + argCode?.let { + val redemptionCode = RedemptionCode.decode(it) - if (redemptionCode is RedemptionCode.AccountRestoration) { - return redemptionCode + if (redemptionCode is RedemptionCode.AccountRestoration) { + return redemptionCode + } } return null } internal inline val OnBoardConnectingFragmentArgs.connectionCode: RedemptionCode.NodeInvite? get() { - val redemptionCode = RedemptionCode.decode(argCode) + argCode?.let { + val redemptionCode = RedemptionCode.decode(it) - if (redemptionCode is RedemptionCode.NodeInvite) { - return redemptionCode + if (redemptionCode is RedemptionCode.NodeInvite) { + return redemptionCode + } } return null } internal inline val OnBoardConnectingFragmentArgs.swarmConnect: RedemptionCode.SwarmConnect? get() { - val redemptionCode = RedemptionCode.decode(argCode) + argCode?.let { + val redemptionCode = RedemptionCode.decode(it) - if (redemptionCode is RedemptionCode.SwarmConnect) { - return redemptionCode + if (redemptionCode is RedemptionCode.SwarmConnect) { + return redemptionCode + } } return null } internal inline val OnBoardConnectingFragmentArgs.swarmClaim: RedemptionCode.SwarmClaim? get() { - val redemptionCode = RedemptionCode.decode(argCode) + argCode?.let { + val redemptionCode = RedemptionCode.decode(it) - if (redemptionCode is RedemptionCode.SwarmClaim) { - return redemptionCode + if (redemptionCode is RedemptionCode.SwarmClaim) { + return redemptionCode + } } return null } internal inline val OnBoardConnectingFragmentArgs.inviteCode: InviteString? - get() = argCode.toValidInviteStringOrNull() + get() { + argCode?.let { + return it.toValidInviteStringOrNull() + } + return null + } @HiltViewModel internal class OnBoardConnectingViewModel @Inject constructor( @@ -94,22 +111,26 @@ internal class OnBoardConnectingViewModel @Inject constructor( handle: SavedStateHandle, val navigator: OnBoardConnectingNavigator, private val keyRestore: KeyRestore, + private val walletDataHandler: WalletDataHandler, private val relayDataHandler: RelayDataHandler, private val torManager: TorManager, private val networkQueryContact: NetworkQueryContact, private val networkQueryInvite: NetworkQueryInvite, private val networkQueryRelayKeys: NetworkQueryRelayKeys, private val onBoardStepHandler: OnBoardStepHandler, + val moshi: Moshi, private val rsa: RSA, ): MotionLayoutViewModel< Any, Context, OnBoardConnectingSideEffect, OnBoardConnectingViewState - >(dispatchers, OnBoardConnectingViewState.Connecting) + >(dispatchers, OnBoardConnectingViewState.Connecting), + CheckAdminCallback { private val args: OnBoardConnectingFragmentArgs by handle.navArgs() + private lateinit var signerManager: SignerManager init { viewModelScope.launch(mainImmediate) { @@ -119,6 +140,14 @@ internal class OnBoardConnectingViewModel @Inject constructor( } } + fun setSignerManager(signerManager: SignerManager) { + signerManager.setWalletDataHandler(walletDataHandler) + signerManager.setMoshi(moshi) + signerManager.setNetworkQueryContact(networkQueryContact) + + this.signerManager = signerManager + } + private fun processCode() { viewModelScope.launch(mainImmediate) { args.restoreCode?.let { restoreCode -> @@ -150,8 +179,12 @@ internal class OnBoardConnectingViewModel @Inject constructor( } ?: args.inviteCode?.let { inviteCode -> redeemInvite(inviteCode) } ?: run { - submitSideEffect(OnBoardConnectingSideEffect.InvalidCode) - navigator.popBackStack() + if (signerManager.isPhoneSignerSettingUp()) { + continuePhoneSignerSetup() + } else { + submitSideEffect(OnBoardConnectingSideEffect.InvalidCode) + navigator.popBackStack() + } } } } @@ -583,4 +616,38 @@ internal class OnBoardConnectingViewModel @Inject constructor( override suspend fun onMotionSceneCompletion(value: Any) { return } + + private fun continuePhoneSignerSetup() { + viewModelScope.launch(mainImmediate) { + signerManager.checkHasAdmin(this@OnBoardConnectingViewModel) + } + } + + override fun checkAdminSucceeded() { + viewModelScope.launch(mainImmediate) { + signerManager.getPublicKeyAndRelayUrl()?.let { publicKeyAndRelayUrl -> + publicKeyAndRelayUrl.second.toRelayUrl()?.let { + getTransportKey( + ip = it, + publicKeyAndRelayUrl.first, + null, + null, + token = null + ) + } ?: run { + checkAdminFailed() + } + } ?: run { + checkAdminFailed() + } + } + } + + override fun checkAdminFailed() { + viewModelScope.launch(mainImmediate) { + signerManager.reset() + submitSideEffect(OnBoardConnectingSideEffect.CheckAdminFailed) + navigator.popBackStack() + } + } } \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-connecting/src/main/res/navigation/on_board_connecting_nav_graph.xml b/sphinx/screens/onboard/onboard-connecting/src/main/res/navigation/on_board_connecting_nav_graph.xml index 7ba108a37f..7738f1afc8 100644 --- a/sphinx/screens/onboard/onboard-connecting/src/main/res/navigation/on_board_connecting_nav_graph.xml +++ b/sphinx/screens/onboard/onboard-connecting/src/main/res/navigation/on_board_connecting_nav_graph.xml @@ -11,7 +11,9 @@ tools:layout="@layout/fragment_on_board_connecting"> + app:argType="string" + app:nullable="true" + android:defaultValue="@null"/> diff --git a/sphinx/screens/onboard/onboard-message/build.gradle b/sphinx/screens/onboard/onboard-message/build.gradle index 4b535784b8..e2d74af012 100644 --- a/sphinx/screens/onboard/onboard-message/build.gradle +++ b/sphinx/screens/onboard/onboard-message/build.gradle @@ -34,6 +34,8 @@ dependencies { // KotlinAndroid implementation project(path: ':android:features:android-feature-screens') implementation project(path: ':kotlin:concepts:authentication:concept-authentication') + implementation project(path: ':sphinx:application:data:concepts:concept-wallet') + implementation project(path: ':sphinx:activity:concepts:concept-signer-manager') implementation project(path: ':kotlin:encoders:base64') implementation deps.androidx.lifecycle.hilt diff --git a/sphinx/screens/onboard/onboard-message/src/main/java/chat/sphinx/onboard/ui/OnBoardMessageFragment.kt b/sphinx/screens/onboard/onboard-message/src/main/java/chat/sphinx/onboard/ui/OnBoardMessageFragment.kt index 74bb28330c..c5ec3e06d6 100644 --- a/sphinx/screens/onboard/onboard-message/src/main/java/chat/sphinx/onboard/ui/OnBoardMessageFragment.kt +++ b/sphinx/screens/onboard/onboard-message/src/main/java/chat/sphinx/onboard/ui/OnBoardMessageFragment.kt @@ -6,6 +6,7 @@ import android.view.View import androidx.fragment.app.viewModels import androidx.navigation.fragment.navArgs import by.kirich1409.viewbindingdelegate.viewBinding +import chat.sphinx.concept_signer_manager.SignerManager import chat.sphinx.insetter_activity.InsetterActivity import chat.sphinx.insetter_activity.addNavigationBarPadding import chat.sphinx.insetter_activity.addStatusBarPadding @@ -28,6 +29,7 @@ import io.matthewnelson.android_feature_screens.ui.sideeffect.SideEffectFragment import io.matthewnelson.android_feature_screens.util.gone import io.matthewnelson.android_feature_screens.util.visible import javax.annotation.meta.Exhaustive +import javax.inject.Inject @AndroidEntryPoint internal class OnBoardMessageFragment: SideEffectFragment< @@ -49,10 +51,15 @@ internal class OnBoardMessageFragment: SideEffectFragment< override val viewModel: OnBoardMessageViewModel by viewModels() override val binding: FragmentOnBoardMessageBinding by viewBinding(FragmentOnBoardMessageBinding::bind) + @Inject + @Suppress("ProtectedInFinal") + protected lateinit var signerManager: SignerManager + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupHeaderAndFooter() + setupSignerManager() CloseAppOnBackPress(view.context) .enableDoubleTapToClose(viewLifecycleOwner, SphinxToastUtils()) @@ -62,10 +69,20 @@ internal class OnBoardMessageFragment: SideEffectFragment< binding.inviterMessageTextView.text = inviterData.message ?: "" binding.buttonContinue.setOnClickListener { - viewModel.presentLoginModal(relayUrl, authorizationToken, transportKey, hMacKey, inviterData) + viewModel.presentLoginModal( + relayUrl, + authorizationToken, + transportKey, + hMacKey, + inviterData + ) } } + private fun setupSignerManager(){ + viewModel.setSignerManager(signerManager) + } + private fun setupHeaderAndFooter() { (requireActivity() as InsetterActivity) .addStatusBarPadding(binding.layoutConstraintOnBoard) diff --git a/sphinx/screens/onboard/onboard-message/src/main/java/chat/sphinx/onboard/ui/OnBoardMessageViewModel.kt b/sphinx/screens/onboard/onboard-message/src/main/java/chat/sphinx/onboard/ui/OnBoardMessageViewModel.kt index 8ca3377a06..00e8920ade 100644 --- a/sphinx/screens/onboard/onboard-message/src/main/java/chat/sphinx/onboard/ui/OnBoardMessageViewModel.kt +++ b/sphinx/screens/onboard/onboard-message/src/main/java/chat/sphinx/onboard/ui/OnBoardMessageViewModel.kt @@ -3,6 +3,8 @@ package chat.sphinx.onboard.ui import android.content.Context import androidx.lifecycle.viewModelScope import chat.sphinx.concept_relay.RelayDataHandler +import chat.sphinx.concept_signer_manager.SignerManager +import chat.sphinx.concept_wallet.WalletDataHandler import chat.sphinx.onboard.navigation.OnBoardMessageNavigator import chat.sphinx.onboard_common.OnBoardStepHandler import chat.sphinx.onboard_common.model.OnBoardInviterData @@ -31,6 +33,7 @@ internal class OnBoardMessageViewModel @Inject constructor( private val navigator: OnBoardMessageNavigator, private val onBoardStepHandler: OnBoardStepHandler, private val relayDataHandler: RelayDataHandler, + private val walletDataHandler: WalletDataHandler, private val authenticationCoordinator: AuthenticationCoordinator ): SideEffectViewModel< Context, @@ -39,6 +42,14 @@ internal class OnBoardMessageViewModel @Inject constructor( >(dispatchers, OnBoardMessageViewState.Idle) { + private lateinit var signerManager: SignerManager + + fun setSignerManager(signerManager: SignerManager) { + signerManager.setWalletDataHandler(walletDataHandler) + + this.signerManager = signerManager + } + private var loginJob: Job? = null fun presentLoginModal( relayUrl: RelayUrl, @@ -120,6 +131,7 @@ internal class OnBoardMessageViewModel @Inject constructor( proceedJob = viewModelScope.launch(mainImmediate) { relayDataHandler.persistAuthorizationToken(authorizationToken) relayDataHandler.persistRelayUrl(relayUrl) + signerManager.persistMnemonic() transportKey?.let { key -> relayDataHandler.persistRelayTransportKey(key) diff --git a/sphinx/screens/onboard/onboard-resources/src/main/res/values-b+fil/strings.xml b/sphinx/screens/onboard/onboard-resources/src/main/res/values-b+fil/strings.xml index 6eb7915d4d..208fa80b3d 100644 --- a/sphinx/screens/onboard/onboard-resources/src/main/res/values-b+fil/strings.xml +++ b/sphinx/screens/onboard/onboard-resources/src/main/res/values-b+fil/strings.xml @@ -37,4 +37,5 @@ "You\'re ready\nto use Sphinx!" "You can send messages,\nspend %d sats, or receive\nup to %d sats." Loading node balance… + Something went wrong while checking admin \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-resources/src/main/res/values-es/strings.xml b/sphinx/screens/onboard/onboard-resources/src/main/res/values-es/strings.xml index a43f296fe8..32cb591f80 100644 --- a/sphinx/screens/onboard/onboard-resources/src/main/res/values-es/strings.xml +++ b/sphinx/screens/onboard/onboard-resources/src/main/res/values-es/strings.xml @@ -44,4 +44,5 @@ "Estás lista/o\npara usar Sphinx!" "Puedes enviar mensajes,\nenviar %d sats, o recibir\nhasta %d sats" Cargando el balance del node… + Ocurrió un error al verificar el administrador diff --git a/sphinx/screens/onboard/onboard-resources/src/main/res/values-ja/strings.xml b/sphinx/screens/onboard/onboard-resources/src/main/res/values-ja/strings.xml index 33145abf5a..b1e7d092a7 100644 --- a/sphinx/screens/onboard/onboard-resources/src/main/res/values-ja/strings.xml +++ b/sphinx/screens/onboard/onboard-resources/src/main/res/values-ja/strings.xml @@ -44,4 +44,5 @@ Sphinxを\n使ってみよう Lightningネットワークで\nメッセージとsatsを送受信\n(現在の上限は送金 %d sats、受金 %d sats sats残高を読み込み中 + Something went wrong while checking admin \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-resources/src/main/res/values-zh/strings.xml b/sphinx/screens/onboard/onboard-resources/src/main/res/values-zh/strings.xml index 66d364f39b..4b9f999367 100644 --- a/sphinx/screens/onboard/onboard-resources/src/main/res/values-zh/strings.xml +++ b/sphinx/screens/onboard/onboard-resources/src/main/res/values-zh/strings.xml @@ -44,4 +44,5 @@ "Sphinx\n可以開始用了!" "你可以傳送訊息,\n 傳送 %d sats, 或收款 %d sats." 餘額下載中... + Something went wrong while checking admin \ No newline at end of file diff --git a/sphinx/screens/onboard/onboard-resources/src/main/res/values/strings.xml b/sphinx/screens/onboard/onboard-resources/src/main/res/values/strings.xml index de62041793..8bdef2c1c9 100644 --- a/sphinx/screens/onboard/onboard-resources/src/main/res/values/strings.xml +++ b/sphinx/screens/onboard/onboard-resources/src/main/res/values/strings.xml @@ -45,4 +45,6 @@ "You\'re ready\nto use Sphinx!" "You can send messages,\nspend %d sats, or receive\nup to %d sats." Loading node balance… + + Something went wrong while checking admin diff --git a/sphinx/screens/profile/profile/build.gradle b/sphinx/screens/profile/profile/build.gradle index 58a59ae74e..c100438bc8 100644 --- a/sphinx/screens/profile/profile/build.gradle +++ b/sphinx/screens/profile/profile/build.gradle @@ -4,6 +4,7 @@ plugins { id 'dagger.hilt.android.plugin' id 'kotlin-android' id 'kotlin-kapt' + id 'kotlinx-serialization' } android { @@ -45,7 +46,6 @@ dependencies { implementation project(path: ':sphinx:application:common:wrappers:wrapper-message-media') implementation project(path: ':sphinx:application:common:resources') - implementation project(path: ':sphinx:application:data:concepts:concept-wallet') implementation project(path: ':sphinx:application:data:concepts:concept-background-login') implementation project(path: ':sphinx:application:data:concepts:repositories:concept-repository-media') implementation project(path: ':sphinx:application:data:concepts:concept-image-loader') @@ -53,6 +53,7 @@ dependencies { implementation project(path: ':sphinx:application:data:concepts:repositories:concept-repository-lightning') implementation project(path: ':sphinx:application:data:concepts:concept-relay') + implementation project(path: ':sphinx:application:network:concepts:queries:concept-network-query-relay-keys') implementation project(path: ':sphinx:application:network:concepts:queries:concept-network-query-crypter') implementation project(path: ':sphinx:application:network:concepts:tor:concept-network-tor') @@ -64,7 +65,9 @@ dependencies { implementation deps.androidx.lifecycle.hilt implementation deps.google.hilt - implementation deps.bip39.sdk implementation deps.jna.sdk + implementation deps.kotlinx.serialization + implementation deps.square.moshi + kapt kaptDeps.google.hilt } diff --git a/sphinx/screens/profile/profile/src/main/java/chat/sphinx/profile/ui/ProfileFragment.kt b/sphinx/screens/profile/profile/src/main/java/chat/sphinx/profile/ui/ProfileFragment.kt index 5c19323cb2..f77754eaf5 100644 --- a/sphinx/screens/profile/profile/src/main/java/chat/sphinx/profile/ui/ProfileFragment.kt +++ b/sphinx/screens/profile/profile/src/main/java/chat/sphinx/profile/ui/ProfileFragment.kt @@ -108,10 +108,9 @@ internal class ProfileFragment: SideEffectFragment< override fun handleOnBackPressed() { if (viewModel.pictureMenuHandler.viewStateContainer.value is MenuBottomViewState.Open) { viewModel.pictureMenuHandler.viewStateContainer.updateViewState(MenuBottomViewState.Closed) - } else { - lifecycleScope.launch(viewModel.mainImmediate) { - profileNavigator.popBackStack() - } + } + lifecycleScope.launch(viewModel.mainImmediate) { + profileNavigator.popBackStack() } } } @@ -132,6 +131,9 @@ internal class ProfileFragment: SideEffectFragment< .addNavigationBarPadding( includeLayoutMenuBottomProfilePic.includeLayoutMenuBottomOptions.root ) + .addNavigationBarPadding( + includeLayoutMenuBottomSigner.includeLayoutMenuBottomOptions.root + ) includeProfileNamePictureHolder.imageViewProfilePicture.setOnClickListener { viewModel.pictureMenuHandler.viewStateContainer.updateViewState(MenuBottomViewState.Open) @@ -263,7 +265,6 @@ internal class ProfileFragment: SideEffectFragment< } override fun onStopTrackingTouch(seekBar: SeekBar?) { - // only persist when tracking is stopped (key up) viewModel.persistPINTimeout() } } @@ -273,10 +274,6 @@ internal class ProfileFragment: SideEffectFragment< viewModel.setGithubPAT() } - buttonProfileAdvancedContainerSigningDevice.setOnClickListener { - viewModel.setupSigningDevice() - } - buttonProfileAdvancedContainerChangePin.setOnClickListener { viewModel.resetPIN() } @@ -459,7 +456,6 @@ internal class ProfileFragment: SideEffectFragment< } } catch (e: NumberFormatException) {} } - } override suspend fun onViewStateFlowCollect(viewState: ProfileViewState) { @@ -474,8 +470,6 @@ internal class ProfileFragment: SideEffectFragment< } includeProfileBasicContainerHolder.root.gone includeProfileAdvancedContainerHolder.root.visible - - includeProfileAdvancedContainerHolder.buttonProfileAdvancedContainerSigningDevice.text = viewState.deviceSetupButtonTitle } is ProfileViewState.Basic -> { includeProfileTabsHolder.apply { diff --git a/sphinx/screens/profile/profile/src/main/java/chat/sphinx/profile/ui/ProfileSideEffect.kt b/sphinx/screens/profile/profile/src/main/java/chat/sphinx/profile/ui/ProfileSideEffect.kt index c9413153d0..6731fa2e03 100644 --- a/sphinx/screens/profile/profile/src/main/java/chat/sphinx/profile/ui/ProfileSideEffect.kt +++ b/sphinx/screens/profile/profile/src/main/java/chat/sphinx/profile/ui/ProfileSideEffect.kt @@ -4,14 +4,11 @@ import android.app.AlertDialog import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.content.DialogInterface import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.FrameLayout import chat.sphinx.profile.R -import chat.sphinx.profile.ui.ProfileViewModel.Companion.BITCOIN_NETWORK_MAIN_NET -import chat.sphinx.profile.ui.ProfileViewModel.Companion.BITCOIN_NETWORK_REG_TEST import chat.sphinx.resources.SphinxToastUtils import chat.sphinx.wrapper_relay.RelayUrl import io.matthewnelson.android_feature_toast_utils.show @@ -92,12 +89,6 @@ internal sealed class ProfileSideEffect: SideEffect() { } } - object InvalidRelayUrl: ProfileSideEffect() { - override suspend fun execute(value: Context) { - SphinxToastUtils(true).show(value, R.string.invalid_relay_url) - } - } - object UpdatingRelayUrl: ProfileSideEffect() { override suspend fun execute(value: Context) { SphinxToastUtils(true).show(value, R.string.testing_new_relay_url) @@ -188,155 +179,6 @@ internal sealed class ProfileSideEffect: SideEffect() { } } - class SigningDeviceInfo( - private val title: String, - private val message: String, - private val defaultValue: String? = null, - private val inputType: Int? = null, - private val callback: (String?) -> Unit, - ): ProfileSideEffect() { - - override suspend fun execute(value: Context) { - - val inputEditTextField = EditText(value) - inputEditTextField.isSingleLine = true - inputType?.let { - inputEditTextField.inputType = it - } - inputEditTextField.setOnFocusChangeListener { _, _ -> - inputEditTextField.post { - val inputMethodManager: InputMethodManager = - value.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.showSoftInput(inputEditTextField, InputMethodManager.SHOW_IMPLICIT) - } - } - inputEditTextField.requestFocus() - inputEditTextField.setText(defaultValue) - - val container = FrameLayout(value) - val params: FrameLayout.LayoutParams = FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - params.leftMargin = value.resources.getDimensionPixelSize(R.dimen.default_layout_margin) - params.rightMargin = value.resources.getDimensionPixelSize(R.dimen.default_layout_margin) - inputEditTextField.layoutParams = params - container.addView(inputEditTextField) - - val builder = AlertDialog.Builder(value, R.style.AlertDialogTheme) - builder.setTitle(title) - builder.setMessage(message) - builder.setView(container) - builder.setPositiveButton(android.R.string.ok) { _, _ -> - val editTextInput = inputEditTextField.text.toString() - - callback.invoke( - if (editTextInput.isEmpty()) { - null - } else { - editTextInput - } - ) - } - builder.setNegativeButton(android.R.string.cancel) { _, _ -> } - - builder.show() - } - } - - class CheckNetwork( - private val callback: () -> Unit, - ): ProfileSideEffect() { - override suspend fun execute(value: Context) { - val builder = AlertDialog.Builder(value, R.style.AlertDialogTheme) - builder.setTitle(value.getString(R.string.network_check_title)) - builder.setMessage(value.getString(R.string.network_check_message)) - builder.setNegativeButton(R.string.no) { _, _ -> } - builder.setPositiveButton(R.string.yes) { _, _ -> - callback.invoke() - } - builder.show() - } - } - - class ShowMnemonicToUser( - private val mnemonic: String, - private val callback: () -> Unit, - ): ProfileSideEffect() { - override suspend fun execute(value: Context) { - val builder = AlertDialog.Builder(value, R.style.AlertDialogTheme) - builder.setTitle(value.getString(R.string.store_mnemonic)) - builder.setMessage(mnemonic) - builder.setNeutralButton(android.R.string.copy) { _, _ -> - (value.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.let { manager -> - val clipData = ClipData.newPlainText("mnemonic", mnemonic) - manager.setPrimaryClip(clipData) - - SphinxToastUtils().show(value, R.string.mnemonic_copied_to_clipboard) - } - callback.invoke() - } - builder.setPositiveButton(android.R.string.ok) { _, _ -> - callback.invoke() - } - builder.show() - } - } - - class CheckBitcoinNetwork( - private val regTestCallback: () -> Unit, - private val mainNetCallback: () -> Unit, - private val callback: () -> Unit, - ): ProfileSideEffect() { - override suspend fun execute(value: Context) { - val builder = AlertDialog.Builder(value, R.style.AlertDialogTheme) - builder.setTitle(value.getString(R.string.select_bitcoin_network)) - - val items = arrayOf(BITCOIN_NETWORK_REG_TEST.toCapitalized(), BITCOIN_NETWORK_MAIN_NET.toCapitalized()) - builder.setSingleChoiceItems(items, 0 - ) { _, p1 -> - when (p1) { - 0 -> { - regTestCallback.invoke() - } - 1 -> { - mainNetCallback.invoke() - } - else -> {} - } - } - builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> - dialog.dismiss() - } - builder.setPositiveButton(android.R.string.ok) { _, _ -> - callback.invoke() - } - builder.show() - } - } - - object SendingSeedToHardware: ProfileSideEffect() { - override suspend fun execute(value: Context) { - SphinxToastUtils().show(value, R.string.sending_seed) - } - } - - object SigningDeviceSuccessfullySet: ProfileSideEffect() { - override suspend fun execute(value: Context) { - SphinxToastUtils(true).show(value, R.string.signing_device_successfully_set) - } - } - - class FailedToSetupSigningDevice( - private val errorMessage: String - ): ProfileSideEffect() { - override suspend fun execute(value: Context) { - SphinxToastUtils(true).show( - value, - value.getString(R.string.error_setting_up_signing_device, errorMessage) - ) - } - } } @Suppress("NOTHING_TO_INLINE") diff --git a/sphinx/screens/profile/profile/src/main/java/chat/sphinx/profile/ui/ProfileViewModel.kt b/sphinx/screens/profile/profile/src/main/java/chat/sphinx/profile/ui/ProfileViewModel.kt index 29468bf7fd..6c281f0840 100644 --- a/sphinx/screens/profile/profile/src/main/java/chat/sphinx/profile/ui/ProfileViewModel.kt +++ b/sphinx/screens/profile/profile/src/main/java/chat/sphinx/profile/ui/ProfileViewModel.kt @@ -4,18 +4,12 @@ import android.app.Application import android.content.Context import android.os.Environment import android.os.StatFs -import android.text.InputType import android.webkit.URLUtil import androidx.lifecycle.viewModelScope import app.cash.exhaustive.Exhaustive -import cash.z.ecc.android.bip39.Mnemonics -import cash.z.ecc.android.bip39.toEntropy -import cash.z.ecc.android.bip39.toSeed import chat.sphinx.camera_view_model_coordinator.request.CameraRequest import chat.sphinx.camera_view_model_coordinator.response.CameraResponse import chat.sphinx.concept_background_login.BackgroundLoginHandler -import chat.sphinx.concept_network_query_crypter.NetworkQueryCrypter -import chat.sphinx.concept_network_query_crypter.model.SendSeedDto import chat.sphinx.concept_network_query_relay_keys.NetworkQueryRelayKeys import chat.sphinx.concept_network_tor.TorManager import chat.sphinx.concept_relay.RelayDataHandler @@ -24,31 +18,26 @@ import chat.sphinx.concept_repository_feed.FeedRepository import chat.sphinx.concept_repository_lightning.LightningRepository import chat.sphinx.concept_repository_media.RepositoryMedia import chat.sphinx.concept_view_model_coordinator.ViewModelCoordinator -import chat.sphinx.concept_wallet.WalletDataHandler import chat.sphinx.kotlin_response.LoadResponse import chat.sphinx.kotlin_response.Response import chat.sphinx.kotlin_response.ResponseError import chat.sphinx.logger.SphinxLogger -import chat.sphinx.logger.d import chat.sphinx.menu_bottom_profile_pic.PictureMenuHandler import chat.sphinx.menu_bottom_profile_pic.PictureMenuViewModel import chat.sphinx.menu_bottom_profile_pic.UpdatingImageViewState import chat.sphinx.profile.R -import chat.sphinx.wrapper_common.FeedRecommendationsToggle -import chat.sphinx.wrapper_common.PreviewsEnabled -import chat.sphinx.wrapper_common.calculateSize -import chat.sphinx.wrapper_common.calculateStoragePercentage -import chat.sphinx.wrapper_common.isTrue +import chat.sphinx.wrapper_common.* import chat.sphinx.wrapper_common.lightning.Sat import chat.sphinx.wrapper_common.message.SphinxCallLink -import chat.sphinx.wrapper_common.toFileSize import chat.sphinx.wrapper_contact.Contact import chat.sphinx.wrapper_contact.PrivatePhoto import chat.sphinx.wrapper_lightning.NodeBalance -import chat.sphinx.wrapper_lightning.WalletMnemonic -import chat.sphinx.wrapper_lightning.toWalletMnemonic -import chat.sphinx.wrapper_relay.* +import chat.sphinx.wrapper_relay.AuthorizationToken +import chat.sphinx.wrapper_relay.RelayUrl +import chat.sphinx.wrapper_relay.isOnionAddress +import chat.sphinx.wrapper_relay.toRelayUrl import chat.sphinx.wrapper_rsa.RsaPublicKey +import com.squareup.moshi.Moshi import dagger.hilt.android.lifecycle.HiltViewModel import io.matthewnelson.android_feature_viewmodel.SideEffectViewModel import io.matthewnelson.android_feature_viewmodel.submitSideEffect @@ -59,10 +48,11 @@ import io.matthewnelson.concept_authentication.coordinator.AuthenticationRespons import io.matthewnelson.concept_authentication.coordinator.ConfirmedPinListener import io.matthewnelson.concept_coroutines.CoroutineDispatchers import io.matthewnelson.concept_encryption_key.EncryptionKey +import io.matthewnelson.concept_views.sideeffect.SideEffectContainer import io.matthewnelson.concept_views.viewstate.ViewStateContainer import io.matthewnelson.crypto_common.annotations.RawPasswordAccess -import io.matthewnelson.crypto_common.clazzes.* -import io.matthewnelson.crypto_common.extensions.toHex +import io.matthewnelson.crypto_common.clazzes.Password +import io.matthewnelson.crypto_common.clazzes.clear import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -71,11 +61,32 @@ import kotlinx.coroutines.withContext import okio.base64.encodeBase64 import org.cryptonode.jncryptor.AES256JNCryptor import org.cryptonode.jncryptor.CryptorException -import uniffi.sphinxrs.deriveSharedSecret -import uniffi.sphinxrs.encrypt -import uniffi.sphinxrs.pubkeyFromSecretKey -import java.security.SecureRandom import javax.inject.Inject +import kotlinx.serialization.Serializable + +@Serializable +data class SampleClass( + val elements: List +) + +@Serializable +data class SampleClassElement( + val key: String, + val value: SampleClassSubElement +) + +@Serializable +data class SampleClassSubElement( + val key: Int, + val value: List +) + +@Serializable +data class SampleClassSubSubElement( + val key1: UInt, + val key2: UInt, + val key3: UInt +) @HiltViewModel internal class ProfileViewModel @Inject constructor( @@ -89,24 +100,16 @@ internal class ProfileViewModel @Inject constructor( private val feedRepository: FeedRepository, private val repositoryMedia: RepositoryMedia, private val networkQueryRelayKeys: NetworkQueryRelayKeys, - private val networkQueryCrypter: NetworkQueryCrypter, private val relayDataHandler: RelayDataHandler, private val torManager: TorManager, private val LOG: SphinxLogger, - private val walletDataHandler: WalletDataHandler, -): SideEffectViewModel< + val moshi: Moshi + ): SideEffectViewModel< Context, ProfileSideEffect, ProfileViewState>(dispatchers, ProfileViewState.Basic), PictureMenuViewModel { - companion object { - const val SIGNING_DEVICE_SHARED_PREFERENCES = "general_settings" - const val SIGNING_DEVICE_SETUP_KEY = "signing-device-setup" - - const val BITCOIN_NETWORK_REG_TEST = "regtest" - const val BITCOIN_NETWORK_MAIN_NET = "mainnet" - } val storageBarViewStateContainer: ViewStateContainer by lazy { ViewStateContainer(StorageBarViewState.Loading) @@ -233,15 +236,10 @@ internal class ProfileViewModel @Inject constructor( } else { updateViewState( ProfileViewState.Advanced( - if (isSigningDeviceSetupDone()) { - app.getString(R.string.configure_signing_device) - } else { app.getString(R.string.setup_signing_device) - } ) ) } - } suspend fun getAccountBalance(): StateFlow = @@ -578,232 +576,6 @@ internal class ProfileViewModel @Inject constructor( _feedRecommendationsStateFlow.value = feedRecommendationsToggle } - private var setupSigningDeviceJob: Job? = null - private var seedDto = SendSeedDto() - - private fun resetSeedDto() { - seedDto = SendSeedDto() - } - - fun setupSigningDevice() { - if (setupSigningDeviceJob?.isActive == true) return - - setupSigningDeviceJob = viewModelScope.launch(mainImmediate) { - submitSideEffect(ProfileSideEffect.CheckNetwork { - viewModelScope.launch(mainImmediate) { - submitSideEffect(ProfileSideEffect.SigningDeviceInfo( - app.getString(R.string.network_name_title), - app.getString(R.string.network_name_message) - ) { networkName -> - viewModelScope.launch(mainImmediate) { - if (networkName == null) { - submitSideEffect(ProfileSideEffect.FailedToSetupSigningDevice("Network can not be empty")) - return@launch - } - - seedDto.ssid = networkName - - submitSideEffect(ProfileSideEffect.SigningDeviceInfo( - app.getString(R.string.network_password_title), - app.getString( - R.string.network_password_message, - networkName ?: "-" - ), - inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - ) { networkPass -> - viewModelScope.launch(mainImmediate) { - if (networkPass == null) { - submitSideEffect(ProfileSideEffect.FailedToSetupSigningDevice("Network password can not be empty")) - return@launch - } - - seedDto.pass = networkPass - - submitSideEffect(ProfileSideEffect.SigningDeviceInfo( - app.getString(R.string.lightning_node_url_title), - app.getString(R.string.lightning_node_url_message), - ) { lightningNodeUrl -> - viewModelScope.launch(mainImmediate) { - if (lightningNodeUrl == null) { - submitSideEffect(ProfileSideEffect.FailedToSetupSigningDevice("Lightning node URL can not be empty")) - return@launch - } - - seedDto.lightningNodeUrl = lightningNodeUrl - - submitSideEffect(ProfileSideEffect.CheckBitcoinNetwork( - regTestCallback = { - seedDto.network = BITCOIN_NETWORK_REG_TEST - }, mainNetCallback = { - seedDto.network = BITCOIN_NETWORK_MAIN_NET - }, callback = { - viewModelScope.launch(mainImmediate) { - linkSigningDevice() - } - } - )) - } - }) - } - }) - } - }) - } - }) - } - } - - private suspend fun linkSigningDevice() { - val secKey = ByteArray(32) - SecureRandom().nextBytes(secKey) - - val sk1 = secKey.toHex() - val pk1 = pubkeyFromSecretKey(sk1) - - var pk2 : String? = null - - if (pk1 == null) { - submitSideEffect(ProfileSideEffect.FailedToSetupSigningDevice("error generating secret key")) - resetSeedDto() - return - } - - seedDto.pubkey = pk1 - - if ( - seedDto.lightningNodeUrl == null || - seedDto.lightningNodeUrl?.isEmpty() == true - ) { - resetSeedDto() - submitSideEffect(ProfileSideEffect.FailedToSetupSigningDevice("lightning node URL can't be empty")) - return - } - - networkQueryCrypter.getCrypterPubKey().collect { loadResponse -> - when (loadResponse) { - is LoadResponse.Loading -> {} - is Response.Error -> { - resetSeedDto() - submitSideEffect(ProfileSideEffect.FailedToSetupSigningDevice("error getting public key from hardware")) - } - is Response.Success -> { - pk2 = loadResponse.value.pubkey - } - } - } - - pk2?.let { nnPk2 -> - val sec1 = deriveSharedSecret(nnPk2, sk1) - val seedAndMnemonic = generateAndPersistMnemonic() - - seedAndMnemonic.second?.let { mnemonic -> - submitSideEffect(ProfileSideEffect.ShowMnemonicToUser( - mnemonic.value - ) { - seedAndMnemonic.first?.let { seed -> - viewModelScope.launch(mainImmediate) { - encryptAndSendSeed(seed, sec1) - } - } - }) - } - } - } - - private suspend fun generateAndPersistMnemonic() : Pair { - var walletMnemonic: WalletMnemonic? = null - var seed: String? = null - - viewModelScope.launch(mainImmediate) { - walletMnemonic = walletDataHandler.retrieveWalletMnemonic() ?: run { - val entropy = (Mnemonics.WordCount.COUNT_12).toEntropy() - - Mnemonics.MnemonicCode(entropy).use { mnemonicCode -> - val wordsArray:MutableList = mutableListOf() - mnemonicCode.words.forEach { word -> - wordsArray.add(word.joinToString("")) - } - val words = wordsArray.joinToString(" ") - - words.toWalletMnemonic()?.let { walletMnemonic -> - if (walletDataHandler.persistWalletMnemonic(walletMnemonic)) { - LOG.d("MNEMONIC WORDS SAVED" , words) - LOG.d("MNEMONIC WORDS SAVED" , words) - } - walletMnemonic - } - } - } - - walletMnemonic?.value?.toCharArray()?.let { words -> - val mnemonic = Mnemonics.MnemonicCode(words) - - val seedData = mnemonic.toSeed().take(32).toByteArray() - seed = seedData.toHex() - } - }.join() - - return Pair(seed, walletMnemonic) - } - - private suspend fun encryptAndSendSeed( - seed: String, - sec1: String - ) { - val nonce = ByteArray(12) - SecureRandom().nextBytes(nonce) - - encrypt(seed, sec1, nonce.toHex()).let { cipher -> - if (cipher.isNotEmpty()) { - seedDto.seed = cipher - - submitSideEffect(ProfileSideEffect.SendingSeedToHardware) - - networkQueryCrypter.sendEncryptedSeed(seedDto).collect { loadResponse -> - when (loadResponse) { - is LoadResponse.Loading -> {} - is Response.Error -> { - resetSeedDto() - submitSideEffect(ProfileSideEffect.FailedToSetupSigningDevice("error sending seed to hardware")) - } - - is Response.Success -> { - submitSideEffect(ProfileSideEffect.SigningDeviceSuccessfullySet) - - setSigningDeviceSetupDone { - switchTabTo(false) - } - } - } - } - } - } - } - - private fun isSigningDeviceSetupDone(): Boolean { - val appContext: Context = app.applicationContext - val sharedPreferences = appContext.getSharedPreferences(SIGNING_DEVICE_SHARED_PREFERENCES, Context.MODE_PRIVATE) - - return sharedPreferences.getBoolean( - SIGNING_DEVICE_SETUP_KEY, - false - ) - } - - private suspend fun setSigningDeviceSetupDone( - callback: () -> Unit - ) { - val appContext: Context = app.applicationContext - val sharedPreferences = appContext.getSharedPreferences(SIGNING_DEVICE_SHARED_PREFERENCES, Context.MODE_PRIVATE) - - withContext(dispatchers.io) { - sharedPreferences.edit().putBoolean(SIGNING_DEVICE_SETUP_KEY, true) - .let { editor -> - if (!editor.commit()) { - editor.apply() - } - callback.invoke() - } - } - } + override val sideEffectContainer: SideEffectContainer + get() = super.sideEffectContainer } diff --git a/sphinx/screens/profile/profile/src/main/res/layout/fragment_profile.xml b/sphinx/screens/profile/profile/src/main/res/layout/fragment_profile.xml index 00651e270d..b0b31a10f0 100644 --- a/sphinx/screens/profile/profile/src/main/res/layout/fragment_profile.xml +++ b/sphinx/screens/profile/profile/src/main/res/layout/fragment_profile.xml @@ -54,4 +54,8 @@ android:id="@+id/include_layout_menu_bottom_profile_pic" layout="@layout/layout_menu_bottom" /> + + diff --git a/sphinx/screens/profile/profile/src/main/res/layout/layout_profile_advanced_container_holder.xml b/sphinx/screens/profile/profile/src/main/res/layout/layout_profile_advanced_container_holder.xml index e91d89845c..30992f1a9a 100644 --- a/sphinx/screens/profile/profile/src/main/res/layout/layout_profile_advanced_container_holder.xml +++ b/sphinx/screens/profile/profile/src/main/res/layout/layout_profile_advanced_container_holder.xml @@ -100,31 +100,6 @@ - - - - - - + app:layout_constraintTop_toBottomOf="@+id/layout_constraint_profile_advanced_container_github_pat">