Remove phoenix code

This commit is contained in:
Kgothatso Ngako
2026-08-02 18:58:36 +02:00
parent b17901973c
commit e9510c4ea5
205 changed files with 0 additions and 22110 deletions

View File

@@ -1,6 +0,0 @@
package fr.acinq.phoenix.data
import fr.acinq.lightning.io.TcpSocket
import fr.acinq.lightning.utils.ServerAddress
actual fun platformElectrumRegtestConf(): ServerAddress = ServerAddress(host = "127.0.0.1", port = 51002, TcpSocket.TLS.DISABLED)

View File

@@ -1,319 +0,0 @@
package fr.acinq.phoenix.db
import app.cash.sqldelight.Transacter
import app.cash.sqldelight.coroutines.asFlow
import fr.acinq.lightning.utils.UUID
import fr.acinq.lightning.utils.currentTimestampMillis
import fr.acinq.phoenix.data.ContactInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CloudKitContactsDb(
private val paymentsDb: SqlitePaymentsDb
): CoroutineScope by MainScope() {
private val db: Transacter = paymentsDb.database
private val queries = paymentsDb.database.cloudKitContactsQueries
/**
* Provides a flow of the count of items within the cloudkit_contacts_queue table.
*/
private val _queueCount = MutableStateFlow<Long>(0)
val queueCount: StateFlow<Long> = _queueCount.asStateFlow()
data class MetadataRow(
val recordCreation: Long,
val recordBlob: ByteArray
)
data class MissingItem(
val contactId: UUID,
val timestamp: Long
)
data class FetchQueueBatchResult(
// The fetched rowid values from the `cloudkit_contacts_queue` table
val rowids: List<Long>,
// Maps `cloudkit_contacts_queue.rowid` to the corresponding ContactId.
// If missing from the map, then the `cloudkit_contacts_queue` row was
// malformed or unrecognized.
val rowidMap: Map<Long, UUID>,
// Maps to the contact information in the database.
// If missing from the map, then the contacts has been deleted from the database.
val rowMap: Map<UUID, ContactInfo>,
// Maps to `cloudkit_contacts_metadata.ckrecord_info`.
// If missing from the map, then then record doesn't exist in the database.
val metadataMap: Map<UUID, ByteArray>,
)
init {
// N.B.: There appears to be a subtle bug in SQLDelight's
// `.asFlow().mapToX()`, as described here:
// https://github.com/ACINQ/phoenix/pull/415
launch {
queries.fetchQueueCount()
.asFlow()
.map {
withContext(Dispatchers.Default) {
db.transactionWithResult {
it.executeAsOne()
}
}
}
.collect { count ->
_queueCount.value = count
}
}
}
suspend fun fetchQueueBatch(limit: Long): FetchQueueBatchResult {
return withContext(Dispatchers.Default) {
val rowids = mutableListOf<Long>()
val rowidMap = mutableMapOf<Long, UUID>()
val rowMap = mutableMapOf<UUID, ContactInfo>()
val metadataMap = mutableMapOf<UUID, ByteArray>()
db.transaction {
// Step 1 of 3:
// Fetch the rows from the `cloudkit_contacts_queue` batch.
// We are fetching the next/oldest X rows from the queue.
val batch = queries.fetchQueueBatch(limit).executeAsList()
// Step 2 of 3:
// Process the batch, and fill out the `rowids` & `rowidMap` variable.
batch.forEach { row ->
rowids.add(row.rowid)
try {
val contactId = row.id
rowidMap[row.rowid] = UUID.fromString(contactId)
} catch (e: Exception) {
// UUID appears to be malformed within the database.
// Nothing we can do here - but let's at least not crash.
}
} // </batch.forEach>
// Remember: there could be duplicates
val uniqueContactIds = rowidMap.values.toSet()
// Step 3 of 3:
// Fetch the corresponding contact info from the database.
uniqueContactIds.forEach { contactId ->
if (paymentsDb.contacts.contactQueries.existsContact(contactId)) {
// appDb.contactQueries.getContact() throws if the contact
// doesn't exist in database.
paymentsDb.contacts.contactQueries.getContact(contactId)?.let {
rowMap[contactId] = it
}
}
}
// Step 4 of 5:
// Fetch the corresponding `cloudkit_contacts_metadata.ckrecord_info`
uniqueContactIds.forEach { contactId ->
queries.fetchMetadata(
id = contactId.toString()
).executeAsOneOrNull()?.let { row ->
metadataMap[contactId] = row.record_blob
}
}
} // </database.transaction>
FetchQueueBatchResult(
rowids = rowids,
rowidMap = rowidMap,
rowMap = rowMap,
metadataMap = metadataMap
)
}
}
suspend fun updateRows(
deleteFromQueue: List<Long>,
deleteFromMetadata: List<UUID>,
updateMetadata: Map<UUID, MetadataRow>
) {
withContext(Dispatchers.Default) {
db.transaction {
deleteFromQueue.forEach { rowid ->
queries.deleteFromQueue(rowid)
}
deleteFromMetadata.forEach { contactId ->
queries.deleteMetadata(
id = contactId.toString()
)
}
updateMetadata.forEach { (contactId, row) ->
val rowExists = queries.existsMetadata(
id = contactId.toString()
).executeAsOne() > 0
if (rowExists) {
queries.updateMetadata(
record_blob = row.recordBlob,
id = contactId.toString()
)
} else {
queries.addMetadata(
id = contactId.toString(),
record_creation = row.recordCreation,
record_blob = row.recordBlob
)
}
}
}
}
}
suspend fun fetchOldestCreation(): Long? {
return withContext(Dispatchers.Default) {
val row = queries.fetchOldestCreation_Contacts().executeAsOneOrNull()
row?.record_creation
}
}
suspend fun updateRows(
downloadedContacts: List<ContactInfo>,
updateMetadata: Map<UUID, MetadataRow>
) {
// We are seeing crashes when accessing the values within the List<PaymentRow>.
// Perhaps because the List was created in Swift ?
// The workaround seems to be to copy the list here,
// or otherwise process it outside of the `withContext` below.
val contacts = downloadedContacts.map { it.copy() }
withContext(Dispatchers.Default) {
val contactQueries = paymentsDb.contacts.contactQueries
db.transaction {
for (contact in contacts) {
val rowExists = queries.existsMetadata(
id = contact.id.toString()
).executeAsOne() > 0
if (!rowExists) {
contactQueries.saveContact(contact, notify = false)
}
}
for ((contactId, row) in updateMetadata) {
val rowExists = queries.existsMetadata(
id = contactId.toString()
).executeAsOne() > 0
if (rowExists) {
queries.updateMetadata(
record_blob = row.recordBlob,
id = contactId.toString()
)
} else {
queries.addMetadata(
id = contactId.toString(),
record_creation = row.recordCreation,
record_blob = row.recordBlob
)
}
} // </cloudkit_contacts_metadata table>
}
}
}
suspend fun enqueueMissingItems() {
withContext(Dispatchers.Default) {
val rawContactQueries = paymentsDb.database.contactsQueries
db.transaction {
// Step 1 of 3:
// Fetch list of contact ID's that are already represented in the cloud.
val cloudContactIds = mutableSetOf<UUID>()
queries.scanMetadata().executeAsList().forEach { id ->
try {
val contactId = UUID.fromString(id)
cloudContactIds.add(contactId)
} catch (e: Exception) {
// UUID appears to be malformed within the database.
// Nothing we can do here - but let's at least not crash.
}
}
// Step 2 of 3:
// Scan local contact ID's, looking to see if any are missing from the cloud.
val missing = mutableListOf<MissingItem>()
rawContactQueries.scanContacts().executeAsList().forEach { row ->
try {
if (!cloudContactIds.contains(row.id)) {
missing.add(MissingItem(
contactId = row.id,
timestamp = row.created_at
))
}
} catch (e: Exception) {
// UUID appears to be malformed within the database.
// Nothing we can do here - but let's at least not crash.
}
}
// Step 3 of 3:
// Add any missing items to the queue.
//
// But in what order do we want to upload them to the cloud ?
//
// We will choose to upload the OLDEST item first.
// This matches how they normally would have been uploaded.
// Also, when a user restores their wallet (e.g. on a new phone),
// we always want to download the newest contacts first.
// And this assumes the newest items in the cloud are the newest contacts.
//
// Since items are uploaded in FIFO order,
// we just need to make the oldest item have the
// smallest `date_added` value.
missing.sortByDescending { it.timestamp }
// The list is now sorted in descending order.
// Which means the newest item is at index 0,
// and the oldest item is at index <last>.
val now = currentTimestampMillis()
missing.forEachIndexed { idx, item ->
queries.addToQueue(
id = item.contactId.toString(),
date_added = now - idx
)
}
}
}
}
suspend fun clearDatabaseTables() {
withContext(Dispatchers.Default) {
db.transaction {
queries.deleteAllFromMetadata()
queries.deleteAllFromQueue()
}
}
}
}

