add endpoints for rebalance costs

This commit is contained in:
Carsten Otto
2021-12-09 15:58:03 +01:00
parent fc4ac8ebd9
commit 820fca691e
7 changed files with 434 additions and 3 deletions

View File

@@ -0,0 +1,67 @@
package de.cotto.lndmanagej.service;
import com.codahale.metrics.annotation.Timed;
import de.cotto.lndmanagej.model.Channel;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.model.SelfPayment;
import org.springframework.stereotype.Component;
@Component
public class OffChainCostService {
private final SelfPaymentsService selfPaymentsService;
private final ChannelService channelService;
public OffChainCostService(SelfPaymentsService selfPaymentsService, ChannelService channelService) {
this.selfPaymentsService = selfPaymentsService;
this.channelService = channelService;
}
@Timed
public Coins getRebalanceSourceCostsForChannel(ChannelId channelId) {
return selfPaymentsService.getSelfPaymentsFromChannel(channelId).stream()
.filter(selfPayment -> memoMentionsChannel(selfPayment, channelId))
.map(SelfPayment::fees)
.reduce(Coins.NONE, Coins::add);
}
@Timed
public Coins getRebalanceSourceCostsForPeer(Pubkey pubkey) {
return channelService.getAllChannelsWith(pubkey).parallelStream()
.map(Channel::getId)
.map(this::getRebalanceSourceCostsForChannel)
.reduce(Coins.NONE, Coins::add);
}
@Timed
public Coins getRebalanceTargetCostsForChannel(ChannelId channelId) {
return selfPaymentsService.getSelfPaymentsToChannel(channelId).stream()
.filter(this::memoDoesNotMentionFirstHopChannel)
.map(SelfPayment::fees)
.reduce(Coins.NONE, Coins::add);
}
@Timed
public Coins getRebalanceTargetCostsForPeer(Pubkey pubkey) {
return channelService.getAllChannelsWith(pubkey).parallelStream()
.map(Channel::getId)
.map(this::getRebalanceTargetCostsForChannel)
.reduce(Coins.NONE, Coins::add);
}
private boolean memoMentionsChannel(SelfPayment selfPayment, ChannelId expectedChannel) {
String memo = selfPayment.memo();
return memo.contains(String.valueOf(expectedChannel.getShortChannelId()))
|| memo.contains(expectedChannel.getCompactForm())
|| memo.contains(expectedChannel.getCompactFormLnd());
}
private boolean memoDoesNotMentionFirstHopChannel(SelfPayment selfPayment) {
ChannelId firstChannel = selfPayment.firstChannel().orElse(null);
if (firstChannel == null) {
return true;
}
return !memoMentionsChannel(selfPayment, firstChannel);
}
}

View File

