refactor(wallet): remove on-chain send/receive, keep on-chain tx display

We are not supporting on-chain send/receive in-app. Detection and
display of on-chain transactions in history remains so deposits/
withdrawals made from another app on the same Spark seed still show
correctly (type, txId, mempool.space link).
This commit is contained in:
The Daniel
2026-06-30 19:01:15 -04:00
parent 9b6e3501e9
commit ca7414704d
6 changed files with 193 additions and 1001 deletions
@@ -356,15 +356,6 @@ class NwcRepository(private val context: Context, private val relayPool: RelayPo
return result.map { (it as Nip47.NwcResponse.MakeInvoiceResult).invoice }
}
override suspend fun getDepositAddress(): Result<String> =
Result.failure(UnsupportedOperationException("NWC does not support on-chain receive"))
override suspend fun prepareOnchainSend(address: String, amountSats: Long): Result<Pair<OnchainFeeQuote, Any>> =
Result.failure(UnsupportedOperationException("NWC does not support on-chain send"))
override suspend fun sendOnchain(prepareData: Any, speed: String): Result<String> =
Result.failure(UnsupportedOperationException("NWC does not support on-chain send"))
suspend fun listNwcTransactions(limit: Int = 50, offset: Int = 0): Result<List<Nip47.Transaction>> {
val result = sendRequest(Nip47.NwcRequest.ListTransactions(limit = limit, offset = offset))
return result.map { (it as Nip47.NwcResponse.ListTransactionsResult).transactions }
@@ -10,7 +10,6 @@ import breez_sdk_spark.EventListener
import breez_sdk_spark.GetInfoRequest
import breez_sdk_spark.ListPaymentsRequest
import breez_sdk_spark.Network
import breez_sdk_spark.OnchainConfirmationSpeed
import breez_sdk_spark.PaymentDetails
import breez_sdk_spark.PaymentType
import breez_sdk_spark.PrepareSendPaymentRequest
@@ -455,57 +454,6 @@ class SparkRepository(
}
}
override suspend fun getDepositAddress(): Result<String> = withContext(Dispatchers.IO) {
try {
val instance = sdk ?: return@withContext Result.failure(Exception("Not connected"))
val response = instance.receivePayment(ReceivePaymentRequest(ReceivePaymentMethod.BitcoinAddress))
Result.success(response.paymentRequest)
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun prepareOnchainSend(address: String, amountSats: Long): Result<Pair<OnchainFeeQuote, Any>> =
withContext(Dispatchers.IO) {
try {
val instance = sdk ?: return@withContext Result.failure(Exception("Not connected"))
val prepareReq = PrepareSendPaymentRequest(
paymentRequest = address,
amount = java.math.BigInteger.valueOf(amountSats)
)
val prepareResponse = instance.prepareSendPayment(prepareReq)
val method = prepareResponse.paymentMethod as? SendPaymentMethod.BitcoinAddress
?: return@withContext Result.failure(Exception("Not a Bitcoin address payment"))
val feeQuote = method.feeQuote
val quote = OnchainFeeQuote(
fastFeeSats = (feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat).toLong(),
mediumFeeSats = (feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat).toLong(),
slowFeeSats = (feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat).toLong()
)
Result.success(Pair(quote, prepareResponse as Any))
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun sendOnchain(prepareData: Any, speed: String): Result<String> = withContext(Dispatchers.IO) {
try {
val instance = sdk ?: return@withContext Result.failure(Exception("Not connected"))
val prepareResponse = prepareData as breez_sdk_spark.PrepareSendPaymentResponse
val confirmationSpeed = when (speed) {
"FAST" -> OnchainConfirmationSpeed.FAST
"SLOW" -> OnchainConfirmationSpeed.SLOW
else -> OnchainConfirmationSpeed.MEDIUM
}
val options = SendPaymentOptions.BitcoinAddress(confirmationSpeed = confirmationSpeed)
val sendResponse = instance.sendPayment(SendPaymentRequest(prepareResponse, options))
emitStatus("Bitcoin sent")
Result.success(sendResponse.payment.id)
} catch (e: Exception) {
emitStatus("Bitcoin send failed: ${e.message}")
Result.failure(e)
}
}
// --- Sync polling ---
@@ -3,12 +3,6 @@ package com.darkwisp.app.repo
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
data class OnchainFeeQuote(
val fastFeeSats: Long,
val mediumFeeSats: Long,
val slowFeeSats: Long
)
interface WalletProvider {
val balance: StateFlow<Long?>
val isConnected: StateFlow<Boolean>
@@ -27,15 +21,6 @@ interface WalletProvider {
suspend fun payInvoice(bolt11: String): Result<String>
suspend fun makeInvoice(amountMsats: Long, description: String, expirySecs: Int = 3600): Result<String>
suspend fun listTransactions(limit: Int = 50, offset: Int = 0): Result<List<WalletTransaction>>
/** Spark only — returns an on-chain deposit address. NWC returns failure. */
suspend fun getDepositAddress(): Result<String>
/** Spark only — prepares an on-chain send and returns fee quote + opaque prepare data. */
suspend fun prepareOnchainSend(address: String, amountSats: Long): Result<Pair<OnchainFeeQuote, Any>>
/** Spark only — broadcasts a prepared on-chain send. */
suspend fun sendOnchain(prepareData: Any, speed: String = "MEDIUM"): Result<String>
}
data class WalletTransaction(
File diff suppressed because it is too large Load Diff
@@ -30,7 +30,6 @@ import com.darkwisp.app.repo.NwcRepository
import com.darkwisp.app.repo.SparkRepository
import com.darkwisp.app.repo.WalletMode
import com.darkwisp.app.repo.WalletModeRepository
import com.darkwisp.app.repo.OnchainFeeQuote
import com.darkwisp.app.repo.WalletProvider
import android.util.Log
import com.darkwisp.app.repo.WalletTransaction
@@ -126,14 +125,6 @@ sealed class WalletPage {
data class SendResult(val success: Boolean, val message: String) : WalletPage()
object ReceiveAmount : WalletPage()
data class ReceiveInvoice(val invoice: String, val amountSats: Long) : WalletPage()
data class OnchainSendAmount(val address: String) : WalletPage()
data class OnchainSendConfirm(
val address: String,
val amountSats: Long,
val feeQuote: OnchainFeeQuote,
val prepareData: Any
) : WalletPage()
data class OnchainSendResult(val success: Boolean, val paymentId: String?, val message: String) : WalletPage()
data class ReceiveSuccess(val amountSats: Long) : WalletPage()
object Transactions : WalletPage()
object Settings : WalletPage()
@@ -205,25 +196,6 @@ class WalletViewModel(
private val _receiveAmount = MutableStateFlow("")
val receiveAmount: StateFlow<String> = _receiveAmount
// On-chain deposit address (Spark only)
private val _depositAddress = MutableStateFlow<String?>(null)
val depositAddress: StateFlow<String?> = _depositAddress
private val _depositAddressLoading = MutableStateFlow(false)
val depositAddressLoading: StateFlow<Boolean> = _depositAddressLoading
private val _depositAddressError = MutableStateFlow<String?>(null)
val depositAddressError: StateFlow<String?> = _depositAddressError
// On-chain send (Spark only)
private val _onchainFeeQuote = MutableStateFlow<OnchainFeeQuote?>(null)
val onchainFeeQuote: StateFlow<OnchainFeeQuote?> = _onchainFeeQuote
private var _onchainPrepareData: Any? = null
private val _onchainSendLoading = MutableStateFlow(false)
val onchainSendLoading: StateFlow<Boolean> = _onchainSendLoading
// Transactions
private val _transactions = MutableStateFlow<List<WalletTransaction>>(emptyList())
val transactions: StateFlow<List<WalletTransaction>> = _transactions
@@ -346,11 +318,6 @@ class WalletViewModel(
// would otherwise stay at the prior wallet's acknowledged value
// until the next refreshState() call.
_seedBackupAcked.value = false
_depositAddress.value = null
_depositAddressError.value = null
_depositAddressLoading.value = false
_onchainFeeQuote.value = null
_onchainPrepareData = null
}
/**
@@ -577,8 +544,6 @@ class WalletViewModel(
_deleteConfirmText.value = ""
_lightningAddressError.value = null
_addressAvailable.value = null
_onchainFeeQuote.value = null
_onchainPrepareData = null
}
val isOnHome: Boolean get() = pageStack.size <= 1
@@ -1301,14 +1266,6 @@ class WalletViewModel(
}
}
private fun isBtcAddress(s: String): Boolean {
val lower = s.lowercase()
if (lower.startsWith("bc1") && s.length >= 14) return true
if ((s.startsWith("1") || s.startsWith("3")) && s.length in 26..35 &&
s.all { it.isLetterOrDigit() && it != '0' && it != 'O' && it != 'I' && it != 'l' }) return true
return false
}
fun processInput(input: String = _sendInput.value) {
val trimmed = input.trim()
.removePrefix("lightning:").removePrefix("LIGHTNING:")
@@ -1333,10 +1290,6 @@ class WalletViewModel(
navigateTo(WalletPage.SendAmount(noffer.raw))
}
}
_walletMode.value == WalletMode.SPARK && isBtcAddress(trimmed) -> {
clearOnchainQuote()
navigateTo(WalletPage.OnchainSendAmount(trimmed))
}
trimmed.lowercase().startsWith("lnbc") -> {
val decoded = Bolt11.decode(trimmed)
if (decoded == null) {
@@ -1359,10 +1312,7 @@ class WalletViewModel(
navigateTo(WalletPage.SendAmount(trimmed))
}
else -> {
_sendError.value = if (_walletMode.value == WalletMode.SPARK)
"Enter a lightning address, BOLT11 invoice, CLINK offer, or Bitcoin address"
else
"Enter a lightning address (user@domain), BOLT11 invoice, or CLINK offer"
_sendError.value = "Enter a lightning address (user@domain), BOLT11 invoice, or CLINK offer"
}
}
}
@@ -1555,74 +1505,6 @@ class WalletViewModel(
}
}
// --- On-chain receive (Spark only) ---
fun loadDepositAddress(force: Boolean = false) {
if (!force && _depositAddress.value != null) return
_depositAddressLoading.value = true
_depositAddressError.value = null
viewModelScope.launch {
sparkRepo.getDepositAddress().fold(
onSuccess = { address -> _depositAddress.value = address },
onFailure = { e -> _depositAddressError.value = e.message ?: "Failed to load address" }
)
_depositAddressLoading.value = false
}
}
// --- On-chain send (Spark only) ---
fun clearOnchainQuote() {
_onchainFeeQuote.value = null
_onchainPrepareData = null
}
fun prepareOnchainSend(address: String, amountSats: Long) {
clearOnchainQuote()
_onchainSendLoading.value = true
_sendError.value = null
viewModelScope.launch {
sparkRepo.prepareOnchainSend(address, amountSats).fold(
onSuccess = { (quote, prepareData) ->
_onchainFeeQuote.value = quote
_onchainPrepareData = prepareData
},
onFailure = { e -> _sendError.value = e.message ?: "Failed to estimate fee" }
)
_onchainSendLoading.value = false
}
}
fun continueToOnchainConfirm(address: String, amountSats: Long) {
val quote = _onchainFeeQuote.value ?: return
val prepareData = _onchainPrepareData ?: return
navigateTo(WalletPage.OnchainSendConfirm(address, amountSats, quote, prepareData))
}
fun sendOnchain(prepareData: Any) {
if (_isLoading.value) return
_isLoading.value = true
viewModelScope.launch {
sparkRepo.sendOnchain(prepareData).fold(
onSuccess = { paymentId ->
pageStack.removeAt(pageStack.lastIndex)
val resultPage = WalletPage.OnchainSendResult(true, paymentId, "Bitcoin sent!")
pageStack.add(resultPage)
_currentPage.value = resultPage
clearOnchainQuote()
refreshBalance()
},
onFailure = { e ->
pageStack.removeAt(pageStack.lastIndex)
val resultPage = WalletPage.OnchainSendResult(false, null, e.message ?: "Send failed")
pageStack.add(resultPage)
_currentPage.value = resultPage
}
)
_isLoading.value = false
}
}
// --- Transactions ---
fun loadTransactions() {
+1 -26
View File
@@ -776,9 +776,6 @@
<string name="wallet_receive_note_label">NOTE (OPTIONAL)</string>
<string name="wallet_receive_note_placeholder">For coffee, etc.</string>
<string name="wallet_receive_create_invoice">Create invoice</string>
<!-- Updated tab labels for Spark on-chain receive -->
<string name="wallet_receive_lightning_tab">Lightning</string>
<string name="wallet_receive_bitcoin_tab">Bitcoin</string>
<!-- Invoice expiry selector -->
<string name="wallet_receive_expires_label">EXPIRES</string>
<string name="wallet_receive_expires_1h">1 hour</string>
@@ -790,29 +787,7 @@
<string name="wallet_copy_address">Copy address</string>
<string name="wallet_share_address">Share address</string>
<string name="wallet_share">Share</string>
<!-- Bitcoin on-chain deposit address -->
<string name="cd_bitcoin_address_qr">Bitcoin address QR code</string>
<!-- On-chain send flow -->
<string name="wallet_onchain_send_title">Send Payment</string>
<string name="wallet_onchain_bitcoin_detected">Bitcoin address detected on-chain payment</string>
<string name="wallet_onchain_amount_sats">Amount (sats)</string>
<string name="wallet_onchain_use_all" formatted="false">Use All (%,d sats)</string>
<string name="wallet_onchain_fetching_fee">Estimating fee…</string>
<string name="wallet_onchain_get_fee_quote">Get Fee Quote</string>
<string name="wallet_onchain_amount">Amount</string>
<string name="wallet_onchain_fee">Network Fee</string>
<string name="wallet_onchain_total">Total</string>
<string name="wallet_onchain_continue">Continue</string>
<string name="wallet_onchain_plus_fee" formatted="false">+ %,d sats fee</string>
<string name="wallet_onchain_sending_to">Sending to:</string>
<string name="wallet_onchain_irreversible_warning">Bitcoin transactions cannot be reversed. Please verify the address is correct.</string>
<string name="wallet_onchain_send_confirm">Confirm Send</string>
<string name="wallet_onchain_payment_sent">Sent</string>
<string name="wallet_onchain_payment_failed">Send Failed</string>
<string name="wallet_onchain_view_mempool">View on mempool.space</string>
<!-- Spark send placeholder (accepts Lightning + Bitcoin addresses) -->
<string name="placeholder_send_spark">Lightning invoice or Bitcoin address</string>
<string name="wallet_send_input_hint_onchain">Lightning or Bitcoin address, invoice, or CLINK offer</string>
<string name="wallet_send_input_hint_onchain">Lightning address, invoice, or CLINK offer</string>
<!-- Generic actions -->
<string name="retry">Retry</string>
<string name="cancel">Cancel</string>