add first hop filter to top-up

fixes #86
This commit is contained in:
Carsten Otto
2023-08-14 18:21:45 +02:00
parent 0b25ba9bcc
commit e09fe3f1db
12 changed files with 223 additions and 32 deletions

View File

@@ -122,10 +122,15 @@ The response shows a somewhat readable representation of the payment progress, i
node Z and node A, the whole payment fails (it is not attempted).
* Invoices (payment requests) created for top-up payments expiry after 30 minutes. This value can be configured as
`expiry_seconds=`.
* HTTP `GET`: `/api/payments/top-up/{pubkey}/amount/{amount}/via/{pubkey}`
* As before, but the pubkey specified after "via" is used for the first hop. As such, this can be used to reduce
outbound liquidity to the "via" peer.
* HTTP `POST`: `/api/payments/top-up/{pubkey}/amount/{amount}`
* allows you to lower the fee rate limit (values higher than the computed fee rate limit are ignored)
* allows you to specify a different fee rate weight
* The value provided as `ignoreFeesForOwnChannels` is ignored, for top-up such fees are never ignored
* HTTP `POST`: `/api/payments/top-up/{pubkey}/amount/{amount}/via/{pubkey}`
* As above, see corresponding GET endpoint
The threshold, i.e. the minimum difference between the current local balance and the requested amount, defaults to
10,000sat. You can configure this value by setting `threshold_sat=` in the configuration file.

View File