@@ -75,7 +75,7 @@ public class SelfPaymentsService {
Pubkey pubkey,
Function<ChannelId, List<SelfPayment>> provider
) {
return channelService.getAllChannelsWith(pubkey).stream()
return channelService.getAllChannelsWith(pubkey).parallelStream()
.map(Channel::getId)
.map(provider)
.flatMap(List::stream)

View File

@@ -0,0 +1,213 @@
package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.model.Payment;
import de.cotto.lndmanagej.model.PaymentHop;
import de.cotto.lndmanagej.model.PaymentRoute;
import de.cotto.lndmanagej.model.SelfPayment;
import de.cotto.lndmanagej.model.SettledInvoice;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_2;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3;
import static de.cotto.lndmanagej.model.CoopClosedChannelFixtures.CLOSED_CHANNEL_2;
import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL;
import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_CREATION_DATE_TIME;
import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_FEES;
import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_HASH;
import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_INDEX;
import static de.cotto.lndmanagej.model.PaymentFixtures.PAYMENT_VALUE;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.ADD_INDEX;
import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.AMOUNT_PAID;
import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.HASH;
import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.SETTLE_DATE;
import static de.cotto.lndmanagej.model.SettledInvoiceFixtures.SETTLE_INDEX;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class OffChainCostServiceTest {
private static final Coins COST_FOR_TWO_SELF_PAYMENTS = PAYMENT_FEES.add(PAYMENT_FEES);
@InjectMocks
private OffChainCostService offChainCostService;
@Mock
private SelfPaymentsService selfPaymentsService;
@Mock
private ChannelService channelService;
@Test
void getRebalanceSourceCostsForChannel_no_self_payments() {
assertThat(offChainCostService.getRebalanceSourceCostsForChannel(CHANNEL_ID)).isEqualTo(Coins.NONE);
}
@Test
void getRebalanceSourceCostsForChannel_short_channel_id_in_memo() {
mockSelfPaymentsFromChannel("rebalance from " + CHANNEL_ID.getShortChannelId());
assertThat(offChainCostService.getRebalanceSourceCostsForChannel(CHANNEL_ID))
.isEqualTo(COST_FOR_TWO_SELF_PAYMENTS);
}
@Test
void getRebalanceSourceCostsForChannel_compact_channel_id_in_memo() {
mockSelfPaymentsFromChannel("rebalance from " + CHANNEL_ID.getCompactForm());
assertThat(offChainCostService.getRebalanceSourceCostsForChannel(CHANNEL_ID))
.isEqualTo(COST_FOR_TWO_SELF_PAYMENTS);
}
@Test
void getRebalanceSourceCostsForChannel_compact_lnd_channel_id_in_memo() {
mockSelfPaymentsFromChannel("rebalance from " + CHANNEL_ID.getCompactFormLnd());
assertThat(offChainCostService.getRebalanceSourceCostsForChannel(CHANNEL_ID))
.isEqualTo(COST_FOR_TWO_SELF_PAYMENTS);
}
@Test
void getRebalanceSourceCostsForChannel_id_not_in_memo() {
mockSelfPaymentsFromChannel("something");
assertThat(offChainCostService.getRebalanceSourceCostsForChannel(CHANNEL_ID))
.isEqualTo(Coins.NONE);
}
@Test
void getRebalanceSourceCostsForPeer() {
ChannelId id1 = LOCAL_OPEN_CHANNEL.getId();
ChannelId id2 = CLOSED_CHANNEL_2.getId();
when(channelService.getAllChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, CLOSED_CHANNEL_2));
when(selfPaymentsService.getSelfPaymentsFromChannel(id1)).thenReturn(List.of(getSelfPayment(id1.toString())));
when(selfPaymentsService.getSelfPaymentsFromChannel(id2)).thenReturn(List.of(getSelfPayment(id2.toString())));
assertThat(offChainCostService.getRebalanceSourceCostsForPeer(PUBKEY))
.isEqualTo(COST_FOR_TWO_SELF_PAYMENTS);
}
@Test
void getRebalanceTargetCostsForChannel_no_self_payments() {
assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2)).isEqualTo(Coins.NONE);
}
@Test
void getRebalanceTargetCostsForChannel_short_channel_id_in_memo() {
mockSelfPaymentsToChannel("foo bar " + CHANNEL_ID_2.getShortChannelId() + "!");
assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2))
.isEqualTo(COST_FOR_TWO_SELF_PAYMENTS);
}
@Test
void getRebalanceTargetCostsForChannel_compact_channel_id_in_memo() {
mockSelfPaymentsToChannel("something something " + CHANNEL_ID_2.getCompactForm());
assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2))
.isEqualTo(COST_FOR_TWO_SELF_PAYMENTS);
}
@Test
void getRebalanceTargetCostsForChannel_compact_lnd_channel_id_in_memo() {
mockSelfPaymentsToChannel(CHANNEL_ID_2.getCompactFormLnd());
assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2))
.isEqualTo(COST_FOR_TWO_SELF_PAYMENTS);
}
@Test
void getRebalanceTargetCostsForChannel_id_not_in_memo() {
mockSelfPaymentsToChannel("something");
assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2))
.isEqualTo(COST_FOR_TWO_SELF_PAYMENTS);
}
@Test
void getRebalanceTargetCostsForChannel_other_channel_id_in_memo() {
mockSelfPaymentsToChannel("rebalance to " + CHANNEL_ID_3);
assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2))
.isEqualTo(COST_FOR_TWO_SELF_PAYMENTS);
}
@Test
void getRebalanceTargetCostsForChannel_source_channel_id_in_memo() {
mockSelfPaymentsToChannel("something: " + CHANNEL_ID);
assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2))
.isEqualTo(Coins.NONE);
}
@Test
void getRebalanceTargetCostsForChannel_without_first_channel_information() {
Payment payment = new Payment(
PAYMENT_INDEX, PAYMENT_HASH, PAYMENT_CREATION_DATE_TIME, PAYMENT_VALUE, PAYMENT_FEES, List.of()
);
SettledInvoice settledInvoice = new SettledInvoice(
ADD_INDEX,
SETTLE_INDEX,
SETTLE_DATE,
HASH,
AMOUNT_PAID,
"something: " + CHANNEL_ID,
Optional.empty(),
Optional.of(CHANNEL_ID_2)
);
SelfPayment selfPayment = new SelfPayment(payment, settledInvoice);
when(selfPaymentsService.getSelfPaymentsToChannel(CHANNEL_ID_2)).thenReturn(List.of(selfPayment));
assertThat(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID_2))
.isEqualTo(PAYMENT_FEES);
}
@Test
void getRebalanceTargetCostsForPeer() {
ChannelId id1 = LOCAL_OPEN_CHANNEL.getId();
ChannelId id2 = CLOSED_CHANNEL_2.getId();
when(channelService.getAllChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, CLOSED_CHANNEL_2));
when(selfPaymentsService.getSelfPaymentsToChannel(id1)).thenReturn(List.of(getSelfPayment("")));
when(selfPaymentsService.getSelfPaymentsToChannel(id2)).thenReturn(List.of(getSelfPayment("")));
assertThat(offChainCostService.getRebalanceTargetCostsForPeer(PUBKEY))
.isEqualTo(COST_FOR_TWO_SELF_PAYMENTS);
}
private void mockSelfPaymentsFromChannel(String memo) {
when(selfPaymentsService.getSelfPaymentsFromChannel(CHANNEL_ID)).thenReturn(List.of(
getSelfPayment(memo),
getSelfPayment(memo)
));
}
private void mockSelfPaymentsToChannel(String memo) {
when(selfPaymentsService.getSelfPaymentsToChannel(CHANNEL_ID_2)).thenReturn(List.of(
getSelfPayment(memo),
getSelfPayment(memo)
));
}
private SelfPayment getSelfPayment(String memo) {
List<PaymentRoute> routes = getSingleRoute();
Payment payment = new Payment(
PAYMENT_INDEX, PAYMENT_HASH, PAYMENT_CREATION_DATE_TIME, PAYMENT_VALUE, PAYMENT_FEES, routes
);
SettledInvoice settledInvoice = new SettledInvoice(
ADD_INDEX,
SETTLE_INDEX,
SETTLE_DATE,
HASH,
AMOUNT_PAID,
memo,
Optional.empty(),
Optional.of(CHANNEL_ID_2)
);
return new SelfPayment(payment, settledInvoice);
}
private List<PaymentRoute> getSingleRoute() {
PaymentHop firstHop = new PaymentHop(CHANNEL_ID, Coins.NONE);
PaymentHop lastHop = new PaymentHop(CHANNEL_ID_2, Coins.NONE);
PaymentRoute route = new PaymentRoute(List.of(firstHop, lastHop));
return List.of(route);
}
}

