bugfix: properly exclude self-payments when computing amount received via payments

This commit is contained in:
Carsten Otto
2022-08-28 15:33:54 +02:00
parent 42205e18c3
commit e0138c6e81
10 changed files with 138 additions and 111 deletions

View File

@@ -76,10 +76,8 @@ public class FlowService {
Coins rebalanceSupportReceived = rebalanceService.getSupportAsTargetAmountToChannel(channelId, maxAge);
Coins rebalanceFeesSent = rebalanceService.getSourceCostsForChannel(channelId, maxAge);
Coins rebalanceSupportFeesSent = rebalanceService.getSupportAsSourceCostsFromChannel(channelId, maxAge);
Coins receivedViaPayments = settledInvoicesService.getAmountReceivedViaChannel(channelId, maxAge)
.subtract(rebalanceReceived)
.subtract(rebalanceSupportReceived)
.maximum(Coins.NONE);
Coins receivedViaPayments =
settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(channelId, maxAge);
return new FlowReport(
forwardedSent,

View File

@@ -16,8 +16,8 @@ public class SettledInvoicesService {
this.dao = dao;
}
public Coins getAmountReceivedViaChannel(ChannelId channelId, Duration maxAge) {
return dao.getInvoicesPaidVia(channelId, maxAge).stream()
public Coins getAmountReceivedViaChannelWithoutSelfPayments(ChannelId channelId, Duration maxAge) {
return dao.getInvoicesWithoutSelfPaymentsPaidVia(channelId, maxAge).stream()
.map(SettledInvoice::receivedVia)
.map(receivedVia -> receivedVia.getOrDefault(channelId, Coins.NONE))
.reduce(Coins.NONE, Coins::add);

View File

@@ -60,7 +60,8 @@ class FlowServiceTest {
lenient().when(rebalanceService.getSourceCostsForChannel(any())).thenReturn(Coins.NONE);
lenient().when(rebalanceService.getSourceCostsForChannel(any(), any())).thenReturn(Coins.NONE);
lenient().when(rebalanceService.getSupportAsSourceCostsFromChannel(any(), any())).thenReturn(Coins.NONE);
lenient().when(settledInvoicesService.getAmountReceivedViaChannel(any(), any())).thenReturn(Coins.NONE);
lenient().when(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(any(), any()))
.thenReturn(Coins.NONE);
}
@Test
@@ -79,9 +80,9 @@ class FlowServiceTest {
mockRebalanceFromTo(DEFAULT_MAX_AGE, CHANNEL_ID_2, 3, 4);
mockRebalanceSupportFromTo(DEFAULT_MAX_AGE, CHANNEL_ID, 555, 6);
mockRebalanceSupportFromTo(DEFAULT_MAX_AGE, CHANNEL_ID_2, 7, 888);
when(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID, DEFAULT_MAX_AGE))
when(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(CHANNEL_ID, DEFAULT_MAX_AGE))
.thenReturn(Coins.ofMilliSatoshis(1_500_000));
when(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID_2, DEFAULT_MAX_AGE))
when(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(CHANNEL_ID_2, DEFAULT_MAX_AGE))
.thenReturn(Coins.ofMilliSatoshis(915_000));
FlowReport flowReport = new FlowReport(
Coins.ofSatoshis(1_000 + 50),
@@ -93,7 +94,7 @@ class FlowServiceTest {
Coins.ofSatoshis(555 + 7),
Coins.ofMilliSatoshis(555 + 7),
Coins.ofSatoshis(6 + 888),
Coins.ofMilliSatoshis(1_500_000 + 915_000 - 2_000 - 4_000 - 6_000 - 888_000)
Coins.ofMilliSatoshis(1_500_000 + 915_000)
);
assertThat(flowService.getFlowReportForPeer(PUBKEY)).isEqualTo(flowReport);
verify(forwardingEventsService).getEventsWithOutgoingChannel(CHANNEL_ID, DEFAULT_MAX_AGE);
@@ -113,9 +114,9 @@ class FlowServiceTest {
mockRebalanceFromTo(maxAge, CHANNEL_ID_2, 333, 4);
mockRebalanceSupportFromTo(maxAge, CHANNEL_ID, 5, 6);
mockRebalanceSupportFromTo(maxAge, CHANNEL_ID_2, 7, 8);
when(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID, maxAge))
when(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(CHANNEL_ID, maxAge))
.thenReturn(Coins.ofMilliSatoshis(1_500_000));
when(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID_2, maxAge))
when(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(CHANNEL_ID_2, maxAge))
.thenReturn(Coins.ofMilliSatoshis(15_000));
when(channelService.getAllChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, CLOSED_CHANNEL_2));
FlowReport flowReport = new FlowReport(
@@ -128,7 +129,7 @@ class FlowServiceTest {
Coins.ofSatoshis(5 + 7),
Coins.ofMilliSatoshis(5 + 7),
Coins.ofSatoshis(6 + 8),
Coins.ofMilliSatoshis(1_500_000 + 15_000 - 2_000 - 4_000 - 6_000 - 8_000)
Coins.ofMilliSatoshis(1_500_000 + 15_000)
);
assertThat(flowService.getFlowReportForPeer(PUBKEY, maxAge)).isEqualTo(flowReport);
}
@@ -139,7 +140,7 @@ class FlowServiceTest {
mockReceived(DEFAULT_MAX_AGE, CHANNEL_ID, 9_001L);
mockRebalanceFromTo(DEFAULT_MAX_AGE, CHANNEL_ID, 100, 200);
mockRebalanceSupportFromTo(DEFAULT_MAX_AGE, CHANNEL_ID, 5, 6);
when(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID, DEFAULT_MAX_AGE))
when(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(CHANNEL_ID, DEFAULT_MAX_AGE))
.thenReturn(Coins.ofMilliSatoshis(1_500_000));
FlowReport flowReport = new FlowReport(
Coins.ofSatoshis(1_050),
@@ -151,7 +152,7 @@ class FlowServiceTest {
Coins.ofSatoshis(5),
Coins.ofMilliSatoshis(5),
Coins.ofSatoshis(6),
Coins.ofMilliSatoshis(1_500_000 - 200_000 - 6_000)
Coins.ofMilliSatoshis(1_500_000)
);
assertThat(flowService.getFlowReportForChannel(CHANNEL_ID)).isEqualTo(flowReport);
verify(forwardingEventsService).getEventsWithOutgoingChannel(CHANNEL_ID, DEFAULT_MAX_AGE);
@@ -165,7 +166,7 @@ class FlowServiceTest {
mockReceived(maxAge, CHANNEL_ID, 1_000L, 8_001L);
mockRebalanceFromTo(maxAge, CHANNEL_ID, 101, 201);
mockRebalanceSupportFromTo(maxAge, CHANNEL_ID, 5, 6);
when(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID, maxAge))
when(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(CHANNEL_ID, maxAge))
.thenReturn(Coins.ofMilliSatoshis(1_500_000));
FlowReport flowReport = new FlowReport(
Coins.ofSatoshis(1_000 + 50),
@@ -177,7 +178,7 @@ class FlowServiceTest {
Coins.ofSatoshis(5),
Coins.ofMilliSatoshis(5),
Coins.ofSatoshis(6),
Coins.ofMilliSatoshis(1_500_000 - 201_000 - 6_000)
Coins.ofMilliSatoshis(1_500_000)
);
assertThat(flowService.getFlowReportForChannel(CHANNEL_ID, maxAge)).isEqualTo(flowReport);
}
@@ -185,7 +186,7 @@ class FlowServiceTest {
@Test
void getFlowReportForChannel_includes_receivedViaPayments() {
Coins expectedReceivedViaPayments = Coins.ofMilliSatoshis(1);
when(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID, DEFAULT_MAX_AGE))
when(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(CHANNEL_ID, DEFAULT_MAX_AGE))
.thenReturn(expectedReceivedViaPayments);
assertThat(flowService.getFlowReportForChannel(CHANNEL_ID).receivedViaPayments())
.isEqualTo(expectedReceivedViaPayments);
@@ -196,24 +197,12 @@ class FlowServiceTest {
Coins expectedReceivedViaPayments = Coins.NONE;
Coins expectedRebalanceReceived = Coins.ofSatoshis(1);
mockRebalanceFromTo(DEFAULT_MAX_AGE, CHANNEL_ID, 0, expectedRebalanceReceived.satoshis());
when(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID, DEFAULT_MAX_AGE))
when(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(CHANNEL_ID, DEFAULT_MAX_AGE))
.thenReturn(expectedReceivedViaPayments);
assertThat(flowService.getFlowReportForChannel(CHANNEL_ID).receivedViaPayments())
.isEqualTo(expectedReceivedViaPayments);
}
@Test
void getFlowReportForChannel_self_payments_do_not_count_for_receivedViaPayments() {
Coins expectedReceivedViaPayments = Coins.ofSatoshis(3);
Coins expectedRebalanceReceived = Coins.ofSatoshis(1);
mockRebalanceFromTo(DEFAULT_MAX_AGE, CHANNEL_ID, 0, expectedRebalanceReceived.satoshis());
when(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID, DEFAULT_MAX_AGE))
.thenReturn(expectedReceivedViaPayments.add(expectedRebalanceReceived));
assertThat(flowService.getFlowReportForChannel(CHANNEL_ID).receivedViaPayments())
.isEqualTo(expectedReceivedViaPayments);
}
private void mockReceived(Duration maxAge, ChannelId channelId, Long... amounts) {
List<ForwardingEvent> events = createListOfEvents(channelId, CHANNEL_ID_2, amounts);
when(forwardingEventsService.getEventsWithIncomingChannel(channelId, maxAge)).thenReturn(events);

View File

@@ -31,30 +31,31 @@ class SettledInvoicesServiceTest {
private SettledInvoicesDao dao;
@Test
void getAmountReceivedViaChannel_no_settled_invoices() {
assertThat(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID, MAX_AGE))
void getAmountReceivedViaChannelWithoutSelfPayments_no_settled_invoices() {
assertThat(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(CHANNEL_ID, MAX_AGE))
.isEqualTo(Coins.NONE);
}
@Test
void getAmountReceivedViaChannel() {
void getAmountReceivedViaChannelWithoutSelfPayments() {
Coins amountReceivedPerInvoice = Coins.ofMilliSatoshis(CHANNEL_ID.getShortChannelId());
when(dao.getInvoicesPaidVia(CHANNEL_ID, MAX_AGE)).thenReturn(List.of(SETTLED_INVOICE));
assertThat(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID, MAX_AGE))
when(dao.getInvoicesWithoutSelfPaymentsPaidVia(CHANNEL_ID, MAX_AGE)).thenReturn(List.of(SETTLED_INVOICE));
assertThat(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(CHANNEL_ID, MAX_AGE))
.isEqualTo(amountReceivedPerInvoice);
}
@Test
void getAmountReceivedViaChannel_two_invoices() {
void getAmountReceivedViaChannelWithoutSelfPayments_two_invoices() {
Coins amountReceivedPerInvoice = Coins.ofMilliSatoshis(CHANNEL_ID.getShortChannelId());
Coins expected = amountReceivedPerInvoice.add(amountReceivedPerInvoice);
when(dao.getInvoicesPaidVia(CHANNEL_ID, MAX_AGE)).thenReturn(List.of(SETTLED_INVOICE, SETTLED_INVOICE_2));
assertThat(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID, MAX_AGE))
when(dao.getInvoicesWithoutSelfPaymentsPaidVia(CHANNEL_ID, MAX_AGE))
.thenReturn(List.of(SETTLED_INVOICE, SETTLED_INVOICE_2));
assertThat(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(CHANNEL_ID, MAX_AGE))
.isEqualTo(expected);
}
@Test
void getAmountReceivedViaChannel_invoice_paid_via_two_channels() {
void getAmountReceivedViaChannelWithoutSelfPayments_invoice_paid_via_two_channels() {
Coins oneMilliSatoshi = Coins.ofMilliSatoshis(1);
SettledInvoice invoice = new SettledInvoice(
SETTLED_INVOICE.addIndex(),
@@ -66,8 +67,8 @@ class SettledInvoicesServiceTest {
SETTLED_INVOICE.keysendMessage(),
Map.of(CHANNEL_ID, oneMilliSatoshi, CHANNEL_ID_2, AMOUNT_PAID.subtract(oneMilliSatoshi))
);
when(dao.getInvoicesPaidVia(CHANNEL_ID, MAX_AGE)).thenReturn(List.of(invoice));
assertThat(settledInvoicesService.getAmountReceivedViaChannel(CHANNEL_ID, MAX_AGE))
when(dao.getInvoicesWithoutSelfPaymentsPaidVia(CHANNEL_ID, MAX_AGE)).thenReturn(List.of(invoice));
assertThat(settledInvoicesService.getAmountReceivedViaChannelWithoutSelfPayments(CHANNEL_ID, MAX_AGE))
.isEqualTo(oneMilliSatoshi);
}
}

View File

@@ -8,8 +8,10 @@ dependencies {
implementation project(':model')
implementation project(':caching')
implementation project(':grpc-adapter')
implementation project(':payments')
testFixturesApi testFixtures(project(':model'))
integrationTestRuntimeOnly 'com.h2database:h2'
integrationTestImplementation project(':payments')
integrationTestImplementation 'org.springframework.boot:spring-boot-starter-data-jpa'
integrationTestImplementation testFixtures(project(':model'))
}

View File

@@ -2,7 +2,11 @@ package de.cotto.lndmanagej.invoices.persistence;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.model.Payment;
import de.cotto.lndmanagej.model.SettledInvoice;
import de.cotto.lndmanagej.payments.persistence.PaymentJpaDto;
import de.cotto.lndmanagej.payments.persistence.PaymentsRepository;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@@ -23,6 +27,9 @@ class SettledInvoicesRepositoryIT {
@Autowired
private SettledInvoicesRepository repository;
@Autowired
private PaymentsRepository paymentsRepository;
@Test
void getMaxSettledIndexWithoutGaps_no_invoice() {
assertThat(repository.getMaxSettledIndexWithoutGaps()).isEqualTo(0);
@@ -61,70 +68,87 @@ class SettledInvoicesRepositoryIT {
assertThat(repository.getMaxAddIndex()).isEqualTo(3L);
}
@Test
void findAllByReceivedViaChannelIdAndSettleDateAfter_no_invoice() {
assertThat(repository.findAllByReceivedViaChannelIdAndSettleDateAfter(CHANNEL_ID.getShortChannelId(), 0))
.isEmpty();
}
@Nested
class GetInvoicesWithoutSelfPaymentsPaidVia {
@Test
void no_invoice() {
assertThat(repository.getInvoicesWithoutSelfPaymentsPaidVia(CHANNEL_ID.getShortChannelId(), 0))
.isEmpty();
}
@Test
void findAllByReceivedViaChannelIdAndSettleDateAfter_wrong_channel_id() {
repository.save(invoice(1, 1, Map.of(CHANNEL_ID, Coins.ofSatoshis(100))));
assertThat(repository.findAllByReceivedViaChannelIdAndSettleDateAfter(CHANNEL_ID_2.getShortChannelId(), 0))
.isEmpty();
}
@Test
void wrong_channel_id() {
repository.save(invoice(1, 1, Map.of(CHANNEL_ID, Coins.ofSatoshis(100))));
assertThat(repository.getInvoicesWithoutSelfPaymentsPaidVia(CHANNEL_ID_2.getShortChannelId(), 0))
.isEmpty();
}
@Test
void findAllByReceivedViaChannelIdAndSettleDateAfter_expected_channel_id() {
Map<ChannelId, Coins> expected = Map.of(CHANNEL_ID, Coins.ofSatoshis(100));
repository.save(invoice(1, 1, expected));
assertReceivedVia(CHANNEL_ID, List.of(expected));
}
@Test
void expected_channel_id() {
Map<ChannelId, Coins> expected = Map.of(CHANNEL_ID, Coins.ofSatoshis(100));
repository.save(invoice(1, 1, expected));
assertReceivedVia(CHANNEL_ID, List.of(expected));
}
@Test
void findAllByReceivedViaChannelIdAndSettleDateAfter_just_within_max_age() {
Map<ChannelId, Coins> tooOld = Map.of(CHANNEL_ID, Coins.ofSatoshis(100));
SettledInvoiceJpaDto invoice = invoice(1, 1, tooOld);
repository.save(invoice);
long timestamp = invoice.getSettleDate() - 1;
List<SettledInvoiceJpaDto> invoices =
repository.findAllByReceivedViaChannelIdAndSettleDateAfter(CHANNEL_ID.getShortChannelId(), timestamp);
assertThat(invoices).isNotEmpty();
}
@Test
void just_within_max_age() {
Map<ChannelId, Coins> tooOld = Map.of(CHANNEL_ID, Coins.ofSatoshis(100));
SettledInvoiceJpaDto invoice = invoice(1, 1, tooOld);
repository.save(invoice);
long timestamp = invoice.getSettleDate() - 1;
List<SettledInvoiceJpaDto> invoices =
repository.getInvoicesWithoutSelfPaymentsPaidVia(CHANNEL_ID.getShortChannelId(), timestamp);
assertThat(invoices).isNotEmpty();
}
@Test
void findAllByReceivedViaChannelIdAndSettleDateAfter_expected_channel_id_but_too_old() {
Map<ChannelId, Coins> tooOld = Map.of(CHANNEL_ID, Coins.ofSatoshis(100));
SettledInvoiceJpaDto invoice = invoice(1, 1, tooOld);
repository.save(invoice);
long timestamp = invoice.getSettleDate();
List<SettledInvoiceJpaDto> invoices =
repository.findAllByReceivedViaChannelIdAndSettleDateAfter(CHANNEL_ID.getShortChannelId(), timestamp);
assertThat(invoices).isEmpty();
}
@Test
void too_old() {
Map<ChannelId, Coins> tooOld = Map.of(CHANNEL_ID, Coins.ofSatoshis(100));
SettledInvoiceJpaDto invoice = invoice(1, 1, tooOld);
repository.save(invoice);
long timestamp = invoice.getSettleDate();
List<SettledInvoiceJpaDto> invoices =
repository.getInvoicesWithoutSelfPaymentsPaidVia(CHANNEL_ID.getShortChannelId(), timestamp);
assertThat(invoices).isEmpty();
}
@Test
void findAllByReceivedViaChannelIdAndSettleDateAfter_one_of_two_channels_matches() {
Map<ChannelId, Coins> expected = Map.of(CHANNEL_ID, Coins.ofSatoshis(100), CHANNEL_ID_2, Coins.ofSatoshis(50));
repository.save(invoice(1, 1, expected));
assertReceivedVia(CHANNEL_ID, List.of(expected));
}
@Test
void self_payment() {
Map<ChannelId, Coins> receivedVia = Map.of(CHANNEL_ID, Coins.ofSatoshis(100));
SettledInvoiceJpaDto invoice = invoice(1, 1, receivedVia);
paymentsRepository.save(payment(invoice.getHash()));
repository.save(invoice);
List<SettledInvoiceJpaDto> invoices =
repository.getInvoicesWithoutSelfPaymentsPaidVia(CHANNEL_ID.getShortChannelId(), 0);
assertThat(invoices).isEmpty();
}
@Test
void findAllByReceivedViaChannelIdAndSettleDateAfter_several_invoices() {
Map<ChannelId, Coins> expected1 = Map.of(CHANNEL_ID, Coins.ofSatoshis(100), CHANNEL_ID_2, Coins.ofSatoshis(50));
Map<ChannelId, Coins> expected2 = Map.of(CHANNEL_ID_3, Coins.ofSatoshis(3), CHANNEL_ID_2, Coins.ofSatoshis(4));
repository.save(invoice(1, 1, expected1));
repository.save(invoice(2, 2, Map.of(CHANNEL_ID, Coins.ofSatoshis(1), CHANNEL_ID_3, Coins.ofSatoshis(2))));
repository.save(invoice(3, 3, expected2));
assertReceivedVia(CHANNEL_ID_2, List.of(expected1, expected2));
}
@Test
void one_of_two_channels_matches() {
Map<ChannelId, Coins> expected =
Map.of(CHANNEL_ID, Coins.ofSatoshis(100), CHANNEL_ID_2, Coins.ofSatoshis(50));
repository.save(invoice(1, 1, expected));
assertReceivedVia(CHANNEL_ID, List.of(expected));
}
private void assertReceivedVia(ChannelId channelId, List<Map<ChannelId, Coins>> expected) {
assertThat(repository.findAllByReceivedViaChannelIdAndSettleDateAfter(channelId.getShortChannelId(), 0))
.map(SettledInvoiceJpaDto::toModel)
.map(SettledInvoice::receivedVia)
.isEqualTo(expected);
@Test
void several_invoices() {
Map<ChannelId, Coins> expected1 =
Map.of(CHANNEL_ID, Coins.ofSatoshis(100), CHANNEL_ID_2, Coins.ofSatoshis(50));
Map<ChannelId, Coins> expected2 =
Map.of(CHANNEL_ID_3, Coins.ofSatoshis(3), CHANNEL_ID_2, Coins.ofSatoshis(4));
repository.save(invoice(1, 1, expected1));
repository.save(invoice(2, 2, Map.of(CHANNEL_ID, Coins.ofSatoshis(1), CHANNEL_ID_3, Coins.ofSatoshis(2))));
repository.save(invoice(3, 3, expected2));
assertReceivedVia(CHANNEL_ID_2, List.of(expected1, expected2));
}
private void assertReceivedVia(ChannelId channelId, List<Map<ChannelId, Coins>> expected) {
assertThat(repository.getInvoicesWithoutSelfPaymentsPaidVia(channelId.getShortChannelId(), 0))
.map(SettledInvoiceJpaDto::toModel)
.map(SettledInvoice::receivedVia)
.isEqualTo(expected);
}
}
private SettledInvoiceJpaDto invoice(int addIndex, int settleIndex) {
@@ -144,4 +168,15 @@ class SettledInvoicesRepositoryIT {
)
);
}
private PaymentJpaDto payment(String hash) {
return PaymentJpaDto.createFromModel(new Payment(
0,
hash,
LocalDateTime.of(2020, 1, 2, 3, 4),
Coins.NONE,
Coins.NONE,
List.of())
);
}
}

View File

@@ -16,5 +16,5 @@ public interface SettledInvoicesDao {
long getSettleIndexOffset();
List<SettledInvoice> getInvoicesPaidVia(ChannelId channelId, Duration maxAge);
List<SettledInvoice> getInvoicesWithoutSelfPaymentsPaidVia(ChannelId channelId, Duration maxAge);
}

View File

@@ -44,13 +44,11 @@ public class SettledInvoicesDaoImpl implements SettledInvoicesDao {
}
@Override
public List<SettledInvoice> getInvoicesPaidVia(ChannelId channelId, Duration maxAge) {
return repository.findAllByReceivedViaChannelIdAndSettleDateAfter(
public List<SettledInvoice> getInvoicesWithoutSelfPaymentsPaidVia(ChannelId channelId, Duration maxAge) {
return repository.getInvoicesWithoutSelfPaymentsPaidVia(
channelId.getShortChannelId(),
getAfterEpochMilliSeconds(maxAge)
).stream()
.map(SettledInvoiceJpaDto::toModel)
.toList();
).stream().map(SettledInvoiceJpaDto::toModel).toList();
}
private long getAfterEpochMilliSeconds(Duration maxAge) {

View File

@@ -13,5 +13,9 @@ public interface SettledInvoicesRepository extends JpaRepository<SettledInvoiceJ
"i.settleIndex = (SELECT COUNT(j) FROM SettledInvoiceJpaDto j WHERE j.settleIndex <= i.settleIndex)")
long getMaxSettledIndexWithoutGaps();
List<SettledInvoiceJpaDto> findAllByReceivedViaChannelIdAndSettleDateAfter(long channelId, long timestamp);
@Query("SELECT s FROM SettledInvoiceJpaDto s " +
"JOIN s.receivedVia v " +
"LEFT JOIN PaymentJpaDto p ON (s.hash = p.hash) " +
"WHERE s.settleDate > ?2 AND v.channelId = ?1 AND p IS NULL")
List<SettledInvoiceJpaDto> getInvoicesWithoutSelfPaymentsPaidVia(long channelId, long timestamp);
}

View File

@@ -82,23 +82,23 @@ class SettledInvoicesDaoImplTest {
@Test
void getInvoicesPaidVia_empty() {
assertThat(dao.getInvoicesPaidVia(CHANNEL_ID, Duration.ofMinutes(1))).isEmpty();
assertThat(dao.getInvoicesWithoutSelfPaymentsPaidVia(CHANNEL_ID, Duration.ofMinutes(1))).isEmpty();
}
@Test
void getInvoicesPaidVia() {
SettledInvoiceJpaDto jpaDto = SettledInvoiceJpaDto.createFromModel(SETTLED_INVOICE);
when(repository.findAllByReceivedViaChannelIdAndSettleDateAfter(eq(CHANNEL_ID.getShortChannelId()), anyLong()))
when(repository.getInvoicesWithoutSelfPaymentsPaidVia(eq(CHANNEL_ID.getShortChannelId()), anyLong()))
.thenReturn(List.of(jpaDto));
assertThat(dao.getInvoicesPaidVia(CHANNEL_ID, MAX_AGE)).containsExactly(SETTLED_INVOICE);
assertThat(dao.getInvoicesWithoutSelfPaymentsPaidVia(CHANNEL_ID, MAX_AGE)).containsExactly(SETTLED_INVOICE);
}
@Test
void getInvoicesPaidVia_with_max_age() {
Duration maxAge = Duration.ofDays(17);
long expectedTimestamp = Instant.now().minus(maxAge).getEpochSecond() * 1_000;
dao.getInvoicesPaidVia(CHANNEL_ID, maxAge);
verify(repository).findAllByReceivedViaChannelIdAndSettleDateAfter(
dao.getInvoicesWithoutSelfPaymentsPaidVia(CHANNEL_ID, maxAge);
verify(repository).getInvoicesWithoutSelfPaymentsPaidVia(
anyLong(),
longThat(timestamp -> Math.abs(expectedTimestamp - timestamp) <= 5_000)
);