Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add advanced options to LN payout form #326

Merged
merged 6 commits into from
Nov 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions api/lightning/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ def resetmc(cls):
return True

@classmethod
def validate_ln_invoice(cls, invoice, num_satoshis):
def validate_ln_invoice(cls, invoice, num_satoshis, routing_budget_ppm):
"""Checks if the submited LN invoice comforms to expectations"""

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

# Max amount RoboSats will pay for routing
max_routing_fee_sats = max(
num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
)
# Start deprecate after v0.3.1 (only else max_routing_fee_sats will remain)
if routing_budget_ppm == 0:
max_routing_fee_sats = max(
num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
)
else:
# End deprecate
max_routing_fee_sats = int(
float(num_satoshis) * float(routing_budget_ppm) / 1000000
)

if route_hints:
routes_cost = []
Expand All @@ -306,7 +313,7 @@ def validate_ln_invoice(cls, invoice, num_satoshis):
# If the cheapest possible private route is more expensive than what RoboSats is willing to pay
if min(routes_cost) >= max_routing_fee_sats:
payout["context"] = {
"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"
"bad_invoice": "The invoice hinted private routes are not payable within the submitted routing budget."
}
return payout

Expand Down
25 changes: 21 additions & 4 deletions api/logics.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ def update_address(cls, order, user, address, mining_fee_rate):
return True, None

@classmethod
def update_invoice(cls, order, user, invoice):
def update_invoice(cls, order, user, invoice, routing_budget_ppm):

# Empty invoice?
if not invoice:
Expand Down Expand Up @@ -754,7 +754,11 @@ def update_invoice(cls, order, user, invoice):
cls.cancel_onchain_payment(order)

num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
payout = LNNode.validate_ln_invoice(invoice, num_satoshis)
routing_budget_sats = float(num_satoshis) * (
float(routing_budget_ppm) / 1000000
)
num_satoshis = int(num_satoshis - routing_budget_sats)
payout = LNNode.validate_ln_invoice(invoice, num_satoshis, routing_budget_ppm)

