mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-04-27 19:26:09 -04:00
feat(gui, tauri): Save settings in Tauri storage (#102)
- Implemented dual persistence strategy: - **User Settings**: Persisted across app restarts using `tauri-plugin-store`. - **Transient State**: Persisted across page reloads using `sessionStorage`. - Added `settingsSlice` reducer for managing persistent user settings. - Updated Redux store configuration to handle multiple persistence layers. - Added a new Settings page in the GUI where users can specify custom Electrum RPC URLs for Bitcoin and Monero node URLs. - Users can input their preferred Electrum server (`ssl://host:port`) and Monero daemon (`http://host:port`). - Input fields include validation to ensure correct URL formats. - Settings persist across application restarts using Tauri's storage plugin. - A reset option is available to revert to default settings. - Improved the Daemon Controller in the Help page: - Renamed `RpcControlBox` to `DaemonControlBox` for clarity. - Users can now start the daemon manually if it isn't running or has failed. - Added a "Restart GUI" button to apply new settings immediately. - Displayed the daemon's status within the controller. - Upgraded Tauri and related plugins to stable version `2.0.0`: - Updated `tauri`, `tauri-build`, and `tauri-utils` to `2.0.0`. - Ensured compatibility with the latest stable release. - Updated Tauri plugins to version `2.0.0`: - `tauri-plugin-clipboard-manager` - `tauri-plugin-shell` - Added new plugins: - `tauri-plugin-store` for settings persistence. - `tauri-plugin-process` to enable application relaunch. - Deferred Context initialization until explicitly triggered from the frontend. - Moved Context setup from the `setup` function to a new `initialize_context` Tauri command. - Allows the application to start without immediately initializing the backend context. - Context initialization now considers user-provided settings for Electrum and Monero nodes. - Introduced a `ValidatedTextField` component for form inputs with validation logic. - Provides immediate feedback on input validity. - Used in the Settings page for Electrum and Monero node URLs. - If the user provides an override Monero remote daemon, we check if it reachable and on the correct network before starting the `monero-wallet-rpc` - Changed `bitcoin_confirmation_target` type from `usize` to `u16`.
This commit is contained in:
parent
d4503a6e9c
commit
253e0b0cf6
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,10 +1,9 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
title: ""
|
||||
labels: ""
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
2
.github/workflows/build-release-binaries.yml
vendored
2
.github/workflows/build-release-binaries.yml
vendored
@ -176,4 +176,4 @@ jobs:
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.DOCKER_IMAGE_NAME }}:${{ github.event.release.tag_name }}
|
||||
if: steps.docker_tags.outputs.preview == 'true'
|
||||
if: steps.docker_tags.outputs.preview == 'true'
|
||||
|
575
Cargo.lock
generated
575
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -66,7 +66,7 @@ OPTIONS:
|
||||
--change-address <bitcoin-change-address> The bitcoin address where any form of change or excess funds should be sent to
|
||||
--receive-address <monero-receive-address> The monero address where you would like to receive monero
|
||||
--seller <seller> The seller's address. Must include a peer ID part, i.e. `/p2p/`
|
||||
|
||||
|
||||
--electrum-rpc <bitcoin-electrum-rpc-url> Provide the Bitcoin Electrum RPC URL
|
||||
--bitcoin-target-block <bitcoin-target-block> Estimate Bitcoin fees such that transactions are confirmed within the specified number of blocks
|
||||
--monero-daemon-address <monero-daemon-address> Specify to connect to a monero daemon of your choice: <host>:<port>
|
||||
|
10
node_modules/.yarn-integrity
generated
vendored
10
node_modules/.yarn-integrity
generated
vendored
@ -1,10 +0,0 @@
|
||||
{
|
||||
"systemParams": "win32-x64-127",
|
||||
"modulesFolders": [],
|
||||
"flags": [],
|
||||
"linkedModules": [],
|
||||
"topLevelPatterns": [],
|
||||
"lockfileEntries": {},
|
||||
"files": [],
|
||||
"artifacts": {}
|
||||
}
|
@ -21,7 +21,7 @@ yarn install && yarn run dev
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo tauri dev
|
||||
cargo tauri dev
|
||||
# let this run as well
|
||||
```
|
||||
|
||||
|
@ -5,9 +5,9 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
||||
/>
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@ -31,6 +31,7 @@
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
overscroll-behavior: none; /* Prevents the bounce effect */
|
||||
overscroll-behavior-y: contain; /* Prevents the bounce effect on the y-axis */
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
@ -19,9 +19,11 @@
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@reduxjs/toolkit": "^2.2.6",
|
||||
"@tauri-apps/api": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||
"@tauri-apps/plugin-process": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||
"@tauri-apps/plugin-store": "^2.0.0",
|
||||
"humanize-duration": "^3.32.1",
|
||||
"lodash": "^4.17.21",
|
||||
"multiaddr": "^10.0.1",
|
||||
@ -39,7 +41,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@tauri-apps/cli": ">=2.0.0-beta.0",
|
||||
"@tauri-apps/cli": "^2.0.0",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/humanize-duration": "^3.27.4",
|
||||
|
@ -1,44 +1,20 @@
|
||||
import { CircularProgress } from "@material-ui/core";
|
||||
import { Button, CircularProgress } from "@material-ui/core";
|
||||
import { Alert, AlertProps } from "@material-ui/lab";
|
||||
import { TauriContextInitializationProgress } from "models/tauriModel";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import { exhaustiveGuard } from "utils/typescriptUtils";
|
||||
|
||||
const FUNNY_INIT_MESSAGES = [
|
||||
"Initializing quantum entanglement...",
|
||||
"Generating one-time pads from cosmic background radiation...",
|
||||
"Negotiating key exchange with aliens...",
|
||||
"Optimizing elliptic curves for maximum sneakiness...",
|
||||
"Transforming plaintext into ciphertext via arcane XOR rituals...",
|
||||
"Salting your hash with exotic mathematical seasonings...",
|
||||
"Performing advanced modular arithmetic gymnastics...",
|
||||
"Consulting the Oracle of Randomness...",
|
||||
"Executing top-secret permutation protocols...",
|
||||
"Summoning prime factors from the mathematical aether...",
|
||||
"Deploying steganographic squirrels to hide your nuts of data...",
|
||||
"Initializing the quantum superposition of your keys...",
|
||||
"Applying post-quantum cryptographic voodoo...",
|
||||
"Encrypting your data with the tears of frustrated regulators...",
|
||||
];
|
||||
|
||||
function LoadingSpinnerAlert({ ...rest }: AlertProps) {
|
||||
return <Alert icon={<CircularProgress size={22} />} {...rest} />;
|
||||
}
|
||||
|
||||
export default function DaemonStatusAlert() {
|
||||
const contextStatus = useAppSelector((s) => s.rpc.status);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [initMessage] = useState(
|
||||
FUNNY_INIT_MESSAGES[Math.floor(Math.random() * FUNNY_INIT_MESSAGES.length)],
|
||||
);
|
||||
|
||||
if (contextStatus == null) {
|
||||
return (
|
||||
<LoadingSpinnerAlert severity="warning">
|
||||
{initMessage}
|
||||
</LoadingSpinnerAlert>
|
||||
);
|
||||
if (contextStatus === null) {
|
||||
return <Alert severity="info">The daemon is not running</Alert>;
|
||||
}
|
||||
|
||||
switch (contextStatus.type) {
|
||||
@ -68,7 +44,20 @@ export default function DaemonStatusAlert() {
|
||||
return <Alert severity="success">The daemon is running</Alert>;
|
||||
case "Failed":
|
||||
return (
|
||||
<Alert severity="error">The daemon has stopped unexpectedly</Alert>
|
||||
<Alert
|
||||
severity="error"
|
||||
action={
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => navigate("/help")}
|
||||
>
|
||||
View Logs
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
The daemon has stopped unexpectedly
|
||||
</Alert>
|
||||
);
|
||||
default:
|
||||
return exhaustiveGuard(contextStatus);
|
||||
|
@ -14,7 +14,7 @@ export default function FundsLeftInWalletAlert() {
|
||||
severity="info"
|
||||
action={
|
||||
<Button
|
||||
color="inherit"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => navigate("/wallet")}
|
||||
>
|
||||
|
@ -14,7 +14,7 @@ export default function UnfinishedSwapsAlert() {
|
||||
variant="filled"
|
||||
action={
|
||||
<Button
|
||||
color="inherit"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => navigate("/history")}
|
||||
>
|
||||
|
@ -53,7 +53,9 @@ export default function ListSellersDialog({
|
||||
}
|
||||
|
||||
function getMultiAddressError(): string | null {
|
||||
return isValidMultiAddressWithPeerId(rendezvousAddress) ? null : "Address is invalid or missing peer ID";
|
||||
return isValidMultiAddressWithPeerId(rendezvousAddress)
|
||||
? null
|
||||
: "Address is invalid or missing peer ID";
|
||||
}
|
||||
|
||||
function handleSuccess({ sellers }: ListSellersResponse) {
|
||||
|
@ -64,7 +64,7 @@ export default function SwapDialog({
|
||||
) : (
|
||||
<>
|
||||
<SwapStatePage state={swap.state} />
|
||||
<SwapStateStepper state={swap.state}/>
|
||||
<SwapStateStepper state={swap.state} />
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
@ -15,12 +15,12 @@ type PathStep = [type: PathType, step: number, isError: boolean];
|
||||
* @param latestState - The latest state of the swap process
|
||||
* @returns A tuple containing [PathType, activeStep, errorFlag]
|
||||
*/
|
||||
function getActiveStep(
|
||||
state: SwapState | null
|
||||
): PathStep {
|
||||
function getActiveStep(state: SwapState | null): PathStep {
|
||||
// In case we cannot infer a correct step from the state
|
||||
function fallbackStep(reason: string) {
|
||||
console.error(`Unable to choose correct stepper type (reason: ${reason}, state: ${JSON.stringify(state)}`);
|
||||
console.error(
|
||||
`Unable to choose correct stepper type (reason: ${reason}, state: ${JSON.stringify(state)}`,
|
||||
);
|
||||
return [PathType.HAPPY_PATH, 0, true] as PathStep;
|
||||
}
|
||||
|
||||
@ -36,12 +36,16 @@ function getActiveStep(
|
||||
|
||||
// If the swap is released but we do not have a previous state we fallback
|
||||
if (latestState === null) {
|
||||
return fallbackStep("Swap has been released but we do not have a previous state saved to display");
|
||||
return fallbackStep(
|
||||
"Swap has been released but we do not have a previous state saved to display",
|
||||
);
|
||||
}
|
||||
|
||||
// This should really never happen. For this statement to be true, the host has to submit a "Released" event twice
|
||||
if(latestState.type === "Released") {
|
||||
return fallbackStep("Both the current and previous states are both of type 'Released'.");
|
||||
if (latestState.type === "Released") {
|
||||
return fallbackStep(
|
||||
"Both the current and previous states are both of type 'Released'.",
|
||||
);
|
||||
}
|
||||
|
||||
switch (latestState.type) {
|
||||
@ -111,8 +115,8 @@ function getActiveStep(
|
||||
return [PathType.UNHAPPY_PATH, 1, true];
|
||||
default:
|
||||
return fallbackStep("No step is assigned to the current state");
|
||||
// TODO: Make this guard work. It should force the compiler to check if we have covered all possible cases.
|
||||
// return exhaustiveGuard(latestState.type);
|
||||
// TODO: Make this guard work. It should force the compiler to check if we have covered all possible cases.
|
||||
// return exhaustiveGuard(latestState.type);
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,7 +162,7 @@ const UNHAPPY_PATH_STEP_LABELS = [
|
||||
export default function SwapStateStepper({
|
||||
state,
|
||||
}: {
|
||||
state: SwapState | null
|
||||
state: SwapState | null;
|
||||
}) {
|
||||
const [pathType, activeStep, error] = getActiveStep(state);
|
||||
|
||||
|
@ -18,11 +18,7 @@ import InitiatedPage from "./init/InitiatedPage";
|
||||
import InitPage from "./init/InitPage";
|
||||
import WaitingForBitcoinDepositPage from "./init/WaitingForBitcoinDepositPage";
|
||||
|
||||
export default function SwapStatePage({
|
||||
state,
|
||||
}: {
|
||||
state: SwapState | null
|
||||
}) {
|
||||
export default function SwapStatePage({ state }: { state: SwapState | null }) {
|
||||
// TODO: Reimplement this using tauri events
|
||||
/*
|
||||
const isSyncingMoneroWallet = useAppSelector(
|
||||
|
55
src-gui/src/renderer/components/other/ValidatedTextField.tsx
Normal file
55
src-gui/src/renderer/components/other/ValidatedTextField.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { TextFieldProps, TextField } from "@material-ui/core";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
interface ValidatedTextFieldProps extends Omit<TextFieldProps, "onChange" | "value"> {
|
||||
value: string | null;
|
||||
isValid: (value: string) => boolean;
|
||||
onValidatedChange: (value: string | null) => void;
|
||||
allowEmpty?: boolean;
|
||||
helperText?: string;
|
||||
}
|
||||
|
||||
export default function ValidatedTextField({
|
||||
label,
|
||||
value = "",
|
||||
isValid,
|
||||
onValidatedChange,
|
||||
helperText = "Invalid input",
|
||||
variant = "standard",
|
||||
allowEmpty = false,
|
||||
...props
|
||||
}: ValidatedTextFieldProps) {
|
||||
const [inputValue, setInputValue] = useState(value || "");
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue: string) => {
|
||||
const trimmedValue = newValue.trim();
|
||||
setInputValue(trimmedValue);
|
||||
|
||||
if (trimmedValue === "" && allowEmpty) {
|
||||
onValidatedChange(null);
|
||||
} else if (isValid(trimmedValue)) {
|
||||
onValidatedChange(trimmedValue);
|
||||
}
|
||||
},
|
||||
[allowEmpty, isValid, onValidatedChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(value || "");
|
||||
}, [value]);
|
||||
|
||||
const isError = allowEmpty && inputValue === "" ? false : !isValid(inputValue);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label={label}
|
||||
value={inputValue}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
error={isError}
|
||||
helperText={isError ? helperText : ""}
|
||||
variant={variant}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
import { Box, makeStyles } from "@material-ui/core";
|
||||
import FolderOpenIcon from "@material-ui/icons/FolderOpen";
|
||||
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
||||
import StopIcon from "@material-ui/icons/Stop";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { useAppSelector, useIsContextAvailable } from "store/hooks";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import InfoBox from "../../modal/swap/InfoBox";
|
||||
import CliLogsBox from "../../other/RenderedCliLog";
|
||||
import { initializeContext } from "renderer/rpc";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import RotateLeftIcon from "@material-ui/icons/RotateLeft";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
actionsOuter: {
|
||||
@ -15,17 +17,26 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export default function RpcControlBox() {
|
||||
const isRunning = useIsContextAvailable();
|
||||
export default function DaemonControlBox() {
|
||||
const classes = useStyles();
|
||||
const logs = useAppSelector((s) => s.rpc.logs);
|
||||
|
||||
// The daemon can be manually started if it has failed or if it has not been started yet
|
||||
const canContextBeManuallyStarted = useAppSelector(
|
||||
(s) => s.rpc.status?.type === "Failed" || s.rpc.status === null,
|
||||
);
|
||||
const isContextInitializing = useAppSelector(
|
||||
(s) => s.rpc.status?.type === "Initializing",
|
||||
);
|
||||
|
||||
const stringifiedDaemonStatus = useAppSelector((s) => s.rpc.status?.type ?? "not started");
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
title={`Daemon Controller`}
|
||||
title={`Daemon Controller (${stringifiedDaemonStatus})`}
|
||||
mainContent={
|
||||
<CliLogsBox
|
||||
label="Swap Daemon Logs (current session only)"
|
||||
label="Logs (current session only)"
|
||||
logs={logs}
|
||||
/>
|
||||
}
|
||||
@ -34,22 +45,22 @@ export default function RpcControlBox() {
|
||||
<PromiseInvokeButton
|
||||
variant="contained"
|
||||
endIcon={<PlayArrowIcon />}
|
||||
disabled={isRunning}
|
||||
onInvoke={() => {
|
||||
throw new Error("Not implemented");
|
||||
}}
|
||||
onInvoke={initializeContext}
|
||||
requiresContext={false}
|
||||
disabled={!canContextBeManuallyStarted}
|
||||
isLoadingOverride={isContextInitializing}
|
||||
displayErrorSnackbar
|
||||
>
|
||||
Start Daemon
|
||||
</PromiseInvokeButton>
|
||||
<PromiseInvokeButton
|
||||
variant="contained"
|
||||
endIcon={<StopIcon />}
|
||||
disabled={!isRunning}
|
||||
onInvoke={() => {
|
||||
throw new Error("Not implemented");
|
||||
}}
|
||||
endIcon={<RotateLeftIcon />}
|
||||
onInvoke={relaunch}
|
||||
requiresContext={false}
|
||||
displayErrorSnackbar
|
||||
>
|
||||
Stop Daemon
|
||||
Restart GUI
|
||||
</PromiseInvokeButton>
|
||||
<PromiseInvokeButton
|
||||
endIcon={<FolderOpenIcon />}
|
||||
@ -57,6 +68,7 @@ export default function RpcControlBox() {
|
||||
size="small"
|
||||
tooltipTitle="Open the data directory of the Swap Daemon in your file explorer"
|
||||
onInvoke={() => {
|
||||
// TODO: Implement this
|
||||
throw new Error("Not implemented");
|
||||
}}
|
||||
/>
|
@ -2,14 +2,15 @@ import { Box, makeStyles } from "@material-ui/core";
|
||||
import ContactInfoBox from "./ContactInfoBox";
|
||||
import DonateInfoBox from "./DonateInfoBox";
|
||||
import FeedbackInfoBox from "./FeedbackInfoBox";
|
||||
import RpcControlBox from "./RpcControlBox";
|
||||
import TorInfoBox from "./TorInfoBox";
|
||||
import DaemonControlBox from "./DaemonControlBox";
|
||||
import SettingsBox from "./SettingsBox";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
display: "flex",
|
||||
gap: theme.spacing(2),
|
||||
flexDirection: "column",
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
@ -18,8 +19,8 @@ export default function HelpPage() {
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<RpcControlBox />
|
||||
<TorInfoBox />
|
||||
<DaemonControlBox />
|
||||
<SettingsBox />
|
||||
<FeedbackInfoBox />
|
||||
<ContactInfoBox />
|
||||
<DonateInfoBox />
|
||||
|
153
src-gui/src/renderer/components/pages/help/SettingsBox.tsx
Normal file
153
src-gui/src/renderer/components/pages/help/SettingsBox.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Typography,
|
||||
IconButton,
|
||||
Box,
|
||||
makeStyles,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
||||
import {
|
||||
resetSettings,
|
||||
setElectrumRpcUrl,
|
||||
setMoneroNodeUrl,
|
||||
} from "store/features/settingsSlice";
|
||||
import { useAppDispatch, useAppSelector } from "store/hooks";
|
||||
import ValidatedTextField from "renderer/components/other/ValidatedTextField";
|
||||
import RefreshIcon from "@material-ui/icons/Refresh";
|
||||
import HelpIcon from '@material-ui/icons/HelpOutline';
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const PLACEHOLDER_ELECTRUM_RPC_URL = "ssl://blockstream.info:700";
|
||||
const PLACEHOLDER_MONERO_NODE_URL = "http://xmr-node.cakewallet.com:18081";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
title: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
}
|
||||
}));
|
||||
|
||||
export default function SettingsBox() {
|
||||
const dispatch = useAppDispatch();
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
title={
|
||||
<Box className={classes.title}>
|
||||
Settings
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
dispatch(resetSettings());
|
||||
}}
|
||||
>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
}
|
||||
additionalContent={
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<ElectrumRpcUrlSetting />
|
||||
<MoneroNodeUrlSetting />
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
}
|
||||
mainContent={
|
||||
<Typography variant="subtitle2">
|
||||
Some of these settings require a restart to take effect.
|
||||
</Typography>
|
||||
}
|
||||
icon={null}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// URL validation function, forces the URL to be in the format of "protocol://host:port/"
|
||||
function isValidUrl(url: string, allowedProtocols: string[]): boolean {
|
||||
const urlPattern = new RegExp(`^(${allowedProtocols.join("|")})://[^\\s]+:\\d+/?$`);
|
||||
return urlPattern.test(url);
|
||||
}
|
||||
|
||||
function ElectrumRpcUrlSetting() {
|
||||
const electrumRpcUrl = useAppSelector((s) => s.settings.electrum_rpc_url);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
function isValid(url: string): boolean {
|
||||
return isValidUrl(url, ["ssl", "tcp"]);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<SettingLabel label="Custom Electrum RPC URL" tooltip="This is the URL of the Electrum server that the GUI will connect to. It is used to sync Bitcoin transactions. If you leave this field empty, the GUI will choose from a list of known servers at random." />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ValidatedTextField
|
||||
label="Electrum RPC URL"
|
||||
value={electrumRpcUrl}
|
||||
isValid={isValid}
|
||||
onValidatedChange={(value) => {
|
||||
dispatch(setElectrumRpcUrl(value));
|
||||
}}
|
||||
fullWidth
|
||||
placeholder={PLACEHOLDER_ELECTRUM_RPC_URL}
|
||||
allowEmpty
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingLabel({ label, tooltip }: { label: ReactNode, tooltip: string | null }) {
|
||||
return <Box style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||
<Box>
|
||||
{label}
|
||||
</Box>
|
||||
<Tooltip title={tooltip}>
|
||||
<IconButton size="small">
|
||||
<HelpIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
|
||||
function MoneroNodeUrlSetting() {
|
||||
const moneroNodeUrl = useAppSelector((s) => s.settings.monero_node_url);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
function isValid(url: string): boolean {
|
||||
return isValidUrl(url, ["http"]);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<SettingLabel label="Custom Monero Node URL" tooltip="This is the URL of the Monero node that the GUI will connect to. Ensure the node is listening for RPC connections over HTTP. If you leave this field empty, the GUI will choose from a list of known nodes at random." />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ValidatedTextField
|
||||
label="Monero Node URL"
|
||||
value={moneroNodeUrl}
|
||||
isValid={isValid}
|
||||
onValidatedChange={(value) => {
|
||||
dispatch(setMoneroNodeUrl(value));
|
||||
}}
|
||||
fullWidth
|
||||
placeholder={PLACEHOLDER_MONERO_NODE_URL}
|
||||
allowEmpty
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
@ -1,21 +1,21 @@
|
||||
import {
|
||||
Box,
|
||||
Link,
|
||||
makeStyles,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Box,
|
||||
Link,
|
||||
makeStyles,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
} from "@material-ui/core";
|
||||
import { OpenInNew } from "@material-ui/icons";
|
||||
import { GetSwapInfoResponse } from "models/tauriModel";
|
||||
import CopyableMonospaceTextBox from "renderer/components/other/CopyableMonospaceTextBox";
|
||||
import MonospaceTextBox from "renderer/components/other/MonospaceTextBox";
|
||||
import {
|
||||
MoneroBitcoinExchangeRate,
|
||||
PiconeroAmount,
|
||||
SatsAmount,
|
||||
MoneroBitcoinExchangeRate,
|
||||
PiconeroAmount,
|
||||
SatsAmount,
|
||||
} from "renderer/components/other/Units";
|
||||
import { isTestnet } from "store/config";
|
||||
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
|
||||
|
@ -176,9 +176,7 @@ function HasProviderSwapWidget({
|
||||
function HasNoProvidersSwapWidget() {
|
||||
const forceShowDialog = useAppSelector((state) => state.swap.state !== null);
|
||||
const isPublicRegistryDown = useAppSelector((state) =>
|
||||
isRegistryDown(
|
||||
state.providers.registry.connectionFailsCount,
|
||||
),
|
||||
isRegistryDown(state.providers.registry.connectionFailsCount),
|
||||
);
|
||||
const classes = useStyles();
|
||||
|
||||
@ -254,9 +252,7 @@ export default function SwapWidget() {
|
||||
const providerLoading = useAppSelector(
|
||||
(state) =>
|
||||
state.providers.registry.providers === null &&
|
||||
!isRegistryDown(
|
||||
state.providers.registry.connectionFailsCount,
|
||||
),
|
||||
!isRegistryDown(state.providers.registry.connectionFailsCount),
|
||||
);
|
||||
|
||||
if (providerLoading) {
|
||||
|
@ -48,9 +48,7 @@ export default function WithdrawWidget() {
|
||||
endIcon={<SendIcon />}
|
||||
size="large"
|
||||
onClick={onShowDialog}
|
||||
disabled={
|
||||
walletBalance === null || walletBalance <= 0
|
||||
}
|
||||
disabled={walletBalance === null || walletBalance <= 0}
|
||||
>
|
||||
Withdraw
|
||||
</Button>
|
||||
|
@ -31,6 +31,7 @@ import { Provider } from "models/apiModel";
|
||||
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
|
||||
import { MoneroRecoveryResponse } from "models/rpcModel";
|
||||
import { ListSellersResponse } from "../models/tauriModel";
|
||||
import logger from "utils/logger";
|
||||
|
||||
export async function initEventListeners() {
|
||||
// This operation is in-expensive
|
||||
@ -38,6 +39,12 @@ export async function initEventListeners() {
|
||||
// TOOD: Replace this with a more reliable mechanism (such as an event replay mechanism)
|
||||
if (await checkContextAvailability()) {
|
||||
store.dispatch(contextStatusEventReceived({ type: "Available" }));
|
||||
} else {
|
||||
// Warning: If we reload the page while the Context is being initialized, this function will throw an error
|
||||
initializeContext().catch((e) => {
|
||||
logger.error(e, "Failed to initialize context on page load. This might be because we reloaded the page while the context was being initialized");
|
||||
});
|
||||
initializeContext();
|
||||
}
|
||||
|
||||
listen<TauriSwapProgressEventWrapper>("swap-progress-update", (event) => {
|
||||
@ -52,8 +59,8 @@ export async function initEventListeners() {
|
||||
|
||||
listen<CliLogEmittedEvent>("cli-log-emitted", (event) => {
|
||||
console.log("Received cli log event", event.payload);
|
||||
store.dispatch(receivedCliLog(event.payload))
|
||||
})
|
||||
store.dispatch(receivedCliLog(event.payload));
|
||||
});
|
||||
}
|
||||
|
||||
async function invoke<ARGS, RESPONSE>(
|
||||
@ -161,3 +168,10 @@ export async function listSellersAtRendezvousPoint(
|
||||
rendezvous_point: rendezvousPointAddress,
|
||||
});
|
||||
}
|
||||
|
||||
export async function initializeContext() {
|
||||
const settings = store.getState().settings;
|
||||
await invokeUnsafe<void>("initialize_context", {
|
||||
settings,
|
||||
});
|
||||
}
|
||||
|
@ -3,29 +3,63 @@ import { persistReducer, persistStore } from "redux-persist";
|
||||
import sessionStorage from "redux-persist/lib/storage/session";
|
||||
import { reducers } from "store/combinedReducer";
|
||||
import { createMainListeners } from "store/middleware/storeListener";
|
||||
import { createStore } from "@tauri-apps/plugin-store";
|
||||
import { getNetworkName } from "store/config";
|
||||
|
||||
// We persist the redux store in sessionStorage
|
||||
// The point of this is to preserve the store across reloads while not persisting it across GUI restarts
|
||||
//
|
||||
// If the user reloads the page, while a swap is running we want to
|
||||
// continue displaying the correct state of the swap
|
||||
const persistConfig = {
|
||||
// Goal: Maintain application state across page reloads while allowing a clean slate on application restart
|
||||
// Settings are persisted across application restarts, while the rest of the state is cleared
|
||||
|
||||
// Persist user settings across application restarts
|
||||
// We use Tauri's storage for settings to ensure they're retained even when the application is closed
|
||||
const rootPersistConfig = {
|
||||
key: "gui-global-state-store",
|
||||
storage: sessionStorage,
|
||||
blacklist: ["settings"],
|
||||
};
|
||||
|
||||
const persistedReducer = persistReducer(
|
||||
persistConfig,
|
||||
combineReducers(reducers),
|
||||
// Use Tauri's store plugin for persistent settings
|
||||
const tauriStore = await createStore(`${getNetworkName()}_settings.bin`, {
|
||||
autoSave: 1000 as unknown as boolean,
|
||||
});
|
||||
|
||||
// Configure how settings are stored and retrieved using Tauri's storage
|
||||
const settingsPersistConfig = {
|
||||
key: "settings",
|
||||
storage: {
|
||||
getItem: (key: string) => tauriStore.get(key),
|
||||
setItem: (key: string, value: unknown) => tauriStore.set(key, value),
|
||||
removeItem: (key: string) => tauriStore.delete(key),
|
||||
},
|
||||
};
|
||||
|
||||
// Create a persisted version of the settings reducer
|
||||
const persistedSettingsReducer = persistReducer(
|
||||
settingsPersistConfig,
|
||||
reducers.settings,
|
||||
);
|
||||
|
||||
// Combine all reducers, using the persisted settings reducer
|
||||
const rootReducer = combineReducers({
|
||||
...reducers,
|
||||
settings: persistedSettingsReducer,
|
||||
});
|
||||
|
||||
// Enable persistence for the entire application state
|
||||
const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
|
||||
|
||||
// Set up the Redux store with persistence and custom middleware
|
||||
export const store = configureStore({
|
||||
reducer: persistedReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().prepend(createMainListeners().middleware),
|
||||
getDefaultMiddleware({
|
||||
// Disable serializable to silence warnings about non-serializable actions
|
||||
serializableCheck: false,
|
||||
}).prepend(createMainListeners().middleware),
|
||||
});
|
||||
|
||||
// Create a persistor to manage the persisted store
|
||||
export const persistor = persistStore(store);
|
||||
|
||||
// TypeScript type definitions for easier use of the store in the application
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
@ -4,6 +4,7 @@ import ratesSlice from "./features/ratesSlice";
|
||||
import rpcSlice from "./features/rpcSlice";
|
||||
import swapReducer from "./features/swapSlice";
|
||||
import torSlice from "./features/torSlice";
|
||||
import settingsSlice from "./features/settingsSlice";
|
||||
|
||||
export const reducers = {
|
||||
swap: swapReducer,
|
||||
@ -12,4 +13,5 @@ export const reducers = {
|
||||
rpc: rpcSlice,
|
||||
alerts: alertsSlice,
|
||||
rates: ratesSlice,
|
||||
settings: settingsSlice,
|
||||
};
|
||||
|
@ -9,8 +9,6 @@ export function getStubTestnetProvider(): ExtendedProviderStatus | null {
|
||||
const stubProviderAddress = import.meta.env
|
||||
.VITE_TESTNET_STUB_PROVIDER_ADDRESS;
|
||||
|
||||
console.log(import.meta.env);
|
||||
|
||||
if (stubProviderAddress != null) {
|
||||
try {
|
||||
const [multiAddr, peerId] =
|
||||
@ -31,3 +29,11 @@ export function getStubTestnetProvider(): ExtendedProviderStatus | null {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getNetworkName(): string {
|
||||
if (isTestnet()) {
|
||||
return "Testnet";
|
||||
}else {
|
||||
return "Mainnet";
|
||||
}
|
||||
}
|
43
src-gui/src/store/features/settingsSlice.ts
Normal file
43
src-gui/src/store/features/settingsSlice.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { TauriSettings } from "models/tauriModel";
|
||||
|
||||
const initialState: TauriSettings = {
|
||||
bitcoin_confirmation_target: 1,
|
||||
electrum_rpc_url: null,
|
||||
monero_node_url: null,
|
||||
};
|
||||
|
||||
const alertsSlice = createSlice({
|
||||
name: "settings",
|
||||
initialState,
|
||||
reducers: {
|
||||
setBitcoinConfirmationTarget(slice, action: PayloadAction<number>) {
|
||||
slice.bitcoin_confirmation_target = action.payload;
|
||||
},
|
||||
setElectrumRpcUrl(slice, action: PayloadAction<string | null>) {
|
||||
if (action.payload === null || action.payload === "") {
|
||||
slice.electrum_rpc_url = null;
|
||||
} else {
|
||||
slice.electrum_rpc_url = action.payload;
|
||||
}
|
||||
},
|
||||
setMoneroNodeUrl(slice, action: PayloadAction<string | null>) {
|
||||
if (action.payload === null || action.payload === "") {
|
||||
slice.monero_node_url = null;
|
||||
} else {
|
||||
slice.monero_node_url = action.payload;
|
||||
}
|
||||
},
|
||||
resetSettings(slice) {
|
||||
return initialState;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setBitcoinConfirmationTarget,
|
||||
setElectrumRpcUrl,
|
||||
setMoneroNodeUrl,
|
||||
resetSettings
|
||||
} = alertsSlice.actions;
|
||||
export default alertsSlice.reducer;
|
@ -3,7 +3,6 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
||||
import { parseDateString } from "utils/parseUtils";
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
||||
@ -29,7 +28,7 @@ export function useIsContextAvailable() {
|
||||
|
||||
export function useSwapInfo(swapId: string | null) {
|
||||
return useAppSelector((state) =>
|
||||
swapId ? (state.rpc.state.swapInfos[swapId] ?? null) : null,
|
||||
swapId ? state.rpc.state.swapInfos[swapId] ?? null : null,
|
||||
);
|
||||
}
|
||||
|
||||
@ -58,3 +57,7 @@ export function useSwapInfosSortedByDate() {
|
||||
(swap) => -parseDateString(swap.start_date),
|
||||
);
|
||||
}
|
||||
|
||||
export function useSettings() {
|
||||
return useAppSelector((state) => state.settings);
|
||||
}
|
||||
|
@ -717,100 +717,104 @@
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.2.tgz#c770006ccc780b2de7b2151fc7f37b49121a21c1"
|
||||
integrity sha512-Yy8So+SoRz8I3NS4Bjh91BICPOSVgdompTIPYTByUqU66AXSIOgmW3Lv1ke3NORPqxdF+RdrZET+8vYai6f4aA==
|
||||
|
||||
"@tauri-apps/api@2.0.0-rc.1":
|
||||
version "2.0.0-rc.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-rc.1.tgz#ec858f239e34792625e311f687fcaca0581e0904"
|
||||
integrity sha512-qubAWjM9sqofUh7fe+7UAbBY3wlkfCyxm+PNRYpq9mnNng7lvSQq3sYsFUEB12AYvgGARZSb54VMVUvRuVLi7w==
|
||||
"@tauri-apps/api@^2.0.0":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.1.tgz#dc49d899fb873b96ee1d46a171384625ba5ad404"
|
||||
integrity sha512-eoQWT+Tq1qSwQpHV+nw1eNYe5B/nm1PoRjQCRiEOS12I1b+X4PUcREfXVX8dPcBT6GrzWGDtaecY0+1p0Rfqlw==
|
||||
|
||||
"@tauri-apps/api@^2.0.0-rc.0":
|
||||
version "2.0.0-rc.3"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-rc.3.tgz#1dd17530de9cafd854f77d3feeca1732a985a81e"
|
||||
integrity sha512-k1erUfnoOFJwL5VNFZz0BQZ2agNstG7CNOjwpdWMl1vOaVuSn4DhJtXB0Deh9lZaaDlfrykKOyZs9c3XXpMi5Q==
|
||||
"@tauri-apps/cli-darwin-arm64@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.1.tgz#5816c0099977f705d1a7249822fa51f5d3c3750a"
|
||||
integrity sha512-oWjCZoFbm57V0eLEkIbc6aUmB4iW65QF7J8JVh5sNzH4xHGP9rzlQarbkg7LOn89z7mFSZpaLJAWlaaZwoV2Ug==
|
||||
|
||||
"@tauri-apps/api@^2.0.0-rc.4":
|
||||
version "2.0.0-rc.4"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-rc.4.tgz#2b4c3493d86382981787c52006c6c9e5bf16bc08"
|
||||
integrity sha512-UNiIhhKG08j4ooss2oEEVexffmWkgkYlC2M3GcX3VPtNsqFgVNL8Mcw/4Y7rO9M9S+ffAMnLOF5ypzyuyb8tyg==
|
||||
"@tauri-apps/cli-darwin-x64@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.1.tgz#84b5b5af0d00ed30ffce0c25b64e7ef1cc6a935f"
|
||||
integrity sha512-bARd5yAnDGpG/FPhSh87+tzQ6D0TPyP2mZ5bg6cioeoXDmry68nT/FBzp87ySR1/KHvuhEQYWM/4RPrDjvI1Yg==
|
||||
|
||||
"@tauri-apps/cli-darwin-arm64@2.0.0-beta.21":
|
||||
version "2.0.0-beta.21"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.21.tgz#9dc6f306b14d58b0b4fbf218ffbb31831e28cf4d"
|
||||
integrity sha512-okI7PRSC6RO4JfrOTqu4oWf0IfBPbkGHisyDOTay6K5uhz4zzry5fFJVa8S/DTrKtdjau4vcik/EDCxiGRun9Q==
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.1.tgz#0fc6ea259b3c53320b45d9dad000e7cd350acd94"
|
||||
integrity sha512-OK3/RpxujoZAUbV7GHe4IPAUsIO6IuWEHT++jHXP+YW5Y7QezGGjQRc43IlWaQYej/yE8wfcrwrbqisc5wtiCw==
|
||||
|
||||
"@tauri-apps/cli-darwin-x64@2.0.0-beta.21":
|
||||
version "2.0.0-beta.21"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-beta.21.tgz#77a0bdd820301f120acbb93c57b6c8acb9ae4f82"
|
||||
integrity sha512-mXoJDXB6CBoqUnFb4TCsSVC6FJRZsN1DHRZAyn6iNLIhOrObcM4L2xz8rzt3WirANwJ/ayrNv95fEt8Fq1jmgA==
|
||||
"@tauri-apps/cli-linux-arm64-gnu@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.1.tgz#3ffbfdc3896c08e4196f9723c79b7e3a67bc552d"
|
||||
integrity sha512-MGSQJduiMEApspMK97mFt4kr6ig0OtxO5SUFpPDfYPw/XmY9utaRa9CEG6LcH8e0GN9xxYMhCv+FeU48spYPhA==
|
||||
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.21":
|
||||
version "2.0.0-beta.21"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-beta.21.tgz#bc9214feff536d917d55bddeadb724555f9ac698"
|
||||
integrity sha512-LYPOx3LE2eZ0g8Zh/HYaNg6B1pZzH4BPMcma7wGZ0XPu+4fKLLGgav13xP2lknLnxiRP9jJCaTIBKXgcQEtLyg==
|
||||
"@tauri-apps/cli-linux-arm64-musl@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.1.tgz#95806815aa78db218ba995f45a691a7d3493497e"
|
||||
integrity sha512-R6+vgxaPpxgGi4suMkQgGuhjMbZzMJfVyWfv2DOE/xxOzSK1BAOc54/HOjfOLxlnkA6uD6V69MwCwXgxW00A2g==
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.21":
|
||||
version "2.0.0-beta.21"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-beta.21.tgz#69167099a4756944eb5d3d15905cbf4d903307ad"
|
||||
integrity sha512-VP2L729tgY889OZj5U436EntjwkI8MyVB+GrvBv8k2mj1nWB651KiVIpcUmsUgjXZ2r01bifN9J0l+3EFEXUAQ==
|
||||
"@tauri-apps/cli-linux-x64-gnu@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.1.tgz#9521005df14bc4d7c6805bb4ed788a34cdfeb3e4"
|
||||
integrity sha512-xrasYQnUZVhKJhBxHAeu4KxZbofaQlsG9KfZ9p1Bx+hmjs5BuujzwMnXsVD2a4l6GPW6gwblf2a6d600rySmWQ==
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.21":
|
||||
version "2.0.0-beta.21"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-beta.21.tgz#d66796e672c2606d2e08a232def55919a5fa9542"
|
||||
integrity sha512-s1rV01RIdowlPHfw7hTBnCEm2C3mZbynF+xpyRSv9vSczu4dpfwILMRwxB4nzMzdJ7RPHsf/R+5Ww86e8QM4Gw==
|
||||
"@tauri-apps/cli-linux-x64-musl@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.1.tgz#9ebeacc7b5a3d379bbcc32e0c3a0612b125929cf"
|
||||
integrity sha512-SPk+EzRTlbvk46p5aURc7O4GihzxbqG80m74vstm0rolnmQ0FX3qqIh3as3cQpDiZWLod4j6EEmX0mTU3QpvXA==
|
||||
|
||||
"@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.21":
|
||||
version "2.0.0-beta.21"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-beta.21.tgz#ed02923c94b71f2377ef5c4cc72bf1de12487296"
|
||||
integrity sha512-yGh7ktUycHT3mAnKxC7cx/vjcbjJzoxQCxnjWpmIayVwq+iXLD1mK7nRXRdJpL/rnBFTqqD29CKuypCEFiq3/A==
|
||||
"@tauri-apps/cli-win32-arm64-msvc@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.1.tgz#6d394c1c8c5fc164e1b4ed5852596d9bd4c4fd4b"
|
||||
integrity sha512-LAELK01eOMyEt+JZLmx4EUOdRuPYr1a+mHjlxAxCnCaS3dpeg/c5/NMZfbRAJbAH4id+STRHIfPXTdCT2zUNAw==
|
||||
|
||||
"@tauri-apps/cli-linux-x64-musl@2.0.0-beta.21":
|
||||
version "2.0.0-beta.21"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-beta.21.tgz#511293e6508a5d41e758d6f0bf98e834b22c63cb"
|
||||
integrity sha512-+79b8O3tsjbGR47pJtcSKGmtqj4rsSxB5AfMb4UCkmoNkbaOzB0YS/ZieUGAb+SHXZ/MMs7mcl96N9SqYOL7hw==
|
||||
"@tauri-apps/cli-win32-ia32-msvc@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.1.tgz#d789acf15bc5bc5a84045c91c78849edee8142f9"
|
||||
integrity sha512-eMUgOS4mAusk5njU2TBxBjCUO1P4cV4uzY5CHihysoXSL2TVQdWrXT42VGeoahJh+yeQWkYFka2s4Bu0iWDMXg==
|
||||
|
||||
"@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.21":
|
||||
version "2.0.0-beta.21"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-beta.21.tgz#736c5dba48385bfebf030f4ad641592f0db14258"
|
||||
integrity sha512-rKlpcjx6t1ECZciMmHT5xkXKjC+O+TVxRKmA21tEq/Ezt7XdnufGko1hduwQmVJWkHxKg6ab7uf98ImMpDC5UA==
|
||||
"@tauri-apps/cli-win32-x64-msvc@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.1.tgz#9f97f166cc0d1ffd3558d41c504f2aaf0cebbf46"
|
||||
integrity sha512-U9esAOcFIv80/slzlpwjkG31Wx1OqbfDgC5KjGT1Dd9iUOSuJZCwbiY7m3rYG2I6RWLfd9zhNu86CVohsKjBfA==
|
||||
|
||||
"@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.21":
|
||||
version "2.0.0-beta.21"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-beta.21.tgz#bf0a8dbfc1d5b724fd9f1ed2db14817821bd9b43"
|
||||
integrity sha512-ExdhvRfgAoZi4/7re6OkmfqsHvTJQgWouTNphHWRilUEqBM7TEQV1UxYtwWfgyOKelyx4cxUYDFAJxootTb2Nw==
|
||||
|
||||
"@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.21":
|
||||
version "2.0.0-beta.21"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-beta.21.tgz#56842ab8088a794276cbf74bf0edcda6e96ee8ee"
|
||||
integrity sha512-JtNTwNXIOfE04Cs3ieTvkdcMyJM9Sujw5MM9zNmusJKE03s/OLqbNK/2ISlcb/puwYGGPhhyYtL5hCmYXIrHHQ==
|
||||
|
||||
"@tauri-apps/cli@>=2.0.0-beta.0":
|
||||
version "2.0.0-beta.21"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.0.0-beta.21.tgz#aef1b9f5d80da38265820ff3ab8558724e3309eb"
|
||||
integrity sha512-lqV4pD0iTs8ASd19slH0eRoVAjbxtD0cCsZFVD7kG4sYkeZ0IkvtxbvnHAOUbALfvnHZr1dVXFDVxQUqJK2OXw==
|
||||
"@tauri-apps/cli@^2.0.0":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.0.1.tgz#04bfb48507fc7c6edff4b1083fd7f5ca7578eb42"
|
||||
integrity sha512-fCheW0iWYWUtFV3ui3HlMhk3ZJpAQ5KJr7B7UmfhDzBSy1h5JBdrCtvDwy+3AcPN+Fg5Ey3JciF8zEP8eBx+vQ==
|
||||
optionalDependencies:
|
||||
"@tauri-apps/cli-darwin-arm64" "2.0.0-beta.21"
|
||||
"@tauri-apps/cli-darwin-x64" "2.0.0-beta.21"
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf" "2.0.0-beta.21"
|
||||
"@tauri-apps/cli-linux-arm64-gnu" "2.0.0-beta.21"
|
||||
"@tauri-apps/cli-linux-arm64-musl" "2.0.0-beta.21"
|
||||
"@tauri-apps/cli-linux-x64-gnu" "2.0.0-beta.21"
|
||||
"@tauri-apps/cli-linux-x64-musl" "2.0.0-beta.21"
|
||||
"@tauri-apps/cli-win32-arm64-msvc" "2.0.0-beta.21"
|
||||
"@tauri-apps/cli-win32-ia32-msvc" "2.0.0-beta.21"
|
||||
"@tauri-apps/cli-win32-x64-msvc" "2.0.0-beta.21"
|
||||
"@tauri-apps/cli-darwin-arm64" "2.0.1"
|
||||
"@tauri-apps/cli-darwin-x64" "2.0.1"
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf" "2.0.1"
|
||||
"@tauri-apps/cli-linux-arm64-gnu" "2.0.1"
|
||||
"@tauri-apps/cli-linux-arm64-musl" "2.0.1"
|
||||
"@tauri-apps/cli-linux-x64-gnu" "2.0.1"
|
||||
"@tauri-apps/cli-linux-x64-musl" "2.0.1"
|
||||
"@tauri-apps/cli-win32-arm64-msvc" "2.0.1"
|
||||
"@tauri-apps/cli-win32-ia32-msvc" "2.0.1"
|
||||
"@tauri-apps/cli-win32-x64-msvc" "2.0.1"
|
||||
|
||||
"@tauri-apps/plugin-clipboard-manager@^2.0.0-rc.0":
|
||||
version "2.0.0-rc.0"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0-rc.0.tgz#8371fa2a2092c67d0cfd9322698c14115735459e"
|
||||
integrity sha512-2fS3wbRQEtorkk3Np2msJUeKCXRqLQ9sSo2FzlFdUPYNzThsu43uWCF55McGLAfltNOvXQIcQLUBf05jbBL/5w==
|
||||
"@tauri-apps/plugin-clipboard-manager@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0.tgz#cf08df2338c055d15a60cbb61140766859e06a77"
|
||||
integrity sha512-V1sXmbjnwfXt/r48RJMwfUmDMSaP/8/YbH4CLNxt+/sf1eHlIP8PRFdFDQwLN0cNQKu2rqQVbG/Wc/Ps6cDUhw==
|
||||
dependencies:
|
||||
"@tauri-apps/api" "^2.0.0-rc.0"
|
||||
"@tauri-apps/api" "^2.0.0"
|
||||
|
||||
"@tauri-apps/plugin-shell@^2.0.0-rc.0":
|
||||
version "2.0.0-rc.1"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-rc.1.tgz#9facf3bbcedfa2de676cb4cfc703687377aa12a3"
|
||||
integrity sha512-JtNROc0rqEwN/g93ig5pK4cl1vUo2yn+osCpY9de64cy/d9hRzof7AuYOgvt/Xcd5VPQmlgo2AGvUh5sQRSR1A==
|
||||
"@tauri-apps/plugin-process@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-process/-/plugin-process-2.0.0.tgz#002fd73f0d7b1ae2a5aacf442aa657e83dc2960b"
|
||||
integrity sha512-OYzi0GnkrF4NAnsHZU7U3tjSoP0PbeAlO7T1Z+vJoBUH9sFQ1NSLqWYWQyf8hcb3gVWe7P1JggjiskO+LST1ug==
|
||||
dependencies:
|
||||
"@tauri-apps/api" "^2.0.0-rc.4"
|
||||
"@tauri-apps/api" "^2.0.0"
|
||||
|
||||
"@tauri-apps/plugin-shell@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0.tgz#b6fc88ab070fd5f620e46405715779aa44eb8428"
|
||||
integrity sha512-OpW2+ycgJLrEoZityWeWYk+6ZWP9VyiAfbO+N/O8VfLkqyOym8kXh7odKDfINx9RAotkSGBtQM4abyKfJDkcUg==
|
||||
dependencies:
|
||||
"@tauri-apps/api" "^2.0.0"
|
||||
|
||||
"@tauri-apps/plugin-store@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-store/-/plugin-store-2.0.0.tgz#7563bff85795bc501ac606dab0c329760ef28134"
|
||||
integrity sha512-l4xsbxAXrKGdBdYNNswrLfcRv3v1kOatdycOcVPYW+jKwkznCr1HEOrPXkPhXsZLSLyYmNXpgfOmdSZNmcykDg==
|
||||
dependencies:
|
||||
"@tauri-apps/api" "^2.0.0"
|
||||
|
||||
"@testing-library/react@^16.0.1":
|
||||
version "16.0.1"
|
||||
|
@ -20,6 +20,10 @@ once_cell = "1"
|
||||
serde = { version = "1", features = [ "derive" ] }
|
||||
serde_json = "1"
|
||||
swap = { path = "../swap", features = [ "tauri" ] }
|
||||
tauri = { version = "2.0.0-rc.1", features = [ "config-json5" ] }
|
||||
tauri = { version = "2.0.0", features = [ "config-json5" ] }
|
||||
tauri-plugin-clipboard-manager = "2.1.0-beta.7"
|
||||
tauri-plugin-shell = "2.0.0-rc.2"
|
||||
tauri-plugin-devtools = "2.0.0"
|
||||
tauri-plugin-process = "2.0.0"
|
||||
tauri-plugin-shell = "2.0.0"
|
||||
tauri-plugin-store = "2.0.0"
|
||||
tracing = "0.1.40"
|
||||
|
@ -7,6 +7,8 @@
|
||||
"core:event:allow-emit",
|
||||
"core:event:default",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"shell:allow-open"
|
||||
"shell:allow-open",
|
||||
"store:default",
|
||||
"process:default"
|
||||
]
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use anyhow::Context as AnyhowContext;
|
||||
use std::result::Result;
|
||||
use std::sync::Arc;
|
||||
use swap::cli::{
|
||||
@ -7,7 +8,7 @@ use swap::cli::{
|
||||
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, ResumeSwapArgs,
|
||||
SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||
},
|
||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle},
|
||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
||||
Context, ContextBuilder,
|
||||
},
|
||||
command::{Bitcoin, Monero},
|
||||
@ -104,13 +105,12 @@ impl State {
|
||||
fn try_get_context(&self) -> Result<Arc<Context>, String> {
|
||||
self.context
|
||||
.clone()
|
||||
.ok_or("Context not available")
|
||||
.to_string_result()
|
||||
.ok_or("Context not available".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up the Tauri application
|
||||
/// Initializes the Tauri state and spawns an async task to set up the Context
|
||||
/// Initializes the Tauri state
|
||||
fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let app_handle = app.app_handle().to_owned();
|
||||
|
||||
@ -118,44 +118,14 @@ fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// If we don't do this, Tauri commands will panic at runtime if no value is present
|
||||
app_handle.manage::<RwLock<State>>(RwLock::new(State::new()));
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let tauri_handle = TauriHandle::new(app_handle.clone());
|
||||
|
||||
let context = ContextBuilder::new(true)
|
||||
.with_bitcoin(Bitcoin {
|
||||
bitcoin_electrum_rpc_url: None,
|
||||
bitcoin_target_block: None,
|
||||
})
|
||||
.with_monero(Monero {
|
||||
monero_daemon_address: None,
|
||||
})
|
||||
.with_json(false)
|
||||
.with_debug(true)
|
||||
.with_tauri(tauri_handle.clone())
|
||||
.build()
|
||||
.await;
|
||||
|
||||
match context {
|
||||
Ok(context) => {
|
||||
let state = app_handle.state::<RwLock<State>>();
|
||||
state.write().await.set_context(Arc::new(context));
|
||||
// To display to the user that the setup is done, we emit an event to the Tauri frontend
|
||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Available);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error while initializing context: {:?}", e);
|
||||
// To display to the user that the setup failed, we emit an event to the Tauri frontend
|
||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Failed);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@ -171,6 +141,7 @@ pub fn run() {
|
||||
suspend_current_swap,
|
||||
cancel_and_refund,
|
||||
is_context_available,
|
||||
initialize_context,
|
||||
])
|
||||
.setup(setup)
|
||||
.build(tauri::generate_context!())
|
||||
@ -220,5 +191,56 @@ tauri_command!(get_history, GetHistoryArgs, no_args);
|
||||
/// Here we define Tauri commands whose implementation is not delegated to the Request trait
|
||||
#[tauri::command]
|
||||
async fn is_context_available(context: tauri::State<'_, RwLock<State>>) -> Result<bool, String> {
|
||||
// TODO: Here we should return more information about status of the context (e.g. initializing, failed)
|
||||
Ok(context.read().await.try_get_context().is_ok())
|
||||
}
|
||||
|
||||
/// Tauri command to initialize the Context
|
||||
#[tauri::command]
|
||||
async fn initialize_context(
|
||||
settings: TauriSettings,
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, RwLock<State>>,
|
||||
) -> Result<(), String> {
|
||||
// Acquire a write lock on the state
|
||||
let mut state_write_lock = state
|
||||
.try_write()
|
||||
.context("Context is already being initialized")
|
||||
.to_string_result()?;
|
||||
|
||||
// Get app handle and create a Tauri handle
|
||||
let tauri_handle = TauriHandle::new(app_handle.clone());
|
||||
|
||||
let context_result = ContextBuilder::new(true)
|
||||
.with_bitcoin(Bitcoin {
|
||||
bitcoin_electrum_rpc_url: settings.electrum_rpc_url,
|
||||
bitcoin_target_block: settings.bitcoin_confirmation_target.into(),
|
||||
})
|
||||
.with_monero(Monero {
|
||||
monero_daemon_address: settings.monero_node_url,
|
||||
})
|
||||
.with_json(false)
|
||||
.with_debug(true)
|
||||
.with_tauri(tauri_handle.clone())
|
||||
.build()
|
||||
.await;
|
||||
|
||||
match context_result {
|
||||
Ok(context_instance) => {
|
||||
state_write_lock.set_context(Arc::new(context_instance));
|
||||
|
||||
tracing::info!("Context initialized");
|
||||
|
||||
// Emit event to frontend
|
||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Available);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = ?e, "Failed to initialize context");
|
||||
|
||||
// Emit event to frontend
|
||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Failed);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,7 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"nullable": [false],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
@ -37,9 +35,7 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
true
|
||||
],
|
||||
"nullable": [true],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
@ -60,10 +56,7 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"nullable": [false, false],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
}
|
||||
@ -99,9 +92,7 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"nullable": [false],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
@ -127,9 +118,7 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"nullable": [false],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
@ -145,9 +134,7 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"nullable": [false],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
@ -163,9 +150,7 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"nullable": [false],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
@ -191,13 +176,11 @@
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"nullable": [false],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT proof\n FROM buffered_transfer_proofs\n WHERE swap_id = ?\n "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ pub struct Defaults {
|
||||
electrum_rpc_url: Url,
|
||||
monero_wallet_rpc_url: Url,
|
||||
price_ticker_ws_url: Url,
|
||||
bitcoin_confirmation_target: usize,
|
||||
bitcoin_confirmation_target: u16,
|
||||
}
|
||||
|
||||
impl GetDefaults for Testnet {
|
||||
@ -185,7 +185,7 @@ mod addr_list {
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Bitcoin {
|
||||
pub electrum_rpc_url: Url,
|
||||
pub target_block: usize,
|
||||
pub target_block: u16,
|
||||
pub finality_confirmations: Option<u32>,
|
||||
#[serde(with = "crate::bitcoin::network")]
|
||||
pub network: bitcoin::Network,
|
||||
|
@ -42,7 +42,7 @@ pub struct Wallet<D = Tree, C = Client> {
|
||||
wallet: Arc<Mutex<bdk::Wallet<D>>>,
|
||||
finality_confirmations: u32,
|
||||
network: Network,
|
||||
target_block: usize,
|
||||
target_block: u16,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
@ -51,7 +51,7 @@ impl Wallet {
|
||||
data_dir: impl AsRef<Path>,
|
||||
xprivkey: ExtendedPrivKey,
|
||||
env_config: env::Config,
|
||||
target_block: usize,
|
||||
target_block: u16,
|
||||
) -> Result<Self> {
|
||||
let data_dir = data_dir.as_ref();
|
||||
let wallet_dir = data_dir.join(WALLET);
|
||||
@ -577,7 +577,7 @@ impl<D, C> Wallet<D, C> {
|
||||
}
|
||||
|
||||
pub trait EstimateFeeRate {
|
||||
fn estimate_feerate(&self, target_block: usize) -> Result<FeeRate>;
|
||||
fn estimate_feerate(&self, target_block: u16) -> Result<FeeRate>;
|
||||
fn min_relay_fee(&self) -> Result<bitcoin::Amount>;
|
||||
}
|
||||
|
||||
@ -589,7 +589,7 @@ pub struct StaticFeeRate {
|
||||
|
||||
#[cfg(test)]
|
||||
impl EstimateFeeRate for StaticFeeRate {
|
||||
fn estimate_feerate(&self, _target_block: usize) -> Result<FeeRate> {
|
||||
fn estimate_feerate(&self, _target_block: u16) -> Result<FeeRate> {
|
||||
Ok(self.fee_rate)
|
||||
}
|
||||
|
||||
@ -726,8 +726,10 @@ impl Client {
|
||||
let config = bdk::electrum_client::ConfigBuilder::default()
|
||||
.retry(5)
|
||||
.build();
|
||||
|
||||
let electrum = bdk::electrum_client::Client::from_config(electrum_rpc_url.as_str(), config)
|
||||
.context("Failed to initialize Electrum RPC client")?;
|
||||
|
||||
// Initially fetch the latest block for storing the height.
|
||||
// We do not act on this subscription after this call.
|
||||
let latest_block = electrum
|
||||
@ -736,6 +738,7 @@ impl Client {
|
||||
|
||||
let client = bdk::electrum_client::Client::new(electrum_rpc_url.as_str())
|
||||
.context("Failed to initialize Electrum RPC client")?;
|
||||
|
||||
let blockchain = ElectrumBlockchain::from(client);
|
||||
let last_sync = Instant::now()
|
||||
.checked_sub(interval)
|
||||
@ -867,10 +870,10 @@ impl Client {
|
||||
}
|
||||
|
||||
impl EstimateFeeRate for Client {
|
||||
fn estimate_feerate(&self, target_block: usize) -> Result<FeeRate> {
|
||||
fn estimate_feerate(&self, target_block: u16) -> Result<FeeRate> {
|
||||
// https://github.com/romanz/electrs/blob/f9cf5386d1b5de6769ee271df5eef324aa9491bc/src/rpc.rs#L213
|
||||
// Returned estimated fees are per BTC/kb.
|
||||
let fee_per_byte = self.electrum.estimate_fee(target_block)?;
|
||||
let fee_per_byte = self.electrum.estimate_fee(target_block.into())?;
|
||||
// we do not expect fees being that high.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
Ok(FeeRate::from_btc_per_kvb(fee_per_byte as f32))
|
||||
|
@ -435,7 +435,7 @@ async fn init_bitcoin_wallet(
|
||||
seed: &Seed,
|
||||
data_dir: PathBuf,
|
||||
env_config: EnvConfig,
|
||||
bitcoin_target_block: usize,
|
||||
bitcoin_target_block: u16,
|
||||
) -> Result<bitcoin::Wallet> {
|
||||
let wallet_dir = data_dir.join("wallet");
|
||||
|
||||
@ -467,14 +467,16 @@ async fn init_monero_wallet(
|
||||
|
||||
let monero_wallet_rpc_process = monero_wallet_rpc
|
||||
.run(network, Some(monero_daemon_address))
|
||||
.await?;
|
||||
.await
|
||||
.context("Failed to start monero-wallet-rpc process")?;
|
||||
|
||||
let monero_wallet = monero::Wallet::open_or_create(
|
||||
monero_wallet_rpc_process.endpoint(),
|
||||
MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(),
|
||||
env_config,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.context("Failed to open or create Monero wallet")?;
|
||||
|
||||
Ok((monero_wallet, monero_wallet_rpc_process))
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
use crate::{monero, network::quote::BidQuote};
|
||||
use anyhow::Result;
|
||||
use bitcoin::Txid;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::Display;
|
||||
use typeshare::typeshare;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
const SWAP_PROGRESS_EVENT_NAME: &str = "swap-progress-update";
|
||||
@ -83,6 +84,7 @@ pub enum TauriContextInitializationProgress {
|
||||
#[derive(Display, Clone, Serialize)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum TauriContextStatusEvent {
|
||||
NotInitialized,
|
||||
Initializing(TauriContextInitializationProgress),
|
||||
Available,
|
||||
Failed,
|
||||
@ -174,5 +176,18 @@ pub enum TauriSwapProgressEvent {
|
||||
#[typeshare]
|
||||
pub struct CliLogEmittedEvent {
|
||||
/// The serialized object containing the log message and metadata.
|
||||
pub buffer: String
|
||||
pub buffer: String,
|
||||
}
|
||||
|
||||
/// This struct contains the settings for the Context
|
||||
#[typeshare]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct TauriSettings {
|
||||
/// This is used for estimating the target block for Bitcoin (fee)
|
||||
pub bitcoin_confirmation_target: u16,
|
||||
/// The URL of the Monero node e.g `http://xmr.node:18081`
|
||||
pub monero_node_url: Option<String>,
|
||||
/// The URL of the Electrum RPC server e.g `ssl://bitcoin.com:50001`
|
||||
#[typeshare(serialized_as = "string")]
|
||||
pub electrum_rpc_url: Option<Url>,
|
||||
}
|
||||
|
@ -30,8 +30,8 @@ const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://blockstream.info:700";
|
||||
// See: https://1209k.com/bitcoin-eye/ele.php?chain=tbtc
|
||||
pub const DEFAULT_ELECTRUM_RPC_URL_TESTNET: &str = "ssl://testnet.foundation.xyz:50002";
|
||||
|
||||
const DEFAULT_BITCOIN_CONFIRMATION_TARGET: usize = 1;
|
||||
pub const DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET: usize = 1;
|
||||
const DEFAULT_BITCOIN_CONFIRMATION_TARGET: u16 = 1;
|
||||
pub const DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET: u16 = 1;
|
||||
|
||||
const DEFAULT_TOR_SOCKS5_PORT: &str = "9050";
|
||||
|
||||
@ -534,11 +534,11 @@ pub struct Bitcoin {
|
||||
long = "bitcoin-target-block",
|
||||
help = "Estimate Bitcoin fees such that transactions are confirmed within the specified number of blocks"
|
||||
)]
|
||||
pub bitcoin_target_block: Option<usize>,
|
||||
pub bitcoin_target_block: Option<u16>,
|
||||
}
|
||||
|
||||
impl Bitcoin {
|
||||
pub fn apply_defaults(self, testnet: bool) -> Result<(Url, usize)> {
|
||||
pub fn apply_defaults(self, testnet: bool) -> Result<(Url, u16)> {
|
||||
let bitcoin_electrum_rpc_url = if let Some(url) = self.bitcoin_electrum_rpc_url {
|
||||
url
|
||||
} else if testnet {
|
||||
|
@ -64,13 +64,13 @@ pub fn init(
|
||||
.with(file_layer)
|
||||
.with(tauri_layer)
|
||||
.with(terminal_layer.json().with_filter(level_filter))
|
||||
.init();
|
||||
.try_init()?;
|
||||
} else {
|
||||
tracing_subscriber::registry()
|
||||
.with(file_layer)
|
||||
.with(tauri_layer)
|
||||
.with(terminal_layer.with_filter(level_filter))
|
||||
.init();
|
||||
.try_init()?;
|
||||
}
|
||||
|
||||
// Now we can use the tracing macros to log messages
|
||||
@ -83,7 +83,11 @@ pub fn init(
|
||||
fn env_filter(level_filter: LevelFilter) -> Result<EnvFilter> {
|
||||
Ok(EnvFilter::from_default_env()
|
||||
.add_directive(Directive::from_str(&format!("asb={}", &level_filter))?)
|
||||
.add_directive(Directive::from_str(&format!("swap={}", &level_filter))?))
|
||||
.add_directive(Directive::from_str(&format!("swap={}", &level_filter))?)
|
||||
.add_directive(Directive::from_str(&format!(
|
||||
"unstoppableswap-gui-rs={}",
|
||||
&level_filter
|
||||
))?))
|
||||
}
|
||||
|
||||
/// A writer that forwards tracing log messages to the tauri guest.
|
||||
|
@ -87,6 +87,7 @@ pub struct WalletRpcProcess {
|
||||
port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct MoneroDaemon {
|
||||
address: &'static str,
|
||||
port: u16,
|
||||
@ -102,6 +103,16 @@ impl MoneroDaemon {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(address: String, network: Network) -> Result<Self, Error> {
|
||||
let (address, port) = extract_host_and_port(address)?;
|
||||
|
||||
Ok(Self {
|
||||
address,
|
||||
port,
|
||||
network,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if the Monero daemon is available by sending a request to its `get_info` endpoint.
|
||||
async fn is_available(&self, client: &reqwest::Client) -> Result<bool, Error> {
|
||||
let url = format!("http://{}:{}/get_info", self.address, self.port);
|
||||
@ -144,7 +155,7 @@ struct MoneroDaemonGetInfoResponse {
|
||||
}
|
||||
|
||||
/// Chooses an available Monero daemon based on the specified network.
|
||||
async fn choose_monero_daemon(network: Network) -> Result<&'static MoneroDaemon, Error> {
|
||||
async fn choose_monero_daemon(network: Network) -> Result<MoneroDaemon, Error> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.https_only(false)
|
||||
@ -159,7 +170,7 @@ async fn choose_monero_daemon(network: Network) -> Result<&'static MoneroDaemon,
|
||||
match daemon.is_available(&client).await {
|
||||
Ok(true) => {
|
||||
tracing::debug!(%daemon, "Found available Monero daemon");
|
||||
return Ok(daemon);
|
||||
return Ok(*daemon);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::debug!(%err, %daemon, "Failed to connect to Monero daemon");
|
||||
@ -320,11 +331,21 @@ impl WalletRpc {
|
||||
.local_addr()?
|
||||
.port();
|
||||
|
||||
let daemon_address = match daemon_address {
|
||||
Some(daemon_address) => daemon_address,
|
||||
None => choose_monero_daemon(network).await?.to_string(),
|
||||
let daemon = match daemon_address {
|
||||
Some(daemon_address) => {
|
||||
let daemon = MoneroDaemon::from_str(daemon_address, network)?;
|
||||
|
||||
if !daemon.is_available(&reqwest::Client::new()).await? {
|
||||
bail!("Specified daemon is not available or not on the correct network");
|
||||
}
|
||||
|
||||
daemon
|
||||
}
|
||||
None => choose_monero_daemon(network).await?,
|
||||
};
|
||||
|
||||
let daemon_address = daemon.to_string();
|
||||
|
||||
tracing::debug!(
|
||||
%daemon_address,
|
||||
%port,
|
||||
@ -465,22 +486,36 @@ impl WalletRpc {
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_host_and_port(address: String) -> Result<(&'static str, u16), Error> {
|
||||
// Strip the protocol (anything before "://")
|
||||
let stripped_address = if let Some(pos) = address.find("://") {
|
||||
address[(pos + 3)..].to_string()
|
||||
} else {
|
||||
address
|
||||
};
|
||||
|
||||
// Split the remaining address into parts (host and port)
|
||||
let parts: Vec<&str> = stripped_address.split(':').collect();
|
||||
|
||||
if parts.len() == 2 {
|
||||
let host = parts[0].to_string();
|
||||
let port = parts[1].parse::<u16>()?;
|
||||
|
||||
// Leak the host string to create a 'static lifetime string
|
||||
let static_str_host: &'static str = Box::leak(host.into_boxed_str());
|
||||
return Ok((static_str_host, port));
|
||||
}
|
||||
|
||||
bail!(
|
||||
"Could not extract host and port from address: {}",
|
||||
stripped_address
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn extract_host_and_port(address: String) -> (&'static str, u16) {
|
||||
let parts: Vec<&str> = address.split(':').collect();
|
||||
|
||||
if parts.len() == 2 {
|
||||
let host = parts[0].to_string();
|
||||
let port = parts[1].parse::<u16>().unwrap();
|
||||
let static_str_host: &'static str = Box::leak(host.into_boxed_str());
|
||||
return (static_str_host, port);
|
||||
}
|
||||
panic!("Could not extract host and port from address: {}", address)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_is_daemon_available_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
@ -501,7 +536,7 @@ mod tests {
|
||||
)
|
||||
.create();
|
||||
|
||||
let (host, port) = extract_host_and_port(server.host_with_port());
|
||||
let (host, port) = extract_host_and_port(server.host_with_port()).unwrap();
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let result = MoneroDaemon::new(host, port, Network::Mainnet)
|
||||
@ -532,7 +567,7 @@ mod tests {
|
||||
)
|
||||
.create();
|
||||
|
||||
let (host, port) = extract_host_and_port(server.host_with_port());
|
||||
let (host, port) = extract_host_and_port(server.host_with_port()).unwrap();
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let result = MoneroDaemon::new(host, port, Network::Stagenet)
|
||||
@ -563,7 +598,7 @@ mod tests {
|
||||
)
|
||||
.create();
|
||||
|
||||
let (host, port) = extract_host_and_port(server.host_with_port());
|
||||
let (host, port) = extract_host_and_port(server.host_with_port()).unwrap();
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let result = MoneroDaemon::new(host, port, Network::Mainnet)
|
||||
|
Loading…
x
Reference in New Issue
Block a user