@@ -127,6 +127,9 @@ public class EdgeComputation {
if (feeRate >= feeRateLimit) {
return true;
}
if (isEdgeToUnwantedFirstHop(channelEdge, paymentOptions, pubkey)) {
return true;
}
if (isIncomingEdge(channelEdge, pubkey)) {
return false;
}
@@ -137,6 +140,22 @@ public class EdgeComputation {
return feeRate >= feeRateLimitFirstHops;
}
private boolean isEdgeToUnwantedFirstHop(
DirectedChannelEdge channelEdge,
PaymentOptions paymentOptions,
Pubkey pubkey
) {
boolean isOutgoingEdge = pubkey.equals(channelEdge.source());
if (!isOutgoingEdge) {
return false;
}
Pubkey peerForFirstHop = paymentOptions.peerForFirstHop().orElse(null);
if (peerForFirstHop == null) {
return false;
}
return !peerForFirstHop.equals(channelEdge.target());
}
private boolean isIncomingEdge(DirectedChannelEdge channelEdge, Pubkey ownPubkey) {
return ownPubkey.equals(channelEdge.target());
}

View File

@@ -51,7 +51,12 @@ public class TopUpService {
this.policyService = policyService;
}
public PaymentStatus topUp(Pubkey pubkey, Coins amount, PaymentOptions paymentOptionsFromRequest) {
public PaymentStatus topUp(
Pubkey pubkey,
Optional<Pubkey> peerForFirstHop,
Coins amount,
PaymentOptions paymentOptionsFromRequest
) {
if (noChannelWith(pubkey)) {
String alias = nodeService.getAlias(pubkey);
return PaymentStatus.createFailure("No channel with %s (%s)".formatted(pubkey, alias));
@@ -71,10 +76,15 @@ public class TopUpService {
return PaymentStatus.createFailure(reason);
}
return sendPayment(pubkey, topUpAmount, paymentOptionsFromRequest);
return sendPayment(pubkey, peerForFirstHop, topUpAmount, paymentOptionsFromRequest);
}
private PaymentStatus sendPayment(Pubkey pubkey, Coins topUpAmount, PaymentOptions paymentOptionsFromRequest) {
private PaymentStatus sendPayment(
Pubkey pubkey,
Optional<Pubkey> peerForFirstHop,
Coins topUpAmount,
PaymentOptions paymentOptions
) {
long ourFeeRate = policyService.getMinimumFeeRateTo(pubkey).orElse(0L);
long peerFeeRate = policyService.getMinimumFeeRateFrom(pubkey).orElse(0L);
if (peerFeeRate >= ourFeeRate) {
@@ -88,8 +98,10 @@ public class TopUpService {
String alias = nodeService.getAlias(pubkey);
return PaymentStatus.createFailure("Unable to create payment request (%s, %s)".formatted(pubkey, alias));
}
PaymentOptions paymentOptions = getPaymentOptions(pubkey, ourFeeRate, peerFeeRate, paymentOptionsFromRequest);
return multiPathPaymentSender.payPaymentRequest(paymentRequest, paymentOptions);
return multiPathPaymentSender.payPaymentRequest(
paymentRequest,
getPaymentOptions(pubkey, peerForFirstHop, ourFeeRate, peerFeeRate, paymentOptions)
);
}
private Coins getThreshold() {
@@ -117,16 +129,25 @@ public class TopUpService {
private PaymentOptions getPaymentOptions(
Pubkey pubkey,
Optional<Pubkey> peerForFirstHop,
long ourFeeRate,
long peerFeeRate,
PaymentOptions paymentOptionsFromRequest
PaymentOptions paymentOptions
) {
long feeRateLimit = Math.min(ourFeeRate, paymentOptionsFromRequest.feeRateLimit().orElse(Long.MAX_VALUE));
return PaymentOptions.forTopUp(
paymentOptionsFromRequest.feeRateWeight().orElse(5),
int feeRateWeight = paymentOptions.feeRateWeight().orElse(5);
long feeRateLimit = Math.min(ourFeeRate, paymentOptions.feeRateLimit().orElse(Long.MAX_VALUE));
return peerForFirstHop.map(value -> PaymentOptions.forTopUp(
feeRateWeight,
feeRateLimit,
peerFeeRate,
pubkey,
value
)).orElseGet(() -> PaymentOptions.forTopUp(
feeRateWeight,
feeRateLimit,
peerFeeRate,
pubkey
);
));
}
}

View File

@@ -9,7 +9,8 @@ public record PaymentOptions(
Optional<Long> feeRateLimit,
Optional<Long> feeRateLimitExceptIncomingHops,
boolean ignoreFeesForOwnChannels,
Optional<Pubkey> peer
Optional<Pubkey> peer,
Optional<Pubkey> peerForFirstHop
) {
public static final PaymentOptions DEFAULT_PAYMENT_OPTIONS = forFeeRateWeight(0);
@@ -19,6 +20,7 @@ public record PaymentOptions(
Optional.empty(),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
);
}
@@ -29,6 +31,7 @@ public record PaymentOptions(
Optional.of(feeRateLimit),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
);
}
@@ -39,7 +42,25 @@ public record PaymentOptions(
Optional.of(feeRateLimit),
Optional.of(Math.max(0, feeRateLimit - peerFeeRate)),
false,
Optional.of(peer)
Optional.of(peer),
Optional.empty()
);
}
public static PaymentOptions forTopUp(
int feeRateWeight,
long feeRateLimit,
long peerFeeRate,
Pubkey peer,
Pubkey peerForFirstHop
) {
return new PaymentOptions(
Optional.of(feeRateWeight),
Optional.of(feeRateLimit),
Optional.of(Math.max(0, feeRateLimit - peerFeeRate)),
false,
Optional.of(peer),
Optional.of(peerForFirstHop)
);
}
}

View File

@@ -13,6 +13,7 @@ import de.cotto.lndmanagej.model.LocalOpenChannel;
import de.cotto.lndmanagej.model.LocalOpenChannelFixtures;
import de.cotto.lndmanagej.model.Policy;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.pickhardtpayments.model.EdgesWithLiquidityInformation;
import de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions;
import de.cotto.lndmanagej.service.BalanceService;
import de.cotto.lndmanagej.service.ChannelService;
@@ -44,6 +45,7 @@ import static de.cotto.lndmanagej.model.PolicyFixtures.POLICY_DISABLED;
import static de.cotto.lndmanagej.model.PolicyFixtures.POLICY_WITH_BASE_FEE;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_3;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_4;
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
import static org.assertj.core.api.Assertions.assertThat;
@@ -312,6 +314,22 @@ class EdgeComputationTest {
.contains(EdgeWithLiquidityInformation.forUpperBound(EDGE, EDGE.capacity()));
}
@Test
void ignores_other_peers_if_peer_for_first_hop_is_set() {
Pubkey ownNode = PUBKEY_4;
Pubkey peerForFirstHop = PUBKEY_3;
PaymentOptions paymentOptions =
PaymentOptions.forTopUp(FEE_RATE_WEIGHT, 999, 0, PUBKEY_2, peerForFirstHop);
DirectedChannelEdge edge1 = new DirectedChannelEdge(CHANNEL_ID, CAPACITY, ownNode, peerForFirstHop, POLICY_1);
DirectedChannelEdge edge2 = new DirectedChannelEdge(CHANNEL_ID, CAPACITY, ownNode, PUBKEY, POLICY_1);
when(grpcGraph.getChannelEdges()).thenReturn(Optional.of(Set.of(edge1, edge2)));
EdgesWithLiquidityInformation edges = edgeComputation.getEdges(paymentOptions, MAX_TIME_LOCK_DELTA);
assertThat(edges.edges().stream().map(EdgeWithLiquidityInformation::endNode)).containsExactly(peerForFirstHop);
}
@Test
void getEdgeWithLiquidityInformation_default() {
when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY_4);

View File

@@ -178,6 +178,7 @@ class TopUpServiceTest {
Optional.of(feeRateLimit),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
);
PaymentOptions expected = new PaymentOptions(
@@ -185,7 +186,8 @@ class TopUpServiceTest {
Optional.of(feeRateLimit),
Optional.of(feeRateLimit - peerFeeRate),
false,
Optional.of(PUBKEY)
Optional.of(PUBKEY),
Optional.empty()
);
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
@@ -203,6 +205,7 @@ class TopUpServiceTest {
Optional.of(feeRateLimit),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
);
PaymentOptions expected = new PaymentOptions(
@@ -210,7 +213,8 @@ class TopUpServiceTest {
Optional.of(ourFeeRate),
Optional.of(ourFeeRate - peerFeeRate),
false,
Optional.of(PUBKEY)
Optional.of(PUBKEY),
Optional.empty()
);
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
@@ -228,6 +232,7 @@ class TopUpServiceTest {
Optional.of(feeRateLimit),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
);
PaymentOptions expected = new PaymentOptions(
@@ -235,7 +240,8 @@ class TopUpServiceTest {
Optional.of(feeRateLimit),
Optional.of(feeRateLimit - peerFeeRate),
false,
Optional.of(PUBKEY)
Optional.of(PUBKEY),
Optional.empty()
);
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
@@ -253,6 +259,7 @@ class TopUpServiceTest {
Optional.of(feeRateLimit),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
);
PaymentOptions expected = new PaymentOptions(
@@ -260,7 +267,8 @@ class TopUpServiceTest {
Optional.of(ourFeeRate),
Optional.of(ourFeeRate - peerFeeRate),
false,
Optional.of(PUBKEY)
Optional.of(PUBKEY),
Optional.empty()
);
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
@@ -278,6 +286,7 @@ class TopUpServiceTest {
Optional.of(feeRateLimit),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
);
PaymentOptions expected = new PaymentOptions(
@@ -285,7 +294,8 @@ class TopUpServiceTest {
Optional.of(feeRateLimit),
Optional.of(0L),
false,
Optional.of(PUBKEY)
Optional.of(PUBKEY),
Optional.empty()
);
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
@@ -299,6 +309,7 @@ class TopUpServiceTest {
Optional.empty(),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
);
PaymentOptions expected = new PaymentOptions(
@@ -306,7 +317,8 @@ class TopUpServiceTest {
Optional.of(OUR_FEE_RATE),
Optional.of(OUR_FEE_RATE - PEER_FEE_RATE),
false,
Optional.of(PUBKEY)
Optional.of(PUBKEY),
Optional.empty()
);
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
@@ -339,6 +351,7 @@ class TopUpServiceTest {
Optional.empty(),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
);
assertTopUp(expectedTopUpAmount, expiry, emptyPaymentOptions, paymentOptions);
@@ -350,14 +363,14 @@ class TopUpServiceTest {
PaymentOptions givenPaymentOptions,
PaymentOptions expectedPaymentOptions
) {
PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, AMOUNT, givenPaymentOptions);
PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, Optional.empty(), AMOUNT, givenPaymentOptions);
verify(grpcInvoices).createPaymentRequest(expectedTopUpAmount, DESCRIPTION, expiry);
verify(multiPathPaymentSender).payPaymentRequest(DECODED_PAYMENT_REQUEST, expectedPaymentOptions);
assertThat(paymentStatus.isPending()).isTrue();
}
private void assertFailure(String reason) {
PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, AMOUNT, DEFAULT_PAYMENT_OPTIONS);
PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, Optional.empty(), AMOUNT, DEFAULT_PAYMENT_OPTIONS);
assertThat(paymentStatus.isFailure()).isTrue();
assertThat(readAll(paymentStatus)).map(InstantWithString::string).containsExactly(reason);
verifyNoInteractions(multiPathPaymentSender);

View File

@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
import java.util.Optional;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2;
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
import static org.assertj.core.api.Assertions.assertThat;
@@ -19,6 +20,7 @@ class PaymentOptionsTest {
Optional.empty(),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
));
}
@@ -30,6 +32,7 @@ class PaymentOptionsTest {
Optional.of(123L),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
));
}
@@ -41,7 +44,20 @@ class PaymentOptionsTest {
Optional.of(123L),
Optional.of(23L),
false,
Optional.of(PUBKEY)
Optional.of(PUBKEY),
Optional.empty()
));
}
@Test
void forTopUp_with_peer_for_first_hop() {
assertThat(PaymentOptions.forTopUp(FEE_RATE_WEIGHT, 123, 100, PUBKEY, PUBKEY_2)).isEqualTo(new PaymentOptions(
Optional.of(FEE_RATE_WEIGHT),
Optional.of(123L),
Optional.of(23L),
false,
Optional.of(PUBKEY),
Optional.of(PUBKEY_2)
));
}

