mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-26 02:36:20 -05:00
feat(gui, cli): Request quotes concurrently at all sellers (#429)
* feat(gui): Implement base structure for new swap ux - refactored file structure to match common projecte structure - implement step get bitcoin * feat(gui): Implement basic multi step modal * feat(gui): Add outline of add choose maker and offer step * feat(gui): Add receive address selector * refactor(gui): format code * feat(gui): Make Swap Overveiw interactive * feat(gui): Add action to swap amount selector to quickly go to deposit bitcoin step * progress * feat(gui, cli): Request quotes concurrently at all sellers * refresh offers occasionally, display progress * progress * feat(gui, cli): Request quotes concurrently at all sellers * refresh offers occasionally, display progress * progress, works again * allow closing dialog without warning if no funds have been locked * progress * feat(gui): Rewrite Swap Components to have flow directly on swap page * feat: log monero_rpc_pool only at >= INFO level * remove full_url, add migration to change scheme of node.monerodevs.org to http * feat: send known_quotes with WaitingForBitcoinDeposit Tauri progress event (even if our balance is too low) * lock swap lock later * refactor(monero-rpc-pool): Pass around tuple of (scheme, host, port) as nodes * refactor(gui): Remove modal for swap and adjust few pages for swap process - Moved files from swap modal to page directory - Use new layouts for init page - Use new layout for depositBTC Step - Use new layout for Offer Page * allow cancel before lock * remove unused code * dynamic layout, chips for amounts * feat(gui): Add breakpoints * remove continue button, add select button on each maker box * add GetCurrentSwapArgs tauri command to only suspend swap if one is actually running * feat(gui): Show all known quotes and disable the ones that aren't available * fix get_current_swap, kill tasks when buy_xmr is cancelled * cleanup: remove CleanupGuard * feat(gui): Add cancel button on every page * refactor(gui): Fix merge issues * refactor(gui): Unify Cancel Button insertion by using a swap base page * refactor(gui): Unify Cancel Button insertion by using a swap base page * refactor(gui): Remove deeply nested relative paths * refactor(gui): Made BaseSwapPage obsolete by moving Cancel Button to SwapStatePage * refactor(gui): Adjust condition for showing SwapSuspendAlert * fix(gui): Fetch previous monero redeem addresses repeatedly * refactor(gui): Remove QR Code from deposit and choose maker page * refactor(gui): Don't display dialog on History page * fix(gui): If no swap was running "suspend_current_swap" will still return success now, less logic in the CancelButton * get offer select working * refactor: dont display cancel button on set redeem address page * feat: add pagination to offers * refactor * emit partial events for list_sellers * refactor: remove torSlice * refactor: use sync (non tokio) mutex for approvals * throttle getSwapInfo calls * feat: add debug page back, add info in suspend dialog about what will happen * refactor: format files * refactor(gui): Remove sortMakers method and replace with method that sorts approvals * refactor(gui): Refactor swap page structure * fix(gui): Add breakpoints to swapSetupInflightPage * feat(gui): Add flag for outdated makers * refactor(gui): Reduce fetch rate for maker quotes * fix(gui): Debug Window size * no unwrap --------- Co-authored-by: b-enedict <benedict.seuss@gmail.com>
This commit is contained in:
parent
7606982de3
commit
210cc04ced
80 changed files with 1744 additions and 1153 deletions
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
NEVER NEVER NEVER upgrade or changr the versions used in dependencies without explicitly asking for confirmation!!
|
||||||
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -7591,15 +7591,6 @@ dependencies = [
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "qrcode"
|
|
||||||
version = "0.14.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
|
|
||||||
dependencies = [
|
|
||||||
"image",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-error"
|
name = "quick-error"
|
||||||
version = "1.2.3"
|
version = "1.2.3"
|
||||||
|
|
@ -9823,7 +9814,6 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pem",
|
"pem",
|
||||||
"proptest",
|
"proptest",
|
||||||
"qrcode",
|
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rand_chacha 0.3.1",
|
"rand_chacha 0.3.1",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
@ -10806,6 +10796,8 @@ dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
|
"futures-util",
|
||||||
|
"hashbrown 0.15.4",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
"@tauri-apps/plugin-store": "^2.0.0",
|
"@tauri-apps/plugin-store": "^2.0.0",
|
||||||
"@tauri-apps/plugin-updater": "2.7.1",
|
"@tauri-apps/plugin-updater": "2.7.1",
|
||||||
"@types/react-redux": "^7.1.34",
|
"@types/react-redux": "^7.1.34",
|
||||||
|
"boring-avatars": "^1.11.2",
|
||||||
"humanize-duration": "^3.32.1",
|
"humanize-duration": "^3.32.1",
|
||||||
"jdenticon": "^3.3.0",
|
"jdenticon": "^3.3.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import {
|
||||||
ExpiredTimelocks,
|
ExpiredTimelocks,
|
||||||
GetSwapInfoResponse,
|
GetSwapInfoResponse,
|
||||||
PendingCompleted,
|
PendingCompleted,
|
||||||
|
QuoteWithAddress,
|
||||||
|
SelectMakerDetails,
|
||||||
TauriBackgroundProgress,
|
TauriBackgroundProgress,
|
||||||
TauriSwapProgressEvent,
|
TauriSwapProgressEvent,
|
||||||
} from "./tauriModel";
|
} from "./tauriModel";
|
||||||
|
|
@ -303,3 +305,49 @@ export function isBitcoinSyncProgress(
|
||||||
): progress is TauriBitcoinSyncProgress {
|
): progress is TauriBitcoinSyncProgress {
|
||||||
return progress.componentName === "SyncingBitcoinWallet";
|
return progress.componentName === "SyncingBitcoinWallet";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PendingSelectMakerApprovalRequest = PendingApprovalRequest & {
|
||||||
|
request: { type: "SelectMaker"; content: SelectMakerDetails };
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SortableQuoteWithAddress extends QuoteWithAddress {
|
||||||
|
expiration_ts?: number;
|
||||||
|
request_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPendingSelectMakerApprovalEvent(
|
||||||
|
event: ApprovalRequest,
|
||||||
|
): event is PendingSelectMakerApprovalRequest {
|
||||||
|
// Check if the request is pending
|
||||||
|
if (event.request_status.state !== "Pending") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the request is a SelectMaker request
|
||||||
|
return event.request.type === "SelectMaker";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if any funds have been locked yet based on the swap progress event
|
||||||
|
* Returns true for events where funds have been locked
|
||||||
|
* @param event The TauriSwapProgressEvent to check
|
||||||
|
* @returns True if funds have been locked, false otherwise
|
||||||
|
*/
|
||||||
|
export function haveFundsBeenLocked(
|
||||||
|
event: TauriSwapProgressEvent | null,
|
||||||
|
): boolean {
|
||||||
|
if (event === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case "RequestingQuote":
|
||||||
|
case "Resuming":
|
||||||
|
case "ReceivedQuote":
|
||||||
|
case "WaitingForBtcDeposit":
|
||||||
|
case "SwapSetupInflight":
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { Box } from "@mui/material";
|
|
||||||
import { Alert } from "@mui/material";
|
|
||||||
import { useAppSelector } from "store/hooks";
|
|
||||||
import { SatsAmount } from "../other/Units";
|
|
||||||
import WalletRefreshButton from "../pages/wallet/WalletRefreshButton";
|
|
||||||
|
|
||||||
export default function RemainingFundsWillBeUsedAlert() {
|
|
||||||
const balance = useAppSelector((s) => s.rpc.state.balance);
|
|
||||||
|
|
||||||
if (balance == null || balance <= 0) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ paddingBottom: 1 }}>
|
|
||||||
<Alert
|
|
||||||
severity="warning"
|
|
||||||
action={<WalletRefreshButton />}
|
|
||||||
variant="filled"
|
|
||||||
>
|
|
||||||
The remaining funds of <SatsAmount amount={balance} /> in the wallet
|
|
||||||
will be used for the next swap
|
|
||||||
</Alert>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -14,6 +14,7 @@ import HumanizedBitcoinBlockDuration from "../../other/HumanizedBitcoinBlockDura
|
||||||
import TruncatedText from "../../other/TruncatedText";
|
import TruncatedText from "../../other/TruncatedText";
|
||||||
import { SwapMoneroRecoveryButton } from "../../pages/history/table/SwapMoneroRecoveryButton";
|
import { SwapMoneroRecoveryButton } from "../../pages/history/table/SwapMoneroRecoveryButton";
|
||||||
import { TimelockTimeline } from "./TimelockTimeline";
|
import { TimelockTimeline } from "./TimelockTimeline";
|
||||||
|
import { useIsSpecificSwapRunning } from "store/hooks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for displaying a list of messages.
|
* Component for displaying a list of messages.
|
||||||
|
|
@ -233,13 +234,15 @@ const UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD = 72 - 4;
|
||||||
*/
|
*/
|
||||||
export default function SwapStatusAlert({
|
export default function SwapStatusAlert({
|
||||||
swap,
|
swap,
|
||||||
isRunning,
|
|
||||||
onlyShowIfUnusualAmountOfTimeHasPassed,
|
onlyShowIfUnusualAmountOfTimeHasPassed,
|
||||||
}: {
|
}: {
|
||||||
swap: GetSwapInfoResponseExt;
|
swap: GetSwapInfoResponseExt;
|
||||||
isRunning: boolean;
|
|
||||||
onlyShowIfUnusualAmountOfTimeHasPassed?: boolean;
|
onlyShowIfUnusualAmountOfTimeHasPassed?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
if (swap == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// If the swap is completed, we do not need to display anything
|
// If the swap is completed, we do not need to display anything
|
||||||
if (!isGetSwapInfoResponseRunningSwap(swap)) {
|
if (!isGetSwapInfoResponseRunningSwap(swap)) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -250,16 +253,18 @@ export default function SwapStatusAlert({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are only showing if an unusual amount of time has passed, we need to check if the swap has been running for a while
|
const hasUnusualAmountOfTimePassed =
|
||||||
if (
|
|
||||||
onlyShowIfUnusualAmountOfTimeHasPassed &&
|
|
||||||
swap.timelock.type === "None" &&
|
swap.timelock.type === "None" &&
|
||||||
swap.timelock.content.blocks_left >
|
swap.timelock.content.blocks_left >
|
||||||
UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD
|
UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD;
|
||||||
) {
|
|
||||||
|
// If we are only showing if an unusual amount of time has passed, we need to check if the swap has been running for a while
|
||||||
|
if (onlyShowIfUnusualAmountOfTimeHasPassed && hasUnusualAmountOfTimePassed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRunning = useIsSpecificSwapRunning(swap.swap_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
key={swap.swap_id}
|
key={swap.swap_id}
|
||||||
|
|
@ -274,7 +279,11 @@ export default function SwapStatusAlert({
|
||||||
>
|
>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
{isRunning ? (
|
{isRunning ? (
|
||||||
"Swap has been running for a while"
|
hasUnusualAmountOfTimePassed ? (
|
||||||
|
"Swap has been running for a while"
|
||||||
|
) : (
|
||||||
|
"Swap is running"
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is not running
|
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is not running
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export default function SwapTxLockAlertsBox() {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
{swaps.map((swap) => (
|
{swaps.map((swap) => (
|
||||||
<SwapStatusAlert key={swap.swap_id} swap={swap} isRunning={false} />
|
<SwapStatusAlert key={swap.swap_id} swap={swap} />
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ export default function MoneroAddressTextField({
|
||||||
setAddresses(response.addresses);
|
setAddresses(response.addresses);
|
||||||
};
|
};
|
||||||
fetchAddresses();
|
fetchAddresses();
|
||||||
|
|
||||||
|
const interval = setInterval(fetchAddresses, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,13 @@ import {
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import CircleIcon from "@mui/icons-material/Circle";
|
||||||
import { suspendCurrentSwap } from "renderer/rpc";
|
import { suspendCurrentSwap } from "renderer/rpc";
|
||||||
import PromiseInvokeButton from "../PromiseInvokeButton";
|
import PromiseInvokeButton from "../PromiseInvokeButton";
|
||||||
|
|
||||||
|
|
@ -20,10 +26,42 @@ export default function SwapSuspendAlert({
|
||||||
}: SwapCancelAlertProps) {
|
}: SwapCancelAlertProps) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose}>
|
<Dialog open={open} onClose={onClose}>
|
||||||
<DialogTitle>Force stop running operation?</DialogTitle>
|
<DialogTitle>Suspend running swap?</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText component="div">
|
||||||
Are you sure you want to force stop the running swap?
|
<List dense>
|
||||||
|
<ListItem sx={{ pl: 0 }}>
|
||||||
|
<ListItemIcon sx={{ minWidth: "30px" }}>
|
||||||
|
<CircleIcon sx={{ fontSize: "8px" }} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="The swap and any network requests between you and the maker will be paused until you resume" />
|
||||||
|
</ListItem>
|
||||||
|
<ListItem sx={{ pl: 0 }}>
|
||||||
|
<ListItemIcon sx={{ minWidth: "30px" }}>
|
||||||
|
<CircleIcon sx={{ fontSize: "8px" }} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<>
|
||||||
|
Refund timelocks will <strong>not</strong> be paused. They
|
||||||
|
will continue to count down until they expire
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem sx={{ pl: 0 }}>
|
||||||
|
<ListItemIcon sx={{ minWidth: "30px" }}>
|
||||||
|
<CircleIcon sx={{ fontSize: "8px" }} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="You can monitor the timelock on the history page" />
|
||||||
|
</ListItem>
|
||||||
|
<ListItem sx={{ pl: 0 }}>
|
||||||
|
<ListItemIcon sx={{ minWidth: "30px" }}>
|
||||||
|
<CircleIcon sx={{ fontSize: "8px" }} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="If the refund timelock expires, a refund will be initiated in the background. This still requires the app to be running." />
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
|
@ -35,7 +73,7 @@ export default function SwapSuspendAlert({
|
||||||
onSuccess={onClose}
|
onSuccess={onClose}
|
||||||
onInvoke={suspendCurrentSwap}
|
onInvoke={suspendCurrentSwap}
|
||||||
>
|
>
|
||||||
Force stop
|
Suspend
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { Box } from "@mui/material";
|
|
||||||
import QRCode from "react-qr-code";
|
|
||||||
|
|
||||||
export default function BitcoinQrCode({ address }: { address: string }) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
height: "100%",
|
|
||||||
margin: "0 auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<QRCode
|
|
||||||
value={`bitcoin:${address}`}
|
|
||||||
size={256}
|
|
||||||
style={{ height: "auto", maxWidth: "100%", width: "100%" }}
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
|
||||||
/* @ts-ignore */
|
|
||||||
viewBox="0 0 256 256"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +1,11 @@
|
||||||
import {
|
import { Box, Dialog, DialogActions, DialogContent } from "@mui/material";
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { swapReset } from "store/features/swapSlice";
|
import { useAppSelector } from "store/hooks";
|
||||||
import { useAppDispatch, useAppSelector, useIsSwapRunning } from "store/hooks";
|
|
||||||
import SwapSuspendAlert from "../SwapSuspendAlert";
|
|
||||||
import DebugPage from "./pages/DebugPage";
|
import DebugPage from "./pages/DebugPage";
|
||||||
import SwapStatePage from "./pages/SwapStatePage";
|
import SwapStatePage from "renderer/components/pages/swap/swap/SwapStatePage";
|
||||||
import SwapDialogTitle from "./SwapDialogTitle";
|
import SwapDialogTitle from "./SwapDialogTitle";
|
||||||
import SwapStateStepper from "./SwapStateStepper";
|
import SwapStateStepper from "./SwapStateStepper";
|
||||||
|
import CancelButton from "renderer/components/pages/swap/swap/CancelButton";
|
||||||
|
|
||||||
export default function SwapDialog({
|
export default function SwapDialog({
|
||||||
open,
|
open,
|
||||||
|
|
@ -22,26 +15,13 @@ export default function SwapDialog({
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const swap = useAppSelector((state) => state.swap);
|
const swap = useAppSelector((state) => state.swap);
|
||||||
const isSwapRunning = useIsSwapRunning();
|
|
||||||
const [debug, setDebug] = useState(false);
|
const [debug, setDebug] = useState(false);
|
||||||
const [openSuspendAlert, setOpenSuspendAlert] = useState(false);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
function onCancel() {
|
|
||||||
if (isSwapRunning) {
|
|
||||||
setOpenSuspendAlert(true);
|
|
||||||
} else {
|
|
||||||
onClose();
|
|
||||||
dispatch(swapReset());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This prevents an issue where the Dialog is shown for a split second without a present swap state
|
// This prevents an issue where the Dialog is shown for a split second without a present swap state
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onCancel} maxWidth="md" fullWidth>
|
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||||
<SwapDialogTitle
|
<SwapDialogTitle
|
||||||
debug={debug}
|
debug={debug}
|
||||||
setDebug={setDebug}
|
setDebug={setDebug}
|
||||||
|
|
@ -78,23 +58,8 @@ export default function SwapDialog({
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onCancel} variant="text">
|
<CancelButton />
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
variant="contained"
|
|
||||||
onClick={onCancel}
|
|
||||||
disabled={isSwapRunning || swap.state === null}
|
|
||||||
>
|
|
||||||
Done
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|
||||||
<SwapSuspendAlert
|
|
||||||
open={openSuspendAlert}
|
|
||||||
onClose={() => setOpenSuspendAlert(false)}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Box, DialogTitle, Typography } from "@mui/material";
|
import { Box, DialogTitle, Typography } from "@mui/material";
|
||||||
import DebugPageSwitchBadge from "./pages/DebugPageSwitchBadge";
|
import DebugPageSwitchBadge from "./pages/DebugPageSwitchBadge";
|
||||||
import FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge";
|
import FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge";
|
||||||
import TorStatusBadge from "./pages/TorStatusBadge";
|
|
||||||
|
|
||||||
export default function SwapDialogTitle({
|
export default function SwapDialogTitle({
|
||||||
title,
|
title,
|
||||||
|
|
@ -24,7 +23,6 @@ export default function SwapDialogTitle({
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gridGap: 1 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gridGap: 1 }}>
|
||||||
<FeedbackSubmitBadge />
|
<FeedbackSubmitBadge />
|
||||||
<DebugPageSwitchBadge enabled={debug} setEnabled={setDebug} />
|
<DebugPageSwitchBadge enabled={debug} setEnabled={setDebug} />
|
||||||
<TorStatusBadge />
|
|
||||||
</Box>
|
</Box>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ function getActiveStep(state: SwapState | null): PathStep | null {
|
||||||
case "ReceivedQuote":
|
case "ReceivedQuote":
|
||||||
case "WaitingForBtcDeposit":
|
case "WaitingForBtcDeposit":
|
||||||
case "SwapSetupInflight":
|
case "SwapSetupInflight":
|
||||||
return [PathType.HAPPY_PATH, 0, isReleased];
|
return null; // No funds have been locked yet
|
||||||
|
|
||||||
// Step 1: Waiting for Bitcoin lock confirmation
|
// Step 1: Waiting for Bitcoin lock confirmation
|
||||||
// Bitcoin has been locked, waiting for the counterparty to lock their XMR
|
// Bitcoin has been locked, waiting for the counterparty to lock their XMR
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,11 @@ import JsonTreeView from "../../../other/JSONViewTree";
|
||||||
import CliLogsBox from "../../../other/RenderedCliLog";
|
import CliLogsBox from "../../../other/RenderedCliLog";
|
||||||
|
|
||||||
export default function DebugPage() {
|
export default function DebugPage() {
|
||||||
const torStdOut = useAppSelector((s) => s.tor.stdOut);
|
|
||||||
const logs = useActiveSwapLogs();
|
const logs = useActiveSwapLogs();
|
||||||
const guiState = useAppSelector((s) => s);
|
|
||||||
const cliState = useActiveSwapInfo();
|
const cliState = useActiveSwapInfo();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box sx={{ padding: 2, display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -27,18 +25,6 @@ export default function DebugPage() {
|
||||||
logs={logs}
|
logs={logs}
|
||||||
label="Logs relevant to the swap (only current session)"
|
label="Logs relevant to the swap (only current session)"
|
||||||
/>
|
/>
|
||||||
<JsonTreeView
|
|
||||||
data={guiState}
|
|
||||||
label="Internal GUI State (inferred from Logs)"
|
|
||||||
/>
|
|
||||||
<JsonTreeView
|
|
||||||
data={cliState}
|
|
||||||
label="Swap Daemon State (exposed via API)"
|
|
||||||
/>
|
|
||||||
<CliLogsBox
|
|
||||||
label="Tor Daemon Logs"
|
|
||||||
logs={(torStdOut || "").split("\n")}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { IconButton, Tooltip } from "@mui/material";
|
|
||||||
import { useAppSelector } from "store/hooks";
|
|
||||||
import TorIcon from "../../../icons/TorIcon";
|
|
||||||
|
|
||||||
export default function TorStatusBadge() {
|
|
||||||
const tor = useAppSelector((s) => s.tor);
|
|
||||||
|
|
||||||
if (tor.processRunning) {
|
|
||||||
return (
|
|
||||||
<Tooltip title="Tor is running in the background">
|
|
||||||
<IconButton size="large">
|
|
||||||
<TorIcon htmlColor="#7D4698" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import SwapStatusAlert from "renderer/components/alert/SwapStatusAlert/SwapStatusAlert";
|
|
||||||
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
|
||||||
import { useActiveSwapInfo, useSwapInfosSortedByDate } from "store/hooks";
|
|
||||||
import { Box } from "@mui/material";
|
|
||||||
|
|
||||||
export default function EncryptedSignatureSentPage() {
|
|
||||||
const swap = useActiveSwapInfo();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
||||||
<SwapStatusAlert
|
|
||||||
swap={swap}
|
|
||||||
isRunning={true}
|
|
||||||
onlyShowIfUnusualAmountOfTimeHasPassed={true}
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
minHeight: "10rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CircularProgressWithSubtitle description="Waiting for them to redeem the Bitcoin" />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
|
||||||
|
|
||||||
export function SyncingMoneroWalletPage() {
|
|
||||||
return (
|
|
||||||
<CircularProgressWithSubtitle description="Syncing Monero wallet with blockchain, this might take a while..." />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
import { Box, TextField, Typography } from "@mui/material";
|
|
||||||
import { BidQuote } from "models/tauriModel";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useAppSelector } from "store/hooks";
|
|
||||||
import { btcToSats, satsToBtc } from "utils/conversionUtils";
|
|
||||||
import { MoneroAmount } from "../../../../other/Units";
|
|
||||||
|
|
||||||
const MONERO_FEE = 0.000016;
|
|
||||||
|
|
||||||
function calcBtcAmountWithoutFees(amount: number, fees: number) {
|
|
||||||
return amount - fees;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DepositAmountHelper({
|
|
||||||
min_deposit_until_swap_will_start,
|
|
||||||
max_deposit_until_maximum_amount_is_reached,
|
|
||||||
min_bitcoin_lock_tx_fee,
|
|
||||||
quote,
|
|
||||||
}: {
|
|
||||||
min_deposit_until_swap_will_start: number;
|
|
||||||
max_deposit_until_maximum_amount_is_reached: number;
|
|
||||||
min_bitcoin_lock_tx_fee: number;
|
|
||||||
quote: BidQuote;
|
|
||||||
}) {
|
|
||||||
const [amount, setAmount] = useState(min_deposit_until_swap_will_start);
|
|
||||||
const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0;
|
|
||||||
|
|
||||||
function getTotalAmountAfterDeposit() {
|
|
||||||
return amount + bitcoinBalance;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasError() {
|
|
||||||
return (
|
|
||||||
amount < min_deposit_until_swap_will_start ||
|
|
||||||
getTotalAmountAfterDeposit() > max_deposit_until_maximum_amount_is_reached
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function calcXMRAmount(): number | null {
|
|
||||||
if (Number.isNaN(amount)) return null;
|
|
||||||
if (hasError()) return null;
|
|
||||||
if (quote.price == null) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
calcBtcAmountWithoutFees(
|
|
||||||
getTotalAmountAfterDeposit(),
|
|
||||||
min_bitcoin_lock_tx_fee,
|
|
||||||
) /
|
|
||||||
quote.price -
|
|
||||||
MONERO_FEE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="subtitle2">
|
|
||||||
Depositing {bitcoinBalance > 0 && <>another</>}
|
|
||||||
</Typography>
|
|
||||||
<TextField
|
|
||||||
error={!!hasError()}
|
|
||||||
value={satsToBtc(amount)}
|
|
||||||
onChange={(e) => setAmount(btcToSats(parseFloat(e.target.value)))}
|
|
||||||
size="small"
|
|
||||||
type="number"
|
|
||||||
sx={{
|
|
||||||
"& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button":
|
|
||||||
{
|
|
||||||
display: "none",
|
|
||||||
},
|
|
||||||
"& input[type=number]": {
|
|
||||||
MozAppearance: "textfield",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography variant="subtitle2">
|
|
||||||
BTC will give you approximately{" "}
|
|
||||||
<MoneroAmount amount={calcXMRAmount()} />.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
import { Box, Typography } from "@mui/material";
|
|
||||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
|
||||||
import BitcoinIcon from "../../../../icons/BitcoinIcon";
|
|
||||||
import { MoneroSatsExchangeRate, SatsAmount } from "../../../../other/Units";
|
|
||||||
import DepositAddressInfoBox from "../../DepositAddressInfoBox";
|
|
||||||
import DepositAmountHelper from "./DepositAmountHelper";
|
|
||||||
import { Alert } from "@mui/material";
|
|
||||||
|
|
||||||
export default function WaitingForBtcDepositPage({
|
|
||||||
deposit_address,
|
|
||||||
min_deposit_until_swap_will_start,
|
|
||||||
max_deposit_until_maximum_amount_is_reached,
|
|
||||||
min_bitcoin_lock_tx_fee,
|
|
||||||
max_giveable,
|
|
||||||
quote,
|
|
||||||
}: TauriSwapProgressEventContent<"WaitingForBtcDeposit">) {
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<DepositAddressInfoBox
|
|
||||||
title="Bitcoin Deposit Address"
|
|
||||||
address={deposit_address}
|
|
||||||
additionalContent={
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
paddingTop: 1,
|
|
||||||
gap: 0.5,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="subtitle2">
|
|
||||||
<ul>
|
|
||||||
{max_giveable > 0 ? (
|
|
||||||
<li>
|
|
||||||
You have already deposited enough funds to swap{" "}
|
|
||||||
<SatsAmount amount={max_giveable} />. However, that is below
|
|
||||||
the minimum amount required to start the swap.
|
|
||||||
</li>
|
|
||||||
) : null}
|
|
||||||
<li>
|
|
||||||
Send any amount between{" "}
|
|
||||||
<SatsAmount amount={min_deposit_until_swap_will_start} /> and{" "}
|
|
||||||
<SatsAmount
|
|
||||||
amount={max_deposit_until_maximum_amount_is_reached}
|
|
||||||
/>{" "}
|
|
||||||
to the address above
|
|
||||||
{max_giveable > 0 && (
|
|
||||||
<> (on top of the already deposited funds)</>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Bitcoin sent to this this address will be converted into
|
|
||||||
Monero at an exchange rate of{" ≈ "}
|
|
||||||
<MoneroSatsExchangeRate
|
|
||||||
rate={quote.price}
|
|
||||||
displayMarkup={true}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
The Network fee of{" ≈ "}
|
|
||||||
<SatsAmount amount={min_bitcoin_lock_tx_fee} /> will
|
|
||||||
automatically be deducted from the deposited coins
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
After the deposit is detected, you'll get to confirm the exact
|
|
||||||
details before your funds are locked
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<DepositAmountHelper
|
|
||||||
min_deposit_until_swap_will_start={
|
|
||||||
min_deposit_until_swap_will_start
|
|
||||||
}
|
|
||||||
max_deposit_until_maximum_amount_is_reached={
|
|
||||||
max_deposit_until_maximum_amount_is_reached
|
|
||||||
}
|
|
||||||
min_bitcoin_lock_tx_fee={min_bitcoin_lock_tx_fee}
|
|
||||||
quote={quote}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Alert severity="info">
|
|
||||||
Please do not use replace-by-fee on your deposit transaction.
|
|
||||||
You'll need to start a new swap if you do. The funds will be
|
|
||||||
available for future swaps.
|
|
||||||
</Alert>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
icon={<BitcoinIcon />}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { DialogContentText } from "@mui/material";
|
import { DialogContentText } from "@mui/material";
|
||||||
import BitcoinTransactionInfoBox from "../../swap/BitcoinTransactionInfoBox";
|
import BitcoinTransactionInfoBox from "renderer/components/pages/swap/swap/components/BitcoinTransactionInfoBox";
|
||||||
|
|
||||||
export default function BtcTxInMempoolPageContent({
|
export default function BtcTxInMempoolPageContent({
|
||||||
withdrawTxId,
|
withdrawTxId,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import { Box, Button, IconButton, Tooltip } from "@mui/material";
|
import { Box, Button, IconButton, Tooltip } from "@mui/material";
|
||||||
import {
|
import { FileCopyOutlined, QrCode as QrCodeIcon } from "@mui/icons-material";
|
||||||
FileCopyOutlined,
|
|
||||||
CropFree as CropFreeIcon,
|
|
||||||
} from "@mui/icons-material";
|
|
||||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import MonospaceTextBox from "./MonospaceTextBox";
|
import MonospaceTextBox from "./MonospaceTextBox";
|
||||||
|
|
@ -111,7 +108,7 @@ export default function ActionableMonospaceTextBox({
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ marginLeft: 1 }}
|
sx={{ marginLeft: 1 }}
|
||||||
>
|
>
|
||||||
<CropFreeIcon />
|
<QrCodeIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Box, Button, Typography } from "@mui/material";
|
import { Box, Button, Typography } from "@mui/material";
|
||||||
import { open } from "@tauri-apps/plugin-shell";
|
import { open } from "@tauri-apps/plugin-shell";
|
||||||
import InfoBox from "../../modal/swap/InfoBox";
|
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
|
||||||
|
|
||||||
const GITHUB_ISSUE_URL =
|
const GITHUB_ISSUE_URL =
|
||||||
"https://github.com/UnstoppableSwap/core/issues/new/choose";
|
"https://github.com/UnstoppableSwap/core/issues/new/choose";
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import {
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import ChatIcon from "@mui/icons-material/Chat";
|
import ChatIcon from "@mui/icons-material/Chat";
|
||||||
import SendIcon from "@mui/icons-material/Send";
|
import SendIcon from "@mui/icons-material/Send";
|
||||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
|
||||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import FolderOpenIcon from "@mui/icons-material/FolderOpen";
|
||||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
import InfoBox from "../../modal/swap/InfoBox";
|
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
|
||||||
import CliLogsBox from "../../other/RenderedCliLog";
|
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
||||||
import { getDataDir, initializeContext } from "renderer/rpc";
|
import { getDataDir, initializeContext } from "renderer/rpc";
|
||||||
import { relaunch } from "@tauri-apps/plugin-process";
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Box, Typography, styled } from "@mui/material";
|
import { Box, Typography, styled } from "@mui/material";
|
||||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
|
||||||
import { useSettings } from "store/hooks";
|
import { useSettings } from "store/hooks";
|
||||||
import { Search } from "@mui/icons-material";
|
import { Search } from "@mui/icons-material";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Link, Typography } from "@mui/material";
|
import { Link, Typography } from "@mui/material";
|
||||||
import MoneroIcon from "../../icons/MoneroIcon";
|
import MoneroIcon from "renderer/components/icons/MoneroIcon";
|
||||||
import DepositAddressInfoBox from "../../modal/swap/DepositAddressInfoBox";
|
import DepositAddressInfoBox from "renderer/components/pages/swap/swap/components/DepositAddressInfoBox";
|
||||||
|
|
||||||
const XMR_DONATE_ADDRESS =
|
const XMR_DONATE_ADDRESS =
|
||||||
"87jS4C7ngk9EHdqFFuxGFgg8AyH63dRUoULshWDybFJaP75UA89qsutG5B1L1QTc4w228nsqsv8EjhL7bz8fB3611Mh98mg";
|
"87jS4C7ngk9EHdqFFuxGFgg8AyH63dRUoULshWDybFJaP75UA89qsutG5B1L1QTc4w228nsqsv8EjhL7bz8fB3611Mh98mg";
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
Link,
|
Link,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { getWalletDescriptor } from "renderer/rpc";
|
import { getWalletDescriptor } from "renderer/rpc";
|
||||||
import { ExportBitcoinWalletResponse } from "models/tauriModel";
|
import { ExportBitcoinWalletResponse } from "models/tauriModel";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Button, Typography } from "@mui/material";
|
import { Button, Typography } from "@mui/material";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import FeedbackDialog from "../../modal/feedback/FeedbackDialog";
|
import FeedbackDialog from "renderer/components/modal/feedback/FeedbackDialog";
|
||||||
import InfoBox from "../../modal/swap/InfoBox";
|
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
|
||||||
|
|
||||||
export default function FeedbackInfoBox() {
|
export default function FeedbackInfoBox() {
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
|
||||||
import { ReliableNodeInfo } from "models/tauriModel";
|
import { ReliableNodeInfo } from "models/tauriModel";
|
||||||
import NetworkWifiIcon from "@mui/icons-material/NetworkWifi";
|
import NetworkWifiIcon from "@mui/icons-material/NetworkWifi";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ import {
|
||||||
|
|
||||||
import { getNetwork } from "store/config";
|
import { getNetwork } from "store/config";
|
||||||
import { currencySymbol } from "utils/formatUtils";
|
import { currencySymbol } from "utils/formatUtils";
|
||||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
|
||||||
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
|
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
|
||||||
import { getNodeStatus } from "renderer/rpc";
|
import { getNodeStatus } from "renderer/rpc";
|
||||||
import { setStatus } from "store/features/nodesSlice";
|
import { setStatus } from "store/features/nodesSlice";
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,13 @@
|
||||||
import { Typography } from "@mui/material";
|
import { Typography } from "@mui/material";
|
||||||
import { useAppSelector } from "store/hooks";
|
|
||||||
import SwapTxLockAlertsBox from "../../alert/SwapTxLockAlertsBox";
|
import SwapTxLockAlertsBox from "../../alert/SwapTxLockAlertsBox";
|
||||||
import SwapDialog from "../../modal/swap/SwapDialog";
|
|
||||||
import HistoryTable from "./table/HistoryTable";
|
import HistoryTable from "./table/HistoryTable";
|
||||||
|
|
||||||
export default function HistoryPage() {
|
export default function HistoryPage() {
|
||||||
const showDialog = useAppSelector((state) => state.swap.state !== null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography variant="h3">History</Typography>
|
<Typography variant="h3">History</Typography>
|
||||||
<SwapTxLockAlertsBox />
|
<SwapTxLockAlertsBox />
|
||||||
<HistoryTable />
|
<HistoryTable />
|
||||||
<SwapDialog open={showDialog} onClose={() => {}} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,20 +12,62 @@ import {
|
||||||
isBobStateNamePossiblyRefundableSwap,
|
isBobStateNamePossiblyRefundableSwap,
|
||||||
} from "models/tauriModelExt";
|
} from "models/tauriModelExt";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { resumeSwap } from "renderer/rpc";
|
import { resumeSwap, suspendCurrentSwap } from "renderer/rpc";
|
||||||
|
import {
|
||||||
|
useIsSpecificSwapRunning,
|
||||||
|
useIsSwapRunning,
|
||||||
|
useIsSwapRunningAndHasFundsLocked,
|
||||||
|
} from "store/hooks";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function SwapResumeButton({
|
export function SwapResumeButton({
|
||||||
swap,
|
swap,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps & { swap: GetSwapInfoResponse }) {
|
}: ButtonProps & { swap: GetSwapInfoResponse }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// We cannot resume at all if the swap of this button is already running
|
||||||
|
const isAlreadyRunning = useIsSpecificSwapRunning(swap.swap_id);
|
||||||
|
|
||||||
|
// If another swap is running, we can resume but only if no funds have been locked
|
||||||
|
// for that swap. If funds have been locked, we cannot resume. If no funds have been locked,
|
||||||
|
// we suspend the other swap and resume this one.
|
||||||
|
const isAnotherSwapRunningAndHasFundsLocked =
|
||||||
|
useIsSwapRunningAndHasFundsLocked() && !isAlreadyRunning;
|
||||||
|
|
||||||
|
async function resume() {
|
||||||
|
// We always suspend the current swap first
|
||||||
|
// If that swap has any funds locked, the button will be disabled
|
||||||
|
// and this function will not be called
|
||||||
|
// If no swap is running, this is a no-op
|
||||||
|
await suspendCurrentSwap();
|
||||||
|
|
||||||
|
// Now resume this swap
|
||||||
|
await resumeSwap(swap.swap_id);
|
||||||
|
|
||||||
|
// Navigate to the swap page
|
||||||
|
navigate(`/swap`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipTitle = isAlreadyRunning
|
||||||
|
? "This swap is already running"
|
||||||
|
: isAnotherSwapRunningAndHasFundsLocked
|
||||||
|
? "Another swap is running. Suspend it first before resuming this one"
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PromiseInvokeButton
|
<PromiseInvokeButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={swap.completed}
|
disabled={
|
||||||
|
swap.completed ||
|
||||||
|
isAlreadyRunning ||
|
||||||
|
isAnotherSwapRunningAndHasFundsLocked
|
||||||
|
}
|
||||||
|
tooltipTitle={tooltipTitle}
|
||||||
endIcon={<PlayArrowIcon />}
|
endIcon={<PlayArrowIcon />}
|
||||||
onInvoke={() => resumeSwap(swap.swap_id)}
|
onInvoke={resume}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -33,32 +75,6 @@ export function SwapResumeButton({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SwapCancelRefundButton({
|
|
||||||
swap,
|
|
||||||
...props
|
|
||||||
}: { swap: GetSwapInfoResponseExt } & ButtonProps) {
|
|
||||||
const cancelOrRefundable =
|
|
||||||
isBobStateNamePossiblyCancellableSwap(swap.state_name) ||
|
|
||||||
isBobStateNamePossiblyRefundableSwap(swap.state_name);
|
|
||||||
|
|
||||||
if (!cancelOrRefundable) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PromiseInvokeButton
|
|
||||||
displayErrorSnackbar={false}
|
|
||||||
{...props}
|
|
||||||
onInvoke={async () => {
|
|
||||||
// TODO: Implement this using the Tauri RPC
|
|
||||||
throw new Error("Not implemented");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Attempt manual Cancel & Refund
|
|
||||||
</PromiseInvokeButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HistoryRowActions(swap: GetSwapInfoResponse) {
|
export default function HistoryRowActions(swap: GetSwapInfoResponse) {
|
||||||
if (swap.state_name === BobStateName.XmrRedeemed) {
|
if (swap.state_name === BobStateName.XmrRedeemed) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import ApiAlertsBox from "./ApiAlertsBox";
|
import ApiAlertsBox from "./ApiAlertsBox";
|
||||||
import SwapWidget from "./SwapWidget";
|
import SwapWidget from "./swap/SwapWidget";
|
||||||
|
|
||||||
export default function SwapPage() {
|
export default function SwapPage() {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,233 +0,0 @@
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Fab,
|
|
||||||
LinearProgress,
|
|
||||||
Paper,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
|
||||||
import InputAdornment from "@mui/material/InputAdornment";
|
|
||||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
|
||||||
import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
|
|
||||||
import { Alert } from "@mui/material";
|
|
||||||
import { ExtendedMakerStatus } from "models/apiModel";
|
|
||||||
import { ChangeEvent, useEffect, useState } from "react";
|
|
||||||
import { useAppSelector } from "store/hooks";
|
|
||||||
import { satsToBtc } from "utils/conversionUtils";
|
|
||||||
import { MakerSubmitDialogOpenButton } from "../../modal/provider/MakerListDialog";
|
|
||||||
import MakerSelect from "../../modal/provider/MakerSelect";
|
|
||||||
import SwapDialog from "../../modal/swap/SwapDialog";
|
|
||||||
|
|
||||||
// After RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN failed reconnection attempts we can assume the public registry is down
|
|
||||||
const RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN = 1;
|
|
||||||
|
|
||||||
function isRegistryDown(reconnectionAttempts: number): boolean {
|
|
||||||
return reconnectionAttempts > RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Title() {
|
|
||||||
return (
|
|
||||||
<Box sx={{ padding: 0 }}>
|
|
||||||
<Typography variant="h5" sx={{ padding: 1 }}>
|
|
||||||
Swap
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function HasMakerSwapWidget({
|
|
||||||
selectedMaker,
|
|
||||||
}: {
|
|
||||||
selectedMaker: ExtendedMakerStatus;
|
|
||||||
}) {
|
|
||||||
const forceShowDialog = useAppSelector((state) => state.swap.state !== null);
|
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
|
||||||
const [btcFieldValue, setBtcFieldValue] = useState<number | string>(
|
|
||||||
satsToBtc(selectedMaker.minSwapAmount),
|
|
||||||
);
|
|
||||||
const [xmrFieldValue, setXmrFieldValue] = useState(1);
|
|
||||||
|
|
||||||
function onBtcAmountChange(event: ChangeEvent<HTMLInputElement>) {
|
|
||||||
setBtcFieldValue(event.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateXmrValue() {
|
|
||||||
const parsedBtcAmount = Number(btcFieldValue);
|
|
||||||
if (Number.isNaN(parsedBtcAmount)) {
|
|
||||||
setXmrFieldValue(0);
|
|
||||||
} else {
|
|
||||||
const convertedXmrAmount =
|
|
||||||
parsedBtcAmount / satsToBtc(selectedMaker.price);
|
|
||||||
setXmrFieldValue(convertedXmrAmount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBtcFieldError(): string | null {
|
|
||||||
const parsedBtcAmount = Number(btcFieldValue);
|
|
||||||
if (Number.isNaN(parsedBtcAmount)) {
|
|
||||||
return "This is not a valid number";
|
|
||||||
}
|
|
||||||
if (parsedBtcAmount < satsToBtc(selectedMaker.minSwapAmount)) {
|
|
||||||
return `The minimum swap amount is ${satsToBtc(
|
|
||||||
selectedMaker.minSwapAmount,
|
|
||||||
)} BTC. Switch to a different maker if you want to swap less.`;
|
|
||||||
}
|
|
||||||
if (parsedBtcAmount > satsToBtc(selectedMaker.maxSwapAmount)) {
|
|
||||||
return `The maximum swap amount is ${satsToBtc(
|
|
||||||
selectedMaker.maxSwapAmount,
|
|
||||||
)} BTC. Switch to a different maker if you want to swap more.`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleGuideDialogOpen() {
|
|
||||||
setShowDialog(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(updateXmrValue, [btcFieldValue, selectedMaker]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
// 'elevation' prop can't be passed down (type def issue)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
<Paper
|
|
||||||
variant="outlined"
|
|
||||||
sx={(theme) => ({
|
|
||||||
width: "min(480px, 100%)",
|
|
||||||
minHeight: "150px",
|
|
||||||
display: "grid",
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
gridGap: theme.spacing(1),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Title />
|
|
||||||
<TextField
|
|
||||||
label="For this many BTC"
|
|
||||||
size="medium"
|
|
||||||
variant="outlined"
|
|
||||||
value={btcFieldValue}
|
|
||||||
onChange={onBtcAmountChange}
|
|
||||||
error={!!getBtcFieldError()}
|
|
||||||
helperText={getBtcFieldError()}
|
|
||||||
autoFocus
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: <InputAdornment position="end">BTC</InputAdornment>,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
|
||||||
<ArrowDownwardIcon fontSize="small" />
|
|
||||||
</Box>
|
|
||||||
<TextField
|
|
||||||
label="You'd receive that many XMR"
|
|
||||||
variant="outlined"
|
|
||||||
size="medium"
|
|
||||||
value={xmrFieldValue.toFixed(6)}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: <InputAdornment position="end">XMR</InputAdornment>,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MakerSelect />
|
|
||||||
<Fab variant="extended" color="primary" onClick={handleGuideDialogOpen}>
|
|
||||||
<SwapHorizIcon sx={{ marginRight: 1 }} />
|
|
||||||
Swap
|
|
||||||
</Fab>
|
|
||||||
<SwapDialog
|
|
||||||
open={showDialog || forceShowDialog}
|
|
||||||
onClose={() => setShowDialog(false)}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function HasNoMakersSwapWidget() {
|
|
||||||
const forceShowDialog = useAppSelector((state) => state.swap.state !== null);
|
|
||||||
const isPublicRegistryDown = useAppSelector((state) =>
|
|
||||||
isRegistryDown(state.makers.registry.connectionFailsCount),
|
|
||||||
);
|
|
||||||
|
|
||||||
const alertBox = isPublicRegistryDown ? (
|
|
||||||
<Alert severity="info">
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
|
||||||
<Typography>
|
|
||||||
Currently, the public registry of makers seems to be unreachable.
|
|
||||||
Here's what you can do:
|
|
||||||
<ul>
|
|
||||||
<li>Try discovering a maker by connecting to a rendezvous point</li>
|
|
||||||
<li>
|
|
||||||
Try again later when the public registry may be reachable again
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Typography>
|
|
||||||
<Box>
|
|
||||||
<MakerSubmitDialogOpenButton />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Alert>
|
|
||||||
) : (
|
|
||||||
<Alert severity="info">
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
|
||||||
<Typography>
|
|
||||||
Currently, there are no makers (trading partners) available in the
|
|
||||||
official registry. Here's what you can do:
|
|
||||||
<ul>
|
|
||||||
<li>Try discovering a maker by connecting to a rendezvous point</li>
|
|
||||||
<li>Add a new maker to the public registry</li>
|
|
||||||
<li>Try again later when more makers may be available</li>
|
|
||||||
</ul>
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ display: "flex", gap: 1 }}>
|
|
||||||
<MakerSubmitDialogOpenButton />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
{alertBox}
|
|
||||||
<SwapDialog open={forceShowDialog} onClose={() => {}} />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MakerLoadingSwapWidget() {
|
|
||||||
return (
|
|
||||||
// 'elevation' prop can't be passed down (type def issue)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
<Box
|
|
||||||
component={Paper}
|
|
||||||
elevation={15}
|
|
||||||
sx={{
|
|
||||||
width: "min(480px, 100%)",
|
|
||||||
minHeight: "150px",
|
|
||||||
display: "grid",
|
|
||||||
padding: 1,
|
|
||||||
gridGap: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Title />
|
|
||||||
<LinearProgress />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SwapWidget() {
|
|
||||||
const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
|
|
||||||
// If we fail more than RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN reconnect attempts, we'll show the "no makers" widget. We can assume the public registry is down.
|
|
||||||
const makerLoading = useAppSelector(
|
|
||||||
(state) =>
|
|
||||||
state.makers.registry.makers === null &&
|
|
||||||
!isRegistryDown(state.makers.registry.connectionFailsCount),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (makerLoading) {
|
|
||||||
return <MakerLoadingSwapWidget />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedMaker === null) {
|
|
||||||
return <HasNoMakersSwapWidget />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <HasMakerSwapWidget selectedMaker={selectedMaker} />;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Box, Button } from "@mui/material";
|
||||||
|
import { haveFundsBeenLocked } from "models/tauriModelExt";
|
||||||
|
import { getCurrentSwapId, suspendCurrentSwap } from "renderer/rpc";
|
||||||
|
import { swapReset } from "store/features/swapSlice";
|
||||||
|
import { useAppDispatch, useAppSelector, useIsSwapRunning } from "store/hooks";
|
||||||
|
import { useState } from "react";
|
||||||
|
import SwapSuspendAlert from "renderer/components/modal/SwapSuspendAlert";
|
||||||
|
|
||||||
|
export default function CancelButton() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const swap = useAppSelector((state) => state.swap);
|
||||||
|
const isSwapRunning = useIsSwapRunning();
|
||||||
|
const [openSuspendAlert, setOpenSuspendAlert] = useState(false);
|
||||||
|
|
||||||
|
const hasFundsBeenLocked = haveFundsBeenLocked(swap.state?.curr);
|
||||||
|
|
||||||
|
async function onCancel() {
|
||||||
|
const swapId = await getCurrentSwapId();
|
||||||
|
|
||||||
|
if (swapId.swap_id !== null) {
|
||||||
|
if (hasFundsBeenLocked && isSwapRunning) {
|
||||||
|
setOpenSuspendAlert(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await suspendCurrentSwap();
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(swapReset());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SwapSuspendAlert
|
||||||
|
open={openSuspendAlert}
|
||||||
|
onClose={() => setOpenSuspendAlert(false)}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{ display: "flex", justifyContent: "flex-start", width: "100%" }}
|
||||||
|
>
|
||||||
|
<Button variant="outlined" onClick={onCancel}>
|
||||||
|
{hasFundsBeenLocked && swap.state?.curr.type !== "Released"
|
||||||
|
? "Suspend"
|
||||||
|
: swap.state?.curr.type === "Released"
|
||||||
|
? "Close"
|
||||||
|
: "Cancel"}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { SwapState } from "models/storeModel";
|
import { SwapState } from "models/storeModel";
|
||||||
import { TauriSwapProgressEventType } from "models/tauriModelExt";
|
import { TauriSwapProgressEventType } from "models/tauriModelExt";
|
||||||
import CircularProgressWithSubtitle from "../CircularProgressWithSubtitle";
|
import CircularProgressWithSubtitle from "./components/CircularProgressWithSubtitle";
|
||||||
import BitcoinPunishedPage from "./done/BitcoinPunishedPage";
|
import BitcoinPunishedPage from "./done/BitcoinPunishedPage";
|
||||||
import {
|
import {
|
||||||
BitcoinRefundedPage,
|
BitcoinRefundedPage,
|
||||||
|
|
@ -20,9 +20,10 @@ import SwapSetupInflightPage from "./in_progress/SwapSetupInflightPage";
|
||||||
import WaitingForXmrConfirmationsBeforeRedeemPage from "./in_progress/WaitingForXmrConfirmationsBeforeRedeemPage";
|
import WaitingForXmrConfirmationsBeforeRedeemPage from "./in_progress/WaitingForXmrConfirmationsBeforeRedeemPage";
|
||||||
import XmrLockedPage from "./in_progress/XmrLockedPage";
|
import XmrLockedPage from "./in_progress/XmrLockedPage";
|
||||||
import XmrLockTxInMempoolPage from "./in_progress/XmrLockInMempoolPage";
|
import XmrLockTxInMempoolPage from "./in_progress/XmrLockInMempoolPage";
|
||||||
import InitPage from "./init/InitPage";
|
|
||||||
import WaitingForBitcoinDepositPage from "./init/WaitingForBitcoinDepositPage";
|
|
||||||
import { exhaustiveGuard } from "utils/typescriptUtils";
|
import { exhaustiveGuard } from "utils/typescriptUtils";
|
||||||
|
import DepositAndChooseOfferPage from "renderer/components/pages/swap/swap/init/deposit_and_choose_offer/DepositAndChooseOfferPage";
|
||||||
|
import InitPage from "./init/InitPage";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
export default function SwapStatePage({ state }: { state: SwapState | null }) {
|
export default function SwapStatePage({ state }: { state: SwapState | null }) {
|
||||||
if (state === null) {
|
if (state === null) {
|
||||||
|
|
@ -41,7 +42,7 @@ export default function SwapStatePage({ state }: { state: SwapState | null }) {
|
||||||
case "WaitingForBtcDeposit":
|
case "WaitingForBtcDeposit":
|
||||||
// This double check is necessary for the typescript compiler to infer types
|
// This double check is necessary for the typescript compiler to infer types
|
||||||
if (state.curr.type === "WaitingForBtcDeposit") {
|
if (state.curr.type === "WaitingForBtcDeposit") {
|
||||||
return <WaitingForBitcoinDepositPage {...state.curr.content} />;
|
return <DepositAndChooseOfferPage {...state.curr.content} />;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "SwapSetupInflight":
|
case "SwapSetupInflight":
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { Box, Button, Dialog, DialogActions, Paper } from "@mui/material";
|
||||||
|
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
|
||||||
|
import SwapStatePage from "renderer/components/pages/swap/swap/SwapStatePage";
|
||||||
|
import CancelButton from "./CancelButton";
|
||||||
|
import SwapStateStepper from "renderer/components/modal/swap/SwapStateStepper";
|
||||||
|
import SwapStatusAlert from "renderer/components/alert/SwapStatusAlert/SwapStatusAlert";
|
||||||
|
import DebugPageSwitchBadge from "renderer/components/modal/swap/pages/DebugPageSwitchBadge";
|
||||||
|
import DebugPage from "renderer/components/modal/swap/pages/DebugPage";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function SwapWidget() {
|
||||||
|
const swap = useAppSelector((state) => state.swap);
|
||||||
|
const swapInfo = useActiveSwapInfo();
|
||||||
|
|
||||||
|
const [debug, setDebug] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{ display: "flex", flexDirection: "column", gap: 2, width: "100%" }}
|
||||||
|
>
|
||||||
|
<SwapStatusAlert swap={swapInfo} onlyShowIfUnusualAmountOfTimeHasPassed />
|
||||||
|
<Dialog fullWidth maxWidth="md" open={debug} onClose={() => setDebug(false)}>
|
||||||
|
<DebugPage />
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="outlined" onClick={() => setDebug(false)}>Close</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: 800,
|
||||||
|
borderRadius: 2,
|
||||||
|
margin: "0 auto",
|
||||||
|
padding: 2,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 2,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SwapStatePage state={swap.state} />
|
||||||
|
{swap.state !== null && (
|
||||||
|
<>
|
||||||
|
<SwapStateStepper state={swap.state} />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CancelButton />
|
||||||
|
<DebugPageSwitchBadge enabled={debug} setEnabled={setDebug} />
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import QRCode from "react-qr-code";
|
||||||
|
|
||||||
|
export default function BitcoinQrCode({ address }: { address: string }) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
padding: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
width: "100%",
|
||||||
|
aspectRatio: "1 / 1",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<QRCode
|
||||||
|
value={`bitcoin:${address}`}
|
||||||
|
size={1}
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
width: "100%",
|
||||||
|
height: "min-content",
|
||||||
|
aspectRatio: 1,
|
||||||
|
}}
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||||
|
/* @ts-ignore */
|
||||||
|
viewBox="0 0 1 1"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Box, DialogContentText } from "@mui/material";
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
import FeedbackInfoBox from "renderer/components/pages/help/FeedbackInfoBox";
|
||||||
import { TauriSwapProgressEventExt } from "models/tauriModelExt";
|
import { TauriSwapProgressEventExt } from "models/tauriModelExt";
|
||||||
|
|
||||||
export default function BitcoinPunishedPage({
|
export default function BitcoinPunishedPage({
|
||||||
|
|
@ -10,7 +10,7 @@ export default function BitcoinPunishedPage({
|
||||||
| TauriSwapProgressEventExt<"CooperativeRedeemRejected">;
|
| TauriSwapProgressEventExt<"CooperativeRedeemRejected">;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
Unfortunately, the swap was unsuccessful. Since you did not refund in
|
Unfortunately, the swap was unsuccessful. Since you did not refund in
|
||||||
time, the Bitcoin has been lost. However, with the cooperation of the
|
time, the Bitcoin has been lost. However, with the cooperation of the
|
||||||
|
|
@ -26,6 +26,6 @@ export default function BitcoinPunishedPage({
|
||||||
)}
|
)}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<FeedbackInfoBox />
|
<FeedbackInfoBox />
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Box, DialogContentText } from "@mui/material";
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||||
import { useActiveSwapInfo } from "store/hooks";
|
import { useActiveSwapInfo } from "store/hooks";
|
||||||
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
import FeedbackInfoBox from "renderer/components/pages/help/FeedbackInfoBox";
|
||||||
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
|
import BitcoinTransactionInfoBox from "renderer/components/pages/swap/swap/components/BitcoinTransactionInfoBox";
|
||||||
|
|
||||||
export function BitcoinRefundPublishedPage({
|
export function BitcoinRefundPublishedPage({
|
||||||
btc_refund_txid,
|
btc_refund_txid,
|
||||||
|
|
@ -66,7 +66,7 @@ function MultiBitcoinRefundedPage({
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
Unfortunately, the swap was not successful. However, rest assured that
|
Unfortunately, the swap was not successful. However, rest assured that
|
||||||
all your Bitcoin has been refunded to the specified address. The swap
|
all your Bitcoin has been refunded to the specified address. The swap
|
||||||
|
|
@ -87,6 +87,6 @@ function MultiBitcoinRefundedPage({
|
||||||
/>
|
/>
|
||||||
<FeedbackInfoBox />
|
<FeedbackInfoBox />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Box, DialogContentText, Typography } from "@mui/material";
|
import { Box, DialogContentText, Typography } from "@mui/material";
|
||||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||||
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
import FeedbackInfoBox from "renderer/components/pages/help/FeedbackInfoBox";
|
||||||
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
|
import MoneroTransactionInfoBox from "renderer/components/pages/swap/swap/components/MoneroTransactionInfoBox";
|
||||||
|
|
||||||
export default function XmrRedeemInMempoolPage(
|
export default function XmrRedeemInMempoolPage(
|
||||||
state: TauriSwapProgressEventContent<"XmrRedeemInMempool">,
|
state: TauriSwapProgressEventContent<"XmrRedeemInMempool">,
|
||||||
|
|
@ -9,7 +9,7 @@ export default function XmrRedeemInMempoolPage(
|
||||||
const xmr_redeem_txid = state.xmr_redeem_txids[0] ?? null;
|
const xmr_redeem_txid = state.xmr_redeem_txids[0] ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
The swap was successful and the Monero has been sent to the following
|
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.
|
address(es). The swap is completed and you may exit the application now.
|
||||||
|
|
@ -77,6 +77,6 @@ export default function XmrRedeemInMempoolPage(
|
||||||
/>
|
/>
|
||||||
<FeedbackInfoBox />
|
<FeedbackInfoBox />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import { Box, DialogContentText } from "@mui/material";
|
||||||
import { TauriSwapProgressEvent } from "models/tauriModel";
|
import { TauriSwapProgressEvent } from "models/tauriModel";
|
||||||
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
||||||
import { useActiveSwapInfo, useActiveSwapLogs } from "store/hooks";
|
import { useActiveSwapInfo, useActiveSwapLogs } from "store/hooks";
|
||||||
import SwapStatePage from "../SwapStatePage";
|
import SwapStatePage from "renderer/components/pages/swap/swap/SwapStatePage";
|
||||||
|
|
||||||
export default function ProcessExitedPage({
|
export default function ProcessExitedPage({
|
||||||
prevState,
|
prevState,
|
||||||
|
|
@ -35,7 +35,7 @@ export default function ProcessExitedPage({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
The swap was stopped but it has not been completed yet. Check the logs
|
The swap was stopped but it has not been completed yet. Check the logs
|
||||||
below for more information. The current GUI state is{" "}
|
below for more information. The current GUI state is{" "}
|
||||||
|
|
@ -45,6 +45,6 @@ export default function ProcessExitedPage({
|
||||||
<Box>
|
<Box>
|
||||||
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
|
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
import CircularProgressWithSubtitle from "renderer/components/pages/swap/swap/components/CircularProgressWithSubtitle";
|
||||||
|
|
||||||
export default function BitcoinCancelledPage() {
|
export default function BitcoinCancelledPage() {
|
||||||
return <CircularProgressWithSubtitle description="Refunding your Bitcoin" />;
|
return <CircularProgressWithSubtitle description="Refunding your Bitcoin" />;
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||||
import { formatConfirmations } from "utils/formatUtils";
|
import { formatConfirmations } from "utils/formatUtils";
|
||||||
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
|
import BitcoinTransactionInfoBox from "renderer/components/pages/swap/swap/components/BitcoinTransactionInfoBox";
|
||||||
import SwapStatusAlert from "renderer/components/alert/SwapStatusAlert/SwapStatusAlert";
|
|
||||||
import { useActiveSwapInfo } from "store/hooks";
|
|
||||||
import { Box, DialogContentText } from "@mui/material";
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
|
|
||||||
// This is the number of blocks after which we consider the swap to be at risk of being unsuccessful
|
// This is the number of blocks after which we consider the swap to be at risk of being unsuccessful
|
||||||
|
|
@ -12,10 +10,8 @@ export default function BitcoinLockTxInMempoolPage({
|
||||||
btc_lock_confirmations,
|
btc_lock_confirmations,
|
||||||
btc_lock_txid,
|
btc_lock_txid,
|
||||||
}: TauriSwapProgressEventContent<"BtcLockTxInMempool">) {
|
}: TauriSwapProgressEventContent<"BtcLockTxInMempool">) {
|
||||||
const swapInfo = useActiveSwapInfo();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<>
|
||||||
{(btc_lock_confirmations === undefined ||
|
{(btc_lock_confirmations === undefined ||
|
||||||
btc_lock_confirmations < BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD) && (
|
btc_lock_confirmations < BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD) && (
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
|
|
@ -32,10 +28,6 @@ export default function BitcoinLockTxInMempoolPage({
|
||||||
gap: "1rem",
|
gap: "1rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{btc_lock_confirmations !== undefined &&
|
|
||||||
btc_lock_confirmations >= BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
|
|
||||||
<SwapStatusAlert swap={swapInfo} isRunning={true} />
|
|
||||||
)}
|
|
||||||
<BitcoinTransactionInfoBox
|
<BitcoinTransactionInfoBox
|
||||||
title="Bitcoin Lock Transaction"
|
title="Bitcoin Lock Transaction"
|
||||||
txId={btc_lock_txid}
|
txId={btc_lock_txid}
|
||||||
|
|
@ -51,6 +43,6 @@ export default function BitcoinLockTxInMempoolPage({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import CircularProgressWithSubtitle from "renderer/components/pages/swap/swap/components/CircularProgressWithSubtitle";
|
||||||
|
|
||||||
|
export default function BitcoinRedeemedPage() {
|
||||||
|
return <CircularProgressWithSubtitle description="Redeeming your Monero" />;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
import CircularProgressWithSubtitle from "../components/CircularProgressWithSubtitle";
|
||||||
|
|
||||||
export default function CancelTimelockExpiredPage() {
|
export default function CancelTimelockExpiredPage() {
|
||||||
return <CircularProgressWithSubtitle description="Cancelling the swap" />;
|
return <CircularProgressWithSubtitle description="Cancelling the swap" />;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import CircularProgressWithSubtitle from "../components/CircularProgressWithSubtitle";
|
||||||
|
import { useActiveSwapInfo, useSwapInfosSortedByDate } from "store/hooks";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
|
export default function EncryptedSignatureSentPage() {
|
||||||
|
return (
|
||||||
|
<CircularProgressWithSubtitle description="Waiting for them to redeem the Bitcoin" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
} from "store/hooks";
|
} from "store/hooks";
|
||||||
import CircularProgressWithSubtitle, {
|
import CircularProgressWithSubtitle, {
|
||||||
LinearProgressWithSubtitle,
|
LinearProgressWithSubtitle,
|
||||||
} from "../../CircularProgressWithSubtitle";
|
} from "../components/CircularProgressWithSubtitle";
|
||||||
|
|
||||||
export default function ReceivedQuotePage() {
|
export default function ReceivedQuotePage() {
|
||||||
const syncProgress = useConservativeBitcoinSyncProgress();
|
const syncProgress = useConservativeBitcoinSyncProgress();
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
import CircularProgressWithSubtitle from "../components/CircularProgressWithSubtitle";
|
||||||
|
|
||||||
export default function RedeemingMoneroPage() {
|
export default function RedeemingMoneroPage() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -5,10 +5,10 @@ import {
|
||||||
TauriSwapProgressEventContent,
|
TauriSwapProgressEventContent,
|
||||||
} from "models/tauriModelExt";
|
} from "models/tauriModelExt";
|
||||||
import { SatsAmount, PiconeroAmount } from "renderer/components/other/Units";
|
import { SatsAmount, PiconeroAmount } from "renderer/components/other/Units";
|
||||||
import { Box, Typography, Divider } from "@mui/material";
|
import { Box, Typography, Divider, Theme } from "@mui/material";
|
||||||
import { useActiveSwapId, usePendingLockBitcoinApproval } from "store/hooks";
|
import { useActiveSwapId, usePendingLockBitcoinApproval } from "store/hooks";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
import CircularProgressWithSubtitle from "../components/CircularProgressWithSubtitle";
|
||||||
import CheckIcon from "@mui/icons-material/Check";
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
import ArrowRightAltIcon from "@mui/icons-material/ArrowRightAlt";
|
import ArrowRightAltIcon from "@mui/icons-material/ArrowRightAlt";
|
||||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||||
|
|
@ -56,13 +56,22 @@ export default function SwapSetupInflightPage({
|
||||||
// Display a loading spinner to the user for as long as the swap_setup request is in flight
|
// Display a loading spinner to the user for as long as the swap_setup request is in flight
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
return (
|
return (
|
||||||
<CircularProgressWithSubtitle
|
<Box
|
||||||
description={
|
sx={{
|
||||||
<>
|
height: 200,
|
||||||
Negotiating offer for <SatsAmount amount={btc_lock_amount} />
|
display: "flex",
|
||||||
</>
|
alignItems: "center",
|
||||||
}
|
justifyContent: "center",
|
||||||
/>
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgressWithSubtitle
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
Negotiating offer for <SatsAmount amount={btc_lock_amount} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,15 +92,15 @@ export default function SwapSetupInflightPage({
|
||||||
{/* Grid layout for perfect alignment */}
|
{/* Grid layout for perfect alignment */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "grid",
|
display: "flex",
|
||||||
gridTemplateColumns: "max-content auto max-content",
|
flexDirection: { xs: "column", lg: "row" },
|
||||||
gap: "1.5rem",
|
gap: "1.5rem",
|
||||||
alignItems: "stretch",
|
alignItems: "stretch",
|
||||||
justifyContent: "center",
|
justifyContent: "space-between",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Row 1: Bitcoin box */}
|
{/* Row 1: Bitcoin box */}
|
||||||
<Box sx={{ height: "100%" }}>
|
<Box sx={{ height: "100%", flex: "0 0 auto" }}>
|
||||||
<BitcoinMainBox
|
<BitcoinMainBox
|
||||||
btc_lock_amount={btc_lock_amount}
|
btc_lock_amount={btc_lock_amount}
|
||||||
btc_network_fee={btc_network_fee}
|
btc_network_fee={btc_network_fee}
|
||||||
|
|
@ -110,7 +119,7 @@ export default function SwapSetupInflightPage({
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Row 1: Monero main box */}
|
{/* Row 1: Monero main box */}
|
||||||
<Box>
|
<Box sx={{ flex: "0 0 auto" }}>
|
||||||
<MoneroMainBox
|
<MoneroMainBox
|
||||||
monero_receive_pool={monero_receive_pool}
|
monero_receive_pool={monero_receive_pool}
|
||||||
xmr_receive_amount={xmr_receive_amount}
|
xmr_receive_amount={xmr_receive_amount}
|
||||||
|
|
@ -120,38 +129,50 @@ export default function SwapSetupInflightPage({
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: 2,
|
marginTop: 4,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
flexDirection: "column",
|
||||||
gap: 2,
|
alignItems: "center",
|
||||||
|
gap: 1.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PromiseInvokeButton
|
<Box sx={{ display: "flex", justifyContent: "center", gap: 2 }}>
|
||||||
variant="text"
|
<PromiseInvokeButton
|
||||||
size="large"
|
variant="text"
|
||||||
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
size="large"
|
||||||
onInvoke={() =>
|
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||||
resolveApproval(request.request_id, false as unknown as object)
|
onInvoke={() =>
|
||||||
}
|
resolveApproval(request.request_id, false as unknown as object)
|
||||||
displayErrorSnackbar
|
}
|
||||||
requiresContext
|
displayErrorSnackbar
|
||||||
>
|
requiresContext
|
||||||
Deny
|
>
|
||||||
</PromiseInvokeButton>
|
Deny
|
||||||
|
</PromiseInvokeButton>
|
||||||
|
|
||||||
<PromiseInvokeButton
|
<PromiseInvokeButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
onInvoke={() =>
|
onInvoke={() =>
|
||||||
resolveApproval(request.request_id, true as unknown as object)
|
resolveApproval(request.request_id, true as unknown as object)
|
||||||
}
|
}
|
||||||
displayErrorSnackbar
|
displayErrorSnackbar
|
||||||
requiresContext
|
requiresContext
|
||||||
endIcon={<CheckIcon />}
|
endIcon={<CheckIcon />}
|
||||||
|
>
|
||||||
|
{`Confirm`}
|
||||||
|
</PromiseInvokeButton>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
textAlign: "center",
|
||||||
|
color: (theme) => theme.palette.text.secondary,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{`Confirm (${timeLeft}s)`}
|
{`Offer expires in ${timeLeft}s`}
|
||||||
</PromiseInvokeButton>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
@ -177,7 +198,15 @@ const BitcoinMainBox = ({
|
||||||
btc_lock_amount: number;
|
btc_lock_amount: number;
|
||||||
btc_network_fee: number;
|
btc_network_fee: number;
|
||||||
}) => (
|
}) => (
|
||||||
<Box sx={{ position: "relative", height: "100%" }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
@ -188,10 +217,10 @@ const BitcoinMainBox = ({
|
||||||
gap: "0.5rem 1rem",
|
gap: "0.5rem 1rem",
|
||||||
borderColor: "warning.main",
|
borderColor: "warning.main",
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
|
flexGrow: 1,
|
||||||
backgroundColor: (theme) => theme.palette.warning.light + "10",
|
backgroundColor: (theme) => theme.palette.warning.light + "10",
|
||||||
background: (theme) =>
|
background: (theme) =>
|
||||||
`linear-gradient(135deg, ${theme.palette.warning.light}20, ${theme.palette.warning.light}05)`,
|
`linear-gradient(135deg, ${theme.palette.warning.light}20, ${theme.palette.warning.light}05)`,
|
||||||
height: "100%", // Match the height of the Monero box
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
|
|
@ -217,10 +246,6 @@ const BitcoinMainBox = ({
|
||||||
{/* Network fee box attached to the bottom */}
|
{/* Network fee box attached to the bottom */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
|
||||||
bottom: "calc(-50%)",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
padding: "0.25rem 0.75rem",
|
padding: "0.25rem 0.75rem",
|
||||||
backgroundColor: (theme) => theme.palette.warning.main,
|
backgroundColor: (theme) => theme.palette.warning.main,
|
||||||
color: (theme) => theme.palette.warning.contrastText,
|
color: (theme) => theme.palette.warning.contrastText,
|
||||||
|
|
@ -271,7 +296,7 @@ const PoolBreakdown = ({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "flex-start",
|
justifyContent: "flex-start",
|
||||||
alignItems: "stretch",
|
alignItems: "stretch",
|
||||||
padding: pool.percentage >= 0.05 ? 1.5 : 1.2,
|
padding: pool.percentage >= 0.05 ? 1.5 : "0.25rem 0.75rem",
|
||||||
border: 1,
|
border: 1,
|
||||||
borderColor:
|
borderColor:
|
||||||
pool.percentage >= 0.05 ? "success.main" : "success.light",
|
pool.percentage >= 0.05 ? "success.main" : "success.light",
|
||||||
|
|
@ -283,7 +308,6 @@ const PoolBreakdown = ({
|
||||||
width: "100%", // Ensure full width
|
width: "100%", // Ensure full width
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
opacity: pool.percentage >= 0.05 ? 1 : 0.75,
|
opacity: pool.percentage >= 0.05 ? 1 : 0.75,
|
||||||
transform: pool.percentage >= 0.05 ? "scale(1)" : "scale(0.95)",
|
|
||||||
animation:
|
animation:
|
||||||
pool.percentage >= 0.05
|
pool.percentage >= 0.05
|
||||||
? "poolPulse 2s ease-in-out infinite"
|
? "poolPulse 2s ease-in-out infinite"
|
||||||
|
|
@ -308,6 +332,7 @@ const PoolBreakdown = ({
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
gap: 0.5,
|
gap: 0.5,
|
||||||
flex: "1 1 0",
|
flex: "1 1 0",
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
|
|
@ -323,18 +348,20 @@ const PoolBreakdown = ({
|
||||||
>
|
>
|
||||||
{pool.label === "user address" ? "Your Wallet" : pool.label}
|
{pool.label === "user address" ? "Your Wallet" : pool.label}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
{pool.label === "user address" && (
|
||||||
variant="body2"
|
<Typography
|
||||||
sx={{
|
variant="body2"
|
||||||
fontFamily: "monospace",
|
sx={{
|
||||||
fontSize: "0.75rem",
|
fontFamily: "monospace",
|
||||||
color: (theme) => theme.palette.text.secondary,
|
fontSize: "0.75rem",
|
||||||
}}
|
color: (theme) => theme.palette.text.secondary,
|
||||||
>
|
}}
|
||||||
<TruncatedText truncateMiddle limit={15}>
|
>
|
||||||
{pool.address}
|
<TruncatedText truncateMiddle limit={15}>
|
||||||
</TruncatedText>
|
{pool.address}
|
||||||
</Typography>
|
</TruncatedText>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -393,7 +420,7 @@ const MoneroMainBox = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: "relative" }}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
@ -460,20 +487,10 @@ const MoneroMainBox = ({
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Secondary Monero content attached to the bottom */}
|
{/* Secondary Monero content attached to the bottom */}
|
||||||
<Box
|
<MoneroSecondaryContent
|
||||||
sx={{
|
monero_receive_pool={monero_receive_pool}
|
||||||
position: "absolute",
|
xmr_receive_amount={xmr_receive_amount}
|
||||||
bottom: "calc(-100%)",
|
/>
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MoneroSecondaryContent
|
|
||||||
monero_receive_pool={monero_receive_pool}
|
|
||||||
xmr_receive_amount={xmr_receive_amount}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -491,8 +508,7 @@ const MoneroSecondaryContent = ({
|
||||||
// Arrow animation styling extracted for reuse
|
// Arrow animation styling extracted for reuse
|
||||||
const arrowSx = {
|
const arrowSx = {
|
||||||
fontSize: "3rem",
|
fontSize: "3rem",
|
||||||
color: (theme: { palette: { primary: { main: string } } }) =>
|
color: (theme: Theme) => theme.palette.primary.main,
|
||||||
theme.palette.primary.main,
|
|
||||||
animation: "slideArrow 2s infinite",
|
animation: "slideArrow 2s infinite",
|
||||||
"@keyframes slideArrow": {
|
"@keyframes slideArrow": {
|
||||||
"0%": {
|
"0%": {
|
||||||
|
|
@ -518,6 +534,7 @@ const AnimatedArrow = () => (
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
flex: "0 0 auto",
|
flex: "0 0 auto",
|
||||||
|
transform: { xs: "rotate(90deg)", lg: "rotate(0deg)" },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowRightAltIcon sx={arrowSx} />
|
<ArrowRightAltIcon sx={arrowSx} />
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Box, DialogContentText } from "@mui/material";
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||||
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
|
import MoneroTransactionInfoBox from "../components/MoneroTransactionInfoBox";
|
||||||
|
|
||||||
export default function WaitingForXmrConfirmationsBeforeRedeemPage({
|
export default function WaitingForXmrConfirmationsBeforeRedeemPage({
|
||||||
xmr_lock_txid,
|
xmr_lock_txid,
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { Box, DialogContentText } from "@mui/material";
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||||
import { formatConfirmations } from "utils/formatUtils";
|
import { formatConfirmations } from "utils/formatUtils";
|
||||||
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
|
import MoneroTransactionInfoBox from "../components/MoneroTransactionInfoBox";
|
||||||
|
import CancelButton from "../CancelButton";
|
||||||
|
|
||||||
export default function XmrLockTxInMempoolPage({
|
export default function XmrLockTxInMempoolPage({
|
||||||
xmr_lock_tx_confirmations,
|
xmr_lock_tx_confirmations,
|
||||||
|
|
@ -11,7 +12,7 @@ export default function XmrLockTxInMempoolPage({
|
||||||
const additionalContent = `Confirmations: ${formatConfirmations(xmr_lock_tx_confirmations, xmr_lock_tx_target_confirmations)}`;
|
const additionalContent = `Confirmations: ${formatConfirmations(xmr_lock_tx_confirmations, xmr_lock_tx_target_confirmations)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
They have published their Monero lock transaction. The swap will proceed
|
They have published their Monero lock transaction. The swap will proceed
|
||||||
once the transaction has been confirmed.
|
once the transaction has been confirmed.
|
||||||
|
|
@ -23,6 +24,8 @@ export default function XmrLockTxInMempoolPage({
|
||||||
additionalContent={additionalContent}
|
additionalContent={additionalContent}
|
||||||
loading
|
loading
|
||||||
/>
|
/>
|
||||||
</Box>
|
|
||||||
|
<CancelButton />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
import CircularProgressWithSubtitle from "../components/CircularProgressWithSubtitle";
|
||||||
|
|
||||||
export default function XmrLockedPage() {
|
export default function XmrLockedPage() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { Box, Paper, Tab, Tabs, Typography } from "@mui/material";
|
import { Box, Paper, Tab, Tabs, Typography } from "@mui/material";
|
||||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import RemainingFundsWillBeUsedAlert from "renderer/components/alert/RemainingFundsWillBeUsedAlert";
|
|
||||||
import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField";
|
import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField";
|
||||||
import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField";
|
import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { buyXmr } from "renderer/rpc";
|
import { buyXmr } from "renderer/rpc";
|
||||||
import { useAppSelector, useSettings } from "store/hooks";
|
import { useSettings } from "store/hooks";
|
||||||
|
|
||||||
export default function InitPage() {
|
export default function InitPage() {
|
||||||
const [redeemAddress, setRedeemAddress] = useState("");
|
const [redeemAddress, setRedeemAddress] = useState("");
|
||||||
|
|
@ -17,12 +16,10 @@ export default function InitPage() {
|
||||||
const [redeemAddressValid, setRedeemAddressValid] = useState(false);
|
const [redeemAddressValid, setRedeemAddressValid] = useState(false);
|
||||||
const [refundAddressValid, setRefundAddressValid] = useState(false);
|
const [refundAddressValid, setRefundAddressValid] = useState(false);
|
||||||
|
|
||||||
const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
|
|
||||||
const donationRatio = useSettings((s) => s.donateToDevelopment);
|
const donationRatio = useSettings((s) => s.donateToDevelopment);
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await buyXmr(
|
await buyXmr(
|
||||||
selectedMaker,
|
|
||||||
useExternalRefundAddress ? refundAddress : null,
|
useExternalRefundAddress ? refundAddress : null,
|
||||||
redeemAddress,
|
redeemAddress,
|
||||||
donationRatio,
|
donationRatio,
|
||||||
|
|
@ -30,7 +27,7 @@ export default function InitPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
@ -38,7 +35,6 @@ export default function InitPage() {
|
||||||
gap: 1.5,
|
gap: 1.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RemainingFundsWillBeUsedAlert />
|
|
||||||
<MoneroAddressTextField
|
<MoneroAddressTextField
|
||||||
label="Monero redeem address"
|
label="Monero redeem address"
|
||||||
address={redeemAddress}
|
address={redeemAddress}
|
||||||
|
|
@ -84,8 +80,7 @@ export default function InitPage() {
|
||||||
<PromiseInvokeButton
|
<PromiseInvokeButton
|
||||||
disabled={
|
disabled={
|
||||||
(!refundAddressValid && useExternalRefundAddress) ||
|
(!refundAddressValid && useExternalRefundAddress) ||
|
||||||
!redeemAddressValid ||
|
!redeemAddressValid
|
||||||
!selectedMaker
|
|
||||||
}
|
}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
@ -95,9 +90,9 @@ export default function InitPage() {
|
||||||
onInvoke={init}
|
onInvoke={init}
|
||||||
displayErrorSnackbar
|
displayErrorSnackbar
|
||||||
>
|
>
|
||||||
Begin swap
|
Continue
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { Typography, Box, Paper, Divider, Pagination } from "@mui/material";
|
||||||
|
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
||||||
|
import MakerOfferItem from "./MakerOfferItem";
|
||||||
|
import { usePendingSelectMakerApproval } from "store/hooks";
|
||||||
|
import MakerDiscoveryStatus from "./MakerDiscoveryStatus";
|
||||||
|
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||||
|
import { SatsAmount } from "renderer/components/other/Units";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { sortApprovalsAndKnownQuotes } from "utils/sortUtils";
|
||||||
|
|
||||||
|
export default function DepositAndChooseOfferPage({
|
||||||
|
deposit_address,
|
||||||
|
max_giveable,
|
||||||
|
known_quotes,
|
||||||
|
}: TauriSwapProgressEventContent<"WaitingForBtcDeposit">) {
|
||||||
|
const pendingSelectMakerApprovals = usePendingSelectMakerApproval();
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const offersPerPage = 3;
|
||||||
|
|
||||||
|
const makerOffers = sortApprovalsAndKnownQuotes(
|
||||||
|
pendingSelectMakerApprovals,
|
||||||
|
known_quotes,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pagination calculations
|
||||||
|
const totalPages = Math.ceil(makerOffers.length / offersPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * offersPerPage;
|
||||||
|
const endIndex = startIndex + offersPerPage;
|
||||||
|
const paginatedOffers = makerOffers.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const handlePageChange = (
|
||||||
|
event: React.ChangeEvent<unknown>,
|
||||||
|
value: number,
|
||||||
|
) => {
|
||||||
|
setCurrentPage(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
elevation={8}
|
||||||
|
sx={{
|
||||||
|
padding: 2,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: { xs: "column", md: "row" },
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ flexGrow: 1, flexShrink: 0, minWidth: "12em" }}>
|
||||||
|
<Typography variant="body1">Bitcoin Balance</Typography>
|
||||||
|
<Typography variant="h5">
|
||||||
|
<SatsAmount amount={max_giveable} />
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider
|
||||||
|
orientation="vertical"
|
||||||
|
flexItem
|
||||||
|
sx={{
|
||||||
|
marginX: { xs: 0, md: 1 },
|
||||||
|
marginY: { xs: 1, md: 0 },
|
||||||
|
display: { xs: "none", md: "block" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Divider
|
||||||
|
orientation="horizontal"
|
||||||
|
flexItem
|
||||||
|
sx={{
|
||||||
|
marginX: { xs: 0, md: 1 },
|
||||||
|
marginY: { xs: 1, md: 0 },
|
||||||
|
display: { xs: "block", md: "none" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexShrink: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">Deposit</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Send Bitcoin to your internal wallet to swap your desired amount of
|
||||||
|
Monero
|
||||||
|
</Typography>
|
||||||
|
<ActionableMonospaceTextBox content={deposit_address} />
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Available Makers Section */}
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">Select an offer</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Maker Discovery Status */}
|
||||||
|
<MakerDiscoveryStatus />
|
||||||
|
|
||||||
|
{/* Real Maker Offers */}
|
||||||
|
<Box>
|
||||||
|
{makerOffers.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
|
{paginatedOffers.map((quote, index) => {
|
||||||
|
return (
|
||||||
|
<MakerOfferItem
|
||||||
|
key={startIndex + index}
|
||||||
|
quoteWithAddress={quote}
|
||||||
|
requestId={quote.request_id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "center", mt: 2 }}>
|
||||||
|
<Pagination
|
||||||
|
count={totalPages}
|
||||||
|
page={currentPage}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TODO: Differentiate between no makers found and still loading */}
|
||||||
|
{makerOffers.length === 0 && (
|
||||||
|
<Paper variant="outlined" sx={{ p: 3, textAlign: "center" }}>
|
||||||
|
<Typography variant="body1" color="textSecondary">
|
||||||
|
Searching for available makers...
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||||
|
Please wait while we find the best offers for your swap.
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { Box, Typography, LinearProgress, Paper } from "@mui/material";
|
||||||
|
import { usePendingBackgroundProcesses } from "store/hooks";
|
||||||
|
|
||||||
|
export default function MakerDiscoveryStatus() {
|
||||||
|
const backgroundProcesses = usePendingBackgroundProcesses();
|
||||||
|
|
||||||
|
// Find active ListSellers processes
|
||||||
|
const listSellersProcesses = backgroundProcesses.filter(
|
||||||
|
([, status]) =>
|
||||||
|
status.componentName === "ListSellers" &&
|
||||||
|
status.progress.type === "Pending",
|
||||||
|
);
|
||||||
|
|
||||||
|
const isActive = listSellersProcesses.length > 0;
|
||||||
|
|
||||||
|
// Default values for inactive state
|
||||||
|
let progress = {
|
||||||
|
rendezvous_points_total: 0,
|
||||||
|
peers_discovered: 0,
|
||||||
|
rendezvous_points_connected: 0,
|
||||||
|
quotes_received: 0,
|
||||||
|
quotes_failed: 0,
|
||||||
|
};
|
||||||
|
let progressValue = 0;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
// Use the first ListSellers process for display
|
||||||
|
const [, status] = listSellersProcesses[0];
|
||||||
|
|
||||||
|
// Type guard to ensure we have ListSellers progress
|
||||||
|
if (
|
||||||
|
status.componentName === "ListSellers" &&
|
||||||
|
status.progress.type === "Pending"
|
||||||
|
) {
|
||||||
|
progress = status.progress.content;
|
||||||
|
|
||||||
|
const totalExpected =
|
||||||
|
progress.rendezvous_points_total + progress.peers_discovered;
|
||||||
|
const totalCompleted =
|
||||||
|
progress.rendezvous_points_connected +
|
||||||
|
progress.quotes_received +
|
||||||
|
progress.quotes_failed;
|
||||||
|
progressValue =
|
||||||
|
totalExpected > 0 ? (totalCompleted / totalExpected) * 100 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
mb: 2,
|
||||||
|
p: 2,
|
||||||
|
border: "1px solid",
|
||||||
|
borderColor: isActive ? "success.main" : "divider",
|
||||||
|
borderRadius: 1,
|
||||||
|
opacity: isActive ? 1 : 0.6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1.5,
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontWeight: "medium",
|
||||||
|
color: isActive ? "info.main" : "text.disabled",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isActive
|
||||||
|
? "Getting offers..."
|
||||||
|
: "Waiting a few seconds before refreshing offers"}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: "flex", gap: 2 }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
color: isActive ? "success.main" : "text.disabled",
|
||||||
|
fontWeight: "medium",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{progress.quotes_received} online
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
color: isActive ? "error.main" : "text.disabled",
|
||||||
|
fontWeight: "medium",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{progress.quotes_failed} offline
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={Math.min(progressValue, 100)}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
opacity: isActive ? 1 : 0.4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { Box, Button, Chip, Paper, Tooltip, Typography } from "@mui/material";
|
||||||
|
import Avatar from "boring-avatars";
|
||||||
|
import { QuoteWithAddress } from "models/tauriModel";
|
||||||
|
import {
|
||||||
|
MoneroSatsExchangeRate,
|
||||||
|
SatsAmount,
|
||||||
|
} from "renderer/components/other/Units";
|
||||||
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
|
import { resolveApproval } from "renderer/rpc";
|
||||||
|
import { isMakerVersionOutdated } from "utils/multiAddrUtils";
|
||||||
|
import WarningIcon from "@mui/icons-material/Warning";
|
||||||
|
|
||||||
|
export default function MakerOfferItem({
|
||||||
|
quoteWithAddress,
|
||||||
|
requestId,
|
||||||
|
}: {
|
||||||
|
requestId?: string;
|
||||||
|
quoteWithAddress: QuoteWithAddress;
|
||||||
|
}) {
|
||||||
|
const { multiaddr, peer_id, quote, version } = quoteWithAddress;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: { xs: "column", sm: "row" },
|
||||||
|
gap: 2,
|
||||||
|
borderRadius: 2,
|
||||||
|
padding: 2,
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: { xs: "stretch", sm: "center" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
size={40}
|
||||||
|
name={peer_id}
|
||||||
|
variant="marble"
|
||||||
|
colors={["#92A1C6", "#146A7C", "#F0AB3D", "#C271B4", "#C20D90"]}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" sx={{ maxWidth: "200px" }} noWrap>
|
||||||
|
{multiaddr}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ maxWidth: "200px" }} noWrap>
|
||||||
|
{peer_id}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: { xs: "column", sm: "row" },
|
||||||
|
gap: 1,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
label={
|
||||||
|
<MoneroSatsExchangeRate
|
||||||
|
rate={quote.price}
|
||||||
|
displayMarkup={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
<SatsAmount amount={quote.min_quantity} /> -{" "}
|
||||||
|
<SatsAmount amount={quote.max_quantity} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
{isMakerVersionOutdated(version) ? (
|
||||||
|
<Tooltip title="Outdated maker version. This may cause issues with the swap.">
|
||||||
|
<Chip
|
||||||
|
color="warning"
|
||||||
|
label={
|
||||||
|
<Box
|
||||||
|
sx={{ display: "flex", alignItems: "center", gap: 0.5 }}
|
||||||
|
>
|
||||||
|
<WarningIcon sx={{ fontSize: "1rem" }} />
|
||||||
|
<Typography variant="body2">{version}</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Chip label={version} size="small" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
|
<PromiseInvokeButton
|
||||||
|
variant="contained"
|
||||||
|
onInvoke={() => resolveApproval(requestId, true as unknown as object)}
|
||||||
|
displayErrorSnackbar
|
||||||
|
disabled={!requestId}
|
||||||
|
tooltipTitle={
|
||||||
|
requestId == null
|
||||||
|
? "You don't have enough Bitcoin to swap with this maker"
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</PromiseInvokeButton>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import { useState } from "react";
|
||||||
import { SatsAmount } from "renderer/components/other/Units";
|
import { SatsAmount } from "renderer/components/other/Units";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
import BitcoinIcon from "../../icons/BitcoinIcon";
|
import BitcoinIcon from "../../icons/BitcoinIcon";
|
||||||
import InfoBox from "../../modal/swap/InfoBox";
|
import InfoBox from "../swap/swap/components/InfoBox";
|
||||||
import WithdrawDialog from "../../modal/wallet/WithdrawDialog";
|
import WithdrawDialog from "../../modal/wallet/WithdrawDialog";
|
||||||
import WalletRefreshButton from "./WalletRefreshButton";
|
import WalletRefreshButton from "./WalletRefreshButton";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,15 @@ const baseTheme: ThemeOptions = {
|
||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
breakpoints: {
|
||||||
|
values: {
|
||||||
|
xs: 0,
|
||||||
|
sm: 600,
|
||||||
|
md: 900,
|
||||||
|
lg: 1000,
|
||||||
|
xl: 1536,
|
||||||
|
},
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
MuiButton: {
|
MuiButton: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import {
|
||||||
ResolveApprovalResponse,
|
ResolveApprovalResponse,
|
||||||
RedactArgs,
|
RedactArgs,
|
||||||
RedactResponse,
|
RedactResponse,
|
||||||
|
GetCurrentSwapResponse,
|
||||||
LabeledMoneroAddress,
|
LabeledMoneroAddress,
|
||||||
} from "models/tauriModel";
|
} from "models/tauriModel";
|
||||||
import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice";
|
import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice";
|
||||||
|
|
@ -174,11 +175,22 @@ export async function withdrawBtc(address: string): Promise<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buyXmr(
|
export async function buyXmr(
|
||||||
seller: Maker,
|
|
||||||
bitcoin_change_address: string | null,
|
bitcoin_change_address: string | null,
|
||||||
monero_receive_address: string,
|
monero_receive_address: string,
|
||||||
donation_percentage: DonateToDevelopmentTip,
|
donation_percentage: DonateToDevelopmentTip,
|
||||||
) {
|
) {
|
||||||
|
// Get all available makers from the Redux store
|
||||||
|
const state = store.getState();
|
||||||
|
const allMakers = [
|
||||||
|
...(state.makers.registry.makers || []),
|
||||||
|
...state.makers.rendezvous.makers,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Convert all makers to multiaddr format
|
||||||
|
const sellers = allMakers.map((maker) =>
|
||||||
|
providerToConcatenatedMultiAddr(maker),
|
||||||
|
);
|
||||||
|
|
||||||
const address_pool: LabeledMoneroAddress[] = [];
|
const address_pool: LabeledMoneroAddress[] = [];
|
||||||
if (donation_percentage !== false) {
|
if (donation_percentage !== false) {
|
||||||
const donation_address = isTestnet()
|
const donation_address = isTestnet()
|
||||||
|
|
@ -206,7 +218,8 @@ export async function buyXmr(
|
||||||
}
|
}
|
||||||
|
|
||||||
await invoke<BuyXmrArgs, BuyXmrResponse>("buy_xmr", {
|
await invoke<BuyXmrArgs, BuyXmrResponse>("buy_xmr", {
|
||||||
seller: providerToConcatenatedMultiAddr(seller),
|
rendezvous_points: PRESET_RENDEZVOUS_POINTS,
|
||||||
|
sellers,
|
||||||
monero_receive_pool: address_pool,
|
monero_receive_pool: address_pool,
|
||||||
bitcoin_change_address,
|
bitcoin_change_address,
|
||||||
});
|
});
|
||||||
|
|
@ -222,6 +235,10 @@ export async function suspendCurrentSwap() {
|
||||||
await invokeNoArgs<SuspendCurrentSwapResponse>("suspend_current_swap");
|
await invokeNoArgs<SuspendCurrentSwapResponse>("suspend_current_swap");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCurrentSwapId() {
|
||||||
|
return await invokeNoArgs<GetCurrentSwapResponse>("get_current_swap");
|
||||||
|
}
|
||||||
|
|
||||||
export async function getMoneroRecoveryKeys(
|
export async function getMoneroRecoveryKeys(
|
||||||
swapId: string,
|
swapId: string,
|
||||||
): Promise<MoneroRecoveryResponse> {
|
): Promise<MoneroRecoveryResponse> {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import makersSlice from "./features/makersSlice";
|
||||||
import ratesSlice from "./features/ratesSlice";
|
import ratesSlice from "./features/ratesSlice";
|
||||||
import rpcSlice from "./features/rpcSlice";
|
import rpcSlice from "./features/rpcSlice";
|
||||||
import swapReducer from "./features/swapSlice";
|
import swapReducer from "./features/swapSlice";
|
||||||
import torSlice from "./features/torSlice";
|
|
||||||
import settingsSlice from "./features/settingsSlice";
|
import settingsSlice from "./features/settingsSlice";
|
||||||
import nodesSlice from "./features/nodesSlice";
|
import nodesSlice from "./features/nodesSlice";
|
||||||
import conversationsSlice from "./features/conversationsSlice";
|
import conversationsSlice from "./features/conversationsSlice";
|
||||||
|
|
@ -12,7 +11,6 @@ import poolSlice from "./features/poolSlice";
|
||||||
export const reducers = {
|
export const reducers = {
|
||||||
swap: swapReducer,
|
swap: swapReducer,
|
||||||
makers: makersSlice,
|
makers: makersSlice,
|
||||||
tor: torSlice,
|
|
||||||
rpc: rpcSlice,
|
rpc: rpcSlice,
|
||||||
alerts: alertsSlice,
|
alerts: alertsSlice,
|
||||||
rates: ratesSlice,
|
rates: ratesSlice,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { SellerStatus } from "models/tauriModel";
|
||||||
import { getStubTestnetMaker } from "store/config";
|
import { getStubTestnetMaker } from "store/config";
|
||||||
import { rendezvousSellerToMakerStatus } from "utils/conversionUtils";
|
import { rendezvousSellerToMakerStatus } from "utils/conversionUtils";
|
||||||
import { isMakerOutdated } from "utils/multiAddrUtils";
|
import { isMakerOutdated } from "utils/multiAddrUtils";
|
||||||
import { sortMakerList } from "utils/sortUtils";
|
|
||||||
|
|
||||||
const stubTestnetMaker = getStubTestnetMaker();
|
const stubTestnetMaker = getStubTestnetMaker();
|
||||||
|
|
||||||
|
|
@ -48,10 +47,10 @@ function selectNewSelectedMaker(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise we'd prefer to switch to a provider that has the newest version
|
// Otherwise we'd prefer to switch to a provider that has the newest version
|
||||||
const providers = sortMakerList([
|
const providers = [
|
||||||
...(slice.registry.makers ?? []),
|
...(slice.registry.makers ?? []),
|
||||||
...(slice.rendezvous.makers ?? []),
|
...(slice.rendezvous.makers ?? []),
|
||||||
]);
|
];
|
||||||
|
|
||||||
return providers.at(0) || null;
|
return providers.at(0) || null;
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +85,6 @@ export const makersSlice = createSlice({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort the provider list and select a new provider if needed
|
// Sort the provider list and select a new provider if needed
|
||||||
slice.rendezvous.makers = sortMakerList(slice.rendezvous.makers);
|
|
||||||
slice.selectedMaker = selectNewSelectedMaker(slice);
|
slice.selectedMaker = selectNewSelectedMaker(slice);
|
||||||
},
|
},
|
||||||
setRegistryMakers(slice, action: PayloadAction<ExtendedMakerStatus[]>) {
|
setRegistryMakers(slice, action: PayloadAction<ExtendedMakerStatus[]>) {
|
||||||
|
|
@ -95,7 +93,6 @@ export const makersSlice = createSlice({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the provider list and select a new provider if needed
|
// Sort the provider list and select a new provider if needed
|
||||||
slice.registry.makers = sortMakerList(action.payload);
|
|
||||||
slice.selectedMaker = selectNewSelectedMaker(slice);
|
slice.selectedMaker = selectNewSelectedMaker(slice);
|
||||||
},
|
},
|
||||||
registryConnectionFailed(slice) {
|
registryConnectionFailed(slice) {
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|
||||||
|
|
||||||
export interface TorSlice {
|
|
||||||
exitCode: number | null;
|
|
||||||
processRunning: boolean;
|
|
||||||
stdOut: string;
|
|
||||||
proxyStatus:
|
|
||||||
| false
|
|
||||||
| {
|
|
||||||
proxyHostname: string;
|
|
||||||
proxyPort: number;
|
|
||||||
bootstrapped: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: TorSlice = {
|
|
||||||
processRunning: false,
|
|
||||||
exitCode: null,
|
|
||||||
stdOut: "",
|
|
||||||
proxyStatus: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const socksListenerRegex =
|
|
||||||
/Opened Socks listener connection.*on (\d+\.\d+\.\d+\.\d+):(\d+)/;
|
|
||||||
const bootstrapDoneRegex = /Bootstrapped 100% \(done\)/;
|
|
||||||
|
|
||||||
export const torSlice = createSlice({
|
|
||||||
name: "tor",
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
torAppendStdOut(slice, action: PayloadAction<string>) {
|
|
||||||
slice.stdOut += action.payload;
|
|
||||||
|
|
||||||
const logs = slice.stdOut.split("\n");
|
|
||||||
logs.forEach((log) => {
|
|
||||||
if (socksListenerRegex.test(log)) {
|
|
||||||
const match = socksListenerRegex.exec(log);
|
|
||||||
if (match) {
|
|
||||||
slice.proxyStatus = {
|
|
||||||
proxyHostname: match[1],
|
|
||||||
proxyPort: Number.parseInt(match[2], 10),
|
|
||||||
bootstrapped: slice.proxyStatus
|
|
||||||
? slice.proxyStatus.bootstrapped
|
|
||||||
: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (bootstrapDoneRegex.test(log)) {
|
|
||||||
if (slice.proxyStatus) {
|
|
||||||
slice.proxyStatus.bootstrapped = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
torInitiate(slice) {
|
|
||||||
slice.processRunning = true;
|
|
||||||
},
|
|
||||||
torProcessExited(
|
|
||||||
slice,
|
|
||||||
action: PayloadAction<{
|
|
||||||
exitCode: number | null;
|
|
||||||
exitSignal: NodeJS.Signals | null;
|
|
||||||
}>,
|
|
||||||
) {
|
|
||||||
slice.processRunning = false;
|
|
||||||
slice.exitCode = action.payload.exitCode;
|
|
||||||
slice.proxyStatus = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { torAppendStdOut, torInitiate, torProcessExited } =
|
|
||||||
torSlice.actions;
|
|
||||||
|
|
||||||
export default torSlice.reducer;
|
|
||||||
|
|
@ -8,6 +8,9 @@ import {
|
||||||
isPendingSeedSelectionApprovalEvent,
|
isPendingSeedSelectionApprovalEvent,
|
||||||
PendingApprovalRequest,
|
PendingApprovalRequest,
|
||||||
PendingLockBitcoinApprovalRequest,
|
PendingLockBitcoinApprovalRequest,
|
||||||
|
PendingSelectMakerApprovalRequest,
|
||||||
|
isPendingSelectMakerApprovalEvent,
|
||||||
|
haveFundsBeenLocked,
|
||||||
PendingSeedSelectionApprovalRequest,
|
PendingSeedSelectionApprovalRequest,
|
||||||
} from "models/tauriModelExt";
|
} from "models/tauriModelExt";
|
||||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||||
|
|
@ -18,7 +21,6 @@ import { isCliLogRelatedToSwap } from "models/cliModel";
|
||||||
import { SettingsState } from "./features/settingsSlice";
|
import { SettingsState } from "./features/settingsSlice";
|
||||||
import { NodesSlice } from "./features/nodesSlice";
|
import { NodesSlice } from "./features/nodesSlice";
|
||||||
import { RatesState } from "./features/ratesSlice";
|
import { RatesState } from "./features/ratesSlice";
|
||||||
import { sortMakerList } from "utils/sortUtils";
|
|
||||||
import {
|
import {
|
||||||
TauriBackgroundProgress,
|
TauriBackgroundProgress,
|
||||||
TauriBitcoinSyncProgress,
|
TauriBitcoinSyncProgress,
|
||||||
|
|
@ -56,7 +58,7 @@ export function useResumeableSwapsCountExcludingPunished() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if we have a swap that is running
|
/// Returns true if we have any swap that is running
|
||||||
export function useIsSwapRunning() {
|
export function useIsSwapRunning() {
|
||||||
return useAppSelector(
|
return useAppSelector(
|
||||||
(state) =>
|
(state) =>
|
||||||
|
|
@ -64,6 +66,46 @@ export function useIsSwapRunning() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if we have a swap that is running and
|
||||||
|
/// that swap has any funds locked
|
||||||
|
export function useIsSwapRunningAndHasFundsLocked() {
|
||||||
|
const swapInfo = useActiveSwapInfo();
|
||||||
|
const swapTauriState = useAppSelector(
|
||||||
|
(state) => state.swap.state?.curr ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the swap is in the Released state, we return false
|
||||||
|
if (swapTauriState?.type === "Released") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the tauri state tells us that funds have been locked, we return true
|
||||||
|
if (haveFundsBeenLocked(swapTauriState)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a database entry (swapInfo) for this swap, we return true
|
||||||
|
if (swapInfo != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if we have a swap that is running
|
||||||
|
export function useIsSpecificSwapRunning(swapId: string | null) {
|
||||||
|
if (swapId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
state.swap.state !== null &&
|
||||||
|
state.swap.state.swapId === swapId &&
|
||||||
|
state.swap.state.curr.type !== "Released",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function useIsContextAvailable() {
|
export function useIsContextAvailable() {
|
||||||
return useAppSelector(
|
return useAppSelector(
|
||||||
(state) => state.rpc.status === TauriContextStatusEvent.Available,
|
(state) => state.rpc.status === TauriContextStatusEvent.Available,
|
||||||
|
|
@ -103,9 +145,7 @@ export function useAllMakers() {
|
||||||
return useAppSelector((state) => {
|
return useAppSelector((state) => {
|
||||||
const registryMakers = state.makers.registry.makers || [];
|
const registryMakers = state.makers.registry.makers || [];
|
||||||
const listSellersMakers = state.makers.rendezvous.makers || [];
|
const listSellersMakers = state.makers.rendezvous.makers || [];
|
||||||
const all = [...registryMakers, ...listSellersMakers];
|
return [...registryMakers, ...listSellersMakers];
|
||||||
|
|
||||||
return sortMakerList(all);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,6 +207,11 @@ export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalReque
|
||||||
return approvals.filter((c) => isPendingLockBitcoinApprovalEvent(c));
|
return approvals.filter((c) => isPendingLockBitcoinApprovalEvent(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function usePendingSelectMakerApproval(): PendingSelectMakerApprovalRequest[] {
|
||||||
|
const approvals = usePendingApprovals();
|
||||||
|
return approvals.filter((c) => isPendingSelectMakerApprovalEvent(c));
|
||||||
|
}
|
||||||
|
|
||||||
export function usePendingSeedSelectionApproval(): PendingSeedSelectionApprovalRequest[] {
|
export function usePendingSeedSelectionApproval(): PendingSeedSelectionApprovalRequest[] {
|
||||||
const approvals = usePendingApprovals();
|
const approvals = usePendingApprovals();
|
||||||
return approvals.filter((c) => isPendingSeedSelectionApprovalEvent(c));
|
return approvals.filter((c) => isPendingSeedSelectionApprovalEvent(c));
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { createListenerMiddleware } from "@reduxjs/toolkit";
|
import { createListenerMiddleware } from "@reduxjs/toolkit";
|
||||||
|
import { throttle, debounce } from "lodash";
|
||||||
import {
|
import {
|
||||||
getAllSwapInfos,
|
getAllSwapInfos,
|
||||||
checkBitcoinBalance,
|
checkBitcoinBalance,
|
||||||
|
|
@ -22,6 +23,33 @@ import {
|
||||||
} from "store/features/conversationsSlice";
|
} from "store/features/conversationsSlice";
|
||||||
import { TauriContextStatusEvent } from "models/tauriModel";
|
import { TauriContextStatusEvent } from "models/tauriModel";
|
||||||
|
|
||||||
|
// Create a Map to store throttled functions per swap_id
|
||||||
|
const throttledGetSwapInfoFunctions = new Map<
|
||||||
|
string,
|
||||||
|
ReturnType<typeof throttle>
|
||||||
|
>();
|
||||||
|
|
||||||
|
// Function to get or create a throttled getSwapInfo for a specific swap_id
|
||||||
|
const getThrottledSwapInfoUpdater = (swapId: string) => {
|
||||||
|
if (!throttledGetSwapInfoFunctions.has(swapId)) {
|
||||||
|
// Create a throttled function that executes at most once every 2 seconds
|
||||||
|
// but will wait for 3 seconds of quiet during rapid calls (using debounce)
|
||||||
|
const debouncedGetSwapInfo = debounce(() => {
|
||||||
|
logger.debug(`Executing getSwapInfo for swap ${swapId}`);
|
||||||
|
getSwapInfo(swapId);
|
||||||
|
}, 3000); // 3 seconds debounce for rapid calls
|
||||||
|
|
||||||
|
const throttledFunction = throttle(debouncedGetSwapInfo, 2000, {
|
||||||
|
leading: true, // Execute immediately on first call
|
||||||
|
trailing: true, // Execute on trailing edge if needed
|
||||||
|
});
|
||||||
|
|
||||||
|
throttledGetSwapInfoFunctions.set(swapId, throttledFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return throttledGetSwapInfoFunctions.get(swapId)!;
|
||||||
|
};
|
||||||
|
|
||||||
export function createMainListeners() {
|
export function createMainListeners() {
|
||||||
const listener = createListenerMiddleware();
|
const listener = createListenerMiddleware();
|
||||||
|
|
||||||
|
|
@ -57,11 +85,14 @@ export function createMainListeners() {
|
||||||
await checkBitcoinBalance();
|
await checkBitcoinBalance();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the swap info
|
// Update the swap info using throttled function
|
||||||
logger.info(
|
logger.info(
|
||||||
"Swap progress event received, updating swap info from database...",
|
"Swap progress event received, scheduling throttled swap info update...",
|
||||||
);
|
);
|
||||||
await getSwapInfo(action.payload.swap_id);
|
const throttledUpdater = getThrottledSwapInfoUpdater(
|
||||||
|
action.payload.swap_id,
|
||||||
|
);
|
||||||
|
throttledUpdater();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,57 @@
|
||||||
import { ExtendedMakerStatus } from "models/apiModel";
|
import {
|
||||||
import { isMakerOnCorrectNetwork, isMakerOutdated } from "./multiAddrUtils";
|
PendingSelectMakerApprovalRequest,
|
||||||
|
SortableQuoteWithAddress,
|
||||||
|
} from "models/tauriModelExt";
|
||||||
|
import { QuoteWithAddress } from "models/tauriModel";
|
||||||
|
import { isMakerVersionOutdated } from "./multiAddrUtils";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
export function sortMakerList(list: ExtendedMakerStatus[]) {
|
export function sortApprovalsAndKnownQuotes(
|
||||||
|
pendingSelectMakerApprovals: PendingSelectMakerApprovalRequest[],
|
||||||
|
known_quotes: QuoteWithAddress[],
|
||||||
|
) {
|
||||||
|
const sortableQuotes = pendingSelectMakerApprovals.map((approval) => {
|
||||||
|
return {
|
||||||
|
...approval.request.content.maker,
|
||||||
|
expiration_ts:
|
||||||
|
approval.request_status.state === "Pending"
|
||||||
|
? approval.request_status.content.expiration_ts
|
||||||
|
: undefined,
|
||||||
|
request_id: approval.request_id,
|
||||||
|
} as SortableQuoteWithAddress;
|
||||||
|
});
|
||||||
|
|
||||||
|
sortableQuotes.push(
|
||||||
|
...known_quotes.map((quote) => ({
|
||||||
|
...quote,
|
||||||
|
request_id: null,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
return sortMakerApprovals(sortableQuotes);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortMakerApprovals(list: SortableQuoteWithAddress[]) {
|
||||||
return (
|
return (
|
||||||
_(list)
|
_(list)
|
||||||
// Filter out makers that are on the wrong network (testnet / mainnet)
|
|
||||||
.filter(isMakerOnCorrectNetwork)
|
|
||||||
// Sort by criteria
|
|
||||||
.orderBy(
|
.orderBy(
|
||||||
[
|
[
|
||||||
// Prefer makers that have a 'version' attribute
|
// Prefer makers that have a 'version' attribute
|
||||||
// If we don't have a version, we cannot clarify if it's outdated or not
|
// If we don't have a version, we cannot clarify if it's outdated or not
|
||||||
(m) => (m.version ? 0 : 1),
|
(m) => (m.version ? 0 : 1),
|
||||||
// Prefer makers that are not outdated
|
// Prefer makers that are not outdated
|
||||||
(m) => (isMakerOutdated(m) ? 1 : 0),
|
(m) => (isMakerVersionOutdated(m.version) ? 1 : 0),
|
||||||
// Prefer makers that have a relevancy score
|
|
||||||
(m) => (m.relevancy == null ? 1 : 0),
|
|
||||||
// Prefer makers with a higher relevancy score
|
|
||||||
(m) => -(m.relevancy ?? 0),
|
|
||||||
// Prefer makers with a minimum quantity > 0
|
// Prefer makers with a minimum quantity > 0
|
||||||
(m) => ((m.minSwapAmount ?? 0) > 0 ? 0 : 1),
|
(m) => ((m.quote.min_quantity ?? 0) > 0 ? 0 : 1),
|
||||||
|
// Prefer approvals over actual quotes
|
||||||
|
(m) => (m.request_id ? 0 : 1),
|
||||||
// Prefer makers with a lower price
|
// Prefer makers with a lower price
|
||||||
(m) => m.price,
|
(m) => m.quote.price,
|
||||||
],
|
],
|
||||||
["asc", "asc", "asc", "asc", "asc"],
|
["asc", "asc", "asc", "asc", "asc"],
|
||||||
)
|
)
|
||||||
// Remove duplicate makers
|
// Remove duplicate makers
|
||||||
.uniqBy((m) => m.peerId)
|
.uniqBy((m) => m.peer_id)
|
||||||
.value()
|
.value()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1554,6 +1554,11 @@ bl@^1.2.1:
|
||||||
readable-stream "^2.3.5"
|
readable-stream "^2.3.5"
|
||||||
safe-buffer "^5.1.1"
|
safe-buffer "^5.1.1"
|
||||||
|
|
||||||
|
boring-avatars@^1.11.2:
|
||||||
|
version "1.11.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/boring-avatars/-/boring-avatars-1.11.2.tgz#365e0b765fb0065ca0cb2fd20c200674d0a9ded6"
|
||||||
|
integrity sha512-3+wkwPeObwS4R37FGXMYViqc4iTrIRj5yzfX9Qy4mnpZ26sX41dGMhsAgmKks1r/uufY1pl4vpgzMWHYfJRb2A==
|
||||||
|
|
||||||
brace-expansion@^1.1.7:
|
brace-expansion@^1.1.7:
|
||||||
version "1.1.12"
|
version "1.1.12"
|
||||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
|
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,10 @@ use swap::cli::{
|
||||||
request::{
|
request::{
|
||||||
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs,
|
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs,
|
||||||
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs,
|
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs,
|
||||||
CheckSeedResponse, ExportBitcoinWalletArgs, GetDataDirArgs, GetHistoryArgs,
|
CheckSeedResponse, ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs,
|
||||||
GetLogsArgs, GetMoneroAddressesArgs, GetSwapInfoArgs, GetSwapInfosAllArgs,
|
GetHistoryArgs, GetLogsArgs, GetMoneroAddressesArgs, GetSwapInfoArgs,
|
||||||
ListSellersArgs, MoneroRecoveryArgs, RedactArgs, ResolveApprovalArgs, ResumeSwapArgs,
|
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
|
||||||
SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
ResolveApprovalArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||||
},
|
},
|
||||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
||||||
Context, ContextBuilder,
|
Context, ContextBuilder,
|
||||||
|
|
@ -195,6 +195,7 @@ pub fn run() {
|
||||||
check_monero_node,
|
check_monero_node,
|
||||||
check_electrum_node,
|
check_electrum_node,
|
||||||
get_wallet_descriptor,
|
get_wallet_descriptor,
|
||||||
|
get_current_swap,
|
||||||
get_data_dir,
|
get_data_dir,
|
||||||
resolve_approval_request,
|
resolve_approval_request,
|
||||||
redact,
|
redact,
|
||||||
|
|
@ -249,6 +250,7 @@ tauri_command!(get_swap_info, GetSwapInfoArgs);
|
||||||
tauri_command!(get_swap_infos_all, GetSwapInfosAllArgs, no_args);
|
tauri_command!(get_swap_infos_all, GetSwapInfosAllArgs, no_args);
|
||||||
tauri_command!(get_history, GetHistoryArgs, no_args);
|
tauri_command!(get_history, GetHistoryArgs, no_args);
|
||||||
tauri_command!(get_monero_addresses, GetMoneroAddressesArgs, no_args);
|
tauri_command!(get_monero_addresses, GetMoneroAddressesArgs, no_args);
|
||||||
|
tauri_command!(get_current_swap, GetCurrentSwapArgs, no_args);
|
||||||
|
|
||||||
/// Here we define Tauri commands whose implementation is not delegated to the Request trait
|
/// Here we define Tauri commands whose implementation is not delegated to the Request trait
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ monero-sys = { path = "../monero-sys" }
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
pem = "3.0"
|
pem = "3.0"
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
qrcode = "0.14"
|
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
rand_chacha = "0.3"
|
rand_chacha = "0.3"
|
||||||
regex = "1.10"
|
regex = "1.10"
|
||||||
|
|
@ -72,9 +71,9 @@ strum = { version = "0.26", features = ["derive"] }
|
||||||
tauri = { version = "2.0", features = ["config-json5"], optional = true, default-features = false }
|
tauri = { version = "2.0", features = ["config-json5"], optional = true, default-features = false }
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
time = "0.3"
|
time = "0.3"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "time", "macros", "sync", "process", "fs", "net", "parking_lot"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "time", "macros", "sync", "process", "fs", "net", "parking_lot", "rt"] }
|
||||||
tokio-tungstenite = { version = "0.15", features = ["rustls-tls"] }
|
tokio-tungstenite = { version = "0.15", features = ["rustls-tls"] }
|
||||||
tokio-util = { version = "0.7", features = ["io", "codec"] }
|
tokio-util = { version = "0.7", features = ["io", "codec", "rt"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
tor-rtcompat = { version = "0.25.0", features = ["tokio"] }
|
tor-rtcompat = { version = "0.25.0", features = ["tokio"] }
|
||||||
tower = { version = "0.4.13", features = ["full"] }
|
tower = { version = "0.4.13", features = ["full"] }
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,40 @@
|
||||||
use super::tauri_bindings::TauriHandle;
|
use super::tauri_bindings::TauriHandle;
|
||||||
use crate::bitcoin::{wallet, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxLock};
|
use crate::bitcoin::{wallet, CancelTimelock, ExpiredTimelocks, PunishTimelock};
|
||||||
use crate::cli::api::tauri_bindings::{TauriEmitter, TauriSwapProgressEvent};
|
use crate::cli::api::tauri_bindings::{SelectMakerDetails, TauriEmitter, TauriSwapProgressEvent};
|
||||||
use crate::cli::api::Context;
|
use crate::cli::api::Context;
|
||||||
use crate::cli::list_sellers::{QuoteWithAddress, UnreachableSeller};
|
use crate::cli::list_sellers::{list_sellers_init, QuoteWithAddress, UnreachableSeller};
|
||||||
use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus};
|
use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus};
|
||||||
use crate::common::{get_logs, redact};
|
use crate::common::{get_logs, redact};
|
||||||
use crate::libp2p_ext::MultiAddrExt;
|
use crate::libp2p_ext::MultiAddrExt;
|
||||||
use crate::monero::wallet_rpc::MoneroDaemon;
|
use crate::monero::wallet_rpc::MoneroDaemon;
|
||||||
use crate::monero::MoneroAddressPool;
|
use crate::monero::MoneroAddressPool;
|
||||||
use crate::network::quote::{BidQuote, ZeroQuoteReceived};
|
use crate::network::quote::{BidQuote, ZeroQuoteReceived};
|
||||||
|
use crate::network::rendezvous::XmrBtcNamespace;
|
||||||
use crate::network::swarm;
|
use crate::network::swarm;
|
||||||
use crate::protocol::bob::{BobState, Swap};
|
use crate::protocol::bob::{BobState, Swap};
|
||||||
use crate::protocol::{bob, State};
|
use crate::protocol::{bob, Database, State};
|
||||||
use crate::{bitcoin, cli, monero};
|
use crate::{bitcoin, cli, monero};
|
||||||
use ::bitcoin::address::NetworkUnchecked;
|
use ::bitcoin::address::NetworkUnchecked;
|
||||||
use ::bitcoin::Txid;
|
use ::bitcoin::Txid;
|
||||||
use ::monero::Network;
|
use ::monero::Network;
|
||||||
use anyhow::{bail, Context as AnyContext, Result};
|
use anyhow::{bail, Context as AnyContext, Result};
|
||||||
|
use arti_client::TorClient;
|
||||||
|
use futures::stream::FuturesUnordered;
|
||||||
|
use futures::StreamExt;
|
||||||
use libp2p::core::Multiaddr;
|
use libp2p::core::Multiaddr;
|
||||||
use libp2p::PeerId;
|
use libp2p::{identity, PeerId};
|
||||||
use monero_seed::{Language, Seed as MoneroSeed};
|
use monero_seed::{Language, Seed as MoneroSeed};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use qrcode::render::unicode;
|
|
||||||
use qrcode::QrCode;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::cmp::min;
|
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tokio_util::task::AbortOnDropHandle;
|
||||||
|
use tor_rtcompat::tokio::TokioRustlsRuntime;
|
||||||
use tracing::debug_span;
|
use tracing::debug_span;
|
||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
use tracing::Span;
|
use tracing::Span;
|
||||||
|
|
@ -58,8 +61,10 @@ fn get_swap_tracing_span(swap_id: Uuid) -> Span {
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct BuyXmrArgs {
|
pub struct BuyXmrArgs {
|
||||||
#[typeshare(serialized_as = "string")]
|
#[typeshare(serialized_as = "Vec<string>")]
|
||||||
pub seller: Multiaddr,
|
pub rendezvous_points: Vec<Multiaddr>,
|
||||||
|
#[typeshare(serialized_as = "Vec<string>")]
|
||||||
|
pub sellers: Vec<Multiaddr>,
|
||||||
#[typeshare(serialized_as = "Option<string>")]
|
#[typeshare(serialized_as = "Option<string>")]
|
||||||
pub bitcoin_change_address: Option<bitcoin::Address<NetworkUnchecked>>,
|
pub bitcoin_change_address: Option<bitcoin::Address<NetworkUnchecked>>,
|
||||||
pub monero_receive_pool: MoneroAddressPool,
|
pub monero_receive_pool: MoneroAddressPool,
|
||||||
|
|
@ -310,8 +315,9 @@ pub struct SuspendCurrentSwapArgs;
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct SuspendCurrentSwapResponse {
|
pub struct SuspendCurrentSwapResponse {
|
||||||
#[typeshare(serialized_as = "string")]
|
// If no swap was running, we still return Ok(...) but this is set to None
|
||||||
pub swap_id: Uuid,
|
#[typeshare(serialized_as = "Option<string>")]
|
||||||
|
pub swap_id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Request for SuspendCurrentSwapArgs {
|
impl Request for SuspendCurrentSwapArgs {
|
||||||
|
|
@ -322,10 +328,19 @@ impl Request for SuspendCurrentSwapArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct GetCurrentSwapArgs;
|
pub struct GetCurrentSwapArgs;
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct GetCurrentSwapResponse {
|
||||||
|
#[typeshare(serialized_as = "Option<string>")]
|
||||||
|
pub swap_id: Option<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Request for GetCurrentSwapArgs {
|
impl Request for GetCurrentSwapArgs {
|
||||||
type Response = serde_json::Value;
|
type Response = GetCurrentSwapResponse;
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
get_current_swap(ctx).await
|
get_current_swap(ctx).await
|
||||||
|
|
@ -463,9 +478,12 @@ pub async fn suspend_current_swap(context: Arc<Context>) -> Result<SuspendCurren
|
||||||
if let Some(id_value) = swap_id {
|
if let Some(id_value) = swap_id {
|
||||||
context.swap_lock.send_suspend_signal().await?;
|
context.swap_lock.send_suspend_signal().await?;
|
||||||
|
|
||||||
Ok(SuspendCurrentSwapResponse { swap_id: id_value })
|
Ok(SuspendCurrentSwapResponse {
|
||||||
|
swap_id: Some(id_value),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
bail!("No swap is currently running")
|
// If no swap was running, we still return Ok(...) with None
|
||||||
|
Ok(SuspendCurrentSwapResponse { swap_id: None })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -593,8 +611,11 @@ pub async fn buy_xmr(
|
||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
context: Arc<Context>,
|
context: Arc<Context>,
|
||||||
) -> Result<BuyXmrResponse, anyhow::Error> {
|
) -> Result<BuyXmrResponse, anyhow::Error> {
|
||||||
|
let _span = get_swap_tracing_span(swap_id);
|
||||||
|
|
||||||
let BuyXmrArgs {
|
let BuyXmrArgs {
|
||||||
seller,
|
rendezvous_points,
|
||||||
|
sellers,
|
||||||
bitcoin_change_address,
|
bitcoin_change_address,
|
||||||
monero_receive_pool,
|
monero_receive_pool,
|
||||||
} = buy_xmr;
|
} = buy_xmr;
|
||||||
|
|
@ -635,13 +656,103 @@ pub async fn buy_xmr(
|
||||||
let env_config = context.config.env_config;
|
let env_config = context.config.env_config;
|
||||||
let seed = context.config.seed.clone().context("Could not get seed")?;
|
let seed = context.config.seed.clone().context("Could not get seed")?;
|
||||||
|
|
||||||
let seller_peer_id = seller
|
// Prepare variables for the quote fetching process
|
||||||
.extract_peer_id()
|
let identity = seed.derive_libp2p_identity();
|
||||||
.context("Seller address must contain peer ID")?;
|
let namespace = context.config.namespace;
|
||||||
|
let tor_client = context.tor_client.clone();
|
||||||
|
let db = Some(context.db.clone());
|
||||||
|
let tauri_handle = context.tauri_handle.clone();
|
||||||
|
|
||||||
|
// Wait for the user to approve a seller and to deposit coins
|
||||||
|
// Calling determine_btc_to_swap
|
||||||
|
let address_len = bitcoin_wallet.new_address().await?.script_pubkey().len();
|
||||||
|
|
||||||
|
let bitcoin_wallet_for_closures = Arc::clone(&bitcoin_wallet);
|
||||||
|
|
||||||
|
// Clone bitcoin_change_address before moving it in the emit call
|
||||||
|
let bitcoin_change_address_for_spawn = bitcoin_change_address.clone();
|
||||||
|
let rendezvous_points_clone = rendezvous_points.clone();
|
||||||
|
let sellers_clone = sellers.clone();
|
||||||
|
|
||||||
|
// Acquire the lock before the user has selected a maker and we already have funds in the wallet
|
||||||
|
// because we need to be able to cancel the determine_btc_to_swap(..)
|
||||||
|
context.swap_lock.acquire_swap_lock(swap_id).await?;
|
||||||
|
|
||||||
|
let (seller_multiaddr, seller_peer_id, quote, tx_lock_amount, tx_lock_fee) = tokio::select! {
|
||||||
|
result = determine_btc_to_swap(
|
||||||
|
move || {
|
||||||
|
let rendezvous_points = rendezvous_points_clone.clone();
|
||||||
|
let sellers = sellers_clone.clone();
|
||||||
|
let namespace = namespace;
|
||||||
|
let identity = identity.clone();
|
||||||
|
let db = db.clone();
|
||||||
|
let tor_client = tor_client.clone();
|
||||||
|
let tauri_handle = tauri_handle.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
fetch_quotes_task(
|
||||||
|
rendezvous_points,
|
||||||
|
namespace,
|
||||||
|
sellers,
|
||||||
|
identity,
|
||||||
|
db,
|
||||||
|
tor_client,
|
||||||
|
tauri_handle,
|
||||||
|
).await
|
||||||
|
})
|
||||||
|
},
|
||||||
|
bitcoin_wallet.new_address(),
|
||||||
|
{
|
||||||
|
let wallet = Arc::clone(&bitcoin_wallet_for_closures);
|
||||||
|
move || {
|
||||||
|
let w = wallet.clone();
|
||||||
|
async move { w.balance().await }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let wallet = Arc::clone(&bitcoin_wallet_for_closures);
|
||||||
|
move || {
|
||||||
|
let w = wallet.clone();
|
||||||
|
async move { w.max_giveable(address_len).await }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let wallet = Arc::clone(&bitcoin_wallet_for_closures);
|
||||||
|
move || {
|
||||||
|
let w = wallet.clone();
|
||||||
|
async move { w.sync().await }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
context.tauri_handle.clone(),
|
||||||
|
swap_id,
|
||||||
|
|quote_with_address| {
|
||||||
|
let tauri_handle = context.tauri_handle.clone();
|
||||||
|
Box::new(async move {
|
||||||
|
let details = SelectMakerDetails {
|
||||||
|
swap_id,
|
||||||
|
btc_amount_to_swap: quote_with_address.quote.max_quantity,
|
||||||
|
maker: quote_with_address,
|
||||||
|
};
|
||||||
|
|
||||||
|
tauri_handle.request_maker_selection(details, 300).await
|
||||||
|
}) as Box<dyn Future<Output = Result<bool>> + Send>
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
result?
|
||||||
|
}
|
||||||
|
_ = context.swap_lock.listen_for_swap_force_suspension() => {
|
||||||
|
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
|
||||||
|
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
||||||
|
bail!("Shutdown signal received");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert the peer_id into the database
|
||||||
|
context.db.insert_peer_id(swap_id, seller_peer_id).await?;
|
||||||
|
|
||||||
context
|
context
|
||||||
.db
|
.db
|
||||||
.insert_address(seller_peer_id, seller.clone())
|
.insert_address(seller_peer_id, seller_multiaddr.clone())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let behaviour = cli::Behaviour::new(
|
let behaviour = cli::Behaviour::new(
|
||||||
|
|
@ -658,7 +769,7 @@ pub async fn buy_xmr(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
swarm.add_peer_address(seller_peer_id, seller);
|
swarm.add_peer_address(seller_peer_id, seller_multiaddr.clone());
|
||||||
|
|
||||||
context
|
context
|
||||||
.db
|
.db
|
||||||
|
|
@ -667,57 +778,19 @@ pub async fn buy_xmr(
|
||||||
|
|
||||||
tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
|
tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
|
||||||
|
|
||||||
context.swap_lock.acquire_swap_lock(swap_id).await?;
|
context.tauri_handle.emit_swap_progress_event(
|
||||||
|
swap_id,
|
||||||
|
TauriSwapProgressEvent::ReceivedQuote(quote.clone()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now create the event loop we use for the swap
|
||||||
|
let (event_loop, event_loop_handle) =
|
||||||
|
EventLoop::new(swap_id, swarm, seller_peer_id, context.db.clone())?;
|
||||||
|
let event_loop = tokio::spawn(event_loop.run().in_current_span());
|
||||||
|
|
||||||
context
|
context
|
||||||
.tauri_handle
|
.tauri_handle
|
||||||
.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::RequestingQuote);
|
.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::ReceivedQuote(quote));
|
||||||
|
|
||||||
let initialize_swap = tokio::select! {
|
|
||||||
biased;
|
|
||||||
_ = context.swap_lock.listen_for_swap_force_suspension() => {
|
|
||||||
tracing::debug!("Shutdown signal received, exiting");
|
|
||||||
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
|
|
||||||
|
|
||||||
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
|
||||||
|
|
||||||
bail!("Shutdown signal received");
|
|
||||||
},
|
|
||||||
result = async {
|
|
||||||
let (event_loop, mut event_loop_handle) =
|
|
||||||
EventLoop::new(swap_id, swarm, seller_peer_id, context.db.clone())?;
|
|
||||||
let event_loop = tokio::spawn(event_loop.run().in_current_span());
|
|
||||||
|
|
||||||
let bid_quote = event_loop_handle.request_quote().await?;
|
|
||||||
|
|
||||||
Ok::<_, anyhow::Error>((event_loop, event_loop_handle, bid_quote))
|
|
||||||
} => {
|
|
||||||
result
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let (event_loop, event_loop_handle, bid_quote) = match initialize_swap {
|
|
||||||
Ok(result) => result,
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(%swap_id, "Swap initialization failed: {:#}", error);
|
|
||||||
|
|
||||||
context
|
|
||||||
.swap_lock
|
|
||||||
.release_swap_lock()
|
|
||||||
.await
|
|
||||||
.expect("Could not release swap lock");
|
|
||||||
|
|
||||||
context
|
|
||||||
.tauri_handle
|
|
||||||
.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
|
||||||
|
|
||||||
bail!(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
context
|
|
||||||
.tauri_handle
|
|
||||||
.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::ReceivedQuote(bid_quote));
|
|
||||||
|
|
||||||
context.tasks.clone().spawn(async move {
|
context.tasks.clone().spawn(async move {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
|
@ -730,6 +803,7 @@ pub async fn buy_xmr(
|
||||||
|
|
||||||
bail!("Shutdown signal received");
|
bail!("Shutdown signal received");
|
||||||
},
|
},
|
||||||
|
|
||||||
event_loop_result = event_loop => {
|
event_loop_result = event_loop => {
|
||||||
match event_loop_result {
|
match event_loop_result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
|
@ -741,36 +815,6 @@ pub async fn buy_xmr(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
swap_result = async {
|
swap_result = async {
|
||||||
let max_givable = || async {
|
|
||||||
let (amount, fee) = bitcoin_wallet.max_giveable(TxLock::script_size()).await?;
|
|
||||||
Ok((amount, fee))
|
|
||||||
};
|
|
||||||
|
|
||||||
let determine_amount = determine_btc_to_swap(
|
|
||||||
context.config.json,
|
|
||||||
bid_quote,
|
|
||||||
bitcoin_wallet.new_address(),
|
|
||||||
|| bitcoin_wallet.balance(),
|
|
||||||
max_givable,
|
|
||||||
|| bitcoin_wallet.sync(),
|
|
||||||
context.tauri_handle.clone(),
|
|
||||||
Some(swap_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
let (tx_lock_amount, tx_lock_fee) = match determine_amount.await {
|
|
||||||
Ok(val) => val,
|
|
||||||
Err(error) => match error.downcast::<ZeroQuoteReceived>() {
|
|
||||||
Ok(_) => {
|
|
||||||
bail!("Seller's XMR balance is currently too low to initiate a swap, please try again later")
|
|
||||||
}
|
|
||||||
Err(other) => bail!(other),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::info!(%tx_lock_amount, %tx_lock_fee, "Determined swap amount");
|
|
||||||
|
|
||||||
context.db.insert_peer_id(swap_id, seller_peer_id).await?;
|
|
||||||
|
|
||||||
let swap = Swap::new(
|
let swap = Swap::new(
|
||||||
Arc::clone(&context.db),
|
Arc::clone(&context.db),
|
||||||
swap_id,
|
swap_id,
|
||||||
|
|
@ -779,7 +823,7 @@ pub async fn buy_xmr(
|
||||||
env_config,
|
env_config,
|
||||||
event_loop_handle,
|
event_loop_handle,
|
||||||
monero_receive_pool.clone(),
|
monero_receive_pool.clone(),
|
||||||
bitcoin_change_address,
|
bitcoin_change_address_for_spawn,
|
||||||
tx_lock_amount,
|
tx_lock_amount,
|
||||||
tx_lock_fee
|
tx_lock_fee
|
||||||
).with_event_emitter(context.tauri_handle.clone());
|
).with_event_emitter(context.tauri_handle.clone());
|
||||||
|
|
@ -809,10 +853,7 @@ pub async fn buy_xmr(
|
||||||
Ok::<_, anyhow::Error>(())
|
Ok::<_, anyhow::Error>(())
|
||||||
}.in_current_span()).await;
|
}.in_current_span()).await;
|
||||||
|
|
||||||
Ok(BuyXmrResponse {
|
Ok(BuyXmrResponse { swap_id, quote })
|
||||||
swap_id,
|
|
||||||
quote: bid_quote,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(fields(method = "resume_swap"), skip(context))]
|
#[tracing::instrument(fields(method = "resume_swap"), skip(context))]
|
||||||
|
|
@ -1202,10 +1243,9 @@ pub async fn monero_recovery(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(fields(method = "get_current_swap"), skip(context))]
|
#[tracing::instrument(fields(method = "get_current_swap"), skip(context))]
|
||||||
pub async fn get_current_swap(context: Arc<Context>) -> Result<serde_json::Value> {
|
pub async fn get_current_swap(context: Arc<Context>) -> Result<GetCurrentSwapResponse> {
|
||||||
Ok(json!({
|
let swap_id = context.swap_lock.get_current_swap_id().await;
|
||||||
"swap_id": context.swap_lock.get_current_swap_id().await,
|
Ok(GetCurrentSwapResponse { swap_id })
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve_approval_request(
|
pub async fn resolve_approval_request(
|
||||||
|
|
@ -1225,133 +1265,270 @@ pub async fn resolve_approval_request(
|
||||||
Ok(ResolveApprovalResponse { success: true })
|
Ok(ResolveApprovalResponse { success: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn qr_code(value: &impl ToString) -> Result<String> {
|
pub async fn fetch_quotes_task(
|
||||||
let code = QrCode::new(value.to_string())?;
|
rendezvous_points: Vec<Multiaddr>,
|
||||||
let qr_code = code
|
namespace: XmrBtcNamespace,
|
||||||
.render::<unicode::Dense1x2>()
|
sellers: Vec<Multiaddr>,
|
||||||
.dark_color(unicode::Dense1x2::Light)
|
identity: identity::Keypair,
|
||||||
.light_color(unicode::Dense1x2::Dark)
|
db: Option<Arc<dyn Database + Send + Sync>>,
|
||||||
.build();
|
tor_client: Option<Arc<TorClient<TokioRustlsRuntime>>>,
|
||||||
Ok(qr_code)
|
tauri_handle: Option<TauriHandle>,
|
||||||
|
) -> Result<(
|
||||||
|
tokio::task::JoinHandle<()>,
|
||||||
|
::tokio::sync::watch::Receiver<Vec<SellerStatus>>,
|
||||||
|
)> {
|
||||||
|
let (tx, rx) = ::tokio::sync::watch::channel(Vec::new());
|
||||||
|
|
||||||
|
let rendezvous_nodes: Vec<_> = rendezvous_points
|
||||||
|
.iter()
|
||||||
|
.filter_map(|addr| addr.split_peer_id())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let fetch_fn = list_sellers_init(
|
||||||
|
rendezvous_nodes,
|
||||||
|
namespace,
|
||||||
|
tor_client,
|
||||||
|
identity,
|
||||||
|
db,
|
||||||
|
tauri_handle,
|
||||||
|
Some(tx.clone()),
|
||||||
|
sellers,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let handle = tokio::task::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let sellers = fetch_fn().await;
|
||||||
|
let _ = tx.send(sellers);
|
||||||
|
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(90)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok((handle, rx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Let this take a refresh interval as an argument
|
||||||
|
pub async fn refresh_wallet_task<FMG, TMG, FB, TB, FS, TS>(
|
||||||
|
max_giveable_fn: FMG,
|
||||||
|
balance_fn: FB,
|
||||||
|
sync_fn: FS,
|
||||||
|
) -> Result<(
|
||||||
|
tokio::task::JoinHandle<()>,
|
||||||
|
::tokio::sync::watch::Receiver<(bitcoin::Amount, bitcoin::Amount)>,
|
||||||
|
)>
|
||||||
|
where
|
||||||
|
TMG: Future<Output = Result<(bitcoin::Amount, bitcoin::Amount)>> + Send + 'static,
|
||||||
|
FMG: Fn() -> TMG + Send + 'static,
|
||||||
|
TB: Future<Output = Result<bitcoin::Amount>> + Send + 'static,
|
||||||
|
FB: Fn() -> TB + Send + 'static,
|
||||||
|
TS: Future<Output = Result<()>> + Send + 'static,
|
||||||
|
FS: Fn() -> TS + Send + 'static,
|
||||||
|
{
|
||||||
|
let (tx, rx) = ::tokio::sync::watch::channel((bitcoin::Amount::ZERO, bitcoin::Amount::ZERO));
|
||||||
|
|
||||||
|
let handle = tokio::task::spawn(async move {
|
||||||
|
loop {
|
||||||
|
// Sync wallet before checking balance
|
||||||
|
let _ = sync_fn().await;
|
||||||
|
|
||||||
|
if let (Ok(balance), Ok((max_giveable, _fee))) =
|
||||||
|
(balance_fn().await, max_giveable_fn().await)
|
||||||
|
{
|
||||||
|
let _ = tx.send((balance, max_giveable));
|
||||||
|
}
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok((handle, rx))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn determine_btc_to_swap<FB, TB, FMG, TMG, FS, TS>(
|
pub async fn determine_btc_to_swap<FB, TB, FMG, TMG, FS, TS, FQ>(
|
||||||
json: bool,
|
quote_fetch_tasks: FQ,
|
||||||
bid_quote: BidQuote,
|
// TODO: Shouldn't this be a function?
|
||||||
get_new_address: impl Future<Output = Result<bitcoin::Address>>,
|
get_new_address: impl Future<Output = Result<bitcoin::Address>>,
|
||||||
balance: FB,
|
balance: FB,
|
||||||
max_giveable_fn: FMG,
|
max_giveable_fn: FMG,
|
||||||
sync: FS,
|
sync: FS,
|
||||||
event_emitter: Option<TauriHandle>,
|
event_emitter: Option<TauriHandle>,
|
||||||
swap_id: Option<Uuid>,
|
swap_id: Uuid,
|
||||||
) -> Result<(bitcoin::Amount, bitcoin::Amount)>
|
request_approval: impl Fn(QuoteWithAddress) -> Box<dyn Future<Output = Result<bool>> + Send>,
|
||||||
|
) -> Result<(
|
||||||
|
Multiaddr,
|
||||||
|
PeerId,
|
||||||
|
BidQuote,
|
||||||
|
bitcoin::Amount,
|
||||||
|
bitcoin::Amount,
|
||||||
|
)>
|
||||||
where
|
where
|
||||||
TB: Future<Output = Result<bitcoin::Amount>>,
|
TB: Future<Output = Result<bitcoin::Amount>> + Send + 'static,
|
||||||
FB: Fn() -> TB,
|
FB: Fn() -> TB + Send + 'static,
|
||||||
TMG: Future<Output = Result<(bitcoin::Amount, bitcoin::Amount)>>,
|
TMG: Future<Output = Result<(bitcoin::Amount, bitcoin::Amount)>> + Send + 'static,
|
||||||
FMG: Fn() -> TMG,
|
FMG: Fn() -> TMG + Send + 'static,
|
||||||
TS: Future<Output = Result<()>>,
|
TS: Future<Output = Result<()>> + Send + 'static,
|
||||||
FS: Fn() -> TS,
|
FS: Fn() -> TS + Send + 'static,
|
||||||
|
FQ: Fn() -> std::pin::Pin<
|
||||||
|
Box<
|
||||||
|
dyn Future<
|
||||||
|
Output = Result<(
|
||||||
|
tokio::task::JoinHandle<()>,
|
||||||
|
::tokio::sync::watch::Receiver<Vec<SellerStatus>>,
|
||||||
|
)>,
|
||||||
|
> + Send,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
{
|
{
|
||||||
if bid_quote.max_quantity == bitcoin::Amount::ZERO {
|
// Start background tasks with watch channels
|
||||||
bail!(ZeroQuoteReceived)
|
let (quote_fetch_handle, mut quotes_rx): (
|
||||||
}
|
_,
|
||||||
|
::tokio::sync::watch::Receiver<Vec<SellerStatus>>,
|
||||||
|
) = quote_fetch_tasks().await?;
|
||||||
|
let (wallet_refresh_handle, mut balance_rx): (
|
||||||
|
_,
|
||||||
|
::tokio::sync::watch::Receiver<(bitcoin::Amount, bitcoin::Amount)>,
|
||||||
|
) = refresh_wallet_task(max_giveable_fn, balance, sync).await?;
|
||||||
|
|
||||||
tracing::info!(
|
// Get the abort handles to kill the background tasks when we exit the function
|
||||||
price = %bid_quote.price,
|
let quote_fetch_abort_handle = AbortOnDropHandle::new(quote_fetch_handle);
|
||||||
minimum_amount = %bid_quote.min_quantity,
|
let wallet_refresh_abort_handle = AbortOnDropHandle::new(wallet_refresh_handle);
|
||||||
maximum_amount = %bid_quote.max_quantity,
|
|
||||||
"Received quote",
|
|
||||||
);
|
|
||||||
|
|
||||||
sync().await.context("Failed to sync of Bitcoin wallet")?;
|
let mut pending_approvals = FuturesUnordered::new();
|
||||||
let (mut max_giveable, mut spending_fee) = max_giveable_fn().await?;
|
|
||||||
|
|
||||||
if max_giveable == bitcoin::Amount::ZERO || max_giveable < bid_quote.min_quantity {
|
let deposit_address = get_new_address.await?;
|
||||||
let deposit_address = get_new_address.await?;
|
|
||||||
let minimum_amount = bid_quote.min_quantity;
|
|
||||||
let maximum_amount = bid_quote.max_quantity;
|
|
||||||
|
|
||||||
// To avoid any issus, we clip maximum_amount to never go above the
|
loop {
|
||||||
// total maximim Bitcoin supply
|
// Get the latest quotes, balance and max_giveable
|
||||||
let maximum_amount = maximum_amount.min(bitcoin::Amount::MAX_MONEY);
|
let quotes = quotes_rx.borrow().clone();
|
||||||
|
let (balance, max_giveable) = *balance_rx.borrow();
|
||||||
|
|
||||||
if !json {
|
let success_quotes = quotes
|
||||||
eprintln!("{}", qr_code(&deposit_address)?);
|
.iter()
|
||||||
}
|
.filter_map(|quote| match quote {
|
||||||
|
SellerStatus::Online(quote_with_address) => Some(quote_with_address.clone()),
|
||||||
|
SellerStatus::Unreachable(_) => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
loop {
|
// Emit a Tauri event
|
||||||
let min_outstanding = bid_quote.min_quantity - max_giveable;
|
event_emitter.emit_swap_progress_event(
|
||||||
let min_bitcoin_lock_tx_fee = spending_fee;
|
swap_id,
|
||||||
let min_deposit_until_swap_will_start = min_outstanding + min_bitcoin_lock_tx_fee;
|
TauriSwapProgressEvent::WaitingForBtcDeposit {
|
||||||
let max_deposit_until_maximum_amount_is_reached = maximum_amount
|
deposit_address: deposit_address.clone(),
|
||||||
.checked_sub(max_giveable)
|
max_giveable: max_giveable,
|
||||||
.context("Overflow when subtracting max_giveable from maximum_amount")?
|
min_bitcoin_lock_tx_fee: balance - max_giveable,
|
||||||
.checked_add(min_bitcoin_lock_tx_fee)
|
known_quotes: success_quotes.clone(),
|
||||||
.context(format!("Overflow when adding min_bitcoin_lock_tx_fee ({min_bitcoin_lock_tx_fee}) to max_giveable ({max_giveable}) with maximum_amount ({maximum_amount})"))?;
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tracing::info!(
|
// Iterate through quotes and find ones that match the balance and max_giveable
|
||||||
"Deposit at least {} to cover the min quantity with fee!",
|
let matching_quotes = success_quotes
|
||||||
min_deposit_until_swap_will_start
|
.iter()
|
||||||
);
|
.filter_map(|quote_with_address| {
|
||||||
tracing::info!(
|
let quote = quote_with_address.quote;
|
||||||
%deposit_address,
|
|
||||||
%min_deposit_until_swap_will_start,
|
|
||||||
%max_deposit_until_maximum_amount_is_reached,
|
|
||||||
%max_giveable,
|
|
||||||
%minimum_amount,
|
|
||||||
%maximum_amount,
|
|
||||||
%min_bitcoin_lock_tx_fee,
|
|
||||||
price = %bid_quote.price,
|
|
||||||
"Waiting for Bitcoin deposit",
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(swap_id) = swap_id {
|
if quote.min_quantity <= max_giveable && quote.max_quantity > bitcoin::Amount::ZERO
|
||||||
event_emitter.emit_swap_progress_event(
|
{
|
||||||
swap_id,
|
let tx_lock_fee = balance - max_giveable;
|
||||||
TauriSwapProgressEvent::WaitingForBtcDeposit {
|
let tx_lock_amount = std::cmp::min(max_giveable, quote.max_quantity);
|
||||||
deposit_address: deposit_address.clone(),
|
|
||||||
max_giveable,
|
|
||||||
min_deposit_until_swap_will_start,
|
|
||||||
max_deposit_until_maximum_amount_is_reached,
|
|
||||||
min_bitcoin_lock_tx_fee,
|
|
||||||
quote: bid_quote,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
(max_giveable, spending_fee) = loop {
|
Some((quote_with_address.clone(), tx_lock_amount, tx_lock_fee))
|
||||||
sync()
|
} else {
|
||||||
.await
|
None
|
||||||
.context("Failed to sync Bitcoin wallet while waiting for deposit")?;
|
|
||||||
let (new_max_givable, new_fee) = max_giveable_fn().await?;
|
|
||||||
|
|
||||||
if new_max_givable > max_giveable {
|
|
||||||
break (new_max_givable, new_fee);
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
// Put approval requests into FuturesUnordered
|
||||||
};
|
for (quote, tx_lock_amount, tx_lock_fee) in matching_quotes {
|
||||||
|
let future = request_approval(quote.clone());
|
||||||
|
|
||||||
let new_balance = balance().await?;
|
pending_approvals.push(async move {
|
||||||
tracing::info!(%new_balance, %max_giveable, "Received Bitcoin");
|
use std::pin::Pin;
|
||||||
|
let pinned_future = Pin::from(future);
|
||||||
|
let approved = pinned_future.await?;
|
||||||
|
|
||||||
if max_giveable < bid_quote.min_quantity {
|
if approved {
|
||||||
tracing::info!("Deposited amount is not enough to cover `min_quantity` when accounting for network fees");
|
Ok::<
|
||||||
continue;
|
Option<(
|
||||||
}
|
Multiaddr,
|
||||||
|
PeerId,
|
||||||
break;
|
BidQuote,
|
||||||
|
bitcoin::Amount,
|
||||||
|
bitcoin::Amount,
|
||||||
|
)>,
|
||||||
|
anyhow::Error,
|
||||||
|
>(Some((
|
||||||
|
quote.multiaddr.clone(),
|
||||||
|
quote.peer_id.clone(),
|
||||||
|
quote.quote.clone(),
|
||||||
|
tx_lock_amount,
|
||||||
|
tx_lock_fee,
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok::<
|
||||||
|
Option<(
|
||||||
|
Multiaddr,
|
||||||
|
PeerId,
|
||||||
|
BidQuote,
|
||||||
|
bitcoin::Amount,
|
||||||
|
bitcoin::Amount,
|
||||||
|
)>,
|
||||||
|
anyhow::Error,
|
||||||
|
>(None)
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let balance = balance().await?;
|
tracing::info!(
|
||||||
let fees = balance - max_giveable;
|
swap_id = ?swap_id,
|
||||||
let max_accepted = bid_quote.max_quantity;
|
pending_approvals = ?pending_approvals.len(),
|
||||||
let btc_swap_amount = min(max_giveable, max_accepted);
|
balance = ?balance,
|
||||||
|
max_giveable = ?max_giveable,
|
||||||
|
quotes = ?quotes,
|
||||||
|
"Waiting for user to select an offer"
|
||||||
|
);
|
||||||
|
|
||||||
Ok((btc_swap_amount, fees))
|
// Listen for approvals, balance changes, or quote changes
|
||||||
|
let result: Option<(
|
||||||
|
Multiaddr,
|
||||||
|
PeerId,
|
||||||
|
BidQuote,
|
||||||
|
bitcoin::Amount,
|
||||||
|
bitcoin::Amount,
|
||||||
|
)> = tokio::select! {
|
||||||
|
// Any approval request completes
|
||||||
|
approval_result = pending_approvals.next(), if !pending_approvals.is_empty() => {
|
||||||
|
match approval_result {
|
||||||
|
Some(Ok(Some(result))) => Some(result),
|
||||||
|
Some(Ok(None)) => None, // User rejected
|
||||||
|
Some(Err(_)) => None, // Error in approval
|
||||||
|
None => None, // No more futures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Balance changed - drop all pending approval requests and and re-calculate
|
||||||
|
_ = balance_rx.changed() => {
|
||||||
|
pending_approvals.clear();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
// Quotes changed - drop all pending approval requests and re-calculate
|
||||||
|
_ = quotes_rx.changed() => {
|
||||||
|
pending_approvals.clear();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If user accepted an offer, return it to start the swap
|
||||||
|
if let Some((multiaddr, peer_id, quote, tx_lock_amount, tx_lock_fee)) = result {
|
||||||
|
quote_fetch_abort_handle.abort();
|
||||||
|
wallet_refresh_abort_handle.abort();
|
||||||
|
|
||||||
|
return Ok((multiaddr, peer_id, quote, tx_lock_amount, tx_lock_fee));
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use super::request::BalanceResponse;
|
use super::request::BalanceResponse;
|
||||||
use crate::bitcoin;
|
use crate::bitcoin;
|
||||||
|
use crate::cli::list_sellers::QuoteWithAddress;
|
||||||
use crate::monero::MoneroAddressPool;
|
use crate::monero::MoneroAddressPool;
|
||||||
use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote};
|
use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote};
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
|
|
@ -9,12 +10,11 @@ use monero_rpc_pool::pool::PoolStatus;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::future::Future;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
use tokio::sync::{oneshot, Mutex as TokioMutex};
|
use tokio::sync::oneshot;
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
@ -51,6 +51,17 @@ pub struct LockBitcoinDetails {
|
||||||
pub swap_id: Uuid,
|
pub swap_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SelectMakerDetails {
|
||||||
|
#[typeshare(serialized_as = "string")]
|
||||||
|
pub swap_id: Uuid,
|
||||||
|
#[typeshare(serialized_as = "number")]
|
||||||
|
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||||
|
pub btc_amount_to_swap: bitcoin::Amount,
|
||||||
|
pub maker: QuoteWithAddress,
|
||||||
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type", content = "content")]
|
#[serde(tag = "type", content = "content")]
|
||||||
|
|
@ -75,6 +86,9 @@ pub enum ApprovalRequestType {
|
||||||
/// Request approval before locking Bitcoin.
|
/// Request approval before locking Bitcoin.
|
||||||
/// Contains specific details for review.
|
/// Contains specific details for review.
|
||||||
LockBitcoin(LockBitcoinDetails),
|
LockBitcoin(LockBitcoinDetails),
|
||||||
|
/// Request approval for maker selection.
|
||||||
|
/// Contains available makers and swap details.
|
||||||
|
SelectMaker(SelectMakerDetails),
|
||||||
/// Request seed selection from user.
|
/// Request seed selection from user.
|
||||||
/// User can choose between random seed or provide their own.
|
/// User can choose between random seed or provide their own.
|
||||||
SeedSelection,
|
SeedSelection,
|
||||||
|
|
@ -101,6 +115,15 @@ struct PendingApproval {
|
||||||
expiration_ts: u64,
|
expiration_ts: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for PendingApproval {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(responder) = self.responder.take() {
|
||||||
|
tracing::debug!("Dropping pending approval because handle was dropped");
|
||||||
|
let _ = responder.send(serde_json::Value::Bool(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct TorBootstrapStatus {
|
pub struct TorBootstrapStatus {
|
||||||
|
|
@ -112,7 +135,7 @@ pub struct TorBootstrapStatus {
|
||||||
#[cfg(feature = "tauri")]
|
#[cfg(feature = "tauri")]
|
||||||
struct TauriHandleInner {
|
struct TauriHandleInner {
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
pending_approvals: TokioMutex<HashMap<Uuid, PendingApproval>>,
|
pending_approvals: Arc<Mutex<HashMap<Uuid, PendingApproval>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -131,7 +154,7 @@ impl TauriHandle {
|
||||||
#[cfg(feature = "tauri")]
|
#[cfg(feature = "tauri")]
|
||||||
Arc::new(TauriHandleInner {
|
Arc::new(TauriHandleInner {
|
||||||
app_handle: tauri_handle,
|
app_handle: tauri_handle,
|
||||||
pending_approvals: TokioMutex::new(HashMap::new()),
|
pending_approvals: Arc::new(Mutex::new(HashMap::new())),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -149,6 +172,7 @@ impl TauriHandle {
|
||||||
|
|
||||||
/// Helper to emit a approval event via the unified event name
|
/// Helper to emit a approval event via the unified event name
|
||||||
fn emit_approval(&self, event: ApprovalRequest) {
|
fn emit_approval(&self, event: ApprovalRequest) {
|
||||||
|
tracing::debug!(?event, "Emitting approval event");
|
||||||
self.emit_unified_event(TauriEvent::Approval(event))
|
self.emit_unified_event(TauriEvent::Approval(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,7 +199,7 @@ impl TauriHandle {
|
||||||
let timeout_secs = timeout_secs.unwrap_or(60 * 60 * 24 * 7);
|
let timeout_secs = timeout_secs.unwrap_or(60 * 60 * 24 * 7);
|
||||||
let expiration_ts = SystemTime::now()
|
let expiration_ts = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.map_err(|e| anyhow!("Failed to get current time: {}", e))?
|
||||||
.as_secs()
|
.as_secs()
|
||||||
+ timeout_secs;
|
+ timeout_secs;
|
||||||
let request = ApprovalRequest {
|
let request = ApprovalRequest {
|
||||||
|
|
@ -188,7 +212,6 @@ impl TauriHandle {
|
||||||
self.emit_approval(request.clone());
|
self.emit_approval(request.clone());
|
||||||
|
|
||||||
tracing::debug!(%request, "Emitted approval request event");
|
tracing::debug!(%request, "Emitted approval request event");
|
||||||
|
|
||||||
// Construct the data structure we use to internally track the approval request
|
// Construct the data structure we use to internally track the approval request
|
||||||
let (responder, receiver) = oneshot::channel();
|
let (responder, receiver) = oneshot::channel();
|
||||||
|
|
||||||
|
|
@ -198,17 +221,28 @@ impl TauriHandle {
|
||||||
responder: Some(responder),
|
responder: Some(responder),
|
||||||
expiration_ts: SystemTime::now()
|
expiration_ts: SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.map_err(|e| anyhow!("Failed to get current time: {}", e))?
|
||||||
.as_secs()
|
.as_secs()
|
||||||
+ timeout_secs,
|
+ timeout_secs,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lock map and insert the pending approval
|
// Lock map and insert the pending approval
|
||||||
{
|
{
|
||||||
let mut pending_map = self.0.pending_approvals.lock().await;
|
let mut pending_map = self
|
||||||
pending_map.insert(request.request_id, pending);
|
.0
|
||||||
|
.pending_approvals
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
|
||||||
|
pending_map.insert(request_id, pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create cleanup guard to handle cancellation
|
||||||
|
let mut cleanup_guard = ApprovalCleanupGuard::new(
|
||||||
|
request_id,
|
||||||
|
self.clone(),
|
||||||
|
self.0.pending_approvals.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
// Determine if the request will be accepted or rejected
|
// Determine if the request will be accepted or rejected
|
||||||
// Either by being resolved by the user, or by timing out
|
// Either by being resolved by the user, or by timing out
|
||||||
let unparsed_response = tokio::select! {
|
let unparsed_response = tokio::select! {
|
||||||
|
|
@ -223,14 +257,18 @@ impl TauriHandle {
|
||||||
let response: Result<Response> = serde_json::from_value(unparsed_response.clone())
|
let response: Result<Response> = serde_json::from_value(unparsed_response.clone())
|
||||||
.context("Failed to parse approval response to expected type");
|
.context("Failed to parse approval response to expected type");
|
||||||
|
|
||||||
let mut map = self.0.pending_approvals.lock().await;
|
let mut map = self
|
||||||
if let Some(_pending) = map.remove(&request.request_id) {
|
.0
|
||||||
|
.pending_approvals
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
|
||||||
|
if let Some(_pending) = map.remove(&request_id) {
|
||||||
let status = if response.is_ok() {
|
let status = if response.is_ok() {
|
||||||
RequestStatus::Resolved {
|
RequestStatus::Resolved {
|
||||||
approve_input: unparsed_response,
|
approve_input: unparsed_response,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RequestStatus::Rejected {}
|
RequestStatus::Rejected
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut approval = request.clone();
|
let mut approval = request.clone();
|
||||||
|
|
@ -260,15 +298,19 @@ impl TauriHandle {
|
||||||
|
|
||||||
#[cfg(feature = "tauri")]
|
#[cfg(feature = "tauri")]
|
||||||
{
|
{
|
||||||
let mut pending_map = self.0.pending_approvals.lock().await;
|
let mut pending_map = self
|
||||||
if let Some(pending) = pending_map.get_mut(&request_id) {
|
.0
|
||||||
let _ = pending
|
.pending_approvals
|
||||||
.responder
|
.lock()
|
||||||
.take()
|
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
|
||||||
.context("Approval responder was already consumed")?
|
if let Some(mut pending) = pending_map.remove(&request_id) {
|
||||||
.send(response);
|
// Send response through oneshot channel
|
||||||
|
if let Some(responder) = pending.responder.take() {
|
||||||
Ok(())
|
let _ = responder.send(response);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Approval responder was already consumed"))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("Approval not found or already handled"))
|
Err(anyhow!("Approval not found or already handled"))
|
||||||
}
|
}
|
||||||
|
|
@ -280,6 +322,7 @@ impl Display for ApprovalRequest {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self.request {
|
match self.request {
|
||||||
ApprovalRequestType::LockBitcoin(..) => write!(f, "LockBitcoin()"),
|
ApprovalRequestType::LockBitcoin(..) => write!(f, "LockBitcoin()"),
|
||||||
|
ApprovalRequestType::SelectMaker(..) => write!(f, "SelectMaker()"),
|
||||||
ApprovalRequestType::SeedSelection => write!(f, "SeedSelection()"),
|
ApprovalRequestType::SeedSelection => write!(f, "SeedSelection()"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -293,6 +336,12 @@ pub trait TauriEmitter {
|
||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
) -> Result<bool>;
|
) -> Result<bool>;
|
||||||
|
|
||||||
|
async fn request_maker_selection(
|
||||||
|
&self,
|
||||||
|
details: SelectMakerDetails,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<bool>;
|
||||||
|
|
||||||
async fn request_seed_selection(&self) -> Result<SeedChoice>;
|
async fn request_seed_selection(&self) -> Result<SeedChoice>;
|
||||||
|
|
||||||
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()>;
|
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()>;
|
||||||
|
|
@ -375,6 +424,20 @@ impl TauriEmitter for TauriHandle {
|
||||||
.unwrap_or(false))
|
.unwrap_or(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn request_maker_selection(
|
||||||
|
&self,
|
||||||
|
details: SelectMakerDetails,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<bool> {
|
||||||
|
Ok(self
|
||||||
|
.request_approval(
|
||||||
|
ApprovalRequestType::SelectMaker(details),
|
||||||
|
Some(timeout_secs),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or(false))
|
||||||
|
}
|
||||||
|
|
||||||
async fn request_seed_selection(&self) -> Result<SeedChoice> {
|
async fn request_seed_selection(&self) -> Result<SeedChoice> {
|
||||||
self.request_approval(ApprovalRequestType::SeedSelection, None)
|
self.request_approval(ApprovalRequestType::SeedSelection, None)
|
||||||
.await
|
.await
|
||||||
|
|
@ -431,6 +494,17 @@ impl TauriEmitter for Option<TauriHandle> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn request_maker_selection(
|
||||||
|
&self,
|
||||||
|
details: SelectMakerDetails,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<bool> {
|
||||||
|
match self {
|
||||||
|
Some(tauri) => tauri.request_maker_selection(details, timeout_secs).await,
|
||||||
|
None => bail!("No Tauri handle available"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn request_seed_selection(&self) -> Result<SeedChoice> {
|
async fn request_seed_selection(&self) -> Result<SeedChoice> {
|
||||||
match self {
|
match self {
|
||||||
Some(tauri) => tauri.request_seed_selection().await,
|
Some(tauri) => tauri.request_seed_selection().await,
|
||||||
|
|
@ -648,14 +722,8 @@ pub enum TauriSwapProgressEvent {
|
||||||
max_giveable: bitcoin::Amount,
|
max_giveable: bitcoin::Amount,
|
||||||
#[typeshare(serialized_as = "number")]
|
#[typeshare(serialized_as = "number")]
|
||||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||||
min_deposit_until_swap_will_start: bitcoin::Amount,
|
|
||||||
#[typeshare(serialized_as = "number")]
|
|
||||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
|
||||||
max_deposit_until_maximum_amount_is_reached: bitcoin::Amount,
|
|
||||||
#[typeshare(serialized_as = "number")]
|
|
||||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
|
||||||
min_bitcoin_lock_tx_fee: bitcoin::Amount,
|
min_bitcoin_lock_tx_fee: bitcoin::Amount,
|
||||||
quote: BidQuote,
|
known_quotes: Vec<QuoteWithAddress>,
|
||||||
},
|
},
|
||||||
SwapSetupInflight {
|
SwapSetupInflight {
|
||||||
#[typeshare(serialized_as = "number")]
|
#[typeshare(serialized_as = "number")]
|
||||||
|
|
@ -795,3 +863,48 @@ pub struct ListSellersProgress {
|
||||||
pub quotes_received: u32,
|
pub quotes_received: u32,
|
||||||
pub quotes_failed: u32,
|
pub quotes_failed: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add this struct before the TauriHandle implementation
|
||||||
|
struct ApprovalCleanupGuard {
|
||||||
|
request_id: Option<Uuid>,
|
||||||
|
approval_store: Arc<Mutex<HashMap<Uuid, PendingApproval>>>,
|
||||||
|
handle: TauriHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApprovalCleanupGuard {
|
||||||
|
fn new(
|
||||||
|
request_id: Uuid,
|
||||||
|
handle: TauriHandle,
|
||||||
|
approval_store: Arc<Mutex<HashMap<Uuid, PendingApproval>>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
request_id: Some(request_id),
|
||||||
|
handle,
|
||||||
|
approval_store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disarm the guard so it won't cleanup on drop (call when normally resolved)
|
||||||
|
fn disarm(&mut self) {
|
||||||
|
self.request_id = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ApprovalCleanupGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(request_id) = self.request_id {
|
||||||
|
tracing::debug!(%request_id, "Approval handle dropped, we should cleanup now");
|
||||||
|
|
||||||
|
// Lock the Mutex
|
||||||
|
if let Ok(mut approval_store) = self.approval_store.lock() {
|
||||||
|
// Check if the request id still present in the map
|
||||||
|
if let Some(mut pending_approval) = approval_store.remove(&request_id) {
|
||||||
|
// If there is still someone listening, send a rejection
|
||||||
|
if let Some(responder) = pending_approval.responder.take() {
|
||||||
|
let _ = responder.send(serde_json::Value::Bool(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,8 @@ where
|
||||||
);
|
);
|
||||||
|
|
||||||
BuyXmrArgs {
|
BuyXmrArgs {
|
||||||
seller,
|
rendezvous_points: vec![],
|
||||||
|
sellers: vec![seller],
|
||||||
bitcoin_change_address,
|
bitcoin_change_address,
|
||||||
monero_receive_pool,
|
monero_receive_pool,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use crate::cli::api::tauri_bindings::{
|
||||||
ListSellersProgress, TauriBackgroundProgress, TauriBackgroundProgressHandle, TauriEmitter,
|
ListSellersProgress, TauriBackgroundProgress, TauriBackgroundProgressHandle, TauriEmitter,
|
||||||
TauriHandle,
|
TauriHandle,
|
||||||
};
|
};
|
||||||
|
use crate::libp2p_ext::MultiAddrExt;
|
||||||
use crate::network::quote::BidQuote;
|
use crate::network::quote::BidQuote;
|
||||||
use crate::network::rendezvous::XmrBtcNamespace;
|
use crate::network::rendezvous::XmrBtcNamespace;
|
||||||
use crate::network::{quote, swarm};
|
use crate::network::{quote, swarm};
|
||||||
|
|
@ -16,7 +17,7 @@ use libp2p::swarm::dial_opts::DialOpts;
|
||||||
use libp2p::swarm::{NetworkBehaviour, SwarmEvent};
|
use libp2p::swarm::{NetworkBehaviour, SwarmEvent};
|
||||||
use libp2p::{identity, ping, rendezvous, Multiaddr, PeerId, Swarm};
|
use libp2p::{identity, ping, rendezvous, Multiaddr, PeerId, Swarm};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, DisplayFromStr};
|
use serde_with::{serde_as, DisplayFromStr};
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -32,6 +33,102 @@ fn build_identify_config(identity: identity::Keypair) -> identify::Config {
|
||||||
identify::Config::new(protocol_version, identity.public()).with_agent_version(agent_version)
|
identify::Config::new(protocol_version, identity.public()).with_agent_version(agent_version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a function that when called will return sorted list of sellers, with [Online](Status::Online) listed first.
|
||||||
|
///
|
||||||
|
/// First uses the rendezvous node to discover peers in the given namespace,
|
||||||
|
/// then fetches a quote from each peer that was discovered. If fetching a quote
|
||||||
|
/// from a discovered peer fails the seller's status will be
|
||||||
|
/// [Unreachable](Status::Unreachable).
|
||||||
|
///
|
||||||
|
/// If a database is provided, it will be used to get the list of peers that
|
||||||
|
/// have already been discovered previously and attempt to fetch a quote from them.
|
||||||
|
pub async fn list_sellers_init(
|
||||||
|
rendezvous_points: Vec<(PeerId, Multiaddr)>,
|
||||||
|
namespace: XmrBtcNamespace,
|
||||||
|
maybe_tor_client: Option<Arc<TorClient<TokioRustlsRuntime>>>,
|
||||||
|
identity: identity::Keypair,
|
||||||
|
db: Option<Arc<dyn Database + Send + Sync>>,
|
||||||
|
tauri_handle: Option<TauriHandle>,
|
||||||
|
sender: Option<::tokio::sync::watch::Sender<Vec<SellerStatus>>>,
|
||||||
|
sellers: Vec<Multiaddr>,
|
||||||
|
) -> Result<
|
||||||
|
impl Fn() -> std::pin::Pin<
|
||||||
|
Box<dyn std::future::Future<Output = Vec<SellerStatus>> + Send + 'static>,
|
||||||
|
> + Send
|
||||||
|
+ Sync
|
||||||
|
+ 'static,
|
||||||
|
> {
|
||||||
|
// Capture variables needed to build an EventLoop on each invocation
|
||||||
|
let rendezvous_points_clone = rendezvous_points.clone();
|
||||||
|
let namespace_clone = namespace;
|
||||||
|
let maybe_tor_client_clone = maybe_tor_client.clone();
|
||||||
|
let identity_clone = identity.clone();
|
||||||
|
let db_clone = db.clone();
|
||||||
|
let tauri_handle_clone = tauri_handle.clone();
|
||||||
|
let sellers_clone = sellers.clone();
|
||||||
|
|
||||||
|
Ok(move || {
|
||||||
|
// Clone captured values inside the closure to avoid moving them and thus implement `Fn`
|
||||||
|
let rendezvous_points = rendezvous_points_clone.clone();
|
||||||
|
let namespace = namespace_clone;
|
||||||
|
let maybe_tor_client = maybe_tor_client_clone.clone();
|
||||||
|
let identity = identity_clone.clone();
|
||||||
|
let db = db_clone.clone();
|
||||||
|
let tauri_handle = tauri_handle_clone.clone();
|
||||||
|
let sender = sender.clone();
|
||||||
|
let sellers = sellers_clone.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
// Build a fresh swarm and event loop for every call so the closure can be invoked multiple times.
|
||||||
|
let behaviour = Behaviour {
|
||||||
|
rendezvous: rendezvous::client::Behaviour::new(identity.clone()),
|
||||||
|
quote: quote::cli(),
|
||||||
|
ping: ping::Behaviour::new(
|
||||||
|
ping::Config::new().with_timeout(Duration::from_secs(60)),
|
||||||
|
),
|
||||||
|
identify: identify::Behaviour::new(build_identify_config(identity.clone())),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Dont use unwrap
|
||||||
|
let swarm = swarm::cli(identity, maybe_tor_client, behaviour)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Get peers from the database, add them to the dial queue
|
||||||
|
let mut external_dial_queue = match db {
|
||||||
|
Some(db) => match db.get_all_peer_addresses().await {
|
||||||
|
Ok(peers) => VecDeque::from(peers),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(%err, "Failed to get peers from database for list_sellers");
|
||||||
|
VecDeque::new()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => VecDeque::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get peers the user has manually passed in, add them to the dial queue
|
||||||
|
for seller_addr in sellers {
|
||||||
|
if let Some((peer_id, multiaddr)) = seller_addr.split_peer_id() {
|
||||||
|
external_dial_queue.push_back((peer_id, vec![multiaddr]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let event_loop = EventLoop::new(
|
||||||
|
swarm,
|
||||||
|
rendezvous_points,
|
||||||
|
namespace,
|
||||||
|
external_dial_queue,
|
||||||
|
tauri_handle,
|
||||||
|
);
|
||||||
|
|
||||||
|
event_loop.run(sender).await
|
||||||
|
})
|
||||||
|
as std::pin::Pin<
|
||||||
|
Box<dyn std::future::Future<Output = Vec<SellerStatus>> + Send + 'static>,
|
||||||
|
>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns sorted list of sellers, with [Online](Status::Online) listed first.
|
/// Returns sorted list of sellers, with [Online](Status::Online) listed first.
|
||||||
///
|
///
|
||||||
/// First uses the rendezvous node to discover peers in the given namespace,
|
/// First uses the rendezvous node to discover peers in the given namespace,
|
||||||
|
|
@ -49,38 +146,23 @@ pub async fn list_sellers(
|
||||||
db: Option<Arc<dyn Database + Send + Sync>>,
|
db: Option<Arc<dyn Database + Send + Sync>>,
|
||||||
tauri_handle: Option<TauriHandle>,
|
tauri_handle: Option<TauriHandle>,
|
||||||
) -> Result<Vec<SellerStatus>> {
|
) -> Result<Vec<SellerStatus>> {
|
||||||
let behaviour = Behaviour {
|
let fetch_fn = list_sellers_init(
|
||||||
rendezvous: rendezvous::client::Behaviour::new(identity.clone()),
|
|
||||||
quote: quote::cli(),
|
|
||||||
ping: ping::Behaviour::new(ping::Config::new().with_timeout(Duration::from_secs(60))),
|
|
||||||
identify: identify::Behaviour::new(build_identify_config(identity.clone())),
|
|
||||||
};
|
|
||||||
let swarm = swarm::cli(identity, maybe_tor_client, behaviour).await?;
|
|
||||||
|
|
||||||
// If a database is passed in: Fetch all peer addresses from the database and fetch quotes from them
|
|
||||||
let external_dial_queue = match db {
|
|
||||||
Some(db) => {
|
|
||||||
let peers = db.get_all_peer_addresses().await?;
|
|
||||||
VecDeque::from(peers)
|
|
||||||
}
|
|
||||||
None => VecDeque::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let event_loop = EventLoop::new(
|
|
||||||
swarm,
|
|
||||||
rendezvous_points,
|
rendezvous_points,
|
||||||
namespace,
|
namespace,
|
||||||
external_dial_queue,
|
maybe_tor_client,
|
||||||
|
identity,
|
||||||
|
db,
|
||||||
tauri_handle,
|
tauri_handle,
|
||||||
);
|
None,
|
||||||
let sellers = event_loop.run().await;
|
Vec::new(),
|
||||||
|
)
|
||||||
Ok(sellers)
|
.await?;
|
||||||
|
Ok(fetch_fn().await)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Serialize, PartialEq, Eq, Hash, Clone, Ord, PartialOrd)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Ord, PartialOrd)]
|
||||||
pub struct QuoteWithAddress {
|
pub struct QuoteWithAddress {
|
||||||
/// The multiaddr of the seller (at which we were able to connect to and get the quote from)
|
/// The multiaddr of the seller (at which we were able to connect to and get the quote from)
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
|
|
@ -578,7 +660,49 @@ impl EventLoop {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(mut self) -> Vec<SellerStatus> {
|
fn build_current_sellers(&self) -> Vec<SellerStatus> {
|
||||||
|
let mut sellers: Vec<SellerStatus> = self
|
||||||
|
.peer_states
|
||||||
|
.values()
|
||||||
|
.filter_map(|peer_state| match peer_state {
|
||||||
|
PeerState::Complete {
|
||||||
|
peer_id,
|
||||||
|
version,
|
||||||
|
quote,
|
||||||
|
reachable_addresses,
|
||||||
|
} => Some(SellerStatus::Online(QuoteWithAddress {
|
||||||
|
peer_id: *peer_id,
|
||||||
|
multiaddr: reachable_addresses[0].clone(),
|
||||||
|
quote: *quote,
|
||||||
|
version: version.clone(),
|
||||||
|
})),
|
||||||
|
PeerState::Failed { peer_id, .. } => {
|
||||||
|
Some(SellerStatus::Unreachable(UnreachableSeller {
|
||||||
|
peer_id: *peer_id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
_ => None, // Skip pending states for partial updates
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
sellers.sort();
|
||||||
|
sellers
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_partial_update(
|
||||||
|
&self,
|
||||||
|
sender: &Option<::tokio::sync::watch::Sender<Vec<SellerStatus>>>,
|
||||||
|
) {
|
||||||
|
if let Some(sender) = sender {
|
||||||
|
let current_sellers = self.build_current_sellers();
|
||||||
|
let _ = sender.send(current_sellers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(
|
||||||
|
mut self,
|
||||||
|
sender: Option<::tokio::sync::watch::Sender<Vec<SellerStatus>>>,
|
||||||
|
) -> Vec<SellerStatus> {
|
||||||
// Dial all rendezvous points initially
|
// Dial all rendezvous points initially
|
||||||
for (peer_id, multiaddr) in &self.rendezvous_points {
|
for (peer_id, multiaddr) in &self.rendezvous_points {
|
||||||
let dial_opts = DialOpts::peer_id(*peer_id)
|
let dial_opts = DialOpts::peer_id(*peer_id)
|
||||||
|
|
@ -787,6 +911,8 @@ impl EventLoop {
|
||||||
|
|
||||||
// If we have pending request to rendezvous points or quote requests, we continue
|
// If we have pending request to rendezvous points or quote requests, we continue
|
||||||
if !all_rendezvous_points_requests_complete || !all_quotes_fetched {
|
if !all_rendezvous_points_requests_complete || !all_quotes_fetched {
|
||||||
|
// Emit partial update with any completed quotes we have so far
|
||||||
|
self.emit_partial_update(&sender);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue