Skip to content

Commit 35fa625

Browse files
authored
Store incoming amounts to payments database (#230)
The `StoreIncomingAmount` action is saved in the payments database as an `IncomingPayment` object. The payment database has a new method to allow insertion of spontaneous payments, such as swap-ins. We use the channel origin to set the incoming payment's `receivedWith` attribute. If channel origin is not defined, the payment origin is a swap-in.
1 parent f8babed commit 35fa625

File tree

4 files changed

+66
-6
lines changed

4 files changed

+66
-6
lines changed

src/commonMain/kotlin/fr/acinq/eclair/db/InMemoryPaymentsDb.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ class InMemoryPaymentsDb : PaymentsDb {
3131
}
3232
}
3333

34+
override suspend fun addAndReceivePayment(preimage: ByteVector32, origin: IncomingPayment.Origin, amount: MilliSatoshi, receivedWith: IncomingPayment.ReceivedWith, createdAt: Long, receivedAt: Long) {
35+
val paymentHash = preimage.sha256()
36+
incoming[paymentHash] = IncomingPayment(preimage, origin, IncomingPayment.Received(amount, receivedWith, receivedAt), createdAt)
37+
}
38+
3439
override suspend fun listReceivedPayments(count: Int, skip: Int, filters: Set<PaymentTypeFilter>): List<IncomingPayment> =
3540
incoming.values
3641
.asSequence()

src/commonMain/kotlin/fr/acinq/eclair/db/PaymentsDb.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ interface IncomingPaymentsDb {
2929
*/
3030
suspend fun receivePayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedWith: IncomingPayment.ReceivedWith, receivedAt: Long = currentTimestampMillis())
3131

