feat(gui): Voluntary donations (#418)

* poc: monero receive pool with multiple redeem addresses for bob with given ratios

* fix: use new monero_receive_pool arg for buy_xmr

* update sweep/sweep_multi to return TxReceipt instead of String containing txid

* fix test (generate 1 block before checking balance after transfer)

* add move distribute function to rust, add property tests

* use rust distribute

* update sqlx cache/tempdb

* sqlx fix

* feat: update ui to display the monero address pool

* fix: remove unused functions, set dispatcher for tracing in wallet threads, use new subtract_fee wallet2 functionality

* Add patch system

* add wallet2_api_allow_subtract_from_fee patch

* apply git patches

* split monero-sys patches into chunks

* refactor

* .sqlx needs to be commited, revert unbound issue

* display pool on XmrRedeemInMempoolPage.tsx page, commit .sqlx folder

* fmt

* refactor

* assert MoneroAddressPool is on correct network, differntiate between stagenet and mainnet donaiton address

* looks ok

* re-add retry logic, database errors, ...

* add test

* add tests

* fmt comments, changelog

---------

Co-authored-by: Binarybaron <binarybaron@protonmail.com>
This commit is contained in:
Raphael 2025-06-25 16:37:47 +02:00 committed by GitHub
parent cd4aa5201a
commit 11b891f530
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 2091 additions and 380 deletions

View file

@ -3,7 +3,7 @@ import { ReactNode } from "react";
type Props = {
id?: string;
title: ReactNode;
title: ReactNode | null;
mainContent: ReactNode;
additionalContent: ReactNode;
loading: boolean;
@ -30,7 +30,7 @@ export default function InfoBox({
gap: 1,
}}
>
<Typography variant="subtitle1">{title}</Typography>
{title ? <Typography variant="subtitle1">{title}</Typography> : null}
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{icon}
{mainContent}

View file

@ -1,6 +1,7 @@
import { Link, Typography } from "@mui/material";
import { Box, Link, Typography } from "@mui/material";
import { ReactNode } from "react";
import InfoBox from "./InfoBox";
import TruncatedText from "renderer/components/other/TruncatedText";
export type TransactionInfoBoxProps = {
title: string;
@ -24,12 +25,14 @@ export default function TransactionInfoBox({
title={title}
mainContent={
<Typography variant="h5">
{txId ?? "Transaction ID not available"}
<TruncatedText truncateMiddle limit={40}>
{txId ?? "Transaction ID not available"}
</TruncatedText>
</Typography>
}
loading={loading}
additionalContent={
<>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="subtitle2">{additionalContent}</Typography>
{explorerUrlCreator != null &&
txId != null && ( // Only show the link if the txId is not null and we have a creator for the explorer URL
@ -39,7 +42,7 @@ export default function TransactionInfoBox({
</Link>
</Typography>
)}
</>
</Box>
}
icon={icon}
/>

View file

@ -1,4 +1,4 @@
import { Box, DialogContentText } from "@mui/material";
import { Box, DialogContentText, Typography } from "@mui/material";
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
@ -11,8 +11,8 @@ export default function XmrRedeemInMempoolPage(
return (
<Box>
<DialogContentText>
The swap was successful and the Monero has been sent to the address you
specified. The swap is completed and you may exit the application now.
The swap was successful and the Monero has been sent to the following
address(es). The swap is completed and you may exit the application now.
</DialogContentText>
<Box
style={{
@ -24,7 +24,55 @@ export default function XmrRedeemInMempoolPage(
<MoneroTransactionInfoBox
title="Monero Redeem Transaction"
txId={xmr_redeem_txid}
additionalContent={`The funds have been sent to the address ${state.xmr_redeem_address}`}
additionalContent={
<Box sx={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
{state.xmr_receive_pool.map((pool, index) => (
<Box
key={index}
sx={{
display: "flex",
flexDirection: "column",
gap: 0.5,
padding: 1,
border: 1,
borderColor: "divider",
borderRadius: 1,
backgroundColor: (theme) => theme.palette.action.hover,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="body2"
sx={(theme) => ({
fontWeight: 600,
color: theme.palette.text.primary,
})}
>
{pool.label} ({pool.percentage}%)
</Typography>
</Box>
<Typography
variant="caption"
sx={{
fontFamily: "monospace",
color: (theme) => theme.palette.text.secondary,
wordBreak: "break-all",
}}
>
{pool.address}
</Typography>
</Box>
))}
</Box>
</Box>
}
loading={false}
/>
<FeedbackInfoBox />

View file

@ -4,17 +4,14 @@ import {
PendingLockBitcoinApprovalRequest,
TauriSwapProgressEventContent,
} from "models/tauriModelExt";
import {
SatsAmount,
PiconeroAmount,
MoneroBitcoinExchangeRateFromAmounts,
} from "renderer/components/other/Units";
import { SatsAmount, PiconeroAmount } from "renderer/components/other/Units";
import { Box, Typography, Divider } from "@mui/material";
import { useActiveSwapId, usePendingLockBitcoinApproval } from "store/hooks";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import InfoBox from "renderer/components/modal/swap/InfoBox";
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
import CheckIcon from "@mui/icons-material/Check";
import ArrowRightAltIcon from "@mui/icons-material/ArrowRightAlt";
import TruncatedText from "renderer/components/other/TruncatedText";
/// A hook that returns the LockBitcoin confirmation request for the active swap
/// Returns null if no confirmation request is found
@ -63,107 +60,412 @@ export default function SwapSetupInflightPage({
);
}
const { btc_network_fee, xmr_receive_amount } =
const { btc_network_fee, monero_receive_pool, xmr_receive_amount } =
request.content.details.content;
return (
<InfoBox
title="Approve Swap"
icon={<></>}
loading={false}
mainContent={
<>
<Divider />
<>
{/* Grid layout for perfect alignment */}
<Box
sx={{
display: "grid",
gridTemplateColumns: "max-content auto max-content",
gap: "1.5rem",
alignItems: "stretch",
justifyContent: "center",
}}
>
{/* Row 1: Bitcoin box */}
<Box>
<BitcoinMainBox btc_lock_amount={btc_lock_amount} />
</Box>
{/* Row 1: Animated arrow */}
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<AnimatedArrow />
</Box>
{/* Row 1: Monero main box */}
<Box>
<MoneroMainBox
monero_receive_pool={monero_receive_pool}
xmr_receive_amount={xmr_receive_amount}
/>
</Box>
{/* Row 2: Empty space */}
<Box />
{/* Row 2: Empty space */}
<Box />
{/* Row 2: Secondary content */}
<Box>
<MoneroSecondaryContent
monero_receive_pool={monero_receive_pool}
xmr_receive_amount={xmr_receive_amount}
/>
</Box>
</Box>
<Box
sx={{
marginTop: 2,
display: "flex",
justifyContent: "center",
gap: 2,
}}
>
<PromiseInvokeButton
variant="text"
size="large"
sx={(theme) => ({ color: theme.palette.text.secondary })}
onInvoke={() => resolveApproval(request.content.request_id, false)}
displayErrorSnackbar
requiresContext
>
Deny
</PromiseInvokeButton>
<PromiseInvokeButton
variant="contained"
color="primary"
size="large"
onInvoke={() => resolveApproval(request.content.request_id, true)}
displayErrorSnackbar
requiresContext
endIcon={<CheckIcon />}
>
{`Confirm (${timeLeft}s)`}
</PromiseInvokeButton>
</Box>
</>
);
}
/**
* Pure presentational components -------------------------------------------------
* They live in the same file to avoid additional imports yet keep
* JSX for the main page tidy. All styling values are kept identical
* to their previous inline counterparts so that the visual appearance
* stays exactly the same while making the code easier to reason about.
*/
interface BitcoinSendSectionProps {
btc_lock_amount: number;
btc_network_fee: number;
}
const BitcoinMainBox = ({ btc_lock_amount }: { btc_lock_amount: number }) => (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: 1.5,
border: 1,
gap: "0.5rem 1rem",
borderColor: "warning.main",
borderRadius: 1,
backgroundColor: (theme) => theme.palette.warning.light + "10",
background: (theme) =>
`linear-gradient(135deg, ${theme.palette.warning.light}20, ${theme.palette.warning.light}05)`,
flex: "1 1 0",
height: "100%", // Match the height of the Monero box
}}
>
<Typography
variant="body1"
sx={(theme) => ({
color: theme.palette.text.primary,
})}
>
You send
</Typography>
<Typography
variant="body1"
sx={(theme) => ({
fontWeight: "bold",
color: theme.palette.warning.dark,
textShadow: "0 1px 2px rgba(0,0,0,0.1)",
})}
>
<SatsAmount amount={btc_lock_amount} />
</Typography>
</Box>
);
interface PoolBreakdownProps {
monero_receive_pool: Array<{
address: string;
label: string;
percentage: number;
}>;
xmr_receive_amount: number;
}
const PoolBreakdown = ({
monero_receive_pool,
xmr_receive_amount,
}: PoolBreakdownProps) => {
// Find the pool entry with the highest percentage to exclude it (since it's shown in main box)
const highestPercentagePool = monero_receive_pool.reduce((prev, current) =>
prev.percentage > current.percentage ? prev : current,
);
// Filter out the highest percentage pool since it's already displayed in the main box
const remainingPools = monero_receive_pool.filter(
(pool) => pool !== highestPercentagePool,
);
return (
<Box
sx={{ display: "flex", flexDirection: "column", gap: 1, width: "100%" }}
>
{remainingPools.map((pool) => (
<Box
key={pool.address}
sx={{
display: "flex",
justifyContent: "flex-start",
alignItems: "stretch",
padding: pool.percentage >= 5 ? 1.5 : 1.2,
border: 1,
borderColor:
pool.percentage >= 5 ? "success.main" : "success.light",
borderRadius: 1,
backgroundColor: (theme) =>
pool.percentage >= 5
? theme.palette.success.light + "10"
: theme.palette.action.hover,
width: "100%", // Ensure full width
minWidth: 0,
opacity: pool.percentage >= 5 ? 1 : 0.75,
transform: pool.percentage >= 5 ? "scale(1)" : "scale(0.95)",
animation:
pool.percentage >= 5
? "poolPulse 2s ease-in-out infinite"
: "none",
"@keyframes poolPulse": {
"0%": {
transform: "scale(1)",
opacity: 1,
},
"50%": {
transform: "scale(1.02)",
opacity: 0.95,
},
"100%": {
transform: "scale(1)",
opacity: 1,
},
},
}}
>
<Box
sx={{
display: "grid",
gridTemplateColumns: "auto 1fr",
rowGap: 1,
columnGap: 2,
alignItems: "center",
marginBlock: 2,
display: "flex",
flexDirection: "column",
gap: 0.5,
flex: "1 1 0",
minWidth: 0,
}}
>
<Typography
sx={(theme) => ({ color: theme.palette.text.secondary })}
>
You send
</Typography>
<Typography>
<SatsAmount amount={btc_lock_amount} />
</Typography>
<Typography
sx={(theme) => ({ color: theme.palette.text.secondary })}
>
Bitcoin network fees
</Typography>
<Typography>
<SatsAmount amount={btc_network_fee} />
</Typography>
<Typography
sx={(theme) => ({ color: theme.palette.text.secondary })}
>
You receive
</Typography>
<Typography
variant="body2"
sx={(theme) => ({
fontWeight: "bold",
color: theme.palette.success.main,
color: theme.palette.text.primary,
fontSize: "0.75rem",
fontWeight: 600,
})}
>
<PiconeroAmount amount={xmr_receive_amount} />
{pool.label === "user address" ? "Your Wallet" : pool.label}
</Typography>
<Typography
sx={(theme) => ({ color: theme.palette.text.secondary })}
variant="body2"
sx={{
fontFamily: "monospace",
fontSize: "0.75rem",
color: (theme) => theme.palette.text.secondary,
}}
>
Exchange rate
</Typography>
<Typography>
<MoneroBitcoinExchangeRateFromAmounts
satsAmount={btc_lock_amount}
piconeroAmount={xmr_receive_amount}
displayMarkup
/>
<TruncatedText truncateMiddle limit={15}>
{pool.address}
</TruncatedText>
</Typography>
</Box>
</>
}
additionalContent={
<Box
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-end",
gap: 0.5,
flex: "0 0 auto",
minWidth: 140,
justifyContent: "center",
}}
>
{pool.percentage >= 5 && (
<Typography
variant="body2"
sx={(theme) => ({
fontWeight: "bold",
color: theme.palette.success.main,
fontSize: "0.875rem",
whiteSpace: "nowrap",
})}
>
<PiconeroAmount
amount={(pool.percentage * Number(xmr_receive_amount)) / 100}
/>
</Typography>
)}
<Typography
variant="caption"
sx={(theme) => ({
color: theme.palette.text.secondary,
whiteSpace: "nowrap",
})}
>
{pool.percentage}%
</Typography>
</Box>
</Box>
))}
</Box>
);
};
interface MoneroReceiveSectionProps {
monero_receive_pool: PoolBreakdownProps["monero_receive_pool"];
xmr_receive_amount: number;
}
const MoneroMainBox = ({
monero_receive_pool,
xmr_receive_amount,
}: MoneroReceiveSectionProps) => {
// Find the pool entry with the highest percentage
const highestPercentagePool = monero_receive_pool.reduce((prev, current) =>
prev.percentage > current.percentage ? prev : current,
);
return (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: 1.5,
border: 1,
gap: "0.5rem 1rem",
borderColor: "success.main",
borderRadius: 1,
backgroundColor: (theme) => theme.palette.success.light + "10",
background: (theme) =>
`linear-gradient(135deg, ${theme.palette.success.light}20, ${theme.palette.success.light}05)`,
flex: "1 1 0",
}}
>
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.25 }}>
<Typography
variant="body1"
sx={(theme) => ({
color: theme.palette.text.primary,
fontWeight: 700,
letterSpacing: 0.5,
})}
>
{highestPercentagePool.label}
</Typography>
<Typography
variant="caption"
sx={{
marginTop: 2,
display: "flex",
justifyContent: "flex-end",
gap: 2,
fontFamily: "monospace",
fontSize: "0.65rem",
color: (theme) => theme.palette.text.secondary,
}}
>
<PromiseInvokeButton
variant="text"
size="large"
sx={(theme) => ({ color: theme.palette.text.secondary })}
onInvoke={() => resolveApproval(request.content.request_id, false)}
displayErrorSnackbar
requiresContext
>
Deny
</PromiseInvokeButton>
<PromiseInvokeButton
variant="contained"
color="primary"
size="large"
onInvoke={() => resolveApproval(request.content.request_id, true)}
displayErrorSnackbar
requiresContext
endIcon={<CheckIcon />}
>
{`Confirm & lock BTC (${timeLeft}s)`}
</PromiseInvokeButton>
</Box>
}
/>
<TruncatedText truncateMiddle limit={15}>
{highestPercentagePool.address}
</TruncatedText>
</Typography>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-end",
justifyContent: "center",
}}
>
<Typography
variant="h5"
sx={(theme) => ({
fontWeight: "bold",
color: theme.palette.success.dark,
textShadow: "0 1px 2px rgba(0,0,0,0.1)",
})}
>
<PiconeroAmount
amount={
(highestPercentagePool.percentage * Number(xmr_receive_amount)) /
100
}
/>
</Typography>
</Box>
</Box>
);
}
};
const MoneroSecondaryContent = ({
monero_receive_pool,
xmr_receive_amount,
}: MoneroReceiveSectionProps) => (
<PoolBreakdown
monero_receive_pool={monero_receive_pool}
xmr_receive_amount={xmr_receive_amount}
/>
);
// Arrow animation styling extracted for reuse
const arrowSx = {
fontSize: "3rem",
color: (theme: any) => theme.palette.primary.main,
animation: "slideArrow 2s infinite",
"@keyframes slideArrow": {
"0%": {
opacity: 0.6,
transform: "translateX(-8px)",
},
"50%": {
opacity: 1,
transform: "translateX(8px)",
},
"100%": {
opacity: 0.6,
transform: "translateX(-8px)",
},
},
} as const;
const AnimatedArrow = () => (
<Box
sx={{
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
alignSelf: "center",
flex: "0 0 auto",
}}
>
<ArrowRightAltIcon sx={arrowSx} />
</Box>
);

View file

@ -6,7 +6,7 @@ import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTe
import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { buyXmr } from "renderer/rpc";
import { useAppSelector } from "store/hooks";
import { useAppSelector, useSettings } from "store/hooks";
export default function InitPage() {
const [redeemAddress, setRedeemAddress] = useState("");
@ -18,12 +18,14 @@ export default function InitPage() {
const [refundAddressValid, setRefundAddressValid] = useState(false);
const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
const donationRatio = useSettings((s) => s.donateToDevelopment);
async function init() {
await buyXmr(
selectedMaker,
useExternalRefundAddress ? refundAddress : null,
redeemAddress,
donationRatio,
);
}

View file

@ -20,16 +20,14 @@ import {
useTheme,
Switch,
SelectChangeEvent,
TextField,
ToggleButton,
ToggleButtonGroup,
Chip,
LinearProgress,
} from "@mui/material";
import {
addNode,
addRendezvousPoint,
Blockchain,
DonateToDevelopmentTip,
FiatCurrency,
moveUpNode,
Network,
@ -41,12 +39,12 @@ import {
setTheme,
setTorEnabled,
setUseMoneroRpcPool,
setDonateToDevelopment,
} from "store/features/settingsSlice";
import { useAppDispatch, useNodes, useSettings } from "store/hooks";
import ValidatedTextField from "renderer/components/other/ValidatedTextField";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import HelpIcon from "@mui/icons-material/HelpOutline";
import { ReactNode, useState, useEffect } from "react";
import { ReactNode, useState } from "react";
import { Theme } from "renderer/components/theme";
import {
Add,
@ -61,8 +59,6 @@ import { getNetwork } from "store/config";
import { currencySymbol } from "utils/formatUtils";
import InfoBox from "renderer/components/modal/swap/InfoBox";
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
import { useAppSelector } from "store/hooks";
import { getNodeStatus } from "renderer/rpc";
import { setStatus } from "store/features/nodesSlice";
@ -95,6 +91,7 @@ export default function SettingsBox() {
<Table>
<TableBody>
<TorSettings />
<DonationTipSetting />
<ElectrumRpcUrlSetting />
<MoneroRpcPoolSetting />
<MoneroNodeUrlSetting />
@ -835,3 +832,127 @@ function RendezvousPointsSetting() {
</TableRow>
);
}
/**
* A setting that allows you to set a development donation tip amount
*/
function DonationTipSetting() {
const donateToDevelopment = useSettings((s) => s.donateToDevelopment);
const dispatch = useAppDispatch();
const handleTipSelect = (tipAmount: DonateToDevelopmentTip) => {
dispatch(setDonateToDevelopment(tipAmount));
};
const formatTipLabel = (tip: DonateToDevelopmentTip) => {
if (tip === false) return "0%";
return `${(tip * 100).toFixed(2)}%`;
};
const getTipButtonColor = (
tip: DonateToDevelopmentTip,
isSelected: boolean,
) => {
// Only show colored if selected and > 0
if (isSelected && tip !== false) {
return "#198754"; // Green for any tip > 0
}
return "#6c757d"; // Gray for all unselected or no tip
};
const getTipButtonSelectedColor = (tip: DonateToDevelopmentTip) => {
if (tip === false) return "#5c636a"; // Darker gray
return "#146c43"; // Darker green for any tip > 0
};
return (
<TableRow>
<TableCell>
<SettingLabel
label="Tip to the developers"
tooltip="Support the development of UnstoppableSwap by donating a small percentage of your swaps. Donations go directly to paying for infrastructure costs and developers"
/>
</TableCell>
<TableCell>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<ToggleButtonGroup
value={donateToDevelopment}
exclusive
onChange={(event, newValue) => {
if (newValue !== null) {
handleTipSelect(newValue);
}
}}
aria-label="Development tip amount"
size="small"
sx={{
width: "100%",
gap: 1,
"& .MuiToggleButton-root": {
flex: 1,
borderRadius: "8px",
fontWeight: "600",
textTransform: "none",
border: "2px solid",
"&:not(:first-of-type)": {
marginLeft: "8px",
borderLeft: "2px solid",
},
},
}}
>
{([false, 0.0005, 0.0075] as const).map((tipAmount) => (
<ToggleButton
key={String(tipAmount)}
value={tipAmount}
sx={{
borderColor: `${getTipButtonColor(tipAmount, donateToDevelopment === tipAmount)} !important`,
color:
donateToDevelopment === tipAmount
? "white"
: getTipButtonColor(
tipAmount,
donateToDevelopment === tipAmount,
),
backgroundColor:
donateToDevelopment === tipAmount
? getTipButtonColor(
tipAmount,
donateToDevelopment === tipAmount,
)
: "transparent",
"&:hover": {
backgroundColor: `${getTipButtonSelectedColor(tipAmount)} !important`,
color: "white !important",
},
"&.Mui-selected": {
backgroundColor: `${getTipButtonColor(tipAmount, true)} !important`,
color: "white !important",
"&:hover": {
backgroundColor: `${getTipButtonSelectedColor(tipAmount)} !important`,
},
},
}}
>
{formatTipLabel(tipAmount)}
</ToggleButton>
))}
</ToggleButtonGroup>
<Typography variant="subtitle2">
<ul style={{ margin: 0, padding: "0 1.5rem" }}>
<li>
Tips go <strong>directly</strong> towards paying for
infrastructure costs and developers
</li>
<li>
Only ever sent for <strong>successful</strong> swaps
</li>{" "}
(refunds are not counted)
<li>Monero is used for the tips, giving you full anonymity</li>
</ul>
</Typography>
</Box>
</TableCell>
</TableRow>
);
}

View file

@ -6,13 +6,12 @@ import {
TableCell,
TableContainer,
TableRow,
Typography,
} from "@mui/material";
import { OpenInNew } from "@mui/icons-material";
import { GetSwapInfoResponse } from "models/tauriModel";
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
import MonospaceTextBox from "renderer/components/other/MonospaceTextBox";
import {
MoneroBitcoinExchangeRate,
MoneroBitcoinExchangeRateFromAmounts,
PiconeroAmount,
SatsAmount,
@ -109,6 +108,48 @@ export default function HistoryRowExpanded({
</Link>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Monero receive pool</TableCell>
<TableCell>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
{swap.monero_receive_pool.map((pool, index) => (
<Box
key={index}
sx={{
display: "flex",
flexDirection: "column",
gap: 0.5,
padding: 1,
border: 1,
borderColor: "divider",
borderRadius: 1,
backgroundColor: (theme) => theme.palette.action.hover,
}}
>
<Typography
variant="body2"
sx={(theme) => ({
fontWeight: 600,
color: theme.palette.text.primary,
})}
>
{pool.label} ({pool.percentage}%)
</Typography>
<Typography
variant="caption"
sx={{
fontFamily: "monospace",
color: (theme) => theme.palette.text.secondary,
wordBreak: "break-all",
}}
>
{pool.address}
</Typography>
</Box>
))}
</Box>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>

View file

@ -27,6 +27,7 @@ import {
ResolveApprovalResponse,
RedactArgs,
RedactResponse,
LabeledMoneroAddress,
} from "models/tauriModel";
import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice";
import { store } from "./store/storeRenderer";
@ -36,14 +37,46 @@ import { MoneroRecoveryResponse } from "models/rpcModel";
import { ListSellersResponse } from "../models/tauriModel";
import logger from "utils/logger";
import { getNetwork, isTestnet } from "store/config";
import { Blockchain, Network } from "store/features/settingsSlice";
import {
Blockchain,
DonateToDevelopmentTip,
Network,
} from "store/features/settingsSlice";
import { setStatus } from "store/features/nodesSlice";
import { discoveredMakersByRendezvous } from "store/features/makersSlice";
import { CliLog } from "models/cliModel";
import { logsToRawString, parseLogsFromString } from "utils/parseUtils";
/// These are the official donation address for the UnstoppableSwap/core project
const DONATION_ADDRESS_MAINNET =
"49LEH26DJGuCyr8xzRAzWPUryzp7bpccC7Hie1DiwyfJEyUKvMFAethRLybDYrFdU1eHaMkKQpUPebY4WT3cSjEvThmpjPa";
const DONATION_ADDRESS_STAGENET =
"56E274CJxTyVuuFG651dLURKyneoJ5LsSA5jMq4By9z9GBNYQKG8y5ejTYkcvZxarZW6if14ve8xXav2byK4aRnvNdKyVxp";
/// Signature by binarybaron for the donation address
/// https://github.com/binarybaron/
///
/// Get the key from:
/// - https://github.com/UnstoppableSwap/core/blob/master/utils/gpg_keys/binarybaron.asc
/// - https://unstoppableswap.net/binarybaron.asc
const DONATION_ADDRESS_MAINNET_SIG = `
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
56E274CJxTyVuuFG651dLURKyneoJ5LsSA5jMq4By9z9GBNYQKG8y5ejTYkcvZxarZW6if14ve8xXav2byK4aRnvNdKyVxp is our donation address (signed by binarybaron)
-----BEGIN PGP SIGNATURE-----
iHUEARYKAB0WIQQ1qETX9LVbxE4YD/GZt10+FHaibgUCaFvzWQAKCRCZt10+FHai
bvC6APoCzCto6RsNYwUr7j1ou3xeVNiwMkUQbE0erKt70pT+tQD/fAvPxHtPyb56
XGFQ0pxL1PKzMd9npBGmGJhC4aTljQ4=
=OUK4
-----END PGP SIGNATURE-----
`;
export const PRESET_RENDEZVOUS_POINTS = [
"/dnsaddr/xxmr.cheap/p2p/12D3KooWMk3QyPS8D1d1vpHZoY7y2MnXdPE5yV6iyPvyuj4zcdxT",
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw",
"/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU",
];
export async function fetchSellersAtPresetRendezvousPoints() {
@ -142,20 +175,39 @@ export async function buyXmr(
seller: Maker,
bitcoin_change_address: string | null,
monero_receive_address: string,
donation_percentage: DonateToDevelopmentTip,
) {
await invoke<BuyXmrArgs, BuyXmrResponse>(
"buy_xmr",
bitcoin_change_address == null
? {
seller: providerToConcatenatedMultiAddr(seller),
monero_receive_address,
}
: {
seller: providerToConcatenatedMultiAddr(seller),
monero_receive_address,
bitcoin_change_address,
},
);
const address_pool: LabeledMoneroAddress[] = [];
if (donation_percentage !== false) {
const donation_address = isTestnet()
? DONATION_ADDRESS_STAGENET
: DONATION_ADDRESS_MAINNET;
address_pool.push(
{
address: monero_receive_address,
percentage: 100 - donation_percentage * 100,
label: "Your wallet",
},
{
address: donation_address,
percentage: donation_percentage * 100,
label: "Tip to the developers",
},
);
} else {
address_pool.push({
address: monero_receive_address,
percentage: 100,
label: "Your wallet",
});
}
await invoke<BuyXmrArgs, BuyXmrResponse>("buy_xmr", {
seller: providerToConcatenatedMultiAddr(seller),
monero_receive_pool: address_pool,
bitcoin_change_address,
});
}
export async function resumeSwap(swapId: string) {

View file

@ -1,6 +1,8 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Theme } from "renderer/components/theme";
export type DonateToDevelopmentTip = false | 0.0005 | 0.0075;
const DEFAULT_RENDEZVOUS_POINTS = [
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw",
@ -22,6 +24,9 @@ export interface SettingsState {
userHasSeenIntroduction: boolean;
/// List of rendezvous points
rendezvousPoints: string[];
/// Does the user want to donate parts of his swaps to funding the development
/// of the project?
donateToDevelopment: DonateToDevelopmentTip;
}
export enum FiatCurrency {
@ -124,6 +129,7 @@ const initialState: SettingsState = {
useMoneroRpcPool: true, // Default to using RPC pool
userHasSeenIntroduction: false,
rendezvousPoints: DEFAULT_RENDEZVOUS_POINTS,
donateToDevelopment: false, // Default to no donation
};
const alertsSlice = createSlice({
@ -212,6 +218,12 @@ const alertsSlice = createSlice({
setUseMoneroRpcPool(slice, action: PayloadAction<boolean>) {
slice.useMoneroRpcPool = action.payload;
},
setDonateToDevelopment(
slice,
action: PayloadAction<DonateToDevelopmentTip>,
) {
slice.donateToDevelopment = action.payload;
},
},
});
@ -228,6 +240,7 @@ export const {
setUserHasSeenIntroduction,
addRendezvousPoint,
removeRendezvousPoint,
setDonateToDevelopment,
} = alertsSlice.actions;
export default alertsSlice.reducer;