View File

@@ -1,14 +0,0 @@
package fr.acinq.phoenix.db
import fr.acinq.phoenix.db.payments.CloudKitInterface
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
class CloudKitDb(
appDb: SqliteAppDb,
paymentsDb: SqlitePaymentsDb
): CloudKitInterface, CoroutineScope by MainScope() {
val contacts = CloudKitContactsDb(paymentsDb)
val payments = CloudKitPaymentsDb(paymentsDb)
}

View File

@@ -1,385 +0,0 @@
package fr.acinq.phoenix.db
import app.cash.sqldelight.Transacter
import app.cash.sqldelight.coroutines.asFlow
import fr.acinq.lightning.db.IncomingPayment
import fr.acinq.lightning.db.OutgoingPayment
import fr.acinq.lightning.db.WalletPayment
import fr.acinq.lightning.utils.UUID
import fr.acinq.lightning.utils.currentTimestampMillis
import fr.acinq.phoenix.data.WalletPaymentInfo
import fr.acinq.phoenix.data.WalletPaymentMetadata
import fr.acinq.phoenix.db.payments.SqliteIncomingPaymentsDb
import fr.acinq.phoenix.db.payments.SqliteOutgoingPaymentsDb
import fr.acinq.phoenix.db.payments.WalletPaymentMetadataRow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CloudKitPaymentsDb(
private val paymentsDb: SqlitePaymentsDb
): CoroutineScope by MainScope() {
private val db: Transacter = paymentsDb.database
private val queries = paymentsDb.database.cloudKitPaymentsQueries
/**
* Provides a flow of the count of items within the cloudkit_payments_queue table.
*/
private val _queueCount = MutableStateFlow<Long>(0)
val queueCount: StateFlow<Long> = _queueCount.asStateFlow()
data class MetadataRow(
val unpaddedSize: Long,
val recordCreation: Long,
val recordBlob: ByteArray
)
data class FetchQueueBatchResult(
// The fetched rowid values from the `cloudkit_payments_queue` table
val rowids: List<Long>,
// Maps `cloudkit_payments_queue.rowid` to the corresponding PaymentRowId.
// If missing from the map, then the `cloudkit_payments_queue` row was
// malformed or unrecognized.
val rowidMap: Map<Long, UUID>,
// Maps to the fetch payment information in the database.
// If missing from the map, then the payment has been deleted from the database.
val rowMap: Map<UUID, WalletPaymentInfo>,
// Maps to `cloudkit_payments_metadata.ckrecord_info`.
// If missing from the map, then then record doesn't exist in the database.
val metadataMap: Map<UUID, ByteArray>
)
init {
// N.B.: There appears to be a subtle bug in SQLDelight's
// `.asFlow().mapToX()`, as described here:
// https://github.com/ACINQ/phoenix/pull/415
launch {
queries.fetchQueueCount()
.asFlow()
.map {
withContext(Dispatchers.Default) {
db.transactionWithResult {
it.executeAsOne()
}
}
}
.collect { count ->
_queueCount.value = count
}
}
}
suspend fun fetchQueueBatch(limit: Long): FetchQueueBatchResult {
return withContext(Dispatchers.Default) {
val ckQueries = paymentsDb.database.cloudKitPaymentsQueries
val rowids = mutableListOf<Long>()
val rowidMap = mutableMapOf<Long, UUID>()
val rowMap = mutableMapOf<UUID, WalletPaymentInfo>()
val metadataMap = mutableMapOf<UUID, ByteArray>()
db.transaction {
// Step 1 of 4:
// Fetch the rows from the `cloudkit_payments_queue` batch.
// We are fetching the next/oldest X rows from the queue.
val batch = ckQueries.fetchQueueBatch(limit).executeAsList()
// Step 2 of 4:
// Process the batch, and fill out the `rowids` & `rowidMap` variable.
batch.forEach { row ->
rowids.add(row.rowid)
rowidMap[row.rowid] = row.id
} // </batch.forEach>
// Remember: there could be duplicates
val uniquePaymentIds = rowidMap.values.toSet()
// Step 3 of 4:
// Fetch the corresponding payment info from the database.
// In order to optimize disk access, we fetch from 1 table at a time.
val metadataPlaceholder = WalletPaymentMetadata()
uniquePaymentIds.forEach { paymentId ->
paymentsDb._getPayment(paymentId)?.let { pair ->
rowMap[paymentId] = WalletPaymentInfo(
payment = pair.first,
metadata = metadataPlaceholder,
contact = null
)
}
}
uniquePaymentIds.forEach { paymentId ->
paymentsDb.metadataQueries.get(paymentId)?.let { metadata ->
rowMap[paymentId]?.let {
rowMap[paymentId] = it.copy(
metadata = metadata,
contact = null
)
}
}
} // </payments_metadata>
// Step 4 of 4:
// Fetch the corresponding `cloudkit_payments_metadata.ckrecord_info`
uniquePaymentIds.forEach { paymentId ->
ckQueries.fetchMetadata(paymentId).executeAsOneOrNull()?.let { row ->
metadataMap[paymentId] = row.record_blob
}
}
} // </db.transaction>
FetchQueueBatchResult(
rowids = rowids,
rowidMap = rowidMap,
rowMap = rowMap,
metadataMap = metadataMap
)
}
}
suspend fun updateRows(
deleteFromQueue: List<Long>,
deleteFromMetadata: List<UUID>,
updateMetadata: Map<UUID, MetadataRow>
) {
withContext(Dispatchers.Default) {
db.transaction {
deleteFromQueue.forEach { rowid ->
queries.deleteFromQueue(rowid)
}
deleteFromMetadata.forEach { paymentId ->
queries.deleteMetadata(paymentId)
}
updateMetadata.forEach { (paymentId, row) ->
val rowExists = queries.existsMetadata(paymentId).executeAsOne() > 0
if (rowExists) {
queries.updateMetadata(
unpadded_size = row.unpaddedSize,
record_blob = row.recordBlob,
id = paymentId
)
} else {
queries.addMetadata(
id = paymentId,
unpadded_size = row.unpaddedSize,
record_creation = row.recordCreation,
record_blob = row.recordBlob
)
}
}
}
}
}
suspend fun fetchMetadata(
id: UUID
): ByteArray? = withContext(Dispatchers.Default) {
val row = queries.fetchMetadata(id = id).executeAsOneOrNull()
row?.record_blob
}
suspend fun fetchOldestCreation(): Long? = withContext(Dispatchers.Default) {
val row = queries.fetchOldestCreation().executeAsOneOrNull()
row?.record_creation
}
suspend fun updateRows(
downloadedPayments: List<WalletPayment>,
downloadedPaymentsMetadata: Map<UUID, WalletPaymentMetadataRow>,
updateMetadata: Map<UUID, MetadataRow>
) {
withContext(Dispatchers.Default) {
val inQueries = paymentsDb.database.paymentsIncomingQueries
val outQueries = paymentsDb.database.paymentsOutgoingQueries
val ckQueries = paymentsDb.database.cloudKitPaymentsQueries
val metaQueries = paymentsDb.database.paymentsMetadataQueries
val incomingPaymentsDb = SqliteIncomingPaymentsDb(paymentsDb.database, paymentsDb.paymentMetadataQueue)
val outgoingPaymentsDb = SqliteOutgoingPaymentsDb(paymentsDb.database, paymentsDb.paymentMetadataQueue)
db.transaction {
downloadedPayments.forEach { payment ->
val paymentId: UUID = payment.id
if (payment is IncomingPayment) {
val existing = inQueries.get(paymentId).executeAsOneOrNull()
if (existing == null) {
incomingPaymentsDb._addIncomingPayment(payment, metadata = null, notify = false)
}
} else if (payment is OutgoingPayment) {
val existing = outQueries.get(paymentId).executeAsOneOrNull()
if (existing == null) {
outgoingPaymentsDb._addOutgoingPayment(payment, metadata = null, notify = false)
}
}
}
downloadedPaymentsMetadata.forEach { (paymentId, row) ->
val rowExists = metaQueries.hasMetadata(paymentId).executeAsOne() > 0
if (!rowExists) {
metaQueries.addMetadata(
payment_id = paymentId,
lnurl_base_type = row.lnurl_base?.first,
lnurl_base_blob = row.lnurl_base?.second,
lnurl_metadata_type = row.lnurl_metadata?.first,
lnurl_metadata_blob = row.lnurl_metadata?.second,
lnurl_successAction_type = row.lnurl_successAction?.first,
lnurl_successAction_blob = row.lnurl_successAction?.second,
lnurl_description = row.lnurl_description,
user_description = row.user_description,
user_notes = row.user_notes,
modified_at = row.modified_at,
original_fiat_type = row.original_fiat?.first,
original_fiat_rate = row.original_fiat?.second,
lightning_address = row.lightning_address
)
}
} // </payments_metadata table>
updateMetadata.forEach { (paymentId, row) ->
val rowExists = ckQueries.existsMetadata(paymentId).executeAsOne() > 0
if (rowExists) {
ckQueries.updateMetadata(
unpadded_size = row.unpaddedSize,
record_blob = row.recordBlob,
id = paymentId
)
} else {
ckQueries.addMetadata(
id = paymentId,
unpadded_size = row.unpaddedSize,
record_creation = row.recordCreation,
record_blob = row.recordBlob
)
}
} // </cloudkit_payments_metadata table>
}
}
}
suspend fun enqueueOutdatedItems() = withContext(Dispatchers.Default) {
val ckQueries = paymentsDb.database.cloudKitPaymentsQueries
db.transaction {
val paymentIds = mutableListOf<UUID>()
ckQueries.listNonZeroSizes().executeAsList().forEach { row ->
paymentIds.add(row.id)
}
for (paymentId in paymentIds) {
ckQueries.addToQueue(
id = paymentId,
date_added = currentTimestampMillis()
)
}
}
}
data class MissingItem(
val paymentId: UUID,
val timestamp: Long
)
suspend fun enqueueMissingItems() {
withContext(Dispatchers.Default) {
val ckQueries = paymentsDb.database.cloudKitPaymentsQueries
db.transaction {
// Step 1 of 3:
// Fetch list of payment ID's that are already represented in the cloud.
val existing = mutableSetOf<UUID>()
ckQueries.scanMetadata().executeAsList().forEach { uuid ->
existing.add(uuid)
}
// Step 2 of 3:
// Scan local payment ID's, looking to see if any are missing from the cloud.
val missing = mutableListOf<MissingItem>()
run {
val inQueries = paymentsDb.database.paymentsIncomingQueries
inQueries.listSuccessfulIds().executeAsList().forEach { row ->
if (!existing.contains(row.id)) {
missing.add(MissingItem(row.id, row.received_at))
}
}
}
run {
val outQueries = paymentsDb.database.paymentsOutgoingQueries
outQueries.listSuccessfulIds().executeAsList().forEach { row ->
if (!existing.contains(row.id)) {
missing.add(MissingItem(row.id, row.completed_at))
}
}
}
// Step 3 of 3:
// Add any missing items to the queue.
//
// But in what order do we want to upload them to the cloud ?
//
// We will choose to upload the OLDEST item first.
// This matches how they normally would have been uploaded.
// Also, when a user restores their wallet (e.g. on a new phone),
// we always want to download the newest payments first.
// And this assumes the newest items in the cloud are the newest payments.
//
// Since items are uploaded in FIFO order,
// we just need to make the oldest item have the
// smallest `date_added` value.
missing.sortByDescending { it.timestamp }
// The list is now sorted in descending order.
// Which means the newest item is at index 0,
// and the oldest item is at index <last>.
val now = currentTimestampMillis()
missing.forEachIndexed { idx, item ->
ckQueries.addToQueue(
id = item.paymentId,
date_added = now - idx
)
}
}
}
}
suspend fun clearDatabaseTables() {
withContext(Dispatchers.Default) {
db.transaction {
queries.deleteAllFromMetadata()
queries.deleteAllFromQueue()
}
}
}
}