View File

@@ -48,6 +48,7 @@ class PaymentsControllerIT {
Optional.of(999L),
Optional.empty(),
false,
Optional.empty(),
Optional.empty()
);
private static final int FINAL_CLTV_EXPIRY = 0;
@@ -181,29 +182,55 @@ class PaymentsControllerIT {
@Test
void topUp() {
when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus);
when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus);
PaymentOptions emptyPaymentOptions = new PaymentOptions(
Optional.empty(),
Optional.empty(),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
);
webTestClient.get().uri(url).exchange().expectStatus().isOk();
verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123), emptyPaymentOptions);
verify(topUpService).topUp(PUBKEY, Optional.empty(), Coins.ofSatoshis(123), emptyPaymentOptions);
}
@Test
void topUp_with_peer_for_first_hop() {
when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus);
PaymentOptions emptyPaymentOptions = new PaymentOptions(
Optional.empty(),
Optional.empty(),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
);
String uri = "%s/top-up/%s/amount/%s/via/%s".formatted(PREFIX, PUBKEY, "123", PUBKEY_2);
webTestClient.get().uri(uri).exchange().expectStatus().isOk();
verify(topUpService).topUp(PUBKEY, Optional.of(PUBKEY_2), Coins.ofSatoshis(123), emptyPaymentOptions);
}
@Test
void with_payment_options() {
when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus);
when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus);
webTestClient.post().uri(url).contentType(APPLICATION_JSON).bodyValue(DTO_AS_STRING).exchange()
.expectStatus().isOk();
verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123), PAYMENT_OPTIONS);
verify(topUpService).topUp(PUBKEY, Optional.empty(), Coins.ofSatoshis(123), PAYMENT_OPTIONS);
}
@Test
void with_peer_for_first_hop_and_payment_options() {
when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus);
String uri = "%s/top-up/%s/amount/%s/via/%s".formatted(PREFIX, PUBKEY, "123", PUBKEY_2);
webTestClient.post().uri(uri).contentType(APPLICATION_JSON).bodyValue(DTO_AS_STRING).exchange()
.expectStatus().isOk();
verify(topUpService).topUp(PUBKEY, Optional.of(PUBKEY_2), Coins.ofSatoshis(123), PAYMENT_OPTIONS);
}
@Test
void no_linebreaks_within_line() {
when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus);
when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus);
webTestClient.get().uri(url).exchange().expectBody(String.class).value(string ->
assertThat(string.substring(0, string.length() - 1)).doesNotContain("\n")
);
@@ -211,7 +238,7 @@ class PaymentsControllerIT {
@Test
void linebreak_at_end_of_line() {
when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus);
when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus);
webTestClient.get().uri(url).exchange().expectBody(String.class).value(string ->
assertThat(string).endsWith("\n")
);

