diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt index 26c7d98..c7d92f1 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt @@ -8,12 +8,11 @@ import fr.acinq.bitcoin.utils.Either import fr.acinq.bitcoin.utils.Try import fr.acinq.bitcoin.utils.toEither import fr.acinq.lightning.BuildVersions -import fr.acinq.lightning.ChannelEvents import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.NodeParams import fr.acinq.lightning.PaymentEvents import fr.acinq.lightning.bin.api.WebsocketProtocolAuthenticationProvider -import fr.acinq.lightning.bin.conf.LSP +import fr.acinq.lightning.bin.csv.WalletPaymentCsvWriter import fr.acinq.lightning.bin.db.SqlitePaymentsDb import fr.acinq.lightning.bin.db.WalletPaymentId import fr.acinq.lightning.bin.json.ApiType.* @@ -31,14 +30,13 @@ import fr.acinq.lightning.blockchain.fee.FeeratePerByte import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelCommand import fr.acinq.lightning.channel.ChannelFundingResponse -import fr.acinq.lightning.channel.LocalFundingStatus import fr.acinq.lightning.channel.states.* import fr.acinq.lightning.crypto.LocalKeyManager import fr.acinq.lightning.db.ChannelCloseOutgoingPayment -import fr.acinq.lightning.io.ChannelClosing import fr.acinq.lightning.io.Peer import fr.acinq.lightning.io.WrappedChannelCommand import fr.acinq.lightning.logging.LoggerFactory +import fr.acinq.lightning.logging.info import fr.acinq.lightning.logging.warning import fr.acinq.lightning.payment.Bolt11Invoice import fr.acinq.lightning.utils.* @@ -439,6 +437,19 @@ class Api( call.respondText(channelClose.txId.toString()) } } + post("export") { + val from = call.parameters.getOptionalLong("from") ?: 0L + val to = call.parameters.getOptionalLong("to") ?: currentTimestampMillis() + val csvPath = datadir / "exports" / "export-${currentTimestampSeconds()}.csv" + log.info { "exporting payments to $csvPath..." } + val csvWriter = WalletPaymentCsvWriter(csvPath) + paymentDb.processSuccessfulPayments(from, to) { payment -> + csvWriter.add(payment) + } + csvWriter.close() + log.info { "csv export completed" } + call.respond("payment history has been exported to $csvPath") + } } route("/websocket") { authenticate(configurations = arrayOf(null, "websocket-protocol"), strategy = AuthenticationStrategy.FirstSuccessful) { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt index fdc538b..310107b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt @@ -1,6 +1,5 @@ package fr.acinq.lightning.bin -import app.cash.sqldelight.EnumColumnAdapter import co.touchlab.kermit.CommonWriter import co.touchlab.kermit.Severity import co.touchlab.kermit.StaticConfig @@ -32,7 +31,7 @@ import fr.acinq.lightning.bin.conf.getOrGenerateSeed import fr.acinq.lightning.bin.db.SqliteChannelsDb import fr.acinq.lightning.bin.db.SqlitePaymentsDb import fr.acinq.lightning.bin.db.WalletPaymentId -import fr.acinq.lightning.bin.db.payments.LightningOutgoingQueries +import fr.acinq.lightning.bin.db.createPhoenixDb import fr.acinq.lightning.bin.json.ApiType import fr.acinq.lightning.bin.logs.FileLogWriter import fr.acinq.lightning.bin.logs.TimestampFormatter @@ -257,24 +256,7 @@ class Phoenixd : CliktCommand() { consoleLog(cyan("offer: ${nodeParams.defaultOffer(lsp.walletParams.trampolineNode.id).first}")) val driver = createAppDbDriver(datadir, chain, nodeParams.nodeId) - val database = PhoenixDatabase( - driver = driver, - lightning_outgoing_payment_partsAdapter = Lightning_outgoing_payment_parts.Adapter( - part_routeAdapter = LightningOutgoingQueries.hopDescAdapter, - part_status_typeAdapter = EnumColumnAdapter() - ), - lightning_outgoing_paymentsAdapter = Lightning_outgoing_payments.Adapter( - status_typeAdapter = EnumColumnAdapter(), - details_typeAdapter = EnumColumnAdapter() - ), - incoming_paymentsAdapter = Incoming_payments.Adapter( - origin_typeAdapter = EnumColumnAdapter(), - received_with_typeAdapter = EnumColumnAdapter() - ), - channel_close_outgoing_paymentsAdapter = Channel_close_outgoing_payments.Adapter( - closing_info_typeAdapter = EnumColumnAdapter() - ), - ) + val database = createPhoenixDb(driver) val channelsDb = SqliteChannelsDb(driver, database) val paymentsDb = SqlitePaymentsDb(database) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/csv/CsvWriter.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/csv/CsvWriter.kt new file mode 100644 index 0000000..c25568d --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/csv/CsvWriter.kt @@ -0,0 +1,43 @@ +package fr.acinq.lightning.bin.csv + +import okio.BufferedSink +import okio.FileSystem +import okio.Path +import okio.buffer + +/** + * A generic class for writing CSV files. + */ +open class CsvWriter(path: Path) { + + private val sink: BufferedSink + + init { + path.parent?.let { dir -> FileSystem.SYSTEM.createDirectories(dir) } + sink = FileSystem.SYSTEM.sink(path, mustCreate = false).buffer() + } + + fun addRow(vararg fields: String) { + val cleanFields = fields.map { processField(it) } + sink.writeUtf8(cleanFields.joinToString(separator = ",", postfix = "\n")) + } + + fun addRow(fields: List) { + addRow(*fields.toTypedArray()) + } + + private fun processField(str: String): String { + return str.findAnyOf(listOf(",", "\"", "\n"))?.let { + // - field must be enclosed in double-quotes + // - a double-quote appearing inside the field must be + // escaped by preceding it with another double quote + "\"${str.replace("\"", "\"\"")}\"" + } ?: str + } + + fun close() { + sink.flush() + sink.close() + } +} + diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/csv/WalletPaymentCsvWriter.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/csv/WalletPaymentCsvWriter.kt new file mode 100644 index 0000000..8e2da60 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/csv/WalletPaymentCsvWriter.kt @@ -0,0 +1,152 @@ +package fr.acinq.lightning.bin.csv + +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.TxId +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.db.* +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sat +import fr.acinq.lightning.utils.sum +import fr.acinq.lightning.utils.toMilliSatoshi +import kotlinx.datetime.Instant +import okio.Path + +/** + * Exports a payments db items to a csv file. + * + * The three main columns are: + * - `type`: can be any of [Type]. + * - `amount_msat`: positive or negative, will be non-zero for all types except [Type.fee_credit]. Summing this value over all rows results in the current balance. + * - `fee_credit_msat`: positive or negative, will be zero for all types except [Type.fee_credit]. Summing this value over all rows results in the current fee credit. + * + * Other columns are metadata (timestamp, payment hash, txid, fee details). + */ +class WalletPaymentCsvWriter(path: Path) : CsvWriter(path) { + + private val FIELD_DATE = "date" + private val FIELD_TYPE = "type" + private val FIELD_AMOUNT_MSAT = "amount_msat" + private val FIELD_FEE_CREDIT_MSAT = "fee_credit_msat" + private val FIELD_MINING_FEE_SAT = "mining_fee_sat" + private val FIELD_SERVICE_FEE_MSAT = "service_fee_msat" + private val FIELD_PAYMENT_HASH = "payment_hash" + private val FIELD_TX_ID = "tx_id" + + init { + addRow(FIELD_DATE, FIELD_TYPE, FIELD_AMOUNT_MSAT, FIELD_FEE_CREDIT_MSAT, FIELD_MINING_FEE_SAT, FIELD_SERVICE_FEE_MSAT, FIELD_PAYMENT_HASH, FIELD_TX_ID) + } + + @Suppress("EnumEntryName") + enum class Type { + legacy_swap_in, + legacy_swap_out, + legacy_pay_to_open, + legacy_pay_to_splice, + swap_in, + swap_out, + fee_bumping, + fee_credit, + lightning_received, + lightning_sent, + liquidity_purchase, + channel_close, + } + + data class Details( + val type: Type, + val amount: MilliSatoshi, + val feeCredit: MilliSatoshi, + val miningFee: Satoshi, + val serviceFee: MilliSatoshi, + val paymentHash: ByteVector32?, + val txId: TxId? + ) + + private fun addRow( + timestamp: Long, + details: Details + ) { + val dateStr = Instant.fromEpochMilliseconds(timestamp).toString() // ISO-8601 format + addRow( + dateStr, + details.type.toString(), + details.amount.msat.toString(), + details.feeCredit.msat.toString(), + details.miningFee.sat.toString(), + details.serviceFee.msat.toString(), + details.paymentHash?.toHex() ?: "", + details.txId?.toString() ?: "" + ) + } + + fun add(payment: WalletPayment) { + val timestamp = payment.completedAt ?: payment.createdAt + + val details: List
= when (payment) { + is IncomingPayment -> when (val origin = payment.origin) { + is IncomingPayment.Origin.Invoice -> extractLightningPaymentParts(payment) + is IncomingPayment.Origin.SwapIn -> listOf( + Details( + type = Type.legacy_swap_in, + amount = payment.amount, + feeCredit = 0.msat, + miningFee = payment.fees.truncateToSatoshi(), + serviceFee = 0.msat, + paymentHash = payment.paymentHash, + txId = null + ) + ) + is IncomingPayment.Origin.OnChain -> listOf(Details(Type.swap_in, amount = payment.amount, feeCredit = 0.msat, miningFee = payment.fees.truncateToSatoshi(), serviceFee = 0.msat, paymentHash = null, txId = origin.txId)) + is IncomingPayment.Origin.Offer -> extractLightningPaymentParts(payment) + } + + is LightningOutgoingPayment -> when (val details = payment.details) { + is LightningOutgoingPayment.Details.Normal -> listOf(Details(Type.lightning_sent, amount = -payment.amount, feeCredit = 0.msat, miningFee = 0.sat, serviceFee = payment.fees, paymentHash = payment.paymentHash, txId = null)) + is LightningOutgoingPayment.Details.SwapOut -> listOf(Details(Type.legacy_swap_out, amount = -payment.amount, feeCredit = 0.msat, miningFee = details.swapOutFee, serviceFee = 0.msat, paymentHash = null, txId = null)) + is LightningOutgoingPayment.Details.Blinded -> listOf(Details(Type.lightning_sent, amount = -payment.amount, feeCredit = 0.msat, miningFee = 0.sat, serviceFee = payment.fees, paymentHash = payment.paymentHash, txId = null)) + } + + is SpliceOutgoingPayment -> listOf(Details(Type.swap_out, amount = -payment.amount, feeCredit = 0.msat, miningFee = payment.miningFees, serviceFee = 0.msat, paymentHash = null, txId = payment.txId)) + is ChannelCloseOutgoingPayment -> listOf(Details(Type.channel_close, amount = -payment.amount, feeCredit = 0.msat, miningFee = payment.miningFees, serviceFee = 0.msat, paymentHash = null, txId = payment.txId)) + is SpliceCpfpOutgoingPayment -> listOf(Details(Type.fee_bumping, amount = -payment.amount, feeCredit = 0.msat, miningFee = payment.miningFees, serviceFee = 0.msat, paymentHash = null, txId = payment.txId)) + is InboundLiquidityOutgoingPayment -> listOf( + Details( + Type.liquidity_purchase, + amount = 0.msat, + feeCredit = -payment.feeCreditUsed, + miningFee = payment.miningFees, + serviceFee = payment.serviceFees.toMilliSatoshi(), + paymentHash = null, + txId = payment.txId + ) + ) + } + + details.forEach { addRow(timestamp, it) } + + } + + private fun extractLightningPaymentParts(payment: IncomingPayment): List
= payment.received?.receivedWith.orEmpty() + .map { + when (it) { + is IncomingPayment.ReceivedWith.LightningPayment -> Details(Type.lightning_received, amount = it.amountReceived, feeCredit = 0.msat, miningFee = 0.sat, serviceFee = 0.msat, paymentHash = payment.paymentHash, txId = null) + is IncomingPayment.ReceivedWith.AddedToFeeCredit -> Details(Type.fee_credit, amount = 0.msat, feeCredit = it.amountReceived, miningFee = 0.sat, serviceFee = 0.msat, paymentHash = payment.paymentHash, txId = null) + is IncomingPayment.ReceivedWith.NewChannel -> Details(Type.legacy_pay_to_open, amount = it.amountReceived, feeCredit = 0.msat, miningFee = it.miningFee, serviceFee = it.serviceFee, paymentHash = payment.paymentHash, txId = it.txId) + is IncomingPayment.ReceivedWith.SpliceIn -> Details(Type.legacy_pay_to_splice, amount = it.amountReceived, feeCredit = 0.msat, miningFee = it.miningFee, serviceFee = it.serviceFee, paymentHash = payment.paymentHash, txId = it.txId) + else -> error("unexpected receivedWith part $it") + } + } + .groupBy { it.type } + .values.map { parts -> + Details( + type = parts.first().type, + amount = parts.map { it.amount }.sum(), + feeCredit = parts.map { it.feeCredit }.sum(), + miningFee = parts.map { it.miningFee }.sum(), + serviceFee = parts.map { it.serviceFee }.sum(), + paymentHash = parts.first().paymentHash, + txId = parts.first().txId + ) + }.toList() +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/PhoenixDbInitHelper.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/PhoenixDbInitHelper.kt new file mode 100644 index 0000000..98b04e0 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/PhoenixDbInitHelper.kt @@ -0,0 +1,25 @@ +package fr.acinq.lightning.bin.db + +import app.cash.sqldelight.EnumColumnAdapter +import app.cash.sqldelight.db.SqlDriver +import fr.acinq.lightning.bin.db.payments.LightningOutgoingQueries +import fr.acinq.phoenix.db.* + +fun createPhoenixDb(driver: SqlDriver) = PhoenixDatabase( + driver = driver, + lightning_outgoing_payment_partsAdapter = Lightning_outgoing_payment_parts.Adapter( + part_routeAdapter = LightningOutgoingQueries.hopDescAdapter, + part_status_typeAdapter = EnumColumnAdapter() + ), + lightning_outgoing_paymentsAdapter = Lightning_outgoing_payments.Adapter( + status_typeAdapter = EnumColumnAdapter(), + details_typeAdapter = EnumColumnAdapter() + ), + incoming_paymentsAdapter = Incoming_payments.Adapter( + origin_typeAdapter = EnumColumnAdapter(), + received_with_typeAdapter = EnumColumnAdapter() + ), + channel_close_outgoing_paymentsAdapter = Channel_close_outgoing_payments.Adapter( + closing_info_typeAdapter = EnumColumnAdapter() + ), +) \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt index e081a16..dcfa1b2 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt @@ -19,16 +19,14 @@ package fr.acinq.lightning.bin.db import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto import fr.acinq.bitcoin.TxId -import fr.acinq.bitcoin.utils.Either +import fr.acinq.lightning.bin.db.csvexport.CsvExportQueries import fr.acinq.lightning.bin.db.payments.* -import fr.acinq.lightning.bin.db.payments.LinkTxToPaymentQueries -import fr.acinq.lightning.bin.db.payments.PaymentsMetadataQueries -import fr.acinq.lightning.channel.ChannelException import fr.acinq.lightning.db.* import fr.acinq.lightning.payment.FinalFailure -import fr.acinq.lightning.utils.* -import fr.acinq.lightning.wire.FailureMessage -import fr.acinq.phoenix.db.* +import fr.acinq.lightning.utils.UUID +import fr.acinq.lightning.utils.currentTimestampMillis +import fr.acinq.lightning.utils.toByteVector32 +import fr.acinq.phoenix.db.PhoenixDatabase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -42,6 +40,7 @@ class SqlitePaymentsDb(val database: PhoenixDatabase) : PaymentsDb { private val linkTxToPaymentQueries = LinkTxToPaymentQueries(database) private val inboundLiquidityQueries = InboundLiquidityQueries(database) val metadataQueries = PaymentsMetadataQueries(database) + val csvExportQueries = CsvExportQueries(database) override suspend fun addOutgoingLightningParts( parentId: UUID, @@ -289,4 +288,42 @@ class SqlitePaymentsDb(val database: PhoenixDatabase) : PaymentsDb { } } } + + private suspend fun listSuccessfulPayments(from: Long, to: Long, limit: Long, offset: Long): List { + return withContext(Dispatchers.Default) { + csvExportQueries.listSuccessfulPaymentIds(from, to, limit, offset).mapNotNull { paymentId -> + when (paymentId) { + is WalletPaymentId.IncomingPaymentId -> { + inQueries.getIncomingPayment(paymentHash = paymentId.paymentHash) + } + is WalletPaymentId.InboundLiquidityOutgoingPaymentId -> { + inboundLiquidityQueries.get(paymentId.id) + } + is WalletPaymentId.LightningOutgoingPaymentId -> { + lightningOutgoingQueries.getPayment(paymentId.id) + } + is WalletPaymentId.SpliceOutgoingPaymentId -> { + spliceOutQueries.getSpliceOutPayment(paymentId.id) + } + is WalletPaymentId.SpliceCpfpOutgoingPaymentId -> { + cpfpQueries.getCpfp(paymentId.id) + } + is WalletPaymentId.ChannelCloseOutgoingPaymentId -> { + channelCloseQueries.getChannelCloseOutgoingPayment(paymentId.id) + } + } + } + } + } + + suspend fun processSuccessfulPayments(from: Long, to: Long, batchSize: Long = 32, process: (WalletPayment) -> Unit) { + var batchOffset = 0L + var fetching = true + while (fetching) { + val results = listSuccessfulPayments(from, to, limit = batchSize, offset = batchOffset) + results.forEach { process(it) } + fetching = results.isNotEmpty() + batchOffset += results.size + } + } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/csvexport/CsvExportQueries.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/csvexport/CsvExportQueries.kt new file mode 100644 index 0000000..8926e33 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/csvexport/CsvExportQueries.kt @@ -0,0 +1,14 @@ +package fr.acinq.lightning.bin.db.csvexport + +import fr.acinq.lightning.bin.db.WalletPaymentId +import fr.acinq.phoenix.db.PhoenixDatabase + +class CsvExportQueries(val database: PhoenixDatabase) { + private val csvExportQueries = database.csvExportQueries + + fun listSuccessfulPaymentIds(from: Long, to: Long, limit: Long, offset: Long): List { + return csvExportQueries.listSuccessfulPaymentIds(startDate = from, endDate = to, limit = limit, offset = offset).executeAsList().mapNotNull { + WalletPaymentId.create(it.type, it.id) + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt b/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt index c4a85db..767d7da 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt @@ -67,6 +67,7 @@ fun main(args: Array) = SendToAddress(), BumpFee(), CloseChannel(), + ExportCsv() ) .main(args) @@ -325,9 +326,9 @@ class LnurlPay : PhoenixCliCommand(name = "lnurlpay", help = "Pay a LNURL", prin private val amountSat by option("--amountSat").long() private val lnurl by option("--lnurl").required() .check("not a valid lnurl-pay link") { - val url = kotlin.runCatching { LnurlParser.extractLnurl(it) }.getOrNull() - url is Lnurl.Request && (url.tag == Lnurl.Tag.Pay || url.tag == null) - } + val url = kotlin.runCatching { LnurlParser.extractLnurl(it) }.getOrNull() + url is Lnurl.Request && (url.tag == Lnurl.Tag.Pay || url.tag == null) + } private val message by option("--message").help { "Optional comment" } override suspend fun httpRequest(): HttpResponse = commonOptions.httpClient.use { it.submitForm( @@ -419,6 +420,20 @@ class CloseChannel : PhoenixCliCommand(name = "closechannel", help = "Close chan } } +class ExportCsv : PhoenixCliCommand(name = "exportcsv", help = "Export transactions to a csv file") { + private val from by option("--from").long().help { "start timestamp in millis since epoch" } + private val to by option("--to").long().help { "end timestamp in millis since epoch" } + override suspend fun httpRequest() = commonOptions.httpClient.use { + it.submitForm( + url = (commonOptions.baseUrl / "export").toString(), + formParameters = parameters { + from?.let { append("from", it.toString()) } + to?.let { append("to", it.toString()) } + } + ) + } +} + operator fun Url.div(path: String) = Url(URLBuilder(this).appendPathSegments(path)) fun String.toByteVector32(): ByteVector32 = kotlin.runCatching { ByteVector32.fromValidHex(this) }.recover { error("'$this' is not a valid 32-bytes hex string") }.getOrThrow() \ No newline at end of file diff --git a/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/CsvExport.sq b/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/CsvExport.sq new file mode 100644 index 0000000..bf0a998 --- /dev/null +++ b/src/commonMain/sqldelight/phoenixdb/fr/acinq/phoenix/db/CsvExport.sq @@ -0,0 +1,63 @@ +listSuccessfulPaymentIds: +SELECT + combined_payments.type AS type, + combined_payments.id AS id, + combined_payments.created_at AS created_at, + combined_payments.completed_at AS completed_at +FROM ( + SELECT + 2 AS type, + id AS id, + created_at AS created_at, + completed_at AS completed_at + FROM lightning_outgoing_payments + WHERE lightning_outgoing_payments.status_type LIKE 'SUCCEEDED_%' + AND lightning_outgoing_payments.completed_at BETWEEN :startDate AND :endDate +UNION ALL + SELECT + 3 AS type, + id AS id, + created_at AS created_at, + locked_at AS completed_at + FROM splice_outgoing_payments + WHERE splice_outgoing_payments.locked_at IS NOT NULL + AND splice_outgoing_payments.locked_at BETWEEN :startDate AND :endDate +UNION ALL + SELECT + 4 AS type, + id AS id, + created_at AS created_at, + locked_at AS completed_at + FROM channel_close_outgoing_payments + WHERE channel_close_outgoing_payments.locked_at IS NOT NULL + AND channel_close_outgoing_payments.locked_at BETWEEN :startDate AND :endDate +UNION ALL + SELECT + 5 AS type, + id AS id, + created_at AS created_at, + locked_at AS completed_at + FROM splice_cpfp_outgoing_payments + WHERE splice_cpfp_outgoing_payments.locked_at IS NOT NULL + AND splice_cpfp_outgoing_payments.locked_at BETWEEN :startDate AND :endDate +UNION ALL + SELECT + 6 AS type, + id AS id, + created_at AS created_at, + locked_at AS completed_at + FROM inbound_liquidity_outgoing_payments + WHERE inbound_liquidity_outgoing_payments.locked_at IS NOT NULL + AND inbound_liquidity_outgoing_payments.locked_at BETWEEN :startDate AND :endDate +UNION ALL + SELECT + 1 AS type, + lower(hex(payment_hash)) AS id, + created_at AS created_at, + received_at AS completed_at + FROM incoming_payments + WHERE incoming_payments.received_at BETWEEN :startDate AND :endDate + AND incoming_payments.received_with_blob IS NOT NULL +) combined_payments +ORDER BY COALESCE(combined_payments.completed_at, combined_payments.created_at) +LIMIT :limit OFFSET :offset; diff --git a/src/commonTest/kotlin/fr/acinq/lightning/bin/db/CsvExportTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/bin/db/CsvExportTestsCommon.kt new file mode 100644 index 0000000..503c57c --- /dev/null +++ b/src/commonTest/kotlin/fr/acinq/lightning/bin/db/CsvExportTestsCommon.kt @@ -0,0 +1,32 @@ +package fr.acinq.lightning.bin.db + +import fr.acinq.bitcoin.Chain +import fr.acinq.bitcoin.PublicKey +import fr.acinq.lightning.bin.createAppDbDriver +import fr.acinq.lightning.bin.datadir +import fr.acinq.lightning.bin.csv.WalletPaymentCsvWriter +import fr.acinq.lightning.utils.currentTimestampMillis +import kotlinx.coroutines.runBlocking +import okio.Path.Companion.toPath +import kotlin.test.Ignore +import kotlin.test.Test + +class CsvExportTestsCommon { + + @Test + @Ignore + fun `export to csv`() { + val driver = createAppDbDriver(datadir, Chain.Testnet3, PublicKey.fromHex("0211dadf19b1268f1f21b0b233e22c4f648d419e2476bfd8fe356479fbad5c146d")) + val database = createPhoenixDb(driver) + val paymentsDb = SqlitePaymentsDb(database) + val csvWriter = WalletPaymentCsvWriter("csv/export.csv".toPath()) + runBlocking { + paymentsDb.processSuccessfulPayments(0, currentTimestampMillis()) { payment -> + csvWriter.add(payment) + } + } + csvWriter.close() + } + + +} \ No newline at end of file