View File

@@ -1,89 +0,0 @@
package fr.acinq.phoenix.db
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.native.NativeSqliteDriver
import app.cash.sqldelight.driver.native.wrapConnection
import co.touchlab.sqliter.DatabaseConfiguration
import fr.acinq.phoenix.db.migrations.v10.AfterVersion10
import fr.acinq.phoenix.db.migrations.v11.AfterVersion11
import fr.acinq.phoenix.db.sqldelight.AppDatabase
import fr.acinq.phoenix.db.sqldelight.ChannelsDatabase
import fr.acinq.phoenix.db.sqldelight.PaymentsDatabase
import fr.acinq.phoenix.utils.PlatformContext
import fr.acinq.phoenix.utils.getDatabaseFilesDirectoryPath
actual fun createChannelsDbDriver(
ctx: PlatformContext,
fileName: String
): SqlDriver {
val schema = ChannelsDatabase.Schema
// The foreign_keys constraint needs to be set via the DatabaseConfiguration:
// https://github.com/cashapp/sqldelight/issues/1356
val dbDir = getDatabaseFilesDirectoryPath(ctx)
val configuration = DatabaseConfiguration(
name = fileName,
version = schema.version.toInt(),
extendedConfig = DatabaseConfiguration.Extended(
basePath = dbDir,
foreignKeyConstraints = true
),
create = { connection ->
wrapConnection(connection) { schema.create(it) }
},
upgrade = { connection, oldVersion, newVersion ->
wrapConnection(connection) { schema.migrate(it, oldVersion.toLong(), newVersion.toLong()) }
}
)
return NativeSqliteDriver(configuration)
}
actual fun createPaymentsDbDriver(
ctx: PlatformContext,
fileName: String,
onError: (String) -> Unit
): SqlDriver {
val schema = PaymentsDatabase.Schema
val dbDir = getDatabaseFilesDirectoryPath(ctx)
val configuration = DatabaseConfiguration(
name = fileName,
version = schema.version.toInt(),
extendedConfig = DatabaseConfiguration.Extended(
basePath = dbDir,
foreignKeyConstraints = true
),
create = { connection ->
wrapConnection(connection) { schema.create(it) }
},
upgrade = { connection, oldVersion, newVersion ->
wrapConnection(connection) { schema.migrate(it, oldVersion.toLong(), newVersion.toLong(), AfterVersion10(onError), AfterVersion11(onError)) }
}
)
return NativeSqliteDriver(configuration)
}
actual fun createAppDbDriver(
ctx: PlatformContext
): SqlDriver {
val schema = AppDatabase.Schema
val name = "app.sqlite"
val dbDir = getDatabaseFilesDirectoryPath(ctx)
val configuration = DatabaseConfiguration(
name = name,
version = schema.version.toInt(),
extendedConfig = DatabaseConfiguration.Extended(
basePath = dbDir,
foreignKeyConstraints = true
),
create = { connection ->
wrapConnection(connection) { schema.create(it) }
},
upgrade = { connection, oldVersion, newVersion ->
wrapConnection(connection) { schema.migrate(it, oldVersion.toLong(), newVersion.toLong()) }
}
)
return NativeSqliteDriver(configuration)
}

View File

@@ -1,36 +0,0 @@
package fr.acinq.phoenix.db
import fr.acinq.lightning.utils.UUID
import fr.acinq.lightning.utils.currentTimestampMillis
import fr.acinq.phoenix.db.payments.CloudKitInterface
import fr.acinq.phoenix.db.sqldelight.PaymentsDatabase
actual fun didSaveWalletPayment(id: UUID, database: PaymentsDatabase) {
database.cloudKitPaymentsQueries.addToQueue(id = id, date_added = currentTimestampMillis())
}
actual fun didDeleteWalletPayment(id: UUID, database: PaymentsDatabase) {
database.cloudKitPaymentsQueries.addToQueue(id = id, date_added = currentTimestampMillis())
}
actual fun didUpdateWalletPaymentMetadata(id: UUID, database: PaymentsDatabase) {
database.cloudKitPaymentsQueries.addToQueue(id = id, date_added = currentTimestampMillis())
}
actual fun didSaveContact(contactId: UUID, database: PaymentsDatabase) {
database.cloudKitContactsQueries.addToQueue(
id = contactId.toString(),
date_added = currentTimestampMillis()
)
}
actual fun didDeleteContact(contactId: UUID, database: PaymentsDatabase) {
database.cloudKitContactsQueries.addToQueue(
id = contactId.toString(),
date_added = currentTimestampMillis()
)
}
actual fun makeCloudKitDb(appDb: SqliteAppDb, paymentsDb: SqlitePaymentsDb): CloudKitInterface? {
return CloudKitDb(appDb, paymentsDb)
}

View File

@@ -1,311 +0,0 @@
package fr.acinq.phoenix.ios
import co.touchlab.kermit.Logger
import com.machankura.compose.AppVersion
import com.machankura.compose.ui.composable.widgets.wallet.WalletAvatars
import fr.acinq.lightning.LiquidityEvents
import fr.acinq.lightning.PaymentEvents
import fr.acinq.lightning.utils.Connection
import fr.acinq.lightning.utils.currentTimestampMillis
import fr.acinq.phoenix.BusinessMonitorJobs
import fr.acinq.phoenix.BusinessRunning
import fr.acinq.phoenix.PhoenixBusiness
import fr.acinq.phoenix.PhoenixGlobal
import fr.acinq.phoenix.data.StartBusinessResult
import fr.acinq.phoenix.data.StartupParams
import fr.acinq.phoenix.data.WalletId
import fr.acinq.phoenix.data.inFlightPaymentsCount
import fr.acinq.phoenix.managers.AppConnectionsDaemon
import fr.acinq.phoenix.managers.NodeParamsManager
import fr.acinq.phoenix.managers.PeerManager
import fr.acinq.phoenix.managers.global.CurrencyManager
import fr.acinq.phoenix.utils.DateUtils
import fr.acinq.phoenix.utils.MnemonicLanguage
import fr.acinq.phoenix.utils.PlatformContext
import fr.acinq.phoenix.utils.preferences.GlobalPrefs
import fr.acinq.phoenix.utils.preferences.InternalPrefs
import fr.acinq.phoenix.utils.preferences.UserPrefs
import fr.acinq.phoenix.utils.preferences.UserWalletMetadata
import fr.acinq.phoenix.utils.preferences.getByWalletIdOrDefault
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
object BusinessManager {
private val log = Logger.withTag("BusinessManager")
private val supervisor = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + supervisor)
private val startupMutex = Mutex()
val phoenixGlobal: PhoenixGlobal = PhoenixGlobal(
ctx = PlatformContext()
)
/** A map of (walletId -> active businesses) */
private val _businessFlow = MutableStateFlow<Map<WalletId, BusinessRunning>>(emptyMap())
val businessFlow = _businessFlow.asStateFlow()
/** Map of jobs monitoring events/payments once business starts */
private val eventsMonitoringJobs = mutableMapOf<WalletId, BusinessMonitorJobs>() //List<Job>>()
suspend fun startNewBusiness(words: List<String>, isHeadless: Boolean): StartBusinessResult = startupMutex.withLock {
val business = PhoenixBusiness(phoenixGlobal)
val walletInfo = try {
val seed = business.walletManager.mnemonicsToSeed(words, wordList = MnemonicLanguage.English.wordlist())
business.walletManager.loadWallet(seed)
} catch (e: Exception) {
return StartBusinessResult.Failure.LoadWalletError
}
val walletId = WalletId(walletInfo.nodeIdHash)
val nodeId = walletInfo.nodeId.toHex()
val dataStoreManager = business.dataStoreManager
val globalPrefs: GlobalPrefs = dataStoreManager.loadGlobalPrefsForWallet()
val walletMetadata = globalPrefs.getAvailableWalletsMeta.first()[walletId] ?: run {
val metadata = UserWalletMetadata(
walletId = walletId,
name = null,
avatar = WalletAvatars.list.random(),
createdAt = currentTimestampMillis(),
isHidden = false
)
globalPrefs.saveAvailableWalletMeta(metadata)
metadata
}
val userPrefs = dataStoreManager.loadUserPrefsForWallet(walletId)
val internalPrefs = dataStoreManager.loadInternalPrefsForWallet(walletId)
val businessInFlow = businessFlow.value[walletId]?.business
if (businessInFlow != null) {
log.i { "business already exists in flow, ignoring..." }
return StartBusinessResult.Success(walletInfo, businessInFlow)
}
return try {
log.i("preparing new business with node_id=$nodeId wallet_id=$walletId...")
// check last used version to display a patch note
val lastVersionUsed = globalPrefs.getLastUsedAppCode.first()
if (lastVersionUsed == null) {
// lastUsedAppCode was added in version 99, and is set up during the wallet creation. So if it's null, this Phoenix was installed prior v99 and we can show a patch note
globalPrefs.saveShowReleaseNoteSinceCode("98")
}
else if (lastVersionUsed < AppVersion.versionCode) {
globalPrefs.saveShowReleaseNoteSinceCode(lastVersionUsed)
}
// update app configuration with user preferences
business.appConfigurationManager.updateElectrumConfig(userPrefs.getElectrumServer.first())
val preferredCurrencies = userPrefs.getFiatCurrencies.first()
business.appConfigurationManager.updatePreferredFiatCurrencies(preferredCurrencies)
business.phoenixGlobal.currencyManager.startMonitoringCurrencies(walletId = walletId.nodeIdHash, currencies = preferredCurrencies)
// setup jobs monitoring the business events
eventsMonitoringJobs[walletId] = BusinessMonitorJobs(
monitorHeadlessPaymentsJob = if (isHeadless) {
scope.launch { monitorPaymentsWhenHeadless(walletId, walletMetadata, business.nodeParamsManager, phoenixGlobal.currencyManager, userPrefs) }
} else null,
monitorNodeEventsJob = scope.launch { monitorNodeEvents(walletId, business.peerManager, business.nodeParamsManager, globalPrefs, internalPrefs) },
monitorFcmTokenJob = scope.launch { monitorFcmToken(globalPrefs, business) },
monitorInFlightPaymentsJob = scope.launch { monitorInFlightPayments(business.peerManager, internalPrefs) },
)
// startup params depend user's settings: Tor and liquidity policy
val startupParams = StartupParams(isTorEnabled = userPrefs.getIsTorEnabled.first(), liquidityPolicy = userPrefs.getLiquidityPolicy.first())
delay(1_000)
// actually start the business
log.i("starting new business with node_id=$nodeId...")
_businessFlow.value += walletId to BusinessRunning(business = business, isHeadless = isHeadless)
business.start(startupParams)
// the node has been started, so we can now increment the last-used build code
globalPrefs.saveLastUsedAppCode(AppVersion.versionCode)
// start watching the swap-in wallet
scope.launch {
business.peerManager.getPeer().startWatchSwapInWallet()
}
log.i("business initialisation has successfully completed")
StartBusinessResult.Success(walletInfo, business)
} catch (e: Exception) {
log.e("there was an error when initialising new business: ", e)
stopBusiness(walletId)
StartBusinessResult.Failure.Generic(e)
}
}
/**
* Updates the matching business in the map of active businesses with a non-headless flag. Should be called when the UI starts a given wallet.
* If called improperly, will not have severe effects ; the app will just show incoming payment notifications.
*/
fun updateBusinessActiveInUI(walletId: WalletId) {
val businessMap = _businessFlow.value.toMutableMap()
businessMap[walletId]?.let {
businessMap[walletId] = it.copy(isHeadless = false)
}
eventsMonitoringJobs[walletId]?.monitorHeadlessPaymentsJob?.cancel()
_businessFlow.value = businessMap
}
fun stopAllHeadlessBusinesses() {
val headlessBusinesses = businessFlow.value.filter { it.value.isHeadless }
log.i("stopping all headless businesses (${headlessBusinesses.size})...")
headlessBusinesses.forEach { doStopBusiness(it.key, it.value) }
_businessFlow.value = businessFlow.value.minus(headlessBusinesses.keys)
}
fun stopAllBusinesses() {
log.i("stopping all businesses...")
businessFlow.value.forEach { doStopBusiness(it.key, it.value) }
_businessFlow.value = emptyMap()
}
fun stopBusiness(walletId: WalletId) {
val businessMap = _businessFlow.value.toMutableMap()
businessMap[walletId]?.let { doStopBusiness(walletId, it)}
businessMap.remove(walletId)
_businessFlow.value = businessMap
}
private fun doStopBusiness(walletId: WalletId, running: BusinessRunning) {
running.business.appConnectionsDaemon?.incrementDisconnectCount(AppConnectionsDaemon.ControlTarget.All)
running.business.stop()
eventsMonitoringJobs.remove(walletId)?.let {
it.monitorHeadlessPaymentsJob?.cancel()
it.monitorFcmTokenJob.cancel()
it.monitorNodeEventsJob.cancel()
it.monitorInFlightPaymentsJob.cancel()
}
phoenixGlobal.currencyManager.stopMonitoringForWallet(walletId.nodeIdHash)
}
fun refreshFcmToken() {
// TODO: FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
// if (!task.isSuccessful) {
// fr.acinq.phoenix.android.BusinessManager.log.warn("fetching FCM registration token failed: ${task.exception?.localizedMessage}")
// return@OnCompleteListener
// }
// task.result?.let { fr.acinq.phoenix.android.BusinessManager.scope.launch { application.globalPrefs.saveFcmToken(it) } }
// })
}
private suspend fun monitorFcmToken(globalPrefs: GlobalPrefs,business: PhoenixBusiness) {
val token = globalPrefs.getFcmToken.filterNotNull().first()
business.connectionsManager.connections.first { it.peer == Connection.ESTABLISHED }
delay(5000)
log.i("registering fcm token=$token")
business.registerFcmToken(token)
}
private suspend fun monitorNodeEvents(walletId: WalletId, peerManager: PeerManager, nodeParamsManager: NodeParamsManager, globalPrefs: GlobalPrefs, internalPrefs: InternalPrefs) {
val monitoringStartedAt = currentTimestampMillis()
combine(
peerManager.swapInNextTimeout,
nodeParamsManager.nodeParams.filterNotNull().first().nodeEvents
) { nextTimeout, nodeEvent ->
nextTimeout to nodeEvent
}.collect { (_, event) ->
// TODO: click on notif must deeplink to the notification screen
when (event) {
is LiquidityEvents.Rejected -> {
log.d("processing liquidity_event=$event")
if (event.source == LiquidityEvents.Source.OnChainWallet) {
// Check the last time a rejected on-chain swap notification has been shown. If recent, we do not want to trigger a notification every time.
val lastRejectedSwap = internalPrefs.getLastRejectedOnchainSwap.first().takeIf {
// However, if the app started < 2 min ago, we always want to display a notification. So we'll ignore this check ^
currentTimestampMillis() - monitoringStartedAt >= 2 * DateUtils.MINUTE_IN_MILLIS
}
if (lastRejectedSwap != null
&& lastRejectedSwap.first == event.amount
&& currentTimestampMillis() - lastRejectedSwap.second <= 2 * DateUtils.HOUR_IN_MILLIS
) {
log.d("ignore this liquidity event as a similar notification was recently displayed")
return@collect
} else {
internalPrefs.saveLastRejectedOnchainSwap(event)
}
}
globalPrefs.getAvailableWalletsMeta.first().getByWalletIdOrDefault(walletId)
when (val reason = event.reason) {
is LiquidityEvents.Rejected.Reason.PolicySetToDisabled -> {
// TODO: SystemNotificationHelper.notifyPaymentRejectedPolicyDisabled(appContext, walletId, walletMetadata, event.source, event.amount, nextTimeout?.second)
}
is LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee -> {
// TODO: SSystemNotificationHelper.notifyPaymentRejectedOverAbsolute(appContext, walletId, walletMetadata, event.source, event.amount, event.fee, reason.maxAbsoluteFee, nextTimeout?.second)
}
is LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee -> {
// TODO: SSystemNotificationHelper.notifyPaymentRejectedOverRelative(appContext, walletId, walletMetadata, event.source, event.amount, event.fee, reason.maxRelativeFeeBasisPoints, nextTimeout?.second)
}
is LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow -> {
// TODO: SSystemNotificationHelper.notifyPaymentRejectedAmountTooLow(appContext, walletId, walletMetadata, event.source, event.amount)
}
// Temporary errors
is LiquidityEvents.Rejected.Reason.ChannelFundingInProgress,
is LiquidityEvents.Rejected.Reason.NoMatchingFundingRate,
is LiquidityEvents.Rejected.Reason.TooManyParts -> {
// TODO: SSystemNotificationHelper.notifyPaymentRejectedFundingError(appContext, walletId, walletMetadata, event.source, event.amount)
}
}
}
else -> Unit
}
}
}
private suspend fun monitorPaymentsWhenHeadless(walletId: WalletId, walletMetadata: UserWalletMetadata, nodeParamsManager: NodeParamsManager, currencyManager: CurrencyManager, userPrefs: UserPrefs) {
nodeParamsManager.nodeParams.filterNotNull().first().nodeEvents.collect { event ->
when (event) {
is PaymentEvents.PaymentReceived -> {
// TODO: SystemNotificationHelper.notifyPaymentsReceived(
// context = appContext,
// userPrefs = userPrefs,
// walletId = walletId,
// userWalletMetadata = walletMetadata,
// paymentId = event.payment.id,
// paymentAmount = event.payment.amountReceived,
// rates = currencyManager.ratesFlow.value,
// )
}
else -> Unit
}
}
}
private suspend fun monitorInFlightPayments(peerManager: PeerManager, internalPrefs: InternalPrefs) {
peerManager.channelsFlow.filterNotNull().collect {
val inFlightPaymentsCount = it.inFlightPaymentsCount()
internalPrefs.saveInFlightPaymentsCount(inFlightPaymentsCount)
// TODO: InflightPaymentsWatcher
// if (inFlightPaymentsCount == 0) {
// InflightPaymentsWatcher.cancel(appContext)
// } else {
// InflightPaymentsWatcher.scheduleOnce(appContext, delay = 2.hours)
// }
}
}
fun clear() {
supervisor.cancel()
}
}

View File

@@ -1,27 +0,0 @@
package fr.acinq.phoenix.managers
import fr.acinq.phoenix.utils.PlatformContext
import kotlinx.cinterop.ExperimentalForeignApi
import okio.Path
import okio.Path.Companion.toPath
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSURL
import platform.Foundation.NSUserDomainMask
@OptIn(ExperimentalForeignApi::class)
actual fun computePreferencePath(
platformContext: PlatformContext,
dataStoreFileName: String
): Path {
val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory(
directory = NSDocumentDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = false,
error = null,
)
val path = requireNotNull(documentDirectory).path + "${Path.DIRECTORY_SEPARATOR}$dataStoreFileName"
return path.toPath()
}

View File

@@ -1,93 +0,0 @@
package fr.acinq.phoenix.managers.global
import fr.acinq.lightning.logging.LoggerFactory
import fr.acinq.lightning.logging.debug
import fr.acinq.phoenix.utils.PlatformContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import platform.Network.nw_path_get_status
import platform.Network.nw_path_monitor_cancel
import platform.Network.nw_path_monitor_create
import platform.Network.nw_path_monitor_set_queue
import platform.Network.nw_path_monitor_set_update_handler
import platform.Network.nw_path_monitor_start
import platform.Network.nw_path_monitor_t
import platform.Network.nw_path_status_invalid
import platform.Network.nw_path_status_satisfiable
import platform.Network.nw_path_status_satisfied
import platform.Network.nw_path_status_unsatisfied
import platform.darwin.dispatch_get_main_queue
actual class NetworkMonitor actual constructor(
loggerFactory: LoggerFactory,
ctx: PlatformContext
) : CoroutineScope by MainScope() {
private val logger = loggerFactory.newLogger(this::class)
private val _networkState = MutableStateFlow(NetworkState.NotAvailable)
actual val networkState: StateFlow<NetworkState> = _networkState
private var enabled = true
private var monitor: nw_path_monitor_t = null
actual fun enable() {
enabled = true
start()
}
actual fun disable() {
enabled = false
stop()
_networkState.value = NetworkState.Available
}
@OptIn(ExperimentalUnsignedTypes::class)
actual fun start() {
if (!enabled || monitor != null) {
return
}
monitor = nw_path_monitor_create()
nw_path_monitor_set_update_handler(monitor) { path ->
val status = when (nw_path_get_status(path)) {
nw_path_status_satisfied -> {
logger.debug { "status = nw_path_status_satisfied" }
NetworkState.Available
}
nw_path_status_satisfiable -> {
logger.debug { "status = nw_path_status_satisfiable" }
NetworkState.Available
}
nw_path_status_unsatisfied -> {
logger.debug { "status = nw_path_status_unsatisfied" }
NetworkState.NotAvailable
}
nw_path_status_invalid -> {
logger.debug { "status = nw_path_status_invalid" }
NetworkState.NotAvailable
}
else -> {
logger.debug { "status = nw_path_status_unknown" }
NetworkState.NotAvailable
}
}
launch { _networkState.value = status }
}
nw_path_monitor_set_queue(monitor, dispatch_get_main_queue())
nw_path_monitor_start(monitor)
}
actual fun stop() {
if (monitor != null) {
// NB: once cancelled, a monitor instance cannot be started again
nw_path_monitor_cancel(monitor)
monitor = null
}
}
}

View File