View File

@@ -22,6 +22,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.Optional;
import static org.springframework.http.MediaType.APPLICATION_NDJSON;
@RestController
@@ -119,6 +121,16 @@ public class PaymentsController {
return topUp(pubkey, amount, new PaymentOptionsDto());
}
@Timed
@GetMapping("/top-up/{pubkey}/amount/{amount}/via/{peerForFirstHop}")
public ResponseEntity<Flux<String>> topUp(
@PathVariable Pubkey pubkey,
@PathVariable long amount,
@PathVariable Pubkey peerForFirstHop
) {
return topUp(pubkey, amount, peerForFirstHop, new PaymentOptionsDto());
}
@Timed
@PostMapping("/top-up/{pubkey}/amount/{amount}")
public ResponseEntity<Flux<String>> topUp(
@@ -126,7 +138,25 @@ public class PaymentsController {
@PathVariable long amount,
@RequestBody PaymentOptionsDto paymentOptionsDto
) {
PaymentStatus paymentStatus = topUpService.topUp(pubkey, Coins.ofSatoshis(amount), paymentOptionsDto.toModel());
PaymentStatus paymentStatus =
topUpService.topUp(pubkey, Optional.empty(), Coins.ofSatoshis(amount), paymentOptionsDto.toModel());
return toStream(paymentStatus);
}
@Timed
@PostMapping("/top-up/{pubkey}/amount/{amount}/via/{peerForFirstHop}")
public ResponseEntity<Flux<String>> topUp(
@PathVariable Pubkey pubkey,
@PathVariable long amount,
@PathVariable Pubkey peerForFirstHop,
@RequestBody PaymentOptionsDto paymentOptionsDto
) {
PaymentStatus paymentStatus = topUpService.topUp(
pubkey,
Optional.of(peerForFirstHop),
Coins.ofSatoshis(amount),
paymentOptionsDto.toModel()
);
return toStream(paymentStatus);
}

View File

@@ -31,6 +31,7 @@ public class PaymentOptionsDto {
Optional.ofNullable(feeRateLimit),
Optional.empty(),
ignoreFeesForOwnChannels,
Optional.empty(),
Optional.empty()
);
}

