diff --git "a/Variant \342\200\224 0.30/.gitignore" "b/Variant \342\200\224 0.30/.gitignore" new file mode 100644 index 0000000..fe4a88b --- /dev/null +++ "b/Variant \342\200\224 0.30/.gitignore" @@ -0,0 +1,19 @@ +*.iml +.gradle +/.idea/ +.DS_Store +/build +/app/build/ +/captures +.externalNativeBuild +.cxx +app-simple-wallet/local.properties +app-advanced-features/local.properties +app-ui-only/local.properties +app-simple-wallet/app/build/ +app/build/ +app-ui-only/app/build/ +app-clean/ +.idea/ +local.properties +app.run.xml diff --git "a/Variant \342\200\224 0.30/app/build.gradle.kts" "b/Variant \342\200\224 0.30/app/build.gradle.kts" new file mode 100644 index 0000000..a8fdd0e --- /dev/null +++ "b/Variant \342\200\224 0.30/app/build.gradle.kts" @@ -0,0 +1,81 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + compileSdk = 34 + + buildFeatures { + viewBinding = true + compose = true + } + + defaultConfig { + applicationId = "com.goldenraven.devkitwallet" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "v0.0.1" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + getByName("debug") { + isDebuggable = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.5" + } + + namespace = "com.goldenraven.devkitwallet" +} + +dependencies { + // basic android dependencies + implementation ("org.jetbrains.kotlin:kotlin-stdlib:1.9.20") + implementation ("androidx.core:core-ktx:1.12.0") + implementation ("com.google.android.material:material:1.10.0") + + // compose + // Adding the Bill of Materials synchronizes dependencies in the androidx.compose namespace + // You can remove the library version in your dependency declarations + implementation(platform("androidx.compose:compose-bom:2023.06.01")) + implementation("androidx.compose.material:material") + implementation("androidx.compose.animation:animation") + implementation("androidx.compose.ui:ui-tooling") + implementation("androidx.compose.runtime:runtime-livedata") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.material3:material3") + implementation("androidx.activity:activity-compose") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") + // implementation("androidx.navigation:navigation-compose:2.4.1") + implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1") + implementation("com.google.accompanist:accompanist-navigation-animation:0.23.1") + implementation("com.google.accompanist:accompanist-systemuicontroller:0.23.1") + + // toolbar + implementation("androidx.appcompat:appcompat:1.6.1") + + // bitcoindevkit + implementation("org.bitcoindevkit:bdk-android:0.30.0") + + // qr codes + implementation("com.google.zxing:core:3.4.1") + + // tests + testImplementation ("junit:junit:4.13.2") + androidTestImplementation ("androidx.test.ext:junit:1.1.5") + androidTestImplementation ("androidx.test.espresso:espresso-core:3.5.1") +} diff --git a/app/proguard-rules.pro "b/Variant \342\200\224 0.30/app/proguard-rules.pro" similarity index 100% rename from app/proguard-rules.pro rename to "Variant \342\200\224 0.30/app/proguard-rules.pro" diff --git "a/Variant \342\200\224 0.30/app/src/main/AndroidManifest.xml" "b/Variant \342\200\224 0.30/app/src/main/AndroidManifest.xml" new file mode 100644 index 0000000..ec27e45 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/AndroidManifest.xml" @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/bip39-english.txt "b/Variant \342\200\224 0.30/app/src/main/assets/bip39-english.txt" similarity index 100% rename from app/src/main/assets/bip39-english.txt rename to "Variant \342\200\224 0.30/app/src/main/assets/bip39-english.txt" diff --git a/app/src/main/ic_launcher_bdk-playstore.png "b/Variant \342\200\224 0.30/app/src/main/ic_launcher_bdk-playstore.png" similarity index 100% rename from app/src/main/ic_launcher_bdk-playstore.png rename to "Variant \342\200\224 0.30/app/src/main/ic_launcher_bdk-playstore.png" diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/DevkitWalletActivity.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/DevkitWalletActivity.kt" new file mode 100644 index 0000000..aa506e5 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/DevkitWalletActivity.kt" @@ -0,0 +1,63 @@ +/* + * Copyright 2021-2023 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 com.goldenraven.devkitwallet + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.util.Log +import androidx.activity.compose.setContent +import com.goldenraven.devkitwallet.domain.Repository +import com.goldenraven.devkitwallet.domain.Wallet +import com.goldenraven.devkitwallet.navigation.HomeNavigation +import com.goldenraven.devkitwallet.navigation.CreateWalletNavigation +import com.goldenraven.devkitwallet.ui.theme.DevkitTheme + +private const val TAG = "DevkitWalletActivity" + +class DevkitWalletActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val onBuildWalletButtonClicked: (WalletCreateType) -> Unit = { walletCreateType -> + try { + // load up a wallet either from scratch or using a BIP39 recovery phrase + when (walletCreateType) { + // if we create a wallet from scratch we don't need a recovery phrase + is WalletCreateType.FROMSCRATCH -> Wallet.createWallet() + + is WalletCreateType.RECOVER -> Wallet.recoverWallet(walletCreateType.recoveryPhrase) + } + setContent { + DevkitTheme { + HomeNavigation() + } + } + } catch(e: Throwable) { + Log.i(TAG, "Could not build wallet: $e") + } + } + + if (Repository.doesWalletExist()) { + Wallet.loadExistingWallet() + setContent { + DevkitTheme { + HomeNavigation() + } + } + } else { + setContent { + DevkitTheme { + CreateWalletNavigation(onBuildWalletButtonClicked) + } + } + } + } +} + +sealed class WalletCreateType { + object FROMSCRATCH : WalletCreateType() + class RECOVER(val recoveryPhrase: String) : WalletCreateType() +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/DevkitWalletApplication.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/DevkitWalletApplication.kt" new file mode 100644 index 0000000..c420c31 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/DevkitWalletApplication.kt" @@ -0,0 +1,29 @@ +/* + * Copyright 2021 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 com.goldenraven.devkitwallet + +import android.app.Application +import android.content.Context +import com.goldenraven.devkitwallet.utils.SharedPreferencesManager +import com.goldenraven.devkitwallet.domain.Repository +import com.goldenraven.devkitwallet.domain.Wallet + +class DevkitWalletApplication : Application() { + override fun onCreate() { + super.onCreate() + + // initialize Wallet object (singleton) with path variable + Wallet.setPath(applicationContext.filesDir.toString()) + + // initialize shared preferences manager object (singleton) + val sharedPreferencesManager = SharedPreferencesManager( + sharedPreferences = applicationContext.getSharedPreferences("current_wallet", Context.MODE_PRIVATE) + ) + + // initialize Repository object with shared preferences + Repository.setSharedPreferences(sharedPreferencesManager) + } +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/domain/ElectrumServer.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/domain/ElectrumServer.kt" new file mode 100644 index 0000000..c064115 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/domain/ElectrumServer.kt" @@ -0,0 +1,76 @@ +/* + * Copyright 2021-2023 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 com.goldenraven.devkitwallet.domain + +import android.util.Log +import org.bitcoindevkit.Blockchain +import org.bitcoindevkit.BlockchainConfig +import org.bitcoindevkit.ElectrumConfig + +private const val TAG = "ElectrumServer" + +class ElectrumServer { + private var useDefaultElectrum: Boolean = true + private var default: Blockchain + private var custom: Blockchain? = null + private var customElectrumURL: String + private val defaultElectrumURL = "ssl://electrum.blockstream.info:60002" + + 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/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/domain/Repository.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/domain/Repository.kt" new file mode 100644 index 0000000..7912603 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/domain/Repository.kt" @@ -0,0 +1,61 @@ +/* + * Copyright 2021-2023 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 com.goldenraven.devkitwallet.domain + +import android.util.Log +import com.goldenraven.devkitwallet.utils.SharedPreferencesManager + +private const val TAG = "Repository" + +object Repository { + + // shared preferences are a way to save/retrieve small pieces of data without building a database + private lateinit var sharedPreferencesManager: SharedPreferencesManager + + fun setSharedPreferences(sharedPrefManager: SharedPreferencesManager) { + sharedPreferencesManager = sharedPrefManager + } + + // take a look at shared preferences and see if the user already has a wallet saved on device + fun doesWalletExist(): Boolean { + val walletInitialized: Boolean = sharedPreferencesManager.walletInitialised + Log.i(TAG, "Value of walletInitialized at launch: $walletInitialized") + return walletInitialized + } + + // save the necessary data for wallet reconstruction in shared preferences + // upon application launch, the wallet can initialize itself using that data + fun saveWallet(path: String, descriptor: String, changeDescriptor: String) { + Log.i( + TAG, + "Saved wallet:\npath -> $path \ndescriptor -> $descriptor \nchange descriptor -> $changeDescriptor" + ) + sharedPreferencesManager.walletInitialised = true + sharedPreferencesManager.path = path + sharedPreferencesManager.descriptor = descriptor + sharedPreferencesManager.changeDescriptor = changeDescriptor + } + + fun saveMnemonic(mnemonic: String) { + Log.i(TAG, "The recovery phrase is: $mnemonic") + sharedPreferencesManager.mnemonic = mnemonic + } + + fun getMnemonic(): String { + return sharedPreferencesManager.mnemonic + } + + fun getInitialWalletData(): RequiredInitialWalletData { + val descriptor: String = sharedPreferencesManager.descriptor + val changeDescriptor: String = sharedPreferencesManager.changeDescriptor + return RequiredInitialWalletData(descriptor, changeDescriptor) + } +} + +data class RequiredInitialWalletData( + val descriptor: String, + val changeDescriptor: String +) diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/domain/Wallet.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/domain/Wallet.kt" new file mode 100644 index 0000000..a113631 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/domain/Wallet.kt" @@ -0,0 +1,214 @@ +/* + * Copyright 2021-2023 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 com.goldenraven.devkitwallet.domain + +import android.util.Log +import com.goldenraven.devkitwallet.ui.screens.wallet.Recipient +import org.bitcoindevkit.* +import org.bitcoindevkit.Wallet as BdkWallet + +private const val TAG = "Wallet" + +object Wallet { + + private lateinit var wallet: BdkWallet + private lateinit var path: String + private lateinit var electrumServer: ElectrumServer + // to use Esplora on regtest locally, use the following address + // private const val regtestEsploraUrl: String = "http://10.0.2.2:3002" + + object LogProgress: Progress { + override fun update(progress: Float, message: String?) { + Log.i(TAG, "Sync wallet") + } + } + + // setting the path requires the application context and is done once by the BdkSampleApplication class + fun setPath(path: String) { + Wallet.path = path + } + + private fun initialize( + descriptor: Descriptor, + changeDescriptor: Descriptor, + ) { + val database = DatabaseConfig.Sqlite(SqliteDbConfiguration("$path/bdk-sqlite")) + wallet = BdkWallet( + descriptor, + changeDescriptor, + Network.TESTNET, + database + ) + } + + fun createBlockchain() { + electrumServer = ElectrumServer() + Log.i(TAG, "Current electrum URL : ${electrumServer.getElectrumURL()}") + } + + fun changeElectrumServer(electrumURL: String) { + electrumServer.createCustomElectrum(electrumURL = electrumURL) + wallet.sync(electrumServer.server, LogProgress) + } + + fun createWallet() { + val mnemonic = Mnemonic(WordCount.WORDS12) + val bip32ExtendedRootKey = DescriptorSecretKey(Network.TESTNET, mnemonic, null) + val descriptor: Descriptor = Descriptor.newBip84(bip32ExtendedRootKey, KeychainKind.EXTERNAL, Network.TESTNET) + val changeDescriptor: Descriptor = Descriptor.newBip84(bip32ExtendedRootKey, KeychainKind.INTERNAL, Network.TESTNET) + initialize( + descriptor = descriptor, + changeDescriptor = changeDescriptor, + ) + Repository.saveWallet(path, descriptor.asStringPrivate(), changeDescriptor.asStringPrivate()) + Repository.saveMnemonic(mnemonic.asString()) + } + + // only create BIP84 compatible wallets + private fun createExternalDescriptor(rootKey: DescriptorSecretKey): String { + val externalPath: DerivationPath = DerivationPath("m/84h/1h/0h/0") + val externalDescriptor = "wpkh(${rootKey.extend(externalPath).asString()})" + Log.i(TAG, "Descriptor for receive addresses is $externalDescriptor") + return externalDescriptor + } + + private fun createInternalDescriptor(rootKey: DescriptorSecretKey): String { + val internalPath: DerivationPath = DerivationPath("m/84h/1h/0h/1") + val internalDescriptor = "wpkh(${rootKey.extend(internalPath).asString()})" + Log.i(TAG, "Descriptor for change addresses is $internalDescriptor") + return internalDescriptor + } + + // if the wallet already exists, its descriptors are stored in shared preferences + fun loadExistingWallet() { + val initialWalletData: RequiredInitialWalletData = Repository.getInitialWalletData() + Log.i(TAG, "Loading existing wallet with descriptor: ${initialWalletData.descriptor}") + Log.i(TAG, "Loading existing wallet with change descriptor: ${initialWalletData.changeDescriptor}") + initialize( + descriptor = Descriptor(initialWalletData.descriptor, Network.TESTNET), + changeDescriptor = Descriptor(initialWalletData.changeDescriptor, Network.TESTNET), + ) + } + + fun recoverWallet(recoveryPhrase: String) { + val mnemonic = Mnemonic.fromString(recoveryPhrase) + val bip32ExtendedRootKey = DescriptorSecretKey(Network.TESTNET, mnemonic, null) + val descriptor: Descriptor = Descriptor.newBip84(bip32ExtendedRootKey, KeychainKind.EXTERNAL, Network.TESTNET) + val changeDescriptor: Descriptor = Descriptor.newBip84(bip32ExtendedRootKey, KeychainKind.INTERNAL, Network.TESTNET) + initialize( + descriptor = descriptor, + changeDescriptor = changeDescriptor, + ) + Repository.saveWallet(path, descriptor.asStringPrivate(), changeDescriptor.asStringPrivate()) + Repository.saveMnemonic(mnemonic.asString()) + } + + @OptIn(ExperimentalUnsignedTypes::class) + fun createTransaction( + recipientList: MutableList, + feeRate: Float, + enableRBF: Boolean, + opReturnMsg: String? + ): PartiallySignedTransaction { + // technique 1 for adding a list of recipients to the TxBuilder + // var txBuilder = TxBuilder() + // for (recipient in recipientList) { + // txBuilder = txBuilder.addRecipient(address = recipient.first, amount = recipient.second) + // } + // txBuilder = txBuilder.feeRate(satPerVbyte = fee_rate) + + // technique 2 for adding a list of recipients to the TxBuilder + var txBuilder = recipientList.fold(TxBuilder()) { builder, recipient -> + val scriptPubkey: Script = Address(recipient.address).scriptPubkey() + builder.addRecipient(scriptPubkey, recipient.amount) + } + if (enableRBF) { + txBuilder = txBuilder.enableRbf() + } + if (!opReturnMsg.isNullOrEmpty()) { + txBuilder = txBuilder.addData(opReturnMsg.toByteArray(charset = Charsets.UTF_8).asUByteArray().toList()) + } + return txBuilder.feeRate(feeRate).finish(wallet).psbt + } + + @OptIn(ExperimentalUnsignedTypes::class) + fun createSendAllTransaction( + recipient: String, + feeRate: Float, + enableRBF: Boolean, + opReturnMsg: String? + ): PartiallySignedTransaction { + val scriptPubkey: Script = Address(recipient).scriptPubkey() + var txBuilder = TxBuilder() + .drainWallet() + .drainTo(scriptPubkey) + .feeRate(satPerVbyte = feeRate) + + if (enableRBF) { + txBuilder = txBuilder.enableRbf() + } + if (!opReturnMsg.isNullOrEmpty()) { + txBuilder = txBuilder.addData(opReturnMsg.toByteArray(charset = Charsets.UTF_8).asUByteArray().toList()) + } + return txBuilder.finish(wallet).psbt + } + + fun createBumpFeeTransaction(txid: String, feeRate: Float): PartiallySignedTransaction { + return BumpFeeTxBuilder(txid = txid, newFeeRate = feeRate) + .enableRbf() + .finish(wallet = wallet) + } + + fun sign(psbt: PartiallySignedTransaction): Boolean { + return wallet.sign(psbt, null) + } + + fun broadcast(signedPsbt: PartiallySignedTransaction): String { + electrumServer.server.broadcast(signedPsbt.extractTx()) + return signedPsbt.txid() + } + + fun getAllTransactions(): List = wallet.listTransactions(true) + + fun getTransaction(txid: String): TransactionDetails? { + val allTransactions = getAllTransactions() + allTransactions.forEach { + if (it.txid == txid) { + return it + } + } + return null + } + + fun sync() { + Log.i(TAG, "Wallet is syncing") + wallet.sync(electrumServer.server, LogProgress) + } + + fun getBalance(): ULong = wallet.getBalance().total + + fun getNewAddress(): AddressInfo = wallet.getAddress(AddressIndex.New) + + fun getLastUnusedAddress(): AddressInfo = wallet.getAddress(AddressIndex.LastUnused) + + fun isBlockChainCreated() = ::electrumServer.isInitialized + + fun getElectrumURL(): String = electrumServer.getElectrumURL() + + fun isElectrumServerDefault(): Boolean = electrumServer.isElectrumServerDefault() + + fun setElectrumSettings(electrumSettings: ElectrumSettings) { + when (electrumSettings) { + ElectrumSettings.DEFAULT -> electrumServer.useDefaultElectrum() + ElectrumSettings.CUSTOM -> electrumServer.useCustomElectrum() + } + } +} + +enum class ElectrumSettings { + DEFAULT, + CUSTOM +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/navigation/CreateWalletNavigation.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/navigation/CreateWalletNavigation.kt" new file mode 100644 index 0000000..f05821b --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/navigation/CreateWalletNavigation.kt" @@ -0,0 +1,58 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.navigation + +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import com.goldenraven.devkitwallet.WalletCreateType +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.screens.intro.WalletChoiceScreen +import com.goldenraven.devkitwallet.ui.screens.intro.WalletRecoveryScreen +import com.google.accompanist.navigation.animation.AnimatedNavHost +import com.google.accompanist.navigation.animation.composable +import com.google.accompanist.navigation.animation.rememberAnimatedNavController + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun CreateWalletNavigation(onBuildWalletButtonClicked: (WalletCreateType) -> Unit) { + val navController: NavHostController = rememberAnimatedNavController() + val animationDuration = 400 + + AnimatedNavHost( + navController = navController, + startDestination = Screen.WalletChoiceScreen.route, + ) { + + composable( + route = Screen.WalletChoiceScreen.route, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popEnterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + }, + ) { WalletChoiceScreen(navController = navController, onBuildWalletButtonClicked) } + + composable( + route = Screen.WalletRecoveryScreen.route, + enterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popEnterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + }, + popExitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + } + ) { WalletRecoveryScreen(navController = navController, onBuildWalletButtonClicked) } + } +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/navigation/HomeNavigation.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/navigation/HomeNavigation.kt" new file mode 100644 index 0000000..aca8a7b --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/navigation/HomeNavigation.kt" @@ -0,0 +1,91 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.navigation + +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.screens.WalletRoot +import com.goldenraven.devkitwallet.ui.screens.drawer.AboutScreen +import com.goldenraven.devkitwallet.ui.screens.drawer.ElectrumScreen +import com.goldenraven.devkitwallet.ui.screens.drawer.RecoveryPhraseScreen +import com.google.accompanist.navigation.animation.AnimatedNavHost +import com.google.accompanist.navigation.animation.composable +import com.google.accompanist.navigation.animation.rememberAnimatedNavController + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun HomeNavigation() { + val navController: NavHostController = rememberAnimatedNavController() + val animationDuration = 400 + + AnimatedNavHost( + navController = navController, + startDestination = Screen.WalletScreen.route, + ) { + + composable( + route = Screen.WalletScreen.route, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popEnterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + }, + ) { WalletRoot(navController = navController) } + + composable( + route = Screen.AboutScreen.route, + enterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popEnterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popExitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + ) { AboutScreen(navController = navController) } + + composable( + route = Screen.RecoveryPhraseScreen.route, + enterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popEnterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popExitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + } + ) { RecoveryPhraseScreen(navController = navController) } + + composable( + route = Screen.ElectrumScreen.route, + enterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popEnterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popExitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + } + ) { ElectrumScreen(navController = navController) } + } +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/navigation/WalletNavigation.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/navigation/WalletNavigation.kt" new file mode 100644 index 0000000..57b707e --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/navigation/WalletNavigation.kt" @@ -0,0 +1,134 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.navigation + +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.material3.DrawerState +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.screens.wallet.RBFScreen +import com.goldenraven.devkitwallet.ui.screens.wallet.ReceiveScreen +import com.goldenraven.devkitwallet.ui.screens.wallet.SendScreen +import com.goldenraven.devkitwallet.ui.screens.wallet.TransactionScreen +import com.goldenraven.devkitwallet.ui.screens.wallet.TransactionsScreen +import com.goldenraven.devkitwallet.ui.screens.wallet.WalletHomeScreen +import com.google.accompanist.navigation.animation.AnimatedNavHost +import com.google.accompanist.navigation.animation.composable +import com.google.accompanist.navigation.animation.rememberAnimatedNavController + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun WalletNavigation(drawerState: DrawerState) { + val navController: NavHostController = rememberAnimatedNavController() + val animationDuration = 400 + + AnimatedNavHost( + navController = navController, + startDestination = Screen.HomeScreen.route, + ) { + + composable( + route = Screen.HomeScreen.route, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popEnterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + }, + ) { WalletHomeScreen(navController, drawerState) } + + composable( + route = Screen.ReceiveScreen.route, + enterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + }, + popEnterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popExitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + } + ) { ReceiveScreen(navController) } + + composable( + route = Screen.SendScreen.route, + enterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + }, + popEnterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popExitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + } + ) { SendScreen(navController) } + + composable( + route = "${Screen.RBFScreen.route}/txid={txid}", + enterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + }, + popEnterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popExitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + } + ) { backStackEntry -> + backStackEntry.arguments?.getString("txid")?.let { + RBFScreen(navController, backStackEntry.arguments?.getString("txid")) + } + } + + composable( + route = Screen.TransactionsScreen.route, + enterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + }, + popEnterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popExitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + } + ) { TransactionsScreen(navController) } + + composable( + route = "${Screen.TransactionScreen.route}/txid={txid}", + enterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + }, + popEnterTransition = { + slideIntoContainer(AnimatedContentScope.SlideDirection.Start, animationSpec = tween(animationDuration)) + }, + popExitTransition = { + slideOutOfContainer(AnimatedContentScope.SlideDirection.End, animationSpec = tween(animationDuration)) + } + ) { backStackEntry -> + backStackEntry.arguments?.getString("txid")?.let { + TransactionScreen(navController, backStackEntry.arguments?.getString("txid")) + } + } + } +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/Screen.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/Screen.kt" new file mode 100644 index 0000000..1ccef2d --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/Screen.kt" @@ -0,0 +1,26 @@ +/* + * Copyright 2021-2023 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 com.goldenraven.devkitwallet.ui + +sealed class Screen(val route: String) { + // create wallet screens + object WalletChoiceScreen : Screen("wallet_choice_screen") + object WalletRecoveryScreen : Screen("wallet_recovery_screen") + + // home screens + object WalletScreen : Screen("wallet_screen") + object AboutScreen : Screen("about_screen") + object RecoveryPhraseScreen : Screen("recovery_phrase_screen") + object ElectrumScreen : Screen("electrum_screen") + + // wallet screens + object HomeScreen : Screen("home_screen") + object SendScreen : Screen("send_screen") + object ReceiveScreen : Screen("receive_screen") + object RBFScreen : Screen("rbf_screen") + object TransactionsScreen : Screen("transactions_screen") + object TransactionScreen : Screen("transaction_screen") +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/components/LoadingAnimation.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/components/LoadingAnimation.kt" new file mode 100644 index 0000000..7bde283 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/components/LoadingAnimation.kt" @@ -0,0 +1,73 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.components + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay + +@Composable +fun LoadingAnimation( + circleColor: Color = Color(0xffE9C46A), + circleSize: Dp = 21.dp, + animationDelay: Int = 800, + initialAlpha: Float = 0.3f +) { + val circles = listOf( + remember { Animatable(initialValue = initialAlpha) }, + remember { Animatable(initialValue = initialAlpha) }, + remember { Animatable(initialValue = initialAlpha) } + ) + + circles.forEachIndexed { index, animatable -> + LaunchedEffect(Unit) { + // Use coroutine delay to sync animations + delay(timeMillis = (animationDelay / circles.size).toLong() * index) + + animatable.animateTo( + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = animationDelay + ), + repeatMode = RepeatMode.Reverse + ) + ) + } + } + + // container for circles + Row { + circles.forEachIndexed { index, animatable -> + // gap between the circles + if (index != 0) Spacer(modifier = Modifier.width(width = 6.dp)) + + Box( + modifier = Modifier + .size(size = circleSize) + .clip(shape = CircleShape) + .background(circleColor.copy(alpha = animatable.value) ) + ) + } + } +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/components/NeutralButton.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/components/NeutralButton.kt" new file mode 100644 index 0000000..aa743fd --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/components/NeutralButton.kt" @@ -0,0 +1,36 @@ +package com.goldenraven.devkitwallet.ui.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.unit.dp +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors + +@Composable +fun NeutralButton(text: String, enabled: Boolean, onClick: () -> Unit) { + Button( + onClick = onClick, + colors = ButtonDefaults.buttonColors( + containerColor = DevkitWalletColors.secondary, + disabledContainerColor = DevkitWalletColors.secondary, + ), + shape = RoundedCornerShape(16.dp), + enabled = enabled, + modifier = Modifier + .height(80.dp) + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = text, + ) + } +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/components/SecondaryScreensAppBar.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/components/SecondaryScreensAppBar.kt" new file mode 100644 index 0000000..bba2683 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/components/SecondaryScreensAppBar.kt" @@ -0,0 +1,44 @@ +/* + * Copyright 2021-2023 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 com.goldenraven.devkitwallet.ui.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.sp +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoRegular + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SecondaryScreensAppBar( + title: String, + navigation: () -> Unit +) { + TopAppBar( + title = { + Text( + text = title, + color = DevkitWalletColors.white, + fontSize = 18.sp, + fontFamily = jetBrainsMonoRegular + ) + }, + navigationIcon = { + IconButton(onClick = navigation) { + Icon( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = "Back", + tint = DevkitWalletColors.white + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = DevkitWalletColors.primaryDark, + ) + ) +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/components/TransactionCards.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/components/TransactionCards.kt" new file mode 100644 index 0000000..b77f3e8 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/components/TransactionCards.kt" @@ -0,0 +1,102 @@ +package com.goldenraven.devkitwallet.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.goldenraven.devkitwallet.ui.screens.wallet.confirmedTransactionsItem +import com.goldenraven.devkitwallet.ui.screens.wallet.pendingTransactionsItem +import com.goldenraven.devkitwallet.ui.screens.wallet.viewTransaction +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoLight +import org.bitcoindevkit.TransactionDetails + +@Composable +fun ConfirmedTransactionCard(details: TransactionDetails, navController: NavController) { + Row( + Modifier + .padding(horizontal = 8.dp, vertical = 6.dp) + .fillMaxWidth() + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ) + .clickable { viewTransaction(navController = navController, txid = details.txid) }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Absolute.SpaceBetween + ) { + Text( + confirmedTransactionsItem(details), + fontFamily = jetBrainsMonoLight, + fontSize = 12.sp, + color = DevkitWalletColors.white, + modifier = Modifier.padding(16.dp) + ) + Box( + modifier = Modifier + .padding(top = 16.dp, end = 16.dp) + .size(size = 24.dp) + .clip(shape = CircleShape) + .background(DevkitWalletColors.secondary) + .align(Alignment.Top) + ) + } +} + +@Composable +fun PendingTransactionCard(details: TransactionDetails, navController: NavController) { + Row( + Modifier + .padding(horizontal = 8.dp, vertical = 6.dp) + .fillMaxWidth() + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ) + .border( + width = 2.dp, + color = DevkitWalletColors.accent1, + shape = RoundedCornerShape(16.dp) + ) + .clickable { + viewTransaction( + navController = navController, + txid = details.txid + ) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Absolute.SpaceBetween + ) { + Text( + pendingTransactionsItem(details), + fontFamily = jetBrainsMonoLight, + fontSize = 12.sp, + color = DevkitWalletColors.white, + modifier = Modifier.padding(16.dp) + ) + Box( + modifier = Modifier + .padding(top = 16.dp, end = 16.dp) + .size(size = 24.dp) + .clip(shape = CircleShape) + .background(Color(0xffE9C46A)) + .align(Alignment.Top) + ) + } +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/WalletRoot.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/WalletRoot.kt" new file mode 100644 index 0000000..4ca9716 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/WalletRoot.kt" @@ -0,0 +1,124 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.material3.NavigationDrawerItemDefaults.colors +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.goldenraven.devkitwallet.R +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.navigation.WalletNavigation +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoLight + +@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) +@Composable +internal fun WalletRoot(navController: NavController) { + val drawerState = rememberDrawerState(DrawerValue.Closed) + + val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email, Icons.Default.Face) + val selectedItem = remember { mutableStateOf(items[0]) } + + val navigationItemColors = colors( + selectedContainerColor = DevkitWalletColors.primary, + unselectedContainerColor = DevkitWalletColors.primary, + selectedTextColor = DevkitWalletColors.white, + unselectedTextColor = DevkitWalletColors.white + ) + + ModalNavigationDrawer ( + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet { + Column( + Modifier + .background(color = DevkitWalletColors.secondary) + .height(300.dp) + .fillMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_testnet_logo), + contentDescription = "Bitcoin testnet logo", + Modifier + .size(90.dp) + .padding(bottom = 16.dp) + ) + Text( + text = "BDK Android Sample Wallet", + color = DevkitWalletColors.white, + fontFamily = jetBrainsMonoLight, + ) + Spacer(modifier = Modifier.padding(8.dp)) + Text( + "Version 0.1.0", + color = DevkitWalletColors.white, + fontFamily = jetBrainsMonoLight, + ) + } + Column( + Modifier.fillMaxHeight().background(color = DevkitWalletColors.primary) + ) { + Spacer(modifier = Modifier.height(16.dp)) + NavigationDrawerItem( + label = { DrawerItemLabel("About") }, + selected = items[0] == selectedItem.value, + onClick = { navController.navigate(Screen.AboutScreen.route) }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + colors = navigationItemColors + ) + NavigationDrawerItem( + label = { DrawerItemLabel("Recovery Phrase") }, + selected = items[1] == selectedItem.value, + onClick = { navController.navigate(Screen.RecoveryPhraseScreen.route) }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + colors = navigationItemColors + ) + NavigationDrawerItem( + label = { DrawerItemLabel("Custom Electrum Server") }, + selected = items[2] == selectedItem.value, + onClick = { navController.navigate(Screen.ElectrumScreen.route) }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + colors = navigationItemColors + ) + } + } + }, + content = { + WalletNavigation( + drawerState = drawerState, + ) + } + ) +} + +@Composable +fun DrawerItemLabel(text: String) { + Text( + text = text, + fontFamily = jetBrainsMonoLight, + ) +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/drawer/AboutScreen.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/drawer/AboutScreen.kt" new file mode 100644 index 0000000..851725e --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/drawer/AboutScreen.kt" @@ -0,0 +1,75 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.screens.drawer + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.goldenraven.devkitwallet.R +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.components.SecondaryScreensAppBar +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoLight + +@Composable +internal fun AboutScreen(navController: NavController) { + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "About", + navigation = { navController.navigate(Screen.WalletScreen.route) } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(DevkitWalletColors.primary) + .padding(paddingValues) + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.bdk_logo), + contentDescription = "Bitcoin testnet logo", + Modifier.size(180.dp) + ) + Spacer(modifier = Modifier.padding(24.dp)) + Text( + text = "This wallet is build for developers to learn how to leverage the Bitcoin Development Kit.", + color = DevkitWalletColors.white, + fontSize = 14.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Start, + modifier = Modifier.padding(all = 8.dp) + ) + } + } +} + +@Preview(device = Devices.PIXEL_4, showBackground = true) +@Composable +internal fun PreviewAboutScreen() { + AboutScreen(rememberNavController()) +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/drawer/ElectrumScreen.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/drawer/ElectrumScreen.kt" new file mode 100644 index 0000000..afc17bf --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/drawer/ElectrumScreen.kt" @@ -0,0 +1,133 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.screens.drawer + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material3.Button +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Switch +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.goldenraven.devkitwallet.domain.ElectrumSettings +import com.goldenraven.devkitwallet.domain.Wallet +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.components.SecondaryScreensAppBar +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoLight + +@Composable +internal fun ElectrumScreen(navController: NavController) { + val focusManager = LocalFocusManager.current + val isBlockChainCreated = Wallet.isBlockChainCreated() + val electrumServer: MutableState = remember { mutableStateOf("") } + val isChecked: MutableState = remember { mutableStateOf(false) } + if (isBlockChainCreated) { + electrumServer.value = Wallet.getElectrumURL() + isChecked.value = Wallet.isElectrumServerDefault() + } + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Custom Electrum Server", + navigation = { navController.navigate(Screen.WalletScreen.route) } + ) + }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(all = 16.dp), + ) { + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "Use default electrum URL", + color = DevkitWalletColors.white, + fontSize = 14.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Center, + ) + Switch( + checked = isChecked.value, + onCheckedChange = { + isChecked.value = it + if (it) { + Wallet.setElectrumSettings(ElectrumSettings.DEFAULT) + } else { + Wallet.setElectrumSettings(ElectrumSettings.CUSTOM) + } + }, + enabled = isBlockChainCreated + ) + } + + OutlinedTextField( + value = electrumServer.value, + onValueChange = { electrumServer.value = it }, + label = { + Text( + text = "Electrum Server", + color = DevkitWalletColors.white, + ) + }, + singleLine = true, + textStyle = TextStyle(fontFamily = jetBrainsMonoLight, color = DevkitWalletColors.white), + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + cursorColor = DevkitWalletColors.accent1, + ), + keyboardActions = KeyboardActions(onDone = { + focusManager.clearFocus() + }), + modifier = Modifier.fillMaxWidth(), + enabled = isBlockChainCreated && !isChecked.value + ) + + Button( + onClick = { + Wallet.changeElectrumServer(electrumServer.value) + focusManager.clearFocus() + }, + modifier = Modifier + .align(alignment = Alignment.End) + .padding(all = 8.dp), + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + enabled = isBlockChainCreated && !isChecked.value + ) { + Text( + text = "Save", + color = DevkitWalletColors.white, + fontSize = 12.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Center, + ) + } + } + } +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/drawer/RecoveryPhraseScreen.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/drawer/RecoveryPhraseScreen.kt" new file mode 100644 index 0000000..3a80e49 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/drawer/RecoveryPhraseScreen.kt" @@ -0,0 +1,60 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.screens.drawer + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.goldenraven.devkitwallet.domain.Repository +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.components.SecondaryScreensAppBar +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoLight + +@Composable +internal fun RecoveryPhraseScreen(navController: NavController) { + + val seedPhrase: String = Repository.getMnemonic() + val wordList: List = seedPhrase.split(" ") + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Your Recovery Phrase", + navigation = { navController.navigate(Screen.WalletScreen.route) } + ) + }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(all = 32.dp) + ) { + wordList.forEachIndexed { index, item -> + Text( + text = "${index + 1}. $item", + modifier = Modifier.weight(weight = 1F), + color = DevkitWalletColors.white, + fontFamily = jetBrainsMonoLight + ) + } + } + } +} + +@Preview(device = Devices.PIXEL_4, showBackground = true) +@Composable +internal fun PreviewRecoveryPhraseScreen() { + RecoveryPhraseScreen(rememberNavController()) +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/intro/WalletChoiceScreen.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/intro/WalletChoiceScreen.kt" new file mode 100644 index 0000000..a37c32b --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/intro/WalletChoiceScreen.kt" @@ -0,0 +1,132 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.screens.intro + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.navigation.NavController +import com.goldenraven.devkitwallet.R +import com.goldenraven.devkitwallet.WalletCreateType +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.ui.theme.devkitTypography +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoLight + +@Composable +internal fun WalletChoiceScreen( + navController: NavController, + onBuildWalletButtonClicked: (WalletCreateType) -> Unit +) { + + Scaffold { paddingValues -> + ConstraintLayout( + modifier = Modifier + .fillMaxSize() + .background(DevkitWalletColors.primary) + .padding(paddingValues) + ) { + val (logo, create, recover) = createRefs() + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 90.dp) + .constrainAs(logo) { + top.linkTo(parent.top) + }, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = R.drawable.ic_testnet_logo), + contentDescription = "Bitcoin testnet logo", + Modifier.size(90.dp) + ) + Spacer(modifier = Modifier.padding(8.dp)) + Text( + text = "BDK\nSample\nTestnet\nWallet", + color = DevkitWalletColors.white, + fontSize = 28.sp, + fontFamily = jetBrainsMonoLight, + ) + } + + Button( + onClick = { onBuildWalletButtonClicked(WalletCreateType.FROMSCRATCH) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .size(width = 300.dp, height = 170.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 8.dp, shape = RoundedCornerShape(16.dp)) + .constrainAs(create) { + bottom.linkTo(recover.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) { + Text( + text = "Create a\nNew Wallet", + // fontSize = 18.sp, + // fontFamily = jetBrainsMonoLight, + // textAlign = TextAlign.Center, + // lineHeight = 28.sp, + ) + } + + Button( + onClick = { navController.navigate(Screen.WalletRecoveryScreen.route) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .size(width = 300.dp, height = 170.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 8.dp, shape = RoundedCornerShape(16.dp)) + .constrainAs(recover) { + bottom.linkTo(parent.bottom, margin = 100.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) { + Text( + text = "Recover an\nExisting Wallet", + // fontSize = 18.sp, + // fontFamily = jetBrainsMonoLight, + // textAlign = TextAlign.Center, + // lineHeight = 28.sp, + ) + } + } + } +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewWalletChoiceScreen() { +// WalletChoiceScreen(rememberNavController()) +// } diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/intro/WalletRecoveryScreen.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/intro/WalletRecoveryScreen.kt" new file mode 100644 index 0000000..3086721 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/intro/WalletRecoveryScreen.kt" @@ -0,0 +1,205 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.screens.intro + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.navigation.NavController +import com.goldenraven.devkitwallet.WalletCreateType +import com.goldenraven.devkitwallet.ui.components.SecondaryScreensAppBar +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoLight + +@Composable +internal fun WalletRecoveryScreen( + navController: NavController, + onBuildWalletButtonClicked: (WalletCreateType) -> Unit +) { + Scaffold( + topBar = { + SecondaryScreensAppBar(title = "Recover a Wallet", navigation = { navController.popBackStack() }) + } + ) { paddingValues -> + + // the screen is broken into 2 parts: the screen title and the body + ConstraintLayout( + modifier = Modifier + .fillMaxHeight(1f) + .padding(paddingValues) + ) { + + val (screenTitle, body) = createRefs() + + val emptyRecoveryPhrase: Map = mapOf( + 1 to "", 2 to "", 3 to "", 4 to "", 5 to "", 6 to "", + 7 to "", 8 to "", 9 to "", 10 to "", 11 to "", 12 to "" + ) + val (recoveryPhraseWordMap, setRecoveryPhraseWordMap) = remember { mutableStateOf(emptyRecoveryPhrase) } + + + // the app name + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth(1f) + .background(DevkitWalletColors.primary) + .constrainAs(screenTitle) { + top.linkTo(parent.top) + } + ) { + Column { + Text( + text = "Enter your 12-word recovery phrase to recover an existing wallet.", + color = DevkitWalletColors.white, + fontSize = 14.sp, + fontFamily = jetBrainsMonoLight, + modifier = Modifier + .padding(top = 70.dp, bottom = 8.dp, start = 16.dp, end = 16.dp) + .align(Alignment.CenterHorizontally) + ) + } + } + + // the body + MyList( + recoveryPhraseWordMap, + setRecoveryPhraseWordMap, + modifier = Modifier + .constrainAs(body) { + top.linkTo(screenTitle.bottom) + bottom.linkTo(parent.bottom) + // bottom.linkTo(button.top) + height = Dimension.fillToConstraints + }, + onClick = { + onBuildWalletButtonClicked(WalletCreateType.RECOVER(buildRecoveryPhrase(recoveryPhraseWordMap))) + } + ) + } + } +} + +@Composable +fun MyList( + recoveryPhraseWordMap: Map, + setRecoveryPhraseWordMap: (Map) -> Unit, + modifier: Modifier, + onClick: () -> Unit +) { + val scrollState = rememberScrollState() + Column( + modifier + .fillMaxWidth(1f) + .background(DevkitWalletColors.primary) + .verticalScroll(state = scrollState), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val focusManager = LocalFocusManager.current + for (i in 1..12) { + WordField(wordNumber = i, recoveryPhraseWordMap, setRecoveryPhraseWordMap, focusManager) + } + Button( + onClick = onClick, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .size(width = 300.dp, height = 100.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 8.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = "Recover Wallet", + fontSize = 20.sp, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + } + } +} + +@Composable +fun WordField( + wordNumber: Int, + recoveryWordMap: Map, + setRecoveryPhraseWordMap: (Map) -> Unit, + focusManager: FocusManager +) { + OutlinedTextField( + value = recoveryWordMap[wordNumber] ?: "elvis is here", + onValueChange = { newText -> + val newMap: MutableMap = recoveryWordMap.toMutableMap() + newMap[wordNumber] = newText + + val updatedMap = newMap.toMap() + setRecoveryPhraseWordMap(updatedMap) + }, + label = { + Text( + text = "Word $wordNumber", + color = DevkitWalletColors.white, + ) + }, + textStyle = TextStyle( + fontSize = 18.sp, + color = DevkitWalletColors.white + ), + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + cursorColor = DevkitWalletColors.accent1, + ), + modifier = Modifier + .padding(8.dp), + keyboardOptions = when (wordNumber) { + 12 -> KeyboardOptions(imeAction = ImeAction.Done) + else -> KeyboardOptions(imeAction = ImeAction.Next) + }, + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) }, + onDone = { focusManager.clearFocus() } + ), + singleLine = true, + ) +} + +// input words can have capital letters, space around them, space inside of them +private fun buildRecoveryPhrase(recoveryPhraseWordMap: Map): String { + var recoveryPhrase = "" + recoveryPhraseWordMap.values.forEach { + recoveryPhrase = recoveryPhrase.plus(it.trim().replace(" ", "").lowercase().plus(" ")) + } + return recoveryPhrase.trim() +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewWalletRecoveryScreen() { +// WalletRecoveryScreen() +// } diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/RBFScreen.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/RBFScreen.kt" new file mode 100644 index 0000000..3b74d6e --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/RBFScreen.kt" @@ -0,0 +1,298 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.screens.wallet + +import android.content.Context +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.navigation.NavController +import com.goldenraven.devkitwallet.domain.Wallet +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoLight +import org.bitcoindevkit.PartiallySignedTransaction +import org.bitcoindevkit.TransactionDetails + +private const val TAG = "RBFScreen" + +@Composable +internal fun RBFScreen( + navController: NavController, + txid: String?, + paddingValues: PaddingValues = PaddingValues(0.dp), +) { + if (txid.isNullOrEmpty()) { + navController.popBackStack() + } + var transaction: TransactionDetails? = getTransaction(txid = txid) + if (transaction == null) { + navController.popBackStack() + } + transaction = transaction as TransactionDetails + val context = LocalContext.current + + val amount = (transaction.sent - transaction.received - (transaction.fee ?: 0UL)).toString() + val feeRate: MutableState = rememberSaveable { mutableStateOf("") } + val (showDialog, setShowDialog) = rememberSaveable { mutableStateOf(false) } + + ConstraintLayout( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(DevkitWalletColors.primary) + ) { + val (screenTitle, transactionInputs, bottomButtons) = createRefs() + + Text( + text = "Send Bitcoin", + color = DevkitWalletColors.white, + fontSize = 28.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Center, + modifier = Modifier + .constrainAs(screenTitle) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + .padding(top = 70.dp) + ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.constrainAs(transactionInputs) { + top.linkTo(screenTitle.bottom) + bottom.linkTo(bottomButtons.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + } + ) { + ShowTxnDetail(name = "Transaction Id",content = txid!!) + ShowTxnDetail(name = "Amount", content = amount) + TransactionFeeInput(feeRate = feeRate) + BumpFeeDialog( + txid = txid, + amount = amount, + feeRate = feeRate, + showDialog = showDialog, + setShowDialog = setShowDialog, + context = context + ) + } + Column( + Modifier + .constrainAs(bottomButtons) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + .padding(bottom = 32.dp) + ) { + Button( + onClick = { setShowDialog(true) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent2), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .height(80.dp) + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = "broadcast transaction", + fontSize = 14.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + } + Button( + onClick = { navController.navigate(Screen.HomeScreen.route) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.primaryLight), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .height(80.dp) + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = "back to wallet", + fontSize = 14.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ShowTxnDetail(name: String, content: String) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(fraction = 0.9f) + ) { + OutlinedTextField( + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.5f), + value = content, + onValueChange = { }, + label = { + Text( + text = name, + color = DevkitWalletColors.white, + ) + }, + singleLine = true, + textStyle = TextStyle(fontFamily = jetBrainsMonoLight, color = DevkitWalletColors.white), + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + cursorColor = DevkitWalletColors.accent1, + ), + enabled = false, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TransactionFeeInput(feeRate: MutableState) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + OutlinedTextField( + modifier = Modifier + .padding(vertical = 8.dp) + .fillMaxWidth(0.9f), + value = feeRate.value, + onValueChange = { newValue: String -> + feeRate.value = newValue.filter { it.isDigit() } + }, + singleLine = true, + textStyle = TextStyle(fontFamily = jetBrainsMonoLight, color = DevkitWalletColors.white), + label = { + Text( + text = "New fee rate", + color = DevkitWalletColors.white, + ) + }, + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + cursorColor = DevkitWalletColors.accent1, + ), + ) + } +} + +@Composable +fun BumpFeeDialog( + txid: String, + amount: String, + showDialog: Boolean, + setShowDialog: (Boolean) -> Unit, + context: Context, + feeRate: MutableState, +) { + if (showDialog) { + var confirmationText = "Confirm Transaction : \nTxid : $txid\nAmount : $amount" + if (feeRate.value.isNotEmpty()) { + confirmationText += "Fee Rate : ${feeRate.value.toULong()}" + } + AlertDialog( + containerColor = DevkitWalletColors.primaryLight, + onDismissRequest = {}, + title = { + Text( + text = "Confirm transaction", + color = DevkitWalletColors.white + ) + }, + text = { + Text( + text = confirmationText, + color = DevkitWalletColors.white + ) + }, + confirmButton = { + TextButton( + onClick = { + if (feeRate.value.isNotEmpty()) { + broadcastTransaction(txid = txid, feeRate = feeRate.value.toFloat()) + } else { + Toast.makeText(context, "Fee is empty!", Toast.LENGTH_SHORT).show() + } + setShowDialog(false) + }, + ) { + Text( + text = "Confirm", + color = DevkitWalletColors.white + ) + } + }, + dismissButton = { + TextButton( + onClick = { + setShowDialog(false) + }, + ) { + Text( + text = "Cancel", + color = DevkitWalletColors.white + ) + } + }, + ) + } +} + +private fun broadcastTransaction(txid: String, feeRate: Float = 1F) { + Log.i(TAG, "Attempting to broadcast transaction with inputs: txid $txid, fee rate: $feeRate") + try { + // create, sign, and broadcast + val psbt: PartiallySignedTransaction = Wallet.createBumpFeeTransaction(txid = txid, feeRate = feeRate) + Wallet.sign(psbt) + val newTxid: String = Wallet.broadcast(psbt) + Log.i(TAG, "Transaction was broadcast! txid: $newTxid") + } catch (e: Throwable) { + Log.i(TAG, "Broadcast error: ${e.message}") + } +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/ReceiveScreen.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/ReceiveScreen.kt" new file mode 100644 index 0000000..ab2ff99 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/ReceiveScreen.kt" @@ -0,0 +1,168 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.screens.wallet + +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.core.graphics.createBitmap +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.components.SecondaryScreensAppBar +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoRegular +import com.goldenraven.devkitwallet.viewmodels.AddressViewModel +import com.google.zxing.BarcodeFormat +import com.google.zxing.common.BitMatrix +import com.google.zxing.qrcode.QRCodeWriter + +private const val TAG = "ReceiveScreen" + +@Composable +internal fun ReceiveScreen( + navController: NavController, + addressViewModel: AddressViewModel = viewModel() +) { + + val address by addressViewModel.address.observeAsState(null) + val addressIndex by addressViewModel.addressIndex.observeAsState("") + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Receive Address", + navigation = { navController.navigate(Screen.HomeScreen.route) } + ) + } + ) { paddingValues -> + ConstraintLayout( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .background(DevkitWalletColors.primary) + ) { + val (QRCode, bottomButtons) = createRefs() + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.constrainAs(QRCode) { + top.linkTo(parent.top) + bottom.linkTo(bottomButtons.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + } + ) { + val QR: ImageBitmap? = address?.let { addressToQR(it) } + Log.i("ReceiveScreen", "New receive address is $address") + if (QR != null) { + Image( + bitmap = QR, + contentDescription = "Bitcoindevkit website QR code", + Modifier.size(250.dp) + ) + Spacer(modifier = Modifier.padding(vertical = 8.dp)) + SelectionContainer { + Text( + text = address!!, + fontFamily = jetBrainsMonoRegular, + color = DevkitWalletColors.white + ) + } + Spacer(modifier = Modifier.padding(vertical = 8.dp)) + Text( + text = "m/84h/1h/0h/0/$addressIndex", + fontFamily = jetBrainsMonoRegular, + color = DevkitWalletColors.white + ) + } + } + + Column( + Modifier + .constrainAs(bottomButtons) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + .padding(bottom = 24.dp) + ) { + Button( + onClick = { addressViewModel.updateAddress() }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .height(80.dp) + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = "generate address", + fontSize = 14.sp, + fontFamily = jetBrainsMonoRegular, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + } + } + } + } +} + +private fun addressToQR(address: String): ImageBitmap? { + Log.i(TAG, "We are generating the QR code for address $address") + try { + val qrCodeWriter: QRCodeWriter = QRCodeWriter() + val bitMatrix: BitMatrix = qrCodeWriter.encode(address, BarcodeFormat.QR_CODE, 1000, 1000) + val bitMap = createBitmap(1000, 1000) + for (x in 0 until 1000) { + for (y in 0 until 1000) { + // DevkitWalletColors.primaryDark for dark and DevkitWalletColors.white for light + bitMap.setPixel(x, y, if (bitMatrix[x, y]) 0xff203b46.toInt() else 0xffffffff.toInt()) + } + } + return bitMap.asImageBitmap() + } catch (e: Throwable) { + Log.i("ReceiveScreen", "Error with QRCode generation, $e") + } + return null +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewReceiveScreen() { +// ReceiveScreen(rememberNavController()) +// } diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/SendScreen.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/SendScreen.kt" new file mode 100644 index 0000000..f46a796 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/SendScreen.kt" @@ -0,0 +1,615 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.screens.wallet + +import android.content.Context +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.BottomSheetScaffold +import androidx.compose.material.BottomSheetScaffoldState +import androidx.compose.material.BottomSheetValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material3.* +import androidx.compose.material.Icon +import androidx.compose.material.rememberBottomSheetScaffoldState +import androidx.compose.material.rememberBottomSheetState +import androidx.compose.material3.Switch +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.navigation.NavController +import com.goldenraven.devkitwallet.domain.Wallet +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoLight +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import com.goldenraven.devkitwallet.R +import com.goldenraven.devkitwallet.ui.components.SecondaryScreensAppBar +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.bitcoindevkit.PartiallySignedTransaction + +private const val TAG = "SendScreen" + +@OptIn(ExperimentalMaterialApi::class) +@Composable +internal fun SendScreen( + navController: NavController, +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val recipientList: MutableList = remember { mutableStateListOf(Recipient(address = "", amount = 0u)) } + val feeRate: MutableState = rememberSaveable { mutableStateOf("") } + val (showDialog, setShowDialog) = rememberSaveable { mutableStateOf(false) } + + val sendAll: MutableState = remember { mutableStateOf(false) } + val rbfEnabled: MutableState = remember { mutableStateOf(false) } + val opReturnMsg: MutableState = remember { mutableStateOf(null) } + + val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( + bottomSheetState = rememberBottomSheetState(initialValue = BottomSheetValue.Collapsed) + ) + + BottomSheetScaffold( + topBar = { + SecondaryScreensAppBar( + title = "Send Bitcoin", + navigation = { navController.navigate(Screen.HomeScreen.route) } + ) + }, + sheetContent = { AdvancedOptions(sendAll, rbfEnabled, opReturnMsg, recipientList) }, + scaffoldState = bottomSheetScaffoldState, + sheetBackgroundColor = DevkitWalletColors.primaryDark, + sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + sheetElevation = 12.dp, + sheetPeekHeight = 0.dp, + ) { paddingValues -> + ConstraintLayout( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(DevkitWalletColors.primary) + ) { + val (transactionInputs, bottomButtons) = createRefs() + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.constrainAs(transactionInputs) { + top.linkTo(parent.top) + bottom.linkTo(bottomButtons.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + } + ) { + TransactionRecipientInput(recipientList = recipientList) + TransactionAmountInput( + recipientList = recipientList, + transactionType = if (sendAll.value) TransactionType.SEND_ALL else TransactionType.DEFAULT + ) + TransactionFeeInput(feeRate = feeRate) + MoreOptions(coroutineScope = coroutineScope, bottomSheetScaffoldState = bottomSheetScaffoldState) + Dialog( + recipientList = recipientList, + feeRate = feeRate, + showDialog = showDialog, + setShowDialog = setShowDialog, + transactionType = if (sendAll.value) TransactionType.SEND_ALL else TransactionType.DEFAULT, + rbfEnabled = rbfEnabled.value, + opReturnMsg = opReturnMsg.value, + context = context + ) + } + Column( + Modifier + .constrainAs(bottomButtons) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + .padding(bottom = 32.dp) + ) { + Button( + onClick = { setShowDialog(true) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent2), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .height(80.dp) + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = "broadcast transaction", + fontSize = 14.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AdvancedOptions( + sendAll: MutableState, + rbfEnabled: MutableState, + opReturnMsg: MutableState, + recipientList: MutableList +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Row( + Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = "Advanced Options", + color = DevkitWalletColors.white, + fontSize = 18.sp, + fontFamily = jetBrainsMonoLight, + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Send All", + color = DevkitWalletColors.white, + fontSize = 14.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + Spacer(modifier = Modifier.width(8.dp)) + Switch( + checked = sendAll.value, + onCheckedChange = { + sendAll.value = !sendAll.value + while (recipientList.size > 1) { recipientList.removeLast() } + }, + colors = SwitchDefaults.colors( + uncheckedBorderColor = DevkitWalletColors.primaryDark, + uncheckedThumbColor = DevkitWalletColors.primaryDark, + uncheckedTrackColor = DevkitWalletColors.white, + checkedThumbColor = DevkitWalletColors.white, + checkedTrackColor = DevkitWalletColors.accent1, + ) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Enable Replace-by-Fee", + color = DevkitWalletColors.white, + fontSize = 14.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + Spacer(modifier = Modifier.width(8.dp)) + Switch( + checked = rbfEnabled.value, + onCheckedChange = { + rbfEnabled.value = !rbfEnabled.value + }, + colors = SwitchDefaults.colors( + uncheckedBorderColor = DevkitWalletColors.primaryDark, + uncheckedThumbColor = DevkitWalletColors.primaryDark, + uncheckedTrackColor = DevkitWalletColors.white, + checkedThumbColor = DevkitWalletColors.white, + checkedTrackColor = DevkitWalletColors.accent1, + ) + ) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.5f), + value = opReturnMsg.value ?: "", + onValueChange = { + opReturnMsg.value = it + }, + label = { + Text( + text = "Optional OP_RETURN message", + color = DevkitWalletColors.white, + ) + }, + singleLine = true, + textStyle = TextStyle(fontFamily = jetBrainsMonoLight, color = DevkitWalletColors.white), + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + cursorColor = DevkitWalletColors.accent1, + ), + ) + } + + Row( + Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = "Number of Recipients", + color = DevkitWalletColors.white, + fontSize = 14.sp, + fontFamily = jetBrainsMonoLight, + ) + } + + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = { if (recipientList.size > 1) { recipientList.removeLast() } }, + enabled = !sendAll.value, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent2), + shape = RoundedCornerShape(16.dp), + modifier = Modifier.width(70.dp) + ) { + Text(text = "-") + } + + Text( + text = "${recipientList.size}", + color = DevkitWalletColors.white, + fontSize = 18.sp, + ) + + Button( + onClick = { recipientList.add(Recipient("", 0u)) }, + enabled = !sendAll.value, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent1), + shape = RoundedCornerShape(16.dp), + modifier = Modifier.width(70.dp) + ) { + Text(text = "+") + } + + + } + + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TransactionRecipientInput(recipientList: MutableList) { + LazyColumn (modifier = Modifier + .fillMaxWidth(0.9f) + .heightIn(max = 100.dp)) { + itemsIndexed(recipientList) { index, _ -> + val recipientAddress: MutableState = rememberSaveable { mutableStateOf("") } + + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.5f), + value = recipientAddress.value, + onValueChange = { + recipientAddress.value = it + recipientList[index].address = it + }, + label = { + Text( + text = "Recipient address ${index + 1}", + color = DevkitWalletColors.white, + ) + }, + singleLine = true, + textStyle = TextStyle(fontFamily = jetBrainsMonoLight, color = DevkitWalletColors.white), + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + cursorColor = DevkitWalletColors.accent1, + ), + ) + } + } + } +} + +fun checkRecipientList( + recipientList: MutableList, + feeRate: MutableState, + context: Context +): Boolean { + if (recipientList.size > 4) { + Toast.makeText(context, "Too many recipients", Toast.LENGTH_SHORT).show() + return false + } + for (recipient in recipientList) { + if (recipient.address == "") { + Toast.makeText(context, "Address is empty", Toast.LENGTH_SHORT).show() + return false + } + } + if (feeRate.value.isBlank()) { + Toast.makeText(context, "Fee rate is empty", Toast.LENGTH_SHORT).show() + return false + } + return true +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TransactionAmountInput(recipientList: MutableList, transactionType: TransactionType) { + LazyColumn (modifier = Modifier + .fillMaxWidth(0.9f) + .heightIn(max = 100.dp)) { + itemsIndexed(recipientList) { index, _ -> + val amount: MutableState = rememberSaveable { mutableStateOf("") } + + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.5f), + value = amount.value, + onValueChange = { + amount.value = it + recipientList[index].amount = it.toULong() + }, + label = { + when (transactionType) { + TransactionType.SEND_ALL -> { + Text( + text = "Amount (Send All)", + color = DevkitWalletColors.white, + ) + } + else -> { + Text( + text = "Amount ${index + 1}", + color = DevkitWalletColors.white, + ) + } + } + }, + singleLine = true, + textStyle = TextStyle(fontFamily = jetBrainsMonoLight, color = DevkitWalletColors.white), + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + cursorColor = DevkitWalletColors.accent1, + ), + enabled = ( + when (transactionType) { + TransactionType.SEND_ALL -> false + else -> true + } + ) + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TransactionFeeInput(feeRate: MutableState) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + OutlinedTextField( + modifier = Modifier + .padding(vertical = 8.dp) + .fillMaxWidth(0.9f), + value = feeRate.value, + onValueChange = { newValue: String -> + feeRate.value = newValue.filter { it.isDigit() } + }, + singleLine = true, + textStyle = TextStyle(fontFamily = jetBrainsMonoLight, color = DevkitWalletColors.white), + label = { + Text( + text = "Fee rate", + color = DevkitWalletColors.white, + ) + }, + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + cursorColor = DevkitWalletColors.accent1, + ), + ) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun MoreOptions(coroutineScope: CoroutineScope, bottomSheetScaffoldState: BottomSheetScaffoldState) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(vertical = 8.dp) + .background(DevkitWalletColors.secondary) + ) { + Button( + onClick = { + coroutineScope.launch { + if (bottomSheetScaffoldState.bottomSheetState.isCollapsed) { + bottomSheetScaffoldState.bottomSheetState.expand() + } else { + bottomSheetScaffoldState.bottomSheetState.collapse() + } + } + }, + colors = ButtonDefaults.buttonColors(Color.Transparent), + modifier = Modifier + .height(50.dp) + .fillMaxWidth(fraction = 0.9f) + .padding(vertical = 8.dp) + ) { + Text( + text = "more options", + fontSize = 14.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + Icon( + painter = painterResource(id = R.drawable.ic_round_expand_more_24), + tint = DevkitWalletColors.white, + contentDescription = "More options" + ) + } + } +} + +@Composable +fun Dialog( + recipientList: MutableList, + feeRate: MutableState, + showDialog: Boolean, + setShowDialog: (Boolean) -> Unit, + transactionType: TransactionType, + rbfEnabled: Boolean, + opReturnMsg: String?, + context: Context, +) { + if (showDialog) { + var confirmationText = "Confirm Transaction : \n" + recipientList.forEach { confirmationText += "${it.address}, ${it.amount}\n"} + if (feeRate.value.isNotEmpty()) { + confirmationText += "Fee Rate : ${feeRate.value.toULong()}" + } + if (!opReturnMsg.isNullOrEmpty()) { + confirmationText += "OP_RETURN Message : $opReturnMsg" + } + AlertDialog( + containerColor = DevkitWalletColors.primaryLight, + onDismissRequest = {}, + title = { + Text( + text = "Confirm transaction", + color = DevkitWalletColors.white + ) + }, + text = { + Text( + text = confirmationText, + color = DevkitWalletColors.white + ) + }, + confirmButton = { + TextButton( + onClick = { + if (checkRecipientList(recipientList = recipientList, feeRate = feeRate, context = context)) { + broadcastTransaction( + recipientList = recipientList, + feeRate = feeRate.value.toFloat(), + transactionType = transactionType, + rbfEnabled = rbfEnabled, + opReturnMsg = opReturnMsg + ) + setShowDialog(false) + } + }, + ) { + Text( + text = "Confirm", + color = DevkitWalletColors.white + ) + } + }, + dismissButton = { + TextButton( + onClick = { + setShowDialog(false) + }, + ) { + Text( + text = "Cancel", + color = DevkitWalletColors.white + ) + } + }, + ) + } +} + +private fun broadcastTransaction( + recipientList: MutableList, + feeRate: Float = 1F, + transactionType: TransactionType, + rbfEnabled: Boolean, + opReturnMsg: String? +) { + Log.i(TAG, "Attempting to broadcast transaction with inputs: recipient, amount: $recipientList, fee rate: $feeRate") + try { + // create, sign, and broadcast + val psbt: PartiallySignedTransaction = when (transactionType) { + TransactionType.DEFAULT -> Wallet.createTransaction(recipientList, feeRate, rbfEnabled, opReturnMsg) + TransactionType.SEND_ALL -> Wallet.createSendAllTransaction(recipientList[0].address, feeRate, rbfEnabled, opReturnMsg) + } + var 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: Throwable) { + Log.i(TAG, "Broadcast error: ${e.message}") + } +} + +data class Recipient(var address: String, var amount: ULong) + +enum class TransactionType { + DEFAULT, + SEND_ALL, +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewSendScreen() { +// SendScreen(rememberNavController()) +// } diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/TransactionScreen.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/TransactionScreen.kt" new file mode 100644 index 0000000..b667fa7 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/TransactionScreen.kt" @@ -0,0 +1,211 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.screens.wallet + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.navigation.NavController +import com.goldenraven.devkitwallet.domain.Wallet +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.components.SecondaryScreensAppBar +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoLight +import com.goldenraven.devkitwallet.utils.timestampToString +import org.bitcoindevkit.TransactionDetails + +@Composable +internal fun TransactionScreen( + navController: NavController, + txid: String?, +) { + val transaction = getTransaction(txid = txid) + if (transaction == null) { + navController.popBackStack() + } + val transactionDetail = getTransactionDetails(transaction = transaction!!) + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Transaction Details", + navigation = { navController.popBackStack() } + ) + } + ) { paddingValues -> + ConstraintLayout( + modifier = Modifier + .fillMaxSize() + .background(DevkitWalletColors.primary) + .padding(paddingValues) + ) { + val (screenTitle, transactions, bottomButton) = createRefs() + + Column( + modifier = Modifier + .constrainAs(screenTitle) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + .padding(top = 70.dp) + ) { + Text( + text = "Transaction", + color = DevkitWalletColors.white, + fontSize = 28.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Text( + text = transactionTitle(transaction = transaction), + color = DevkitWalletColors.white, + fontSize = 14.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + + + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.constrainAs(transactions) { + top.linkTo(screenTitle.bottom) + bottom.linkTo(bottomButton.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + } + ) { + items(transactionDetail) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(all = 16.dp) + ) { + Text( + text = "${it.first} :", + fontSize = 16.sp, + fontFamily = jetBrainsMonoLight, + color = DevkitWalletColors.white, + ) + Text( + text = it.second, + fontSize = 16.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.End, + color = DevkitWalletColors.white, + modifier = Modifier.fillMaxWidth() + ) + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + .constrainAs(bottomButton) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) { + TransactionDetailButton( + content = "increase fees", + navController = navController, + txid = txid + ) + } + } + + } +} + +@Composable +fun TransactionDetailButton(content: String, navController: NavController, txid: String?) { + Button( + onClick = { + when (content) { + "increase fees" -> { + navController.navigate("${Screen.RBFScreen.route}/txid=$txid") + } + "back to transaction list" -> { + navController.popBackStack() + } + } + }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .height(60.dp) + .fillMaxWidth() + ) { + Text( + text = content, + fontSize = 14.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + } +} + +fun getTransactionDetails(transaction: TransactionDetails): List> { + val transactionDetails = mutableListOf>() + + if (transaction.confirmationTime != null) { + transactionDetails.add(Pair("Status", "Confirmed")) + transactionDetails.add(Pair("Timestamp", transaction.confirmationTime!!.timestamp.timestampToString())) + transactionDetails.add(Pair("Received", (if (transaction.received < transaction.sent) 0 else transaction.received).toString())) + transactionDetails.add(Pair("Sent", (if (transaction.sent < transaction.received) 0 else transaction.sent - transaction.received - transaction.fee!!).toString())) + transactionDetails.add(Pair("Fees", transaction.fee.toString())) + transactionDetails.add(Pair("Block", transaction.confirmationTime!!.height.toString())) + } else { + transactionDetails.add(Pair("Status", "Pending")) + transactionDetails.add(Pair("Timestamp", "Pending")) + transactionDetails.add(Pair("Received", (if (transaction.received < transaction.sent) 0 else transaction.received).toString())) + transactionDetails.add(Pair("Sent", (if (transaction.sent < transaction.received) 0 else transaction.sent - transaction.received - transaction.fee!!).toString())) + transactionDetails.add(Pair("Fees", transaction.fee.toString())) + } + return transactionDetails +} + +fun transactionTitle(transaction: TransactionDetails): String { + return transaction.txid +} + +fun getTransaction(txid: String?): TransactionDetails? { + if (txid.isNullOrEmpty()) { + return null + } + return Wallet.getTransaction(txid = txid) +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/TransactionsScreen.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/TransactionsScreen.kt" new file mode 100644 index 0000000..35d0b11 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/TransactionsScreen.kt" @@ -0,0 +1,114 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.screens.wallet + +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.goldenraven.devkitwallet.domain.Wallet +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.components.SecondaryScreensAppBar +import com.goldenraven.devkitwallet.ui.components.ConfirmedTransactionCard +import com.goldenraven.devkitwallet.ui.components.PendingTransactionCard +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.utils.timestampToString +import org.bitcoindevkit.TransactionDetails + +private const val TAG = "TransactionsScreen" + +@Composable +internal fun TransactionsScreen(navController: NavController) { + val allTransactions: List = Wallet.getAllTransactions() + val (confirmedTransactions, unconfirmedTransactions) = sortTransactions(allTransactions) + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Transactions History", + navigation = { navController.navigate(Screen.HomeScreen.route) } + ) + } + ) { paddingValues -> + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .background(DevkitWalletColors.primary) + .padding(top = 6.dp) + .verticalScroll(state = scrollState) + ) { + if (unconfirmedTransactions.isNotEmpty()) { + unconfirmedTransactions.forEach { + PendingTransactionCard(details = it, navController = navController) + } + } + if (confirmedTransactions.isNotEmpty()) { + confirmedTransactions.forEach { + ConfirmedTransactionCard(it, navController) + } + } + } + } +} + +fun viewTransaction(navController: NavController, txid: String) { + navController.navigate("${Screen.TransactionScreen.route}/txid=$txid") +} + +private fun sortTransactions(transactions: List): Transactions { + val confirmedTransactions = mutableListOf() + val unconfirmedTransactions = mutableListOf() + transactions.forEach { tx -> + if (tx.confirmationTime != null) confirmedTransactions.add(tx) else unconfirmedTransactions.add(tx) + } + return Transactions( + confirmedTransactions = confirmedTransactions, + unconfirmedTransactions = unconfirmedTransactions + ) +} + +fun pendingTransactionsItem(transaction: TransactionDetails): String { + return buildString { + Log.i(TAG, "Pending transaction list item: $transaction") + appendLine("Timestamp: Pending") + appendLine("Received: ${transaction.received}") + appendLine("Sent: ${transaction.sent}") + appendLine("Fees: ${transaction.fee}") + append("Txid: ${transaction.txid.take(n = 8)}...${transaction.txid.takeLast(n = 8)}") + } +} + +fun confirmedTransactionsItem(transaction: TransactionDetails): String { + return buildString { + Log.i(TAG, "Transaction list item: $transaction") + appendLine("Timestamp: ${transaction.confirmationTime!!.timestamp.timestampToString()}") + appendLine("Received: ${transaction.received}") + appendLine("Sent: ${transaction.sent}") + appendLine("Block: ${transaction.confirmationTime!!.height}") + append("Txid: ${transaction.txid.take(n = 8)}...${transaction.txid.takeLast(n = 8)}") + } +} + +data class Transactions( + val confirmedTransactions: List, + val unconfirmedTransactions: List +) + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewTransactionsScreen() { +// TransactionsScreen(rememberNavController()) +// } diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/WalletHomeScreen.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/WalletHomeScreen.kt" new file mode 100644 index 0000000..04f16a9 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/screens/wallet/WalletHomeScreen.kt" @@ -0,0 +1,275 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.screens.wallet + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Menu +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import com.goldenraven.devkitwallet.R +import com.goldenraven.devkitwallet.domain.Wallet +import com.goldenraven.devkitwallet.ui.Screen +import com.goldenraven.devkitwallet.ui.components.LoadingAnimation +import com.goldenraven.devkitwallet.ui.components.NeutralButton +import com.goldenraven.devkitwallet.ui.theme.DevkitWalletColors +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoLight +import com.goldenraven.devkitwallet.ui.theme.jetBrainsMonoSemiBold +import com.goldenraven.devkitwallet.utils.formatInBtc +import com.goldenraven.devkitwallet.viewmodels.CurrencyUnit +import com.goldenraven.devkitwallet.viewmodels.WalletViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +private const val TAG = "WalletHomeScreen" + +@Composable +internal fun WalletHomeScreen( + navController: NavHostController, + drawerState: DrawerState, + walletViewModel: WalletViewModel = viewModel(), +) { + + val networkAvailable: Boolean = isOnline(LocalContext.current) + val syncing by walletViewModel.syncing.observeAsState(true) + val balance by walletViewModel.balance.observeAsState() + val unit by walletViewModel.unit.observeAsState() + if (networkAvailable && !Wallet.isBlockChainCreated()) { + Log.i(TAG, "Creating new blockchain") + Wallet.createBlockchain() + } + + val interactionSource = remember { MutableInteractionSource() } + val scope: CoroutineScope = rememberCoroutineScope() + + Scaffold( + topBar = { WalletAppBar(scope = scope, drawerState = drawerState) } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(DevkitWalletColors.primary) + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.padding(24.dp)) + Row( + Modifier + .clickable( + interactionSource, + indication = null, + onClick = { walletViewModel.switchUnit() } + ) + .fillMaxWidth(0.9f) + .padding(horizontal = 8.dp) + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ) + .height(100.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + when(unit) { + CurrencyUnit.Bitcoin, null -> { + Image( + painter = painterResource(id = R.drawable.ic_bitcoin_logo), + contentDescription = "Bitcoin testnet logo", + Modifier + .align(Alignment.CenterVertically) + .rotate(-13f) + ) + Text( + text = balance.formatInBtc(), + fontFamily = jetBrainsMonoSemiBold, + fontSize = 32.sp, + color = DevkitWalletColors.white + ) + } + CurrencyUnit.Satoshi -> { + Text( + text = "$balance sat", + fontFamily = jetBrainsMonoSemiBold, + fontSize = 32.sp, + color = DevkitWalletColors.white + ) + } + } + } + Spacer(modifier = Modifier.padding(4.dp)) + Row( + modifier = Modifier.height(40.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (syncing) LoadingAnimation() + } + + if (!networkAvailable) { + Row( + Modifier + .fillMaxWidth() + .background(color = DevkitWalletColors.accent2) + .height(50.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = "Network unavailable", + fontFamily = jetBrainsMonoLight, + fontSize = 18.sp, + color = DevkitWalletColors.white + ) + } + } + + NeutralButton( + text = "sync", + enabled = networkAvailable, + onClick = { walletViewModel.updateBalance() } + ) + + NeutralButton( + text = "transaction history", + enabled = networkAvailable, + onClick = { navController.navigate(Screen.TransactionsScreen.route) } + ) + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .height(140.dp) + .fillMaxWidth(0.9f) + ) { + Button( + onClick = { navController.navigate(Screen.ReceiveScreen.route) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent1), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .height(160.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = "receive", + fontSize = 16.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.End, + lineHeight = 28.sp, + modifier = Modifier + .fillMaxWidth(0.4f) + .align(Alignment.Bottom) + ) + } + + Button( + onClick = { navController.navigate(Screen.SendScreen.route) }, + colors = ButtonDefaults.buttonColors( + containerColor = DevkitWalletColors.accent2, + disabledContainerColor = DevkitWalletColors.accent2, + ), + shape = RoundedCornerShape(16.dp), + enabled = networkAvailable, + modifier = Modifier + .height(160.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = "send", + fontSize = 16.sp, + fontFamily = jetBrainsMonoLight, + textAlign = TextAlign.End, + lineHeight = 28.sp, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Bottom) + ) + } + } + } + + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun WalletAppBar(scope: CoroutineScope, drawerState: DrawerState) { + CenterAlignedTopAppBar( + title = { + Text( + text = "BDK Sample Wallet", + color = DevkitWalletColors.white, + fontFamily = jetBrainsMonoSemiBold, + fontSize = 20.sp, + ) + }, + navigationIcon = { + IconButton(onClick = { scope.launch { drawerState.open() } }) { + Icon( + imageVector = Icons.Rounded.Menu, + contentDescription = "Open drawer", + tint = DevkitWalletColors.white + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = DevkitWalletColors.primaryDark, + ) + ) +} + +fun isOnline(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val capabilities = + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + if (capabilities != null) { + when { + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> { + 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 + } + } + } + return false +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/theme/DevkitWalletColors.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/theme/DevkitWalletColors.kt" new file mode 100644 index 0000000..636b4e9 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/theme/DevkitWalletColors.kt" @@ -0,0 +1,18 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.theme + +import androidx.compose.ui.graphics.Color + +object DevkitWalletColors { + val primaryDark: Color = Color(0xFF203B46) // App bar + val primary: Color = Color(0xFF264653) // Background + val primaryLight: Color = Color(0xFF335F70) // Behind balance primary light + val white: Color = Color(0xffffffff) // Most text + val secondary: Color = Color(0xFF2A9D8F) // Buttons + val accent1: Color = Color(0xFFE9C46A) // Receive button + val accent2: Color = Color(0xFFE76F51) // Send button +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/theme/Fonts.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/theme/Fonts.kt" new file mode 100644 index 0000000..1c2a803 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/theme/Fonts.kt" @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.theme + +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import com.goldenraven.devkitwallet.R + +val jetBrainsMonoLight = FontFamily( + Font( + resId = R.font.jetbrains_mono_light, + weight = FontWeight.Light, + style = FontStyle.Normal + ) +) + +val jetBrainsMonoRegular = FontFamily( + Font( + resId = R.font.jetbrains_mono_regular, + weight = FontWeight.Normal, + style = FontStyle.Normal + ) +) + +val jetBrainsMonoSemiBold = FontFamily( + Font( + resId = R.font.jetbrains_mono_semibold, + weight = FontWeight.SemiBold, + style = FontStyle.Normal + ) +) diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/theme/Theme.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/theme/Theme.kt" new file mode 100644 index 0000000..0aebbd4 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/theme/Theme.kt" @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable + +@Composable +fun DevkitTheme(content: @Composable () -> Unit) { + MaterialTheme( + // colorScheme = devkitColors, + // shapes = devkitShapes, + typography = devkitTypography, + content = content + ) +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/theme/Type.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/theme/Type.kt" new file mode 100644 index 0000000..a4b9055 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/ui/theme/Type.kt" @@ -0,0 +1,23 @@ +/* + * Copyright 2020-2023 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 com.goldenraven.devkitwallet.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +internal val devkitTypography = Typography( + labelLarge = TextStyle( + fontFamily = jetBrainsMonoLight, + fontWeight = FontWeight.Light, + fontSize = 16.sp, + lineHeight = 28.sp + ), +) + +// These are the default text styles used by Material3 components: +// Buttons: labelLarge diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/utils/FormatInBtc.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/utils/FormatInBtc.kt" new file mode 100644 index 0000000..5d0c406 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/utils/FormatInBtc.kt" @@ -0,0 +1,12 @@ +package com.goldenraven.devkitwallet.utils + +import java.text.DecimalFormat + +fun ULong?.formatInBtc(): String { + val balanceInSats = if (this == 0UL || this == null) { + 0F + } else { + this.toFloat().div(100_000_000) + } + return DecimalFormat("0.00000000").format(balanceInSats) +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/utils/SharedPreferencesManager.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/utils/SharedPreferencesManager.kt" new file mode 100644 index 0000000..6467fb3 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/utils/SharedPreferencesManager.kt" @@ -0,0 +1,45 @@ + +package com.goldenraven.devkitwallet.utils + +import android.content.SharedPreferences + + +private const val PREFS_WALLET_INITIALISED = "initialized" +private const val PREFS_PATH = "path" +private const val PREFS_DESCRIPTOR = "descriptor" +private const val PREFS_CHANGE_DESCRIPTOR = "changeDescriptor" +private const val PREFS_MNEMONIC = "mnemonic" + +class SharedPreferencesManager(private val sharedPreferences: SharedPreferences) { + + var path: String + get() = sharedPreferences.getString(PREFS_PATH, "") ?: "" + set(value) { + sharedPreferences.edit()?.putString(PREFS_PATH, value)?.apply() + } + + var descriptor: String + get() = sharedPreferences.getString(PREFS_DESCRIPTOR, "") ?: "" + set(value) { + sharedPreferences.edit()?.putString(PREFS_DESCRIPTOR, value)?.apply() + } + + var changeDescriptor: String + get() = sharedPreferences.getString(PREFS_CHANGE_DESCRIPTOR, "") ?: "" + set(value) { + sharedPreferences.edit()?.putString(PREFS_CHANGE_DESCRIPTOR, value)?.apply() + } + + var mnemonic: String + get() = sharedPreferences.getString(PREFS_MNEMONIC, "No seed phrase saved") + ?: "Seed phrase not there" + set(value) { + sharedPreferences.edit()?.putString(PREFS_MNEMONIC, value)?.apply() + } + + var walletInitialised: Boolean + get() = sharedPreferences.getBoolean(PREFS_WALLET_INITIALISED, false) + set(value) { + sharedPreferences.edit()?.putBoolean(PREFS_WALLET_INITIALISED, value)?.apply() + } +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/utils/Timestamps.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/utils/Timestamps.kt" new file mode 100644 index 0000000..9e17505 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/utils/Timestamps.kt" @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 com.goldenraven.devkitwallet.utils + +import android.text.format.DateFormat +import java.util.Calendar +import java.util.Locale + +// extension function on the ULong timestamp provided in the Transaction.Confirmed type +fun ULong.timestampToString(): String { + val calendar = Calendar.getInstance(Locale.ENGLISH) + calendar.timeInMillis = (this * 1000u).toLong() + return DateFormat.format("MMMM d yyyy HH:mm", calendar).toString() +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/viewmodels/AddressViewModel.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/viewmodels/AddressViewModel.kt" new file mode 100644 index 0000000..0c3074e --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/viewmodels/AddressViewModel.kt" @@ -0,0 +1,22 @@ +package com.goldenraven.devkitwallet.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.goldenraven.devkitwallet.domain.Wallet + +internal class AddressViewModel : ViewModel() { + private var _address: MutableLiveData = MutableLiveData(null) + val address: LiveData + get() = _address + + private var _addressIndex: MutableLiveData = MutableLiveData(0u) + val addressIndex: LiveData + get() = _addressIndex + + fun updateAddress() { + val newAddress = Wallet.getNewAddress() + _address.value = newAddress.address.asString() + _addressIndex.value = newAddress.index + } +} diff --git "a/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/viewmodels/WalletViewModel.kt" "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/viewmodels/WalletViewModel.kt" new file mode 100644 index 0000000..e99eb15 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/java/com/goldenraven/devkitwallet/viewmodels/WalletViewModel.kt" @@ -0,0 +1,49 @@ +package com.goldenraven.devkitwallet.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.goldenraven.devkitwallet.domain.Wallet +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +internal class WalletViewModel : ViewModel() { + + private var _balance: MutableLiveData = MutableLiveData(0u) + val balance: LiveData + get() = _balance + + private var _syncing: MutableLiveData = MutableLiveData(false) + val syncing: LiveData + get() = _syncing + + private var _unit: MutableLiveData = MutableLiveData(CurrencyUnit.Bitcoin) + val unit: LiveData + get() = _unit + + fun switchUnit() { + _unit.value = when (_unit.value) { + CurrencyUnit.Bitcoin -> CurrencyUnit.Satoshi + CurrencyUnit.Satoshi -> CurrencyUnit.Bitcoin + null -> CurrencyUnit.Bitcoin + } + } + + fun updateBalance() { + _syncing.value = true + viewModelScope.launch(Dispatchers.IO) { + Wallet.sync() + withContext(Dispatchers.Main) { + _balance.value = Wallet.getBalance() + _syncing.value = false + } + } + } +} + +enum class CurrencyUnit { + Bitcoin, + Satoshi +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml "b/Variant \342\200\224 0.30/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" similarity index 100% rename from app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to "Variant \342\200\224 0.30/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" diff --git a/app/src/main/res/drawable/bdk_logo.xml "b/Variant \342\200\224 0.30/app/src/main/res/drawable/bdk_logo.xml" similarity index 100% rename from app/src/main/res/drawable/bdk_logo.xml rename to "Variant \342\200\224 0.30/app/src/main/res/drawable/bdk_logo.xml" diff --git "a/Variant \342\200\224 0.30/app/src/main/res/drawable/faucet_address.png" "b/Variant \342\200\224 0.30/app/src/main/res/drawable/faucet_address.png" new file mode 100644 index 0000000..9a632f1 Binary files /dev/null and "b/Variant \342\200\224 0.30/app/src/main/res/drawable/faucet_address.png" differ diff --git a/app/src/main/res/drawable/ic_bitcoin_logo.xml "b/Variant \342\200\224 0.30/app/src/main/res/drawable/ic_bitcoin_logo.xml" similarity index 100% rename from app/src/main/res/drawable/ic_bitcoin_logo.xml rename to "Variant \342\200\224 0.30/app/src/main/res/drawable/ic_bitcoin_logo.xml" diff --git "a/Variant \342\200\224 0.30/app/src/main/res/drawable/ic_launcher_background.xml" "b/Variant \342\200\224 0.30/app/src/main/res/drawable/ic_launcher_background.xml" new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/res/drawable/ic_launcher_background.xml" @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_bdk_background.xml "b/Variant \342\200\224 0.30/app/src/main/res/drawable/ic_launcher_bdk_background.xml" similarity index 100% rename from app/src/main/res/drawable/ic_launcher_bdk_background.xml rename to "Variant \342\200\224 0.30/app/src/main/res/drawable/ic_launcher_bdk_background.xml" diff --git a/app/src/main/res/drawable/ic_launcher_bdk_foreground.xml "b/Variant \342\200\224 0.30/app/src/main/res/drawable/ic_launcher_bdk_foreground.xml" similarity index 100% rename from app/src/main/res/drawable/ic_launcher_bdk_foreground.xml rename to "Variant \342\200\224 0.30/app/src/main/res/drawable/ic_launcher_bdk_foreground.xml" diff --git "a/Variant \342\200\224 0.30/app/src/main/res/drawable/ic_round_expand_more_24.xml" "b/Variant \342\200\224 0.30/app/src/main/res/drawable/ic_round_expand_more_24.xml" new file mode 100644 index 0000000..c66295f --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/res/drawable/ic_round_expand_more_24.xml" @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_testnet_logo.xml "b/Variant \342\200\224 0.30/app/src/main/res/drawable/ic_testnet_logo.xml" similarity index 100% rename from app/src/main/res/drawable/ic_testnet_logo.xml rename to "Variant \342\200\224 0.30/app/src/main/res/drawable/ic_testnet_logo.xml" diff --git a/app/src/main/res/drawable/launch_screen.xml "b/Variant \342\200\224 0.30/app/src/main/res/drawable/launch_screen.xml" similarity index 100% rename from app/src/main/res/drawable/launch_screen.xml rename to "Variant \342\200\224 0.30/app/src/main/res/drawable/launch_screen.xml" diff --git "a/Variant \342\200\224 0.30/app/src/main/res/font/fira_mono.ttf" "b/Variant \342\200\224 0.30/app/src/main/res/font/fira_mono.ttf" new file mode 100644 index 0000000..558bb75 Binary files /dev/null and "b/Variant \342\200\224 0.30/app/src/main/res/font/fira_mono.ttf" differ diff --git "a/Variant \342\200\224 0.30/app/src/main/res/font/fira_mono_medium.ttf" "b/Variant \342\200\224 0.30/app/src/main/res/font/fira_mono_medium.ttf" new file mode 100644 index 0000000..034b637 Binary files /dev/null and "b/Variant \342\200\224 0.30/app/src/main/res/font/fira_mono_medium.ttf" differ diff --git "a/Variant \342\200\224 0.30/app/src/main/res/font/jetbrains_mono_light.ttf" "b/Variant \342\200\224 0.30/app/src/main/res/font/jetbrains_mono_light.ttf" new file mode 100644 index 0000000..15f15a2 Binary files /dev/null and "b/Variant \342\200\224 0.30/app/src/main/res/font/jetbrains_mono_light.ttf" differ diff --git "a/Variant \342\200\224 0.30/app/src/main/res/font/jetbrains_mono_regular.ttf" "b/Variant \342\200\224 0.30/app/src/main/res/font/jetbrains_mono_regular.ttf" new file mode 100644 index 0000000..dff66cc Binary files /dev/null and "b/Variant \342\200\224 0.30/app/src/main/res/font/jetbrains_mono_regular.ttf" differ diff --git "a/Variant \342\200\224 0.30/app/src/main/res/font/jetbrains_mono_semibold.ttf" "b/Variant \342\200\224 0.30/app/src/main/res/font/jetbrains_mono_semibold.ttf" new file mode 100644 index 0000000..a70e69b Binary files /dev/null and "b/Variant \342\200\224 0.30/app/src/main/res/font/jetbrains_mono_semibold.ttf" differ diff --git "a/Variant \342\200\224 0.30/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" new file mode 100644 index 0000000..eca70cf --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml" similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml" diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml" similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml" diff --git "a/Variant \342\200\224 0.30/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" new file mode 100644 index 0000000..eca70cf --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-hdpi/ic_launcher.png" similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-hdpi/ic_launcher.png" diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png" similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png" diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png" similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png" diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-hdpi/ic_launcher_round.png" similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-hdpi/ic_launcher_round.png" diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-mdpi/ic_launcher.png" similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-mdpi/ic_launcher.png" diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png" similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png" diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png" similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png" diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-mdpi/ic_launcher_round.png" similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-mdpi/ic_launcher_round.png" diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-xhdpi/ic_launcher.png" similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-xhdpi/ic_launcher.png" diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png" similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png" diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png" similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png" diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png" similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png" diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png" similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png" diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png" similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png" diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png" similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png" diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png" similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png" diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png" similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png" diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png "b/Variant \342\200\224 0.30/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png" similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to "Variant \342\200\224 0.30/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png" diff --git a/app/src/main/res/values/colors.xml "b/Variant \342\200\224 0.30/app/src/main/res/values/colors.xml" similarity index 100% rename from app/src/main/res/values/colors.xml rename to "Variant \342\200\224 0.30/app/src/main/res/values/colors.xml" diff --git "a/Variant \342\200\224 0.30/app/src/main/res/values/strings.xml" "b/Variant \342\200\224 0.30/app/src/main/res/values/strings.xml" new file mode 100644 index 0000000..6817bce --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/res/values/strings.xml" @@ -0,0 +1,3 @@ + + BDK Android Sample Wallet + diff --git "a/Variant \342\200\224 0.30/app/src/main/res/values/themes.xml" "b/Variant \342\200\224 0.30/app/src/main/res/values/themes.xml" new file mode 100644 index 0000000..6e1c488 --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/main/res/values/themes.xml" @@ -0,0 +1,11 @@ + + + + + + diff --git "a/Variant \342\200\224 0.30/app/src/test/java/com/goldenraven/devkitwallet/ExampleUnitTest.kt" "b/Variant \342\200\224 0.30/app/src/test/java/com/goldenraven/devkitwallet/ExampleUnitTest.kt" new file mode 100644 index 0000000..b8c38be --- /dev/null +++ "b/Variant \342\200\224 0.30/app/src/test/java/com/goldenraven/devkitwallet/ExampleUnitTest.kt" @@ -0,0 +1,12 @@ +package com.goldenraven.devkitwallet + +import org.junit.Test + +import org.junit.Assert.* + +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git "a/Variant \342\200\224 0.30/build.gradle.kts" "b/Variant \342\200\224 0.30/build.gradle.kts" new file mode 100644 index 0000000..3e7b674 --- /dev/null +++ "b/Variant \342\200\224 0.30/build.gradle.kts" @@ -0,0 +1,4 @@ +plugins { + id("com.android.application").version("8.2.0").apply(false) + id("org.jetbrains.kotlin.android").version("1.9.20").apply(false) +} diff --git "a/Variant \342\200\224 0.30/gradle.properties" "b/Variant \342\200\224 0.30/gradle.properties" new file mode 100644 index 0000000..a69daa6 --- /dev/null +++ "b/Variant \342\200\224 0.30/gradle.properties" @@ -0,0 +1,7 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +kotlin.code.style=official +android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/gradle/wrapper/gradle-wrapper.jar "b/Variant \342\200\224 0.30/gradle/wrapper/gradle-wrapper.jar" similarity index 100% rename from gradle/wrapper/gradle-wrapper.jar rename to "Variant \342\200\224 0.30/gradle/wrapper/gradle-wrapper.jar" diff --git "a/Variant \342\200\224 0.30/gradle/wrapper/gradle-wrapper.properties" "b/Variant \342\200\224 0.30/gradle/wrapper/gradle-wrapper.properties" new file mode 100644 index 0000000..15de902 --- /dev/null +++ "b/Variant \342\200\224 0.30/gradle/wrapper/gradle-wrapper.properties" @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew "b/Variant \342\200\224 0.30/gradlew" similarity index 100% rename from gradlew rename to "Variant \342\200\224 0.30/gradlew" diff --git a/gradlew.bat "b/Variant \342\200\224 0.30/gradlew.bat" similarity index 100% rename from gradlew.bat rename to "Variant \342\200\224 0.30/gradlew.bat" diff --git "a/Variant \342\200\224 0.30/settings.gradle.kts" "b/Variant \342\200\224 0.30/settings.gradle.kts" new file mode 100644 index 0000000..a8febb5 --- /dev/null +++ "b/Variant \342\200\224 0.30/settings.gradle.kts" @@ -0,0 +1,23 @@ +rootProject.name = "BDK Android Example Wallet" +include("app") + +pluginManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + + // snapshot repository + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") + + // Local Maven (~/.m2/repository/) + // mavenLocal() + } +} diff --git a/.editorconfig "b/Variant \342\200\224 1.0/.editorconfig" similarity index 100% rename from .editorconfig rename to "Variant \342\200\224 1.0/.editorconfig" diff --git a/.gitignore "b/Variant \342\200\224 1.0/.gitignore" similarity index 100% rename from .gitignore rename to "Variant \342\200\224 1.0/.gitignore" diff --git "a/Variant \342\200\224 1.0/LICENSE" "b/Variant \342\200\224 1.0/LICENSE" new file mode 100644 index 0000000..8061725 --- /dev/null +++ "b/Variant \342\200\224 1.0/LICENSE" @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2021 thunderbiscuit and contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git "a/Variant \342\200\224 1.0/app/build.gradle.kts" "b/Variant \342\200\224 1.0/app/build.gradle.kts" new file mode 100644 index 0000000..47966ff --- /dev/null +++ "b/Variant \342\200\224 1.0/app/build.gradle.kts" @@ -0,0 +1,117 @@ +import org.jlleitschuh.gradle.ktlint.reporter.ReporterType + +plugins { + id("com.android.application") version "8.7.1" + id("org.jetbrains.kotlin.android") version "2.1.10" + id("org.jetbrains.kotlin.plugin.compose") version "2.1.10" + id("org.jetbrains.kotlin.plugin.serialization") version "2.1.10" + id("com.google.protobuf") version "0.9.4" + id("org.jlleitschuh.gradle.ktlint") version "12.1.2" +} + +// This is the version of the app that is displayed in the UI on the drawer. +val variantName = "Version 0.1.0/Esplora" + +android { + namespace = "org.bitcoindevkit.devkitwallet" + compileSdk = 35 + + buildFeatures { + viewBinding = true + compose = true + buildConfig = true + } + + defaultConfig { + applicationId = "org.bitcoindevkit.devkitwallet" + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "v0.1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField("String", "VARIANT_NAME", "\"$variantName\"") + } + + buildTypes { + getByName("debug") { + isDebuggable = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + // Basic android dependencies + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.23") + implementation("androidx.core:core-ktx:1.13.1") + implementation("com.google.android.material:material:1.12.0") + implementation("androidx.datastore:datastore:1.1.1") + implementation("com.google.protobuf:protobuf-javalite:3.18.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation("androidx.core:core-splashscreen:1.0.1") + + // Jetpack Compose + // Adding the Bill of Materials synchronizes dependencies in the androidx.compose namespace + // You can remove the library version in your dependency declarations + implementation(platform("androidx.compose:compose-bom:2025.02.00")) + implementation("androidx.compose.animation:animation") + implementation("androidx.compose.ui:ui-tooling") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.material3:material3") + implementation("androidx.activity:activity-compose") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5") + implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1") + implementation("androidx.navigation:navigation-compose:2.8.0") + implementation("com.google.accompanist:accompanist-systemuicontroller:0.23.1") + + // Icons + implementation("androidx.compose.material:material-icons-extended:1.7.8") + implementation("com.composables:icons-lucide:1.0.0") + + // Toolbar + implementation("androidx.appcompat:appcompat:1.7.0") + + // Bitcoin Development Kit + implementation("org.bitcoindevkit:bdk-android:1.2.0") + + // QR codes + implementation("com.google.zxing:core:3.5.3") + + // Tests + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.25.0" + } + + generateProtoTasks { + all().forEach { task -> + task.builtins { + create("java") { + option("lite") + } + } + } + } +} + +ktlint { + version = "1.5.0" + ignoreFailures = false + reporters { + reporter(ReporterType.PLAIN).apply { outputToConsole = true } + } +} diff --git "a/Variant \342\200\224 1.0/app/proguard-rules.pro" "b/Variant \342\200\224 1.0/app/proguard-rules.pro" new file mode 100644 index 0000000..ff59496 --- /dev/null +++ "b/Variant \342\200\224 1.0/app/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.kts. +# +# 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/app/src/main/AndroidManifest.xml "b/Variant \342\200\224 1.0/app/src/main/AndroidManifest.xml" similarity index 100% rename from app/src/main/AndroidManifest.xml rename to "Variant \342\200\224 1.0/app/src/main/AndroidManifest.xml" diff --git "a/Variant \342\200\224 1.0/app/src/main/assets/bip39-english.txt" "b/Variant \342\200\224 1.0/app/src/main/assets/bip39-english.txt" new file mode 100644 index 0000000..f78ccaf --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/assets/bip39-english.txt" @@ -0,0 +1,2048 @@ +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 \ No newline at end of file diff --git "a/Variant \342\200\224 1.0/app/src/main/ic_launcher_bdk-playstore.png" "b/Variant \342\200\224 1.0/app/src/main/ic_launcher_bdk-playstore.png" new file mode 100644 index 0000000..b834711 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/ic_launcher_bdk-playstore.png" differ diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/data/TxDetails.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/data/TxDetails.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/data/TxDetails.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/data/TxDetails.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/data/UserPreferencesSerializer.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/data/UserPreferencesSerializer.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/data/UserPreferencesSerializer.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/data/UserPreferencesSerializer.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/data/WalletConfigs.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/data/WalletConfigs.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/data/WalletConfigs.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/data/WalletConfigs.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt" diff --git "a/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt" "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt" new file mode 100644 index 0000000..7dd1f0f --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt" @@ -0,0 +1,58 @@ +/* + * Copyright 2021-2025 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(EsploraClient("https://blockstream.info/testnet/api/"), true) + } + Network.TESTNET4 -> throw IllegalArgumentException("This app does not support testnet 4 yet") + Network.SIGNET -> { + config.addClient(EsploraClient("http://signet.bitcoindevkit.net"), true) + } + Network.BITCOIN -> throw IllegalArgumentException("This app does not support mainnet") + } + return config + } + } +} diff --git "a/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Constants.kt" "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Constants.kt" new file mode 100644 index 0000000..2efc4fc --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Constants.kt" @@ -0,0 +1,2060 @@ +package org.bitcoindevkit.devkitwallet.domain + +import org.bitcoindevkit.Network + +val supportedNetworks: List = listOf( + Network.SIGNET, + Network.TESTNET, + Network.REGTEST, +) + +val bip39WordList: List = listOf( + "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" +) diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/CurrencyUnit.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/CurrencyUnit.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/domain/CurrencyUnit.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/CurrencyUnit.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/DwLogger.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/DwLogger.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/domain/DwLogger.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/DwLogger.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/UserPreferencesRepository.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/UserPreferencesRepository.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/domain/UserPreferencesRepository.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/UserPreferencesRepository.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/FormatInBtc.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/FormatInBtc.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/FormatInBtc.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/FormatInBtc.kt" diff --git "a/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/ProtobufExtensions.kt" "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/ProtobufExtensions.kt" new file mode 100644 index 0000000..29687cc --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/ProtobufExtensions.kt" @@ -0,0 +1,28 @@ +/* + * Copyright 2021-2025 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.utils + +import org.bitcoindevkit.Network +import org.bitcoindevkit.devkitwallet.data.ActiveWalletNetwork + +fun Network.intoProto(): ActiveWalletNetwork { + return when (this) { + Network.REGTEST -> ActiveWalletNetwork.REGTEST + Network.TESTNET -> ActiveWalletNetwork.TESTNET + Network.TESTNET4 -> throw IllegalArgumentException("Bitcoin testnet 4 network is not supported") + Network.SIGNET -> ActiveWalletNetwork.SIGNET + Network.BITCOIN -> throw IllegalArgumentException("Bitcoin mainnet network is not supported") + } +} + +fun ActiveWalletNetwork.intoDomain(): Network { + return when (this) { + ActiveWalletNetwork.TESTNET -> Network.TESTNET + ActiveWalletNetwork.SIGNET -> Network.SIGNET + ActiveWalletNetwork.REGTEST -> Network.REGTEST + ActiveWalletNetwork.UNRECOGNIZED -> throw IllegalArgumentException("Unrecognized network") + } +} diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/Timestamps.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/Timestamps.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/Timestamps.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/Timestamps.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/CreateWalletNavigation.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/CreateWalletNavigation.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/CreateWalletNavigation.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/CreateWalletNavigation.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/Destinations.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/Destinations.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/Destinations.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/Destinations.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/HomeNavigation.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/HomeNavigation.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/HomeNavigation.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/HomeNavigation.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/WalletNavigation.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/WalletNavigation.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/WalletNavigation.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/WalletNavigation.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/DevkitWalletColors.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/DevkitWalletColors.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/DevkitWalletColors.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/DevkitWalletColors.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Fonts.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Fonts.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Fonts.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Fonts.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Theme.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Theme.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Theme.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Theme.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Type.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Type.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Type.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Type.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/CustomSnackbar.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/CustomSnackbar.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/CustomSnackbar.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/CustomSnackbar.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/LoadingAnimation.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/LoadingAnimation.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/LoadingAnimation.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/LoadingAnimation.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/NeutralButton.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/NeutralButton.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/NeutralButton.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/NeutralButton.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/RadioButtonWithLabel.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/RadioButtonWithLabel.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/RadioButtonWithLabel.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/RadioButtonWithLabel.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/SecondaryScreensAppBar.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/SecondaryScreensAppBar.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/SecondaryScreensAppBar.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/SecondaryScreensAppBar.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/TransactionCards.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/TransactionCards.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/TransactionCards.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/TransactionCards.kt" diff --git "a/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/WalletOptionsCard.kt" "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/WalletOptionsCard.kt" new file mode 100644 index 0000000..2e5788d --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/WalletOptionsCard.kt" @@ -0,0 +1,130 @@ +package org.bitcoindevkit.devkitwallet.presentation.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.bitcoindevkit.Network +import org.bitcoindevkit.devkitwallet.data.ActiveWalletScriptType +import org.bitcoindevkit.devkitwallet.domain.supportedNetworks +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.intro.displayString + +@Composable +fun WalletOptionsCard( + scriptTypes: List, + selectedNetwork: MutableState, + selectedScriptType: MutableState, +) { + Column( + Modifier + .fillMaxWidth() + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = "Network", + fontFamily = monoRegular, + fontSize = 14.sp, + color = DevkitWalletColors.white, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(top = 8.dp, bottom = 8.dp) + ) + + HorizontalDivider( + color = DevkitWalletColors.primaryDark, + thickness = 4.dp, + modifier = Modifier.padding(bottom = 8.dp) + ) + + supportedNetworks.forEachIndexed { index, it -> + RadioButtonWithLabel( + label = it.displayString(), + isSelected = selectedNetwork.value == it, + onSelect = { selectedNetwork.value = it } + ) + if (index == 2) Spacer(modifier = Modifier.padding(bottom = 8.dp)) + } + + Text( + text = "Script Type", + fontFamily = monoRegular, + fontSize = 14.sp, + color = DevkitWalletColors.white, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(top = 16.dp, bottom = 8.dp) + ) + + HorizontalDivider( + color = DevkitWalletColors.primaryDark, + thickness = 4.dp, + modifier = Modifier.padding(bottom = 8.dp) + ) + + scriptTypes.forEachIndexed { index, it -> + RadioButtonWithLabel( + label = it.displayString(), + isSelected = selectedScriptType.value == it, + onSelect = { selectedScriptType.value = it } + ) + if (index == 1) Spacer(modifier = Modifier.padding(bottom = 8.dp)) + } + } +} + +@Composable +fun NetworkOptionsCard(selectedNetwork: MutableState) { + Column( + Modifier + .fillMaxWidth() + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = "Network", + fontFamily = monoRegular, + fontSize = 14.sp, + color = DevkitWalletColors.white, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(top = 8.dp, bottom = 8.dp) + ) + + HorizontalDivider( + color = DevkitWalletColors.primaryDark, + thickness = 4.dp, + modifier = Modifier.padding(bottom = 8.dp) + ) + + supportedNetworks.forEachIndexed { index, it -> + RadioButtonWithLabel( + label = it.displayString(), + isSelected = selectedNetwork.value == it, + onSelect = { selectedNetwork.value = it } + ) + if (index == 2) Spacer(modifier = Modifier.padding(bottom = 8.dp)) + } + } +} diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/WalletRoot.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/WalletRoot.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/WalletRoot.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/WalletRoot.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/ActiveWalletsScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/ActiveWalletsScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/ActiveWalletsScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/ActiveWalletsScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/CreateNewWallet.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/CreateNewWallet.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/CreateNewWallet.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/CreateNewWallet.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/RecoverWalletScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/RecoverWalletScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/RecoverWalletScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/RecoverWalletScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/WalletChoiceScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/WalletChoiceScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/WalletChoiceScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/WalletChoiceScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/RBFScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/RBFScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/RBFScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/RBFScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/ReceiveScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/ReceiveScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/ReceiveScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/ReceiveScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionHistoryScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionHistoryScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionHistoryScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionHistoryScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/AddressViewModel.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/AddressViewModel.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/AddressViewModel.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/AddressViewModel.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviReceiveScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviReceiveScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviReceiveScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviReceiveScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviSendScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviSendScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviSendScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviSendScreen.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt "b/Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt rename to "Variant \342\200\224 1.0/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt" diff --git "a/Variant \342\200\224 1.0/app/src/main/proto/wallets.proto" "b/Variant \342\200\224 1.0/app/src/main/proto/wallets.proto" new file mode 100644 index 0000000..2f1f8a8 --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/proto/wallets.proto" @@ -0,0 +1,32 @@ +syntax = "proto3"; + +option java_package = "org.bitcoindevkit.devkitwallet.data"; +option java_multiple_files = true; + +message UserPreferences { + bool introDone = 1; + repeated SingleWallet wallets = 2; +} + +message SingleWallet { + string id = 1; + string name = 2; + ActiveWalletNetwork network = 3; + ActiveWalletScriptType scriptType = 4; + string descriptor = 5; + string changeDescriptor = 6; + string recoveryPhrase = 7; + bool fullScanCompleted = 8; +} + +enum ActiveWalletNetwork { + TESTNET = 0; + SIGNET = 1; + REGTEST = 2; +} + +enum ActiveWalletScriptType { + P2WPKH = 0; + P2TR = 1; + UNKNOWN = 2; +} diff --git "a/Variant \342\200\224 1.0/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" "b/Variant \342\200\224 1.0/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git "a/Variant \342\200\224 1.0/app/src/main/res/drawable/bdk_logo.xml" "b/Variant \342\200\224 1.0/app/src/main/res/drawable/bdk_logo.xml" new file mode 100644 index 0000000..a28d65b --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/res/drawable/bdk_logo.xml" @@ -0,0 +1,24 @@ + + + + + diff --git "a/Variant \342\200\224 1.0/app/src/main/res/drawable/ic_bitcoin_logo.xml" "b/Variant \342\200\224 1.0/app/src/main/res/drawable/ic_bitcoin_logo.xml" new file mode 100644 index 0000000..d57d1bb --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/res/drawable/ic_bitcoin_logo.xml" @@ -0,0 +1,12 @@ + + + + diff --git "a/Variant \342\200\224 1.0/app/src/main/res/drawable/ic_launcher_bdk_background.xml" "b/Variant \342\200\224 1.0/app/src/main/res/drawable/ic_launcher_bdk_background.xml" new file mode 100644 index 0000000..6884ff0 --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/res/drawable/ic_launcher_bdk_background.xml" @@ -0,0 +1,9 @@ + + + + + + + diff --git "a/Variant \342\200\224 1.0/app/src/main/res/drawable/ic_launcher_bdk_foreground.xml" "b/Variant \342\200\224 1.0/app/src/main/res/drawable/ic_launcher_bdk_foreground.xml" new file mode 100644 index 0000000..227c9e3 --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/res/drawable/ic_launcher_bdk_foreground.xml" @@ -0,0 +1,29 @@ + + + + + + + diff --git "a/Variant \342\200\224 1.0/app/src/main/res/drawable/ic_testnet_logo.xml" "b/Variant \342\200\224 1.0/app/src/main/res/drawable/ic_testnet_logo.xml" new file mode 100644 index 0000000..d57d1bb --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/res/drawable/ic_testnet_logo.xml" @@ -0,0 +1,12 @@ + + + + diff --git "a/Variant \342\200\224 1.0/app/src/main/res/drawable/launch_screen.xml" "b/Variant \342\200\224 1.0/app/src/main/res/drawable/launch_screen.xml" new file mode 100644 index 0000000..52e7de9 --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/res/drawable/launch_screen.xml" @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/font/ia_writer_mono_bold.ttf "b/Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_mono_bold.ttf" similarity index 100% rename from app/src/main/res/font/ia_writer_mono_bold.ttf rename to "Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_mono_bold.ttf" diff --git a/app/src/main/res/font/ia_writer_mono_bold_italic.ttf "b/Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_mono_bold_italic.ttf" similarity index 100% rename from app/src/main/res/font/ia_writer_mono_bold_italic.ttf rename to "Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_mono_bold_italic.ttf" diff --git a/app/src/main/res/font/ia_writer_mono_regular.ttf "b/Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_mono_regular.ttf" similarity index 100% rename from app/src/main/res/font/ia_writer_mono_regular.ttf rename to "Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_mono_regular.ttf" diff --git a/app/src/main/res/font/ia_writer_mono_regular_italic.ttf "b/Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_mono_regular_italic.ttf" similarity index 100% rename from app/src/main/res/font/ia_writer_mono_regular_italic.ttf rename to "Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_mono_regular_italic.ttf" diff --git a/app/src/main/res/font/ia_writer_quattro_bold.ttf "b/Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_quattro_bold.ttf" similarity index 100% rename from app/src/main/res/font/ia_writer_quattro_bold.ttf rename to "Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_quattro_bold.ttf" diff --git a/app/src/main/res/font/ia_writer_quattro_bold_italic.ttf "b/Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_quattro_bold_italic.ttf" similarity index 100% rename from app/src/main/res/font/ia_writer_quattro_bold_italic.ttf rename to "Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_quattro_bold_italic.ttf" diff --git a/app/src/main/res/font/ia_writer_quattro_regular.ttf "b/Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_quattro_regular.ttf" similarity index 100% rename from app/src/main/res/font/ia_writer_quattro_regular.ttf rename to "Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_quattro_regular.ttf" diff --git a/app/src/main/res/font/ia_writer_quattro_regular_italic.ttf "b/Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_quattro_regular_italic.ttf" similarity index 100% rename from app/src/main/res/font/ia_writer_quattro_regular_italic.ttf rename to "Variant \342\200\224 1.0/app/src/main/res/font/ia_writer_quattro_regular_italic.ttf" diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to "Variant \342\200\224 1.0/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml" new file mode 100644 index 0000000..af9e637 --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml" @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml" new file mode 100644 index 0000000..af9e637 --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml" @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to "Variant \342\200\224 1.0/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-hdpi/ic_launcher.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-hdpi/ic_launcher.png" new file mode 100644 index 0000000..a571e60 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-hdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..b94c1ed Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..07db04c Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-hdpi/ic_launcher_round.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-hdpi/ic_launcher_round.png" new file mode 100644 index 0000000..61da551 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-hdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-mdpi/ic_launcher.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-mdpi/ic_launcher.png" new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-mdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..d4fb078 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..efe9825 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-mdpi/ic_launcher_round.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-mdpi/ic_launcher_round.png" new file mode 100644 index 0000000..db5080a Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-mdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-xhdpi/ic_launcher.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xhdpi/ic_launcher.png" new file mode 100644 index 0000000..6dba46d Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xhdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..3bde8f4 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..5d4b7d9 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png" new file mode 100644 index 0000000..da31a87 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" new file mode 100644 index 0000000..15ac681 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..66f997c Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..ad3ace1 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png" new file mode 100644 index 0000000..b216f2d Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" new file mode 100644 index 0000000..f25a419 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..00ab356 Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..28da01a Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png" "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png" new file mode 100644 index 0000000..e96783c Binary files /dev/null and "b/Variant \342\200\224 1.0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 1.0/app/src/main/res/values/colors.xml" "b/Variant \342\200\224 1.0/app/src/main/res/values/colors.xml" new file mode 100644 index 0000000..a66aa6b --- /dev/null +++ "b/Variant \342\200\224 1.0/app/src/main/res/values/colors.xml" @@ -0,0 +1,4 @@ + + + #203b46 + diff --git a/app/src/main/res/values/splash.xml "b/Variant \342\200\224 1.0/app/src/main/res/values/splash.xml" similarity index 100% rename from app/src/main/res/values/splash.xml rename to "Variant \342\200\224 1.0/app/src/main/res/values/splash.xml" diff --git a/app/src/main/res/values/strings.xml "b/Variant \342\200\224 1.0/app/src/main/res/values/strings.xml" similarity index 100% rename from app/src/main/res/values/strings.xml rename to "Variant \342\200\224 1.0/app/src/main/res/values/strings.xml" diff --git a/app/src/main/res/values/themes.xml "b/Variant \342\200\224 1.0/app/src/main/res/values/themes.xml" similarity index 100% rename from app/src/main/res/values/themes.xml rename to "Variant \342\200\224 1.0/app/src/main/res/values/themes.xml" diff --git a/app/src/test/java/org/bitcoindevkit/devkitwallet/ExampleUnitTest.kt "b/Variant \342\200\224 1.0/app/src/test/java/org/bitcoindevkit/devkitwallet/ExampleUnitTest.kt" similarity index 100% rename from app/src/test/java/org/bitcoindevkit/devkitwallet/ExampleUnitTest.kt rename to "Variant \342\200\224 1.0/app/src/test/java/org/bitcoindevkit/devkitwallet/ExampleUnitTest.kt" diff --git a/gradle.properties "b/Variant \342\200\224 1.0/gradle.properties" similarity index 100% rename from gradle.properties rename to "Variant \342\200\224 1.0/gradle.properties" diff --git "a/Variant \342\200\224 1.0/gradle/wrapper/gradle-wrapper.jar" "b/Variant \342\200\224 1.0/gradle/wrapper/gradle-wrapper.jar" new file mode 100644 index 0000000..7454180 Binary files /dev/null and "b/Variant \342\200\224 1.0/gradle/wrapper/gradle-wrapper.jar" differ diff --git "a/Variant \342\200\224 1.0/gradle/wrapper/gradle-wrapper.properties" "b/Variant \342\200\224 1.0/gradle/wrapper/gradle-wrapper.properties" new file mode 100644 index 0000000..1e2fbf0 --- /dev/null +++ "b/Variant \342\200\224 1.0/gradle/wrapper/gradle-wrapper.properties" @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git "a/Variant \342\200\224 1.0/gradlew" "b/Variant \342\200\224 1.0/gradlew" new file mode 100755 index 0000000..744e882 --- /dev/null +++ "b/Variant \342\200\224 1.0/gradlew" @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git "a/Variant \342\200\224 1.0/gradlew.bat" "b/Variant \342\200\224 1.0/gradlew.bat" new file mode 100644 index 0000000..107acd3 --- /dev/null +++ "b/Variant \342\200\224 1.0/gradlew.bat" @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/justfile "b/Variant \342\200\224 1.0/justfile" similarity index 100% rename from justfile rename to "Variant \342\200\224 1.0/justfile" diff --git a/settings.gradle.kts "b/Variant \342\200\224 1.0/settings.gradle.kts" similarity index 100% rename from settings.gradle.kts rename to "Variant \342\200\224 1.0/settings.gradle.kts" diff --git "a/Variant \342\200\224 Esplora/.editorconfig" "b/Variant \342\200\224 Esplora/.editorconfig" new file mode 100644 index 0000000..8ac4356 --- /dev/null +++ "b/Variant \342\200\224 Esplora/.editorconfig" @@ -0,0 +1,14 @@ +# Root .editorconfig file +root = true + +[*.{kt,kts}] +indent_style = space +max_line_length = 120 + +ktlint_standard_trailing-comma-on-call-site = disabled +ktlint_standard_multiline-expression-wrapping = disabled +ktlint_standard_string-template-indent = disabled +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 5 +ktlint_function_naming_ignore_when_annotated_with=Composable +ktlint_standard_function-expression-body = disabled +ktlint_standard_class-signature = disabled diff --git "a/Variant \342\200\224 Esplora/.gitignore" "b/Variant \342\200\224 Esplora/.gitignore" new file mode 100644 index 0000000..a23b05c --- /dev/null +++ "b/Variant \342\200\224 Esplora/.gitignore" @@ -0,0 +1,21 @@ +*.iml +.gradle +/.idea/ +.DS_Store +/build +/app/build/ +/captures +.externalNativeBuild +.cxx +app-simple-wallet/local.properties +app-advanced-features/local.properties +app-ui-only/local.properties +app-simple-wallet/app/build/ +app/build/ +app-ui-only/app/build/ +app-clean/ +.idea/ +local.properties +app.run.xml +release/ +.kotlin/ diff --git a/app/build.gradle.kts "b/Variant \342\200\224 Esplora/app/build.gradle.kts" similarity index 100% rename from app/build.gradle.kts rename to "Variant \342\200\224 Esplora/app/build.gradle.kts" diff --git "a/Variant \342\200\224 Esplora/app/proguard-rules.pro" "b/Variant \342\200\224 Esplora/app/proguard-rules.pro" new file mode 100644 index 0000000..ff59496 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/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.kts. +# +# 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/Variant \342\200\224 Esplora/app/src/main/AndroidManifest.xml" "b/Variant \342\200\224 Esplora/app/src/main/AndroidManifest.xml" new file mode 100644 index 0000000..db99496 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/AndroidManifest.xml" @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + diff --git "a/Variant \342\200\224 Esplora/app/src/main/assets/bip39-english.txt" "b/Variant \342\200\224 Esplora/app/src/main/assets/bip39-english.txt" new file mode 100644 index 0000000..f78ccaf --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/assets/bip39-english.txt" @@ -0,0 +1,2048 @@ +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 \ No newline at end of file diff --git "a/Variant \342\200\224 Esplora/app/src/main/ic_launcher_bdk-playstore.png" "b/Variant \342\200\224 Esplora/app/src/main/ic_launcher_bdk-playstore.png" new file mode 100644 index 0000000..b834711 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/ic_launcher_bdk-playstore.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/data/TxDetails.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/data/TxDetails.kt" new file mode 100644 index 0000000..a3ab433 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/data/TxDetails.kt" @@ -0,0 +1,27 @@ +/* + * Copyright 2021-2025 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 org.bitcoindevkit.FeeRate +import org.bitcoindevkit.Transaction + +data class TxDetails( + val transaction: Transaction, + val txid: String, + val sent: ULong, + val received: ULong, + val fee: ULong, + val feeRate: FeeRate?, + val pending: Boolean, + val confirmationBlock: ConfirmationBlock?, + val confirmationTimestamp: Timestamp?, +) + +@JvmInline +value class Timestamp(val timestamp: ULong) + +@JvmInline +value class ConfirmationBlock(val height: UInt) diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/data/UserPreferencesSerializer.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/data/UserPreferencesSerializer.kt" new file mode 100644 index 0000000..2394fd6 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/data/UserPreferencesSerializer.kt" @@ -0,0 +1,28 @@ +/* + * Copyright 2021-2025 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 androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream + +object UserPreferencesSerializer : Serializer { + override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): UserPreferences { + try { + return UserPreferences.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo(t: UserPreferences, output: OutputStream) { + t.writeTo(output) + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/data/WalletConfigs.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/data/WalletConfigs.kt" new file mode 100644 index 0000000..d04d14e --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/data/WalletConfigs.kt" @@ -0,0 +1,24 @@ +/* + * Copyright 2021-2025 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 org.bitcoindevkit.Descriptor +import org.bitcoindevkit.Network + +data class NewWalletConfig( + val name: String, + val network: Network, + val scriptType: ActiveWalletScriptType, +) + +data class RecoverWalletConfig( + val name: String, + val network: Network, + val scriptType: ActiveWalletScriptType?, + val recoveryPhrase: String?, + val descriptor: Descriptor, + val changeDescriptor: Descriptor, +) diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt" new file mode 100644 index 0000000..520f80f --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt" @@ -0,0 +1,48 @@ +/* + * Copyright 2021-2025 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.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 + + fun endpoint(): String +} + +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) + } + + override fun endpoint(): String { + return url + } +} diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt rename to "Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt" diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Constants.kt "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Constants.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Constants.kt rename to "Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Constants.kt" diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/CurrencyUnit.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/CurrencyUnit.kt" new file mode 100644 index 0000000..8e82b91 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/CurrencyUnit.kt" @@ -0,0 +1,11 @@ +/* + * Copyright 2021-2025 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 + +enum class CurrencyUnit { + Bitcoin, + Satoshi, +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/DwLogger.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/DwLogger.kt" new file mode 100644 index 0000000..31ef49a --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/DwLogger.kt" @@ -0,0 +1,51 @@ +/* + * Copyright 2021-2025 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 java.time.Instant +import java.time.ZoneId +import java.time.temporal.ChronoUnit + +object DwLogger { + private const val MAX_LOGS = 5000 + private val logEntries = ArrayDeque(MAX_LOGS) + private val lock = Any() + + fun log(tag: LogLevel, message: String) { + synchronized(lock) { + if (logEntries.size >= MAX_LOGS) { + logEntries.removeLast() + } + val millis = System.currentTimeMillis() + val dateTime = Instant.ofEpochMilli(millis) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime() + .truncatedTo(ChronoUnit.SECONDS) + + logEntries.addFirst("$dateTime $tag $message") + } + } + + fun getLogs(): List { + synchronized(lock) { + return logEntries.toList() + } + } + + enum class LogLevel { + INFO, + WARN, + ERROR; + + override fun toString(): String { + return when (this) { + INFO -> "[INFO] " + WARN -> "[WARN] " + ERROR -> "[ERROR]" + } + } + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/UserPreferencesRepository.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/UserPreferencesRepository.kt" new file mode 100644 index 0000000..7d57a5c --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/UserPreferencesRepository.kt" @@ -0,0 +1,52 @@ +/* + * Copyright 2021-2025 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 androidx.datastore.core.DataStore +import kotlinx.coroutines.flow.first +import org.bitcoindevkit.devkitwallet.data.SingleWallet +import org.bitcoindevkit.devkitwallet.data.UserPreferences + +class UserPreferencesRepository( + private val userPreferencesStore: DataStore, +) { + suspend fun fetchIntroDone(): Boolean { + return userPreferencesStore.data.first().introDone + } + + suspend fun setIntroDone() { + userPreferencesStore.updateData { currentPreferences -> + currentPreferences.toBuilder().setIntroDone(true).build() + } + } + + suspend fun fetchActiveWallets(): List { + return userPreferencesStore.data.first().walletsList + } + + suspend fun updateActiveWallets(singleWallet: SingleWallet) { + userPreferencesStore.updateData { currentPreferences -> + currentPreferences.toBuilder().addWallets(singleWallet).build() + } + } + + suspend fun setFullScanCompleted(walletId: String) { + userPreferencesStore.updateData { currentPreferences -> + val updatedWalletsList = currentPreferences.walletsList.map { wallet -> + if (wallet.id == walletId) { + wallet.toBuilder().setFullScanCompleted(true).build() + } else { + wallet + } + } + currentPreferences + .toBuilder() + .clearWallets() + .addAllWallets(updatedWalletsList) + .build() + } + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt" new file mode 100644 index 0000000..1ec59bd --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt" @@ -0,0 +1,393 @@ +/* + * Copyright 2021-2025 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 android.util.Log +import kotlinx.coroutines.runBlocking +import org.bitcoindevkit.Address +import org.bitcoindevkit.AddressInfo +import org.bitcoindevkit.Amount +import org.bitcoindevkit.CanonicalTx +import org.bitcoindevkit.ChainPosition +import org.bitcoindevkit.Connection +import org.bitcoindevkit.Descriptor +import org.bitcoindevkit.DescriptorSecretKey +import org.bitcoindevkit.FeeRate +import org.bitcoindevkit.KeychainKind +import org.bitcoindevkit.Mnemonic +import org.bitcoindevkit.Network +import org.bitcoindevkit.Psbt +import org.bitcoindevkit.Script +import org.bitcoindevkit.TxBuilder +import org.bitcoindevkit.Update +import org.bitcoindevkit.WordCount +import org.bitcoindevkit.devkitwallet.data.ActiveWalletScriptType +import org.bitcoindevkit.devkitwallet.data.ConfirmationBlock +import org.bitcoindevkit.devkitwallet.data.NewWalletConfig +import org.bitcoindevkit.devkitwallet.data.RecoverWalletConfig +import org.bitcoindevkit.devkitwallet.data.SingleWallet +import org.bitcoindevkit.devkitwallet.data.Timestamp +import org.bitcoindevkit.devkitwallet.data.TxDetails +import org.bitcoindevkit.devkitwallet.domain.utils.intoDomain +import org.bitcoindevkit.devkitwallet.domain.utils.intoProto +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.Recipient +import java.util.UUID +import org.bitcoindevkit.Wallet as BdkWallet + +private const val TAG = "Wallet" + +class Wallet private constructor( + private val wallet: BdkWallet, + private val walletSecrets: WalletSecrets, + private val connection: Connection, + private var fullScanCompleted: Boolean, + private val walletId: String, + private val userPreferencesRepository: UserPreferencesRepository, + blockchainClientsConfig: BlockchainClientsConfig, +) { + private var currentBlockchainClient: BlockchainClient? = blockchainClientsConfig.getClient() + + fun getWalletSecrets(): WalletSecrets { + return walletSecrets + } + + fun createTransaction(recipientList: List, feeRate: FeeRate): Psbt { + // technique 1 for adding a list of recipients to the TxBuilder + // var txBuilder = TxBuilder() + // for (recipient in recipientList) { + // txBuilder = txBuilder.addRecipient(address = recipient.first, amount = recipient.second) + // } + // txBuilder = txBuilder.feeRate(satPerVbyte = fee_rate) + + // technique 2 for adding a list of recipients to the TxBuilder + var txBuilder = + recipientList.fold(TxBuilder()) { builder, recipient -> + // val address = Address(recipient.address) + val scriptPubKey: Script = Address(recipient.address, Network.TESTNET).scriptPubkey() + builder.addRecipient(scriptPubKey, Amount.fromSat(recipient.amount)) + } + // if (!opReturnMsg.isNullOrEmpty()) { + // txBuilder = txBuilder.addData(opReturnMsg.toByteArray(charset = Charsets.UTF_8).asUByteArray().toList()) + // } + return txBuilder.feeRate(feeRate).finish(wallet) + } + + // @OptIn(ExperimentalUnsignedTypes::class) + // fun createSendAllTransaction( + // recipient: String, + // feeRate: Float, + // enableRBF: Boolean, + // opReturnMsg: String? + // ): PartiallySignedTransaction { + // val scriptPubkey: Script = Address(recipient).scriptPubkey() + // var txBuilder = TxBuilder() + // .drainWallet() + // .drainTo(scriptPubkey) + // .feeRate(satPerVbyte = feeRate) + // + // if (enableRBF) { + // txBuilder = txBuilder.enableRbf() + // } + // if (!opReturnMsg.isNullOrEmpty()) { + // txBuilder = txBuilder.addData(opReturnMsg.toByteArray(charset = Charsets.UTF_8).asUByteArray().toList()) + // } + // return txBuilder.finish(wallet).psbt + // } + + // fun createBumpFeeTransaction(txid: String, feeRate: Float): PartiallySignedTransaction { + // return BumpFeeTxBuilder(txid = txid, newFeeRate = feeRate) + // .enableRbf() + // .finish(wallet = wallet) + // } + + fun sign(psbt: Psbt): Boolean { + return wallet.sign(psbt) + } + + fun broadcast(signedPsbt: Psbt): String { + currentBlockchainClient?.broadcast(signedPsbt.extractTx()) ?: throw IllegalStateException( + "Blockchain client not initialized" + ) + return signedPsbt.extractTx().computeTxid() + } + + private fun getAllTransactions(): List = wallet.transactions() + + fun getAllTxDetails(): List { + val transactions = getAllTransactions() + return transactions.map { tx -> + val txid = tx.transaction.computeTxid() + val (sent, received) = wallet.sentAndReceived(tx.transaction) + var feeRate: FeeRate? = null + var fee: Amount? = null + // TODO: I don't know why we're getting negative fees here, but it looks like a bug + try { + fee = wallet.calculateFee(tx.transaction) + } catch (e: Exception) { + Log.e(TAG, "Error calculating fee rate for tx $txid: $e") + } + try { + feeRate = wallet.calculateFeeRate(tx.transaction) + } catch (e: Exception) { + Log.e(TAG, "Error calculating fee for tx $txid: $e") + } + + val (confirmationBlock, confirmationTimestamp, pending) = + when (val position = tx.chainPosition) { + is ChainPosition.Unconfirmed -> Triple(null, null, true) + is ChainPosition.Confirmed -> + Triple( + ConfirmationBlock(position.confirmationBlockTime.blockId.height), + Timestamp(position.confirmationBlockTime.confirmationTime), + false + ) + } + TxDetails( + tx.transaction, + txid, + sent.toSat(), + received.toSat(), + fee?.toSat() ?: 0uL, + feeRate, + pending, + confirmationBlock, + confirmationTimestamp + ) + } + } + + // fun getTransaction(txid: String): TransactionDetails? { + // val allTransactions = getAllTransactions() + // allTransactions.forEach { + // if (it.txid == txid) { + // return it + // } + // } + // return null + // } + + private fun fullScan() { + val fullScanRequest = wallet.startFullScan().build() + val update: Update = + currentBlockchainClient?.fullScan( + fullScanRequest = fullScanRequest, + stopGap = 20u, + ) ?: throw IllegalStateException("Blockchain client not initialized") + wallet.applyUpdate(update) + wallet.persist(connection) + } + + fun sync() { + if (!fullScanCompleted) { + Log.i(TAG, "Full scan required") + fullScan() + runBlocking { + userPreferencesRepository.setFullScanCompleted(walletId) + fullScanCompleted = true + } + } else { + Log.i(TAG, "Just a normal sync!") + val syncRequest = wallet.startSyncWithRevealedSpks().build() + val update = + currentBlockchainClient?.sync( + syncRequest = syncRequest, + ) ?: throw IllegalStateException("Blockchain client not initialized") + wallet.applyUpdate(update) + wallet.persist(connection) + } + } + + fun getBalance(): ULong = wallet.balance().total.toSat() + + fun getNewAddress(): AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL) + + fun getClientEndpoint(): String = currentBlockchainClient?.endpoint() ?: "No active client" + + companion object { + fun createWallet( + newWalletConfig: NewWalletConfig, + internalAppFilesPath: String, + userPreferencesRepository: UserPreferencesRepository, + ): Wallet { + val mnemonic = Mnemonic(WordCount.WORDS12) + val bip32ExtendedRootKey = DescriptorSecretKey(newWalletConfig.network, mnemonic, null) + val descriptor: Descriptor = createScriptAppropriateDescriptor( + newWalletConfig.scriptType, + bip32ExtendedRootKey, + newWalletConfig.network, + KeychainKind.EXTERNAL + ) + val changeDescriptor: Descriptor = createScriptAppropriateDescriptor( + newWalletConfig.scriptType, + bip32ExtendedRootKey, + newWalletConfig.network, + KeychainKind.INTERNAL + ) + val walletId = UUID.randomUUID().toString() + val connection = Connection("$internalAppFilesPath/wallet-${walletId.take(8)}.sqlite3") + + // Create SingleWallet object for saving to datastore + val newWalletForDatastore: SingleWallet = SingleWallet + .newBuilder() + .setId(walletId) + .setName(newWalletConfig.name) + .setNetwork(newWalletConfig.network.intoProto()) + .setScriptType(newWalletConfig.scriptType) + .setDescriptor(descriptor.toStringWithSecret()) + .setChangeDescriptor(changeDescriptor.toStringWithSecret()) + .setRecoveryPhrase(mnemonic.toString()) + .build() + + // TODO: launch this correctly, not on the main thread + // Save the new wallet to the datastore + runBlocking { userPreferencesRepository.updateActiveWallets(newWalletForDatastore) } + + val bdkWallet = + BdkWallet( + descriptor = descriptor, + changeDescriptor = changeDescriptor, + network = newWalletConfig.network, + connection = connection, + ) + + val walletSecrets = WalletSecrets(descriptor, changeDescriptor, mnemonic.toString()) + + return Wallet( + wallet = bdkWallet, + walletSecrets = walletSecrets, + connection = connection, + fullScanCompleted = false, + walletId = walletId, + userPreferencesRepository = userPreferencesRepository, + blockchainClientsConfig = BlockchainClientsConfig.createDefaultConfig(newWalletConfig.network) + ) + } + + fun loadActiveWallet( + activeWallet: SingleWallet, + internalAppFilesPath: String, + userPreferencesRepository: UserPreferencesRepository, + ): Wallet { + val descriptor = Descriptor(activeWallet.descriptor, activeWallet.network.intoDomain()) + val changeDescriptor = Descriptor(activeWallet.changeDescriptor, activeWallet.network.intoDomain()) + val connection = Connection("$internalAppFilesPath/wallet-${activeWallet.id.take(8)}.sqlite3") + val bdkWallet = BdkWallet.load( + descriptor = descriptor, + changeDescriptor = changeDescriptor, + connection = connection + ) + + val walletSecrets = WalletSecrets(descriptor, changeDescriptor, activeWallet.recoveryPhrase) + return Wallet( + wallet = bdkWallet, + walletSecrets = walletSecrets, + connection = connection, + fullScanCompleted = activeWallet.fullScanCompleted, + walletId = activeWallet.id, + userPreferencesRepository = userPreferencesRepository, + blockchainClientsConfig = BlockchainClientsConfig.createDefaultConfig(activeWallet.network.intoDomain()) + ) + } + + fun recoverWallet( + recoverWalletConfig: RecoverWalletConfig, + internalAppFilesPath: String, + userPreferencesRepository: UserPreferencesRepository, + ): Wallet { + Log.i(TAG, "Recovering wallet with config: $recoverWalletConfig") + var descriptor: Descriptor? = null + var changeDescriptor: Descriptor? = null + var mnemonicString: String = "" + + // If there is a recovery phrase, we use it to recover the wallet + if (recoverWalletConfig.recoveryPhrase != null && recoverWalletConfig.scriptType != null) { + val mnemonic: Mnemonic = Mnemonic.fromString(recoverWalletConfig.recoveryPhrase) + mnemonicString = mnemonic.toString() + val bip32ExtendedRootKey = DescriptorSecretKey(recoverWalletConfig.network, mnemonic, null) + descriptor = createScriptAppropriateDescriptor( + recoverWalletConfig.scriptType, + bip32ExtendedRootKey, + recoverWalletConfig.network, + KeychainKind.EXTERNAL + ) + changeDescriptor = createScriptAppropriateDescriptor( + recoverWalletConfig.scriptType, + bip32ExtendedRootKey, + recoverWalletConfig.network, + KeychainKind.INTERNAL + ) + } else { + descriptor = recoverWalletConfig.descriptor + changeDescriptor = recoverWalletConfig.changeDescriptor + } + val walletId = UUID.randomUUID().toString() + val connection = Connection("$internalAppFilesPath/wallet-${walletId.take(8)}.sqlite3") + + // Create SingleWallet object for saving to datastore + val newWalletForDatastore: SingleWallet = SingleWallet + .newBuilder() + .setId(walletId) + .setName(recoverWalletConfig.name) + .setNetwork(recoverWalletConfig.network.intoProto()) + .setScriptType(recoverWalletConfig.scriptType ?: ActiveWalletScriptType.UNKNOWN) + .setDescriptor(descriptor.toStringWithSecret()) + .setChangeDescriptor(changeDescriptor.toStringWithSecret()) + .setRecoveryPhrase(mnemonicString) + .build() + + // TODO: launch this correctly, not on the main thread + // Save the new wallet to the datastore + runBlocking { userPreferencesRepository.updateActiveWallets(newWalletForDatastore) } + + val bdkWallet = BdkWallet( + descriptor = descriptor, + changeDescriptor = changeDescriptor, + connection = connection, + network = recoverWalletConfig.network, + ) + + val walletSecrets = WalletSecrets(descriptor, changeDescriptor, mnemonicString) + return Wallet( + wallet = bdkWallet, + walletSecrets = walletSecrets, + connection = connection, + fullScanCompleted = false, + walletId = walletId, + userPreferencesRepository = userPreferencesRepository, + blockchainClientsConfig = BlockchainClientsConfig.createDefaultConfig(recoverWalletConfig.network) + ) + } + } +} + +fun createScriptAppropriateDescriptor( + scriptType: ActiveWalletScriptType, + bip32ExtendedRootKey: DescriptorSecretKey, + network: Network, + keychain: KeychainKind, +): Descriptor { + return if (keychain == KeychainKind.EXTERNAL) { + when (scriptType) { + ActiveWalletScriptType.P2WPKH -> Descriptor.newBip84(bip32ExtendedRootKey, KeychainKind.EXTERNAL, network) + ActiveWalletScriptType.P2TR -> Descriptor.newBip86(bip32ExtendedRootKey, KeychainKind.EXTERNAL, network) + ActiveWalletScriptType.UNKNOWN -> TODO() + ActiveWalletScriptType.UNRECOGNIZED -> TODO() + } + } else { + when (scriptType) { + ActiveWalletScriptType.P2WPKH -> Descriptor.newBip84(bip32ExtendedRootKey, KeychainKind.INTERNAL, network) + ActiveWalletScriptType.P2TR -> Descriptor.newBip86(bip32ExtendedRootKey, KeychainKind.INTERNAL, network) + ActiveWalletScriptType.UNKNOWN -> TODO() + ActiveWalletScriptType.UNRECOGNIZED -> TODO() + } + } +} + +data class WalletSecrets( + val descriptor: Descriptor, + val changeDescriptor: Descriptor, + val recoveryPhrase: String, +) diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/FormatInBtc.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/FormatInBtc.kt" new file mode 100644 index 0000000..b6f247a --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/FormatInBtc.kt" @@ -0,0 +1,18 @@ +/* + * Copyright 2021-2025 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.utils + +import java.text.DecimalFormat + +fun ULong?.formatInBtc(): String { + val balanceInSats = + if (this == 0UL || this == null) { + 0F + } else { + this.toDouble().div(100_000_000) + } + return DecimalFormat("0.00000000").format(balanceInSats) +} diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/ProtobufExtensions.kt "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/ProtobufExtensions.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/ProtobufExtensions.kt rename to "Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/ProtobufExtensions.kt" diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/Timestamps.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/Timestamps.kt" new file mode 100644 index 0000000..d132ffb --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/Timestamps.kt" @@ -0,0 +1,17 @@ +/* + * Copyright 2021-2025 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.utils + +import android.text.format.DateFormat +import java.util.Calendar +import java.util.Locale + +// extension function on the ULong timestamp provided in the Transaction.Confirmed type +fun ULong.timestampToString(): String { + val calendar = Calendar.getInstance(Locale.ENGLISH) + calendar.timeInMillis = (this * 1000u).toLong() + return DateFormat.format("MMMM d yyyy HH:mm", calendar).toString() +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt" new file mode 100644 index 0000000..8afb7b0 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt" @@ -0,0 +1,118 @@ +/* + * Copyright 2021-2025 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.presentation + +import android.content.Context +import android.os.Bundle +import android.util.Log +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.datastore.core.DataStore +import androidx.datastore.dataStore +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import org.bitcoindevkit.devkitwallet.data.NewWalletConfig +import org.bitcoindevkit.devkitwallet.data.RecoverWalletConfig +import org.bitcoindevkit.devkitwallet.data.SingleWallet +import org.bitcoindevkit.devkitwallet.data.UserPreferences +import org.bitcoindevkit.devkitwallet.data.UserPreferencesSerializer +import org.bitcoindevkit.devkitwallet.domain.DwLogger +import org.bitcoindevkit.devkitwallet.domain.DwLogger.LogLevel.INFO +import org.bitcoindevkit.devkitwallet.domain.UserPreferencesRepository +import org.bitcoindevkit.devkitwallet.domain.Wallet +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.CreateWalletNavigation +import org.bitcoindevkit.devkitwallet.presentation.navigation.HomeNavigation +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitTheme +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.intro.OnboardingScreen + +private const val TAG = "DevkitWalletActivity" +private val Context.userPreferencesStore: DataStore by dataStore( + fileName = "user_preferences.pb", + serializer = UserPreferencesSerializer +) + +class DevkitWalletActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + + super.onCreate(savedInstanceState) + + // Initialize Devkit Wallet Logger (used in the LogsScreen) + DwLogger.log(INFO, "Devkit Wallet app started") + + val userPreferencesRepository = UserPreferencesRepository(userPreferencesStore) + val onBuildWalletButtonClicked: (WalletCreateType) -> Unit = { walletCreateType -> + try { + val activeWallet = when (walletCreateType) { + is WalletCreateType.FROMSCRATCH -> Wallet.createWallet( + newWalletConfig = walletCreateType.newWalletConfig, + internalAppFilesPath = filesDir.absolutePath, + userPreferencesRepository = userPreferencesRepository + ) + is WalletCreateType.LOADEXISTING -> Wallet.loadActiveWallet( + activeWallet = walletCreateType.activeWallet, + internalAppFilesPath = filesDir.absolutePath, + userPreferencesRepository = userPreferencesRepository, + ) + is WalletCreateType.RECOVER -> Wallet.recoverWallet( + recoverWalletConfig = walletCreateType.recoverWalletConfig, + internalAppFilesPath = filesDir.absolutePath, + userPreferencesRepository = userPreferencesRepository, + ) + } + setContent { + DevkitTheme { + HomeNavigation(activeWallet) + } + } + } catch (e: Throwable) { + Log.i(TAG, "Could not build wallet: $e") + } + } + + lifecycleScope.launch { + val activeWallets = + async { + userPreferencesRepository.fetchActiveWallets() + }.await() + + val onboardingDone = + async { + userPreferencesRepository.fetchIntroDone() + }.await() + + val onFinishOnboarding: () -> Unit = { + lifecycleScope.launch { userPreferencesRepository.setIntroDone() } + setContent { + DevkitTheme { + CreateWalletNavigation(onBuildWalletButtonClicked, activeWallets) + } + } + } + + setContent { + if (!onboardingDone) { + DwLogger.log(INFO, "First time opening the app, triggering onboarding screen") + OnboardingScreen(onFinishOnboarding) + } else { + DevkitTheme { + CreateWalletNavigation(onBuildWalletButtonClicked, activeWallets) + } + } + } + } + } +} + +sealed class WalletCreateType { + data class FROMSCRATCH(val newWalletConfig: NewWalletConfig) : WalletCreateType() + + data class LOADEXISTING(val activeWallet: SingleWallet) : WalletCreateType() + + data class RECOVER(val recoverWalletConfig: RecoverWalletConfig) : WalletCreateType() +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/CreateWalletNavigation.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/CreateWalletNavigation.kt" new file mode 100644 index 0000000..6cb61ff --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/CreateWalletNavigation.kt" @@ -0,0 +1,133 @@ +/* + * Copyright 2021-2025 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.presentation.navigation + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import org.bitcoindevkit.devkitwallet.data.SingleWallet +import org.bitcoindevkit.devkitwallet.presentation.WalletCreateType +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 + +@Composable +fun CreateWalletNavigation(onBuildWalletButtonClicked: (WalletCreateType) -> Unit, activeWallets: List) { + val navController: NavHostController = rememberNavController() + val animationDuration = 400 + + NavHost( + navController = navController, + startDestination = WalletChoiceScreen, + ) { + composable( + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + }, + ) { WalletChoiceScreen(navController = navController) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + } + ) { + ActiveWalletsScreen( + activeWallets = activeWallets, + navController = navController, + onBuildWalletButtonClicked + ) + } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + } + ) { CreateNewWalletScreen(navController = navController, onBuildWalletButtonClicked) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + } + ) { RecoverWalletScreen(onAction = onBuildWalletButtonClicked, navController = navController) } + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/Destinations.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/Destinations.kt" new file mode 100644 index 0000000..0eeaaf2 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/Destinations.kt" @@ -0,0 +1,56 @@ +/* + * Copyright 2021-2025 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.presentation.navigation + +import kotlinx.serialization.Serializable + +// Create wallet navigation destinations +@Serializable +object WalletChoiceScreen + +@Serializable +object ActiveWalletsScreen + +@Serializable +object CreateNewWalletScreen + +@Serializable +object WalletRecoveryScreen + +// Home navigation destinations +@Serializable +object WalletScreen + +@Serializable +object AboutScreen + +@Serializable +object RecoveryPhraseScreen + +@Serializable +object BlockchainClientScreen + +@Serializable +object LogsScreen + +// Wallet navigation destinations +@Serializable +object HomeScreen + +@Serializable +object ReceiveScreen + +@Serializable +object SendScreen + +@Serializable +object TransactionHistoryScreen + +@Serializable +data class TransactionScreen(val txid: String) + +@Serializable +data class RbfScreen(val txid: String) diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/HomeNavigation.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/HomeNavigation.kt" new file mode 100644 index 0000000..19b7cf6 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/HomeNavigation.kt" @@ -0,0 +1,162 @@ +/* + * Copyright 2021-2025 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.presentation.navigation + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import org.bitcoindevkit.devkitwallet.domain.Wallet +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.WalletRoot +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.viewmodels.WalletViewModel + +private const val ANIMATION_DURATION: Int = 400 + +@Composable +fun HomeNavigation(activeWallet: Wallet) { + val navController: NavHostController = rememberNavController() + val walletViewModel = WalletViewModel(activeWallet) + + NavHost( + navController = navController, + startDestination = WalletScreen, + ) { + composable( + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + ) { WalletRoot(navController = navController, activeWallet = activeWallet, walletViewModel = walletViewModel) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + ) { AboutScreen(navController = navController) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + } + ) { RecoveryDataScreen(activeWallet.getWalletSecrets(), navController = navController) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + } + ) { + BlockchainClientScreen( + state = walletViewModel.state, + navController = navController + ) + } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + } + ) { LogsScreen(navController = navController) } + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/WalletNavigation.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/WalletNavigation.kt" new file mode 100644 index 0000000..fa89c1d --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/WalletNavigation.kt" @@ -0,0 +1,202 @@ +/* + * Copyright 2021-2025 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.presentation.navigation + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.material3.DrawerState +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute +import org.bitcoindevkit.devkitwallet.domain.Wallet +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 +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.wallet.TransactionHistoryScreen +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.wallet.TransactionScreen +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.wallet.WalletHomeScreen +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.AddressViewModel +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.SendViewModel +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.WalletViewModel + +private const val ANIMATION_DURATION: Int = 400 + +@Composable +fun WalletNavigation(drawerState: DrawerState, activeWallet: Wallet, walletViewModel: WalletViewModel) { + val navController: NavHostController = rememberNavController() + val addressViewModel = AddressViewModel(activeWallet) + val sendViewModel = SendViewModel(activeWallet) + + NavHost( + navController = navController, + startDestination = HomeScreen, + ) { + composable( + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + ) { WalletHomeScreen(navController, drawerState, walletViewModel) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + } + ) { + ReceiveScreen( + state = addressViewModel.state, + onAction = addressViewModel::onAction, + navController + ) + } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + } + ) { SendScreen(navController, sendViewModel) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + } + ) { + val args = it.toRoute() + RBFScreen(args.txid, navController) + } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + } + ) { TransactionHistoryScreen(navController, activeWallet) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + } + ) { + val args = it.toRoute() + TransactionScreen(args.txid, navController) + } + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/DevkitWalletColors.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/DevkitWalletColors.kt" new file mode 100644 index 0000000..2f91ab0 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/DevkitWalletColors.kt" @@ -0,0 +1,21 @@ +/* + * Copyright 2021-2025 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.presentation.theme + +import androidx.compose.ui.graphics.Color + +@Suppress("ktlint:standard:comment-spacing") +object DevkitWalletColors { + val primaryDark: Color = Color(0xFF203B46) // App bar + val primary: Color = Color(0xFF264653) // Background + val primaryLight: Color = Color(0xFF335F70) // Behind balance primary light + val white: Color = Color(0xffffffff) // Most text + val secondary: Color = Color(0xFF2A9D8F) // Buttons + val accent1: Color = Color(0xFFE9C46A) // Receive button + val accent2: Color = Color(0xFFE76F51) // Send button +} + +internal val TestPink = Color(0xffff1493) diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Fonts.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Fonts.kt" new file mode 100644 index 0000000..f143e82 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Fonts.kt" @@ -0,0 +1,44 @@ +/* + * Copyright 2021-2025 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.presentation.theme + +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import org.bitcoindevkit.devkitwallet.R + +val quattroRegular = FontFamily( + Font( + resId = R.font.ia_writer_quattro_regular, + weight = FontWeight.Normal, + style = FontStyle.Normal + ) +) + +val quattroBold = FontFamily( + Font( + resId = R.font.ia_writer_quattro_bold, + weight = FontWeight.Bold, + style = FontStyle.Normal + ) +) + +val monoRegular = FontFamily( + Font( + resId = R.font.ia_writer_mono_regular, + weight = FontWeight.Normal, + style = FontStyle.Normal + ) +) + +val monoBold = FontFamily( + Font( + resId = R.font.ia_writer_mono_bold, + weight = FontWeight.Bold, + style = FontStyle.Normal + ) +) diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Theme.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Theme.kt" new file mode 100644 index 0000000..5743fb8 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Theme.kt" @@ -0,0 +1,22 @@ +/* + * Copyright 2021-2025 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.presentation.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable + +@Composable +fun DevkitTheme(content: @Composable () -> Unit) { + MaterialTheme( + // colorScheme = devkitColors, + // shapes = devkitShapes, + typography = devkitTypography, + content = content + ) +} + +// NOTES ON THE UI +// - The standard padding is 32dp for start/end, 16dp for top/bottom diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Type.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Type.kt" new file mode 100644 index 0000000..2d13fb1 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Type.kt" @@ -0,0 +1,32 @@ +/* + * Copyright 2021-2025 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.presentation.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +internal val devkitTypography = + Typography( + labelLarge = + TextStyle( + fontFamily = quattroRegular, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 28.sp + ), + ) + +// These are the default text styles used by Material3 components: +// Buttons: labelLarge + +internal val standardText = TextStyle( + color = DevkitWalletColors.white, + fontFamily = quattroRegular, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, +) diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/CustomSnackbar.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/CustomSnackbar.kt" new file mode 100644 index 0000000..178cbae --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/CustomSnackbar.kt" @@ -0,0 +1,41 @@ +package org.bitcoindevkit.devkitwallet.presentation.ui.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarData +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.composables.icons.lucide.Lucide +import com.composables.icons.lucide.X +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular + +@Composable +fun CustomSnackbar(data: SnackbarData) { + Snackbar( + modifier = Modifier.padding(12.dp), + action = { + IconButton( + onClick = { data.performAction() } + ) { + Icon( + imageVector = Lucide.X, + contentDescription = "Ok", + tint = DevkitWalletColors.white + ) + } + }, + containerColor = DevkitWalletColors.primaryLight, + ) { + Text( + text = data.visuals.message, + fontFamily = quattroRegular, + fontSize = 14.sp + ) + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/LoadingAnimation.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/LoadingAnimation.kt" new file mode 100644 index 0000000..992045d --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/LoadingAnimation.kt" @@ -0,0 +1,74 @@ +/* + * Copyright 2021-2025 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.presentation.ui.components + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors + +@Composable +fun LoadingAnimation( + circleColor: Color = DevkitWalletColors.accent1, + circleSize: Dp = 21.dp, + animationDelay: Int = 800, + initialAlpha: Float = 0.3f, +) { + val circles = listOf( + remember { Animatable(initialValue = initialAlpha) }, + remember { Animatable(initialValue = initialAlpha) }, + remember { Animatable(initialValue = initialAlpha) } + ) + + circles.forEachIndexed { index, animatable -> + LaunchedEffect(Unit) { + // Use coroutine delay to sync animations + delay(timeMillis = (animationDelay / circles.size).toLong() * index) + + animatable.animateTo( + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = animationDelay + ), + repeatMode = RepeatMode.Reverse + ) + ) + } + } + + // container for circles + Row { + circles.forEachIndexed { index, animatable -> + // gap between the circles + if (index != 0) Spacer(modifier = Modifier.width(width = 6.dp)) + + Box( + modifier = Modifier + .size(size = circleSize) + .clip(shape = CircleShape) + .background(circleColor.copy(alpha = animatable.value)) + ) + } + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/NeutralButton.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/NeutralButton.kt" new file mode 100644 index 0000000..74c374f --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/NeutralButton.kt" @@ -0,0 +1,42 @@ +/* + * Copyright 2021-2025 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.presentation.ui.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.unit.dp +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors + +@Composable +fun NeutralButton(text: String, enabled: Boolean = true, modifier: Modifier? = null, onClick: () -> Unit) { + Button( + onClick = onClick, + colors = + ButtonDefaults.buttonColors( + containerColor = DevkitWalletColors.secondary, + disabledContainerColor = DevkitWalletColors.secondary, + ), + shape = RoundedCornerShape(16.dp), + enabled = enabled, + modifier = modifier ?: Modifier + .height(80.dp) + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = text, + ) + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/RadioButtonWithLabel.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/RadioButtonWithLabel.kt" new file mode 100644 index 0000000..b1fb8af --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/RadioButtonWithLabel.kt" @@ -0,0 +1,53 @@ +package org.bitcoindevkit.devkitwallet.presentation.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular + +@Composable +fun RadioButtonWithLabel(label: String, isSelected: Boolean, onSelect: () -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .padding(0.dp) + .selectable( + selected = isSelected, + onClick = onSelect + ) + ) { + RadioButton( + selected = isSelected, + onClick = onSelect, + colors = RadioButtonDefaults.colors( + selectedColor = DevkitWalletColors.accent1, + unselectedColor = DevkitWalletColors.accent2 + ), + modifier = Modifier + .padding(start = 8.dp) + .size(40.dp) + ) + Text( + text = label, + color = DevkitWalletColors.white, + fontFamily = monoRegular, + fontSize = 12.sp, + modifier = Modifier + .clickable(onClick = onSelect) + .padding(0.dp) + ) + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/SecondaryScreensAppBar.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/SecondaryScreensAppBar.kt" new file mode 100644 index 0000000..cdea6a0 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/SecondaryScreensAppBar.kt" @@ -0,0 +1,47 @@ +/* + * Copyright 2021-2025 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.presentation.ui.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.sp +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SecondaryScreensAppBar(title: String, navigation: () -> Unit) { + TopAppBar( + title = { + Text( + text = title, + color = DevkitWalletColors.white, + fontSize = 18.sp, + fontFamily = quattroRegular + ) + }, + navigationIcon = { + IconButton(onClick = navigation) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = "Back", + tint = DevkitWalletColors.white + ) + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = DevkitWalletColors.primaryDark, + ) + ) +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/TransactionCards.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/TransactionCards.kt" new file mode 100644 index 0000000..292d695 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/TransactionCards.kt" @@ -0,0 +1,134 @@ +/* + * Copyright 2021-2025 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.presentation.ui.components + +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.data.TxDetails +import org.bitcoindevkit.devkitwallet.domain.utils.timestampToString +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.wallet.viewTransaction + +private const val TAG = "TransactionCards" + +@Composable +fun ConfirmedTransactionCard(details: TxDetails, navController: NavController) { + Row( + Modifier + .padding(horizontal = 8.dp, vertical = 6.dp) + .fillMaxWidth() + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ).clickable { viewTransaction(navController = navController, txid = details.txid) }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Absolute.SpaceBetween + ) { + Text( + confirmedTransactionsItem(details), + fontFamily = monoRegular, + fontSize = 12.sp, + lineHeight = 20.sp, + color = DevkitWalletColors.white, + modifier = Modifier.padding(16.dp) + ) + Box( + modifier = Modifier + .padding(top = 16.dp, end = 16.dp) + .size(size = 24.dp) + .clip(shape = CircleShape) + .background(DevkitWalletColors.secondary) + .align(Alignment.Top) + ) + } +} + +@Composable +fun PendingTransactionCard(details: TxDetails, navController: NavController) { + Row( + Modifier + .padding(horizontal = 8.dp, vertical = 6.dp) + .fillMaxWidth() + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ).border( + width = 2.dp, + color = DevkitWalletColors.accent1, + shape = RoundedCornerShape(16.dp) + ).clickable { + // viewTransaction( + // navController = navController, + // txid = details.txid + // ) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Absolute.SpaceBetween + ) { + Text( + pendingTransactionsItem(details), + fontFamily = monoRegular, + fontSize = 12.sp, + color = DevkitWalletColors.white, + modifier = Modifier.padding(16.dp) + ) + Box( + modifier = Modifier + .padding(top = 16.dp, end = 16.dp) + .size(size = 24.dp) + .clip(shape = CircleShape) + .background(DevkitWalletColors.accent1) + .align(Alignment.Top) + ) + } +} + +fun pendingTransactionsItem(txDetails: TxDetails): String { + return buildString { + Log.i(TAG, "Pending transaction list item: $txDetails") + + appendLine("Confirmation time: Pending") + appendLine("Received: ${txDetails.received}") + appendLine("Sent: ${txDetails.sent}") + appendLine("Total fee: ${txDetails.fee} sat") + appendLine("Fee rate: ${txDetails.feeRate?.toSatPerVbCeil() ?: 0} sat/vbyte") + append("Txid: ${txDetails.txid.take(n = 8)}...${txDetails.txid.takeLast(n = 8)}") + } +} + +fun confirmedTransactionsItem(txDetails: TxDetails): String { + return buildString { + Log.i(TAG, "Transaction list item: $txDetails") + + appendLine("Confirmation time: ${txDetails.confirmationTimestamp?.timestamp?.timestampToString()}") + appendLine("Received: ${txDetails.received} sat") + appendLine("Sent: ${txDetails.sent} sat") + appendLine("Total fee: ${txDetails.fee} sat") + appendLine("Fee rate: ${txDetails.feeRate?.toSatPerVbCeil() ?: 0} sat/vbyte") + appendLine("Block: ${txDetails.confirmationBlock?.height}") + append("Txid: ${txDetails.txid.take(n = 8)}...${txDetails.txid.takeLast(n = 8)}") + } +} diff --git a/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/WalletOptionsCard.kt "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/WalletOptionsCard.kt" similarity index 100% rename from app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/WalletOptionsCard.kt rename to "Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/WalletOptionsCard.kt" diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/WalletRoot.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/WalletRoot.kt" new file mode 100644 index 0000000..8347f8d --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/WalletRoot.kt" @@ -0,0 +1,190 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.Face +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.NavigationDrawerItemDefaults.colors +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.composables.icons.lucide.History +import com.composables.icons.lucide.Info +import com.composables.icons.lucide.Lucide +import com.composables.icons.lucide.SatelliteDish +import com.composables.icons.lucide.ScrollText +import org.bitcoindevkit.devkitwallet.BuildConfig +import org.bitcoindevkit.devkitwallet.R +import org.bitcoindevkit.devkitwallet.domain.Wallet +import org.bitcoindevkit.devkitwallet.presentation.navigation.AboutScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.BlockchainClientScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.LogsScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.RecoveryPhraseScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletNavigation +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.WalletViewModel + +@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) +@Composable +internal fun WalletRoot(navController: NavController, activeWallet: Wallet, walletViewModel: WalletViewModel) { + val drawerState = rememberDrawerState(DrawerValue.Closed) + + val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email, Icons.Default.Face) + val selectedItem = remember { mutableStateOf(items[0]) } + + val navigationItemColors = + colors( + selectedContainerColor = DevkitWalletColors.primary, + unselectedContainerColor = DevkitWalletColors.primary, + selectedTextColor = DevkitWalletColors.white, + unselectedTextColor = DevkitWalletColors.white + ) + + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet( + drawerContainerColor = DevkitWalletColors.primary + ) { + Column( + Modifier + .background(color = DevkitWalletColors.secondary) + .height(300.dp) + .fillMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_testnet_logo), + contentDescription = "Bitcoin testnet logo", + modifier = Modifier + .size(90.dp) + .padding(bottom = 16.dp) + ) + Text( + text = "Devkit Wallet", + color = DevkitWalletColors.white, + fontFamily = quattroRegular, + ) + Spacer(modifier = Modifier.padding(4.dp)) + Text( + text = "The sample wallet on Android for BDK.", + color = DevkitWalletColors.white, + fontFamily = quattroRegular, + fontSize = 12.sp, + fontStyle = androidx.compose.ui.text.font.FontStyle.Italic + ) + Spacer(modifier = Modifier.padding(16.dp)) + Text( + text = BuildConfig.VARIANT_NAME, + style = standardText + ) + } + Column( + Modifier + .fillMaxHeight() + .background(color = DevkitWalletColors.primary) + ) { + Spacer(modifier = Modifier.height(16.dp)) + NavigationDrawerItem( + icon = { Icon(Lucide.Info, contentDescription = "About", tint = DevkitWalletColors.white) }, + label = { DrawerItemLabel("About") }, + selected = items[0] == selectedItem.value, + onClick = { navController.navigate(AboutScreen) }, + colors = navigationItemColors, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + ) + NavigationDrawerItem( + icon = { + Icon( + Lucide.History, + contentDescription = "Wallet Recovery Data", + tint = DevkitWalletColors.white + ) + }, + label = { DrawerItemLabel("Wallet Recovery Data") }, + selected = items[1] == selectedItem.value, + onClick = { navController.navigate(RecoveryPhraseScreen) }, + colors = navigationItemColors, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + ) + NavigationDrawerItem( + icon = { + Icon( + Lucide.SatelliteDish, + contentDescription = "Esplora Client", + tint = DevkitWalletColors.white + ) + }, + label = { DrawerItemLabel("Esplora Client") }, + selected = items[2] == selectedItem.value, + onClick = { navController.navigate(BlockchainClientScreen) }, + colors = navigationItemColors, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + ) + NavigationDrawerItem( + icon = { + Icon( + Lucide.ScrollText, + contentDescription = "Logs", + tint = DevkitWalletColors.white + ) + }, + label = { DrawerItemLabel("Logs") }, + selected = items[3] == selectedItem.value, + onClick = { navController.navigate(LogsScreen) }, + colors = navigationItemColors, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + ) + } + } + }, + content = { + WalletNavigation( + drawerState = drawerState, + activeWallet = activeWallet, + walletViewModel = walletViewModel + ) + } + ) +} + +@Composable +fun DrawerItemLabel(text: String) { + Text( + text = text, + fontFamily = quattroRegular, + ) +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt" new file mode 100644 index 0000000..0c7369e --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt" @@ -0,0 +1,82 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.drawer + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import org.bitcoindevkit.devkitwallet.R +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +private val message: String = """ + This wallet is build for: + + 1. Developers interested in learning how to leverage the Bitcoin Development Kit on Android. + + 2. Any bitcoiner looking for a Signet/Testnet/Regtest wallet! +""".trimIndent() + +@Composable +internal fun AboutScreen(navController: NavController) { + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "About", + navigation = { navController.navigate(WalletScreen) } + ) + }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.padding(24.dp)) + Image( + painter = painterResource(id = R.drawable.bdk_logo), + contentDescription = "BDK logo", + Modifier.size(180.dp) + ) + Spacer(modifier = Modifier.padding(24.dp)) + Text( + text = message, + color = DevkitWalletColors.white, + textAlign = TextAlign.Start, + fontFamily = quattroRegular, + modifier = Modifier.padding(all = 8.dp) + ) + } + } +} + +@Preview(device = Devices.PIXEL_4, showBackground = true) +@Composable +internal fun PreviewAboutScreen() { + AboutScreen(rememberNavController()) +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt" new file mode 100644 index 0000000..ba01e18 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt" @@ -0,0 +1,129 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.drawer + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroBold +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenState + +@Composable +internal fun BlockchainClientScreen(state: WalletScreenState, navController: NavController) { + val focusManager = LocalFocusManager.current + // val isBlockChainCreated = Wallet.isBlockChainCreated() + val serverEndpoint: MutableState = remember { mutableStateOf("") } + val isChecked: MutableState = remember { mutableStateOf(false) } + // if (isBlockChainCreated) { + // electrumServer.value = Wallet.getElectrumURL() + // isChecked.value = Wallet.isElectrumServerDefault() + // } + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Esplora Client", + navigation = { navController.navigate(WalletScreen) } + ) + }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(all = 16.dp), + ) { + Text( + text = "Current Esplora client endpoint", + color = DevkitWalletColors.white, + fontSize = 18.sp, + fontFamily = quattroBold, + ) + Text( + text = state.esploraEndpoint, + style = standardText, + ) + // Row(verticalAlignment = Alignment.CenterVertically) { + // Text( + // text = "Use default electrum URL", + // color = DevkitWalletColors.white, + // fontSize = 14.sp, + // textAlign = TextAlign.Center, + // ) + // Switch( + // checked = isChecked.value, + // onCheckedChange = { + // isChecked.value = it + // if (it) { + // Wallet.setElectrumSettings(ElectrumSettings.DEFAULT) + // } else { + // Wallet.setElectrumSettings(ElectrumSettings.CUSTOM) + // } + // }, + // enabled = isBlockChainCreated + // ) + // } + + // OutlinedTextField( + // value = electrumServer.value, + // onValueChange = { electrumServer.value = it }, + // label = { + // Text( + // text = "Electrum Server", + // color = DevkitWalletColors.white, + // ) + // }, + // singleLine = true, + // textStyle = TextStyle(color = DevkitWalletColors.white), + // colors = TextFieldDefaults.outlinedTextFieldColors( + // focusedBorderColor = DevkitWalletColors.accent1, + // unfocusedBorderColor = DevkitWalletColors.white, + // cursorColor = DevkitWalletColors.accent1, + // ), + // keyboardActions = KeyboardActions(onDone = { + // focusManager.clearFocus() + // }), + // modifier = Modifier.fillMaxWidth(), + // enabled = isBlockChainCreated && !isChecked.value + // ) + + // Button( + // onClick = { + // Wallet.changeElectrumServer(electrumServer.value) + // focusManager.clearFocus() + // }, + // modifier = Modifier + // .align(alignment = Alignment.End) + // .padding(all = 8.dp), + // colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + // enabled = isBlockChainCreated && !isChecked.value + // ) { + // Text( + // text = "Save", + // color = DevkitWalletColors.white, + // fontSize = 12.sp, + // textAlign = TextAlign.Center, + // ) + // } + } + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt" new file mode 100644 index 0000000..f7acc9c --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt" @@ -0,0 +1,61 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.drawer + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.domain.DwLogger +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +@Composable +fun LogsScreen(navController: NavController) { + val logs: List = remember { DwLogger.getLogs() } + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Logs", + navigation = { navController.navigate(WalletScreen) } + ) + }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp) + ) { + items(logs) { logLine -> + Text( + text = logLine, + style = standardText, + maxLines = 1, + overflow = TextOverflow.Visible, + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + ) + } + } + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt" new file mode 100644 index 0000000..5a8024b --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt" @@ -0,0 +1,221 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.drawer + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.composables.icons.lucide.ClipboardCopy +import com.composables.icons.lucide.Lucide +import org.bitcoindevkit.devkitwallet.domain.WalletSecrets +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular +import org.bitcoindevkit.devkitwallet.presentation.ui.components.NeutralButton +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +private val MESSAGE: String = """ + The next screen will show your recovery phrase and descriptors. Make sure no one else is looking at your screen. +""".trimIndent() + +@Composable +internal fun RecoveryDataScreen(walletSecrets: WalletSecrets, navController: NavController) { + val (currentIndex, setCurrentIndex) = remember { mutableIntStateOf(0) } + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Your Wallet Recovery Data", + navigation = { navController.navigate(WalletScreen) } + ) + }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + Crossfade( + modifier = Modifier.padding(paddingValues), + targetState = currentIndex, + label = "", + animationSpec = tween( + durationMillis = 1000, + delayMillis = 200, + ) + ) { screen -> + when (screen) { + 0 -> WarningText(setCurrentIndex = setCurrentIndex) + 1 -> RecoveryPhrase(walletSecrets = walletSecrets) + } + } + } +} + +@Composable +fun WarningText(setCurrentIndex: (Int) -> Unit) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 32.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = MESSAGE, + color = DevkitWalletColors.white, + fontFamily = quattroRegular + ) + Spacer(modifier = Modifier.padding(16.dp)) + NeutralButton( + text = "See my recovery data", + enabled = true + ) { setCurrentIndex(1) } + } +} + +@Composable +fun RecoveryPhrase(walletSecrets: WalletSecrets) { + val context = LocalContext.current + Column( + modifier = Modifier + .fillMaxSize() + .padding(all = 32.dp) + ) { + Text( + text = "Write down your recovery phrase and keep it in a safe place.", + color = DevkitWalletColors.white, + fontFamily = quattroRegular + ) + Spacer(modifier = Modifier.padding(8.dp)) + Box { + SelectionContainer { + Text( + modifier = Modifier + .clickable { + simpleCopyClipboard( + walletSecrets.recoveryPhrase, + context + ) + }.background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ).padding(12.dp), + text = walletSecrets.recoveryPhrase, + fontFamily = monoRegular, + color = DevkitWalletColors.white + ) + } + Icon( + Lucide.ClipboardCopy, + tint = Color.White, + contentDescription = "Copy to clipboard", + modifier = Modifier + .padding(8.dp) + .size(20.dp) + .align(Alignment.BottomEnd) + ) + } + Spacer(modifier = Modifier.padding(16.dp)) + Text( + text = "These are your descriptors.", + color = DevkitWalletColors.white, + fontFamily = quattroRegular + ) + Spacer(modifier = Modifier.padding(8.dp)) + Box { + SelectionContainer { + Text( + modifier = Modifier + .clickable { + simpleCopyClipboard( + walletSecrets.descriptor.toStringWithSecret(), + context + ) + }.background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ).padding(12.dp), + text = walletSecrets.descriptor.toStringWithSecret(), + fontFamily = monoRegular, + color = DevkitWalletColors.white + ) + } + Icon( + Lucide.ClipboardCopy, + tint = Color.White, + contentDescription = "Copy to clipboard", + modifier = Modifier + .padding(8.dp) + .size(20.dp) + .align(Alignment.BottomEnd) + ) + } + Spacer(modifier = Modifier.padding(4.dp)) + Box { + SelectionContainer { + Text( + modifier = Modifier + .clickable { + simpleCopyClipboard( + walletSecrets.changeDescriptor.toStringWithSecret(), + context + ) + }.background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ).padding(12.dp), + text = walletSecrets.changeDescriptor.toStringWithSecret(), + fontFamily = monoRegular, + color = DevkitWalletColors.white + ) + } + Icon( + Lucide.ClipboardCopy, + tint = Color.White, + contentDescription = "Copy to clipboard", + modifier = Modifier + .padding(8.dp) + .size(20.dp) + .align(Alignment.BottomEnd) + ) + } + } +} + +fun simpleCopyClipboard(content: String, context: Context) { + val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip: ClipData = ClipData.newPlainText("", content) + clipboard.setPrimaryClip(clip) +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewRecoveryPhraseScreen() { +// RecoveryPhraseScreen() +// } diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/ActiveWalletsScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/ActiveWalletsScreen.kt" new file mode 100644 index 0000000..84f65b9 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/ActiveWalletsScreen.kt" @@ -0,0 +1,119 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.intro + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.data.SingleWallet +import org.bitcoindevkit.devkitwallet.domain.DwLogger +import org.bitcoindevkit.devkitwallet.domain.DwLogger.LogLevel.INFO +import org.bitcoindevkit.devkitwallet.presentation.WalletCreateType +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +private const val TAG = "ActiveWalletsScreen" + +@Composable +internal fun ActiveWalletsScreen( + activeWallets: List, + navController: NavController, + onBuildWalletButtonClicked: (WalletCreateType) -> Unit, +) { + Scaffold( + topBar = { + SecondaryScreensAppBar(title = "Choose a Wallet", navigation = { navController.navigateUp() }) + }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + Spacer(modifier = Modifier.height(12.dp)) + activeWallets.forEach { + ActiveWalletCard(wallet = it, onBuildWalletButtonClicked) + } + if (activeWallets.isEmpty()) { + Text( + text = "No active wallets.", + fontSize = 16.sp, + fontFamily = quattroRegular, + color = DevkitWalletColors.white, + modifier = Modifier.padding(16.dp).align(Alignment.CenterHorizontally) + ) + } + Spacer(modifier = Modifier.height(12.dp)) + } + } +} + +@Composable +fun ActiveWalletCard(wallet: SingleWallet, onBuildWalletButtonClicked: (WalletCreateType) -> Unit) { + Column( + Modifier + .fillMaxWidth() + // Padding outside the card + .padding(horizontal = 16.dp, vertical = 8.dp) + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ) + // Padding inside the card + .padding(horizontal = 4.dp, vertical = 8.dp) + .clickable { + DwLogger.log(INFO, "Activating existing wallet: ${wallet.name}") + onBuildWalletButtonClicked(WalletCreateType.LOADEXISTING(wallet)) + }, + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.Start + ) { + DataField("Name", wallet.name) + DataField("Network", wallet.network.toString()) + DataField("Script Type", wallet.scriptType.toString()) + } +} + +@Composable +fun DataField(name: String, value: String) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Absolute.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + ) { + Text( + text = name, + style = standardText, + lineHeight = 18.sp, + ) + Text( + text = value, + style = standardText, + lineHeight = 18.sp, + ) + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/CreateNewWallet.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/CreateNewWallet.kt" new file mode 100644 index 0000000..44ffc30 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/CreateNewWallet.kt" @@ -0,0 +1,157 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.intro + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.navigation.NavController +import org.bitcoindevkit.Network +import org.bitcoindevkit.devkitwallet.data.ActiveWalletScriptType +import org.bitcoindevkit.devkitwallet.data.NewWalletConfig +import org.bitcoindevkit.devkitwallet.domain.DwLogger +import org.bitcoindevkit.devkitwallet.domain.DwLogger.LogLevel.INFO +import org.bitcoindevkit.devkitwallet.presentation.WalletCreateType +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.ui.components.NeutralButton +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar +import org.bitcoindevkit.devkitwallet.presentation.ui.components.WalletOptionsCard + +@Composable +internal fun CreateNewWalletScreen( + navController: NavController, + onBuildWalletButtonClicked: (WalletCreateType) -> Unit, +) { + Scaffold( + topBar = { + SecondaryScreensAppBar(title = "Create a New Wallet", navigation = { navController.navigateUp() }) + }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + + ConstraintLayout( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .padding(vertical = 16.dp) + ) { + val (choices, button) = createRefs() + + val walletName: MutableState = remember { mutableStateOf("") } + val selectedNetwork: MutableState = remember { mutableStateOf(Network.SIGNET) } + val selectedScriptType: MutableState = + remember { mutableStateOf(ActiveWalletScriptType.P2TR) } + val scriptTypes = listOf(ActiveWalletScriptType.P2TR, ActiveWalletScriptType.P2WPKH) + + Column( + modifier = Modifier + .constrainAs(choices) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }.fillMaxSize() + .background(color = DevkitWalletColors.primary) + .padding(horizontal = 32.dp) + ) { + OutlinedTextField( + modifier = Modifier + .padding(bottom = 8.dp) + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + value = walletName.value, + onValueChange = { walletName.value = it }, + label = { + Text( + text = "Give your wallet a name", + style = standardText, + color = DevkitWalletColors.white, + ) + }, + singleLine = true, + textStyle = TextStyle(fontFamily = monoRegular, color = DevkitWalletColors.white), + colors = OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + + Spacer(modifier = Modifier.padding(12.dp)) + WalletOptionsCard(scriptTypes, selectedNetwork, selectedScriptType) + Spacer(modifier = Modifier.padding(16.dp)) + } + + Column( + modifier = Modifier + .constrainAs(button) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }.fillMaxWidth() + .padding(horizontal = 32.dp) + ) { + NeutralButton( + text = "Create Wallet", + enabled = true, + modifier = Modifier + .height(80.dp) + .fillMaxWidth() + .padding(vertical = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)), + onClick = { + val newWalletConfig = NewWalletConfig( + name = walletName.value, + network = selectedNetwork.value, + scriptType = selectedScriptType.value + ) + DwLogger.log(INFO, "Creating new wallet named ${newWalletConfig.name}") + onBuildWalletButtonClicked(WalletCreateType.FROMSCRATCH(newWalletConfig)) + } + ) + } + } + } +} + +fun ActiveWalletScriptType.displayString(): String { + return when (this) { + ActiveWalletScriptType.P2TR -> "P2TR (Taproot, BIP-86)" + ActiveWalletScriptType.P2WPKH -> "P2WPKH (Native Segwit, BIP-84)" + ActiveWalletScriptType.UNKNOWN -> TODO() + ActiveWalletScriptType.UNRECOGNIZED -> TODO() + } +} + +fun Network.displayString(): String { + return when (this) { + Network.REGTEST -> "Regtest" + Network.TESTNET -> "Testnet 3" + Network.TESTNET4 -> "Testnet 4" + Network.SIGNET -> "Signet" + Network.BITCOIN -> TODO() + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt" new file mode 100644 index 0000000..1d17ca3 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt" @@ -0,0 +1,176 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.intro + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import org.bitcoindevkit.devkitwallet.R +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.devkitTypography + +@Composable +fun OnboardingScreen(onFinishOnboarding: () -> Unit) { + val (currentIndex, setCurrentIndex) = remember { mutableIntStateOf(1) } + + @Suppress("ktlint:standard:max-line-length") + val messages = listOf( + "Easter egg #1: \uD83E\uDD5A", + "Welcome to the Devkit Wallet! This app is a playground for developers and bitcoin enthusiasts to experiment with bitcoin's test networks.", + "It is developed with the Bitcoin Dev Kit, a powerful set of libraries produced and maintained by the Bitcoin Dev Kit Foundation.\n\nThe variant of the app you have installed in the Esplora variant, which uses Esplora clients to fetch blockchain data for the wallet.", + "The Foundation maintains this app as a way to showcase the capabilities of the Bitcoin Dev Kit and to provide a starting point for developers to build their own apps.\n\nIt is not a production application, and only works for testnet, signet, and regtest. Have fun!" + ) + + ConstraintLayout( + modifier = Modifier + .fillMaxSize() + .background(DevkitWalletColors.primary) + ) { + val (logo, intro, progress, buttons) = createRefs() + + Image( + painter = painterResource(id = R.drawable.bdk_logo), + contentDescription = "Bitcoin Dev Kit logo", + Modifier + .size(180.dp) + .constrainAs(logo) { + top.linkTo(parent.top, margin = 90.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) + + Crossfade( + modifier = Modifier.constrainAs(intro) { + top.linkTo(logo.bottom, margin = 90.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + targetState = currentIndex, + label = "", + animationSpec = tween( + durationMillis = 1000, + delayMillis = 200, + ) + ) { screen -> + when (screen) { + 0 -> IntroTextPart(messages[0]) + 1 -> IntroTextPart(messages[1]) + 2 -> IntroTextPart(messages[2]) + 3 -> IntroTextPart(messages[3]) + } + } + + Row( + modifier = Modifier.constrainAs(progress) { + bottom.linkTo(buttons.top, margin = 32.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .padding(horizontal = 8.dp) + .size(size = 16.dp) + .clip(shape = CircleShape) + .background( + if (currentIndex == 1) DevkitWalletColors.accent1 else DevkitWalletColors.accent1.copy(alpha = 0.3f) + ) + ) + Box( + modifier = Modifier + .padding(horizontal = 8.dp) + .size(size = 16.dp) + .clip(shape = CircleShape) + .background( + if (currentIndex == 2) DevkitWalletColors.accent1 else DevkitWalletColors.accent1.copy(alpha = 0.3f) + ) + ) + Box( + modifier = Modifier + .padding(horizontal = 8.dp) + .size(size = 16.dp) + .clip(shape = CircleShape) + .background( + if (currentIndex == 3) DevkitWalletColors.accent1 else DevkitWalletColors.accent1.copy(alpha = 0.3f) + ) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp) + .constrainAs(buttons) { + bottom.linkTo(parent.bottom, margin = 32.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "Previous", + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { setCurrentIndex((currentIndex - 1).coerceIn(0, 3)) }, + color = DevkitWalletColors.white, + style = devkitTypography.labelLarge + ) + Text( + text = if (currentIndex < 3) "Next" else "Awesome!", + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + if (currentIndex < 3) { + setCurrentIndex( + (currentIndex + 1).coerceIn(0, 3) + ) + } else { + onFinishOnboarding() + } + }, + color = DevkitWalletColors.white, + style = devkitTypography.labelLarge + ) + } + } +} + +@Composable +fun IntroTextPart(message: String) { + Text( + text = message, + modifier = Modifier.padding(horizontal = 32.dp), + color = DevkitWalletColors.white, + style = devkitTypography.labelLarge + ) +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/RecoverWalletScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/RecoverWalletScreen.kt" new file mode 100644 index 0000000..220ea19 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/RecoverWalletScreen.kt" @@ -0,0 +1,386 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.intro + +import android.util.Log +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.launch +import org.bitcoindevkit.Descriptor +import org.bitcoindevkit.DescriptorSecretKey +import org.bitcoindevkit.KeychainKind +import org.bitcoindevkit.Mnemonic +import org.bitcoindevkit.Network +import org.bitcoindevkit.devkitwallet.data.ActiveWalletScriptType +import org.bitcoindevkit.devkitwallet.data.RecoverWalletConfig +import org.bitcoindevkit.devkitwallet.domain.DwLogger +import org.bitcoindevkit.devkitwallet.domain.DwLogger.LogLevel.INFO +import org.bitcoindevkit.devkitwallet.domain.bip39WordList +import org.bitcoindevkit.devkitwallet.domain.createScriptAppropriateDescriptor +import org.bitcoindevkit.devkitwallet.presentation.WalletCreateType +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.ui.components.CustomSnackbar +import org.bitcoindevkit.devkitwallet.presentation.ui.components.NetworkOptionsCard +import org.bitcoindevkit.devkitwallet.presentation.ui.components.NeutralButton +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar +import org.bitcoindevkit.devkitwallet.presentation.ui.components.WalletOptionsCard + +@Composable +internal fun RecoverWalletScreen(onAction: (WalletCreateType) -> Unit, navController: NavController) { + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + Scaffold( + topBar = { SecondaryScreensAppBar(title = "Recover a Wallet", navigation = { navController.navigateUp() }) }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) { data -> + CustomSnackbar(data) + } + }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + var selectedIndex by remember { mutableIntStateOf(0) } + val options = listOf("Descriptor", "Recovery Phrase") + + var descriptorString by remember { mutableStateOf("") } + var changeDescriptorString by remember { mutableStateOf("") } + var recoveryPhrase by remember { mutableStateOf("") } + + var walletName by remember { mutableStateOf("") } + val selectedNetwork: MutableState = remember { mutableStateOf(Network.SIGNET) } + val selectedScriptType: MutableState = + remember { mutableStateOf(ActiveWalletScriptType.P2TR) } + val scriptTypes = listOf(ActiveWalletScriptType.P2TR, ActiveWalletScriptType.P2WPKH) + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + SingleChoiceSegmentedButtonRow { + options.forEachIndexed { index, label -> + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = options.size + ), + onClick = { selectedIndex = index }, + selected = index == selectedIndex, + label = { Text(text = label, fontSize = 12.sp, color = Color.White) }, + colors = SegmentedButtonDefaults.colors( + activeContainerColor = DevkitWalletColors.primaryLight, + activeContentColor = DevkitWalletColors.primaryLight, + activeBorderColor = DevkitWalletColors.primaryLight, + inactiveContainerColor = DevkitWalletColors.primaryDark, + inactiveContentColor = DevkitWalletColors.primaryDark, + inactiveBorderColor = DevkitWalletColors.primaryDark, + ), + border = BorderStroke(4.dp, DevkitWalletColors.primaryDark), + icon = { }, + modifier = Modifier.width(180.dp).padding(top = 8.dp) + ) + } + } + Spacer(modifier = Modifier.padding(12.dp)) + + if (selectedIndex == 0) { + DescriptorInput( + walletName, + descriptorString, + changeDescriptorString, + selectedNetwork, + walletNameOnValueChange = { walletName = it }, + descriptorOnValueChange = { descriptorString = it }, + changeDescriptorOnValueChange = { changeDescriptorString = it } + ) + Spacer(modifier = Modifier.weight(1f)) + } else { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(horizontal = 32.dp) + ) { + WalletOptionsCard(scriptTypes, selectedNetwork, selectedScriptType) + Spacer(modifier = Modifier.padding(12.dp)) + OutlinedTextField( + modifier = Modifier + .padding(bottom = 8.dp) + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + value = walletName, + onValueChange = { walletName = it }, + label = { + Text( + text = "Give your wallet a name", + style = standardText, + ) + }, + singleLine = true, + textStyle = TextStyle(fontFamily = monoRegular, color = DevkitWalletColors.white), + colors = OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + RecoveryPhraseInput(recoveryPhrase, onValueChange = { recoveryPhrase = it }) + } + Spacer(modifier = Modifier.weight(1f)) + } + NeutralButton( + text = "Recover Wallet", + enabled = true, + onClick = { + if (descriptorString.isNotEmpty() && recoveryPhrase.isNotEmpty()) { + scope.launch { + snackbarHostState.showSnackbar( + "You cannot recover using both a descriptor and a recovery phrase at the same time." + ) + } + } + if (descriptorString.isEmpty() && recoveryPhrase.isEmpty()) { + scope.launch { + snackbarHostState.showSnackbar( + "You must provide either a descriptor or a recovery phrase to recover a wallet." + ) + } + } + if (descriptorString.isNotEmpty() && changeDescriptorString.isEmpty()) { + scope.launch { + snackbarHostState.showSnackbar( + "You must provide two descriptors for recovery." + ) + } + } + if (descriptorString.isEmpty() && changeDescriptorString.isNotEmpty()) { + scope.launch { + snackbarHostState.showSnackbar( + "You must provide two descriptors for recovery." + ) + } + } + if (recoveryPhrase.isNotEmpty()) { + Log.i("RecoverWalletScreen", "Recovering wallet with recovery phrase") + val parsingResult = parseRecoveryPhrase(recoveryPhrase) + + if (parsingResult is RecoveryPhraseValidationResult.Invalid) { + scope.launch { + snackbarHostState.showSnackbar(parsingResult.reason) + } + } else if (parsingResult is RecoveryPhraseValidationResult.ProbablyValid) { + val mnemonic = Mnemonic.fromString(parsingResult.recoveryPhrase) + val bip32ExtendedRootKey = DescriptorSecretKey(selectedNetwork.value, mnemonic, null) + val descriptor = createScriptAppropriateDescriptor( + scriptType = selectedScriptType.value, + bip32ExtendedRootKey = bip32ExtendedRootKey, + network = selectedNetwork.value, + keychain = KeychainKind.EXTERNAL + ) + val changeDescriptor = createScriptAppropriateDescriptor( + scriptType = selectedScriptType.value, + bip32ExtendedRootKey = bip32ExtendedRootKey, + network = selectedNetwork.value, + keychain = KeychainKind.INTERNAL + ) + val recoverWalletConfig = RecoverWalletConfig( + name = walletName, + network = selectedNetwork.value, + scriptType = selectedScriptType.value, + descriptor = descriptor, + changeDescriptor = changeDescriptor, + recoveryPhrase = parsingResult.recoveryPhrase + ) + DwLogger.log(INFO, "Recovering wallet with recovery phrase (name: $walletName)") + onAction(WalletCreateType.RECOVER(recoverWalletConfig)) + } + } + if (descriptorString.isNotEmpty() && changeDescriptorString.isNotEmpty()) { + Log.i("RecoverWalletScreen", "Recovering wallet with descriptors") + + val descriptor = Descriptor(descriptorString, selectedNetwork.value) + val changeDescriptor = Descriptor(changeDescriptorString, selectedNetwork.value) + val recoverWalletConfig = RecoverWalletConfig( + name = walletName, + network = selectedNetwork.value, + scriptType = null, + descriptor = descriptor, + changeDescriptor = changeDescriptor, + recoveryPhrase = null + ) + DwLogger.log(INFO, "Recovering wallet with descriptors (name: $walletName)") + onAction(WalletCreateType.RECOVER(recoverWalletConfig)) + } + } + ) + } + } +} + +@Composable +fun DescriptorInput( + walletName: String, + descriptor: String, + changeDescriptor: String, + selectedNetwork: MutableState, + walletNameOnValueChange: (String) -> Unit, + descriptorOnValueChange: (String) -> Unit, + changeDescriptorOnValueChange: (String) -> Unit, +) { + Column( + Modifier.padding(horizontal = 32.dp) + ) { + NetworkOptionsCard( + selectedNetwork + ) + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .padding(top = 16.dp), + value = walletName, + onValueChange = { walletNameOnValueChange(it) }, + label = { + Text( + text = "Give your wallet a name", + style = standardText + ) + }, + singleLine = true, + textStyle = TextStyle(fontFamily = monoRegular, color = DevkitWalletColors.white), + colors = OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth().padding(top = 16.dp), + value = descriptor, + onValueChange = { descriptorOnValueChange(it) }, + label = { + Text( + text = "Input your descriptor here", + style = standardText + ) + }, + singleLine = false, + minLines = 5, + textStyle = TextStyle(fontFamily = quattroRegular, fontSize = 12.sp, color = DevkitWalletColors.white), + colors = OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth().padding(top = 16.dp), + value = changeDescriptor, + onValueChange = { changeDescriptorOnValueChange(it) }, + label = { + Text( + text = "Input your change descriptor here", + style = standardText, + ) + }, + singleLine = false, + minLines = 5, + textStyle = TextStyle(fontFamily = quattroRegular, fontSize = 12.sp, color = DevkitWalletColors.white), + colors = OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + } +} + +@Composable +fun RecoveryPhraseInput(recoveryPhrase: String, onValueChange: (String) -> Unit) { + Column { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = recoveryPhrase, + onValueChange = { onValueChange(it) }, + label = { + Text( + text = "Input 12-word recovery phrase here", + style = standardText, + ) + }, + singleLine = false, + minLines = 5, + textStyle = TextStyle(fontFamily = quattroRegular, fontSize = 12.sp, color = DevkitWalletColors.white), + colors = OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + } +} + +private fun parseRecoveryPhrase(recoveryPhrase: String): RecoveryPhraseValidationResult { + val words = recoveryPhrase.trim().split(" ") + if (words.size != 12) { + return RecoveryPhraseValidationResult.Invalid("Recovery phrase must have 12 words") + } + if (words.any { it !in bip39WordList }) { + return RecoveryPhraseValidationResult.Invalid("Invalid word in recovery phrase") + } + return RecoveryPhraseValidationResult.ProbablyValid(recoveryPhrase) +} + +sealed class RecoveryPhraseValidationResult { + data class ProbablyValid(val recoveryPhrase: String) : RecoveryPhraseValidationResult() + + data class Invalid(val reason: String) : RecoveryPhraseValidationResult() +} + +@Preview(device = Devices.PIXEL_4, showBackground = true) +@Composable +internal fun PreviewWalletRecoveryScreen() { + RecoverWalletScreen( + onAction = {}, + navController = rememberNavController() + ) +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/WalletChoiceScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/WalletChoiceScreen.kt" new file mode 100644 index 0000000..70ec51f --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/WalletChoiceScreen.kt" @@ -0,0 +1,149 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.intro + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.R +import org.bitcoindevkit.devkitwallet.presentation.navigation.ActiveWalletsScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.CreateNewWalletScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletRecoveryScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoBold + +@Composable +internal fun WalletChoiceScreen(navController: NavController) { + Scaffold( + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + ConstraintLayout( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + val (logo, active, create, recover) = createRefs() + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 90.dp) + .constrainAs(logo) { + top.linkTo(parent.top) + }, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = R.drawable.ic_testnet_logo), + contentDescription = "Bitcoin testnet logo", + Modifier.size(90.dp) + ) + Spacer(modifier = Modifier.padding(8.dp)) + Text( + text = "Devkit\nWallet", + color = DevkitWalletColors.white, + fontSize = 28.sp, + lineHeight = 38.sp, + fontFamily = monoBold, + ) + } + + Button( + onClick = { navController.navigate(ActiveWalletsScreen) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + enabled = true, + modifier = Modifier + .size(width = 300.dp, height = 150.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 8.dp, shape = RoundedCornerShape(16.dp)) + .constrainAs(active) { + bottom.linkTo(create.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) { + Text( + text = "Use an\nActive Wallet", + // fontSize = 18.sp, + textAlign = TextAlign.Center, + // lineHeight = 28.sp, + ) + } + + Button( + onClick = { navController.navigate(CreateNewWalletScreen) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .size(width = 300.dp, height = 150.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 8.dp, shape = RoundedCornerShape(16.dp)) + .constrainAs(create) { + bottom.linkTo(recover.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) { + Text( + text = "Create a\nNew Wallet", + // fontSize = 18.sp, + textAlign = TextAlign.Center, + // lineHeight = 28.sp, + ) + } + + Button( + onClick = { navController.navigate(WalletRecoveryScreen) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .size(width = 300.dp, height = 150.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 8.dp, shape = RoundedCornerShape(16.dp)) + .constrainAs(recover) { + bottom.linkTo(parent.bottom, margin = 70.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) { + Text( + text = "Recover an\nExisting Wallet", + // fontSize = 18.sp, + textAlign = TextAlign.Center, + // lineHeight = 28.sp, + ) + } + } + } +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewWalletChoiceScreen() { +// WalletChoiceScreen(rememberNavController()) +// } diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/RBFScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/RBFScreen.kt" new file mode 100644 index 0000000..bc7a814 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/RBFScreen.kt" @@ -0,0 +1,260 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.wallet + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController + +private const val TAG = "RBFScreen" + +@Composable +internal fun RBFScreen( + txid: String?, + navController: NavController, + paddingValues: PaddingValues = PaddingValues(0.dp), +) { +// if (txid.isNullOrEmpty()) { +// navController.popBackStack() +// } +// var transaction: TransactionDetails? = getTransaction(txid = txid) +// if (transaction == null) { +// navController.popBackStack() +// } +// transaction = transaction as TransactionDetails +// val context = LocalContext.current +// +// val amount = (transaction.sent - transaction.received - (transaction.fee ?: 0UL)).toString() +// val feeRate: MutableState = rememberSaveable { mutableStateOf("") } +// val (showDialog, setShowDialog) = rememberSaveable { mutableStateOf(false) } +// +// ConstraintLayout( +// modifier = Modifier +// .fillMaxSize() +// .padding(paddingValues) +// .background(DevkitWalletColors.primary) +// ) { +// val (screenTitle, transactionInputs, bottomButtons) = createRefs() +// +// Text( +// text = "Send Bitcoin", +// color = DevkitWalletColors.white, +// fontSize = 28.sp, +// textAlign = TextAlign.Center, +// modifier = Modifier +// .constrainAs(screenTitle) { +// top.linkTo(parent.top) +// start.linkTo(parent.start) +// end.linkTo(parent.end) +// } +// .padding(top = 70.dp) +// ) +// +// Column( +// horizontalAlignment = Alignment.CenterHorizontally, +// verticalArrangement = Arrangement.Center, +// modifier = Modifier.constrainAs(transactionInputs) { +// top.linkTo(screenTitle.bottom) +// bottom.linkTo(bottomButtons.top) +// start.linkTo(parent.start) +// end.linkTo(parent.end) +// height = Dimension.fillToConstraints +// } +// ) { +// ShowTxnDetail(name = "Transaction Id",content = txid!!) +// ShowTxnDetail(name = "Amount", content = amount) +// TransactionFeeInput(feeRate = feeRate) +// BumpFeeDialog( +// txid = txid, +// amount = amount, +// feeRate = feeRate, +// showDialog = showDialog, +// setShowDialog = setShowDialog, +// context = context +// ) +// } +// Column( +// Modifier +// .constrainAs(bottomButtons) { +// bottom.linkTo(parent.bottom) +// start.linkTo(parent.start) +// end.linkTo(parent.end) +// } +// .padding(bottom = 32.dp) +// ) { +// Button( +// onClick = { setShowDialog(true) }, +// colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent2), +// shape = RoundedCornerShape(16.dp), +// modifier = Modifier +// .height(80.dp) +// .fillMaxWidth(0.9f) +// .padding(vertical = 8.dp, horizontal = 8.dp) +// .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) +// ) { +// Text( +// text = "broadcast transaction", +// fontSize = 14.sp, +// textAlign = TextAlign.Center, +// lineHeight = 28.sp, +// ) +// } +// Button( +// onClick = { navController.navigate(Screen.HomeScreen.route) }, +// colors = ButtonDefaults.buttonColors(DevkitWalletColors.primaryLight), +// shape = RoundedCornerShape(16.dp), +// modifier = Modifier +// .height(80.dp) +// .fillMaxWidth(0.9f) +// .padding(vertical = 8.dp, horizontal = 8.dp) +// .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) +// ) { +// Text( +// text = "back to wallet", +// fontSize = 14.sp, +// textAlign = TextAlign.Center, +// lineHeight = 28.sp, +// ) +// } +// } +// } +} +// +// @OptIn(ExperimentalMaterial3Api::class) +// @Composable +// private fun ShowTxnDetail(name: String, content: String) { +// Row( +// verticalAlignment = Alignment.CenterVertically, +// modifier = Modifier.fillMaxWidth(fraction = 0.9f) +// ) { +// OutlinedTextField( +// modifier = Modifier +// .padding(vertical = 8.dp) +// .weight(0.5f), +// value = content, +// onValueChange = { }, +// label = { +// Text( +// text = name, +// color = DevkitWalletColors.white, +// ) +// }, +// singleLine = true, +// textStyle = TextStyle(color = DevkitWalletColors.white), +// colors = TextFieldDefaults.outlinedTextFieldColors( +// focusedBorderColor = DevkitWalletColors.accent1, +// unfocusedBorderColor = DevkitWalletColors.white, +// cursorColor = DevkitWalletColors.accent1, +// ), +// enabled = false, +// ) +// } +// } +// +// @OptIn(ExperimentalMaterial3Api::class) +// @Composable +// private fun TransactionFeeInput(feeRate: MutableState) { +// Column(horizontalAlignment = Alignment.CenterHorizontally) { +// OutlinedTextField( +// modifier = Modifier +// .padding(vertical = 8.dp) +// .fillMaxWidth(0.9f), +// value = feeRate.value, +// onValueChange = { newValue: String -> +// feeRate.value = newValue.filter { it.isDigit() } +// }, +// singleLine = true, +// textStyle = TextStyle(color = DevkitWalletColors.white), +// label = { +// Text( +// text = "New fee rate", +// color = DevkitWalletColors.white, +// ) +// }, +// colors = TextFieldDefaults.outlinedTextFieldColors( +// focusedBorderColor = DevkitWalletColors.accent1, +// unfocusedBorderColor = DevkitWalletColors.white, +// cursorColor = DevkitWalletColors.accent1, +// ), +// ) +// } +// } +// +// @Composable +// fun BumpFeeDialog( +// txid: String, +// amount: String, +// showDialog: Boolean, +// setShowDialog: (Boolean) -> Unit, +// context: Context, +// feeRate: MutableState, +// ) { +// if (showDialog) { +// var confirmationText = "Confirm Transaction : \nTxid : $txid\nAmount : $amount" +// if (feeRate.value.isNotEmpty()) { +// confirmationText += "Fee Rate : ${feeRate.value.toULong()}" +// } +// AlertDialog( +// containerColor = DevkitWalletColors.primaryLight, +// onDismissRequest = {}, +// title = { +// Text( +// text = "Confirm transaction", +// color = DevkitWalletColors.white +// ) +// }, +// text = { +// Text( +// text = confirmationText, +// color = DevkitWalletColors.white +// ) +// }, +// confirmButton = { +// TextButton( +// onClick = { +// if (feeRate.value.isNotEmpty()) { +// broadcastTransaction(txid = txid, feeRate = feeRate.value.toFloat()) +// } else { +// Toast.makeText(context, "Fee is empty!", Toast.LENGTH_SHORT).show() +// } +// setShowDialog(false) +// }, +// ) { +// Text( +// text = "Confirm", +// color = DevkitWalletColors.white +// ) +// } +// }, +// dismissButton = { +// TextButton( +// onClick = { +// setShowDialog(false) +// }, +// ) { +// Text( +// text = "Cancel", +// color = DevkitWalletColors.white +// ) +// } +// }, +// ) +// } +// } +// +// private fun broadcastTransaction(txid: String, feeRate: Float = 1F) { +// Log.i(TAG, "Attempting to broadcast transaction with inputs: txid $txid, fee rate: $feeRate") +// try { +// // create, sign, and broadcast +// val psbt: PartiallySignedTransaction = Wallet.createBumpFeeTransaction(txid = txid, feeRate = feeRate) +// Wallet.sign(psbt) +// val newTxid: String = Wallet.broadcast(psbt) +// Log.i(TAG, "Transaction was broadcast! txid: $newTxid") +// } catch (e: Throwable) { +// Log.i(TAG, "Broadcast error: ${e.message}") +// } +// } diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/ReceiveScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/ReceiveScreen.kt" new file mode 100644 index 0000000..ebc406c --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/ReceiveScreen.kt" @@ -0,0 +1,234 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.wallet + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.core.graphics.createBitmap +import androidx.navigation.NavController +import com.composables.icons.lucide.ClipboardCopy +import com.composables.icons.lucide.Lucide +import com.google.zxing.BarcodeFormat +import com.google.zxing.common.BitMatrix +import com.google.zxing.qrcode.QRCodeWriter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.bitcoindevkit.devkitwallet.presentation.navigation.HomeScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.ReceiveScreenAction +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.ReceiveScreenState +import androidx.core.graphics.set + +private const val TAG = "ReceiveScreen" + +@Composable +internal fun ReceiveScreen( + state: ReceiveScreenState, + onAction: (ReceiveScreenAction) -> Unit, + navController: NavController, +) { + Log.i(TAG, "We are recomposing the ReceiveScreen") + val snackbarHostState = remember { SnackbarHostState() } + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { + SecondaryScreensAppBar( + title = "Receive Address", + navigation = { navController.navigate(HomeScreen) } + ) + }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + ConstraintLayout( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + val (QRCode, bottomButtons) = createRefs() + val context = LocalContext.current + val scope = rememberCoroutineScope() + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .constrainAs(QRCode) { + top.linkTo(parent.top) + bottom.linkTo(bottomButtons.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + }.padding(horizontal = 32.dp) + ) { + val qr: ImageBitmap? = state.address?.let { addressToQR(it) } + Log.i("ReceiveScreen", "New receive address is ${state.address}") + if (qr != null) { + Image( + bitmap = qr, + contentDescription = "Bitcoindevkit website QR code", + Modifier.size(250.dp).clip(RoundedCornerShape(16.dp)) + ) + Spacer(modifier = Modifier.padding(vertical = 16.dp)) + Box { + SelectionContainer { + Text( + modifier = Modifier + .clickable { + copyToClipboard( + state.address, + context, + scope, + snackbarHostState, + null, + ) + }.background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ).padding(12.dp), + text = state.address.chunked(4).joinToString(" "), + fontFamily = monoRegular, + color = DevkitWalletColors.white + ) + } + Icon( + Lucide.ClipboardCopy, + tint = Color.White, + contentDescription = "Copy to clipboard", + modifier = Modifier + .padding(8.dp) + .size(20.dp) + .align(Alignment.BottomEnd) + ) + } + Spacer(modifier = Modifier.padding(vertical = 16.dp)) + Text( + text = "Wallet address index: ${state.addressIndex}", + fontFamily = monoRegular, + color = DevkitWalletColors.white, + modifier = Modifier.align(Alignment.Start) + ) + } + } + + Column( + Modifier + .constrainAs(bottomButtons) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }.padding(bottom = 24.dp) + ) { + Button( + onClick = { onAction(ReceiveScreenAction.UpdateAddress) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .height(80.dp) + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = "Generate address", + style = standardText, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + } + } + } + } +} + +private fun addressToQR(address: String): ImageBitmap? { + Log.i(TAG, "We are generating the QR code for address $address") + try { + val qrCodeWriter: QRCodeWriter = QRCodeWriter() + val bitMatrix: BitMatrix = qrCodeWriter.encode(address, BarcodeFormat.QR_CODE, 1000, 1000) + val bitMap = createBitmap(1000, 1000) + for (x in 0 until 1000) { + for (y in 0 until 1000) { + // DevkitWalletColors.primaryDark for dark and DevkitWalletColors.white for light + bitMap[x, y] = + if (bitMatrix[x, y]) DevkitWalletColors.primaryDark.toArgb() else DevkitWalletColors.white.toArgb() + } + } + return bitMap.asImageBitmap() + } catch (e: Throwable) { + Log.i("ReceiveScreen", "Error with QRCode generation, $e") + } + return null +} + +fun copyToClipboard( + content: String, + context: Context, + scope: CoroutineScope, + snackbarHostState: SnackbarHostState, + setCopyClicked: ( + (Boolean) -> Unit + )?, +) { + val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip: ClipData = ClipData.newPlainText("", content) + clipboard.setPrimaryClip(clip) + scope.launch { + snackbarHostState.showSnackbar("Copied address to clipboard!") + delay(1000) + if (setCopyClicked != null) { + setCopyClicked(false) + } + } +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewReceiveScreen() { +// ReceiveScreen(rememberNavController()) +// } diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt" new file mode 100644 index 0000000..474ba1a --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt" @@ -0,0 +1,516 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.wallet + +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.navigation.NavController +import com.composables.icons.lucide.Lucide +import com.composables.icons.lucide.UserRoundMinus +import com.composables.icons.lucide.UserRoundPlus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.bitcoindevkit.devkitwallet.presentation.navigation.HomeScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.ui.components.NeutralButton +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.SendViewModel +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.Recipient +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.SendScreenAction +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.TransactionType +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.TxDataBundle + +private const val TAG = "SendScreen" + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SendScreen(navController: NavController, sendViewModel: SendViewModel) { + val onAction = sendViewModel::onAction + + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val recipientList: MutableList = remember { mutableStateListOf(Recipient(address = "", amount = 0u)) } + val feeRate: MutableState = rememberSaveable { mutableStateOf("") } + val (showDialog, setShowDialog) = rememberSaveable { mutableStateOf(false) } + val sendAll: MutableState = remember { mutableStateOf(false) } + + val bottomSheetScaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState() + + BottomSheetScaffold( + topBar = { + SecondaryScreensAppBar( + title = "Send Bitcoin", + navigation = { navController.navigate(HomeScreen) } + ) + }, + sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + sheetContent = { AdvancedOptions(sendAll, recipientList) }, + sheetContainerColor = DevkitWalletColors.primaryDark, + scaffoldState = bottomSheetScaffoldState, + sheetPeekHeight = 0.dp, + ) { paddingValues -> + ConstraintLayout( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(DevkitWalletColors.primary) + ) { + val (transactionInputs, bottomButtons) = createRefs() + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .constrainAs(transactionInputs) { + top.linkTo(parent.top) + bottom.linkTo(bottomButtons.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + } + ) { + TransactionRecipientInput(recipientList = recipientList) + TransactionAmountInput( + recipientList = recipientList, + transactionType = if (sendAll.value) TransactionType.SEND_ALL else TransactionType.STANDARD + ) + TransactionFeeInput(feeRate = feeRate) + Dialog( + recipientList = recipientList, + feeRate = feeRate, + showDialog = showDialog, + setShowDialog = setShowDialog, + transactionType = if (sendAll.value) TransactionType.SEND_ALL else TransactionType.STANDARD, + context = context, + onAction = onAction + ) + } + Column( + Modifier + .constrainAs(bottomButtons) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }.padding(bottom = 32.dp) + ) { + MoreOptions(coroutineScope = coroutineScope, bottomSheetScaffoldState = bottomSheetScaffoldState) + Button( + onClick = { setShowDialog(true) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent2), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .height(80.dp) + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = "Broadcast transaction", + ) + } + } + } + } +} + +@Composable +internal fun AdvancedOptions( + sendAll: MutableState, + recipientList: MutableList, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Send All", + style = standardText, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + Spacer(modifier = Modifier.width(8.dp)) + Switch( + checked = sendAll.value, + onCheckedChange = { + sendAll.value = !sendAll.value + while (recipientList.size > 1) { + recipientList.removeLast() + } + }, + colors = SwitchDefaults.colors( + uncheckedBorderColor = DevkitWalletColors.primaryDark, + uncheckedThumbColor = DevkitWalletColors.primaryDark, + uncheckedTrackColor = DevkitWalletColors.white, + checkedThumbColor = DevkitWalletColors.white, + checkedTrackColor = DevkitWalletColors.accent1 + ) + ) + } + + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Number of Recipients", + style = standardText + ) + Text( + text = "${recipientList.size}", + style = standardText + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy((-1).dp), + modifier = Modifier.padding(top = 4.dp) + ) { + val startShape = RoundedCornerShape(topStart = 6.dp, bottomStart = 6.dp) + val endShape = RoundedCornerShape(topEnd = 6.dp, bottomEnd = 6.dp) + Box( + Modifier + .clip(startShape) + .border(2.dp, Color.White, startShape) + .clickable(role = Role.Button) { + recipientList.add(Recipient("", 0u)) + }.padding(horizontal = 12.dp, vertical = 10.dp) + ) { + Icon(Lucide.UserRoundPlus, tint = DevkitWalletColors.white, contentDescription = null) + } + Box( + Modifier + .clip(endShape) + .border(2.dp, Color.White, endShape) + .clickable(role = Role.Button) { + if (recipientList.size > 1) { + recipientList.removeLast() + } + }.padding(horizontal = 12.dp, vertical = 10.dp) + ) { + Icon(Lucide.UserRoundMinus, tint = DevkitWalletColors.white, contentDescription = null) + } + } + // Button( + // onClick = { + // if (recipientList.size > 1) { + // recipientList.removeLast() + // } + // }, + // enabled = !sendAll.value, + // colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent2), + // shape = RoundedCornerShape(16.dp), + // modifier = Modifier.width(70.dp) + // ) { + // Text(text = "-") + // } + // Button( + // onClick = { recipientList.add(Recipient("", 0u)) }, + // enabled = !sendAll.value, + // colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent1), + // shape = RoundedCornerShape(16.dp), + // modifier = Modifier.width(70.dp) + // ) { + // Text(text = "+") + // } + } + + Spacer(modifier = Modifier.height(32.dp)) + } +} + +@Composable +private fun TransactionRecipientInput(recipientList: MutableList) { + LazyColumn( + modifier = Modifier + .fillMaxWidth(0.9f) + .heightIn(max = 100.dp) + ) { + itemsIndexed(recipientList) { index, _ -> + val recipientAddress: MutableState = rememberSaveable { mutableStateOf("") } + + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.5f), + value = recipientAddress.value, + onValueChange = { + recipientAddress.value = it + recipientList[index].address = it + }, + label = { + Text( + text = "Recipient address ${index + 1}", + color = DevkitWalletColors.white, + ) + }, + singleLine = true, + textStyle = TextStyle(color = DevkitWalletColors.white), + colors = OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white + ) + ) + } + } + } +} + +fun checkRecipientList( + recipientList: MutableList, + feeRate: MutableState, + context: Context, +): Boolean { + if (recipientList.size > 4) { + Toast.makeText(context, "Too many recipients", Toast.LENGTH_SHORT).show() + return false + } + for (recipient in recipientList) { + if (recipient.address == "") { + Toast.makeText(context, "Address is empty", Toast.LENGTH_SHORT).show() + return false + } + } + if (feeRate.value.isBlank()) { + Toast.makeText(context, "Fee rate is empty", Toast.LENGTH_SHORT).show() + return false + } + return true +} + +@Composable +private fun TransactionAmountInput(recipientList: MutableList, transactionType: TransactionType) { + LazyColumn( + modifier = Modifier + .fillMaxWidth(0.9f) + .heightIn(max = 100.dp) + ) { + itemsIndexed(recipientList) { index, _ -> + val amount: MutableState = rememberSaveable { mutableStateOf("") } + + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + modifier = Modifier + .padding(vertical = 8.dp) + .weight(0.5f), + value = amount.value, + onValueChange = { + amount.value = it + recipientList[index].amount = it.toULong() + }, + label = { + when (transactionType) { + TransactionType.SEND_ALL -> { + Text( + text = "Amount (Send All)", + color = DevkitWalletColors.white, + ) + } + else -> { + Text( + text = "Amount ${index + 1}", + color = DevkitWalletColors.white, + ) + } + } + }, + singleLine = true, + textStyle = TextStyle(color = DevkitWalletColors.white), + colors = OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white + ), + enabled = ( + when (transactionType) { + TransactionType.SEND_ALL -> false + else -> true + } + ) + ) + } + } + } +} + +@Composable +private fun TransactionFeeInput(feeRate: MutableState) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + OutlinedTextField( + modifier = Modifier + .padding(vertical = 8.dp) + .fillMaxWidth(0.9f), + value = feeRate.value, + onValueChange = { newValue: String -> + feeRate.value = newValue.filter { it.isDigit() } + }, + singleLine = true, + textStyle = TextStyle(color = DevkitWalletColors.white), + label = { + Text( + text = "Fee rate", + color = DevkitWalletColors.white, + ) + }, + colors = OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white + ), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MoreOptions(coroutineScope: CoroutineScope, bottomSheetScaffoldState: BottomSheetScaffoldState) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + NeutralButton( + text = "Advanced options", + onClick = { + coroutineScope.launch { + bottomSheetScaffoldState.bottomSheetState.expand() + } + }, + ) + } +} + +@Composable +private fun Dialog( + recipientList: MutableList, + feeRate: MutableState, + showDialog: Boolean, + setShowDialog: (Boolean) -> Unit, + transactionType: TransactionType, + context: Context, + onAction: (SendScreenAction) -> Unit, +) { + if (showDialog) { + var confirmationText = "Confirm Transaction : \n" + recipientList.forEach { confirmationText += "${it.address}, ${it.amount}\n" } + if (feeRate.value.isNotEmpty()) { + confirmationText += "Fee Rate : ${feeRate.value.toULong()}" + } + AlertDialog( + containerColor = DevkitWalletColors.primaryLight, + onDismissRequest = {}, + title = { + Text( + text = "Confirm transaction", + color = DevkitWalletColors.white + ) + }, + text = { + Text( + text = confirmationText, + color = DevkitWalletColors.white + ) + }, + confirmButton = { + TextButton( + onClick = { + if (checkRecipientList(recipientList = recipientList, feeRate = feeRate, context = context)) { + val txDataBundle = + TxDataBundle( + recipients = recipientList, + feeRate = feeRate.value.toULong(), + transactionType = transactionType, + ) + onAction(SendScreenAction.Broadcast(txDataBundle)) + setShowDialog(false) + } + }, + ) { + Text( + text = "Confirm", + color = DevkitWalletColors.white + ) + } + }, + dismissButton = { + TextButton( + onClick = { + setShowDialog(false) + }, + ) { + Text( + text = "Cancel", + color = DevkitWalletColors.white + ) + } + }, + ) + } +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewSendScreen() { +// SendScreen(rememberNavController()) +// } diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionHistoryScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionHistoryScreen.kt" new file mode 100644 index 0000000..9e903b0 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionHistoryScreen.kt" @@ -0,0 +1,71 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.wallet + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.domain.Wallet +import org.bitcoindevkit.devkitwallet.presentation.navigation.HomeScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.TransactionScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.ui.components.ConfirmedTransactionCard +import org.bitcoindevkit.devkitwallet.presentation.ui.components.PendingTransactionCard +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +private const val TAG = "TransactionHistoryScreen" + +@Composable +internal fun TransactionHistoryScreen(navController: NavController, activeWallet: Wallet) { + val (pendingTransactions, confirmedTransactions) = activeWallet.getAllTxDetails().partition { it.pending } + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Transaction History", + navigation = { navController.navigate(HomeScreen) } + ) + }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .padding(top = 6.dp) + .verticalScroll(state = scrollState) + ) { + if (pendingTransactions.isNotEmpty()) { + pendingTransactions.forEach { + PendingTransactionCard(details = it, navController = navController) + } + } + if (confirmedTransactions.isNotEmpty()) { + confirmedTransactions.sortedBy { it.confirmationBlock?.height }.forEach { + ConfirmedTransactionCard(it, navController) + } + } + } + } +} + +fun viewTransaction(navController: NavController, txid: String) { + navController.navigate(TransactionScreen(txid)) +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewTransactionsScreen() { +// TransactionsScreen(rememberNavController()) +// } diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt" new file mode 100644 index 0000000..e580a43 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt" @@ -0,0 +1,199 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.wallet + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.presentation.navigation.RbfScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +@Composable +internal fun TransactionScreen(txid: String?, navController: NavController) { + // val transaction = getTransaction(txid = txid) + // if (transaction == null) { + // navController.popBackStack() + // } + // val transactionDetail = getTransactionDetails(transaction = transaction!!) + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Transaction Details", + navigation = { navController.navigateUp() } + ) + }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + ConstraintLayout( + modifier = Modifier + .fillMaxSize() + .background(DevkitWalletColors.primary) + .padding(paddingValues) + ) { + val (screenTitle, transactions, bottomButton) = createRefs() + + Column( + modifier = Modifier + .constrainAs(screenTitle) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }.padding(top = 70.dp) + ) { + Text( + text = "Transaction", + color = DevkitWalletColors.white, + fontSize = 28.sp, + fontFamily = monoRegular, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + // Text( + // text = transactionTitle(transaction = transaction), + // color = DevkitWalletColors.white, + // fontSize = 14.sp, + // textAlign = TextAlign.Center, + // modifier = Modifier.padding(horizontal = 16.dp) + // ) + } + + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .constrainAs(transactions) { + top.linkTo(screenTitle.bottom) + bottom.linkTo(bottomButton.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + } + ) { + // items(transactionDetail) { + // Row( + // modifier = Modifier + // .fillMaxWidth() + // .padding(all = 16.dp) + // ) { + // Text( + // text = "${it.first} :", + // fontSize = 16.sp, + // color = DevkitWalletColors.white, + // ) + // Text( + // text = it.second, + // fontSize = 16.sp, + // textAlign = TextAlign.End, + // color = DevkitWalletColors.white, + // modifier = Modifier.fillMaxWidth() + // ) + // } + // } + } + + Column( + modifier = Modifier + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + .constrainAs(bottomButton) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) { + TransactionDetailButton( + content = "increase fees", + navController = navController, + txid = txid + ) + } + } + } +} + +@Composable +fun TransactionDetailButton(content: String, navController: NavController, txid: String?) { + Button( + onClick = { + when (content) { + "increase fees" -> { + navController.navigate(RbfScreen(txid!!)) + } + "back to transaction list" -> { + navController.navigateUp() + } + } + }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .height(60.dp) + .fillMaxWidth() + ) { + Text( + text = content, + fontSize = 14.sp, + fontFamily = monoRegular, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + } +} + +// fun getTransactionDetails(transaction: TransactionDetails): List> { +// val transactionDetails = mutableListOf>() +// +// if (transaction.confirmationTime != null) { +// transactionDetails.add(Pair("Status", "Confirmed")) +// transactionDetails.add(Pair("Timestamp", transaction.confirmationTime!!.timestamp.timestampToString())) +// transactionDetails.add(Pair("Received", (if (transaction.received < transaction.sent) 0 else transaction.received).toString())) +// transactionDetails.add(Pair("Sent", (if (transaction.sent < transaction.received) 0 else transaction.sent - transaction.received - transaction.fee!!).toString())) +// transactionDetails.add(Pair("Fees", transaction.fee.toString())) +// transactionDetails.add(Pair("Block", transaction.confirmationTime!!.height.toString())) +// } else { +// transactionDetails.add(Pair("Status", "Pending")) +// transactionDetails.add(Pair("Timestamp", "Pending")) +// transactionDetails.add(Pair("Received", (if (transaction.received < transaction.sent) 0 else transaction.received).toString())) +// transactionDetails.add(Pair("Sent", (if (transaction.sent < transaction.received) 0 else transaction.sent - transaction.received - transaction.fee!!).toString())) +// transactionDetails.add(Pair("Fees", transaction.fee.toString())) +// } +// return transactionDetails +// } +// +// fun transactionTitle(transaction: TransactionDetails): String { +// return transaction.txid +// } +// +// fun getTransaction(txid: String?): TransactionDetails? { +// if (txid.isNullOrEmpty()) { +// return null +// } +// return Wallet.getTransaction(txid = txid) +// } diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt" new file mode 100644 index 0000000..ca96998 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt" @@ -0,0 +1,281 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.wallet + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.CurrencyBitcoin +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.DrawerState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import com.composables.icons.lucide.Lucide +import com.composables.icons.lucide.Menu +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.bitcoindevkit.devkitwallet.domain.CurrencyUnit +import org.bitcoindevkit.devkitwallet.domain.utils.formatInBtc +import org.bitcoindevkit.devkitwallet.presentation.navigation.ReceiveScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.SendScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.TransactionHistoryScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroBold +import org.bitcoindevkit.devkitwallet.presentation.ui.components.LoadingAnimation +import org.bitcoindevkit.devkitwallet.presentation.ui.components.NeutralButton +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.WalletViewModel +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenAction +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenState + +private const val TAG = "WalletHomeScreen" + +@Composable +internal fun WalletHomeScreen( + navController: NavHostController, + drawerState: DrawerState, + walletViewModel: WalletViewModel, +) { + val networkAvailable: Boolean = isOnline(LocalContext.current) + val state: WalletScreenState = walletViewModel.state + val onAction = walletViewModel::onAction + + val interactionSource = remember { MutableInteractionSource() } + val scope: CoroutineScope = rememberCoroutineScope() + + Scaffold( + topBar = { WalletAppBar(scope = scope, drawerState = drawerState) }, + containerColor = DevkitWalletColors.primary + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.padding(24.dp)) + Row( + Modifier + .clickable( + interactionSource, + indication = null, + onClick = { onAction(WalletScreenAction.SwitchUnit) } + ).fillMaxWidth(0.9f) + .padding(horizontal = 8.dp) + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp) + ).height(100.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + when (state.unit) { + CurrencyUnit.Bitcoin -> { + Icon( + imageVector = Icons.Rounded.CurrencyBitcoin, + tint = DevkitWalletColors.white, + contentDescription = "Bitcoin testnet logo", + modifier = Modifier + .align(Alignment.CenterVertically) + .size(48.dp) + ) + Text( + text = state.balance.formatInBtc(), + fontFamily = monoRegular, + fontSize = 32.sp, + color = DevkitWalletColors.white + ) + } + CurrencyUnit.Satoshi -> { + Text( + text = "${state.balance} sat", + fontFamily = monoRegular, + fontSize = 32.sp, + color = DevkitWalletColors.white + ) + } + } + } + Spacer(modifier = Modifier.padding(4.dp)) + if (networkAvailable) { + Row( + modifier = Modifier.height(40.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (state.syncing) LoadingAnimation() + } + } + + if (!networkAvailable) { + Row( + Modifier + .fillMaxWidth() + .background(color = DevkitWalletColors.accent2) + .height(50.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = "Network unavailable", + fontFamily = monoRegular, + fontSize = 16.sp, + color = DevkitWalletColors.white + ) + } + } + + NeutralButton( + text = "sync", + enabled = networkAvailable, + onClick = { onAction(WalletScreenAction.UpdateBalance) } + ) + + NeutralButton( + text = "transaction history", + enabled = networkAvailable, + onClick = { navController.navigate(TransactionHistoryScreen) } + ) + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .height(140.dp) + .fillMaxWidth(0.9f) + ) { + Button( + onClick = { navController.navigate(ReceiveScreen) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent1), + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .height(160.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = "receive", + fontSize = 16.sp, + textAlign = TextAlign.End, + lineHeight = 28.sp, + modifier = Modifier + .fillMaxWidth(0.4f) + .align(Alignment.Bottom) + ) + } + + Button( + onClick = { navController.navigate(SendScreen) }, + colors = + ButtonDefaults.buttonColors( + containerColor = DevkitWalletColors.accent2, + disabledContainerColor = DevkitWalletColors.accent2, + ), + shape = RoundedCornerShape(16.dp), + enabled = networkAvailable, + modifier = Modifier + .height(160.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + ) { + Text( + text = "send", + fontSize = 16.sp, + textAlign = TextAlign.End, + lineHeight = 28.sp, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Bottom) + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun WalletAppBar(scope: CoroutineScope, drawerState: DrawerState) { + CenterAlignedTopAppBar( + title = { + Text( + text = "Devkit Wallet", + color = DevkitWalletColors.white, + // fontFamily = quattroRegular, + fontFamily = quattroBold, + fontSize = 20.sp, + ) + }, + navigationIcon = { + IconButton(onClick = { scope.launch { drawerState.open() } }) { + Icon( + imageVector = Lucide.Menu, + contentDescription = "Open drawer", + tint = DevkitWalletColors.white + ) + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = DevkitWalletColors.primaryDark, + ) + ) +} + +fun isOnline(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val capabilities = + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + if (capabilities != null) { + when { + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> { + 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 + } + } + } + return false +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/AddressViewModel.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/AddressViewModel.kt" new file mode 100644 index 0000000..e68d44f --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/AddressViewModel.kt" @@ -0,0 +1,38 @@ +/* + * Copyright 2021-2025 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.presentation.viewmodels + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import org.bitcoindevkit.AddressInfo +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.ReceiveScreenAction +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.ReceiveScreenState + +internal class AddressViewModel(private val wallet: Wallet) : ViewModel() { + var state: ReceiveScreenState by mutableStateOf(ReceiveScreenState()) + private set + + fun onAction(action: ReceiveScreenAction) { + when (action) { + is ReceiveScreenAction.UpdateAddress -> updateAddress() + } + } + + private fun updateAddress() { + val newAddress: AddressInfo = wallet.getNewAddress() + DwLogger.log(INFO, "Revealing new address at index ${newAddress.index}") + + state = ReceiveScreenState( + address = newAddress.address.toString(), + addressIndex = newAddress.index + ) + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt" new file mode 100644 index 0000000..9e50297 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt" @@ -0,0 +1,50 @@ +/* + * Copyright 2021-2025 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.presentation.viewmodels + +import android.util.Log +import androidx.lifecycle.ViewModel +import org.bitcoindevkit.FeeRate +import org.bitcoindevkit.Psbt +import org.bitcoindevkit.devkitwallet.domain.Wallet +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.SendScreenAction +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.TransactionType +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.TxDataBundle + +private const val TAG = "SendViewModel" + +internal class SendViewModel(private val wallet: Wallet) : ViewModel() { + fun onAction(action: SendScreenAction) { + when (action) { + is SendScreenAction.Broadcast -> broadcast(action.txDataBundle) + } + } + + 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), + ) + // 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 txid: String = wallet.broadcast(psbt) + Log.i(TAG, "Transaction was broadcast! txid: $txid") + } else { + Log.i(TAG, "Transaction not signed.") + } + } catch (e: Throwable) { + Log.i(TAG, "Broadcast error: ${e.message}") + } + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt" new file mode 100644 index 0000000..692dfbb --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt" @@ -0,0 +1,68 @@ +/* + * Copyright 2021-2025 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.presentation.viewmodels + +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.bitcoindevkit.devkitwallet.domain.CurrencyUnit +import org.bitcoindevkit.devkitwallet.domain.Wallet +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenAction +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenState + +private const val TAG = "WalletViewModel" + +class WalletViewModel( + private val wallet: Wallet, +) : ViewModel() { + var state: WalletScreenState by mutableStateOf(WalletScreenState()) + private set + + init { + updateClientEndpoint() + } + + fun onAction(action: WalletScreenAction) { + when (action) { + WalletScreenAction.UpdateBalance -> updateBalance() + WalletScreenAction.SwitchUnit -> switchUnit() + } + } + + private fun switchUnit() { + state = when (state.unit) { + CurrencyUnit.Bitcoin -> state.copy(unit = CurrencyUnit.Satoshi) + CurrencyUnit.Satoshi -> state.copy(unit = CurrencyUnit.Bitcoin) + } + } + + private fun updateBalance() { + state = state.copy(syncing = true) + viewModelScope.launch(Dispatchers.IO) { + wallet.sync() + withContext(Dispatchers.Main) { + val newBalance = wallet.getBalance() + Log.i(TAG, "New balance: $newBalance") + state = state.copy(balance = newBalance, syncing = false) + } + } + } + + private fun updateClientEndpoint() { + viewModelScope.launch(Dispatchers.IO) { + val endpoint = wallet.getClientEndpoint() + withContext(Dispatchers.Main) { + state = state.copy(esploraEndpoint = endpoint) + } + } + } +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviReceiveScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviReceiveScreen.kt" new file mode 100644 index 0000000..fb3f954 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviReceiveScreen.kt" @@ -0,0 +1,15 @@ +/* + * Copyright 2021-2025 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.presentation.viewmodels.mvi + +data class ReceiveScreenState( + val address: String? = null, + val addressIndex: UInt? = null, +) + +sealed interface ReceiveScreenAction { + data object UpdateAddress : ReceiveScreenAction +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviSendScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviSendScreen.kt" new file mode 100644 index 0000000..224018f --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviSendScreen.kt" @@ -0,0 +1,27 @@ +/* + * Copyright 2021-2025 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.presentation.viewmodels.mvi + +// data class SendScreenState( +// val address: String? = null, +// ) + +sealed class SendScreenAction { + data class Broadcast(val txDataBundle: TxDataBundle) : SendScreenAction() +} + +data class TxDataBundle( + val recipients: List, + val feeRate: ULong, + val transactionType: TransactionType, +) + +data class Recipient(var address: String, var amount: ULong) + +enum class TransactionType { + STANDARD, + SEND_ALL, +} diff --git "a/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt" "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt" new file mode 100644 index 0000000..cef5212 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt" @@ -0,0 +1,21 @@ +/* + * Copyright 2021-2025 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.presentation.viewmodels.mvi + +import org.bitcoindevkit.devkitwallet.domain.CurrencyUnit + +data class WalletScreenState( + val balance: ULong = 0u, + val syncing: Boolean = false, + val unit: CurrencyUnit = CurrencyUnit.Bitcoin, + val esploraEndpoint: String = "", +) + +sealed interface WalletScreenAction { + data object UpdateBalance : WalletScreenAction + + data object SwitchUnit : WalletScreenAction +} diff --git a/app/src/main/proto/wallets.proto "b/Variant \342\200\224 Esplora/app/src/main/proto/wallets.proto" similarity index 100% rename from app/src/main/proto/wallets.proto rename to "Variant \342\200\224 Esplora/app/src/main/proto/wallets.proto" diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/drawable/bdk_logo.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/drawable/bdk_logo.xml" new file mode 100644 index 0000000..a28d65b --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/drawable/bdk_logo.xml" @@ -0,0 +1,24 @@ + + + + + diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/drawable/ic_bitcoin_logo.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/drawable/ic_bitcoin_logo.xml" new file mode 100644 index 0000000..d57d1bb --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/drawable/ic_bitcoin_logo.xml" @@ -0,0 +1,12 @@ + + + + diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/drawable/ic_launcher_bdk_background.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/drawable/ic_launcher_bdk_background.xml" new file mode 100644 index 0000000..6884ff0 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/drawable/ic_launcher_bdk_background.xml" @@ -0,0 +1,9 @@ + + + + + + + diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/drawable/ic_launcher_bdk_foreground.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/drawable/ic_launcher_bdk_foreground.xml" new file mode 100644 index 0000000..227c9e3 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/drawable/ic_launcher_bdk_foreground.xml" @@ -0,0 +1,29 @@ + + + + + + + diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/drawable/ic_testnet_logo.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/drawable/ic_testnet_logo.xml" new file mode 100644 index 0000000..d57d1bb --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/drawable/ic_testnet_logo.xml" @@ -0,0 +1,12 @@ + + + + diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/drawable/launch_screen.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/drawable/launch_screen.xml" new file mode 100644 index 0000000..52e7de9 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/drawable/launch_screen.xml" @@ -0,0 +1,11 @@ + + + + + + + diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_mono_bold.ttf" "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_mono_bold.ttf" new file mode 100644 index 0000000..c9c06a2 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_mono_bold.ttf" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_mono_bold_italic.ttf" "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_mono_bold_italic.ttf" new file mode 100644 index 0000000..1030c9e Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_mono_bold_italic.ttf" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_mono_regular.ttf" "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_mono_regular.ttf" new file mode 100644 index 0000000..1308a4c Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_mono_regular.ttf" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_mono_regular_italic.ttf" "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_mono_regular_italic.ttf" new file mode 100644 index 0000000..4333952 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_mono_regular_italic.ttf" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_quattro_bold.ttf" "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_quattro_bold.ttf" new file mode 100644 index 0000000..8e83653 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_quattro_bold.ttf" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_quattro_bold_italic.ttf" "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_quattro_bold_italic.ttf" new file mode 100644 index 0000000..0ae7e61 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_quattro_bold_italic.ttf" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_quattro_regular.ttf" "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_quattro_regular.ttf" new file mode 100644 index 0000000..f8eb282 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_quattro_regular.ttf" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_quattro_regular_italic.ttf" "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_quattro_regular_italic.ttf" new file mode 100644 index 0000000..a213e85 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/font/ia_writer_quattro_regular_italic.ttf" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" new file mode 100644 index 0000000..2f8431b --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" @@ -0,0 +1,5 @@ + + + + + diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml" new file mode 100644 index 0000000..af9e637 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml" @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml" new file mode 100644 index 0000000..af9e637 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml" @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" new file mode 100644 index 0000000..2f8431b --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" @@ -0,0 +1,5 @@ + + + + + diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-hdpi/ic_launcher.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-hdpi/ic_launcher.png" new file mode 100644 index 0000000..a571e60 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-hdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..b94c1ed Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..07db04c Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-hdpi/ic_launcher_round.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-hdpi/ic_launcher_round.png" new file mode 100644 index 0000000..61da551 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-hdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-mdpi/ic_launcher.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-mdpi/ic_launcher.png" new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-mdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..d4fb078 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..efe9825 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-mdpi/ic_launcher_round.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-mdpi/ic_launcher_round.png" new file mode 100644 index 0000000..db5080a Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-mdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xhdpi/ic_launcher.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xhdpi/ic_launcher.png" new file mode 100644 index 0000000..6dba46d Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xhdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..3bde8f4 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..5d4b7d9 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png" new file mode 100644 index 0000000..da31a87 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" new file mode 100644 index 0000000..15ac681 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..66f997c Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..ad3ace1 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png" new file mode 100644 index 0000000..b216f2d Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" new file mode 100644 index 0000000..f25a419 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..00ab356 Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..28da01a Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png" "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png" new file mode 100644 index 0000000..e96783c Binary files /dev/null and "b/Variant \342\200\224 Esplora/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/values/colors.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/values/colors.xml" new file mode 100644 index 0000000..a66aa6b --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/values/colors.xml" @@ -0,0 +1,4 @@ + + + #203b46 + diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/values/splash.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/values/splash.xml" new file mode 100644 index 0000000..b712912 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/values/splash.xml" @@ -0,0 +1,8 @@ + + + + diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/values/strings.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/values/strings.xml" new file mode 100644 index 0000000..d0e88cc --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/values/strings.xml" @@ -0,0 +1,3 @@ + + Devkit Wallet + diff --git "a/Variant \342\200\224 Esplora/app/src/main/res/values/themes.xml" "b/Variant \342\200\224 Esplora/app/src/main/res/values/themes.xml" new file mode 100644 index 0000000..69b1d7e --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/main/res/values/themes.xml" @@ -0,0 +1,7 @@ + + + + diff --git "a/Variant \342\200\224 Esplora/app/src/test/java/org/bitcoindevkit/devkitwallet/ExampleUnitTest.kt" "b/Variant \342\200\224 Esplora/app/src/test/java/org/bitcoindevkit/devkitwallet/ExampleUnitTest.kt" new file mode 100644 index 0000000..636ec13 --- /dev/null +++ "b/Variant \342\200\224 Esplora/app/src/test/java/org/bitcoindevkit/devkitwallet/ExampleUnitTest.kt" @@ -0,0 +1,11 @@ +package org.bitcoindevkit.devkitwallet + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git "a/Variant \342\200\224 Esplora/gradle.properties" "b/Variant \342\200\224 Esplora/gradle.properties" new file mode 100644 index 0000000..000bd8f --- /dev/null +++ "b/Variant \342\200\224 Esplora/gradle.properties" @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.enableJetifier=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git "a/Variant \342\200\224 Esplora/gradle/wrapper/gradle-wrapper.jar" "b/Variant \342\200\224 Esplora/gradle/wrapper/gradle-wrapper.jar" new file mode 100644 index 0000000..7454180 Binary files /dev/null and "b/Variant \342\200\224 Esplora/gradle/wrapper/gradle-wrapper.jar" differ diff --git a/gradle/wrapper/gradle-wrapper.properties "b/Variant \342\200\224 Esplora/gradle/wrapper/gradle-wrapper.properties" similarity index 100% rename from gradle/wrapper/gradle-wrapper.properties rename to "Variant \342\200\224 Esplora/gradle/wrapper/gradle-wrapper.properties" diff --git "a/Variant \342\200\224 Esplora/gradlew" "b/Variant \342\200\224 Esplora/gradlew" new file mode 100755 index 0000000..744e882 --- /dev/null +++ "b/Variant \342\200\224 Esplora/gradlew" @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git "a/Variant \342\200\224 Esplora/gradlew.bat" "b/Variant \342\200\224 Esplora/gradlew.bat" new file mode 100644 index 0000000..107acd3 --- /dev/null +++ "b/Variant \342\200\224 Esplora/gradlew.bat" @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git "a/Variant \342\200\224 Esplora/justfile" "b/Variant \342\200\224 Esplora/justfile" new file mode 100644 index 0000000..c11adf2 --- /dev/null +++ "b/Variant \342\200\224 Esplora/justfile" @@ -0,0 +1,8 @@ +@list: + just --list + +check: + ./gradlew ktlintCheck + +format: + ./gradlew ktlintFormat diff --git "a/Variant \342\200\224 Esplora/settings.gradle.kts" "b/Variant \342\200\224 Esplora/settings.gradle.kts" new file mode 100644 index 0000000..7d091f4 --- /dev/null +++ "b/Variant \342\200\224 Esplora/settings.gradle.kts" @@ -0,0 +1,23 @@ +rootProject.name = "Devkit Wallet" +include("app") + +pluginManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + + // Snapshots repository + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") + + // Local Maven (~/.m2/repository/) + // mavenLocal() + } +} diff --git "a/Variant \342\200\224 Kyoto/.editorconfig" "b/Variant \342\200\224 Kyoto/.editorconfig" new file mode 100644 index 0000000..65f9d3c --- /dev/null +++ "b/Variant \342\200\224 Kyoto/.editorconfig" @@ -0,0 +1,13 @@ +# Root .editorconfig file +root = true + +[*.{kt,kts}] +indent_style = space +max_line_length = 120 +ktlint_standard_trailing-comma-on-call-site = disabled +ktlint_standard_multiline-expression-wrapping = disabled +ktlint_standard_string-template-indent = disabled +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 5 +ktlint_function_naming_ignore_when_annotated_with=Composable +ktlint_standard_function-expression-body = disabled +ktlint_standard_class-signature = disabled diff --git "a/Variant \342\200\224 Kyoto/.gitignore" "b/Variant \342\200\224 Kyoto/.gitignore" new file mode 100644 index 0000000..9e65d19 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/.gitignore" @@ -0,0 +1,20 @@ +*.iml +.gradle +/.idea/ +.DS_Store +/build +/app/build/ +/captures +.externalNativeBuild +.cxx +app-simple-wallet/local.properties +app-advanced-features/local.properties +app-ui-only/local.properties +app-simple-wallet/app/build/ +app/build/ +app-ui-only/app/build/ +app-clean/ +.idea/ +local.properties +app.run.xml +release/ diff --git "a/Variant \342\200\224 Kyoto/LICENSE" "b/Variant \342\200\224 Kyoto/LICENSE" new file mode 100644 index 0000000..8061725 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/LICENSE" @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2021 thunderbiscuit and contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git "a/Variant \342\200\224 Kyoto/app/build.gradle.kts" "b/Variant \342\200\224 Kyoto/app/build.gradle.kts" new file mode 100644 index 0000000..4ba3830 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/build.gradle.kts" @@ -0,0 +1,116 @@ +import org.jlleitschuh.gradle.ktlint.reporter.ReporterType + +plugins { + id("com.android.application") version "8.7.1" + id("org.jetbrains.kotlin.android") version "2.1.10" + id("org.jetbrains.kotlin.plugin.compose") version "2.1.10" + id("org.jetbrains.kotlin.plugin.serialization") version "2.1.10" + id("com.google.protobuf") version "0.9.4" + id("org.jlleitschuh.gradle.ktlint") version "12.1.2" +} + +// This is the version of the app that is displayed in the UI on the drawer. +val variantName = "Version 0.1.0/Kyoto" + +android { + namespace = "org.bitcoindevkit.devkitwallet" + compileSdk = 35 + + buildFeatures { + viewBinding = true + compose = true + } + + defaultConfig { + applicationId = "org.bitcoindevkit.devkitwallet" + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "v0.1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField("String", "VARIANT_NAME", "\"$variantName\"") + } + + buildTypes { + getByName("debug") { + isDebuggable = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + // Basic android dependencies + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.23") + implementation("androidx.core:core-ktx:1.13.1") + implementation("com.google.android.material:material:1.12.0") + implementation("androidx.datastore:datastore:1.1.1") + implementation("com.google.protobuf:protobuf-javalite:3.18.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation("androidx.core:core-splashscreen:1.0.1") + + // Jetpack Compose + // Adding the Bill of Materials synchronizes dependencies in the androidx.compose namespace + // You can remove the library version in your dependency declarations + implementation(platform("androidx.compose:compose-bom:2025.02.00")) + implementation("androidx.compose.animation:animation") + implementation("androidx.compose.ui:ui-tooling") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.material3:material3") + implementation("androidx.activity:activity-compose") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5") + implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1") + implementation("androidx.navigation:navigation-compose:2.8.0") + implementation("com.google.accompanist:accompanist-systemuicontroller:0.23.1") + + // Icons + implementation("androidx.compose.material:material-icons-extended:1.7.8") + implementation("com.composables:icons-lucide:1.0.0") + + // Toolbar + implementation("androidx.appcompat:appcompat:1.7.0") + + // Bitcoin Development Kit + implementation("org.bitcoindevkit:bdk-android:1.2.0") + + // QR codes + implementation("com.google.zxing:core:3.5.3") + + // Tests + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.25.0" + } + + generateProtoTasks { + all().forEach { task -> + task.builtins { + create("java") { + option("lite") + } + } + } + } +} + +ktlint { + version = "1.5.0" + ignoreFailures = false + reporters { + reporter(ReporterType.PLAIN).apply { outputToConsole = true } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/proguard-rules.pro" "b/Variant \342\200\224 Kyoto/app/proguard-rules.pro" new file mode 100644 index 0000000..ff59496 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/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.kts. +# +# 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/Variant \342\200\224 Kyoto/app/src/main/AndroidManifest.xml" "b/Variant \342\200\224 Kyoto/app/src/main/AndroidManifest.xml" new file mode 100644 index 0000000..cbe7c27 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/AndroidManifest.xml" @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + diff --git "a/Variant \342\200\224 Kyoto/app/src/main/assets/bip39-english.txt" "b/Variant \342\200\224 Kyoto/app/src/main/assets/bip39-english.txt" new file mode 100644 index 0000000..f78ccaf --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/assets/bip39-english.txt" @@ -0,0 +1,2048 @@ +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 \ No newline at end of file diff --git "a/Variant \342\200\224 Kyoto/app/src/main/ic_launcher_bdk-playstore.png" "b/Variant \342\200\224 Kyoto/app/src/main/ic_launcher_bdk-playstore.png" new file mode 100644 index 0000000..b834711 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/ic_launcher_bdk-playstore.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/data/TxDetails.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/data/TxDetails.kt" new file mode 100644 index 0000000..91325d9 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/data/TxDetails.kt" @@ -0,0 +1,22 @@ +package org.bitcoindevkit.devkitwallet.data + +import org.bitcoindevkit.FeeRate +import org.bitcoindevkit.Transaction + +data class TxDetails( + val transaction: Transaction, + val txid: String, + val sent: ULong, + val received: ULong, + val fee: ULong, + val feeRate: FeeRate?, + val pending: Boolean, + val confirmationBlock: ConfirmationBlock?, + val confirmationTimestamp: Timestamp?, +) + +@JvmInline +value class Timestamp(val timestamp: ULong) + +@JvmInline +value class ConfirmationBlock(val height: UInt) diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/data/UserPreferencesSerializer.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/data/UserPreferencesSerializer.kt" new file mode 100644 index 0000000..6b4b50f --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/data/UserPreferencesSerializer.kt" @@ -0,0 +1,28 @@ +/* + * Copyright 2021-2024 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 androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream + +object UserPreferencesSerializer : Serializer { + override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): UserPreferences { + try { + return UserPreferences.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo(t: UserPreferences, output: OutputStream) { + t.writeTo(output) + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/data/WalletConfigs.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/data/WalletConfigs.kt" new file mode 100644 index 0000000..c0ebe5d --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/data/WalletConfigs.kt" @@ -0,0 +1,24 @@ +/* + * Copyright 2021-2024 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 org.bitcoindevkit.Descriptor +import org.bitcoindevkit.Network + +data class NewWalletConfig( + val name: String, + val network: Network, + val scriptType: ActiveWalletScriptType, +) + +data class RecoverWalletConfig( + val name: String, + val network: Network, + val scriptType: ActiveWalletScriptType?, + val recoveryPhrase: String?, + val descriptor: Descriptor, + val changeDescriptor: Descriptor, +) diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/ActiveWalletsRepository.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/ActiveWalletsRepository.kt" new file mode 100644 index 0000000..4ba3964 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/ActiveWalletsRepository.kt" @@ -0,0 +1,53 @@ +/* + * Copyright 2021-2024 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 androidx.datastore.core.DataStore +import kotlinx.coroutines.flow.first +import org.bitcoindevkit.devkitwallet.data.SingleWallet +import org.bitcoindevkit.devkitwallet.data.UserPreferences + +class UserPreferencesRepository( + private val userPreferencesStore: DataStore, +) { + suspend fun fetchIntroDone(): Boolean { + return userPreferencesStore.data.first().introDone + } + + suspend fun setIntroDone() { + userPreferencesStore.updateData { currentPreferences -> + currentPreferences.toBuilder().setIntroDone(true).build() + } + } + + suspend fun fetchActiveWallets(): List { + return userPreferencesStore.data.first().walletsList + } + + suspend fun updateActiveWallets(singleWallet: SingleWallet) { + userPreferencesStore.updateData { currentPreferences -> + currentPreferences.toBuilder().addWallets(singleWallet).build() + } + } + + suspend fun setFullScanCompleted(walletId: String) { + userPreferencesStore.updateData { currentPreferences -> + val updatedWalletsList = + currentPreferences.walletsList.map { wallet -> + if (wallet.id == walletId) { + wallet.toBuilder().setFullScanCompleted(true).build() + } else { + wallet + } + } + currentPreferences + .toBuilder() + .clearWallets() + .addAllWallets(updatedWalletsList) + .build() + } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt" new file mode 100644 index 0000000..ec4b798 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClient.kt" @@ -0,0 +1,63 @@ +/* + * Copyright 2021-2024 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/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt" new file mode 100644 index 0000000..e2f6178 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/BlockchainClientsConfig.kt" @@ -0,0 +1,58 @@ +/* + * Copyright 2021-2024 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/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Constants.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Constants.kt" new file mode 100644 index 0000000..154659c --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Constants.kt" @@ -0,0 +1,2062 @@ +package org.bitcoindevkit.devkitwallet.domain + +import org.bitcoindevkit.Network + +val supportedNetworks: List = + listOf( + Network.SIGNET, + Network.TESTNET, + Network.REGTEST, + ) + +val bip39WordList: List = + listOf( + "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", + ) diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/CurrencyUnit.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/CurrencyUnit.kt" new file mode 100644 index 0000000..c1bc8d5 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/CurrencyUnit.kt" @@ -0,0 +1,11 @@ +/* + * Copyright 2021-2024 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 + +enum class CurrencyUnit { + Bitcoin, + Satoshi, +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/DwLogger.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/DwLogger.kt" new file mode 100644 index 0000000..55d072d --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/DwLogger.kt" @@ -0,0 +1,49 @@ +package org.bitcoindevkit.devkitwallet.domain + +import java.time.Instant +import java.time.ZoneId +import java.time.temporal.ChronoUnit + +object DwLogger { + private const val MAX_LOGS = 5000 + private val logEntries = ArrayDeque(MAX_LOGS) + private val lock = Any() + + fun log(tag: LogLevel, message: String) { + synchronized(lock) { + if (logEntries.size >= MAX_LOGS) { + logEntries.removeLast() + } + val millis = System.currentTimeMillis() + val dateTime = + Instant + .ofEpochMilli(millis) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime() + .truncatedTo(ChronoUnit.SECONDS) + + logEntries.addFirst("$dateTime $tag $message") + } + } + + fun getLogs(): List { + synchronized(lock) { + return logEntries.toList() + } + } + + enum class LogLevel { + INFO, + WARN, + ERROR, + ; + + override fun toString(): String { + return when (this) { + INFO -> "[INFO] " + WARN -> "[WARN] " + ERROR -> "[ERROR]" + } + } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/ElectrumServer.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/ElectrumServer.kt" new file mode 100644 index 0000000..bfe7342 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/ElectrumServer.kt" @@ -0,0 +1,74 @@ +/* + * Copyright 2021-2024 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/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt" new file mode 100644 index 0000000..8df4428 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt" @@ -0,0 +1,400 @@ +/* + * Copyright 2021-2024 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 android.util.Log +import kotlinx.coroutines.runBlocking +import org.bitcoindevkit.Address +import org.bitcoindevkit.AddressInfo +import org.bitcoindevkit.Amount +import org.bitcoindevkit.CanonicalTx +import org.bitcoindevkit.ChainPosition +import org.bitcoindevkit.Connection +import org.bitcoindevkit.Descriptor +import org.bitcoindevkit.DescriptorSecretKey +import org.bitcoindevkit.FeeRate +import org.bitcoindevkit.IpAddress +import org.bitcoindevkit.KeychainKind +import org.bitcoindevkit.CbfBuilder +import org.bitcoindevkit.Mnemonic +import org.bitcoindevkit.Network +import org.bitcoindevkit.Peer +import org.bitcoindevkit.Psbt +import org.bitcoindevkit.ScanType +import org.bitcoindevkit.Script +import org.bitcoindevkit.TxBuilder +import org.bitcoindevkit.Update +import org.bitcoindevkit.WordCount +import org.bitcoindevkit.devkitwallet.data.ActiveWalletScriptType +import org.bitcoindevkit.devkitwallet.data.ConfirmationBlock +import org.bitcoindevkit.devkitwallet.data.NewWalletConfig +import org.bitcoindevkit.devkitwallet.data.RecoverWalletConfig +import org.bitcoindevkit.devkitwallet.data.SingleWallet +import org.bitcoindevkit.devkitwallet.data.Timestamp +import org.bitcoindevkit.devkitwallet.data.TxDetails +import org.bitcoindevkit.devkitwallet.domain.utils.intoDomain +import org.bitcoindevkit.devkitwallet.domain.utils.intoProto +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.Recipient +import org.bitcoindevkit.CbfClient +import java.util.UUID +import org.bitcoindevkit.Wallet as BdkWallet + +private const val TAG = "Wallet" + +class Wallet private constructor( + private val wallet: BdkWallet, + private val walletSecrets: WalletSecrets, + private val connection: Connection, + private var fullScanCompleted: Boolean, + private val walletId: String, + private val userPreferencesRepository: UserPreferencesRepository, + private val internalAppFilesPath: String, + blockchainClientsConfig: BlockchainClientsConfig, +) { + private var currentBlockchainClient: BlockchainClient? = blockchainClientsConfig.getClient() + public var kyotoClient: CbfClient? = null + + fun getWalletSecrets(): WalletSecrets { + return walletSecrets + } + + fun createTransaction(recipientList: List, feeRate: FeeRate, opReturnMsg: String?): Psbt { + // technique 1 for adding a list of recipients to the TxBuilder + // var txBuilder = TxBuilder() + // for (recipient in recipientList) { + // txBuilder = txBuilder.addRecipient(address = recipient.first, amount = recipient.second) + // } + // txBuilder = txBuilder.feeRate(satPerVbyte = fee_rate) + + // technique 2 for adding a list of recipients to the TxBuilder + var txBuilder = + recipientList.fold(TxBuilder()) { builder, recipient -> + // val address = Address(recipient.address) + val scriptPubKey: Script = Address(recipient.address, Network.TESTNET).scriptPubkey() + builder.addRecipient(scriptPubKey, Amount.fromSat(recipient.amount)) + } + // if (!opReturnMsg.isNullOrEmpty()) { + // txBuilder = txBuilder.addData(opReturnMsg.toByteArray(charset = Charsets.UTF_8).asUByteArray().toList()) + // } + return txBuilder.feeRate(feeRate).finish(wallet) + } + + // @OptIn(ExperimentalUnsignedTypes::class) + // fun createSendAllTransaction( + // recipient: String, + // feeRate: Float, + // enableRBF: Boolean, + // opReturnMsg: String? + // ): PartiallySignedTransaction { + // val scriptPubkey: Script = Address(recipient).scriptPubkey() + // var txBuilder = TxBuilder() + // .drainWallet() + // .drainTo(scriptPubkey) + // .feeRate(satPerVbyte = feeRate) + // + // if (enableRBF) { + // txBuilder = txBuilder.enableRbf() + // } + // if (!opReturnMsg.isNullOrEmpty()) { + // txBuilder = txBuilder.addData(opReturnMsg.toByteArray(charset = Charsets.UTF_8).asUByteArray().toList()) + // } + // return txBuilder.finish(wallet).psbt + // } + + // fun createBumpFeeTransaction(txid: String, feeRate: Float): PartiallySignedTransaction { + // return BumpFeeTxBuilder(txid = txid, newFeeRate = feeRate) + // .enableRbf() + // .finish(wallet = wallet) + // } + + fun sign(psbt: Psbt): Boolean { + return wallet.sign(psbt) + } + + fun broadcast(signedPsbt: Psbt): String { + currentBlockchainClient?.broadcast(signedPsbt.extractTx()) + ?: throw IllegalStateException("Blockchain client not initialized") + return signedPsbt.extractTx().computeTxid() + } + + private fun getAllTransactions(): List = wallet.transactions() + + fun getAllTxDetails(): List { + val transactions = getAllTransactions() + return transactions.map { tx -> + val txid = tx.transaction.computeTxid() + val (sent, received) = wallet.sentAndReceived(tx.transaction) + var feeRate: FeeRate? = null + var fee: Amount? = null + // TODO: I don't know why we're getting negative fees here, but it looks like a bug + try { + fee = wallet.calculateFee(tx.transaction) + } catch (e: Exception) { + Log.e(TAG, "Error calculating fee rate for tx $txid: $e") + } + try { + feeRate = wallet.calculateFeeRate(tx.transaction) + } catch (e: Exception) { + Log.e(TAG, "Error calculating fee for tx $txid: $e") + } + + val (confirmationBlock, confirmationTimestamp, pending) = + when (val position = tx.chainPosition) { + is ChainPosition.Unconfirmed -> Triple(null, null, true) + is ChainPosition.Confirmed -> + Triple( + ConfirmationBlock(position.confirmationBlockTime.blockId.height), + Timestamp(position.confirmationBlockTime.confirmationTime), + false, + ) + } + TxDetails( + tx.transaction, + txid, + sent.toSat(), + received.toSat(), + fee?.toSat() ?: 0uL, + feeRate, + pending, + confirmationBlock, + confirmationTimestamp + ) + } + } + + // fun getTransaction(txid: String): TransactionDetails? { + // val allTransactions = getAllTransactions() + // allTransactions.forEach { + // if (it.txid == txid) { + // return it + // } + // } + // return null + // } + + fun getBalance(): ULong = wallet.balance().total.toSat() + + fun getNewAddress(): AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL) + + fun startKyotoNode() { + Log.i(TAG, "Starting Kyoto node") + // val ip: IpAddress = IpAddress.fromIpv4(68u, 47u, 229u, 218u) // Signet + val ip: IpAddress = IpAddress.fromIpv4(10u, 0u, 2u, 2u) // Regtest + val peer1: Peer = Peer(ip, 18444u, false) // Regtest + // 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.New) + .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) + } + + companion object { + fun createWallet( + newWalletConfig: NewWalletConfig, + internalAppFilesPath: String, + userPreferencesRepository: UserPreferencesRepository, + ): Wallet { + val mnemonic = Mnemonic(WordCount.WORDS12) + val bip32ExtendedRootKey = DescriptorSecretKey(newWalletConfig.network, mnemonic, null) + val descriptor: Descriptor = + createScriptAppropriateDescriptor( + newWalletConfig.scriptType, + bip32ExtendedRootKey, + newWalletConfig.network, + KeychainKind.EXTERNAL, + ) + val changeDescriptor: Descriptor = + createScriptAppropriateDescriptor( + newWalletConfig.scriptType, + bip32ExtendedRootKey, + newWalletConfig.network, + KeychainKind.INTERNAL, + ) + val walletId = UUID.randomUUID().toString() + val connection = Connection("$internalAppFilesPath/wallet-${walletId.take(8)}.sqlite3") + + // Create SingleWallet object for saving to datastore + val newWalletForDatastore: SingleWallet = + SingleWallet + .newBuilder() + .setId(walletId) + .setName(newWalletConfig.name) + .setNetwork(newWalletConfig.network.intoProto()) + .setScriptType(ActiveWalletScriptType.P2WPKH) + .setDescriptor(descriptor.toStringWithSecret()) + .setChangeDescriptor(changeDescriptor.toStringWithSecret()) + .setRecoveryPhrase(mnemonic.toString()) + .build() + + // TODO: launch this correctly, not on the main thread + // Save the new wallet to the datastore + runBlocking { userPreferencesRepository.updateActiveWallets(newWalletForDatastore) } + + val bdkWallet = + BdkWallet( + descriptor = descriptor, + changeDescriptor = changeDescriptor, + network = newWalletConfig.network, + connection = connection, + ) + + val walletSecrets = WalletSecrets(descriptor, changeDescriptor, mnemonic.toString()) + + return Wallet( + wallet = bdkWallet, + walletSecrets = walletSecrets, + connection = connection, + fullScanCompleted = false, + walletId = walletId, + userPreferencesRepository = userPreferencesRepository, + internalAppFilesPath = internalAppFilesPath, + blockchainClientsConfig = BlockchainClientsConfig.createDefaultConfig(newWalletConfig.network), + ) + } + + fun loadActiveWallet( + activeWallet: SingleWallet, + internalAppFilesPath: String, + userPreferencesRepository: UserPreferencesRepository, + ): Wallet { + val descriptor = Descriptor(activeWallet.descriptor, activeWallet.network.intoDomain()) + val changeDescriptor = Descriptor(activeWallet.changeDescriptor, activeWallet.network.intoDomain()) + val connection = Connection("$internalAppFilesPath/wallet-${activeWallet.id.take(8)}.sqlite3") + val bdkWallet = + BdkWallet.load( + descriptor = descriptor, + changeDescriptor = changeDescriptor, + connection = connection, + ) + + val walletSecrets = WalletSecrets(descriptor, changeDescriptor, activeWallet.recoveryPhrase) + return Wallet( + wallet = bdkWallet, + walletSecrets = walletSecrets, + connection = connection, + fullScanCompleted = activeWallet.fullScanCompleted, + walletId = activeWallet.id, + userPreferencesRepository = userPreferencesRepository, + internalAppFilesPath = internalAppFilesPath, + blockchainClientsConfig = BlockchainClientsConfig.createDefaultConfig( + activeWallet.network.intoDomain() + ), + ) + } + + fun recoverWallet( + recoverWalletConfig: RecoverWalletConfig, + internalAppFilesPath: String, + userPreferencesRepository: UserPreferencesRepository, + ): Wallet { + Log.i(TAG, "Recovering wallet with config: $recoverWalletConfig") + var descriptor: Descriptor? = null + var changeDescriptor: Descriptor? = null + var mnemonicString: String = "" + + // If there is a recovery phrase, we use it to recover the wallet + if (recoverWalletConfig.recoveryPhrase != null && recoverWalletConfig.scriptType != null) { + val mnemonic: Mnemonic = Mnemonic.fromString(recoverWalletConfig.recoveryPhrase) + mnemonicString = mnemonic.toString() + val bip32ExtendedRootKey = DescriptorSecretKey(recoverWalletConfig.network, mnemonic, null) + descriptor = + createScriptAppropriateDescriptor( + recoverWalletConfig.scriptType, + bip32ExtendedRootKey, + recoverWalletConfig.network, + KeychainKind.EXTERNAL, + ) + changeDescriptor = + createScriptAppropriateDescriptor( + recoverWalletConfig.scriptType, + bip32ExtendedRootKey, + recoverWalletConfig.network, + KeychainKind.INTERNAL, + ) + } else { + descriptor = recoverWalletConfig.descriptor + changeDescriptor = recoverWalletConfig.changeDescriptor + } + val walletId = UUID.randomUUID().toString() + val connection = Connection("$internalAppFilesPath/wallet-${walletId.take(8)}.sqlite3") + + // Create SingleWallet object for saving to datastore + val newWalletForDatastore: SingleWallet = + SingleWallet + .newBuilder() + .setId(walletId) + .setName(recoverWalletConfig.name) + .setNetwork(recoverWalletConfig.network.intoProto()) + .setScriptType(recoverWalletConfig.scriptType ?: ActiveWalletScriptType.UNKNOWN) + .setDescriptor(descriptor.toStringWithSecret()) + .setChangeDescriptor(changeDescriptor.toStringWithSecret()) + .setRecoveryPhrase(mnemonicString) + .build() + + // TODO: launch this correctly, not on the main thread + // Save the new wallet to the datastore + runBlocking { userPreferencesRepository.updateActiveWallets(newWalletForDatastore) } + + val bdkWallet = + BdkWallet( + descriptor = descriptor, + changeDescriptor = changeDescriptor, + connection = connection, + network = recoverWalletConfig.network, + ) + + val walletSecrets = WalletSecrets(descriptor, changeDescriptor, mnemonicString) + return Wallet( + wallet = bdkWallet, + walletSecrets = walletSecrets, + connection = connection, + fullScanCompleted = false, + walletId = walletId, + userPreferencesRepository = userPreferencesRepository, + internalAppFilesPath = internalAppFilesPath, + blockchainClientsConfig = BlockchainClientsConfig.createDefaultConfig(recoverWalletConfig.network), + ) + } + } +} + +fun createScriptAppropriateDescriptor( + scriptType: ActiveWalletScriptType, + bip32ExtendedRootKey: DescriptorSecretKey, + network: Network, + keychain: KeychainKind, +): Descriptor { + return when (scriptType) { + ActiveWalletScriptType.P2WPKH -> Descriptor.newBip84(bip32ExtendedRootKey, keychain, network) + ActiveWalletScriptType.P2TR -> Descriptor.newBip86(bip32ExtendedRootKey, keychain, network) + ActiveWalletScriptType.UNKNOWN -> TODO() + ActiveWalletScriptType.UNRECOGNIZED -> TODO() + } +} + +data class WalletSecrets( + val descriptor: Descriptor, + val changeDescriptor: Descriptor, + val recoveryPhrase: String, +) diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/FormatInBtc.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/FormatInBtc.kt" new file mode 100644 index 0000000..3ce6bc7 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/FormatInBtc.kt" @@ -0,0 +1,18 @@ +/* + * Copyright 2021-2024 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.utils + +import java.text.DecimalFormat + +fun ULong?.formatInBtc(): String { + val balanceInSats = + if (this == 0UL || this == null) { + 0F + } else { + this.toDouble().div(100_000_000) + } + return DecimalFormat("0.00000000").format(balanceInSats) +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/ProtobufExtensions.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/ProtobufExtensions.kt" new file mode 100644 index 0000000..6d2d5a9 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/ProtobufExtensions.kt" @@ -0,0 +1,28 @@ +/* + * Copyright 2021-2024 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.utils + +import org.bitcoindevkit.Network +import org.bitcoindevkit.devkitwallet.data.ActiveWalletNetwork + +fun Network.intoProto(): ActiveWalletNetwork { + return when (this) { + Network.TESTNET -> ActiveWalletNetwork.TESTNET + Network.TESTNET4 -> throw IllegalArgumentException("Bitcoin testnet 4 network is not supported") + Network.SIGNET -> ActiveWalletNetwork.SIGNET + Network.REGTEST -> ActiveWalletNetwork.REGTEST + Network.BITCOIN -> throw IllegalArgumentException("Bitcoin mainnet network is not supported") + } +} + +fun ActiveWalletNetwork.intoDomain(): Network { + return when (this) { + ActiveWalletNetwork.TESTNET -> Network.TESTNET + ActiveWalletNetwork.SIGNET -> Network.SIGNET + ActiveWalletNetwork.REGTEST -> Network.REGTEST + ActiveWalletNetwork.UNRECOGNIZED -> throw IllegalArgumentException("Unrecognized network") + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/Timestamps.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/Timestamps.kt" new file mode 100644 index 0000000..349331b --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/domain/utils/Timestamps.kt" @@ -0,0 +1,17 @@ +/* + * Copyright 2021-2024 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.utils + +import android.text.format.DateFormat +import java.util.Calendar +import java.util.Locale + +// extension function on the ULong timestamp provided in the Transaction.Confirmed type +fun ULong.timestampToString(): String { + val calendar = Calendar.getInstance(Locale.ENGLISH) + calendar.timeInMillis = (this * 1000u).toLong() + return DateFormat.format("MMMM d yyyy HH:mm", calendar).toString() +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt" new file mode 100644 index 0000000..d639dbd --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/DevkitWalletActivity.kt" @@ -0,0 +1,122 @@ +/* + * Copyright 2021-2024 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.presentation + +import android.content.Context +import android.os.Bundle +import android.util.Log +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.datastore.core.DataStore +import androidx.datastore.dataStore +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import org.bitcoindevkit.devkitwallet.data.NewWalletConfig +import org.bitcoindevkit.devkitwallet.data.RecoverWalletConfig +import org.bitcoindevkit.devkitwallet.data.SingleWallet +import org.bitcoindevkit.devkitwallet.data.UserPreferences +import org.bitcoindevkit.devkitwallet.data.UserPreferencesSerializer +import org.bitcoindevkit.devkitwallet.domain.DwLogger +import org.bitcoindevkit.devkitwallet.domain.DwLogger.LogLevel.INFO +import org.bitcoindevkit.devkitwallet.domain.UserPreferencesRepository +import org.bitcoindevkit.devkitwallet.domain.Wallet +import org.bitcoindevkit.devkitwallet.presentation.navigation.CreateWalletNavigation +import org.bitcoindevkit.devkitwallet.presentation.navigation.HomeNavigation +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitTheme +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.intro.OnboardingScreen + +private const val TAG = "DevkitWalletActivity" +private val Context.userPreferencesStore: DataStore by dataStore( + fileName = "user_preferences.pb", + serializer = UserPreferencesSerializer, +) + +class DevkitWalletActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + + super.onCreate(savedInstanceState) + + // Initialize Devkit Wallet Logger (used in the LogsScreen) + DwLogger.log(INFO, "Devkit Wallet app started") + + val userPreferencesRepository = UserPreferencesRepository(userPreferencesStore) + val onBuildWalletButtonClicked: (WalletCreateType) -> Unit = { walletCreateType -> + try { + val activeWallet = + when (walletCreateType) { + is WalletCreateType.FROMSCRATCH -> + Wallet.createWallet( + newWalletConfig = walletCreateType.newWalletConfig, + internalAppFilesPath = filesDir.absolutePath, + userPreferencesRepository = userPreferencesRepository, + ) + is WalletCreateType.LOADEXISTING -> + Wallet.loadActiveWallet( + activeWallet = walletCreateType.activeWallet, + internalAppFilesPath = filesDir.absolutePath, + userPreferencesRepository = userPreferencesRepository, + ) + is WalletCreateType.RECOVER -> + Wallet.recoverWallet( + recoverWalletConfig = walletCreateType.recoverWalletConfig, + internalAppFilesPath = filesDir.absolutePath, + userPreferencesRepository = userPreferencesRepository, + ) + } + setContent { + DevkitTheme { + HomeNavigation(activeWallet) + } + } + } catch (e: Throwable) { + Log.i(TAG, "Could not build wallet: $e") + } + } + + lifecycleScope.launch { + val activeWallets = + async { + userPreferencesRepository.fetchActiveWallets() + }.await() + + val onboardingDone = + async { + userPreferencesRepository.fetchIntroDone() + }.await() + + val onFinishOnboarding: () -> Unit = { + lifecycleScope.launch { userPreferencesRepository.setIntroDone() } + setContent { + DevkitTheme { + CreateWalletNavigation(onBuildWalletButtonClicked, activeWallets) + } + } + } + + setContent { + if (!onboardingDone) { + DwLogger.log(INFO, "First time opening the app, triggering onboarding screen") + OnboardingScreen(onFinishOnboarding) + } else { + DevkitTheme { + CreateWalletNavigation(onBuildWalletButtonClicked, activeWallets) + } + } + } + } + } +} + +sealed class WalletCreateType { + data class FROMSCRATCH(val newWalletConfig: NewWalletConfig) : WalletCreateType() + + data class LOADEXISTING(val activeWallet: SingleWallet) : WalletCreateType() + + data class RECOVER(val recoverWalletConfig: RecoverWalletConfig) : WalletCreateType() +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/CreateWalletNavigation.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/CreateWalletNavigation.kt" new file mode 100644 index 0000000..3e5a24b --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/CreateWalletNavigation.kt" @@ -0,0 +1,133 @@ +/* + * Copyright 2021-2024 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.presentation.navigation + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import org.bitcoindevkit.devkitwallet.data.SingleWallet +import org.bitcoindevkit.devkitwallet.presentation.WalletCreateType +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 + +@Composable +fun CreateWalletNavigation(onBuildWalletButtonClicked: (WalletCreateType) -> Unit, activeWallets: List) { + val navController: NavHostController = rememberNavController() + val animationDuration = 400 + + NavHost( + navController = navController, + startDestination = WalletChoiceScreen, + ) { + composable( + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + }, + ) { WalletChoiceScreen(navController = navController) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + }, + ) { + ActiveWalletsScreen( + activeWallets = activeWallets, + navController = navController, + onBuildWalletButtonClicked + ) + } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + }, + ) { CreateNewWalletScreen(navController = navController, onBuildWalletButtonClicked) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + }, + ) { RecoverWalletScreen(onAction = onBuildWalletButtonClicked, navController = navController) } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/Destinations.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/Destinations.kt" new file mode 100644 index 0000000..195c3e0 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/Destinations.kt" @@ -0,0 +1,56 @@ +/* + * Copyright 2021-2024 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.presentation.navigation + +import kotlinx.serialization.Serializable + +// Create wallet navigation destinations +@Serializable +object WalletChoiceScreen + +@Serializable +object ActiveWalletsScreen + +@Serializable +object CreateNewWalletScreen + +@Serializable +object WalletRecoveryScreen + +// Home navigation destinations +@Serializable +object WalletScreen + +@Serializable +object AboutScreen + +@Serializable +object RecoveryPhraseScreen + +@Serializable +object BlockchainClientScreen + +@Serializable +object LogsScreen + +// Wallet navigation destinations +@Serializable +object HomeScreen + +@Serializable +object ReceiveScreen + +@Serializable +object SendScreen + +@Serializable +object TransactionHistoryScreen + +@Serializable +data class TransactionScreen(val txid: String) + +@Serializable +data class RbfScreen(val txid: String) diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/HomeNavigation.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/HomeNavigation.kt" new file mode 100644 index 0000000..0c49997 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/HomeNavigation.kt" @@ -0,0 +1,189 @@ +/* + * Copyright 2021-2024 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.presentation.navigation + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import org.bitcoindevkit.devkitwallet.domain.Wallet +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.WalletRoot +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.viewmodels.WalletViewModel + +@Composable +fun HomeNavigation(activeWallet: Wallet) { + val navController: NavHostController = rememberNavController() + val animationDuration = 400 + val walletViewModel = WalletViewModel(activeWallet) + + NavHost( + navController = navController, + startDestination = WalletScreen, + ) { + composable( + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(animationDuration) + ) + }, + ) { WalletRoot(activeWallet = activeWallet, walletViewModel = walletViewModel, navController = navController) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + ) { AboutScreen(navController = navController) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + ) { RecoveryDataScreen(activeWallet.getWalletSecrets(), navController = navController) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + ) { + BlockchainClientScreen( + state = walletViewModel.state, + onAction = walletViewModel::onAction, + navController = navController, + ) + } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + ) { LogsScreen(navController = navController) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(animationDuration) + ) + }, + ) { LogsScreen(navController = navController) } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/WalletNavigation.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/WalletNavigation.kt" new file mode 100644 index 0000000..990e15c --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/navigation/WalletNavigation.kt" @@ -0,0 +1,210 @@ +/* + * Copyright 2021-2024 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.presentation.navigation + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.material3.DrawerState +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute +import org.bitcoindevkit.devkitwallet.domain.Wallet +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 +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.wallet.TransactionHistoryScreen +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.wallet.TransactionScreen +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.wallet.WalletHomeScreen +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.AddressViewModel +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.SendViewModel +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.WalletViewModel + +private const val ANIMATION_DURATION: Int = 400 + +@Composable +internal fun WalletNavigation(drawerState: DrawerState, walletViewModel: WalletViewModel, activeWallet: Wallet) { + val navController: NavHostController = rememberNavController() + // val walletViewModel = WalletViewModel(activeWallet) + val addressViewModel = AddressViewModel(activeWallet) + val sendViewModel = SendViewModel(activeWallet) + + NavHost( + navController = navController, + startDestination = HomeScreen, + ) { + composable( + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + ) { + WalletHomeScreen( + state = walletViewModel.state, + onAction = walletViewModel::onAction, + drawerState = drawerState, + navController = navController, + ) + } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + ) { + ReceiveScreen( + state = addressViewModel.state, + onAction = addressViewModel::onAction, + navController = navController, + ) + } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + ) { SendScreen(navController, sendViewModel) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + ) { + val args = it.toRoute() + RBFScreen(args.txid, navController) + } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + ) { TransactionHistoryScreen(navController, activeWallet) } + + composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(ANIMATION_DURATION) + ) + }, + ) { + val args = it.toRoute() + TransactionScreen(args.txid, navController) + } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/DevkitWalletColors.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/DevkitWalletColors.kt" new file mode 100644 index 0000000..69c6819 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/DevkitWalletColors.kt" @@ -0,0 +1,20 @@ +/* + * Copyright 2021-2024 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.presentation.theme + +import androidx.compose.ui.graphics.Color + +object DevkitWalletColors { + val primaryDark: Color = Color(0xFF203B46) // App bar + val primary: Color = Color(0xFF264653) // Background + val primaryLight: Color = Color(0xFF335F70) // Behind balance primary light + val white: Color = Color(0xffffffff) // Most text + val secondary: Color = Color(0xFF2A9D8F) // Buttons + val accent1: Color = Color(0xFFE9C46A) // Receive button + val accent2: Color = Color(0xFFE76F51) // Send button +} + +internal val TestPink = Color(0xffff1493) diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Fonts.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Fonts.kt" new file mode 100644 index 0000000..ff1e557 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Fonts.kt" @@ -0,0 +1,48 @@ +/* + * Copyright 2021-2024 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.presentation.theme + +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import org.bitcoindevkit.devkitwallet.R + +val quattroRegular = + FontFamily( + Font( + resId = R.font.ia_writer_quattro_regular, + weight = FontWeight.Normal, + style = FontStyle.Normal, + ), + ) + +val quattroBold = + FontFamily( + Font( + resId = R.font.ia_writer_quattro_bold, + weight = FontWeight.Bold, + style = FontStyle.Normal, + ), + ) + +val monoRegular = + FontFamily( + Font( + resId = R.font.ia_writer_mono_regular, + weight = FontWeight.Normal, + style = FontStyle.Normal, + ), + ) + +val monoBold = + FontFamily( + Font( + resId = R.font.ia_writer_mono_bold, + weight = FontWeight.Bold, + style = FontStyle.Normal, + ), + ) diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Theme.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Theme.kt" new file mode 100644 index 0000000..25f305d --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Theme.kt" @@ -0,0 +1,22 @@ +/* + * Copyright 2021-2024 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.presentation.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable + +@Composable +fun DevkitTheme(content: @Composable () -> Unit) { + MaterialTheme( + // colorScheme = devkitColors, + // shapes = devkitShapes, + typography = devkitTypography, + content = content, + ) +} + +// NOTES ON THE UI +// - The standard padding is 32dp for start/end, 16dp for top/bottom diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Type.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Type.kt" new file mode 100644 index 0000000..2d794f3 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/theme/Type.kt" @@ -0,0 +1,42 @@ +/* + * Copyright 2021-2024 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.presentation.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +internal val devkitTypography = + Typography( + labelLarge = + TextStyle( + fontFamily = quattroRegular, + fontWeight = FontWeight.Normal, + color = DevkitWalletColors.white, + fontSize = 16.sp, + lineHeight = 28.sp, + ), + ) + +val introText = + TextStyle( + fontFamily = quattroRegular, + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + lineHeight = 28.sp, + ) + +// These are the default text styles used by Material3 components: +// Buttons: labelLarge + +internal val standardText = + TextStyle( + color = DevkitWalletColors.white, + fontFamily = quattroRegular, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + ) diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/CustomSnackbar.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/CustomSnackbar.kt" new file mode 100644 index 0000000..69c4529 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/CustomSnackbar.kt" @@ -0,0 +1,41 @@ +package org.bitcoindevkit.devkitwallet.presentation.ui.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarData +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.composables.icons.lucide.Lucide +import com.composables.icons.lucide.X +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular + +@Composable +fun CustomSnackbar(data: SnackbarData) { + Snackbar( + modifier = Modifier.padding(12.dp), + action = { + IconButton( + onClick = { data.performAction() }, + ) { + Icon( + imageVector = Lucide.X, + contentDescription = "Ok", + tint = DevkitWalletColors.white, + ) + } + }, + containerColor = DevkitWalletColors.primaryLight, + ) { + Text( + text = data.visuals.message, + fontFamily = quattroRegular, + fontSize = 14.sp, + ) + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/LoadingAnimation.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/LoadingAnimation.kt" new file mode 100644 index 0000000..7b3c5c0 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/LoadingAnimation.kt" @@ -0,0 +1,77 @@ +/* + * Copyright 2021-2024 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.presentation.ui.components + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay + +@Composable +fun LoadingAnimation( + circleColor: Color = Color(0xffE9C46A), + circleSize: Dp = 21.dp, + animationDelay: Int = 800, + initialAlpha: Float = 0.3f, +) { + val circles = + listOf( + remember { Animatable(initialValue = initialAlpha) }, + remember { Animatable(initialValue = initialAlpha) }, + remember { Animatable(initialValue = initialAlpha) }, + ) + + circles.forEachIndexed { index, animatable -> + LaunchedEffect(Unit) { + // Use coroutine delay to sync animations + delay(timeMillis = (animationDelay / circles.size).toLong() * index) + + animatable.animateTo( + targetValue = 1f, + animationSpec = + infiniteRepeatable( + animation = + tween( + durationMillis = animationDelay, + ), + repeatMode = RepeatMode.Reverse, + ), + ) + } + } + + // container for circles + Row { + circles.forEachIndexed { index, animatable -> + // gap between the circles + if (index != 0) Spacer(modifier = Modifier.width(width = 6.dp)) + + Box( + modifier = + Modifier + .size(size = circleSize) + .clip(shape = CircleShape) + .background(circleColor.copy(alpha = animatable.value)), + ) + } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/NeutralButton.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/NeutralButton.kt" new file mode 100644 index 0000000..f798c57 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/NeutralButton.kt" @@ -0,0 +1,43 @@ +/* + * Copyright 2021-2024 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.presentation.ui.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.unit.dp +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors + +@Composable +fun NeutralButton(text: String, enabled: Boolean, modifier: Modifier? = null, onClick: () -> Unit) { + Button( + onClick = onClick, + colors = + ButtonDefaults.buttonColors( + containerColor = DevkitWalletColors.secondary, + disabledContainerColor = DevkitWalletColors.secondary, + ), + shape = RoundedCornerShape(16.dp), + enabled = enabled, + modifier = + modifier ?: Modifier + .height(80.dp) + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)), + ) { + Text( + text = text, + ) + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/RadioButtonWithLabel.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/RadioButtonWithLabel.kt" new file mode 100644 index 0000000..c5ead51 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/RadioButtonWithLabel.kt" @@ -0,0 +1,57 @@ +package org.bitcoindevkit.devkitwallet.presentation.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular + +@Composable +fun RadioButtonWithLabel(label: String, isSelected: Boolean, onSelect: () -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = + Modifier + .padding(0.dp) + .selectable( + selected = isSelected, + onClick = onSelect, + ), + ) { + RadioButton( + selected = isSelected, + onClick = onSelect, + colors = + RadioButtonDefaults.colors( + selectedColor = DevkitWalletColors.accent1, + unselectedColor = DevkitWalletColors.accent2, + ), + modifier = + Modifier + .padding(start = 8.dp) + .size(40.dp), + ) + Text( + text = label, + color = DevkitWalletColors.white, + fontFamily = monoRegular, + fontSize = 12.sp, + modifier = + Modifier + .clickable(onClick = onSelect) + .padding(0.dp), + ) + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/SecondaryScreensAppBar.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/SecondaryScreensAppBar.kt" new file mode 100644 index 0000000..8475df4 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/SecondaryScreensAppBar.kt" @@ -0,0 +1,47 @@ +/* + * Copyright 2021-2024 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.presentation.ui.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.sp +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SecondaryScreensAppBar(title: String, navigation: () -> Unit) { + TopAppBar( + title = { + Text( + text = title, + color = DevkitWalletColors.white, + fontSize = 18.sp, + fontFamily = quattroRegular, + ) + }, + navigationIcon = { + IconButton(onClick = navigation) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = "Back", + tint = DevkitWalletColors.white, + ) + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = DevkitWalletColors.primaryDark, + ), + ) +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/TransactionCards.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/TransactionCards.kt" new file mode 100644 index 0000000..cb1db1a --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/TransactionCards.kt" @@ -0,0 +1,136 @@ +/* + * Copyright 2021-2024 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.presentation.ui.components + +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.data.TxDetails +import org.bitcoindevkit.devkitwallet.domain.utils.timestampToString +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.wallet.viewTransaction + +private const val TAG = "TransactionCards" + +@Composable +fun ConfirmedTransactionCard(details: TxDetails, navController: NavController) { + Row( + Modifier + .padding(horizontal = 8.dp, vertical = 6.dp) + .fillMaxWidth() + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp), + ).clickable { viewTransaction(navController = navController, txid = details.txid) }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Absolute.SpaceBetween, + ) { + Text( + confirmedTransactionsItem(details), + fontFamily = monoRegular, + fontSize = 12.sp, + lineHeight = 20.sp, + color = DevkitWalletColors.white, + modifier = Modifier.padding(16.dp), + ) + Box( + modifier = + Modifier + .padding(top = 16.dp, end = 16.dp) + .size(size = 24.dp) + .clip(shape = CircleShape) + .background(DevkitWalletColors.secondary) + .align(Alignment.Top), + ) + } +} + +@Composable +fun PendingTransactionCard(details: TxDetails, navController: NavController) { + Row( + Modifier + .padding(horizontal = 8.dp, vertical = 6.dp) + .fillMaxWidth() + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp), + ).border( + width = 2.dp, + color = DevkitWalletColors.accent1, + shape = RoundedCornerShape(16.dp), + ).clickable { + viewTransaction( + navController = navController, + txid = details.txid, + ) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Absolute.SpaceBetween, + ) { + Text( + pendingTransactionsItem(details), + fontFamily = monoRegular, + fontSize = 12.sp, + color = DevkitWalletColors.white, + modifier = Modifier.padding(16.dp), + ) + Box( + modifier = + Modifier + .padding(top = 16.dp, end = 16.dp) + .size(size = 24.dp) + .clip(shape = CircleShape) + .background(Color(0xffE9C46A)) + .align(Alignment.Top), + ) + } +} + +fun pendingTransactionsItem(txDetails: TxDetails): String { + return buildString { + Log.i(TAG, "Pending transaction list item: $txDetails") + + appendLine("Confirmation time: Pending") + appendLine("Received: ${txDetails.received}") + appendLine("Sent: ${txDetails.sent}") + appendLine("Total fee: ${txDetails.fee} sat") + appendLine("Fee rate: ${txDetails.feeRate?.toSatPerVbCeil() ?: 0} sat/vbyte") + append("Txid: ${txDetails.txid.take(n = 8)}...${txDetails.txid.takeLast(n = 8)}") + } +} + +fun confirmedTransactionsItem(txDetails: TxDetails): String { + return buildString { + Log.i(TAG, "Transaction list item: $txDetails") + + appendLine("Confirmation time: ${txDetails.confirmationTimestamp?.timestamp?.timestampToString()}") + appendLine("Received: ${txDetails.received} sat") + appendLine("Sent: ${txDetails.sent} sat") + appendLine("Total fee: ${txDetails.fee} sat") + appendLine("Fee rate: ${txDetails.feeRate?.toSatPerVbCeil() ?: 0} sat/vbyte") + appendLine("Block: ${txDetails.confirmationBlock?.height}") + append("Txid: ${txDetails.txid.take(n = 8)}...${txDetails.txid.takeLast(n = 8)}") + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/WalletOptionsCard.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/WalletOptionsCard.kt" new file mode 100644 index 0000000..82e1699 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/components/WalletOptionsCard.kt" @@ -0,0 +1,133 @@ +package org.bitcoindevkit.devkitwallet.presentation.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.bitcoindevkit.Network +import org.bitcoindevkit.devkitwallet.data.ActiveWalletScriptType +import org.bitcoindevkit.devkitwallet.domain.supportedNetworks +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.ui.screens.intro.displayString + +@Composable +fun WalletOptionsCard( + scriptTypes: List, + selectedNetwork: MutableState, + selectedScriptType: MutableState, +) { + Column( + Modifier + .fillMaxWidth() + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp), + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start, + ) { + Text( + text = "Network", + fontFamily = monoRegular, + fontSize = 14.sp, + color = DevkitWalletColors.white, + modifier = + Modifier + .align(Alignment.CenterHorizontally) + .padding(top = 8.dp, bottom = 8.dp), + ) + + HorizontalDivider( + color = DevkitWalletColors.primaryDark, + thickness = 4.dp, + modifier = Modifier.padding(bottom = 8.dp), + ) + + supportedNetworks.forEachIndexed { index, it -> + RadioButtonWithLabel( + label = it.displayString(), + isSelected = selectedNetwork.value == it, + onSelect = { selectedNetwork.value = it }, + ) + if (index == 2) Spacer(modifier = Modifier.padding(bottom = 8.dp)) + } + + Text( + text = "Script Type", + fontFamily = monoRegular, + fontSize = 14.sp, + color = DevkitWalletColors.white, + modifier = + Modifier + .align(Alignment.CenterHorizontally) + .padding(top = 16.dp, bottom = 8.dp), + ) + + HorizontalDivider( + color = DevkitWalletColors.primaryDark, + thickness = 4.dp, + modifier = Modifier.padding(bottom = 8.dp), + ) + + scriptTypes.forEachIndexed { index, it -> + RadioButtonWithLabel( + label = it.displayString(), + isSelected = selectedScriptType.value == it, + onSelect = { selectedScriptType.value = it }, + ) + if (index == 1) Spacer(modifier = Modifier.padding(bottom = 8.dp)) + } + } +} + +@Composable +fun NetworkOptionsCard(selectedNetwork: MutableState) { + Column( + Modifier + .fillMaxWidth() + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp), + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start, + ) { + Text( + text = "Network", + fontFamily = monoRegular, + fontSize = 14.sp, + color = DevkitWalletColors.white, + modifier = + Modifier + .align(Alignment.CenterHorizontally) + .padding(top = 8.dp, bottom = 8.dp), + ) + + HorizontalDivider( + color = DevkitWalletColors.primaryDark, + thickness = 4.dp, + modifier = Modifier.padding(bottom = 8.dp), + ) + + supportedNetworks.forEachIndexed { index, it -> + RadioButtonWithLabel( + label = it.displayString(), + isSelected = selectedNetwork.value == it, + onSelect = { selectedNetwork.value = it }, + ) + if (index == 2) Spacer(modifier = Modifier.padding(bottom = 8.dp)) + } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/WalletRoot.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/WalletRoot.kt" new file mode 100644 index 0000000..73193b9 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/WalletRoot.kt" @@ -0,0 +1,190 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.Face +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.NavigationDrawerItemDefaults.colors +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.composables.icons.lucide.History +import com.composables.icons.lucide.Info +import com.composables.icons.lucide.Lucide +import com.composables.icons.lucide.SatelliteDish +import com.composables.icons.lucide.ScrollText +import org.bitcoindevkit.devkitwallet.BuildConfig +import org.bitcoindevkit.devkitwallet.R +import org.bitcoindevkit.devkitwallet.domain.Wallet +import org.bitcoindevkit.devkitwallet.presentation.navigation.AboutScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.BlockchainClientScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.LogsScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.RecoveryPhraseScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletNavigation +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.WalletViewModel + +@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) +@Composable +internal fun WalletRoot(activeWallet: Wallet, walletViewModel: WalletViewModel, navController: NavController) { + val drawerState = rememberDrawerState(DrawerValue.Closed) + + val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email, Icons.Default.Face) + val selectedItem = remember { mutableStateOf(items[0]) } + + val navigationItemColors = + colors( + selectedContainerColor = DevkitWalletColors.primary, + unselectedContainerColor = DevkitWalletColors.primary, + selectedTextColor = DevkitWalletColors.white, + unselectedTextColor = DevkitWalletColors.white, + ) + + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet( + drawerContainerColor = DevkitWalletColors.primary, + ) { + Column( + Modifier + .background(color = DevkitWalletColors.secondary) + .height(300.dp) + .fillMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + painter = painterResource(id = R.drawable.ic_testnet_logo), + contentDescription = "Bitcoin testnet logo", + Modifier + .size(90.dp) + .padding(bottom = 16.dp), + ) + Text( + text = "Devkit Wallet", + color = DevkitWalletColors.white, + fontFamily = quattroRegular, + ) + Spacer(modifier = Modifier.padding(4.dp)) + Text( + text = "The sample wallet on Android for BDK.", + color = DevkitWalletColors.white, + fontFamily = quattroRegular, + fontSize = 12.sp, + fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, + ) + Spacer(modifier = Modifier.padding(16.dp)) + Text( + text = BuildConfig.VARIANT_NAME, + style = standardText, + ) + } + Column( + Modifier + .fillMaxHeight() + .background(color = DevkitWalletColors.primary), + ) { + Spacer(modifier = Modifier.height(16.dp)) + NavigationDrawerItem( + icon = { Icon(Lucide.Info, contentDescription = "About", tint = DevkitWalletColors.white) }, + label = { DrawerItemLabel("About") }, + selected = items[0] == selectedItem.value, + onClick = { navController.navigate(AboutScreen) }, + colors = navigationItemColors, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + ) + NavigationDrawerItem( + icon = { + Icon( + Lucide.History, + contentDescription = "Wallet Recovery Data", + tint = DevkitWalletColors.white + ) + }, + label = { DrawerItemLabel("Wallet Recovery Data") }, + selected = items[1] == selectedItem.value, + onClick = { navController.navigate(RecoveryPhraseScreen) }, + colors = navigationItemColors, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + ) + NavigationDrawerItem( + icon = { + Icon( + Lucide.SatelliteDish, + contentDescription = "Esplora Client", + tint = DevkitWalletColors.white + ) + }, + label = { DrawerItemLabel("Compact Block Filters Node") }, + selected = items[2] == selectedItem.value, + onClick = { navController.navigate(BlockchainClientScreen) }, + colors = navigationItemColors, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + ) + NavigationDrawerItem( + icon = { + Icon( + Lucide.ScrollText, + contentDescription = "Logs", + tint = DevkitWalletColors.white + ) + }, + label = { DrawerItemLabel("Logs") }, + selected = items[3] == selectedItem.value, + onClick = { navController.navigate(LogsScreen) }, + colors = navigationItemColors, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + ) + } + } + }, + content = { + WalletNavigation( + drawerState = drawerState, + activeWallet = activeWallet, + walletViewModel = walletViewModel, + ) + }, + ) +} + +@Composable +fun DrawerItemLabel(text: String) { + Text( + text = text, + fontFamily = quattroRegular, + ) +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt" new file mode 100644 index 0000000..4e21a2b --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/AboutScreen.kt" @@ -0,0 +1,112 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.drawer + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import org.bitcoindevkit.devkitwallet.R +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.devkitTypography +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +private val message: String = + """ + This wallet is build for: + + 1. Developers interested in learning how to leverage the Bitcoin Development Kit on Android. + + 2. Any bitcoiner looking for a Signet/Testnet/Regtest wallet! + """.trimIndent() + +@Composable +internal fun AboutScreen(navController: NavController) { + val mUriHandler = LocalUriHandler.current + val openSourceRepository = + remember { { mUriHandler.openUri("https://github.com/bitcoindevkit/bdk-kotlin-example-wallet") } } + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "About", + navigation = { navController.navigate(WalletScreen) }, + ) + }, + containerColor = DevkitWalletColors.primary, + ) { paddingValues -> + Column( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 100.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.padding(24.dp)) + Image( + painter = painterResource(id = R.drawable.bdk_logo), + contentDescription = "Old School BDK Logo", + Modifier.size(180.dp), + ) + Spacer(modifier = Modifier.padding(24.dp)) + Text( + text = "This wallet is build for:\n\n1. Developers interested in learning how to leverage the Bitcoin Development Kit on Android.\n\n2. Any bitcoiner looking for a Signet/Testnet/Regtest wallet!", + color = DevkitWalletColors.white, + style = devkitTypography.labelLarge, + lineHeight = 26.sp, + modifier = Modifier.padding(all = 8.dp), + ) + Spacer(modifier = Modifier.padding(8.dp)) + Text( + text = "You are using the Compact Block Filters (CBF) version of the wallet.", + color = DevkitWalletColors.white, + style = devkitTypography.labelLarge, + lineHeight = 26.sp, + modifier = Modifier.padding(all = 8.dp), + ) + Spacer(modifier = Modifier.padding(8.dp)) + Text( + text = "Check out the source code for the wallet on GitHub.", + color = DevkitWalletColors.white, + style = devkitTypography.labelLarge, + textDecoration = TextDecoration.Underline, + lineHeight = 26.sp, + modifier = + Modifier + .padding(all = 8.dp) + .clickable(onClick = openSourceRepository), + ) + } + } +} + +@Preview(device = Devices.PIXEL_4, showBackground = true) +@Composable +internal fun PreviewAboutScreen() { + AboutScreen(rememberNavController()) +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt" new file mode 100644 index 0000000..ca0a9c6 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt" @@ -0,0 +1,134 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.drawer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +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.WalletScreenAction +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenState + +@Composable +internal fun BlockchainClientScreen( + state: WalletScreenState, + onAction: (WalletScreenAction) -> Unit, + navController: NavController, +) { + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Compact Block Filters Node", + navigation = { navController.navigate(WalletScreen) }, + ) + }, + containerColor = DevkitWalletColors.primary, + ) { paddingValues -> + Column( + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(vertical = 32.dp, horizontal = 16.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + val status = if (state.kyotoNodeStatus == KyotoNodeStatus.Running) "Online" else "Offline" + Text( + text = "CBF Node Status: $status", + color = DevkitWalletColors.white, + fontSize = 14.sp, + fontFamily = monoRegular, + textAlign = TextAlign.Start, + ) + Box( + modifier = + Modifier + .padding(horizontal = 8.dp) + .size(size = 21.dp) + .clip(shape = CircleShape) + .background( + if (state.kyotoNodeStatus == KyotoNodeStatus.Running) { + Color( + 0xFF2A9D8F + ) + } else { + Color(0xFFE76F51) + } + ), + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth().padding(top = 16.dp), + ) { + Text( + text = "Latest known block:", + color = DevkitWalletColors.white, + fontSize = 14.sp, + fontFamily = monoRegular, + textAlign = TextAlign.Start, + ) + Text( + text = "${state.latestBlock}", + color = DevkitWalletColors.white, + fontSize = 14.sp, + fontFamily = monoRegular, + textAlign = TextAlign.Start, + ) + } + + Spacer(modifier = Modifier.padding(16.dp)) + + NeutralButton( + text = "Start Node", + enabled = state.kyotoNodeStatus == KyotoNodeStatus.Stopped, + onClick = { onAction(WalletScreenAction.StartKyotoNode) }, + ) + NeutralButton( + text = "Start Sync", + enabled = state.kyotoNodeStatus == KyotoNodeStatus.Running, + onClick = { onAction(WalletScreenAction.StartKyotoSync) }, + ) + NeutralButton( + text = "Stop Node", + enabled = state.kyotoNodeStatus == KyotoNodeStatus.Running, + onClick = { onAction(WalletScreenAction.StopKyotoNode) }, + ) + } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/CustomBlockchainClient.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/CustomBlockchainClient.kt" new file mode 100644 index 0000000..bb6cdc4 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/CustomBlockchainClient.kt" @@ -0,0 +1,115 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.drawer + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +@Composable +internal fun CustomBlockchainClient(navController: NavController) { + val focusManager = LocalFocusManager.current + // val isBlockChainCreated = Wallet.isBlockChainCreated() + val electrumServer: MutableState = remember { mutableStateOf("") } + val isChecked: MutableState = remember { mutableStateOf(false) } + // if (isBlockChainCreated) { + // electrumServer.value = Wallet.getElectrumURL() + // isChecked.value = Wallet.isElectrumServerDefault() + // } + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Custom Blockchain Client", + navigation = { navController.navigate(WalletScreen) }, + ) + }, + containerColor = DevkitWalletColors.primary, + ) { paddingValues -> + Column( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(all = 16.dp), + ) { + // Row(verticalAlignment = Alignment.CenterVertically) { + // Text( + // text = "Use default electrum URL", + // color = DevkitWalletColors.white, + // fontSize = 14.sp, + // textAlign = TextAlign.Center, + // ) + // Switch( + // checked = isChecked.value, + // onCheckedChange = { + // isChecked.value = it + // if (it) { + // Wallet.setElectrumSettings(ElectrumSettings.DEFAULT) + // } else { + // Wallet.setElectrumSettings(ElectrumSettings.CUSTOM) + // } + // }, + // enabled = isBlockChainCreated + // ) + // } + + // OutlinedTextField( + // value = electrumServer.value, + // onValueChange = { electrumServer.value = it }, + // label = { + // Text( + // text = "Electrum Server", + // color = DevkitWalletColors.white, + // ) + // }, + // singleLine = true, + // textStyle = TextStyle(color = DevkitWalletColors.white), + // colors = TextFieldDefaults.outlinedTextFieldColors( + // focusedBorderColor = DevkitWalletColors.accent1, + // unfocusedBorderColor = DevkitWalletColors.white, + // cursorColor = DevkitWalletColors.accent1, + // ), + // keyboardActions = KeyboardActions(onDone = { + // focusManager.clearFocus() + // }), + // modifier = Modifier.fillMaxWidth(), + // enabled = isBlockChainCreated && !isChecked.value + // ) + + // Button( + // onClick = { + // Wallet.changeElectrumServer(electrumServer.value) + // focusManager.clearFocus() + // }, + // modifier = Modifier + // .align(alignment = Alignment.End) + // .padding(all = 8.dp), + // colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + // enabled = isBlockChainCreated && !isChecked.value + // ) { + // Text( + // text = "Save", + // color = DevkitWalletColors.white, + // fontSize = 12.sp, + // textAlign = TextAlign.Center, + // ) + // } + } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt" new file mode 100644 index 0000000..d930a00 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/LogsScreen.kt" @@ -0,0 +1,63 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.drawer + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.domain.DwLogger +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +@Composable +fun LogsScreen(navController: NavController) { + val logs: List = remember { DwLogger.getLogs() } + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Logs", + navigation = { navController.navigate(WalletScreen) }, + ) + }, + containerColor = DevkitWalletColors.primary, + ) { paddingValues -> + LazyColumn( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp), + ) { + items(logs) { logLine -> + Text( + text = logLine, + style = standardText, + maxLines = 1, + overflow = TextOverflow.Visible, + modifier = + Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + ) + } + } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt" new file mode 100644 index 0000000..55bccce --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryDataScreen.kt" @@ -0,0 +1,231 @@ +/* + * Copyright 2021-2025 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.presentation.ui.screens.drawer + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.composables.icons.lucide.ClipboardCopy +import com.composables.icons.lucide.Lucide +import org.bitcoindevkit.devkitwallet.domain.WalletSecrets +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular +import org.bitcoindevkit.devkitwallet.presentation.ui.components.NeutralButton +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +private val MESSAGE: String = + """ + The next screen will show your recovery phrase and descriptors. Make sure no one else is looking at your screen. + """.trimIndent() + +@Composable +internal fun RecoveryDataScreen(walletSecrets: WalletSecrets, navController: NavController) { + val (currentIndex, setCurrentIndex) = remember { mutableIntStateOf(0) } + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Your Wallet Recovery Data", + navigation = { navController.navigate(WalletScreen) }, + ) + }, + containerColor = DevkitWalletColors.primary, + ) { paddingValues -> + Crossfade( + modifier = Modifier.padding(paddingValues), + targetState = currentIndex, + label = "", + animationSpec = + tween( + durationMillis = 1000, + delayMillis = 200, + ), + ) { screen -> + when (screen) { + 0 -> WarningText(setCurrentIndex = setCurrentIndex) + 1 -> RecoveryPhrase(walletSecrets = walletSecrets) + } + } + } +} + +@Composable +fun WarningText(setCurrentIndex: (Int) -> Unit) { + Column( + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 32.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = MESSAGE, + color = DevkitWalletColors.white, + fontFamily = quattroRegular, + ) + Spacer(modifier = Modifier.padding(16.dp)) + NeutralButton( + text = "See my recovery data", + enabled = true, + ) { setCurrentIndex(1) } + } +} + +@Composable +fun RecoveryPhrase(walletSecrets: WalletSecrets) { + val context = LocalContext.current + Column( + modifier = + Modifier + .fillMaxSize() + .padding(all = 32.dp), + ) { + Text( + text = "Write down your recovery phrase and keep it in a safe place.", + color = DevkitWalletColors.white, + fontFamily = quattroRegular, + ) + Spacer(modifier = Modifier.padding(8.dp)) + Box { + SelectionContainer { + Text( + modifier = + Modifier + .clickable { + simpleCopyClipboard( + walletSecrets.recoveryPhrase, + context, + ) + }.background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp), + ).padding(12.dp), + text = walletSecrets.recoveryPhrase, + fontFamily = monoRegular, + color = DevkitWalletColors.white, + ) + } + Icon( + Lucide.ClipboardCopy, + tint = Color.White, + contentDescription = "Copy to clipboard", + modifier = + Modifier + .padding(8.dp) + .size(20.dp) + .align(Alignment.BottomEnd), + ) + } + Spacer(modifier = Modifier.padding(16.dp)) + Text( + text = "These are your descriptors.", + color = DevkitWalletColors.white, + fontFamily = quattroRegular, + ) + Spacer(modifier = Modifier.padding(8.dp)) + Box { + SelectionContainer { + Text( + modifier = + Modifier + .clickable { + simpleCopyClipboard( + walletSecrets.descriptor.toStringWithSecret(), + context, + ) + }.background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp), + ).padding(12.dp), + text = walletSecrets.descriptor.toStringWithSecret(), + fontFamily = monoRegular, + color = DevkitWalletColors.white, + ) + } + Icon( + Lucide.ClipboardCopy, + tint = Color.White, + contentDescription = "Copy to clipboard", + modifier = + Modifier + .padding(8.dp) + .size(20.dp) + .align(Alignment.BottomEnd), + ) + } + Spacer(modifier = Modifier.padding(4.dp)) + Box { + SelectionContainer { + Text( + modifier = + Modifier + .clickable { + simpleCopyClipboard( + walletSecrets.changeDescriptor.toStringWithSecret(), + context, + ) + }.background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp), + ).padding(12.dp), + text = walletSecrets.changeDescriptor.toStringWithSecret(), + fontFamily = monoRegular, + color = DevkitWalletColors.white, + ) + } + Icon( + Lucide.ClipboardCopy, + tint = Color.White, + contentDescription = "Copy to clipboard", + modifier = + Modifier + .padding(8.dp) + .size(20.dp) + .align(Alignment.BottomEnd), + ) + } + } +} + +fun simpleCopyClipboard(content: String, context: Context) { + val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip: ClipData = ClipData.newPlainText("", content) + clipboard.setPrimaryClip(clip) +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewRecoveryPhraseScreen() { +// RecoveryPhraseScreen() +// } diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryPhraseScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/RecoveryPhraseScreen.kt" new file mode 100644 index 0000000..e69de29 diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/ActiveWalletsScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/ActiveWalletsScreen.kt" new file mode 100644 index 0000000..65257b3 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/ActiveWalletsScreen.kt" @@ -0,0 +1,95 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.intro + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.data.SingleWallet +import org.bitcoindevkit.devkitwallet.domain.DwLogger +import org.bitcoindevkit.devkitwallet.domain.DwLogger.LogLevel.INFO +import org.bitcoindevkit.devkitwallet.presentation.WalletCreateType +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +private const val TAG = "ActiveWalletsScreen" + +@Composable +internal fun ActiveWalletsScreen( + activeWallets: List, + navController: NavController, + onBuildWalletButtonClicked: (WalletCreateType) -> Unit, +) { + Scaffold( + topBar = { + SecondaryScreensAppBar(title = "Choose a Wallet", navigation = { navController.navigateUp() }) + }, + containerColor = DevkitWalletColors.primary, + ) { paddingValues -> + Column( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + activeWallets.forEach { + ActiveWalletCard(wallet = it, onBuildWalletButtonClicked) + } + } + if (activeWallets.isEmpty()) { + Text( + text = "No active wallets.", + fontSize = 16.sp, + fontFamily = quattroRegular, + color = DevkitWalletColors.white, + modifier = Modifier.padding(16.dp), + ) + } + } +} + +@Composable +fun ActiveWalletCard(wallet: SingleWallet, onBuildWalletButtonClicked: (WalletCreateType) -> Unit) { + Row( + Modifier + .padding(horizontal = 8.dp, vertical = 6.dp) + .fillMaxWidth() + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp), + ).clickable { + DwLogger.log(INFO, "Activating existing wallet: ${wallet.name}") + onBuildWalletButtonClicked(WalletCreateType.LOADEXISTING(wallet)) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Absolute.SpaceBetween, + ) { + Text( + "Name: ${wallet.name}\nNetwork: ${wallet.network}\nScript Type: ${wallet.scriptType}", + fontFamily = monoRegular, + fontSize = 12.sp, + lineHeight = 20.sp, + color = DevkitWalletColors.white, + modifier = Modifier.padding(16.dp), + ) + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/CreateNewWallet.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/CreateNewWallet.kt" new file mode 100644 index 0000000..7bb611e --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/CreateNewWallet.kt" @@ -0,0 +1,296 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.intro + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.navigation.NavController +import org.bitcoindevkit.Network +import org.bitcoindevkit.devkitwallet.data.ActiveWalletScriptType +import org.bitcoindevkit.devkitwallet.data.NewWalletConfig +import org.bitcoindevkit.devkitwallet.domain.DwLogger +import org.bitcoindevkit.devkitwallet.domain.DwLogger.LogLevel.INFO +import org.bitcoindevkit.devkitwallet.presentation.WalletCreateType +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.ui.components.NeutralButton +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar +import org.bitcoindevkit.devkitwallet.presentation.ui.components.WalletOptionsCard + +@Composable +internal fun CreateNewWalletScreen( + navController: NavController, + onBuildWalletButtonClicked: (WalletCreateType) -> Unit, +) { + Scaffold( + topBar = { + SecondaryScreensAppBar(title = "Create a New Wallet", navigation = { navController.navigateUp() }) + }, + containerColor = DevkitWalletColors.primary, + ) { paddingValues -> + + ConstraintLayout( + modifier = + Modifier + .padding(paddingValues) + .fillMaxSize() + .padding(vertical = 16.dp), + ) { + val (choices, button) = createRefs() + + val walletName: MutableState = remember { mutableStateOf("") } + val selectedNetwork: MutableState = remember { mutableStateOf(Network.SIGNET) } + val selectedScriptType: MutableState = + remember { mutableStateOf(ActiveWalletScriptType.P2TR) } + val scriptTypes = listOf(ActiveWalletScriptType.P2TR, ActiveWalletScriptType.P2WPKH) + + Column( + modifier = + Modifier + .constrainAs(choices) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }.fillMaxSize() + .background(color = DevkitWalletColors.primary) + .padding(horizontal = 32.dp), + ) { + OutlinedTextField( + modifier = + Modifier + .padding(bottom = 8.dp) + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + value = walletName.value, + onValueChange = { walletName.value = it }, + label = { + Text( + text = "Give your wallet a name", + style = standardText, + color = DevkitWalletColors.white, + ) + }, + singleLine = true, + textStyle = TextStyle(fontFamily = monoRegular, color = DevkitWalletColors.white), + colors = + OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + + Spacer(modifier = Modifier.padding(12.dp)) + WalletOptionsCard(scriptTypes, selectedNetwork, selectedScriptType) + Spacer(modifier = Modifier.padding(16.dp)) + } + + Column( + modifier = + Modifier + .constrainAs(button) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }.fillMaxWidth() + .padding(horizontal = 32.dp), + ) { + NeutralButton( + text = "Create Wallet", + enabled = true, + modifier = + Modifier + .height(80.dp) + .fillMaxWidth() + .padding(vertical = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)), + onClick = { + val newWalletConfig = + NewWalletConfig( + name = walletName.value, + network = selectedNetwork.value, + scriptType = selectedScriptType.value, + ) + DwLogger.log(INFO, "Creating new wallet named ${newWalletConfig.name}") + onBuildWalletButtonClicked(WalletCreateType.FROMSCRATCH(newWalletConfig)) + }, + ) + } + } + } +} + +@Composable +fun NetworkOptionCard(networks: List, selectedNetwork: MutableState) { + Column( + Modifier + .fillMaxWidth() + .border( + width = 2.dp, + color = DevkitWalletColors.secondary, + shape = RoundedCornerShape(16.dp), + ).background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp), + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start, + ) { + Text( + text = "Network", + fontFamily = monoRegular, + fontSize = 18.sp, + color = DevkitWalletColors.white, + modifier = Modifier.padding(top = 8.dp, start = 8.dp, bottom = 8.dp), + ) + + HorizontalDivider( + color = DevkitWalletColors.secondary, + thickness = 2.dp, + modifier = Modifier.padding(bottom = 8.dp) + ) + + networks.forEachIndexed { index, it -> + RadioButtonWithLabel( + label = it.displayString(), + isSelected = selectedNetwork.value == it, + onSelect = { selectedNetwork.value = it }, + ) + if (index == 2) Spacer(modifier = Modifier.padding(bottom = 8.dp)) + } + } +} + +@Composable +fun ScriptTypeOptionCard( + scriptTypes: List, + selectedScriptType: MutableState, +) { + Column( + Modifier + .fillMaxWidth() + .border( + width = 2.dp, + color = DevkitWalletColors.secondary, + shape = RoundedCornerShape(16.dp), + ).background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp), + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start, + ) { + Text( + text = "Script Type", + fontFamily = monoRegular, + fontSize = 18.sp, + color = DevkitWalletColors.white, + modifier = Modifier.padding(top = 8.dp, start = 8.dp, bottom = 8.dp), + ) + + HorizontalDivider( + color = DevkitWalletColors.secondary, + thickness = 2.dp, + modifier = Modifier.padding(bottom = 8.dp) + ) + + scriptTypes.forEachIndexed { index, it -> + RadioButtonWithLabel( + label = it.displayString(), + isSelected = selectedScriptType.value == it, + onSelect = { selectedScriptType.value = it }, + ) + if (index == 1) Spacer(modifier = Modifier.padding(bottom = 8.dp)) + } + } +} + +@Composable +fun RadioButtonWithLabel(label: String, isSelected: Boolean, onSelect: () -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + modifier = + Modifier + .padding(0.dp) + .selectable( + selected = isSelected, + onClick = onSelect, + ), + ) { + RadioButton( + selected = isSelected, + onClick = onSelect, + colors = + RadioButtonDefaults.colors( + selectedColor = DevkitWalletColors.accent1, + unselectedColor = DevkitWalletColors.accent2, + ), + modifier = + Modifier + .padding(0.dp), + ) + Text( + text = label, + color = DevkitWalletColors.white, + fontFamily = monoRegular, + fontSize = 14.sp, + modifier = + Modifier + .clickable(onClick = onSelect) + .padding(0.dp), + ) + } +} + +fun ActiveWalletScriptType.displayString(): String { + return when (this) { + ActiveWalletScriptType.P2TR -> "P2TR (Taproot, BIP-86)" + ActiveWalletScriptType.P2WPKH -> "P2WPKH (Native Segwit, BIP-84)" + ActiveWalletScriptType.UNKNOWN -> TODO() + ActiveWalletScriptType.UNRECOGNIZED -> TODO() + } +} + +fun Network.displayString(): String { + return when (this) { + Network.TESTNET -> "Testnet 3" + Network.TESTNET4 -> "Testnet 4" + Network.REGTEST -> "Regtest" + Network.SIGNET -> "Signet" + Network.BITCOIN -> TODO() + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt" new file mode 100644 index 0000000..30ded03 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/OnboardingScreen.kt" @@ -0,0 +1,198 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.intro + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import org.bitcoindevkit.devkitwallet.R +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.devkitTypography +import org.bitcoindevkit.devkitwallet.presentation.theme.introText + +@Composable +fun OnboardingScreen(onFinishOnboarding: () -> Unit) { + val (currentIndex, setCurrentIndex) = remember { mutableIntStateOf(1) } + val messages = + listOf( + "Easter egg #1: \uD83E\uDD5A", + "Welcome to the Devkit Wallet! This app is a playground for developers and bitcoin enthusiasts to experiment with bitcoin's test networks.", + "It is developed with the Bitcoin Dev Kit, a powerful set of libraries produced and maintained by the Bitcoin Dev Kit Foundation.", + "This version of the app is using Compact Block Filters to sync its wallets.", + "The Foundation maintains this app as a way to showcase the capabilities of the Bitcoin Dev Kit and to provide a starting point for developers to build their own apps.\n\nIt is not a production application, and only works for testnet, signet, and regtest. Have fun!", + ) + + ConstraintLayout( + modifier = + Modifier + .fillMaxSize() + .background(DevkitWalletColors.primary), + ) { + val (logo, intro, progress, buttons) = createRefs() + + Image( + painter = painterResource(id = R.drawable.bdk_logo), + contentDescription = "Bitcoin Dev Kit logo", + Modifier + .size(180.dp) + .constrainAs(logo) { + top.linkTo(parent.top, margin = 90.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) + + Crossfade( + modifier = + Modifier.constrainAs(intro) { + top.linkTo(logo.bottom, margin = 90.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + targetState = currentIndex, + label = "", + animationSpec = + tween( + durationMillis = 1000, + delayMillis = 200, + ), + ) { screen -> + when (screen) { + 0 -> IntroTextPart(messages[0]) + 1 -> IntroTextPart(messages[1]) + 2 -> IntroTextPart(messages[2]) + 3 -> IntroTextPart(messages[3]) + 4 -> IntroTextPart(messages[4]) + } + } + + Row( + modifier = + Modifier.constrainAs(progress) { + bottom.linkTo(buttons.top, margin = 32.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + horizontalArrangement = Arrangement.Center, + ) { + Box( + modifier = + Modifier + .padding(horizontal = 8.dp) + .size(size = 16.dp) + .clip(shape = CircleShape) + .background( + if (currentIndex == 1) Color(0xffE9C46A) else Color(0xffE9C46A).copy(alpha = 0.3f), + ), + ) + Box( + modifier = + Modifier + .padding(horizontal = 8.dp) + .size(size = 16.dp) + .clip(shape = CircleShape) + .background( + if (currentIndex == 2) Color(0xffE9C46A) else Color(0xffE9C46A).copy(alpha = 0.3f), + ), + ) + Box( + modifier = + Modifier + .padding(horizontal = 8.dp) + .size(size = 16.dp) + .clip(shape = CircleShape) + .background( + if (currentIndex == 3) Color(0xffE9C46A) else Color(0xffE9C46A).copy(alpha = 0.3f), + ), + ) + Box( + modifier = + Modifier + .padding(horizontal = 8.dp) + .size(size = 16.dp) + .clip(shape = CircleShape) + .background( + if (currentIndex == 4) Color(0xffE9C46A) else Color(0xffE9C46A).copy(alpha = 0.3f), + ), + ) + } + + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp) + .constrainAs(buttons) { + bottom.linkTo(parent.bottom, margin = 32.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "Previous", + modifier = + Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { setCurrentIndex((currentIndex - 1).coerceIn(0, 3)) }, + color = DevkitWalletColors.white, + style = devkitTypography.labelLarge, + ) + Text( + text = if (currentIndex < 4) "Next" else "Awesome!", + modifier = + Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { + if (currentIndex < 4) { + setCurrentIndex( + (currentIndex + 1).coerceIn(0, 4) + ) + } else { + onFinishOnboarding() + } + }, + color = DevkitWalletColors.white, + style = devkitTypography.labelLarge, + ) + } + } +} + +@Composable +fun IntroTextPart(message: String) { + Text( + text = message, + modifier = Modifier.padding(horizontal = 32.dp), + color = DevkitWalletColors.white, + style = introText, + ) +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/RecoverWalletScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/RecoverWalletScreen.kt" new file mode 100644 index 0000000..c401dbd --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/RecoverWalletScreen.kt" @@ -0,0 +1,402 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.intro + +import android.util.Log +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.launch +import org.bitcoindevkit.Descriptor +import org.bitcoindevkit.DescriptorSecretKey +import org.bitcoindevkit.KeychainKind +import org.bitcoindevkit.Mnemonic +import org.bitcoindevkit.Network +import org.bitcoindevkit.devkitwallet.data.ActiveWalletScriptType +import org.bitcoindevkit.devkitwallet.data.RecoverWalletConfig +import org.bitcoindevkit.devkitwallet.domain.DwLogger +import org.bitcoindevkit.devkitwallet.domain.DwLogger.LogLevel.INFO +import org.bitcoindevkit.devkitwallet.domain.bip39WordList +import org.bitcoindevkit.devkitwallet.domain.createScriptAppropriateDescriptor +import org.bitcoindevkit.devkitwallet.presentation.WalletCreateType +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.ui.components.CustomSnackbar +import org.bitcoindevkit.devkitwallet.presentation.ui.components.NetworkOptionsCard +import org.bitcoindevkit.devkitwallet.presentation.ui.components.NeutralButton +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar +import org.bitcoindevkit.devkitwallet.presentation.ui.components.WalletOptionsCard + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun RecoverWalletScreen(onAction: (WalletCreateType) -> Unit, navController: NavController) { + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + Scaffold( + topBar = { SecondaryScreensAppBar(title = "Recover a Wallet", navigation = { navController.navigateUp() }) }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) { data -> + CustomSnackbar(data) + } + }, + containerColor = DevkitWalletColors.primary, + ) { paddingValues -> + var selectedIndex by remember { mutableIntStateOf(0) } + val options = listOf("Descriptor", "Recovery Phrase") + + var descriptorString by remember { mutableStateOf("") } + var changeDescriptorString by remember { mutableStateOf("") } + var recoveryPhrase by remember { mutableStateOf("") } + + var walletName by remember { mutableStateOf("") } + val selectedNetwork: MutableState = remember { mutableStateOf(Network.SIGNET) } + val selectedScriptType: MutableState = + remember { mutableStateOf(ActiveWalletScriptType.P2TR) } + val scriptTypes = listOf(ActiveWalletScriptType.P2TR, ActiveWalletScriptType.P2WPKH) + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + SingleChoiceSegmentedButtonRow { + options.forEachIndexed { index, label -> + SegmentedButton( + shape = + SegmentedButtonDefaults.itemShape( + index = index, + count = options.size, + ), + onClick = { selectedIndex = index }, + selected = index == selectedIndex, + label = { Text(text = label, fontSize = 12.sp, color = Color.White) }, + colors = + SegmentedButtonDefaults.colors( + activeContainerColor = DevkitWalletColors.primaryLight, + activeContentColor = DevkitWalletColors.primaryLight, + activeBorderColor = DevkitWalletColors.primaryLight, + inactiveContainerColor = DevkitWalletColors.primaryDark, + inactiveContentColor = DevkitWalletColors.primaryDark, + inactiveBorderColor = DevkitWalletColors.primaryDark, + ), + border = BorderStroke(4.dp, DevkitWalletColors.primaryDark), + icon = { }, + modifier = Modifier.width(180.dp).padding(top = 8.dp), + ) + } + } + Spacer(modifier = Modifier.padding(12.dp)) + + if (selectedIndex == 0) { + DescriptorInput( + walletName, + descriptorString, + changeDescriptorString, + selectedNetwork, + walletNameOnValueChange = { walletName = it }, + descriptorOnValueChange = { descriptorString = it }, + changeDescriptorOnValueChange = { changeDescriptorString = it }, + ) + Spacer(modifier = Modifier.weight(1f)) + } else { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(horizontal = 32.dp), + ) { + WalletOptionsCard(scriptTypes, selectedNetwork, selectedScriptType) + Spacer(modifier = Modifier.padding(12.dp)) + OutlinedTextField( + modifier = + Modifier + .padding(bottom = 8.dp) + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + value = walletName, + onValueChange = { walletName = it }, + label = { + Text( + text = "Give your wallet a name", + style = standardText, + ) + }, + singleLine = true, + textStyle = TextStyle(fontFamily = monoRegular, color = DevkitWalletColors.white), + colors = + OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + RecoveryPhraseInput(recoveryPhrase, onValueChange = { recoveryPhrase = it }) + } + Spacer(modifier = Modifier.weight(1f)) + } + NeutralButton( + text = "Recover Wallet", + enabled = true, + onClick = { + if (descriptorString.isNotEmpty() && recoveryPhrase.isNotEmpty()) { + scope.launch { + snackbarHostState.showSnackbar( + "You cannot recover using both a descriptor and a recovery phrase at the same time.", + ) + } + } + if (descriptorString.isEmpty() && recoveryPhrase.isEmpty()) { + scope.launch { + snackbarHostState.showSnackbar( + "You must provide either a descriptor or a recovery phrase to recover a wallet.", + ) + } + } + if (descriptorString.isNotEmpty() && changeDescriptorString.isEmpty()) { + scope.launch { + snackbarHostState.showSnackbar( + "You must provide two descriptors for recovery.", + ) + } + } + if (descriptorString.isEmpty() && changeDescriptorString.isNotEmpty()) { + scope.launch { + snackbarHostState.showSnackbar( + "You must provide two descriptors for recovery.", + ) + } + } + if (recoveryPhrase.isNotEmpty()) { + Log.i("RecoverWalletScreen", "Recovering wallet with recovery phrase") + val parsingResult = parseRecoveryPhrase(recoveryPhrase) + + if (parsingResult is RecoveryPhraseValidationResult.Invalid) { + scope.launch { + snackbarHostState.showSnackbar(parsingResult.reason) + } + } else if (parsingResult is RecoveryPhraseValidationResult.ProbablyValid) { + val mnemonic = Mnemonic.fromString(parsingResult.recoveryPhrase) + val bip32ExtendedRootKey = DescriptorSecretKey(selectedNetwork.value, mnemonic, null) + val descriptor = + createScriptAppropriateDescriptor( + scriptType = selectedScriptType.value, + bip32ExtendedRootKey = bip32ExtendedRootKey, + network = selectedNetwork.value, + keychain = KeychainKind.EXTERNAL, + ) + val changeDescriptor = + createScriptAppropriateDescriptor( + scriptType = selectedScriptType.value, + bip32ExtendedRootKey = bip32ExtendedRootKey, + network = selectedNetwork.value, + keychain = KeychainKind.INTERNAL, + ) + val recoverWalletConfig = + RecoverWalletConfig( + name = walletName, + network = selectedNetwork.value, + scriptType = selectedScriptType.value, + descriptor = descriptor, + changeDescriptor = changeDescriptor, + recoveryPhrase = parsingResult.recoveryPhrase, + ) + DwLogger.log(INFO, "Recovering wallet with recovery phrase (name: $walletName)") + onAction(WalletCreateType.RECOVER(recoverWalletConfig)) + } + } + if (descriptorString.isNotEmpty() && changeDescriptorString.isNotEmpty()) { + Log.i("RecoverWalletScreen", "Recovering wallet with descriptors") + + val descriptor = Descriptor(descriptorString, selectedNetwork.value) + val changeDescriptor = Descriptor(changeDescriptorString, selectedNetwork.value) + val recoverWalletConfig = + RecoverWalletConfig( + name = walletName, + network = selectedNetwork.value, + scriptType = null, + descriptor = descriptor, + changeDescriptor = changeDescriptor, + recoveryPhrase = null, + ) + DwLogger.log(INFO, "Recovering wallet with descriptors (name: $walletName)") + onAction(WalletCreateType.RECOVER(recoverWalletConfig)) + } + }, + ) + } + } +} + +@Composable +fun DescriptorInput( + walletName: String, + descriptor: String, + changeDescriptor: String, + selectedNetwork: MutableState, + walletNameOnValueChange: (String) -> Unit, + descriptorOnValueChange: (String) -> Unit, + changeDescriptorOnValueChange: (String) -> Unit, +) { + Column( + Modifier.padding(horizontal = 32.dp), + ) { + NetworkOptionsCard( + selectedNetwork, + ) + OutlinedTextField( + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .padding(top = 16.dp), + value = walletName, + onValueChange = { walletNameOnValueChange(it) }, + label = { + Text( + text = "Give your wallet a name", + style = standardText, + ) + }, + singleLine = true, + textStyle = TextStyle(fontFamily = monoRegular, color = DevkitWalletColors.white), + colors = + OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth().padding(top = 16.dp), + value = descriptor, + onValueChange = { descriptorOnValueChange(it) }, + label = { + Text( + text = "Input your descriptor here", + style = standardText, + ) + }, + singleLine = false, + minLines = 5, + textStyle = TextStyle(fontFamily = quattroRegular, fontSize = 12.sp, color = DevkitWalletColors.white), + colors = + OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth().padding(top = 16.dp), + value = changeDescriptor, + onValueChange = { changeDescriptorOnValueChange(it) }, + label = { + Text( + text = "Input your change descriptor here", + style = standardText, + ) + }, + singleLine = false, + minLines = 5, + textStyle = TextStyle(fontFamily = quattroRegular, fontSize = 12.sp, color = DevkitWalletColors.white), + colors = + OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + } +} + +@Composable +fun RecoveryPhraseInput(recoveryPhrase: String, onValueChange: (String) -> Unit) { + Column { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = recoveryPhrase, + onValueChange = { onValueChange(it) }, + label = { + Text( + text = "Input 12-word recovery phrase here", + style = standardText, + ) + }, + singleLine = false, + minLines = 5, + textStyle = TextStyle(fontFamily = quattroRegular, fontSize = 12.sp, color = DevkitWalletColors.white), + colors = + OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + } +} + +private fun parseRecoveryPhrase(recoveryPhrase: String): RecoveryPhraseValidationResult { + val words = recoveryPhrase.trim().split(" ") + if (words.size != 12) { + return RecoveryPhraseValidationResult.Invalid("Recovery phrase must have 12 words") + } + if (words.any { it !in bip39WordList }) { + return RecoveryPhraseValidationResult.Invalid("Invalid word in recovery phrase") + } + return RecoveryPhraseValidationResult.ProbablyValid(recoveryPhrase) +} + +sealed class RecoveryPhraseValidationResult { + data class ProbablyValid(val recoveryPhrase: String) : RecoveryPhraseValidationResult() + + data class Invalid(val reason: String) : RecoveryPhraseValidationResult() +} + +@Preview(device = Devices.PIXEL_4, showBackground = true) +@Composable +internal fun PreviewWalletRecoveryScreen() { + RecoverWalletScreen( + onAction = {}, + navController = rememberNavController(), + ) +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/WalletChoiceScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/WalletChoiceScreen.kt" new file mode 100644 index 0000000..9185d6b --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/intro/WalletChoiceScreen.kt" @@ -0,0 +1,154 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.intro + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.R +import org.bitcoindevkit.devkitwallet.presentation.navigation.ActiveWalletsScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.CreateNewWalletScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.WalletRecoveryScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoBold + +@Composable +internal fun WalletChoiceScreen(navController: NavController) { + Scaffold( + containerColor = DevkitWalletColors.primary, + ) { paddingValues -> + ConstraintLayout( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + val (logo, active, create, recover) = createRefs() + + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 90.dp) + .constrainAs(logo) { + top.linkTo(parent.top) + }, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = R.drawable.ic_testnet_logo), + contentDescription = "Bitcoin testnet logo", + Modifier.size(90.dp), + ) + Spacer(modifier = Modifier.padding(8.dp)) + Text( + text = "Devkit\nWallet", + color = DevkitWalletColors.white, + fontSize = 28.sp, + lineHeight = 38.sp, + fontFamily = monoBold, + ) + } + + Button( + onClick = { navController.navigate(ActiveWalletsScreen) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + enabled = true, + modifier = + Modifier + .size(width = 300.dp, height = 150.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 8.dp, shape = RoundedCornerShape(16.dp)) + .constrainAs(active) { + bottom.linkTo(create.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) { + Text( + text = "Use an\nActive Wallet", + // fontSize = 18.sp, + textAlign = TextAlign.Center, + // lineHeight = 28.sp, + ) + } + + Button( + onClick = { navController.navigate(CreateNewWalletScreen) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = + Modifier + .size(width = 300.dp, height = 150.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 8.dp, shape = RoundedCornerShape(16.dp)) + .constrainAs(create) { + bottom.linkTo(recover.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) { + Text( + text = "Create a\nNew Wallet", + // fontSize = 18.sp, + textAlign = TextAlign.Center, + // lineHeight = 28.sp, + ) + } + + Button( + onClick = { navController.navigate(WalletRecoveryScreen) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = + Modifier + .size(width = 300.dp, height = 150.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 8.dp, shape = RoundedCornerShape(16.dp)) + .constrainAs(recover) { + bottom.linkTo(parent.bottom, margin = 70.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) { + Text( + text = "Recover an\nExisting Wallet", + // fontSize = 18.sp, + textAlign = TextAlign.Center, + // lineHeight = 28.sp, + ) + } + } + } +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewWalletChoiceScreen() { +// WalletChoiceScreen(rememberNavController()) +// } diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/RBFScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/RBFScreen.kt" new file mode 100644 index 0000000..7d64939 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/RBFScreen.kt" @@ -0,0 +1,260 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.wallet + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController + +private const val TAG = "RBFScreen" + +@Composable +internal fun RBFScreen( + txid: String?, + navController: NavController, + paddingValues: PaddingValues = PaddingValues(0.dp), +) { +// if (txid.isNullOrEmpty()) { +// navController.popBackStack() +// } +// var transaction: TransactionDetails? = getTransaction(txid = txid) +// if (transaction == null) { +// navController.popBackStack() +// } +// transaction = transaction as TransactionDetails +// val context = LocalContext.current +// +// val amount = (transaction.sent - transaction.received - (transaction.fee ?: 0UL)).toString() +// val feeRate: MutableState = rememberSaveable { mutableStateOf("") } +// val (showDialog, setShowDialog) = rememberSaveable { mutableStateOf(false) } +// +// ConstraintLayout( +// modifier = Modifier +// .fillMaxSize() +// .padding(paddingValues) +// .background(DevkitWalletColors.primary) +// ) { +// val (screenTitle, transactionInputs, bottomButtons) = createRefs() +// +// Text( +// text = "Send Bitcoin", +// color = DevkitWalletColors.white, +// fontSize = 28.sp, +// textAlign = TextAlign.Center, +// modifier = Modifier +// .constrainAs(screenTitle) { +// top.linkTo(parent.top) +// start.linkTo(parent.start) +// end.linkTo(parent.end) +// } +// .padding(top = 70.dp) +// ) +// +// Column( +// horizontalAlignment = Alignment.CenterHorizontally, +// verticalArrangement = Arrangement.Center, +// modifier = Modifier.constrainAs(transactionInputs) { +// top.linkTo(screenTitle.bottom) +// bottom.linkTo(bottomButtons.top) +// start.linkTo(parent.start) +// end.linkTo(parent.end) +// height = Dimension.fillToConstraints +// } +// ) { +// ShowTxnDetail(name = "Transaction Id",content = txid!!) +// ShowTxnDetail(name = "Amount", content = amount) +// TransactionFeeInput(feeRate = feeRate) +// BumpFeeDialog( +// txid = txid, +// amount = amount, +// feeRate = feeRate, +// showDialog = showDialog, +// setShowDialog = setShowDialog, +// context = context +// ) +// } +// Column( +// Modifier +// .constrainAs(bottomButtons) { +// bottom.linkTo(parent.bottom) +// start.linkTo(parent.start) +// end.linkTo(parent.end) +// } +// .padding(bottom = 32.dp) +// ) { +// Button( +// onClick = { setShowDialog(true) }, +// colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent2), +// shape = RoundedCornerShape(16.dp), +// modifier = Modifier +// .height(80.dp) +// .fillMaxWidth(0.9f) +// .padding(vertical = 8.dp, horizontal = 8.dp) +// .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) +// ) { +// Text( +// text = "broadcast transaction", +// fontSize = 14.sp, +// textAlign = TextAlign.Center, +// lineHeight = 28.sp, +// ) +// } +// Button( +// onClick = { navController.navigate(Screen.HomeScreen.route) }, +// colors = ButtonDefaults.buttonColors(DevkitWalletColors.primaryLight), +// shape = RoundedCornerShape(16.dp), +// modifier = Modifier +// .height(80.dp) +// .fillMaxWidth(0.9f) +// .padding(vertical = 8.dp, horizontal = 8.dp) +// .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) +// ) { +// Text( +// text = "back to wallet", +// fontSize = 14.sp, +// textAlign = TextAlign.Center, +// lineHeight = 28.sp, +// ) +// } +// } +// } +} +// +// @OptIn(ExperimentalMaterial3Api::class) +// @Composable +// private fun ShowTxnDetail(name: String, content: String) { +// Row( +// verticalAlignment = Alignment.CenterVertically, +// modifier = Modifier.fillMaxWidth(fraction = 0.9f) +// ) { +// OutlinedTextField( +// modifier = Modifier +// .padding(vertical = 8.dp) +// .weight(0.5f), +// value = content, +// onValueChange = { }, +// label = { +// Text( +// text = name, +// color = DevkitWalletColors.white, +// ) +// }, +// singleLine = true, +// textStyle = TextStyle(color = DevkitWalletColors.white), +// colors = TextFieldDefaults.outlinedTextFieldColors( +// focusedBorderColor = DevkitWalletColors.accent1, +// unfocusedBorderColor = DevkitWalletColors.white, +// cursorColor = DevkitWalletColors.accent1, +// ), +// enabled = false, +// ) +// } +// } +// +// @OptIn(ExperimentalMaterial3Api::class) +// @Composable +// private fun TransactionFeeInput(feeRate: MutableState) { +// Column(horizontalAlignment = Alignment.CenterHorizontally) { +// OutlinedTextField( +// modifier = Modifier +// .padding(vertical = 8.dp) +// .fillMaxWidth(0.9f), +// value = feeRate.value, +// onValueChange = { newValue: String -> +// feeRate.value = newValue.filter { it.isDigit() } +// }, +// singleLine = true, +// textStyle = TextStyle(color = DevkitWalletColors.white), +// label = { +// Text( +// text = "New fee rate", +// color = DevkitWalletColors.white, +// ) +// }, +// colors = TextFieldDefaults.outlinedTextFieldColors( +// focusedBorderColor = DevkitWalletColors.accent1, +// unfocusedBorderColor = DevkitWalletColors.white, +// cursorColor = DevkitWalletColors.accent1, +// ), +// ) +// } +// } +// +// @Composable +// fun BumpFeeDialog( +// txid: String, +// amount: String, +// showDialog: Boolean, +// setShowDialog: (Boolean) -> Unit, +// context: Context, +// feeRate: MutableState, +// ) { +// if (showDialog) { +// var confirmationText = "Confirm Transaction : \nTxid : $txid\nAmount : $amount" +// if (feeRate.value.isNotEmpty()) { +// confirmationText += "Fee Rate : ${feeRate.value.toULong()}" +// } +// AlertDialog( +// containerColor = DevkitWalletColors.primaryLight, +// onDismissRequest = {}, +// title = { +// Text( +// text = "Confirm transaction", +// color = DevkitWalletColors.white +// ) +// }, +// text = { +// Text( +// text = confirmationText, +// color = DevkitWalletColors.white +// ) +// }, +// confirmButton = { +// TextButton( +// onClick = { +// if (feeRate.value.isNotEmpty()) { +// broadcastTransaction(txid = txid, feeRate = feeRate.value.toFloat()) +// } else { +// Toast.makeText(context, "Fee is empty!", Toast.LENGTH_SHORT).show() +// } +// setShowDialog(false) +// }, +// ) { +// Text( +// text = "Confirm", +// color = DevkitWalletColors.white +// ) +// } +// }, +// dismissButton = { +// TextButton( +// onClick = { +// setShowDialog(false) +// }, +// ) { +// Text( +// text = "Cancel", +// color = DevkitWalletColors.white +// ) +// } +// }, +// ) +// } +// } +// +// private fun broadcastTransaction(txid: String, feeRate: Float = 1F) { +// Log.i(TAG, "Attempting to broadcast transaction with inputs: txid $txid, fee rate: $feeRate") +// try { +// // create, sign, and broadcast +// val psbt: PartiallySignedTransaction = Wallet.createBumpFeeTransaction(txid = txid, feeRate = feeRate) +// Wallet.sign(psbt) +// val newTxid: String = Wallet.broadcast(psbt) +// Log.i(TAG, "Transaction was broadcast! txid: $newTxid") +// } catch (e: Throwable) { +// Log.i(TAG, "Broadcast error: ${e.message}") +// } +// } diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/ReceiveScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/ReceiveScreen.kt" new file mode 100644 index 0000000..145c023 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/ReceiveScreen.kt" @@ -0,0 +1,239 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.wallet + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.core.graphics.createBitmap +import androidx.navigation.NavController +import com.composables.icons.lucide.ClipboardCopy +import com.composables.icons.lucide.Lucide +import com.google.zxing.BarcodeFormat +import com.google.zxing.common.BitMatrix +import com.google.zxing.qrcode.QRCodeWriter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.bitcoindevkit.devkitwallet.presentation.navigation.HomeScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.ReceiveScreenAction +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.ReceiveScreenState + +private const val TAG = "ReceiveScreen" + +@Composable +internal fun ReceiveScreen( + state: ReceiveScreenState, + onAction: (ReceiveScreenAction) -> Unit, + navController: NavController, +) { + Log.i(TAG, "We are recomposing the ReceiveScreen") + val snackbarHostState = + remember { + SnackbarHostState() + } + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { + SecondaryScreensAppBar( + title = "Receive Address", + navigation = { navController.navigate(HomeScreen) }, + ) + }, + containerColor = DevkitWalletColors.primary, + ) { paddingValues -> + ConstraintLayout( + modifier = + Modifier + .padding(paddingValues) + .fillMaxSize(), + ) { + val (QRCode, bottomButtons) = createRefs() + val context = LocalContext.current + val scope = rememberCoroutineScope() + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = + Modifier + .constrainAs(QRCode) { + top.linkTo(parent.top) + bottom.linkTo(bottomButtons.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + }.padding(horizontal = 32.dp), + ) { + val QR: ImageBitmap? = state.address?.let { addressToQR(it) } + Log.i("ReceiveScreen", "New receive address is ${state.address}") + if (QR != null) { + Image( + bitmap = QR, + contentDescription = "Bitcoindevkit website QR code", + Modifier.size(250.dp).clip(RoundedCornerShape(16.dp)), + ) + Spacer(modifier = Modifier.padding(vertical = 16.dp)) + Box { + SelectionContainer { + Text( + modifier = + Modifier + .clickable { + copyToClipboard( + state.address, + context, + scope, + snackbarHostState, + null, + ) + }.background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp), + ).padding(12.dp), + text = state.address.chunked(4).joinToString(" "), + fontFamily = monoRegular, + color = DevkitWalletColors.white, + ) + } + Icon( + Lucide.ClipboardCopy, + tint = Color.White, + contentDescription = "Copy to clipboard", + modifier = + Modifier + .padding(8.dp) + .size(20.dp) + .align(Alignment.BottomEnd), + ) + } + Spacer(modifier = Modifier.padding(vertical = 16.dp)) + Text( + text = "Wallet address index: ${state.addressIndex}", + fontFamily = monoRegular, + color = DevkitWalletColors.white, + modifier = Modifier.align(Alignment.Start), + ) + } + } + + Column( + Modifier + .constrainAs(bottomButtons) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }.padding(bottom = 24.dp), + ) { + Button( + onClick = { onAction(ReceiveScreenAction.UpdateAddress) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = + Modifier + .height(80.dp) + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)), + ) { + Text( + text = "Generate address", + style = standardText, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + } + } + } + } +} + +private fun addressToQR(address: String): ImageBitmap? { + Log.i(TAG, "We are generating the QR code for address $address") + try { + val qrCodeWriter: QRCodeWriter = QRCodeWriter() + val bitMatrix: BitMatrix = qrCodeWriter.encode(address, BarcodeFormat.QR_CODE, 1000, 1000) + val bitMap = createBitmap(1000, 1000) + for (x in 0 until 1000) { + for (y in 0 until 1000) { + // DevkitWalletColors.primaryDark for dark and DevkitWalletColors.white for light + bitMap.setPixel(x, y, if (bitMatrix[x, y]) 0xff203b46.toInt() else 0xffffffff.toInt()) + } + } + return bitMap.asImageBitmap() + } catch (e: Throwable) { + Log.i("ReceiveScreen", "Error with QRCode generation, $e") + } + return null +} + +fun copyToClipboard( + content: String, + context: Context, + scope: CoroutineScope, + snackbarHostState: SnackbarHostState, + setCopyClicked: ( + (Boolean) -> Unit + )?, +) { + val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip: ClipData = ClipData.newPlainText("", content) + clipboard.setPrimaryClip(clip) + scope.launch { + snackbarHostState.showSnackbar("Copied address to clipboard!") + delay(1000) + if (setCopyClicked != null) { + setCopyClicked(false) + } + } +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewReceiveScreen() { +// ReceiveScreen(rememberNavController()) +// } diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt" new file mode 100644 index 0000000..32b0128 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt" @@ -0,0 +1,568 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.wallet + +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.BottomSheetScaffoldState +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.navigation.NavController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.bitcoindevkit.devkitwallet.presentation.navigation.HomeScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.standardText +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.SendViewModel +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.Recipient +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.SendScreenAction +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.TransactionType +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.TxDataBundle + +private const val TAG = "SendScreen" + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SendScreen(navController: NavController, sendViewModel: SendViewModel) { + val onAction = sendViewModel::onAction + + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val recipientList: MutableList = remember { mutableStateListOf(Recipient(address = "", amount = 0u)) } + val feeRate: MutableState = rememberSaveable { mutableStateOf("") } + val (showDialog, setShowDialog) = rememberSaveable { mutableStateOf(false) } + + val sendAll: MutableState = remember { mutableStateOf(false) } + val opReturnMsg: MutableState = remember { mutableStateOf(null) } + + val bottomSheetScaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState() + + BottomSheetScaffold( + topBar = { + SecondaryScreensAppBar( + title = "Send Bitcoin", + navigation = { navController.navigate(HomeScreen) }, + ) + }, + sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + sheetContent = { AdvancedOptions(sendAll, opReturnMsg, recipientList) }, + sheetContainerColor = DevkitWalletColors.primaryDark, + scaffoldState = bottomSheetScaffoldState, + sheetPeekHeight = 0.dp, + ) { paddingValues -> + ConstraintLayout( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues) + .background(DevkitWalletColors.primary), + ) { + val (transactionInputs, bottomButtons) = createRefs() + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = + Modifier.constrainAs(transactionInputs) { + top.linkTo(parent.top) + bottom.linkTo(bottomButtons.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + }, + ) { + TransactionRecipientInput(recipientList = recipientList) + TransactionAmountInput( + recipientList = recipientList, + transactionType = if (sendAll.value) TransactionType.SEND_ALL else TransactionType.STANDARD, + ) + TransactionFeeInput(feeRate = feeRate) + MoreOptions(coroutineScope = coroutineScope, bottomSheetScaffoldState = bottomSheetScaffoldState) + Dialog( + recipientList = recipientList, + feeRate = feeRate, + showDialog = showDialog, + setShowDialog = setShowDialog, + transactionType = if (sendAll.value) TransactionType.SEND_ALL else TransactionType.STANDARD, + opReturnMsg = opReturnMsg.value, + context = context, + onAction = onAction, + ) + } + Column( + Modifier + .constrainAs(bottomButtons) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }.padding(bottom = 32.dp), + ) { + Button( + onClick = { setShowDialog(true) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent2), + shape = RoundedCornerShape(16.dp), + modifier = + Modifier + .height(80.dp) + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)), + ) { + Text( + text = "broadcast transaction", + fontSize = 14.sp, + fontFamily = quattroRegular, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + } + } + } + } +} + +@Composable +internal fun AdvancedOptions( + sendAll: MutableState, + opReturnMsg: MutableState, + recipientList: MutableList, +) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + Row( + Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = "Advanced Options", + color = DevkitWalletColors.white, + fontSize = 18.sp, + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = "Send All", + style = standardText, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + Spacer(modifier = Modifier.width(8.dp)) + Switch( + checked = sendAll.value, + onCheckedChange = { + sendAll.value = !sendAll.value + while (recipientList.size > 1) { + recipientList.removeLast() + } + }, + colors = + SwitchDefaults.colors( + uncheckedBorderColor = DevkitWalletColors.primaryDark, + uncheckedThumbColor = DevkitWalletColors.primaryDark, + uncheckedTrackColor = DevkitWalletColors.white, + checkedThumbColor = DevkitWalletColors.white, + checkedTrackColor = DevkitWalletColors.accent1, + ), + ) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + modifier = + Modifier + .padding(vertical = 8.dp) + .weight(0.5f), + value = opReturnMsg.value ?: "", + onValueChange = { + opReturnMsg.value = it + }, + label = { + Text( + text = "Optional OP_RETURN message", + color = DevkitWalletColors.white, + ) + }, + singleLine = true, + textStyle = TextStyle(color = DevkitWalletColors.white), + colors = + OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + } + + Row( + Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = "Number of Recipients", + style = standardText, + ) + } + + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Button( + onClick = { + if (recipientList.size > 1) { + recipientList.removeLast() + } + }, + enabled = !sendAll.value, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent2), + shape = RoundedCornerShape(16.dp), + modifier = Modifier.width(70.dp), + ) { + Text(text = "-") + } + + Text( + text = "${recipientList.size}", + color = DevkitWalletColors.white, + fontSize = 18.sp, + ) + + Button( + onClick = { recipientList.add(Recipient("", 0u)) }, + enabled = !sendAll.value, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent1), + shape = RoundedCornerShape(16.dp), + modifier = Modifier.width(70.dp), + ) { + Text(text = "+") + } + } + + Spacer(modifier = Modifier.height(32.dp)) + } +} + +@Composable +private fun TransactionRecipientInput(recipientList: MutableList) { + LazyColumn( + modifier = + Modifier + .fillMaxWidth(0.9f) + .heightIn(max = 100.dp), + ) { + itemsIndexed(recipientList) { index, _ -> + val recipientAddress: MutableState = rememberSaveable { mutableStateOf("") } + + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + modifier = + Modifier + .padding(vertical = 8.dp) + .weight(0.5f), + value = recipientAddress.value, + onValueChange = { + recipientAddress.value = it + recipientList[index].address = it + }, + label = { + Text( + text = "Recipient address ${index + 1}", + color = DevkitWalletColors.white, + ) + }, + singleLine = true, + textStyle = TextStyle(color = DevkitWalletColors.white), + colors = + OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + } + } + } +} + +fun checkRecipientList( + recipientList: MutableList, + feeRate: MutableState, + context: Context, +): Boolean { + if (recipientList.size > 4) { + Toast.makeText(context, "Too many recipients", Toast.LENGTH_SHORT).show() + return false + } + for (recipient in recipientList) { + if (recipient.address == "") { + Toast.makeText(context, "Address is empty", Toast.LENGTH_SHORT).show() + return false + } + } + if (feeRate.value.isBlank()) { + Toast.makeText(context, "Fee rate is empty", Toast.LENGTH_SHORT).show() + return false + } + return true +} + +@Composable +private fun TransactionAmountInput(recipientList: MutableList, transactionType: TransactionType) { + LazyColumn( + modifier = + Modifier + .fillMaxWidth(0.9f) + .heightIn(max = 100.dp), + ) { + itemsIndexed(recipientList) { index, _ -> + val amount: MutableState = rememberSaveable { mutableStateOf("") } + + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + modifier = + Modifier + .padding(vertical = 8.dp) + .weight(0.5f), + value = amount.value, + onValueChange = { + amount.value = it + recipientList[index].amount = it.toULong() + }, + label = { + when (transactionType) { + TransactionType.SEND_ALL -> { + Text( + text = "Amount (Send All)", + color = DevkitWalletColors.white, + ) + } + else -> { + Text( + text = "Amount ${index + 1}", + color = DevkitWalletColors.white, + ) + } + } + }, + singleLine = true, + textStyle = TextStyle(color = DevkitWalletColors.white), + colors = + OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + enabled = ( + when (transactionType) { + TransactionType.SEND_ALL -> false + else -> true + } + ), + ) + } + } + } +} + +@Composable +private fun TransactionFeeInput(feeRate: MutableState) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + OutlinedTextField( + modifier = + Modifier + .padding(vertical = 8.dp) + .fillMaxWidth(0.9f), + value = feeRate.value, + onValueChange = { newValue: String -> + feeRate.value = newValue.filter { it.isDigit() } + }, + singleLine = true, + textStyle = TextStyle(color = DevkitWalletColors.white), + label = { + Text( + text = "Fee rate", + color = DevkitWalletColors.white, + ) + }, + colors = + OutlinedTextFieldDefaults.colors( + cursorColor = DevkitWalletColors.accent1, + focusedBorderColor = DevkitWalletColors.accent1, + unfocusedBorderColor = DevkitWalletColors.white, + ), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MoreOptions(coroutineScope: CoroutineScope, bottomSheetScaffoldState: BottomSheetScaffoldState) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = + Modifier + .padding(vertical = 8.dp) + .background(DevkitWalletColors.secondary), + ) { + Button( + onClick = { + coroutineScope.launch { + bottomSheetScaffoldState.bottomSheetState.expand() + } + }, + colors = ButtonDefaults.buttonColors(Color.Transparent), + modifier = + Modifier + .height(60.dp) + .fillMaxWidth(fraction = 0.9f) + .padding(vertical = 8.dp), + ) { + Text( + text = "advanced options", + style = standardText, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + } + } +} + +@Composable +private fun Dialog( + recipientList: MutableList, + feeRate: MutableState, + showDialog: Boolean, + setShowDialog: (Boolean) -> Unit, + transactionType: TransactionType, + opReturnMsg: String?, + context: Context, + onAction: (SendScreenAction) -> Unit, +) { + if (showDialog) { + var confirmationText = "Confirm Transaction : \n" + recipientList.forEach { confirmationText += "${it.address}, ${it.amount}\n" } + if (feeRate.value.isNotEmpty()) { + confirmationText += "Fee Rate : ${feeRate.value.toULong()}" + } + if (!opReturnMsg.isNullOrEmpty()) { + confirmationText += "OP_RETURN Message : $opReturnMsg" + } + AlertDialog( + containerColor = DevkitWalletColors.primaryLight, + onDismissRequest = {}, + title = { + Text( + text = "Confirm transaction", + color = DevkitWalletColors.white, + ) + }, + text = { + Text( + text = confirmationText, + color = DevkitWalletColors.white, + ) + }, + confirmButton = { + TextButton( + onClick = { + if (checkRecipientList(recipientList = recipientList, feeRate = feeRate, context = context)) { + val txDataBundle = + TxDataBundle( + recipients = recipientList, + feeRate = feeRate.value.toULong(), + transactionType = transactionType, + opReturnMsg = opReturnMsg, + ) + onAction(SendScreenAction.Broadcast(txDataBundle)) + setShowDialog(false) + } + }, + ) { + Text( + text = "Confirm", + color = DevkitWalletColors.white, + ) + } + }, + dismissButton = { + TextButton( + onClick = { + setShowDialog(false) + }, + ) { + Text( + text = "Cancel", + color = DevkitWalletColors.white, + ) + } + }, + ) + } +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewSendScreen() { +// SendScreen(rememberNavController()) +// } diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionHistoryScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionHistoryScreen.kt" new file mode 100644 index 0000000..c4aa255 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionHistoryScreen.kt" @@ -0,0 +1,72 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.wallet + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.domain.Wallet +import org.bitcoindevkit.devkitwallet.presentation.navigation.HomeScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.TransactionScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.ui.components.ConfirmedTransactionCard +import org.bitcoindevkit.devkitwallet.presentation.ui.components.PendingTransactionCard +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +private const val TAG = "TransactionHistoryScreen" + +@Composable +internal fun TransactionHistoryScreen(navController: NavController, activeWallet: Wallet) { + val (pendingTransactions, confirmedTransactions) = activeWallet.getAllTxDetails().partition { it.pending } + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Transaction History", + navigation = { navController.navigate(HomeScreen) }, + ) + }, + containerColor = DevkitWalletColors.primary, + ) { paddingValues -> + val scrollState = rememberScrollState() + Column( + modifier = + Modifier + .padding(paddingValues) + .fillMaxSize() + .padding(top = 6.dp) + .verticalScroll(state = scrollState), + ) { + if (pendingTransactions.isNotEmpty()) { + pendingTransactions.forEach { + PendingTransactionCard(details = it, navController = navController) + } + } + if (confirmedTransactions.isNotEmpty()) { + confirmedTransactions.sortedBy { it.confirmationBlock?.height }.forEach { + ConfirmedTransactionCard(it, navController) + } + } + } + } +} + +fun viewTransaction(navController: NavController, txid: String) { + navController.navigate(TransactionScreen(txid)) +} + +// @Preview(device = Devices.PIXEL_4, showBackground = true) +// @Composable +// internal fun PreviewTransactionsScreen() { +// TransactionsScreen(rememberNavController()) +// } diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt" new file mode 100644 index 0000000..149e9dd --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/TransactionScreen.kt" @@ -0,0 +1,203 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.wallet + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.navigation.NavController +import org.bitcoindevkit.devkitwallet.presentation.navigation.RbfScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar + +@Composable +internal fun TransactionScreen(txid: String?, navController: NavController) { + // val transaction = getTransaction(txid = txid) + // if (transaction == null) { + // navController.popBackStack() + // } + // val transactionDetail = getTransactionDetails(transaction = transaction!!) + + Scaffold( + topBar = { + SecondaryScreensAppBar( + title = "Transaction Details", + navigation = { navController.navigateUp() }, + ) + }, + containerColor = DevkitWalletColors.primary, + ) { paddingValues -> + ConstraintLayout( + modifier = + Modifier + .fillMaxSize() + .background(DevkitWalletColors.primary) + .padding(paddingValues), + ) { + val (screenTitle, transactions, bottomButton) = createRefs() + + Column( + modifier = + Modifier + .constrainAs(screenTitle) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }.padding(top = 70.dp), + ) { + Text( + text = "Transaction", + color = DevkitWalletColors.white, + fontSize = 28.sp, + fontFamily = monoRegular, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + // Text( + // text = transactionTitle(transaction = transaction), + // color = DevkitWalletColors.white, + // fontSize = 14.sp, + // textAlign = TextAlign.Center, + // modifier = Modifier.padding(horizontal = 16.dp) + // ) + } + + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = + Modifier.constrainAs(transactions) { + top.linkTo(screenTitle.bottom) + bottom.linkTo(bottomButton.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + }, + ) { + // items(transactionDetail) { + // Row( + // modifier = Modifier + // .fillMaxWidth() + // .padding(all = 16.dp) + // ) { + // Text( + // text = "${it.first} :", + // fontSize = 16.sp, + // color = DevkitWalletColors.white, + // ) + // Text( + // text = it.second, + // fontSize = 16.sp, + // textAlign = TextAlign.End, + // color = DevkitWalletColors.white, + // modifier = Modifier.fillMaxWidth() + // ) + // } + // } + } + + Column( + modifier = + Modifier + .fillMaxWidth(0.9f) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + .constrainAs(bottomButton) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) { + TransactionDetailButton( + content = "increase fees", + navController = navController, + txid = txid, + ) + } + } + } +} + +@Composable +fun TransactionDetailButton(content: String, navController: NavController, txid: String?) { + Button( + onClick = { + when (content) { + "increase fees" -> { + navController.navigate(RbfScreen(txid!!)) + } + "back to transaction list" -> { + navController.navigateUp() + } + } + }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.secondary), + shape = RoundedCornerShape(16.dp), + modifier = + Modifier + .height(60.dp) + .fillMaxWidth(), + ) { + Text( + text = content, + fontSize = 14.sp, + fontFamily = monoRegular, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + ) + } +} + +// fun getTransactionDetails(transaction: TransactionDetails): List> { +// val transactionDetails = mutableListOf>() +// +// if (transaction.confirmationTime != null) { +// transactionDetails.add(Pair("Status", "Confirmed")) +// transactionDetails.add(Pair("Timestamp", transaction.confirmationTime!!.timestamp.timestampToString())) +// transactionDetails.add(Pair("Received", (if (transaction.received < transaction.sent) 0 else transaction.received).toString())) +// transactionDetails.add(Pair("Sent", (if (transaction.sent < transaction.received) 0 else transaction.sent - transaction.received - transaction.fee!!).toString())) +// transactionDetails.add(Pair("Fees", transaction.fee.toString())) +// transactionDetails.add(Pair("Block", transaction.confirmationTime!!.height.toString())) +// } else { +// transactionDetails.add(Pair("Status", "Pending")) +// transactionDetails.add(Pair("Timestamp", "Pending")) +// transactionDetails.add(Pair("Received", (if (transaction.received < transaction.sent) 0 else transaction.received).toString())) +// transactionDetails.add(Pair("Sent", (if (transaction.sent < transaction.received) 0 else transaction.sent - transaction.received - transaction.fee!!).toString())) +// transactionDetails.add(Pair("Fees", transaction.fee.toString())) +// } +// return transactionDetails +// } +// +// fun transactionTitle(transaction: TransactionDetails): String { +// return transaction.txid +// } +// +// fun getTransaction(txid: String?): TransactionDetails? { +// if (txid.isNullOrEmpty()) { +// return null +// } +// return Wallet.getTransaction(txid = txid) +// } diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt" new file mode 100644 index 0000000..0321d4d --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/WalletHomeScreen.kt" @@ -0,0 +1,305 @@ +/* + * Copyright 2021-2024 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.presentation.ui.screens.wallet + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.CurrencyBitcoin +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.DrawerState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import com.composables.icons.lucide.Lucide +import com.composables.icons.lucide.Menu +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.bitcoindevkit.devkitwallet.domain.CurrencyUnit +import org.bitcoindevkit.devkitwallet.domain.utils.formatInBtc +import org.bitcoindevkit.devkitwallet.presentation.navigation.ReceiveScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.SendScreen +import org.bitcoindevkit.devkitwallet.presentation.navigation.TransactionHistoryScreen +import org.bitcoindevkit.devkitwallet.presentation.theme.DevkitWalletColors +import org.bitcoindevkit.devkitwallet.presentation.theme.monoRegular +import org.bitcoindevkit.devkitwallet.presentation.theme.quattroBold +import org.bitcoindevkit.devkitwallet.presentation.ui.components.CustomSnackbar +import org.bitcoindevkit.devkitwallet.presentation.ui.components.NeutralButton +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenAction +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenState + +private const val TAG = "WalletHomeScreen" + +@Composable +internal fun WalletHomeScreen( + state: WalletScreenState, + onAction: (WalletScreenAction) -> Unit, + drawerState: DrawerState, + navController: NavHostController, +) { + val snackbarHostState = remember { SnackbarHostState() } + val networkAvailable: Boolean = isOnline(LocalContext.current) + val interactionSource = remember { MutableInteractionSource() } + val scope: CoroutineScope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + onAction(WalletScreenAction.UpdateBalance) + } + + Scaffold( + topBar = { WalletAppBar(scope = scope, drawerState = drawerState) }, + containerColor = DevkitWalletColors.primary, + // snackbarHost = { SnackbarHost(snackbarHostState) }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) { data -> + CustomSnackbar(data) + } + }, + ) { paddingValues -> + + // If a new snackbar has be triggered, show it + state.snackbarMessage?.let { message -> + Log.i("WalletHomeScreen", "Showing snackbar: $message") + LaunchedEffect(message) { + scope.launch { + snackbarHostState.showSnackbar(message) + onAction(WalletScreenAction.ClearSnackbar) + } + } + } + + Column( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.padding(24.dp)) + Row( + Modifier + .clickable( + interactionSource, + indication = null, + onClick = { onAction(WalletScreenAction.SwitchUnit) }, + ).fillMaxWidth(0.9f) + .padding(horizontal = 8.dp) + .background( + color = DevkitWalletColors.primaryLight, + shape = RoundedCornerShape(16.dp), + ).height(100.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + when (state.unit) { + CurrencyUnit.Bitcoin -> { + Icon( + imageVector = Icons.Rounded.CurrencyBitcoin, + tint = DevkitWalletColors.white, + contentDescription = "Bitcoin testnet logo", + modifier = + Modifier + .align(Alignment.CenterVertically) + .size(48.dp), + ) + Text( + text = state.balance.formatInBtc(), + fontFamily = monoRegular, + fontSize = 32.sp, + color = DevkitWalletColors.white, + ) + } + CurrencyUnit.Satoshi -> { + Text( + text = "${state.balance} sat", + fontFamily = monoRegular, + fontSize = 32.sp, + color = DevkitWalletColors.white, + ) + } + } + } + Spacer(modifier = Modifier.padding(4.dp)) + if (networkAvailable) { + Row( + modifier = Modifier.height(40.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // if (state.syncing) LoadingAnimation() + } + } + + if (!networkAvailable) { + Row( + Modifier + .fillMaxWidth() + .background(color = DevkitWalletColors.accent2) + .height(50.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = "Network unavailable", + fontFamily = monoRegular, + fontSize = 16.sp, + color = DevkitWalletColors.white, + ) + } + } + + NeutralButton( + text = "transaction history", + enabled = networkAvailable, + onClick = { navController.navigate(TransactionHistoryScreen) }, + ) + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .height(140.dp) + .fillMaxWidth(0.9f), + ) { + Button( + onClick = { navController.navigate(ReceiveScreen) }, + colors = ButtonDefaults.buttonColors(DevkitWalletColors.accent1), + shape = RoundedCornerShape(16.dp), + modifier = + Modifier + .height(160.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)), + ) { + Text( + text = "receive", + fontSize = 16.sp, + textAlign = TextAlign.End, + lineHeight = 28.sp, + modifier = + Modifier + .fillMaxWidth(0.4f) + .align(Alignment.Bottom), + ) + } + + Button( + onClick = { navController.navigate(SendScreen) }, + colors = + ButtonDefaults.buttonColors( + containerColor = DevkitWalletColors.accent2, + disabledContainerColor = DevkitWalletColors.accent2, + ), + shape = RoundedCornerShape(16.dp), + enabled = networkAvailable, + modifier = + Modifier + .height(160.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)), + ) { + Text( + text = "send", + fontSize = 16.sp, + textAlign = TextAlign.End, + lineHeight = 28.sp, + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.Bottom), + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun WalletAppBar(scope: CoroutineScope, drawerState: DrawerState) { + CenterAlignedTopAppBar( + title = { + Text( + text = "Devkit Wallet", + color = DevkitWalletColors.white, + // fontFamily = quattroRegular, + fontFamily = quattroBold, + fontSize = 20.sp, + ) + }, + navigationIcon = { + IconButton(onClick = { scope.launch { drawerState.open() } }) { + Icon( + imageVector = Lucide.Menu, + contentDescription = "Open drawer", + tint = DevkitWalletColors.white, + ) + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = DevkitWalletColors.primaryDark, + ), + ) +} + +fun isOnline(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val capabilities = + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + if (capabilities != null) { + when { + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> { + 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 + } + } + } + return false +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/AddressViewModel.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/AddressViewModel.kt" new file mode 100644 index 0000000..dff1dd0 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/AddressViewModel.kt" @@ -0,0 +1,39 @@ +/* + * Copyright 2021-2024 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.presentation.viewmodels + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import org.bitcoindevkit.AddressInfo +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.ReceiveScreenAction +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.ReceiveScreenState + +internal class AddressViewModel(private val wallet: Wallet) : ViewModel() { + var state: ReceiveScreenState by mutableStateOf(ReceiveScreenState()) + private set + + fun onAction(action: ReceiveScreenAction) { + when (action) { + is ReceiveScreenAction.UpdateAddress -> updateAddress() + } + } + + private fun updateAddress() { + val newAddress: AddressInfo = wallet.getNewAddress() + DwLogger.log(INFO, "Revealing new address at index ${newAddress.index}") + + state = + ReceiveScreenState( + address = newAddress.address.toString(), + addressIndex = newAddress.index, + ) + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt" new file mode 100644 index 0000000..4cc7ca5 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/SendViewModel.kt" @@ -0,0 +1,51 @@ +/* + * Copyright 2021-2024 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.presentation.viewmodels + +import android.util.Log +import androidx.lifecycle.ViewModel +import org.bitcoindevkit.FeeRate +import org.bitcoindevkit.Psbt +import org.bitcoindevkit.devkitwallet.domain.Wallet +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.SendScreenAction +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.TransactionType +import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.TxDataBundle + +private const val TAG = "SendViewModel" + +internal class SendViewModel(private val wallet: Wallet) : ViewModel() { + fun onAction(action: SendScreenAction) { + when (action) { + is SendScreenAction.Broadcast -> broadcast(action.txDataBundle) + } + } + + 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") + } + 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: Throwable) { + Log.i(TAG, "Broadcast error: ${e.message}") + } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt" new file mode 100644 index 0000000..29e0056 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/WalletViewModel.kt" @@ -0,0 +1,157 @@ +/* + * Copyright 2021-2024 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.presentation.viewmodels + +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +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.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 + +private const val TAG = "WalletViewModel" + +internal class WalletViewModel( + private val wallet: Wallet, +) : ViewModel() { + var state: WalletScreenState by mutableStateOf(WalletScreenState()) + private set + + private val kyotoCoroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO) + private var latestBlock: Int = 0 + + fun onAction(action: WalletScreenAction) { + when (action) { + WalletScreenAction.SwitchUnit -> switchUnit() + WalletScreenAction.UpdateBalance -> updateBalance() + WalletScreenAction.StartKyotoNode -> startKyotoNode() + WalletScreenAction.StopKyotoNode -> stopKyotoNode() + WalletScreenAction.StartKyotoSync -> startKyotoSync() + WalletScreenAction.ClearSnackbar -> clearSnackbar() + } + } + + private fun showSnackbar(message: String) { + state = state.copy(snackbarMessage = message) + } + + private fun clearSnackbar() { + state = state.copy(snackbarMessage = null) + } + + private fun switchUnit() { + state = + when (state.unit) { + CurrencyUnit.Bitcoin -> state.copy(unit = CurrencyUnit.Satoshi) + CurrencyUnit.Satoshi -> state.copy(unit = CurrencyUnit.Bitcoin) + } + } + + private fun updateLatestBlock(blockHeight: UInt) { + state = state.copy(latestBlock = blockHeight) + } + + private fun updateBalance() { + viewModelScope.launch(Dispatchers.IO) { + val newBalance = wallet.getBalance() + Log.i("Kyoto", "New balance: $newBalance") + DwLogger.log(INFO, "New balance: $newBalance") + + state = state.copy(balance = newBalance) + Log.i("Kyoto", "New state object: $state") + DwLogger.log(INFO, "New state object: $state") + } + } + + private fun startKyotoNode() { + Log.i("Kyoto", "Starting Kyoto node") + DwLogger.log(INFO, "Starting Kyoto node") + wallet.startKyotoNode() + state = state.copy(kyotoNodeStatus = KyotoNodeStatus.Running) + } + + private fun startKyotoSync() { + Log.i("Kyoto", "Starting Kyoto sync") + DwLogger.log(INFO, "Starting Kyoto sync") + 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) + } + updateBalance() + } + } + + kyotoCoroutineScope.launch { + while (wallet.kyotoClient != null) { + val nextLog: org.bitcoindevkit.Log = wallet.kyotoClient!!.nextLog() + Log.i("Kyoto", "LOG: $nextLog") + val logString = nextLog.toString() + if (logString.contains("Compact Filter Headers")) { + val regex = Regex("""\d+/\d+""") + + val lastNumber = regex.findAll(logString) + .lastOrNull() + ?.value + ?.split("/") + ?.getOrNull(1) + ?.toIntOrNull() + + if (lastNumber != null) { + if (lastNumber > latestBlock) { + latestBlock = lastNumber + // Log.i("Kyoto", "New block: $latestBlock") + 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") + } + } + } + + 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() + + 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) + } + } + } +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviReceiveScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviReceiveScreen.kt" new file mode 100644 index 0000000..c79ba1f --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviReceiveScreen.kt" @@ -0,0 +1,15 @@ +/* + * Copyright 2021-2024 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.presentation.viewmodels.mvi + +data class ReceiveScreenState( + val address: String? = null, + val addressIndex: UInt? = null, +) + +sealed interface ReceiveScreenAction { + data object UpdateAddress : ReceiveScreenAction +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviSendScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviSendScreen.kt" new file mode 100644 index 0000000..431bd4e --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviSendScreen.kt" @@ -0,0 +1,29 @@ +/* + * Copyright 2021-2024 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.presentation.viewmodels.mvi + +// data class SendScreenState( +// val address: String? = null, +// ) + +sealed class SendScreenAction { + data class Broadcast(val txDataBundle: TxDataBundle) : SendScreenAction() +} + +data class TxDataBundle( + val recipients: List, + val feeRate: ULong, + val transactionType: TransactionType, + val rbfDisabled: Boolean = false, + val opReturnMsg: String? = null, +) + +data class Recipient(var address: String, var amount: ULong) + +enum class TransactionType { + STANDARD, + SEND_ALL, +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt" "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt" new file mode 100644 index 0000000..c996c49 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/viewmodels/mvi/MviWalletScreen.kt" @@ -0,0 +1,35 @@ +/* + * Copyright 2021-2024 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.presentation.viewmodels.mvi + +import org.bitcoindevkit.devkitwallet.domain.CurrencyUnit + +data class WalletScreenState( + val balance: ULong = 0u, + val unit: CurrencyUnit = CurrencyUnit.Bitcoin, + val latestBlock: UInt = 0u, + val snackbarMessage: String? = null, + val kyotoNodeStatus: KyotoNodeStatus = KyotoNodeStatus.Stopped, +) + +sealed interface WalletScreenAction { + data object UpdateBalance : WalletScreenAction + + data object SwitchUnit : WalletScreenAction + + data object StartKyotoNode : WalletScreenAction + + data object StopKyotoNode : WalletScreenAction + + data object StartKyotoSync : WalletScreenAction + + data object ClearSnackbar : WalletScreenAction +} + +enum class KyotoNodeStatus { + Running, + Stopped, +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/proto/wallets.proto" "b/Variant \342\200\224 Kyoto/app/src/main/proto/wallets.proto" new file mode 100644 index 0000000..2f1f8a8 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/proto/wallets.proto" @@ -0,0 +1,32 @@ +syntax = "proto3"; + +option java_package = "org.bitcoindevkit.devkitwallet.data"; +option java_multiple_files = true; + +message UserPreferences { + bool introDone = 1; + repeated SingleWallet wallets = 2; +} + +message SingleWallet { + string id = 1; + string name = 2; + ActiveWalletNetwork network = 3; + ActiveWalletScriptType scriptType = 4; + string descriptor = 5; + string changeDescriptor = 6; + string recoveryPhrase = 7; + bool fullScanCompleted = 8; +} + +enum ActiveWalletNetwork { + TESTNET = 0; + SIGNET = 1; + REGTEST = 2; +} + +enum ActiveWalletScriptType { + P2WPKH = 0; + P2TR = 1; + UNKNOWN = 2; +} diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/drawable/bdk_logo.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable/bdk_logo.xml" new file mode 100644 index 0000000..a28d65b --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable/bdk_logo.xml" @@ -0,0 +1,24 @@ + + + + + diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/drawable/ic_bitcoin_logo.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable/ic_bitcoin_logo.xml" new file mode 100644 index 0000000..d57d1bb --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable/ic_bitcoin_logo.xml" @@ -0,0 +1,12 @@ + + + + diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/drawable/ic_launcher_bdk_background.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable/ic_launcher_bdk_background.xml" new file mode 100644 index 0000000..6884ff0 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable/ic_launcher_bdk_background.xml" @@ -0,0 +1,9 @@ + + + + + + + diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/drawable/ic_launcher_bdk_foreground.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable/ic_launcher_bdk_foreground.xml" new file mode 100644 index 0000000..227c9e3 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable/ic_launcher_bdk_foreground.xml" @@ -0,0 +1,29 @@ + + + + + + + diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/drawable/ic_testnet_logo.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable/ic_testnet_logo.xml" new file mode 100644 index 0000000..d57d1bb --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable/ic_testnet_logo.xml" @@ -0,0 +1,12 @@ + + + + diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/drawable/launch_screen.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable/launch_screen.xml" new file mode 100644 index 0000000..52e7de9 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/drawable/launch_screen.xml" @@ -0,0 +1,11 @@ + + + + + + + diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_mono_bold.ttf" "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_mono_bold.ttf" new file mode 100644 index 0000000..c9c06a2 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_mono_bold.ttf" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_mono_bold_italic.ttf" "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_mono_bold_italic.ttf" new file mode 100644 index 0000000..1030c9e Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_mono_bold_italic.ttf" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_mono_regular.ttf" "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_mono_regular.ttf" new file mode 100644 index 0000000..1308a4c Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_mono_regular.ttf" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_mono_regular_italic.ttf" "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_mono_regular_italic.ttf" new file mode 100644 index 0000000..4333952 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_mono_regular_italic.ttf" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_quattro_bold.ttf" "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_quattro_bold.ttf" new file mode 100644 index 0000000..8e83653 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_quattro_bold.ttf" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_quattro_bold_italic.ttf" "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_quattro_bold_italic.ttf" new file mode 100644 index 0000000..0ae7e61 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_quattro_bold_italic.ttf" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_quattro_regular.ttf" "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_quattro_regular.ttf" new file mode 100644 index 0000000..f8eb282 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_quattro_regular.ttf" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_quattro_regular_italic.ttf" "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_quattro_regular_italic.ttf" new file mode 100644 index 0000000..a213e85 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/font/ia_writer_quattro_regular_italic.ttf" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" new file mode 100644 index 0000000..2f8431b --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" @@ -0,0 +1,5 @@ + + + + + diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml" new file mode 100644 index 0000000..af9e637 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk.xml" @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml" new file mode 100644 index 0000000..af9e637 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-anydpi-v26/ic_launcher_bdk_round.xml" @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" new file mode 100644 index 0000000..2f8431b --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" @@ -0,0 +1,5 @@ + + + + + diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-hdpi/ic_launcher.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-hdpi/ic_launcher.png" new file mode 100644 index 0000000..a571e60 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-hdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..b94c1ed Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-hdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..07db04c Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-hdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-hdpi/ic_launcher_round.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-hdpi/ic_launcher_round.png" new file mode 100644 index 0000000..61da551 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-hdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-mdpi/ic_launcher.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-mdpi/ic_launcher.png" new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-mdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..d4fb078 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-mdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..efe9825 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-mdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-mdpi/ic_launcher_round.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-mdpi/ic_launcher_round.png" new file mode 100644 index 0000000..db5080a Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-mdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xhdpi/ic_launcher.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xhdpi/ic_launcher.png" new file mode 100644 index 0000000..6dba46d Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xhdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..3bde8f4 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..5d4b7d9 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xhdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png" new file mode 100644 index 0000000..da31a87 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" new file mode 100644 index 0000000..15ac681 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..66f997c Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..ad3ace1 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxhdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png" new file mode 100644 index 0000000..b216f2d Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" new file mode 100644 index 0000000..f25a419 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png" new file mode 100644 index 0000000..00ab356 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png" new file mode 100644 index 0000000..28da01a Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bdk_round.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png" "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png" new file mode 100644 index 0000000..e96783c Binary files /dev/null and "b/Variant \342\200\224 Kyoto/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png" differ diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/values/colors.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/values/colors.xml" new file mode 100644 index 0000000..a66aa6b --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/values/colors.xml" @@ -0,0 +1,4 @@ + + + #203b46 + diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/values/splash.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/values/splash.xml" new file mode 100644 index 0000000..b712912 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/values/splash.xml" @@ -0,0 +1,8 @@ + + + + diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/values/strings.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/values/strings.xml" new file mode 100644 index 0000000..d0e88cc --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/values/strings.xml" @@ -0,0 +1,3 @@ + + Devkit Wallet + diff --git "a/Variant \342\200\224 Kyoto/app/src/main/res/values/themes.xml" "b/Variant \342\200\224 Kyoto/app/src/main/res/values/themes.xml" new file mode 100644 index 0000000..69b1d7e --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/main/res/values/themes.xml" @@ -0,0 +1,7 @@ + + + + diff --git "a/Variant \342\200\224 Kyoto/app/src/test/java/org/bitcoindevkit/devkitwallet/ExampleUnitTest.kt" "b/Variant \342\200\224 Kyoto/app/src/test/java/org/bitcoindevkit/devkitwallet/ExampleUnitTest.kt" new file mode 100644 index 0000000..636ec13 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/app/src/test/java/org/bitcoindevkit/devkitwallet/ExampleUnitTest.kt" @@ -0,0 +1,11 @@ +package org.bitcoindevkit.devkitwallet + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git "a/Variant \342\200\224 Kyoto/gradle.properties" "b/Variant \342\200\224 Kyoto/gradle.properties" new file mode 100644 index 0000000..a69daa6 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/gradle.properties" @@ -0,0 +1,7 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +kotlin.code.style=official +android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git "a/Variant \342\200\224 Kyoto/gradle/wrapper/gradle-wrapper.jar" "b/Variant \342\200\224 Kyoto/gradle/wrapper/gradle-wrapper.jar" new file mode 100644 index 0000000..7454180 Binary files /dev/null and "b/Variant \342\200\224 Kyoto/gradle/wrapper/gradle-wrapper.jar" differ diff --git "a/Variant \342\200\224 Kyoto/gradle/wrapper/gradle-wrapper.properties" "b/Variant \342\200\224 Kyoto/gradle/wrapper/gradle-wrapper.properties" new file mode 100644 index 0000000..1e2fbf0 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/gradle/wrapper/gradle-wrapper.properties" @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git "a/Variant \342\200\224 Kyoto/gradlew" "b/Variant \342\200\224 Kyoto/gradlew" new file mode 100755 index 0000000..744e882 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/gradlew" @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git "a/Variant \342\200\224 Kyoto/gradlew.bat" "b/Variant \342\200\224 Kyoto/gradlew.bat" new file mode 100644 index 0000000..107acd3 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/gradlew.bat" @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git "a/Variant \342\200\224 Kyoto/justfile" "b/Variant \342\200\224 Kyoto/justfile" new file mode 100644 index 0000000..c11adf2 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/justfile" @@ -0,0 +1,8 @@ +@list: + just --list + +check: + ./gradlew ktlintCheck + +format: + ./gradlew ktlintFormat diff --git "a/Variant \342\200\224 Kyoto/settings.gradle.kts" "b/Variant \342\200\224 Kyoto/settings.gradle.kts" new file mode 100644 index 0000000..98bc263 --- /dev/null +++ "b/Variant \342\200\224 Kyoto/settings.gradle.kts" @@ -0,0 +1,23 @@ +rootProject.name = "Devkit Wallet" +include("app") + +pluginManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + + // Snapshots repository + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") + + // Local Maven (~/.m2/repository/) + mavenLocal() + } +} diff --git a/images/screenshots.png b/assets/screenshots.png similarity index 100% rename from images/screenshots.png rename to assets/screenshots.png