mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-05-02 14:56:10 -04:00
feat(tauri, gui): Send event on changes to details, timelocks and tx_lock confirmations (#100)
- Send event when new swap state is inserated into database. The event only has the `swap_id` attached. The frontend then sends a request to the `get_swap_info` command to retrieve the updated version - Send event when the Bitcoin lock transaction gets a new confirmation - A new `watcher` daemon runs contineously and sends an event when the timelock updated. The event has the the `swap_id` and the timelock attached - Display logs on `ProcessExitedPage` (if swap was stopped prematurely) - Rename `CliLogEmittedEvent` to `TauriLogEvent` - Apply env_filter to tracing terminal writer to silence logging from other crates - Add `.env.*` files in `src-gui` to `.gitingore` Closes #93 and #12
This commit is contained in:
parent
e6dc7ddcef
commit
8f33fe5b41
28 changed files with 429 additions and 208 deletions
3
src-gui/.gitignore
vendored
3
src-gui/.gitignore
vendored
|
@ -26,3 +26,6 @@ dist-ssr
|
|||
|
||||
# Autogenerated bindings
|
||||
src/models/tauriModel.ts
|
||||
|
||||
# Env files
|
||||
.env.*
|
||||
|
|
|
@ -29,6 +29,25 @@ function isCliLog(log: unknown): log is CliLog {
|
|||
);
|
||||
}
|
||||
|
||||
export function isCliLogRelatedToSwap(
|
||||
log: CliLog | string,
|
||||
swapId: string,
|
||||
): boolean {
|
||||
// If we only have a string, simply check if the string contains the swap id
|
||||
// This provides reasonable backwards compatability
|
||||
if (typeof log === "string") {
|
||||
return log.includes(swapId);
|
||||
}
|
||||
|
||||
// If we have a parsed object as the log, check if
|
||||
// - the log has the swap id as an attribute
|
||||
// - there exists a span which has the swap id as an attribute
|
||||
return (
|
||||
log.fields["swap_id"] === swapId ||
|
||||
(log.spans?.some((span) => span["swap_id"] === swapId) ?? false)
|
||||
);
|
||||
}
|
||||
|
||||
export function parseCliLogString(log: string): CliLog | string {
|
||||
try {
|
||||
const parsed = JSON.parse(log);
|
||||
|
|
|
@ -61,7 +61,11 @@ function getActiveStep(state: SwapState | null): PathStep {
|
|||
// Step 1: Waiting for Bitcoin lock confirmation
|
||||
// Bitcoin has been locked, waiting for the counterparty to lock their XMR
|
||||
case "BtcLockTxInMempool":
|
||||
return [PathType.HAPPY_PATH, 1, isReleased];
|
||||
// We only display the first step as completed if the Bitcoin lock has been confirmed
|
||||
if(latestState.content.btc_lock_confirmations > 0) {
|
||||
return [PathType.HAPPY_PATH, 1, isReleased];
|
||||
}
|
||||
return [PathType.HAPPY_PATH, 0, isReleased];
|
||||
|
||||
// Still Step 1: Both Bitcoin and XMR have been locked, waiting for Monero lock to be confirmed
|
||||
case "XmrLockTxInMempool":
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import { Box, DialogContentText } from "@material-ui/core";
|
||||
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
|
||||
import {
|
||||
useActiveSwapInfo,
|
||||
useActiveSwapLogs,
|
||||
useAppSelector,
|
||||
} from "store/hooks";
|
||||
import JsonTreeView from "../../../other/JSONViewTree";
|
||||
import CliLogsBox from "../../../other/RenderedCliLog";
|
||||
|
||||
export default function DebugPage() {
|
||||
const torStdOut = useAppSelector((s) => s.tor.stdOut);
|
||||
const logs = useAppSelector((s) => s.swap.logs);
|
||||
const logs = useActiveSwapLogs();
|
||||
const guiState = useAppSelector((s) => s);
|
||||
const cliState = useActiveSwapInfo();
|
||||
|
||||
|
@ -19,7 +23,10 @@ export default function DebugPage() {
|
|||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
|
||||
<CliLogsBox
|
||||
logs={logs}
|
||||
label="Logs relevant to the swap (only current session)"
|
||||
/>
|
||||
<JsonTreeView
|
||||
data={guiState}
|
||||
label="Internal GUI State (inferred from Logs)"
|
||||
|
|
|
@ -19,20 +19,10 @@ import InitPage from "./init/InitPage";
|
|||
import WaitingForBitcoinDepositPage from "./init/WaitingForBitcoinDepositPage";
|
||||
|
||||
export default function SwapStatePage({ state }: { state: SwapState | null }) {
|
||||
// TODO: Reimplement this using tauri events
|
||||
/*
|
||||
const isSyncingMoneroWallet = useAppSelector(
|
||||
(state) => state.rpc.state.moneroWallet.isSyncing,
|
||||
);
|
||||
|
||||
if (isSyncingMoneroWallet) {
|
||||
return <SyncingMoneroWalletPage />;
|
||||
}
|
||||
*/
|
||||
|
||||
if (state === null) {
|
||||
return <InitPage />;
|
||||
}
|
||||
|
||||
switch (state.curr.type) {
|
||||
case "Initiated":
|
||||
return <InitiatedPage />;
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
import { Box, DialogContentText } from "@material-ui/core";
|
||||
import { SwapSpawnType } from "models/cliModel";
|
||||
import { SwapStateProcessExited } from "models/storeModel";
|
||||
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
|
||||
import CliLogsBox from "../../../../other/RenderedCliLog";
|
||||
|
||||
export default function ProcessExitedAndNotDonePage({
|
||||
state,
|
||||
}: {
|
||||
state: SwapStateProcessExited;
|
||||
}) {
|
||||
const swap = useActiveSwapInfo();
|
||||
const logs = useAppSelector((s) => s.swap.logs);
|
||||
const spawnType = useAppSelector((s) => s.swap.spawnType);
|
||||
|
||||
function getText() {
|
||||
const isCancelRefund = spawnType === SwapSpawnType.CANCEL_REFUND;
|
||||
const hasRpcError = state.rpcError != null;
|
||||
const hasSwap = swap != null;
|
||||
|
||||
const messages = [];
|
||||
|
||||
messages.push(
|
||||
isCancelRefund
|
||||
? "The manual cancel and refund was unsuccessful."
|
||||
: "The swap exited unexpectedly without completing.",
|
||||
);
|
||||
|
||||
if (!hasSwap && !isCancelRefund) {
|
||||
messages.push("No funds were locked.");
|
||||
}
|
||||
|
||||
messages.push(
|
||||
hasRpcError
|
||||
? "Check the error and the logs below for more information."
|
||||
: "Check the logs below for more information.",
|
||||
);
|
||||
|
||||
if (hasSwap) {
|
||||
messages.push(`The swap is in the "${swap.state_name}" state.`);
|
||||
if (!isCancelRefund) {
|
||||
messages.push(
|
||||
"Try resuming the swap or attempt to initiate a manual cancel and refund.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return messages.join(" ");
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<DialogContentText>{getText()}</DialogContentText>
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "0.5rem",
|
||||
}}
|
||||
>
|
||||
{state.rpcError && (
|
||||
<CliLogsBox
|
||||
logs={[state.rpcError]}
|
||||
label="Error returned by the Swap Daemon"
|
||||
/>
|
||||
)}
|
||||
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
import { Box, DialogContentText } from "@material-ui/core";
|
||||
import { TauriSwapProgressEvent } from "models/tauriModel";
|
||||
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
||||
import { useActiveSwapInfo, useActiveSwapLogs } from "store/hooks";
|
||||
import SwapStatePage from "../SwapStatePage";
|
||||
|
||||
export default function ProcessExitedPage({
|
||||
|
@ -8,8 +11,11 @@ export default function ProcessExitedPage({
|
|||
prevState: TauriSwapProgressEvent | null;
|
||||
swapId: string;
|
||||
}) {
|
||||
const swap = useActiveSwapInfo();
|
||||
const logs = useActiveSwapLogs();
|
||||
|
||||
// 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)
|
||||
// We only show the last state if its a final state (XmrRedeemInMempool, BtcRefunded, BtcPunished, CooperativeRedeemRejected)
|
||||
if (
|
||||
prevState != null &&
|
||||
(prevState.type === "XmrRedeemInMempool" ||
|
||||
|
@ -28,15 +34,17 @@ export default function ProcessExitedPage({
|
|||
);
|
||||
}
|
||||
|
||||
// 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
|
||||
</>
|
||||
<Box>
|
||||
<DialogContentText>
|
||||
The swap was stopped but it has not been completed yet. Check the logs
|
||||
below for more information. The current GUI state is{" "}
|
||||
{prevState?.type ?? "unknown"}. The current database state is{" "}
|
||||
{swap?.state_name ?? "unknown"}.
|
||||
</DialogContentText>
|
||||
<Box>
|
||||
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
// 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 <ProcessExitedAndNotDonePage state={state} />;
|
||||
}
|
||||
|
|
|
@ -81,7 +81,9 @@ export default function CliLogsBox({
|
|||
setSearchQuery={setSearchQuery}
|
||||
rows={memoizedLogs.map((log) =>
|
||||
typeof log === "string" ? (
|
||||
<Typography component="pre">{log}</Typography>
|
||||
<Typography key={log} component="pre">
|
||||
{log}
|
||||
</Typography>
|
||||
) : (
|
||||
<RenderedCliLog log={log} key={JSON.stringify(log)} />
|
||||
),
|
||||
|
|
|
@ -21,7 +21,7 @@ export default function ScrollablePaperTextBox({
|
|||
copyValue: string;
|
||||
searchQuery: string | null;
|
||||
setSearchQuery?: ((query: string) => void) | null;
|
||||
minHeight: string;
|
||||
minHeight?: string;
|
||||
}) {
|
||||
const virtuaEl = useRef<VListHandle | null>(null);
|
||||
|
||||
|
|
|
@ -15,15 +15,13 @@ export function AmountWithUnit({
|
|||
fixedPrecision: number;
|
||||
dollarRate?: Amount;
|
||||
}) {
|
||||
const title =
|
||||
dollarRate != null && amount != null
|
||||
? `≈ $${(dollarRate * amount).toFixed(2)}`
|
||||
: "";
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
arrow
|
||||
title={
|
||||
dollarRate != null && amount != null
|
||||
? `≈ $${(dollarRate * amount).toFixed(2)}`
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<Tooltip arrow title={title}>
|
||||
<span>
|
||||
{amount != null
|
||||
? Number.parseFloat(amount.toFixed(fixedPrecision))
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
BalanceResponse,
|
||||
BuyXmrArgs,
|
||||
BuyXmrResponse,
|
||||
CliLogEmittedEvent,
|
||||
TauriLogEvent,
|
||||
GetLogsArgs,
|
||||
GetLogsResponse,
|
||||
GetSwapInfoResponse,
|
||||
|
@ -18,14 +18,18 @@ import {
|
|||
TauriSwapProgressEventWrapper,
|
||||
WithdrawBtcArgs,
|
||||
WithdrawBtcResponse,
|
||||
TauriDatabaseStateEvent,
|
||||
TauriTimelockChangeEvent,
|
||||
GetSwapInfoArgs,
|
||||
} from "models/tauriModel";
|
||||
import {
|
||||
contextStatusEventReceived,
|
||||
receivedCliLog,
|
||||
rpcSetBalance,
|
||||
rpcSetSwapInfo,
|
||||
timelockChangeEventReceived,
|
||||
} from "store/features/rpcSlice";
|
||||
import { swapTauriEventReceived } from "store/features/swapSlice";
|
||||
import { swapProgressEventReceived } from "store/features/swapSlice";
|
||||
import { store } from "./store/storeRenderer";
|
||||
import { Provider } from "models/apiModel";
|
||||
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
|
||||
|
@ -49,7 +53,7 @@ export async function initEventListeners() {
|
|||
|
||||
listen<TauriSwapProgressEventWrapper>("swap-progress-update", (event) => {
|
||||
console.log("Received swap progress event", event.payload);
|
||||
store.dispatch(swapTauriEventReceived(event.payload));
|
||||
store.dispatch(swapProgressEventReceived(event.payload));
|
||||
});
|
||||
|
||||
listen<TauriContextStatusEvent>("context-init-progress-update", (event) => {
|
||||
|
@ -57,10 +61,25 @@ export async function initEventListeners() {
|
|||
store.dispatch(contextStatusEventReceived(event.payload));
|
||||
});
|
||||
|
||||
listen<CliLogEmittedEvent>("cli-log-emitted", (event) => {
|
||||
listen<TauriLogEvent>("cli-log-emitted", (event) => {
|
||||
console.log("Received cli log event", event.payload);
|
||||
store.dispatch(receivedCliLog(event.payload));
|
||||
});
|
||||
|
||||
listen<TauriDatabaseStateEvent>("swap-database-state-update", (event) => {
|
||||
console.log("Received swap database state update event", event.payload);
|
||||
getSwapInfo(event.payload.swap_id);
|
||||
|
||||
// This is ugly but it's the best we can do for now
|
||||
// Sometimes we are too quick to fetch the swap info and the new state is not yet reflected
|
||||
// in the database. So we wait a bit before fetching the new state
|
||||
setTimeout(() => getSwapInfo(event.payload.swap_id), 3000);
|
||||
});
|
||||
|
||||
listen<TauriTimelockChangeEvent>('timelock-change', (event) => {
|
||||
console.log('Received timelock change event', event.payload);
|
||||
store.dispatch(timelockChangeEventReceived(event.payload));
|
||||
})
|
||||
}
|
||||
|
||||
async function invoke<ARGS, RESPONSE>(
|
||||
|
@ -93,6 +112,17 @@ export async function getAllSwapInfos() {
|
|||
});
|
||||
}
|
||||
|
||||
export async function getSwapInfo(swapId: string) {
|
||||
const response = await invoke<GetSwapInfoArgs, GetSwapInfoResponse>(
|
||||
"get_swap_info",
|
||||
{
|
||||
swap_id: swapId,
|
||||
},
|
||||
);
|
||||
|
||||
store.dispatch(rpcSetSwapInfo(response));
|
||||
}
|
||||
|
||||
export async function withdrawBtc(address: string): Promise<string> {
|
||||
const response = await invoke<WithdrawBtcArgs, WithdrawBtcResponse>(
|
||||
"withdraw_btc",
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
|
||||
import {
|
||||
CliLogEmittedEvent,
|
||||
TauriLogEvent,
|
||||
GetSwapInfoResponse,
|
||||
TauriContextStatusEvent,
|
||||
TauriDatabaseStateEvent,
|
||||
TauriTimelockChangeEvent,
|
||||
} from "models/tauriModel";
|
||||
import { MoneroRecoveryResponse } from "../../models/rpcModel";
|
||||
import { GetSwapInfoResponseExt } from "models/tauriModelExt";
|
||||
import { getLogsAndStringsFromRawFileString } from "utils/parseUtils";
|
||||
import { CliLog } from "models/cliModel";
|
||||
import logger from "utils/logger";
|
||||
|
||||
interface State {
|
||||
balance: number | null;
|
||||
|
@ -52,7 +55,7 @@ export const rpcSlice = createSlice({
|
|||
name: "rpc",
|
||||
initialState,
|
||||
reducers: {
|
||||
receivedCliLog(slice, action: PayloadAction<CliLogEmittedEvent>) {
|
||||
receivedCliLog(slice, action: PayloadAction<TauriLogEvent>) {
|
||||
const buffer = action.payload.buffer;
|
||||
const logs = getLogsAndStringsFromRawFileString(buffer);
|
||||
slice.logs = slice.logs.concat(logs);
|
||||
|
@ -63,6 +66,16 @@ export const rpcSlice = createSlice({
|
|||
) {
|
||||
slice.status = action.payload;
|
||||
},
|
||||
timelockChangeEventReceived(
|
||||
slice,
|
||||
action: PayloadAction<TauriTimelockChangeEvent>
|
||||
) {
|
||||
if (slice.state.swapInfos[action.payload.swap_id]) {
|
||||
slice.state.swapInfos[action.payload.swap_id].timelock = action.payload.timelock;
|
||||
} else {
|
||||
logger.warn(`Received timelock change event for unknown swap ${action.payload.swap_id}`);
|
||||
}
|
||||
},
|
||||
rpcSetBalance(slice, action: PayloadAction<number>) {
|
||||
slice.state.balance = action.payload;
|
||||
},
|
||||
|
@ -110,6 +123,7 @@ export const {
|
|||
rpcSetSwapInfo,
|
||||
rpcSetMoneroRecoveryKeys,
|
||||
rpcResetMoneroRecoveryKeys,
|
||||
timelockChangeEventReceived
|
||||
} = rpcSlice.actions;
|
||||
|
||||
export default rpcSlice.reducer;
|
||||
|
|
|
@ -14,7 +14,7 @@ export const swapSlice = createSlice({
|
|||
name: "swap",
|
||||
initialState,
|
||||
reducers: {
|
||||
swapTauriEventReceived(
|
||||
swapProgressEventReceived(
|
||||
swap,
|
||||
action: PayloadAction<TauriSwapProgressEventWrapper>,
|
||||
) {
|
||||
|
@ -42,6 +42,6 @@ export const swapSlice = createSlice({
|
|||
},
|
||||
});
|
||||
|
||||
export const { swapReset, swapTauriEventReceived } = swapSlice.actions;
|
||||
export const { swapReset, swapProgressEventReceived } = swapSlice.actions;
|
||||
|
||||
export default swapSlice.reducer;
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { sortBy } from "lodash";
|
||||
import { GetSwapInfoResponseExt } from "models/tauriModelExt";
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
||||
import { parseDateString } from "utils/parseUtils";
|
||||
import { useMemo } from "react";
|
||||
import { isCliLogRelatedToSwap } from "models/cliModel";
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
@ -26,7 +29,9 @@ export function useIsContextAvailable() {
|
|||
return useAppSelector((state) => state.rpc.status?.type === "Available");
|
||||
}
|
||||
|
||||
export function useSwapInfo(swapId: string | null) {
|
||||
export function useSwapInfo(
|
||||
swapId: string | null,
|
||||
): GetSwapInfoResponseExt | null {
|
||||
return useAppSelector((state) =>
|
||||
swapId ? state.rpc.state.swapInfos[swapId] ?? null : null,
|
||||
);
|
||||
|
@ -36,11 +41,21 @@ export function useActiveSwapId() {
|
|||
return useAppSelector((s) => s.swap.state?.swapId ?? null);
|
||||
}
|
||||
|
||||
export function useActiveSwapInfo() {
|
||||
export function useActiveSwapInfo(): GetSwapInfoResponseExt | null {
|
||||
const swapId = useActiveSwapId();
|
||||
return useSwapInfo(swapId);
|
||||
}
|
||||
|
||||
export function useActiveSwapLogs() {
|
||||
const swapId = useActiveSwapId();
|
||||
const logs = useAppSelector((s) => s.rpc.logs);
|
||||
|
||||
return useMemo(
|
||||
() => logs.filter((log) => isCliLogRelatedToSwap(log, swapId)),
|
||||
[logs, swapId],
|
||||
);
|
||||
}
|
||||
|
||||
export function useAllProviders() {
|
||||
return useAppSelector((state) => {
|
||||
const registryProviders = state.providers.registry.providers || [];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue