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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import {
BuyXmrArgs,
BuyXmrResponse,
GetSwapInfoResponse,
MoneroRecoveryArgs,
ResumeSwapArgs,
ResumeSwapResponse,
SuspendCurrentSwapResponse,
@ -23,8 +24,16 @@ import { swapTauriEventReceived } from "store/features/swapSlice";
import { store } from "./store/storeRenderer";
import { Provider } from "models/apiModel";
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
import { MoneroRecoveryResponse } from "models/rpcModel";
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) => {
console.log("Received swap progress event", event.payload);
store.dispatch(swapTauriEventReceived(event.payload));
@ -99,3 +108,19 @@ export async function resumeSwap(swapId: string) {
export async function suspendCurrentSwap() {
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() {
const swapInfos = useAppSelector((state) => state.rpc.state.swapInfos);
return sortBy(
Object.values(swapInfos),
(swap) => -parseDateString(swap.start_date),

View File

@ -3,8 +3,8 @@ use std::sync::Arc;
use swap::cli::{
api::{
request::{
BalanceArgs, BuyXmrArgs, GetHistoryArgs, GetSwapInfosAllArgs, ResumeSwapArgs,
SuspendCurrentSwapArgs, WithdrawBtcArgs,
BalanceArgs, BuyXmrArgs, GetHistoryArgs, GetSwapInfosAllArgs, MoneroRecoveryArgs,
ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
},
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle},
Context, ContextBuilder,
@ -167,7 +167,9 @@ pub fn run() {
buy_xmr,
resume_swap,
get_history,
suspend_current_swap
monero_recovery,
suspend_current_swap,
is_context_available,
])
.setup(setup)
.build(tauri::generate_context!())
@ -203,6 +205,15 @@ tauri_command!(get_balance, BalanceArgs);
tauri_command!(buy_xmr, BuyXmrArgs);
tauri_command!(resume_swap, ResumeSwapArgs);
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!(get_swap_infos_all, GetSwapInfosAllArgs, 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())
}