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
|
# Autogenerated bindings
|
||||||
src/models/tauriModel.ts
|
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 {
|
export function parseCliLogString(log: string): CliLog | string {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(log);
|
const parsed = JSON.parse(log);
|
||||||
|
|
|
@ -61,7 +61,11 @@ function getActiveStep(state: SwapState | null): PathStep {
|
||||||
// Step 1: Waiting for Bitcoin lock confirmation
|
// Step 1: Waiting for Bitcoin lock confirmation
|
||||||
// Bitcoin has been locked, waiting for the counterparty to lock their XMR
|
// Bitcoin has been locked, waiting for the counterparty to lock their XMR
|
||||||
case "BtcLockTxInMempool":
|
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
|
// Still Step 1: Both Bitcoin and XMR have been locked, waiting for Monero lock to be confirmed
|
||||||
case "XmrLockTxInMempool":
|
case "XmrLockTxInMempool":
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { Box, DialogContentText } from "@material-ui/core";
|
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 JsonTreeView from "../../../other/JSONViewTree";
|
||||||
import CliLogsBox from "../../../other/RenderedCliLog";
|
import CliLogsBox from "../../../other/RenderedCliLog";
|
||||||
|
|
||||||
export default function DebugPage() {
|
export default function DebugPage() {
|
||||||
const torStdOut = useAppSelector((s) => s.tor.stdOut);
|
const torStdOut = useAppSelector((s) => s.tor.stdOut);
|
||||||
const logs = useAppSelector((s) => s.swap.logs);
|
const logs = useActiveSwapLogs();
|
||||||
const guiState = useAppSelector((s) => s);
|
const guiState = useAppSelector((s) => s);
|
||||||
const cliState = useActiveSwapInfo();
|
const cliState = useActiveSwapInfo();
|
||||||
|
|
||||||
|
@ -19,7 +23,10 @@ export default function DebugPage() {
|
||||||
gap: "8px",
|
gap: "8px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
|
<CliLogsBox
|
||||||
|
logs={logs}
|
||||||
|
label="Logs relevant to the swap (only current session)"
|
||||||
|
/>
|
||||||
<JsonTreeView
|
<JsonTreeView
|
||||||
data={guiState}
|
data={guiState}
|
||||||
label="Internal GUI State (inferred from Logs)"
|
label="Internal GUI State (inferred from Logs)"
|
||||||
|
|
|
@ -19,20 +19,10 @@ import InitPage from "./init/InitPage";
|
||||||
import WaitingForBitcoinDepositPage from "./init/WaitingForBitcoinDepositPage";
|
import WaitingForBitcoinDepositPage from "./init/WaitingForBitcoinDepositPage";
|
||||||
|
|
||||||
export default function SwapStatePage({ state }: { state: SwapState | null }) {
|
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) {
|
if (state === null) {
|
||||||
return <InitPage />;
|
return <InitPage />;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (state.curr.type) {
|
switch (state.curr.type) {
|
||||||
case "Initiated":
|
case "Initiated":
|
||||||
return <InitiatedPage />;
|
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 { TauriSwapProgressEvent } from "models/tauriModel";
|
||||||
|
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
||||||
|
import { useActiveSwapInfo, useActiveSwapLogs } from "store/hooks";
|
||||||
import SwapStatePage from "../SwapStatePage";
|
import SwapStatePage from "../SwapStatePage";
|
||||||
|
|
||||||
export default function ProcessExitedPage({
|
export default function ProcessExitedPage({
|
||||||
|
@ -8,8 +11,11 @@ export default function ProcessExitedPage({
|
||||||
prevState: TauriSwapProgressEvent | null;
|
prevState: TauriSwapProgressEvent | null;
|
||||||
swapId: string;
|
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
|
// 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 (
|
if (
|
||||||
prevState != null &&
|
prevState != null &&
|
||||||
(prevState.type === "XmrRedeemInMempool" ||
|
(prevState.type === "XmrRedeemInMempool" ||
|
||||||
|
@ -28,15 +34,17 @@ export default function ProcessExitedPage({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Display something useful here
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box>
|
||||||
If the swap is not a "done" state (or we don't have a db state because the
|
<DialogContentText>
|
||||||
swap did complete the SwapSetup yet) we should tell the user and show logs
|
The swap was stopped but it has not been completed yet. Check the logs
|
||||||
Not implemented yet
|
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}
|
setSearchQuery={setSearchQuery}
|
||||||
rows={memoizedLogs.map((log) =>
|
rows={memoizedLogs.map((log) =>
|
||||||
typeof log === "string" ? (
|
typeof log === "string" ? (
|
||||||
<Typography component="pre">{log}</Typography>
|
<Typography key={log} component="pre">
|
||||||
|
{log}
|
||||||
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<RenderedCliLog log={log} key={JSON.stringify(log)} />
|
<RenderedCliLog log={log} key={JSON.stringify(log)} />
|
||||||
),
|
),
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default function ScrollablePaperTextBox({
|
||||||
copyValue: string;
|
copyValue: string;
|
||||||
searchQuery: string | null;
|
searchQuery: string | null;
|
||||||
setSearchQuery?: ((query: string) => void) | null;
|
setSearchQuery?: ((query: string) => void) | null;
|
||||||
minHeight: string;
|
minHeight?: string;
|
||||||
}) {
|
}) {
|
||||||
const virtuaEl = useRef<VListHandle | null>(null);
|
const virtuaEl = useRef<VListHandle | null>(null);
|
||||||
|
|
||||||
|
|
|
@ -15,15 +15,13 @@ export function AmountWithUnit({
|
||||||
fixedPrecision: number;
|
fixedPrecision: number;
|
||||||
dollarRate?: Amount;
|
dollarRate?: Amount;
|
||||||
}) {
|
}) {
|
||||||
|
const title =
|
||||||
|
dollarRate != null && amount != null
|
||||||
|
? `≈ $${(dollarRate * amount).toFixed(2)}`
|
||||||
|
: "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip arrow title={title}>
|
||||||
arrow
|
|
||||||
title={
|
|
||||||
dollarRate != null && amount != null
|
|
||||||
? `≈ $${(dollarRate * amount).toFixed(2)}`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>
|
<span>
|
||||||
{amount != null
|
{amount != null
|
||||||
? Number.parseFloat(amount.toFixed(fixedPrecision))
|
? Number.parseFloat(amount.toFixed(fixedPrecision))
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
BalanceResponse,
|
BalanceResponse,
|
||||||
BuyXmrArgs,
|
BuyXmrArgs,
|
||||||
BuyXmrResponse,
|
BuyXmrResponse,
|
||||||
CliLogEmittedEvent,
|
TauriLogEvent,
|
||||||
GetLogsArgs,
|
GetLogsArgs,
|
||||||
GetLogsResponse,
|
GetLogsResponse,
|
||||||
GetSwapInfoResponse,
|
GetSwapInfoResponse,
|
||||||
|
@ -18,14 +18,18 @@ import {
|
||||||
TauriSwapProgressEventWrapper,
|
TauriSwapProgressEventWrapper,
|
||||||
WithdrawBtcArgs,
|
WithdrawBtcArgs,
|
||||||
WithdrawBtcResponse,
|
WithdrawBtcResponse,
|
||||||
|
TauriDatabaseStateEvent,
|
||||||
|
TauriTimelockChangeEvent,
|
||||||
|
GetSwapInfoArgs,
|
||||||
} from "models/tauriModel";
|
} from "models/tauriModel";
|
||||||
import {
|
import {
|
||||||
contextStatusEventReceived,
|
contextStatusEventReceived,
|
||||||
receivedCliLog,
|
receivedCliLog,
|
||||||
rpcSetBalance,
|
rpcSetBalance,
|
||||||
rpcSetSwapInfo,
|
rpcSetSwapInfo,
|
||||||
|
timelockChangeEventReceived,
|
||||||
} from "store/features/rpcSlice";
|
} from "store/features/rpcSlice";
|
||||||
import { swapTauriEventReceived } from "store/features/swapSlice";
|
import { swapProgressEventReceived } 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";
|
||||||
|
@ -49,7 +53,7 @@ export async function initEventListeners() {
|
||||||
|
|
||||||
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(swapProgressEventReceived(event.payload));
|
||||||
});
|
});
|
||||||
|
|
||||||
listen<TauriContextStatusEvent>("context-init-progress-update", (event) => {
|
listen<TauriContextStatusEvent>("context-init-progress-update", (event) => {
|
||||||
|
@ -57,10 +61,25 @@ export async function initEventListeners() {
|
||||||
store.dispatch(contextStatusEventReceived(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);
|
console.log("Received cli log event", event.payload);
|
||||||
store.dispatch(receivedCliLog(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>(
|
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> {
|
export async function withdrawBtc(address: string): Promise<string> {
|
||||||
const response = await invoke<WithdrawBtcArgs, WithdrawBtcResponse>(
|
const response = await invoke<WithdrawBtcArgs, WithdrawBtcResponse>(
|
||||||
"withdraw_btc",
|
"withdraw_btc",
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
|
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
|
||||||
import {
|
import {
|
||||||
CliLogEmittedEvent,
|
TauriLogEvent,
|
||||||
GetSwapInfoResponse,
|
GetSwapInfoResponse,
|
||||||
TauriContextStatusEvent,
|
TauriContextStatusEvent,
|
||||||
|
TauriDatabaseStateEvent,
|
||||||
|
TauriTimelockChangeEvent,
|
||||||
} from "models/tauriModel";
|
} from "models/tauriModel";
|
||||||
import { MoneroRecoveryResponse } from "../../models/rpcModel";
|
import { MoneroRecoveryResponse } from "../../models/rpcModel";
|
||||||
import { GetSwapInfoResponseExt } from "models/tauriModelExt";
|
import { GetSwapInfoResponseExt } from "models/tauriModelExt";
|
||||||
import { getLogsAndStringsFromRawFileString } from "utils/parseUtils";
|
import { getLogsAndStringsFromRawFileString } from "utils/parseUtils";
|
||||||
import { CliLog } from "models/cliModel";
|
import { CliLog } from "models/cliModel";
|
||||||
|
import logger from "utils/logger";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
balance: number | null;
|
balance: number | null;
|
||||||
|
@ -52,7 +55,7 @@ export const rpcSlice = createSlice({
|
||||||
name: "rpc",
|
name: "rpc",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
receivedCliLog(slice, action: PayloadAction<CliLogEmittedEvent>) {
|
receivedCliLog(slice, action: PayloadAction<TauriLogEvent>) {
|
||||||
const buffer = action.payload.buffer;
|
const buffer = action.payload.buffer;
|
||||||
const logs = getLogsAndStringsFromRawFileString(buffer);
|
const logs = getLogsAndStringsFromRawFileString(buffer);
|
||||||
slice.logs = slice.logs.concat(logs);
|
slice.logs = slice.logs.concat(logs);
|
||||||
|
@ -63,6 +66,16 @@ export const rpcSlice = createSlice({
|
||||||
) {
|
) {
|
||||||
slice.status = action.payload;
|
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>) {
|
rpcSetBalance(slice, action: PayloadAction<number>) {
|
||||||
slice.state.balance = action.payload;
|
slice.state.balance = action.payload;
|
||||||
},
|
},
|
||||||
|
@ -110,6 +123,7 @@ export const {
|
||||||
rpcSetSwapInfo,
|
rpcSetSwapInfo,
|
||||||
rpcSetMoneroRecoveryKeys,
|
rpcSetMoneroRecoveryKeys,
|
||||||
rpcResetMoneroRecoveryKeys,
|
rpcResetMoneroRecoveryKeys,
|
||||||
|
timelockChangeEventReceived
|
||||||
} = rpcSlice.actions;
|
} = rpcSlice.actions;
|
||||||
|
|
||||||
export default rpcSlice.reducer;
|
export default rpcSlice.reducer;
|
||||||
|
|
|
@ -14,7 +14,7 @@ export const swapSlice = createSlice({
|
||||||
name: "swap",
|
name: "swap",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
swapTauriEventReceived(
|
swapProgressEventReceived(
|
||||||
swap,
|
swap,
|
||||||
action: PayloadAction<TauriSwapProgressEventWrapper>,
|
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;
|
export default swapSlice.reducer;
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { sortBy } from "lodash";
|
import { sortBy } from "lodash";
|
||||||
|
import { GetSwapInfoResponseExt } from "models/tauriModelExt";
|
||||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||||
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
||||||
import { parseDateString } from "utils/parseUtils";
|
import { parseDateString } from "utils/parseUtils";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { isCliLogRelatedToSwap } from "models/cliModel";
|
||||||
|
|
||||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||||
|
@ -26,7 +29,9 @@ export function useIsContextAvailable() {
|
||||||
return useAppSelector((state) => state.rpc.status?.type === "Available");
|
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) =>
|
return useAppSelector((state) =>
|
||||||
swapId ? state.rpc.state.swapInfos[swapId] ?? null : null,
|
swapId ? state.rpc.state.swapInfos[swapId] ?? null : null,
|
||||||
);
|
);
|
||||||
|
@ -36,11 +41,21 @@ export function useActiveSwapId() {
|
||||||
return useAppSelector((s) => s.swap.state?.swapId ?? null);
|
return useAppSelector((s) => s.swap.state?.swapId ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useActiveSwapInfo() {
|
export function useActiveSwapInfo(): GetSwapInfoResponseExt | null {
|
||||||
const swapId = useActiveSwapId();
|
const swapId = useActiveSwapId();
|
||||||
return useSwapInfo(swapId);
|
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() {
|
export function useAllProviders() {
|
||||||
return useAppSelector((state) => {
|
return useAppSelector((state) => {
|
||||||
const registryProviders = state.providers.registry.providers || [];
|
const registryProviders = state.providers.registry.providers || [];
|
||||||
|
|
|
@ -5,8 +5,8 @@ use swap::cli::{
|
||||||
api::{
|
api::{
|
||||||
request::{
|
request::{
|
||||||
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, GetHistoryArgs, GetLogsArgs,
|
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, GetHistoryArgs, GetLogsArgs,
|
||||||
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, ResumeSwapArgs,
|
GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs,
|
||||||
SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||||
},
|
},
|
||||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
||||||
Context, ContextBuilder,
|
Context, ContextBuilder,
|
||||||
|
@ -40,7 +40,7 @@ impl<T, E: ToString> ToStringResult<T> for Result<T, E> {
|
||||||
/// async fn get_balance(context: tauri::State<'...>, args: BalanceArgs) -> Result<BalanceArgs::Response, String> {
|
/// async fn get_balance(context: tauri::State<'...>, args: BalanceArgs) -> Result<BalanceArgs::Response, String> {
|
||||||
/// args.handle(context.inner().clone()).await.to_string_result()
|
/// args.handle(context.inner().clone()).await.to_string_result()
|
||||||
/// }
|
/// }
|
||||||
///
|
/// ```
|
||||||
/// # Example 2
|
/// # Example 2
|
||||||
/// ```ignored
|
/// ```ignored
|
||||||
/// tauri_command!(get_balance, BalanceArgs, no_args);
|
/// tauri_command!(get_balance, BalanceArgs, no_args);
|
||||||
|
@ -130,6 +130,7 @@ pub fn run() {
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
get_balance,
|
get_balance,
|
||||||
|
get_swap_info,
|
||||||
get_swap_infos_all,
|
get_swap_infos_all,
|
||||||
withdraw_btc,
|
withdraw_btc,
|
||||||
buy_xmr,
|
buy_xmr,
|
||||||
|
@ -185,6 +186,7 @@ tauri_command!(cancel_and_refund, CancelAndRefundArgs);
|
||||||
|
|
||||||
// These commands require no arguments
|
// 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_info, GetSwapInfoArgs);
|
||||||
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);
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,7 @@ pub async fn main() -> Result<()> {
|
||||||
|
|
||||||
match cmd {
|
match cmd {
|
||||||
Command::Start { resume_only } => {
|
Command::Start { resume_only } => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite).await?;
|
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite, None).await?;
|
||||||
|
|
||||||
// check and warn for duplicate rendezvous points
|
// check and warn for duplicate rendezvous points
|
||||||
let mut rendezvous_addrs = config.network.rendezvous_point.clone();
|
let mut rendezvous_addrs = config.network.rendezvous_point.clone();
|
||||||
|
@ -233,7 +233,7 @@ pub async fn main() -> Result<()> {
|
||||||
event_loop.run().await;
|
event_loop.run().await;
|
||||||
}
|
}
|
||||||
Command::History => {
|
Command::History => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadOnly).await?;
|
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadOnly, None).await?;
|
||||||
|
|
||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
|
|
||||||
|
@ -293,7 +293,7 @@ pub async fn main() -> Result<()> {
|
||||||
tracing::info!(%bitcoin_balance, %monero_balance, "Current balance");
|
tracing::info!(%bitcoin_balance, %monero_balance, "Current balance");
|
||||||
}
|
}
|
||||||
Command::Cancel { swap_id } => {
|
Command::Cancel { swap_id } => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite).await?;
|
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite, None).await?;
|
||||||
|
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
|
|
||||||
|
@ -302,7 +302,7 @@ pub async fn main() -> Result<()> {
|
||||||
tracing::info!("Cancel transaction successfully published with id {}", txid);
|
tracing::info!("Cancel transaction successfully published with id {}", txid);
|
||||||
}
|
}
|
||||||
Command::Refund { swap_id } => {
|
Command::Refund { swap_id } => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite).await?;
|
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite, None).await?;
|
||||||
|
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
let monero_wallet = init_monero_wallet(&config, env_config).await?;
|
let monero_wallet = init_monero_wallet(&config, env_config).await?;
|
||||||
|
@ -318,7 +318,7 @@ pub async fn main() -> Result<()> {
|
||||||
tracing::info!("Monero successfully refunded");
|
tracing::info!("Monero successfully refunded");
|
||||||
}
|
}
|
||||||
Command::Punish { swap_id } => {
|
Command::Punish { swap_id } => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite).await?;
|
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite, None).await?;
|
||||||
|
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
|
|
||||||
|
@ -327,7 +327,7 @@ pub async fn main() -> Result<()> {
|
||||||
tracing::info!("Punish transaction successfully published with id {}", txid);
|
tracing::info!("Punish transaction successfully published with id {}", txid);
|
||||||
}
|
}
|
||||||
Command::SafelyAbort { swap_id } => {
|
Command::SafelyAbort { swap_id } => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite).await?;
|
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite, None).await?;
|
||||||
|
|
||||||
safely_abort(swap_id, db).await?;
|
safely_abort(swap_id, db).await?;
|
||||||
|
|
||||||
|
@ -337,7 +337,7 @@ pub async fn main() -> Result<()> {
|
||||||
swap_id,
|
swap_id,
|
||||||
do_not_await_finality,
|
do_not_await_finality,
|
||||||
} => {
|
} => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite).await?;
|
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite, None).await?;
|
||||||
|
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
|
|
||||||
|
|
|
@ -280,7 +280,7 @@ impl Subscription {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_until(&self, mut predicate: impl FnMut(&ScriptStatus) -> bool) -> Result<()> {
|
pub async fn wait_until(&self, mut predicate: impl FnMut(&ScriptStatus) -> bool) -> Result<()> {
|
||||||
let mut receiver = self.receiver.clone();
|
let mut receiver = self.receiver.clone();
|
||||||
|
|
||||||
while !predicate(&receiver.borrow()) {
|
while !predicate(&receiver.borrow()) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ pub mod command;
|
||||||
mod event_loop;
|
mod event_loop;
|
||||||
mod list_sellers;
|
mod list_sellers;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
|
pub mod watcher;
|
||||||
|
|
||||||
pub use behaviour::{Behaviour, OutEvent};
|
pub use behaviour::{Behaviour, OutEvent};
|
||||||
pub use cancel_and_refund::{cancel, cancel_and_refund, refund};
|
pub use cancel_and_refund::{cancel, cancel_and_refund, refund};
|
||||||
|
|
|
@ -27,6 +27,8 @@ use tracing::Level;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::watcher::Watcher;
|
||||||
|
|
||||||
static START: Once = Once::new();
|
static START: Once = Once::new();
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
@ -306,25 +308,31 @@ impl ContextBuilder {
|
||||||
TauriContextInitializationProgress::OpeningBitcoinWallet,
|
TauriContextInitializationProgress::OpeningBitcoinWallet,
|
||||||
));
|
));
|
||||||
|
|
||||||
let bitcoin_wallet = {
|
let bitcoin_wallet = if let Some(bitcoin) = self.bitcoin {
|
||||||
if let Some(bitcoin) = self.bitcoin {
|
let (url, target_block) = bitcoin.apply_defaults(self.is_testnet)?;
|
||||||
let (bitcoin_electrum_rpc_url, bitcoin_target_block) =
|
Some(Arc::new(
|
||||||
bitcoin.apply_defaults(self.is_testnet)?;
|
init_bitcoin_wallet(url, &seed, data_dir.clone(), env_config, target_block).await?,
|
||||||
Some(Arc::new(
|
))
|
||||||
init_bitcoin_wallet(
|
} else {
|
||||||
bitcoin_electrum_rpc_url,
|
None
|
||||||
&seed,
|
|
||||||
data_dir.clone(),
|
|
||||||
env_config,
|
|
||||||
bitcoin_target_block,
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let db = open_db(
|
||||||
|
data_dir.join("sqlite"),
|
||||||
|
AccessMode::ReadWrite,
|
||||||
|
self.tauri_handle.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// If we are connected to the Bitcoin blockchain and if there is a handle to Tauri present,
|
||||||
|
// we start a background task to watch for timelock changes.
|
||||||
|
if let Some(wallet) = bitcoin_wallet.clone() {
|
||||||
|
if self.tauri_handle.is_some() {
|
||||||
|
let watcher = Watcher::new(wallet, db.clone(), self.tauri_handle.clone());
|
||||||
|
tokio::spawn(watcher.run());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We initialize the Monero wallet below
|
// We initialize the Monero wallet below
|
||||||
// To display the progress to the user, we emit events to the Tauri frontend
|
// To display the progress to the user, we emit events to the Tauri frontend
|
||||||
self.tauri_handle
|
self.tauri_handle
|
||||||
|
@ -353,7 +361,7 @@ impl ContextBuilder {
|
||||||
let tor_socks5_port = self.tor.map_or(9050, |tor| tor.tor_socks5_port);
|
let tor_socks5_port = self.tor.map_or(9050, |tor| tor.tor_socks5_port);
|
||||||
|
|
||||||
let context = Context {
|
let context = Context {
|
||||||
db: open_db(data_dir.join("sqlite"), AccessMode::ReadWrite).await?,
|
db,
|
||||||
bitcoin_wallet,
|
bitcoin_wallet,
|
||||||
monero_wallet,
|
monero_wallet,
|
||||||
monero_rpc_process,
|
monero_rpc_process,
|
||||||
|
@ -396,7 +404,7 @@ impl Context {
|
||||||
bitcoin_wallet: Some(bob_bitcoin_wallet),
|
bitcoin_wallet: Some(bob_bitcoin_wallet),
|
||||||
monero_wallet: Some(bob_monero_wallet),
|
monero_wallet: Some(bob_monero_wallet),
|
||||||
config,
|
config,
|
||||||
db: open_db(db_path, AccessMode::ReadWrite)
|
db: open_db(db_path, AccessMode::ReadWrite, None)
|
||||||
.await
|
.await
|
||||||
.expect("Could not open sqlite database"),
|
.expect("Could not open sqlite database"),
|
||||||
monero_rpc_process: None,
|
monero_rpc_process: None,
|
||||||
|
|
|
@ -478,52 +478,36 @@ pub async fn get_swap_info(
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|state| {
|
.find_map(|state| {
|
||||||
if let State::Bob(BobState::SwapSetupCompleted(state2)) = state {
|
let State::Bob(BobState::SwapSetupCompleted(state2)) = state else {
|
||||||
let xmr_amount = state2.xmr;
|
return None;
|
||||||
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 xmr_amount = state2.xmr;
|
||||||
Some((
|
let btc_amount = state2.tx_lock.lock_amount();
|
||||||
xmr_amount,
|
let tx_cancel_fee = state2.tx_cancel_fee;
|
||||||
btc_amount,
|
let tx_refund_fee = state2.tx_refund_fee;
|
||||||
tx_lock_id,
|
let tx_lock_id = state2.tx_lock.txid();
|
||||||
tx_cancel_fee,
|
let btc_refund_address = state2.refund_address.to_string();
|
||||||
tx_refund_fee,
|
|
||||||
tx_lock_fee,
|
let Ok(tx_lock_fee) = state2.tx_lock.fee() else {
|
||||||
btc_refund_address,
|
return None;
|
||||||
state2.cancel_timelock,
|
};
|
||||||
state2.punish_timelock,
|
|
||||||
))
|
Some((
|
||||||
} else {
|
xmr_amount,
|
||||||
None
|
btc_amount,
|
||||||
}
|
tx_lock_id,
|
||||||
} else {
|
tx_cancel_fee,
|
||||||
None
|
tx_refund_fee,
|
||||||
}
|
tx_lock_fee,
|
||||||
|
btc_refund_address,
|
||||||
|
state2.cancel_timelock,
|
||||||
|
state2.punish_timelock,
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.with_context(|| "Did not find SwapSetupCompleted state for swap")?;
|
.with_context(|| "Did not find SwapSetupCompleted state for swap")?;
|
||||||
|
|
||||||
let timelock = match swap_state.clone() {
|
let timelock = swap_state.expired_timelocks(bitcoin_wallet.clone()).await?;
|
||||||
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 {
|
Ok(GetSwapInfoResponse {
|
||||||
swap_id: args.swap_id,
|
swap_id: args.swap_id,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{monero, network::quote::BidQuote};
|
use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use bitcoin::Txid;
|
use bitcoin::Txid;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -7,9 +7,11 @@ use typeshare::typeshare;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const SWAP_PROGRESS_EVENT_NAME: &str = "swap-progress-update";
|
|
||||||
const CONTEXT_INIT_PROGRESS_EVENT_NAME: &str = "context-init-progress-update";
|
|
||||||
const CLI_LOG_EMITTED_EVENT_NAME: &str = "cli-log-emitted";
|
const CLI_LOG_EMITTED_EVENT_NAME: &str = "cli-log-emitted";
|
||||||
|
const SWAP_PROGRESS_EVENT_NAME: &str = "swap-progress-update";
|
||||||
|
const SWAP_STATE_CHANGE_EVENT_NAME: &str = "swap-database-state-update";
|
||||||
|
const TIMELOCK_CHANGE_EVENT_NAME: &str = "timelock-change";
|
||||||
|
const CONTEXT_INIT_PROGRESS_EVENT_NAME: &str = "context-init-progress-update";
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TauriHandle(
|
pub struct TauriHandle(
|
||||||
|
@ -50,11 +52,25 @@ pub trait TauriEmitter {
|
||||||
let _ = self.emit_tauri_event(CONTEXT_INIT_PROGRESS_EVENT_NAME, event);
|
let _ = self.emit_tauri_event(CONTEXT_INIT_PROGRESS_EVENT_NAME, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn emit_cli_log_event(&self, event: CliLogEmittedEvent) {
|
fn emit_cli_log_event(&self, event: TauriLogEvent) {
|
||||||
let _ = self
|
let _ = self
|
||||||
.emit_tauri_event(CLI_LOG_EMITTED_EVENT_NAME, event)
|
.emit_tauri_event(CLI_LOG_EMITTED_EVENT_NAME, event)
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn emit_swap_state_change_event(&self, swap_id: Uuid) {
|
||||||
|
let _ = self.emit_tauri_event(
|
||||||
|
SWAP_STATE_CHANGE_EVENT_NAME,
|
||||||
|
TauriDatabaseStateEvent { swap_id },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_timelock_change_event(&self, swap_id: Uuid, timelock: Option<ExpiredTimelocks>) {
|
||||||
|
let _ = self.emit_tauri_event(
|
||||||
|
TIMELOCK_CHANGE_EVENT_NAME,
|
||||||
|
TauriTimelockChangeEvent { swap_id, timelock },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TauriEmitter for TauriHandle {
|
impl TauriEmitter for TauriHandle {
|
||||||
|
@ -174,11 +190,26 @@ pub enum TauriSwapProgressEvent {
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
pub struct CliLogEmittedEvent {
|
pub struct TauriLogEvent {
|
||||||
/// The serialized object containing the log message and metadata.
|
/// The serialized object containing the log message and metadata.
|
||||||
pub buffer: String,
|
pub buffer: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct TauriDatabaseStateEvent {
|
||||||
|
#[typeshare(serialized_as = "string")]
|
||||||
|
swap_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct TauriTimelockChangeEvent {
|
||||||
|
#[typeshare(serialized_as = "string")]
|
||||||
|
swap_id: Uuid,
|
||||||
|
timelock: Option<ExpiredTimelocks>,
|
||||||
|
}
|
||||||
|
|
||||||
/// This struct contains the settings for the Context
|
/// This struct contains the settings for the Context
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
|
105
swap/src/cli/watcher.rs
Normal file
105
swap/src/cli/watcher.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
use super::api::tauri_bindings::TauriEmitter;
|
||||||
|
use crate::bitcoin::{ExpiredTimelocks, Wallet};
|
||||||
|
use crate::cli::api::tauri_bindings::TauriHandle;
|
||||||
|
use crate::protocol::bob::BobState;
|
||||||
|
use crate::protocol::{Database, State};
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// A long running task which watches for changes to timelocks
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Watcher {
|
||||||
|
wallet: Arc<Wallet>,
|
||||||
|
database: Arc<dyn Database + Send + Sync>,
|
||||||
|
tauri: Option<TauriHandle>,
|
||||||
|
/// This saves for every running swap the last known timelock status
|
||||||
|
cached_timelocks: HashMap<Uuid, Option<ExpiredTimelocks>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Watcher {
|
||||||
|
/// How often to check for changes (in seconds)
|
||||||
|
const CHECK_INTERVAL: u64 = 30;
|
||||||
|
|
||||||
|
/// Create a new Watcher
|
||||||
|
pub fn new(
|
||||||
|
wallet: Arc<Wallet>,
|
||||||
|
database: Arc<dyn Database + Send + Sync>,
|
||||||
|
tauri: Option<TauriHandle>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
wallet,
|
||||||
|
database,
|
||||||
|
cached_timelocks: HashMap::new(),
|
||||||
|
tauri,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start running the watcher event loop.
|
||||||
|
/// Should be done in a new task using [`tokio::spawn`].
|
||||||
|
pub async fn run(mut self) {
|
||||||
|
// Note: since this is de-facto a daemon, we have to gracefully handle errors
|
||||||
|
// (which in our case means logging the error message and trying again later)
|
||||||
|
loop {
|
||||||
|
// Fetch current transactions and timelocks
|
||||||
|
let current_swaps = match self.get_current_swaps().await {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(error=%e, "Failed to fetch current transactions, retrying later");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for changes for every current swap
|
||||||
|
for (swap_id, state) in current_swaps {
|
||||||
|
// Determine if the timelock has expired for the current swap.
|
||||||
|
// We intentionally do not skip swaps with a None timelock status, as this represents a valid state.
|
||||||
|
// When a swap reaches its final state, the timelock becomes irrelevant, but it is still important to explicitly send None
|
||||||
|
// This indicates that the timelock no longer needs to be displayed in the GUI
|
||||||
|
let new_timelock_status = match state.expired_timelocks(self.wallet.clone()).await {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(error=%e, swap_id=%swap_id, "Failed to check timelock status, retrying later");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the status changed
|
||||||
|
if let Some(old_status) = self.cached_timelocks.get(&swap_id) {
|
||||||
|
// And send a tauri event if it did
|
||||||
|
if *old_status != new_timelock_status {
|
||||||
|
self.tauri
|
||||||
|
.emit_timelock_change_event(swap_id, new_timelock_status);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If this is the first time we see this swap, send a tauri event, too
|
||||||
|
self.tauri
|
||||||
|
.emit_timelock_change_event(swap_id, new_timelock_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new status
|
||||||
|
self.cached_timelocks.insert(swap_id, new_timelock_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep and check again later
|
||||||
|
tokio::time::sleep(Duration::from_secs(Watcher::CHECK_INTERVAL)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function for fetching the current list of swaps
|
||||||
|
async fn get_current_swaps(&self) -> Result<Vec<(Uuid, BobState)>> {
|
||||||
|
Ok(self
|
||||||
|
.database
|
||||||
|
.all()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
// Filter for BobState
|
||||||
|
.filter_map(|(uuid, state)| match state {
|
||||||
|
State::Bob(bob_state) => Some((uuid, bob_state)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,7 +52,6 @@ macro_rules! regex_find_placeholders {
|
||||||
($pattern:expr, $create_placeholder:expr, $replacements:expr, $input:expr) => {{
|
($pattern:expr, $create_placeholder:expr, $replacements:expr, $input:expr) => {{
|
||||||
// compile the regex pattern
|
// compile the regex pattern
|
||||||
static REGEX: once_cell::sync::Lazy<regex::Regex> = once_cell::sync::Lazy::new(|| {
|
static REGEX: once_cell::sync::Lazy<regex::Regex> = once_cell::sync::Lazy::new(|| {
|
||||||
tracing::debug!("initializing regex");
|
|
||||||
regex::Regex::new($pattern).expect("invalid regex pattern")
|
regex::Regex::new($pattern).expect("invalid regex pattern")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
use tracing_subscriber::{fmt, EnvFilter, Layer};
|
use tracing_subscriber::{fmt, EnvFilter, Layer};
|
||||||
|
|
||||||
use crate::cli::api::tauri_bindings::{CliLogEmittedEvent, TauriEmitter, TauriHandle};
|
use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriLogEvent};
|
||||||
|
|
||||||
/// Output formats for logging messages.
|
/// Output formats for logging messages.
|
||||||
pub enum Format {
|
pub enum Format {
|
||||||
|
@ -63,13 +63,13 @@ pub fn init(
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(file_layer)
|
.with(file_layer)
|
||||||
.with(tauri_layer)
|
.with(tauri_layer)
|
||||||
.with(terminal_layer.json().with_filter(level_filter))
|
.with(terminal_layer.json().with_filter(env_filter(level_filter)?))
|
||||||
.try_init()?;
|
.try_init()?;
|
||||||
} else {
|
} else {
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(file_layer)
|
.with(file_layer)
|
||||||
.with(tauri_layer)
|
.with(tauri_layer)
|
||||||
.with(terminal_layer.with_filter(level_filter))
|
.with(terminal_layer.with_filter(env_filter(level_filter)?))
|
||||||
.try_init()?;
|
.try_init()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ impl std::io::Write for TauriWriter {
|
||||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||||
|
|
||||||
// Then send to tauri
|
// Then send to tauri
|
||||||
self.tauri_handle.emit_cli_log_event(CliLogEmittedEvent {
|
self.tauri_handle.emit_cli_log_event(TauriLogEvent {
|
||||||
buffer: utf8_string,
|
buffer: utf8_string,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ pub use alice::Alice;
|
||||||
pub use bob::Bob;
|
pub use bob::Bob;
|
||||||
pub use sqlite::SqliteDatabase;
|
pub use sqlite::SqliteDatabase;
|
||||||
|
|
||||||
|
use crate::cli::api::tauri_bindings::TauriHandle;
|
||||||
use crate::fs::ensure_directory_exists;
|
use crate::fs::ensure_directory_exists;
|
||||||
use crate::protocol::{Database, State};
|
use crate::protocol::{Database, State};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
@ -92,16 +93,25 @@ pub enum AccessMode {
|
||||||
pub async fn open_db(
|
pub async fn open_db(
|
||||||
sqlite_path: impl AsRef<Path>,
|
sqlite_path: impl AsRef<Path>,
|
||||||
access_mode: AccessMode,
|
access_mode: AccessMode,
|
||||||
|
tauri_handle: impl Into<Option<TauriHandle>>,
|
||||||
) -> Result<Arc<dyn Database + Send + Sync>> {
|
) -> Result<Arc<dyn Database + Send + Sync>> {
|
||||||
if sqlite_path.as_ref().exists() {
|
if sqlite_path.as_ref().exists() {
|
||||||
tracing::debug!("Using existing sqlite database.");
|
tracing::debug!("Using existing sqlite database.");
|
||||||
let sqlite = SqliteDatabase::open(sqlite_path, access_mode).await?;
|
|
||||||
|
let sqlite = SqliteDatabase::open(sqlite_path, access_mode)
|
||||||
|
.await?
|
||||||
|
.with_tauri_handle(tauri_handle.into());
|
||||||
|
|
||||||
Ok(Arc::new(sqlite))
|
Ok(Arc::new(sqlite))
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!("Creating and using new sqlite database.");
|
tracing::debug!("Creating and using new sqlite database.");
|
||||||
|
|
||||||
ensure_directory_exists(sqlite_path.as_ref())?;
|
ensure_directory_exists(sqlite_path.as_ref())?;
|
||||||
tokio::fs::File::create(&sqlite_path).await?;
|
tokio::fs::File::create(&sqlite_path).await?;
|
||||||
let sqlite = SqliteDatabase::open(sqlite_path, access_mode).await?;
|
let sqlite = SqliteDatabase::open(sqlite_path, access_mode)
|
||||||
|
.await?
|
||||||
|
.with_tauri_handle(tauri_handle.into());
|
||||||
|
|
||||||
Ok(Arc::new(sqlite))
|
Ok(Arc::new(sqlite))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::cli::api::tauri_bindings::TauriEmitter;
|
||||||
|
use crate::cli::api::tauri_bindings::TauriHandle;
|
||||||
use crate::database::Swap;
|
use crate::database::Swap;
|
||||||
use crate::monero::{Address, TransferProof};
|
use crate::monero::{Address, TransferProof};
|
||||||
use crate::protocol::{Database, State};
|
use crate::protocol::{Database, State};
|
||||||
|
@ -15,6 +17,7 @@ use super::AccessMode;
|
||||||
|
|
||||||
pub struct SqliteDatabase {
|
pub struct SqliteDatabase {
|
||||||
pool: Pool<Sqlite>,
|
pool: Pool<Sqlite>,
|
||||||
|
tauri_handle: Option<TauriHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SqliteDatabase {
|
impl SqliteDatabase {
|
||||||
|
@ -30,7 +33,10 @@ impl SqliteDatabase {
|
||||||
let options = options.disable_statement_logging();
|
let options = options.disable_statement_logging();
|
||||||
|
|
||||||
let pool = SqlitePool::connect_with(options.to_owned()).await?;
|
let pool = SqlitePool::connect_with(options.to_owned()).await?;
|
||||||
let mut sqlite = Self { pool };
|
let mut sqlite = Self {
|
||||||
|
pool,
|
||||||
|
tauri_handle: None,
|
||||||
|
};
|
||||||
|
|
||||||
if !read_only {
|
if !read_only {
|
||||||
sqlite.run_migrations().await?;
|
sqlite.run_migrations().await?;
|
||||||
|
@ -39,6 +45,11 @@ impl SqliteDatabase {
|
||||||
Ok(sqlite)
|
Ok(sqlite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_tauri_handle(mut self, tauri_handle: impl Into<Option<TauriHandle>>) -> Self {
|
||||||
|
self.tauri_handle = tauri_handle.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_migrations(&mut self) -> anyhow::Result<()> {
|
async fn run_migrations(&mut self) -> anyhow::Result<()> {
|
||||||
sqlx::migrate!("./migrations").run(&self.pool).await?;
|
sqlx::migrate!("./migrations").run(&self.pool).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -204,9 +215,9 @@ impl Database for SqliteDatabase {
|
||||||
let mut conn = self.pool.acquire().await?;
|
let mut conn = self.pool.acquire().await?;
|
||||||
let entered_at = OffsetDateTime::now_utc();
|
let entered_at = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
let swap_id = swap_id.to_string();
|
|
||||||
let swap = serde_json::to_string(&Swap::from(state))?;
|
let swap = serde_json::to_string(&Swap::from(state))?;
|
||||||
let entered_at = entered_at.to_string();
|
let entered_at = entered_at.to_string();
|
||||||
|
let swap_id_str = swap_id.to_string();
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
|
@ -216,13 +227,17 @@ impl Database for SqliteDatabase {
|
||||||
state
|
state
|
||||||
) values (?, ?, ?);
|
) values (?, ?, ?);
|
||||||
"#,
|
"#,
|
||||||
swap_id,
|
swap_id_str,
|
||||||
entered_at,
|
entered_at,
|
||||||
swap
|
swap
|
||||||
)
|
)
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Emit event to Tauri, the frontend will then send another request to get the latest state
|
||||||
|
// This is why we don't send the state here
|
||||||
|
self.tauri_handle.emit_swap_state_change_event(swap_id);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::bitcoin::wallet::{EstimateFeeRate, Subscription};
|
use crate::bitcoin::wallet::{EstimateFeeRate, Subscription};
|
||||||
use crate::bitcoin::{
|
use crate::bitcoin::{
|
||||||
self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
|
self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
|
||||||
TxLock, Txid,
|
TxLock, Txid, Wallet,
|
||||||
};
|
};
|
||||||
use crate::monero::wallet::WatchRequest;
|
use crate::monero::wallet::WatchRequest;
|
||||||
use crate::monero::{self, TxHash};
|
use crate::monero::{self, TxHash};
|
||||||
|
@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof;
|
use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
@ -76,6 +77,35 @@ impl fmt::Display for BobState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BobState {
|
||||||
|
/// Fetch the expired timelocks for the swap.
|
||||||
|
/// Depending on the State, there are no locks to expire.
|
||||||
|
pub async fn expired_timelocks(
|
||||||
|
&self,
|
||||||
|
bitcoin_wallet: Arc<Wallet>,
|
||||||
|
) -> Result<Option<ExpiredTimelocks>> {
|
||||||
|
Ok(match self.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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct State0 {
|
pub struct State0 {
|
||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::bitcoin::wallet::ScriptStatus;
|
||||||
use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund};
|
use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund};
|
||||||
use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent};
|
use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent};
|
||||||
use crate::cli::EventLoopHandle;
|
use crate::cli::EventLoopHandle;
|
||||||
|
@ -119,12 +120,17 @@ async fn next_state(
|
||||||
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
|
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
|
||||||
|
|
||||||
// Alice and Bob have exchanged info
|
// Alice and Bob have exchanged info
|
||||||
|
// Sign the Bitcoin lock transaction
|
||||||
let (state3, tx_lock) = state2.lock_btc().await?;
|
let (state3, tx_lock) = state2.lock_btc().await?;
|
||||||
let signed_tx = bitcoin_wallet
|
let signed_tx = bitcoin_wallet
|
||||||
.sign_and_finalize(tx_lock.clone().into())
|
.sign_and_finalize(tx_lock.clone().into())
|
||||||
.await
|
.await
|
||||||
.context("Failed to sign Bitcoin lock transaction")?;
|
.context("Failed to sign Bitcoin lock transaction")?;
|
||||||
|
|
||||||
|
// Publish the signed Bitcoin lock transaction
|
||||||
|
let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
|
||||||
|
|
||||||
|
// Emit an event to tauri that the the swap started
|
||||||
event_emitter.emit_swap_progress_event(
|
event_emitter.emit_swap_progress_event(
|
||||||
swap_id,
|
swap_id,
|
||||||
TauriSwapProgressEvent::Started {
|
TauriSwapProgressEvent::Started {
|
||||||
|
@ -134,8 +140,6 @@ async fn next_state(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
|
|
||||||
|
|
||||||
BobState::BtcLocked {
|
BobState::BtcLocked {
|
||||||
state3,
|
state3,
|
||||||
monero_wallet_restore_blockheight,
|
monero_wallet_restore_blockheight,
|
||||||
|
@ -187,8 +191,21 @@ async fn next_state(
|
||||||
|
|
||||||
// Wait for either Alice to send the XMR transfer proof or until we can cancel the swap
|
// Wait for either Alice to send the XMR transfer proof or until we can cancel the swap
|
||||||
let transfer_proof_watcher = event_loop_handle.recv_transfer_proof();
|
let transfer_proof_watcher = event_loop_handle.recv_transfer_proof();
|
||||||
let cancel_timelock_expires =
|
let cancel_timelock_expires = tx_lock_status.wait_until(|status| {
|
||||||
tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock);
|
// Emit a tauri event on new confirmations
|
||||||
|
if let ScriptStatus::Confirmed(confirmed) = status {
|
||||||
|
event_emitter.emit_swap_progress_event(
|
||||||
|
swap_id,
|
||||||
|
TauriSwapProgressEvent::BtcLockTxInMempool {
|
||||||
|
btc_lock_txid: state3.tx_lock_id(),
|
||||||
|
btc_lock_confirmations: u64::from(confirmed.confirmations()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop when the cancel timelock expires
|
||||||
|
status.is_confirmed_with(state3.cancel_timelock)
|
||||||
|
});
|
||||||
|
|
||||||
select! {
|
select! {
|
||||||
// Alice sent us the transfer proof for the Monero she locked
|
// Alice sent us the transfer proof for the Monero she locked
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue