Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7ddf6be
fix: point transfers to manual setup after blocktank sunset
ovitrif Oct 26, 2026
1e0ef74
fix: show the sunset sheet in every language
ovitrif Oct 26, 2026
b8cdd9d
fix: keep geo state on stop and add changelog
ovitrif Oct 26, 2026
ff2d43e
fix: stop hardware broadcast retries of unpaid orders
ovitrif Oct 26, 2026
c395e78
fix: match the sunset figma frames
ovitrif Oct 26, 2026
c3cbdf7
fix: open the bitkit blog from learn more
ovitrif Oct 26, 2026
a5b4a6a
fix: drop background payments that relied on blocktank
ovitrif Oct 26, 2026
a1c333c
fix: clear lint findings after background payments removal
ovitrif Oct 26, 2026
f8641f4
fix: repurpose background payments for paykit notifications
ovitrif Oct 26, 2026
64e3637
fix: block transfers when the geocheck fails
ovitrif Oct 26, 2026
c5f14db
fix: shorten the payment reminder text
ovitrif Oct 26, 2026
2dfbc86
fix: word the background service notification for all payments
ovitrif Oct 26, 2026
1cda841
fix: keep the notification grant after the asking sheet closes
ovitrif Oct 26, 2026
9e10dc3
fix: stop treating blocktank nodes as trusted peers
ovitrif Oct 26, 2026
b99d874
fix: stop retrying an unpaid blocktank order while transfers are unav…
ovitrif Oct 27, 2026
4b6ab77
feat: keep lightning through breez sdk on spark
ovitrif Oct 26, 2026
ad2842e
feat: show spark in lightning connections
ovitrif Oct 26, 2026
ec6d2f5
feat: let dev builds pay spark invoices and fund spark
ovitrif Oct 26, 2026
dbbe7e4
feat: run dev builds on spark's regtest chain
ovitrif Oct 26, 2026
c847867
feat: show the spark claim fee before transfer to spending
ovitrif Oct 26, 2026
650ba48
feat: show spark as uncapped receiving without channel options
ovitrif Oct 26, 2026
c32cd54
feat: publish a spark lightning endpoint on paykit
ovitrif Oct 26, 2026
bfca63e
ci: pass the breez api key to release builds
ovitrif Oct 26, 2026
3591356
ci: require the breez api key for release builds
ovitrif Oct 26, 2026
bb4fa07
fix: list spark deposits at once and quiet the receiving limit
ovitrif Oct 26, 2026
f542aaa
fix: show no limit while spark can receive
ovitrif Oct 26, 2026
e6b6f85
feat: back up spark exit data to vss
ovitrif Oct 26, 2026
5376785
fix: back up spark exit data when spark connected first
ovitrif Oct 26, 2026
567e1a8
fix: settle spark payments through their completion event
ovitrif Oct 27, 2026
972e5ec
fix: stop republishing paid spark invoices and record spark fees
ovitrif Oct 27, 2026
9726d18
fix: show spark withdrawals as transfers with their fee
ovitrif Oct 27, 2026
f96bae9
fix: restore spark exit data before spark connects
ovitrif Oct 27, 2026
0eda717
fix: show the most spark may keep to claim a deposit
ovitrif Oct 27, 2026
bfc43b5
fix: finish spark-only transfer to savings without waiting
ovitrif Oct 27, 2026
d48bfcf
fix: stop loading a payment request whose details fail to resolve
ovitrif Oct 27, 2026
ae55b36
test: shorten the failed request presentation test name
ovitrif Oct 27, 2026
024b6bb
fix: settle a spark send that finished before it was watched
ovitrif Oct 27, 2026
3cbe1ff
feat: add usdt on spark via breez sdk
ovitrif Oct 27, 2026
446cdfd
chore: add changelog fragment
ovitrif Oct 27, 2026
8ae8ffc
fix: pay fixed-amount spark invoices as they stand
ovitrif Oct 27, 2026
fb1ebbc
feat: pin a test dollar token on mainnet spark
ovitrif Oct 27, 2026
9d5c4bf
chore: wrap the spark dollar token note
ovitrif Oct 27, 2026
a7456a2
feat: hold usdb on spark and convert it with bitcoin
ovitrif Oct 27, 2026
88e240d
feat: make spark dollars payable by pubky over paykit
ovitrif Oct 27, 2026
d0e1207
chore: describe the usdb balance in the changelog
ovitrif Oct 27, 2026
fce7ddb
fix: stop dollar conversions spending can't cover and show fees in do…
ovitrif Oct 27, 2026
2616c13
feat: show spark dollar activity, receipts and sends like other payments
ovitrif Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/release-internal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ jobs:
KEYSTORE_PASSWORD: ${{ secrets.INTERNAL_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.INTERNAL_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.INTERNAL_KEY_PASSWORD }}
# Optional: without it the build passes and Spark (Lightning without channels) stays off.
BREEZ_API_KEY: ${{ secrets.BREEZ_API_KEY }}
run: |
set -euo pipefail
./gradlew assembleMainnetRelease --no-daemon --stacktrace
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ jobs:
printf '%s' "$BITKIT_KEYSTORE_BASE64" | base64 --decode > "$keystore_path"
echo "KEYSTORE_FILE=$keystore_path" >> "$GITHUB_ENV"

