Skip to content

Commit 86e6bed

Browse files
Add advanced options to LN payout form (#326)
* Add advanced options to LN payout form * Complete amount calcs * Temporary working solution for lnproxy web only (uses text instead of json) * Update LNpayment model and logics to use user's routing budget * Add handle lnproxyserver networks (i2p, tor, clearnet) / (mainnet,testnet) * Small fixes
1 parent 6b2dedc commit 86e6bed

File tree

23 files changed

+735
-89
lines changed

23 files changed

+735
-89
lines changed

api/lightning/node.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ def resetmc(cls):
258258
return True
259259

260260
@classmethod
261-
def validate_ln_invoice(cls, invoice, num_satoshis):
261+
def validate_ln_invoice(cls, invoice, num_satoshis, routing_budget_ppm):
262262
"""Checks if the submited LN invoice comforms to expectations"""
263263

264264
payout = {
@@ -283,10 +283,17 @@ def validate_ln_invoice(cls, invoice, num_satoshis):
283283
route_hints = payreq_decoded.route_hints
284284

285285
# Max amount RoboSats will pay for routing
286-
max_routing_fee_sats = max(
287-
num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
288-
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
289-
)
286+
# Start deprecate after v0.3.1 (only else max_routing_fee_sats will remain)
287+
if routing_budget_ppm == 0:
288+
max_routing_fee_sats = max(
289+
num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
290+
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
291+
)
292+
else:
293+
# End deprecate
294+
max_routing_fee_sats = int(
295+
float(num_satoshis) * float(routing_budget_ppm) / 1000000
296+
)
290297

291298
if route_hints:
292299
routes_cost = []
@@ -306,7 +313,7 @@ def validate_ln_invoice(cls, invoice, num_satoshis):
306313
# If the cheapest possible private route is more expensive than what RoboSats is willing to pay
307314
if min(routes_cost) >= max_routing_fee_sats:
308315
payout["context"] = {
309-
"bad_invoice": "The invoice submitted only has a trick on the routing hints, you might be using an incompatible wallet (probably Muun? Use an onchain address instead!). Check the wallet compatibility guide at wallets.robosats.com"
316+
"bad_invoice": "The invoice hinted private routes are not payable within the submitted routing budget."
310317
}
311318
return payout
312319

api/logics.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ def update_address(cls, order, user, address, mining_fee_rate):
721721
return True, None
722722

723723
@classmethod
724-
def update_invoice(cls, order, user, invoice):
724+
def update_invoice(cls, order, user, invoice, routing_budget_ppm):
725725

726726
# Empty invoice?
727727
if not invoice:
@@ -754,7 +754,11 @@ def update_invoice(cls, order, user, invoice):
754754
cls.cancel_onchain_payment(order)
755755

756756
num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
757-
payout = LNNode.validate_ln_invoice(invoice, num_satoshis)
757+
routing_budget_sats = float(num_satoshis) * (
758+
float(routing_budget_ppm) / 1000000
759+
)
760+
num_satoshis = int(num_satoshis - routing_budget_sats)
761+
payout = LNNode.validate_ln_invoice(invoice, num_satoshis, routing_budget_ppm)
758762

759763
if not payout["valid"]:
760764
return False, payout["context"]
@@ -765,6 +769,8 @@ def update_invoice(cls, order, user, invoice):
765769
sender=User.objects.get(username=ESCROW_USERNAME),
766770
order_paid_LN=order, # In case this user has other payouts, update the one related to this order.
767771
receiver=user,
772+
routing_budget_ppm=routing_budget_ppm,
773+
routing_budget_sats=routing_budget_sats,
768774
# if there is a LNPayment matching these above, it updates that one with defaults below.
769775
defaults={
770776
"invoice": invoice,
@@ -1679,7 +1685,9 @@ def summarize_trade(cls, order, user):
16791685
else:
16801686
summary["received_sats"] = order.payout.num_satoshis
16811687
summary["trade_fee_sats"] = round(
1682-
order.last_satoshis - summary["received_sats"]
1688+
order.last_satoshis
1689+
- summary["received_sats"]
1690+
- order.payout.routing_budget_sats
16831691
)
16841692
# Only add context for swap costs if the user is the swap recipient. Peer should not know whether it was a swap
16851693
if users[order_user] == user and order.is_swap:
@@ -1716,11 +1724,20 @@ def summarize_trade(cls, order, user):
17161724
order.contract_finalization_time - order.last_satoshis_time
17171725
)
17181726
if not order.is_swap:
1727+
platform_summary["routing_budget_sats"] = order.payout.routing_budget_sats
1728+
# Start Deprecated after v0.3.1
17191729
platform_summary["routing_fee_sats"] = order.payout.fee
1730+
# End Deprecated after v0.3.1
17201731
platform_summary["trade_revenue_sats"] = int(
17211732
order.trade_escrow.num_satoshis
17221733
- order.payout.num_satoshis
1723-
- order.payout.fee
1734+
# Start Deprecated after v0.3.1 (will be `- order.payout.routing_budget_sats`)
1735+
- (
1736+
order.payout.fee
1737+
if order.payout.routing_budget_sats == 0
1738+
else order.payout.routing_budget_sats
1739+
)
1740+
# End Deprecated after v0.3.1
17241741
)
17251742
else:
17261743
platform_summary["routing_fee_sats"] = 0

api/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ class FailureReason(models.IntegerChoices):
126126
MaxValueValidator(1.5 * MAX_TRADE),
127127
]
128128
)
129+
# Routing budget in PPM
130+
routing_budget_ppm = models.PositiveBigIntegerField(
131+
default=0,
132+
null=False,
133+
validators=[
134+
MinValueValidator(0),
135+
MaxValueValidator(100000),
136+
],
137+
)
138+
# Routing budget in Sats. Only for reporting summaries.
139+
routing_budget_sats = models.DecimalField(
140+
max_digits=10, decimal_places=3, default=0, null=False, blank=False
141+
)
129142
# Fee in sats with mSats decimals fee_msat
130143
fee = models.DecimalField(
131144
max_digits=10, decimal_places=3, default=0, null=False, blank=False

api/serializers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,14 @@ class UpdateOrderSerializer(serializers.Serializer):
489489
invoice = serializers.CharField(
490490
max_length=2000, allow_null=True, allow_blank=True, default=None
491491
)
492+
routing_budget_ppm = serializers.IntegerField(
493+
default=0,
494+
min_value=0,
495+
max_value=100000,
496+
allow_null=True,
497+
required=False,
498+
help_text="Max budget to allocate for routing in PPM",
499+
)
492500
address = serializers.CharField(
493501
max_length=100, allow_null=True, allow_blank=True, default=None
494502
)

api/tasks.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,23 @@ def follow_send_payment(hash):
8686
from api.models import LNPayment, Order
8787

8888
lnpayment = LNPayment.objects.get(payment_hash=hash)
89-
fee_limit_sat = int(
90-
max(
91-
lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
92-
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
89+
# Start deprecate after v0.3.1 (only else max_routing_fee_sats will remain)
90+
if lnpayment.routing_budget_ppm == 0:
91+
fee_limit_sat = int(
92+
max(
93+
lnpayment.num_satoshis
94+
* float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
95+
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
96+
)
97+
) # 1000 ppm or 10 sats
98+
else:
99+
# End deprecate
100+
# Defaults is 0ppm. Set by the user over API. Defaults to 1000 ppm on ReactJS frontend.
101+
fee_limit_sat = int(
102+
float(lnpayment.num_satoshis)
103+
* float(lnpayment.routing_budget_ppm)
104+
/ 1000000
93105
)
94-
) # 1000 ppm or 10 sats
95106
timeout_seconds = int(config("PAYOUT_TIMEOUT_SECONDS"))
96107

97108
request = LNNode.routerrpc.SendPaymentRequest(
@@ -145,7 +156,6 @@ def follow_send_payment(hash):
145156
],
146157
"IN_FLIGHT": False,
147158
}
148-
print(context)
149159

150160
# If failed due to not route, reset mission control. (This won't scale well, just a temporary fix)
151161
# ResetMC deactivate temporary for tests

api/views.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,7 @@ def take_update_confirm_dispute_cancel(self, request, format=None):
501501
# 5.b)'update_address' 6)'submit_statement' (in dispute), 7)'rate_user' , 8)'rate_platform'
502502
action = serializer.data.get("action")
503503
invoice = serializer.data.get("invoice")
504+
routing_budget_ppm = serializer.data.get("routing_budget_ppm", 0)
504505
address = serializer.data.get("address")
505506
mining_fee_rate = serializer.data.get("mining_fee_rate")
506507
statement = serializer.data.get("statement")
@@ -543,7 +544,9 @@ def take_update_confirm_dispute_cancel(self, request, format=None):
543544

544545
# 2) If action is 'update invoice'
545546
elif action == "update_invoice":
546-
valid, context = Logics.update_invoice(order, request.user, invoice)
547+
valid, context = Logics.update_invoice(
548+
order, request.user, invoice, routing_budget_ppm
549+
)
547550
if not valid:
548551
return Response(context, status.HTTP_400_BAD_REQUEST)
549552

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ services:
5353
environment:
5454
TOR_PROXY_IP: 127.0.0.1
5555
TOR_PROXY_PORT: 9050
56-
ROBOSATS_ONION: robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
56+
ROBOSATS_ONION: robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion
5757
network_mode: service:tor
5858
volumes:
5959
- ./frontend/static:/usr/src/robosats/static

frontend/src/basic/Main.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
181181
return await data;
182182
};
183183