32+
/** Add and receive a payment. Use this method when receiving a spontaneous payment, for example a swap-in payment. */
33+
suspend fun addAndReceivePayment(preimage: ByteVector32, origin: IncomingPayment.Origin, amount: MilliSatoshi, receivedWith: IncomingPayment.ReceivedWith, createdAt: Long = currentTimestampMillis(), receivedAt: Long = currentTimestampMillis())
34+
3235
/** List received payments (with most recent payments first). */
3336
suspend fun listReceivedPayments(count: Int, skip: Int, filters: Set<PaymentTypeFilter> = setOf()): List<IncomingPayment>
3437
}
@@ -115,8 +118,8 @@ data class IncomingPayment(val preimage: ByteVector32, val origin: Origin, val r
115118
/** KeySend payments are spontaneous donations for which we didn't create an invoice. */
116119
object KeySend : Origin()
117120

118-
/** Swap-in works by sending an on-chain transaction to a swap server, which will pay us in exchange. */
119-
data class SwapIn(val amount: MilliSatoshi, val address: String, val paymentRequest: PaymentRequest?) : Origin()
121+
/** Swap-in works by sending an on-chain transaction to a swap server, which will pay us in exchange. We may not know the origin address. */
122+
data class SwapIn(val address: String?) : Origin()
120123

121124
fun matchesFilters(filters: Set<PaymentTypeFilter>): Boolean = when (this) {
122125
is Invoice -> filters.isEmpty() || filters.contains(PaymentTypeFilter.Normal)
@@ -132,7 +135,7 @@ data class IncomingPayment(val preimage: ByteVector32, val origin: Origin, val r
132135

133136
/** Payment was received via existing lightning channels. */
134137
object LightningPayment : ReceivedWith() {
135-
override val fees: MilliSatoshi = 0.msat
138+
override val fees: MilliSatoshi = 0.msat // with Lightning, the fee is paid by the sender
136139
}
137140

138141
/** Payment was received via a new channel opened to us. */

src/commonMain/kotlin/fr/acinq/eclair/io/Peer.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ class Peer(
304304

305305
fun openListenerEventSubscription() = listenerEventChannel.openSubscription()
306306

307-
private suspend fun sendToPeer(msg: LightningMessage) {
307+
suspend fun sendToPeer(msg: LightningMessage) {
308308
val encoded = LightningMessage.encode(msg)
309309
// Avoids polluting the logs with pongs
310310
if (msg !is Pong) logger.info { "n:$remoteNodeId sending $msg" }
@@ -367,6 +367,40 @@ class Peer(
367367
action is ChannelAction.Storage.StoreHtlcInfos -> {
368368
action.htlcs.forEach { db.channels.addHtlcInfo(actualChannelId, it.commitmentNumber, it.paymentHash, it.cltvExpiry) }
369369
}
370+
action is ChannelAction.Storage.StoreIncomingAmount -> {
371+
logger.info { "storing incoming amount=${action.amount} with origin=${action.origin}" }
372+
when (action.origin) {
373+
null -> {
374+
logger.warning { "n:$remoteNodeId c:$actualChannelId incoming amount with empty origin, store minimal information" }
375+
val fakePreimage = actualChannelId.sha256()
376+
db.payments.addAndReceivePayment(
377+
preimage = fakePreimage,
378+
origin = IncomingPayment.Origin.SwapIn(address = ""),
379+
amount = action.amount,
380+
receivedWith = IncomingPayment.ReceivedWith.NewChannel(fees = 0.msat, channelId = actualChannelId)
381+
)
382+
}
383+
is ChannelOrigin.PayToOpenOrigin -> {
384+
if (db.payments.getIncomingPayment(action.origin.paymentHash) != null) {
385+
db.payments.receivePayment(paymentHash = action.origin.paymentHash, amount = action.amount, receivedWith = IncomingPayment.ReceivedWith.NewChannel(
386+
fees = action.origin.fee.toMilliSatoshi(),
387+
channelId = actualChannelId
388+
))
389+
} else {
390+
logger.warning { "n:$remoteNodeId c:$actualChannelId ignored pay-to-open storage, no payments in db for hash=${action.origin.paymentHash}" }
391+
}
392+
}
393+
is ChannelOrigin.SwapInOrigin -> {
394+
val fakePreimage = actualChannelId.sha256()
395+
db.payments.addAndReceivePayment(
396+
preimage = fakePreimage,
397+
origin = IncomingPayment.Origin.SwapIn(address = action.origin.bitcoinAddress),
398+
amount = action.amount,
399+
receivedWith = IncomingPayment.ReceivedWith.NewChannel(fees = action.origin.fee.toMilliSatoshi(), channelId = actualChannelId)
400+
)
401+
}
402+
}
403+
}
370404
action is ChannelAction.Storage.GetHtlcInfos -> {
371405
val htlcInfos = db.channels.listHtlcInfos(actualChannelId, action.commitmentNumber).map { ChannelAction.Storage.HtlcInfo(actualChannelId, action.commitmentNumber, it.first, it.second) }
372406
input.send(WrappedChannelEvent(actualChannelId, ChannelEvent.GetHtlcInfosResponse(action.revokedCommitTxId, htlcInfos)))

src/commonTest/kotlin/fr/acinq/eclair/db/PaymentsDbTestsCommon.kt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ class PaymentsDbTestsCommon : EclairTestSuite() {
3434
assertEquals(pending.copy(received = IncomingPayment.Received(200_000.msat, IncomingPayment.ReceivedWith.LightningPayment, 110)), received)
3535
}
3636

37+
@Test
38+
fun `add and receive incoming payments`() = runSuspendTest {
39+
val db = InMemoryPaymentsDb()
40+
val preimage = randomBytes32()
41+
val channelId = randomBytes32()
42+
val amount = MilliSatoshi(50_000_000)
43+
val origin = IncomingPayment.Origin.SwapIn("1PwLgmRdDjy5GAKWyp8eyAC4SFzWuboLLb")
44+
val receivedWith = IncomingPayment.ReceivedWith.NewChannel(fees = MilliSatoshi(1234), channelId = channelId)
45+
assertNull(db.getIncomingPayment(randomBytes32()))
46+
47+
db.addAndReceivePayment(preimage = preimage, origin = origin, amount = amount, receivedWith = receivedWith)
48+
val payment = db.getIncomingPayment(Crypto.sha256(preimage).toByteVector32())
49+
assertNotNull(payment)
50+
assertEquals(origin, payment.origin)
51+
assertNotNull(payment.received)
52+
assertEquals(receivedWith, payment.received?.receivedWith)
53+
}
54+
3755
@Test
3856
fun `reject duplicate payment hash`() = runSuspendTest {
3957
val (db, preimage, pr) = createFixture()
@@ -73,7 +91,7 @@ class PaymentsDbTestsCommon : EclairTestSuite() {
7391

7492
val preimage2 = randomBytes32()
7593
val received2 = createInvoice(preimage2)
76-
db.addIncomingPayment(preimage2, IncomingPayment.Origin.SwapIn(150_000.msat, "1PwLgmRdDjy5GAKWyp8eyAC4SFzWuboLLb", received2))
94+
db.addIncomingPayment(preimage2, IncomingPayment.Origin.SwapIn("1PwLgmRdDjy5GAKWyp8eyAC4SFzWuboLLb"))
7795
db.receivePayment(received2.paymentHash, 180_000.msat, IncomingPayment.ReceivedWith.NewChannel(100.msat, channelId = null), 60)
7896
val payment2 = db.getIncomingPayment(received2.paymentHash)!!
7997

@@ -279,7 +297,7 @@ class PaymentsDbTestsCommon : EclairTestSuite() {
279297

280298
val (preimage1, preimage2, preimage3, preimage4) = listOf(randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32())
281299
val incoming1 = IncomingPayment(preimage1, IncomingPayment.Origin.Invoice(createInvoice(preimage1)), null, createdAt = 20)
282-
val incoming2 = IncomingPayment(preimage2, IncomingPayment.Origin.SwapIn(20_000.msat, "1PwLgmRdDjy5GAKWyp8eyAC4SFzWuboLLb", null), null, createdAt = 21)
300+
val incoming2 = IncomingPayment(preimage2, IncomingPayment.Origin.SwapIn("1PwLgmRdDjy5GAKWyp8eyAC4SFzWuboLLb"), null, createdAt = 21)
283301
val incoming3 = IncomingPayment(preimage3, IncomingPayment.Origin.Invoice(createInvoice(preimage3)), null, createdAt = 22)
284302
val incoming4 = IncomingPayment(preimage4, IncomingPayment.Origin.Invoice(createInvoice(preimage4)), null, createdAt = 23)
285303
listOf(incoming1, incoming2, incoming3, incoming4).forEach { db.addIncomingPayment(it.preimage, it.origin, it.createdAt) }

0 commit comments

Comments
 (0)