if not payout["valid"]:
return False, payout["context"]
Expand All @@ -765,6 +769,8 @@ def update_invoice(cls, order, user, invoice):
sender=User.objects.get(username=ESCROW_USERNAME),
order_paid_LN=order, # In case this user has other payouts, update the one related to this order.
receiver=user,
routing_budget_ppm=routing_budget_ppm,
routing_budget_sats=routing_budget_sats,
# if there is a LNPayment matching these above, it updates that one with defaults below.
defaults={
"invoice": invoice,
Expand Down Expand Up @@ -1679,7 +1685,9 @@ def summarize_trade(cls, order, user):
else:
summary["received_sats"] = order.payout.num_satoshis
summary["trade_fee_sats"] = round(
order.last_satoshis - summary["received_sats"]
order.last_satoshis
- summary["received_sats"]
- order.payout.routing_budget_sats
)
# Only add context for swap costs if the user is the swap recipient. Peer should not know whether it was a swap
if users[order_user] == user and order.is_swap:
Expand Down Expand Up @@ -1716,11 +1724,20 @@ def summarize_trade(cls, order, user):
order.contract_finalization_time - order.last_satoshis_time
)
if not order.is_swap:
platform_summary["routing_budget_sats"] = order.payout.routing_budget_sats
# Start Deprecated after v0.3.1
platform_summary["routing_fee_sats"] = order.payout.fee
# End Deprecated after v0.3.1
platform_summary["trade_revenue_sats"] = int(
order.trade_escrow.num_satoshis
- order.payout.num_satoshis
- order.payout.fee
# Start Deprecated after v0.3.1 (will be `- order.payout.routing_budget_sats`)
- (
order.payout.fee
if order.payout.routing_budget_sats == 0
else order.payout.routing_budget_sats
)
# End Deprecated after v0.3.1
)
else:
platform_summary["routing_fee_sats"] = 0
Expand Down
13 changes: 13 additions & 0 deletions api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,19 @@ class FailureReason(models.IntegerChoices):
MaxValueValidator(1.5 * MAX_TRADE),
]
)
# Routing budget in PPM
routing_budget_ppm = models.PositiveBigIntegerField(
default=0,
null=False,
validators=[
MinValueValidator(0),
MaxValueValidator(100000),
],
)
# Routing budget in Sats. Only for reporting summaries.
routing_budget_sats = models.DecimalField(
max_digits=10, decimal_places=3, default=0, null=False, blank=False
)
# Fee in sats with mSats decimals fee_msat
fee = models.DecimalField(
max_digits=10, decimal_places=3, default=0, null=False, blank=False
Expand Down
8 changes: 8 additions & 0 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,14 @@ class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(
max_length=2000, allow_null=True, allow_blank=True, default=None
)
routing_budget_ppm = serializers.IntegerField(
default=0,
min_value=0,
max_value=100000,
allow_null=True,
required=False,
help_text="Max budget to allocate for routing in PPM",
)
address = serializers.CharField(
max_length=100, allow_null=True, allow_blank=True, default=None
)
Expand Down
22 changes: 16 additions & 6 deletions api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,23 @@ def follow_send_payment(hash):
from api.models import LNPayment, Order

lnpayment = LNPayment.objects.get(payment_hash=hash)
fee_limit_sat = int(
max(
lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
# Start deprecate after v0.3.1 (only else max_routing_fee_sats will remain)
if lnpayment.routing_budget_ppm == 0:
fee_limit_sat = int(
max(
lnpayment.num_satoshis
* float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
)
) # 1000 ppm or 10 sats
else:
# End deprecate
# Defaults is 0ppm. Set by the user over API. Defaults to 1000 ppm on ReactJS frontend.
fee_limit_sat = int(
float(lnpayment.num_satoshis)
* float(lnpayment.routing_budget_ppm)
/ 1000000
)
) # 1000 ppm or 10 sats
timeout_seconds = int(config("PAYOUT_TIMEOUT_SECONDS"))

request = LNNode.routerrpc.SendPaymentRequest(
Expand Down Expand Up @@ -145,7 +156,6 @@ def follow_send_payment(hash):
],
"IN_FLIGHT": False,
}
print(context)

# If failed due to not route, reset mission control. (This won't scale well, just a temporary fix)
# ResetMC deactivate temporary for tests
Expand Down
5 changes: 4 additions & 1 deletion api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ def take_update_confirm_dispute_cancel(self, request, format=None):
# 5.b)'update_address' 6)'submit_statement' (in dispute), 7)'rate_user' , 8)'rate_platform'
action = serializer.data.get("action")
invoice = serializer.data.get("invoice")
routing_budget_ppm = serializer.data.get("routing_budget_ppm", 0)
address = serializer.data.get("address")
mining_fee_rate = serializer.data.get("mining_fee_rate")
statement = serializer.data.get("statement")
Expand Down Expand Up @@ -543,7 +544,9 @@ def take_update_confirm_dispute_cancel(self, request, format=None):

# 2) If action is 'update invoice'
elif action == "update_invoice":
valid, context = Logics.update_invoice(order, request.user, invoice)
valid, context = Logics.update_invoice(
order, request.user, invoice, routing_budget_ppm
)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ services:
environment:
TOR_PROXY_IP: 127.0.0.1
TOR_PROXY_PORT: 9050
ROBOSATS_ONION: robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
ROBOSATS_ONION: robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion
network_mode: service:tor
volumes:
- ./frontend/static:/usr/src/robosats/static
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/basic/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
return await data;
};

const fetchInfo = function () {
const fetchInfo = function (setNetwork?: boolean) {
setInfo({ ...info, loading: true });
apiClient.get(baseUrl, '/api/info/').then((data: Info) => {
const versionInfo: any = checkVer(data.version.major, data.version.minor, data.version.patch);
Expand All @@ -192,12 +192,16 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
clientVersion: versionInfo.clientVersion,
loading: false,
});
// Sets Setting network from coordinator API param if accessing via web
if (setNetwork) {
setSettings({ ...settings, network: data.network });
}
});
};

useEffect(() => {
if (open.stats || open.coordinator || info.coordinatorVersion == 'v?.?.?') {
fetchInfo();
fetchInfo(info.coordinatorVersion == 'v?.?.?');
}
}, [open.stats, open.coordinator]);

Expand Down Expand Up @@ -424,6 +428,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
<OrderPage
baseUrl={baseUrl}
order={order}
settings={settings}
setOrder={setOrder}
setCurrentOrder={setCurrentOrder}
badOrder={badOrder}
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/basic/OrderPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import TradeBox from '../../components/TradeBox';
import OrderDetails from '../../components/OrderDetails';

import { Page } from '../NavBar';
import { Order } from '../../models';
import { Order, Settings } from '../../models';
import { apiClient } from '../../services/api';

interface OrderPageProps {
windowSize: { width: number; height: number };
order: Order;
settings: Settings;
setOrder: (state: Order) => void;
setCurrentOrder: (state: number) => void;
fetchOrder: () => void;
Expand All @@ -27,6 +28,7 @@ interface OrderPageProps {
const OrderPage = ({
windowSize,
order,
settings,
setOrder,
setCurrentOrder,
badOrder,
Expand Down Expand Up @@ -128,6 +130,7 @@ const OrderPage = ({
>
<TradeBox
order={order}
settings={settings}
setOrder={setOrder}
setBadOrder={setBadOrder}
baseUrl={baseUrl}
Expand Down Expand Up @@ -170,6 +173,7 @@ const OrderPage = ({
<div style={{ display: tab == 'contract' ? '' : 'none' }}>
<TradeBox
order={order}
settings={settings}
setOrder={setOrder}
setBadOrder={setBadOrder}
baseUrl={baseUrl}
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/components/Notifications/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { StringIfPlural, useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import {
Tooltip,
Alert,
Expand Down Expand Up @@ -134,7 +134,7 @@ const Notifications = ({
title: t('Order has expired'),
severity: 'warning',
onClick: moveToOrderPage,
sound: undefined,
sound: audio.ding,
timeout: 30000,
pageTitle: `${t('😪 Expired!')} - ${basePageTitle}`,
},
Expand Down Expand Up @@ -262,7 +262,6 @@ const Notifications = ({
} else if (order?.is_seller && status > 7 && oldStatus < 7) {
message = Messages.escrowLocked;
} else if ([9, 10].includes(status) && oldStatus < 9) {
console.log('yoooo');
message = Messages.chat;
} else if (order?.is_seller && [13, 14, 15].includes(status) && oldStatus < 13) {
message = Messages.successful;
Expand Down Expand Up @@ -333,7 +332,6 @@ const Notifications = ({
return (
<StyledTooltip
open={show}
style={{ padding: 0, backgroundColor: 'black' }}
placement={windowWidth > 60 ? 'left' : 'bottom'}
title={
<Alert
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/OrderDetails/LinearDeterminate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const LinearDeterminate = ({ expiresAt, totalSecsExp }: Props): JSX.Element => {
sx={{ height: '0.4em' }}
variant='determinate'
value={progress}
color={progress < 20 ? 'secondary' : 'primary'}
color={progress < 25 ? 'secondary' : 'primary'}
/>
</Box>
);
Expand Down
19 changes: 10 additions & 9 deletions frontend/src/components/OrderDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Grid,
Collapse,
useTheme,
Typography,
} from '@mui/material';

import Countdown, { CountdownRenderProps, zeroPad } from 'react-countdown';
Expand Down Expand Up @@ -86,25 +87,25 @@ const OrderDetails = ({
// Render a completed state
return <span> {t('The order has expired')}</span>;
} else {
let col = 'inherit';
let color = 'inherit';
const fraction_left = total / 1000 / order.total_secs_exp;
// Make orange at 25% of time left
if (fraction_left < 0.25) {
col = 'orange';
color = theme.palette.warning.main;
}
// Make red at 10% of time left
if (fraction_left < 0.1) {
col = 'red';
color = theme.palette.error.main;
}
// Render a countdown, bold when less than 25%
return fraction_left < 0.25 ? (
<b>
<span style={{ color: col }}>
{`${hours}h ${zeroPad(minutes)}m ${zeroPad(seconds)}s `}
</span>
</b>
<Typography color={color}>
<b>{`${hours}h ${zeroPad(minutes)}m ${zeroPad(seconds)}s `}</b>
</Typography>
) : (
<span style={{ color: col }}>{`${hours}h ${zeroPad(minutes)}m ${zeroPad(seconds)}s `}</span>
<Typography color={color}>
{`${hours}h ${zeroPad(minutes)}m ${zeroPad(seconds)}s `}
</Typography>
);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const ChatHeader: React.FC<Props> = ({ connected, peerConnected, turtleMode, set
>
<Typography align='center' variant='caption' sx={{ color: connectedTextColor }}>
{t('Peer') + ': '}
{peerConnected ? t('connected') : t('disconnected')}
{connected ? (peerConnected ? t('connected') : t('disconnected')) : '...waiting'}
</Typography>
</Paper>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
useEffect(() => {
if (![9, 10].includes(status)) {
connection?.close();
setConnection(undefined);
}
}, [status]);

Expand Down
Loading