184-
const fetchInfo = function () {
184+
const fetchInfo = function (setNetwork?: boolean) {
185185
setInfo({ ...info, loading: true });
186186
apiClient.get(baseUrl, '/api/info/').then((data: Info) => {
187187
const versionInfo: any = checkVer(data.version.major, data.version.minor, data.version.patch);
@@ -192,12 +192,16 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
192192
clientVersion: versionInfo.clientVersion,
193193
loading: false,
194194
});
195+
// Sets Setting network from coordinator API param if accessing via web
196+
if (setNetwork) {
197+
setSettings({ ...settings, network: data.network });
198+
}
195199
});
196200
};
197201

198202
useEffect(() => {
199203
if (open.stats || open.coordinator || info.coordinatorVersion == 'v?.?.?') {
200-
fetchInfo();
204+
fetchInfo(info.coordinatorVersion == 'v?.?.?');
201205
}
202206
}, [open.stats, open.coordinator]);
203207

@@ -424,6 +428,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
424428
<OrderPage
425429
baseUrl={baseUrl}
426430
order={order}
431+
settings={settings}
427432
setOrder={setOrder}
428433
setCurrentOrder={setCurrentOrder}
429434
badOrder={badOrder}

frontend/src/basic/OrderPage/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import TradeBox from '../../components/TradeBox';
77
import OrderDetails from '../../components/OrderDetails';
88