View File

@@ -25,6 +25,7 @@ import static de.cotto.lndmanagej.pickhardtpayments.model.MultiPathPaymentFixtur
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -44,6 +45,7 @@ class PaymentsControllerTest {
Optional.of(999L),
Optional.empty(),
false,
Optional.empty(),
Optional.empty()
);
PAYMENT_OPTIONS_DTO = new PaymentOptionsDto();
@@ -145,23 +147,39 @@ class PaymentsControllerTest {
@Test
void topUp() {
when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus);
when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus);
PaymentOptions emptyPaymentOptions = new PaymentOptions(
Optional.empty(),
Optional.empty(),
Optional.empty(),
true,
Optional.empty(),
Optional.empty()
);
assertThat(controller.topUp(PUBKEY, 123).getStatusCode()).isEqualTo(HttpStatus.OK);
verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123), emptyPaymentOptions);
verify(topUpService).topUp(PUBKEY, Optional.empty(), Coins.ofSatoshis(123), emptyPaymentOptions);
}
@Test
void topUp_with_peer_for_first_hop() {
when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus);
assertThat(controller.topUp(PUBKEY, 123, PUBKEY_2).getStatusCode()).isEqualTo(HttpStatus.OK);
verify(topUpService).topUp(any(), eq(Optional.of(PUBKEY_2)), any(), any());
}
@Test
void topUp_with_payment_options() {
when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus);
when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus);
assertThat(controller.topUp(PUBKEY, 123, PAYMENT_OPTIONS_DTO).getStatusCode()).isEqualTo(HttpStatus.OK);
verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123), PAYMENT_OPTIONS);
verify(topUpService).topUp(PUBKEY, Optional.empty(), Coins.ofSatoshis(123), PAYMENT_OPTIONS);
}
@Test
void topUp_with_peer_for_first_hop_and_payment_options() {
when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus);
assertThat(controller.topUp(PUBKEY, 123, PUBKEY_2, PAYMENT_OPTIONS_DTO).getStatusCode())
.isEqualTo(HttpStatus.OK);
verify(topUpService).topUp(PUBKEY, Optional.of(PUBKEY_2), Coins.ofSatoshis(123), PAYMENT_OPTIONS);
}
@Test

View File

@@ -31,6 +31,7 @@ class PaymentOptionsDtoTest {
Optional.of(feeRateLimit),
Optional.empty(),
DEFAULT_PAYMENT_OPTIONS.ignoreFeesForOwnChannels(),
Optional.empty(),
Optional.empty()
);
assertThat(dto.toModel()).isEqualTo(expected);
@@ -45,6 +46,7 @@ class PaymentOptionsDtoTest {
Optional.empty(),
Optional.empty(),
false,
Optional.empty(),
Optional.empty()
);
assertThat(dto.toModel()).isEqualTo(expected);