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
This commit is contained in:
Reckless_Satoshi 2022-11-24 17:42:30 +00:00 committed by GitHub
parent 6b2dedce13
commit 86e6bed37c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 737 additions and 91 deletions

View File

@ -258,7 +258,7 @@ class LNNode:
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 = {
@ -283,10 +283,17 @@ class LNNode:
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 = []
@ -306,7 +313,7 @@ class LNNode:
# 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

View File

@ -721,7 +721,7 @@ class Logics:
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:
@ -754,7 +754,11 @@ class Logics:
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"]
@ -765,6 +769,8 @@ class Logics:
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,
@ -1679,7 +1685,9 @@ class Logics:
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:
@ -1716,11 +1724,20 @@ class Logics:
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

View File

@ -126,6 +126,19 @@ class LNPayment(models.Model):
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

View File

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

View File

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

View File

@ -501,6 +501,7 @@ class OrderView(viewsets.ViewSet):
# 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")
@ -543,7 +544,9 @@ class OrderView(viewsets.ViewSet):
# 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)

View File

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

View File

@ -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);
@ -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]);
@ -424,6 +428,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
<OrderPage
baseUrl={baseUrl}
order={order}
settings={settings}
setOrder={setOrder}
setCurrentOrder={setCurrentOrder}
badOrder={badOrder}

View File

