Merged
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.ldk
package to.bitkit.services

import android.content.Context
import androidx.test.core.app.ApplicationProvider
Expand Down
24 changes: 23 additions & 1 deletion app/src/main/java/to/bitkit/Constants.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
package to.bitkit

import android.util.Log
import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.Tag.APP
import to.bitkit.env.Network
import to.bitkit.ext.ensureDir
Expand All@@ -13,6 +14,7 @@ import org.lightningdevkit.ldknode.Network as LdkNetwork
internal object Tag {
const val FCM = "FCM"
const val LDK = "LDK"
const val LSP = "LSP"
const val BDK = "BDK"
const val DEV = "DEV"
const val APP = "APP"
Expand DownExpand Up@@ -81,13 +83,33 @@ data class LnPeer(
val nodeId: String,
val host: String,
val port: String,
val isConnected: Boolean = false,
val isPersisted: Boolean = false,
) {
constructor(nodeId: String, address: String) : this(
constructor(
nodeId: String,
address: String,
isConnected: Boolean = false,
isPersisted: Boolean = false,
) : this(
nodeId,
address.substringBefore(":"),
address.substringAfter(":"),
isConnected,
isPersisted,
)

val address get() = "$host:$port"
override fun toString() = "$nodeId@${address}"

companion object {
fun PeerDetails.toLnPeer(): LnPeer {
return LnPeer(
nodeId = nodeId,
address = address,
isConnected = isConnected,
isPersisted = isPersisted,
)
}
}
}
14 changes: 9 additions & 5 deletions app/src/main/java/to/bitkit/LauncherActivity.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,22 +2,26 @@ package to.bitkit

import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import to.bitkit.ldk.warmupNode
import to.bitkit.ui.MainActivity
import to.bitkit.ui.SharedViewModel
import to.bitkit.ui.initNotificationChannel
import to.bitkit.ui.logFcmToken

@AndroidEntryPoint
class LauncherActivity : AppCompatActivity() {
private val sharedViewModel by viewModels<SharedViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initNotificationChannel()
logFcmToken()
// TODO share mainViewModel in both activities, move warmupNode to it & call it suspending
runBlocking { warmupNode() }
startActivity(Intent(this, MainActivity::class.java))
sharedViewModel.warmupNode()
startActivity(Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
})
}
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.di
package to.bitkit.async

import android.util.Log
import kotlinx.coroutines.CoroutineScope
Expand All@@ -14,7 +14,7 @@ import java.util.concurrent.ThreadFactory
import kotlin.coroutines.CoroutineContext

enum class ServiceQueue {
LDK, BDK, MIGRATION;
LDK, BDK, LSP, MIGRATION;

private val scope by lazy { CoroutineScope(dispatcher("$name-queue".lowercase()) + SupervisorJob()) }

Expand Down
57 changes: 57 additions & 0 deletions app/src/main/java/to/bitkit/data/LspApi.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
package to.bitkit.data

import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.Serializable
import javax.inject.Inject

interface LspApi {
suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest)
suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse
}

class BlocktankApi @Inject constructor(
private val client: HttpClient,
) : LspApi {
private val baseUrl = "https://api.stag.blocktank.to"
private val notificationsApi = "$baseUrl/notifications/api/device"

override suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) {
post(notificationsApi, payload)
}

override suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse {
return post("$notificationsApi/$deviceToken/test-notification", payload)
}

private suspend inline fun <reified T> post(url: String, payload: T) = client.post(url) { setBody(payload) }
}

@Serializable
data class RegisterDeviceRequest(
val deviceToken: String,
val publicKey: String,
val features: List<String>,
val nodeId: String,
val isoTimestamp: String,
val signature: String,
)

@Serializable
data class TestNotificationRequest(
val data: Data,
) {
@Serializable
data class Data(
val source: String,
val type: String,
val payload: Payload,
) {
@Serializable
data class Payload(
val secretMessage: String,
)
}
}
34 changes: 0 additions & 34 deletions app/src/main/java/to/bitkit/data/Models.kt

This file was deleted.

32 changes: 32 additions & 0 deletions app/src/main/java/to/bitkit/data/RestApi.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import to.bitkit.REST
import to.bitkit.ext.toHex
import javax.inject.Inject
Expand DownExpand Up@@ -62,3 +64,33 @@ class EsploraApi @Inject constructor(
return client.get("$REST/tx/${txid}/outspend/${outputIndex}").body()
}
}

@Serializable
data class Tx(
val txid: String,
val status: TxStatus,
)

@Serializable
data class TxStatus(
@SerialName("confirmed")
val isConfirmed: Boolean,
@SerialName("block_height")
val blockHeight: Int? = null,
@SerialName("block_hash")
val blockHash: String? = null,
)

@Serializable
data class OutputSpent(
val spent: Boolean,
)

@Serializable
data class MerkleProof(
@SerialName("block_height")
val blockHeight: Int,
@Suppress("ArrayInDataClass")
val merkle: Array<String>,
val pos: Int,
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,6 @@ class AndroidKeyStore(
val cipher = Cipher.getInstance(transformation).apply { init(Cipher.DECRYPT_MODE, secretKey, spec) }

val decryptedDataBytes = cipher.doFinal(actualEncryptedData)
return decryptedDataBytes.toString(Charsets.UTF_8)
return decryptedDataBytes.decodeToString()
}
}
26 changes: 21 additions & 5 deletions app/src/main/java/to/bitkit/di/HttpModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,27 +6,34 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.ANDROID
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import to.bitkit.data.BlocktankApi
import to.bitkit.data.EsploraApi
import to.bitkit.data.LspApi
import to.bitkit.data.RestApi
import javax.inject.Singleton

val json = Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}

@Module
@InstallIn(SingletonComponent::class)
object HttpModule {
@Provides
@Singleton
fun provideJson(): Json {
return Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}
return json
}

@Provides
Expand All@@ -40,9 +47,18 @@ object HttpModule {
install(ContentNegotiation) {
json(json = json)
}
defaultRequest { // Set default request properties
contentType(ContentType.Application.Json)
}
}
}

@Provides
@Singleton
fun provideLspApi(blocktankApi: BlocktankApi): LspApi {
return blocktankApi
}

@Provides
@Singleton
fun provideRestApi(esploraApi: EsploraApi): RestApi {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/di/ServicesModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.bdk.BitcoinService
import to.bitkit.ldk.LightningService
import to.bitkit.services.BitcoinService
import to.bitkit.services.LightningService

@Module
@InstallIn(SingletonComponent::class)
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/to/bitkit/di/ViewModelModule.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
package to.bitkit.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.services.BlocktankService
import to.bitkit.ui.SharedViewModel
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ViewModelModule {
@Singleton
@Provides
fun provideSharedViewModel(
@BgDispatcher bgDispatcher: CoroutineDispatcher,
blocktankService: BlocktankService,
): SharedViewModel {
return SharedViewModel(
bgDispatcher,
blocktankService,
)
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/ext/ByteArray.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,3 +36,5 @@ fun Any.convertToByteArray(): ByteArray {
fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags)

fun String.fromBase64(flags: Int = Base64.DEFAULT): ByteArray = Base64.decode(this, flags)

val String.uByteList get() = this.toByteArray(Charsets.UTF_8).map { it.toUByte() }
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/ext/Context.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ internal fun toast(
text: String,
duration: Int = Toast.LENGTH_SHORT,
) {
with(currentActivity<MainActivity>()) {
currentActivity<MainActivity>().run {
Toast.makeText(this, text, duration).show()
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.ldk
package to.bitkit.services

import android.content.Context
import androidx.test.core.app.ApplicationProvider
Expand Down
24 changes: 23 additions & 1 deletion app/src/main/java/to/bitkit/Constants.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
package to.bitkit

import android.util.Log
import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.Tag.APP
import to.bitkit.env.Network
import to.bitkit.ext.ensureDir
Expand All@@ -13,6 +14,7 @@ import org.lightningdevkit.ldknode.Network as LdkNetwork
internal object Tag {
const val FCM = "FCM"
const val LDK = "LDK"
const val LSP = "LSP"
const val BDK = "BDK"
const val DEV = "DEV"
const val APP = "APP"
Expand DownExpand Up@@ -81,13 +83,33 @@ data class LnPeer(
val nodeId: String,
val host: String,
val port: String,
val isConnected: Boolean = false,
val isPersisted: Boolean = false,
) {
constructor(nodeId: String, address: String) : this(
constructor(
nodeId: String,
address: String,
isConnected: Boolean = false,
isPersisted: Boolean = false,
) : this(
nodeId,
address.substringBefore(":"),
address.substringAfter(":"),
isConnected,
isPersisted,
)

val address get() = "$host:$port"
override fun toString() = "$nodeId@${address}"

companion object {
fun PeerDetails.toLnPeer(): LnPeer {
return LnPeer(
nodeId = nodeId,
address = address,
isConnected = isConnected,
isPersisted = isPersisted,
)
}
}
}
14 changes: 9 additions & 5 deletions app/src/main/java/to/bitkit/LauncherActivity.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,22 +2,26 @@ package to.bitkit

import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import to.bitkit.ldk.warmupNode
import to.bitkit.ui.MainActivity
import to.bitkit.ui.SharedViewModel
import to.bitkit.ui.initNotificationChannel
import to.bitkit.ui.logFcmToken

@AndroidEntryPoint
class LauncherActivity : AppCompatActivity() {
private val sharedViewModel by viewModels<SharedViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initNotificationChannel()
logFcmToken()
// TODO share mainViewModel in both activities, move warmupNode to it & call it suspending
runBlocking { warmupNode() }
startActivity(Intent(this, MainActivity::class.java))
sharedViewModel.warmupNode()
startActivity(Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
})
}
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.di
package to.bitkit.async

import android.util.Log
import kotlinx.coroutines.CoroutineScope
Expand All@@ -14,7 +14,7 @@ import java.util.concurrent.ThreadFactory
import kotlin.coroutines.CoroutineContext

enum class ServiceQueue {
LDK, BDK, MIGRATION;
LDK, BDK, LSP, MIGRATION;

private val scope by lazy { CoroutineScope(dispatcher("$name-queue".lowercase()) + SupervisorJob()) }

Expand Down
57 changes: 57 additions & 0 deletions app/src/main/java/to/bitkit/data/LspApi.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
package to.bitkit.data

import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.Serializable
import javax.inject.Inject

interface LspApi {
suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest)
suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse
}

class BlocktankApi @Inject constructor(
private val client: HttpClient,
) : LspApi {
private val baseUrl = "https://api.stag.blocktank.to"
private val notificationsApi = "$baseUrl/notifications/api/device"

override suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) {
post(notificationsApi, payload)
}

override suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse {
return post("$notificationsApi/$deviceToken/test-notification", payload)
}

private suspend inline fun <reified T> post(url: String, payload: T) = client.post(url) { setBody(payload) }
}

@Serializable
data class RegisterDeviceRequest(
val deviceToken: String,
val publicKey: String,
val features: List<String>,
val nodeId: String,
val isoTimestamp: String,
val signature: String,
)

@Serializable
data class TestNotificationRequest(
val data: Data,
) {
@Serializable
data class Data(
val source: String,
val type: String,
val payload: Payload,
) {
@Serializable
data class Payload(
val secretMessage: String,
)
}
}
34 changes: 0 additions & 34 deletions app/src/main/java/to/bitkit/data/Models.kt

This file was deleted.

32 changes: 32 additions & 0 deletions app/src/main/java/to/bitkit/data/RestApi.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import to.bitkit.REST
import to.bitkit.ext.toHex
import javax.inject.Inject
Expand DownExpand Up@@ -62,3 +64,33 @@ class EsploraApi @Inject constructor(
return client.get("$REST/tx/${txid}/outspend/${outputIndex}").body()
}
}

@Serializable
data class Tx(
val txid: String,
val status: TxStatus,
)

@Serializable
data class TxStatus(
@SerialName("confirmed")
val isConfirmed: Boolean,
@SerialName("block_height")
val blockHeight: Int? = null,
@SerialName("block_hash")
val blockHash: String? = null,
)

@Serializable
data class OutputSpent(
val spent: Boolean,
)

@Serializable
data class MerkleProof(
@SerialName("block_height")
val blockHeight: Int,
@Suppress("ArrayInDataClass")
val merkle: Array<String>,
val pos: Int,
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,6 @@ class AndroidKeyStore(
val cipher = Cipher.getInstance(transformation).apply { init(Cipher.DECRYPT_MODE, secretKey, spec) }

val decryptedDataBytes = cipher.doFinal(actualEncryptedData)
return decryptedDataBytes.toString(Charsets.UTF_8)
return decryptedDataBytes.decodeToString()
}
}
26 changes: 21 additions & 5 deletions app/src/main/java/to/bitkit/di/HttpModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,27 +6,34 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.ANDROID
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import to.bitkit.data.BlocktankApi
import to.bitkit.data.EsploraApi
import to.bitkit.data.LspApi
import to.bitkit.data.RestApi
import javax.inject.Singleton

val json = Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}

@Module
@InstallIn(SingletonComponent::class)
object HttpModule {
@Provides
@Singleton
fun provideJson(): Json {
return Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}
return json
}

@Provides
Expand All@@ -40,9 +47,18 @@ object HttpModule {
install(ContentNegotiation) {
json(json = json)
}
defaultRequest { // Set default request properties
contentType(ContentType.Application.Json)
}
}
}

@Provides
@Singleton
fun provideLspApi(blocktankApi: BlocktankApi): LspApi {
return blocktankApi
}

@Provides
@Singleton
fun provideRestApi(esploraApi: EsploraApi): RestApi {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/di/ServicesModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.bdk.BitcoinService
import to.bitkit.ldk.LightningService
import to.bitkit.services.BitcoinService
import to.bitkit.services.LightningService

@Module
@InstallIn(SingletonComponent::class)
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/to/bitkit/di/ViewModelModule.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
package to.bitkit.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.services.BlocktankService
import to.bitkit.ui.SharedViewModel
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ViewModelModule {
@Singleton
@Provides
fun provideSharedViewModel(
@BgDispatcher bgDispatcher: CoroutineDispatcher,
blocktankService: BlocktankService,
): SharedViewModel {
return SharedViewModel(
bgDispatcher,
blocktankService,
)
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/ext/ByteArray.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,3 +36,5 @@ fun Any.convertToByteArray(): ByteArray {
fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags)

fun String.fromBase64(flags: Int = Base64.DEFAULT): ByteArray = Base64.decode(this, flags)

val String.uByteList get() = this.toByteArray(Charsets.UTF_8).map { it.toUByte() }
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/ext/Context.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ internal fun toast(
text: String,
duration: Int = Toast.LENGTH_SHORT,
) {
with(currentActivity<MainActivity>()) {
currentActivity<MainActivity>().run {
Toast.makeText(this, text, duration).show()
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.ldk
package to.bitkit.services

import android.content.Context
import androidx.test.core.app.ApplicationProvider
Expand Down
24 changes: 23 additions & 1 deletion app/src/main/java/to/bitkit/Constants.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
package to.bitkit

import android.util.Log
import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.Tag.APP
import to.bitkit.env.Network
import to.bitkit.ext.ensureDir
Expand All@@ -13,6 +14,7 @@ import org.lightningdevkit.ldknode.Network as LdkNetwork
internal object Tag {
const val FCM = "FCM"
const val LDK = "LDK"
const val LSP = "LSP"
const val BDK = "BDK"
const val DEV = "DEV"
const val APP = "APP"
Expand DownExpand Up@@ -81,13 +83,33 @@ data class LnPeer(
val nodeId: String,
val host: String,
val port: String,
val isConnected: Boolean = false,
val isPersisted: Boolean = false,
) {
constructor(nodeId: String, address: String) : this(
constructor(
nodeId: String,
address: String,
isConnected: Boolean = false,
isPersisted: Boolean = false,
) : this(
nodeId,
address.substringBefore(":"),
address.substringAfter(":"),
isConnected,
isPersisted,
)

val address get() = "$host:$port"
override fun toString() = "$nodeId@${address}"

companion object {
fun PeerDetails.toLnPeer(): LnPeer {
return LnPeer(
nodeId = nodeId,
address = address,
isConnected = isConnected,
isPersisted = isPersisted,
)
}
}
}
14 changes: 9 additions & 5 deletions app/src/main/java/to/bitkit/LauncherActivity.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,22 +2,26 @@ package to.bitkit

import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import to.bitkit.ldk.warmupNode
import to.bitkit.ui.MainActivity
import to.bitkit.ui.SharedViewModel
import to.bitkit.ui.initNotificationChannel
import to.bitkit.ui.logFcmToken

@AndroidEntryPoint
class LauncherActivity : AppCompatActivity() {
private val sharedViewModel by viewModels<SharedViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initNotificationChannel()
logFcmToken()
// TODO share mainViewModel in both activities, move warmupNode to it & call it suspending
runBlocking { warmupNode() }
startActivity(Intent(this, MainActivity::class.java))
sharedViewModel.warmupNode()
startActivity(Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
})
}
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.di
package to.bitkit.async

import android.util.Log
import kotlinx.coroutines.CoroutineScope
Expand All@@ -14,7 +14,7 @@ import java.util.concurrent.ThreadFactory
import kotlin.coroutines.CoroutineContext

enum class ServiceQueue {
LDK, BDK, MIGRATION;
LDK, BDK, LSP, MIGRATION;

private val scope by lazy { CoroutineScope(dispatcher("$name-queue".lowercase()) + SupervisorJob()) }

Expand Down
57 changes: 57 additions & 0 deletions app/src/main/java/to/bitkit/data/LspApi.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
package to.bitkit.data

import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.Serializable
import javax.inject.Inject

interface LspApi {
suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest)
suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse
}

class BlocktankApi @Inject constructor(
private val client: HttpClient,
) : LspApi {
private val baseUrl = "https://api.stag.blocktank.to"
private val notificationsApi = "$baseUrl/notifications/api/device"

override suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) {
post(notificationsApi, payload)
}

override suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse {
return post("$notificationsApi/$deviceToken/test-notification", payload)
}

private suspend inline fun <reified T> post(url: String, payload: T) = client.post(url) { setBody(payload) }
}

@Serializable
data class RegisterDeviceRequest(
val deviceToken: String,
val publicKey: String,
val features: List<String>,
val nodeId: String,
val isoTimestamp: String,
val signature: String,
)

@Serializable
data class TestNotificationRequest(
val data: Data,
) {
@Serializable
data class Data(
val source: String,
val type: String,
val payload: Payload,
) {
@Serializable
data class Payload(
val secretMessage: String,
)
}
}
34 changes: 0 additions & 34 deletions app/src/main/java/to/bitkit/data/Models.kt

This file was deleted.

32 changes: 32 additions & 0 deletions app/src/main/java/to/bitkit/data/RestApi.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import to.bitkit.REST
import to.bitkit.ext.toHex
import javax.inject.Inject
Expand DownExpand Up@@ -62,3 +64,33 @@ class EsploraApi @Inject constructor(
return client.get("$REST/tx/${txid}/outspend/${outputIndex}").body()
}
}

@Serializable
data class Tx(
val txid: String,
val status: TxStatus,
)

@Serializable
data class TxStatus(
@SerialName("confirmed")
val isConfirmed: Boolean,
@SerialName("block_height")
val blockHeight: Int? = null,
@SerialName("block_hash")
val blockHash: String? = null,
)

@Serializable
data class OutputSpent(
val spent: Boolean,
)

@Serializable
data class MerkleProof(
@SerialName("block_height")
val blockHeight: Int,
@Suppress("ArrayInDataClass")
val merkle: Array<String>,
val pos: Int,
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,6 @@ class AndroidKeyStore(
val cipher = Cipher.getInstance(transformation).apply { init(Cipher.DECRYPT_MODE, secretKey, spec) }

val decryptedDataBytes = cipher.doFinal(actualEncryptedData)
return decryptedDataBytes.toString(Charsets.UTF_8)
return decryptedDataBytes.decodeToString()
}
}
26 changes: 21 additions & 5 deletions app/src/main/java/to/bitkit/di/HttpModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,27 +6,34 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.ANDROID
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import to.bitkit.data.BlocktankApi
import to.bitkit.data.EsploraApi
import to.bitkit.data.LspApi
import to.bitkit.data.RestApi
import javax.inject.Singleton

val json = Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}

@Module
@InstallIn(SingletonComponent::class)
object HttpModule {
@Provides
@Singleton
fun provideJson(): Json {
return Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}
return json
}

@Provides
Expand All@@ -40,9 +47,18 @@ object HttpModule {
install(ContentNegotiation) {
json(json = json)
}
defaultRequest { // Set default request properties
contentType(ContentType.Application.Json)
}
}
}

@Provides
@Singleton
fun provideLspApi(blocktankApi: BlocktankApi): LspApi {
return blocktankApi
}

@Provides
@Singleton
fun provideRestApi(esploraApi: EsploraApi): RestApi {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/di/ServicesModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.bdk.BitcoinService
import to.bitkit.ldk.LightningService
import to.bitkit.services.BitcoinService
import to.bitkit.services.LightningService

@Module
@InstallIn(SingletonComponent::class)
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/to/bitkit/di/ViewModelModule.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
package to.bitkit.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.services.BlocktankService
import to.bitkit.ui.SharedViewModel
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ViewModelModule {
@Singleton
@Provides
fun provideSharedViewModel(
@BgDispatcher bgDispatcher: CoroutineDispatcher,
blocktankService: BlocktankService,
): SharedViewModel {
return SharedViewModel(
bgDispatcher,
blocktankService,
)
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/ext/ByteArray.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,3 +36,5 @@ fun Any.convertToByteArray(): ByteArray {
fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags)

fun String.fromBase64(flags: Int = Base64.DEFAULT): ByteArray = Base64.decode(this, flags)

val String.uByteList get() = this.toByteArray(Charsets.UTF_8).map { it.toUByte() }
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/ext/Context.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ internal fun toast(
text: String,
duration: Int = Toast.LENGTH_SHORT,
) {
with(currentActivity<MainActivity>()) {
currentActivity<MainActivity>().run {
Toast.makeText(this, text, duration).show()
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.ldk
package to.bitkit.services

import android.content.Context
import androidx.test.core.app.ApplicationProvider
Expand Down
24 changes: 23 additions & 1 deletion app/src/main/java/to/bitkit/Constants.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
package to.bitkit

import android.util.Log
import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.Tag.APP
import to.bitkit.env.Network
import to.bitkit.ext.ensureDir
Expand All@@ -13,6 +14,7 @@ import org.lightningdevkit.ldknode.Network as LdkNetwork
internal object Tag {
const val FCM = "FCM"
const val LDK = "LDK"
const val LSP = "LSP"
const val BDK = "BDK"
const val DEV = "DEV"
const val APP = "APP"
Expand DownExpand Up@@ -81,13 +83,33 @@ data class LnPeer(
val nodeId: String,
val host: String,
val port: String,
val isConnected: Boolean = false,
val isPersisted: Boolean = false,
) {
constructor(nodeId: String, address: String) : this(
constructor(
nodeId: String,
address: String,
isConnected: Boolean = false,
isPersisted: Boolean = false,
) : this(
nodeId,
address.substringBefore(":"),
address.substringAfter(":"),
isConnected,
isPersisted,
)

val address get() = "$host:$port"
override fun toString() = "$nodeId@${address}"

companion object {
fun PeerDetails.toLnPeer(): LnPeer {
return LnPeer(
nodeId = nodeId,
address = address,
isConnected = isConnected,
isPersisted = isPersisted,
)
}
}
}
14 changes: 9 additions & 5 deletions app/src/main/java/to/bitkit/LauncherActivity.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,22 +2,26 @@ package to.bitkit

import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import to.bitkit.ldk.warmupNode
import to.bitkit.ui.MainActivity
import to.bitkit.ui.SharedViewModel
import to.bitkit.ui.initNotificationChannel
import to.bitkit.ui.logFcmToken

@AndroidEntryPoint
class LauncherActivity : AppCompatActivity() {
private val sharedViewModel by viewModels<SharedViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initNotificationChannel()
logFcmToken()
// TODO share mainViewModel in both activities, move warmupNode to it & call it suspending
runBlocking { warmupNode() }
startActivity(Intent(this, MainActivity::class.java))
sharedViewModel.warmupNode()
startActivity(Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
})
}
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.di
package to.bitkit.async

import android.util.Log
import kotlinx.coroutines.CoroutineScope
Expand All@@ -14,7 +14,7 @@ import java.util.concurrent.ThreadFactory
import kotlin.coroutines.CoroutineContext

enum class ServiceQueue {
LDK, BDK, MIGRATION;
LDK, BDK, LSP, MIGRATION;

private val scope by lazy { CoroutineScope(dispatcher("$name-queue".lowercase()) + SupervisorJob()) }

Expand Down
57 changes: 57 additions & 0 deletions app/src/main/java/to/bitkit/data/LspApi.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
package to.bitkit.data

import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.Serializable
import javax.inject.Inject

interface LspApi {
suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest)
suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse
}

class BlocktankApi @Inject constructor(
private val client: HttpClient,
) : LspApi {
private val baseUrl = "https://api.stag.blocktank.to"
private val notificationsApi = "$baseUrl/notifications/api/device"

override suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) {
post(notificationsApi, payload)
}

override suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse {
return post("$notificationsApi/$deviceToken/test-notification", payload)
}

private suspend inline fun <reified T> post(url: String, payload: T) = client.post(url) { setBody(payload) }
}

@Serializable
data class RegisterDeviceRequest(
val deviceToken: String,
val publicKey: String,
val features: List<String>,
val nodeId: String,
val isoTimestamp: String,
val signature: String,
)

@Serializable
data class TestNotificationRequest(
val data: Data,
) {
@Serializable
data class Data(
val source: String,
val type: String,
val payload: Payload,
) {
@Serializable
data class Payload(
val secretMessage: String,
)
}
}
34 changes: 0 additions & 34 deletions app/src/main/java/to/bitkit/data/Models.kt

This file was deleted.

32 changes: 32 additions & 0 deletions app/src/main/java/to/bitkit/data/RestApi.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import to.bitkit.REST
import to.bitkit.ext.toHex
import javax.inject.Inject
Expand DownExpand Up@@ -62,3 +64,33 @@ class EsploraApi @Inject constructor(
return client.get("$REST/tx/${txid}/outspend/${outputIndex}").body()
}
}

@Serializable
data class Tx(
val txid: String,
val status: TxStatus,
)

@Serializable
data class TxStatus(
@SerialName("confirmed")
val isConfirmed: Boolean,
@SerialName("block_height")
val blockHeight: Int? = null,
@SerialName("block_hash")
val blockHash: String? = null,
)

@Serializable
data class OutputSpent(
val spent: Boolean,
)

@Serializable
data class MerkleProof(
@SerialName("block_height")
val blockHeight: Int,
@Suppress("ArrayInDataClass")
val merkle: Array<String>,
val pos: Int,
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,6 @@ class AndroidKeyStore(
val cipher = Cipher.getInstance(transformation).apply { init(Cipher.DECRYPT_MODE, secretKey, spec) }

val decryptedDataBytes = cipher.doFinal(actualEncryptedData)
return decryptedDataBytes.toString(Charsets.UTF_8)
return decryptedDataBytes.decodeToString()
}
}
26 changes: 21 additions & 5 deletions app/src/main/java/to/bitkit/di/HttpModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,27 +6,34 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.ANDROID
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import to.bitkit.data.BlocktankApi
import to.bitkit.data.EsploraApi
import to.bitkit.data.LspApi
import to.bitkit.data.RestApi
import javax.inject.Singleton

val json = Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}

@Module
@InstallIn(SingletonComponent::class)
object HttpModule {
@Provides
@Singleton
fun provideJson(): Json {
return Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}
return json
}

@Provides
Expand All@@ -40,9 +47,18 @@ object HttpModule {
install(ContentNegotiation) {
json(json = json)
}
defaultRequest { // Set default request properties
contentType(ContentType.Application.Json)
}
}
}

@Provides
@Singleton
fun provideLspApi(blocktankApi: BlocktankApi): LspApi {
return blocktankApi
}

@Provides
@Singleton
fun provideRestApi(esploraApi: EsploraApi): RestApi {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/di/ServicesModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.bdk.BitcoinService
import to.bitkit.ldk.LightningService
import to.bitkit.services.BitcoinService
import to.bitkit.services.LightningService

@Module
@InstallIn(SingletonComponent::class)
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/to/bitkit/di/ViewModelModule.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
package to.bitkit.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.services.BlocktankService
import to.bitkit.ui.SharedViewModel
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ViewModelModule {
@Singleton
@Provides
fun provideSharedViewModel(
@BgDispatcher bgDispatcher: CoroutineDispatcher,
blocktankService: BlocktankService,
): SharedViewModel {
return SharedViewModel(
bgDispatcher,
blocktankService,
)
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/ext/ByteArray.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,3 +36,5 @@ fun Any.convertToByteArray(): ByteArray {
fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags)

fun String.fromBase64(flags: Int = Base64.DEFAULT): ByteArray = Base64.decode(this, flags)

val String.uByteList get() = this.toByteArray(Charsets.UTF_8).map { it.toUByte() }
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/ext/Context.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ internal fun toast(
text: String,
duration: Int = Toast.LENGTH_SHORT,
) {
with(currentActivity<MainActivity>()) {
currentActivity<MainActivity>().run {
Toast.makeText(this, text, duration).show()
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.ldk
package to.bitkit.services

import android.content.Context
import androidx.test.core.app.ApplicationProvider
Expand Down
24 changes: 23 additions & 1 deletion app/src/main/java/to/bitkit/Constants.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
package to.bitkit

import android.util.Log
import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.Tag.APP
import to.bitkit.env.Network
import to.bitkit.ext.ensureDir
Expand All@@ -13,6 +14,7 @@ import org.lightningdevkit.ldknode.Network as LdkNetwork
internal object Tag {
const val FCM = "FCM"
const val LDK = "LDK"
const val LSP = "LSP"
const val BDK = "BDK"
const val DEV = "DEV"
const val APP = "APP"
Expand DownExpand Up@@ -81,13 +83,33 @@ data class LnPeer(
val nodeId: String,
val host: String,
val port: String,
val isConnected: Boolean = false,
val isPersisted: Boolean = false,
) {
constructor(nodeId: String, address: String) : this(
constructor(
nodeId: String,
address: String,
isConnected: Boolean = false,
isPersisted: Boolean = false,
) : this(
nodeId,
address.substringBefore(":"),
address.substringAfter(":"),
isConnected,
isPersisted,
)

val address get() = "$host:$port"
override fun toString() = "$nodeId@${address}"

companion object {
fun PeerDetails.toLnPeer(): LnPeer {
return LnPeer(
nodeId = nodeId,
address = address,
isConnected = isConnected,
isPersisted = isPersisted,
)
}
}
}
14 changes: 9 additions & 5 deletions app/src/main/java/to/bitkit/LauncherActivity.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,22 +2,26 @@ package to.bitkit

import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import to.bitkit.ldk.warmupNode
import to.bitkit.ui.MainActivity
import to.bitkit.ui.SharedViewModel
import to.bitkit.ui.initNotificationChannel
import to.bitkit.ui.logFcmToken

@AndroidEntryPoint
class LauncherActivity : AppCompatActivity() {
private val sharedViewModel by viewModels<SharedViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initNotificationChannel()
logFcmToken()
// TODO share mainViewModel in both activities, move warmupNode to it & call it suspending
runBlocking { warmupNode() }
startActivity(Intent(this, MainActivity::class.java))
sharedViewModel.warmupNode()
startActivity(Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
})
}
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.di
package to.bitkit.async

import android.util.Log
import kotlinx.coroutines.CoroutineScope
Expand All@@ -14,7 +14,7 @@ import java.util.concurrent.ThreadFactory
import kotlin.coroutines.CoroutineContext

enum class ServiceQueue {
LDK, BDK, MIGRATION;
LDK, BDK, LSP, MIGRATION;

private val scope by lazy { CoroutineScope(dispatcher("$name-queue".lowercase()) + SupervisorJob()) }

Expand Down
57 changes: 57 additions & 0 deletions app/src/main/java/to/bitkit/data/LspApi.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
package to.bitkit.data

import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.Serializable
import javax.inject.Inject

interface LspApi {
suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest)
suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse
}

class BlocktankApi @Inject constructor(
private val client: HttpClient,
) : LspApi {
private val baseUrl = "https://api.stag.blocktank.to"
private val notificationsApi = "$baseUrl/notifications/api/device"

override suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) {
post(notificationsApi, payload)
}

override suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse {
return post("$notificationsApi/$deviceToken/test-notification", payload)
}

private suspend inline fun <reified T> post(url: String, payload: T) = client.post(url) { setBody(payload) }
}

@Serializable
data class RegisterDeviceRequest(
val deviceToken: String,
val publicKey: String,
val features: List<String>,
val nodeId: String,
val isoTimestamp: String,
val signature: String,
)

@Serializable
data class TestNotificationRequest(
val data: Data,
) {
@Serializable
data class Data(
val source: String,
val type: String,
val payload: Payload,
) {
@Serializable
data class Payload(
val secretMessage: String,
)
}
}
34 changes: 0 additions & 34 deletions app/src/main/java/to/bitkit/data/Models.kt

This file was deleted.

32 changes: 32 additions & 0 deletions app/src/main/java/to/bitkit/data/RestApi.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import to.bitkit.REST
import to.bitkit.ext.toHex
import javax.inject.Inject
Expand DownExpand Up@@ -62,3 +64,33 @@ class EsploraApi @Inject constructor(
return client.get("$REST/tx/${txid}/outspend/${outputIndex}").body()
}
}

@Serializable
data class Tx(
val txid: String,
val status: TxStatus,
)

@Serializable
data class TxStatus(
@SerialName("confirmed")
val isConfirmed: Boolean,
@SerialName("block_height")
val blockHeight: Int? = null,
@SerialName("block_hash")
val blockHash: String? = null,
)

@Serializable
data class OutputSpent(
val spent: Boolean,
)

@Serializable
data class MerkleProof(
@SerialName("block_height")
val blockHeight: Int,
@Suppress("ArrayInDataClass")
val merkle: Array<String>,
val pos: Int,
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,6 @@ class AndroidKeyStore(
val cipher = Cipher.getInstance(transformation).apply { init(Cipher.DECRYPT_MODE, secretKey, spec) }

val decryptedDataBytes = cipher.doFinal(actualEncryptedData)
return decryptedDataBytes.toString(Charsets.UTF_8)
return decryptedDataBytes.decodeToString()
}
}
26 changes: 21 additions & 5 deletions app/src/main/java/to/bitkit/di/HttpModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,27 +6,34 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.ANDROID
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import to.bitkit.data.BlocktankApi
import to.bitkit.data.EsploraApi
import to.bitkit.data.LspApi
import to.bitkit.data.RestApi
import javax.inject.Singleton

val json = Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}

@Module
@InstallIn(SingletonComponent::class)
object HttpModule {
@Provides
@Singleton
fun provideJson(): Json {
return Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}
return json
}

@Provides
Expand All@@ -40,9 +47,18 @@ object HttpModule {
install(ContentNegotiation) {
json(json = json)
}
defaultRequest { // Set default request properties
contentType(ContentType.Application.Json)
}
}
}

@Provides
@Singleton
fun provideLspApi(blocktankApi: BlocktankApi): LspApi {
return blocktankApi
}

@Provides
@Singleton
fun provideRestApi(esploraApi: EsploraApi): RestApi {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/di/ServicesModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.bdk.BitcoinService
import to.bitkit.ldk.LightningService
import to.bitkit.services.BitcoinService
import to.bitkit.services.LightningService

@Module
@InstallIn(SingletonComponent::class)
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/to/bitkit/di/ViewModelModule.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
package to.bitkit.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.services.BlocktankService
import to.bitkit.ui.SharedViewModel
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ViewModelModule {
@Singleton
@Provides
fun provideSharedViewModel(
@BgDispatcher bgDispatcher: CoroutineDispatcher,
blocktankService: BlocktankService,
): SharedViewModel {
return SharedViewModel(
bgDispatcher,
blocktankService,
)
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/ext/ByteArray.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,3 +36,5 @@ fun Any.convertToByteArray(): ByteArray {
fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags)

fun String.fromBase64(flags: Int = Base64.DEFAULT): ByteArray = Base64.decode(this, flags)

val String.uByteList get() = this.toByteArray(Charsets.UTF_8).map { it.toUByte() }
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/ext/Context.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ internal fun toast(
text: String,
duration: Int = Toast.LENGTH_SHORT,
) {
with(currentActivity<MainActivity>()) {
currentActivity<MainActivity>().run {
Toast.makeText(this, text, duration).show()
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.ldk
package to.bitkit.services

import android.content.Context
import androidx.test.core.app.ApplicationProvider
Expand Down
24 changes: 23 additions & 1 deletion app/src/main/java/to/bitkit/Constants.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
package to.bitkit

import android.util.Log
import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.Tag.APP
import to.bitkit.env.Network
import to.bitkit.ext.ensureDir
Expand All@@ -13,6 +14,7 @@ import org.lightningdevkit.ldknode.Network as LdkNetwork
internal object Tag {
const val FCM = "FCM"
const val LDK = "LDK"
const val LSP = "LSP"
const val BDK = "BDK"
const val DEV = "DEV"
const val APP = "APP"
Expand DownExpand Up@@ -81,13 +83,33 @@ data class LnPeer(
val nodeId: String,
val host: String,
val port: String,
val isConnected: Boolean = false,
val isPersisted: Boolean = false,
) {
constructor(nodeId: String, address: String) : this(
constructor(
nodeId: String,
address: String,
isConnected: Boolean = false,
isPersisted: Boolean = false,
) : this(
nodeId,
address.substringBefore(":"),
address.substringAfter(":"),
isConnected,
isPersisted,
)

val address get() = "$host:$port"
override fun toString() = "$nodeId@${address}"

companion object {
fun PeerDetails.toLnPeer(): LnPeer {
return LnPeer(
nodeId = nodeId,
address = address,
isConnected = isConnected,
isPersisted = isPersisted,
)
}
}
}
14 changes: 9 additions & 5 deletions app/src/main/java/to/bitkit/LauncherActivity.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,22 +2,26 @@ package to.bitkit

import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import to.bitkit.ldk.warmupNode
import to.bitkit.ui.MainActivity
import to.bitkit.ui.SharedViewModel
import to.bitkit.ui.initNotificationChannel
import to.bitkit.ui.logFcmToken

@AndroidEntryPoint
class LauncherActivity : AppCompatActivity() {
private val sharedViewModel by viewModels<SharedViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initNotificationChannel()
logFcmToken()
// TODO share mainViewModel in both activities, move warmupNode to it & call it suspending
runBlocking { warmupNode() }
startActivity(Intent(this, MainActivity::class.java))
sharedViewModel.warmupNode()
startActivity(Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
})
}
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.di
package to.bitkit.async

import android.util.Log
import kotlinx.coroutines.CoroutineScope
Expand All@@ -14,7 +14,7 @@ import java.util.concurrent.ThreadFactory
import kotlin.coroutines.CoroutineContext

enum class ServiceQueue {
LDK, BDK, MIGRATION;
LDK, BDK, LSP, MIGRATION;

private val scope by lazy { CoroutineScope(dispatcher("$name-queue".lowercase()) + SupervisorJob()) }

Expand Down
57 changes: 57 additions & 0 deletions app/src/main/java/to/bitkit/data/LspApi.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
package to.bitkit.data

import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.Serializable
import javax.inject.Inject

interface LspApi {
suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest)
suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse
}

class BlocktankApi @Inject constructor(
private val client: HttpClient,
) : LspApi {
private val baseUrl = "https://api.stag.blocktank.to"
private val notificationsApi = "$baseUrl/notifications/api/device"

override suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) {
post(notificationsApi, payload)
}

override suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse {
return post("$notificationsApi/$deviceToken/test-notification", payload)
}

private suspend inline fun <reified T> post(url: String, payload: T) = client.post(url) { setBody(payload) }
}

@Serializable
data class RegisterDeviceRequest(
val deviceToken: String,
val publicKey: String,
val features: List<String>,
val nodeId: String,
val isoTimestamp: String,
val signature: String,
)

@Serializable
data class TestNotificationRequest(
val data: Data,
) {
@Serializable
data class Data(
val source: String,
val type: String,
val payload: Payload,
) {
@Serializable
data class Payload(
val secretMessage: String,
)
}
}
34 changes: 0 additions & 34 deletions app/src/main/java/to/bitkit/data/Models.kt

This file was deleted.

32 changes: 32 additions & 0 deletions app/src/main/java/to/bitkit/data/RestApi.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import to.bitkit.REST
import to.bitkit.ext.toHex
import javax.inject.Inject
Expand DownExpand Up@@ -62,3 +64,33 @@ class EsploraApi @Inject constructor(
return client.get("$REST/tx/${txid}/outspend/${outputIndex}").body()
}
}

@Serializable
data class Tx(
val txid: String,
val status: TxStatus,
)

@Serializable
data class TxStatus(
@SerialName("confirmed")
val isConfirmed: Boolean,
@SerialName("block_height")
val blockHeight: Int? = null,
@SerialName("block_hash")
val blockHash: String? = null,
)

@Serializable
data class OutputSpent(
val spent: Boolean,
)

@Serializable
data class MerkleProof(
@SerialName("block_height")
val blockHeight: Int,
@Suppress("ArrayInDataClass")
val merkle: Array<String>,
val pos: Int,
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,6 @@ class AndroidKeyStore(
val cipher = Cipher.getInstance(transformation).apply { init(Cipher.DECRYPT_MODE, secretKey, spec) }

val decryptedDataBytes = cipher.doFinal(actualEncryptedData)
return decryptedDataBytes.toString(Charsets.UTF_8)
return decryptedDataBytes.decodeToString()
}
}
26 changes: 21 additions & 5 deletions app/src/main/java/to/bitkit/di/HttpModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,27 +6,34 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.ANDROID
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import to.bitkit.data.BlocktankApi
import to.bitkit.data.EsploraApi
import to.bitkit.data.LspApi
import to.bitkit.data.RestApi
import javax.inject.Singleton

val json = Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}

@Module
@InstallIn(SingletonComponent::class)
object HttpModule {
@Provides
@Singleton
fun provideJson(): Json {
return Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}
return json
}

@Provides
Expand All@@ -40,9 +47,18 @@ object HttpModule {
install(ContentNegotiation) {
json(json = json)
}
defaultRequest { // Set default request properties
contentType(ContentType.Application.Json)
}
}
}

@Provides
@Singleton
fun provideLspApi(blocktankApi: BlocktankApi): LspApi {
return blocktankApi
}

@Provides
@Singleton
fun provideRestApi(esploraApi: EsploraApi): RestApi {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/di/ServicesModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.bdk.BitcoinService
import to.bitkit.ldk.LightningService
import to.bitkit.services.BitcoinService
import to.bitkit.services.LightningService

@Module
@InstallIn(SingletonComponent::class)
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/to/bitkit/di/ViewModelModule.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
package to.bitkit.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.services.BlocktankService
import to.bitkit.ui.SharedViewModel
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ViewModelModule {
@Singleton
@Provides
fun provideSharedViewModel(
@BgDispatcher bgDispatcher: CoroutineDispatcher,
blocktankService: BlocktankService,
): SharedViewModel {
return SharedViewModel(
bgDispatcher,
blocktankService,
)
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/ext/ByteArray.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,3 +36,5 @@ fun Any.convertToByteArray(): ByteArray {
fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags)

fun String.fromBase64(flags: Int = Base64.DEFAULT): ByteArray = Base64.decode(this, flags)

val String.uByteList get() = this.toByteArray(Charsets.UTF_8).map { it.toUByte() }
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/ext/Context.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ internal fun toast(
text: String,
duration: Int = Toast.LENGTH_SHORT,
) {
with(currentActivity<MainActivity>()) {
currentActivity<MainActivity>().run {
Toast.makeText(this, text, duration).show()
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.ldk
package to.bitkit.services

import android.content.Context
import androidx.test.core.app.ApplicationProvider
Expand Down
24 changes: 23 additions & 1 deletion app/src/main/java/to/bitkit/Constants.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
package to.bitkit

import android.util.Log
import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.Tag.APP
import to.bitkit.env.Network
import to.bitkit.ext.ensureDir
Expand All@@ -13,6 +14,7 @@ import org.lightningdevkit.ldknode.Network as LdkNetwork
internal object Tag {
const val FCM = "FCM"
const val LDK = "LDK"
const val LSP = "LSP"
const val BDK = "BDK"
const val DEV = "DEV"
const val APP = "APP"
Expand DownExpand Up@@ -81,13 +83,33 @@ data class LnPeer(
val nodeId: String,
val host: String,
val port: String,
val isConnected: Boolean = false,
val isPersisted: Boolean = false,
) {
constructor(nodeId: String, address: String) : this(
constructor(
nodeId: String,
address: String,
isConnected: Boolean = false,
isPersisted: Boolean = false,
) : this(
nodeId,
address.substringBefore(":"),
address.substringAfter(":"),
isConnected,
isPersisted,
)

val address get() = "$host:$port"
override fun toString() = "$nodeId@${address}"

companion object {
fun PeerDetails.toLnPeer(): LnPeer {
return LnPeer(
nodeId = nodeId,
address = address,
isConnected = isConnected,
isPersisted = isPersisted,
)
}
}
}
14 changes: 9 additions & 5 deletions app/src/main/java/to/bitkit/LauncherActivity.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,22 +2,26 @@ package to.bitkit

import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import to.bitkit.ldk.warmupNode
import to.bitkit.ui.MainActivity
import to.bitkit.ui.SharedViewModel
import to.bitkit.ui.initNotificationChannel
import to.bitkit.ui.logFcmToken

@AndroidEntryPoint
class LauncherActivity : AppCompatActivity() {
private val sharedViewModel by viewModels<SharedViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initNotificationChannel()
logFcmToken()
// TODO share mainViewModel in both activities, move warmupNode to it & call it suspending
runBlocking { warmupNode() }
startActivity(Intent(this, MainActivity::class.java))
sharedViewModel.warmupNode()
startActivity(Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
})
}
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.di
package to.bitkit.async

import android.util.Log
import kotlinx.coroutines.CoroutineScope
Expand All@@ -14,7 +14,7 @@ import java.util.concurrent.ThreadFactory
import kotlin.coroutines.CoroutineContext

enum class ServiceQueue {
LDK, BDK, MIGRATION;
LDK, BDK, LSP, MIGRATION;

private val scope by lazy { CoroutineScope(dispatcher("$name-queue".lowercase()) + SupervisorJob()) }

Expand Down
57 changes: 57 additions & 0 deletions app/src/main/java/to/bitkit/data/LspApi.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
package to.bitkit.data

import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.Serializable
import javax.inject.Inject

interface LspApi {
suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest)
suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse
}

class BlocktankApi @Inject constructor(
private val client: HttpClient,
) : LspApi {
private val baseUrl = "https://api.stag.blocktank.to"
private val notificationsApi = "$baseUrl/notifications/api/device"

override suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) {
post(notificationsApi, payload)
}

override suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse {
return post("$notificationsApi/$deviceToken/test-notification", payload)
}

private suspend inline fun <reified T> post(url: String, payload: T) = client.post(url) { setBody(payload) }
}

@Serializable
data class RegisterDeviceRequest(
val deviceToken: String,
val publicKey: String,
val features: List<String>,
val nodeId: String,
val isoTimestamp: String,
val signature: String,
)

@Serializable
data class TestNotificationRequest(
val data: Data,
) {
@Serializable
data class Data(
val source: String,
val type: String,
val payload: Payload,
) {
@Serializable
data class Payload(
val secretMessage: String,
)
}
}
34 changes: 0 additions & 34 deletions app/src/main/java/to/bitkit/data/Models.kt

This file was deleted.

32 changes: 32 additions & 0 deletions app/src/main/java/to/bitkit/data/RestApi.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import to.bitkit.REST
import to.bitkit.ext.toHex
import javax.inject.Inject
Expand DownExpand Up@@ -62,3 +64,33 @@ class EsploraApi @Inject constructor(
return client.get("$REST/tx/${txid}/outspend/${outputIndex}").body()
}
}

@Serializable
data class Tx(
val txid: String,
val status: TxStatus,
)

@Serializable
data class TxStatus(
@SerialName("confirmed")
val isConfirmed: Boolean,
@SerialName("block_height")
val blockHeight: Int? = null,
@SerialName("block_hash")
val blockHash: String? = null,
)

@Serializable
data class OutputSpent(
val spent: Boolean,
)

@Serializable
data class MerkleProof(
@SerialName("block_height")
val blockHeight: Int,
@Suppress("ArrayInDataClass")
val merkle: Array<String>,
val pos: Int,
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,6 @@ class AndroidKeyStore(
val cipher = Cipher.getInstance(transformation).apply { init(Cipher.DECRYPT_MODE, secretKey, spec) }

val decryptedDataBytes = cipher.doFinal(actualEncryptedData)
return decryptedDataBytes.toString(Charsets.UTF_8)
return decryptedDataBytes.decodeToString()
}
}
26 changes: 21 additions & 5 deletions app/src/main/java/to/bitkit/di/HttpModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,27 +6,34 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.ANDROID
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import to.bitkit.data.BlocktankApi
import to.bitkit.data.EsploraApi
import to.bitkit.data.LspApi
import to.bitkit.data.RestApi
import javax.inject.Singleton

val json = Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}

@Module
@InstallIn(SingletonComponent::class)
object HttpModule {
@Provides
@Singleton
fun provideJson(): Json {
return Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}
return json
}

@Provides
Expand All@@ -40,9 +47,18 @@ object HttpModule {
install(ContentNegotiation) {
json(json = json)
}
defaultRequest { // Set default request properties
contentType(ContentType.Application.Json)
}
}
}

@Provides
@Singleton
fun provideLspApi(blocktankApi: BlocktankApi): LspApi {
return blocktankApi
}

@Provides
@Singleton
fun provideRestApi(esploraApi: EsploraApi): RestApi {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/di/ServicesModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.bdk.BitcoinService
import to.bitkit.ldk.LightningService
import to.bitkit.services.BitcoinService
import to.bitkit.services.LightningService

@Module
@InstallIn(SingletonComponent::class)
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/to/bitkit/di/ViewModelModule.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
package to.bitkit.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.services.BlocktankService
import to.bitkit.ui.SharedViewModel
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ViewModelModule {
@Singleton
@Provides
fun provideSharedViewModel(
@BgDispatcher bgDispatcher: CoroutineDispatcher,
blocktankService: BlocktankService,
): SharedViewModel {
return SharedViewModel(
bgDispatcher,
blocktankService,
)
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/ext/ByteArray.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,3 +36,5 @@ fun Any.convertToByteArray(): ByteArray {
fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags)

fun String.fromBase64(flags: Int = Base64.DEFAULT): ByteArray = Base64.decode(this, flags)

val String.uByteList get() = this.toByteArray(Charsets.UTF_8).map { it.toUByte() }
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/ext/Context.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ internal fun toast(
text: String,
duration: Int = Toast.LENGTH_SHORT,
) {
with(currentActivity<MainActivity>()) {
currentActivity<MainActivity>().run {
Toast.makeText(this, text, duration).show()
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.ldk
package to.bitkit.services

import android.content.Context
import androidx.test.core.app.ApplicationProvider
Expand Down
24 changes: 23 additions & 1 deletion app/src/main/java/to/bitkit/Constants.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
package to.bitkit

import android.util.Log
import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.Tag.APP
import to.bitkit.env.Network
import to.bitkit.ext.ensureDir
Expand All@@ -13,6 +14,7 @@ import org.lightningdevkit.ldknode.Network as LdkNetwork
internal object Tag {
const val FCM = "FCM"
const val LDK = "LDK"
const val LSP = "LSP"
const val BDK = "BDK"
const val DEV = "DEV"
const val APP = "APP"
Expand DownExpand Up@@ -81,13 +83,33 @@ data class LnPeer(
val nodeId: String,
val host: String,
val port: String,
val isConnected: Boolean = false,
val isPersisted: Boolean = false,
) {
constructor(nodeId: String, address: String) : this(
constructor(
nodeId: String,
address: String,
isConnected: Boolean = false,
isPersisted: Boolean = false,
) : this(
nodeId,
address.substringBefore(":"),
address.substringAfter(":"),
isConnected,
isPersisted,
)

val address get() = "$host:$port"
override fun toString() = "$nodeId@${address}"

companion object {
fun PeerDetails.toLnPeer(): LnPeer {
return LnPeer(
nodeId = nodeId,
address = address,
isConnected = isConnected,
isPersisted = isPersisted,
)
}
}
}
14 changes: 9 additions & 5 deletions app/src/main/java/to/bitkit/LauncherActivity.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,22 +2,26 @@ package to.bitkit

import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import to.bitkit.ldk.warmupNode
import to.bitkit.ui.MainActivity
import to.bitkit.ui.SharedViewModel
import to.bitkit.ui.initNotificationChannel
import to.bitkit.ui.logFcmToken

@AndroidEntryPoint
class LauncherActivity : AppCompatActivity() {
private val sharedViewModel by viewModels<SharedViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initNotificationChannel()
logFcmToken()
// TODO share mainViewModel in both activities, move warmupNode to it & call it suspending
runBlocking { warmupNode() }
startActivity(Intent(this, MainActivity::class.java))
sharedViewModel.warmupNode()
startActivity(Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
})
}
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
package to.bitkit.di
package to.bitkit.async

import android.util.Log
import kotlinx.coroutines.CoroutineScope
Expand All@@ -14,7 +14,7 @@ import java.util.concurrent.ThreadFactory
import kotlin.coroutines.CoroutineContext

enum class ServiceQueue {
LDK, BDK, MIGRATION;
LDK, BDK, LSP, MIGRATION;

private val scope by lazy { CoroutineScope(dispatcher("$name-queue".lowercase()) + SupervisorJob()) }

Expand Down
57 changes: 57 additions & 0 deletions app/src/main/java/to/bitkit/data/LspApi.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
package to.bitkit.data

import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.Serializable
import javax.inject.Inject

interface LspApi {
suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest)
suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse
}

class BlocktankApi @Inject constructor(
private val client: HttpClient,
) : LspApi {
private val baseUrl = "https://api.stag.blocktank.to"
private val notificationsApi = "$baseUrl/notifications/api/device"

override suspend fun registerDeviceForNotifications(payload: RegisterDeviceRequest) {
post(notificationsApi, payload)
}

override suspend fun testNotification(deviceToken: String, payload: TestNotificationRequest): HttpResponse {
return post("$notificationsApi/$deviceToken/test-notification", payload)
}

private suspend inline fun <reified T> post(url: String, payload: T) = client.post(url) { setBody(payload) }
}

@Serializable
data class RegisterDeviceRequest(
val deviceToken: String,
val publicKey: String,
val features: List<String>,
val nodeId: String,
val isoTimestamp: String,
val signature: String,
)

@Serializable
data class TestNotificationRequest(
val data: Data,
) {
@Serializable
data class Data(
val source: String,
val type: String,
val payload: Payload,
) {
@Serializable
data class Payload(
val secretMessage: String,
)
}
}
34 changes: 0 additions & 34 deletions app/src/main/java/to/bitkit/data/Models.kt

This file was deleted.

32 changes: 32 additions & 0 deletions app/src/main/java/to/bitkit/data/RestApi.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import to.bitkit.REST
import to.bitkit.ext.toHex
import javax.inject.Inject
Expand DownExpand Up@@ -62,3 +64,33 @@ class EsploraApi @Inject constructor(
return client.get("$REST/tx/${txid}/outspend/${outputIndex}").body()
}
}

@Serializable
data class Tx(
val txid: String,
val status: TxStatus,
)

@Serializable
data class TxStatus(
@SerialName("confirmed")
val isConfirmed: Boolean,
@SerialName("block_height")
val blockHeight: Int? = null,
@SerialName("block_hash")
val blockHash: String? = null,
)

@Serializable
data class OutputSpent(
val spent: Boolean,
)

@Serializable
data class MerkleProof(
@SerialName("block_height")
val blockHeight: Int,
@Suppress("ArrayInDataClass")
val merkle: Array<String>,
val pos: Int,
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,6 @@ class AndroidKeyStore(
val cipher = Cipher.getInstance(transformation).apply { init(Cipher.DECRYPT_MODE, secretKey, spec) }

val decryptedDataBytes = cipher.doFinal(actualEncryptedData)
return decryptedDataBytes.toString(Charsets.UTF_8)
return decryptedDataBytes.decodeToString()
}
}
26 changes: 21 additions & 5 deletions app/src/main/java/to/bitkit/di/HttpModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,27 +6,34 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.ANDROID
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import to.bitkit.data.BlocktankApi
import to.bitkit.data.EsploraApi
import to.bitkit.data.LspApi
import to.bitkit.data.RestApi
import javax.inject.Singleton

val json = Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}

@Module
@InstallIn(SingletonComponent::class)
object HttpModule {
@Provides
@Singleton
fun provideJson(): Json {
return Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}
return json
}

@Provides
Expand All@@ -40,9 +47,18 @@ object HttpModule {
install(ContentNegotiation) {
json(json = json)
}
defaultRequest { // Set default request properties
contentType(ContentType.Application.Json)
}
}
}

@Provides
@Singleton
fun provideLspApi(blocktankApi: BlocktankApi): LspApi {
return blocktankApi
}

@Provides
@Singleton
fun provideRestApi(esploraApi: EsploraApi): RestApi {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/di/ServicesModule.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.bdk.BitcoinService
import to.bitkit.ldk.LightningService
import to.bitkit.services.BitcoinService
import to.bitkit.services.LightningService

@Module
@InstallIn(SingletonComponent::class)
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/to/bitkit/di/ViewModelModule.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
package to.bitkit.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import to.bitkit.services.BlocktankService
import to.bitkit.ui.SharedViewModel
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ViewModelModule {
@Singleton
@Provides
fun provideSharedViewModel(
@BgDispatcher bgDispatcher: CoroutineDispatcher,
blocktankService: BlocktankService,
): SharedViewModel {
return SharedViewModel(
bgDispatcher,
blocktankService,
)
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/ext/ByteArray.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,3 +36,5 @@ fun Any.convertToByteArray(): ByteArray {
fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags)

fun String.fromBase64(flags: Int = Base64.DEFAULT): ByteArray = Base64.decode(this, flags)

val String.uByteList get() = this.toByteArray(Charsets.UTF_8).map { it.toUByte() }
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/ext/Context.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ internal fun toast(
text: String,
duration: Int = Toast.LENGTH_SHORT,
) {
with(currentActivity<MainActivity>()) {
currentActivity<MainActivity>().run {
Toast.makeText(this, text, duration).show()
}
}
Expand Down
Loading