mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-26 10:46:23 -05:00
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:
parent
cd4aa5201a
commit
11b891f530
48 changed files with 2091 additions and 380 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue