mirror of
https://github.com/aljazceru/lnd-manageJ.git
synced 2025-12-17 14:14:24 +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).
|
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
|
* Invoices (payment requests) created for top-up payments expiry after 30 minutes. This value can be configured as
|
||||||
`expiry_seconds=`.
|
`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}`
|
* 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 lower the fee rate limit (values higher than the computed fee rate limit are ignored)
|
||||||
* allows you to specify a different fee rate weight
|
* allows you to specify a different fee rate weight
|
||||||
* The value provided as `ignoreFeesForOwnChannels` is ignored, for top-up such fees are never ignored
|
* 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
|
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.
|
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) {
|
if (feeRate >= feeRateLimit) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (isEdgeToUnwantedFirstHop(channelEdge, paymentOptions, pubkey)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (isIncomingEdge(channelEdge, pubkey)) {
|
if (isIncomingEdge(channelEdge, pubkey)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -137,6 +140,22 @@ public class EdgeComputation {
|
|||||||
return feeRate >= feeRateLimitFirstHops;
|
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) {
|
private boolean isIncomingEdge(DirectedChannelEdge channelEdge, Pubkey ownPubkey) {
|
||||||
return ownPubkey.equals(channelEdge.target());
|
return ownPubkey.equals(channelEdge.target());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,12 @@ public class TopUpService {
|
|||||||
this.policyService = policyService;
|
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)) {
|
if (noChannelWith(pubkey)) {
|
||||||
String alias = nodeService.getAlias(pubkey);
|
String alias = nodeService.getAlias(pubkey);
|
||||||
return PaymentStatus.createFailure("No channel with %s (%s)".formatted(pubkey, alias));
|
return PaymentStatus.createFailure("No channel with %s (%s)".formatted(pubkey, alias));
|
||||||
@@ -71,10 +76,15 @@ public class TopUpService {
|
|||||||
return PaymentStatus.createFailure(reason);
|
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 ourFeeRate = policyService.getMinimumFeeRateTo(pubkey).orElse(0L);
|
||||||
long peerFeeRate = policyService.getMinimumFeeRateFrom(pubkey).orElse(0L);
|
long peerFeeRate = policyService.getMinimumFeeRateFrom(pubkey).orElse(0L);
|
||||||
if (peerFeeRate >= ourFeeRate) {
|
if (peerFeeRate >= ourFeeRate) {
|
||||||
@@ -88,8 +98,10 @@ public class TopUpService {
|
|||||||
String alias = nodeService.getAlias(pubkey);
|
String alias = nodeService.getAlias(pubkey);
|
||||||
return PaymentStatus.createFailure("Unable to create payment request (%s, %s)".formatted(pubkey, alias));
|
return PaymentStatus.createFailure("Unable to create payment request (%s, %s)".formatted(pubkey, alias));
|
||||||
}
|
}
|
||||||
PaymentOptions paymentOptions = getPaymentOptions(pubkey, ourFeeRate, peerFeeRate, paymentOptionsFromRequest);
|
return multiPathPaymentSender.payPaymentRequest(
|
||||||
return multiPathPaymentSender.payPaymentRequest(paymentRequest, paymentOptions);
|
paymentRequest,
|
||||||
|
getPaymentOptions(pubkey, peerForFirstHop, ourFeeRate, peerFeeRate, paymentOptions)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Coins getThreshold() {
|
private Coins getThreshold() {
|
||||||
@@ -117,16 +129,25 @@ public class TopUpService {
|
|||||||
|
|
||||||
private PaymentOptions getPaymentOptions(
|
private PaymentOptions getPaymentOptions(
|
||||||
Pubkey pubkey,
|
Pubkey pubkey,
|
||||||
|
Optional<Pubkey> peerForFirstHop,
|
||||||
long ourFeeRate,
|
long ourFeeRate,
|
||||||
long peerFeeRate,
|
long peerFeeRate,
|
||||||
PaymentOptions paymentOptionsFromRequest
|
PaymentOptions paymentOptions
|
||||||
) {
|
) {
|
||||||
long feeRateLimit = Math.min(ourFeeRate, paymentOptionsFromRequest.feeRateLimit().orElse(Long.MAX_VALUE));
|
int feeRateWeight = paymentOptions.feeRateWeight().orElse(5);
|
||||||
return PaymentOptions.forTopUp(
|
long feeRateLimit = Math.min(ourFeeRate, paymentOptions.feeRateLimit().orElse(Long.MAX_VALUE));
|
||||||
paymentOptionsFromRequest.feeRateWeight().orElse(5),
|
|
||||||
|
return peerForFirstHop.map(value -> PaymentOptions.forTopUp(
|
||||||
|
feeRateWeight,
|
||||||
|
feeRateLimit,
|
||||||
|
peerFeeRate,
|
||||||
|
pubkey,
|
||||||
|
value
|
||||||
|
)).orElseGet(() -> PaymentOptions.forTopUp(
|
||||||
|
feeRateWeight,
|
||||||
feeRateLimit,
|
feeRateLimit,
|
||||||
peerFeeRate,
|
peerFeeRate,
|
||||||
pubkey
|
pubkey
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ public record PaymentOptions(
|
|||||||
Optional<Long> feeRateLimit,
|
Optional<Long> feeRateLimit,
|
||||||
Optional<Long> feeRateLimitExceptIncomingHops,
|
Optional<Long> feeRateLimitExceptIncomingHops,
|
||||||
boolean ignoreFeesForOwnChannels,
|
boolean ignoreFeesForOwnChannels,
|
||||||
Optional<Pubkey> peer
|
Optional<Pubkey> peer,
|
||||||
|
Optional<Pubkey> peerForFirstHop
|
||||||
) {
|
) {
|
||||||
public static final PaymentOptions DEFAULT_PAYMENT_OPTIONS = forFeeRateWeight(0);
|
public static final PaymentOptions DEFAULT_PAYMENT_OPTIONS = forFeeRateWeight(0);
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ public record PaymentOptions(
|
|||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -29,6 +31,7 @@ public record PaymentOptions(
|
|||||||
Optional.of(feeRateLimit),
|
Optional.of(feeRateLimit),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -39,7 +42,25 @@ public record PaymentOptions(
|
|||||||
Optional.of(feeRateLimit),
|
Optional.of(feeRateLimit),
|
||||||
Optional.of(Math.max(0, feeRateLimit - peerFeeRate)),
|
Optional.of(Math.max(0, feeRateLimit - peerFeeRate)),
|
||||||
false,
|
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.LocalOpenChannelFixtures;
|
||||||
import de.cotto.lndmanagej.model.Policy;
|
import de.cotto.lndmanagej.model.Policy;
|
||||||
import de.cotto.lndmanagej.model.Pubkey;
|
import de.cotto.lndmanagej.model.Pubkey;
|
||||||
|
import de.cotto.lndmanagej.pickhardtpayments.model.EdgesWithLiquidityInformation;
|
||||||
import de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions;
|
import de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions;
|
||||||
import de.cotto.lndmanagej.service.BalanceService;
|
import de.cotto.lndmanagej.service.BalanceService;
|
||||||
import de.cotto.lndmanagej.service.ChannelService;
|
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.PolicyFixtures.POLICY_WITH_BASE_FEE;
|
||||||
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
|
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_2;
|
||||||
|
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_3;
|
||||||
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_4;
|
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_4;
|
||||||
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
|
import static de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@@ -312,6 +314,22 @@ class EdgeComputationTest {
|
|||||||
.contains(EdgeWithLiquidityInformation.forUpperBound(EDGE, EDGE.capacity()));
|
.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
|
@Test
|
||||||
void getEdgeWithLiquidityInformation_default() {
|
void getEdgeWithLiquidityInformation_default() {
|
||||||
when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY_4);
|
when(grpcGetInfo.getPubkey()).thenReturn(PUBKEY_4);
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ class TopUpServiceTest {
|
|||||||
Optional.of(feeRateLimit),
|
Optional.of(feeRateLimit),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
PaymentOptions expected = new PaymentOptions(
|
PaymentOptions expected = new PaymentOptions(
|
||||||
@@ -185,7 +186,8 @@ class TopUpServiceTest {
|
|||||||
Optional.of(feeRateLimit),
|
Optional.of(feeRateLimit),
|
||||||
Optional.of(feeRateLimit - peerFeeRate),
|
Optional.of(feeRateLimit - peerFeeRate),
|
||||||
false,
|
false,
|
||||||
Optional.of(PUBKEY)
|
Optional.of(PUBKEY),
|
||||||
|
Optional.empty()
|
||||||
);
|
);
|
||||||
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
||||||
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
||||||
@@ -203,6 +205,7 @@ class TopUpServiceTest {
|
|||||||
Optional.of(feeRateLimit),
|
Optional.of(feeRateLimit),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
PaymentOptions expected = new PaymentOptions(
|
PaymentOptions expected = new PaymentOptions(
|
||||||
@@ -210,7 +213,8 @@ class TopUpServiceTest {
|
|||||||
Optional.of(ourFeeRate),
|
Optional.of(ourFeeRate),
|
||||||
Optional.of(ourFeeRate - peerFeeRate),
|
Optional.of(ourFeeRate - peerFeeRate),
|
||||||
false,
|
false,
|
||||||
Optional.of(PUBKEY)
|
Optional.of(PUBKEY),
|
||||||
|
Optional.empty()
|
||||||
);
|
);
|
||||||
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
||||||
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
||||||
@@ -228,6 +232,7 @@ class TopUpServiceTest {
|
|||||||
Optional.of(feeRateLimit),
|
Optional.of(feeRateLimit),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
PaymentOptions expected = new PaymentOptions(
|
PaymentOptions expected = new PaymentOptions(
|
||||||
@@ -235,7 +240,8 @@ class TopUpServiceTest {
|
|||||||
Optional.of(feeRateLimit),
|
Optional.of(feeRateLimit),
|
||||||
Optional.of(feeRateLimit - peerFeeRate),
|
Optional.of(feeRateLimit - peerFeeRate),
|
||||||
false,
|
false,
|
||||||
Optional.of(PUBKEY)
|
Optional.of(PUBKEY),
|
||||||
|
Optional.empty()
|
||||||
);
|
);
|
||||||
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
||||||
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
||||||
@@ -253,6 +259,7 @@ class TopUpServiceTest {
|
|||||||
Optional.of(feeRateLimit),
|
Optional.of(feeRateLimit),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
PaymentOptions expected = new PaymentOptions(
|
PaymentOptions expected = new PaymentOptions(
|
||||||
@@ -260,7 +267,8 @@ class TopUpServiceTest {
|
|||||||
Optional.of(ourFeeRate),
|
Optional.of(ourFeeRate),
|
||||||
Optional.of(ourFeeRate - peerFeeRate),
|
Optional.of(ourFeeRate - peerFeeRate),
|
||||||
false,
|
false,
|
||||||
Optional.of(PUBKEY)
|
Optional.of(PUBKEY),
|
||||||
|
Optional.empty()
|
||||||
);
|
);
|
||||||
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
||||||
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
||||||
@@ -278,6 +286,7 @@ class TopUpServiceTest {
|
|||||||
Optional.of(feeRateLimit),
|
Optional.of(feeRateLimit),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
PaymentOptions expected = new PaymentOptions(
|
PaymentOptions expected = new PaymentOptions(
|
||||||
@@ -285,7 +294,8 @@ class TopUpServiceTest {
|
|||||||
Optional.of(feeRateLimit),
|
Optional.of(feeRateLimit),
|
||||||
Optional.of(0L),
|
Optional.of(0L),
|
||||||
false,
|
false,
|
||||||
Optional.of(PUBKEY)
|
Optional.of(PUBKEY),
|
||||||
|
Optional.empty()
|
||||||
);
|
);
|
||||||
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
||||||
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
||||||
@@ -299,6 +309,7 @@ class TopUpServiceTest {
|
|||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
PaymentOptions expected = new PaymentOptions(
|
PaymentOptions expected = new PaymentOptions(
|
||||||
@@ -306,7 +317,8 @@ class TopUpServiceTest {
|
|||||||
Optional.of(OUR_FEE_RATE),
|
Optional.of(OUR_FEE_RATE),
|
||||||
Optional.of(OUR_FEE_RATE - PEER_FEE_RATE),
|
Optional.of(OUR_FEE_RATE - PEER_FEE_RATE),
|
||||||
false,
|
false,
|
||||||
Optional.of(PUBKEY)
|
Optional.of(PUBKEY),
|
||||||
|
Optional.empty()
|
||||||
);
|
);
|
||||||
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
when(balanceService.getAvailableLocalBalanceForPeer(PUBKEY)).thenReturn(Coins.NONE);
|
||||||
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
assertTopUp(AMOUNT, DEFAULT_EXPIRY, given, expected);
|
||||||
@@ -339,6 +351,7 @@ class TopUpServiceTest {
|
|||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
assertTopUp(expectedTopUpAmount, expiry, emptyPaymentOptions, paymentOptions);
|
assertTopUp(expectedTopUpAmount, expiry, emptyPaymentOptions, paymentOptions);
|
||||||
@@ -350,14 +363,14 @@ class TopUpServiceTest {
|
|||||||
PaymentOptions givenPaymentOptions,
|
PaymentOptions givenPaymentOptions,
|
||||||
PaymentOptions expectedPaymentOptions
|
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(grpcInvoices).createPaymentRequest(expectedTopUpAmount, DESCRIPTION, expiry);
|
||||||
verify(multiPathPaymentSender).payPaymentRequest(DECODED_PAYMENT_REQUEST, expectedPaymentOptions);
|
verify(multiPathPaymentSender).payPaymentRequest(DECODED_PAYMENT_REQUEST, expectedPaymentOptions);
|
||||||
assertThat(paymentStatus.isPending()).isTrue();
|
assertThat(paymentStatus.isPending()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertFailure(String reason) {
|
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(paymentStatus.isFailure()).isTrue();
|
||||||
assertThat(readAll(paymentStatus)).map(InstantWithString::string).containsExactly(reason);
|
assertThat(readAll(paymentStatus)).map(InstantWithString::string).containsExactly(reason);
|
||||||
verifyNoInteractions(multiPathPaymentSender);
|
verifyNoInteractions(multiPathPaymentSender);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
|
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 de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ class PaymentOptionsTest {
|
|||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -30,6 +32,7 @@ class PaymentOptionsTest {
|
|||||||
Optional.of(123L),
|
Optional.of(123L),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -41,7 +44,20 @@ class PaymentOptionsTest {
|
|||||||
Optional.of(123L),
|
Optional.of(123L),
|
||||||
Optional.of(23L),
|
Optional.of(23L),
|
||||||
false,
|
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.of(999L),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
false,
|
false,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
private static final int FINAL_CLTV_EXPIRY = 0;
|
private static final int FINAL_CLTV_EXPIRY = 0;
|
||||||
@@ -181,29 +182,55 @@ class PaymentsControllerIT {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void topUp() {
|
void topUp() {
|
||||||
when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus);
|
when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus);
|
||||||
PaymentOptions emptyPaymentOptions = new PaymentOptions(
|
PaymentOptions emptyPaymentOptions = new PaymentOptions(
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
webTestClient.get().uri(url).exchange().expectStatus().isOk();
|
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
|
@Test
|
||||||
void with_payment_options() {
|
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()
|
webTestClient.post().uri(url).contentType(APPLICATION_JSON).bodyValue(DTO_AS_STRING).exchange()
|
||||||
.expectStatus().isOk();
|
.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
|
@Test
|
||||||
void no_linebreaks_within_line() {
|
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 ->
|
webTestClient.get().uri(url).exchange().expectBody(String.class).value(string ->
|
||||||
assertThat(string.substring(0, string.length() - 1)).doesNotContain("\n")
|
assertThat(string.substring(0, string.length() - 1)).doesNotContain("\n")
|
||||||
);
|
);
|
||||||
@@ -211,7 +238,7 @@ class PaymentsControllerIT {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void linebreak_at_end_of_line() {
|
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 ->
|
webTestClient.get().uri(url).exchange().expectBody(String.class).value(string ->
|
||||||
assertThat(string).endsWith("\n")
|
assertThat(string).endsWith("\n")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.springframework.http.MediaType.APPLICATION_NDJSON;
|
import static org.springframework.http.MediaType.APPLICATION_NDJSON;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -119,6 +121,16 @@ public class PaymentsController {
|
|||||||
return topUp(pubkey, amount, new PaymentOptionsDto());
|
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
|
@Timed
|
||||||
@PostMapping("/top-up/{pubkey}/amount/{amount}")
|
@PostMapping("/top-up/{pubkey}/amount/{amount}")
|
||||||
public ResponseEntity<Flux<String>> topUp(
|
public ResponseEntity<Flux<String>> topUp(
|
||||||
@@ -126,7 +138,25 @@ public class PaymentsController {
|
|||||||
@PathVariable long amount,
|
@PathVariable long amount,
|
||||||
@RequestBody PaymentOptionsDto paymentOptionsDto
|
@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);
|
return toStream(paymentStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public class PaymentOptionsDto {
|
|||||||
Optional.ofNullable(feeRateLimit),
|
Optional.ofNullable(feeRateLimit),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
ignoreFeesForOwnChannels,
|
ignoreFeesForOwnChannels,
|
||||||
|
Optional.empty(),
|
||||||
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 de.cotto.lndmanagej.pickhardtpayments.model.PaymentOptions.DEFAULT_PAYMENT_OPTIONS;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ class PaymentsControllerTest {
|
|||||||
Optional.of(999L),
|
Optional.of(999L),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
false,
|
false,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
PAYMENT_OPTIONS_DTO = new PaymentOptionsDto();
|
PAYMENT_OPTIONS_DTO = new PaymentOptionsDto();
|
||||||
@@ -145,23 +147,39 @@ class PaymentsControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void topUp() {
|
void topUp() {
|
||||||
when(topUpService.topUp(any(), any(), any())).thenReturn(paymentStatus);
|
when(topUpService.topUp(any(), any(), any(), any())).thenReturn(paymentStatus);
|
||||||
PaymentOptions emptyPaymentOptions = new PaymentOptions(
|
PaymentOptions emptyPaymentOptions = new PaymentOptions(
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
true,
|
true,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
assertThat(controller.topUp(PUBKEY, 123).getStatusCode()).isEqualTo(HttpStatus.OK);
|
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
|
@Test
|
||||||
void topUp_with_payment_options() {
|
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);
|
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
|
@Test
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class PaymentOptionsDtoTest {
|
|||||||
Optional.of(feeRateLimit),
|
Optional.of(feeRateLimit),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
DEFAULT_PAYMENT_OPTIONS.ignoreFeesForOwnChannels(),
|
DEFAULT_PAYMENT_OPTIONS.ignoreFeesForOwnChannels(),
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
assertThat(dto.toModel()).isEqualTo(expected);
|
assertThat(dto.toModel()).isEqualTo(expected);
|
||||||
@@ -45,6 +46,7 @@ class PaymentOptionsDtoTest {
|
|||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
false,
|
false,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
);
|
);
|
||||||
assertThat(dto.toModel()).isEqualTo(expected);
|
assertThat(dto.toModel()).isEqualTo(expected);
|
||||||
|
|||||||
Reference in New Issue
Block a user