Merge branch 'main' into fix-5746

This commit is contained in:
Carsten Otto
2022-05-27 23:09:47 +02:00
11 changed files with 472 additions and 83 deletions

View File

@@ -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

View File

@@ -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()
);

View File

@@ -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)
);
}
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}