View File

@@ -16,8 +16,8 @@ public class PaymentFixtures {
public static final String PAYMENT_HASH_3 = "aaa1";
public static final Coins PAYMENT_VALUE = Coins.ofSatoshis(1_000_000);
public static final Coins PAYMENT_FEES = Coins.ofMilliSatoshis(10);
private static final List<PaymentRoute> ONE_ROUTE_4_TO_2 = List.of(PAYMENT_ROUTE_4_TO_2);
private static final List<PaymentRoute> TWO_ROUTES = List.of(PAYMENT_ROUTE_4_TO_1, PAYMENT_ROUTE_3_TO_1);
public static final List<PaymentRoute> ONE_ROUTE_4_TO_2 = List.of(PAYMENT_ROUTE_4_TO_2);
public static final List<PaymentRoute> TWO_ROUTES = List.of(PAYMENT_ROUTE_4_TO_1, PAYMENT_ROUTE_3_TO_1);
public static final LocalDateTime PAYMENT_CREATION_DATE_TIME =
LocalDateTime.of(2021, 12, 5, 22, 22, 22, 500_000_000);

View File

@@ -0,0 +1,60 @@
package de.cotto.lndmanagej.controller;
import de.cotto.lndmanagej.model.ChannelIdResolver;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.service.OffChainCostService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@WebMvcTest(controllers = OffChainCostsController.class)
class OffChainCostsControllerIT {
private static final String CHANNEL_PREFIX = "/api/channel/" + CHANNEL_ID.getShortChannelId() + "/";
private static final String NODE_PREFIX = "/api/node/" + PUBKEY + "/";
@Autowired
private MockMvc mockMvc;
@MockBean
@SuppressWarnings("unused")
private ChannelIdResolver channelIdResolver;
@MockBean
private OffChainCostService offChainCostService;
@Test
void getRebalanceSourceCostsForChannel() throws Exception {
when(offChainCostService.getRebalanceSourceCostsForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(123));
mockMvc.perform(get(CHANNEL_PREFIX + "/rebalance-source-costs/"))
.andExpect(content().string("123"));
}
@Test
void getRebalanceSourceCostsForPeer() throws Exception {
when(offChainCostService.getRebalanceSourceCostsForPeer(PUBKEY)).thenReturn(Coins.ofMilliSatoshis(124));
mockMvc.perform(get(NODE_PREFIX + "/rebalance-source-costs/"))
.andExpect(content().string("124"));
}
@Test
void getRebalanceTargetCostsForChannel() throws Exception {
when(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(125));
mockMvc.perform(get(CHANNEL_PREFIX + "/rebalance-target-costs/"))
.andExpect(content().string("125"));
}
@Test
void getRebalanceTargetCostsForPeer() throws Exception {
when(offChainCostService.getRebalanceTargetCostsForPeer(PUBKEY)).thenReturn(Coins.ofMilliSatoshis(126));
mockMvc.perform(get(NODE_PREFIX + "/rebalance-target-costs/"))
.andExpect(content().string("126"));
}
}

View File

@@ -0,0 +1,44 @@
package de.cotto.lndmanagej.controller;
import com.codahale.metrics.annotation.Timed;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.service.OffChainCostService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/")
public class OffChainCostsController {
private final OffChainCostService offChainCostService;
public OffChainCostsController(OffChainCostService offChainCostService) {
this.offChainCostService = offChainCostService;
}
@Timed
@GetMapping("/node/{pubkey}/rebalance-source-costs")
public long getRebalanceSourceCostsForPeer(@PathVariable Pubkey pubkey) {
return offChainCostService.getRebalanceSourceCostsForPeer(pubkey).milliSatoshis();
}
@Timed
@GetMapping("/channel/{channelId}/rebalance-source-costs")
public long getRebalanceSourceCostsForChannel(@PathVariable ChannelId channelId) {
return offChainCostService.getRebalanceSourceCostsForChannel(channelId).milliSatoshis();
}
@Timed
@GetMapping("/node/{pubkey}/rebalance-target-costs")
public long getRebalanceTargetCostsForPeer(@PathVariable Pubkey pubkey) {
return offChainCostService.getRebalanceTargetCostsForPeer(pubkey).milliSatoshis();
}
@Timed
@GetMapping("/channel/{channelId}/rebalance-target-costs")
public long getRebalanceTargetCostsForChannel(@PathVariable ChannelId channelId) {
return offChainCostService.getRebalanceTargetCostsForChannel(channelId).milliSatoshis();
}
}

View File

@@ -0,0 +1,47 @@
package de.cotto.lndmanagej.controller;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.service.OffChainCostService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class OffChainCostsControllerTest {
@InjectMocks
private OffChainCostsController offChainCostsController;
@Mock
private OffChainCostService offChainCostService;
@Test
void getRebalanceSourceCostsForChannel() {
when(offChainCostService.getRebalanceSourceCostsForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(123));
assertThat(offChainCostsController.getRebalanceSourceCostsForChannel(CHANNEL_ID)).isEqualTo(123);
}
@Test
void getRebalanceSourceCostsForPeer() {
when(offChainCostService.getRebalanceSourceCostsForPeer(PUBKEY)).thenReturn(Coins.ofMilliSatoshis(123));
assertThat(offChainCostsController.getRebalanceSourceCostsForPeer(PUBKEY)).isEqualTo(123);
}
@Test
void getRebalanceTargetCostsForChannel() {
when(offChainCostService.getRebalanceTargetCostsForChannel(CHANNEL_ID)).thenReturn(Coins.ofMilliSatoshis(123));
assertThat(offChainCostsController.getRebalanceTargetCostsForChannel(CHANNEL_ID)).isEqualTo(123);
}
@Test
void getRebalanceTargetCostsForPeer() {
when(offChainCostService.getRebalanceTargetCostsForPeer(PUBKEY)).thenReturn(Coins.ofMilliSatoshis(123));
assertThat(offChainCostsController.getRebalanceTargetCostsForPeer(PUBKEY)).isEqualTo(123);
}
}