feat: swap history tauri connector

This commit is contained in:
binarybaron 2024-08-08 12:02:59 +02:00
parent cdd6635c8f
commit 2e1b6f6b43
No known key found for this signature in database
GPG Key ID: 99B75D3E1476A26E
22 changed files with 1315 additions and 1297 deletions

103
Cargo.lock generated
View File

@ -1601,15 +1601,6 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "encoding_rs"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
dependencies = [
"cfg-if",
]
[[package]]
name = "enum-as-inner"
version = "0.3.4"
@ -2763,25 +2754,6 @@ dependencies = [
"nom",
]
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "itertools"
version = "0.10.5"
@ -4089,17 +4061,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "open"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d2c909a3fce3bd80efef4cd1c6c056bd9376a8fe06fcfdbebaf32cb485a7e37"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "open-metrics-client"
version = "0.14.0"
@ -4135,16 +4096,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "os_pipe"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -5932,16 +5883,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shared_child"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "shell-words"
version = "1.1.0"
@ -6729,44 +6670,6 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-plugin"
version = "2.0.0-rc.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51a5c65ab8536a7e27b70ecbb0713ab42e8508acd9af1bc4a0817ccf7caf3165"
dependencies = [
"anyhow",
"glob",
"plist",
"schemars",
"serde",
"serde_json",
"tauri-utils",
"toml 0.8.2",
"walkdir",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.0.0-rc.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9209f6c32caec61e156a5616f7d80ba7683ca4a0a5641cbe5d3086ab371aaab2"
dependencies = [
"encoding_rs",
"log",
"open",
"os_pipe",
"regex",
"schemars",
"serde",
"serde_json",
"shared_child",
"tauri",
"tauri-plugin",
"thiserror",
"tokio",
]
[[package]]
name = "tauri-runtime"
version = "2.0.0-rc.1"
@ -7683,7 +7586,7 @@ dependencies = [
"swap",
"tauri",
"tauri-build",
"tauri-plugin-shell",
"uuid",
]
[[package]]
@ -7731,9 +7634,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uuid"
version = "1.9.1"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [
"getrandom 0.2.15",
"serde",

View File

@ -1,24 +1,24 @@
import { piconerosToXmr, satsToBtc } from 'utils/conversionUtils';
import { exhaustiveGuard } from 'utils/typescriptUtils';
import { piconerosToXmr, satsToBtc } from "utils/conversionUtils";
import { exhaustiveGuard } from "utils/typescriptUtils";
export enum RpcMethod {
GET_BTC_BALANCE = 'get_bitcoin_balance',
WITHDRAW_BTC = 'withdraw_btc',
BUY_XMR = 'buy_xmr',
RESUME_SWAP = 'resume_swap',
LIST_SELLERS = 'list_sellers',
CANCEL_REFUND_SWAP = 'cancel_refund_swap',
GET_SWAP_INFO = 'get_swap_info',
SUSPEND_CURRENT_SWAP = 'suspend_current_swap',
GET_HISTORY = 'get_history',
GET_MONERO_RECOVERY_KEYS = 'get_monero_recovery_info',
GET_BTC_BALANCE = "get_bitcoin_balance",
WITHDRAW_BTC = "withdraw_btc",
BUY_XMR = "buy_xmr",
RESUME_SWAP = "resume_swap",
LIST_SELLERS = "list_sellers",
CANCEL_REFUND_SWAP = "cancel_refund_swap",
GET_SWAP_INFO = "get_swap_info",
SUSPEND_CURRENT_SWAP = "suspend_current_swap",
GET_HISTORY = "get_history",
GET_MONERO_RECOVERY_KEYS = "get_monero_recovery_info",
}
export enum RpcProcessStateType {
STARTED = 'starting...',
LISTENING_FOR_CONNECTIONS = 'running',
EXITED = 'exited',
NOT_STARTED = 'not started',
STARTED = "starting...",
LISTENING_FOR_CONNECTIONS = "running",
EXITED = "exited",
NOT_STARTED = "not started",
}
export type RawRpcResponseSuccess<T> = {
@ -38,13 +38,13 @@ export type RawRpcResponse<T> = RawRpcResponseSuccess<T> | RawRpcResponseError;
export function isSuccessResponse<T>(
response: RawRpcResponse<T>,
): response is RawRpcResponseSuccess<T> {
return 'result' in response;
return "result" in response;
}
export function isErrorResponse<T>(
response: RawRpcResponse<T>,
): response is RawRpcResponseError {
return 'error' in response;
return "error" in response;
}
export interface RpcSellerStatus {
@ -56,7 +56,7 @@ export interface RpcSellerStatus {
max_quantity: number;
};
}
| 'Unreachable';
| "Unreachable";
multiaddr: string;
}
@ -80,7 +80,7 @@ export type SwapTimelockInfoCancelled = {
};
};
export type SwapTimelockInfoPunished = 'Punish';
export type SwapTimelockInfoPunished = "Punish";
export type SwapTimelockInfo =
| SwapTimelockInfoNone
@ -90,19 +90,19 @@ export type SwapTimelockInfo =
export function isSwapTimelockInfoNone(
info: SwapTimelockInfo,
): info is SwapTimelockInfoNone {
return typeof info === 'object' && 'None' in info;
return typeof info === "object" && "None" in info;
}
export function isSwapTimelockInfoCancelled(
info: SwapTimelockInfo,
): info is SwapTimelockInfoCancelled {
return typeof info === 'object' && 'Cancel' in info;
return typeof info === "object" && "Cancel" in info;
}
export function isSwapTimelockInfoPunished(
info: SwapTimelockInfo,
): info is SwapTimelockInfoPunished {
return info === 'Punish';
return info === "Punish";
}
export type SwapSellerInfo = {
@ -111,21 +111,21 @@ export type SwapSellerInfo = {
};
export interface GetSwapInfoResponse {
swapId: string;
swap_id: string;
completed: boolean;
seller: SwapSellerInfo;
startDate: string;
stateName: SwapStateName;
start_date: string;
state_name: SwapStateName;
timelock: null | SwapTimelockInfo;
txLockId: string;
txCancelFee: number;
txRefundFee: number;
txLockFee: number;
btcAmount: number;
xmrAmount: number;
btcRefundAddress: string;
cancelTimelock: number;
punishTimelock: number;
tx_lock_id: string;
tx_cancel_fee: number;
tx_refund_fee: number;
tx_lock_fee: number;
btc_amount: number;
xmr_amount: number;
btc_refund_address: string;
cancel_timelock: number;
punish_timelock: number;
}
export type MoneroRecoveryResponse = {
@ -144,19 +144,19 @@ export interface GetHistoryResponse {
}
export enum SwapStateName {
Started = 'quote has been requested',
SwapSetupCompleted = 'execution setup done',
BtcLocked = 'btc is locked',
XmrLockProofReceived = 'XMR lock transaction transfer proof received',
XmrLocked = 'xmr is locked',
EncSigSent = 'encrypted signature is sent',
BtcRedeemed = 'btc is redeemed',
CancelTimelockExpired = 'cancel timelock is expired',
BtcCancelled = 'btc is cancelled',
BtcRefunded = 'btc is refunded',
XmrRedeemed = 'xmr is redeemed',
BtcPunished = 'btc is punished',
SafelyAborted = 'safely aborted',
Started = "quote has been requested",
SwapSetupCompleted = "execution setup done",
BtcLocked = "btc is locked",
XmrLockProofReceived = "XMR lock transaction transfer proof received",
XmrLocked = "xmr is locked",
EncSigSent = "encrypted signature is sent",
BtcRedeemed = "btc is redeemed",
CancelTimelockExpired = "cancel timelock is expired",
BtcCancelled = "btc is cancelled",
BtcRefunded = "btc is refunded",
XmrRedeemed = "xmr is redeemed",
BtcPunished = "btc is punished",
SafelyAborted = "safely aborted",
}
export type SwapStateNameRunningSwap = Exclude<
@ -275,7 +275,7 @@ export function isSwapStateNamePossiblyRefundableSwap(
export function isGetSwapInfoResponseRunningSwap(
response: GetSwapInfoResponse,
): response is GetSwapInfoResponseRunningSwap {
return isSwapStateNameRunningSwap(response.stateName);
return isSwapStateNameRunningSwap(response.state_name);
}
export function isSwapMoneroRecoverable(swapStateName: SwapStateName): boolean {
@ -286,46 +286,46 @@ export function isSwapMoneroRecoverable(swapStateName: SwapStateName): boolean {
export function getHumanReadableDbStateType(type: SwapStateName): string {
switch (type) {
case SwapStateName.Started:
return 'Quote has been requested';
return "Quote has been requested";
case SwapStateName.SwapSetupCompleted:
return 'Swap has been initiated';
return "Swap has been initiated";
case SwapStateName.BtcLocked:
return 'Bitcoin has been locked';
return "Bitcoin has been locked";
case SwapStateName.XmrLockProofReceived:
return 'Monero lock transaction transfer proof has been received';
return "Monero lock transaction transfer proof has been received";
case SwapStateName.XmrLocked:
return 'Monero has been locked';
return "Monero has been locked";
case SwapStateName.EncSigSent:
return 'Encrypted signature has been sent';
return "Encrypted signature has been sent";
case SwapStateName.BtcRedeemed:
return 'Bitcoin has been redeemed';
return "Bitcoin has been redeemed";
case SwapStateName.CancelTimelockExpired:
return 'Cancel timelock has expired';
return "Cancel timelock has expired";
case SwapStateName.BtcCancelled:
return 'Swap has been cancelled';
return "Swap has been cancelled";
case SwapStateName.BtcRefunded:
return 'Bitcoin has been refunded';
return "Bitcoin has been refunded";
case SwapStateName.XmrRedeemed:
return 'Monero has been redeemed';
return "Monero has been redeemed";
case SwapStateName.BtcPunished:
return 'Bitcoin has been punished';
return "Bitcoin has been punished";
case SwapStateName.SafelyAborted:
return 'Swap has been safely aborted';
return "Swap has been safely aborted";
default:
return exhaustiveGuard(type);
}
}
export function getSwapTxFees(swap: GetSwapInfoResponse): number {
return satsToBtc(swap.txLockFee);
return satsToBtc(swap.tx_lock_fee);
}
export function getSwapBtcAmount(swap: GetSwapInfoResponse): number {
return satsToBtc(swap.btcAmount);
return satsToBtc(swap.btc_amount);
}
export function getSwapXmrAmount(swap: GetSwapInfoResponse): number {
return piconerosToXmr(swap.xmrAmount);
return piconerosToXmr(swap.xmr_amount);
}
export function getSwapExchangeRate(swap: GetSwapInfoResponse): number {

View File

@ -1,11 +1,11 @@
import { makeStyles } from '@material-ui/core';
import { Alert, AlertTitle } from '@material-ui/lab';
import { useActiveSwapInfo } from 'store/hooks';
import { makeStyles } from "@material-ui/core";
import { Alert, AlertTitle } from "@material-ui/lab";
import { useActiveSwapInfo } from "store/hooks";
import {
isSwapTimelockInfoCancelled,
isSwapTimelockInfoNone,
} from 'models/rpcModel';
import HumanizedBitcoinBlockDuration from '../other/HumanizedBitcoinBlockDuration';
} from "models/rpcModel";
import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
const useStyles = makeStyles((theme) => ({
outer: {
@ -33,31 +33,32 @@ export default function SwapMightBeCancelledAlert({
}
const { timelock } = swap;
const punishTimelockOffset = swap.punishTimelock;
const punishTimelockOffset = swap.punish_timelock;
return (
<Alert severity="warning" className={classes.outer} variant="filled">
<AlertTitle>Be careful!</AlertTitle>
The swap provider has taken a long time to lock their Monero. This might
mean that:
The swap provider has taken a long time to lock their Monero. This
might mean that:
<ul className={classes.list}>
<li>
There is a technical issue that prevents them from locking their funds
There is a technical issue that prevents them from locking
their funds
</li>
<li>They are a malicious actor (unlikely)</li>
</ul>
<br />
There is still hope for the swap to be successful but you have to be extra
careful. Regardless of why it has taken them so long, it is important that
you refund the swap within the required time period if the swap is not
completed. If you fail to to do so, you will be punished and lose your
money.
There is still hope for the swap to be successful but you have to be
extra careful. Regardless of why it has taken them so long, it is
important that you refund the swap within the required time period
if the swap is not completed. If you fail to to do so, you will be
punished and lose your money.
<ul className={classes.list}>
{isSwapTimelockInfoNone(timelock) && (
<>
<li>
<strong>
You will be able to refund in about{' '}
You will be able to refund in about{" "}
<HumanizedBitcoinBlockDuration
blocks={timelock.None.blocks_left}
/>
@ -66,9 +67,13 @@ export default function SwapMightBeCancelledAlert({
<li>
<strong>
If you have not refunded or completed the swap in about{' '}
If you have not refunded or completed the swap
in about{" "}
<HumanizedBitcoinBlockDuration
blocks={timelock.None.blocks_left + punishTimelockOffset}
blocks={
timelock.None.blocks_left +
punishTimelockOffset
}
/>
, you will lose your funds.
</strong>
@ -78,7 +83,8 @@ export default function SwapMightBeCancelledAlert({
{isSwapTimelockInfoCancelled(timelock) && (
<li>
<strong>
If you have not refunded or completed the swap in about{' '}
If you have not refunded or completed the swap in
about{" "}
<HumanizedBitcoinBlockDuration
blocks={timelock.Cancel.blocks_left}
/>
@ -88,8 +94,8 @@ export default function SwapMightBeCancelledAlert({
)}
<li>
As long as you see this screen, the swap will be refunded
automatically when the time comes. If this fails, you have to manually
refund by navigating to the History page.
automatically when the time comes. If this fails, you have
to manually refund by navigating to the History page.
</li>
</ul>
</Alert>

View File

@ -1,12 +1,12 @@
import { Alert, AlertTitle } from '@material-ui/lab/';
import { Box, makeStyles } from '@material-ui/core';
import { ReactNode } from 'react';
import { exhaustiveGuard } from 'utils/typescriptUtils';
import { Alert, AlertTitle } from "@material-ui/lab/";
import { Box, makeStyles } from "@material-ui/core";
import { ReactNode } from "react";
import { exhaustiveGuard } from "utils/typescriptUtils";
import {
SwapCancelRefundButton,
SwapResumeButton,
} from '../pages/history/table/HistoryRowActions';
import HumanizedBitcoinBlockDuration from '../other/HumanizedBitcoinBlockDuration';
} from "../pages/history/table/HistoryRowActions";
import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
import {
GetSwapInfoResponse,
GetSwapInfoResponseRunningSwap,
@ -17,18 +17,18 @@ import {
SwapStateName,
SwapTimelockInfoCancelled,
SwapTimelockInfoNone,
} from '../../../models/rpcModel';
import { SwapMoneroRecoveryButton } from '../pages/history/table/SwapMoneroRecoveryButton';
} from "../../../models/rpcModel";
import { SwapMoneroRecoveryButton } from "../pages/history/table/SwapMoneroRecoveryButton";
const useStyles = makeStyles({
box: {
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
display: "flex",
flexDirection: "column",
gap: "0.5rem",
},
list: {
padding: '0px',
margin: '0px',
padding: "0px",
margin: "0px",
},
});
@ -60,13 +60,17 @@ const BitcoinRedeemedStateAlert = ({ swap }: { swap: GetSwapInfoResponse }) => {
<Box className={classes.box}>
<MessageList
messages={[
'The Bitcoin has been redeemed by the other party',
'There is no risk of losing funds. You can take your time',
'The Monero will be automatically redeemed to the address you provided as soon as you resume the swap',
'If this step fails, you can manually redeem the funds',
"The Bitcoin has been redeemed by the other party",
"There is no risk of losing funds. You can take your time",
"The Monero will be automatically redeemed to the address you provided as soon as you resume the swap",
"If this step fails, you can manually redeem the funds",
]}
/>
<SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" />
<SwapMoneroRecoveryButton
swap={swap}
size="small"
variant="contained"
/>
</Box>
);
};
@ -87,13 +91,16 @@ const BitcoinLockedNoTimelockExpiredStateAlert = ({
<MessageList
messages={[
<>
Your Bitcoin is locked. If the swap is not completed in approximately{' '}
<HumanizedBitcoinBlockDuration blocks={timelock.None.blocks_left} />,
you need to refund
Your Bitcoin is locked. If the swap is not completed in
approximately{" "}
<HumanizedBitcoinBlockDuration
blocks={timelock.None.blocks_left}
/>
, you need to refund
</>,
<>
You will lose your funds if you do not refund or complete the swap
within{' '}
You will lose your funds if you do not refund or complete the
swap within{" "}
<HumanizedBitcoinBlockDuration
blocks={timelock.None.blocks_left + punishTimelockOffset}
/>
@ -121,17 +128,21 @@ const BitcoinPossiblyCancelledAlert = ({
<Box className={classes.box}>
<MessageList
messages={[
'The swap was cancelled because it did not complete in time',
'You must resume the swap immediately to refund your Bitcoin. If that fails, you can manually refund it',
"The swap was cancelled because it did not complete in time",
"You must resume the swap immediately to refund your Bitcoin. If that fails, you can manually refund it",
<>
You will lose your funds if you do not refund within{' '}
You will lose your funds if you do not refund within{" "}
<HumanizedBitcoinBlockDuration
blocks={timelock.Cancel.blocks_left}
/>
</>,
]}
/>
<SwapCancelRefundButton swap={swap} size="small" variant="contained" />
<SwapCancelRefundButton
swap={swap}
size="small"
variant="contained"
/>
</Box>
);
};
@ -154,7 +165,7 @@ function SwapAlertStatusText({
}: {
swap: GetSwapInfoResponseRunningSwap;
}) {
switch (swap.stateName) {
switch (swap.state_name) {
// This is the state where the swap is safe because the other party has redeemed the Bitcoin
// It cannot be punished anymore
case SwapStateName.BtcRedeemed:
@ -172,7 +183,7 @@ function SwapAlertStatusText({
if (isSwapTimelockInfoNone(swap.timelock)) {
return (
<BitcoinLockedNoTimelockExpiredStateAlert
punishTimelockOffset={swap.punishTimelock}
punishTimelockOffset={swap.punish_timelock}
timelock={swap.timelock}
/>
);
@ -197,7 +208,7 @@ function SwapAlertStatusText({
}
return <ImmediateActionAlert />;
default:
return exhaustiveGuard(swap.stateName);
return exhaustiveGuard(swap.state_name);
}
}
@ -219,13 +230,13 @@ export default function SwapStatusAlert({
return (
<Alert
key={swap.swapId}
key={swap.swap_id}
severity="warning"
action={<SwapResumeButton swap={swap} />}
variant="filled"
>
<AlertTitle>
Swap {swap.swapId.substring(0, 5)}... is unfinished
Swap {swap.swap_id.substring(0, 5)}... is unfinished
</AlertTitle>
<SwapAlertStatusText swap={swap} />
</Alert>

View File

@ -1,11 +1,11 @@
import { Box, makeStyles } from '@material-ui/core';
import { useSwapInfosSortedByDate } from 'store/hooks';
import SwapStatusAlert from './SwapStatusAlert';
import { Box, makeStyles } from "@material-ui/core";
import { useSwapInfosSortedByDate } from "store/hooks";
import SwapStatusAlert from "./SwapStatusAlert";
const useStyles = makeStyles((theme) => ({
outer: {
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
gap: theme.spacing(1),
},
}));
@ -21,7 +21,7 @@ export default function SwapTxLockAlertsBox() {
return (
<Box className={classes.outer}>
{swaps.map((swap) => (
<SwapStatusAlert key={swap.swapId} swap={swap} />
<SwapStatusAlert key={swap.swap_id} swap={swap} />
))}
</Box>
);

View File

@ -9,28 +9,25 @@ import {
MenuItem,
Select,
TextField,
} from '@material-ui/core';
import { useState } from 'react';
import { useSnackbar } from 'notistack';
import {
useActiveSwapInfo,
useAppSelector,
} from 'store/hooks';
import { parseDateString } from 'utils/parseUtils';
import { store } from 'renderer/store/storeRenderer';
import { CliLog } from 'models/cliModel';
import { submitFeedbackViaHttp } from '../../../api';
import { PiconeroAmount } from '../../other/Units';
import LoadingButton from '../../other/LoadingButton';
} from "@material-ui/core";
import { useState } from "react";
import { useSnackbar } from "notistack";
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import { parseDateString } from "utils/parseUtils";
import { store } from "renderer/store/storeRenderer";
import { CliLog } from "models/cliModel";
import { submitFeedbackViaHttp } from "../../../api";
import { PiconeroAmount } from "../../other/Units";
import LoadingButton from "../../other/LoadingButton";
async function submitFeedback(body: string, swapId: string | number) {
let attachedBody = '';
let attachedBody = "";
if (swapId !== 0 && typeof swapId === 'string') {
if (swapId !== 0 && typeof swapId === "string") {
const swapInfo = store.getState().rpc.state.swapInfos[swapId];
const logs = [] as CliLog[];
throw new Error('Not implemented');
throw new Error("Not implemented");
if (swapInfo === undefined) {
throw new Error(`Swap with id ${swapId} not found`);
@ -38,7 +35,7 @@ async function submitFeedback(body: string, swapId: string | number) {
attachedBody = `${JSON.stringify(swapInfo, null, 4)} \n\nLogs: ${logs
.map((l) => JSON.stringify(l))
.join('\n====\n')}`;
.join("\n====\n")}`;
}
await submitFeedbackViaHttp(body, attachedBody);
@ -70,10 +67,11 @@ function SwapSelectDropDown({
>
<MenuItem value={0}>Do not attach logs</MenuItem>
{swaps.map((swap) => (
<MenuItem value={swap.swapId}>
Swap {swap.swapId.substring(0, 5)}... from{' '}
{new Date(parseDateString(swap.startDate)).toDateString()} (
<PiconeroAmount amount={swap.xmrAmount} />)
<MenuItem value={swap.swap_id}>
Swap {swap.swap_id.substring(0, 5)}... from{" "}
{new Date(parseDateString(swap.start_date)).toDateString()}{" "}
(
<PiconeroAmount amount={swap.xmr_amount} />)
</MenuItem>
))}
</Select>
@ -90,14 +88,14 @@ export default function FeedbackDialog({
onClose: () => void;
}) {
const [pending, setPending] = useState(false);
const [bodyText, setBodyText] = useState('');
const [bodyText, setBodyText] = useState("");
const currentSwapId = useActiveSwapInfo();
const { enqueueSnackbar } = useSnackbar();
const [selectedAttachedSwap, setSelectedAttachedSwap] = useState<
string | number
>(currentSwapId?.swapId || 0);
>(currentSwapId?.swap_id || 0);
const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH;
@ -106,13 +104,19 @@ export default function FeedbackDialog({
<DialogTitle>Submit Feedback</DialogTitle>
<DialogContent>
<DialogContentText>
Got something to say? Drop us a message below. If you had an issue
with a specific swap, select it from the dropdown to attach the logs.
It will help us figure out what went wrong. Hit that submit button
when you are ready. We appreciate you taking the time to share your
thoughts!
Got something to say? Drop us a message below. If you had an
issue with a specific swap, select it from the dropdown to
attach the logs. It will help us figure out what went wrong.
Hit that submit button when you are ready. We appreciate you
taking the time to share your thoughts!
</DialogContentText>
<Box style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Box
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
}}
>
<TextField
variant="outlined"
value={bodyText}
@ -120,7 +124,7 @@ export default function FeedbackDialog({
label={
bodyTooLong
? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})`
: 'Feedback'
: "Feedback"
}
multiline
minRows={4}
@ -146,15 +150,24 @@ export default function FeedbackDialog({
try {
setPending(true);
await submitFeedback(bodyText, selectedAttachedSwap);
enqueueSnackbar('Feedback submitted successfully!', {
variant: 'success',
});
await submitFeedback(
bodyText,
selectedAttachedSwap,
);
enqueueSnackbar(
"Feedback submitted successfully!",
{
variant: "success",
},
);
} catch (e) {
console.error(`Failed to submit feedback: ${e}`);
enqueueSnackbar(`Failed to submit feedback (${e})`, {
variant: 'error',
});
enqueueSnackbar(
`Failed to submit feedback (${e})`,
{
variant: "error",
},
);
} finally {
setPending(false);
}

View File

@ -1,12 +1,12 @@
import { Step, StepLabel, Stepper, Typography } from '@material-ui/core';
import { SwapSpawnType } from 'models/cliModel';
import { SwapStateName } from 'models/rpcModel';
import { useActiveSwapInfo, useAppSelector } from 'store/hooks';
import { exhaustiveGuard } from 'utils/typescriptUtils';
import { Step, StepLabel, Stepper, Typography } from "@material-ui/core";
import { SwapSpawnType } from "models/cliModel";
import { SwapStateName } from "models/rpcModel";
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import { exhaustiveGuard } from "utils/typescriptUtils";
export enum PathType {
HAPPY_PATH = 'happy path',
UNHAPPY_PATH = 'unhappy path',
HAPPY_PATH = "happy path",
UNHAPPY_PATH = "unhappy path",
}
function getActiveStep(
@ -150,9 +150,12 @@ function UnhappyPathStepper({
export default function SwapStateStepper() {
const currentSwapSpawnType = useAppSelector((s) => s.swap.spawnType);
const stateName = useActiveSwapInfo()?.stateName ?? null;
const stateName = useActiveSwapInfo()?.state_name ?? null;
const processExited = useAppSelector((s) => !s.swap.processRunning);
const [pathType, activeStep, error] = getActiveStep(stateName, processExited);
const [pathType, activeStep, error] = getActiveStep(
stateName,
processExited,
);
// If the current swap is being manually cancelled and refund, we want to show the unhappy path even though the current state is not a "unhappy" state
if (currentSwapSpawnType === SwapSpawnType.CANCEL_REFUND) {

View File

@ -1,8 +1,8 @@
import { Box, DialogContentText } from '@material-ui/core';
import { SwapStateBtcRefunded } from 'models/storeModel';
import { useActiveSwapInfo } from 'store/hooks';
import BitcoinTransactionInfoBox from '../../BitcoinTransactionInfoBox';
import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox';
import { Box, DialogContentText } from "@material-ui/core";
import { SwapStateBtcRefunded } from "models/storeModel";
import { useActiveSwapInfo } from "store/hooks";
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
export default function BitcoinRefundedPage({
state,
@ -11,21 +11,22 @@ export default function BitcoinRefundedPage({
}) {
const swap = useActiveSwapInfo();
const additionalContent = swap
? `Refund address: ${swap.btcRefundAddress}`
? `Refund address: ${swap.btc_refund_address}`
: null;
return (
<Box>
<DialogContentText>
Unfortunately, the swap was not successful. However, rest assured that
all your Bitcoin has been refunded to the specified address. The swap
process is now complete, and you are free to exit the application.
Unfortunately, the swap was not successful. However, rest
assured that all your Bitcoin has been refunded to the specified
address. The swap process is now complete, and you are free to
exit the application.
</DialogContentText>
<Box
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
display: "flex",
flexDirection: "column",
gap: "0.5rem",
}}
>
{state && (

View File

@ -1,8 +1,8 @@
import { Box, DialogContentText } from '@material-ui/core';
import { useActiveSwapInfo, useAppSelector } from 'store/hooks';
import { SwapStateProcessExited } from 'models/storeModel';
import CliLogsBox from '../../../../other/RenderedCliLog';
import { SwapSpawnType } from 'models/cliModel';
import { Box, DialogContentText } from "@material-ui/core";
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import { SwapStateProcessExited } from "models/storeModel";
import CliLogsBox from "../../../../other/RenderedCliLog";
import { SwapSpawnType } from "models/cliModel";
export default function ProcessExitedAndNotDonePage({
state,
@ -22,30 +22,30 @@ export default function ProcessExitedAndNotDonePage({
messages.push(
isCancelRefund
? 'The manual cancel and refund was unsuccessful.'
: 'The swap exited unexpectedly without completing.',
? "The manual cancel and refund was unsuccessful."
: "The swap exited unexpectedly without completing.",
);
if (!hasSwap && !isCancelRefund) {
messages.push('No funds were locked.');
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.',
? "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.stateName}" state.`);
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.',
"Try resuming the swap or attempt to initiate a manual cancel and refund.",
);
}
}
return messages.join(' ');
return messages.join(" ");
}
return (
@ -53,9 +53,9 @@ export default function ProcessExitedAndNotDonePage({
<DialogContentText>{getText()}</DialogContentText>
<Box
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
display: "flex",
flexDirection: "column",
gap: "0.5rem",
}}
>
{state.rpcError && (

View File

@ -1,17 +1,17 @@
import { useActiveSwapInfo } from 'store/hooks';
import { SwapStateName } from 'models/rpcModel';
import { useActiveSwapInfo } from "store/hooks";
import { SwapStateName } from "models/rpcModel";
import {
isSwapStateBtcPunished,
isSwapStateBtcRefunded,
isSwapStateXmrRedeemInMempool,
SwapStateProcessExited,
} from '../../../../../../models/storeModel';
import XmrRedeemInMempoolPage from '../done/XmrRedeemInMempoolPage';
import BitcoinPunishedPage from '../done/BitcoinPunishedPage';
} from "../../../../../../models/storeModel";
import XmrRedeemInMempoolPage from "../done/XmrRedeemInMempoolPage";
import BitcoinPunishedPage from "../done/BitcoinPunishedPage";
// eslint-disable-next-line import/no-cycle
import SwapStatePage from '../SwapStatePage';
import BitcoinRefundedPage from '../done/BitcoinRefundedPage';
import ProcessExitedAndNotDonePage from './ProcessExitedAndNotDonePage';
import SwapStatePage from "../SwapStatePage";
import BitcoinRefundedPage from "../done/BitcoinRefundedPage";
import ProcessExitedAndNotDonePage from "./ProcessExitedAndNotDonePage";
type ProcessExitedPageProps = {
state: SwapStateProcessExited;
@ -31,13 +31,13 @@ export default function ProcessExitedPage({ state }: ProcessExitedPageProps) {
// If we don't have a swap state for a "done" state, we should fall back to using the database to display as much information as we can
if (swap) {
if (swap.stateName === SwapStateName.XmrRedeemed) {
if (swap.state_name === SwapStateName.XmrRedeemed) {
return <XmrRedeemInMempoolPage state={null} />;
}
if (swap.stateName === SwapStateName.BtcRefunded) {
if (swap.state_name === SwapStateName.BtcRefunded) {
return <BitcoinRefundedPage state={null} />;
}
if (swap.stateName === SwapStateName.BtcPunished) {
if (swap.state_name === SwapStateName.BtcPunished) {
return <BitcoinPunishedPage />;
}
}

View File

@ -5,20 +5,20 @@ import {
makeStyles,
TableCell,
TableRow,
} from '@material-ui/core';
import { useState } from 'react';
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
} from "@material-ui/core";
import { useState } from "react";
import ArrowForwardIcon from "@material-ui/icons/ArrowForward";
import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp";
import {
getHumanReadableDbStateType,
getSwapBtcAmount,
getSwapXmrAmount,
GetSwapInfoResponse,
} from '../../../../../models/rpcModel';
import HistoryRowActions from './HistoryRowActions';
import HistoryRowExpanded from './HistoryRowExpanded';
import { BitcoinAmount, MoneroAmount } from '../../../other/Units';
} from "../../../../../models/rpcModel";
import HistoryRowActions from "./HistoryRowActions";
import HistoryRowExpanded from "./HistoryRowExpanded";
import { BitcoinAmount, MoneroAmount } from "../../../other/Units";
type HistoryRowProps = {
swap: GetSwapInfoResponse;
@ -26,8 +26,8 @@ type HistoryRowProps = {
const useStyles = makeStyles((theme) => ({
amountTransferContainer: {
display: 'flex',
alignItems: 'center',
display: "flex",
alignItems: "center",
gap: theme.spacing(1),
},
}));
@ -60,15 +60,27 @@ export default function HistoryRow({ swap }: HistoryRowProps) {
<>
<TableRow>
<TableCell>
<IconButton size="small" onClick={() => setExpanded(!expanded)}>
{expanded ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
<IconButton
size="small"
onClick={() => setExpanded(!expanded)}
>
{expanded ? (
<KeyboardArrowUpIcon />
) : (
<KeyboardArrowDownIcon />
)}
</IconButton>
</TableCell>
<TableCell>{swap.swapId.substring(0, 5)}...</TableCell>
<TableCell>{swap.swap_id.substring(0, 5)}...</TableCell>
<TableCell>
<AmountTransfer xmrAmount={xmrAmount} btcAmount={btcAmount} />
<AmountTransfer
xmrAmount={xmrAmount}
btcAmount={btcAmount}
/>
</TableCell>
<TableCell>
{getHumanReadableDbStateType(swap.state_name)}
</TableCell>
<TableCell>{getHumanReadableDbStateType(swap.stateName)}</TableCell>
<TableCell>
<HistoryRowActions swap={swap} />
</TableCell>

View File

@ -1,16 +1,16 @@
import { Tooltip } from '@material-ui/core';
import Button, { ButtonProps } from '@material-ui/core/Button/Button';
import DoneIcon from '@material-ui/icons/Done';
import ErrorIcon from '@material-ui/icons/Error';
import { green, red } from '@material-ui/core/colors';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import IpcInvokeButton from '../../../IpcInvokeButton';
import { Tooltip } from "@material-ui/core";
import Button, { ButtonProps } from "@material-ui/core/Button/Button";
import DoneIcon from "@material-ui/icons/Done";
import ErrorIcon from "@material-ui/icons/Error";
import { green, red } from "@material-ui/core/colors";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import IpcInvokeButton from "../../../IpcInvokeButton";
import {
GetSwapInfoResponse,
SwapStateName,
isSwapStateNamePossiblyCancellableSwap,
isSwapStateNamePossiblyRefundableSwap,
} from '../../../../../models/rpcModel';
} from "../../../../../models/rpcModel";
export function SwapResumeButton({
swap,
@ -22,7 +22,7 @@ export function SwapResumeButton({
color="primary"
disabled={swap.completed}
ipcChannel="spawn-resume-swap"
ipcArgs={[swap.swapId]}
ipcArgs={[swap.swap_id]}
endIcon={<PlayArrowIcon />}
requiresRpc
{...props}
@ -37,8 +37,8 @@ export function SwapCancelRefundButton({
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
const cancelOrRefundable =
isSwapStateNamePossiblyCancellableSwap(swap.stateName) ||
isSwapStateNamePossiblyRefundableSwap(swap.stateName);
isSwapStateNamePossiblyCancellableSwap(swap.state_name) ||
isSwapStateNamePossiblyRefundableSwap(swap.state_name);
if (!cancelOrRefundable) {
return <></>;
@ -47,7 +47,7 @@ export function SwapCancelRefundButton({
return (
<IpcInvokeButton
ipcChannel="spawn-cancel-refund"
ipcArgs={[swap.swapId]}
ipcArgs={[swap.swap_id]}
requiresRpc
displayErrorSnackbar={false}
{...props}
@ -62,7 +62,7 @@ export default function HistoryRowActions({
}: {
swap: GetSwapInfoResponse;
}) {
if (swap.stateName === SwapStateName.XmrRedeemed) {
if (swap.state_name === SwapStateName.XmrRedeemed) {
return (
<Tooltip title="The swap is completed because you have redeemed the XMR">
<DoneIcon style={{ color: green[500] }} />
@ -70,7 +70,7 @@ export default function HistoryRowActions({
);
}
if (swap.stateName === SwapStateName.BtcRefunded) {
if (swap.state_name === SwapStateName.BtcRefunded) {
return (
<Tooltip title="The swap is completed because your BTC have been refunded">
<DoneIcon style={{ color: green[500] }} />
@ -78,7 +78,7 @@ export default function HistoryRowActions({
);
}
if (swap.stateName === SwapStateName.BtcPunished) {
if (swap.state_name === SwapStateName.BtcPunished) {
return (
<Tooltip title="The swap is completed because you have been punished">
<ErrorIcon style={{ color: red[500] }} />

View File

@ -7,9 +7,9 @@ import {
TableCell,
TableContainer,
TableRow,
} from '@material-ui/core';
import { getBitcoinTxExplorerUrl } from 'utils/conversionUtils';
import { isTestnet } from 'store/config';
} from "@material-ui/core";
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
import { isTestnet } from "store/config";
import {
getHumanReadableDbStateType,
getSwapBtcAmount,
@ -17,25 +17,25 @@ import {
getSwapTxFees,
getSwapXmrAmount,
GetSwapInfoResponse,
} from '../../../../../models/rpcModel';
import SwapLogFileOpenButton from './SwapLogFileOpenButton';
import { SwapCancelRefundButton } from './HistoryRowActions';
import { SwapMoneroRecoveryButton } from './SwapMoneroRecoveryButton';
} from "../../../../../models/rpcModel";
import SwapLogFileOpenButton from "./SwapLogFileOpenButton";
import { SwapCancelRefundButton } from "./HistoryRowActions";
import { SwapMoneroRecoveryButton } from "./SwapMoneroRecoveryButton";
import {
BitcoinAmount,
MoneroAmount,
MoneroBitcoinExchangeRate,
} from 'renderer/components/other/Units';
} from "renderer/components/other/Units";
const useStyles = makeStyles((theme) => ({
outer: {
display: 'grid',
display: "grid",
padding: theme.spacing(1),
gap: theme.spacing(1),
},
actionsOuter: {
display: 'flex',
flexDirection: 'row',
display: "flex",
flexDirection: "row",
gap: theme.spacing(1),
},
}));
@ -47,7 +47,7 @@ export default function HistoryRowExpanded({
}) {
const classes = useStyles();
const { seller, startDate } = swap;
const { seller, start_date: startDate } = swap;
const btcAmount = getSwapBtcAmount(swap);
const xmrAmount = getSwapXmrAmount(swap);
const txFees = getSwapTxFees(swap);
@ -64,12 +64,12 @@ export default function HistoryRowExpanded({
</TableRow>
<TableRow>
<TableCell>Swap ID</TableCell>
<TableCell>{swap.swapId}</TableCell>
<TableCell>{swap.swap_id}</TableCell>
</TableRow>
<TableRow>
<TableCell>State Name</TableCell>
<TableCell>
{getHumanReadableDbStateType(swap.stateName)}
{getHumanReadableDbStateType(swap.state_name)}
</TableCell>
</TableRow>
<TableRow>
@ -87,7 +87,9 @@ export default function HistoryRowExpanded({
<TableRow>
<TableCell>Exchange Rate</TableCell>
<TableCell>
<MoneroBitcoinExchangeRate rate={exchangeRate} />
<MoneroBitcoinExchangeRate
rate={exchangeRate}
/>
</TableCell>
</TableRow>
<TableRow>
@ -99,17 +101,20 @@ export default function HistoryRowExpanded({
<TableRow>
<TableCell>Provider Address</TableCell>
<TableCell>
<Box>{seller.addresses.join(', ')}</Box>
<Box>{seller.addresses.join(", ")}</Box>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Bitcoin lock transaction</TableCell>
<TableCell>
<Link
href={getBitcoinTxExplorerUrl(swap.txLockId, isTestnet())}
href={getBitcoinTxExplorerUrl(
swap.tx_lock_id,
isTestnet(),
)}
target="_blank"
>
{swap.txLockId}
{swap.tx_lock_id}
</Link>
</TableCell>
</TableRow>
@ -118,11 +123,15 @@ export default function HistoryRowExpanded({
</TableContainer>
<Box className={classes.actionsOuter}>
<SwapLogFileOpenButton
swapId={swap.swapId}
swapId={swap.swap_id}
variant="outlined"
size="small"
/>
<SwapCancelRefundButton swap={swap} variant="contained" size="small" />
<SwapCancelRefundButton
swap={swap}
variant="contained"
size="small"
/>
<SwapMoneroRecoveryButton
swap={swap}
variant="contained"

View File

@ -8,14 +8,14 @@ import {
TableContainer,
TableHead,
TableRow,
} from '@material-ui/core';
import { sortBy } from 'lodash';
import { parseDateString } from 'utils/parseUtils';
} from "@material-ui/core";
import { sortBy } from "lodash";
import { parseDateString } from "utils/parseUtils";
import {
useAppSelector,
useSwapInfosSortedByDate,
} from '../../../../../store/hooks';
import HistoryRow from './HistoryRow';
} from "../../../../../store/hooks";
import HistoryRow from "./HistoryRow";
const useStyles = makeStyles((theme) => ({
outer: {
@ -43,7 +43,7 @@ export default function HistoryTable() {
</TableHead>
<TableBody>
{swapSortedByDate.map((swap) => (
<HistoryRow swap={swap} key={swap.swapId} />
<HistoryRow swap={swap} key={swap.swap_id} />
))}
</TableBody>
</Table>

View File

@ -1,4 +1,4 @@
import { ButtonProps } from '@material-ui/core/Button/Button';
import { ButtonProps } from "@material-ui/core/Button/Button";
import {
Box,
Button,
@ -7,16 +7,16 @@ import {
DialogContent,
DialogContentText,
Link,
} from '@material-ui/core';
import { useAppDispatch, useAppSelector } from 'store/hooks';
import { rpcResetMoneroRecoveryKeys } from 'store/features/rpcSlice';
} from "@material-ui/core";
import { useAppDispatch, useAppSelector } from "store/hooks";
import { rpcResetMoneroRecoveryKeys } from "store/features/rpcSlice";
import {
GetSwapInfoResponse,
isSwapMoneroRecoverable,
} from '../../../../../models/rpcModel';
import IpcInvokeButton from '../../../IpcInvokeButton';
import DialogHeader from '../../../modal/DialogHeader';
import ScrollablePaperTextBox from '../../../other/ScrollablePaperTextBox';
} from "../../../../../models/rpcModel";
import IpcInvokeButton from "../../../IpcInvokeButton";
import DialogHeader from "../../../modal/DialogHeader";
import ScrollablePaperTextBox from "../../../other/ScrollablePaperTextBox";
function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) {
const dispatch = useAppDispatch();
@ -26,54 +26,55 @@ function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) {
dispatch(rpcResetMoneroRecoveryKeys());
}
if (keys === null || keys.swapId !== swap.swapId) {
if (keys === null || keys.swapId !== swap.swap_id) {
return <></>;
}
return (
<Dialog open onClose={onClose} maxWidth="sm" fullWidth>
<DialogHeader
title={`Recovery Keys for swap ${swap.swapId.substring(0, 5)}...`}
title={`Recovery Keys for swap ${swap.swap_id.substring(0, 5)}...`}
/>
<DialogContent>
<DialogContentText>
You can use the keys below to manually redeem the Monero funds from
the multi-signature wallet.
You can use the keys below to manually redeem the Monero
funds from the multi-signature wallet.
<ul>
<li>
This is useful if the swap daemon fails to redeem the funds itself
This is useful if the swap daemon fails to redeem
the funds itself
</li>
<li>
If you have come this far, there is no risk of losing funds. You
are the only one with access to these keys and can use them to
access your funds
If you have come this far, there is no risk of
losing funds. You are the only one with access to
these keys and can use them to access your funds
</li>
<li>
View{' '}
View{" "}
<Link
href="https://www.getmonero.org/resources/user-guides/restore_from_keys.html"
target="_blank"
rel="noreferrer"
>
this guide
</Link>{' '}
for a detailed description on how to import the keys and spend the
funds.
</Link>{" "}
for a detailed description on how to import the keys
and spend the funds.
</li>
</ul>
</DialogContentText>
<Box
style={{
display: 'flex',
gap: '0.5rem',
flexDirection: 'column',
display: "flex",
gap: "0.5rem",
flexDirection: "column",
}}
>
{[
['Primary Address', keys.keys.address],
['View Key', keys.keys.view_key],
['Spend Key', keys.keys.spend_key],
['Restore Height', keys.keys.restore_height.toString()],
["Primary Address", keys.keys.address],
["View Key", keys.keys.view_key],
["Spend Key", keys.keys.spend_key],
["Restore Height", keys.keys.restore_height.toString()],
].map(([title, value]) => (
<ScrollablePaperTextBox
minHeight="2rem"
@ -97,7 +98,7 @@ export function SwapMoneroRecoveryButton({
swap,
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
const isRecoverable = isSwapMoneroRecoverable(swap.stateName);
const isRecoverable = isSwapMoneroRecoverable(swap.state_name);
if (!isRecoverable) {
return <></>;
@ -107,7 +108,7 @@ export function SwapMoneroRecoveryButton({
<>
<IpcInvokeButton
ipcChannel="spawn-monero-recovery"
ipcArgs={[swap.swapId]}
ipcArgs={[swap.swap_id]}
requiresRpc
{...props}
>

View File

@ -1,7 +1,7 @@
import { invoke } from "@tauri-apps/api/core";
import { BalanceBitcoinResponse } from "models/rpcModel";
import { store } from "./store/storeRenderer";
import { rpcSetBalance } from "store/features/rpcSlice";
import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice";
export async function checkBitcoinBalance() {
// TODO: use tauri-bindgen here
@ -13,6 +13,7 @@ export async function checkBitcoinBalance() {
}
export async function getRawSwapInfos() {
const response = await invoke("swap_infos");
const response = await invoke("swap_infos_all");
console.log(response);
(response as any[]).forEach((info) => store.dispatch(rpcSetSwapInfo(info)));
}

View File

@ -1,11 +1,11 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ExtendedProviderStatus, ProviderStatus } from 'models/apiModel';
import { MoneroWalletRpcUpdateState } from 'models/storeModel';
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
import { MoneroWalletRpcUpdateState } from "models/storeModel";
import {
GetSwapInfoResponse,
MoneroRecoveryResponse,
RpcProcessStateType,
} from '../../models/rpcModel';
} from "../../models/rpcModel";
import {
CliLog,
isCliLog,
@ -14,8 +14,8 @@ import {
isCliLogFinishedSyncingMoneroWallet,
isCliLogStartedRpcServer,
isCliLogStartedSyncingMoneroWallet,
} from '../../models/cliModel';
import { getLogsAndStringsFromRawFileString } from 'utils/parseUtils';
} from "../../models/cliModel";
import { getLogsAndStringsFromRawFileString } from "utils/parseUtils";
type Process =
| {
@ -82,7 +82,7 @@ const initialState: RPCSlice = {
};
export const rpcSlice = createSlice({
name: 'rpc',
name: "rpc",
initialState,
reducers: {
rpcAddLogs(slice, action: PayloadAction<(CliLog | string)[]>) {
@ -110,7 +110,7 @@ export const rpcSlice = createSlice({
downloadUrl: log.fields.download_url,
};
if (log.fields.progress === '100%') {
if (log.fields.progress === "100%") {
slice.state.moneroWalletRpc.updateState = false;
}
} else if (isCliLogStartedSyncingMoneroWallet(log)) {
@ -169,7 +169,7 @@ export const rpcSlice = createSlice({
slice.state.withdrawTxId = null;
},
rpcSetSwapInfo(slice, action: PayloadAction<GetSwapInfoResponse>) {
slice.state.swapInfos[action.payload.swapId] = action.payload;
slice.state.swapInfos[action.payload.swap_id] = action.payload;
},
rpcSetEndpointBusy(slice, action: PayloadAction<string>) {
if (!slice.busyEndpoints.includes(action.payload)) {

View File

@ -1,7 +1,7 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from 'renderer/store/storeRenderer';
import { sortBy } from 'lodash';
import { parseDateString } from 'utils/parseUtils';
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
import { sortBy } from "lodash";
import { parseDateString } from "utils/parseUtils";
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
@ -22,7 +22,7 @@ export function useIsSwapRunning() {
export function useSwapInfo(swapId: string | null) {
return useAppSelector((state) =>
swapId ? state.rpc.state.swapInfos[swapId] ?? null : null,
swapId ? (state.rpc.state.swapInfos[swapId] ?? null) : null,
);
}
@ -51,6 +51,6 @@ export function useSwapInfosSortedByDate() {
const swapInfos = useAppSelector((state) => state.rpc.state.swapInfos);
return sortBy(
Object.values(swapInfos),
(swap) => -parseDateString(swap.startDate),
(swap) => -parseDateString(swap.start_date),
);
}

View File

@ -21,4 +21,4 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
swap = { path = "../swap" }
tauri = { version = "2.0.0-rc.1", features = ["config-json5"] }
tauri-plugin-shell = "2.0.0-rc.0"
uuid = "1.10.0"

View File

@ -3,5 +3,5 @@
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": ["shell:allow-open"]
"permissions": []
}

View File

@ -3,11 +3,15 @@ use std::sync::Arc;
use once_cell::sync::OnceCell;
use swap::{
api::{
request::{get_balance, BalanceArgs, BalanceResponse},
request::{
get_balance, get_swap_info, get_swap_infos_all, BalanceArgs, BalanceResponse,
GetSwapInfoResponse,
},
Context,
},
cli::command::{Bitcoin, Monero},
};
use uuid::Uuid;
// Lazy load the Context
static CONTEXT: OnceCell<Arc<Context>> = OnceCell::new();
@ -26,6 +30,15 @@ async fn balance() -> Result<BalanceResponse, String> {
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn swap_infos_all() -> Result<Vec<GetSwapInfoResponse>, String> {
let context = CONTEXT.get().unwrap();
get_swap_infos_all(context.clone())
.await
.map_err(|e| e.to_string())
}
fn setup<'a>(app: &'a mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
tauri::async_runtime::block_on(async {
let context = Context::build(
@ -56,8 +69,7 @@ fn setup<'a>(app: &'a mut tauri::App) -> Result<(), Box<dyn std::error::Error>>
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![balance])
.invoke_handler(tauri::generate_handler![balance, swap_infos_all])
.setup(setup)
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -7,6 +7,7 @@ use crate::network::swarm;
use crate::protocol::bob::{BobState, Swap};
use crate::protocol::{bob, State};
use crate::{bitcoin, cli, monero, rpc};
use ::bitcoin::Txid;
use anyhow::{bail, Context as AnyContext, Result};
use libp2p::core::Multiaddr;
use qrcode::render::unicode;
@ -52,22 +53,6 @@ pub struct MoneroRecoveryArgs {
pub swap_id: Uuid,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ResumeSwapResponse {
pub result: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BalanceResponse {
pub balance: u64, // in satoshis
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BuyXmrResponse {
pub swap_id: String,
pub quote: BidQuote, // You'll need to import or define BidQuote
}
#[derive(Debug, Eq, PartialEq)]
pub struct WithdrawBtcArgs {
pub amount: Option<Amount>,
@ -94,6 +79,52 @@ pub struct GetSwapInfoArgs {
pub swap_id: Uuid,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ResumeSwapResponse {
pub result: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BalanceResponse {
pub balance: u64, // in satoshis
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BuyXmrResponse {
pub swap_id: String,
pub quote: BidQuote, // You'll need to import or define BidQuote
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetHistoryResponse {
swaps: Vec<(Uuid, String)>,
}
#[derive(Serialize)]
pub struct GetSwapInfoResponse {
pub swap_id: Uuid,
pub seller: Seller,
pub completed: bool,
pub start_date: String,
pub state_name: String,
pub xmr_amount: u64,
pub btc_amount: u64,
pub tx_lock_id: Txid,
pub tx_cancel_fee: u64,
pub tx_refund_fee: u64,
pub tx_lock_fee: u64,
pub btc_refund_address: String,
pub cancel_timelock: u32,
pub punish_timelock: u32,
pub timelock: Option<ExpiredTimelocks>,
}
#[derive(Serialize, Deserialize)]
pub struct Seller {
pub peer_id: String,
pub addresses: Vec<Multiaddr>,
}
// TODO: We probably dont even need this.
// We can just call the method directly from the RPC server, the CLI and the Tauri connector
#[derive(Debug, PartialEq)]
@ -231,8 +262,23 @@ async fn suspend_current_swap(context: Arc<Context>) -> Result<serde_json::Value
}
}
pub async fn get_swap_infos_all(context: Arc<Context>) -> Result<Vec<GetSwapInfoResponse>> {
let swap_ids = context.db.all().await?;
let mut swap_infos = Vec::new();
for (swap_id, _) in swap_ids {
let swap_info = get_swap_info(GetSwapInfoArgs { swap_id }, context.clone()).await?;
swap_infos.push(swap_info);
}
Ok(swap_infos)
}
// #[tracing::instrument(fields(method="get_swap_info", swap_id = args.swap_id), skip(context))]
async fn get_swap_info(args: GetSwapInfoArgs, context: Arc<Context>) -> Result<serde_json::Value> {
pub async fn get_swap_info(
args: GetSwapInfoArgs,
context: Arc<Context>,
) -> Result<GetSwapInfoResponse> {
let bitcoin_wallet = context
.bitcoin_wallet
.as_ref()
@ -311,38 +357,38 @@ async fn get_swap_info(args: GetSwapInfoArgs, context: Arc<Context>) -> Result<s
}
BobState::BtcLocked { state3: state, .. }
| BobState::XmrLockProofReceived { state, .. } => {
Some(state.expired_timelock(bitcoin_wallet).await)
Some(state.expired_timelock(bitcoin_wallet).await?)
}
BobState::XmrLocked(state) | BobState::EncSigSent(state) => {
Some(state.expired_timelock(bitcoin_wallet).await)
Some(state.expired_timelock(bitcoin_wallet).await?)
}
BobState::CancelTimelockExpired(state) | BobState::BtcCancelled(state) => {
Some(state.expired_timelock(bitcoin_wallet).await)
Some(state.expired_timelock(bitcoin_wallet).await?)
}
BobState::BtcPunished { .. } => Some(Ok(ExpiredTimelocks::Punish)),
BobState::BtcPunished { .. } => Some(ExpiredTimelocks::Punish),
BobState::BtcRefunded(_) | BobState::BtcRedeemed(_) | BobState::XmrRedeemed { .. } => None,
};
Ok(json!({
"swapId": args.swap_id,
"seller": {
"peerId": peerId.to_string(),
"addresses": addresses
Ok(GetSwapInfoResponse {
swap_id: args.swap_id,
seller: Seller {
peer_id: peerId.to_string(),
addresses,
},
"completed": is_completed,
"startDate": start_date,
"stateName": state_name,
"xmrAmount": xmr_amount,
"btcAmount": btc_amount,
"txLockId": tx_lock_id,
"txCancelFee": tx_cancel_fee,
"txRefundFee": tx_refund_fee,
"txLockFee": tx_lock_fee,
"btcRefundAddress": btc_refund_address.to_string(),
"cancelTimelock": cancel_timelock,
"punishTimelock": punish_timelock,
"timelock": timelock.map(|tl| tl.map(|tl| json!(tl)).unwrap_or(json!(null))).unwrap_or(json!(null)),
}))
completed: is_completed,
start_date,
state_name,
xmr_amount: xmr_amount.as_piconero(),
btc_amount,
tx_lock_id,
tx_cancel_fee,
tx_refund_fee,
tx_lock_fee,
btc_refund_address: btc_refund_address.to_string(),
cancel_timelock: cancel_timelock.into(),
punish_timelock: punish_timelock.into(),
timelock,
})
}
async fn buy_xmr(buy_xmr: BuyXmrArgs, context: Arc<Context>) -> Result<serde_json::Value> {
@ -653,7 +699,7 @@ async fn cancel_and_refund(
})
}
async fn get_history(context: Arc<Context>) -> Result<serde_json::Value> {
async fn get_history(context: Arc<Context>) -> Result<GetHistoryResponse> {
let swaps = context.db.all().await?;
let mut vec: Vec<(Uuid, String)> = Vec::new();
for (swap_id, state) in swaps {
@ -661,7 +707,7 @@ async fn get_history(context: Arc<Context>) -> Result<serde_json::Value> {
vec.push((swap_id, state.to_string()));
}
Ok(json!({ "swaps": vec }))
Ok(GetHistoryResponse { swaps: vec })
}
async fn get_raw_states(context: Arc<Context>) -> Result<serde_json::Value> {