@@ -1,210 +0,0 @@
package fr.acinq.phoenix.security
import kotlinx.cinterop.ByteVar
import kotlinx.cinterop.CValuesRef
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.MemScope
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.alloc
import kotlinx.cinterop.convert
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.pin
import kotlinx.cinterop.ptr
import kotlinx.cinterop.refTo
import kotlinx.cinterop.value
import platform.CoreCrypto.CCAlgorithm
import platform.CoreCrypto.CCCryptorCreateWithMode
import platform.CoreCrypto.CCCryptorFinal
import platform.CoreCrypto.CCCryptorGetOutputLength
import platform.CoreCrypto.CCCryptorRefVar
import platform.CoreCrypto.CCCryptorRelease
import platform.CoreCrypto.CCCryptorStatus
import platform.CoreCrypto.CCCryptorUpdate
import platform.CoreCrypto.CCMode
import platform.CoreCrypto.CCOperation
import platform.CoreCrypto.CCPadding
import platform.CoreCrypto.ccPKCS7Padding
import platform.CoreCrypto.kCCAlgorithmAES
import platform.CoreCrypto.kCCAlignmentError
import platform.CoreCrypto.kCCBufferTooSmall
import platform.CoreCrypto.kCCDecodeError
import platform.CoreCrypto.kCCDecrypt
import platform.CoreCrypto.kCCEncrypt
import platform.CoreCrypto.kCCMemoryFailure
import platform.CoreCrypto.kCCModeCBC
import platform.CoreCrypto.kCCParamError
import platform.CoreCrypto.kCCSuccess
import platform.CoreCrypto.kCCUnimplemented
import platform.posix.size_tVar
@OptIn(ExperimentalForeignApi::class)
private val almostEmptyArrayPinned = ByteArray(1).pin()
@ExperimentalForeignApi
fun ByteArray.safeRefTo(index: Int): CValuesRef<ByteVar> {
if (index == size) return almostEmptyArrayPinned.addressOf(0)
return refTo(index)
}
/**
* Might just load up cryptography-kotlin as a dependency as it would allow us to have this logic in commonMain
* https://github.com/whyoleg/cryptography-kotlin.git
*/
@OptIn(ExperimentalForeignApi::class)
class CCCipher(
private val algorithm: CCAlgorithm,
private val mode: CCMode,
private val padding: CCPadding,
private val key: ByteArray,
) {
companion object {
fun phoenixCipherForKey(key: ByteArray): CCCipher {
return CCCipher(
algorithm = kCCAlgorithmAES,
mode = kCCModeCBC,
padding = ccPKCS7Padding,
key = key,
)
}
}
fun encrypt(iv: ByteArray?, plaintext: ByteArray): ByteArray = memScoped {
useCryptor { cryptorRef ->
cryptorRef.create(kCCEncrypt, iv?.refTo(0))
val ciphertextOutput = ByteArray(cryptorRef.outputLength(plaintext.size))
val dataOutMoved = alloc<size_tVar>()
val moved = cryptorRef.update(
dataIn = plaintext.safeRefTo(0),
dataInLength = plaintext.size,
dataOut = ciphertextOutput.safeRefTo(0),
dataOutAvailable = ciphertextOutput.size,
dataOutMoved = dataOutMoved,
)
if (ciphertextOutput.size != moved) cryptorRef.final(
dataOut = ciphertextOutput.refTo(moved),
dataOutAvailable = ciphertextOutput.size - moved,
dataOutMoved = dataOutMoved,
)
ciphertextOutput
}
}
fun decrypt(iv: ByteArray?, ciphertext: ByteArray, ciphertextStartIndex: Int): ByteArray = memScoped {
useCryptor { cryptorRef ->
cryptorRef.create(kCCDecrypt, iv?.refTo(0))
val plaintextOutput = ByteArray(cryptorRef.outputLength(ciphertext.size - ciphertextStartIndex))
val dataOutMoved = alloc<size_tVar>()
var moved = cryptorRef.update(
dataIn = ciphertext.safeRefTo(ciphertextStartIndex),
dataInLength = ciphertext.size - ciphertextStartIndex,
dataOut = plaintextOutput.safeRefTo(0),
dataOutAvailable = plaintextOutput.size,
dataOutMoved = dataOutMoved
)
if (plaintextOutput.size != moved) moved += cryptorRef.final(
dataOut = plaintextOutput.refTo(moved),
dataOutAvailable = plaintextOutput.size - moved,
dataOutMoved = dataOutMoved
)
if (plaintextOutput.size == moved) {
plaintextOutput
} else {
plaintextOutput.copyOf(moved)
}
}
}
private inline fun <T> MemScope.useCryptor(block: (cryptorRef: CCCryptorRefVar) -> T): T {
val cryptorRef = alloc<CCCryptorRefVar>()
try {
return block(cryptorRef)
} finally {
CCCryptorRelease(cryptorRef.value)
}
}
private fun CCCryptorRefVar.create(op: CCOperation, iv: CValuesRef<*>?) {
checkResult(
CCCryptorCreateWithMode(
op = op,
cryptorRef = ptr,
alg = algorithm,
mode = mode,
padding = padding,
key = key.refTo(0),
keyLength = key.size.convert(),
iv = iv,
// unused options
options = 0.convert(),
tweak = null,
tweakLength = 0.convert(),
numRounds = 0,
)
)
}
private fun CCCryptorRefVar.outputLength(inputLength: Int): Int {
return CCCryptorGetOutputLength(
cryptorRef = value,
inputLength = inputLength.convert(),
final = true
).convert()
}
private fun CCCryptorRefVar.update(
dataIn: CValuesRef<*>,
dataInLength: Int,
dataOut: CValuesRef<*>,
dataOutAvailable: Int,
dataOutMoved: size_tVar,
): Int {
checkResult(
CCCryptorUpdate(
cryptorRef = value,
dataIn = dataIn,
dataInLength = dataInLength.convert(),
dataOut = dataOut,
dataOutAvailable = dataOutAvailable.convert(),
dataOutMoved = dataOutMoved.ptr
)
)
return dataOutMoved.value.convert()
}
private fun CCCryptorRefVar.final(
dataOut: CValuesRef<*>,
dataOutAvailable: Int,
dataOutMoved: size_tVar,
): Int {
checkResult(
CCCryptorFinal(
cryptorRef = value,
dataOut = dataOut,
dataOutAvailable = dataOutAvailable.convert(),
dataOutMoved = dataOutMoved.ptr
)
)
return dataOutMoved.value.convert()
}
}
private fun checkResult(result: CCCryptorStatus) {
error(
when (result) {
kCCSuccess -> return
kCCParamError -> "Illegal parameter value."
kCCBufferTooSmall -> "Insufficient buffer provided for specified operation."
kCCMemoryFailure -> "Memory allocation failure."
kCCAlignmentError -> "Input size was not aligned properly."
kCCDecodeError -> "Input data did not decode or decrypt properly."
kCCUnimplemented -> "Function not implemented for the current algorithm."
else -> "CCCrypt failed with code $result"
}
)
}

View File

@@ -1,259 +0,0 @@
package fr.acinq.phoenix.security
import co.touchlab.kermit.Logger
import com.machankura.compose.AppVersion
import fr.acinq.lightning.Lightning
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.alloc
import kotlinx.cinterop.allocArrayOf
import kotlinx.cinterop.convert
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.usePinned
import kotlinx.cinterop.value
import platform.CoreFoundation.CFAutorelease
import platform.CoreFoundation.CFDictionaryAddValue
import platform.CoreFoundation.CFDictionaryCreateMutable
import platform.CoreFoundation.CFDictionaryRef
import platform.CoreFoundation.CFStringRef
import platform.CoreFoundation.CFTypeRef
import platform.CoreFoundation.CFTypeRefVar
import platform.CoreFoundation.kCFBooleanFalse
import platform.CoreFoundation.kCFBooleanTrue
import platform.Foundation.CFBridgingRelease
import platform.Foundation.CFBridgingRetain
import platform.Foundation.NSData
import platform.Foundation.NSKeyedArchiver
import platform.Foundation.NSKeyedUnarchiver
import platform.Foundation.NSNumber
import platform.Foundation.NSString
import platform.Foundation.NSUTF8StringEncoding
import platform.Foundation.create
import platform.Foundation.dataUsingEncoding
import platform.Security.SecItemAdd
import platform.Security.SecItemCopyMatching
import platform.Security.SecItemUpdate
import platform.Security.kSecAttrAccessGroup
import platform.Security.kSecAttrAccessible
import platform.Security.kSecAttrAccessibleAfterFirstUnlock
import platform.Security.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
import platform.Security.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
import platform.Security.kSecAttrAccessibleWhenUnlocked
import platform.Security.kSecAttrAccessibleWhenUnlockedThisDeviceOnly
import platform.Security.kSecAttrAccount
import platform.Security.kSecAttrService
import platform.Security.kSecClass
import platform.Security.kSecClassGenericPassword
import platform.Security.kSecMatchLimit
import platform.Security.kSecMatchLimitOne
import platform.Security.kSecReturnData
import platform.Security.kSecValueData
import platform.darwin.OSStatus
import platform.darwin.noErr
import platform.posix.memcpy
/***
* If the KVault repo was being maintained we would've probably just used that as a dependency
*
* https://github.com/Liftric/KVault
*/
@OptIn(ExperimentalForeignApi::class)
object KeyChainHelper {
private val accessibility: Accessible = Accessible.AfterFirstUnlock
/**
* kSecAttrAccessible attributes wrapper.
* attribute enables you to control item availability relative to the lock state of the device.
* It also lets you specify eligibility for restoration to a new device.
* If the attribute ends with the string ThisDeviceOnly, the item can be restored to the same device
* that created a backup, but it isnt migrated when restoring another devices backup data.
*/
enum class Accessible(val value: CFStringRef?) {
WhenPasscodeSetThisDeviceOnly(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly),
WhenUnlockedThisDeviceOnly(kSecAttrAccessibleWhenUnlockedThisDeviceOnly),
WhenUnlocked(kSecAttrAccessibleWhenUnlocked),
AfterFirstUnlock(kSecAttrAccessibleAfterFirstUnlock),
AfterFirstUnlockThisDeviceOnly(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)
}
private val log = Logger.withTag("KeyChainHelper")
/**
* Returns the data value of an object in the store.
* @param forKey The key to query
* @return The stored bytes value
*/
fun data(forKey: String): ByteArray? {
log.i("Getting byteArray for $forKey")
return value(forKey)?.toByteArray()
}
private fun value(forKey: String): NSData? = context(forKey) { (account) ->
log.i("value for $forKey")
val query = query(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to account,
kSecReturnData to kCFBooleanTrue,
kSecMatchLimit to kSecMatchLimitOne,
)
log.i("query: $query")
memScoped {
val result = alloc<CFTypeRefVar>()
SecItemCopyMatching(query, result.ptr)
CFBridgingRelease(result.value) as? NSData
}
}
fun set(key: String, dataValue: ByteArray): Boolean {
log.i("set for $key")
return addOrUpdate(key, dataValue.toNSData())
}
private fun addOrUpdate(key: String, value: NSData?): Boolean {
log.i("addOrUpdate for $key")
return if (existsObject(key)) {
update(key, value)
} else {
add(key, value)
}
}
/**
* Checks if object with the given key exists in the Keychain.
* @param forKey The key to query
* @return True or false, depending on whether it is in the Keychain or not
*/
fun existsObject(forKey: String): Boolean = context(forKey) { (account) ->
log.i("existsObject for $forKey")
val query = query(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to account,
kSecReturnData to kCFBooleanFalse,
)
log.i("query: $query")
SecItemCopyMatching(query, null)
.validate()
}
private fun add(key: String, value: NSData?): Boolean = context(key, value) { (account, data) ->
log.i("add value for $key")
val query = query(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to account,
kSecValueData to data,
kSecAttrAccessible to accessibility.value
)
log.i("query: $query")
SecItemAdd(query, null)
.validate()
}
private fun update(key: String, value: Any?): Boolean = context(key, value) { (account, data) ->
log.i("update for $key")
val query = query(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to account,
kSecReturnData to kCFBooleanFalse,
)
val updateQuery = query(
kSecValueData to data
)
SecItemUpdate(query, updateQuery)
.validate()
}
private class Context(val refs: Map<CFStringRef?, CFTypeRef?>) {
fun query(vararg pairs: Pair<CFStringRef?, CFTypeRef?>): CFDictionaryRef? {
val map = mapOf(*pairs).plus(refs.filter { it.value != null })
return CFDictionaryCreateMutable(
null, map.size.convert(), null, null
).apply {
map.entries.forEach { CFDictionaryAddValue(this, it.key, it.value) }
}.apply {
CFAutorelease(this)
}
}
}
private fun <T> context(vararg values: Any?, block: Context.(List<CFTypeRef?>) -> T): T {
val standard = mapOf(
kSecAttrService to CFBridgingRetain(AppVersion.serviceName),
kSecAttrAccessGroup to CFBridgingRetain(null)
)
val custom = arrayOf(*values).map { CFBridgingRetain(it) }
return block.invoke(Context(standard), custom).apply {
standard.values.plus(custom).forEach { CFBridgingRelease(it) }
}
}
private fun String.toNSData(): NSData? =
NSString.create(string = this).dataUsingEncoding(NSUTF8StringEncoding)
private fun NSNumber.toNSData() = NSKeyedArchiver.archivedDataWithRootObject(this)
private fun NSData.toNSNumber() = NSKeyedUnarchiver.unarchiveObjectWithData(this) as? NSNumber
private val NSData.stringValue: String?
get() = NSString.create(this, NSUTF8StringEncoding) as String?
private fun NSData.toByteArray(): ByteArray =
ByteArray(length.toInt()).apply {
if (isNotEmpty()) {
usePinned {
memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length)
}
}
}
private fun ByteArray.toNSData(): NSData =
memScoped {
NSData.create(bytes = allocArrayOf(this@toNSData), length = this@toNSData.size.convert())
}
private fun OSStatus.validate(): Boolean {
log.i("validate: ${toUInt()}")
return toUInt() == noErr
}
private fun getOrCreateKeyNoAuthRequired(): ByteArray {
log.i("getOrCreateKeyNoAuthRequired")
data(KeyStoreNames.KEY_NO_AUTH)?.let {
log.i("Key (${KeyStoreNames.KEY_NO_AUTH}) found")
return it
}
log.i("Key (${KeyStoreNames.KEY_NO_AUTH}) not found need to create new key")
// TODO: Generate secretKey using keychain
val secretKey = Lightning.randomKey().value.toByteArray()
if (set(KeyStoreNames.KEY_NO_AUTH, secretKey)) {
log.i("Successfully set key")
} else {
log.i("failed to save key")
}
return secretKey
}
private fun getKeyForName(keyName: String): ByteArray = when (keyName) {
KeyStoreNames.KEY_NO_AUTH -> getOrCreateKeyNoAuthRequired()
KeyStoreNames.KEY_FOR_PINCODE_V1 -> getOrCreateKeyNoAuthRequired()
else -> throw IllegalArgumentException("unhandled key=$keyName")
}
internal fun getEncryptionCipher(keyName: String): CCCipher {
val key = getKeyForName(keyName)
return CCCipher.phoenixCipherForKey(key)
}
internal fun getDecryptionCipher(keyName: String): CCCipher {
val key = getKeyForName(keyName)
return CCCipher.phoenixCipherForKey(key)
}
}

View File

@@ -1,25 +0,0 @@
package fr.acinq.phoenix.security
import fr.acinq.lightning.Lightning
actual fun keyStoreDecryption(
keyName: String,
iv: ByteArray,
ciphertext: ByteArray
): ByteArray = KeyChainHelper.getDecryptionCipher(
keyName = keyName,
).decrypt(
iv,
ciphertext,
0
)
actual fun keyStoreEncryption(keyName: String, plainText: ByteArray): Pair<ByteArray, ByteArray> {
val iv = Lightning.randomBytes(16)
val cipherText = KeyChainHelper.getEncryptionCipher(keyName).encrypt(
iv,
plainText
)
return Pair(iv, cipherText)
}

View File

@@ -1,31 +0,0 @@
package fr.acinq.phoenix.utils
import co.touchlab.kermit.Severity
import platform.Foundation.NSCachesDirectory
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSSearchPathForDirectoriesInDomains
import platform.Foundation.NSTemporaryDirectory
import platform.Foundation.NSUserDomainMask
actual class PlatformContext(
val logger: ((Severity, String, String) -> Unit)? = null
)
actual fun getApplicationFilesDirectoryPath(ctx: PlatformContext): String =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0] as String
actual fun getApplicationCacheDirectoryPath(ctx: PlatformContext): String =
NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, true)[0] as String
actual fun getDatabaseFilesDirectoryPath(ctx: PlatformContext): String? {
return NSFileManager.defaultManager.containerURLForSecurityApplicationGroupIdentifier(
groupIdentifier = "group.co.acinq.phoenix"
)?.URLByAppendingPathComponent(
pathComponent = "databases",
isDirectory = true
)?.path
}
actual fun getTemporaryDirectoryPath(ctx: PlatformContext): String =
NSTemporaryDirectory()

View File

@@ -1,15 +0,0 @@
package fr.acinq.phoenix.utils.extensions
import fr.acinq.phoenix.data.DecryptSeedResult
actual inline fun gracefulSingleSeedDecryption(action: () -> DecryptSeedResult): DecryptSeedResult = try {
action.invoke()
} catch (e: Throwable) {
return DecryptSeedResult.Failure.DecryptionError(e)
}
actual inline fun gracefulMultiSeedDecryption(action: () -> DecryptSeedResult): DecryptSeedResult = try {
action.invoke()
} catch (e: Exception) {
return DecryptSeedResult.Failure.DecryptionError(e)
}

View File

@@ -1,15 +0,0 @@
package fr.acinq.phoenix.utils.logger
import co.touchlab.kermit.LogWriter
import co.touchlab.kermit.NSLogWriter
import co.touchlab.kermit.OSLogWriter
import fr.acinq.phoenix.utils.PlatformContext
actual fun phoenixLogWriters(ctx: PlatformContext): List<LogWriter> {
return if (ctx.logger != null) {
listOf(NSLogWriter())
} else {
// TODO: OSLogWriter is disabled for now, as the current version of OSLogStore is buggy
listOf(OSLogWriter())
}
}