diff --git a/api/lightning/node.py b/api/lightning/node.py index 4b85c4df0..386acb567 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -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 = { @@ -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 = [] @@ -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 diff --git a/api/logics.py b/api/logics.py index d718bd04f..f2b229a7e 100644 --- a/api/logics.py +++ b/api/logics.py @@ -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: @@ -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"] @@ -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, @@ -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: @@ -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 diff --git a/api/models.py b/api/models.py index 790d67477..a4da1f719 100644 --- a/api/models.py +++ b/api/models.py @@ -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 diff --git a/api/serializers.py b/api/serializers.py index 0c9cfd086..ded1e039d 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -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 ) diff --git a/api/tasks.py b/api/tasks.py index 4a6fc57b7..700db183d 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -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 diff --git a/api/views.py b/api/views.py index 0d61507a1..457ba9adc 100644 --- a/api/views.py +++ b/api/views.py @@ -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") @@ -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) diff --git a/docker-compose.yml b/docker-compose.yml index bf02580a1..49e8f4233 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/frontend/src/basic/Main.tsx b/frontend/src/basic/Main.tsx index b2bd1d8dd..28e03a697 100644 --- a/frontend/src/basic/Main.tsx +++ b/frontend/src/basic/Main.tsx @@ -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 => { 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 = ({ > 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 ( 60 ? 'left' : 'bottom'} title={ { sx={{ height: '0.4em' }} variant='determinate' value={progress} - color={progress < 20 ? 'secondary' : 'primary'} + color={progress < 25 ? 'secondary' : 'primary'} /> ); diff --git a/frontend/src/components/OrderDetails/index.tsx b/frontend/src/components/OrderDetails/index.tsx index 91ca6b29a..0b062056c 100644 --- a/frontend/src/components/OrderDetails/index.tsx +++ b/frontend/src/components/OrderDetails/index.tsx @@ -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 {t('The order has expired')}; } 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 ? ( - - - {`${hours}h ${zeroPad(minutes)}m ${zeroPad(seconds)}s `} - - + + {`${hours}h ${zeroPad(minutes)}m ${zeroPad(seconds)}s `} + ) : ( - {`${hours}h ${zeroPad(minutes)}m ${zeroPad(seconds)}s `} + + {`${hours}h ${zeroPad(minutes)}m ${zeroPad(seconds)}s `} + ); } }; diff --git a/frontend/src/components/TradeBox/EncryptedChat/ChatHeader/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/ChatHeader/index.tsx index 968d6daf8..57783b8b6 100644 --- a/frontend/src/components/TradeBox/EncryptedChat/ChatHeader/index.tsx +++ b/frontend/src/components/TradeBox/EncryptedChat/ChatHeader/index.tsx @@ -77,7 +77,7 @@ const ChatHeader: React.FC = ({ connected, peerConnected, turtleMode, set > {t('Peer') + ': '} - {peerConnected ? t('connected') : t('disconnected')} + {connected ? (peerConnected ? t('connected') : t('disconnected')) : '...waiting'} diff --git a/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx index 9f6114546..99347f70a 100644 --- a/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx +++ b/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx @@ -72,6 +72,7 @@ const EncryptedSocketChat: React.FC = ({ useEffect(() => { if (![9, 10].includes(status)) { connection?.close(); + setConnection(undefined); } }, [status]); diff --git a/frontend/src/components/TradeBox/Forms/LightningPayout.tsx b/frontend/src/components/TradeBox/Forms/LightningPayout.tsx index febfec4dc..2bc50b6c0 100644 --- a/frontend/src/components/TradeBox/Forms/LightningPayout.tsx +++ b/frontend/src/components/TradeBox/Forms/LightningPayout.tsx @@ -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(false); + const [badLnproxyServer, setBadLnproxyServer] = useState(''); + + 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 ( - - - {t('Submit a valid invoice for {{amountSats}} Satoshis.', { - amountSats: pn(order.invoice_amount), - })} - +
+ + {t('Advanced options')} + { + const checked = e.target.checked; + setLightning({ + ...lightning, + advancedOptions: checked, + }); + }} + /> + - - + + + + + + + + + + ), + }} + inputProps={{ + style: { + textAlign: 'center', + }, + }} + onChange={onRoutingBudgetChange} + /> + + + {window.NativeRobosats === undefined ? ( + + +
+ + setLightning({ + ...lightning, + useLnproxy: e.target.checked, + invoice: e.target.checked ? '' : lightning.invoice, + }) + } + checked={lightning.useLnproxy} + control={} + label={ + + {t('Use Lnproxy')} + + } + /> +
+
+
+ ) : ( + <> + )} + + + + + + + {t('Server')} + + {badLnproxyServer != '' ? ( + {t(badLnproxyServer)} + ) : ( + <> + )} + + + + + + + + ), + }} + inputProps={{ + style: { + textAlign: 'center', + }, + }} + onChange={onProxyBudgetChange} + /> + + + + +
+
+ + +
+ + {t('Submit invoice for {{amountSats}} Sats', { + amountSats: pn( + lightning.useLnproxy ? lightning.lnproxyAmount : lightning.amount, + ), + })} + + + + systemClient.copyToClipboard( + lightning.useLnproxy ? lightning.lnproxyAmount : lightning.amount, + ) + } + > + + + +
+
+ + + {lightning.useLnproxy ? ( + + setLightning({ ...lightning, lnproxyInvoice: e.target.value ?? '' }) + } + /> + ) : ( + <> + )} + setLightning({ ...lightning, invoice: e.target.value ?? '' })} + /> + + + + {lightning.useLnproxy ? ( + + {t('Wrap')} + + ) : ( + <> + )} + onClickSubmit(lightning.invoice)} + variant='outlined' + color='primary' + > + {t('Submit')} + + +
+
- - + setLightning({ ...lightning, invoice: e.target.value ?? '' })} - /> - - - onClickSubmit(lightning.invoice)} - variant='outlined' - color='primary' > - {t('Submit')} - + + + +
+ + setLightning({ + ...lightning, + useCustomBudget: e.target.checked, + routingBudgetSats: defaultLightning.routingBudgetSats, + routingBudgetPPM: defaultLightning.routingBudgetPPM, + }) + } + control={} + label={ + + {t('Use custom routing budget')} + + } + /> +
+
+
+ +
+ +
*/} + + +
); diff --git a/frontend/src/components/TradeBox/Prompts/Payout.tsx b/frontend/src/components/TradeBox/Prompts/Payout.tsx index fc8db73f7..1cc8004dd 100644 --- a/frontend/src/components/TradeBox/Prompts/Payout.tsx +++ b/frontend/src/components/TradeBox/Prompts/Payout.tsx @@ -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)} > - +
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', }} > @@ -46,6 +48,7 @@ export const RoutingFailedPrompt = ({ loadingLightning, lightning, setLightning, + settings, }: RoutingFailedPromptProps): JSX.Element => { const { t } = useTranslation(); @@ -95,6 +98,7 @@ export const RoutingFailedPrompt = ({ @@ -273,9 +273,9 @@ const TradeSummary = ({ diff --git a/frontend/src/components/TradeBox/index.tsx b/frontend/src/components/TradeBox/index.tsx index 835752176..f0b503d11 100644 --- a/frontend/src/components/TradeBox/index.tsx +++ b/frontend/src/components/TradeBox/index.tsx @@ -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 (