99
import { Page } from '../NavBar';
10-
import { Order } from '../../models';
10+
import { Order, Settings } from '../../models';
1111
import { apiClient } from '../../services/api';
1212

1313
interface OrderPageProps {
1414
windowSize: { width: number; height: number };
1515
order: Order;
16+
settings: Settings;
1617
setOrder: (state: Order) => void;
1718
setCurrentOrder: (state: number) => void;
1819
fetchOrder: () => void;
@@ -27,6 +28,7 @@ interface OrderPageProps {
2728
const OrderPage = ({
2829
windowSize,
2930
order,
31+
settings,
3032
setOrder,
3133
setCurrentOrder,
3234
badOrder,
@@ -128,6 +130,7 @@ const OrderPage = ({
128130
>
129131
<TradeBox
130132
order={order}
133+
settings={settings}
131134
setOrder={setOrder}
132135
setBadOrder={setBadOrder}
133136
baseUrl={baseUrl}
@@ -170,6 +173,7 @@ const OrderPage = ({
170173
<div style={{ display: tab == 'contract' ? '' : 'none' }}>
171174
<TradeBox
172175
order={order}
176+
settings={settings}
173177
setOrder={setOrder}
174178
setBadOrder={setBadOrder}
175179
baseUrl={baseUrl}

frontend/src/components/Notifications/index.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useEffect, useState } from 'react';
2-
import { StringIfPlural, useTranslation } from 'react-i18next';
2+
import { useTranslation } from 'react-i18next';
33
import {
44
Tooltip,
55
Alert,
@@ -134,7 +134,7 @@ const Notifications = ({
134134
title: t('Order has expired'),
135135
severity: 'warning',
136136
onClick: moveToOrderPage,
137-
sound: undefined,
137+
sound: audio.ding,
138138
timeout: 30000,
139139
pageTitle: `${t('😪 Expired!')} - ${basePageTitle}`,
140140
},
@@ -262,7 +262,6 @@ const Notifications = ({
262262
} else if (order?.is_seller && status > 7 && oldStatus < 7) {
263263
message = Messages.escrowLocked;
264264
} else if ([9, 10].includes(status) && oldStatus < 9) {
265-
console.log('yoooo');
266265
message = Messages.chat;
267266
} else if (order?.is_seller && [13, 14, 15].includes(status) && oldStatus < 13) {
268267
message = Messages.successful;
@@ -333,7 +332,6 @@ const Notifications = ({
333332
return (
334333
<StyledTooltip
335334
open={show}
336-
style={{ padding: 0, backgroundColor: 'black' }}
337335
placement={windowWidth > 60 ? 'left' : 'bottom'}
338336
title={
339337
<Alert

0 commit comments

Comments
 (0)