mirror of
https://github.com/aljazceru/lnd-manageJ.git
synced 2025-12-17 06:04:25 +01:00
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ public class PaymentOptionsDto {
|
||||
Optional.ofNullable(feeRateLimit),
|
||||
Optional.empty(),
|
||||
ignoreFeesForOwnChannels,
|
||||
Optional.empty(),
|
||||
Optional.empty()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user