- name: Check Breez API key
env:
BREEZ_API_KEY: ${{ secrets.BREEZ_API_KEY }}
run: |
if [ -z "${BREEZ_API_KEY// /}" ]; then
echo "::error::BREEZ_API_KEY is missing from the release environment; a release without it ships with Spark (Lightning without channels) off."
exit 1
fi

- name: Build release artifacts
env:
GPR_USER: ${{ secrets.GPR_USER || github.actor }}
Expand All @@ -74,6 +83,7 @@ jobs:
KEYSTORE_PASSWORD: ${{ secrets.BITKIT_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.BITKIT_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.BITKIT_KEY_PASSWORD }}
BREEZ_API_KEY: ${{ secrets.BREEZ_API_KEY }}
run: |
set -euo pipefail
./gradlew assembleMainnetRelease bundleMainnetRelease --no-daemon --stacktrace
Expand Down
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,8 @@ androidComponents {
"USDT_RPC_URL",
localProp("USDT_RPC_URL").orElse("https://bitkit.to/v1/usdt/chain-rpc").stringField(),
)
buildConfigFields.put("BREEZ_API_KEY", localProp("BREEZ_API_KEY").orElse("").stringField())
buildConfigFields.put("ESPLORA_URL", localProp("ESPLORA_URL").orElse("").stringField())
buildConfigFields.put("E2E", e2eEnv.booleanField())
buildConfigFields.put("E2E_BACKEND", e2eBackendEnv.stringField())
buildConfigFields.put("E2E_LOCAL_HOST", e2eLocalHostEnv.stringField())
Expand Down Expand Up @@ -564,6 +566,7 @@ dependencies {
implementation(libs.bitkit.core)
}
implementation(libs.paykit)
implementation(libs.breez.sdk.spark) { exclude(group = "net.java.dev.jna", module = "jna") }
implementation(libs.vss.client)
if (localBitkitCoreAar == null) nativeDebugSymbols(libs.bitkit.core.nativeDebugSymbolsArtifact())
nativeDebugSymbols(libs.ldk.node.android.nativeDebugSymbolsArtifact())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@ class FundingScreenTest {
assertEquals(0, manualClicks)
}

@Test
fun whenGeoBlocked_onlyManualSetupShouldInvoke() {
setContent(isGeoBlocked = true)

clickableTransferOption().performClick()
composeTestRule.onNodeWithTag("FundReceive").performClick()
composeTestRule.onNodeWithTag("FundManual").performClick()
composeTestRule.waitForIdle()

assertEquals(0, transferClicks)
assertEquals(0, fundClicks)
assertEquals(1, manualClicks)
}

@Test
fun whenFundableBalanceIsZero_transferShouldNotRouteToTheTransferFlow() {
setContent(fundableBalance = 0uL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import com.synonym.bitkitcore.usdtFormatAmount
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
Expand All @@ -32,8 +33,12 @@ import to.bitkit.domain.commands.NotifyPendingPaymentResolved
import to.bitkit.domain.commands.NotifyPendingPaymentResolvedHandler
import to.bitkit.ext.activityManager
import to.bitkit.models.NewTransactionSheetDetails
import to.bitkit.models.NewTransactionSheetDirection
import to.bitkit.models.NewTransactionSheetType
import to.bitkit.models.NotificationDetails
import to.bitkit.repositories.LightningRepo
import to.bitkit.repositories.SparkRepo
import to.bitkit.repositories.SparkUsdTransfer
import to.bitkit.repositories.WalletRepo
import to.bitkit.services.NodeEventHandler
import to.bitkit.services.NodeServiceFgState
Expand Down Expand Up @@ -79,6 +84,9 @@ class LightningNodeService : Service() {
@Inject
lateinit var nodeServiceFgState: NodeServiceFgState

@Inject
lateinit var sparkRepo: SparkRepo

private var hasStartedNode = false

private val nodeEventHandler: NodeEventHandler = { event ->
Expand All @@ -91,6 +99,7 @@ class LightningNodeService : Service() {
private fun setupService() {
if (hasStartedNode) return
hasStartedNode = true
serviceScope.launch { sparkRepo.usdReceived.collect { notifySparkUsdReceived(it) } }

serviceScope.launch {
lightningRepo.start(
Expand Down Expand Up @@ -144,6 +153,18 @@ class LightningNodeService : Service() {
pushNotification(notification.title, notification.body)
}

private suspend fun notifySparkUsdReceived(transfer: SparkUsdTransfer) {
if (App.currentActivity?.value != null) return
val sheet = NewTransactionSheetDetails(
type = NewTransactionSheetType.LIGHTNING,
direction = NewTransactionSheetDirection.RECEIVED,
usdtAmount = transfer.amount,
sparkUsdTransferId = transfer.id,
)
cacheStore.setBackgroundReceive(sheet)
pushNotification(getString(R.string.usdt__received_title_spark), "$ " + usdtFormatAmount(transfer.amount))
}

private suspend fun handlePendingPaymentResolved(event: Event) {
val command = NotifyPendingPaymentResolved.Command.from(event) ?: return

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ import javax.inject.Singleton

/**
* Builds the user-facing "Payment Received" notification content, applying the same currency
* formatting rules everywhere a received payment is surfaced (foreground service, in-app handler,
* and the background [to.bitkit.fcm.WakeNodeWorker] push path).
* formatting rules everywhere a received payment is surfaced (foreground service and in-app handler).
*/
@Singleton
class ReceivedNotificationContent @Inject constructor(
Expand Down
72 changes: 30 additions & 42 deletions app/src/main/java/to/bitkit/env/Env.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@ import android.os.Build
import com.synonym.bitkitcore.UsdtDestination
import org.lightningdevkit.ldknode.LogLevel
import org.lightningdevkit.ldknode.Network
import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.BuildConfig
import to.bitkit.BuildConfig.VERSION_NAME
import to.bitkit.ext.ensureDir
import to.bitkit.ext.of
import to.bitkit.models.BlocktankNotificationType
import to.bitkit.models.NodePeer
import to.bitkit.utils.Logger
import java.io.File
import kotlin.io.path.Path
Expand All @@ -35,6 +31,29 @@ internal object Env {
).filter { it.first in enabled }.map { it.second }
}

val breezApiKey: String? get() = BuildConfig.BREEZ_API_KEY.takeIf { it.isNotBlank() }

/**
* The Spark token held as the dollar balance on Spark, matched by identifier since anyone can issue a token
* with any ticker. Mainnet holds USDB, Brale's 6-decimal dollar token on Spark; regtest holds a 6-decimal
* "Test USDB" token issued on Lightspark's regtest.
*/
/** Orchestra, which bridges Spark dollars to Arbitrum USDT, serves mainnet only. */
val isSparkBridgeAvailable: Boolean get() = network == Network.BITCOIN

val sparkUsdTokenId: String?
get() = when (network) {
Network.REGTEST -> "btknrt1wrr0qtshpyuzrx5p0mvd605dnwvfv3dcpfexqgfzpgl3rykdacsq6p8z6l"
Network.BITCOIN -> "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87"
else -> null
}

/**
* Esplora endpoint for the node's chain source on regtest builds, set to put the wallet on Spark's regtest chain
* through `scripts/spark-regtest-esplora.py`; Electrum is used when unset.
*/
val regtestEsploraUrl: String?
get() = BuildConfig.ESPLORA_URL.takeIf { it.isNotBlank() && network == Network.REGTEST }
val isDebug = BuildConfig.DEBUG
const val isE2eTest = BuildConfig.E2E
const val isGeoblockingEnabled = BuildConfig.GEO
Expand All @@ -52,14 +71,6 @@ internal object Env {

val ldkLogLevel = LogLevel.TRACE

val trustedLnPeers
get() = when (network) {
Network.BITCOIN -> listOf(Peers.lnd1, Peers.lnd3, Peers.lnd4)
Network.REGTEST -> listOf(Peers.stag)
Network.TESTNET -> listOf(Peers.stag)
else -> listOf()
}

/**
* Whether LN -> onchain swaps can reach a Boltz backend. Boltz only serves a public API on
* mainnet: its testnet deployment is deprecated and regtest resolves to a local backend that
Expand All @@ -71,9 +82,6 @@ internal object Env {
const val fxRateStaleThreshold = 10 * 60 * 1000L // 10 minutes in millis
const val lspOrdersRefreshInterval = 2 * 60 * 1000L // 2 minutes in millis

val pushNotificationFeatures = BlocktankNotificationType.entries
const val derivationName = "bitkit-notifications"

const val FILE_PROVIDER_AUTHORITY = "${BuildConfig.APPLICATION_ID}.fileprovider"
const val SUPPORT_EMAIL = "support@synonym.to"
const val PIN_LENGTH = 4
Expand Down Expand Up @@ -140,11 +148,6 @@ internal object Env {
Network.BITCOIN -> "$blocktankBaseUrl/api"
else -> "$blocktankBaseUrl/blocktank/api/v2"
}
val blocktankNotificationApiUrl
get() = when (network) {
Network.BITCOIN -> "$blocktankBaseUrl/api/notifications"
else -> "$blocktankBaseUrl/notifications/api"
}

val btcRatesServer
get() = when (network) {
Expand Down Expand Up @@ -177,6 +180,7 @@ internal object Env {
const val BITKIT_TELEGRAM = "https://t.me/bitkitchat"
const val BITKIT_GITHUB = "https://github.com/synonymdev"
const val BITKIT_HELP_CENTER = "https://help.bitkit.to"
const val BLOCKTANK_SUNSET_URL = "https://blog.bitkit.to/"
const val TERMS_OF_USE_URL = "https://bitkit.to/terms-of-use"
const val PRIVACY_POLICY_URL = "https://bitkit.to/privacy-policy"
const val STORING_BITCOINS_URL = "https://en.bitcoin.it/wiki/Storing_bitcoins"
Expand Down Expand Up @@ -238,6 +242,12 @@ internal object Env {

fun ldkStoragePath(walletIndex: Int) = storagePathOf(walletIndex, network.name.lowercase(), "ldk")

fun sparkStoragePath(walletIndex: Int) = storagePathOf(walletIndex, network.name.lowercase(), "spark")

/** A restored Spark exit data backup waiting to be imported once Spark connects; outside Spark's storage. */
fun sparkExitRestorePath(walletIndex: Int) =
Path(storagePathOf(walletIndex, network.name.lowercase(), "spark-restore"), "exit-state.json").toString()

fun bitkitCoreStoragePath(walletIndex: Int): String {
return storagePathOf(walletIndex, network.name.lowercase(), "core")
}
Expand Down Expand Up @@ -283,28 +293,6 @@ object Defaults {
const val TAG_MAX_LENGTH = 20
}

object Peers {
val stag = PeerDetails.of("028a8910b0048630d4eb17af25668cdd7ea6f2d8ae20956e7a06e2ae46ebcb69fc@34.65.86.104:9400")
val lnd1 = PeerDetails.of("039b8b4dd1d88c2c5db374290cda397a8f5d79f312d6ea5d5bfdfc7c6ff363eae3@34.65.111.104:9735")
val lnd3 = PeerDetails.of("03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b@34.65.191.64:9735")
val lnd4 = PeerDetails.of("02a371038863605300d0b3fc9de0cf5ccb57728b7f8906535709a831b16e311187@34.65.153.174:9735")

object Known {
val stag = NodePeer(Peers.stag, name = "Synonym-Own-Regtest-0")
val lnd1 = NodePeer(Peers.lnd1, name = "Blocktank-LND1")
val lnd3 = NodePeer(Peers.lnd3, name = "Blocktank-LND3")
val lnd4 = NodePeer(Peers.lnd4, name = "Blocktank-LND4")

fun find(peer: PeerDetails): NodePeer? = when (peer.nodeId) {
stag.peerDetails.nodeId -> stag
lnd1.peerDetails.nodeId -> lnd1
lnd3.peerDetails.nodeId -> lnd3
lnd4.peerDetails.nodeId -> lnd4
else -> null
}
}
}

private object ElectrumServers {
object MAINNET {
const val ESPLORA = "ssl://bitkit.to:9999"
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/to/bitkit/ext/LightningBalance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ fun LightningBalance.channelId(): String {
}
}

fun LightningBalance.counterpartyNodeId(): String {
return when (this) {
is LightningBalance.ClaimableOnChannelClose -> this.counterpartyNodeId
is LightningBalance.ClaimableAwaitingConfirmations -> this.counterpartyNodeId
is LightningBalance.ContentiousClaimable -> this.counterpartyNodeId
is LightningBalance.MaybeTimeoutClaimableHtlc -> this.counterpartyNodeId
is LightningBalance.MaybePreimageClaimableHtlc -> this.counterpartyNodeId
is LightningBalance.CounterpartyRevokedOutputClaimable -> this.counterpartyNodeId
}
}

fun LightningBalance.claimableAtHeight(): UInt? = when (this) {
is LightningBalance.ClaimableAwaitingConfirmations -> this.confirmationHeight
is LightningBalance.ContentiousClaimable -> this.timeoutHeight
Expand Down
Loading
Loading