diff --git a/.gitignore b/.gitignore index 4d72214..eb93920 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ app.run.xml release/ .claude/ design/ +website/.venv/ +website/site/ +website/.cache/ diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/data/Kyoto.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/data/Kyoto.kt new file mode 100644 index 0000000..9a73ceb --- /dev/null +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/data/Kyoto.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2021-2026 thunderbiscuit and contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE file. + */ + +package org.bitcoindevkit.devkitwallet.data + +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import org.bitcoindevkit.CbfBuilder +import org.bitcoindevkit.CbfClient +import org.bitcoindevkit.CbfNode +import org.bitcoindevkit.Info +import org.bitcoindevkit.IpAddress +import org.bitcoindevkit.Network +import org.bitcoindevkit.Peer +import org.bitcoindevkit.ScanType +import org.bitcoindevkit.Transaction +import org.bitcoindevkit.Update +import org.bitcoindevkit.Wallet +import org.bitcoindevkit.Warning +import org.bitcoindevkit.Wtxid +import kotlin.collections.listOf + +private const val TAG = "KyotoClient" + +// TODO: Document this class well +class Kyoto private constructor( + private val kyotoNode: CbfNode, + private val kyotoClient: CbfClient, +) { + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) + + fun start(): Flow { + kyotoNode.run() + + return flow { + // Set this to stop under certain circumstances + while (true) { + val update = kyotoClient.update() + emit(update) + } + } + } + + fun infoLog(): SharedFlow { + val sharedFlow = MutableSharedFlow(replay = 0) + scope.launch { + while (true) { + val info = kyotoClient.nextInfo() + sharedFlow.emit(info) + } + } + return sharedFlow + } + + fun warningLog(): SharedFlow { + val sharedFlow = MutableSharedFlow(replay = 0) + scope.launch { + while (true) { + val warning = kyotoClient.nextWarning() + sharedFlow.emit(warning) + } + } + return sharedFlow + } + + fun logToLogcat() { + scope.launch { + infoLog().collect { + Log.i(TAG, it.toString()) + } + } + scope.launch { + warningLog().collect { + Log.i(TAG, it.toString()) + } + } + } + + fun lookupHost(hostname: String): List { + return kyotoClient.lookupHost(hostname) + } + + suspend fun broadcast(transaction: Transaction): Wtxid { + return kyotoClient.broadcast(transaction) + } + + fun connect(peer: Peer) { + kyotoClient.connect(peer) + } + + fun isRunning(): Boolean { + return kyotoClient.isRunning() + } + + fun shutdown() { + kyotoClient.shutdown() + } + + companion object { + private var instance: Kyoto? = null + + fun getInstance(): Kyoto = instance ?: throw KyotoNotInitialized() + + fun create(wallet: Wallet, dataDir: String, network: Network): Kyoto { + Log.i(TAG, "Starting Kyoto node") + val peers: List = when (network) { + Network.REGTEST -> { + val ip: IpAddress = IpAddress.fromIpv4(10u, 0u, 2u, 2u) + val peer1: Peer = Peer(ip, 18444u, false) + listOf(peer1) + } + + Network.SIGNET -> { + val ip: IpAddress = IpAddress.fromIpv4(68u, 47u, 229u, 218u) + val peer1: Peer = Peer(ip, null, false) + listOf(peer1) + } + + else -> { + listOf() + } + } + + val (client, node) = + CbfBuilder() + .dataDir(dataDir) + .peers(peers) + .connections(1u) + .scanType(ScanType.Sync) + .build(wallet) + + return Kyoto(node, client).also { instance = it } + } + } +} + +class KyotoNotInitialized : Exception() diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt deleted file mode 100644 index d8a2b08..0000000 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2021-2026 thunderbiscuit and contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE file. - */ - -package org.bitcoindevkit.devkitwallet.domain - -import org.bitcoindevkit.FullScanRequest -import org.bitcoindevkit.SyncRequest -import org.bitcoindevkit.Transaction -import org.bitcoindevkit.Update -import org.bitcoindevkit.ElectrumClient as BdkElectrumClient -import org.bitcoindevkit.EsploraClient as BdkEsploraClient - -interface BlockchainClient { - fun clientId(): String - - fun fullScan(fullScanRequest: FullScanRequest, stopGap: ULong): Update - - fun sync(syncRequest: SyncRequest): Update - - fun broadcast(transaction: Transaction): Unit -} - -class EsploraClient(private val url: String) : BlockchainClient { - private val client = BdkEsploraClient(url) - - override fun clientId(): String { - return url - } - - override fun fullScan(fullScanRequest: FullScanRequest, stopGap: ULong): Update { - return client.fullScan(fullScanRequest, stopGap, parallelRequests = 2u) - } - - override fun sync(syncRequest: SyncRequest): Update { - return client.sync(syncRequest, parallelRequests = 2u) - } - - override fun broadcast(transaction: Transaction) { - client.broadcast(transaction) - } -} - -class ElectrumClient(private val url: String) : BlockchainClient { - private val client = BdkElectrumClient(url) - - override fun clientId(): String { - return url - } - - override fun fullScan(fullScanRequest: FullScanRequest, stopGap: ULong): Update { - return client.fullScan(fullScanRequest, stopGap, batchSize = 10uL, fetchPrevTxouts = true) - } - - override fun sync(syncRequest: SyncRequest): Update { - return client.sync(syncRequest, batchSize = 2uL, fetchPrevTxouts = true) - } - - override fun broadcast(transaction: Transaction) { - throw NotImplementedError("ElectrumClient.broadcast() is not implemented") - } -} diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt deleted file mode 100644 index a64a9df..0000000 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2021-2026 thunderbiscuit and contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE file. - */ - -package org.bitcoindevkit.devkitwallet.domain - -import org.bitcoindevkit.Network - -class BlockchainClientsConfig { - private var defaultClient: BlockchainClient? = null - private val allClients: MutableList = mutableListOf() - - fun getClient(): BlockchainClient? { - return defaultClient - } - - fun addClient(client: BlockchainClient, setDefault: Boolean) { - allClients.forEach { - if (it.clientId() == client.clientId()) { - throw IllegalArgumentException( - "Client with url ${client.clientId()} already exists" - ) - } - } - if (allClients.size >= 8) throw IllegalArgumentException("Maximum number of clients (8) reached") - allClients.add(client) - if (setDefault) { - defaultClient = client - } - } - - fun setDefaultClient(clientId: String) { - val client = allClients.find { it.clientId() == clientId } - if (client == null) throw IllegalArgumentException("Client with url $clientId not found") - defaultClient = client - } - - companion object { - fun createDefaultConfig(network: Network): BlockchainClientsConfig { - val config = BlockchainClientsConfig() - when (network) { - Network.REGTEST -> { - config.addClient(EsploraClient("http://10.0.2.2:3002"), true) - } - Network.TESTNET -> { - config.addClient(ElectrumClient("ssl://electrum.blockstream.info:60002"), true) - } - Network.TESTNET4 -> throw IllegalArgumentException("This app does not support testnet 4 yet") - Network.SIGNET -> { - config.addClient(ElectrumClient("ssl://mempool.space:60602"), true) - } - Network.BITCOIN -> throw IllegalArgumentException("This app does not support mainnet") - } - return config - } - } -} diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/ElectrumServer.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/ElectrumServer.kt deleted file mode 100644 index 0dae900..0000000 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/ElectrumServer.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2021-2026 thunderbiscuit and contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE file. - */ - -package org.bitcoindevkit.devkitwallet.domain - -private const val TAG = "ElectrumServer" - -class ElectrumServer { - // private var useDefaultElectrum: Boolean = true - // private var default: Blockchain - // private val esploraClient: EsploraClient = EsploraClient("https://esplora.testnet.kuutamo.cloud/") - // private var custom: Blockchain? = null - // private var customElectrumURL: String - // private val defaultElectrumURL = "tcp://10.0.2.2:60401" - // private val defaultElectrumURL = "ssl://electrum.blockstream.info:60002" - // private val defaultElectrumURL = "tcp://127.0.0.1:60401" - - // init { - // val blockchainConfig = BlockchainConfig.Electrum(ElectrumConfig( - // url = defaultElectrumURL, - // socks5 = null, - // retry = 5u, - // timeout = null, - // stopGap = 10u, - // validateDomain = true - // )) - // customElectrumURL = "" - // default = Blockchain(blockchainConfig) - // } - // - // val server: Blockchain - // get() = if (useDefaultElectrum) this.default else this.custom!! - - // if you're looking to test different public Electrum servers we recommend these 3: - // ssl://electrum.blockstream.info:60002 - // tcp://electrum.blockstream.info:60001 - // tcp://testnet.aranguren.org:51001 - // fun createCustomElectrum(electrumURL: String) { - // customElectrumURL = electrumURL - // val blockchainConfig = BlockchainConfig.Electrum(ElectrumConfig( - // url = customElectrumURL, - // socks5 = null, - // retry = 5u, - // timeout = null, - // stopGap = 10u, - // validateDomain = true - // )) - // custom = Blockchain(blockchainConfig) - // useCustomElectrum() - // Log.i(TAG, "New Electrum Server URL : $customElectrumURL") - // } - - // fun useCustomElectrum() { - // useDefaultElectrum = false - // } - // - // fun useDefaultElectrum() { - // useDefaultElectrum = true - // } - // - // fun isElectrumServerDefault(): Boolean { - // return useDefaultElectrum - // } - // - // fun getElectrumURL(): String { - // return if (useDefaultElectrum) { - // defaultElectrumURL - // } else { - // customElectrumURL - // } - // } -} diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt index e5e5857..9a5834f 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt @@ -10,22 +10,16 @@ import kotlinx.coroutines.runBlocking import org.bitcoindevkit.Address import org.bitcoindevkit.AddressInfo import org.bitcoindevkit.Amount -import org.bitcoindevkit.BlockId import org.bitcoindevkit.CanonicalTx -import org.bitcoindevkit.CbfBuilder -import org.bitcoindevkit.CbfClient import org.bitcoindevkit.ChainPosition import org.bitcoindevkit.Descriptor import org.bitcoindevkit.DescriptorSecretKey import org.bitcoindevkit.FeeRate -import org.bitcoindevkit.IpAddress import org.bitcoindevkit.KeychainKind import org.bitcoindevkit.Mnemonic import org.bitcoindevkit.Network -import org.bitcoindevkit.Peer import org.bitcoindevkit.Persister import org.bitcoindevkit.Psbt -import org.bitcoindevkit.ScanType import org.bitcoindevkit.Script import org.bitcoindevkit.TxBuilder import org.bitcoindevkit.Update @@ -46,22 +40,23 @@ import org.bitcoindevkit.Wallet as BdkWallet private const val TAG = "Wallet" class Wallet private constructor( - private val wallet: BdkWallet, + val wallet: BdkWallet, private val walletSecrets: WalletSecrets, private val connection: Persister, private var fullScanCompleted: Boolean, private val walletId: String, private val userPreferencesRepository: UserPreferencesRepository, - private val internalAppFilesPath: String, - blockchainClientsConfig: BlockchainClientsConfig, + val internalAppFilesPath: String, + val network: Network, ) { - private var currentBlockchainClient: BlockchainClient? = blockchainClientsConfig.getClient() - public var kyotoClient: CbfClient? = null - fun getWalletSecrets(): WalletSecrets { return walletSecrets } + fun bestBlock(): UInt { + return wallet.latestCheckpoint().height + } + fun createTransaction(recipientList: List, feeRate: FeeRate, opReturnMsg: String?): Psbt { // technique 1 for adding a list of recipients to the TxBuilder // var txBuilder = TxBuilder() @@ -74,7 +69,7 @@ class Wallet private constructor( var txBuilder = recipientList.fold(TxBuilder()) { builder, recipient -> // val address = Address(recipient.address) - val scriptPubKey: Script = Address(recipient.address, Network.TESTNET).scriptPubkey() + val scriptPubKey: Script = Address(recipient.address, this.network).scriptPubkey() builder.addRecipient(scriptPubKey, Amount.fromSat(recipient.amount)) } // if (!opReturnMsg.isNullOrEmpty()) { @@ -115,11 +110,11 @@ class Wallet private constructor( return wallet.sign(psbt) } - fun broadcast(signedPsbt: Psbt): String { - currentBlockchainClient?.broadcast(signedPsbt.extractTx()) - ?: throw IllegalStateException("Blockchain client not initialized") - return signedPsbt.extractTx().computeTxid().toString() - } + // fun broadcast(signedPsbt: Psbt): String { + // currentBlockchainClient?.broadcast(signedPsbt.extractTx()) + // ?: throw IllegalStateException("Blockchain client not initialized") + // return signedPsbt.extractTx().computeTxid().toString() + // } private fun getAllTransactions(): List = wallet.transactions() @@ -144,13 +139,17 @@ class Wallet private constructor( val (confirmationBlock, confirmationTimestamp, pending) = when (val position = tx.chainPosition) { - is ChainPosition.Unconfirmed -> Triple(null, null, true) - is ChainPosition.Confirmed -> + is ChainPosition.Unconfirmed -> { + Triple(null, null, true) + } + + is ChainPosition.Confirmed -> { Triple( ConfirmationBlock(position.confirmationBlockTime.blockId.height), Timestamp(position.confirmationBlockTime.confirmationTime), false, ) + } } TxDetails( tx.transaction, @@ -180,39 +179,10 @@ class Wallet private constructor( fun getNewAddress(): AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL) - fun getLastCheckpoint(): BlockId = wallet.latestCheckpoint() - - fun startKyotoNode() { - Log.i(TAG, "Starting Kyoto node") - // Regtest - val ip: IpAddress = IpAddress.fromIpv4(10u, 0u, 2u, 2u) - val peer1: Peer = Peer(ip, 18444u, false) - - // Signet - // val ip: IpAddress = IpAddress.fromIpv4(68u, 47u, 229u, 218u) - // val peer1: Peer = Peer(ip, null, false) - val peers: List = listOf(peer1) - - val (client, node) = - CbfBuilder() - .dataDir(this.internalAppFilesPath) - .peers(peers) - .connections(1u) - .scanType(ScanType.Sync) - .build(this.wallet) - - node.run() - kyotoClient = client - Log.i(TAG, "Kyoto node started") - } - - suspend fun stopKyotoNode() { - kyotoClient?.shutdown() - } - fun applyUpdate(update: Update) { wallet.applyUpdate(update) wallet.persist(connection) + Log.i("KYOTOTEST", "Wallet applied a Kyoto update") } companion object { @@ -275,7 +245,7 @@ class Wallet private constructor( walletId = walletId, userPreferencesRepository = userPreferencesRepository, internalAppFilesPath = internalAppFilesPath, - blockchainClientsConfig = BlockchainClientsConfig.createDefaultConfig(newWalletConfig.network), + network = newWalletConfig.network ) } @@ -303,9 +273,7 @@ class Wallet private constructor( walletId = activeWallet.id, userPreferencesRepository = userPreferencesRepository, internalAppFilesPath = internalAppFilesPath, - blockchainClientsConfig = BlockchainClientsConfig.createDefaultConfig( - activeWallet.network.intoDomain() - ), + network = activeWallet.network.intoDomain() ) } @@ -379,7 +347,7 @@ class Wallet private constructor( walletId = walletId, userPreferencesRepository = userPreferencesRepository, internalAppFilesPath = internalAppFilesPath, - blockchainClientsConfig = BlockchainClientsConfig.createDefaultConfig(recoverWalletConfig.network), + network = recoverWalletConfig.network ) } } diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt index c4cf8d4..3b5672d 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt @@ -8,8 +8,8 @@ package org.bitcoindevkit.devkitwallet.presentation import android.content.Context import android.os.Bundle import android.util.Log -import androidx.activity.compose.setContent import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -58,24 +58,29 @@ class DevkitWalletActivity : ComponentActivity() { try { activeWallet = when (walletCreateType) { - is WalletCreateType.FROMSCRATCH -> + is WalletCreateType.FROMSCRATCH -> { Wallet.createWallet( newWalletConfig = walletCreateType.newWalletConfig, internalAppFilesPath = filesDir.absolutePath, userPreferencesRepository = userPreferencesRepository, ) - is WalletCreateType.LOADEXISTING -> + } + + is WalletCreateType.LOADEXISTING -> { Wallet.loadActiveWallet( activeWallet = walletCreateType.activeWallet, internalAppFilesPath = filesDir.absolutePath, userPreferencesRepository = userPreferencesRepository, ) - is WalletCreateType.RECOVER -> + } + + is WalletCreateType.RECOVER -> { Wallet.recoverWallet( recoverWalletConfig = walletCreateType.recoverWalletConfig, internalAppFilesPath = filesDir.absolutePath, userPreferencesRepository = userPreferencesRepository, ) + } } } catch (e: Throwable) { Log.i(TAG, "Could not build wallet: $e") diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/AppNavigation.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/AppNavigation.kt index ac37b4c..5abbe77 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/AppNavigation.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/AppNavigation.kt @@ -24,15 +24,15 @@ import androidx.navigation.toRoute import org.bitcoindevkit.devkitwallet.data.SingleWallet import org.bitcoindevkit.devkitwallet.domain.Wallet import org.bitcoindevkit.devkitwallet.presentation.WalletCreateType -import org.bitcoindevkit.devkitwallet.presentation.ui.screens.drawer.AboutScreen -import org.bitcoindevkit.devkitwallet.presentation.ui.screens.drawer.BlockchainClientScreen -import org.bitcoindevkit.devkitwallet.presentation.ui.screens.drawer.LogsScreen -import org.bitcoindevkit.devkitwallet.presentation.ui.screens.drawer.RecoveryDataScreen -import org.bitcoindevkit.devkitwallet.presentation.ui.screens.drawer.SettingsScreen import org.bitcoindevkit.devkitwallet.presentation.ui.screens.intro.ActiveWalletsScreen import org.bitcoindevkit.devkitwallet.presentation.ui.screens.intro.CreateNewWalletScreen import org.bitcoindevkit.devkitwallet.presentation.ui.screens.intro.RecoverWalletScreen import org.bitcoindevkit.devkitwallet.presentation.ui.screens.intro.WalletChoiceScreen +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings.AboutScreen +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings.BlockchainClientScreen +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings.LogsScreen +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings.RecoveryDataScreen +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings.SettingsScreen import org.bitcoindevkit.devkitwallet.presentation.ui.screens.wallet.RBFScreen import org.bitcoindevkit.devkitwallet.presentation.ui.screens.wallet.ReceiveScreen import org.bitcoindevkit.devkitwallet.presentation.ui.screens.wallet.SendScreen diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/AboutScreen.kt similarity index 98% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt rename to app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/AboutScreen.kt index 43766e8..e14c22f 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/AboutScreen.kt @@ -3,7 +3,7 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE file. */ -package org.bitcoindevkit.devkitwallet.presentation.ui.screens.drawer +package org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings import androidx.compose.foundation.Image import androidx.compose.foundation.clickable diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/BlockchainClientScreen.kt similarity index 89% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt rename to app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/BlockchainClientScreen.kt index a940411..7b4e1e6 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/BlockchainClientScreen.kt @@ -3,7 +3,7 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE file. */ -package org.bitcoindevkit.devkitwallet.presentation.ui.screens.drawer +package org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -31,7 +31,7 @@ import androidx.navigation.NavController import org.bitcoindevkit.devkitwallet.presentation.theme.inter import org.bitcoindevkit.devkitwallet.presentation.ui.components.NeutralButton import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar -import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.KyotoNodeStatus +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.CbfNodeStatus import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenAction import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenState @@ -66,7 +66,7 @@ internal fun BlockchainClientScreen( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth(), ) { - val status = if (state.kyotoNodeStatus == KyotoNodeStatus.Running) "Online" else "Offline" + val status = if (state.kyotoNodeStatus == CbfNodeStatus.Running) "Online" else "Offline" Text( text = "CBF Node Status: $status", color = colorScheme.onSurface, @@ -81,7 +81,7 @@ internal fun BlockchainClientScreen( .size(size = 21.dp) .clip(shape = CircleShape) .background( - if (state.kyotoNodeStatus == KyotoNodeStatus.Running) { + if (state.kyotoNodeStatus == CbfNodeStatus.Running) { Color(0xFF8FD998) } else { Color(0xFFE76F51) @@ -103,7 +103,7 @@ internal fun BlockchainClientScreen( textAlign = TextAlign.Start, ) Text( - text = "${state.latestBlock}", + text = "${state.bestBlockHeight}", color = colorScheme.onSurface, fontSize = 14.sp, fontFamily = inter, @@ -115,12 +115,12 @@ internal fun BlockchainClientScreen( NeutralButton( text = "Start Node", - enabled = state.kyotoNodeStatus == KyotoNodeStatus.Stopped, - onClick = { onAction(WalletScreenAction.StartKyotoNode) }, + enabled = state.kyotoNodeStatus == CbfNodeStatus.Stopped, + onClick = { onAction(WalletScreenAction.ActivateCbfNode) }, ) NeutralButton( text = "Stop Node", - enabled = state.kyotoNodeStatus == KyotoNodeStatus.Running, + enabled = state.kyotoNodeStatus == CbfNodeStatus.Running, onClick = { onAction(WalletScreenAction.StopKyotoNode) }, ) } diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/CustomBlockchainClient.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/CustomBlockchainClient.kt similarity index 95% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/CustomBlockchainClient.kt rename to app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/CustomBlockchainClient.kt index 282b143..7cff349 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/CustomBlockchainClient.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/CustomBlockchainClient.kt @@ -3,7 +3,7 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE file. */ -package org.bitcoindevkit.devkitwallet.presentation.ui.screens.drawer +package org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/LogsScreen.kt similarity index 96% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt rename to app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/LogsScreen.kt index 2f6c6f7..b1118af 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/LogsScreen.kt @@ -3,7 +3,7 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE file. */ -package org.bitcoindevkit.devkitwallet.presentation.ui.screens.drawer +package org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.fillMaxSize diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/RecoveryDataScreen.kt similarity index 99% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt rename to app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/RecoveryDataScreen.kt index b7150ba..d02b277 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/RecoveryDataScreen.kt @@ -3,7 +3,7 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE file. */ -package org.bitcoindevkit.devkitwallet.presentation.ui.screens.drawer +package org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings import android.content.ClipData import android.content.ClipboardManager diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/SettingsScreen.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/SettingsScreen.kt similarity index 98% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/SettingsScreen.kt rename to app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/SettingsScreen.kt index a58cbd7..593cbe9 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/SettingsScreen.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/settings/SettingsScreen.kt @@ -3,7 +3,7 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE file. */ -package org.bitcoindevkit.devkitwallet.presentation.ui.screens.drawer +package org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt index 7697ce0..12c4186 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt @@ -447,7 +447,7 @@ private fun ConfirmDialog( if (checkRecipientList(recipientList = recipientList, feeRate = feeRate, context = context)) { val txDataBundle = TxDataBundle( - recipients = recipientList, + recipients = recipientList.toList(), feeRate = feeRate.value.toULong(), transactionType = transactionType, opReturnMsg = opReturnMsg, diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt index a8daf02..2c2a8e5 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt @@ -85,6 +85,7 @@ fun TransactionDetailButton(content: String, navController: NavController, txid: "increase fees" -> { navController.navigate(RbfScreen(txid!!)) } + "back to transaction list" -> { navController.navigateUp() } diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt index f4b0e10..c3aff27 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt @@ -141,6 +141,7 @@ internal fun WalletHomeScreen( color = colorScheme.onSurface, ) } + CurrencyUnit.Satoshi -> { Text( text = "${state.balance} sat", @@ -397,10 +398,12 @@ fun isOnline(context: Context): Boolean { Log.i("Internet", "NetworkCapabilities.TRANSPORT_CELLULAR") return true } + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> { Log.i("Internet", "NetworkCapabilities.TRANSPORT_WIFI") return true } + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> { Log.i("Internet", "NetworkCapabilities.TRANSPORT_ETHERNET") return true diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt index 5469988..3a0a345 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt @@ -7,8 +7,12 @@ package org.bitcoindevkit.devkitwallet.presentation.viewmodels import android.util.Log import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch import org.bitcoindevkit.FeeRate import org.bitcoindevkit.Psbt +import org.bitcoindevkit.devkitwallet.data.Kyoto +import org.bitcoindevkit.devkitwallet.data.KyotoNotInitialized import org.bitcoindevkit.devkitwallet.domain.Wallet import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.SendScreenAction import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.TransactionType @@ -24,28 +28,40 @@ internal class SendViewModel(private val wallet: Wallet) : ViewModel() { } private fun broadcast(txInfo: TxDataBundle) { - try { - // Create, sign, and broadcast - val psbt: Psbt = - when (txInfo.transactionType) { - TransactionType.STANDARD -> - wallet.createTransaction( - recipientList = txInfo.recipients, - feeRate = FeeRate.fromSatPerVb(txInfo.feeRate), - opReturnMsg = txInfo.opReturnMsg, - ) - // TransactionType.SEND_ALL -> Wallet.createSendAllTransaction(recipientList[0].address, FeeRate.fromSatPerVb(feeRate), rbfEnabled, opReturnMsg) - TransactionType.SEND_ALL -> throw NotImplementedError("Send all not implemented") + Log.i(TAG, "The tx data bundle is $txInfo") + + // TODO: Add error snackbar if Kyoto node is not running, or maybe simply disable the button + viewModelScope.launch { + try { + // Create, sign, and broadcast + val psbt: Psbt = + when (txInfo.transactionType) { + TransactionType.STANDARD -> { + wallet.createTransaction( + recipientList = txInfo.recipients, + feeRate = FeeRate.fromSatPerVb(txInfo.feeRate), + opReturnMsg = txInfo.opReturnMsg, + ) + } + + // TransactionType.SEND_ALL -> Wallet.createSendAllTransaction(recipientList[0].address, FeeRate.fromSatPerVb(feeRate), rbfEnabled, opReturnMsg) + TransactionType.SEND_ALL -> { + throw NotImplementedError("Send all not implemented") + } + } + val isSigned = wallet.sign(psbt) + if (isSigned) { + val transaction = psbt.extractTx() + val wtxid: String = Kyoto.getInstance().broadcast(transaction).toString() + Log.i(TAG, "Transaction was broadcast! txid: $wtxid") + } else { + Log.i(TAG, "Transaction not signed.") } - val isSigned = wallet.sign(psbt) - if (isSigned) { - val txid: String = wallet.broadcast(psbt) - Log.i(TAG, "Transaction was broadcast! txid: $txid") - } else { - Log.i(TAG, "Transaction not signed.") + } catch (e: KyotoNotInitialized) { + Log.i(TAG, "Kyoto was not initialized! Transaction cannot be broadcast.") + } catch (e: Throwable) { + Log.i(TAG, "Broadcast error message: ${e.message}") } - } catch (e: Throwable) { - Log.i(TAG, "Broadcast error: ${e.message}") } } } diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt index 01bc86c..4ebc587 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt @@ -13,14 +13,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch -import org.bitcoindevkit.Warning +import org.bitcoindevkit.devkitwallet.data.Kyoto import org.bitcoindevkit.devkitwallet.domain.CurrencyUnit import org.bitcoindevkit.devkitwallet.domain.DwLogger import org.bitcoindevkit.devkitwallet.domain.DwLogger.LogLevel.INFO import org.bitcoindevkit.devkitwallet.domain.Wallet -import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.KyotoNodeStatus import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenAction import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenState @@ -33,15 +31,16 @@ internal class WalletViewModel( private set private val kyotoCoroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO) - private var latestBlock: Int = 0 + private var kyoto: Kyoto? = null + @Suppress("ktlint:standard:no-multi-spaces") fun onAction(action: WalletScreenAction) { when (action) { - WalletScreenAction.SwitchUnit -> switchUnit() - WalletScreenAction.UpdateBalance -> updateBalance() - WalletScreenAction.StartKyotoNode -> startKyotoNode() - WalletScreenAction.StopKyotoNode -> stopKyotoNode() - WalletScreenAction.ClearSnackbar -> clearSnackbar() + WalletScreenAction.SwitchUnit -> switchUnit() + WalletScreenAction.UpdateBalance -> updateBalance() + WalletScreenAction.ActivateCbfNode -> activateKyoto() + WalletScreenAction.StopKyotoNode -> stopKyotoNode() + WalletScreenAction.ClearSnackbar -> clearSnackbar() } } @@ -62,7 +61,7 @@ internal class WalletViewModel( } private fun updateLatestBlock(blockHeight: UInt) { - state = state.copy(latestBlock = blockHeight) + state = state.copy(bestBlockHeight = blockHeight) } private fun updateBalance() { @@ -77,65 +76,27 @@ internal class WalletViewModel( } } - private fun startKyotoNode() { - Log.i("Kyoto", "Starting Kyoto node") - DwLogger.log(INFO, "Starting Kyoto node") - wallet.startKyotoNode() - state = state.copy(kyotoNodeStatus = KyotoNodeStatus.Running) - - Log.i("Kyoto", "Starting Kyoto sync") - DwLogger.log(INFO, "Starting Kyoto sync") + private fun activateKyoto() { + val dataDir = wallet.internalAppFilesPath + this.kyoto = Kyoto.create(wallet.wallet, dataDir, wallet.network) + val updatesFlow = kyoto!!.start() kyotoCoroutineScope.launch { - while (wallet.kyotoClient != null) { - val update = wallet.kyotoClient?.update() - if (update == null) { - Log.i("Kyoto", "UPDATE: Update is null") - } else { - Log.i("Kyoto", "UPDATE: Applying an update to the wallet") - wallet.applyUpdate(update) - } + updatesFlow.collect { + Log.i(TAG, "Collecting a flow update") + wallet.applyUpdate(it) updateBalance() + updateBestBlock() } } - - kyotoCoroutineScope.launch { - while (wallet.kyotoClient != null) { - val nextInfo = wallet.kyotoClient!!.nextInfo() - Log.i("Kyoto", "LOG: $nextInfo") - val lastNumber = wallet.getLastCheckpoint().height.toInt() - - if (lastNumber > latestBlock) { - latestBlock = lastNumber - updateLatestBlock(latestBlock.toUInt()) - showSnackbar("New block mined! $latestBlock \uD83C\uDF89\uD83C\uDF89") - } - } - } - - kyotoCoroutineScope.launch { - while (wallet.kyotoClient != null) { - val nextWarning: Warning = wallet.kyotoClient!!.nextWarning() - Log.i("Kyoto", "WARNING: $nextWarning") - } - } + kyoto!!.logToLogcat() } private fun stopKyotoNode() { - Log.i("Kyoto", "Stopping Kyoto node") - DwLogger.log(INFO, "Stopping Kyoto node") - viewModelScope.launch { - try { - Log.i("Kyoto", "Calling wallet.stopKyotoNode() on thread: ${Thread.currentThread().name}") - wallet.stopKyotoNode() - - // Cancel all coroutines started by startKyotoSync - kyotoCoroutineScope.coroutineContext.cancelChildren() + kyoto!!.shutdown() + } - Log.i("Kyoto", "Kyoto node stopped successfully.") - state = state.copy(kyotoNodeStatus = KyotoNodeStatus.Stopped) - } catch (e: Exception) { - Log.e("Kyoto", "Error stopping Kyoto node: ${e.message}", e) - } - } + private fun updateBestBlock() { + val bestBlockHeight = wallet.bestBlock() + state = state.copy(bestBlockHeight = bestBlockHeight) } } diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt index 0606212..d2726a6 100644 --- a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt +++ b/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt @@ -10,9 +10,9 @@ import org.bitcoindevkit.devkitwallet.domain.CurrencyUnit data class WalletScreenState( val balance: ULong = 0u, val unit: CurrencyUnit = CurrencyUnit.Bitcoin, - val latestBlock: UInt = 0u, + val bestBlockHeight: UInt = 0u, val snackbarMessage: String? = null, - val kyotoNodeStatus: KyotoNodeStatus = KyotoNodeStatus.Stopped, + val kyotoNodeStatus: CbfNodeStatus = CbfNodeStatus.Stopped, ) sealed interface WalletScreenAction { @@ -20,14 +20,14 @@ sealed interface WalletScreenAction { data object SwitchUnit : WalletScreenAction - data object StartKyotoNode : WalletScreenAction + data object ActivateCbfNode : WalletScreenAction data object StopKyotoNode : WalletScreenAction data object ClearSnackbar : WalletScreenAction } -enum class KyotoNodeStatus { +enum class CbfNodeStatus { Running, Stopped, }