From 956a26d1810a58c00a335ad71704a000d131933e Mon Sep 17 00:00:00 2001 From: binarybaron Date: Sun, 18 Aug 2024 19:31:45 +0200 Subject: [PATCH 1/9] refactor(gui): Reorganize imports --- src-gui/src/renderer/components/App.tsx | 8 ++++---- .../src/renderer/components/navigation/Navigation.tsx | 4 ++-- .../components/navigation/NavigationFooter.tsx | 10 +++++----- .../components/navigation/NavigationHeader.tsx | 4 ++-- .../components/navigation/RouteListItemIconButton.tsx | 2 +- .../renderer/components/other/ExpandableSearchBox.tsx | 4 ++-- src-gui/src/renderer/components/other/JSONViewTree.tsx | 4 ++-- .../src/renderer/components/other/LoadingButton.tsx | 2 +- .../src/renderer/components/other/RenderedCliLog.tsx | 2 +- .../renderer/components/pages/help/DonateInfoBox.tsx | 2 +- .../renderer/components/pages/help/FeedbackInfoBox.tsx | 2 +- .../src/renderer/components/pages/help/HelpPage.tsx | 4 ++-- .../renderer/components/pages/help/RpcControlBox.tsx | 8 ++++---- .../src/renderer/components/pages/help/TorInfoBox.tsx | 4 ++-- .../renderer/components/pages/history/HistoryPage.tsx | 4 ++-- .../src/renderer/components/pages/swap/SwapPage.tsx | 2 +- .../components/pages/wallet/WithdrawWidget.tsx | 8 ++++---- .../components/snackbar/GlobalSnackbarProvider.tsx | 4 ++-- src-gui/src/renderer/index.tsx | 8 ++++---- src-gui/src/store/combinedReducer.ts | 8 ++++---- src-gui/src/store/hooks.ts | 2 +- src-gui/vite.config.ts | 2 +- 22 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src-gui/src/renderer/components/App.tsx b/src-gui/src/renderer/components/App.tsx index dee26c509..0123e2026 100644 --- a/src-gui/src/renderer/components/App.tsx +++ b/src-gui/src/renderer/components/App.tsx @@ -1,12 +1,12 @@ -import { Box, makeStyles, CssBaseline } from "@material-ui/core"; -import { createTheme, ThemeProvider } from "@material-ui/core/styles"; +import { Box, CssBaseline, makeStyles } from "@material-ui/core"; import { indigo } from "@material-ui/core/colors"; -import { MemoryRouter as Router, Routes, Route } from "react-router-dom"; +import { createTheme, ThemeProvider } from "@material-ui/core/styles"; +import { Route, MemoryRouter as Router, Routes } from "react-router-dom"; import Navigation, { drawerWidth } from "./navigation/Navigation"; +import HelpPage from "./pages/help/HelpPage"; import HistoryPage from "./pages/history/HistoryPage"; import SwapPage from "./pages/swap/SwapPage"; import WalletPage from "./pages/wallet/WalletPage"; -import HelpPage from "./pages/help/HelpPage"; import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider"; const useStyles = makeStyles((theme) => ({ diff --git a/src-gui/src/renderer/components/navigation/Navigation.tsx b/src-gui/src/renderer/components/navigation/Navigation.tsx index 5a8bc344f..2e9c1c940 100644 --- a/src-gui/src/renderer/components/navigation/Navigation.tsx +++ b/src-gui/src/renderer/components/navigation/Navigation.tsx @@ -1,6 +1,6 @@ -import { Drawer, makeStyles, Box } from "@material-ui/core"; -import NavigationHeader from "./NavigationHeader"; +import { Box, Drawer, makeStyles } from "@material-ui/core"; import NavigationFooter from "./NavigationFooter"; +import NavigationHeader from "./NavigationHeader"; export const drawerWidth = 240; diff --git a/src-gui/src/renderer/components/navigation/NavigationFooter.tsx b/src-gui/src/renderer/components/navigation/NavigationFooter.tsx index a8612a9bf..e4d7f1c19 100644 --- a/src-gui/src/renderer/components/navigation/NavigationFooter.tsx +++ b/src-gui/src/renderer/components/navigation/NavigationFooter.tsx @@ -1,13 +1,13 @@ -import RedditIcon from "@material-ui/icons/Reddit"; -import GitHubIcon from "@material-ui/icons/GitHub"; import { Box, makeStyles } from "@material-ui/core"; -import LinkIconButton from "../icons/LinkIconButton"; -import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert"; +import GitHubIcon from "@material-ui/icons/GitHub"; +import RedditIcon from "@material-ui/icons/Reddit"; import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert"; +import MoneroWalletRpcUpdatingAlert from "../alert/MoneroWalletRpcUpdatingAlert"; import RpcStatusAlert from "../alert/RpcStatusAlert"; +import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert"; import DiscordIcon from "../icons/DiscordIcon"; +import LinkIconButton from "../icons/LinkIconButton"; import { DISCORD_URL } from "../pages/help/ContactInfoBox"; -import MoneroWalletRpcUpdatingAlert from "../alert/MoneroWalletRpcUpdatingAlert"; const useStyles = makeStyles((theme) => ({ outer: { diff --git a/src-gui/src/renderer/components/navigation/NavigationHeader.tsx b/src-gui/src/renderer/components/navigation/NavigationHeader.tsx index ae18c02c0..848633e59 100644 --- a/src-gui/src/renderer/components/navigation/NavigationHeader.tsx +++ b/src-gui/src/renderer/components/navigation/NavigationHeader.tsx @@ -1,8 +1,8 @@ import { Box, List } from "@material-ui/core"; -import SwapHorizOutlinedIcon from "@material-ui/icons/SwapHorizOutlined"; -import HistoryOutlinedIcon from "@material-ui/icons/HistoryOutlined"; import AccountBalanceWalletIcon from "@material-ui/icons/AccountBalanceWallet"; import HelpOutlineIcon from "@material-ui/icons/HelpOutline"; +import HistoryOutlinedIcon from "@material-ui/icons/HistoryOutlined"; +import SwapHorizOutlinedIcon from "@material-ui/icons/SwapHorizOutlined"; import RouteListItemIconButton from "./RouteListItemIconButton"; import UnfinishedSwapsBadge from "./UnfinishedSwapsCountBadge"; diff --git a/src-gui/src/renderer/components/navigation/RouteListItemIconButton.tsx b/src-gui/src/renderer/components/navigation/RouteListItemIconButton.tsx index a9a3e2ac0..0df64d8b3 100644 --- a/src-gui/src/renderer/components/navigation/RouteListItemIconButton.tsx +++ b/src-gui/src/renderer/components/navigation/RouteListItemIconButton.tsx @@ -1,6 +1,6 @@ +import { ListItem, ListItemIcon, ListItemText } from "@material-ui/core"; import { ReactNode } from "react"; import { useNavigate } from "react-router-dom"; -import { ListItem, ListItemIcon, ListItemText } from "@material-ui/core"; export default function RouteListItemIconButton({ name, diff --git a/src-gui/src/renderer/components/other/ExpandableSearchBox.tsx b/src-gui/src/renderer/components/other/ExpandableSearchBox.tsx index 0afaf5ff4..c8be76631 100644 --- a/src-gui/src/renderer/components/other/ExpandableSearchBox.tsx +++ b/src-gui/src/renderer/components/other/ExpandableSearchBox.tsx @@ -1,7 +1,7 @@ -import { useState } from "react"; import { Box, IconButton, TextField } from "@material-ui/core"; -import SearchIcon from "@material-ui/icons/Search"; import CloseIcon from "@material-ui/icons/Close"; +import SearchIcon from "@material-ui/icons/Search"; +import { useState } from "react"; export function ExpandableSearchBox({ query, diff --git a/src-gui/src/renderer/components/other/JSONViewTree.tsx b/src-gui/src/renderer/components/other/JSONViewTree.tsx index f2aa67639..87cf63b5b 100644 --- a/src-gui/src/renderer/components/other/JSONViewTree.tsx +++ b/src-gui/src/renderer/components/other/JSONViewTree.tsx @@ -1,7 +1,7 @@ -import TreeView from "@material-ui/lab/TreeView"; -import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import ChevronRightIcon from "@material-ui/icons/ChevronRight"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import TreeItem from "@material-ui/lab/TreeItem"; +import TreeView from "@material-ui/lab/TreeView"; import ScrollablePaperTextBox from "./ScrollablePaperTextBox"; interface JsonTreeViewProps { diff --git a/src-gui/src/renderer/components/other/LoadingButton.tsx b/src-gui/src/renderer/components/other/LoadingButton.tsx index 480e3fd1b..beb13da81 100644 --- a/src-gui/src/renderer/components/other/LoadingButton.tsx +++ b/src-gui/src/renderer/components/other/LoadingButton.tsx @@ -1,6 +1,6 @@ -import React from "react"; import Button, { ButtonProps } from "@material-ui/core/Button"; import CircularProgress from "@material-ui/core/CircularProgress"; +import React from "react"; interface LoadingButtonProps extends ButtonProps { loading: boolean; diff --git a/src-gui/src/renderer/components/other/RenderedCliLog.tsx b/src-gui/src/renderer/components/other/RenderedCliLog.tsx index ab91ea8f6..50e4bf9f5 100644 --- a/src-gui/src/renderer/components/other/RenderedCliLog.tsx +++ b/src-gui/src/renderer/components/other/RenderedCliLog.tsx @@ -1,6 +1,6 @@ import { Box, Chip, Typography } from "@material-ui/core"; -import { useMemo, useState } from "react"; import { CliLog } from "models/cliModel"; +import { useMemo, useState } from "react"; import { logsToRawString } from "utils/parseUtils"; import ScrollablePaperTextBox from "./ScrollablePaperTextBox"; diff --git a/src-gui/src/renderer/components/pages/help/DonateInfoBox.tsx b/src-gui/src/renderer/components/pages/help/DonateInfoBox.tsx index 9e4538016..b87de9ccb 100644 --- a/src-gui/src/renderer/components/pages/help/DonateInfoBox.tsx +++ b/src-gui/src/renderer/components/pages/help/DonateInfoBox.tsx @@ -1,6 +1,6 @@ import { Typography } from "@material-ui/core"; -import DepositAddressInfoBox from "../../modal/swap/DepositAddressInfoBox"; import MoneroIcon from "../../icons/MoneroIcon"; +import DepositAddressInfoBox from "../../modal/swap/DepositAddressInfoBox"; const XMR_DONATE_ADDRESS = "87jS4C7ngk9EHdqFFuxGFgg8AyH63dRUoULshWDybFJaP75UA89qsutG5B1L1QTc4w228nsqsv8EjhL7bz8fB3611Mh98mg"; diff --git a/src-gui/src/renderer/components/pages/help/FeedbackInfoBox.tsx b/src-gui/src/renderer/components/pages/help/FeedbackInfoBox.tsx index faea798e9..19614337e 100644 --- a/src-gui/src/renderer/components/pages/help/FeedbackInfoBox.tsx +++ b/src-gui/src/renderer/components/pages/help/FeedbackInfoBox.tsx @@ -1,7 +1,7 @@ import { Button, Typography } from "@material-ui/core"; import { useState } from "react"; -import InfoBox from "../../modal/swap/InfoBox"; import FeedbackDialog from "../../modal/feedback/FeedbackDialog"; +import InfoBox from "../../modal/swap/InfoBox"; export default function FeedbackInfoBox() { const [showDialog, setShowDialog] = useState(false); diff --git a/src-gui/src/renderer/components/pages/help/HelpPage.tsx b/src-gui/src/renderer/components/pages/help/HelpPage.tsx index b0dc67205..bdce7292c 100644 --- a/src-gui/src/renderer/components/pages/help/HelpPage.tsx +++ b/src-gui/src/renderer/components/pages/help/HelpPage.tsx @@ -1,9 +1,9 @@ import { Box, makeStyles } from "@material-ui/core"; import ContactInfoBox from "./ContactInfoBox"; -import FeedbackInfoBox from "./FeedbackInfoBox"; import DonateInfoBox from "./DonateInfoBox"; -import TorInfoBox from "./TorInfoBox"; +import FeedbackInfoBox from "./FeedbackInfoBox"; import RpcControlBox from "./RpcControlBox"; +import TorInfoBox from "./TorInfoBox"; const useStyles = makeStyles((theme) => ({ outer: { diff --git a/src-gui/src/renderer/components/pages/help/RpcControlBox.tsx b/src-gui/src/renderer/components/pages/help/RpcControlBox.tsx index 3ae2a3de3..1d36922d6 100644 --- a/src-gui/src/renderer/components/pages/help/RpcControlBox.tsx +++ b/src-gui/src/renderer/components/pages/help/RpcControlBox.tsx @@ -1,12 +1,12 @@ import { Box, makeStyles } from "@material-ui/core"; -import IpcInvokeButton from "renderer/components/IpcInvokeButton"; -import { useAppSelector } from "store/hooks"; -import StopIcon from "@material-ui/icons/Stop"; +import FolderOpenIcon from "@material-ui/icons/FolderOpen"; import PlayArrowIcon from "@material-ui/icons/PlayArrow"; +import StopIcon from "@material-ui/icons/Stop"; import { RpcProcessStateType } from "models/rpcModel"; +import IpcInvokeButton from "renderer/components/IpcInvokeButton"; +import { useAppSelector } from "store/hooks"; import InfoBox from "../../modal/swap/InfoBox"; import CliLogsBox from "../../other/RenderedCliLog"; -import FolderOpenIcon from "@material-ui/icons/FolderOpen"; const useStyles = makeStyles((theme) => ({ actionsOuter: { diff --git a/src-gui/src/renderer/components/pages/help/TorInfoBox.tsx b/src-gui/src/renderer/components/pages/help/TorInfoBox.tsx index 6e9c992d0..c4ef58e4e 100644 --- a/src-gui/src/renderer/components/pages/help/TorInfoBox.tsx +++ b/src-gui/src/renderer/components/pages/help/TorInfoBox.tsx @@ -1,8 +1,8 @@ import { Box, makeStyles, Typography } from "@material-ui/core"; +import PlayArrowIcon from "@material-ui/icons/PlayArrow"; +import StopIcon from "@material-ui/icons/Stop"; import IpcInvokeButton from "renderer/components/IpcInvokeButton"; import { useAppSelector } from "store/hooks"; -import StopIcon from "@material-ui/icons/Stop"; -import PlayArrowIcon from "@material-ui/icons/PlayArrow"; import InfoBox from "../../modal/swap/InfoBox"; import CliLogsBox from "../../other/RenderedCliLog"; diff --git a/src-gui/src/renderer/components/pages/history/HistoryPage.tsx b/src-gui/src/renderer/components/pages/history/HistoryPage.tsx index 15170d700..d4ec82d8a 100644 --- a/src-gui/src/renderer/components/pages/history/HistoryPage.tsx +++ b/src-gui/src/renderer/components/pages/history/HistoryPage.tsx @@ -1,8 +1,8 @@ import { Typography } from "@material-ui/core"; import { useIsSwapRunning } from "store/hooks"; -import HistoryTable from "./table/HistoryTable"; -import SwapDialog from "../../modal/swap/SwapDialog"; import SwapTxLockAlertsBox from "../../alert/SwapTxLockAlertsBox"; +import SwapDialog from "../../modal/swap/SwapDialog"; +import HistoryTable from "./table/HistoryTable"; export default function HistoryPage() { const showDialog = useIsSwapRunning(); diff --git a/src-gui/src/renderer/components/pages/swap/SwapPage.tsx b/src-gui/src/renderer/components/pages/swap/SwapPage.tsx index d60cadd67..bb3da695d 100644 --- a/src-gui/src/renderer/components/pages/swap/SwapPage.tsx +++ b/src-gui/src/renderer/components/pages/swap/SwapPage.tsx @@ -1,6 +1,6 @@ import { Box, makeStyles } from "@material-ui/core"; -import SwapWidget from "./SwapWidget"; import ApiAlertsBox from "./ApiAlertsBox"; +import SwapWidget from "./SwapWidget"; const useStyles = makeStyles((theme) => ({ outer: { diff --git a/src-gui/src/renderer/components/pages/wallet/WithdrawWidget.tsx b/src-gui/src/renderer/components/pages/wallet/WithdrawWidget.tsx index c43705d9b..6cbcf9f9b 100644 --- a/src-gui/src/renderer/components/pages/wallet/WithdrawWidget.tsx +++ b/src-gui/src/renderer/components/pages/wallet/WithdrawWidget.tsx @@ -1,13 +1,13 @@ import { Box, Button, makeStyles, Typography } from "@material-ui/core"; -import { useState } from "react"; import SendIcon from "@material-ui/icons/Send"; -import { useAppSelector, useIsRpcEndpointBusy } from "store/hooks"; import { RpcMethod } from "models/rpcModel"; +import { useState } from "react"; +import { SatsAmount } from "renderer/components/other/Units"; +import { useAppSelector, useIsRpcEndpointBusy } from "store/hooks"; import BitcoinIcon from "../../icons/BitcoinIcon"; +import InfoBox from "../../modal/swap/InfoBox"; import WithdrawDialog from "../../modal/wallet/WithdrawDialog"; import WalletRefreshButton from "./WalletRefreshButton"; -import InfoBox from "../../modal/swap/InfoBox"; -import { SatsAmount } from "renderer/components/other/Units"; const useStyles = makeStyles((theme) => ({ title: { diff --git a/src-gui/src/renderer/components/snackbar/GlobalSnackbarProvider.tsx b/src-gui/src/renderer/components/snackbar/GlobalSnackbarProvider.tsx index 30c9d7d39..bd2d071d1 100644 --- a/src-gui/src/renderer/components/snackbar/GlobalSnackbarProvider.tsx +++ b/src-gui/src/renderer/components/snackbar/GlobalSnackbarProvider.tsx @@ -1,11 +1,11 @@ +import { IconButton, styled } from "@material-ui/core"; +import { Close } from "@material-ui/icons"; import { MaterialDesignContent, SnackbarKey, SnackbarProvider, useSnackbar, } from "notistack"; -import { IconButton, styled } from "@material-ui/core"; -import { Close } from "@material-ui/icons"; import { ReactNode } from "react"; const StyledMaterialDesignContent = styled(MaterialDesignContent)(() => ({ diff --git a/src-gui/src/renderer/index.tsx b/src-gui/src/renderer/index.tsx index b0eaf3808..59221bcd6 100644 --- a/src-gui/src/renderer/index.tsx +++ b/src-gui/src/renderer/index.tsx @@ -1,18 +1,18 @@ import { render } from "react-dom"; import { Provider } from "react-redux"; -import { store } from "./store/storeRenderer"; -import { setRegistryProviders } from "store/features/providersSlice"; import { setAlerts } from "store/features/alertsSlice"; -import { setXmrPrice, setBtcPrice } from "store/features/ratesSlice"; +import { setRegistryProviders } from "store/features/providersSlice"; +import { setBtcPrice, setXmrPrice } from "store/features/ratesSlice"; +import logger from "../utils/logger"; import { fetchAlertsViaHttp, fetchBtcPrice, fetchProvidersViaHttp, fetchXmrPrice, } from "./api"; -import logger from "../utils/logger"; import App from "./components/App"; import { checkBitcoinBalance, getRawSwapInfos } from "./rpc"; +import { store } from "./store/storeRenderer"; setTimeout(() => { checkBitcoinBalance(); diff --git a/src-gui/src/store/combinedReducer.ts b/src-gui/src/store/combinedReducer.ts index 02e440f0c..5acd08396 100644 --- a/src-gui/src/store/combinedReducer.ts +++ b/src-gui/src/store/combinedReducer.ts @@ -1,9 +1,9 @@ -import swapReducer from "./features/swapSlice"; -import providersSlice from "./features/providersSlice"; -import torSlice from "./features/torSlice"; -import rpcSlice from "./features/rpcSlice"; import alertsSlice from "./features/alertsSlice"; +import providersSlice from "./features/providersSlice"; import ratesSlice from "./features/ratesSlice"; +import rpcSlice from "./features/rpcSlice"; +import swapReducer from "./features/swapSlice"; +import torSlice from "./features/torSlice"; export const reducers = { swap: swapReducer, diff --git a/src-gui/src/store/hooks.ts b/src-gui/src/store/hooks.ts index 91e7a985b..1a39f3485 100644 --- a/src-gui/src/store/hooks.ts +++ b/src-gui/src/store/hooks.ts @@ -1,6 +1,6 @@ +import { sortBy } from "lodash"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; import type { AppDispatch, RootState } from "renderer/store/storeRenderer"; -import { sortBy } from "lodash"; import { parseDateString } from "utils/parseUtils"; // Use throughout your app instead of plain `useDispatch` and `useSelector` diff --git a/src-gui/vite.config.ts b/src-gui/vite.config.ts index fac54f2e2..4a2412ecb 100644 --- a/src-gui/vite.config.ts +++ b/src-gui/vite.config.ts @@ -1,6 +1,6 @@ -import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import { internalIpV4 } from "internal-ip"; +import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; // @ts-expect-error process is a nodejs global From 6088bd18b4f5aa0b7c03ae2168144024c62d3310 Mon Sep 17 00:00:00 2001 From: binarybaron Date: Mon, 26 Aug 2024 15:03:03 +0200 Subject: [PATCH 2/9] chore: Add prettier plugin to dprint config --- dprint.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dprint.json b/dprint.json index 8f18b0bf1..4c675de9f 100644 --- a/dprint.json +++ b/dprint.json @@ -13,6 +13,7 @@ "plugins": [ "https://plugins.dprint.dev/markdown-0.13.1.wasm", "https://github.com/thomaseizinger/dprint-plugin-cargo-toml/releases/download/0.1.0/cargo-toml-0.1.0.wasm", - "https://plugins.dprint.dev/exec-0.3.5.json@d687dda57be0fe9a0088ccdaefa5147649ff24127d8b3ea227536c68ee7abeab" + "https://plugins.dprint.dev/exec-0.3.5.json@d687dda57be0fe9a0088ccdaefa5147649ff24127d8b3ea227536c68ee7abeab", + "https://plugins.dprint.dev/prettier-0.26.6.json@0118376786f37496e41bb19dbcfd1e7214e2dc859a55035c5e54d1107b4c9c57" ] } From fea1e66c642ec1d8109c620942fbf4ee673815d3 Mon Sep 17 00:00:00 2001 From: binarybaron Date: Mon, 26 Aug 2024 15:04:22 +0200 Subject: [PATCH 3/9] chore(gui): Upgrade @tauri-apps/api and add eslint --- src-gui/eslint.config.js | 20 ++++++++++++++++++++ src-gui/package.json | 8 ++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src-gui/eslint.config.js diff --git a/src-gui/eslint.config.js b/src-gui/eslint.config.js new file mode 100644 index 000000000..7259274dd --- /dev/null +++ b/src-gui/eslint.config.js @@ -0,0 +1,20 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import pluginReact from "eslint-plugin-react"; + +export default [ + { + ignores: ["node_modules", "dist"], + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + pluginReact.configs.flat.recommended, + { + files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], + languageOptions: { globals: globals.browser }, + rules: { + "react/react-in-jsx-scope": "off", + }, + }, +]; diff --git a/src-gui/package.json b/src-gui/package.json index c483bc8e5..4926bc02d 100644 --- a/src-gui/package.json +++ b/src-gui/package.json @@ -14,8 +14,7 @@ "@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-beta.0", - "@tauri-apps/plugin-shell": ">=2.0.0-beta.0", + "@tauri-apps/api": "2.0.0-rc.1", "humanize-duration": "^3.32.1", "lodash": "^4.17.21", "multiaddr": "^10.0.1", @@ -31,6 +30,7 @@ "virtua": "^0.33.2" }, "devDependencies": { + "@eslint/js": "^9.9.0", "@tauri-apps/cli": ">=2.0.0-beta.0", "@types/humanize-duration": "^3.27.4", "@types/lodash": "^4.17.6", @@ -39,8 +39,12 @@ "@types/react-dom": "^18.2.7", "@types/semver": "^7.5.8", "@vitejs/plugin-react": "^4.2.1", + "eslint": "^9.9.0", + "eslint-plugin-react": "^7.35.0", + "globals": "^15.9.0", "internal-ip": "^7.0.0", "typescript": "^5.2.2", + "typescript-eslint": "^8.1.0", "vite": "^5.3.1", "vite-tsconfig-paths": "^4.3.2" } From 4939d6352450224abae9007f8e58906d806494f1 Mon Sep 17 00:00:00 2001 From: binarybaron Date: Mon, 26 Aug 2024 15:18:11 +0200 Subject: [PATCH 4/9] refactor(swap, tauri_bindings): Overhaul API architecture and introduce Tauri events - Implement trait-based request handling in api/request.rs - Add Tauri bindings and event system in api/tauri_bindings.rs - Refactor CLI command parsing and execution in cli/command.rs - Update RPC methods to use new request handling approach - Emit Tauri events in swap/src/protocol/bob/swap.rs - Add typescript type bindings use typeshare crate --- swap/Cargo.toml | 5 +- swap/src/api.rs | 94 +++---- swap/src/api/request.rs | 489 ++++++++++++++++++++++++--------- swap/src/api/tauri_bindings.rs | 141 ++++++++++ swap/src/bin/swap.rs | 8 +- swap/src/bitcoin/cancel.rs | 3 + swap/src/bitcoin/timelocks.rs | 5 +- swap/src/cli/command.rs | 128 ++++----- swap/src/monero.rs | 2 + swap/src/network/quote.rs | 6 + swap/src/protocol/bob.rs | 9 + swap/src/protocol/bob/swap.rs | 130 ++++++++- swap/src/rpc/methods.rs | 63 ++--- swap/tests/rpc.rs | 10 +- 14 files changed, 780 insertions(+), 313 deletions(-) create mode 100644 swap/src/api/tauri_bindings.rs diff --git a/swap/Cargo.toml b/swap/Cargo.toml index f30af3a52..df47942bd 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -51,6 +51,7 @@ libp2p = { version = "0.42.2", default-features = false, features = [ "ping", "rendezvous", "identify", + "serde", ] } monero = { version = "0.12", features = [ "serde_support" ] } monero-rpc = { path = "../monero-rpc" } @@ -67,7 +68,7 @@ reqwest = { version = "0.12", features = [ ], default-features = false } rust_decimal = { version = "1", features = [ "serde-float" ] } rust_decimal_macros = "1" -serde = { version = "1", features = [ "derive" ] } +serde = { version = "1.0", features = [ "derive" ] } serde_cbor = "0.11" serde_json = "1" serde_with = { version = "1", features = [ "macros" ] } @@ -85,6 +86,7 @@ sqlx = { version = "0.6.3", features = [ ] } structopt = "0.3" strum = { version = "0.26", features = [ "derive" ] } +tauri = { version = "2.0.0-rc.1", features = [ "config-json5" ] } thiserror = "1" time = "0.3" tokio = { version = "1", features = [ @@ -118,6 +120,7 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [ "tracing-log", "json", ] } +typeshare = "1.0.3" url = { version = "2", features = [ "serde" ] } uuid = { version = "1.9", features = [ "serde", "v4" ] } void = "1" diff --git a/swap/src/api.rs b/swap/src/api.rs index 75427810b..dbb86f1e8 100644 --- a/swap/src/api.rs +++ b/swap/src/api.rs @@ -1,4 +1,5 @@ pub mod request; +pub mod tauri_bindings; use crate::cli::command::{Bitcoin, Monero, Tor}; use crate::database::open_db; use crate::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet}; @@ -13,10 +14,13 @@ use std::fmt; use std::future::Future; use std::net::SocketAddr; use std::path::PathBuf; -use std::sync::{Arc, Mutex, Once}; +use std::sync::{Arc, Mutex as SyncMutex, Once}; +use tauri::AppHandle; +use tauri_bindings::TauriHandle; use tokio::sync::{broadcast, broadcast::Sender, Mutex as TokioMutex, RwLock}; use tokio::task::JoinHandle; use url::Url; +use uuid::Uuid; static START: Once = Once::new(); @@ -33,8 +37,6 @@ pub struct Config { is_testnet: bool, } -use uuid::Uuid; - #[derive(Default)] pub struct PendingTaskList(TokioMutex>>); @@ -64,6 +66,13 @@ impl PendingTaskList { } } +/// The `SwapLock` manages the state of the current swap, ensuring that only one swap can be active at a time. +/// It includes: +/// - A lock for the current swap (`current_swap`) +/// - A broadcast channel for suspension signals (`suspension_trigger`) +/// +/// The `SwapLock` provides methods to acquire and release the swap lock, and to listen for suspension signals. +/// This ensures that swap operations do not overlap and can be safely suspended if needed. pub struct SwapLock { current_swap: RwLock>, suspension_trigger: Sender<()>, @@ -157,17 +166,22 @@ impl Default for SwapLock { } } -// workaround for warning over monero_rpc_process which we must own but not read -#[allow(dead_code)] +/// Holds shared data for different parts of the CLI. +/// +/// Some components are optional, allowing initialization of only necessary parts. +/// For example, the `history` command doesn't require wallet initialization. +/// +/// Many fields are wrapped in `Arc` for thread-safe sharing. #[derive(Clone)] pub struct Context { pub db: Arc, - bitcoin_wallet: Option>, - monero_wallet: Option>, - monero_rpc_process: Option>>, pub swap_lock: Arc, pub config: Config, pub tasks: Arc, + tauri_handle: Option, + bitcoin_wallet: Option>, + monero_wallet: Option>, + monero_rpc_process: Option>>, } #[allow(clippy::too_many_arguments)] @@ -216,7 +230,7 @@ impl Context { let monero_daemon_address = monero.apply_defaults(is_testnet); let (wlt, prc) = init_monero_wallet(data_dir.clone(), monero_daemon_address, env_config).await?; - (Some(Arc::new(wlt)), Some(prc)) + (Some(Arc::new(wlt)), Some(Arc::new(SyncMutex::new(prc)))) } else { (None, None) } @@ -228,7 +242,7 @@ impl Context { db: open_db(data_dir.join("sqlite")).await?, bitcoin_wallet, monero_wallet, - monero_rpc_process: monero_rpc_process.map(|prc| Arc::new(Mutex::new(prc))), + monero_rpc_process, config: Config { tor_socks5_port, namespace: XmrBtcNamespace::from_is_testnet(is_testnet), @@ -242,11 +256,18 @@ impl Context { }, swap_lock: Arc::new(SwapLock::new()), tasks: Arc::new(PendingTaskList::default()), + tauri_handle: None, }; Ok(context) } + pub fn with_tauri_handle(mut self, tauri_handle: AppHandle) -> Self { + self.tauri_handle = Some(TauriHandle::new(tauri_handle)); + + self + } + pub async fn for_harness( seed: Seed, env_config: EnvConfig, @@ -266,6 +287,7 @@ impl Context { monero_rpc_process: None, swap_lock: Arc::new(SwapLock::new()), tasks: Arc::new(PendingTaskList::default()), + tauri_handle: None, } } @@ -386,12 +408,6 @@ impl Config { #[cfg(test)] pub mod api_test { use super::*; - use crate::api::request::{Method, Request}; - - use libp2p::Multiaddr; - use request::BuyXmrArgs; - use std::str::FromStr; - use uuid::Uuid; pub const MULTI_ADDRESS: &str = "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi"; @@ -426,50 +442,4 @@ pub mod api_test { } } } - - impl Request { - pub fn buy_xmr(is_testnet: bool) -> Request { - let seller = Multiaddr::from_str(MULTI_ADDRESS).unwrap(); - let bitcoin_change_address = { - if is_testnet { - bitcoin::Address::from_str(BITCOIN_TESTNET_ADDRESS).unwrap() - } else { - bitcoin::Address::from_str(BITCOIN_MAINNET_ADDRESS).unwrap() - } - }; - - let monero_receive_address = { - if is_testnet { - monero::Address::from_str(MONERO_STAGENET_ADDRESS).unwrap() - } else { - monero::Address::from_str(MONERO_MAINNET_ADDRESS).unwrap() - } - }; - - Request::new(Method::BuyXmr(BuyXmrArgs { - seller, - bitcoin_change_address, - monero_receive_address, - swap_id: Uuid::new_v4(), - })) - } - - pub fn resume() -> Request { - Request::new(Method::Resume { - swap_id: Uuid::from_str(SWAP_ID).unwrap(), - }) - } - - pub fn cancel() -> Request { - Request::new(Method::CancelAndRefund { - swap_id: Uuid::from_str(SWAP_ID).unwrap(), - }) - } - - pub fn refund() -> Request { - Request::new(Method::CancelAndRefund { - swap_id: Uuid::from_str(SWAP_ID).unwrap(), - }) - } - } } diff --git a/swap/src/api/request.rs b/swap/src/api/request.rs index 93eaaebc3..0d7a37b9e 100644 --- a/swap/src/api/request.rs +++ b/swap/src/api/request.rs @@ -1,5 +1,7 @@ +use super::tauri_bindings::TauriHandle; +use crate::api::tauri_bindings::{TauriEmitter, TauriSwapProgressEvent}; use crate::api::Context; -use crate::bitcoin::{Amount, ExpiredTimelocks, TxLock}; +use crate::bitcoin::{CancelTimelock, ExpiredTimelocks, PunishTimelock, TxLock}; use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus}; use crate::libp2p_ext::MultiAddrExt; use crate::network::quote::{BidQuote, ZeroQuoteReceived}; @@ -10,11 +12,11 @@ use crate::{bitcoin, cli, monero, rpc}; use ::bitcoin::Txid; use anyhow::{bail, Context as AnyContext, Result}; use libp2p::core::Multiaddr; +use libp2p::PeerId; use qrcode::render::unicode; use qrcode::QrCode; use serde::{Deserialize, Serialize}; use serde_json::json; -use serde_json::Value as JsonValue; use std::cmp::min; use std::convert::TryInto; use std::future::Future; @@ -22,144 +24,334 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use tracing::Instrument; +use typeshare::typeshare; use uuid::Uuid; -#[derive(PartialEq, Debug)] -pub struct Request { - pub cmd: Method, - pub log_reference: Option, +/// This trait is implemented by all types of request args that +/// the CLI can handle. +/// It provides a unified abstraction that can be useful for generics. +#[allow(async_fn_in_trait)] +pub trait Request { + type Response: Serialize; + async fn request(self, ctx: Arc) -> Result; } -#[derive(Debug, Eq, PartialEq)] +// BuyXmr +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct BuyXmrArgs { + #[typeshare(serialized_as = "string")] pub seller: Multiaddr, + #[typeshare(serialized_as = "string")] pub bitcoin_change_address: bitcoin::Address, + #[typeshare(serialized_as = "string")] pub monero_receive_address: monero::Address, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct BuyXmrResponse { + #[typeshare(serialized_as = "string")] pub swap_id: Uuid, + pub quote: BidQuote, } -#[derive(Debug, Eq, PartialEq)] -pub struct ResumeArgs { +impl Request for BuyXmrArgs { + type Response = BuyXmrResponse; + + async fn request(self, ctx: Arc) -> Result { + buy_xmr(self, ctx).await + } +} + +// ResumeSwap +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ResumeSwapArgs { + #[typeshare(serialized_as = "string")] pub swap_id: Uuid, } -#[derive(Debug, Eq, PartialEq)] +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct ResumeSwapResponse { + pub result: String, +} + +impl Request for ResumeSwapArgs { + type Response = ResumeSwapResponse; + + async fn request(self, ctx: Arc) -> Result { + resume_swap(self, ctx).await + } +} + +// CancelAndRefund +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct CancelAndRefundArgs { + #[typeshare(serialized_as = "string")] pub swap_id: Uuid, } -#[derive(Debug, Eq, PartialEq)] +impl Request for CancelAndRefundArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + cancel_and_refund(self, ctx).await + } +} + +// MoneroRecovery +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct MoneroRecoveryArgs { + #[typeshare(serialized_as = "string")] pub swap_id: Uuid, } +impl Request for MoneroRecoveryArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + monero_recovery(self, ctx).await + } +} + +// WithdrawBtc +#[typeshare] #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct WithdrawBtcArgs { - pub amount: Option, + #[typeshare(serialized_as = "number")] + #[serde(default, with = "::bitcoin::util::amount::serde::as_sat::opt")] + pub amount: Option, + #[typeshare(serialized_as = "string")] pub address: bitcoin::Address, } -#[derive(Debug, Eq, PartialEq)] -pub struct BalanceArgs { - pub force_refresh: bool, +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct WithdrawBtcResponse { + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub amount: bitcoin::Amount, + pub txid: String, +} + +impl Request for WithdrawBtcArgs { + type Response = WithdrawBtcResponse; + + async fn request(self, ctx: Arc) -> Result { + withdraw_btc(self, ctx).await + } } -#[derive(Debug, Eq, PartialEq)] +// ListSellers +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct ListSellersArgs { + #[typeshare(serialized_as = "string")] pub rendezvous_point: Multiaddr, } -#[derive(Debug, Eq, PartialEq)] -pub struct StartDaemonArgs { - pub server_address: Option, -} +impl Request for ListSellersArgs { + type Response = serde_json::Value; -#[derive(Debug, Eq, PartialEq)] -pub struct GetSwapInfoArgs { - pub swap_id: Uuid, + async fn request(self, ctx: Arc) -> Result { + list_sellers(self, ctx).await + } } -#[derive(Serialize, Deserialize, Debug)] -pub struct ResumeSwapResponse { - pub result: String, +// StartDaemon +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct StartDaemonArgs { + #[typeshare(serialized_as = "string")] + pub server_address: Option, } -#[derive(Serialize, Deserialize, Debug)] -pub struct BalanceResponse { - pub balance: u64, // in satoshis -} +impl Request for StartDaemonArgs { + type Response = serde_json::Value; -#[derive(Serialize, Deserialize, Debug)] -pub struct BuyXmrResponse { - pub swap_id: String, - pub quote: BidQuote, // You'll need to import or define BidQuote + async fn request(self, ctx: Arc) -> Result { + start_daemon(self, (*ctx).clone()).await + } } -#[derive(Serialize, Deserialize, Debug)] -pub struct GetHistoryResponse { - swaps: Vec<(Uuid, String)>, +// GetSwapInfo +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct GetSwapInfoArgs { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, } +#[typeshare] #[derive(Serialize)] pub struct GetSwapInfoResponse { + #[typeshare(serialized_as = "string")] pub swap_id: Uuid, pub seller: Seller, pub completed: bool, pub start_date: String, + #[typeshare(serialized_as = "string")] pub state_name: String, - pub xmr_amount: u64, - pub btc_amount: u64, + #[typeshare(serialized_as = "number")] + pub xmr_amount: monero::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub btc_amount: bitcoin::Amount, + #[typeshare(serialized_as = "string")] pub tx_lock_id: Txid, - pub tx_cancel_fee: u64, - pub tx_refund_fee: u64, - pub tx_lock_fee: u64, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub tx_cancel_fee: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub tx_refund_fee: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub tx_lock_fee: bitcoin::Amount, pub btc_refund_address: String, - pub cancel_timelock: u32, - pub punish_timelock: u32, + pub cancel_timelock: CancelTimelock, + pub punish_timelock: PunishTimelock, pub timelock: Option, } +impl Request for GetSwapInfoArgs { + type Response = GetSwapInfoResponse; + + async fn request(self, ctx: Arc) -> Result { + get_swap_info(self, ctx).await + } +} + +// Balance +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct BalanceArgs { + pub force_refresh: bool, +} + +#[typeshare] #[derive(Serialize, Deserialize, Debug)] -pub struct WithdrawBtcResponse { - amount: u64, - txid: String, +pub struct BalanceResponse { + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub balance: bitcoin::Amount, +} + +impl Request for BalanceArgs { + type Response = BalanceResponse; + + async fn request(self, ctx: Arc) -> Result { + get_balance(self, ctx).await + } +} + +// GetHistory +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct GetHistoryArgs; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetHistoryEntry { + #[typeshare(serialized_as = "string")] + swap_id: Uuid, + state: String, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct GetHistoryResponse { + pub swaps: Vec, } -#[derive(Serialize, Deserialize)] +impl Request for GetHistoryArgs { + type Response = GetHistoryResponse; + + async fn request(self, ctx: Arc) -> Result { + get_history(ctx).await + } +} + +// Additional structs +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] pub struct Seller { - pub peer_id: String, - pub addresses: Vec, -} - -// TODO: We probably dont even need this. -// We can just call the method directly from the RPC server, the CLI and the Tauri connector -#[derive(Debug, PartialEq)] -pub enum Method { - BuyXmr(BuyXmrArgs), - Resume(ResumeArgs), - CancelAndRefund(CancelAndRefundArgs), - MoneroRecovery(MoneroRecoveryArgs), - History, - Config, - WithdrawBtc(WithdrawBtcArgs), - Balance(BalanceArgs), - ListSellers(ListSellersArgs), - ExportBitcoinWallet, - SuspendCurrentSwap, - StartDaemon(StartDaemonArgs), - GetCurrentSwap, - GetSwapInfo(GetSwapInfoArgs), - GetRawStates, + #[typeshare(serialized_as = "string")] + pub peer_id: PeerId, + pub addresses: Vec, +} + +// Suspend current swap +#[derive(Debug, Deserialize)] +pub struct SuspendCurrentSwapArgs; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct SuspendCurrentSwapResponse { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + +impl Request for SuspendCurrentSwapArgs { + type Response = SuspendCurrentSwapResponse; + + async fn request(self, ctx: Arc) -> Result { + suspend_current_swap(ctx).await + } +} + +pub struct GetCurrentSwap; + +impl Request for GetCurrentSwap { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + get_current_swap(ctx).await + } +} + +pub struct GetConfig; + +impl Request for GetConfig { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + get_config(ctx).await + } +} + +pub struct ExportBitcoinWalletArgs; + +impl Request for ExportBitcoinWalletArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + export_bitcoin_wallet(ctx).await + } +} + +pub struct GetConfigArgs; + +impl Request for GetConfigArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + get_config(ctx).await + } } #[tracing::instrument(fields(method = "suspend_current_swap"), skip(context))] -pub async fn suspend_current_swap(context: Arc) -> Result { +pub async fn suspend_current_swap(context: Arc) -> Result { let swap_id = context.swap_lock.get_current_swap_id().await; if let Some(id_value) = swap_id { context.swap_lock.send_suspend_signal().await?; - Ok(json!({ "swapId": id_value })) + Ok(SuspendCurrentSwapResponse { swap_id: id_value }) } else { bail!("No swap is currently running") } @@ -191,7 +383,7 @@ pub async fn get_swap_info( let state = context.db.get_state(args.swap_id).await?; let is_completed = state.swap_finished(); - let peerId = context + let peer_id = context .db .get_peer_id(args.swap_id) .await @@ -199,14 +391,13 @@ pub async fn get_swap_info( let addresses = context .db - .get_addresses(peerId) + .get_addresses(peer_id) .await .with_context(|| "Could not get addressess")?; let start_date = context.db.get_swap_start_date(args.swap_id).await?; let swap_state: BobState = state.try_into()?; - let state_name = format!("{}", swap_state); let ( xmr_amount, @@ -226,15 +417,13 @@ pub async fn get_swap_info( .find_map(|state| { if let State::Bob(BobState::SwapSetupCompleted(state2)) = state { let xmr_amount = state2.xmr; - let btc_amount = state2.tx_lock.lock_amount().to_sat(); - let tx_cancel_fee = state2.tx_cancel_fee.to_sat(); - let tx_refund_fee = state2.tx_refund_fee.to_sat(); + let btc_amount = state2.tx_lock.lock_amount(); + let tx_cancel_fee = state2.tx_cancel_fee; + let tx_refund_fee = state2.tx_refund_fee; let tx_lock_id = state2.tx_lock.txid(); let btc_refund_address = state2.refund_address.to_string(); if let Ok(tx_lock_fee) = state2.tx_lock.fee() { - let tx_lock_fee = tx_lock_fee.to_sat(); - Some(( xmr_amount, btc_amount, @@ -255,7 +444,7 @@ pub async fn get_swap_info( }) .with_context(|| "Did not find SwapSetupCompleted state for swap")?; - let timelock = match swap_state { + let timelock = match swap_state.clone() { BobState::Started { .. } | BobState::SafelyAborted | BobState::SwapSetupCompleted(_) => { None } @@ -276,21 +465,21 @@ pub async fn get_swap_info( Ok(GetSwapInfoResponse { swap_id: args.swap_id, seller: Seller { - peer_id: peerId.to_string(), - addresses, + peer_id, + addresses: addresses.iter().map(|a| a.to_string()).collect(), }, completed: is_completed, start_date, - state_name, - xmr_amount: xmr_amount.as_piconero(), + state_name: format!("{}", swap_state), + xmr_amount, btc_amount, tx_lock_id, tx_cancel_fee, tx_refund_fee, tx_lock_fee, btc_refund_address: btc_refund_address.to_string(), - cancel_timelock: cancel_timelock.into(), - punish_timelock: punish_timelock.into(), + cancel_timelock, + punish_timelock, timelock, }) } @@ -299,13 +488,15 @@ pub async fn get_swap_info( pub async fn buy_xmr( buy_xmr: BuyXmrArgs, context: Arc, -) -> Result { +) -> Result { let BuyXmrArgs { seller, bitcoin_change_address, monero_receive_address, - swap_id, } = buy_xmr; + + let swap_id = Uuid::new_v4(); + let bitcoin_wallet = Arc::clone( context .bitcoin_wallet @@ -358,6 +549,9 @@ pub async fn buy_xmr( _ = 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 { @@ -382,16 +576,28 @@ pub async fn buy_xmr( .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 { 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"); }, event_loop_result = event_loop => { @@ -416,6 +622,7 @@ pub async fn buy_xmr( max_givable, || bitcoin_wallet.sync(), estimate_fee, + context.tauri_handle.clone(), ); let (amount, fees) = match determine_amount.await { @@ -442,7 +649,7 @@ pub async fn buy_xmr( monero_receive_address, bitcoin_change_address, amount, - ); + ).with_event_emitter(context.tauri_handle.clone()); bob::run(swap).await } => { @@ -463,18 +670,24 @@ pub async fn buy_xmr( .release_swap_lock() .await .expect("Could not release swap lock"); + + context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); + Ok::<_, anyhow::Error>(()) }.in_current_span()).await; - Ok(json!({ - "swapId": swap_id.to_string(), - "quote": bid_quote, - })) + Ok(BuyXmrResponse { + swap_id, + quote: bid_quote, + }) } #[tracing::instrument(fields(method = "resume_swap"), skip(context))] -pub async fn resume_swap(resume: ResumeArgs, context: Arc) -> Result { - let ResumeArgs { swap_id } = resume; +pub async fn resume_swap( + resume: ResumeSwapArgs, + context: Arc, +) -> Result { + let ResumeSwapArgs { swap_id } = resume; context.swap_lock.acquire_swap_lock(swap_id).await?; let seller_peer_id = context.db.get_peer_id(swap_id).await?; @@ -531,7 +744,8 @@ pub async fn resume_swap(resume: ResumeArgs, context: Arc) -> Result) -> Result { 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"); }, @@ -571,13 +788,17 @@ pub async fn resume_swap(resume: ResumeArgs, context: Arc) -> Result(()) } .in_current_span(), ).await; - Ok(json!({ - "result": "ok", - })) + + Ok(ResumeSwapResponse { + result: "OK".to_string(), + }) } #[tracing::instrument(fields(method = "cancel_and_refund"), skip(context))] @@ -602,6 +823,10 @@ pub async fn cancel_and_refund( .await .expect("Could not release swap lock"); + context + .tauri_handle + .emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); + state.map(|state| { json!({ "result": state, @@ -612,10 +837,13 @@ pub async fn cancel_and_refund( #[tracing::instrument(fields(method = "get_history"), skip(context))] pub async fn get_history(context: Arc) -> Result { let swaps = context.db.all().await?; - let mut vec: Vec<(Uuid, String)> = Vec::new(); + let mut vec: Vec = Vec::new(); for (swap_id, state) in swaps { let state: BobState = state.try_into()?; - vec.push((swap_id, state.to_string())); + vec.push(GetHistoryEntry { + swap_id, + state: state.to_string(), + }) } Ok(GetHistoryResponse { swaps: vec }) @@ -659,7 +887,7 @@ pub async fn withdraw_btc( .context("Could not get Bitcoin wallet")?; let amount = match amount { - Some(amount) => Amount::from_sat(amount), + Some(amount) => amount, None => { bitcoin_wallet .max_giveable(address.script_pubkey().len()) @@ -677,7 +905,7 @@ pub async fn withdraw_btc( Ok(WithdrawBtcResponse { txid: signed_tx.txid().to_string(), - amount: amount.to_sat(), + amount, }) } @@ -728,7 +956,7 @@ pub async fn get_balance(balance: BalanceArgs, context: Arc) -> Result< } Ok(BalanceResponse { - balance: bitcoin_balance.to_sat(), + balance: bitcoin_balance, }) } @@ -838,26 +1066,6 @@ pub async fn get_current_swap(context: Arc) -> Result Request { - Request { - cmd, - log_reference: None, - } - } - - pub fn with_id(cmd: Method, id: Option) -> Request { - Request { - cmd, - log_reference: id, - } - } - - pub async fn call(self, _: Arc) -> Result { - unreachable!("This function should never be called") - } -} - fn qr_code(value: &impl ToString) -> Result { let code = QrCode::new(value.to_string())?; let qr_code = code @@ -868,6 +1076,7 @@ fn qr_code(value: &impl ToString) -> Result { Ok(qr_code) } +#[allow(clippy::too_many_arguments)] pub async fn determine_btc_to_swap( json: bool, bid_quote: BidQuote, @@ -876,18 +1085,19 @@ pub async fn determine_btc_to_swap( max_giveable_fn: FMG, sync: FS, estimate_fee: FFE, -) -> Result<(Amount, Amount)> + event_emitter: Option, +) -> Result<(bitcoin::Amount, bitcoin::Amount)> where - TB: Future>, + TB: Future>, FB: Fn() -> TB, - TMG: Future>, + TMG: Future>, FMG: Fn() -> TMG, TS: Future>, FS: Fn() -> TS, - FFE: Fn(Amount) -> TFE, - TFE: Future>, + FFE: Fn(bitcoin::Amount) -> TFE, + TFE: Future>, { - if bid_quote.max_quantity == Amount::ZERO { + if bid_quote.max_quantity == bitcoin::Amount::ZERO { bail!(ZeroQuoteReceived) } @@ -901,7 +1111,7 @@ where sync().await?; let mut max_giveable = max_giveable_fn().await?; - if max_giveable == Amount::ZERO || max_giveable < bid_quote.min_quantity { + if max_giveable == bitcoin::Amount::ZERO || max_giveable < bid_quote.min_quantity { let deposit_address = get_new_address.await?; let minimum_amount = bid_quote.min_quantity; let maximum_amount = bid_quote.max_quantity; @@ -933,6 +1143,19 @@ where "Waiting for Bitcoin deposit", ); + // TODO: Use the real swap id here + event_emitter.emit_swap_progress_event( + Uuid::new_v4(), + TauriSwapProgressEvent::WaitingForBtcDeposit { + 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.clone(), + }, + ); + max_giveable = loop { sync().await?; let new_max_givable = max_giveable_fn().await?; diff --git a/swap/src/api/tauri_bindings.rs b/swap/src/api/tauri_bindings.rs new file mode 100644 index 000000000..f705a3c5c --- /dev/null +++ b/swap/src/api/tauri_bindings.rs @@ -0,0 +1,141 @@ +/** + * TOOD: Perhaps we should move this to the `src-tauri` package. + */ +use anyhow::Result; +use bitcoin::Txid; +use serde::Serialize; +use std::sync::Arc; +use tauri::{AppHandle, Emitter}; +use typeshare::typeshare; +use uuid::Uuid; + +use crate::{monero, network::quote::BidQuote}; + +static SWAP_PROGRESS_EVENT_NAME: &str = "swap-progress-update"; + +#[derive(Debug, Clone)] +pub struct TauriHandle(Arc); + +impl TauriHandle { + pub fn new(tauri_handle: AppHandle) -> Self { + Self(Arc::new(tauri_handle)) + } + + pub fn emit_tauri_event(&self, event: &str, payload: S) -> Result<()> { + self.0.emit(event, payload).map_err(|e| e.into()) + } +} + +pub trait TauriEmitter { + fn emit_tauri_event( + &self, + event: &str, + payload: S, + ) -> Result<()>; + + fn emit_swap_progress_event(&self, swap_id: Uuid, event: TauriSwapProgressEvent) { + let _ = self.emit_tauri_event( + SWAP_PROGRESS_EVENT_NAME, + TauriSwapProgressEventWrapper { swap_id, event }, + ); + } +} + +impl TauriEmitter for TauriHandle { + fn emit_tauri_event( + &self, + event: &str, + payload: S, + ) -> Result<()> { + self.emit_tauri_event(event, payload) + } +} + +impl TauriEmitter for Option { + fn emit_tauri_event( + &self, + event: &str, + payload: S, + ) -> Result<()> { + match self { + Some(tauri) => tauri.emit_tauri_event(event, payload), + None => Ok(()), + } + } +} + +#[derive(Serialize, Clone)] +#[typeshare] +pub struct TauriSwapProgressEventWrapper { + #[typeshare(serialized_as = "string")] + swap_id: Uuid, + event: TauriSwapProgressEvent, +} + +#[derive(Serialize, Clone)] +#[serde(tag = "type", content = "content")] +#[typeshare] +pub enum TauriSwapProgressEvent { + Initiated, + ReceivedQuote(BidQuote), + WaitingForBtcDeposit { + #[typeshare(serialized_as = "string")] + deposit_address: bitcoin::Address, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + max_giveable: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + min_deposit_until_swap_will_start: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + max_deposit_until_maximum_amount_is_reached: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + min_bitcoin_lock_tx_fee: bitcoin::Amount, + quote: BidQuote, + }, + Started { + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + btc_lock_amount: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + btc_tx_lock_fee: bitcoin::Amount, + }, + BtcLockTxInMempool { + #[typeshare(serialized_as = "string")] + btc_lock_txid: bitcoin::Txid, + #[typeshare(serialized_as = "number")] + btc_lock_confirmations: u64, + }, + XmrLockTxInMempool { + #[typeshare(serialized_as = "string")] + xmr_lock_txid: monero::TxHash, + #[typeshare(serialized_as = "number")] + xmr_lock_tx_confirmations: u64, + }, + XmrLocked, + BtcRedeemed, + XmrRedeemInMempool { + #[typeshare(serialized_as = "string")] + xmr_redeem_txid: monero::TxHash, + #[typeshare(serialized_as = "string")] + xmr_redeem_address: monero::Address, + }, + BtcCancelled { + #[typeshare(serialized_as = "string")] + btc_cancel_txid: Txid, + }, + BtcRefunded { + #[typeshare(serialized_as = "string")] + btc_refund_txid: Txid, + }, + BtcPunished, + AttemptingCooperativeRedeem, + CooperativeRedeemAccepted, + CooperativeRedeemRejected { + reason: String, + }, + Released, +} diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 82d9be73d..0db97f6c8 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -11,10 +11,10 @@ #![forbid(unsafe_code)] #![allow(non_snake_case)] -use swap::cli::command::{parse_args_and_apply_defaults, ParseResult}; -use swap::common::check_latest_version; use anyhow::Result; use std::env; +use swap::cli::command::{parse_args_and_apply_defaults, ParseResult}; +use swap::common::check_latest_version; #[tokio::main] pub async fn main() -> Result<()> { @@ -23,7 +23,9 @@ pub async fn main() -> Result<()> { } match parse_args_and_apply_defaults(env::args_os()).await? { - ParseResult::Success => {} + ParseResult::Success(context) => { + context.tasks.wait_for_tasks().await?; + } ParseResult::PrintAndExitZero { message } => { println!("{}", message); std::process::exit(0); diff --git a/swap/src/bitcoin/cancel.rs b/swap/src/bitcoin/cancel.rs index 34612148a..cde8e2100 100644 --- a/swap/src/bitcoin/cancel.rs +++ b/swap/src/bitcoin/cancel.rs @@ -16,6 +16,7 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::fmt; use std::ops::Add; +use typeshare::typeshare; /// Represent a timelock, expressed in relative block height as defined in /// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki). @@ -23,6 +24,7 @@ use std::ops::Add; /// mined. #[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(transparent)] +#[typeshare] pub struct CancelTimelock(u32); impl From for u32 { @@ -69,6 +71,7 @@ impl fmt::Display for CancelTimelock { /// mined. #[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(transparent)] +#[typeshare] pub struct PunishTimelock(u32); impl From for u32 { diff --git a/swap/src/bitcoin/timelocks.rs b/swap/src/bitcoin/timelocks.rs index 427bef802..62e11e92f 100644 --- a/swap/src/bitcoin/timelocks.rs +++ b/swap/src/bitcoin/timelocks.rs @@ -3,6 +3,7 @@ use bdk::electrum_client::HeaderNotification; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use std::ops::Add; +use typeshare::typeshare; /// Represent a block height, or block number, expressed in absolute block /// count. E.g. The transaction was included in block #655123, 655123 block @@ -37,7 +38,9 @@ impl Add for BlockHeight { } } -#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)] +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[serde(tag = "type", content = "content")] pub enum ExpiredTimelocks { None { blocks_left: u32 }, Cancel { blocks_left: u32 }, diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index b00e65e74..fa3740f7f 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -1,8 +1,8 @@ use crate::api::request::{ buy_xmr, cancel_and_refund, export_bitcoin_wallet, get_balance, get_config, get_history, list_sellers, monero_recovery, resume_swap, start_daemon, withdraw_btc, BalanceArgs, - BuyXmrArgs, CancelAndRefundArgs, ListSellersArgs, MoneroRecoveryArgs, ResumeArgs, - StartDaemonArgs, WithdrawBtcArgs, + BuyXmrArgs, CancelAndRefundArgs, ExportBitcoinWalletArgs, GetConfigArgs, GetHistoryArgs, + ListSellersArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, StartDaemonArgs, WithdrawBtcArgs, }; use crate::api::Context; use crate::bitcoin::{bitcoin_address, Amount}; @@ -38,7 +38,7 @@ const DEFAULT_TOR_SOCKS5_PORT: &str = "9050"; #[derive(Debug)] pub enum ParseResult { /// The arguments we were invoked in. - Success, + Success(Arc), /// A flag or command was given that does not need further processing other /// than printing the provided message. /// @@ -65,7 +65,7 @@ where let json = args.json; let is_testnet = args.testnet; let data = args.data; - let result = match args.cmd { + let result: Result> = match args.cmd { CliCommand::BuyXmr { seller: Seller { seller }, bitcoin, @@ -93,36 +93,33 @@ where .await?, ); - buy_xmr( - BuyXmrArgs { - seller, - bitcoin_change_address, - monero_receive_address, - swap_id: Uuid::new_v4(), - }, - context, - ) + BuyXmrArgs { + seller, + bitcoin_change_address, + monero_receive_address, + } + .request(context.clone()) .await?; - Ok(()) as Result<(), anyhow::Error> + Ok(context) } CliCommand::History => { let context = Arc::new( Context::build(None, None, None, data, is_testnet, debug, json, None).await?, ); - get_history(context).await?; + GetHistoryArgs {}.request(context.clone()).await?; - Ok(()) + Ok(context) } CliCommand::Config => { let context = Arc::new( Context::build(None, None, None, data, is_testnet, debug, json, None).await?, ); - get_config(context).await?; + GetConfigArgs {}.request(context.clone()).await?; - Ok(()) + Ok(context) } CliCommand::Balance { bitcoin } => { let context = Arc::new( @@ -139,15 +136,13 @@ where .await?, ); - get_balance( - BalanceArgs { - force_refresh: true, - }, - context, - ) + BalanceArgs { + force_refresh: true, + } + .request(context.clone()) .await?; - Ok(()) + Ok(context) } CliCommand::StartDaemon { server_address, @@ -155,21 +150,25 @@ where monero, tor, } => { - let context = Context::build( - Some(bitcoin), - Some(monero), - Some(tor), - data, - is_testnet, - debug, - json, - server_address, - ) - .await?; + let context = Arc::new( + Context::build( + Some(bitcoin), + Some(monero), + Some(tor), + data, + is_testnet, + debug, + json, + server_address, + ) + .await?, + ); - start_daemon(StartDaemonArgs { server_address }, context).await?; + StartDaemonArgs { server_address } + .request(context.clone()) + .await?; - Ok(()) + Ok(context) } CliCommand::WithdrawBtc { bitcoin, @@ -192,16 +191,11 @@ where .await?, ); - withdraw_btc( - WithdrawBtcArgs { - amount: amount.map(Amount::to_sat), - address, - }, - context, - ) - .await?; + WithdrawBtcArgs { amount, address } + .request(context.clone()) + .await?; - Ok(()) + Ok(context) } CliCommand::Resume { swap_id: SwapId { swap_id }, @@ -223,9 +217,9 @@ where .await?, ); - resume_swap(ResumeArgs { swap_id }, context).await?; + ResumeSwapArgs { swap_id }.request(context.clone()).await?; - Ok(()) + Ok(context) } CliCommand::CancelAndRefund { swap_id: SwapId { swap_id }, @@ -246,9 +240,11 @@ where .await?, ); - cancel_and_refund(CancelAndRefundArgs { swap_id }, context).await?; + CancelAndRefundArgs { swap_id } + .request(context.clone()) + .await?; - Ok(()) + Ok(context) } CliCommand::ListSellers { rendezvous_point, @@ -258,9 +254,11 @@ where Context::build(None, None, Some(tor), data, is_testnet, debug, json, None).await?, ); - list_sellers(ListSellersArgs { rendezvous_point }, context).await?; + ListSellersArgs { rendezvous_point } + .request(context.clone()) + .await?; - Ok(()) + Ok(context) } CliCommand::ExportBitcoinWallet { bitcoin } => { let context = Arc::new( @@ -277,9 +275,9 @@ where .await?, ); - export_bitcoin_wallet(context).await?; + ExportBitcoinWalletArgs {}.request(context.clone()).await?; - Ok(()) + Ok(context) } CliCommand::MoneroRecovery { swap_id: SwapId { swap_id }, @@ -288,15 +286,15 @@ where Context::build(None, None, None, data, is_testnet, debug, json, None).await?, ); - monero_recovery(MoneroRecoveryArgs { swap_id }, context).await?; + MoneroRecoveryArgs { swap_id } + .request(context.clone()) + .await?; - Ok(()) + Ok(context) } }; - result?; - - Ok(ParseResult::Success) + Ok(ParseResult::Success(result?)) } #[derive(structopt::StructOpt, Debug)] @@ -1067,18 +1065,14 @@ mod tests { let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); let (is_testnet, debug, json) = (true, true, false); - let (expected_config, expected_request) = ( - Config::default(is_testnet, None, debug, json), - Request::resume(), - ); + let expected_config = Config::default(is_testnet, None, debug, json); - let (actual_config, actual_request) = match args { - ParseResult::Context(context, request) => (context.config.clone(), request), + let actual_config = match args { + ParseResult::Context(context, request) => context.config.clone(), _ => panic!("Couldn't parse result"), }; assert_eq!(actual_config, expected_config); - assert_eq!(actual_request, Box::new(expected_request)); // given_buy_xmr_on_mainnet_with_json_then_json_set let raw_ars = vec![ diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 8205e75f9..c02bf5cb2 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -4,6 +4,7 @@ mod wallet_rpc; pub use ::monero::network::Network; pub use ::monero::{Address, PrivateKey, PublicKey}; pub use curve25519_dalek::scalar::Scalar; +use typeshare::typeshare; pub use wallet::Wallet; pub use wallet_rpc::{WalletRpc, WalletRpcProcess}; @@ -86,6 +87,7 @@ impl From for PublicKey { pub struct PublicViewKey(PublicKey); #[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd)] +#[typeshare(serialized_as = "number")] pub struct Amount(u64); // Median tx fees on Monero as found here: https://www.monero.how/monero-transaction-fees, XMR 0.000_008 * 2 (to be on the safe side) diff --git a/swap/src/network/quote.rs b/swap/src/network/quote.rs index caf99e09c..d5eccf0f4 100644 --- a/swap/src/network/quote.rs +++ b/swap/src/network/quote.rs @@ -7,6 +7,7 @@ use libp2p::request_response::{ }; use libp2p::PeerId; use serde::{Deserialize, Serialize}; +use typeshare::typeshare; const PROTOCOL: &str = "/comit/xmr/btc/bid-quote/1.0.0"; pub type OutEvent = RequestResponseEvent<(), BidQuote>; @@ -25,15 +26,20 @@ impl ProtocolName for BidQuoteProtocol { /// Represents a quote for buying XMR. #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[typeshare] pub struct BidQuote { /// The price at which the maker is willing to buy at. #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + #[typeshare(serialized_as = "number")] pub price: bitcoin::Amount, /// The minimum quantity the maker is willing to buy. + /// #[typeshare(serialized_as = "number")] #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + #[typeshare(serialized_as = "number")] pub min_quantity: bitcoin::Amount, /// The maximum quantity the maker is willing to buy. #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + #[typeshare(serialized_as = "number")] pub max_quantity: bitcoin::Amount, } diff --git a/swap/src/protocol/bob.rs b/swap/src/protocol/bob.rs index 0ef3e241c..d9d90ac97 100644 --- a/swap/src/protocol/bob.rs +++ b/swap/src/protocol/bob.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::Result; use uuid::Uuid; +use crate::api::tauri_bindings::TauriHandle; use crate::protocol::Database; use crate::{bitcoin, cli, env, monero}; @@ -22,6 +23,7 @@ pub struct Swap { pub env_config: env::Config, pub id: Uuid, pub monero_receive_address: monero::Address, + pub event_emitter: Option, } impl Swap { @@ -49,6 +51,7 @@ impl Swap { env_config, id, monero_receive_address, + event_emitter: None, } } @@ -73,6 +76,12 @@ impl Swap { env_config, id, monero_receive_address, + event_emitter: None, }) } + + pub fn with_event_emitter(mut self, event_emitter: Option) -> Self { + self.event_emitter = event_emitter; + self + } } diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 5db986904..4551414ec 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -1,3 +1,4 @@ +use crate::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent}; use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund}; use crate::cli::EventLoopHandle; use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected}; @@ -48,6 +49,7 @@ pub async fn run_until( swap.bitcoin_wallet.as_ref(), swap.monero_wallet.as_ref(), swap.monero_receive_address, + swap.event_emitter.clone(), ) .await?; @@ -73,6 +75,7 @@ async fn next_state( bitcoin_wallet: &bitcoin::Wallet, monero_wallet: &monero::Wallet, monero_receive_address: monero::Address, + event_emitter: Option, ) -> Result { tracing::debug!(%state, "Advancing state"); @@ -120,6 +123,16 @@ async fn next_state( .sign_and_finalize(tx_lock.clone().into()) .await .context("Failed to sign Bitcoin lock transaction")?; + + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::Started { + btc_lock_amount: tx_lock.lock_amount(), + // TODO: Replace this with the actual fee + btc_tx_lock_fee: bitcoin::Amount::ZERO, + }, + ); + let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?; BobState::BtcLocked { @@ -133,6 +146,15 @@ async fn next_state( state3, monero_wallet_restore_blockheight, } => { + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::BtcLockTxInMempool { + btc_lock_txid: state3.tx_lock_id(), + // TODO: Replace this with the actual confirmations + btc_lock_confirmations: 0, + }, + ); + let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; if let ExpiredTimelocks::None { .. } = state3.expired_timelock(bitcoin_wallet).await? { @@ -188,6 +210,14 @@ async fn next_state( lock_transfer_proof, monero_wallet_restore_blockheight, } => { + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::XmrLockTxInMempool { + xmr_lock_txid: lock_transfer_proof.tx_hash(), + xmr_lock_tx_confirmations: 0, + }, + ); + let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await; if let ExpiredTimelocks::None { .. } = state.expired_timelock(bitcoin_wallet).await? { @@ -207,6 +237,7 @@ async fn next_state( }, } } + // TODO: Send Tauri event here everytime we receive a new confirmation result = tx_lock_status.wait_until_confirmed_with(state.cancel_timelock) => { result?; BobState::CancelTimelockExpired(state.cancel(monero_wallet_restore_blockheight)) @@ -217,6 +248,8 @@ async fn next_state( } } BobState::XmrLocked(state) => { + event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::XmrLocked); + // In case we send the encrypted signature to Alice, but she doesn't give us a confirmation // We need to check if she still published the Bitcoin redeem transaction // Otherwise we risk staying stuck in "XmrLocked" @@ -272,10 +305,21 @@ async fn next_state( } } BobState::BtcRedeemed(state) => { + event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::BtcRedeemed); + state .redeem_xmr(monero_wallet, swap_id.to_string(), monero_receive_address) .await?; + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::XmrRedeemInMempool { + // TODO: Replace this with the actual txid + xmr_redeem_txid: monero::TxHash("placeholder".to_string()), + xmr_redeem_address: monero_receive_address, + }, + ); + BobState::XmrRedeemed { tx_lock_id: state.tx_lock_id(), } @@ -288,6 +332,13 @@ async fn next_state( BobState::BtcCancelled(state4) } BobState::BtcCancelled(state) => { + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::BtcCancelled { + btc_cancel_txid: state.construct_tx_cancel()?.txid(), + }, + ); + // Bob has cancelled the swap match state.expired_timelock(bitcoin_wallet).await? { ExpiredTimelocks::None { .. } => { @@ -308,8 +359,24 @@ async fn next_state( } } } - BobState::BtcRefunded(state4) => BobState::BtcRefunded(state4), + BobState::BtcRefunded(state4) => { + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::BtcRefunded { + btc_refund_txid: state4.signed_refund_transaction()?.txid(), + }, + ); + + BobState::BtcRefunded(state4) + } BobState::BtcPunished { state, tx_lock_id } => { + event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::BtcPunished); + + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::AttemptingCooperativeRedeem, + ); + tracing::info!("Attempting to cooperatively redeem XMR after being punished"); let response = event_loop_handle .request_cooperative_xmr_redeem(swap_id) @@ -321,7 +388,13 @@ async fn next_state( "Alice has accepted our request to cooperatively redeem the XMR" ); + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::CooperativeRedeemAccepted, + ); + let s_a = monero::PrivateKey { scalar: s_a }; + let state5 = state.attempt_cooperative_redeem(s_a); match state5 @@ -329,33 +402,78 @@ async fn next_state( .await { Ok(_) => { + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::XmrRedeemInMempool { + xmr_redeem_txid: monero::TxHash("placeholder".to_string()), + xmr_redeem_address: monero_receive_address, + }, + ); + return Ok(BobState::XmrRedeemed { tx_lock_id }); } Err(error) => { - return Err(error) - .context("Failed to redeem XMR with revealed XMR key"); + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::CooperativeRedeemRejected { + reason: error.to_string(), + }, + ); + + let err: std::result::Result<_, anyhow::Error> = + Err(error).context("Failed to redeem XMR with revealed XMR key"); + + return err; } } } Ok(Rejected { reason, .. }) => { + let err = Err(reason.clone()) + .context("Alice rejected our request for cooperative XMR redeem"); + + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::CooperativeRedeemRejected { + reason: reason.to_string(), + }, + ); + tracing::error!( ?reason, "Alice rejected our request for cooperative XMR redeem" ); - return Err(reason) - .context("Alice rejected our request for cooperative XMR redeem"); + + return err; } Err(error) => { tracing::error!( ?error, "Failed to request cooperative XMR redeem from Alice" ); + + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::CooperativeRedeemRejected { + reason: error.to_string(), + }, + ); + return Err(error) .context("Failed to request cooperative XMR redeem from Alice"); } }; } BobState::SafelyAborted => BobState::SafelyAborted, - BobState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id }, + BobState::XmrRedeemed { tx_lock_id } => { + // TODO: Replace this with the actual txid + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::XmrRedeemInMempool { + xmr_redeem_txid: monero::TxHash("placeholder".to_string()), + xmr_redeem_address: monero_receive_address, + }, + ); + BobState::XmrRedeemed { tx_lock_id } + } }) } diff --git a/swap/src/rpc/methods.rs b/swap/src/rpc/methods.rs index d50e029ac..ca707290b 100644 --- a/swap/src/rpc/methods.rs +++ b/swap/src/rpc/methods.rs @@ -1,8 +1,8 @@ use crate::api::request::{ - buy_xmr, cancel_and_refund, get_balance, get_current_swap, get_history, get_raw_states, - get_swap_info, list_sellers, monero_recovery, resume_swap, suspend_current_swap, withdraw_btc, - BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, GetSwapInfoArgs, ListSellersArgs, Method, - MoneroRecoveryArgs, ResumeArgs, WithdrawBtcArgs, + get_current_swap, get_history, get_raw_states, + suspend_current_swap, + BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, GetSwapInfoArgs, ListSellersArgs, + MoneroRecoveryArgs, Request, ResumeSwapArgs, WithdrawBtcArgs, }; use crate::api::Context; use crate::bitcoin::bitcoin_address; @@ -42,7 +42,8 @@ pub fn register_modules(outer_context: Context) -> Result> { let swap_id = as_uuid(swap_id) .ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?; - get_swap_info(GetSwapInfoArgs { swap_id }, context) + GetSwapInfoArgs { swap_id } + .request(context) .await .to_jsonrpsee_result() })?; @@ -60,7 +61,8 @@ pub fn register_modules(outer_context: Context) -> Result> { jsonrpsee_core::Error::Custom("force_refesh is not a boolean".to_string()) })?; - get_balance(BalanceArgs { force_refresh }, context) + BalanceArgs { force_refresh } + .request(context) .await .to_jsonrpsee_result() })?; @@ -83,7 +85,8 @@ pub fn register_modules(outer_context: Context) -> Result> { let swap_id = as_uuid(swap_id) .ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?; - resume_swap(ResumeArgs { swap_id }, context) + ResumeSwapArgs { swap_id } + .request(context) .await .to_jsonrpsee_result() })?; @@ -98,7 +101,8 @@ pub fn register_modules(outer_context: Context) -> Result> { let swap_id = as_uuid(swap_id) .ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?; - cancel_and_refund(CancelAndRefundArgs { swap_id }, context) + CancelAndRefundArgs { swap_id } + .request(context) .await .to_jsonrpsee_result() })?; @@ -116,7 +120,8 @@ pub fn register_modules(outer_context: Context) -> Result> { jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()) })?; - monero_recovery(MoneroRecoveryArgs { swap_id }, context) + MoneroRecoveryArgs { swap_id } + .request(context) .await .to_jsonrpsee_result() }, @@ -130,8 +135,7 @@ pub fn register_modules(outer_context: Context) -> Result> { ::bitcoin::Amount::from_str_in(amount_str, ::bitcoin::Denomination::Bitcoin) .map_err(|_| { jsonrpsee_core::Error::Custom("Unable to parse amount".to_string()) - })? - .to_sat(), + })?, ) } else { None @@ -145,13 +149,11 @@ pub fn register_modules(outer_context: Context) -> Result> { let withdraw_address = bitcoin_address::validate(withdraw_address, context.config.env_config.bitcoin_network)?; - withdraw_btc( - WithdrawBtcArgs { - amount, - address: withdraw_address, - }, - context, - ) + WithdrawBtcArgs { + amount, + address: withdraw_address, + } + .request(context) .await .to_jsonrpsee_result() })?; @@ -187,15 +189,12 @@ pub fn register_modules(outer_context: Context) -> Result> { })?) .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; - buy_xmr( - BuyXmrArgs { - seller, - bitcoin_change_address, - monero_receive_address, - swap_id: Uuid::new_v4(), - }, - context, - ) + BuyXmrArgs { + seller, + bitcoin_change_address, + monero_receive_address, + } + .request(context) .await .to_jsonrpsee_result() })?; @@ -214,12 +213,10 @@ pub fn register_modules(outer_context: Context) -> Result> { jsonrpsee_core::Error::Custom("Could not parse valid multiaddr".to_string()) })?; - list_sellers( - ListSellersArgs { - rendezvous_point: rendezvous_point.clone(), - }, - context, - ) + ListSellersArgs { + rendezvous_point: rendezvous_point.clone(), + } + .request(context) .await .to_jsonrpsee_result() })?; diff --git a/swap/tests/rpc.rs b/swap/tests/rpc.rs index 5dc640d48..1c92b3cc1 100644 --- a/swap/tests/rpc.rs +++ b/swap/tests/rpc.rs @@ -15,7 +15,7 @@ mod test { use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; - use swap::api::request::{Method, Request}; + use swap::api::request::{start_daemon, StartDaemonArgs}; use swap::api::Context; use crate::harness::alice_run_until::is_xmr_lock_transaction_sent; @@ -39,18 +39,14 @@ mod test { harness_ctx: TestContext, ) -> (Client, MakeCapturingWriter, Arc) { let writer = capture_logs(LevelFilter::DEBUG); - let server_address: SocketAddr = SERVER_ADDRESS.parse().unwrap(); - - let request = Request::new(Method::StartDaemon { - server_address: Some(server_address), - }); + let server_address: Option = SERVER_ADDRESS.parse().unwrap().into(); let context = Arc::new(harness_ctx.get_bob_context().await); let context_clone = context.clone(); tokio::spawn(async move { - if let Err(err) = request.call(context_clone).await { + if let Err(err) = start_daemon(StartDaemonArgs { server_address }, context).await { println!("Failed to initialize daemon for testing: {}", err); } }); From 9b0023174ba44b7367019fc5b6da7255e8225cfe Mon Sep 17 00:00:00 2001 From: binarybaron Date: Mon, 26 Aug 2024 15:23:46 +0200 Subject: [PATCH 5/9] feat(gui): Add typeshare definitions --- src-gui/README.md | 6 + src-gui/src/models/tauriModel.ts | 185 +++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 src-gui/src/models/tauriModel.ts diff --git a/src-gui/README.md b/src-gui/README.md index 102e36689..2e7cd9c9f 100644 --- a/src-gui/README.md +++ b/src-gui/README.md @@ -5,3 +5,9 @@ This template should help get you started developing with Tauri, React and Types ## Recommended IDE Setup - [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) + +## Generate bindings for Tauri API + +```bash +typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src +``` diff --git a/src-gui/src/models/tauriModel.ts b/src-gui/src/models/tauriModel.ts new file mode 100644 index 000000000..b7b317aed --- /dev/null +++ b/src-gui/src/models/tauriModel.ts @@ -0,0 +1,185 @@ +/* + Generated by typeshare 1.9.2 +*/ + +/** + * Represent a timelock, expressed in relative block height as defined in + * [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki). + * E.g. The timelock expires 10 blocks after the reference transaction is + * mined. + */ +export type CancelTimelock = number; + +/** + * Represent a timelock, expressed in relative block height as defined in + * [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki). + * E.g. The timelock expires 10 blocks after the reference transaction is + * mined. + */ +export type PunishTimelock = number; + +export type Amount = number; + +export interface BuyXmrArgs { + seller: string; + bitcoin_change_address: string; + monero_receive_address: string; +} + +export interface ResumeArgs { + swap_id: string; +} + +export interface CancelAndRefundArgs { + swap_id: string; +} + +export interface MoneroRecoveryArgs { + swap_id: string; +} + +export interface WithdrawBtcArgs { + amount?: number; + address: string; +} + +export interface BalanceArgs { + force_refresh: boolean; +} + +export interface ListSellersArgs { + rendezvous_point: string; +} + +export interface StartDaemonArgs { + server_address: string; +} + +export interface GetSwapInfoArgs { + swap_id: string; +} + +export interface ResumeSwapResponse { + result: string; +} + +export interface BalanceResponse { + balance: number; +} + +/** Represents a quote for buying XMR. */ +export interface BidQuote { + /** The price at which the maker is willing to buy at. */ + price: number; + /** + * The minimum quantity the maker is willing to buy. + * #[typeshare(serialized_as = "number")] + */ + min_quantity: number; + /** The maximum quantity the maker is willing to buy. */ + max_quantity: number; +} + +export interface BuyXmrResponse { + swap_id: string; + quote: BidQuote; +} + +export interface GetHistoryEntry { + swap_id: string; + state: string; +} + +export interface GetHistoryResponse { + swaps: GetHistoryEntry[]; +} + +export interface Seller { + peer_id: string; + addresses: string[]; +} + +export type ExpiredTimelocks = + | { type: "None", content: { + blocks_left: number; +}} + | { type: "Cancel", content: { + blocks_left: number; +}} + | { type: "Punish", content?: undefined }; + +export interface GetSwapInfoResponse { + swap_id: string; + seller: Seller; + completed: boolean; + start_date: string; + state_name: string; + xmr_amount: number; + btc_amount: number; + tx_lock_id: string; + tx_cancel_fee: number; + tx_refund_fee: number; + tx_lock_fee: number; + btc_refund_address: string; + cancel_timelock: CancelTimelock; + punish_timelock: PunishTimelock; + timelock?: ExpiredTimelocks; +} + +export interface WithdrawBtcResponse { + amount: number; + txid: string; +} + +export interface SuspendCurrentSwapResponse { + swap_id: string; +} + +export type TauriSwapProgressEvent = + | { type: "Initiated", content?: undefined } + | { type: "ReceivedQuote", content: BidQuote } + | { type: "WaitingForBtcDeposit", content: { + deposit_address: string; + max_giveable: number; + min_deposit_until_swap_will_start: number; + max_deposit_until_maximum_amount_is_reached: number; + min_bitcoin_lock_tx_fee: number; + quote: BidQuote; +}} + | { type: "Started", content: { + btc_lock_amount: number; + btc_tx_lock_fee: number; +}} + | { type: "BtcLockTxInMempool", content: { + btc_lock_txid: string; + btc_lock_confirmations: number; +}} + | { type: "XmrLockTxInMempool", content: { + xmr_lock_txid: string; + xmr_lock_tx_confirmations: number; +}} + | { type: "XmrLocked", content?: undefined } + | { type: "BtcRedeemed", content?: undefined } + | { type: "XmrRedeemInMempool", content: { + xmr_redeem_txid: string; + xmr_redeem_address: string; +}} + | { type: "BtcCancelled", content: { + btc_cancel_txid: string; +}} + | { type: "BtcRefunded", content: { + btc_refund_txid: string; +}} + | { type: "BtcPunished", content?: undefined } + | { type: "AttemptingCooperativeRedeem", content?: undefined } + | { type: "CooperativeRedeemAccepted", content?: undefined } + | { type: "CooperativeRedeemRejected", content: { + reason: string; +}} + | { type: "Released", content?: undefined }; + +export interface TauriSwapProgressEventWrapper { + swap_id: string; + event: TauriSwapProgressEvent; +} + From d54f5c6c77556639d79acde4c214b8a4bd9eba98 Mon Sep 17 00:00:00 2001 From: binarybaron Date: Mon, 26 Aug 2024 15:26:21 +0200 Subject: [PATCH 6/9] refactor(tauri): Use macro and new Request trait for command generation --- src-tauri/capabilities/default.json | 10 ++-- src-tauri/src/lib.rs | 87 +++++++++++++---------------- src-tauri/tauri.conf.json | 58 +++++++++---------- 3 files changed, 73 insertions(+), 82 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index d66865717..034e17388 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -1,7 +1,7 @@ { - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "Capability for the main window", - "windows": ["main"], - "permissions": [] + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": ["core:event:allow-emit", "core:event:default"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2655e3ebc..98462d571 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,15 +3,14 @@ use std::sync::Arc; use swap::{ api::{ request::{ - get_balance as get_balance_impl, get_swap_infos_all as get_swap_infos_all_impl, - withdraw_btc as withdraw_btc_impl, BalanceArgs, BalanceResponse, GetSwapInfoResponse, - WithdrawBtcArgs, WithdrawBtcResponse, + BalanceArgs, BuyXmrArgs, GetHistoryArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, + WithdrawBtcArgs, }, Context, }, cli::command::{Bitcoin, Monero}, }; -use tauri::{Manager, RunEvent, State}; +use tauri::{Manager, RunEvent}; trait ToStringResult { fn to_string_result(self) -> Result; @@ -20,57 +19,44 @@ trait ToStringResult { // Implement the trait for Result impl ToStringResult for Result { fn to_string_result(self) -> Result { - match self { - Ok(value) => Ok(value), - Err(err) => Err(err.to_string()), - } + self.map_err(|e| e.to_string()) } } -#[tauri::command] -async fn get_balance(context: State<'_, Arc>) -> Result { - get_balance_impl( - BalanceArgs { - force_refresh: true, - }, - context.inner().clone(), - ) - .await - .to_string_result() -} - -#[tauri::command] -async fn get_swap_infos_all( - context: State<'_, Arc>, -) -> Result, String> { - get_swap_infos_all_impl(context.inner().clone()) - .await - .to_string_result() -} - -/*macro_rules! tauri_command { - ($command_name:ident, $command_args:ident, $command_response:ident) => { +/// This macro is used to create boilerplate functions as tauri commands +/// that simply delegate handling to the respective request type. +/// +/// # Example +/// ```ignored +/// tauri_command!(get_balance, BalanceArgs); +/// ``` +/// will resolve to +/// ```ignored +/// #[tauri::command] +/// async fn get_balance(context: tauri::State<'...>, args: BalanceArgs) -> Result { +/// args.handle(context.inner().clone()).await.to_string_result() +/// } +/// ``` +macro_rules! tauri_command { + ($fn_name:ident, $request_name:ident) => { #[tauri::command] - async fn $command_name( - context: State<'_, Context>, - args: $command_args, - ) -> Result<$command_response, String> { - swap::api::request::$command_name(args, context) + async fn $fn_name( + context: tauri::State<'_, Arc>, + args: $request_name, + ) -> Result<<$request_name as swap::api::request::Request>::Response, String> { + <$request_name as swap::api::request::Request>::request(args, context.inner().clone()) .await .to_string_result() } }; -}*/ - -#[tauri::command] -async fn withdraw_btc( - context: State<'_, Arc>, - args: WithdrawBtcArgs, -) -> Result { - withdraw_btc_impl(args, context.inner().clone()) - .await - .to_string_result() } +tauri_command!(get_balance, BalanceArgs); +tauri_command!(get_swap_infos_all, BalanceArgs); +tauri_command!(buy_xmr, BuyXmrArgs); +tauri_command!(get_history, GetHistoryArgs); +tauri_command!(resume_swap, ResumeSwapArgs); +tauri_command!(withdraw_btc, WithdrawBtcArgs); +tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs); fn setup<'a>(app: &'a mut tauri::App) -> Result<(), Box> { tauri::async_runtime::block_on(async { @@ -90,7 +76,8 @@ fn setup<'a>(app: &'a mut tauri::App) -> Result<(), Box> None, ) .await - .unwrap(); + .unwrap() + .with_tauri_handle(app.app_handle().to_owned()); app.manage(Arc::new(context)); }); @@ -104,7 +91,11 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ get_balance, get_swap_infos_all, - withdraw_btc + withdraw_btc, + buy_xmr, + resume_swap, + get_history, + suspend_current_swap ]) .setup(setup) .build(tauri::generate_context!()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ffe6d3ee1..a1f1b566b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,32 +1,32 @@ { - "productName": "unstoppableswap-gui-rs", - "version": "0.1.0", - "identifier": "net.unstoppableswap.gui", - "build": { - "devUrl": "http://localhost:1420", - "frontendDist": "../src-gui/dist" - }, - "app": { - "windows": [ - { - "title": "unstoppableswap-gui-rs", - "width": 800, - "height": 600 - } - ], - "security": { - "csp": "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' http://localhost:1234" - } - }, - "bundle": { - "active": true, - "targets": "all", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ] + "productName": "unstoppableswap-gui-rs", + "version": "0.1.0", + "identifier": "net.unstoppableswap.gui", + "build": { + "devUrl": "http://localhost:1420", + "frontendDist": "../src-gui/dist" + }, + "app": { + "windows": [ + { + "title": "unstoppableswap-gui-rs", + "width": 800, + "height": 600 + } + ], + "security": { + "csp": "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' http://localhost:1234 https://api.unstoppableswap.net" } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } } From cf641bc8bb26d1f3963959294042a79ca54ace36 Mon Sep 17 00:00:00 2001 From: binarybaron Date: Mon, 26 Aug 2024 15:32:28 +0200 Subject: [PATCH 7/9] feat(gui): Migrate to Tauri events - Replace Electron IPC with Tauri invoke() for API calls - Implement TauriSwapProgressEvent for state management - Remove IpcInvokeButton, replace with PromiseInvokeButton - Update models: new tauriModel.ts, refactor rpcModel.ts - Simplify SwapSlice state, remove processRunning flag - Refactor SwapStatePage to use TauriSwapProgressEvent - Update HistoryRow and HistoryRowActions for new data structures - Remove unused Electron-specific components (e.g., RpcStatusAlert) - Update dependencies: React 18, Material-UI v4 to v5 - Implement typeshare for Rust/TypeScript type synchronization - Add BobStateName enum for more precise swap state tracking - Refactor utility functions for Tauri compatibility - Remove JSONStream and other Electron-specific dependencies --- src-gui/src/models/cliModel.ts | 393 ---- src-gui/src/models/downloaderModel.ts | 4 - src-gui/src/models/rpcModel.ts | 221 -- src-gui/src/models/storeModel.ts | 218 +- src-gui/src/models/tauriModelExt.ts | 155 ++ .../renderer/components/IpcInvokeButton.tsx | 166 -- .../components/PromiseInvokeButton.tsx | 13 +- .../alert/MoneroWalletRpcUpdatingAlert.tsx | 5 +- .../alert/RemainingFundsWillBeUsedAlert.tsx | 4 +- .../components/alert/RpcStatusAlert.tsx | 7 +- .../alert/SwapMightBeCancelledAlert.tsx | 5 +- .../components/alert/SwapStatusAlert.tsx | 117 +- .../renderer/components/icons/BitcoinIcon.tsx | 1 - .../renderer/components/icons/DiscordIcon.tsx | 2 +- .../components/icons/LinkIconButton.tsx | 2 +- .../renderer/components/icons/MoneroIcon.tsx | 1 - .../src/renderer/components/icons/TorIcon.tsx | 1 - .../inputs/BitcoinAddressTextField.tsx | 4 +- .../inputs/MoneroAddressTextField.tsx | 4 +- .../components/modal/SwapSuspendAlert.tsx | 11 +- .../modal/feedback/FeedbackDialog.tsx | 10 +- .../modal/listSellers/ListSellersDialog.tsx | 28 +- .../modal/provider/ProviderInfo.tsx | 4 +- .../modal/provider/ProviderListDialog.tsx | 20 +- .../modal/provider/ProviderSelect.tsx | 4 +- .../modal/provider/ProviderSubmitDialog.tsx | 8 +- .../components/modal/swap/BitcoinQrCode.tsx | 2 +- .../modal/swap/BitcoinTransactionInfoBox.tsx | 4 +- .../modal/swap/DepositAddressInfoBox.tsx | 6 +- .../modal/swap/MoneroTransactionInfoBox.tsx | 4 +- .../components/modal/swap/SwapDialog.tsx | 19 +- .../components/modal/swap/SwapDialogTitle.tsx | 4 +- .../modal/swap/SwapStateStepper.tsx | 43 +- .../components/modal/swap/pages/DebugPage.tsx | 2 +- .../modal/swap/pages/FeedbackSubmitBadge.tsx | 2 +- .../modal/swap/pages/SwapStatePage.tsx | 152 +- .../swap/pages/done/BitcoinRefundedPage.tsx | 28 +- .../pages/done/XmrRedeemInMempoolPage.tsx | 43 +- .../exited/ProcessExitedAndNotDonePage.tsx | 6 +- .../swap/pages/exited/ProcessExitedPage.tsx | 70 +- .../BitcoinLockTxInMempoolPage.tsx | 19 +- .../swap/pages/in_progress/StartedPage.tsx | 27 +- .../in_progress/XmrLockInMempoolPage.tsx | 15 +- .../swap/pages/init/DepositAmountHelper.tsx | 42 +- .../init/DownloadingMoneroWalletRpcPage.tsx | 2 +- .../modal/swap/pages/init/InitPage.tsx | 18 +- .../modal/swap/pages/init/InitiatedPage.tsx | 2 +- .../init/WaitingForBitcoinDepositPage.tsx | 47 +- .../modal/wallet/WithdrawDialog.tsx | 15 +- .../modal/wallet/WithdrawDialogContent.tsx | 2 +- .../modal/wallet/WithdrawStepper.tsx | 1 - .../modal/wallet/pages/AddressInputPage.tsx | 5 +- .../pages/BitcoinWithdrawTxInMempoolPage.tsx | 5 +- .../navigation/NavigationFooter.tsx | 7 +- .../other/ScrollablePaperTextBox.tsx | 4 +- .../src/renderer/components/other/Units.tsx | 21 +- .../components/pages/help/RpcControlBox.tsx | 30 +- .../components/pages/help/TorInfoBox.tsx | 22 +- .../components/pages/history/HistoryPage.tsx | 4 +- .../pages/history/table/HistoryRow.tsx | 35 +- .../pages/history/table/HistoryRowActions.tsx | 59 +- .../history/table/HistoryRowExpanded.tsx | 62 +- .../pages/history/table/HistoryTable.tsx | 9 +- .../history/table/SwapLogFileOpenButton.tsx | 17 +- .../table/SwapMoneroRecoveryButton.tsx | 31 +- .../components/pages/swap/SwapWidget.tsx | 27 +- .../pages/wallet/WalletRefreshButton.tsx | 4 +- src-gui/src/renderer/index.tsx | 11 +- src-gui/src/renderer/rpc.ts | 84 +- src-gui/src/store/config.ts | 6 - src-gui/src/store/features/providersSlice.ts | 4 +- src-gui/src/store/features/rpcSlice.ts | 62 +- src-gui/src/store/features/swapSlice.ts | 318 +-- src-gui/src/store/hooks.ts | 9 +- src-gui/src/utils/multiAddrUtils.ts | 2 +- src-gui/src/utils/parseUtils.ts | 35 +- src-gui/yarn.lock | 1783 +++++++++++++++-- 77 files changed, 2478 insertions(+), 2161 deletions(-) delete mode 100644 src-gui/src/models/downloaderModel.ts create mode 100644 src-gui/src/models/tauriModelExt.ts delete mode 100644 src-gui/src/renderer/components/IpcInvokeButton.tsx diff --git a/src-gui/src/models/cliModel.ts b/src-gui/src/models/cliModel.ts index 266e61c6f..31423ca79 100644 --- a/src-gui/src/models/cliModel.ts +++ b/src-gui/src/models/cliModel.ts @@ -18,396 +18,3 @@ export interface CliLog { [index: string]: unknown; }[]; } - -export function isCliLog(log: unknown): log is CliLog { - if (log && typeof log === "object") { - return ( - "timestamp" in (log as CliLog) && - "level" in (log as CliLog) && - "fields" in (log as CliLog) && - typeof (log as CliLog).fields?.message === "string" - ); - } - return false; -} - -export interface CliLogStartedRpcServer extends CliLog { - fields: { - message: "Started RPC server"; - addr: string; - }; -} - -export function isCliLogStartedRpcServer( - log: CliLog, -): log is CliLogStartedRpcServer { - return log.fields.message === "Started RPC server"; -} - -export interface CliLogReleasingSwapLockLog extends CliLog { - fields: { - message: "Releasing swap lock"; - swap_id: string; - }; -} - -export function isCliLogReleasingSwapLockLog( - log: CliLog, -): log is CliLogReleasingSwapLockLog { - return log.fields.message === "Releasing swap lock"; -} - -export interface CliLogApiCallError extends CliLog { - fields: { - message: "API call resulted in an error"; - err: string; - }; -} - -export function isCliLogApiCallError(log: CliLog): log is CliLogApiCallError { - return log.fields.message === "API call resulted in an error"; -} - -export interface CliLogAcquiringSwapLockLog extends CliLog { - fields: { - message: "Acquiring swap lock"; - swap_id: string; - }; -} - -export function isCliLogAcquiringSwapLockLog( - log: CliLog, -): log is CliLogAcquiringSwapLockLog { - return log.fields.message === "Acquiring swap lock"; -} - -export interface CliLogReceivedQuote extends CliLog { - fields: { - message: "Received quote"; - price: string; - minimum_amount: string; - maximum_amount: string; - }; -} - -export function isCliLogReceivedQuote(log: CliLog): log is CliLogReceivedQuote { - return log.fields.message === "Received quote"; -} - -export interface CliLogWaitingForBtcDeposit extends CliLog { - fields: { - message: "Waiting for Bitcoin deposit"; - deposit_address: string; - min_deposit_until_swap_will_start: string; - max_deposit_until_maximum_amount_is_reached: string; - max_giveable: string; - minimum_amount: string; - maximum_amount: string; - min_bitcoin_lock_tx_fee: string; - price: string; - }; -} - -export function isCliLogWaitingForBtcDeposit( - log: CliLog, -): log is CliLogWaitingForBtcDeposit { - return log.fields.message === "Waiting for Bitcoin deposit"; -} - -export interface CliLogReceivedBtc extends CliLog { - fields: { - message: "Received Bitcoin"; - max_giveable: string; - new_balance: string; - }; -} - -export function isCliLogReceivedBtc(log: CliLog): log is CliLogReceivedBtc { - return log.fields.message === "Received Bitcoin"; -} - -export interface CliLogDeterminedSwapAmount extends CliLog { - fields: { - message: "Determined swap amount"; - amount: string; - fees: string; - }; -} - -export function isCliLogDeterminedSwapAmount( - log: CliLog, -): log is CliLogDeterminedSwapAmount { - return log.fields.message === "Determined swap amount"; -} - -export interface CliLogStartedSwap extends CliLog { - fields: { - message: "Starting new swap"; - swap_id: string; - }; -} - -export function isCliLogStartedSwap(log: CliLog): log is CliLogStartedSwap { - return log.fields.message === "Starting new swap"; -} - -export interface CliLogPublishedBtcTx extends CliLog { - fields: { - message: "Published Bitcoin transaction"; - txid: string; - kind: "lock" | "cancel" | "withdraw" | "refund"; - }; -} - -export function isCliLogPublishedBtcTx( - log: CliLog, -): log is CliLogPublishedBtcTx { - return log.fields.message === "Published Bitcoin transaction"; -} - -export interface CliLogBtcTxFound extends CliLog { - fields: { - message: "Found relevant Bitcoin transaction"; - txid: string; - status: string; - }; -} - -export function isCliLogBtcTxFound(log: CliLog): log is CliLogBtcTxFound { - return log.fields.message === "Found relevant Bitcoin transaction"; -} - -export interface CliLogBtcTxStatusChanged extends CliLog { - fields: { - message: "Bitcoin transaction status changed"; - txid: string; - new_status: string; - }; -} - -export function isCliLogBtcTxStatusChanged( - log: CliLog, -): log is CliLogBtcTxStatusChanged { - return log.fields.message === "Bitcoin transaction status changed"; -} - -export interface CliLogAliceLockedXmr extends CliLog { - fields: { - message: "Alice locked Monero"; - txid: string; - }; -} - -export function isCliLogAliceLockedXmr( - log: CliLog, -): log is CliLogAliceLockedXmr { - return log.fields.message === "Alice locked Monero"; -} - -export interface CliLogReceivedXmrLockTxConfirmation extends CliLog { - fields: { - message: "Received new confirmation for Monero lock tx"; - txid: string; - seen_confirmations: string; - needed_confirmations: string; - }; -} - -export function isCliLogReceivedXmrLockTxConfirmation( - log: CliLog, -): log is CliLogReceivedXmrLockTxConfirmation { - return log.fields.message === "Received new confirmation for Monero lock tx"; -} - -export interface CliLogAdvancingState extends CliLog { - fields: { - message: "Advancing state"; - state: - | "quote has been requested" - | "execution setup done" - | "btc is locked" - | "XMR lock transaction transfer proof received" - | "xmr is locked" - | "encrypted signature is sent" - | "btc is redeemed" - | "cancel timelock is expired" - | "btc is cancelled" - | "btc is refunded" - | "xmr is redeemed" - | "btc is punished" - | "safely aborted"; - }; -} - -export function isCliLogAdvancingState( - log: CliLog, -): log is CliLogAdvancingState { - return log.fields.message === "Advancing state"; -} - -export interface CliLogRedeemedXmr extends CliLog { - fields: { - message: "Successfully transferred XMR to wallet"; - monero_receive_address: string; - txid: string; - }; -} - -export function isCliLogRedeemedXmr(log: CliLog): log is CliLogRedeemedXmr { - return log.fields.message === "Successfully transferred XMR to wallet"; -} - -export interface YouHaveBeenPunishedCliLog extends CliLog { - fields: { - message: "You have been punished for not refunding in time"; - }; -} - -export function isYouHaveBeenPunishedCliLog( - log: CliLog, -): log is YouHaveBeenPunishedCliLog { - return ( - log.fields.message === "You have been punished for not refunding in time" - ); -} - -function getCliLogSpanAttribute(log: CliLog, key: string): T | null { - const span = log.spans?.find((s) => s[key]); - if (!span) { - return null; - } - return span[key] as T; -} - -export function getCliLogSpanSwapId(log: CliLog): string | null { - return getCliLogSpanAttribute(log, "swap_id"); -} - -export function getCliLogSpanLogReferenceId(log: CliLog): string | null { - return ( - getCliLogSpanAttribute(log, "log_reference_id")?.replace( - /"/g, - "", - ) || null - ); -} - -export function hasCliLogOneOfMultipleSpans( - log: CliLog, - spanNames: string[], -): boolean { - return log.spans?.some((s) => spanNames.includes(s.name)) ?? false; -} - -export interface CliLogStartedSyncingMoneroWallet extends CliLog { - fields: { - message: "Syncing Monero wallet"; - current_sync_height?: boolean; - }; -} - -export function isCliLogStartedSyncingMoneroWallet( - log: CliLog, -): log is CliLogStartedSyncingMoneroWallet { - return log.fields.message === "Syncing Monero wallet"; -} - -export interface CliLogFinishedSyncingMoneroWallet extends CliLog { - fields: { - message: "Synced Monero wallet"; - }; -} - -export interface CliLogFailedToSyncMoneroWallet extends CliLog { - fields: { - message: "Failed to sync Monero wallet"; - error: string; - }; -} - -export function isCliLogFailedToSyncMoneroWallet( - log: CliLog, -): log is CliLogFailedToSyncMoneroWallet { - return log.fields.message === "Failed to sync Monero wallet"; -} - -export function isCliLogFinishedSyncingMoneroWallet( - log: CliLog, -): log is CliLogFinishedSyncingMoneroWallet { - return log.fields.message === "Monero wallet synced"; -} - -export interface CliLogDownloadingMoneroWalletRpc extends CliLog { - fields: { - message: "Downloading monero-wallet-rpc"; - progress: string; - size: string; - download_url: string; - }; -} - -export function isCliLogDownloadingMoneroWalletRpc( - log: CliLog, -): log is CliLogDownloadingMoneroWalletRpc { - return log.fields.message === "Downloading monero-wallet-rpc"; -} - -export interface CliLogStartedSyncingMoneroWallet extends CliLog { - fields: { - message: "Syncing Monero wallet"; - current_sync_height?: boolean; - }; -} - -export interface CliLogDownloadingMoneroWalletRpc extends CliLog { - fields: { - message: "Downloading monero-wallet-rpc"; - progress: string; - size: string; - download_url: string; - }; -} - -export interface CliLogGotNotificationForNewBlock extends CliLog { - fields: { - message: "Got notification for new block"; - block_height: string; - }; -} - -export function isCliLogGotNotificationForNewBlock( - log: CliLog, -): log is CliLogGotNotificationForNewBlock { - return log.fields.message === "Got notification for new block"; -} - -export interface CliLogAttemptingToCooperativelyRedeemXmr extends CliLog { - fields: { - message: "Attempting to cooperatively redeem XMR after being punished"; - }; -} - -export function isCliLogAttemptingToCooperativelyRedeemXmr( - log: CliLog, -): log is CliLogAttemptingToCooperativelyRedeemXmr { - return ( - log.fields.message === - "Attempting to cooperatively redeem XMR after being punished" - ); -} - -export interface CliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr - extends CliLog { - fields: { - message: "Alice has accepted our request to cooperatively redeem the XMR"; - }; -} - -export function isCliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr( - log: CliLog, -): log is CliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr { - return ( - log.fields.message === - "Alice has accepted our request to cooperatively redeem the XMR" - ); -} diff --git a/src-gui/src/models/downloaderModel.ts b/src-gui/src/models/downloaderModel.ts deleted file mode 100644 index 779d4378c..000000000 --- a/src-gui/src/models/downloaderModel.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Binary { - dirPath: string; // Path without filename appended - fileName: string; -} diff --git a/src-gui/src/models/rpcModel.ts b/src-gui/src/models/rpcModel.ts index b1524b90e..57f946098 100644 --- a/src-gui/src/models/rpcModel.ts +++ b/src-gui/src/models/rpcModel.ts @@ -1,6 +1,3 @@ -import { piconerosToXmr, satsToBtc } from "utils/conversionUtils"; -import { exhaustiveGuard } from "utils/typescriptUtils"; - export enum RpcMethod { GET_BTC_BALANCE = "get_bitcoin_balance", WITHDRAW_BTC = "withdraw_btc", @@ -110,227 +107,9 @@ export type SwapSellerInfo = { addresses: string[]; }; -export interface GetSwapInfoResponse { - swap_id: string; - completed: boolean; - seller: SwapSellerInfo; - start_date: string; - state_name: SwapStateName; - timelock: null | SwapTimelockInfo; - tx_lock_id: string; - tx_cancel_fee: number; - tx_refund_fee: number; - tx_lock_fee: number; - btc_amount: number; - xmr_amount: number; - btc_refund_address: string; - cancel_timelock: number; - punish_timelock: number; -} - export type MoneroRecoveryResponse = { address: string; spend_key: string; view_key: string; restore_height: number; }; - -export interface BalanceBitcoinResponse { - balance: number; -} - -export interface GetHistoryResponse { - swaps: [swapId: string, stateName: SwapStateName][]; -} - -export enum SwapStateName { - Started = "quote has been requested", - SwapSetupCompleted = "execution setup done", - BtcLocked = "btc is locked", - XmrLockProofReceived = "XMR lock transaction transfer proof received", - XmrLocked = "xmr is locked", - EncSigSent = "encrypted signature is sent", - BtcRedeemed = "btc is redeemed", - CancelTimelockExpired = "cancel timelock is expired", - BtcCancelled = "btc is cancelled", - BtcRefunded = "btc is refunded", - XmrRedeemed = "xmr is redeemed", - BtcPunished = "btc is punished", - SafelyAborted = "safely aborted", -} - -export type SwapStateNameRunningSwap = Exclude< - SwapStateName, - | SwapStateName.Started - | SwapStateName.SwapSetupCompleted - | SwapStateName.BtcRefunded - | SwapStateName.BtcPunished - | SwapStateName.SafelyAborted - | SwapStateName.XmrRedeemed ->; - -export type GetSwapInfoResponseRunningSwap = GetSwapInfoResponse & { - stateName: SwapStateNameRunningSwap; -}; - -export function isSwapStateNameRunningSwap( - state: SwapStateName, -): state is SwapStateNameRunningSwap { - return ![ - SwapStateName.Started, - SwapStateName.SwapSetupCompleted, - SwapStateName.BtcRefunded, - SwapStateName.BtcPunished, - SwapStateName.SafelyAborted, - SwapStateName.XmrRedeemed, - ].includes(state); -} - -export type SwapStateNameCompletedSwap = - | SwapStateName.XmrRedeemed - | SwapStateName.BtcRefunded - | SwapStateName.BtcPunished - | SwapStateName.SafelyAborted; - -export function isSwapStateNameCompletedSwap( - state: SwapStateName, -): state is SwapStateNameCompletedSwap { - return [ - SwapStateName.XmrRedeemed, - SwapStateName.BtcRefunded, - SwapStateName.BtcPunished, - SwapStateName.SafelyAborted, - ].includes(state); -} - -export type SwapStateNamePossiblyCancellableSwap = - | SwapStateName.BtcLocked - | SwapStateName.XmrLockProofReceived - | SwapStateName.XmrLocked - | SwapStateName.EncSigSent - | SwapStateName.CancelTimelockExpired; - -/** -Checks if a swap is in a state where it can possibly be cancelled - -The following conditions must be met: - - The bitcoin must be locked - - The bitcoin must not be redeemed - - The bitcoin must not be cancelled - - The bitcoin must not be refunded - - The bitcoin must not be punished - -See: https://github.com/comit-network/xmr-btc-swap/blob/7023e75bb51ab26dff4c8fcccdc855d781ca4b15/swap/src/cli/cancel.rs#L16-L35 - */ -export function isSwapStateNamePossiblyCancellableSwap( - state: SwapStateName, -): state is SwapStateNamePossiblyCancellableSwap { - return [ - SwapStateName.BtcLocked, - SwapStateName.XmrLockProofReceived, - SwapStateName.XmrLocked, - SwapStateName.EncSigSent, - SwapStateName.CancelTimelockExpired, - ].includes(state); -} - -export type SwapStateNamePossiblyRefundableSwap = - | SwapStateName.BtcLocked - | SwapStateName.XmrLockProofReceived - | SwapStateName.XmrLocked - | SwapStateName.EncSigSent - | SwapStateName.CancelTimelockExpired - | SwapStateName.BtcCancelled; - -/** -Checks if a swap is in a state where it can possibly be refunded (meaning it's not impossible) - -The following conditions must be met: - - The bitcoin must be locked - - The bitcoin must not be redeemed - - The bitcoin must not be refunded - - The bitcoin must not be punished - -See: https://github.com/comit-network/xmr-btc-swap/blob/7023e75bb51ab26dff4c8fcccdc855d781ca4b15/swap/src/cli/refund.rs#L16-L34 - */ -export function isSwapStateNamePossiblyRefundableSwap( - state: SwapStateName, -): state is SwapStateNamePossiblyRefundableSwap { - return [ - SwapStateName.BtcLocked, - SwapStateName.XmrLockProofReceived, - SwapStateName.XmrLocked, - SwapStateName.EncSigSent, - SwapStateName.CancelTimelockExpired, - SwapStateName.BtcCancelled, - ].includes(state); -} - -/** - * Type guard for GetSwapInfoResponseRunningSwap - * "running" means the swap is in progress and not yet completed - * If a swap is not "running" it means it is either completed or no Bitcoin have been locked yet - * @param response - */ -export function isGetSwapInfoResponseRunningSwap( - response: GetSwapInfoResponse, -): response is GetSwapInfoResponseRunningSwap { - return isSwapStateNameRunningSwap(response.state_name); -} - -export function isSwapMoneroRecoverable(swapStateName: SwapStateName): boolean { - return [SwapStateName.BtcRedeemed].includes(swapStateName); -} - -// See https://github.com/comit-network/xmr-btc-swap/blob/50ae54141255e03dba3d2b09036b1caa4a63e5a3/swap/src/protocol/bob/state.rs#L55 -export function getHumanReadableDbStateType(type: SwapStateName): string { - switch (type) { - case SwapStateName.Started: - return "Quote has been requested"; - case SwapStateName.SwapSetupCompleted: - return "Swap has been initiated"; - case SwapStateName.BtcLocked: - return "Bitcoin has been locked"; - case SwapStateName.XmrLockProofReceived: - return "Monero lock transaction transfer proof has been received"; - case SwapStateName.XmrLocked: - return "Monero has been locked"; - case SwapStateName.EncSigSent: - return "Encrypted signature has been sent"; - case SwapStateName.BtcRedeemed: - return "Bitcoin has been redeemed"; - case SwapStateName.CancelTimelockExpired: - return "Cancel timelock has expired"; - case SwapStateName.BtcCancelled: - return "Swap has been cancelled"; - case SwapStateName.BtcRefunded: - return "Bitcoin has been refunded"; - case SwapStateName.XmrRedeemed: - return "Monero has been redeemed"; - case SwapStateName.BtcPunished: - return "Bitcoin has been punished"; - case SwapStateName.SafelyAborted: - return "Swap has been safely aborted"; - default: - return exhaustiveGuard(type); - } -} - -export function getSwapTxFees(swap: GetSwapInfoResponse): number { - return satsToBtc(swap.tx_lock_fee); -} - -export function getSwapBtcAmount(swap: GetSwapInfoResponse): number { - return satsToBtc(swap.btc_amount); -} - -export function getSwapXmrAmount(swap: GetSwapInfoResponse): number { - return piconerosToXmr(swap.xmr_amount); -} - -export function getSwapExchangeRate(swap: GetSwapInfoResponse): number { - const btcAmount = getSwapBtcAmount(swap); - const xmrAmount = getSwapXmrAmount(swap); - - return btcAmount / xmrAmount; -} diff --git a/src-gui/src/models/storeModel.ts b/src-gui/src/models/storeModel.ts index c67cdb277..72edf0b3b 100644 --- a/src-gui/src/models/storeModel.ts +++ b/src-gui/src/models/storeModel.ts @@ -1,218 +1,12 @@ import { CliLog, SwapSpawnType } from "./cliModel"; -import { Provider } from "./apiModel"; +import { TauriSwapProgressEvent } from "./tauriModel"; export interface SwapSlice { - state: SwapState | null; + state: { + curr: TauriSwapProgressEvent; + prev: TauriSwapProgressEvent | null; + swapId: string; + } | null; logs: CliLog[]; - processRunning: boolean; - provider: Provider | null; spawnType: SwapSpawnType | null; - swapId: string | null; -} - -export type MoneroWalletRpcUpdateState = { - progress: string; - downloadUrl: string; -}; - -export interface SwapState { - type: SwapStateType; -} - -export enum SwapStateType { - INITIATED = "initiated", - RECEIVED_QUOTE = "received quote", - WAITING_FOR_BTC_DEPOSIT = "waiting for btc deposit", - STARTED = "started", - BTC_LOCK_TX_IN_MEMPOOL = "btc lock tx is in mempool", - XMR_LOCK_TX_IN_MEMPOOL = "xmr lock tx is in mempool", - XMR_LOCKED = "xmr is locked", - BTC_REDEEMED = "btc redeemed", - XMR_REDEEM_IN_MEMPOOL = "xmr redeem tx is in mempool", - PROCESS_EXITED = "process exited", - BTC_CANCELLED = "btc cancelled", - BTC_REFUNDED = "btc refunded", - BTC_PUNISHED = "btc punished", - ATTEMPTING_COOPERATIVE_REDEEM = "attempting cooperative redeem", - COOPERATIVE_REDEEM_REJECTED = "cooperative redeem rejected", -} - -export function isSwapState(state?: SwapState | null): state is SwapState { - return state?.type != null; -} - -export interface SwapStateInitiated extends SwapState { - type: SwapStateType.INITIATED; -} - -export function isSwapStateInitiated( - state?: SwapState | null, -): state is SwapStateInitiated { - return state?.type === SwapStateType.INITIATED; -} - -export interface SwapStateReceivedQuote extends SwapState { - type: SwapStateType.RECEIVED_QUOTE; - price: number; - minimumSwapAmount: number; - maximumSwapAmount: number; -} - -export function isSwapStateReceivedQuote( - state?: SwapState | null, -): state is SwapStateReceivedQuote { - return state?.type === SwapStateType.RECEIVED_QUOTE; -} - -export interface SwapStateWaitingForBtcDeposit extends SwapState { - type: SwapStateType.WAITING_FOR_BTC_DEPOSIT; - depositAddress: string; - maxGiveable: number; - minimumAmount: number; - maximumAmount: number; - minDeposit: number; - maxDeposit: number; - minBitcoinLockTxFee: number; - price: number | null; -} - -export function isSwapStateWaitingForBtcDeposit( - state?: SwapState | null, -): state is SwapStateWaitingForBtcDeposit { - return state?.type === SwapStateType.WAITING_FOR_BTC_DEPOSIT; -} - -export interface SwapStateStarted extends SwapState { - type: SwapStateType.STARTED; - txLockDetails: { - amount: number; - fees: number; - } | null; -} - -export function isSwapStateStarted( - state?: SwapState | null, -): state is SwapStateStarted { - return state?.type === SwapStateType.STARTED; -} - -export interface SwapStateBtcLockInMempool extends SwapState { - type: SwapStateType.BTC_LOCK_TX_IN_MEMPOOL; - bobBtcLockTxId: string; - bobBtcLockTxConfirmations: number; -} - -export function isSwapStateBtcLockInMempool( - state?: SwapState | null, -): state is SwapStateBtcLockInMempool { - return state?.type === SwapStateType.BTC_LOCK_TX_IN_MEMPOOL; -} - -export interface SwapStateXmrLockInMempool extends SwapState { - type: SwapStateType.XMR_LOCK_TX_IN_MEMPOOL; - aliceXmrLockTxId: string; - aliceXmrLockTxConfirmations: number; -} - -export function isSwapStateXmrLockInMempool( - state?: SwapState | null, -): state is SwapStateXmrLockInMempool { - return state?.type === SwapStateType.XMR_LOCK_TX_IN_MEMPOOL; -} - -export interface SwapStateXmrLocked extends SwapState { - type: SwapStateType.XMR_LOCKED; -} - -export function isSwapStateXmrLocked( - state?: SwapState | null, -): state is SwapStateXmrLocked { - return state?.type === SwapStateType.XMR_LOCKED; -} - -export interface SwapStateBtcRedemeed extends SwapState { - type: SwapStateType.BTC_REDEEMED; -} - -export function isSwapStateBtcRedemeed( - state?: SwapState | null, -): state is SwapStateBtcRedemeed { - return state?.type === SwapStateType.BTC_REDEEMED; -} - -export interface SwapStateAttemptingCooperativeRedeeem extends SwapState { - type: SwapStateType.ATTEMPTING_COOPERATIVE_REDEEM; -} - -export function isSwapStateAttemptingCooperativeRedeeem( - state?: SwapState | null, -): state is SwapStateAttemptingCooperativeRedeeem { - return state?.type === SwapStateType.ATTEMPTING_COOPERATIVE_REDEEM; -} - -export interface SwapStateCooperativeRedeemRejected extends SwapState { - type: SwapStateType.COOPERATIVE_REDEEM_REJECTED; - reason: string; -} - -export function isSwapStateCooperativeRedeemRejected( - state?: SwapState | null, -): state is SwapStateCooperativeRedeemRejected { - return state?.type === SwapStateType.COOPERATIVE_REDEEM_REJECTED; -} - -export interface SwapStateXmrRedeemInMempool extends SwapState { - type: SwapStateType.XMR_REDEEM_IN_MEMPOOL; - bobXmrRedeemTxId: string; - bobXmrRedeemAddress: string; -} - -export function isSwapStateXmrRedeemInMempool( - state?: SwapState | null, -): state is SwapStateXmrRedeemInMempool { - return state?.type === SwapStateType.XMR_REDEEM_IN_MEMPOOL; -} - -export interface SwapStateBtcCancelled extends SwapState { - type: SwapStateType.BTC_CANCELLED; - btcCancelTxId: string; -} - -export function isSwapStateBtcCancelled( - state?: SwapState | null, -): state is SwapStateBtcCancelled { - return state?.type === SwapStateType.BTC_CANCELLED; -} - -export interface SwapStateBtcRefunded extends SwapState { - type: SwapStateType.BTC_REFUNDED; - bobBtcRefundTxId: string; -} - -export function isSwapStateBtcRefunded( - state?: SwapState | null, -): state is SwapStateBtcRefunded { - return state?.type === SwapStateType.BTC_REFUNDED; -} - -export interface SwapStateBtcPunished extends SwapState { - type: SwapStateType.BTC_PUNISHED; -} - -export function isSwapStateBtcPunished( - state?: SwapState | null, -): state is SwapStateBtcPunished { - return state?.type === SwapStateType.BTC_PUNISHED; -} - -export interface SwapStateProcessExited extends SwapState { - type: SwapStateType.PROCESS_EXITED; - prevState: SwapState | null; - rpcError: string | null; -} - -export function isSwapStateProcessExited( - state?: SwapState | null, -): state is SwapStateProcessExited { - return state?.type === SwapStateType.PROCESS_EXITED; } diff --git a/src-gui/src/models/tauriModelExt.ts b/src-gui/src/models/tauriModelExt.ts new file mode 100644 index 000000000..fe24da11f --- /dev/null +++ b/src-gui/src/models/tauriModelExt.ts @@ -0,0 +1,155 @@ +import { + ExpiredTimelocks, + GetSwapInfoResponse, + TauriSwapProgressEvent, +} from "./tauriModel"; + +export type TauriSwapProgressEventContent< + T extends TauriSwapProgressEvent["type"], +> = Extract["content"]; + +// See /swap/src/protocol/bob/state.rs#L57 +// TODO: Replace this with a typeshare definition +export enum BobStateName { + Started = "quote has been requested", + SwapSetupCompleted = "execution setup done", + BtcLocked = "btc is locked", + XmrLockProofReceived = "XMR lock transaction transfer proof received", + XmrLocked = "xmr is locked", + EncSigSent = "encrypted signature is sent", + BtcRedeemed = "btc is redeemed", + CancelTimelockExpired = "cancel timelock is expired", + BtcCancelled = "btc is cancelled", + BtcRefunded = "btc is refunded", + XmrRedeemed = "xmr is redeemed", + BtcPunished = "btc is punished", + SafelyAborted = "safely aborted", +} + +// TODO: This is a temporary solution until we have a typeshare definition for BobStateName +export type GetSwapInfoResponseExt = GetSwapInfoResponse & { + state_name: BobStateName; +}; + +export type TimelockNone = Extract; +export type TimelockCancel = Extract; +export type TimelockPunish = Extract; + +export type BobStateNameRunningSwap = Exclude< + BobStateName, + | BobStateName.Started + | BobStateName.SwapSetupCompleted + | BobStateName.BtcRefunded + | BobStateName.BtcPunished + | BobStateName.SafelyAborted + | BobStateName.XmrRedeemed +>; + +export type GetSwapInfoResponseExtRunningSwap = GetSwapInfoResponseExt & { + stateName: BobStateNameRunningSwap; +}; + +export function isBobStateNameRunningSwap( + state: BobStateName, +): state is BobStateNameRunningSwap { + return ![ + BobStateName.Started, + BobStateName.SwapSetupCompleted, + BobStateName.BtcRefunded, + BobStateName.BtcPunished, + BobStateName.SafelyAborted, + BobStateName.XmrRedeemed, + ].includes(state); +} + +export type BobStateNameCompletedSwap = + | BobStateName.XmrRedeemed + | BobStateName.BtcRefunded + | BobStateName.BtcPunished + | BobStateName.SafelyAborted; + +export function isBobStateNameCompletedSwap( + state: BobStateName, +): state is BobStateNameCompletedSwap { + return [ + BobStateName.XmrRedeemed, + BobStateName.BtcRefunded, + BobStateName.BtcPunished, + BobStateName.SafelyAborted, + ].includes(state); +} + +export type BobStateNamePossiblyCancellableSwap = + | BobStateName.BtcLocked + | BobStateName.XmrLockProofReceived + | BobStateName.XmrLocked + | BobStateName.EncSigSent + | BobStateName.CancelTimelockExpired; + +/** +Checks if a swap is in a state where it can possibly be cancelled + +The following conditions must be met: + - The bitcoin must be locked + - The bitcoin must not be redeemed + - The bitcoin must not be cancelled + - The bitcoin must not be refunded + - The bitcoin must not be punished + +See: https://github.com/comit-network/xmr-btc-swap/blob/7023e75bb51ab26dff4c8fcccdc855d781ca4b15/swap/src/cli/cancel.rs#L16-L35 + */ +export function isBobStateNamePossiblyCancellableSwap( + state: BobStateName, +): state is BobStateNamePossiblyCancellableSwap { + return [ + BobStateName.BtcLocked, + BobStateName.XmrLockProofReceived, + BobStateName.XmrLocked, + BobStateName.EncSigSent, + BobStateName.CancelTimelockExpired, + ].includes(state); +} + +export type BobStateNamePossiblyRefundableSwap = + | BobStateName.BtcLocked + | BobStateName.XmrLockProofReceived + | BobStateName.XmrLocked + | BobStateName.EncSigSent + | BobStateName.CancelTimelockExpired + | BobStateName.BtcCancelled; + +/** +Checks if a swap is in a state where it can possibly be refunded (meaning it's not impossible) + +The following conditions must be met: + - The bitcoin must be locked + - The bitcoin must not be redeemed + - The bitcoin must not be refunded + - The bitcoin must not be punished + +See: https://github.com/comit-network/xmr-btc-swap/blob/7023e75bb51ab26dff4c8fcccdc855d781ca4b15/swap/src/cli/refund.rs#L16-L34 + */ +export function isBobStateNamePossiblyRefundableSwap( + state: BobStateName, +): state is BobStateNamePossiblyRefundableSwap { + return [ + BobStateName.BtcLocked, + BobStateName.XmrLockProofReceived, + BobStateName.XmrLocked, + BobStateName.EncSigSent, + BobStateName.CancelTimelockExpired, + BobStateName.BtcCancelled, + ].includes(state); +} + +/** + * Type guard for GetSwapInfoResponseExt + * "running" means the swap is in progress and not yet completed + * If a swap is not "running" it means it is either completed or no Bitcoin have been locked yet + * @param response + */ +export function isGetSwapInfoResponseRunningSwap( + response: GetSwapInfoResponseExt, +): response is GetSwapInfoResponseExtRunningSwap { + return isBobStateNameRunningSwap(response.state_name); +} diff --git a/src-gui/src/renderer/components/IpcInvokeButton.tsx b/src-gui/src/renderer/components/IpcInvokeButton.tsx deleted file mode 100644 index b3e5182ad..000000000 --- a/src-gui/src/renderer/components/IpcInvokeButton.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { - Button, - ButtonProps, - CircularProgress, - IconButton, - Tooltip, -} from "@material-ui/core"; -import { ReactElement, ReactNode, useEffect, useState } from "react"; -import { useSnackbar } from "notistack"; -import { useAppSelector } from "store/hooks"; -import { RpcProcessStateType } from "models/rpcModel"; -import { isExternalRpc } from "store/config"; - -function IpcButtonTooltip({ - requiresRpcAndNotReady, - children, - processType, - tooltipTitle, -}: { - requiresRpcAndNotReady: boolean; - children: ReactElement; - processType: RpcProcessStateType; - tooltipTitle?: string; -}) { - if (tooltipTitle) { - return {children}; - } - - const getMessage = () => { - if (!requiresRpcAndNotReady) return ""; - - switch (processType) { - case RpcProcessStateType.LISTENING_FOR_CONNECTIONS: - return ""; - case RpcProcessStateType.STARTED: - return "Cannot execute this action because the Swap Daemon is still starting and not yet ready to accept connections. Please wait a moment and try again"; - case RpcProcessStateType.EXITED: - return "Cannot execute this action because the Swap Daemon has been stopped. Please start the Swap Daemon again to continue"; - case RpcProcessStateType.NOT_STARTED: - return "Cannot execute this action because the Swap Daemon has not been started yet. Please start the Swap Daemon first"; - default: - return ""; - } - }; - - return ( - - {children} - - ); -} - -interface IpcInvokeButtonProps { - ipcArgs: unknown[]; - ipcChannel: string; - onSuccess?: (data: T) => void; - isLoadingOverride?: boolean; - isIconButton?: boolean; - loadIcon?: ReactNode; - requiresRpc?: boolean; - disabled?: boolean; - displayErrorSnackbar?: boolean; - tooltipTitle?: string; -} - -const DELAY_BEFORE_SHOWING_LOADING_MS = 0; - -export default function IpcInvokeButton({ - disabled, - ipcChannel, - ipcArgs, - onSuccess, - onClick, - endIcon, - loadIcon, - isLoadingOverride, - isIconButton, - requiresRpc, - displayErrorSnackbar, - tooltipTitle, - ...rest -}: IpcInvokeButtonProps & ButtonProps) { - const { enqueueSnackbar } = useSnackbar(); - - const rpcProcessType = useAppSelector((state) => state.rpc.process.type); - const isRpcReady = - rpcProcessType === RpcProcessStateType.LISTENING_FOR_CONNECTIONS; - const [isPending, setIsPending] = useState(false); - const [hasMinLoadingTimePassed, setHasMinLoadingTimePassed] = useState(false); - - const isLoading = (isPending && hasMinLoadingTimePassed) || isLoadingOverride; - const actualEndIcon = isLoading - ? loadIcon || - : endIcon; - - useEffect(() => { - setHasMinLoadingTimePassed(false); - setTimeout( - () => setHasMinLoadingTimePassed(true), - DELAY_BEFORE_SHOWING_LOADING_MS, - ); - }, [isPending]); - - async function handleClick(event: React.MouseEvent) { - onClick?.(event); - - if (!isPending) { - setIsPending(true); - try { - // const result = await ipcRenderer.invoke(ipcChannel, ...ipcArgs); - throw new Error("Not implemented"); - // onSuccess?.(result); - } catch (e: unknown) { - if (displayErrorSnackbar) { - enqueueSnackbar((e as Error).message, { - autoHideDuration: 60 * 1000, - variant: "error", - }); - } - } finally { - setIsPending(false); - } - } - } - - const requiresRpcAndNotReady = - !!requiresRpc && !isRpcReady && !isExternalRpc(); - const isDisabled = disabled || requiresRpcAndNotReady || isLoading; - - return ( - - - {isIconButton ? ( - - {actualEndIcon} - - ) : ( - - Force stop - + ); diff --git a/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx b/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx index 1f468e898..093b0384f 100644 --- a/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx +++ b/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx @@ -10,15 +10,15 @@ import { Select, TextField, } from "@material-ui/core"; -import { useState } from "react"; +import { CliLog } from "models/cliModel"; import { useSnackbar } from "notistack"; +import { useState } from "react"; +import { store } from "renderer/store/storeRenderer"; import { useActiveSwapInfo, useAppSelector } from "store/hooks"; import { parseDateString } from "utils/parseUtils"; -import { store } from "renderer/store/storeRenderer"; -import { CliLog } from "models/cliModel"; import { submitFeedbackViaHttp } from "../../../api"; -import { PiconeroAmount } from "../../other/Units"; import LoadingButton from "../../other/LoadingButton"; +import { PiconeroAmount } from "../../other/Units"; async function submitFeedback(body: string, swapId: string | number) { let attachedBody = ""; @@ -67,7 +67,7 @@ function SwapSelectDropDown({ > Do not attach logs {swaps.map((swap) => ( - + Swap {swap.swap_id.substring(0, 5)}... from{" "} {new Date(parseDateString(swap.start_date)).toDateString()} ( ) diff --git a/src-gui/src/renderer/components/modal/listSellers/ListSellersDialog.tsx b/src-gui/src/renderer/components/modal/listSellers/ListSellersDialog.tsx index 63948c154..f8f6af2f9 100644 --- a/src-gui/src/renderer/components/modal/listSellers/ListSellersDialog.tsx +++ b/src-gui/src/renderer/components/modal/listSellers/ListSellersDialog.tsx @@ -1,20 +1,20 @@ -import { ChangeEvent, useState } from "react"; import { - DialogTitle, + Box, + Button, + Chip, Dialog, + DialogActions, DialogContent, DialogContentText, - TextField, - DialogActions, - Button, - Box, - Chip, + DialogTitle, makeStyles, + TextField, Theme, } from "@material-ui/core"; import { Multiaddr } from "multiaddr"; import { useSnackbar } from "notistack"; -import IpcInvokeButton from "../../IpcInvokeButton"; +import { ChangeEvent, useState } from "react"; +import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; const PRESET_RENDEZVOUS_POINTS = [ "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", @@ -53,7 +53,7 @@ export default function ListSellersDialog({ return "The multi address must contain the peer id (/p2p/)"; } return null; - } catch (e) { + } catch { return "Not a valid multi address"; } } @@ -119,17 +119,17 @@ export default function ListSellersDialog({ - { + throw new Error("Not implemented"); + }} > Connect - + ); diff --git a/src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx b/src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx index 9e0916422..77be7dc5f 100644 --- a/src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx +++ b/src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx @@ -1,11 +1,11 @@ -import { makeStyles, Box, Typography, Chip, Tooltip } from "@material-ui/core"; +import { Box, Chip, makeStyles, Tooltip, Typography } from "@material-ui/core"; import { VerifiedUser } from "@material-ui/icons"; -import { satsToBtc, secondsToDays } from "utils/conversionUtils"; import { ExtendedProviderStatus } from "models/apiModel"; import { MoneroBitcoinExchangeRate, SatsAmount, } from "renderer/components/other/Units"; +import { satsToBtc, secondsToDays } from "utils/conversionUtils"; const useStyles = makeStyles((theme) => ({ content: { diff --git a/src-gui/src/renderer/components/modal/provider/ProviderListDialog.tsx b/src-gui/src/renderer/components/modal/provider/ProviderListDialog.tsx index d718f8cb1..a0e0021a3 100644 --- a/src-gui/src/renderer/components/modal/provider/ProviderListDialog.tsx +++ b/src-gui/src/renderer/components/modal/provider/ProviderListDialog.tsx @@ -1,31 +1,31 @@ import { Avatar, + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, List, ListItem, ListItemAvatar, ListItemText, - DialogTitle, - Dialog, - DialogActions, - Button, - DialogContent, makeStyles, - CircularProgress, } from "@material-ui/core"; import AddIcon from "@material-ui/icons/Add"; -import { useState } from "react"; import SearchIcon from "@material-ui/icons/Search"; import { ExtendedProviderStatus } from "models/apiModel"; +import { RpcMethod } from "models/rpcModel"; +import { useState } from "react"; +import { setSelectedProvider } from "store/features/providersSlice"; import { useAllProviders, useAppDispatch, useIsRpcEndpointBusy, } from "store/hooks"; -import { setSelectedProvider } from "store/features/providersSlice"; -import { RpcMethod } from "models/rpcModel"; -import ProviderSubmitDialog from "./ProviderSubmitDialog"; import ListSellersDialog from "../listSellers/ListSellersDialog"; import ProviderInfo from "./ProviderInfo"; +import ProviderSubmitDialog from "./ProviderSubmitDialog"; const useStyles = makeStyles({ dialogContent: { diff --git a/src-gui/src/renderer/components/modal/provider/ProviderSelect.tsx b/src-gui/src/renderer/components/modal/provider/ProviderSelect.tsx index 7757dd84d..8be291dbb 100644 --- a/src-gui/src/renderer/components/modal/provider/ProviderSelect.tsx +++ b/src-gui/src/renderer/components/modal/provider/ProviderSelect.tsx @@ -1,9 +1,9 @@ import { - makeStyles, + Box, Card, CardContent, - Box, IconButton, + makeStyles, } from "@material-ui/core"; import ArrowForwardIosIcon from "@material-ui/icons/ArrowForwardIos"; import { useState } from "react"; diff --git a/src-gui/src/renderer/components/modal/provider/ProviderSubmitDialog.tsx b/src-gui/src/renderer/components/modal/provider/ProviderSubmitDialog.tsx index 8697b650e..f5122763b 100644 --- a/src-gui/src/renderer/components/modal/provider/ProviderSubmitDialog.tsx +++ b/src-gui/src/renderer/components/modal/provider/ProviderSubmitDialog.tsx @@ -1,14 +1,14 @@ -import { ChangeEvent, useState } from "react"; import { - DialogTitle, + Button, Dialog, + DialogActions, DialogContent, DialogContentText, + DialogTitle, TextField, - DialogActions, - Button, } from "@material-ui/core"; import { Multiaddr } from "multiaddr"; +import { ChangeEvent, useState } from "react"; type ProviderSubmitDialogProps = { open: boolean; diff --git a/src-gui/src/renderer/components/modal/swap/BitcoinQrCode.tsx b/src-gui/src/renderer/components/modal/swap/BitcoinQrCode.tsx index 87bede82f..1304c43d2 100644 --- a/src-gui/src/renderer/components/modal/swap/BitcoinQrCode.tsx +++ b/src-gui/src/renderer/components/modal/swap/BitcoinQrCode.tsx @@ -1,5 +1,5 @@ -import QRCode from "react-qr-code"; import { Box } from "@material-ui/core"; +import QRCode from "react-qr-code"; export default function BitcoinQrCode({ address }: { address: string }) { return ( diff --git a/src-gui/src/renderer/components/modal/swap/BitcoinTransactionInfoBox.tsx b/src-gui/src/renderer/components/modal/swap/BitcoinTransactionInfoBox.tsx index 4ef54f568..b2861a603 100644 --- a/src-gui/src/renderer/components/modal/swap/BitcoinTransactionInfoBox.tsx +++ b/src-gui/src/renderer/components/modal/swap/BitcoinTransactionInfoBox.tsx @@ -1,7 +1,7 @@ +import { ReactNode } from "react"; +import BitcoinIcon from "renderer/components/icons/BitcoinIcon"; import { isTestnet } from "store/config"; import { getBitcoinTxExplorerUrl } from "utils/conversionUtils"; -import BitcoinIcon from "renderer/components/icons/BitcoinIcon"; -import { ReactNode } from "react"; import TransactionInfoBox from "./TransactionInfoBox"; type Props = { diff --git a/src-gui/src/renderer/components/modal/swap/DepositAddressInfoBox.tsx b/src-gui/src/renderer/components/modal/swap/DepositAddressInfoBox.tsx index 5a8d3684d..c6ee589e1 100644 --- a/src-gui/src/renderer/components/modal/swap/DepositAddressInfoBox.tsx +++ b/src-gui/src/renderer/components/modal/swap/DepositAddressInfoBox.tsx @@ -1,9 +1,9 @@ -import { ReactNode } from "react"; import { Box, Typography } from "@material-ui/core"; import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined"; -import InfoBox from "./InfoBox"; -import ClipboardIconButton from "./ClipbiardIconButton"; +import { ReactNode } from "react"; import BitcoinQrCode from "./BitcoinQrCode"; +import ClipboardIconButton from "./ClipbiardIconButton"; +import InfoBox from "./InfoBox"; type Props = { title: string; diff --git a/src-gui/src/renderer/components/modal/swap/MoneroTransactionInfoBox.tsx b/src-gui/src/renderer/components/modal/swap/MoneroTransactionInfoBox.tsx index d1e156df1..9f37d6cbb 100644 --- a/src-gui/src/renderer/components/modal/swap/MoneroTransactionInfoBox.tsx +++ b/src-gui/src/renderer/components/modal/swap/MoneroTransactionInfoBox.tsx @@ -1,7 +1,7 @@ +import { ReactNode } from "react"; +import MoneroIcon from "renderer/components/icons/MoneroIcon"; import { isTestnet } from "store/config"; import { getMoneroTxExplorerUrl } from "utils/conversionUtils"; -import MoneroIcon from "renderer/components/icons/MoneroIcon"; -import { ReactNode } from "react"; import TransactionInfoBox from "./TransactionInfoBox"; type Props = { diff --git a/src-gui/src/renderer/components/modal/swap/SwapDialog.tsx b/src-gui/src/renderer/components/modal/swap/SwapDialog.tsx index ca8e0e78c..d6931403b 100644 --- a/src-gui/src/renderer/components/modal/swap/SwapDialog.tsx +++ b/src-gui/src/renderer/components/modal/swap/SwapDialog.tsx @@ -1,4 +1,3 @@ -import { useState } from "react"; import { Button, Dialog, @@ -6,13 +5,14 @@ import { DialogContent, makeStyles, } from "@material-ui/core"; -import { useAppDispatch, useAppSelector } from "store/hooks"; +import { useState } from "react"; import { swapReset } from "store/features/swapSlice"; -import SwapStatePage from "./pages/SwapStatePage"; -import SwapStateStepper from "./SwapStateStepper"; +import { useAppDispatch, useAppSelector, useIsSwapRunning } from "store/hooks"; import SwapSuspendAlert from "../SwapSuspendAlert"; -import SwapDialogTitle from "./SwapDialogTitle"; import DebugPage from "./pages/DebugPage"; +import SwapStatePage from "./pages/SwapStatePage"; +import SwapDialogTitle from "./SwapDialogTitle"; +import SwapStateStepper from "./SwapStateStepper"; const useStyles = makeStyles({ content: { @@ -32,16 +32,17 @@ export default function SwapDialog({ }) { const classes = useStyles(); const swap = useAppSelector((state) => state.swap); + const isSwapRunning = useIsSwapRunning(); const [debug, setDebug] = useState(false); const [openSuspendAlert, setOpenSuspendAlert] = useState(false); const dispatch = useAppDispatch(); function onCancel() { - if (swap.processRunning) { + if (isSwapRunning) { setOpenSuspendAlert(true); } else { onClose(); - setTimeout(() => dispatch(swapReset()), 0); + dispatch(swapReset()); } } @@ -61,7 +62,7 @@ export default function SwapDialog({ ) : ( <> - + )} @@ -75,7 +76,7 @@ export default function SwapDialog({ color="primary" variant="contained" onClick={onCancel} - disabled={!(swap.state !== null && !swap.processRunning)} + disabled={isSwapRunning} > Done diff --git a/src-gui/src/renderer/components/modal/swap/SwapDialogTitle.tsx b/src-gui/src/renderer/components/modal/swap/SwapDialogTitle.tsx index 7fd5ccb34..351062c85 100644 --- a/src-gui/src/renderer/components/modal/swap/SwapDialogTitle.tsx +++ b/src-gui/src/renderer/components/modal/swap/SwapDialogTitle.tsx @@ -1,7 +1,7 @@ import { Box, DialogTitle, makeStyles, Typography } from "@material-ui/core"; -import TorStatusBadge from "./pages/TorStatusBadge"; -import FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge"; import DebugPageSwitchBadge from "./pages/DebugPageSwitchBadge"; +import FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge"; +import TorStatusBadge from "./pages/TorStatusBadge"; const useStyles = makeStyles((theme) => ({ root: { diff --git a/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx b/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx index df32baa28..c9bcf6c97 100644 --- a/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx +++ b/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx @@ -1,7 +1,11 @@ import { Step, StepLabel, Stepper, Typography } from "@material-ui/core"; import { SwapSpawnType } from "models/cliModel"; -import { SwapStateName } from "models/rpcModel"; -import { useActiveSwapInfo, useAppSelector } from "store/hooks"; +import { BobStateName } from "models/tauriModelExt"; +import { + useActiveSwapInfo, + useAppSelector, + useIsSwapRunning, +} from "store/hooks"; import { exhaustiveGuard } from "utils/typescriptUtils"; export enum PathType { @@ -9,8 +13,10 @@ export enum PathType { UNHAPPY_PATH = "unhappy path", } +// TODO: Consider using a TauriProgressEvent here instead of BobStateName +// TauriProgressEvent is always up to date, BobStateName is not (needs to be periodically fetched) function getActiveStep( - stateName: SwapStateName | null, + stateName: BobStateName | null, processExited: boolean, ): [PathType, number, boolean] { switch (stateName) { @@ -18,56 +24,56 @@ function getActiveStep( // Step: 0 (Waiting for Bitcoin lock tx to be published) case null: return [PathType.HAPPY_PATH, 0, false]; - case SwapStateName.Started: - case SwapStateName.SwapSetupCompleted: + case BobStateName.Started: + case BobStateName.SwapSetupCompleted: return [PathType.HAPPY_PATH, 0, processExited]; // Step: 1 (Waiting for Bitcoin Lock confirmation and XMR Lock Publication) // We have locked the Bitcoin and are waiting for the other party to lock their XMR - case SwapStateName.BtcLocked: + case BobStateName.BtcLocked: return [PathType.HAPPY_PATH, 1, processExited]; // Step: 2 (Waiting for XMR Lock confirmation) // We have locked the Bitcoin and the other party has locked their XMR - case SwapStateName.XmrLockProofReceived: + case BobStateName.XmrLockProofReceived: return [PathType.HAPPY_PATH, 1, processExited]; // Step: 3 (Sending Encrypted Signature and waiting for Bitcoin Redemption) // The XMR lock transaction has been confirmed // We now need to send the encrypted signature to the other party and wait for them to redeem the Bitcoin - case SwapStateName.XmrLocked: - case SwapStateName.EncSigSent: + case BobStateName.XmrLocked: + case BobStateName.EncSigSent: return [PathType.HAPPY_PATH, 2, processExited]; // Step: 4 (Waiting for XMR Redemption) - case SwapStateName.BtcRedeemed: + case BobStateName.BtcRedeemed: return [PathType.HAPPY_PATH, 3, processExited]; // Step: 4 (Completed) (Swap completed, XMR redeemed) - case SwapStateName.XmrRedeemed: + case BobStateName.XmrRedeemed: return [PathType.HAPPY_PATH, 4, false]; // Edge Case of Happy Path where the swap is safely aborted. We "fail" at the first step. - case SwapStateName.SafelyAborted: + case BobStateName.SafelyAborted: return [PathType.HAPPY_PATH, 0, true]; // // Unhappy Path // Step: 1 (Cancelling swap, checking if cancel transaction has been published already by the other party) - case SwapStateName.CancelTimelockExpired: + case BobStateName.CancelTimelockExpired: return [PathType.UNHAPPY_PATH, 0, processExited]; // Step: 2 (Attempt to publish the Bitcoin refund transaction) - case SwapStateName.BtcCancelled: + case BobStateName.BtcCancelled: return [PathType.UNHAPPY_PATH, 1, processExited]; // Step: 2 (Completed) (Bitcoin refunded) - case SwapStateName.BtcRefunded: + case BobStateName.BtcRefunded: return [PathType.UNHAPPY_PATH, 2, false]; // Step: 2 (We failed to publish the Bitcoin refund transaction) // We failed to publish the Bitcoin refund transaction because the timelock has expired. // We will be punished. Nothing we can do about it now. - case SwapStateName.BtcPunished: + case BobStateName.BtcPunished: return [PathType.UNHAPPY_PATH, 1, true]; default: return exhaustiveGuard(stateName); @@ -149,11 +155,14 @@ function UnhappyPathStepper({ } export default function SwapStateStepper() { + // TODO: There's no equivalent of this with Tauri yet. const currentSwapSpawnType = useAppSelector((s) => s.swap.spawnType); + const stateName = useActiveSwapInfo()?.state_name ?? null; - const processExited = useAppSelector((s) => !s.swap.processRunning); + const processExited = !useIsSwapRunning(); const [pathType, activeStep, error] = getActiveStep(stateName, processExited); + // TODO: Fix this to work with Tauri // If the current swap is being manually cancelled and refund, we want to show the unhappy path even though the current state is not a "unhappy" state if (currentSwapSpawnType === SwapSpawnType.CANCEL_REFUND) { return ; diff --git a/src-gui/src/renderer/components/modal/swap/pages/DebugPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/DebugPage.tsx index 991e396d8..00d068e2c 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/DebugPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/DebugPage.tsx @@ -1,7 +1,7 @@ import { Box, DialogContentText } from "@material-ui/core"; import { useActiveSwapInfo, useAppSelector } from "store/hooks"; -import CliLogsBox from "../../../other/RenderedCliLog"; import JsonTreeView from "../../../other/JSONViewTree"; +import CliLogsBox from "../../../other/RenderedCliLog"; export default function DebugPage() { const torStdOut = useAppSelector((s) => s.tor.stdOut); diff --git a/src-gui/src/renderer/components/modal/swap/pages/FeedbackSubmitBadge.tsx b/src-gui/src/renderer/components/modal/swap/pages/FeedbackSubmitBadge.tsx index 5aa22a331..563840900 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/FeedbackSubmitBadge.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/FeedbackSubmitBadge.tsx @@ -1,7 +1,7 @@ import { IconButton } from "@material-ui/core"; import FeedbackIcon from "@material-ui/icons/Feedback"; -import FeedbackDialog from "../../feedback/FeedbackDialog"; import { useState } from "react"; +import FeedbackDialog from "../../feedback/FeedbackDialog"; export default function FeedbackSubmitBadge() { const [showFeedbackDialog, setShowFeedbackDialog] = useState(false); diff --git a/src-gui/src/renderer/components/modal/swap/pages/SwapStatePage.tsx b/src-gui/src/renderer/components/modal/swap/pages/SwapStatePage.tsx index 17d528e97..f779d3d85 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/SwapStatePage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/SwapStatePage.tsx @@ -1,43 +1,28 @@ import { Box } from "@material-ui/core"; -import { useAppSelector } from "store/hooks"; -import { - isSwapStateBtcCancelled, - isSwapStateBtcLockInMempool, - isSwapStateBtcPunished, - isSwapStateBtcRedemeed, - isSwapStateBtcRefunded, - isSwapStateInitiated, - isSwapStateProcessExited, - isSwapStateReceivedQuote, - isSwapStateStarted, - isSwapStateWaitingForBtcDeposit, - isSwapStateXmrLocked, - isSwapStateXmrLockInMempool, - isSwapStateXmrRedeemInMempool, - SwapState, -} from "../../../../../models/storeModel"; -import InitiatedPage from "./init/InitiatedPage"; -import WaitingForBitcoinDepositPage from "./init/WaitingForBitcoinDepositPage"; -import StartedPage from "./in_progress/StartedPage"; -import BitcoinLockTxInMempoolPage from "./in_progress/BitcoinLockTxInMempoolPage"; -import XmrLockTxInMempoolPage from "./in_progress/XmrLockInMempoolPage"; -// eslint-disable-next-line import/no-cycle -import ProcessExitedPage from "./exited/ProcessExitedPage"; +import { SwapSlice } from "models/storeModel"; +import CircularProgressWithSubtitle from "../CircularProgressWithSubtitle"; +import BitcoinPunishedPage from "./done/BitcoinPunishedPage"; +import BitcoinRefundedPage from "./done/BitcoinRefundedPage"; import XmrRedeemInMempoolPage from "./done/XmrRedeemInMempoolPage"; -import ReceivedQuotePage from "./in_progress/ReceivedQuotePage"; +import ProcessExitedPage from "./exited/ProcessExitedPage"; +import BitcoinCancelledPage from "./in_progress/BitcoinCancelledPage"; +import BitcoinLockTxInMempoolPage from "./in_progress/BitcoinLockTxInMempoolPage"; import BitcoinRedeemedPage from "./in_progress/BitcoinRedeemedPage"; -import InitPage from "./init/InitPage"; +import ReceivedQuotePage from "./in_progress/ReceivedQuotePage"; +import StartedPage from "./in_progress/StartedPage"; import XmrLockedPage from "./in_progress/XmrLockedPage"; -import BitcoinCancelledPage from "./in_progress/BitcoinCancelledPage"; -import BitcoinRefundedPage from "./done/BitcoinRefundedPage"; -import BitcoinPunishedPage from "./done/BitcoinPunishedPage"; -import { SyncingMoneroWalletPage } from "./in_progress/SyncingMoneroWalletPage"; +import XmrLockTxInMempoolPage from "./in_progress/XmrLockInMempoolPage"; +import InitiatedPage from "./init/InitiatedPage"; +import InitPage from "./init/InitPage"; +import WaitingForBitcoinDepositPage from "./init/WaitingForBitcoinDepositPage"; export default function SwapStatePage({ - swapState, + state, }: { - swapState: SwapState | null; + state: SwapSlice["state"]; }) { + // TODO: Reimplement this using tauri events + /* const isSyncingMoneroWallet = useAppSelector( (state) => state.rpc.state.moneroWallet.isSyncing, ); @@ -45,62 +30,57 @@ export default function SwapStatePage({ if (isSyncingMoneroWallet) { return ; } + */ - if (swapState === null) { + if (state === null) { return ; } - if (isSwapStateInitiated(swapState)) { - return ; - } - if (isSwapStateReceivedQuote(swapState)) { - return ; - } - if (isSwapStateWaitingForBtcDeposit(swapState)) { - return ; - } - if (isSwapStateStarted(swapState)) { - return ; - } - if (isSwapStateBtcLockInMempool(swapState)) { - return ; - } - if (isSwapStateXmrLockInMempool(swapState)) { - return ; - } - if (isSwapStateXmrLocked(swapState)) { - return ; + switch (state.curr.type) { + case "Initiated": + return ; + case "ReceivedQuote": + return ; + case "WaitingForBtcDeposit": + return ; + case "Started": + return ; + case "BtcLockTxInMempool": + return ; + case "XmrLockTxInMempool": + return ; + case "XmrLocked": + return ; + case "BtcRedeemed": + return ; + case "XmrRedeemInMempool": + return ; + case "BtcCancelled": + return ; + case "BtcRefunded": + return ; + case "BtcPunished": + return ; + case "AttemptingCooperativeRedeem": + return ( + + ); + case "CooperativeRedeemAccepted": + return ( + + ); + case "CooperativeRedeemRejected": + return ; + case "Released": + return ; + default: + // TODO: Use this when we have all states implemented, ensures we don't forget to implement a state + // return exhaustiveGuard(state.curr.type); + return ( + + No information to display +
+ State: {JSON.stringify(state, null, 4)} +
+ ); } - if (isSwapStateBtcRedemeed(swapState)) { - return ; - } - if (isSwapStateXmrRedeemInMempool(swapState)) { - return ; - } - if (isSwapStateBtcCancelled(swapState)) { - return ; - } - if (isSwapStateBtcRefunded(swapState)) { - return ; - } - if (isSwapStateBtcPunished(swapState)) { - return ; - } - if (isSwapStateProcessExited(swapState)) { - return ; - } - - console.error( - `No swap state page found for swap state State: ${JSON.stringify( - swapState, - null, - 4, - )}`, - ); - return ( - - No information to display -
- State: ${JSON.stringify(swapState, null, 4)} -
- ); } diff --git a/src-gui/src/renderer/components/modal/swap/pages/done/BitcoinRefundedPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/done/BitcoinRefundedPage.tsx index 9e13ff492..4197d3c46 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/done/BitcoinRefundedPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/done/BitcoinRefundedPage.tsx @@ -1,14 +1,13 @@ import { Box, DialogContentText } from "@material-ui/core"; -import { SwapStateBtcRefunded } from "models/storeModel"; +import { TauriSwapProgressEventContent } from "models/tauriModelExt"; import { useActiveSwapInfo } from "store/hooks"; -import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox"; import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox"; +import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox"; export default function BitcoinRefundedPage({ - state, -}: { - state: SwapStateBtcRefunded | null; -}) { + btc_refund_txid, +}: TauriSwapProgressEventContent<"BtcRefunded">) { + // TODO: Reimplement this using Tauri const swap = useActiveSwapInfo(); const additionalContent = swap ? `Refund address: ${swap.btc_refund_address}` @@ -28,14 +27,15 @@ export default function BitcoinRefundedPage({ gap: "0.5rem", }} > - {state && ( - - )} + { + // TODO: We should display the confirmation count here + } + diff --git a/src-gui/src/renderer/components/modal/swap/pages/done/XmrRedeemInMempoolPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/done/XmrRedeemInMempoolPage.tsx index 9f91dfe84..d499d33a0 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/done/XmrRedeemInMempoolPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/done/XmrRedeemInMempoolPage.tsx @@ -1,23 +1,18 @@ import { Box, DialogContentText } from "@material-ui/core"; -import { SwapStateXmrRedeemInMempool } from "models/storeModel"; -import { useActiveSwapInfo } from "store/hooks"; -import { getSwapXmrAmount } from "models/rpcModel"; -import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox"; +import { TauriSwapProgressEventContent } from "models/tauriModelExt"; import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox"; - -type XmrRedeemInMempoolPageProps = { - state: SwapStateXmrRedeemInMempool | null; -}; +import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox"; export default function XmrRedeemInMempoolPage({ - state, -}: XmrRedeemInMempoolPageProps) { - const swap = useActiveSwapInfo(); - const additionalContent = swap - ? `This transaction transfers ${getSwapXmrAmount(swap).toFixed(6)} XMR to ${ - state?.bobXmrRedeemAddress - }` - : null; + xmr_redeem_address, + xmr_redeem_txid, +}: TauriSwapProgressEventContent<"XmrRedeemInMempool">) { + // TODO: Reimplement this using Tauri + //const additionalContent = swap + // ? `This transaction transfers ${getSwapXmrAmount(swap).toFixed(6)} XMR to ${ + // state?.bobXmrRedeemAddress + // }` + // : null; return ( @@ -32,16 +27,12 @@ export default function XmrRedeemInMempoolPage({ gap: "0.5rem", }} > - {state && ( - <> - - - )} + diff --git a/src-gui/src/renderer/components/modal/swap/pages/exited/ProcessExitedAndNotDonePage.tsx b/src-gui/src/renderer/components/modal/swap/pages/exited/ProcessExitedAndNotDonePage.tsx index 69dd3cb1f..62423d348 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/exited/ProcessExitedAndNotDonePage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/exited/ProcessExitedAndNotDonePage.tsx @@ -1,8 +1,8 @@ import { Box, DialogContentText } from "@material-ui/core"; -import { useActiveSwapInfo, useAppSelector } from "store/hooks"; +import { SwapSpawnType } from "models/cliModel"; import { SwapStateProcessExited } from "models/storeModel"; +import { useActiveSwapInfo, useAppSelector } from "store/hooks"; import CliLogsBox from "../../../../other/RenderedCliLog"; -import { SwapSpawnType } from "models/cliModel"; export default function ProcessExitedAndNotDonePage({ state, @@ -18,7 +18,7 @@ export default function ProcessExitedAndNotDonePage({ const hasRpcError = state.rpcError != null; const hasSwap = swap != null; - let messages = []; + const messages = []; messages.push( isCancelRefund diff --git a/src-gui/src/renderer/components/modal/swap/pages/exited/ProcessExitedPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/exited/ProcessExitedPage.tsx index 68fa2e993..6835a6937 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/exited/ProcessExitedPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/exited/ProcessExitedPage.tsx @@ -1,47 +1,41 @@ -import { useActiveSwapInfo } from "store/hooks"; -import { SwapStateName } from "models/rpcModel"; -import { - isSwapStateBtcPunished, - isSwapStateBtcRefunded, - isSwapStateXmrRedeemInMempool, - SwapStateProcessExited, -} from "../../../../../../models/storeModel"; -import XmrRedeemInMempoolPage from "../done/XmrRedeemInMempoolPage"; -import BitcoinPunishedPage from "../done/BitcoinPunishedPage"; -// eslint-disable-next-line import/no-cycle +import { TauriSwapProgressEvent } from "models/tauriModel"; import SwapStatePage from "../SwapStatePage"; -import BitcoinRefundedPage from "../done/BitcoinRefundedPage"; -import ProcessExitedAndNotDonePage from "./ProcessExitedAndNotDonePage"; -type ProcessExitedPageProps = { - state: SwapStateProcessExited; -}; - -export default function ProcessExitedPage({ state }: ProcessExitedPageProps) { - const swap = useActiveSwapInfo(); - - // If we have a swap state, for a "done" state we should use it to display additional information that can't be extracted from the database +export default function ProcessExitedPage({ + prevState, + swapId, +}: { + prevState: TauriSwapProgressEvent | null; + swapId: string; +}) { + // If we have a previous state, we can show the user the last state of the swap + // We only show the last state if its a final state (XmrRedeemInMempool, BtcRefunded, BtcPunished) if ( - isSwapStateXmrRedeemInMempool(state.prevState) || - isSwapStateBtcRefunded(state.prevState) || - isSwapStateBtcPunished(state.prevState) + prevState != null && + (prevState.type === "XmrRedeemInMempool" || + prevState.type === "BtcRefunded" || + prevState.type === "BtcPunished") ) { - return ; + return ( + + ); } - // If we don't have a swap state for a "done" state, we should fall back to using the database to display as much information as we can - if (swap) { - if (swap.state_name === SwapStateName.XmrRedeemed) { - return ; - } - if (swap.state_name === SwapStateName.BtcRefunded) { - return ; - } - if (swap.state_name === SwapStateName.BtcPunished) { - return ; - } - } + // TODO: Display something useful here + return ( + <> + If the swap is not a "done" state (or we don't have a db state because the + swap did complete the SwapSetup yet) we should tell the user and show logs + Not implemented yet + + ); // If the swap is not a "done" state (or we don't have a db state because the swap did complete the SwapSetup yet) we should tell the user and show logs - return ; + // return ; } diff --git a/src-gui/src/renderer/components/modal/swap/pages/in_progress/BitcoinLockTxInMempoolPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/in_progress/BitcoinLockTxInMempoolPage.tsx index cfbcba12e..75ebf88ae 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/in_progress/BitcoinLockTxInMempoolPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/in_progress/BitcoinLockTxInMempoolPage.tsx @@ -1,19 +1,16 @@ import { Box, DialogContentText } from "@material-ui/core"; -import { SwapStateBtcLockInMempool } from "models/storeModel"; -import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox"; +import { TauriSwapProgressEventContent } from "models/tauriModelExt"; import SwapMightBeCancelledAlert from "../../../../alert/SwapMightBeCancelledAlert"; - -type BitcoinLockTxInMempoolPageProps = { - state: SwapStateBtcLockInMempool; -}; +import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox"; export default function BitcoinLockTxInMempoolPage({ - state, -}: BitcoinLockTxInMempoolPageProps) { + btc_lock_confirmations, + btc_lock_txid, +}: TauriSwapProgressEventContent<"BtcLockTxInMempool">) { return ( The Bitcoin lock transaction has been published. The swap will proceed @@ -22,14 +19,14 @@ export default function BitcoinLockTxInMempoolPage({ Most swap providers require one confirmation before locking their Monero
- Confirmations: {state.bobBtcLockTxConfirmations} + Confirmations: {btc_lock_confirmations} } /> diff --git a/src-gui/src/renderer/components/modal/swap/pages/in_progress/StartedPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/in_progress/StartedPage.tsx index 172c658fe..a338365d5 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/in_progress/StartedPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/in_progress/StartedPage.tsx @@ -1,16 +1,19 @@ -import { SwapStateStarted } from "models/storeModel"; -import { BitcoinAmount } from "renderer/components/other/Units"; +import { TauriSwapProgressEventContent } from "models/tauriModelExt"; +import { SatsAmount } from "renderer/components/other/Units"; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle"; -export default function StartedPage({ state }: { state: SwapStateStarted }) { - const description = state.txLockDetails ? ( - <> - Locking with a - network fee of - - ) : ( - "Locking Bitcoin" +export default function StartedPage({ + btc_lock_amount, + btc_tx_lock_fee, +}: TauriSwapProgressEventContent<"Started">) { + return ( + + Locking with a network fee of{" "} + + + } + /> ); - - return ; } diff --git a/src-gui/src/renderer/components/modal/swap/pages/in_progress/XmrLockInMempoolPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/in_progress/XmrLockInMempoolPage.tsx index 5265dfa43..f5c2615f1 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/in_progress/XmrLockInMempoolPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/in_progress/XmrLockInMempoolPage.tsx @@ -1,15 +1,12 @@ import { Box, DialogContentText } from "@material-ui/core"; -import { SwapStateXmrLockInMempool } from "models/storeModel"; +import { TauriSwapProgressEventContent } from "models/tauriModelExt"; import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox"; -type XmrLockTxInMempoolPageProps = { - state: SwapStateXmrLockInMempool; -}; - export default function XmrLockTxInMempoolPage({ - state, -}: XmrLockTxInMempoolPageProps) { - const additionalContent = `Confirmations: ${state.aliceXmrLockTxConfirmations}/10`; + xmr_lock_tx_confirmations, + xmr_lock_txid, +}: TauriSwapProgressEventContent<"XmrLockTxInMempool">) { + const additionalContent = `Confirmations: ${xmr_lock_tx_confirmations}/10`; return ( @@ -20,7 +17,7 @@ export default function XmrLockTxInMempoolPage({ diff --git a/src-gui/src/renderer/components/modal/swap/pages/init/DepositAmountHelper.tsx b/src-gui/src/renderer/components/modal/swap/pages/init/DepositAmountHelper.tsx index b17232412..3a9135d4e 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/init/DepositAmountHelper.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/init/DepositAmountHelper.tsx @@ -1,8 +1,8 @@ -import { useState } from "react"; import { Box, makeStyles, TextField, Typography } from "@material-ui/core"; -import { SwapStateWaitingForBtcDeposit } from "models/storeModel"; +import { BidQuote } from "models/tauriModel"; +import { useState } from "react"; import { useAppSelector } from "store/hooks"; -import { satsToBtc } from "utils/conversionUtils"; +import { btcToSats, satsToBtc } from "utils/conversionUtils"; import { MoneroAmount } from "../../../../other/Units"; const MONERO_FEE = 0.000016; @@ -29,42 +29,42 @@ function calcBtcAmountWithoutFees(amount: number, fees: number) { } export default function DepositAmountHelper({ - state, + min_deposit_until_swap_will_start, + max_deposit_until_maximum_amount_is_reached, + min_bitcoin_lock_tx_fee, + quote, }: { - state: SwapStateWaitingForBtcDeposit; + min_deposit_until_swap_will_start: number; + max_deposit_until_maximum_amount_is_reached: number; + min_bitcoin_lock_tx_fee: number; + quote: BidQuote; }) { const classes = useStyles(); - const [amount, setAmount] = useState(state.minDeposit); + const [amount, setAmount] = useState(min_deposit_until_swap_will_start); const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0; function getTotalAmountAfterDeposit() { - return amount + satsToBtc(bitcoinBalance); + return amount + bitcoinBalance; } function hasError() { return ( - amount < state.minDeposit || - getTotalAmountAfterDeposit() > state.maximumAmount + 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 (state.price == null) return null; - - console.log( - `Calculating calcBtcAmountWithoutFees(${getTotalAmountAfterDeposit()}, ${ - state.minBitcoinLockTxFee - }) / ${state.price} - ${MONERO_FEE}`, - ); + if (quote.price == null) return null; return ( calcBtcAmountWithoutFees( getTotalAmountAfterDeposit(), - state.minBitcoinLockTxFee, + min_bitcoin_lock_tx_fee, ) / - state.price - + quote.price - MONERO_FEE ); } @@ -75,9 +75,9 @@ export default function DepositAmountHelper({ Depositing {bitcoinBalance > 0 && <>another} setAmount(parseFloat(e.target.value))} + error={!!hasError()} + value={satsToBtc(amount)} + onChange={(e) => setAmount(btcToSats(parseFloat(e.target.value)))} size="small" type="number" className={classes.textField} diff --git a/src-gui/src/renderer/components/modal/swap/pages/init/DownloadingMoneroWalletRpcPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/init/DownloadingMoneroWalletRpcPage.tsx index f03d7e29a..9485c2a23 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/init/DownloadingMoneroWalletRpcPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/init/DownloadingMoneroWalletRpcPage.tsx @@ -1,5 +1,5 @@ -import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle"; import { MoneroWalletRpcUpdateState } from "../../../../../../models/storeModel"; +import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle"; export default function DownloadingMoneroWalletRpcPage({ updateState, diff --git a/src-gui/src/renderer/components/modal/swap/pages/init/InitPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/init/InitPage.tsx index 394000211..11555fe35 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/init/InitPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/init/InitPage.tsx @@ -1,12 +1,12 @@ import { Box, DialogContentText, makeStyles } from "@material-ui/core"; +import PlayArrowIcon from "@material-ui/icons/PlayArrow"; import { useState } from "react"; import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField"; import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField"; +import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; +import { buyXmr } from "renderer/rpc"; import { useAppSelector } from "store/hooks"; -import PlayArrowIcon from "@material-ui/icons/PlayArrow"; -import { isTestnet } from "store/config"; import RemainingFundsWillBeUsedAlert from "../../../../alert/RemainingFundsWillBeUsedAlert"; -import IpcInvokeButton from "../../../../IpcInvokeButton"; const useStyles = makeStyles((theme) => ({ initButton: { @@ -29,6 +29,10 @@ export default function InitPage() { (state) => state.providers.selectedProvider, ); + async function init() { + await buyXmr(selectedProvider, refundAddress, redeemAddress); + } + return ( @@ -58,7 +62,7 @@ export default function InitPage() { /> - } - ipcChannel="spawn-buy-xmr" - ipcArgs={[selectedProvider, redeemAddress, refundAddress]} - displayErrorSnackbar={false} + onClick={init} > Start swap - + ); } diff --git a/src-gui/src/renderer/components/modal/swap/pages/init/InitiatedPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/init/InitiatedPage.tsx index 894b4e459..da46af35c 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/init/InitiatedPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/init/InitiatedPage.tsx @@ -1,5 +1,5 @@ -import { useAppSelector } from "store/hooks"; import { SwapSpawnType } from "models/cliModel"; +import { useAppSelector } from "store/hooks"; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle"; export default function InitiatedPage() { diff --git a/src-gui/src/renderer/components/modal/swap/pages/init/WaitingForBitcoinDepositPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/init/WaitingForBitcoinDepositPage.tsx index a29e70575..17cdc5fb7 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/init/WaitingForBitcoinDepositPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/init/WaitingForBitcoinDepositPage.tsx @@ -1,14 +1,10 @@ import { Box, makeStyles, Typography } from "@material-ui/core"; -import { SwapStateWaitingForBtcDeposit } from "models/storeModel"; +import { TauriSwapProgressEventContent } from "models/tauriModelExt"; import { useAppSelector } from "store/hooks"; -import DepositAddressInfoBox from "../../DepositAddressInfoBox"; import BitcoinIcon from "../../../../icons/BitcoinIcon"; +import { MoneroSatsExchangeRate, SatsAmount } from "../../../../other/Units"; +import DepositAddressInfoBox from "../../DepositAddressInfoBox"; import DepositAmountHelper from "./DepositAmountHelper"; -import { - BitcoinAmount, - MoneroBitcoinExchangeRate, - SatsAmount, -} from "../../../../other/Units"; const useStyles = makeStyles((theme) => ({ amountHelper: { @@ -23,13 +19,13 @@ const useStyles = makeStyles((theme) => ({ }, })); -type WaitingForBtcDepositPageProps = { - state: SwapStateWaitingForBtcDeposit; -}; - export default function WaitingForBtcDepositPage({ - state, -}: WaitingForBtcDepositPageProps) { + deposit_address, + min_deposit_until_swap_will_start, + max_deposit_until_maximum_amount_is_reached, + min_bitcoin_lock_tx_fee, + quote, +}: TauriSwapProgressEventContent<"WaitingForBtcDeposit">) { const classes = useStyles(); const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0; @@ -38,7 +34,7 @@ export default function WaitingForBtcDepositPage({ @@ -51,9 +47,11 @@ export default function WaitingForBtcDepositPage({ ) : null}
  • Send any amount between{" "} - and{" "} - to the address - above + and{" "} + {" "} + to the address above {bitcoinBalance > 0 && ( <> (on top of the already deposited funds) )} @@ -61,11 +59,11 @@ export default function WaitingForBtcDepositPage({
  • All Bitcoin sent to this this address will converted into Monero at an exchance rate of{" "} - +
  • The network fee of{" "} - will + will automatically be deducted from the deposited coins
  • @@ -74,7 +72,16 @@ export default function WaitingForBtcDepositPage({
  • - +
    } icon={} diff --git a/src-gui/src/renderer/components/modal/wallet/WithdrawDialog.tsx b/src-gui/src/renderer/components/modal/wallet/WithdrawDialog.tsx index 6a8bcb233..4ae8f0204 100644 --- a/src-gui/src/renderer/components/modal/wallet/WithdrawDialog.tsx +++ b/src-gui/src/renderer/components/modal/wallet/WithdrawDialog.tsx @@ -1,14 +1,10 @@ import { Button, Dialog, DialogActions } from "@material-ui/core"; -import { useAppDispatch, useIsRpcEndpointBusy } from "store/hooks"; -import { RpcMethod } from "models/rpcModel"; -import { rpcResetWithdrawTxId } from "store/features/rpcSlice"; -import WithdrawStatePage from "./WithdrawStatePage"; -import DialogHeader from "../DialogHeader"; -import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import { useState } from "react"; +import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import { withdrawBtc } from "renderer/rpc"; -import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage"; +import DialogHeader from "../DialogHeader"; import AddressInputPage from "./pages/AddressInputPage"; +import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage"; import WithdrawDialogContent from "./WithdrawDialogContent"; export default function WithdrawDialog({ @@ -42,10 +38,7 @@ export default function WithdrawDialog({ setWithdrawAddressValid={setWithdrawAddressValid} /> ) : ( - + )} diff --git a/src-gui/src/renderer/components/modal/wallet/WithdrawDialogContent.tsx b/src-gui/src/renderer/components/modal/wallet/WithdrawDialogContent.tsx index b0a40a0ad..bf65b66dd 100644 --- a/src-gui/src/renderer/components/modal/wallet/WithdrawDialogContent.tsx +++ b/src-gui/src/renderer/components/modal/wallet/WithdrawDialogContent.tsx @@ -1,5 +1,5 @@ -import { ReactNode } from "react"; import { Box, DialogContent, makeStyles } from "@material-ui/core"; +import { ReactNode } from "react"; import WithdrawStepper from "./WithdrawStepper"; const useStyles = makeStyles({ diff --git a/src-gui/src/renderer/components/modal/wallet/WithdrawStepper.tsx b/src-gui/src/renderer/components/modal/wallet/WithdrawStepper.tsx index 6b19408c7..edfbcafb2 100644 --- a/src-gui/src/renderer/components/modal/wallet/WithdrawStepper.tsx +++ b/src-gui/src/renderer/components/modal/wallet/WithdrawStepper.tsx @@ -1,5 +1,4 @@ import { Step, StepLabel, Stepper } from "@material-ui/core"; -import { useAppSelector, useIsRpcEndpointBusy } from "store/hooks"; function getActiveStep(isPending: boolean, withdrawTxId: string | null) { if (isPending) { diff --git a/src-gui/src/renderer/components/modal/wallet/pages/AddressInputPage.tsx b/src-gui/src/renderer/components/modal/wallet/pages/AddressInputPage.tsx index fd135c2ec..09102816e 100644 --- a/src-gui/src/renderer/components/modal/wallet/pages/AddressInputPage.tsx +++ b/src-gui/src/renderer/components/modal/wallet/pages/AddressInputPage.tsx @@ -1,8 +1,5 @@ -import { useState } from "react"; -import { Button, DialogActions, DialogContentText } from "@material-ui/core"; +import { DialogContentText } from "@material-ui/core"; import BitcoinAddressTextField from "../../../inputs/BitcoinAddressTextField"; -import WithdrawDialogContent from "../WithdrawDialogContent"; -import IpcInvokeButton from "../../../IpcInvokeButton"; export default function AddressInputPage({ withdrawAddress, diff --git a/src-gui/src/renderer/components/modal/wallet/pages/BitcoinWithdrawTxInMempoolPage.tsx b/src-gui/src/renderer/components/modal/wallet/pages/BitcoinWithdrawTxInMempoolPage.tsx index 00f876e25..45cf6b1be 100644 --- a/src-gui/src/renderer/components/modal/wallet/pages/BitcoinWithdrawTxInMempoolPage.tsx +++ b/src-gui/src/renderer/components/modal/wallet/pages/BitcoinWithdrawTxInMempoolPage.tsx @@ -1,13 +1,10 @@ -import { Button, DialogActions, DialogContentText } from "@material-ui/core"; +import { DialogContentText } from "@material-ui/core"; import BitcoinTransactionInfoBox from "../../swap/BitcoinTransactionInfoBox"; -import WithdrawDialogContent from "../WithdrawDialogContent"; export default function BtcTxInMempoolPageContent({ withdrawTxId, - onCancel, }: { withdrawTxId: string; - onCancel: () => void; }) { return ( <> diff --git a/src-gui/src/renderer/components/navigation/NavigationFooter.tsx b/src-gui/src/renderer/components/navigation/NavigationFooter.tsx index e4d7f1c19..701cff623 100644 --- a/src-gui/src/renderer/components/navigation/NavigationFooter.tsx +++ b/src-gui/src/renderer/components/navigation/NavigationFooter.tsx @@ -3,7 +3,6 @@ import GitHubIcon from "@material-ui/icons/GitHub"; import RedditIcon from "@material-ui/icons/Reddit"; import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert"; import MoneroWalletRpcUpdatingAlert from "../alert/MoneroWalletRpcUpdatingAlert"; -import RpcStatusAlert from "../alert/RpcStatusAlert"; import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert"; import DiscordIcon from "../icons/DiscordIcon"; import LinkIconButton from "../icons/LinkIconButton"; @@ -29,7 +28,11 @@ export default function NavigationFooter() { - + + { + // TODO: Uncomment when we have implemented a way for the UI to be displayed before the context has been initialized + // + } diff --git a/src-gui/src/renderer/components/other/ScrollablePaperTextBox.tsx b/src-gui/src/renderer/components/other/ScrollablePaperTextBox.tsx index af9304960..731e2d295 100644 --- a/src-gui/src/renderer/components/other/ScrollablePaperTextBox.tsx +++ b/src-gui/src/renderer/components/other/ScrollablePaperTextBox.tsx @@ -1,9 +1,9 @@ import { Box, Divider, IconButton, Paper, Typography } from "@material-ui/core"; -import { ReactNode, useRef } from "react"; +import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined"; import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp"; +import { ReactNode, useRef } from "react"; import { VList, VListHandle } from "virtua"; -import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined"; import { ExpandableSearchBox } from "./ExpandableSearchBox"; const MIN_HEIGHT = "10rem"; diff --git a/src-gui/src/renderer/components/other/Units.tsx b/src-gui/src/renderer/components/other/Units.tsx index 90420ddee..b14b554b9 100644 --- a/src-gui/src/renderer/components/other/Units.tsx +++ b/src-gui/src/renderer/components/other/Units.tsx @@ -1,6 +1,6 @@ -import { piconerosToXmr, satsToBtc } from "utils/conversionUtils"; import { Tooltip } from "@material-ui/core"; import { useAppSelector } from "store/hooks"; +import { piconerosToXmr, satsToBtc } from "utils/conversionUtils"; type Amount = number | null | undefined; @@ -64,10 +64,27 @@ export function MoneroAmount({ amount }: { amount: Amount }) { ); } -export function MoneroBitcoinExchangeRate({ rate }: { rate: Amount }) { +export function MoneroBitcoinExchangeRate( + state: { rate: Amount } | { satsAmount: number; piconeroAmount: number }, +) { + if ("rate" in state) { + return ( + + ); + } + + const rate = + satsToBtc(state.satsAmount) / piconerosToXmr(state.piconeroAmount); + return ; } +export function MoneroSatsExchangeRate({ rate }: { rate: Amount }) { + const btc = satsToBtc(rate); + + return ; +} + export function SatsAmount({ amount }: { amount: Amount }) { const btcAmount = amount == null ? null : satsToBtc(amount); return ; diff --git a/src-gui/src/renderer/components/pages/help/RpcControlBox.tsx b/src-gui/src/renderer/components/pages/help/RpcControlBox.tsx index 1d36922d6..571f41cd5 100644 --- a/src-gui/src/renderer/components/pages/help/RpcControlBox.tsx +++ b/src-gui/src/renderer/components/pages/help/RpcControlBox.tsx @@ -3,7 +3,7 @@ import FolderOpenIcon from "@material-ui/icons/FolderOpen"; import PlayArrowIcon from "@material-ui/icons/PlayArrow"; import StopIcon from "@material-ui/icons/Stop"; import { RpcProcessStateType } from "models/rpcModel"; -import IpcInvokeButton from "renderer/components/IpcInvokeButton"; +import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import { useAppSelector } from "store/hooks"; import InfoBox from "../../modal/swap/InfoBox"; import CliLogsBox from "../../other/RenderedCliLog"; @@ -36,34 +36,34 @@ export default function RpcControlBox() { } additionalContent={ - } disabled={isRunning} - requiresRpc={false} + onClick={() => { + throw new Error("Not implemented"); + }} > Start Daemon - - + } disabled={!isRunning} - requiresRpc={false} + onClick={() => { + throw new Error("Not implemented"); + }} > Stop Daemon - - + } - requiresRpc={false} isIconButton size="small" tooltipTitle="Open the data directory of the Swap Daemon in your file explorer" + onClick={() => { + throw new Error("Not implemented"); + }} /> } diff --git a/src-gui/src/renderer/components/pages/help/TorInfoBox.tsx b/src-gui/src/renderer/components/pages/help/TorInfoBox.tsx index c4ef58e4e..cc4eab918 100644 --- a/src-gui/src/renderer/components/pages/help/TorInfoBox.tsx +++ b/src-gui/src/renderer/components/pages/help/TorInfoBox.tsx @@ -1,7 +1,7 @@ import { Box, makeStyles, Typography } from "@material-ui/core"; import PlayArrowIcon from "@material-ui/icons/PlayArrow"; import StopIcon from "@material-ui/icons/Stop"; -import IpcInvokeButton from "renderer/components/IpcInvokeButton"; +import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import { useAppSelector } from "store/hooks"; import InfoBox from "../../modal/swap/InfoBox"; import CliLogsBox from "../../other/RenderedCliLog"; @@ -42,26 +42,26 @@ export default function TorInfoBox() { } additionalContent={ - } - requiresRpc={false} + onClick={() => { + throw new Error("Not implemented"); + }} > Start Tor - - + } - requiresRpc={false} + onClick={() => { + throw new Error("Not implemented"); + }} > Stop Tor - + } icon={null} diff --git a/src-gui/src/renderer/components/pages/history/HistoryPage.tsx b/src-gui/src/renderer/components/pages/history/HistoryPage.tsx index d4ec82d8a..1ebe499b4 100644 --- a/src-gui/src/renderer/components/pages/history/HistoryPage.tsx +++ b/src-gui/src/renderer/components/pages/history/HistoryPage.tsx @@ -1,11 +1,11 @@ import { Typography } from "@material-ui/core"; -import { useIsSwapRunning } from "store/hooks"; +import { useAppSelector } from "store/hooks"; import SwapTxLockAlertsBox from "../../alert/SwapTxLockAlertsBox"; import SwapDialog from "../../modal/swap/SwapDialog"; import HistoryTable from "./table/HistoryTable"; export default function HistoryPage() { - const showDialog = useIsSwapRunning(); + const showDialog = useAppSelector((state) => state.swap.state !== null); return ( <> diff --git a/src-gui/src/renderer/components/pages/history/table/HistoryRow.tsx b/src-gui/src/renderer/components/pages/history/table/HistoryRow.tsx index 3f3149ba8..d325c93ab 100644 --- a/src-gui/src/renderer/components/pages/history/table/HistoryRow.tsx +++ b/src-gui/src/renderer/components/pages/history/table/HistoryRow.tsx @@ -6,23 +6,14 @@ import { TableCell, TableRow, } from "@material-ui/core"; -import { useState } from "react"; import ArrowForwardIcon from "@material-ui/icons/ArrowForward"; import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp"; -import { - getHumanReadableDbStateType, - getSwapBtcAmount, - getSwapXmrAmount, - GetSwapInfoResponse, -} from "../../../../../models/rpcModel"; +import { GetSwapInfoResponse } from "models/tauriModel"; +import { useState } from "react"; +import { PiconeroAmount, SatsAmount } from "../../../other/Units"; import HistoryRowActions from "./HistoryRowActions"; import HistoryRowExpanded from "./HistoryRowExpanded"; -import { BitcoinAmount, MoneroAmount } from "../../../other/Units"; - -type HistoryRowProps = { - swap: GetSwapInfoResponse; -}; const useStyles = makeStyles((theme) => ({ amountTransferContainer: { @@ -43,17 +34,14 @@ function AmountTransfer({ return ( - + - + ); } -export default function HistoryRow({ swap }: HistoryRowProps) { - const btcAmount = getSwapBtcAmount(swap); - const xmrAmount = getSwapXmrAmount(swap); - +export default function HistoryRow(swap: GetSwapInfoResponse) { const [expanded, setExpanded] = useState(false); return ( @@ -64,13 +52,16 @@ export default function HistoryRow({ swap }: HistoryRowProps) { {expanded ? : } - {swap.swap_id.substring(0, 5)}... + {swap.swap_id} - + - {getHumanReadableDbStateType(swap.state_name)} + {swap.state_name.toString()} - + diff --git a/src-gui/src/renderer/components/pages/history/table/HistoryRowActions.tsx b/src-gui/src/renderer/components/pages/history/table/HistoryRowActions.tsx index 35b23c246..a6428c72b 100644 --- a/src-gui/src/renderer/components/pages/history/table/HistoryRowActions.tsx +++ b/src-gui/src/renderer/components/pages/history/table/HistoryRowActions.tsx @@ -1,68 +1,65 @@ import { Tooltip } from "@material-ui/core"; -import Button, { ButtonProps } from "@material-ui/core/Button/Button"; +import { ButtonProps } from "@material-ui/core/Button/Button"; +import { green, red } from "@material-ui/core/colors"; import DoneIcon from "@material-ui/icons/Done"; import ErrorIcon from "@material-ui/icons/Error"; -import { green, red } from "@material-ui/core/colors"; import PlayArrowIcon from "@material-ui/icons/PlayArrow"; -import IpcInvokeButton from "../../../IpcInvokeButton"; +import { GetSwapInfoResponse } from "models/tauriModel"; import { - GetSwapInfoResponse, - SwapStateName, - isSwapStateNamePossiblyCancellableSwap, - isSwapStateNamePossiblyRefundableSwap, -} from "../../../../../models/rpcModel"; + BobStateName, + GetSwapInfoResponseExt, + isBobStateNamePossiblyCancellableSwap, + isBobStateNamePossiblyRefundableSwap, +} from "models/tauriModelExt"; +import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; +import { resumeSwap } from "renderer/rpc"; export function SwapResumeButton({ swap, ...props -}: { swap: GetSwapInfoResponse } & ButtonProps) { +}: ButtonProps & { swap: GetSwapInfoResponse }) { return ( - } - requiresRpc + onClick={() => resumeSwap(swap.swap_id)} {...props} > Resume - + ); } export function SwapCancelRefundButton({ swap, ...props -}: { swap: GetSwapInfoResponse } & ButtonProps) { +}: { swap: GetSwapInfoResponseExt } & ButtonProps) { const cancelOrRefundable = - isSwapStateNamePossiblyCancellableSwap(swap.state_name) || - isSwapStateNamePossiblyRefundableSwap(swap.state_name); + isBobStateNamePossiblyCancellableSwap(swap.state_name) || + isBobStateNamePossiblyRefundableSwap(swap.state_name); if (!cancelOrRefundable) { return <>; } return ( - { + // TODO: Implement this using the Tauri RPC + throw new Error("Not implemented"); + }} > Attempt manual Cancel & Refund - + ); } -export default function HistoryRowActions({ - swap, -}: { - swap: GetSwapInfoResponse; -}) { - if (swap.state_name === SwapStateName.XmrRedeemed) { +export default function HistoryRowActions(swap: GetSwapInfoResponse) { + if (swap.state_name === BobStateName.XmrRedeemed) { return ( @@ -70,7 +67,7 @@ export default function HistoryRowActions({ ); } - if (swap.state_name === SwapStateName.BtcRefunded) { + if (swap.state_name === BobStateName.BtcRefunded) { return ( @@ -78,7 +75,9 @@ export default function HistoryRowActions({ ); } - if (swap.state_name === SwapStateName.BtcPunished) { + // TODO: Display a button here to attempt a cooperative redeem + // See this PR: https://github.com/UnstoppableSwap/unstoppableswap-gui/pull/212 + if (swap.state_name === BobStateName.BtcPunished) { return ( diff --git a/src-gui/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx b/src-gui/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx index 4bbb562a0..a691dc396 100644 --- a/src-gui/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx +++ b/src-gui/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx @@ -8,24 +8,15 @@ import { TableContainer, TableRow, } from "@material-ui/core"; -import { getBitcoinTxExplorerUrl } from "utils/conversionUtils"; -import { isTestnet } from "store/config"; -import { - getHumanReadableDbStateType, - getSwapBtcAmount, - getSwapExchangeRate, - getSwapTxFees, - getSwapXmrAmount, - GetSwapInfoResponse, -} from "../../../../../models/rpcModel"; -import SwapLogFileOpenButton from "./SwapLogFileOpenButton"; -import { SwapCancelRefundButton } from "./HistoryRowActions"; -import { SwapMoneroRecoveryButton } from "./SwapMoneroRecoveryButton"; +import { GetSwapInfoResponse } from "models/tauriModel"; import { - BitcoinAmount, - MoneroAmount, MoneroBitcoinExchangeRate, + PiconeroAmount, + SatsAmount, } from "renderer/components/other/Units"; +import { isTestnet } from "store/config"; +import { getBitcoinTxExplorerUrl } from "utils/conversionUtils"; +import SwapLogFileOpenButton from "./SwapLogFileOpenButton"; const useStyles = makeStyles((theme) => ({ outer: { @@ -47,12 +38,6 @@ export default function HistoryRowExpanded({ }) { const classes = useStyles(); - const { seller, start_date: startDate } = swap; - const btcAmount = getSwapBtcAmount(swap); - const xmrAmount = getSwapXmrAmount(swap); - const txFees = getSwapTxFees(swap); - const exchangeRate = getSwapExchangeRate(swap); - return ( @@ -60,7 +45,7 @@ export default function HistoryRowExpanded({ Started on - {startDate} + {swap.start_date} Swap ID @@ -68,38 +53,39 @@ export default function HistoryRowExpanded({ State Name - - {getHumanReadableDbStateType(swap.state_name)} - + {swap.state_name} Monero Amount - + Bitcoin Amount - + Exchange Rate - + Bitcoin Network Fees - + Provider Address - {seller.addresses.join(", ")} + {swap.seller.addresses.join(", ")} @@ -122,12 +108,16 @@ export default function HistoryRowExpanded({ variant="outlined" size="small" /> - - + {/* + // TOOD: reimplement these buttons using Tauri + + + + */} ); diff --git a/src-gui/src/renderer/components/pages/history/table/HistoryTable.tsx b/src-gui/src/renderer/components/pages/history/table/HistoryTable.tsx index acc40a096..43b012d0c 100644 --- a/src-gui/src/renderer/components/pages/history/table/HistoryTable.tsx +++ b/src-gui/src/renderer/components/pages/history/table/HistoryTable.tsx @@ -9,12 +9,7 @@ import { TableHead, TableRow, } from "@material-ui/core"; -import { sortBy } from "lodash"; -import { parseDateString } from "utils/parseUtils"; -import { - useAppSelector, - useSwapInfosSortedByDate, -} from "../../../../../store/hooks"; +import { useSwapInfosSortedByDate } from "../../../../../store/hooks"; import HistoryRow from "./HistoryRow"; const useStyles = makeStyles((theme) => ({ @@ -43,7 +38,7 @@ export default function HistoryTable() { {swapSortedByDate.map((swap) => ( - + ))} diff --git a/src-gui/src/renderer/components/pages/history/table/SwapLogFileOpenButton.tsx b/src-gui/src/renderer/components/pages/history/table/SwapLogFileOpenButton.tsx index dc96d8558..8d899b535 100644 --- a/src-gui/src/renderer/components/pages/history/table/SwapLogFileOpenButton.tsx +++ b/src-gui/src/renderer/components/pages/history/table/SwapLogFileOpenButton.tsx @@ -1,4 +1,3 @@ -import { ButtonProps } from "@material-ui/core/Button/Button"; import { Button, Dialog, @@ -6,9 +5,10 @@ import { DialogContent, DialogTitle, } from "@material-ui/core"; -import { useState } from "react"; +import { ButtonProps } from "@material-ui/core/Button/Button"; import { CliLog } from "models/cliModel"; -import IpcInvokeButton from "../../../IpcInvokeButton"; +import { useState } from "react"; +import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import CliLogsBox from "../../../other/RenderedCliLog"; export default function SwapLogFileOpenButton({ @@ -19,16 +19,17 @@ export default function SwapLogFileOpenButton({ return ( <> - { setLogs(data as CliLog[]); }} + onClick={async () => { + throw new Error("Not implemented"); + }} {...props} > - view log - + View log + {logs && ( setLogs(null)} fullWidth maxWidth="lg"> Logs of swap {swapId} diff --git a/src-gui/src/renderer/components/pages/history/table/SwapMoneroRecoveryButton.tsx b/src-gui/src/renderer/components/pages/history/table/SwapMoneroRecoveryButton.tsx index f40877be7..ad0ad51c7 100644 --- a/src-gui/src/renderer/components/pages/history/table/SwapMoneroRecoveryButton.tsx +++ b/src-gui/src/renderer/components/pages/history/table/SwapMoneroRecoveryButton.tsx @@ -1,4 +1,3 @@ -import { ButtonProps } from "@material-ui/core/Button/Button"; import { Box, Button, @@ -8,17 +7,17 @@ import { DialogContentText, Link, } from "@material-ui/core"; -import { useAppDispatch, useAppSelector } from "store/hooks"; +import { ButtonProps } from "@material-ui/core/Button/Button"; +import { GetSwapInfoArgs } from "models/tauriModel"; import { rpcResetMoneroRecoveryKeys } from "store/features/rpcSlice"; -import { - GetSwapInfoResponse, - isSwapMoneroRecoverable, -} from "../../../../../models/rpcModel"; -import IpcInvokeButton from "../../../IpcInvokeButton"; +import { useAppDispatch, useAppSelector } from "store/hooks"; import DialogHeader from "../../../modal/DialogHeader"; import ScrollablePaperTextBox from "../../../other/ScrollablePaperTextBox"; -function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) { +function MoneroRecoveryKeysDialog() { + // TODO: Reimplement this using the new Tauri API + return null; + const dispatch = useAppDispatch(); const keys = useAppSelector((s) => s.rpc.state.moneroRecovery); @@ -96,24 +95,28 @@ function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) { export function SwapMoneroRecoveryButton({ swap, ...props -}: { swap: GetSwapInfoResponse } & ButtonProps) { +}: { swap: GetSwapInfoArgs } & ButtonProps) { + return <> ; + /* TODO: Reimplement this using the new Tauri API const isRecoverable = isSwapMoneroRecoverable(swap.state_name); + if (!isRecoverable) { return <>; } return ( <> - { + throw new Error("Not implemented"); + }} {...props} > Display Monero Recovery Keys - + ); + */ } diff --git a/src-gui/src/renderer/components/pages/swap/SwapWidget.tsx b/src-gui/src/renderer/components/pages/swap/SwapWidget.tsx index a332b980d..cd85699c1 100644 --- a/src-gui/src/renderer/components/pages/swap/SwapWidget.tsx +++ b/src-gui/src/renderer/components/pages/swap/SwapWidget.tsx @@ -1,27 +1,26 @@ -import { ChangeEvent, useEffect, useState } from "react"; import { - makeStyles, Box, + Fab, + LinearProgress, + makeStyles, Paper, - Typography, TextField, - LinearProgress, - Fab, + Typography, } from "@material-ui/core"; import InputAdornment from "@material-ui/core/InputAdornment"; import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward"; import SwapHorizIcon from "@material-ui/icons/SwapHoriz"; import { Alert } from "@material-ui/lab"; -import { satsToBtc } from "utils/conversionUtils"; -import { useAppSelector } from "store/hooks"; import { ExtendedProviderStatus } from "models/apiModel"; -import { isSwapState } from "models/storeModel"; -import SwapDialog from "../../modal/swap/SwapDialog"; -import ProviderSelect from "../../modal/provider/ProviderSelect"; +import { ChangeEvent, useEffect, useState } from "react"; +import { useAppSelector } from "store/hooks"; +import { satsToBtc } from "utils/conversionUtils"; import { ListSellersDialogOpenButton, ProviderSubmitDialogOpenButton, } from "../../modal/provider/ProviderListDialog"; +import ProviderSelect from "../../modal/provider/ProviderSelect"; +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; @@ -84,9 +83,7 @@ function HasProviderSwapWidget({ }) { const classes = useStyles(); - const forceShowDialog = useAppSelector((state) => - isSwapState(state.swap.state), - ); + const forceShowDialog = useAppSelector((state) => state.swap.state !== null); const [showDialog, setShowDialog] = useState(false); const [btcFieldValue, setBtcFieldValue] = useState( satsToBtc(selectedProvider.minSwapAmount), @@ -177,9 +174,7 @@ function HasProviderSwapWidget({ } function HasNoProvidersSwapWidget() { - const forceShowDialog = useAppSelector((state) => - isSwapState(state.swap.state), - ); + const forceShowDialog = useAppSelector((state) => state.swap.state !== null); const isPublicRegistryDown = useAppSelector((state) => isRegistryDown( state.providers.registry.failedReconnectAttemptsSinceLastSuccess, diff --git a/src-gui/src/renderer/components/pages/wallet/WalletRefreshButton.tsx b/src-gui/src/renderer/components/pages/wallet/WalletRefreshButton.tsx index 7744dca7a..54a6c9a8e 100644 --- a/src-gui/src/renderer/components/pages/wallet/WalletRefreshButton.tsx +++ b/src-gui/src/renderer/components/pages/wallet/WalletRefreshButton.tsx @@ -1,8 +1,6 @@ -import { Button, CircularProgress, IconButton } from "@material-ui/core"; import RefreshIcon from "@material-ui/icons/Refresh"; -import IpcInvokeButton from "../../IpcInvokeButton"; -import { checkBitcoinBalance } from "renderer/rpc"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; +import { checkBitcoinBalance } from "renderer/rpc"; export default function WalletRefreshButton() { return ( diff --git a/src-gui/src/renderer/index.tsx b/src-gui/src/renderer/index.tsx index 59221bcd6..80c12345a 100644 --- a/src-gui/src/renderer/index.tsx +++ b/src-gui/src/renderer/index.tsx @@ -1,4 +1,4 @@ -import { render } from "react-dom"; +import { createRoot } from "react-dom/client"; import { Provider } from "react-redux"; import { setAlerts } from "store/features/alertsSlice"; import { setRegistryProviders } from "store/features/providersSlice"; @@ -14,16 +14,17 @@ import App from "./components/App"; import { checkBitcoinBalance, getRawSwapInfos } from "./rpc"; import { store } from "./store/storeRenderer"; -setTimeout(() => { +setInterval(() => { checkBitcoinBalance(); getRawSwapInfos(); -}, 10000); +}, 5000); -render( +const container = document.getElementById("root"); +const root = createRoot(container!); +root.render( , - document.getElementById("root"), ); async function fetchInitialData() { diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index ed5826a9a..0a9e98c9f 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -1,31 +1,89 @@ -import { invoke } from "@tauri-apps/api/core"; -import { store } from "./store/storeRenderer"; +import { invoke as invokeUnsafe } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { + BalanceArgs, + BalanceResponse, + BuyXmrArgs, + BuyXmrResponse, + GetSwapInfoResponse, + ResumeSwapArgs, + ResumeSwapResponse, + SuspendCurrentSwapResponse, + TauriSwapProgressEventWrapper, + WithdrawBtcArgs, + WithdrawBtcResponse, +} from "models/tauriModel"; import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice"; +import { swapTauriEventReceived } from "store/features/swapSlice"; +import { store } from "./store/storeRenderer"; +import { Provider } from "models/apiModel"; +import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils"; + +listen("swap-progress-update", (event) => { + console.log("Received swap progress event", event.payload); + store.dispatch(swapTauriEventReceived(event.payload)); +}); + +async function invoke( + command: string, + args: ARGS, +): Promise { + return invokeUnsafe(command, { + args: args as Record, + }) as Promise; +} + +async function invokeNoArgs(command: string): Promise { + return invokeUnsafe(command, {}) as Promise; +} export async function checkBitcoinBalance() { - const response = (await invoke("get_balance")) as { - balance: number; - }; + const response = await invoke("get_balance", { + force_refresh: true, + }); store.dispatch(rpcSetBalance(response.balance)); } export async function getRawSwapInfos() { - const response = await invoke("get_swap_infos_all"); + const response = + await invokeNoArgs("get_swap_infos_all"); - (response as any[]).forEach((info) => store.dispatch(rpcSetSwapInfo(info))); + response.forEach((swapInfo) => { + store.dispatch(rpcSetSwapInfo(swapInfo)); + }); } export async function withdrawBtc(address: string): Promise { - const response = (await invoke("withdraw_btc", { - args: { + const response = await invoke( + "withdraw_btc", + { address, amount: null, }, - })) as { - txid: string; - amount: number; - }; + ); return response.txid; } + +export async function buyXmr( + seller: Provider, + bitcoin_change_address: string, + monero_receive_address: string, +) { + await invoke("buy_xmr", { + seller: providerToConcatenatedMultiAddr(seller), + bitcoin_change_address, + monero_receive_address, + }); +} + +export async function resumeSwap(swapId: string) { + await invoke("resume_swap", { + swap_id: swapId, + }); +} + +export async function suspendCurrentSwap() { + await invokeNoArgs("suspend_current_swap"); +} diff --git a/src-gui/src/store/config.ts b/src-gui/src/store/config.ts index 036f7d705..03eeb6f3c 100644 --- a/src-gui/src/store/config.ts +++ b/src-gui/src/store/config.ts @@ -2,14 +2,8 @@ import { ExtendedProviderStatus } from "models/apiModel"; export const isTestnet = () => true; -export const isExternalRpc = () => true; - export const isDevelopment = true; export function getStubTestnetProvider(): ExtendedProviderStatus | null { return null; } - -export const getPlatform = () => { - return "mac"; -}; diff --git a/src-gui/src/store/features/providersSlice.ts b/src-gui/src/store/features/providersSlice.ts index 03ce7bb31..6e00cdd22 100644 --- a/src-gui/src/store/features/providersSlice.ts +++ b/src-gui/src/store/features/providersSlice.ts @@ -1,8 +1,8 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel"; -import { sortProviderList } from "utils/sortUtils"; -import { isProviderCompatible } from "utils/multiAddrUtils"; import { getStubTestnetProvider } from "store/config"; +import { isProviderCompatible } from "utils/multiAddrUtils"; +import { sortProviderList } from "utils/sortUtils"; const stubTestnetProvider = getStubTestnetProvider(); diff --git a/src-gui/src/store/features/rpcSlice.ts b/src-gui/src/store/features/rpcSlice.ts index 599a04b50..df5caba14 100644 --- a/src-gui/src/store/features/rpcSlice.ts +++ b/src-gui/src/store/features/rpcSlice.ts @@ -1,21 +1,12 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel"; -import { MoneroWalletRpcUpdateState } from "models/storeModel"; +import { GetSwapInfoResponse } from "models/tauriModel"; +import { CliLog } from "../../models/cliModel"; import { - GetSwapInfoResponse, MoneroRecoveryResponse, RpcProcessStateType, } from "../../models/rpcModel"; -import { - CliLog, - isCliLog, - isCliLogDownloadingMoneroWalletRpc, - isCliLogFailedToSyncMoneroWallet, - isCliLogFinishedSyncingMoneroWallet, - isCliLogStartedRpcServer, - isCliLogStartedSyncingMoneroWallet, -} from "../../models/cliModel"; -import { getLogsAndStringsFromRawFileString } from "utils/parseUtils"; +import { GetSwapInfoResponseExt } from "models/tauriModelExt"; type Process = | { @@ -41,7 +32,7 @@ interface State { withdrawTxId: string | null; rendezvous_discovered_sellers: (ExtendedProviderStatus | ProviderStatus)[]; swapInfos: { - [swapId: string]: GetSwapInfoResponse; + [swapId: string]: GetSwapInfoResponseExt; }; moneroRecovery: { swapId: string; @@ -51,7 +42,8 @@ interface State { isSyncing: boolean; }; moneroWalletRpc: { - updateState: false | MoneroWalletRpcUpdateState; + // TODO: Reimplement this using Tauri + updateState: false; }; } @@ -85,44 +77,6 @@ export const rpcSlice = createSlice({ name: "rpc", initialState, reducers: { - rpcAddLogs(slice, action: PayloadAction<(CliLog | string)[]>) { - if ( - slice.process.type === RpcProcessStateType.STARTED || - slice.process.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS || - slice.process.type === RpcProcessStateType.EXITED - ) { - const logs = action.payload; - slice.process.logs.push(...logs); - - logs.filter(isCliLog).forEach((log) => { - if ( - isCliLogStartedRpcServer(log) && - slice.process.type === RpcProcessStateType.STARTED - ) { - slice.process = { - type: RpcProcessStateType.LISTENING_FOR_CONNECTIONS, - logs: slice.process.logs, - address: log.fields.addr, - }; - } else if (isCliLogDownloadingMoneroWalletRpc(log)) { - slice.state.moneroWalletRpc.updateState = { - progress: log.fields.progress, - downloadUrl: log.fields.download_url, - }; - - if (log.fields.progress === "100%") { - slice.state.moneroWalletRpc.updateState = false; - } - } else if (isCliLogStartedSyncingMoneroWallet(log)) { - slice.state.moneroWallet.isSyncing = true; - } else if (isCliLogFinishedSyncingMoneroWallet(log)) { - slice.state.moneroWallet.isSyncing = false; - } else if (isCliLogFailedToSyncMoneroWallet(log)) { - slice.state.moneroWallet.isSyncing = false; - } - }); - } - }, rpcInitiate(slice) { slice.process = { type: RpcProcessStateType.STARTED, @@ -169,7 +123,8 @@ export const rpcSlice = createSlice({ slice.state.withdrawTxId = null; }, rpcSetSwapInfo(slice, action: PayloadAction) { - slice.state.swapInfos[action.payload.swap_id] = action.payload; + slice.state.swapInfos[action.payload.swap_id] = + action.payload as GetSwapInfoResponseExt; }, rpcSetEndpointBusy(slice, action: PayloadAction) { if (!slice.busyEndpoints.includes(action.payload)) { @@ -202,7 +157,6 @@ export const rpcSlice = createSlice({ export const { rpcProcessExited, - rpcAddLogs, rpcInitiate, rpcSetBalance, rpcSetWithdrawTxId, diff --git a/src-gui/src/store/features/swapSlice.ts b/src-gui/src/store/features/swapSlice.ts index 2a28896ed..7d0932198 100644 --- a/src-gui/src/store/features/swapSlice.ts +++ b/src-gui/src/store/features/swapSlice.ts @@ -1,55 +1,12 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { extractAmountFromUnitString } from "utils/parseUtils"; -import { Provider } from "models/apiModel"; -import { - isSwapStateBtcLockInMempool, - isSwapStateProcessExited, - isSwapStateXmrLockInMempool, - SwapSlice, - SwapStateAttemptingCooperativeRedeeem, - SwapStateBtcCancelled, - SwapStateBtcLockInMempool, - SwapStateBtcPunished, - SwapStateBtcRedemeed, - SwapStateBtcRefunded, - SwapStateInitiated, - SwapStateProcessExited, - SwapStateReceivedQuote, - SwapStateStarted, - SwapStateType, - SwapStateWaitingForBtcDeposit, - SwapStateXmrLocked, - SwapStateXmrLockInMempool, - SwapStateXmrRedeemInMempool, -} from "../../models/storeModel"; -import { - isCliLogAliceLockedXmr, - isCliLogBtcTxStatusChanged, - isCliLogPublishedBtcTx, - isCliLogReceivedQuote, - isCliLogReceivedXmrLockTxConfirmation, - isCliLogRedeemedXmr, - isCliLogStartedSwap, - isCliLogWaitingForBtcDeposit, - CliLog, - isCliLogAdvancingState, - SwapSpawnType, - isCliLogBtcTxFound, - isCliLogReleasingSwapLockLog, - isYouHaveBeenPunishedCliLog, - isCliLogAcquiringSwapLockLog, - isCliLogApiCallError, - isCliLogDeterminedSwapAmount, - isCliLogAttemptingToCooperativelyRedeemXmr, -} from "../../models/cliModel"; -import logger from "../../utils/logger"; +import { TauriSwapProgressEventWrapper } from "models/tauriModel"; +import { SwapSlice } from "../../models/storeModel"; const initialState: SwapSlice = { state: null, - processRunning: false, - swapId: null, logs: [], - provider: null, + + // TODO: Remove this and replace logic entirely with Tauri events spawnType: null, }; @@ -57,266 +14,27 @@ export const swapSlice = createSlice({ name: "swap", initialState, reducers: { - swapAddLog( - slice, - action: PayloadAction<{ logs: CliLog[]; isFromRestore: boolean }>, + swapTauriEventReceived( + swap, + action: PayloadAction, ) { - const { logs } = action.payload; - slice.logs.push(...logs); - - logs.forEach((log) => { - if ( - isCliLogAcquiringSwapLockLog(log) && - !action.payload.isFromRestore - ) { - slice.processRunning = true; - slice.swapId = log.fields.swap_id; - // TODO: Maybe we can infer more info here (state) from the log - } else if (isCliLogReceivedQuote(log)) { - const price = extractAmountFromUnitString(log.fields.price); - const minimumSwapAmount = extractAmountFromUnitString( - log.fields.minimum_amount, - ); - const maximumSwapAmount = extractAmountFromUnitString( - log.fields.maximum_amount, - ); - - if ( - price != null && - minimumSwapAmount != null && - maximumSwapAmount != null - ) { - const nextState: SwapStateReceivedQuote = { - type: SwapStateType.RECEIVED_QUOTE, - price, - minimumSwapAmount, - maximumSwapAmount, - }; - - slice.state = nextState; - } - } else if (isCliLogWaitingForBtcDeposit(log)) { - const maxGiveable = extractAmountFromUnitString( - log.fields.max_giveable, - ); - const minDeposit = extractAmountFromUnitString( - log.fields.min_deposit_until_swap_will_start, - ); - const maxDeposit = extractAmountFromUnitString( - log.fields.max_deposit_until_maximum_amount_is_reached, - ); - const minimumAmount = extractAmountFromUnitString( - log.fields.minimum_amount, - ); - const maximumAmount = extractAmountFromUnitString( - log.fields.maximum_amount, - ); - const minBitcoinLockTxFee = extractAmountFromUnitString( - log.fields.min_bitcoin_lock_tx_fee, - ); - const price = extractAmountFromUnitString(log.fields.price); - - const depositAddress = log.fields.deposit_address; - - if ( - maxGiveable != null && - minimumAmount != null && - maximumAmount != null && - minDeposit != null && - maxDeposit != null && - minBitcoinLockTxFee != null && - price != null - ) { - const nextState: SwapStateWaitingForBtcDeposit = { - type: SwapStateType.WAITING_FOR_BTC_DEPOSIT, - depositAddress, - maxGiveable, - minimumAmount, - maximumAmount, - minDeposit, - maxDeposit, - price, - minBitcoinLockTxFee, - }; - - slice.state = nextState; - } - } else if (isCliLogDeterminedSwapAmount(log)) { - const amount = extractAmountFromUnitString(log.fields.amount); - const fees = extractAmountFromUnitString(log.fields.fees); - - const nextState: SwapStateStarted = { - type: SwapStateType.STARTED, - txLockDetails: - amount != null && fees != null ? { amount, fees } : null, - }; - - slice.state = nextState; - } else if (isCliLogStartedSwap(log)) { - if (slice.state?.type !== SwapStateType.STARTED) { - const nextState: SwapStateStarted = { - type: SwapStateType.STARTED, - txLockDetails: null, - }; - - slice.state = nextState; - } - - slice.swapId = log.fields.swap_id; - } else if (isCliLogPublishedBtcTx(log)) { - if (log.fields.kind === "lock") { - const nextState: SwapStateBtcLockInMempool = { - type: SwapStateType.BTC_LOCK_TX_IN_MEMPOOL, - bobBtcLockTxId: log.fields.txid, - bobBtcLockTxConfirmations: 0, - }; - - slice.state = nextState; - } else if (log.fields.kind === "cancel") { - const nextState: SwapStateBtcCancelled = { - type: SwapStateType.BTC_CANCELLED, - btcCancelTxId: log.fields.txid, - }; - - slice.state = nextState; - } else if (log.fields.kind === "refund") { - const nextState: SwapStateBtcRefunded = { - type: SwapStateType.BTC_REFUNDED, - bobBtcRefundTxId: log.fields.txid, - }; - - slice.state = nextState; - } - } else if (isCliLogBtcTxStatusChanged(log) || isCliLogBtcTxFound(log)) { - if (isSwapStateBtcLockInMempool(slice.state)) { - if (slice.state.bobBtcLockTxId === log.fields.txid) { - const newStatusText = isCliLogBtcTxStatusChanged(log) - ? log.fields.new_status - : log.fields.status; - - if (newStatusText.startsWith("confirmed with")) { - const confirmations = Number.parseInt( - newStatusText.split(" ")[2], - 10, - ); - - slice.state.bobBtcLockTxConfirmations = confirmations; - } - } - } - } else if (isCliLogAliceLockedXmr(log)) { - const nextState: SwapStateXmrLockInMempool = { - type: SwapStateType.XMR_LOCK_TX_IN_MEMPOOL, - aliceXmrLockTxId: log.fields.txid, - aliceXmrLockTxConfirmations: 0, - }; - - slice.state = nextState; - } else if (isCliLogReceivedXmrLockTxConfirmation(log)) { - if (isSwapStateXmrLockInMempool(slice.state)) { - if (slice.state.aliceXmrLockTxId === log.fields.txid) { - slice.state.aliceXmrLockTxConfirmations = Number.parseInt( - log.fields.seen_confirmations, - 10, - ); - } - } - } else if (isCliLogAdvancingState(log)) { - if (log.fields.state === "xmr is locked") { - const nextState: SwapStateXmrLocked = { - type: SwapStateType.XMR_LOCKED, - }; - - slice.state = nextState; - } else if (log.fields.state === "btc is redeemed") { - const nextState: SwapStateBtcRedemeed = { - type: SwapStateType.BTC_REDEEMED, - }; - - slice.state = nextState; - } - } else if (isCliLogRedeemedXmr(log)) { - const nextState: SwapStateXmrRedeemInMempool = { - type: SwapStateType.XMR_REDEEM_IN_MEMPOOL, - bobXmrRedeemTxId: log.fields.txid, - bobXmrRedeemAddress: log.fields.monero_receive_address, - }; - - slice.state = nextState; - } else if (isYouHaveBeenPunishedCliLog(log)) { - const nextState: SwapStateBtcPunished = { - type: SwapStateType.BTC_PUNISHED, - }; - - slice.state = nextState; - } else if (isCliLogAttemptingToCooperativelyRedeemXmr(log)) { - const nextState: SwapStateAttemptingCooperativeRedeeem = { - type: SwapStateType.ATTEMPTING_COOPERATIVE_REDEEM, - }; - - slice.state = nextState; - } else if ( - isCliLogReleasingSwapLockLog(log) && - !action.payload.isFromRestore - ) { - const nextState: SwapStateProcessExited = { - type: SwapStateType.PROCESS_EXITED, - prevState: slice.state, - rpcError: null, - }; - - slice.state = nextState; - slice.processRunning = false; - } else if (isCliLogApiCallError(log) && !action.payload.isFromRestore) { - if (isSwapStateProcessExited(slice.state)) { - slice.state.rpcError = log.fields.err; - } - } else { - logger.debug({ log }, `Swap log was not reduced`); - } - }); + if (swap.state === null || action.payload.swap_id !== swap.state.swapId) { + swap.state = { + curr: action.payload.event, + prev: null, + swapId: action.payload.swap_id, + }; + } else { + swap.state.prev = swap.state.curr; + swap.state.curr = action.payload.event; + } }, swapReset() { return initialState; }, - swapInitiate( - swap, - action: PayloadAction<{ - provider: Provider | null; - spawnType: SwapSpawnType; - swapId: string | null; - }>, - ) { - const nextState: SwapStateInitiated = { - type: SwapStateType.INITIATED, - }; - - swap.processRunning = true; - swap.state = nextState; - swap.logs = []; - swap.provider = action.payload.provider; - swap.spawnType = action.payload.spawnType; - swap.swapId = action.payload.swapId; - }, - swapProcessExited(swap, action: PayloadAction) { - if (!swap.processRunning) { - logger.warn(`swapProcessExited called on a swap that is not running`); - return; - } - - const nextState: SwapStateProcessExited = { - type: SwapStateType.PROCESS_EXITED, - prevState: swap.state, - rpcError: action.payload, - }; - - swap.state = nextState; - swap.processRunning = false; - }, }, }); -export const { swapInitiate, swapProcessExited, swapReset, swapAddLog } = - swapSlice.actions; +export const { swapReset, swapTauriEventReceived } = swapSlice.actions; export default swapSlice.reducer; diff --git a/src-gui/src/store/hooks.ts b/src-gui/src/store/hooks.ts index 1a39f3485..c3ff6219a 100644 --- a/src-gui/src/store/hooks.ts +++ b/src-gui/src/store/hooks.ts @@ -17,17 +17,20 @@ export function useResumeableSwapsCount() { } export function useIsSwapRunning() { - return useAppSelector((state) => state.swap.state !== null); + return useAppSelector( + (state) => + state.swap.state !== null && state.swap.state.curr.type !== "Released", + ); } 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, ); } export function useActiveSwapId() { - return useAppSelector((s) => s.swap.swapId); + return useAppSelector((s) => s.swap.state?.swapId ?? null); } export function useActiveSwapInfo() { diff --git a/src-gui/src/utils/multiAddrUtils.ts b/src-gui/src/utils/multiAddrUtils.ts index 79277e680..119969f26 100644 --- a/src-gui/src/utils/multiAddrUtils.ts +++ b/src-gui/src/utils/multiAddrUtils.ts @@ -1,6 +1,6 @@ +import { ExtendedProviderStatus, Provider } from "models/apiModel"; import { Multiaddr } from "multiaddr"; import semver from "semver"; -import { ExtendedProviderStatus, Provider } from "models/apiModel"; import { isTestnet } from "store/config"; const MIN_ASB_VERSION = "0.12.0"; diff --git a/src-gui/src/utils/parseUtils.ts b/src-gui/src/utils/parseUtils.ts index 5c29001b9..6dfe93be2 100644 --- a/src-gui/src/utils/parseUtils.ts +++ b/src-gui/src/utils/parseUtils.ts @@ -1,4 +1,4 @@ -import { CliLog, isCliLog } from "models/cliModel"; +import { CliLog } from "models/cliModel"; /* Extract btc amount from string @@ -17,21 +17,28 @@ export function extractAmountFromUnitString(text: string): number | null { return null; } -// E.g 2021-12-29 14:25:59.64082 +00:00:00 +// E.g: 2024-08-19 6:11:37.475038 +00:00:00 export function parseDateString(str: string): number { - const parts = str.split(" ").slice(0, -1); - if (parts.length !== 2) { - throw new Error( - `Date string does not consist solely of date and time Str: ${str} Parts: ${parts}`, - ); + // Split the string and take only the date and time parts + const [datePart, timePart] = str.split(" "); + + if (!datePart || !timePart) { + throw new Error(`Invalid date string format: ${str}`); } - const wholeString = parts.join(" "); - const date = Date.parse(wholeString); + + // Parse time part + const [hours, minutes, seconds] = timePart.split(":"); + const paddedHours = hours.padStart(2, "0"); // Ensure two-digit hours + + // Combine date and time parts, ensuring two-digit hours + const dateTimeString = `${datePart}T${paddedHours}:${minutes}:${seconds.split(".")[0]}Z`; + + const date = Date.parse(dateTimeString); + if (Number.isNaN(date)) { - throw new Error( - `Date string could not be parsed Str: ${str} Parts: ${parts}`, - ); + throw new Error(`Date string could not be parsed: ${str}`); } + return date; } @@ -50,13 +57,15 @@ export function getLogsAndStringsFromRawFileString( return getLinesOfString(rawFileData).map((line) => { try { return JSON.parse(line); - } catch (e) { + } catch { return line; } }); } export function getLogsFromRawFileString(rawFileData: string): CliLog[] { + // TODO: Reimplement this using Tauri + return []; return getLogsAndStringsFromRawFileString(rawFileData).filter(isCliLog); } diff --git a/src-gui/yarn.lock b/src-gui/yarn.lock index d9b883bcc..be7f5b943 100644 --- a/src-gui/yarn.lock +++ b/src-gui/yarn.lock @@ -339,6 +339,62 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.11.0": + version "4.11.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae" + integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A== + +"@eslint/config-array@^0.17.1": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.17.1.tgz#d9b8b8b6b946f47388f32bedfd3adf29ca8f8910" + integrity sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA== + dependencies: + "@eslint/object-schema" "^2.1.4" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/eslintrc@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6" + integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@9.9.0", "@eslint/js@^9.9.0": + version "9.9.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.9.0.tgz#d8437adda50b3ed4401964517b64b4f59b0e2638" + integrity sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug== + +"@eslint/object-schema@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.4.tgz#9e69f8bb4031e11df79e03db09f9dbbae1740843" + integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ== + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.0.tgz#6d86b8cb322660f03d3f0aa94b99bdd8e172d570" + integrity sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew== + "@jridgewell/gen-mapping@^0.3.5": version "0.3.5" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" @@ -453,15 +509,26 @@ prop-types "^15.7.2" react-is "^16.8.0 || ^17.0.0" -"@open-rpc/client-js@^1.8.1": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@open-rpc/client-js/-/client-js-1.8.1.tgz#73b5a5bf237f24b14c3c89205b1fca3aea213213" - integrity sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g== +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: - isomorphic-fetch "^3.0.0" - isomorphic-ws "^5.0.0" - strict-event-emitter-types "^2.0.0" - ws "^7.0.0" + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" "@reduxjs/toolkit@^2.2.6": version "2.2.6" @@ -558,10 +625,10 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4" integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g== -"@tauri-apps/api@2.0.0-beta.14", "@tauri-apps/api@>=2.0.0-beta.0": - version "2.0.0-beta.14" - resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.14.tgz#8c1c65c07559cd29c5103a99e0abe5331cc2246f" - integrity sha512-YLYgHqdwWswr4Y70+hRzaLD6kLIUgHhE3shLXNquPiTaQ9+cX3Q2dB0AFfqsua6NXYFNe7LfkmMzaqEzqv3yQg== +"@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/cli-darwin-arm64@2.0.0-beta.21": version "2.0.0-beta.21" @@ -629,13 +696,6 @@ "@tauri-apps/cli-win32-ia32-msvc" "2.0.0-beta.21" "@tauri-apps/cli-win32-x64-msvc" "2.0.0-beta.21" -"@tauri-apps/plugin-shell@>=2.0.0-beta.0": - version "2.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-beta.7.tgz#43159959ff8ef83435df6d64be381606f6e02130" - integrity sha512-oJxWbEiNRcoMM0PrePjJnjPHEAN1sbYuWaQ1QMtLPdjHsl83RLk+RpFzkL5WvtGknfiKY7T2qEthOID4br+mvg== - dependencies: - "@tauri-apps/api" "2.0.0-beta.14" - "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -669,13 +729,6 @@ dependencies: "@babel/types" "^7.20.7" -"@types/connect@^3.4.33": - version "3.4.38" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" - integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== - dependencies: - "@types/node" "*" - "@types/estree@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" @@ -691,18 +744,6 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.6.tgz#193ced6a40c8006cfc1ca3f4553444fb38f0e543" integrity sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA== -"@types/node@*": - version "20.14.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.11.tgz#09b300423343460455043ddd4d0ded6ac579b74b" - integrity sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA== - dependencies: - undici-types "~5.26.4" - -"@types/node@^12.12.54": - version "12.20.55" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" - integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== - "@types/node@^20.14.10": version "20.14.10" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a" @@ -747,12 +788,86 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== -"@types/ws@^7.4.4": - version "7.4.7" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" - integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== - dependencies: - "@types/node" "*" +"@typescript-eslint/eslint-plugin@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.1.0.tgz#3c020deeaaba82a6f741d00dacf172c53be4911f" + integrity sha512-LlNBaHFCEBPHyD4pZXb35mzjGkuGKXU5eeCA1SxvHfiRES0E82dOounfVpL4DCqYvJEKab0bZIA0gCRpdLKkCw== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.1.0" + "@typescript-eslint/type-utils" "8.1.0" + "@typescript-eslint/utils" "8.1.0" + "@typescript-eslint/visitor-keys" "8.1.0" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/parser@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.1.0.tgz#b7e77f5fa212df59eba51ecd4986f194bccc2303" + integrity sha512-U7iTAtGgJk6DPX9wIWPPOlt1gO57097G06gIcl0N0EEnNw8RGD62c+2/DiP/zL7KrkqnnqF7gtFGR7YgzPllTA== + dependencies: + "@typescript-eslint/scope-manager" "8.1.0" + "@typescript-eslint/types" "8.1.0" + "@typescript-eslint/typescript-estree" "8.1.0" + "@typescript-eslint/visitor-keys" "8.1.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.1.0.tgz#dd8987d2efebb71d230a1c71d82e84a7aead5c3d" + integrity sha512-DsuOZQji687sQUjm4N6c9xABJa7fjvfIdjqpSIIVOgaENf2jFXiM9hIBZOL3hb6DHK9Nvd2d7zZnoMLf9e0OtQ== + dependencies: + "@typescript-eslint/types" "8.1.0" + "@typescript-eslint/visitor-keys" "8.1.0" + +"@typescript-eslint/type-utils@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.1.0.tgz#dbf5a4308166dfc37a36305390dea04a3a3b5048" + integrity sha512-oLYvTxljVvsMnldfl6jIKxTaU7ok7km0KDrwOt1RHYu6nxlhN3TIx8k5Q52L6wR33nOwDgM7VwW1fT1qMNfFIA== + dependencies: + "@typescript-eslint/typescript-estree" "8.1.0" + "@typescript-eslint/utils" "8.1.0" + debug "^4.3.4" + ts-api-utils "^1.3.0" + +"@typescript-eslint/types@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.1.0.tgz#fbf1eaa668a7e444ac507732ca9d3c3468e5db9c" + integrity sha512-q2/Bxa0gMOu/2/AKALI0tCKbG2zppccnRIRCW6BaaTlRVaPKft4oVYPp7WOPpcnsgbr0qROAVCVKCvIQ0tbWog== + +"@typescript-eslint/typescript-estree@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.1.0.tgz#c44e5667683c0bb5caa43192e27de6a994f4e4c4" + integrity sha512-NTHhmufocEkMiAord/g++gWKb0Fr34e9AExBRdqgWdVBaKoei2dIyYKD9Q0jBnvfbEA5zaf8plUFMUH6kQ0vGg== + dependencies: + "@typescript-eslint/types" "8.1.0" + "@typescript-eslint/visitor-keys" "8.1.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/utils@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.1.0.tgz#a922985a43d2560ce0d293be79148fa80c1325e0" + integrity sha512-ypRueFNKTIFwqPeJBfeIpxZ895PQhNyH4YID6js0UoBImWYoSjBsahUn9KMiJXh94uOjVBgHD9AmkyPsPnFwJA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.1.0" + "@typescript-eslint/types" "8.1.0" + "@typescript-eslint/typescript-estree" "8.1.0" + +"@typescript-eslint/visitor-keys@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.1.0.tgz#ab2b3a9699a8ddebf0c205e133f114c1fed9daad" + integrity sha512-ba0lNI19awqZ5ZNKh6wCModMwoZs457StTebQ0q1NP58zSi2F6MOZRXwfKZy+jB78JNJ/WH8GSh2IQNzXX8Nag== + dependencies: + "@typescript-eslint/types" "8.1.0" + eslint-visitor-keys "^3.4.3" "@vitejs/plugin-react@^4.2.1": version "4.3.1" @@ -765,14 +880,6 @@ "@types/babel__core" "^7.20.5" react-refresh "^0.14.2" -JSONStream@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" - integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -780,6 +887,31 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.12.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -787,16 +919,144 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-includes@^3.1.6, array-includes@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.findlast@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.flat@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + atomic-sleep@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + browserslist@^4.22.2: version "4.23.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96" @@ -815,6 +1075,22 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + caniuse-lite@^1.0.30001629: version "1.0.30001640" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz#32c467d4bf1f1a0faa63fc793c2ba81169e7652f" @@ -829,6 +1105,14 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + clsx@^1.0.4, clsx@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" @@ -841,27 +1125,39 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + colorette@^2.0.7: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== -commander@^2.20.3: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -cross-spawn@^7.0.3: +cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -888,6 +1184,33 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + dateformat@^4.6.3: version "4.6.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" @@ -900,6 +1223,18 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: dependencies: ms "2.1.2" +debug@^4.3.2, debug@^4.3.4: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + default-gateway@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" @@ -907,10 +1242,30 @@ default-gateway@^6.0.3: dependencies: execa "^5.0.0" -delay@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" - integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" dns-over-http-resolver@^1.2.3: version "1.2.3" @@ -921,6 +1276,13 @@ dns-over-http-resolver@^1.2.3: native-fetch "^3.0.0" receptacle "^1.3.2" +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" @@ -946,17 +1308,121 @@ err-code@^3.0.1: resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" integrity sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA== -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-iterator-helpers@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" + integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.2" + safe-array-concat "^1.1.2" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== dependencies: - es6-promise "^4.0.3" + hasown "^2.0.0" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" esbuild@^0.21.3: version "0.21.5" @@ -997,6 +1463,126 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-plugin-react@^7.35.0: + version "7.35.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz#00b1e4559896710e58af6358898f2ff917ea4c41" + integrity sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA== + dependencies: + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" + array.prototype.flatmap "^1.3.2" + array.prototype.tosorted "^1.1.4" + doctrine "^2.1.0" + es-iterator-helpers "^1.0.19" + estraverse "^5.3.0" + hasown "^2.0.2" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.8" + object.fromentries "^2.0.8" + object.values "^1.2.0" + prop-types "^15.8.1" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.11" + string.prototype.repeat "^1.0.0" + +eslint-scope@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.2.tgz#5cbb33d4384c9136083a71190d548158fe128f94" + integrity sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" + integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== + +eslint@^9.9.0: + version "9.9.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.9.0.tgz#8d214e69ae4debeca7ae97daebbefe462072d975" + integrity sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.11.0" + "@eslint/config-array" "^0.17.1" + "@eslint/eslintrc" "^3.1.0" + "@eslint/js" "9.9.0" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.3.0" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.0.2" + eslint-visitor-keys "^4.0.0" + espree "^10.1.0" + esquery "^1.5.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^10.0.1, espree@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.1.0.tgz#8788dae611574c0f070691f522e4116c5a11fc56" + integrity sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA== + dependencies: + acorn "^8.12.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.0.0" + +esquery@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -1022,16 +1608,37 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -eyes@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" - integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== - fast-copy@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.2.tgz#59c68f59ccbcac82050ba992e0d5c389097c9d35" integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ== +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + fast-redact@^3.1.1: version "3.5.0" resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" @@ -1042,26 +1649,159 @@ fast-safe-stringify@^2.1.1: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +globals@^15.9.0: + version "15.9.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.9.0.tgz#e9de01771091ffbc37db5714dab484f9f69ff399" + integrity sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA== + +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + globrex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" @@ -1072,11 +1812,64 @@ goober@^2.0.33: resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.14.tgz#4a5c94fc34dc086a8e6035360ae1800005135acd" integrity sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg== +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + help-me@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6" @@ -1109,11 +1902,29 @@ ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +ignore@^5.2.0, ignore@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + immer@^10.0.3: version "10.1.1" resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc" integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw== +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + internal-ip@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-7.0.0.tgz#5b1c6a9d7e188aa73a1b69717daf50c8d8ed774f" @@ -1124,6 +1935,15 @@ internal-ip@^7.0.0: is-ip "^3.1.0" p-event "^4.2.0" +internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + ip-regex@^4.0.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" @@ -1134,6 +1954,88 @@ ipaddr.js@^2.0.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.13.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" + integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1, is-date-object@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + is-in-browser@^1.0.2, is-in-browser@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" @@ -1146,51 +2048,119 @@ is-ip@^3.1.0: dependencies: ip-regex "^4.0.0" +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isomorphic-fetch@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" - integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== dependencies: - node-fetch "^2.6.1" - whatwg-fetch "^3.4.1" - -isomorphic-ws@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== - -isomorphic-ws@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" - integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== - -jayson@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.1.tgz#282ff13d3cea09776db684b7eeca98c47b2fa99a" - integrity sha512-5ZWm4Q/0DHPyeMfAsrwViwUS2DMVsQgWh8bEEIVTkfb3DzHZ2L3G5WUnF+AKmGjjM9r1uAv73SaqC1/U4RL45w== - dependencies: - "@types/connect" "^3.4.33" - "@types/node" "^12.12.54" - "@types/ws" "^7.4.4" - JSONStream "^1.3.5" - commander "^2.20.3" - delay "^5.0.0" - es6-promisify "^5.0.0" - eyes "^0.1.8" - isomorphic-ws "^4.0.1" - json-stringify-safe "^5.0.1" - uuid "^8.3.2" - ws "^7.5.10" + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" joycon@^3.1.1: version "3.1.1" @@ -1202,26 +2172,38 @@ joycon@^3.1.1: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -json-stringify-safe@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonparse@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" - integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== - jss-plugin-camel-case@^10.5.1: version "10.10.0" resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz#27ea159bab67eb4837fa0260204eb7925d4daa1c" @@ -1292,6 +2274,43 @@ jss@10.10.0, jss@^10.5.1: is-in-browser "^1.1.3" tiny-warning "^1.0.2" +"jsx-ast-utils@^2.4.1 || ^3.0.0": + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -1316,11 +2335,38 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -1363,12 +2409,10 @@ native-fetch@^3.0.0: resolved "https://registry.yarnpkg.com/native-fetch/-/native-fetch-3.0.0.tgz#06ccdd70e79e171c365c75117959cf4fe14a09bb" integrity sha512-G3Z7vx0IFb/FQ4JxvtqGABsOTIqRWvgQz6e+erkB+JJD6LrszQtMozEHI4EkmgZQvnGHrpLVzUWk7t4sJCIkVw== -node-fetch@^2.6.1: - version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== node-releases@^2.0.14: version "2.0.14" @@ -1395,6 +2439,54 @@ object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4, object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +object.fromentries@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.values@^1.1.6, object.values@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + on-exit-leak-free@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" @@ -1414,6 +2506,18 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + p-event@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5" @@ -1426,6 +2530,20 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-timeout@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" @@ -1433,16 +2551,43 @@ p-timeout@^3.1.0: dependencies: p-finally "^1.0.0" +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + picocolors@^1.0.0, picocolors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + pino-abstract-transport@^1.0.0, pino-abstract-transport@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" @@ -1498,6 +2643,11 @@ popper.js@1.16.1-lts: resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1-lts.tgz#cf6847b807da3799d80ee3d6d2f90df8a3f50b05" integrity sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA== +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + postcss@^8.4.39: version "8.4.39" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3" @@ -1507,6 +2657,11 @@ postcss@^8.4.39: picocolors "^1.0.1" source-map-js "^1.2.0" +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + process-warning@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" @@ -1534,11 +2689,21 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + qr.js@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ== +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + quick-format-unescaped@^4.0.3: version "4.0.4" resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" @@ -1648,16 +2813,58 @@ redux@^5.0.1: resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== +reflect.getprototypeof@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + reselect@^5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rollup@^4.13.0: version "4.18.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.18.0.tgz#497f60f0c5308e4602cf41136339fbf87d5f5dda" @@ -1683,11 +2890,37 @@ rollup@^4.13.0: "@rollup/rollup-win32-x64-msvc" "4.18.0" fsevents "~2.3.2" +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + safe-stable-stringify@^2.3.1: version "2.4.3" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" @@ -1710,11 +2943,38 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +semver@^7.6.0: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + semver@^7.6.2: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -1727,11 +2987,26 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +side-channel@^1.0.4, side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + sonic-boom@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.0.1.tgz#515b7cef2c9290cb362c4536388ddeece07aed30" @@ -1749,10 +3024,59 @@ split2@^4.0.0: resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== -strict-event-emitter-types@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f" - integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA== +string.prototype.matchall@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" + +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" string_decoder@^1.3.0: version "1.3.0" @@ -1761,6 +3085,13 @@ string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" @@ -1778,6 +3109,23 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + thread-stream@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" @@ -1785,11 +3133,6 @@ thread-stream@^3.0.0: dependencies: real-require "^0.2.0" -"through@>=2.2.7 <3": - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" @@ -1800,16 +3143,83 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-api-utils@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== tsconfck@^3.0.3: version "3.1.1" resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.1.tgz#c7284913262c293b43b905b8b034f524de4a3162" integrity sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ== +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typescript-eslint@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.1.0.tgz#c43a3543ab34c37b7f88deb4ff18b9764aed0b60" + integrity sha512-prB2U3jXPJLpo1iVLN338Lvolh6OrcCZO+9Yv6AR+tvegPPptYCDBIHiEEUdqRi8gAv2bXNKfMUrgAd2ejn/ow== + dependencies: + "@typescript-eslint/eslint-plugin" "8.1.0" + "@typescript-eslint/parser" "8.1.0" + "@typescript-eslint/utils" "8.1.0" + typescript@^5.2.2: version "5.5.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" @@ -1822,6 +3232,16 @@ uint8arrays@^3.0.0: dependencies: multiformats "^9.4.2" +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" @@ -1835,16 +3255,18 @@ update-browserslist-db@^1.0.16: escalade "^3.1.2" picocolors "^1.0.1" +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + use-sync-external-store@^1.0.0: version "1.2.2" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - varint@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" @@ -1875,23 +3297,55 @@ vite@^5.3.1: optionalDependencies: fsevents "~2.3.3" -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-fetch@^3.4.1: - version "3.6.20" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" - integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-builtin-type@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" + integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== + dependencies: + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.2" + which-typed-array "^1.1.15" + +which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" which@^2.0.1: version "2.0.2" @@ -1900,17 +3354,22 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -ws@^7.0.0, ws@^7.5.10: - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 5425a83871692bd88212ce556e94089ab5bfaa11 Mon Sep 17 00:00:00 2001 From: binarybaron Date: Mon, 26 Aug 2024 15:39:59 +0200 Subject: [PATCH 8/9] bump: Rust version to 1.80 --- README.md | 2 +- rust-toolchain.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 35adc5f0b..b84c72407 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Please have a look at the [contribution guidelines](./CONTRIBUTING.md). ## Rust Version Support Please note that only the latest stable Rust toolchain is supported. -All stable toolchains since 1.74 _should_ work. +All stable toolchains since 1.80 _should_ work. ## Contact diff --git a/rust-toolchain.toml b/rust-toolchain.toml index cc47d97ac..19b708c66 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.74" # also update this in the readme, changelog, and github actions +channel = "1.80" # also update this in the readme, changelog, and github actions components = ["clippy"] targets = ["armv7-unknown-linux-gnueabihf"] From 349035d321ceedd95432c5792a3a4189bb099bf6 Mon Sep 17 00:00:00 2001 From: binarybaron Date: Mon, 26 Aug 2024 15:40:50 +0200 Subject: [PATCH 9/9] refactor(tauri, swap): move rpc api to cli/api --- Cargo.lock | 23 +- src-tauri/src/lib.rs | 8 +- swap/src/cli.rs | 1 + swap/src/{ => cli}/api.rs | 0 swap/src/cli/api/request.rs | 1188 ++++++++++++++++++++++ swap/src/{ => cli}/api/tauri_bindings.rs | 18 +- swap/src/cli/command.rs | 16 +- swap/src/cli/list_sellers.rs | 1 + swap/src/kraken.rs | 2 + swap/src/lib.rs | 1 - swap/src/network.rs | 2 +- swap/src/protocol/bob.rs | 2 +- swap/src/protocol/bob/swap.rs | 2 +- swap/src/rpc.rs | 2 +- swap/src/rpc/methods.rs | 11 +- 15 files changed, 1231 insertions(+), 46 deletions(-) rename swap/src/{ => cli}/api.rs (100%) create mode 100644 swap/src/cli/api/request.rs rename swap/src/{ => cli}/api/tauri_bindings.rs (90%) diff --git a/Cargo.lock b/Cargo.lock index ea46b0e7e..c74ca7bd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2252,7 +2252,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 1.1.3", "proc-macro-error", "proc-macro2", "quote", @@ -3761,7 +3761,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "424f6e86263cd5294cbd7f1e95746b95aca0e0d66bff31e5a40d6baa87b4aa99" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 1.1.3", "proc-macro-error", "proc-macro2", "quote", @@ -3897,7 +3897,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 1.1.3", "proc-macro2", "quote", "syn 1.0.109", @@ -4550,12 +4550,12 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ - "once_cell", - "toml_edit 0.19.15", + "thiserror", + "toml 0.5.11", ] [[package]] @@ -7054,6 +7054,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.7.8" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 98462d571..aa1c0467f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,7 +1,7 @@ use std::result::Result; use std::sync::Arc; use swap::{ - api::{ + cli::api::{ request::{ BalanceArgs, BuyXmrArgs, GetHistoryArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs, @@ -25,7 +25,7 @@ impl ToStringResult for Result { /// This macro is used to create boilerplate functions as tauri commands /// that simply delegate handling to the respective request type. -/// +/// /// # Example /// ```ignored /// tauri_command!(get_balance, BalanceArgs); @@ -43,8 +43,8 @@ macro_rules! tauri_command { async fn $fn_name( context: tauri::State<'_, Arc>, args: $request_name, - ) -> Result<<$request_name as swap::api::request::Request>::Response, String> { - <$request_name as swap::api::request::Request>::request(args, context.inner().clone()) + ) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> { + <$request_name as swap::cli::api::request::Request>::request(args, context.inner().clone()) .await .to_string_result() } diff --git a/swap/src/cli.rs b/swap/src/cli.rs index f0faf146d..a87f19cf6 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -5,6 +5,7 @@ mod event_loop; mod list_sellers; pub mod tracing; pub mod transport; +pub mod api; pub use behaviour::{Behaviour, OutEvent}; pub use cancel_and_refund::{cancel, cancel_and_refund, refund}; diff --git a/swap/src/api.rs b/swap/src/cli/api.rs similarity index 100% rename from swap/src/api.rs rename to swap/src/cli/api.rs diff --git a/swap/src/cli/api/request.rs b/swap/src/cli/api/request.rs new file mode 100644 index 000000000..8c2fa6fa1 --- /dev/null +++ b/swap/src/cli/api/request.rs @@ -0,0 +1,1188 @@ +use super::tauri_bindings::TauriHandle; +use crate::cli::api::tauri_bindings::{TauriEmitter, TauriSwapProgressEvent}; +use crate::cli::api::Context; +use crate::bitcoin::{CancelTimelock, ExpiredTimelocks, PunishTimelock, TxLock}; +use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus}; +use crate::libp2p_ext::MultiAddrExt; +use crate::network::quote::{BidQuote, ZeroQuoteReceived}; +use crate::network::swarm; +use crate::protocol::bob::{BobState, Swap}; +use crate::protocol::{bob, State}; +use crate::{bitcoin, cli, monero, rpc}; +use ::bitcoin::Txid; +use anyhow::{bail, Context as AnyContext, Result}; +use libp2p::core::Multiaddr; +use libp2p::PeerId; +use qrcode::render::unicode; +use qrcode::QrCode; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::cmp::min; +use std::convert::TryInto; +use std::future::Future; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; +use tracing::Instrument; +use typeshare::typeshare; +use uuid::Uuid; + +/// This trait is implemented by all types of request args that +/// the CLI can handle. +/// It provides a unified abstraction that can be useful for generics. +#[allow(async_fn_in_trait)] +pub trait Request { + type Response: Serialize; + async fn request(self, ctx: Arc) -> Result; +} + +// BuyXmr +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct BuyXmrArgs { + #[typeshare(serialized_as = "string")] + pub seller: Multiaddr, + #[typeshare(serialized_as = "string")] + pub bitcoin_change_address: bitcoin::Address, + #[typeshare(serialized_as = "string")] + pub monero_receive_address: monero::Address, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct BuyXmrResponse { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, + pub quote: BidQuote, +} + +impl Request for BuyXmrArgs { + type Response = BuyXmrResponse; + + async fn request(self, ctx: Arc) -> Result { + buy_xmr(self, ctx).await + } +} + +// ResumeSwap +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ResumeSwapArgs { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct ResumeSwapResponse { + pub result: String, +} + +impl Request for ResumeSwapArgs { + type Response = ResumeSwapResponse; + + async fn request(self, ctx: Arc) -> Result { + resume_swap(self, ctx).await + } +} + +// CancelAndRefund +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct CancelAndRefundArgs { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + +impl Request for CancelAndRefundArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + cancel_and_refund(self, ctx).await + } +} + +// MoneroRecovery +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct MoneroRecoveryArgs { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + +impl Request for MoneroRecoveryArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + monero_recovery(self, ctx).await + } +} + +// WithdrawBtc +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct WithdrawBtcArgs { + #[typeshare(serialized_as = "number")] + #[serde(default, with = "::bitcoin::util::amount::serde::as_sat::opt")] + pub amount: Option, + #[typeshare(serialized_as = "string")] + pub address: bitcoin::Address, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct WithdrawBtcResponse { + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub amount: bitcoin::Amount, + pub txid: String, +} + +impl Request for WithdrawBtcArgs { + type Response = WithdrawBtcResponse; + + async fn request(self, ctx: Arc) -> Result { + withdraw_btc(self, ctx).await + } +} + +// ListSellers +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ListSellersArgs { + #[typeshare(serialized_as = "string")] + pub rendezvous_point: Multiaddr, +} + +impl Request for ListSellersArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + list_sellers(self, ctx).await + } +} + +// StartDaemon +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct StartDaemonArgs { + #[typeshare(serialized_as = "string")] + pub server_address: Option, +} + +impl Request for StartDaemonArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + start_daemon(self, (*ctx).clone()).await + } +} + +// GetSwapInfo +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct GetSwapInfoArgs { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + +#[typeshare] +#[derive(Serialize)] +pub struct GetSwapInfoResponse { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, + pub seller: Seller, + pub completed: bool, + pub start_date: String, + #[typeshare(serialized_as = "string")] + pub state_name: String, + #[typeshare(serialized_as = "number")] + pub xmr_amount: monero::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub btc_amount: bitcoin::Amount, + #[typeshare(serialized_as = "string")] + pub tx_lock_id: Txid, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub tx_cancel_fee: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub tx_refund_fee: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub tx_lock_fee: bitcoin::Amount, + pub btc_refund_address: String, + pub cancel_timelock: CancelTimelock, + pub punish_timelock: PunishTimelock, + pub timelock: Option, +} + +impl Request for GetSwapInfoArgs { + type Response = GetSwapInfoResponse; + + async fn request(self, ctx: Arc) -> Result { + get_swap_info(self, ctx).await + } +} + +// Balance +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct BalanceArgs { + pub force_refresh: bool, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct BalanceResponse { + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub balance: bitcoin::Amount, +} + +impl Request for BalanceArgs { + type Response = BalanceResponse; + + async fn request(self, ctx: Arc) -> Result { + get_balance(self, ctx).await + } +} + +// GetHistory +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct GetHistoryArgs; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetHistoryEntry { + #[typeshare(serialized_as = "string")] + swap_id: Uuid, + state: String, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct GetHistoryResponse { + pub swaps: Vec, +} + +impl Request for GetHistoryArgs { + type Response = GetHistoryResponse; + + async fn request(self, ctx: Arc) -> Result { + get_history(ctx).await + } +} + +// Additional structs +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct Seller { + #[typeshare(serialized_as = "string")] + pub peer_id: PeerId, + pub addresses: Vec, +} + +// Suspend current swap +#[derive(Debug, Deserialize)] +pub struct SuspendCurrentSwapArgs; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct SuspendCurrentSwapResponse { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + +impl Request for SuspendCurrentSwapArgs { + type Response = SuspendCurrentSwapResponse; + + async fn request(self, ctx: Arc) -> Result { + suspend_current_swap(ctx).await + } +} + +pub struct GetCurrentSwap; + +impl Request for GetCurrentSwap { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + get_current_swap(ctx).await + } +} + +pub struct GetConfig; + +impl Request for GetConfig { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + get_config(ctx).await + } +} + +pub struct ExportBitcoinWalletArgs; + +impl Request for ExportBitcoinWalletArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + export_bitcoin_wallet(ctx).await + } +} + +pub struct GetConfigArgs; + +impl Request for GetConfigArgs { + type Response = serde_json::Value; + + async fn request(self, ctx: Arc) -> Result { + get_config(ctx).await + } +} + +#[tracing::instrument(fields(method = "suspend_current_swap"), skip(context))] +pub async fn suspend_current_swap(context: Arc) -> Result { + let swap_id = context.swap_lock.get_current_swap_id().await; + + if let Some(id_value) = swap_id { + context.swap_lock.send_suspend_signal().await?; + + Ok(SuspendCurrentSwapResponse { swap_id: id_value }) + } else { + bail!("No swap is currently running") + } +} + +#[tracing::instrument(fields(method = "get_swap_infos_all"), skip(context))] +pub async fn get_swap_infos_all(context: Arc) -> Result> { + let swap_ids = context.db.all().await?; + let mut swap_infos = Vec::new(); + + for (swap_id, _) in swap_ids { + let swap_info = get_swap_info(GetSwapInfoArgs { swap_id }, context.clone()).await?; + swap_infos.push(swap_info); + } + + Ok(swap_infos) +} + +#[tracing::instrument(fields(method = "get_swap_info"), skip(context))] +pub async fn get_swap_info( + args: GetSwapInfoArgs, + context: Arc, +) -> Result { + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + let state = context.db.get_state(args.swap_id).await?; + let is_completed = state.swap_finished(); + + let peer_id = context + .db + .get_peer_id(args.swap_id) + .await + .with_context(|| "Could not get PeerID")?; + + let addresses = context + .db + .get_addresses(peer_id) + .await + .with_context(|| "Could not get addressess")?; + + let start_date = context.db.get_swap_start_date(args.swap_id).await?; + + let swap_state: BobState = state.try_into()?; + + let ( + xmr_amount, + btc_amount, + tx_lock_id, + tx_cancel_fee, + tx_refund_fee, + tx_lock_fee, + btc_refund_address, + cancel_timelock, + punish_timelock, + ) = context + .db + .get_states(args.swap_id) + .await? + .iter() + .find_map(|state| { + if let State::Bob(BobState::SwapSetupCompleted(state2)) = state { + let xmr_amount = state2.xmr; + let btc_amount = state2.tx_lock.lock_amount(); + let tx_cancel_fee = state2.tx_cancel_fee; + let tx_refund_fee = state2.tx_refund_fee; + let tx_lock_id = state2.tx_lock.txid(); + let btc_refund_address = state2.refund_address.to_string(); + + if let Ok(tx_lock_fee) = state2.tx_lock.fee() { + Some(( + xmr_amount, + btc_amount, + tx_lock_id, + tx_cancel_fee, + tx_refund_fee, + tx_lock_fee, + btc_refund_address, + state2.cancel_timelock, + state2.punish_timelock, + )) + } else { + None + } + } else { + None + } + }) + .with_context(|| "Did not find SwapSetupCompleted state for swap")?; + + let timelock = match swap_state.clone() { + BobState::Started { .. } | BobState::SafelyAborted | BobState::SwapSetupCompleted(_) => { + None + } + BobState::BtcLocked { state3: state, .. } + | BobState::XmrLockProofReceived { state, .. } => { + Some(state.expired_timelock(bitcoin_wallet).await?) + } + BobState::XmrLocked(state) | BobState::EncSigSent(state) => { + Some(state.expired_timelock(bitcoin_wallet).await?) + } + BobState::CancelTimelockExpired(state) | BobState::BtcCancelled(state) => { + Some(state.expired_timelock(bitcoin_wallet).await?) + } + BobState::BtcPunished { .. } => Some(ExpiredTimelocks::Punish), + BobState::BtcRefunded(_) | BobState::BtcRedeemed(_) | BobState::XmrRedeemed { .. } => None, + }; + + Ok(GetSwapInfoResponse { + swap_id: args.swap_id, + seller: Seller { + peer_id, + addresses: addresses.iter().map(|a| a.to_string()).collect(), + }, + completed: is_completed, + start_date, + state_name: format!("{}", swap_state), + xmr_amount, + btc_amount, + tx_lock_id, + tx_cancel_fee, + tx_refund_fee, + tx_lock_fee, + btc_refund_address: btc_refund_address.to_string(), + cancel_timelock, + punish_timelock, + timelock, + }) +} + +#[tracing::instrument(fields(method = "buy_xmr"), skip(context))] +pub async fn buy_xmr( + buy_xmr: BuyXmrArgs, + context: Arc, +) -> Result { + let BuyXmrArgs { + seller, + bitcoin_change_address, + monero_receive_address, + } = buy_xmr; + + let swap_id = Uuid::new_v4(); + + let bitcoin_wallet = Arc::clone( + context + .bitcoin_wallet + .as_ref() + .expect("Could not find Bitcoin wallet"), + ); + let monero_wallet = Arc::clone( + context + .monero_wallet + .as_ref() + .context("Could not get Monero wallet")?, + ); + let env_config = context.config.env_config; + let seed = context.config.seed.clone().context("Could not get seed")?; + + let seller_peer_id = seller + .extract_peer_id() + .context("Seller address must contain peer ID")?; + context + .db + .insert_address(seller_peer_id, seller.clone()) + .await?; + + let behaviour = cli::Behaviour::new( + seller_peer_id, + env_config, + bitcoin_wallet.clone(), + (seed.derive_libp2p_identity(), context.config.namespace), + ); + let mut swarm = swarm::cli( + seed.derive_libp2p_identity(), + context.config.tor_socks5_port, + behaviour, + ) + .await?; + + swarm.behaviour_mut().add_address(seller_peer_id, seller); + + context + .db + .insert_monero_address(swap_id, monero_receive_address) + .await?; + + tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized"); + + context.swap_lock.acquire_swap_lock(swap_id).await?; + + 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 { + 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"); + }, + event_loop_result = event_loop => { + match event_loop_result { + Ok(_) => { + tracing::debug!(%swap_id, "EventLoop completed") + } + Err(error) => { + tracing::error!(%swap_id, "EventLoop failed: {:#}", error) + } + } + }, + swap_result = async { + let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size()); + let estimate_fee = |amount| bitcoin_wallet.estimate_fee(TxLock::weight(), amount); + + let determine_amount = determine_btc_to_swap( + context.config.json, + bid_quote, + bitcoin_wallet.new_address(), + || bitcoin_wallet.balance(), + max_givable, + || bitcoin_wallet.sync(), + estimate_fee, + context.tauri_handle.clone() + ); + + let (amount, fees) = match determine_amount.await { + Ok(val) => val, + Err(error) => match error.downcast::() { + Ok(_) => { + bail!("Seller's XMR balance is currently too low to initiate a swap, please try again later") + } + Err(other) => bail!(other), + }, + }; + + tracing::info!(%amount, %fees, "Determined swap amount"); + + context.db.insert_peer_id(swap_id, seller_peer_id).await?; + + let swap = Swap::new( + Arc::clone(&context.db), + swap_id, + Arc::clone(&bitcoin_wallet), + monero_wallet, + env_config, + event_loop_handle, + monero_receive_address, + bitcoin_change_address, + amount, + ).with_event_emitter(context.tauri_handle.clone()); + + bob::run(swap).await + } => { + match swap_result { + Ok(state) => { + tracing::debug!(%swap_id, state=%state, "Swap completed") + } + Err(error) => { + tracing::error!(%swap_id, "Failed to complete swap: {:#}", error) + } + } + }, + }; + tracing::debug!(%swap_id, "Swap completed"); + + context + .swap_lock + .release_swap_lock() + .await + .expect("Could not release swap lock"); + + context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); + + Ok::<_, anyhow::Error>(()) + }.in_current_span()).await; + + Ok(BuyXmrResponse { + swap_id, + quote: bid_quote, + }) +} + +#[tracing::instrument(fields(method = "resume_swap"), skip(context))] +pub async fn resume_swap( + resume: ResumeSwapArgs, + context: Arc, +) -> Result { + let ResumeSwapArgs { swap_id } = resume; + context.swap_lock.acquire_swap_lock(swap_id).await?; + + let seller_peer_id = context.db.get_peer_id(swap_id).await?; + let seller_addresses = context.db.get_addresses(seller_peer_id).await?; + + let seed = context + .config + .seed + .as_ref() + .context("Could not get seed")? + .derive_libp2p_identity(); + + let behaviour = cli::Behaviour::new( + seller_peer_id, + context.config.env_config, + Arc::clone( + context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?, + ), + (seed.clone(), context.config.namespace), + ); + let mut swarm = swarm::cli(seed.clone(), context.config.tor_socks5_port, behaviour).await?; + let our_peer_id = swarm.local_peer_id(); + + tracing::debug!(peer_id = %our_peer_id, "Network layer initialized"); + + for seller_address in seller_addresses { + swarm + .behaviour_mut() + .add_address(seller_peer_id, seller_address); + } + + let (event_loop, event_loop_handle) = + EventLoop::new(swap_id, swarm, seller_peer_id, context.db.clone())?; + let monero_receive_address = context.db.get_monero_address(swap_id).await?; + let swap = Swap::from_db( + Arc::clone(&context.db), + swap_id, + Arc::clone( + context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?, + ), + Arc::clone( + context + .monero_wallet + .as_ref() + .context("Could not get Monero wallet")?, + ), + context.config.env_config, + event_loop_handle, + monero_receive_address, + ) + .await? + .with_event_emitter(context.tauri_handle.clone()); + + context.tasks.clone().spawn( + async move { + let handle = tokio::spawn(event_loop.run().in_current_span()); + 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"); + }, + + event_loop_result = handle => { + match event_loop_result { + Ok(_) => { + tracing::debug!(%swap_id, "EventLoop completed during swap resume") + } + Err(error) => { + tracing::error!(%swap_id, "EventLoop failed during swap resume: {:#}", error) + } + } + }, + swap_result = bob::run(swap) => { + match swap_result { + Ok(state) => { + tracing::debug!(%swap_id, state=%state, "Swap completed after resuming") + } + Err(error) => { + tracing::error!(%swap_id, "Failed to resume swap: {:#}", 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); + + Ok::<(), anyhow::Error>(()) + } + .in_current_span(), + ).await; + + Ok(ResumeSwapResponse { + result: "OK".to_string(), + }) +} + +#[tracing::instrument(fields(method = "cancel_and_refund"), skip(context))] +pub async fn cancel_and_refund( + cancel_and_refund: CancelAndRefundArgs, + context: Arc, +) -> Result { + let CancelAndRefundArgs { swap_id } = cancel_and_refund; + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + context.swap_lock.acquire_swap_lock(swap_id).await?; + + let state = + cli::cancel_and_refund(swap_id, Arc::clone(bitcoin_wallet), Arc::clone(&context.db)).await; + + context + .swap_lock + .release_swap_lock() + .await + .expect("Could not release swap lock"); + + context + .tauri_handle + .emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); + + state.map(|state| { + json!({ + "result": state, + }) + }) +} + +#[tracing::instrument(fields(method = "get_history"), skip(context))] +pub async fn get_history(context: Arc) -> Result { + let swaps = context.db.all().await?; + let mut vec: Vec = Vec::new(); + for (swap_id, state) in swaps { + let state: BobState = state.try_into()?; + vec.push(GetHistoryEntry { + swap_id, + state: state.to_string(), + }) + } + + Ok(GetHistoryResponse { swaps: vec }) +} + +#[tracing::instrument(fields(method = "get_raw_states"), skip(context))] +pub async fn get_raw_states(context: Arc) -> Result { + let raw_history = context.db.raw_all().await?; + + Ok(json!({ "raw_states": raw_history })) +} + +#[tracing::instrument(fields(method = "get_config"), skip(context))] +pub async fn get_config(context: Arc) -> Result { + let data_dir_display = context.config.data_dir.display(); + tracing::info!(path=%data_dir_display, "Data directory"); + tracing::info!(path=%format!("{}/logs", data_dir_display), "Log files directory"); + tracing::info!(path=%format!("{}/sqlite", data_dir_display), "Sqlite file location"); + tracing::info!(path=%format!("{}/seed.pem", data_dir_display), "Seed file location"); + tracing::info!(path=%format!("{}/monero", data_dir_display), "Monero-wallet-rpc directory"); + tracing::info!(path=%format!("{}/wallet", data_dir_display), "Internal bitcoin wallet directory"); + + Ok(json!({ + "log_files": format!("{}/logs", data_dir_display), + "sqlite": format!("{}/sqlite", data_dir_display), + "seed": format!("{}/seed.pem", data_dir_display), + "monero-wallet-rpc": format!("{}/monero", data_dir_display), + "bitcoin_wallet": format!("{}/wallet", data_dir_display), + })) +} + +#[tracing::instrument(fields(method = "withdraw_btc"), skip(context))] +pub async fn withdraw_btc( + withdraw_btc: WithdrawBtcArgs, + context: Arc, +) -> Result { + let WithdrawBtcArgs { address, amount } = withdraw_btc; + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + let amount = match amount { + Some(amount) => amount, + None => { + bitcoin_wallet + .max_giveable(address.script_pubkey().len()) + .await? + } + }; + let psbt = bitcoin_wallet + .send_to_address(address, amount, None) + .await?; + let signed_tx = bitcoin_wallet.sign_and_finalize(psbt).await?; + + bitcoin_wallet + .broadcast(signed_tx.clone(), "withdraw") + .await?; + + Ok(WithdrawBtcResponse { + txid: signed_tx.txid().to_string(), + amount, + }) +} + +#[tracing::instrument(fields(method = "start_daemon"), skip(context))] +pub async fn start_daemon( + start_daemon: StartDaemonArgs, + context: Context, +) -> Result { + let StartDaemonArgs { server_address } = start_daemon; + // Default to 127.0.0.1:1234 + let server_address = server_address.unwrap_or("127.0.0.1:1234".parse()?); + + let (addr, server_handle) = rpc::run_server(server_address, context).await?; + + tracing::info!(%addr, "Started RPC server"); + + server_handle.stopped().await; + + tracing::info!("Stopped RPC server"); + + Ok(json!({})) +} + +#[tracing::instrument(fields(method = "get_balance"), skip(context))] +pub async fn get_balance(balance: BalanceArgs, context: Arc) -> Result { + let BalanceArgs { force_refresh } = balance; + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + if force_refresh { + bitcoin_wallet.sync().await?; + } + + let bitcoin_balance = bitcoin_wallet.balance().await?; + + if force_refresh { + tracing::info!( + balance = %bitcoin_balance, + "Checked Bitcoin balance", + ); + } else { + tracing::debug!( + balance = %bitcoin_balance, + "Current Bitcoin balance as of last sync", + ); + } + + Ok(BalanceResponse { + balance: bitcoin_balance, + }) +} + +#[tracing::instrument(fields(method = "list_sellers"), skip(context))] +pub async fn list_sellers( + list_sellers: ListSellersArgs, + context: Arc, +) -> Result { + let ListSellersArgs { rendezvous_point } = list_sellers; + let rendezvous_node_peer_id = rendezvous_point + .extract_peer_id() + .context("Rendezvous node address must contain peer ID")?; + + let identity = context + .config + .seed + .as_ref() + .context("Cannot extract seed")? + .derive_libp2p_identity(); + + let sellers = list_sellers_impl( + rendezvous_node_peer_id, + rendezvous_point, + context.config.namespace, + context.config.tor_socks5_port, + identity, + ) + .await?; + + for seller in &sellers { + match seller.status { + SellerStatus::Online(quote) => { + tracing::info!( + price = %quote.price.to_string(), + min_quantity = %quote.min_quantity.to_string(), + max_quantity = %quote.max_quantity.to_string(), + status = "Online", + address = %seller.multiaddr.to_string(), + "Fetched peer status" + ); + } + SellerStatus::Unreachable => { + tracing::info!( + status = "Unreachable", + address = %seller.multiaddr.to_string(), + "Fetched peer status" + ); + } + } + } + + Ok(json!({ "sellers": sellers })) +} + +#[tracing::instrument(fields(method = "export_bitcoin_wallet"), skip(context))] +pub async fn export_bitcoin_wallet(context: Arc) -> Result { + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + let wallet_export = bitcoin_wallet.wallet_export("cli").await?; + tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet"); + Ok(json!({ + "descriptor": wallet_export.to_string(), + })) +} + +#[tracing::instrument(fields(method = "monero_recovery"), skip(context))] +pub async fn monero_recovery( + monero_recovery: MoneroRecoveryArgs, + context: Arc, +) -> Result { + let MoneroRecoveryArgs { swap_id } = monero_recovery; + let swap_state: BobState = context.db.get_state(swap_id).await?.try_into()?; + + if let BobState::BtcRedeemed(state5) = swap_state { + let (spend_key, view_key) = state5.xmr_keys(); + let restore_height = state5.monero_wallet_restore_blockheight.height; + + let address = monero::Address::standard( + context.config.env_config.monero_network, + monero::PublicKey::from_private_key(&spend_key), + monero::PublicKey::from(view_key.public()), + ); + + tracing::info!(restore_height=%restore_height, address=%address, spend_key=%spend_key, view_key=%view_key, "Monero recovery information"); + + Ok(json!({ + "address": address, + "spend_key": spend_key.to_string(), + "view_key": view_key.to_string(), + "restore_height": state5.monero_wallet_restore_blockheight.height, + })) + } else { + bail!( + "Cannot print monero recovery information in state {}, only possible for BtcRedeemed", + swap_state + ) + } +} + +#[tracing::instrument(fields(method = "get_current_swap"), skip(context))] +pub async fn get_current_swap(context: Arc) -> Result { + Ok(json!({ + "swap_id": context.swap_lock.get_current_swap_id().await + })) +} + +fn qr_code(value: &impl ToString) -> Result { + let code = QrCode::new(value.to_string())?; + let qr_code = code + .render::() + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build(); + Ok(qr_code) +} + +#[allow(clippy::too_many_arguments)] +pub async fn determine_btc_to_swap( + json: bool, + bid_quote: BidQuote, + get_new_address: impl Future>, + balance: FB, + max_giveable_fn: FMG, + sync: FS, + estimate_fee: FFE, + event_emitter: Option, +) -> Result<(bitcoin::Amount, bitcoin::Amount)> +where + TB: Future>, + FB: Fn() -> TB, + TMG: Future>, + FMG: Fn() -> TMG, + TS: Future>, + FS: Fn() -> TS, + FFE: Fn(bitcoin::Amount) -> TFE, + TFE: Future>, +{ + if bid_quote.max_quantity == bitcoin::Amount::ZERO { + bail!(ZeroQuoteReceived) + } + + tracing::info!( + price = %bid_quote.price, + minimum_amount = %bid_quote.min_quantity, + maximum_amount = %bid_quote.max_quantity, + "Received quote", + ); + + sync().await?; + let mut max_giveable = max_giveable_fn().await?; + + if max_giveable == bitcoin::Amount::ZERO || max_giveable < bid_quote.min_quantity { + let deposit_address = get_new_address.await?; + let minimum_amount = bid_quote.min_quantity; + let maximum_amount = bid_quote.max_quantity; + + if !json { + eprintln!("{}", qr_code(&deposit_address)?); + } + + loop { + let min_outstanding = bid_quote.min_quantity - max_giveable; + let min_bitcoin_lock_tx_fee = estimate_fee(min_outstanding).await?; + let min_deposit_until_swap_will_start = min_outstanding + min_bitcoin_lock_tx_fee; + let max_deposit_until_maximum_amount_is_reached = + maximum_amount - max_giveable + min_bitcoin_lock_tx_fee; + + tracing::info!( + "Deposit at least {} to cover the min quantity with fee!", + min_deposit_until_swap_will_start + ); + tracing::info!( + %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", + ); + + // TODO: Use the real swap id here + event_emitter.emit_swap_progress_event( + Uuid::new_v4(), + TauriSwapProgressEvent::WaitingForBtcDeposit { + 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.clone(), + }, + ); + + max_giveable = loop { + sync().await?; + let new_max_givable = max_giveable_fn().await?; + + if new_max_givable > max_giveable { + break new_max_givable; + } + + tokio::time::sleep(Duration::from_secs(1)).await; + }; + + let new_balance = balance().await?; + tracing::info!(%new_balance, %max_giveable, "Received Bitcoin"); + + if max_giveable < bid_quote.min_quantity { + tracing::info!("Deposited amount is not enough to cover `min_quantity` when accounting for network fees"); + continue; + } + + break; + } + }; + + let balance = balance().await?; + let fees = balance - max_giveable; + let max_accepted = bid_quote.max_quantity; + let btc_swap_amount = min(max_giveable, max_accepted); + + Ok((btc_swap_amount, fees)) +} diff --git a/swap/src/api/tauri_bindings.rs b/swap/src/cli/api/tauri_bindings.rs similarity index 90% rename from swap/src/api/tauri_bindings.rs rename to swap/src/cli/api/tauri_bindings.rs index f705a3c5c..f0788fa4a 100644 --- a/swap/src/api/tauri_bindings.rs +++ b/swap/src/cli/api/tauri_bindings.rs @@ -27,11 +27,7 @@ impl TauriHandle { } pub trait TauriEmitter { - fn emit_tauri_event( - &self, - event: &str, - payload: S, - ) -> Result<()>; + fn emit_tauri_event(&self, event: &str, payload: S) -> Result<()>; fn emit_swap_progress_event(&self, swap_id: Uuid, event: TauriSwapProgressEvent) { let _ = self.emit_tauri_event( @@ -42,21 +38,13 @@ pub trait TauriEmitter { } impl TauriEmitter for TauriHandle { - fn emit_tauri_event( - &self, - event: &str, - payload: S, - ) -> Result<()> { + fn emit_tauri_event(&self, event: &str, payload: S) -> Result<()> { self.emit_tauri_event(event, payload) } } impl TauriEmitter for Option { - fn emit_tauri_event( - &self, - event: &str, - payload: S, - ) -> Result<()> { + fn emit_tauri_event(&self, event: &str, payload: S) -> Result<()> { match self { Some(tauri) => tauri.emit_tauri_event(event, payload), None => Ok(()), diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index fa3740f7f..e78a46665 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -1,10 +1,9 @@ -use crate::api::request::{ - buy_xmr, cancel_and_refund, export_bitcoin_wallet, get_balance, get_config, get_history, - list_sellers, monero_recovery, resume_swap, start_daemon, withdraw_btc, BalanceArgs, - BuyXmrArgs, CancelAndRefundArgs, ExportBitcoinWalletArgs, GetConfigArgs, GetHistoryArgs, - ListSellersArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, StartDaemonArgs, WithdrawBtcArgs, +use crate::cli::api::request::{ + BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, ExportBitcoinWalletArgs, GetConfigArgs, + GetHistoryArgs, ListSellersArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, StartDaemonArgs, + WithdrawBtcArgs, }; -use crate::api::Context; +use crate::cli::api::Context; use crate::bitcoin::{bitcoin_address, Amount}; use crate::monero; use crate::monero::monero_address; @@ -544,15 +543,14 @@ struct Seller { mod tests { use super::*; - use crate::api::api_test::*; - use crate::api::Config; + use crate::cli::api::api_test::*; + use crate::cli::api::Config; use crate::monero::monero_address::MoneroAddressNetworkMismatch; const BINARY_NAME: &str = "swap"; const ARGS_DATA_DIR: &str = "/tmp/dir/"; #[tokio::test] - // this test is very long, however it just checks that various CLI arguments sets the // internal Context and Request properly. It is unlikely to fail and splitting it in various // tests would require to run the tests sequantially which is very slow (due to the context diff --git a/swap/src/cli/list_sellers.rs b/swap/src/cli/list_sellers.rs index 381c561f9..85abe2636 100644 --- a/swap/src/cli/list_sellers.rs +++ b/swap/src/cli/list_sellers.rs @@ -73,6 +73,7 @@ pub enum Status { Unreachable, } +#[allow(unused)] #[derive(Debug)] enum OutEvent { Rendezvous(rendezvous::client::Event), diff --git a/swap/src/kraken.rs b/swap/src/kraken.rs index 29062114b..b4562dc60 100644 --- a/swap/src/kraken.rs +++ b/swap/src/kraken.rs @@ -264,6 +264,7 @@ mod wire { #[serde(transparent)] pub struct TickerUpdate(Vec); + #[allow(unused)] #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum TickerField { @@ -277,6 +278,7 @@ mod wire { ask: Vec, } + #[allow(unused)] #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum RateElement { diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 3e575fc68..ddad3eab1 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -16,7 +16,6 @@ missing_copy_implementations )] -pub mod api; pub mod asb; pub mod bitcoin; pub mod cli; diff --git a/swap/src/network.rs b/swap/src/network.rs index 527c04fc4..893881201 100644 --- a/swap/src/network.rs +++ b/swap/src/network.rs @@ -13,5 +13,5 @@ pub mod tor_transport; pub mod transfer_proof; pub mod transport; -#[cfg(any(test, feature = "test"))] +#[cfg(test)] pub mod test; diff --git a/swap/src/protocol/bob.rs b/swap/src/protocol/bob.rs index d9d90ac97..97a20aa34 100644 --- a/swap/src/protocol/bob.rs +++ b/swap/src/protocol/bob.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::Result; use uuid::Uuid; -use crate::api::tauri_bindings::TauriHandle; +use crate::cli::api::tauri_bindings::TauriHandle; use crate::protocol::Database; use crate::{bitcoin, cli, env, monero}; diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 4551414ec..0d01228d6 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -1,4 +1,4 @@ -use crate::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent}; +use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent}; use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund}; use crate::cli::EventLoopHandle; use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected}; diff --git a/swap/src/rpc.rs b/swap/src/rpc.rs index 75ec9ae03..a6503c12c 100644 --- a/swap/src/rpc.rs +++ b/swap/src/rpc.rs @@ -1,4 +1,4 @@ -use crate::api::Context; +use crate::cli::api::Context; use std::net::SocketAddr; use thiserror::Error; use tower_http::cors::CorsLayer; diff --git a/swap/src/rpc/methods.rs b/swap/src/rpc/methods.rs index ca707290b..a239733e3 100644 --- a/swap/src/rpc/methods.rs +++ b/swap/src/rpc/methods.rs @@ -1,10 +1,9 @@ -use crate::api::request::{ - get_current_swap, get_history, get_raw_states, - suspend_current_swap, - BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, GetSwapInfoArgs, ListSellersArgs, - MoneroRecoveryArgs, Request, ResumeSwapArgs, WithdrawBtcArgs, +use crate::cli::api::request::{ + get_current_swap, get_history, get_raw_states, suspend_current_swap, BalanceArgs, BuyXmrArgs, + CancelAndRefundArgs, GetSwapInfoArgs, ListSellersArgs, MoneroRecoveryArgs, Request, + ResumeSwapArgs, WithdrawBtcArgs, }; -use crate::api::Context; +use crate::cli::api::Context; use crate::bitcoin::bitcoin_address; use crate::monero::monero_address; use crate::{bitcoin, monero};