@ -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;
@ -27,6 +28,7 @@ interface OrderPageProps {
const OrderPage = ({
windowSize,
order,
settings,
setOrder,
setCurrentOrder,
badOrder,
@ -128,6 +130,7 @@ const OrderPage = ({
>
<TradeBox
order={order}
settings={settings}
setOrder={setOrder}
setBadOrder={setBadOrder}
baseUrl={baseUrl}
@ -170,6 +173,7 @@ const OrderPage = ({
<div style={{ display: tab == 'contract' ? '' : 'none' }}>
<TradeBox
order={order}
settings={settings}
setOrder={setOrder}
setBadOrder={setBadOrder}
baseUrl={baseUrl}

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { StringIfPlural, useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import {
Tooltip,
Alert,
@ -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}`,
},
@ -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;
@ -333,7 +332,6 @@ const Notifications = ({
return (
<StyledTooltip
open={show}
style={{ padding: 0, backgroundColor: 'black' }}
placement={windowWidth > 60 ? 'left' : 'bottom'}
title={
<Alert

View File

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

View File

@ -12,6 +12,7 @@ import {
Grid,
Collapse,
useTheme,
Typography,
} from '@mui/material';
import Countdown, { CountdownRenderProps, zeroPad } from 'react-countdown';
@ -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>
);
}
};

View File

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

View File

@ -72,6 +72,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
useEffect(() => {
if (![9, 10].includes(status)) {
connection?.close();
setConnection(undefined);
}
}, [status]);

View File

@ -1,28 +1,71 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Grid, Typography, TextField } from '@mui/material';
import { Order } from '../../../models';
import {
Box,
Grid,
Typography,
TextField,
Tooltip,
FormControlLabel,
Checkbox,
useTheme,
Collapse,
Switch,
MenuItem,
Select,
InputAdornment,
Button,
FormControl,
InputLabel,
IconButton,
FormHelperText,
} from '@mui/material';
import { Order, Settings } from '../../../models';
import WalletsButton from '../WalletsButton';
import { LoadingButton } from '@mui/lab';
import { pn } from '../../../utils';
import { ContentCopy, RoundaboutRight, Route, SelfImprovement } from '@mui/icons-material';
import { apiClient } from '../../../services/api';
import lnproxies from '../../../../static/lnproxies.json';
import { systemClient } from '../../../services/System';
export interface LightningForm {
invoice: string;
routingBudget: number;
amount: number;
advancedOptions: boolean;
useCustomBudget: boolean;
routingBudgetUnit: 'PPM' | 'Sats';
routingBudgetPPM: number;
routingBudgetSats: number | undefined;
badInvoice: string;
useLnproxy: boolean;
lnproxyServer: string;
lnproxyBudget: number;
lnproxyInvoice: string;
lnproxyAmount: number;
lnproxyServer: number;
lnproxyBudgetUnit: 'PPM' | 'Sats';
lnproxyBudgetPPM: number;
lnproxyBudgetSats: number;
badLnproxy: string;
}
export const defaultLightning: LightningForm = {
invoice: '',
routingBudget: 0,
amount: 0,
advancedOptions: false,
useCustomBudget: false,
routingBudgetUnit: 'PPM',
routingBudgetPPM: 1000,
routingBudgetSats: undefined,
badInvoice: '',
useLnproxy: false,
lnproxyServer: '',
lnproxyBudget: 0,
lnproxyInvoice: '',
lnproxyAmount: 0,
lnproxyServer: 0,
lnproxyBudgetUnit: 'Sats',
lnproxyBudgetPPM: 0,
lnproxyBudgetSats: 0,
badLnproxy: '',
};
@ -32,6 +75,7 @@ interface LightningPayoutFormProps {
lightning: LightningForm;
setLightning: (state: LightningForm) => void;
onClickSubmit: (invoice: string) => void;
settings: Settings;
}
export const LightningPayoutForm = ({
@ -40,48 +84,551 @@ export const LightningPayoutForm = ({
onClickSubmit,
lightning,
setLightning,
settings,
}: LightningPayoutFormProps): JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
const [loadingLnproxy, setLoadingLnproxy] = useState<boolean>(false);
const [badLnproxyServer, setBadLnproxyServer] = useState<string>('');
const computeInvoiceAmount = function () {
const tradeAmount = order.trade_satoshis;
return Math.floor(tradeAmount - tradeAmount * (lightning.routingBudgetPPM / 1000000));
};
const validateInvoice = function (invoice: string, targetAmount: number) {
const invoiceAmount = Number(invoice.substring(4, 5 + Math.floor(Math.log10(targetAmount))));
if (targetAmount != invoiceAmount && invoice.length > 20) {
return 'Invalid invoice amount';
} else {
return '';
}
};
useEffect(() => {
const amount = computeInvoiceAmount();
setLightning({
...lightning,
amount,
lnproxyAmount: amount - lightning.lnproxyBudgetSats,
routingBudgetSats:
lightning.routingBudgetSats == undefined
? Math.ceil((amount / 1000000) * lightning.routingBudgetPPM)
: lightning.routingBudgetSats,
});
}, [lightning.routingBudgetPPM]);
useEffect(() => {
if (lightning.invoice != '') {
setLightning({
...lightning,
badInvoice: validateInvoice(lightning.invoice, lightning.amount),
});
}
}, [lightning.invoice, lightning.amount]);
useEffect(() => {
if (lightning.lnproxyInvoice != '') {
setLightning({
...lightning,
badLnproxy: validateInvoice(lightning.lnproxyInvoice, lightning.lnproxyAmount),
});
}
}, [lightning.lnproxyInvoice, lightning.lnproxyAmount]);
const lnproxyUrl = function () {
console.log(settings);
const bitcoinNetwork = settings?.network ?? 'mainnet';
let internetNetwork: 'Clearnet' | 'I2P' | 'TOR' = 'Clearnet';
if (settings.host?.includes('.i2p')) {
internetNetwork = 'I2P';
} else if (settings.host?.includes('.onion') || window.NativeRobosats != undefined) {
internetNetwork = 'TOR';
}
const url = lnproxies[lightning.lnproxyServer][`${bitcoinNetwork}${internetNetwork}`];
if (url != 'undefined') {
return url;
} else {
setBadLnproxyServer(
t(`Server not available for {{bitcoinNetwork}} bitcoin over {{internetNetwork}}`, {
bitcoinNetwork,
internetNetwork: t(internetNetwork),
}),
);
}
};
useEffect(() => {
setBadLnproxyServer('');
lnproxyUrl();
}, [lightning.lnproxyServer]);
// const fetchLnproxy = function () {
// setLoadingLnproxy(true);
// apiClient
// .get(
// lnproxyUrl(),
// `/api/${lightning.lnproxyInvoice}${lightning.lnproxyBudgetSats > 0 ? `?routing_msat=${lightning.lnproxyBudgetSats * 1000}` : ''}`,
// )
// };
// Lnproxy API does not return JSON, therefore not compatible with current apiClient service
// Does not work on Android robosats!
const fetchLnproxy = function () {
setLoadingLnproxy(true);
fetch(
lnproxyUrl() +
`/api/${lightning.lnproxyInvoice}${
lightning.lnproxyBudgetSats > 0
? `?routing_msat=${lightning.lnproxyBudgetSats * 1000}`
: ''
}`,
)
.then((response) => response.text())
.then((text) => {
if (text.includes('lnproxy error')) {
setLightning({ ...lightning, badLnproxy: text });
} else {
setLightning({ ...lightning, invoice: text, badLnproxy: '' });
}
})
.catch(() => {
setLightning({ ...lightning, badLnproxy: 'Lnproxy server uncaught error' });
})
.finally(() => {
setLoadingLnproxy(false);
});
};
const onProxyBudgetChange = function (e) {
if (isFinite(e.target.value) && e.target.value >= 0) {
let lnproxyBudgetSats;
let lnproxyBudgetPPM;
if (lightning.lnproxyBudgetUnit === 'Sats') {
lnproxyBudgetSats = Math.floor(e.target.value);
lnproxyBudgetPPM = Math.round((lnproxyBudgetSats * 1000000) / lightning.amount);
} else {
lnproxyBudgetPPM = e.target.value;
lnproxyBudgetSats = Math.ceil((lightning.amount / 1000000) * lnproxyBudgetPPM);
}
if (lnproxyBudgetPPM < 99999) {
const lnproxyAmount = lightning.amount - lnproxyBudgetSats;
setLightning({ ...lightning, lnproxyBudgetSats, lnproxyBudgetPPM, lnproxyAmount });
}
}
};
const onRoutingBudgetChange = function (e) {
const tradeAmount = order.trade_satoshis;
if (isFinite(e.target.value) && e.target.value >= 0) {
let routingBudgetSats;
let routingBudgetPPM;
if (lightning.routingBudgetUnit === 'Sats') {
routingBudgetSats = Math.floor(e.target.value);
routingBudgetPPM = Math.round((routingBudgetSats * 1000000) / tradeAmount);
} else {
routingBudgetPPM = e.target.value;
routingBudgetSats = Math.ceil((lightning.amount / 1000000) * routingBudgetPPM);
}
if (routingBudgetPPM < 99999) {
const amount = Math.floor(
tradeAmount - tradeAmount * (lightning.routingBudgetPPM / 1000000),
);
setLightning({ ...lightning, routingBudgetSats, routingBudgetPPM, amount });
}
}
};
const lnProxyBudgetHelper = function () {
let text = '';
if (lightning.lnproxyBudgetSats < 0) {
text = 'Must be positive';
} else if (lightning.lnproxyBudgetPPM > 10000) {
text = 'Too high! (That is more than 1%)';
}
return text;
};
const routingBudgetHelper = function () {
let text = '';
if (lightning.routingBudgetSats < 0) {
text = 'Must be positive';
} else if (lightning.routingBudgetPPM > 10000) {
text = 'Too high! (That is more than 1%)';
}
return text;
};
return (
<Grid container direction='column' justifyContent='flex-start' alignItems='center' spacing={1}>
<Grid item xs={12}>
<Typography variant='body2'>
{t('Submit a valid invoice for {{amountSats}} Satoshis.', {
amountSats: pn(order.invoice_amount),
})}
</Typography>
</Grid>
<Grid item xs={12}>
<WalletsButton />
</Grid>
<Grid item xs={12}>
<TextField
fullWidth={true}
error={lightning.badInvoice != ''}
helperText={lightning.badInvoice ? t(lightning.badInvoice) : ''}
label={t('Payout Lightning Invoice')}
required
value={lightning.invoice}
inputProps={{
style: { textAlign: 'center', maxHeight: '14.28em' },
<div style={{ height: '0.3em' }} />
<Grid
item
style={{
display: 'flex',
alignItems: 'center',
height: '1.1em',
}}
>
<Typography color='text.primary'>{t('Advanced options')}</Typography>
<Switch
size='small'
checked={lightning.advancedOptions}
onChange={(e) => {
const checked = e.target.checked;
setLightning({
...lightning,
advancedOptions: checked,
});
}}
multiline
minRows={4}
maxRows={8}
onChange={(e) => setLightning({ ...lightning, invoice: e.target.value ?? '' })}
/>
<SelfImprovement sx={{ color: 'text.primary' }} />
</Grid>
<Grid item xs={12}>
<LoadingButton
loading={loading}
onClick={() => onClickSubmit(lightning.invoice)}
variant='outlined'
color='primary'
<Grid item>
<Box
sx={{
backgroundColor: 'background.paper',
border: '1px solid',
width: '18em',
borderRadius: '0.3em',
borderColor: theme.palette.mode === 'dark' ? '#434343' : '#c4c4c4',
padding: '1em',
}}
>
{t('Submit')}
</LoadingButton>
<Grid
container
direction='column'
justifyContent='flex-start'
alignItems='center'
spacing={0.5}
>
<Collapse in={lightning.advancedOptions}>
<Grid
container
direction='column'
justifyContent='flex-start'
alignItems='center'
spacing={0.5}
padding={0.5}
>
<Grid item>
<TextField
sx={{ width: '14em' }}
disabled={!lightning.advancedOptions}
error={routingBudgetHelper() != ''}
helperText={routingBudgetHelper()}
label={t('Routing Budget')}
required={true}
value={
lightning.routingBudgetUnit == 'PPM'
? lightning.routingBudgetPPM
: lightning.routingBudgetSats
}
variant='outlined'
InputProps={{
endAdornment: (
<InputAdornment position='end'>
<Button
variant='text'
onClick={() => {
setLightning({
...lightning,
routingBudgetUnit:
lightning.routingBudgetUnit == 'PPM' ? 'Sats' : 'PPM',
});
}}
>
{lightning.routingBudgetUnit}
</Button>
</InputAdornment>
),
}}
inputProps={{
style: {
textAlign: 'center',
},
}}
onChange={onRoutingBudgetChange}
/>
</Grid>
{window.NativeRobosats === undefined ? (
<Grid item>
<Tooltip
enterTouchDelay={0}
leaveTouchDelay={4000}
placement='top'
title={t(
`Wrap this invoice using a Lnproxy server to protect your privacy (hides the receiving wallet).`,
)}
>
<div>
<FormControlLabel
onChange={(e) =>
setLightning({
...lightning,
useLnproxy: e.target.checked,
invoice: e.target.checked ? '' : lightning.invoice,
})
}
checked={lightning.useLnproxy}
control={<Checkbox />}
label={
<Typography color={lightning.useLnproxy ? 'primary' : 'text.secondary'}>
{t('Use Lnproxy')}
</Typography>
}
/>
</div>
</Tooltip>
</Grid>
) : (
<></>
)}
<Grid item>
<Collapse in={lightning.useLnproxy}>
<Grid
container
direction='column'
justifyContent='flex-start'
alignItems='center'
spacing={1}
>
<Grid item>
<FormControl error={badLnproxyServer != ''}>
<InputLabel id='select-label'>{t('Server')}</InputLabel>
<Select
sx={{ width: '14em' }}
label={t('Server')}
labelId='select-label'
value={lightning.lnproxyServer}
onChange={(e) =>
setLightning({ ...lightning, lnproxyServer: Number(e.target.value) })
}
>
{lnproxies.map((lnproxyServer, index) => (
<MenuItem key={index} value={index}>
<Typography>{lnproxyServer.name}</Typography>
</MenuItem>
))}
</Select>
{badLnproxyServer != '' ? (
<FormHelperText>{t(badLnproxyServer)}</FormHelperText>
) : (
<></>
)}
</FormControl>
</Grid>
<Grid item>
<TextField
sx={{ width: '14em' }}
disabled={!lightning.useLnproxy}
error={lnProxyBudgetHelper() != ''}
helperText={lnProxyBudgetHelper()}
label={t('Proxy Budget')}
value={
lightning.lnproxyBudgetUnit == 'PPM'
? lightning.lnproxyBudgetPPM
: lightning.lnproxyBudgetSats
}
variant='outlined'
InputProps={{
endAdornment: (
<InputAdornment position='end'>
<Button
variant='text'
onClick={() => {
setLightning({
...lightning,
lnproxyBudgetUnit:
lightning.lnproxyBudgetUnit == 'PPM' ? 'Sats' : 'PPM',
});
}}
>
{lightning.lnproxyBudgetUnit}
</Button>
</InputAdornment>
),
}}
inputProps={{
style: {
textAlign: 'center',
},
}}
onChange={onProxyBudgetChange}
/>
</Grid>
</Grid>
</Collapse>
</Grid>
</Grid>
</Collapse>
<Grid item>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Typography align='center' variant='body2'>
{t('Submit invoice for {{amountSats}} Sats', {
amountSats: pn(
lightning.useLnproxy ? lightning.lnproxyAmount : lightning.amount,
),
})}
</Typography>
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
<IconButton
sx={{ height: '0.5em' }}
onClick={() =>
systemClient.copyToClipboard(
lightning.useLnproxy ? lightning.lnproxyAmount : lightning.amount,
)
}
>
<ContentCopy sx={{ width: '0.8em' }} />
</IconButton>
</Tooltip>
</div>
</Grid>
<Grid item>
{lightning.useLnproxy ? (
<TextField
fullWidth={true}
disabled={!lightning.useLnproxy}
error={lightning.badLnproxy != ''}
helperText={lightning.badLnproxy ? t(lightning.badLnproxy) : ''}
label={t('Invoice to wrap')}
required
value={lightning.lnproxyInvoice}
inputProps={{
style: { textAlign: 'center' },
}}
variant='outlined'
onChange={(e) =>
setLightning({ ...lightning, lnproxyInvoice: e.target.value ?? '' })
}
/>
) : (
<></>
)}
<TextField
fullWidth={true}
sx={lightning.useLnproxy ? { borderRadius: 0 } : {}}
disabled={lightning.useLnproxy}
error={lightning.badInvoice != ''}
helperText={lightning.badInvoice ? t(lightning.badInvoice) : ''}
label={lightning.useLnproxy ? t('Wrapped invoice') : t('Payout Lightning Invoice')}
required
value={lightning.invoice}
inputProps={{
style: { textAlign: 'center', maxHeight: '8em' },
}}
variant={lightning.useLnproxy ? 'filled' : 'standard'}
multiline={lightning.useLnproxy ? false : true}
minRows={3}
maxRows={5}
onChange={(e) => setLightning({ ...lightning, invoice: e.target.value ?? '' })}
/>
</Grid>
<Grid item>
{lightning.useLnproxy ? (
<LoadingButton
loading={loadingLnproxy}
disabled={
lightning.lnproxyInvoice.length < 20 || badLnproxyServer || lightning.badLnproxy
}
onClick={fetchLnproxy}
variant='outlined'
color='primary'
>
{t('Wrap')}
</LoadingButton>
) : (
<></>
)}
<LoadingButton
loading={loading}
disabled={lightning.invoice == ''}
onClick={() => onClickSubmit(lightning.invoice)}
variant='outlined'
color='primary'
>
{t('Submit')}
</LoadingButton>
</Grid>
</Grid>
</Box>
</Grid>
{/* <Grid item>
<Box
sx={{
backgroundColor: 'background.paper',
border: '1px solid',
borderRadius: '0.3em',
width: '18em',
borderColor: theme.palette.mode === 'dark' ? '#434343' : '#c4c4c4',
'&:hover': {
borderColor: theme.palette.mode === 'dark' ? '#ffffff' : '#2f2f2f',
},
}}
>
<Grid
container
direction='column'
justifyContent='flex-start'
alignItems='center'
spacing={0.5}
padding={0.5}
>
<Collapse in={lightning.advancedOptions}>
<Tooltip
enterTouchDelay={0}
leaveTouchDelay={4000}
placement='top'
title={t(
`Set custom routing budget for the payout. If you don't know what this is, simply do not touch.`,
)}
>
<div>
<FormControlLabel
checked={lightning.useCustomBudget}
onChange={(e) =>
setLightning({
...lightning,
useCustomBudget: e.target.checked,
routingBudgetSats: defaultLightning.routingBudgetSats,
routingBudgetPPM: defaultLightning.routingBudgetPPM,
})
}
control={<Checkbox />}
label={
<Typography
style={{ display: 'flex', alignItems: 'center' }}
color={lightning.useCustomBudget ? 'primary' : 'text.secondary'}
>
{t('Use custom routing budget')}
</Typography>
}
/>
</div>
</Tooltip>
</Collapse>
</Grid>
</Box>
</Grid> */}
<Grid item>
<WalletsButton />
</Grid>
</Grid>
);

View File

@ -4,7 +4,7 @@ import { Grid, Typography, ToggleButtonGroup, ToggleButton } from '@mui/material
import currencies from '../../../../static/assets/currencies.json';
import { Order } from '../../../models';
import { Order, Settings } from '../../../models';
import { pn } from '../../../utils';
import { Bolt, Link } from '@mui/icons-material';
import { LightningPayoutForm, LightningForm, OnchainPayoutForm, OnchainForm } from '../Forms';
@ -19,6 +19,7 @@ interface PayoutPrompProps {
onchain: OnchainForm;
setOnchain: (state: OnchainForm) => void;
loadingOnchain: boolean;
settings: Settings;
}
export const PayoutPrompt = ({
@ -31,6 +32,7 @@ export const PayoutPrompt = ({
loadingOnchain,
onchain,
setOnchain,
settings,
}: PayoutPrompProps): JSX.Element => {
const { t } = useTranslation();
const currencyCode: string = currencies[`${order.currency}`];
@ -65,9 +67,9 @@ export const PayoutPrompt = ({
size='small'
value={tab}
exclusive
onChange={(mouseEvent, value: string) => setTab(value)}
onChange={(mouseEvent, value) => setTab(value == null ? tab : value)}
>
<ToggleButton value='lightning' disableRipple={true}>
<ToggleButton value='lightning'>
<div
style={{
display: 'flex',
@ -97,6 +99,7 @@ export const PayoutPrompt = ({
<Grid item style={{ display: tab == 'lightning' ? '' : 'none' }}>
<LightningPayoutForm
order={order}
settings={settings}
loading={loadingLightning}
lightning={lightning}
setLightning={setLightning}

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Box, CircularProgress, Grid, Typography, useTheme } from '@mui/material';
import Countdown, { CountdownRenderProps, zeroPad } from 'react-countdown';
import { Order } from '../../../models';
import { Order, Settings } from '../../../models';
import { LightningForm, LightningPayoutForm } from '../Forms';
interface RoutingFailedPromptProps {
@ -12,6 +12,7 @@ interface RoutingFailedPromptProps {
lightning: LightningForm;
loadingLightning: boolean;
setLightning: (state: LightningForm) => void;
settings: Settings;
}
interface FailureReasonProps {
@ -28,6 +29,7 @@ const FailureReason = ({ failureReason }: FailureReasonProps): JSX.Element => {
backgroundColor: theme.palette.background.paper,
borderRadius: '0.3em',
border: `1px solid ${theme.palette.text.secondary}`,
padding: '0.5em',
}}
>
<Typography variant='body2' align='center'>
@ -46,6 +48,7 @@ export const RoutingFailedPrompt = ({
loadingLightning,
lightning,
setLightning,
settings,
}: RoutingFailedPromptProps): JSX.Element => {
const { t } = useTranslation();
@ -95,6 +98,7 @@ export const RoutingFailedPrompt = ({
<Grid item>
<LightningPayoutForm
order={order}
settings={settings}
loading={loadingLightning}
lightning={lightning}
setLightning={setLightning}

View File

@ -263,7 +263,7 @@ const TradeSummary = ({
primary={t('{{revenueSats}} Sats', {
revenueSats: pn(platformSummary.trade_revenue_sats),
})}
secondary={t('Platform trade revenue')}
secondary={t('Coordinator trade revenue')}
/>
</ListItem>
@ -273,9 +273,9 @@ const TradeSummary = ({
</ListItemIcon>
<ListItemText
primary={t('{{routingFeeSats}} MiliSats', {
routingFeeSats: pn(platformSummary.routing_fee_sats),
routingFeeSats: pn(platformSummary.routing_budget_sats),
})}
secondary={t('Platform covered routing fee')}
secondary={t('Routing budget')}
/>
</ListItem>

View File

@ -44,7 +44,7 @@ import {
defaultDispute,
} from './Forms';
import { Order } from '../../models';
import { Order, Settings } from '../../models';
import { EncryptedChatMessage } from './EncryptedChat';
import { systemClient } from '../../services/System';
import CollabCancelAlert from './CollabCancelAlert';
@ -96,12 +96,14 @@ interface TradeBoxProps {
setBadOrder: (state: string | undefined) => void;
onRenewOrder: () => void;
onStartAgain: () => void;
settings: Settings;
baseUrl: string;
}
const TradeBox = ({
order,
setOrder,
settings,
baseUrl,
setBadOrder,
onRenewOrder,
@ -134,6 +136,7 @@ const TradeBox = ({
| 'submit_statement'
| 'rate_platform';
invoice?: string;
routing_budget_ppm?: number;
address?: string;
mining_fee_rate?: number;
statement?: string;
@ -143,6 +146,7 @@ const TradeBox = ({
const submitAction = function ({
action,
invoice,
routing_budget_ppm,
address,
mining_fee_rate,
statement,
@ -152,6 +156,7 @@ const TradeBox = ({
.post(baseUrl, '/api/order/?order_id=' + order.id, {
action,
invoice,
routing_budget_ppm,
address,
mining_fee_rate,
statement,
@ -201,7 +206,11 @@ const TradeBox = ({
const updateInvoice = function (invoice: string) {
setLoadingButtons({ ...noLoadingButtons, submitInvoice: true });
submitAction({ action: 'update_invoice', invoice });
submitAction({
action: 'update_invoice',
invoice,
routing_budget_ppm: lightning.routingBudgetPPM,
});
};
const updateAddress = function () {
@ -252,7 +261,7 @@ const TradeBox = ({
setWaitingWebln(true);
setOpen({ ...open, webln: true });
webln
.makeInvoice(order.trade_satoshis)
.makeInvoice(() => lightning.amount)
.then((invoice: any) => {
if (invoice) {
updateInvoice(invoice.paymentRequest);
@ -377,6 +386,7 @@ const TradeBox = ({
return (
<PayoutPrompt
order={order}
settings={settings}
onClickSubmitInvoice={updateInvoice}
loadingLightning={loadingButtons.submitInvoice}
lightning={lightning}
@ -424,6 +434,7 @@ const TradeBox = ({
return (
<PayoutPrompt
order={order}
settings={settings}
onClickSubmitInvoice={updateInvoice}
loadingLightning={loadingButtons.submitInvoice}
lightning={lightning}
@ -549,6 +560,7 @@ const TradeBox = ({
return (
<RoutingFailedPrompt
order={order}
settings={settings}
onClickSubmitInvoice={updateInvoice}
loadingLightning={loadingButtons.submitInvoice}
lightning={lightning}

View File

@ -1,5 +1,6 @@
export interface Coordinator {
alias: string;
enabled: boolean;
description: string | undefined;
coverLetter: string | undefined;
logo: string;

View File

@ -15,7 +15,7 @@ export interface TradeCoordinatorSummary {
contract_timestamp: Date;
contract_total_time: number;
contract_exchange_rate: number;
routing_fee_sats: number;
routing_budget_sats: number;
trade_revenue_sats: number;
}

View File

@ -1,9 +1,10 @@
[
{
"alias": "Inception",
"enabled": "true",
"description": "RoboSats original and experimental coordinator",
"coverLetter": "N/A",
"contact_methods": {
"contact": {
"email": "robosats@protonmail.com",
"telegram": "@robosats",
"twitter": "@robosats",

View File

@ -0,0 +1,11 @@
[
{
"name": "↬ Lnproxy Dev",
"mainnetClearnet": "lnproxy.org",
"mainnetTOR": "rdq6tvulanl7aqtupmoboyk2z3suzkdwurejwyjyjf4itr3zhxrm2lad.onion",
"mainnetI2P": "undefined",
"testnetClearnet": "undefined",
"testnetTOR": "undefined",
"testnetI2P": "undefined"
}
]