package swiss.qpq.gajumobile import android.os.Bundle import android.view.WindowManager import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.platform.LocalContext import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ProcessLifecycleOwner import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import swiss.qpq.gajumobile.data.AppStateManager import swiss.qpq.gajumobile.data.WalletRepository import swiss.qpq.gajumobile.data.models.Account import swiss.qpq.gajumobile.data.models.GridsSignRequest import swiss.qpq.gajumobile.data.models.GridsSignType import swiss.qpq.gajumobile.data.models.Wallet import swiss.qpq.gajumobile.data.models.toGridsSignRequest import swiss.qpq.gajumobile.security.AppLifecycleObserver import swiss.qpq.gajumobile.security.BiometricHelper import swiss.qpq.gajumobile.security.SecurityManager import swiss.qpq.gajumobile.security.KeyManager import swiss.qpq.gajumobile.security.AccountAirlock import swiss.qpq.gajumobile.security.GajuNative import swiss.qpq.gajumobile.ui.screens.AccountNamingScreen import swiss.qpq.gajumobile.ui.screens.CreateWalletScreen import swiss.qpq.gajumobile.ui.screens.DashboardScreen import swiss.qpq.gajumobile.ui.screens.DeleteConfirmationScreen import swiss.qpq.gajumobile.ui.screens.EnvironmentInfoScreen import swiss.qpq.gajumobile.ui.screens.ExplorerScreen import swiss.qpq.gajumobile.ui.screens.FetchingScreen import swiss.qpq.gajumobile.ui.screens.LockScreen import swiss.qpq.gajumobile.ui.screens.ManageAccountScreen import swiss.qpq.gajumobile.ui.screens.ManageWalletsScreen import swiss.qpq.gajumobile.ui.screens.MnemonicRecoveryScreen import swiss.qpq.gajumobile.ui.screens.MnemonicVerifyScreen import swiss.qpq.gajumobile.ui.screens.QRReceiveScreen import swiss.qpq.gajumobile.ui.screens.QRScannerScreen import swiss.qpq.gajumobile.ui.screens.SendFormScreen import swiss.qpq.gajumobile.ui.screens.SendReviewScreen import swiss.qpq.gajumobile.ui.screens.SettingsScreen import swiss.qpq.gajumobile.ui.screens.SetupChoiceScreen import swiss.qpq.gajumobile.ui.screens.SignRequestScreen import swiss.qpq.gajumobile.ui.screens.ViewMnemonicScreen import swiss.qpq.gajumobile.ui.theme.MyApplicationTheme import swiss.qpq.gajumaru.core.formatting.GajuFormat import swiss.qpq.gajumaru.core.tools.Grids import swiss.qpq.gajumaru.core.tools.Http import swiss.qpq.gajumaru.core.tools.NodeClient import swiss.qpq.gajumaru.core.tools.TransactionService import java.math.BigInteger import java.util.UUID class MainActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() super.onCreate(savedInstanceState) enableEdgeToEdge() window.isNavigationBarContrastEnforced = false window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver(this)) setContent { val context = LocalContext.current val isLocked by SecurityManager.isLocked.collectAsState() var lockErrorMessage by remember { mutableStateOf(null) } val walletRepo = remember { WalletRepository(context) } val appStateManager = remember { AppStateManager(context) } MyApplicationTheme { if (isLocked) { LockScreen(errorMessage = lockErrorMessage) { BiometricHelper.showBiometricPrompt( activity = this, onSuccess = { lockErrorMessage = null SecurityManager.unlock() }, ) { err -> lockErrorMessage = err } } } else { val routerError: (String) -> Unit = { message -> Toast.makeText(this@MainActivity, message, Toast.LENGTH_LONG).show() } GajuRouter(this@MainActivity, walletRepo, appStateManager, routerError) } } } } } enum class Screen { CREATE_WALLET, SETUP_CHOICE, MNEMONIC_RECOVERY, ACCOUNT_NAMING, DASHBOARD, QR_SCANNER, QR_RECEIVE, SETTINGS, MANAGE_ACCOUNT, MANAGE_WALLETS, DELETE_CONFIRM, EXPLORER, VIEW_MNEMONIC, VERIFY_MNEMONIC, ENVIRONMENT_INFO, SEND_FORM, SEND_REVIEW, SIGN_REQUEST, FETCHING_SIGN_REQUEST } @Composable fun GajuRouter( context: android.content.Context, walletRepo: WalletRepository, appStateManager: AppStateManager, onError: (String) -> Unit, ) { val appState by appStateManager.state.collectAsState() val scope = rememberCoroutineScope() val initialScreen = remember { val wallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } if (wallets.isEmpty()) Screen.CREATE_WALLET else Screen.DASHBOARD } val screenStack = remember { mutableStateListOf(initialScreen) } val currentScreen = screenStack.last() fun navigateTo(screen: Screen) { screenStack.add(screen) } fun navigateBack() { if (screenStack.size > 1) { screenStack.removeAt(screenStack.size - 1) } } BackHandler(enabled = screenStack.size > 1) { navigateBack() } var pendingWalletName by rememberSaveable { mutableStateOf("") } var pendingNetworkId by rememberSaveable { mutableStateOf("groot.mainnet") } var pendingAccount by remember { mutableStateOf(null) } var setupRecovered by rememberSaveable { mutableStateOf(value = false) } var selectedWalletId by rememberSaveable { mutableStateOf("") } var selectedAccountId by rememberSaveable { mutableStateOf("") } var deleteType by rememberSaveable { mutableStateOf("") } var deleteName by rememberSaveable { mutableStateOf("") } var viewMnemonicAccount by remember { mutableStateOf(null) } var viewMnemonicPhrase by remember { mutableStateOf?>(null) } var verifyMnemonicAccount by remember { mutableStateOf(null) } var verifyMnemonicPhrase by remember { mutableStateOf?>(null) } var activeWalletIndex by rememberSaveable { mutableIntStateOf(0) } var activeAccountIndex by rememberSaveable { mutableIntStateOf(0) } var nodeStatusResult by remember { mutableStateOf?>(null) } var nodeStatusError by remember { mutableStateOf(null) } var pendingScanResult by remember { mutableStateOf(null) } var pendingRecipient by rememberSaveable { mutableStateOf("") } var pendingAmountStr by rememberSaveable { mutableStateOf("") } var pendingPayload by rememberSaveable { mutableStateOf("") } var pendingGas by rememberSaveable { mutableStateOf("") } var pendingGasPrice by rememberSaveable { mutableStateOf("") } var pendingTTL by rememberSaveable { mutableStateOf("") } var pendingSignRequest by remember { mutableStateOf(null) } var pendingSignUrl by rememberSaveable { mutableStateOf("") } var activeFetchJob by remember { mutableStateOf(null) } when (currentScreen) { Screen.CREATE_WALLET -> { CreateWalletScreen( onWalletCreated = { name, networkId -> pendingWalletName = name pendingNetworkId = networkId selectedWalletId = "" navigateTo(Screen.SETUP_CHOICE) }, onBack = if (screenStack.size > 1) { { navigateBack() } } else null, ) } Screen.SETUP_CHOICE -> { SetupChoiceScreen( onCreate = { try { val masterKey = KeyManager.getMasterKey(context) val phrase = AccountAirlock.generateMnemonic() val account = AccountAirlock.createAccount("New Account", phrase, masterKey) AccountAirlock.wipePhrase(phrase) pendingAccount = account setupRecovered = false navigateTo(Screen.ACCOUNT_NAMING) } catch (e: Throwable) { val msg = e.message ?: e.toString() onError(context.getString(R.string.err_generate_account, msg)) android.util.Log.e("GajuRouter", "Account generation failed", e) } }, onRecover = { setupRecovered = true navigateTo(Screen.MNEMONIC_RECOVERY) }, onBack = { navigateBack() }, ) } Screen.MNEMONIC_RECOVERY -> { MnemonicRecoveryScreen( onMnemonicConfirmed = { phrase -> try { val masterKey = KeyManager.getMasterKey(context) val account = AccountAirlock.createAccount("Recovered Account", phrase, masterKey) AccountAirlock.wipePhrase(phrase) pendingAccount = account navigateTo(Screen.ACCOUNT_NAMING) } catch (e: Exception) { onError(context.getString(R.string.err_recover_account, e.message ?: "")) } }, onBack = { navigateBack() }, ) } Screen.ACCOUNT_NAMING -> { val account = pendingAccount ?: return val akId = account.gajuId AccountNamingScreen( publicKey = akId, onAccountNamed = { name -> val finalAccount = account.copy(label = name) if (selectedWalletId.isNotEmpty()) { val wallet = walletRepo.loadWallet(selectedWalletId) wallet?.let { walletRepo.saveWallet(it.copy(accounts = it.accounts + finalAccount)) } } else { val endpoints = if (pendingNetworkId == "groot.mainnet") Wallet.DEFAULT_MAINNET else Wallet.DEFAULT_TESTNET val wallet = Wallet( id = UUID.randomUUID().toString(), name = pendingWalletName.ifBlank { "My Wallet" }, networkId = pendingNetworkId, endpoints = endpoints, accounts = listOf(finalAccount), ) walletRepo.saveWallet(wallet) } if (!setupRecovered) { appStateManager.markUnverified(finalAccount.id) } selectedWalletId = "" appStateManager.notifyWalletsChanged() screenStack.clear() screenStack.add(Screen.DASHBOARD) }, onBack = { navigateBack() }, ) } Screen.VIEW_MNEMONIC -> { val acc = viewMnemonicAccount ?: return val phrase = viewMnemonicPhrase ?: return ViewMnemonicScreen(label = acc.label, phrase = phrase) { AccountAirlock.wipePhrase(phrase) viewMnemonicAccount = null viewMnemonicPhrase = null navigateBack() } } Screen.VERIFY_MNEMONIC -> { val acc = verifyMnemonicAccount ?: return val phrase = verifyMnemonicPhrase ?: return MnemonicVerifyScreen( label = acc.label, expectedPhrase = phrase, onVerified = { appStateManager.clearVerified(acc.id) AccountAirlock.wipePhrase(phrase) verifyMnemonicAccount = null verifyMnemonicPhrase = null navigateBack() }, onBack = { AccountAirlock.wipePhrase(phrase) verifyMnemonicAccount = null verifyMnemonicPhrase = null navigateBack() } ) } Screen.DELETE_CONFIRM -> { DeleteConfirmationScreen( type = deleteType, name = deleteName, onConfirm = { if (deleteType == "WALLET") { walletRepo.deleteWallet(selectedWalletId) } else { walletRepo.deleteAccount(selectedWalletId, selectedAccountId) } appStateManager.notifyWalletsChanged() // Pop back to Dashboard by removing the confirmation and the management screen repeat(2) { if (screenStack.size > 1) screenStack.removeAt(screenStack.size - 1) } }, onCancel = { navigateBack() } ) } Screen.DASHBOARD -> { val wallets = remember(appState.walletsVersion) { walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } } DashboardScreen( wallets = wallets, selectedWalletIndex = activeWalletIndex, onWalletIndexChange = { activeWalletIndex = it }, selectedAccountIndex = activeAccountIndex, onAccountIndexChange = { activeAccountIndex = it }, balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat), balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit), onSend = { navigateTo(Screen.QR_SCANNER) }, onGrids = { navigateTo(Screen.QR_SCANNER) }, onReceive = { navigateTo(Screen.QR_RECEIVE) }, onAddAccount = { walletId -> selectedWalletId = walletId navigateTo(Screen.SETUP_CHOICE) }, onAccountSettings = { walletId, accountId -> selectedWalletId = walletId selectedAccountId = accountId navigateTo(Screen.MANAGE_ACCOUNT) }, onRefresh = { walletId, accountId -> scope.launch { val updated = walletRepo.refreshAccount(walletId, accountId) if (updated != null) { appStateManager.notifyWalletsChanged() } else { onError(context.getString(R.string.err_refresh_failed)) } } }, onNavigate = { dest -> val screen = when (dest) { "settings" -> Screen.SETTINGS "transactions" -> Screen.EXPLORER "environment_info" -> Screen.ENVIRONMENT_INFO "manage_wallets" -> Screen.MANAGE_WALLETS "CREATE_WALLET" -> Screen.CREATE_WALLET "WIPE_DATA" -> { walletRepo.listWallets().forEach { walletRepo.deleteWallet(it) } appStateManager.notifyWalletsChanged() navigateTo(Screen.CREATE_WALLET) null } else -> null } screen?.let { navigateTo(it) } }, ) } Screen.MANAGE_WALLETS -> { val wallets = remember(appState.walletsVersion) { walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } } ManageWalletsScreen( wallets = wallets, onAddWallet = { selectedWalletId = "" navigateTo(Screen.CREATE_WALLET) }, onRenameWallet = { id, newName -> val wallet = walletRepo.loadWallet(id) wallet?.let { walletRepo.saveWallet(it.copy(name = newName)) appStateManager.notifyWalletsChanged() } }, onDeleteWallet = { id -> val wallet = walletRepo.loadWallet(id) wallet?.let { selectedWalletId = id deleteType = "WALLET" deleteName = it.name navigateTo(Screen.DELETE_CONFIRM) } }, onUpdateEndpoints = { id, endpoints -> val wallet = walletRepo.loadWallet(id) wallet?.let { walletRepo.saveWallet(it.copy(endpoints = endpoints)) appStateManager.notifyWalletsChanged() } }, onAddEndpoint = { walletId, host, port, useTls -> scope.launch { try { val wallet = walletRepo.loadWallet(walletId) ?: return@launch val endpoint = swiss.qpq.gajumobile.data.models.Endpoint(host, port, useTls) val client = NodeClient(listOf(NodeClient.Endpoint(host, port, useTls))) val status = withContext(Dispatchers.IO) { client.status() } val reportedNetworkId = status["network_id"] as? String if ((reportedNetworkId != null && reportedNetworkId != wallet.networkId)) { val allWallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } val targetWallet = allWallets.find { it.networkId == reportedNetworkId } if (targetWallet != null) { val targetEndpoints = targetWallet.endpoints + endpoint walletRepo.saveWallet(targetWallet.copy(endpoints = targetEndpoints)) appStateManager.notifyWalletsChanged() onError(context.getString(R.string.err_network_mismatch_added, reportedNetworkId, targetWallet.name)) } else { nodeStatusError = context.getString(R.string.err_critical_mismatch_create, reportedNetworkId, wallet.networkId) } } else { val newList = wallet.endpoints + endpoint walletRepo.saveWallet(wallet.copy(endpoints = newList)) appStateManager.notifyWalletsChanged() onError(context.getString(R.string.msg_endpoint_verified)) } } catch (e: Exception) { // Even if status check fails, we might still want to add it? // User said: "We should test the endpoints and fall back to HTTP" // If it fails completely, maybe don't add it or warn the user. nodeStatusError = "Failed to verify endpoint: ${e.message}. It has not been added." } } }, onCheckStatus = { walletId, index -> scope.launch { try { val wallet = walletRepo.loadWallet(walletId) ?: return@launch val endpoint = wallet.endpoints.getOrNull(index) ?: return@launch val client = NodeClient(listOf(NodeClient.Endpoint(endpoint.host, endpoint.port, endpoint.useTls))) val status = withContext(Dispatchers.IO) { client.status() } val reportedNetworkId = status["network_id"] as? String if ((reportedNetworkId != null && reportedNetworkId != wallet.networkId)) { // Mismatch logic val allWallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } val targetWallet = allWallets.find { it.networkId == reportedNetworkId } if (targetWallet != null) { // Move endpoint val sourceEndpoints = wallet.endpoints.toMutableList().apply { removeAt(index) } val targetEndpoints = targetWallet.endpoints + endpoint walletRepo.saveWallet(wallet.copy(endpoints = sourceEndpoints)) walletRepo.saveWallet(targetWallet.copy(endpoints = targetEndpoints)) appStateManager.notifyWalletsChanged() onError(context.getString(R.string.err_network_mismatch_moved, reportedNetworkId, targetWallet.name)) } else { // Warn for discard nodeStatusError = context.getString(R.string.err_critical_mismatch_fix, reportedNetworkId, wallet.networkId) } } else { nodeStatusResult = status } } catch (e: Exception) { nodeStatusError = e.message ?: "Unknown error" } } }, statusResult = nodeStatusResult, statusError = nodeStatusError, onClearStatus = { nodeStatusResult = null nodeStatusError = null }, onBack = { navigateBack() }, ) } Screen.MANAGE_ACCOUNT -> { val wallet = remember(selectedWalletId, appState.walletsVersion) { walletRepo.loadWallet(selectedWalletId) } ?: return val account = wallet.accounts.find { it.id == selectedAccountId } ?: return val isUnverified = appState.unverifiedAccountIds.contains(account.id) ManageAccountScreen( account = account, walletName = wallet.name, balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat), balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit), isUnverified = isUnverified, onViewMnemonic = { try { val masterKey = KeyManager.getMasterKey(context) val phrase = AccountAirlock.deriveMnemonic(account.privateKeyEnvelope, masterKey) viewMnemonicAccount = account viewMnemonicPhrase = phrase navigateTo(Screen.VIEW_MNEMONIC) } catch (e: Exception) { onError(context.getString(R.string.err_auth_failed, e.message ?: "")) } }, onVerifyMnemonic = { try { val masterKey = KeyManager.getMasterKey(context) val phrase = AccountAirlock.deriveMnemonic(account.privateKeyEnvelope, masterKey) verifyMnemonicAccount = account verifyMnemonicPhrase = phrase navigateTo(Screen.VERIFY_MNEMONIC) } catch (e: Exception) { onError(context.getString(R.string.err_auth_failed, e.message ?: "")) } }, onRename = { newName -> val updatedAccounts = wallet.accounts.map { if (it.id == account.id) it.copy(label = newName) else it } walletRepo.saveWallet(wallet.copy(accounts = updatedAccounts)) appStateManager.notifyWalletsChanged() }, onDelete = { deleteType = "ACCOUNT" deleteName = account.label navigateTo(Screen.DELETE_CONFIRM) } ) } Screen.SETTINGS -> { SettingsScreen( balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat), onFormatChange = { type -> appStateManager.setBalanceFormat(type.name) }, onNavigate = { dest -> if (dest == "WIPE_DATA") { walletRepo.listWallets().forEach { walletRepo.deleteWallet(it) } appStateManager.notifyWalletsChanged() screenStack.clear() screenStack.add(Screen.CREATE_WALLET) } else { val screen = when (dest) { "dashboard" -> Screen.DASHBOARD "transactions" -> Screen.EXPLORER "environment_info" -> Screen.ENVIRONMENT_INFO "manage_wallets" -> Screen.MANAGE_WALLETS else -> null } if (screen != null) { screenStack.clear() screenStack.add(screen) } } } ) } Screen.ENVIRONMENT_INFO -> { EnvironmentInfoScreen(onBack = { navigateBack() }) } Screen.FETCHING_SIGN_REQUEST -> { FetchingScreen( url = pendingSignUrl, onCancel = { activeFetchJob?.cancel() navigateBack() } ) } Screen.QR_SCANNER -> QRScannerScreen( onScanResult = { url -> try { val result = Grids.parse(url) if (result.verb == Grids.Verb.SIGN) { pendingSignUrl = result.url navigateTo(Screen.FETCHING_SIGN_REQUEST) activeFetchJob = scope.launch { try { android.util.Log.i("GajuRouter", "Fetching GRIDS request from: ${result.url}") @Suppress("UNCHECKED_CAST") val response = withContext(Dispatchers.IO) { Http.get(result.url) as? Map } if (response != null) { pendingSignRequest = response.toGridsSignRequest() // Remove fetching screen and add request screen screenStack.removeAt(screenStack.size - 1) navigateTo(Screen.SIGN_REQUEST) } else { onError(context.getString(R.string.err_fetch_sig_failed)) navigateBack() } } catch (e: Exception) { if (activeFetchJob?.isCancelled == false) { onError(context.getString(R.string.err_fetch_failed, e.message ?: "")) navigateBack() } } finally { activeFetchJob = null } } } else { pendingScanResult = result navigateTo(Screen.SEND_FORM) } } catch (e: Exception) { onError(context.getString(R.string.err_invalid_grids_url, e.message ?: "")) } }, onGenerate = { navigateTo(Screen.QR_RECEIVE) }, onUpload = {}, onManualEntry = { pendingScanResult = null navigateTo(Screen.SEND_FORM) }, onBack = { navigateBack() }, ) Screen.SEND_FORM -> { val wallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } val activeWallet = wallets.getOrNull(activeWalletIndex) val activeAccount = activeWallet?.accounts?.getOrNull(activeAccountIndex) if (activeAccount == null) { onError(context.getString(R.string.err_no_active_account)) navigateBack() return } val result = pendingScanResult val initialRecipient = result?.recipient ?: "" val initialAmount = if (result?.amount != null) { val type = GajuFormat.Type.valueOf(appState.balanceFormat) val unit = GajuFormat.Unit.valueOf(appState.balanceUnit) GajuFormat.amount(GajuFormat.standardSpec(type, unit), result.amount) } else "" val initialPayload = if (result?.payload != null) String(result.payload, Charsets.UTF_8) else "" SendFormScreen( fromAccount = activeAccount, balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat), balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit), initialRecipient = initialRecipient, initialAmount = initialAmount, initialPayload = initialPayload, onReview = { recipient, amountStr, payload, gas, gasPrice, ttl -> pendingRecipient = recipient pendingAmountStr = amountStr pendingPayload = payload pendingGas = gas pendingGasPrice = gasPrice pendingTTL = ttl navigateTo(Screen.SEND_REVIEW) }, onBack = { navigateBack() }, ) } Screen.SEND_REVIEW -> { val wallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } val activeWallet = wallets.getOrNull(activeWalletIndex) val activeAccount = activeWallet?.accounts?.getOrNull(activeAccountIndex) if (activeAccount == null) { onError(context.getString(R.string.err_no_active_account)) navigateBack() return } SendReviewScreen( senderLabel = activeAccount.label, recipientLabel = "Recipient", recipientId = pendingRecipient, amountStr = pendingAmountStr, onSend = { scope.launch { try { // 1. Get next nonce and current height val (nonce, height) = withContext(Dispatchers.IO) { val client = NodeClient(activeWallet.endpoints.map { NodeClient.Endpoint(it.host, it.port) }) client.nextNonce(activeAccount.gajuId) to client.topHeight() } // 2. Derive seed from airlock val masterKey = KeyManager.getMasterKey(context) val seed = AccountAirlock.derivePrivateKey(activeAccount.privateKeyEnvelope, masterKey) // 3. Build and sign tx val amountPucks = BigInteger(GajuFormat.read(pendingAmountStr)) val signedTx = TransactionService.buildSpendTx( activeWallet.networkId, activeAccount.gajuId, pendingRecipient, amountPucks, BigInteger(pendingGasPrice), BigInteger(pendingGas), height + pendingTTL.toLong(), nonce, pendingPayload, seed ) // 4. Wipe seed GajuNative.memzero(seed) // 5. Post tx val result = withContext(Dispatchers.IO) { val client = NodeClient(activeWallet.endpoints.map { NodeClient.Endpoint(it.host, it.port) }) client.postTx(signedTx) } val hash = result["hash"] as? String ?: "Unknown" onError(context.getString(R.string.msg_tx_posted, hash)) // Success! Go back to dashboard screenStack.clear() screenStack.add(Screen.DASHBOARD) } catch (e: Throwable) { val msg = e.message ?: e.toString() onError(context.getString(R.string.err_send_failed, msg)) android.util.Log.e("GajuRouter", "Send failed", e) } } }, onBack = { navigateBack() } ) } Screen.SIGN_REQUEST -> { val wallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } val activeWallet = wallets.getOrNull(activeWalletIndex) val activeAccount = activeWallet?.accounts?.getOrNull(activeAccountIndex) val request = pendingSignRequest ?: return // If request specifies an ID, try to find that account val signAccount = if (request.publicId != null && request.publicId != "false") { wallets.flatMap { it.accounts }.find { it.gajuId == request.publicId } } else { activeAccount } if (signAccount == null) { onError(context.getString(R.string.err_no_matching_account)) navigateBack() return } SignRequestScreen( request = request, originUrl = pendingSignUrl, accountLabel = signAccount.label, accountId = signAccount.gajuId, onSign = { scope.launch { try { val masterKey = KeyManager.getMasterKey(context) val signedRequest = mutableMapOf() // Essential keys present in all GRIDS responses signedRequest["grids"] = request.grids signedRequest["chain"] = request.chain signedRequest["network_id"] = request.networkId signedRequest["type"] = request.type.toProtocolString() signedRequest["public_id"] = signAccount.gajuId // Must be the actual ID string, never 'false' when (request.type) { GridsSignType.MESSAGE -> { val sig = AccountAirlock.signMessage(signAccount.privateKeyEnvelope, masterKey, request.payload) signedRequest["payload"] = request.payload signedRequest["signature"] = android.util.Base64.encodeToString(sig, android.util.Base64.NO_WRAP) } GridsSignType.BINARY -> { val binary = swiss.qpq.gajumaru.core.encoding.ApiEncoder.decode(request.payload).payload() val sig = AccountAirlock.signBinary(signAccount.privateKeyEnvelope, masterKey, binary) signedRequest["payload"] = request.payload signedRequest["signature"] = android.util.Base64.encodeToString(sig, android.util.Base64.NO_WRAP) } GridsSignType.TX -> { val nid = request.networkId ?: activeWallet?.networkId ?: "groot.mainnet" val txBinary = swiss.qpq.gajumaru.core.encoding.ApiEncoder.decode(request.payload).payload() signedRequest["payload"] = AccountAirlock.signTx(signAccount.privateKeyEnvelope, masterKey, txBinary, nid) signedRequest["signed"] = true } else -> { throw Exception("Cannot sign unknown request type") } } android.util.Log.i("GajuRouter", "POSTing GRIDS response to $pendingSignUrl") withContext(Dispatchers.IO) { Http.post(pendingSignUrl, signedRequest) } onError(context.getString(R.string.msg_sig_submitted)) screenStack.clear() screenStack.add(Screen.DASHBOARD) } catch (e: Throwable) { val msg = e.message ?: e.toString() onError(context.getString(R.string.err_sig_failed, msg)) android.util.Log.e("GajuRouter", "Signing failed", e) } } }, onCancel = { navigateBack() } ) } Screen.QR_RECEIVE -> { val wallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) } val activeWallet = wallets.getOrNull(activeWalletIndex) val activeAccount = activeWallet?.accounts?.getOrNull(activeAccountIndex) if (activeAccount == null) { onError(context.getString(R.string.err_no_active_account)) navigateBack() return } QRReceiveScreen( account = activeAccount, networkId = activeWallet.networkId, balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat), balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit), onDone = { navigateBack() }, ) } Screen.EXPLORER -> { ExplorerScreen( onNavigate = { dest -> val screen = when (dest) { "dashboard" -> Screen.DASHBOARD "settings" -> Screen.SETTINGS "manage_wallets" -> Screen.MANAGE_WALLETS else -> null } if (screen != null) { screenStack.clear() screenStack.add(screen) } } ) } } }