feat(gui): Add button to display monero redeem recovery keys (#64)

This commit is contained in:
binarybaron 2024-09-06 14:23:36 +02:00 committed by GitHub
parent e4bddd2287
commit 177e3e9949
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 90 additions and 49 deletions

View File

@ -11,22 +11,22 @@ import { ReactNode, useState } from "react";
import { useIsContextAvailable } from "store/hooks"; import { useIsContextAvailable } from "store/hooks";
interface PromiseInvokeButtonProps<T> { interface PromiseInvokeButtonProps<T> {
onSuccess: (data: T) => void | null; onSuccess?: (data: T) => void | null;
onClick: () => Promise<T>; onInvoke: () => Promise<T>;
onPendingChange: (isPending: boolean) => void | null; onPendingChange?: (isPending: boolean) => void | null;
isLoadingOverride: boolean; isLoadingOverride?: boolean;
isIconButton: boolean; isIconButton?: boolean;
loadIcon: ReactNode; loadIcon?: ReactNode;
disabled: boolean; disabled?: boolean;
displayErrorSnackbar: boolean; displayErrorSnackbar?: boolean;
tooltipTitle: string | null; tooltipTitle?: string | null;
requiresContext: boolean; requiresContext?: boolean;
} }
export default function PromiseInvokeButton<T>({ export default function PromiseInvokeButton<T>({
disabled = false, disabled = false,
onSuccess = null, onSuccess = null,
onClick, onInvoke,
endIcon, endIcon,
loadIcon = null, loadIcon = null,
isLoadingOverride = false, isLoadingOverride = false,
@ -36,7 +36,7 @@ export default function PromiseInvokeButton<T>({
requiresContext = true, requiresContext = true,
tooltipTitle = null, tooltipTitle = null,
...rest ...rest
}: ButtonProps & PromiseInvokeButtonProps<T>) { }: PromiseInvokeButtonProps<T> & ButtonProps) {
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
const isContextAvailable = useIsContextAvailable(); const isContextAvailable = useIsContextAvailable();
@ -52,7 +52,7 @@ export default function PromiseInvokeButton<T>({
try { try {
onPendingChange?.(true); onPendingChange?.(true);
setIsPending(true); setIsPending(true);
const result = await onClick(); const result = await onInvoke();
onSuccess?.(result); onSuccess?.(result);
} catch (e: unknown) { } catch (e: unknown) {
if (displayErrorSnackbar) { if (displayErrorSnackbar) {

View File

@ -33,7 +33,7 @@ export default function SwapSuspendAlert({
<PromiseInvokeButton <PromiseInvokeButton
color="primary" color="primary"
onSuccess={onClose} onSuccess={onClose}
onClick={suspendCurrentSwap} onInvoke={suspendCurrentSwap}
> >
Force stop Force stop
</PromiseInvokeButton> </PromiseInvokeButton>

View File

@ -124,7 +124,7 @@ export default function ListSellersDialog({
disabled={!(rendezvousAddress && !getMultiAddressError())} disabled={!(rendezvousAddress && !getMultiAddressError())}
color="primary" color="primary"
onSuccess={handleSuccess} onSuccess={handleSuccess}
onClick={() => { onInvoke={() => {
throw new Error("Not implemented"); throw new Error("Not implemented");
}} }}
> >

View File

@ -71,7 +71,7 @@ export default function InitPage() {
size="large" size="large"
className={classes.initButton} className={classes.initButton}
endIcon={<PlayArrowIcon />} endIcon={<PlayArrowIcon />}
onClick={init} onInvoke={init}
displayErrorSnackbar displayErrorSnackbar
> >
Start swap Start swap

View File

@ -51,7 +51,7 @@ export default function WithdrawDialog({
variant="contained" variant="contained"
color="primary" color="primary"
disabled={!withdrawAddressValid} disabled={!withdrawAddressValid}
onClick={() => withdrawBtc(withdrawAddress)} onInvoke={() => withdrawBtc(withdrawAddress)}
onPendingChange={(pending) => { onPendingChange={(pending) => {
console.log("pending", pending); console.log("pending", pending);
setPending(pending); setPending(pending);

View File

@ -36,7 +36,7 @@ export default function RpcControlBox() {
variant="contained" variant="contained"
endIcon={<PlayArrowIcon />} endIcon={<PlayArrowIcon />}
disabled={isRunning} disabled={isRunning}
onClick={() => { onInvoke={() => {
throw new Error("Not implemented"); throw new Error("Not implemented");
}} }}
> >
@ -46,7 +46,7 @@ export default function RpcControlBox() {
variant="contained" variant="contained"
endIcon={<StopIcon />} endIcon={<StopIcon />}
disabled={!isRunning} disabled={!isRunning}
onClick={() => { onInvoke={() => {
throw new Error("Not implemented"); throw new Error("Not implemented");
}} }}
> >
@ -57,7 +57,7 @@ export default function RpcControlBox() {
isIconButton isIconButton
size="small" size="small"
tooltipTitle="Open the data directory of the Swap Daemon in your file explorer" tooltipTitle="Open the data directory of the Swap Daemon in your file explorer"
onClick={() => { onInvoke={() => {
throw new Error("Not implemented"); throw new Error("Not implemented");
}} }}
/> />

View File

@ -46,7 +46,7 @@ export default function TorInfoBox() {
variant="contained" variant="contained"
disabled={isTorRunning} disabled={isTorRunning}
endIcon={<PlayArrowIcon />} endIcon={<PlayArrowIcon />}
onClick={() => { onInvoke={() => {
throw new Error("Not implemented"); throw new Error("Not implemented");
}} }}
> >
@ -56,7 +56,7 @@ export default function TorInfoBox() {
variant="contained" variant="contained"
disabled={!isTorRunning} disabled={!isTorRunning}
endIcon={<StopIcon />} endIcon={<StopIcon />}
onClick={() => { onInvoke={() => {
throw new Error("Not implemented"); throw new Error("Not implemented");
}} }}
> >

View File

@ -24,7 +24,7 @@ export function SwapResumeButton({
color="primary" color="primary"
disabled={swap.completed} disabled={swap.completed}
endIcon={<PlayArrowIcon />} endIcon={<PlayArrowIcon />}
onClick={() => resumeSwap(swap.swap_id)} onInvoke={() => resumeSwap(swap.swap_id)}
{...props} {...props}
> >
Resume Resume
@ -48,7 +48,7 @@ export function SwapCancelRefundButton({
<PromiseInvokeButton <PromiseInvokeButton
displayErrorSnackbar={false} displayErrorSnackbar={false}
{...props} {...props}
onClick={async () => { onInvoke={async () => {
// TODO: Implement this using the Tauri RPC // TODO: Implement this using the Tauri RPC
throw new Error("Not implemented"); throw new Error("Not implemented");
}} }}

View File

@ -23,7 +23,7 @@ export default function SwapLogFileOpenButton({
onSuccess={(data) => { onSuccess={(data) => {
setLogs(data as CliLog[]); setLogs(data as CliLog[]);
}} }}
onClick={async () => { onInvoke={async () => {
throw new Error("Not implemented"); throw new Error("Not implemented");
}} }}
{...props} {...props}

View File

@ -8,16 +8,22 @@ import {
Link, Link,
} from "@material-ui/core"; } from "@material-ui/core";
import { ButtonProps } from "@material-ui/core/Button/Button"; import { ButtonProps } from "@material-ui/core/Button/Button";
import { GetSwapInfoArgs } from "models/tauriModel"; import { BobStateName, GetSwapInfoResponseExt } from "models/tauriModelExt";
import { rpcResetMoneroRecoveryKeys } from "store/features/rpcSlice"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { getMoneroRecoveryKeys } from "renderer/rpc";
import { store } from "renderer/store/storeRenderer";
import {
rpcResetMoneroRecoveryKeys,
rpcSetMoneroRecoveryKeys,
} from "store/features/rpcSlice";
import { useAppDispatch, useAppSelector } from "store/hooks"; import { useAppDispatch, useAppSelector } from "store/hooks";
import DialogHeader from "../../../modal/DialogHeader"; import DialogHeader from "../../../modal/DialogHeader";
import ScrollablePaperTextBox from "../../../other/ScrollablePaperTextBox"; import ScrollablePaperTextBox from "../../../other/ScrollablePaperTextBox";
function MoneroRecoveryKeysDialog() { function MoneroRecoveryKeysDialog({
// TODO: Reimplement this using the new Tauri API swap_id,
return null; ...rest
}: GetSwapInfoResponseExt) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const keys = useAppSelector((s) => s.rpc.state.moneroRecovery); const keys = useAppSelector((s) => s.rpc.state.moneroRecovery);
@ -25,14 +31,14 @@ function MoneroRecoveryKeysDialog() {
dispatch(rpcResetMoneroRecoveryKeys()); dispatch(rpcResetMoneroRecoveryKeys());
} }
if (keys === null || keys.swapId !== swap.swap_id) { if (keys === null || keys.swapId !== swap_id) {
return <></>; return null;
} }
return ( return (
<Dialog open onClose={onClose} maxWidth="sm" fullWidth> <Dialog open onClose={onClose} maxWidth="sm" fullWidth>
<DialogHeader <DialogHeader
title={`Recovery Keys for swap ${swap.swap_id.substring(0, 5)}...`} title={`Recovery Keys for swap ${swap_id.substring(0, 5)}...`}
/> />
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
@ -40,7 +46,7 @@ function MoneroRecoveryKeysDialog() {
the multi-signature wallet. the multi-signature wallet.
<ul> <ul>
<li> <li>
This is useful if the swap daemon fails to redeem the funds itself This is useful if the application fails to redeem the funds itself
</li> </li>
<li> <li>
If you have come this far, there is no risk of losing funds. You If you have come this far, there is no risk of losing funds. You
@ -79,6 +85,7 @@ function MoneroRecoveryKeysDialog() {
title={title} title={title}
copyValue={value} copyValue={value}
rows={[value]} rows={[value]}
key={title}
/> />
))} ))}
</Box> </Box>
@ -95,11 +102,8 @@ function MoneroRecoveryKeysDialog() {
export function SwapMoneroRecoveryButton({ export function SwapMoneroRecoveryButton({
swap, swap,
...props ...props
}: { swap: GetSwapInfoArgs } & ButtonProps) { }: { swap: GetSwapInfoResponseExt } & ButtonProps) {
return <> </>; const isRecoverable = swap.state_name === BobStateName.BtcRedeemed;
/* TODO: Reimplement this using the new Tauri API
const isRecoverable = isSwapMoneroRecoverable(swap.state_name);
if (!isRecoverable) { if (!isRecoverable) {
return <></>; return <></>;
@ -108,15 +112,15 @@ export function SwapMoneroRecoveryButton({
return ( return (
<> <>
<PromiseInvokeButton <PromiseInvokeButton
onClick={async () => { onInvoke={() => getMoneroRecoveryKeys(swap.swap_id)}
throw new Error("Not implemented"); onSuccess={(keys) =>
}} store.dispatch(rpcSetMoneroRecoveryKeys([swap.swap_id, keys]))
}
{...props} {...props}
> >
Display Monero Recovery Keys Display Monero Recovery Keys
</PromiseInvokeButton> </PromiseInvokeButton>
<MoneroRecoveryKeysDialog swap={swap} /> <MoneroRecoveryKeysDialog {...swap} />
</> </>
); );
*/
} }

View File

@ -7,7 +7,7 @@ export default function WalletRefreshButton() {
<PromiseInvokeButton <PromiseInvokeButton
endIcon={<RefreshIcon />} endIcon={<RefreshIcon />}
isIconButton isIconButton
onClick={() => checkBitcoinBalance()} onInvoke={() => checkBitcoinBalance()}
size="small" size="small"
/> />
); );

View File

@ -6,6 +6,7 @@ import {
BuyXmrArgs, BuyXmrArgs,
BuyXmrResponse, BuyXmrResponse,
GetSwapInfoResponse, GetSwapInfoResponse,
MoneroRecoveryArgs,
ResumeSwapArgs, ResumeSwapArgs,
ResumeSwapResponse, ResumeSwapResponse,
SuspendCurrentSwapResponse, SuspendCurrentSwapResponse,
@ -23,8 +24,16 @@ import { swapTauriEventReceived } from "store/features/swapSlice";
import { store } from "./store/storeRenderer"; import { store } from "./store/storeRenderer";
import { Provider } from "models/apiModel"; import { Provider } from "models/apiModel";
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils"; import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
import { MoneroRecoveryResponse } from "models/rpcModel";
export async function initEventListeners() { export async function initEventListeners() {
// This operation is in-expensive
// We do this in case we miss the context init progress event because the frontend took too long to load
// TOOD: Replace this with a more reliable mechanism (such as an event replay mechanism)
if (await checkContextAvailability()) {
store.dispatch(contextStatusEventReceived({ type: "Available" }));
}
listen<TauriSwapProgressEventWrapper>("swap-progress-update", (event) => { listen<TauriSwapProgressEventWrapper>("swap-progress-update", (event) => {
console.log("Received swap progress event", event.payload); console.log("Received swap progress event", event.payload);
store.dispatch(swapTauriEventReceived(event.payload)); store.dispatch(swapTauriEventReceived(event.payload));
@ -99,3 +108,19 @@ export async function resumeSwap(swapId: string) {
export async function suspendCurrentSwap() { export async function suspendCurrentSwap() {
await invokeNoArgs<SuspendCurrentSwapResponse>("suspend_current_swap"); await invokeNoArgs<SuspendCurrentSwapResponse>("suspend_current_swap");
} }
export async function getMoneroRecoveryKeys(
swapId: string,
): Promise<MoneroRecoveryResponse> {
return await invoke<MoneroRecoveryArgs, MoneroRecoveryResponse>(
"monero_recovery",
{
swap_id: swapId,
},
);
}
export async function checkContextAvailability(): Promise<boolean> {
const available = await invokeNoArgs<boolean>("is_context_available");
return available;
}

View File

@ -56,6 +56,7 @@ export function useAllProviders() {
export function useSwapInfosSortedByDate() { export function useSwapInfosSortedByDate() {
const swapInfos = useAppSelector((state) => state.rpc.state.swapInfos); const swapInfos = useAppSelector((state) => state.rpc.state.swapInfos);
return sortBy( return sortBy(
Object.values(swapInfos), Object.values(swapInfos),
(swap) => -parseDateString(swap.start_date), (swap) => -parseDateString(swap.start_date),

View File

@ -3,8 +3,8 @@ use std::sync::Arc;
use swap::cli::{ use swap::cli::{
api::{ api::{
request::{ request::{
BalanceArgs, BuyXmrArgs, GetHistoryArgs, GetSwapInfosAllArgs, ResumeSwapArgs, BalanceArgs, BuyXmrArgs, GetHistoryArgs, GetSwapInfosAllArgs, MoneroRecoveryArgs,
SuspendCurrentSwapArgs, WithdrawBtcArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
}, },
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle}, tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle},
Context, ContextBuilder, Context, ContextBuilder,
@ -167,7 +167,9 @@ pub fn run() {
buy_xmr, buy_xmr,
resume_swap, resume_swap,
get_history, get_history,
suspend_current_swap monero_recovery,
suspend_current_swap,
is_context_available,
]) ])
.setup(setup) .setup(setup)
.build(tauri::generate_context!()) .build(tauri::generate_context!())
@ -203,6 +205,15 @@ tauri_command!(get_balance, BalanceArgs);
tauri_command!(buy_xmr, BuyXmrArgs); tauri_command!(buy_xmr, BuyXmrArgs);
tauri_command!(resume_swap, ResumeSwapArgs); tauri_command!(resume_swap, ResumeSwapArgs);
tauri_command!(withdraw_btc, WithdrawBtcArgs); tauri_command!(withdraw_btc, WithdrawBtcArgs);
tauri_command!(monero_recovery, MoneroRecoveryArgs);
// These commands require no arguments
tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args); tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args);
tauri_command!(get_swap_infos_all, GetSwapInfosAllArgs, no_args); tauri_command!(get_swap_infos_all, GetSwapInfosAllArgs, no_args);
tauri_command!(get_history, GetHistoryArgs, no_args); tauri_command!(get_history, GetHistoryArgs, no_args);
/// Here we define Tauri commands whose implementation is not delegated to the Request trait
#[tauri::command]
async fn is_context_available(context: tauri::State<'_, RwLock<State>>) -> Result<bool, String> {
Ok(context.read().await.try_get_context().is_ok())
}