mirror of
https://github.com/aljazceru/lnd-manageJ.git
synced 2026-01-20 14:34:24 +01:00
Merge branch 'main' into fix-5746
This commit is contained in:
@@ -29,10 +29,10 @@ online_changes_threshold=50
|
||||
quantization=10000
|
||||
piecewise_linear_approximations=5
|
||||
use_mission_control=true
|
||||
liquidity_information_max_age_in_seconds=3600
|
||||
liquidity_information_max_age_in_seconds=600
|
||||
|
||||
[top-up]
|
||||
expiry_seconds=30
|
||||
expiry_seconds=1800
|
||||
threshold_sat=10000
|
||||
sleep_after_failure_milliseconds=500
|
||||
max_retries_after_failure=5
|
||||
|
||||
@@ -13,6 +13,7 @@ import java.util.Map;
|
||||
|
||||
import static de.cotto.lndmanagej.configuration.PickhardtPaymentsConfigurationSettings.PIECEWISE_LINEAR_APPROXIMATIONS;
|
||||
import static de.cotto.lndmanagej.configuration.PickhardtPaymentsConfigurationSettings.QUANTIZATION;
|
||||
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
|
||||
|
||||
@Component
|
||||
public class FlowComputation {
|
||||
@@ -45,7 +46,7 @@ public class FlowComputation {
|
||||
Map.of(target, amount),
|
||||
quantization,
|
||||
piecewiseLinearApproximations,
|
||||
paymentOptions.feeRateWeight(),
|
||||
paymentOptions.feeRateWeight().orElse(DEFAULT_PAYMENT_OPTIONS.feeRateWeight().orElseThrow()),
|
||||
grpcGetInfo.getPubkey(),
|
||||
paymentOptions.ignoreFeesForOwnChannels()
|
||||
);
|
||||
|
||||
@@ -51,7 +51,7 @@ public class TopUpService {
|
||||
this.policyService = policyService;
|
||||
}
|
||||
|
||||
public PaymentStatus topUp(Pubkey pubkey, Coins amount) {
|
||||
public PaymentStatus topUp(Pubkey pubkey, 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 +71,10 @@ public class TopUpService {
|
||||
return PaymentStatus.createFailure(reason);
|
||||
}
|
||||
|
||||
return sendPayment(pubkey, topUpAmount);
|
||||
return sendPayment(pubkey, topUpAmount, paymentOptionsFromRequest);
|
||||
}
|
||||
|
||||
private PaymentStatus sendPayment(Pubkey pubkey, Coins topUpAmount) {
|
||||
private PaymentStatus sendPayment(Pubkey pubkey, Coins topUpAmount, PaymentOptions paymentOptionsFromRequest) {
|
||||
long ourFeeRate = policyService.getMinimumFeeRateTo(pubkey).orElse(0L);
|
||||
long peerFeeRate = policyService.getMinimumFeeRateFrom(pubkey).orElse(0L);
|
||||
if (peerFeeRate >= ourFeeRate) {
|
||||
@@ -88,7 +88,7 @@ public class TopUpService {
|
||||
String alias = nodeService.getAlias(pubkey);
|
||||
return PaymentStatus.createFailure("Unable to create payment request (%s, %s)".formatted(pubkey, alias));
|
||||
}
|
||||
PaymentOptions paymentOptions = PaymentOptions.forTopUp(ourFeeRate, peerFeeRate, pubkey);
|
||||
PaymentOptions paymentOptions = getPaymentOptions(pubkey, ourFeeRate, peerFeeRate, paymentOptionsFromRequest);
|
||||
return multiPathPaymentSender.payPaymentRequest(paymentRequest, paymentOptions);
|
||||
}
|
||||
|
||||
@@ -114,4 +114,23 @@ public class TopUpService {
|
||||
ChannelId channelId = channelService.getOpenChannelsWith(pubkey).iterator().next().getId();
|
||||
return "Topping up channel %s with %s (%s)".formatted(channelId, pubkey, alias);
|
||||
}
|
||||
|
||||
private PaymentOptions getPaymentOptions(
|
||||
Pubkey pubkey,
|
||||
long ourFeeRate,
|
||||
long peerFeeRate,
|
||||
PaymentOptions paymentOptionsFromRequest
|
||||
) {
|
||||
long feeRateLimitExceptIncomingHops = Math.min(
|
||||
ourFeeRate - peerFeeRate,
|
||||
paymentOptionsFromRequest.feeRateLimitExceptIncomingHops().orElse(Long.MAX_VALUE)
|
||||
);
|
||||
return new PaymentOptions(
|
||||
Optional.of(paymentOptionsFromRequest.feeRateWeight().orElse(5)),
|
||||
Optional.of(Math.min(ourFeeRate, paymentOptionsFromRequest.feeRateLimit().orElse(Long.MAX_VALUE))),
|
||||
Optional.of(feeRateLimitExceptIncomingHops),
|
||||
false,
|
||||
Optional.of(pubkey)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import de.cotto.lndmanagej.model.Pubkey;
|
||||
import java.util.Optional;
|
||||
|
||||
public record PaymentOptions(
|
||||
int feeRateWeight,
|
||||
Optional<Integer> feeRateWeight,
|
||||
Optional<Long> feeRateLimit,
|
||||
Optional<Long> feeRateLimitExceptIncomingHops,
|
||||
boolean ignoreFeesForOwnChannels,
|
||||
@@ -14,16 +14,28 @@ public record PaymentOptions(
|
||||
public static final PaymentOptions DEFAULT_PAYMENT_OPTIONS = forFeeRateWeight(0);
|
||||
|
||||
public static PaymentOptions forFeeRateWeight(int feeRateWeight) {
|
||||
return new PaymentOptions(feeRateWeight, Optional.empty(), Optional.empty(), true, Optional.empty());
|
||||
return new PaymentOptions(
|
||||
Optional.of(feeRateWeight),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
true,
|
||||
Optional.empty()
|
||||
);
|
||||
}
|
||||
|
||||
public static PaymentOptions forFeeRateLimit(long feeRateLimit) {
|
||||
return new PaymentOptions(0, Optional.of(feeRateLimit), Optional.empty(), true, Optional.empty());
|
||||
return new PaymentOptions(
|
||||
Optional.of(0),
|
||||
Optional.of(feeRateLimit),
|
||||
Optional.empty(),
|
||||
true,
|
||||
Optional.empty()
|
||||
);
|
||||
}
|
||||
|
||||
public static PaymentOptions forTopUp(long ourFeeRate, long peerFeeRate, Pubkey peer) {
|
||||
return new PaymentOptions(
|
||||
5,
|
||||
Optional.of(5),
|
||||
Optional.of(ourFeeRate),
|
||||
Optional.of(ourFeeRate - peerFeeRate),
|
||||
false,
|
||||
|
||||
@@ -28,6 +28,7 @@ import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
|
||||
import static de.cotto.lndmanagej.model.DecodedPaymentRequestFixtures.DECODED_PAYMENT_REQUEST;
|
||||
import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL;
|
||||
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
|
||||
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;
|
||||
@@ -162,6 +163,113 @@ class TopUpServiceTest {
|
||||
assertTopUp(AMOUNT, DEFAULT_EXPIRY);
|
||||
}
|
||||
|
||||
// CPD-OFF
|
||||
@Test
|
||||
void uses_lower_fee_rate_limit_if_configured() {
|
||||
long feeRateLimit = 111L;
|
||||
PaymentOptions given = new PaymentOptions(
|
||||
Optional.empty(),
|
||||
Optional.of(feeRateLimit),
|
||||
Optional.empty(),
|
||||
true,
|
||||
Optional.empty()
|
||||
);
|
||||
PaymentOptions expected = new PaymentOptions(
|
||||
Optional.of(5),
|
||||
Optional.of(feeRateLimit),
|
||||
Optional.of(OUR_FEE_RATE - PEER_FEE_RATE),
|
||||
false,
|
||||
Optional.of(PUBKEY)
|
||||
);
|
||||
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
||||
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignores_configured_lower_fee_rate_limit_if_too_high() {
|
||||
long feeRateLimit = OUR_FEE_RATE + 1;
|
||||
PaymentOptions given = new PaymentOptions(
|
||||
Optional.empty(),
|
||||
Optional.of(feeRateLimit),
|
||||
Optional.empty(),
|
||||
true,
|
||||
Optional.empty()
|
||||
);
|
||||
PaymentOptions expected = new PaymentOptions(
|
||||
Optional.of(5),
|
||||
Optional.of(OUR_FEE_RATE),
|
||||
Optional.of(OUR_FEE_RATE - PEER_FEE_RATE),
|
||||
false,
|
||||
Optional.of(PUBKEY)
|
||||
);
|
||||
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
||||
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void uses_lower_fee_rate_limit_except_incoming_hops_if_configured() {
|
||||
long feeRateLimitExceptIncomingHops = 0L;
|
||||
PaymentOptions given = new PaymentOptions(
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of(feeRateLimitExceptIncomingHops),
|
||||
true,
|
||||
Optional.empty()
|
||||
);
|
||||
PaymentOptions expected = new PaymentOptions(
|
||||
Optional.of(5),
|
||||
Optional.of(OUR_FEE_RATE),
|
||||
Optional.of(feeRateLimitExceptIncomingHops),
|
||||
false,
|
||||
Optional.of(PUBKEY)
|
||||
);
|
||||
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
||||
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignores_configured_lower_fee_rate_limit_except_incoming_hops_if_too_high() {
|
||||
long feeRateLimitExceptIncomingHops = 2L;
|
||||
PaymentOptions given = new PaymentOptions(
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of(feeRateLimitExceptIncomingHops),
|
||||
true,
|
||||
Optional.empty()
|
||||
);
|
||||
PaymentOptions expected = new PaymentOptions(
|
||||
Optional.of(5),
|
||||
Optional.of(OUR_FEE_RATE),
|
||||
Optional.of(OUR_FEE_RATE - PEER_FEE_RATE),
|
||||
false,
|
||||
Optional.of(PUBKEY)
|
||||
);
|
||||
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
||||
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void uses_fee_rate_weight_if_configured() {
|
||||
int feeRateWeight = 0;
|
||||
PaymentOptions given = new PaymentOptions(
|
||||
Optional.of(feeRateWeight),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
true,
|
||||
Optional.empty()
|
||||
);
|
||||
PaymentOptions expected = new PaymentOptions(
|
||||
Optional.of(feeRateWeight),
|
||||
Optional.of(OUR_FEE_RATE),
|
||||
Optional.of(OUR_FEE_RATE - PEER_FEE_RATE),
|
||||
false,
|
||||
Optional.of(PUBKEY)
|
||||
);
|
||||
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
||||
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
||||
}
|
||||
// CPD-ON
|
||||
|
||||
@Test
|
||||
void uses_configured_expiry() {
|
||||
int expiry = 900;
|
||||
@@ -182,15 +290,31 @@ class TopUpServiceTest {
|
||||
}
|
||||
|
||||
private void assertTopUp(Coins expectedTopUpAmount, Duration expiry) {
|
||||
PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, AMOUNT);
|
||||
verify(grpcInvoices).createPaymentRequest(expectedTopUpAmount, DESCRIPTION, expiry);
|
||||
PaymentOptions paymentOptions = PaymentOptions.forTopUp(OUR_FEE_RATE, PEER_FEE_RATE, PUBKEY);
|
||||
verify(multiPathPaymentSender).payPaymentRequest(DECODED_PAYMENT_REQUEST, paymentOptions);
|
||||
PaymentOptions emptyPaymentOptions = new PaymentOptions(
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
true,
|
||||
Optional.empty()
|
||||
);
|
||||
assertTopUp(expectedTopUpAmount, expiry, emptyPaymentOptions, paymentOptions);
|
||||
}
|
||||
|
||||
private void assertTopUp(
|
||||
Coins expectedTopUpAmount,
|
||||
Duration expiry,
|
||||
PaymentOptions givenPaymentOptions,
|
||||
PaymentOptions expectedPaymentOptions
|
||||
) {
|
||||
PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, 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);
|
||||
PaymentStatus paymentStatus = topUpService.topUp(PUBKEY, AMOUNT, DEFAULT_PAYMENT_OPTIONS);
|
||||
assertThat(paymentStatus.isFailure()).isTrue();
|
||||
assertThat(paymentStatus.getMessages().stream().map(InstantWithString::string)).containsExactly(reason);
|
||||
verifyNoInteractions(multiPathPaymentSender);
|
||||
|
||||
@@ -11,20 +11,35 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class PaymentOptionsTest {
|
||||
@Test
|
||||
void forFeeRateWeight() {
|
||||
assertThat(PaymentOptions.forFeeRateWeight(12))
|
||||
.isEqualTo(new PaymentOptions(12, Optional.empty(), Optional.empty(), true, Optional.empty()));
|
||||
assertThat(PaymentOptions.forFeeRateWeight(12)).isEqualTo(new PaymentOptions(
|
||||
Optional.of(12),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
true,
|
||||
Optional.empty()
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forFeeRateLimit() {
|
||||
assertThat(PaymentOptions.forFeeRateLimit(123))
|
||||
.isEqualTo(new PaymentOptions(0, Optional.of(123L), Optional.empty(), true, Optional.empty()));
|
||||
assertThat(PaymentOptions.forFeeRateLimit(123)).isEqualTo(new PaymentOptions(
|
||||
Optional.of(0),
|
||||
Optional.of(123L),
|
||||
Optional.empty(),
|
||||
true,
|
||||
Optional.empty()
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forTopUp() {
|
||||
assertThat(PaymentOptions.forTopUp(123, 100, PUBKEY))
|
||||
.isEqualTo(new PaymentOptions(5, Optional.of(123L), Optional.of(23L), false, Optional.of(PUBKEY)));
|
||||
assertThat(PaymentOptions.forTopUp(123, 100, PUBKEY)).isEqualTo(new PaymentOptions(
|
||||
Optional.of(5),
|
||||
Optional.of(123L),
|
||||
Optional.of(23L),
|
||||
false,
|
||||
Optional.of(PUBKEY)
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -17,20 +17,26 @@ import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
|
||||
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3;
|
||||
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_5;
|
||||
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_4;
|
||||
import static de.cotto.lndmanagej.model.RouteFixtures.ROUTE;
|
||||
import static de.cotto.lndmanagej.pickhardtpayments.model.MultiPathPaymentFixtures.MULTI_PATH_PAYMENT;
|
||||
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.core.Is.is;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@@ -40,6 +46,20 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
class PickhardtPaymentsControllerIT {
|
||||
private static final String PREFIX = "/beta/pickhardt-payments";
|
||||
private static final String PAYMENT_REQUEST = "xxx";
|
||||
private static final PaymentOptions PAYMENT_OPTIONS = new PaymentOptions(
|
||||
Optional.of(123),
|
||||
Optional.of(999L),
|
||||
Optional.of(777L),
|
||||
false,
|
||||
Optional.of(PUBKEY_4)
|
||||
);
|
||||
private static final String DTO_AS_STRING = "{" +
|
||||
" \"feeRateWeight\": 123," +
|
||||
" \"feeRateLimit\": 999," +
|
||||
" \"feeRateLimitExceptIncomingHops\": 777," +
|
||||
" \"ignoreFeesForOwnChannels\": false," +
|
||||
" \"peer\": \"000000000000000000000000000000000000000000000000000000000000000004\"" +
|
||||
"}";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@@ -73,12 +93,11 @@ class PickhardtPaymentsControllerIT {
|
||||
}
|
||||
|
||||
@Test
|
||||
void payPaymentRequest_with_fee_rate_weight() throws Exception {
|
||||
int feeRateWeight = 987;
|
||||
PaymentOptions paymentOptions = PaymentOptions.forFeeRateWeight(feeRateWeight);
|
||||
when(multiPathPaymentSender.payPaymentRequest(PAYMENT_REQUEST, paymentOptions)).thenReturn(paymentStatus);
|
||||
String url = "%s/pay-payment-request/%s/fee-rate-weight/%d".formatted(PREFIX, PAYMENT_REQUEST, feeRateWeight);
|
||||
mockMvc.perform(get(url)).andExpect(status().isOk());
|
||||
void payPaymentRequest_with_payment_options() throws Exception {
|
||||
when(multiPathPaymentSender.payPaymentRequest(PAYMENT_REQUEST, PAYMENT_OPTIONS)).thenReturn(paymentStatus);
|
||||
String url = "%s/pay-payment-request/%s".formatted(PREFIX, PAYMENT_REQUEST);
|
||||
mockMvc.perform(post(url).contentType(APPLICATION_JSON).content(DTO_AS_STRING))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -118,15 +137,13 @@ class PickhardtPaymentsControllerIT {
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendTo_with_fee_rate_weight() throws Exception {
|
||||
int feeRateWeight = 999;
|
||||
void sendTo_with_payment_options() throws Exception {
|
||||
Coins amount = MULTI_PATH_PAYMENT.amount();
|
||||
PaymentOptions paymentOptions = PaymentOptions.forFeeRateWeight(feeRateWeight);
|
||||
when(multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY, amount, paymentOptions))
|
||||
when(multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY, amount, PAYMENT_OPTIONS))
|
||||
.thenReturn(MULTI_PATH_PAYMENT);
|
||||
String url = "%s/to/%s/amount/%d/fee-rate-weight/%d"
|
||||
.formatted(PREFIX, PUBKEY, amount.satoshis(), feeRateWeight);
|
||||
mockMvc.perform(get(url)).andExpect(status().isOk());
|
||||
String url = "%s/to/%s/amount/%d".formatted(PREFIX, PUBKEY, amount.satoshis());
|
||||
mockMvc.perform(post(url).contentType(APPLICATION_JSON).content(DTO_AS_STRING))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -152,21 +169,27 @@ class PickhardtPaymentsControllerIT {
|
||||
}
|
||||
|
||||
@Test
|
||||
void send_with_fee_rate_weight() throws Exception {
|
||||
int feeRateWeight = 999;
|
||||
void send_with_payment_options() throws Exception {
|
||||
Coins amount = MULTI_PATH_PAYMENT.amount();
|
||||
PaymentOptions paymentOptions = PaymentOptions.forFeeRateWeight(feeRateWeight);
|
||||
when(multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, amount, paymentOptions))
|
||||
when(multiPathPaymentSplitter.getMultiPathPayment(PUBKEY, PUBKEY_2, amount, PAYMENT_OPTIONS))
|
||||
.thenReturn(MULTI_PATH_PAYMENT);
|
||||
String url = "%s/from/%s/to/%s/amount/%d/fee-rate-weight/%d"
|
||||
.formatted(PREFIX, PUBKEY, PUBKEY_2, amount.satoshis(), feeRateWeight);
|
||||
mockMvc.perform(get(url)).andExpect(status().isOk());
|
||||
String url = "%s/from/%s/to/%s/amount/%d".formatted(PREFIX, PUBKEY, PUBKEY_2, amount.satoshis());
|
||||
mockMvc.perform(post(url).contentType(APPLICATION_JSON).content(DTO_AS_STRING))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void topUp() throws Exception {
|
||||
String url = "%s/top-up/%s/amount/%s".formatted(PREFIX, PUBKEY, "123");
|
||||
mockMvc.perform(get(url)).andExpect(status().isOk());
|
||||
verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123));
|
||||
verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123), DEFAULT_PAYMENT_OPTIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void topUp_with_payment_options() throws Exception {
|
||||
when(topUpService.topUp(any(), any(), any())).thenReturn(new PaymentStatus(HexString.EMPTY));
|
||||
String url = "%s/top-up/%s/amount/%s".formatted(PREFIX, PUBKEY, "123");
|
||||
mockMvc.perform(post(url).contentType(APPLICATION_JSON).content(DTO_AS_STRING)).andExpect(status().isOk());
|
||||
verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123), PAYMENT_OPTIONS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.cotto.lndmanagej.controller;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import de.cotto.lndmanagej.controller.dto.MultiPathPaymentDto;
|
||||
import de.cotto.lndmanagej.controller.dto.PaymentOptionsDto;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.Pubkey;
|
||||
import de.cotto.lndmanagej.pickhardtpayments.MultiPathPaymentSender;
|
||||
@@ -14,16 +15,19 @@ import de.cotto.lndmanagej.service.GraphService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
|
||||
import static org.springframework.http.MediaType.APPLICATION_NDJSON;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/beta/pickhardt-payments/")
|
||||
public class PickhardtPaymentsController {
|
||||
private static final PaymentOptionsDto PAYMENT_OPTIONS_DTO = PaymentOptionsDto.DEFAULT;
|
||||
|
||||
private final MultiPathPaymentSplitter multiPathPaymentSplitter;
|
||||
private final MultiPathPaymentSender multiPathPaymentSender;
|
||||
private final PaymentStatusStream paymentStatusStream;
|
||||
@@ -47,56 +51,41 @@ public class PickhardtPaymentsController {
|
||||
@Timed
|
||||
@GetMapping("/pay-payment-request/{paymentRequest}")
|
||||
public ResponseEntity<StreamingResponseBody> payPaymentRequest(@PathVariable String paymentRequest) {
|
||||
return payPaymentRequest(paymentRequest, DEFAULT_PAYMENT_OPTIONS.feeRateWeight());
|
||||
return payPaymentRequest(paymentRequest, PAYMENT_OPTIONS_DTO);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GetMapping("/pay-payment-request/{paymentRequest}/fee-rate-weight/{feeRateWeight}")
|
||||
@PostMapping("/pay-payment-request/{paymentRequest}")
|
||||
public ResponseEntity<StreamingResponseBody> payPaymentRequest(
|
||||
@PathVariable String paymentRequest,
|
||||
@PathVariable int feeRateWeight
|
||||
@RequestBody PaymentOptionsDto paymentOptionsDto
|
||||
) {
|
||||
PaymentOptions paymentOptions = PaymentOptions.forFeeRateWeight(feeRateWeight);
|
||||
PaymentOptions paymentOptions = paymentOptionsDto.toModel();
|
||||
PaymentStatus paymentStatus = multiPathPaymentSender.payPaymentRequest(paymentRequest, paymentOptions);
|
||||
return toStream(paymentStatus);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GetMapping("/to/{pubkey}/amount/{amount}/fee-rate-weight/{feeRateWeight}")
|
||||
public MultiPathPaymentDto sendTo(
|
||||
@PathVariable Pubkey pubkey,
|
||||
@PathVariable long amount,
|
||||
@PathVariable int feeRateWeight
|
||||
) {
|
||||
Coins coins = Coins.ofSatoshis(amount);
|
||||
PaymentOptions paymentOptions = PaymentOptions.forFeeRateWeight(feeRateWeight);
|
||||
MultiPathPayment multiPathPaymentTo =
|
||||
multiPathPaymentSplitter.getMultiPathPaymentTo(pubkey, coins, paymentOptions);
|
||||
return MultiPathPaymentDto.fromModel(multiPathPaymentTo);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GetMapping("/to/{pubkey}/amount/{amount}")
|
||||
public MultiPathPaymentDto sendTo(
|
||||
@PathVariable Pubkey pubkey,
|
||||
@PathVariable long amount
|
||||
) {
|
||||
return sendTo(pubkey, amount, DEFAULT_PAYMENT_OPTIONS.feeRateWeight());
|
||||
return sendTo(pubkey, amount, PAYMENT_OPTIONS_DTO);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GetMapping("/from/{source}/to/{target}/amount/{amount}/fee-rate-weight/{feeRateWeight}")
|
||||
public MultiPathPaymentDto send(
|
||||
@PathVariable Pubkey source,
|
||||
@PathVariable Pubkey target,
|
||||
@PostMapping("/to/{pubkey}/amount/{amount}")
|
||||
public MultiPathPaymentDto sendTo(
|
||||
@PathVariable Pubkey pubkey,
|
||||
@PathVariable long amount,
|
||||
@PathVariable int feeRateWeight
|
||||
@RequestBody PaymentOptionsDto paymentOptionsDto
|
||||
) {
|
||||
Coins coins = Coins.ofSatoshis(amount);
|
||||
PaymentOptions paymentOptions = PaymentOptions.forFeeRateWeight(feeRateWeight);
|
||||
MultiPathPayment multiPathPayment =
|
||||
multiPathPaymentSplitter.getMultiPathPayment(source, target, coins, paymentOptions);
|
||||
return MultiPathPaymentDto.fromModel(multiPathPayment);
|
||||
PaymentOptions paymentOptions = paymentOptionsDto.toModel();
|
||||
MultiPathPayment multiPathPaymentTo =
|
||||
multiPathPaymentSplitter.getMultiPathPaymentTo(pubkey, coins, paymentOptions);
|
||||
return MultiPathPaymentDto.fromModel(multiPathPaymentTo);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -106,13 +95,38 @@ public class PickhardtPaymentsController {
|
||||
@PathVariable Pubkey target,
|
||||
@PathVariable long amount
|
||||
) {
|
||||
return send(source, target, amount, DEFAULT_PAYMENT_OPTIONS.feeRateWeight());
|
||||
return send(source, target, amount, PAYMENT_OPTIONS_DTO);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PostMapping("/from/{source}/to/{target}/amount/{amount}")
|
||||
public MultiPathPaymentDto send(
|
||||
@PathVariable Pubkey source,
|
||||
@PathVariable Pubkey target,
|
||||
@PathVariable long amount,
|
||||
@RequestBody PaymentOptionsDto paymentOptionsDto
|
||||
) {
|
||||
Coins coins = Coins.ofSatoshis(amount);
|
||||
PaymentOptions paymentOptions = paymentOptionsDto.toModel();
|
||||
MultiPathPayment multiPathPayment =
|
||||
multiPathPaymentSplitter.getMultiPathPayment(source, target, coins, paymentOptions);
|
||||
return MultiPathPaymentDto.fromModel(multiPathPayment);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GetMapping("/top-up/{pubkey}/amount/{amount}")
|
||||
public ResponseEntity<StreamingResponseBody> topUp(@PathVariable Pubkey pubkey, @PathVariable long amount) {
|
||||
PaymentStatus paymentStatus = topUpService.topUp(pubkey, Coins.ofSatoshis(amount));
|
||||
return topUp(pubkey, amount, PAYMENT_OPTIONS_DTO);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PostMapping("/top-up/{pubkey}/amount/{amount}")
|
||||
public ResponseEntity<StreamingResponseBody> topUp(
|
||||
@PathVariable Pubkey pubkey,
|
||||
@PathVariable long amount,
|
||||
@RequestBody PaymentOptionsDto paymentOptionsDto
|
||||
) {
|
||||
PaymentStatus paymentStatus = topUpService.topUp(pubkey, Coins.ofSatoshis(amount), paymentOptionsDto.toModel());
|
||||
return toStream(paymentStatus);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package de.cotto.lndmanagej.controller.dto;
|
||||
|
||||
import de.cotto.lndmanagej.model.Pubkey;
|
||||
import de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Optional;
|
||||
|
||||
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
|
||||
|
||||
public class PaymentOptionsDto {
|
||||
public static final PaymentOptionsDto DEFAULT = new PaymentOptionsDto();
|
||||
|
||||
static {
|
||||
DEFAULT.setFeeRateWeight(DEFAULT_PAYMENT_OPTIONS.feeRateWeight().orElse(null));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Integer feeRateWeight;
|
||||
@Nullable
|
||||
private Long feeRateLimit;
|
||||
private boolean ignoreFeesForOwnChannels;
|
||||
@Nullable
|
||||
private Pubkey peer;
|
||||
@Nullable
|
||||
private Long feeRateLimitExceptIncomingHops;
|
||||
|
||||
public PaymentOptionsDto() {
|
||||
ignoreFeesForOwnChannels = DEFAULT_PAYMENT_OPTIONS.ignoreFeesForOwnChannels();
|
||||
}
|
||||
|
||||
public PaymentOptions toModel() {
|
||||
|
||||
return new PaymentOptions(
|
||||
Optional.ofNullable(feeRateWeight),
|
||||
Optional.ofNullable(feeRateLimit),
|
||||
Optional.ofNullable(feeRateLimitExceptIncomingHops),
|
||||
ignoreFeesForOwnChannels,
|
||||
Optional.ofNullable(peer)
|
||||
);
|
||||
}
|
||||
|
||||
public void setFeeRateWeight(@Nullable Integer feeRateWeight) {
|
||||
this.feeRateWeight = feeRateWeight;
|
||||
}
|
||||
|
||||
public void setFeeRateLimit(@Nullable Long feeRateLimit) {
|
||||
this.feeRateLimit = feeRateLimit;
|
||||
}
|
||||
|
||||
public void setIgnoreFeesForOwnChannels(boolean ignoreFeesForOwnChannels) {
|
||||
this.ignoreFeesForOwnChannels = ignoreFeesForOwnChannels;
|
||||
}
|
||||
|
||||
public void setPeer(@Nullable Pubkey peer) {
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
public void setFeeRateLimitExceptIncomingHops(@Nullable Long feeRateLimitExceptIncomingHops) {
|
||||
this.feeRateLimitExceptIncomingHops = feeRateLimitExceptIncomingHops;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.cotto.lndmanagej.controller;
|
||||
|
||||
import de.cotto.lndmanagej.controller.dto.MultiPathPaymentDto;
|
||||
import de.cotto.lndmanagej.controller.dto.PaymentOptionsDto;
|
||||
import de.cotto.lndmanagej.model.Coins;
|
||||
import de.cotto.lndmanagej.model.HexString;
|
||||
import de.cotto.lndmanagej.pickhardtpayments.MultiPathPaymentSender;
|
||||
@@ -18,9 +19,11 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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.model.PubkeyFixtures.PUBKEY_4;
|
||||
import static de.cotto.lndmanagej.pickhardtpayments.model.MultiPathPaymentFixtures.MULTI_PATH_PAYMENT;
|
||||
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -35,6 +38,27 @@ class PickhardtPaymentsControllerTest {
|
||||
private static final String PAYMENT_REQUEST = "xxx";
|
||||
private static final String STREAM_RESPONSE = "beep beep boop!";
|
||||
|
||||
private static final PaymentOptions PAYMENT_OPTIONS;
|
||||
private static final PaymentOptionsDto PAYMENT_OPTIONS_DTO;
|
||||
|
||||
static {
|
||||
PAYMENT_OPTIONS = new PaymentOptions(
|
||||
Optional.of(123),
|
||||
Optional.of(999L),
|
||||
Optional.of(777L),
|
||||
false,
|
||||
Optional.of(PUBKEY_4)
|
||||
);
|
||||
PAYMENT_OPTIONS_DTO = new PaymentOptionsDto();
|
||||
PAYMENT_OPTIONS_DTO.setFeeRateWeight(PAYMENT_OPTIONS.feeRateWeight().orElse(null));
|
||||
PAYMENT_OPTIONS_DTO.setFeeRateLimit(PAYMENT_OPTIONS.feeRateLimit().orElse(null));
|
||||
PAYMENT_OPTIONS_DTO.setFeeRateLimitExceptIncomingHops(
|
||||
PAYMENT_OPTIONS.feeRateLimitExceptIncomingHops().orElse(null)
|
||||
);
|
||||
PAYMENT_OPTIONS_DTO.setIgnoreFeesForOwnChannels(PAYMENT_OPTIONS.ignoreFeesForOwnChannels());
|
||||
PAYMENT_OPTIONS_DTO.setPeer(PAYMENT_OPTIONS.peer().orElse(null));
|
||||
}
|
||||
|
||||
@InjectMocks
|
||||
private PickhardtPaymentsController controller;
|
||||
|
||||
@@ -70,10 +94,10 @@ class PickhardtPaymentsControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void payPaymentRequest_with_fee_rate_weight() {
|
||||
void payPaymentRequest_with_payment_options() {
|
||||
PaymentOptions paymentOptions = PaymentOptions.forFeeRateWeight(456);
|
||||
when(multiPathPaymentSender.payPaymentRequest(PAYMENT_REQUEST, paymentOptions)).thenReturn(paymentStatus);
|
||||
assertThat(controller.payPaymentRequest(PAYMENT_REQUEST, 456).getStatusCode())
|
||||
assertThat(controller.payPaymentRequest(PAYMENT_REQUEST, withFeeRateWeight(456)).getStatusCode())
|
||||
.isEqualTo(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@@ -86,12 +110,12 @@ class PickhardtPaymentsControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendTo_with_fee_rate_weight() {
|
||||
void sendTo_with_payment_options() {
|
||||
int feeRateWeight = 10;
|
||||
PaymentOptions paymentOptions = PaymentOptions.forFeeRateWeight(feeRateWeight);
|
||||
when(multiPathPaymentSplitter.getMultiPathPaymentTo(PUBKEY, Coins.ofSatoshis(456), paymentOptions))
|
||||
.thenReturn(MULTI_PATH_PAYMENT);
|
||||
assertThat(controller.sendTo(PUBKEY, 456, feeRateWeight))
|
||||
assertThat(controller.sendTo(PUBKEY, 456, withFeeRateWeight(feeRateWeight)))
|
||||
.isEqualTo(MultiPathPaymentDto.fromModel(MULTI_PATH_PAYMENT));
|
||||
}
|
||||
|
||||
@@ -108,22 +132,27 @@ class PickhardtPaymentsControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void send_with_fee_rate_weight() {
|
||||
int feeRateWeight = 20;
|
||||
void send_with_payment_options() {
|
||||
when(multiPathPaymentSplitter.getMultiPathPayment(
|
||||
PUBKEY,
|
||||
PUBKEY_2,
|
||||
Coins.ofSatoshis(123),
|
||||
PaymentOptions.forFeeRateWeight(feeRateWeight)
|
||||
PAYMENT_OPTIONS
|
||||
)).thenReturn(MULTI_PATH_PAYMENT);
|
||||
assertThat(controller.send(PUBKEY, PUBKEY_2, 123, feeRateWeight))
|
||||
assertThat(controller.send(PUBKEY, PUBKEY_2, 123, PAYMENT_OPTIONS_DTO))
|
||||
.isEqualTo(MultiPathPaymentDto.fromModel(MULTI_PATH_PAYMENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void topUp() {
|
||||
controller.topUp(PUBKEY, 123);
|
||||
verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123));
|
||||
assertThat(controller.topUp(PUBKEY, 123).getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123), DEFAULT_PAYMENT_OPTIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void topUp_with_payment_options() {
|
||||
assertThat(controller.topUp(PUBKEY, 123, PAYMENT_OPTIONS_DTO).getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
verify(topUpService).topUp(PUBKEY, Coins.ofSatoshis(123), PAYMENT_OPTIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -131,4 +160,10 @@ class PickhardtPaymentsControllerTest {
|
||||
controller.resetGraph();
|
||||
verify(graphService).resetCache();
|
||||
}
|
||||
|
||||
private static PaymentOptionsDto withFeeRateWeight(int feeRateWeight) {
|
||||
PaymentOptionsDto dto = new PaymentOptionsDto();
|
||||
dto.setFeeRateWeight(feeRateWeight);
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.cotto.lndmanagej.controller.dto;
|
||||
|
||||
import de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
|
||||
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PaymentOptionsDtoTest {
|
||||
@Test
|
||||
void toModel_default() {
|
||||
assertThat(PaymentOptionsDto.DEFAULT.toModel()).isEqualTo(DEFAULT_PAYMENT_OPTIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void just_fee_rate_weight() {
|
||||
PaymentOptionsDto dto = new PaymentOptionsDto();
|
||||
dto.setFeeRateWeight(123);
|
||||
assertThat(dto.toModel()).isEqualTo(PaymentOptions.forFeeRateWeight(123));
|
||||
}
|
||||
|
||||
@Test
|
||||
void feeRateLimit() {
|
||||
PaymentOptionsDto dto = new PaymentOptionsDto();
|
||||
long feeRateLimit = 555;
|
||||
dto.setFeeRateLimit(feeRateLimit);
|
||||
PaymentOptions expected = new PaymentOptions(
|
||||
Optional.empty(),
|
||||
Optional.of(feeRateLimit),
|
||||
Optional.empty(),
|
||||
DEFAULT_PAYMENT_OPTIONS.ignoreFeesForOwnChannels(),
|
||||
Optional.empty()
|
||||
);
|
||||
assertThat(dto.toModel()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void feeRateLimit_and_feeRateLimitExceptIncomingHops() {
|
||||
PaymentOptionsDto dto = new PaymentOptionsDto();
|
||||
long feeRateLimit = 555;
|
||||
long feeRateLimitExceptIncomingHops = 777;
|
||||
dto.setFeeRateLimit(feeRateLimit);
|
||||
dto.setFeeRateLimitExceptIncomingHops(feeRateLimitExceptIncomingHops);
|
||||
PaymentOptions expected = new PaymentOptions(
|
||||
Optional.empty(),
|
||||
Optional.of(feeRateLimit),
|
||||
Optional.of(feeRateLimitExceptIncomingHops),
|
||||
DEFAULT_PAYMENT_OPTIONS.ignoreFeesForOwnChannels(),
|
||||
Optional.empty()
|
||||
);
|
||||
assertThat(dto.toModel()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoreFeesForOwnChannels() {
|
||||
PaymentOptionsDto dto = new PaymentOptionsDto();
|
||||
dto.setIgnoreFeesForOwnChannels(false);
|
||||
PaymentOptions expected = new PaymentOptions(
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
false,
|
||||
Optional.empty()
|
||||
);
|
||||
assertThat(dto.toModel()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void peer() {
|
||||
PaymentOptionsDto dto = new PaymentOptionsDto();
|
||||
dto.setPeer(PUBKEY);
|
||||
PaymentOptions expected = new PaymentOptions(
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
DEFAULT_PAYMENT_OPTIONS.ignoreFeesForOwnChannels(),
|
||||
Optional.of(PUBKEY)
|
||||
);
|
||||
assertThat(dto.toModel()).isEqualTo(expected);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user