Skip to content

Commit

Permalink
feat(tauri, gui): Send event on changes to details, timelocks and tx_…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
binarybaron authored Oct 9, 2024
1 parent e6dc7dd commit 8f33fe5
Show file tree
Hide file tree
Showing 28 changed files with 429 additions and 208 deletions.
3 changes: 3 additions & 0 deletions src-gui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ dist-ssr

# Autogenerated bindings
src/models/tauriModel.ts

# Env files
.env.*
19 changes: 19 additions & 0 deletions src-gui/src/models/cliModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
13 changes: 10 additions & 3 deletions src-gui/src/renderer/components/modal/swap/pages/DebugPage.tsx
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 />;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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" ||
Expand All @@ -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} />;
}
4 changes: 3 additions & 1 deletion src-gui/src/renderer/components/other/RenderedCliLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)} />
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
14 changes: 6 additions & 8 deletions src-gui/src/renderer/components/other/Units.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
38 changes: 34 additions & 4 deletions src-gui/src/renderer/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
BalanceResponse,
BuyXmrArgs,
BuyXmrResponse,
CliLogEmittedEvent,
TauriLogEvent,
GetLogsArgs,
GetLogsResponse,
GetSwapInfoResponse,
Expand All @@ -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";
Expand All @@ -49,18 +53,33 @@ 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) => {
console.log("Received context init progress event", event.payload);
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>(
Expand Down Expand Up @@ -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",
Expand Down
18 changes: 16 additions & 2 deletions src-gui/src/store/features/rpcSlice.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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;
},
Expand Down Expand Up @@ -110,6 +123,7 @@ export const {
rpcSetSwapInfo,
rpcSetMoneroRecoveryKeys,
rpcResetMoneroRecoveryKeys,
timelockChangeEventReceived
} = rpcSlice.actions;

export default rpcSlice.reducer;
Loading

0 comments on commit 8f33fe5

Please sign in to comment.