Merge pull request #10 from UnstoppableSwap/tauri-events

feat: Send progress events from Host to Guest
This commit is contained in:
binarybaron 2024-08-26 16:14:29 +02:00 committed by GitHub
commit e3547fbec2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
125 changed files with 4827 additions and 2653 deletions

23
Cargo.lock generated
View File

@ -2252,7 +2252,7 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro-crate 1.1.3",
"proc-macro-error",
"proc-macro2",
"quote",
@ -3761,7 +3761,7 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "424f6e86263cd5294cbd7f1e95746b95aca0e0d66bff31e5a40d6baa87b4aa99"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro-crate 1.1.3",
"proc-macro-error",
"proc-macro2",
"quote",
@ -3897,7 +3897,7 @@ version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro-crate 1.1.3",
"proc-macro2",
"quote",
"syn 1.0.109",
@ -4550,12 +4550,12 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a"
dependencies = [
"once_cell",
"toml_edit 0.19.15",
"thiserror",
"toml 0.5.11",
]
[[package]]
@ -7054,6 +7054,15 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml"
version = "0.7.8"

View File

@ -7,5 +7,3 @@ This is the monorepo containing the source code for all of our core projects:
- and other crates we use in our binaries
If you're just here for the software, head over to the [releases](https://github.com/UnstoppableSwap/xmr-btc-swap/releases/latest) tab and grab the binary for your operating system! If you're just looking for documentation, check out our [docs page](https://docs.unstoppableswap.net/) or our [github docs](docs/README.md).
If you are looking for help or encountered an issue, feel free to open an issue.

View File

@ -13,6 +13,7 @@
"plugins": [
"https://plugins.dprint.dev/markdown-0.13.1.wasm",
"https://github.com/thomaseizinger/dprint-plugin-cargo-toml/releases/download/0.1.0/cargo-toml-0.1.0.wasm",
"https://plugins.dprint.dev/exec-0.3.5.json@d687dda57be0fe9a0088ccdaefa5147649ff24127d8b3ea227536c68ee7abeab"
"https://plugins.dprint.dev/exec-0.3.5.json@d687dda57be0fe9a0088ccdaefa5147649ff24127d8b3ea227536c68ee7abeab",
"https://plugins.dprint.dev/prettier-0.26.6.json@0118376786f37496e41bb19dbcfd1e7214e2dc859a55035c5e54d1107b4c9c57"
]
}

View File

@ -1,4 +1,4 @@
[toolchain]
channel = "1.74" # also update this in the readme, changelog, and github actions
channel = "1.80" # also update this in the readme, changelog, and github actions
components = ["clippy"]
targets = ["armv7-unknown-linux-gnueabihf"]

View File

@ -5,3 +5,9 @@ This template should help get you started developing with Tauri, React and Types
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
## Generate bindings for Tauri API
```bash
typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src
```

20
src-gui/eslint.config.js Normal file
View File

@ -0,0 +1,20 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
export default [
{
ignores: ["node_modules", "dist"],
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
{
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
languageOptions: { globals: globals.browser },
rules: {
"react/react-in-jsx-scope": "off",
},
},
];

View File

@ -14,8 +14,7 @@
"@material-ui/icons": "^4.11.3",
"@material-ui/lab": "^4.0.0-alpha.61",
"@reduxjs/toolkit": "^2.2.6",
"@tauri-apps/api": ">=2.0.0-beta.0",
"@tauri-apps/plugin-shell": ">=2.0.0-beta.0",
"@tauri-apps/api": "2.0.0-rc.1",
"humanize-duration": "^3.32.1",
"lodash": "^4.17.21",
"multiaddr": "^10.0.1",
@ -31,6 +30,7 @@
"virtua": "^0.33.2"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@tauri-apps/cli": ">=2.0.0-beta.0",
"@types/humanize-duration": "^3.27.4",
"@types/lodash": "^4.17.6",
@ -39,8 +39,12 @@
"@types/react-dom": "^18.2.7",
"@types/semver": "^7.5.8",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^9.9.0",
"eslint-plugin-react": "^7.35.0",
"globals": "^15.9.0",
"internal-ip": "^7.0.0",
"typescript": "^5.2.2",
"typescript-eslint": "^8.1.0",
"vite": "^5.3.1",
"vite-tsconfig-paths": "^4.3.2"
}

View File

@ -18,396 +18,3 @@ export interface CliLog {
[index: string]: unknown;
}[];
}
export function isCliLog(log: unknown): log is CliLog {
if (log && typeof log === "object") {
return (
"timestamp" in (log as CliLog) &&
"level" in (log as CliLog) &&
"fields" in (log as CliLog) &&
typeof (log as CliLog).fields?.message === "string"
);
}
return false;
}
export interface CliLogStartedRpcServer extends CliLog {
fields: {
message: "Started RPC server";
addr: string;
};
}
export function isCliLogStartedRpcServer(
log: CliLog,
): log is CliLogStartedRpcServer {
return log.fields.message === "Started RPC server";
}
export interface CliLogReleasingSwapLockLog extends CliLog {
fields: {
message: "Releasing swap lock";
swap_id: string;
};
}
export function isCliLogReleasingSwapLockLog(
log: CliLog,
): log is CliLogReleasingSwapLockLog {
return log.fields.message === "Releasing swap lock";
}
export interface CliLogApiCallError extends CliLog {
fields: {
message: "API call resulted in an error";
err: string;
};
}
export function isCliLogApiCallError(log: CliLog): log is CliLogApiCallError {
return log.fields.message === "API call resulted in an error";
}
export interface CliLogAcquiringSwapLockLog extends CliLog {
fields: {
message: "Acquiring swap lock";
swap_id: string;
};
}
export function isCliLogAcquiringSwapLockLog(
log: CliLog,
): log is CliLogAcquiringSwapLockLog {
return log.fields.message === "Acquiring swap lock";
}
export interface CliLogReceivedQuote extends CliLog {
fields: {
message: "Received quote";
price: string;
minimum_amount: string;
maximum_amount: string;
};
}
export function isCliLogReceivedQuote(log: CliLog): log is CliLogReceivedQuote {
return log.fields.message === "Received quote";
}
export interface CliLogWaitingForBtcDeposit extends CliLog {
fields: {
message: "Waiting for Bitcoin deposit";
deposit_address: string;
min_deposit_until_swap_will_start: string;
max_deposit_until_maximum_amount_is_reached: string;
max_giveable: string;
minimum_amount: string;
maximum_amount: string;
min_bitcoin_lock_tx_fee: string;
price: string;
};
}
export function isCliLogWaitingForBtcDeposit(
log: CliLog,
): log is CliLogWaitingForBtcDeposit {
return log.fields.message === "Waiting for Bitcoin deposit";
}
export interface CliLogReceivedBtc extends CliLog {
fields: {
message: "Received Bitcoin";
max_giveable: string;
new_balance: string;
};
}
export function isCliLogReceivedBtc(log: CliLog): log is CliLogReceivedBtc {
return log.fields.message === "Received Bitcoin";
}
export interface CliLogDeterminedSwapAmount extends CliLog {
fields: {
message: "Determined swap amount";
amount: string;
fees: string;
};
}
export function isCliLogDeterminedSwapAmount(
log: CliLog,
): log is CliLogDeterminedSwapAmount {
return log.fields.message === "Determined swap amount";
}
export interface CliLogStartedSwap extends CliLog {
fields: {
message: "Starting new swap";
swap_id: string;
};
}
export function isCliLogStartedSwap(log: CliLog): log is CliLogStartedSwap {
return log.fields.message === "Starting new swap";
}
export interface CliLogPublishedBtcTx extends CliLog {
fields: {
message: "Published Bitcoin transaction";
txid: string;
kind: "lock" | "cancel" | "withdraw" | "refund";
};
}
export function isCliLogPublishedBtcTx(
log: CliLog,
): log is CliLogPublishedBtcTx {
return log.fields.message === "Published Bitcoin transaction";
}
export interface CliLogBtcTxFound extends CliLog {
fields: {
message: "Found relevant Bitcoin transaction";
txid: string;
status: string;
};
}
export function isCliLogBtcTxFound(log: CliLog): log is CliLogBtcTxFound {
return log.fields.message === "Found relevant Bitcoin transaction";
}
export interface CliLogBtcTxStatusChanged extends CliLog {
fields: {
message: "Bitcoin transaction status changed";
txid: string;
new_status: string;
};
}
export function isCliLogBtcTxStatusChanged(
log: CliLog,
): log is CliLogBtcTxStatusChanged {
return log.fields.message === "Bitcoin transaction status changed";
}
export interface CliLogAliceLockedXmr extends CliLog {
fields: {
message: "Alice locked Monero";
txid: string;
};
}
export function isCliLogAliceLockedXmr(
log: CliLog,
): log is CliLogAliceLockedXmr {
return log.fields.message === "Alice locked Monero";
}
export interface CliLogReceivedXmrLockTxConfirmation extends CliLog {
fields: {
message: "Received new confirmation for Monero lock tx";
txid: string;
seen_confirmations: string;
needed_confirmations: string;
};
}
export function isCliLogReceivedXmrLockTxConfirmation(
log: CliLog,
): log is CliLogReceivedXmrLockTxConfirmation {
return log.fields.message === "Received new confirmation for Monero lock tx";
}
export interface CliLogAdvancingState extends CliLog {
fields: {
message: "Advancing state";
state:
| "quote has been requested"
| "execution setup done"
| "btc is locked"
| "XMR lock transaction transfer proof received"
| "xmr is locked"
| "encrypted signature is sent"
| "btc is redeemed"
| "cancel timelock is expired"
| "btc is cancelled"
| "btc is refunded"
| "xmr is redeemed"
| "btc is punished"
| "safely aborted";
};
}
export function isCliLogAdvancingState(
log: CliLog,
): log is CliLogAdvancingState {
return log.fields.message === "Advancing state";
}
export interface CliLogRedeemedXmr extends CliLog {
fields: {
message: "Successfully transferred XMR to wallet";
monero_receive_address: string;
txid: string;
};
}
export function isCliLogRedeemedXmr(log: CliLog): log is CliLogRedeemedXmr {
return log.fields.message === "Successfully transferred XMR to wallet";
}
export interface YouHaveBeenPunishedCliLog extends CliLog {
fields: {
message: "You have been punished for not refunding in time";
};
}
export function isYouHaveBeenPunishedCliLog(
log: CliLog,
): log is YouHaveBeenPunishedCliLog {
return (
log.fields.message === "You have been punished for not refunding in time"
);
}
function getCliLogSpanAttribute<T>(log: CliLog, key: string): T | null {
const span = log.spans?.find((s) => s[key]);
if (!span) {
return null;
}
return span[key] as T;
}
export function getCliLogSpanSwapId(log: CliLog): string | null {
return getCliLogSpanAttribute<string>(log, "swap_id");
}
export function getCliLogSpanLogReferenceId(log: CliLog): string | null {
return (
getCliLogSpanAttribute<string>(log, "log_reference_id")?.replace(
/"/g,
"",
) || null
);
}
export function hasCliLogOneOfMultipleSpans(
log: CliLog,
spanNames: string[],
): boolean {
return log.spans?.some((s) => spanNames.includes(s.name)) ?? false;
}
export interface CliLogStartedSyncingMoneroWallet extends CliLog {
fields: {
message: "Syncing Monero wallet";
current_sync_height?: boolean;
};
}
export function isCliLogStartedSyncingMoneroWallet(
log: CliLog,
): log is CliLogStartedSyncingMoneroWallet {
return log.fields.message === "Syncing Monero wallet";
}
export interface CliLogFinishedSyncingMoneroWallet extends CliLog {
fields: {
message: "Synced Monero wallet";
};
}
export interface CliLogFailedToSyncMoneroWallet extends CliLog {
fields: {
message: "Failed to sync Monero wallet";
error: string;
};
}
export function isCliLogFailedToSyncMoneroWallet(
log: CliLog,
): log is CliLogFailedToSyncMoneroWallet {
return log.fields.message === "Failed to sync Monero wallet";
}
export function isCliLogFinishedSyncingMoneroWallet(
log: CliLog,
): log is CliLogFinishedSyncingMoneroWallet {
return log.fields.message === "Monero wallet synced";
}
export interface CliLogDownloadingMoneroWalletRpc extends CliLog {
fields: {
message: "Downloading monero-wallet-rpc";
progress: string;
size: string;
download_url: string;
};
}
export function isCliLogDownloadingMoneroWalletRpc(
log: CliLog,
): log is CliLogDownloadingMoneroWalletRpc {
return log.fields.message === "Downloading monero-wallet-rpc";
}
export interface CliLogStartedSyncingMoneroWallet extends CliLog {
fields: {
message: "Syncing Monero wallet";
current_sync_height?: boolean;
};
}
export interface CliLogDownloadingMoneroWalletRpc extends CliLog {
fields: {
message: "Downloading monero-wallet-rpc";
progress: string;
size: string;
download_url: string;
};
}
export interface CliLogGotNotificationForNewBlock extends CliLog {
fields: {
message: "Got notification for new block";
block_height: string;
};
}
export function isCliLogGotNotificationForNewBlock(
log: CliLog,
): log is CliLogGotNotificationForNewBlock {
return log.fields.message === "Got notification for new block";
}
export interface CliLogAttemptingToCooperativelyRedeemXmr extends CliLog {
fields: {
message: "Attempting to cooperatively redeem XMR after being punished";
};
}
export function isCliLogAttemptingToCooperativelyRedeemXmr(
log: CliLog,
): log is CliLogAttemptingToCooperativelyRedeemXmr {
return (
log.fields.message ===
"Attempting to cooperatively redeem XMR after being punished"
);
}
export interface CliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr
extends CliLog {
fields: {
message: "Alice has accepted our request to cooperatively redeem the XMR";
};
}
export function isCliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr(
log: CliLog,
): log is CliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr {
return (
log.fields.message ===
"Alice has accepted our request to cooperatively redeem the XMR"
);
}

View File

@ -1,4 +0,0 @@
export interface Binary {
dirPath: string; // Path without filename appended
fileName: string;
}

View File

@ -1,6 +1,3 @@
import { piconerosToXmr, satsToBtc } from "utils/conversionUtils";
import { exhaustiveGuard } from "utils/typescriptUtils";
export enum RpcMethod {
GET_BTC_BALANCE = "get_bitcoin_balance",
WITHDRAW_BTC = "withdraw_btc",
@ -110,227 +107,9 @@ export type SwapSellerInfo = {
addresses: string[];
};
export interface GetSwapInfoResponse {
swap_id: string;
completed: boolean;
seller: SwapSellerInfo;
start_date: string;
state_name: SwapStateName;
timelock: null | SwapTimelockInfo;
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 = {
address: string;
spend_key: string;
view_key: string;
restore_height: number;
};
export interface BalanceBitcoinResponse {
balance: number;
}
export interface GetHistoryResponse {
swaps: [swapId: string, stateName: SwapStateName][];
}
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",
}
export type SwapStateNameRunningSwap = Exclude<
SwapStateName,
| SwapStateName.Started
| SwapStateName.SwapSetupCompleted
| SwapStateName.BtcRefunded
| SwapStateName.BtcPunished
| SwapStateName.SafelyAborted
| SwapStateName.XmrRedeemed
>;
export type GetSwapInfoResponseRunningSwap = GetSwapInfoResponse & {
stateName: SwapStateNameRunningSwap;
};
export function isSwapStateNameRunningSwap(
state: SwapStateName,
): state is SwapStateNameRunningSwap {
return ![
SwapStateName.Started,
SwapStateName.SwapSetupCompleted,
SwapStateName.BtcRefunded,
SwapStateName.BtcPunished,
SwapStateName.SafelyAborted,
SwapStateName.XmrRedeemed,
].includes(state);
}
export type SwapStateNameCompletedSwap =
| SwapStateName.XmrRedeemed
| SwapStateName.BtcRefunded
| SwapStateName.BtcPunished
| SwapStateName.SafelyAborted;
export function isSwapStateNameCompletedSwap(
state: SwapStateName,
): state is SwapStateNameCompletedSwap {
return [
SwapStateName.XmrRedeemed,
SwapStateName.BtcRefunded,
SwapStateName.BtcPunished,
SwapStateName.SafelyAborted,
].includes(state);
}
export type SwapStateNamePossiblyCancellableSwap =
| SwapStateName.BtcLocked
| SwapStateName.XmrLockProofReceived
| SwapStateName.XmrLocked
| SwapStateName.EncSigSent
| SwapStateName.CancelTimelockExpired;
/**
Checks if a swap is in a state where it can possibly be cancelled
The following conditions must be met:
- The bitcoin must be locked
- The bitcoin must not be redeemed
- The bitcoin must not be cancelled
- The bitcoin must not be refunded
- The bitcoin must not be punished
See: https://github.com/comit-network/xmr-btc-swap/blob/7023e75bb51ab26dff4c8fcccdc855d781ca4b15/swap/src/cli/cancel.rs#L16-L35
*/
export function isSwapStateNamePossiblyCancellableSwap(
state: SwapStateName,
): state is SwapStateNamePossiblyCancellableSwap {
return [
SwapStateName.BtcLocked,
SwapStateName.XmrLockProofReceived,
SwapStateName.XmrLocked,
SwapStateName.EncSigSent,
SwapStateName.CancelTimelockExpired,
].includes(state);
}
export type SwapStateNamePossiblyRefundableSwap =
| SwapStateName.BtcLocked
| SwapStateName.XmrLockProofReceived
| SwapStateName.XmrLocked
| SwapStateName.EncSigSent
| SwapStateName.CancelTimelockExpired
| SwapStateName.BtcCancelled;
/**
Checks if a swap is in a state where it can possibly be refunded (meaning it's not impossible)
The following conditions must be met:
- The bitcoin must be locked
- The bitcoin must not be redeemed
- The bitcoin must not be refunded
- The bitcoin must not be punished
See: https://github.com/comit-network/xmr-btc-swap/blob/7023e75bb51ab26dff4c8fcccdc855d781ca4b15/swap/src/cli/refund.rs#L16-L34
*/
export function isSwapStateNamePossiblyRefundableSwap(
state: SwapStateName,
): state is SwapStateNamePossiblyRefundableSwap {
return [
SwapStateName.BtcLocked,
SwapStateName.XmrLockProofReceived,
SwapStateName.XmrLocked,
SwapStateName.EncSigSent,
SwapStateName.CancelTimelockExpired,
SwapStateName.BtcCancelled,
].includes(state);
}
/**
* Type guard for GetSwapInfoResponseRunningSwap
* "running" means the swap is in progress and not yet completed
* If a swap is not "running" it means it is either completed or no Bitcoin have been locked yet
* @param response
*/
export function isGetSwapInfoResponseRunningSwap(
response: GetSwapInfoResponse,
): response is GetSwapInfoResponseRunningSwap {
return isSwapStateNameRunningSwap(response.state_name);
}
export function isSwapMoneroRecoverable(swapStateName: SwapStateName): boolean {
return [SwapStateName.BtcRedeemed].includes(swapStateName);
}
// See https://github.com/comit-network/xmr-btc-swap/blob/50ae54141255e03dba3d2b09036b1caa4a63e5a3/swap/src/protocol/bob/state.rs#L55
export function getHumanReadableDbStateType(type: SwapStateName): string {
switch (type) {
case SwapStateName.Started:
return "Quote has been requested";
case SwapStateName.SwapSetupCompleted:
return "Swap has been initiated";
case SwapStateName.BtcLocked:
return "Bitcoin has been locked";
case SwapStateName.XmrLockProofReceived:
return "Monero lock transaction transfer proof has been received";
case SwapStateName.XmrLocked:
return "Monero has been locked";
case SwapStateName.EncSigSent:
return "Encrypted signature has been sent";
case SwapStateName.BtcRedeemed:
return "Bitcoin has been redeemed";
case SwapStateName.CancelTimelockExpired:
return "Cancel timelock has expired";
case SwapStateName.BtcCancelled:
return "Swap has been cancelled";
case SwapStateName.BtcRefunded:
return "Bitcoin has been refunded";
case SwapStateName.XmrRedeemed:
return "Monero has been redeemed";
case SwapStateName.BtcPunished:
return "Bitcoin has been punished";
case SwapStateName.SafelyAborted:
return "Swap has been safely aborted";
default:
return exhaustiveGuard(type);
}
}
export function getSwapTxFees(swap: GetSwapInfoResponse): number {
return satsToBtc(swap.tx_lock_fee);
}
export function getSwapBtcAmount(swap: GetSwapInfoResponse): number {
return satsToBtc(swap.btc_amount);
}
export function getSwapXmrAmount(swap: GetSwapInfoResponse): number {
return piconerosToXmr(swap.xmr_amount);
}
export function getSwapExchangeRate(swap: GetSwapInfoResponse): number {
const btcAmount = getSwapBtcAmount(swap);
const xmrAmount = getSwapXmrAmount(swap);
return btcAmount / xmrAmount;
}

View File

@ -1,218 +1,12 @@
import { CliLog, SwapSpawnType } from "./cliModel";
import { Provider } from "./apiModel";
import { TauriSwapProgressEvent } from "./tauriModel";
export interface SwapSlice {
state: SwapState | null;
logs: CliLog[];
processRunning: boolean;
provider: Provider | null;
spawnType: SwapSpawnType | null;
swapId: string | null;
}
export type MoneroWalletRpcUpdateState = {
progress: string;
downloadUrl: string;
};
export interface SwapState {
type: SwapStateType;
}
export enum SwapStateType {
INITIATED = "initiated",
RECEIVED_QUOTE = "received quote",
WAITING_FOR_BTC_DEPOSIT = "waiting for btc deposit",
STARTED = "started",
BTC_LOCK_TX_IN_MEMPOOL = "btc lock tx is in mempool",
XMR_LOCK_TX_IN_MEMPOOL = "xmr lock tx is in mempool",
XMR_LOCKED = "xmr is locked",
BTC_REDEEMED = "btc redeemed",
XMR_REDEEM_IN_MEMPOOL = "xmr redeem tx is in mempool",
PROCESS_EXITED = "process exited",
BTC_CANCELLED = "btc cancelled",
BTC_REFUNDED = "btc refunded",
BTC_PUNISHED = "btc punished",
ATTEMPTING_COOPERATIVE_REDEEM = "attempting cooperative redeem",
COOPERATIVE_REDEEM_REJECTED = "cooperative redeem rejected",
}
export function isSwapState(state?: SwapState | null): state is SwapState {
return state?.type != null;
}
export interface SwapStateInitiated extends SwapState {
type: SwapStateType.INITIATED;
}
export function isSwapStateInitiated(
state?: SwapState | null,
): state is SwapStateInitiated {
return state?.type === SwapStateType.INITIATED;
}
export interface SwapStateReceivedQuote extends SwapState {
type: SwapStateType.RECEIVED_QUOTE;
price: number;
minimumSwapAmount: number;
maximumSwapAmount: number;
}
export function isSwapStateReceivedQuote(
state?: SwapState | null,
): state is SwapStateReceivedQuote {
return state?.type === SwapStateType.RECEIVED_QUOTE;
}
export interface SwapStateWaitingForBtcDeposit extends SwapState {
type: SwapStateType.WAITING_FOR_BTC_DEPOSIT;
depositAddress: string;
maxGiveable: number;
minimumAmount: number;
maximumAmount: number;
minDeposit: number;
maxDeposit: number;
minBitcoinLockTxFee: number;
price: number | null;
}
export function isSwapStateWaitingForBtcDeposit(
state?: SwapState | null,
): state is SwapStateWaitingForBtcDeposit {
return state?.type === SwapStateType.WAITING_FOR_BTC_DEPOSIT;
}
export interface SwapStateStarted extends SwapState {
type: SwapStateType.STARTED;
txLockDetails: {
amount: number;
fees: number;
state: {
curr: TauriSwapProgressEvent;
prev: TauriSwapProgressEvent | null;
swapId: string;
} | null;
}
export function isSwapStateStarted(
state?: SwapState | null,
): state is SwapStateStarted {
return state?.type === SwapStateType.STARTED;
}
export interface SwapStateBtcLockInMempool extends SwapState {
type: SwapStateType.BTC_LOCK_TX_IN_MEMPOOL;
bobBtcLockTxId: string;
bobBtcLockTxConfirmations: number;
}
export function isSwapStateBtcLockInMempool(
state?: SwapState | null,
): state is SwapStateBtcLockInMempool {
return state?.type === SwapStateType.BTC_LOCK_TX_IN_MEMPOOL;
}
export interface SwapStateXmrLockInMempool extends SwapState {
type: SwapStateType.XMR_LOCK_TX_IN_MEMPOOL;
aliceXmrLockTxId: string;
aliceXmrLockTxConfirmations: number;
}
export function isSwapStateXmrLockInMempool(
state?: SwapState | null,
): state is SwapStateXmrLockInMempool {
return state?.type === SwapStateType.XMR_LOCK_TX_IN_MEMPOOL;
}
export interface SwapStateXmrLocked extends SwapState {
type: SwapStateType.XMR_LOCKED;
}
export function isSwapStateXmrLocked(
state?: SwapState | null,
): state is SwapStateXmrLocked {
return state?.type === SwapStateType.XMR_LOCKED;
}
export interface SwapStateBtcRedemeed extends SwapState {
type: SwapStateType.BTC_REDEEMED;
}
export function isSwapStateBtcRedemeed(
state?: SwapState | null,
): state is SwapStateBtcRedemeed {
return state?.type === SwapStateType.BTC_REDEEMED;
}
export interface SwapStateAttemptingCooperativeRedeeem extends SwapState {
type: SwapStateType.ATTEMPTING_COOPERATIVE_REDEEM;
}
export function isSwapStateAttemptingCooperativeRedeeem(
state?: SwapState | null,
): state is SwapStateAttemptingCooperativeRedeeem {
return state?.type === SwapStateType.ATTEMPTING_COOPERATIVE_REDEEM;
}
export interface SwapStateCooperativeRedeemRejected extends SwapState {
type: SwapStateType.COOPERATIVE_REDEEM_REJECTED;
reason: string;
}
export function isSwapStateCooperativeRedeemRejected(
state?: SwapState | null,
): state is SwapStateCooperativeRedeemRejected {
return state?.type === SwapStateType.COOPERATIVE_REDEEM_REJECTED;
}
export interface SwapStateXmrRedeemInMempool extends SwapState {
type: SwapStateType.XMR_REDEEM_IN_MEMPOOL;
bobXmrRedeemTxId: string;
bobXmrRedeemAddress: string;
}
export function isSwapStateXmrRedeemInMempool(
state?: SwapState | null,
): state is SwapStateXmrRedeemInMempool {
return state?.type === SwapStateType.XMR_REDEEM_IN_MEMPOOL;
}
export interface SwapStateBtcCancelled extends SwapState {
type: SwapStateType.BTC_CANCELLED;
btcCancelTxId: string;
}
export function isSwapStateBtcCancelled(
state?: SwapState | null,
): state is SwapStateBtcCancelled {
return state?.type === SwapStateType.BTC_CANCELLED;
}
export interface SwapStateBtcRefunded extends SwapState {
type: SwapStateType.BTC_REFUNDED;
bobBtcRefundTxId: string;
}
export function isSwapStateBtcRefunded(
state?: SwapState | null,
): state is SwapStateBtcRefunded {
return state?.type === SwapStateType.BTC_REFUNDED;
}
export interface SwapStateBtcPunished extends SwapState {
type: SwapStateType.BTC_PUNISHED;
}
export function isSwapStateBtcPunished(
state?: SwapState | null,
): state is SwapStateBtcPunished {
return state?.type === SwapStateType.BTC_PUNISHED;
}
export interface SwapStateProcessExited extends SwapState {
type: SwapStateType.PROCESS_EXITED;
prevState: SwapState | null;
rpcError: string | null;
}
export function isSwapStateProcessExited(
state?: SwapState | null,
): state is SwapStateProcessExited {
return state?.type === SwapStateType.PROCESS_EXITED;
logs: CliLog[];
spawnType: SwapSpawnType | null;
}

View File

@ -0,0 +1,185 @@
/*
Generated by typeshare 1.9.2
*/
/**
* Represent a timelock, expressed in relative block height as defined in
* [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki).
* E.g. The timelock expires 10 blocks after the reference transaction is
* mined.
*/
export type CancelTimelock = number;
/**
* Represent a timelock, expressed in relative block height as defined in
* [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki).
* E.g. The timelock expires 10 blocks after the reference transaction is
* mined.
*/
export type PunishTimelock = number;
export type Amount = number;
export interface BuyXmrArgs {
seller: string;
bitcoin_change_address: string;
monero_receive_address: string;
}
export interface ResumeArgs {
swap_id: string;
}
export interface CancelAndRefundArgs {
swap_id: string;
}
export interface MoneroRecoveryArgs {
swap_id: string;
}
export interface WithdrawBtcArgs {
amount?: number;
address: string;
}
export interface BalanceArgs {
force_refresh: boolean;
}
export interface ListSellersArgs {
rendezvous_point: string;
}
export interface StartDaemonArgs {
server_address: string;
}
export interface GetSwapInfoArgs {
swap_id: string;
}
export interface ResumeSwapResponse {
result: string;
}
export interface BalanceResponse {
balance: number;
}
/** Represents a quote for buying XMR. */
export interface BidQuote {
/** The price at which the maker is willing to buy at. */
price: number;
/**
* The minimum quantity the maker is willing to buy.
* #[typeshare(serialized_as = "number")]
*/
min_quantity: number;
/** The maximum quantity the maker is willing to buy. */
max_quantity: number;
}
export interface BuyXmrResponse {
swap_id: string;
quote: BidQuote;
}
export interface GetHistoryEntry {
swap_id: string;
state: string;
}
export interface GetHistoryResponse {
swaps: GetHistoryEntry[];
}
export interface Seller {
peer_id: string;
addresses: string[];
}
export type ExpiredTimelocks =
| { type: "None", content: {
blocks_left: number;
}}
| { type: "Cancel", content: {
blocks_left: number;
}}
| { type: "Punish", content?: undefined };
export interface GetSwapInfoResponse {
swap_id: string;
seller: Seller;
completed: boolean;
start_date: string;
state_name: string;
xmr_amount: number;
btc_amount: number;
tx_lock_id: string;
tx_cancel_fee: number;
tx_refund_fee: number;
tx_lock_fee: number;
btc_refund_address: string;
cancel_timelock: CancelTimelock;
punish_timelock: PunishTimelock;
timelock?: ExpiredTimelocks;
}
export interface WithdrawBtcResponse {
amount: number;
txid: string;
}
export interface SuspendCurrentSwapResponse {
swap_id: string;
}
export type TauriSwapProgressEvent =
| { type: "Initiated", content?: undefined }
| { type: "ReceivedQuote", content: BidQuote }
| { type: "WaitingForBtcDeposit", content: {
deposit_address: string;
max_giveable: number;
min_deposit_until_swap_will_start: number;
max_deposit_until_maximum_amount_is_reached: number;
min_bitcoin_lock_tx_fee: number;
quote: BidQuote;
}}
| { type: "Started", content: {
btc_lock_amount: number;
btc_tx_lock_fee: number;
}}
| { type: "BtcLockTxInMempool", content: {
btc_lock_txid: string;
btc_lock_confirmations: number;
}}
| { type: "XmrLockTxInMempool", content: {
xmr_lock_txid: string;
xmr_lock_tx_confirmations: number;
}}
| { type: "XmrLocked", content?: undefined }
| { type: "BtcRedeemed", content?: undefined }
| { type: "XmrRedeemInMempool", content: {
xmr_redeem_txid: string;
xmr_redeem_address: string;
}}
| { type: "BtcCancelled", content: {
btc_cancel_txid: string;
}}
| { type: "BtcRefunded", content: {
btc_refund_txid: string;
}}
| { type: "BtcPunished", content?: undefined }
| { type: "AttemptingCooperativeRedeem", content?: undefined }
| { type: "CooperativeRedeemAccepted", content?: undefined }
| { type: "CooperativeRedeemRejected", content: {
reason: string;
}}
| { type: "Released", content?: undefined };
export interface TauriSwapProgressEventWrapper {
swap_id: string;
event: TauriSwapProgressEvent;
}

View File

@ -0,0 +1,155 @@
import {
ExpiredTimelocks,
GetSwapInfoResponse,
TauriSwapProgressEvent,
} from "./tauriModel";
export type TauriSwapProgressEventContent<
T extends TauriSwapProgressEvent["type"],
> = Extract<TauriSwapProgressEvent, { type: T }>["content"];
// See /swap/src/protocol/bob/state.rs#L57
// TODO: Replace this with a typeshare definition
export enum BobStateName {
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",
}
// TODO: This is a temporary solution until we have a typeshare definition for BobStateName
export type GetSwapInfoResponseExt = GetSwapInfoResponse & {
state_name: BobStateName;
};
export type TimelockNone = Extract<ExpiredTimelocks, { type: "None" }>;
export type TimelockCancel = Extract<ExpiredTimelocks, { type: "Cancel" }>;
export type TimelockPunish = Extract<ExpiredTimelocks, { type: "Punish" }>;
export type BobStateNameRunningSwap = Exclude<
BobStateName,
| BobStateName.Started
| BobStateName.SwapSetupCompleted
| BobStateName.BtcRefunded
| BobStateName.BtcPunished
| BobStateName.SafelyAborted
| BobStateName.XmrRedeemed
>;
export type GetSwapInfoResponseExtRunningSwap = GetSwapInfoResponseExt & {
stateName: BobStateNameRunningSwap;
};
export function isBobStateNameRunningSwap(
state: BobStateName,
): state is BobStateNameRunningSwap {
return ![
BobStateName.Started,
BobStateName.SwapSetupCompleted,
BobStateName.BtcRefunded,
BobStateName.BtcPunished,
BobStateName.SafelyAborted,
BobStateName.XmrRedeemed,
].includes(state);
}
export type BobStateNameCompletedSwap =
| BobStateName.XmrRedeemed
| BobStateName.BtcRefunded
| BobStateName.BtcPunished
| BobStateName.SafelyAborted;
export function isBobStateNameCompletedSwap(
state: BobStateName,
): state is BobStateNameCompletedSwap {
return [
BobStateName.XmrRedeemed,
BobStateName.BtcRefunded,
BobStateName.BtcPunished,
BobStateName.SafelyAborted,
].includes(state);
}
export type BobStateNamePossiblyCancellableSwap =
| BobStateName.BtcLocked
| BobStateName.XmrLockProofReceived
| BobStateName.XmrLocked
| BobStateName.EncSigSent
| BobStateName.CancelTimelockExpired;
/**
Checks if a swap is in a state where it can possibly be cancelled
The following conditions must be met:
- The bitcoin must be locked
- The bitcoin must not be redeemed
- The bitcoin must not be cancelled
- The bitcoin must not be refunded
- The bitcoin must not be punished
See: https://github.com/comit-network/xmr-btc-swap/blob/7023e75bb51ab26dff4c8fcccdc855d781ca4b15/swap/src/cli/cancel.rs#L16-L35
*/
export function isBobStateNamePossiblyCancellableSwap(
state: BobStateName,
): state is BobStateNamePossiblyCancellableSwap {
return [
BobStateName.BtcLocked,
BobStateName.XmrLockProofReceived,
BobStateName.XmrLocked,
BobStateName.EncSigSent,
BobStateName.CancelTimelockExpired,
].includes(state);
}
export type BobStateNamePossiblyRefundableSwap =
| BobStateName.BtcLocked
| BobStateName.XmrLockProofReceived
| BobStateName.XmrLocked
| BobStateName.EncSigSent
| BobStateName.CancelTimelockExpired
| BobStateName.BtcCancelled;
/**
Checks if a swap is in a state where it can possibly be refunded (meaning it's not impossible)
The following conditions must be met:
- The bitcoin must be locked
- The bitcoin must not be redeemed
- The bitcoin must not be refunded
- The bitcoin must not be punished
See: https://github.com/comit-network/xmr-btc-swap/blob/7023e75bb51ab26dff4c8fcccdc855d781ca4b15/swap/src/cli/refund.rs#L16-L34
*/
export function isBobStateNamePossiblyRefundableSwap(
state: BobStateName,
): state is BobStateNamePossiblyRefundableSwap {
return [
BobStateName.BtcLocked,
BobStateName.XmrLockProofReceived,
BobStateName.XmrLocked,
BobStateName.EncSigSent,
BobStateName.CancelTimelockExpired,
BobStateName.BtcCancelled,
].includes(state);
}
/**
* Type guard for GetSwapInfoResponseExt
* "running" means the swap is in progress and not yet completed
* If a swap is not "running" it means it is either completed or no Bitcoin have been locked yet
* @param response
*/
export function isGetSwapInfoResponseRunningSwap(
response: GetSwapInfoResponseExt,
): response is GetSwapInfoResponseExtRunningSwap {
return isBobStateNameRunningSwap(response.state_name);
}

View File

@ -1,12 +1,12 @@
import { Box, makeStyles, CssBaseline } from "@material-ui/core";
import { createTheme, ThemeProvider } from "@material-ui/core/styles";
import { Box, CssBaseline, makeStyles } from "@material-ui/core";
import { indigo } from "@material-ui/core/colors";
import { MemoryRouter as Router, Routes, Route } from "react-router-dom";
import { createTheme, ThemeProvider } from "@material-ui/core/styles";
import { Route, MemoryRouter as Router, Routes } from "react-router-dom";
import Navigation, { drawerWidth } from "./navigation/Navigation";
import HelpPage from "./pages/help/HelpPage";
import HistoryPage from "./pages/history/HistoryPage";
import SwapPage from "./pages/swap/SwapPage";
import WalletPage from "./pages/wallet/WalletPage";
import HelpPage from "./pages/help/HelpPage";
import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider";
const useStyles = makeStyles((theme) => ({

View File

@ -1,166 +0,0 @@
import {
Button,
ButtonProps,
CircularProgress,
IconButton,
Tooltip,
} from "@material-ui/core";
import { ReactElement, ReactNode, useEffect, useState } from "react";
import { useSnackbar } from "notistack";
import { useAppSelector } from "store/hooks";
import { RpcProcessStateType } from "models/rpcModel";
import { isExternalRpc } from "store/config";
function IpcButtonTooltip({
requiresRpcAndNotReady,
children,
processType,
tooltipTitle,
}: {
requiresRpcAndNotReady: boolean;
children: ReactElement;
processType: RpcProcessStateType;
tooltipTitle?: string;
}) {
if (tooltipTitle) {
return <Tooltip title={tooltipTitle}>{children}</Tooltip>;
}
const getMessage = () => {
if (!requiresRpcAndNotReady) return "";
switch (processType) {
case RpcProcessStateType.LISTENING_FOR_CONNECTIONS:
return "";
case RpcProcessStateType.STARTED:
return "Cannot execute this action because the Swap Daemon is still starting and not yet ready to accept connections. Please wait a moment and try again";
case RpcProcessStateType.EXITED:
return "Cannot execute this action because the Swap Daemon has been stopped. Please start the Swap Daemon again to continue";
case RpcProcessStateType.NOT_STARTED:
return "Cannot execute this action because the Swap Daemon has not been started yet. Please start the Swap Daemon first";
default:
return "";
}
};
return (
<Tooltip title={getMessage()} color="red">
{children}
</Tooltip>
);
}
interface IpcInvokeButtonProps<T> {
ipcArgs: unknown[];
ipcChannel: string;
onSuccess?: (data: T) => void;
isLoadingOverride?: boolean;
isIconButton?: boolean;
loadIcon?: ReactNode;
requiresRpc?: boolean;
disabled?: boolean;
displayErrorSnackbar?: boolean;
tooltipTitle?: string;
}
const DELAY_BEFORE_SHOWING_LOADING_MS = 0;
export default function IpcInvokeButton<T>({
disabled,
ipcChannel,
ipcArgs,
onSuccess,
onClick,
endIcon,
loadIcon,
isLoadingOverride,
isIconButton,
requiresRpc,
displayErrorSnackbar,
tooltipTitle,
...rest
}: IpcInvokeButtonProps<T> & ButtonProps) {
const { enqueueSnackbar } = useSnackbar();
const rpcProcessType = useAppSelector((state) => state.rpc.process.type);
const isRpcReady =
rpcProcessType === RpcProcessStateType.LISTENING_FOR_CONNECTIONS;
const [isPending, setIsPending] = useState(false);
const [hasMinLoadingTimePassed, setHasMinLoadingTimePassed] = useState(false);
const isLoading = (isPending && hasMinLoadingTimePassed) || isLoadingOverride;
const actualEndIcon = isLoading
? loadIcon || <CircularProgress size="1rem" />
: endIcon;
useEffect(() => {
setHasMinLoadingTimePassed(false);
setTimeout(
() => setHasMinLoadingTimePassed(true),
DELAY_BEFORE_SHOWING_LOADING_MS,
);
}, [isPending]);
async function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
onClick?.(event);
if (!isPending) {
setIsPending(true);
try {
// const result = await ipcRenderer.invoke(ipcChannel, ...ipcArgs);
throw new Error("Not implemented");
// onSuccess?.(result);
} catch (e: unknown) {
if (displayErrorSnackbar) {
enqueueSnackbar((e as Error).message, {
autoHideDuration: 60 * 1000,
variant: "error",
});
}
} finally {
setIsPending(false);
}
}
}
const requiresRpcAndNotReady =
!!requiresRpc && !isRpcReady && !isExternalRpc();
const isDisabled = disabled || requiresRpcAndNotReady || isLoading;
return (
<IpcButtonTooltip
requiresRpcAndNotReady={requiresRpcAndNotReady}
processType={rpcProcessType}
tooltipTitle={tooltipTitle}
>
<span>
{isIconButton ? (
<IconButton
onClick={handleClick}
disabled={isDisabled}
{...(rest as any)}
>
{actualEndIcon}
</IconButton>
) : (
<Button
onClick={handleClick}
disabled={isDisabled}
endIcon={actualEndIcon}
{...rest}
/>
)}
</span>
</IpcButtonTooltip>
);
}
IpcInvokeButton.defaultProps = {
requiresRpc: true,
disabled: false,
onSuccess: undefined,
isLoadingOverride: false,
isIconButton: false,
loadIcon: undefined,
displayErrorSnackbar: true,
};

View File

@ -1,9 +1,9 @@
import { Button, ButtonProps, IconButton, Tooltip } from "@material-ui/core";
import { Button, ButtonProps, IconButton } from "@material-ui/core";
import CircularProgress from "@material-ui/core/CircularProgress";
import { useSnackbar } from "notistack";
import { ReactNode, useEffect, useState } from "react";
import { ReactNode, useState } from "react";
interface IpcInvokeButtonProps<T> {
interface PromiseInvokeButtonProps<T> {
onSuccess?: (data: T) => void;
onClick: () => Promise<T>;
onPendingChange?: (isPending: boolean) => void;
@ -24,10 +24,9 @@ export default function PromiseInvokeButton<T>({
isLoadingOverride,
isIconButton,
displayErrorSnackbar,
tooltipTitle,
onPendingChange,
...rest
}: IpcInvokeButtonProps<T> & ButtonProps) {
}: ButtonProps & PromiseInvokeButtonProps<T>) {
const { enqueueSnackbar } = useSnackbar();
const [isPending, setIsPending] = useState(false);
@ -42,11 +41,11 @@ export default function PromiseInvokeButton<T>({
try {
onPendingChange?.(true);
setIsPending(true);
let result = await onClick();
const result = await onClick();
onSuccess?.(result);
} catch (e: unknown) {
if (displayErrorSnackbar) {
enqueueSnackbar(e as String, {
enqueueSnackbar(e as string, {
autoHideDuration: 60 * 1000,
variant: "error",
});

View File

@ -1,8 +1,11 @@
import { Alert } from "@material-ui/lab";
import { Box, LinearProgress } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useAppSelector } from "store/hooks";
export default function MoneroWalletRpcUpdatingAlert() {
// TODO: Reimplement this using Tauri Events
return <></>;
const updateState = useAppSelector(
(s) => s.rpc.state.moneroWalletRpc.updateState,
);

View File

@ -1,8 +1,8 @@
import { Alert } from "@material-ui/lab";
import { Box, makeStyles } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useAppSelector } from "store/hooks";
import WalletRefreshButton from "../pages/wallet/WalletRefreshButton";
import { SatsAmount } from "../other/Units";
import WalletRefreshButton from "../pages/wallet/WalletRefreshButton";
const useStyles = makeStyles((theme) => ({
outer: {

View File

@ -1,8 +1,11 @@
import { Alert } from "@material-ui/lab";
import { CircularProgress } from "@material-ui/core";
import { useAppSelector } from "store/hooks";
import { Alert } from "@material-ui/lab";
import { RpcProcessStateType } from "models/rpcModel";
import { useAppSelector } from "store/hooks";
// TODO: Reimplement this using Tauri
// Currently the RPC process is always available, so this component is not needed
// since the UI is only displayed when the RPC process is available
export default function RpcStatusAlert() {
const rpcProcess = useAppSelector((s) => s.rpc.process);
if (rpcProcess.type === RpcProcessStateType.STARTED) {

View File

@ -1,10 +1,10 @@
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 { useActiveSwapInfo } from "store/hooks";
import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
const useStyles = makeStyles((theme) => ({
@ -21,6 +21,9 @@ export default function SwapMightBeCancelledAlert({
}: {
bobBtcLockTxConfirmations: number;
}) {
// TODO: Reimplement this using Tauri
return <></>;
const classes = useStyles();
const swap = useActiveSwapInfo();

View File

@ -1,23 +1,19 @@
import { Alert, AlertTitle } from "@material-ui/lab/";
import { Box, makeStyles } from "@material-ui/core";
import { Alert, AlertTitle } from "@material-ui/lab/";
import { GetSwapInfoResponse } from "models/tauriModel";
import {
BobStateName,
GetSwapInfoResponseExt,
TimelockCancel,
TimelockNone,
} from "models/tauriModelExt";
import { ReactNode } from "react";
import { exhaustiveGuard } from "utils/typescriptUtils";
import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
import {
SwapCancelRefundButton,
SwapResumeButton,
} from "../pages/history/table/HistoryRowActions";
import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
import {
GetSwapInfoResponse,
GetSwapInfoResponseRunningSwap,
isGetSwapInfoResponseRunningSwap,
isSwapTimelockInfoCancelled,
isSwapTimelockInfoNone,
isSwapTimelockInfoPunished,
SwapStateName,
SwapTimelockInfoCancelled,
SwapTimelockInfoNone,
} from "../../../models/rpcModel";
import { SwapMoneroRecoveryButton } from "../pages/history/table/SwapMoneroRecoveryButton";
const useStyles = makeStyles({
@ -42,7 +38,6 @@ const MessageList = ({ messages }: { messages: ReactNode[] }) => {
return (
<ul className={classes.list}>
{messages.map((msg, i) => (
// eslint-disable-next-line react/no-array-index-key
<li key={i}>{msg}</li>
))}
</ul>
@ -81,21 +76,21 @@ const BitcoinLockedNoTimelockExpiredStateAlert = ({
timelock,
punishTimelockOffset,
}: {
timelock: SwapTimelockInfoNone;
timelock: TimelockNone;
punishTimelockOffset: number;
}) => (
<MessageList
messages={[
<>
Your Bitcoin is locked. If the swap is not completed in approximately{" "}
<HumanizedBitcoinBlockDuration blocks={timelock.None.blocks_left} />,
<HumanizedBitcoinBlockDuration blocks={timelock.content.blocks_left} />,
you need to refund
</>,
<>
You will lose your funds if you do not refund or complete the swap
You might lose your funds if you do not refund or complete the swap
within{" "}
<HumanizedBitcoinBlockDuration
blocks={timelock.None.blocks_left + punishTimelockOffset}
blocks={timelock.content.blocks_left + punishTimelockOffset}
/>
</>,
]}
@ -113,8 +108,8 @@ const BitcoinPossiblyCancelledAlert = ({
swap,
timelock,
}: {
swap: GetSwapInfoResponse;
timelock: SwapTimelockInfoCancelled;
swap: GetSwapInfoResponseExt;
timelock: TimelockCancel;
}) => {
const classes = useStyles();
return (
@ -124,9 +119,9 @@ const BitcoinPossiblyCancelledAlert = ({
"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 might lose your funds if you do not refund within{" "}
<HumanizedBitcoinBlockDuration
blocks={timelock.Cancel.blocks_left}
blocks={timelock.content.blocks_left}
/>
</>,
]}
@ -149,55 +144,52 @@ const ImmediateActionAlert = () => (
* @param swap - The swap information.
* @returns JSX.Element | null
*/
function SwapAlertStatusText({
swap,
}: {
swap: GetSwapInfoResponseRunningSwap;
}) {
function SwapAlertStatusText({ swap }: { swap: GetSwapInfoResponseExt }) {
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:
case BobStateName.BtcRedeemed:
return <BitcoinRedeemedStateAlert swap={swap} />;
// These are states that are at risk of punishment because the Bitcoin have been locked
// but has not been redeemed yet by the other party
case SwapStateName.BtcLocked:
case SwapStateName.XmrLockProofReceived:
case SwapStateName.XmrLocked:
case SwapStateName.EncSigSent:
case SwapStateName.CancelTimelockExpired:
case SwapStateName.BtcCancelled:
if (swap.timelock !== null) {
if (isSwapTimelockInfoNone(swap.timelock)) {
return (
<BitcoinLockedNoTimelockExpiredStateAlert
punishTimelockOffset={swap.punish_timelock}
timelock={swap.timelock}
/>
);
}
case BobStateName.BtcLocked:
case BobStateName.XmrLockProofReceived:
case BobStateName.XmrLocked:
case BobStateName.EncSigSent:
case BobStateName.CancelTimelockExpired:
case BobStateName.BtcCancelled:
if (swap.timelock != null) {
switch (swap.timelock.type) {
case "None":
return (
<BitcoinLockedNoTimelockExpiredStateAlert
punishTimelockOffset={swap.punish_timelock}
timelock={swap.timelock}
/>
);
if (isSwapTimelockInfoCancelled(swap.timelock)) {
return (
<BitcoinPossiblyCancelledAlert
timelock={swap.timelock}
swap={swap}
/>
);
}
case "Cancel":
return (
<BitcoinPossiblyCancelledAlert
timelock={swap.timelock}
swap={swap}
/>
);
case "Punish":
return <ImmediateActionAlert />;
if (isSwapTimelockInfoPunished(swap.timelock)) {
return <ImmediateActionAlert />;
default:
// We have covered all possible timelock states above
// If we reach this point, it means we have missed a case
exhaustiveGuard(swap.timelock);
}
// We have covered all possible timelock states above
// If we reach this point, it means we have missed a case
return exhaustiveGuard(swap.timelock);
}
return <ImmediateActionAlert />;
default:
return exhaustiveGuard(swap.state_name);
// TODO: fix the exhaustive guard
// return exhaustiveGuard(swap.state_name);
return <></>;
}
}
@ -209,11 +201,12 @@ function SwapAlertStatusText({
export default function SwapStatusAlert({
swap,
}: {
swap: GetSwapInfoResponse;
swap: GetSwapInfoResponseExt;
}): JSX.Element | null {
// If the swap is not running, there is no need to display the alert
// This is either because the swap is finished or has not started yet (e.g. in the setup phase, no Bitcoin locked)
if (!isGetSwapInfoResponseRunningSwap(swap)) {
// If the swap is completed, there is no need to display the alert
// TODO: Here we should also check if the swap is in a state where any funds can be lost
// TODO: If the no Bitcoin have been locked yet, we can safely ignore the swap
if (swap.completed) {
return null;
}

View File

@ -3,7 +3,6 @@ import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function BitcoinIcon(props: SvgIconProps) {
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<SvgIcon {...props}>
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@ -1,5 +1,5 @@
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
import { SvgIcon } from "@material-ui/core";
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function DiscordIcon(props: SvgIconProps) {
return (

View File

@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { IconButton } from "@material-ui/core";
import { ReactNode } from "react";
export default function LinkIconButton({
url,

View File

@ -3,7 +3,6 @@ import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function MoneroIcon(props: SvgIconProps) {
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<SvgIcon {...props}>
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@ -3,7 +3,6 @@ import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function TorIcon(props: SvgIconProps) {
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<SvgIcon {...props}>
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@ -1,8 +1,8 @@
import { useEffect } from "react";
import { TextField } from "@material-ui/core";
import { TextFieldProps } from "@material-ui/core/TextField/TextField";
import { isBtcAddressValid } from "utils/conversionUtils";
import { useEffect } from "react";
import { isTestnet } from "store/config";
import { isBtcAddressValid } from "utils/conversionUtils";
export default function BitcoinAddressTextField({
address,

View File

@ -1,8 +1,8 @@
import { useEffect } from "react";
import { TextField } from "@material-ui/core";
import { TextFieldProps } from "@material-ui/core/TextField/TextField";
import { isXmrAddressValid } from "utils/conversionUtils";
import { useEffect } from "react";
import { isTestnet } from "store/config";
import { isXmrAddressValid } from "utils/conversionUtils";
export default function MoneroAddressTextField({
address,

View File

@ -6,7 +6,8 @@ import {
DialogContentText,
DialogTitle,
} from "@material-ui/core";
import IpcInvokeButton from "../IpcInvokeButton";
import { suspendCurrentSwap } from "renderer/rpc";
import PromiseInvokeButton from "../PromiseInvokeButton";
type SwapCancelAlertProps = {
open: boolean;
@ -29,15 +30,13 @@ export default function SwapSuspendAlert({
<Button onClick={onClose} color="primary">
No
</Button>
<IpcInvokeButton
ipcChannel="suspend-current-swap"
ipcArgs={[]}
<PromiseInvokeButton
color="primary"
onSuccess={onClose}
requiresRpc
onClick={suspendCurrentSwap}
>
Force stop
</IpcInvokeButton>
</PromiseInvokeButton>
</DialogActions>
</Dialog>
);

View File

@ -10,15 +10,15 @@ import {
Select,
TextField,
} from "@material-ui/core";
import { useState } from "react";
import { CliLog } from "models/cliModel";
import { useSnackbar } from "notistack";
import { useState } from "react";
import { store } from "renderer/store/storeRenderer";
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";
import { PiconeroAmount } from "../../other/Units";
async function submitFeedback(body: string, swapId: string | number) {
let attachedBody = "";
@ -67,7 +67,7 @@ function SwapSelectDropDown({
>
<MenuItem value={0}>Do not attach logs</MenuItem>
{swaps.map((swap) => (
<MenuItem value={swap.swap_id}>
<MenuItem value={swap.swap_id} key={swap.swap_id}>
Swap {swap.swap_id.substring(0, 5)}... from{" "}
{new Date(parseDateString(swap.start_date)).toDateString()} (
<PiconeroAmount amount={swap.xmr_amount} />)

View File

@ -1,20 +1,20 @@
import { ChangeEvent, useState } from "react";
import {
DialogTitle,
Box,
Button,
Chip,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
TextField,
DialogActions,
Button,
Box,
Chip,
DialogTitle,
makeStyles,
TextField,
Theme,
} from "@material-ui/core";
import { Multiaddr } from "multiaddr";
import { useSnackbar } from "notistack";
import IpcInvokeButton from "../../IpcInvokeButton";
import { ChangeEvent, useState } from "react";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
const PRESET_RENDEZVOUS_POINTS = [
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
@ -53,7 +53,7 @@ export default function ListSellersDialog({
return "The multi address must contain the peer id (/p2p/)";
}
return null;
} catch (e) {
} catch {
return "Not a valid multi address";
}
}
@ -119,17 +119,17 @@ export default function ListSellersDialog({
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<IpcInvokeButton
<PromiseInvokeButton
variant="contained"
disabled={!(rendezvousAddress && !getMultiAddressError())}
color="primary"
onSuccess={handleSuccess}
ipcChannel="spawn-list-sellers"
ipcArgs={[rendezvousAddress]}
requiresRpc
onClick={() => {
throw new Error("Not implemented");
}}
>
Connect
</IpcInvokeButton>
</PromiseInvokeButton>
</DialogActions>
</Dialog>
);

View File

@ -1,11 +1,11 @@
import { makeStyles, Box, Typography, Chip, Tooltip } from "@material-ui/core";
import { Box, Chip, makeStyles, Tooltip, Typography } from "@material-ui/core";
import { VerifiedUser } from "@material-ui/icons";
import { satsToBtc, secondsToDays } from "utils/conversionUtils";
import { ExtendedProviderStatus } from "models/apiModel";
import {
MoneroBitcoinExchangeRate,
SatsAmount,
} from "renderer/components/other/Units";
import { satsToBtc, secondsToDays } from "utils/conversionUtils";
const useStyles = makeStyles((theme) => ({
content: {

View File

@ -1,31 +1,31 @@
import {
Avatar,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
List,
ListItem,
ListItemAvatar,
ListItemText,
DialogTitle,
Dialog,
DialogActions,
Button,
DialogContent,
makeStyles,
CircularProgress,
} from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import { useState } from "react";
import SearchIcon from "@material-ui/icons/Search";
import { ExtendedProviderStatus } from "models/apiModel";
import { RpcMethod } from "models/rpcModel";
import { useState } from "react";
import { setSelectedProvider } from "store/features/providersSlice";
import {
useAllProviders,
useAppDispatch,
useIsRpcEndpointBusy,
} from "store/hooks";
import { setSelectedProvider } from "store/features/providersSlice";
import { RpcMethod } from "models/rpcModel";
import ProviderSubmitDialog from "./ProviderSubmitDialog";
import ListSellersDialog from "../listSellers/ListSellersDialog";
import ProviderInfo from "./ProviderInfo";
import ProviderSubmitDialog from "./ProviderSubmitDialog";
const useStyles = makeStyles({
dialogContent: {

View File

@ -1,9 +1,9 @@
import {
makeStyles,
Box,
Card,
CardContent,
Box,
IconButton,
makeStyles,
} from "@material-ui/core";
import ArrowForwardIosIcon from "@material-ui/icons/ArrowForwardIos";
import { useState } from "react";

View File

@ -1,14 +1,14 @@
import { ChangeEvent, useState } from "react";
import {
DialogTitle,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
TextField,
DialogActions,
Button,
} from "@material-ui/core";
import { Multiaddr } from "multiaddr";
import { ChangeEvent, useState } from "react";
type ProviderSubmitDialogProps = {
open: boolean;

View File

@ -1,5 +1,5 @@
import QRCode from "react-qr-code";
import { Box } from "@material-ui/core";
import QRCode from "react-qr-code";
export default function BitcoinQrCode({ address }: { address: string }) {
return (

View File

@ -1,7 +1,7 @@
import { ReactNode } from "react";
import BitcoinIcon from "renderer/components/icons/BitcoinIcon";
import { isTestnet } from "store/config";
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
import BitcoinIcon from "renderer/components/icons/BitcoinIcon";
import { ReactNode } from "react";
import TransactionInfoBox from "./TransactionInfoBox";
type Props = {

View File

@ -1,9 +1,9 @@
import { ReactNode } from "react";
import { Box, Typography } from "@material-ui/core";
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
import InfoBox from "./InfoBox";
import ClipboardIconButton from "./ClipbiardIconButton";
import { ReactNode } from "react";
import BitcoinQrCode from "./BitcoinQrCode";
import ClipboardIconButton from "./ClipbiardIconButton";
import InfoBox from "./InfoBox";
type Props = {
title: string;

View File

@ -1,7 +1,7 @@
import { ReactNode } from "react";
import MoneroIcon from "renderer/components/icons/MoneroIcon";
import { isTestnet } from "store/config";
import { getMoneroTxExplorerUrl } from "utils/conversionUtils";
import MoneroIcon from "renderer/components/icons/MoneroIcon";
import { ReactNode } from "react";
import TransactionInfoBox from "./TransactionInfoBox";
type Props = {

View File

@ -1,4 +1,3 @@
import { useState } from "react";
import {
Button,
Dialog,
@ -6,13 +5,14 @@ import {
DialogContent,
makeStyles,
} from "@material-ui/core";
import { useAppDispatch, useAppSelector } from "store/hooks";
import { useState } from "react";
import { swapReset } from "store/features/swapSlice";
import SwapStatePage from "./pages/SwapStatePage";
import SwapStateStepper from "./SwapStateStepper";
import { useAppDispatch, useAppSelector, useIsSwapRunning } from "store/hooks";
import SwapSuspendAlert from "../SwapSuspendAlert";
import SwapDialogTitle from "./SwapDialogTitle";
import DebugPage from "./pages/DebugPage";
import SwapStatePage from "./pages/SwapStatePage";
import SwapDialogTitle from "./SwapDialogTitle";
import SwapStateStepper from "./SwapStateStepper";
const useStyles = makeStyles({
content: {
@ -32,16 +32,17 @@ export default function SwapDialog({
}) {
const classes = useStyles();
const swap = useAppSelector((state) => state.swap);
const isSwapRunning = useIsSwapRunning();
const [debug, setDebug] = useState(false);
const [openSuspendAlert, setOpenSuspendAlert] = useState(false);
const dispatch = useAppDispatch();
function onCancel() {
if (swap.processRunning) {
if (isSwapRunning) {
setOpenSuspendAlert(true);
} else {
onClose();
setTimeout(() => dispatch(swapReset()), 0);
dispatch(swapReset());
}
}
@ -61,7 +62,7 @@ export default function SwapDialog({
<DebugPage />
) : (
<>
<SwapStatePage swapState={swap.state} />
<SwapStatePage state={swap.state} />
<SwapStateStepper />
</>
)}
@ -75,7 +76,7 @@ export default function SwapDialog({
color="primary"
variant="contained"
onClick={onCancel}
disabled={!(swap.state !== null && !swap.processRunning)}
disabled={isSwapRunning}
>
Done
</Button>

View File

@ -1,7 +1,7 @@
import { Box, DialogTitle, makeStyles, Typography } from "@material-ui/core";
import TorStatusBadge from "./pages/TorStatusBadge";
import FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge";
import DebugPageSwitchBadge from "./pages/DebugPageSwitchBadge";
import FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge";
import TorStatusBadge from "./pages/TorStatusBadge";
const useStyles = makeStyles((theme) => ({
root: {

View File

@ -1,7 +1,11 @@
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 { BobStateName } from "models/tauriModelExt";
import {
useActiveSwapInfo,
useAppSelector,
useIsSwapRunning,
} from "store/hooks";
import { exhaustiveGuard } from "utils/typescriptUtils";
export enum PathType {
@ -9,8 +13,10 @@ export enum PathType {
UNHAPPY_PATH = "unhappy path",
}
// TODO: Consider using a TauriProgressEvent here instead of BobStateName
// TauriProgressEvent is always up to date, BobStateName is not (needs to be periodically fetched)
function getActiveStep(
stateName: SwapStateName | null,
stateName: BobStateName | null,
processExited: boolean,
): [PathType, number, boolean] {
switch (stateName) {
@ -18,56 +24,56 @@ function getActiveStep(
// Step: 0 (Waiting for Bitcoin lock tx to be published)
case null:
return [PathType.HAPPY_PATH, 0, false];
case SwapStateName.Started:
case SwapStateName.SwapSetupCompleted:
case BobStateName.Started:
case BobStateName.SwapSetupCompleted:
return [PathType.HAPPY_PATH, 0, processExited];
// Step: 1 (Waiting for Bitcoin Lock confirmation and XMR Lock Publication)
// We have locked the Bitcoin and are waiting for the other party to lock their XMR
case SwapStateName.BtcLocked:
case BobStateName.BtcLocked:
return [PathType.HAPPY_PATH, 1, processExited];
// Step: 2 (Waiting for XMR Lock confirmation)
// We have locked the Bitcoin and the other party has locked their XMR
case SwapStateName.XmrLockProofReceived:
case BobStateName.XmrLockProofReceived:
return [PathType.HAPPY_PATH, 1, processExited];
// Step: 3 (Sending Encrypted Signature and waiting for Bitcoin Redemption)
// The XMR lock transaction has been confirmed
// We now need to send the encrypted signature to the other party and wait for them to redeem the Bitcoin
case SwapStateName.XmrLocked:
case SwapStateName.EncSigSent:
case BobStateName.XmrLocked:
case BobStateName.EncSigSent:
return [PathType.HAPPY_PATH, 2, processExited];
// Step: 4 (Waiting for XMR Redemption)
case SwapStateName.BtcRedeemed:
case BobStateName.BtcRedeemed:
return [PathType.HAPPY_PATH, 3, processExited];
// Step: 4 (Completed) (Swap completed, XMR redeemed)
case SwapStateName.XmrRedeemed:
case BobStateName.XmrRedeemed:
return [PathType.HAPPY_PATH, 4, false];
// Edge Case of Happy Path where the swap is safely aborted. We "fail" at the first step.
case SwapStateName.SafelyAborted:
case BobStateName.SafelyAborted:
return [PathType.HAPPY_PATH, 0, true];
// // Unhappy Path
// Step: 1 (Cancelling swap, checking if cancel transaction has been published already by the other party)
case SwapStateName.CancelTimelockExpired:
case BobStateName.CancelTimelockExpired:
return [PathType.UNHAPPY_PATH, 0, processExited];
// Step: 2 (Attempt to publish the Bitcoin refund transaction)
case SwapStateName.BtcCancelled:
case BobStateName.BtcCancelled:
return [PathType.UNHAPPY_PATH, 1, processExited];
// Step: 2 (Completed) (Bitcoin refunded)
case SwapStateName.BtcRefunded:
case BobStateName.BtcRefunded:
return [PathType.UNHAPPY_PATH, 2, false];
// Step: 2 (We failed to publish the Bitcoin refund transaction)
// We failed to publish the Bitcoin refund transaction because the timelock has expired.
// We will be punished. Nothing we can do about it now.
case SwapStateName.BtcPunished:
case BobStateName.BtcPunished:
return [PathType.UNHAPPY_PATH, 1, true];
default:
return exhaustiveGuard(stateName);
@ -149,11 +155,14 @@ function UnhappyPathStepper({
}
export default function SwapStateStepper() {
// TODO: There's no equivalent of this with Tauri yet.
const currentSwapSpawnType = useAppSelector((s) => s.swap.spawnType);
const stateName = useActiveSwapInfo()?.state_name ?? null;
const processExited = useAppSelector((s) => !s.swap.processRunning);
const processExited = !useIsSwapRunning();
const [pathType, activeStep, error] = getActiveStep(stateName, processExited);
// TODO: Fix this to work with Tauri
// 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) {
return <UnhappyPathStepper activeStep={0} error={error} />;

View File

@ -1,7 +1,7 @@
import { Box, DialogContentText } from "@material-ui/core";
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import CliLogsBox from "../../../other/RenderedCliLog";
import JsonTreeView from "../../../other/JSONViewTree";
import CliLogsBox from "../../../other/RenderedCliLog";
export default function DebugPage() {
const torStdOut = useAppSelector((s) => s.tor.stdOut);

View File

@ -1,7 +1,7 @@
import { IconButton } from "@material-ui/core";
import FeedbackIcon from "@material-ui/icons/Feedback";
import FeedbackDialog from "../../feedback/FeedbackDialog";
import { useState } from "react";
import FeedbackDialog from "../../feedback/FeedbackDialog";
export default function FeedbackSubmitBadge() {
const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);

View File

@ -1,43 +1,28 @@
import { Box } from "@material-ui/core";
import { useAppSelector } from "store/hooks";
import {
isSwapStateBtcCancelled,
isSwapStateBtcLockInMempool,
isSwapStateBtcPunished,
isSwapStateBtcRedemeed,
isSwapStateBtcRefunded,
isSwapStateInitiated,
isSwapStateProcessExited,
isSwapStateReceivedQuote,
isSwapStateStarted,
isSwapStateWaitingForBtcDeposit,
isSwapStateXmrLocked,
isSwapStateXmrLockInMempool,
isSwapStateXmrRedeemInMempool,
SwapState,
} from "../../../../../models/storeModel";
import InitiatedPage from "./init/InitiatedPage";
import WaitingForBitcoinDepositPage from "./init/WaitingForBitcoinDepositPage";
import StartedPage from "./in_progress/StartedPage";
import BitcoinLockTxInMempoolPage from "./in_progress/BitcoinLockTxInMempoolPage";
import XmrLockTxInMempoolPage from "./in_progress/XmrLockInMempoolPage";
// eslint-disable-next-line import/no-cycle
import ProcessExitedPage from "./exited/ProcessExitedPage";
import XmrRedeemInMempoolPage from "./done/XmrRedeemInMempoolPage";
import ReceivedQuotePage from "./in_progress/ReceivedQuotePage";
import BitcoinRedeemedPage from "./in_progress/BitcoinRedeemedPage";
import InitPage from "./init/InitPage";
import XmrLockedPage from "./in_progress/XmrLockedPage";
import BitcoinCancelledPage from "./in_progress/BitcoinCancelledPage";
import BitcoinRefundedPage from "./done/BitcoinRefundedPage";
import { SwapSlice } from "models/storeModel";
import CircularProgressWithSubtitle from "../CircularProgressWithSubtitle";
import BitcoinPunishedPage from "./done/BitcoinPunishedPage";
import { SyncingMoneroWalletPage } from "./in_progress/SyncingMoneroWalletPage";
import BitcoinRefundedPage from "./done/BitcoinRefundedPage";
import XmrRedeemInMempoolPage from "./done/XmrRedeemInMempoolPage";
import ProcessExitedPage from "./exited/ProcessExitedPage";
import BitcoinCancelledPage from "./in_progress/BitcoinCancelledPage";
import BitcoinLockTxInMempoolPage from "./in_progress/BitcoinLockTxInMempoolPage";
import BitcoinRedeemedPage from "./in_progress/BitcoinRedeemedPage";
import ReceivedQuotePage from "./in_progress/ReceivedQuotePage";
import StartedPage from "./in_progress/StartedPage";
import XmrLockedPage from "./in_progress/XmrLockedPage";
import XmrLockTxInMempoolPage from "./in_progress/XmrLockInMempoolPage";
import InitiatedPage from "./init/InitiatedPage";
import InitPage from "./init/InitPage";
import WaitingForBitcoinDepositPage from "./init/WaitingForBitcoinDepositPage";
export default function SwapStatePage({
swapState,
state,
}: {
swapState: SwapState | null;
state: SwapSlice["state"];
}) {
// TODO: Reimplement this using tauri events
/*
const isSyncingMoneroWallet = useAppSelector(
(state) => state.rpc.state.moneroWallet.isSyncing,
);
@ -45,62 +30,57 @@ export default function SwapStatePage({
if (isSyncingMoneroWallet) {
return <SyncingMoneroWalletPage />;
}
*/
if (swapState === null) {
if (state === null) {
return <InitPage />;
}
if (isSwapStateInitiated(swapState)) {
return <InitiatedPage />;
switch (state.curr.type) {
case "Initiated":
return <InitiatedPage />;
case "ReceivedQuote":
return <ReceivedQuotePage />;
case "WaitingForBtcDeposit":
return <WaitingForBitcoinDepositPage {...state.curr.content} />;
case "Started":
return <StartedPage {...state.curr.content} />;
case "BtcLockTxInMempool":
return <BitcoinLockTxInMempoolPage {...state.curr.content} />;
case "XmrLockTxInMempool":
return <XmrLockTxInMempoolPage {...state.curr.content} />;
case "XmrLocked":
return <XmrLockedPage />;
case "BtcRedeemed":
return <BitcoinRedeemedPage />;
case "XmrRedeemInMempool":
return <XmrRedeemInMempoolPage {...state.curr.content} />;
case "BtcCancelled":
return <BitcoinCancelledPage />;
case "BtcRefunded":
return <BitcoinRefundedPage {...state.curr.content} />;
case "BtcPunished":
return <BitcoinPunishedPage />;
case "AttemptingCooperativeRedeem":
return (
<CircularProgressWithSubtitle description="Attempting to redeem the Monero with the help of the other party" />
);
case "CooperativeRedeemAccepted":
return (
<CircularProgressWithSubtitle description="The other party is cooperating with us to redeem the Monero..." />
);
case "CooperativeRedeemRejected":
return <BitcoinPunishedPage />;
case "Released":
return <ProcessExitedPage prevState={state.prev} swapId={state.swapId} />;
default:
// TODO: Use this when we have all states implemented, ensures we don't forget to implement a state
// return exhaustiveGuard(state.curr.type);
return (
<Box>
No information to display
<br />
State: {JSON.stringify(state, null, 4)}
</Box>
);
}
if (isSwapStateReceivedQuote(swapState)) {
return <ReceivedQuotePage />;
}
if (isSwapStateWaitingForBtcDeposit(swapState)) {
return <WaitingForBitcoinDepositPage state={swapState} />;
}
if (isSwapStateStarted(swapState)) {
return <StartedPage state={swapState} />;
}
if (isSwapStateBtcLockInMempool(swapState)) {
return <BitcoinLockTxInMempoolPage state={swapState} />;
}
if (isSwapStateXmrLockInMempool(swapState)) {
return <XmrLockTxInMempoolPage state={swapState} />;
}
if (isSwapStateXmrLocked(swapState)) {
return <XmrLockedPage />;
}
if (isSwapStateBtcRedemeed(swapState)) {
return <BitcoinRedeemedPage />;
}
if (isSwapStateXmrRedeemInMempool(swapState)) {
return <XmrRedeemInMempoolPage state={swapState} />;
}
if (isSwapStateBtcCancelled(swapState)) {
return <BitcoinCancelledPage />;
}
if (isSwapStateBtcRefunded(swapState)) {
return <BitcoinRefundedPage state={swapState} />;
}
if (isSwapStateBtcPunished(swapState)) {
return <BitcoinPunishedPage />;
}
if (isSwapStateProcessExited(swapState)) {
return <ProcessExitedPage state={swapState} />;
}
console.error(
`No swap state page found for swap state State: ${JSON.stringify(
swapState,
null,
4,
)}`,
);
return (
<Box>
No information to display
<br />
State: ${JSON.stringify(swapState, null, 4)}
</Box>
);
}

View File

@ -1,14 +1,13 @@
import { Box, DialogContentText } from "@material-ui/core";
import { SwapStateBtcRefunded } from "models/storeModel";
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import { useActiveSwapInfo } from "store/hooks";
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
export default function BitcoinRefundedPage({
state,
}: {
state: SwapStateBtcRefunded | null;
}) {
btc_refund_txid,
}: TauriSwapProgressEventContent<"BtcRefunded">) {
// TODO: Reimplement this using Tauri
const swap = useActiveSwapInfo();
const additionalContent = swap
? `Refund address: ${swap.btc_refund_address}`
@ -28,14 +27,15 @@ export default function BitcoinRefundedPage({
gap: "0.5rem",
}}
>
{state && (
<BitcoinTransactionInfoBox
title="Bitcoin Refund Transaction"
txId={state.bobBtcRefundTxId}
loading={false}
additionalContent={additionalContent}
/>
)}
{
// TODO: We should display the confirmation count here
}
<BitcoinTransactionInfoBox
title="Bitcoin Refund Transaction"
txId={btc_refund_txid}
loading={false}
additionalContent={additionalContent}
/>
<FeedbackInfoBox />
</Box>
</Box>

View File

@ -1,23 +1,18 @@
import { Box, DialogContentText } from "@material-ui/core";
import { SwapStateXmrRedeemInMempool } from "models/storeModel";
import { useActiveSwapInfo } from "store/hooks";
import { getSwapXmrAmount } from "models/rpcModel";
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
type XmrRedeemInMempoolPageProps = {
state: SwapStateXmrRedeemInMempool | null;
};
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
export default function XmrRedeemInMempoolPage({
state,
}: XmrRedeemInMempoolPageProps) {
const swap = useActiveSwapInfo();
const additionalContent = swap
? `This transaction transfers ${getSwapXmrAmount(swap).toFixed(6)} XMR to ${
state?.bobXmrRedeemAddress
}`
: null;
xmr_redeem_address,
xmr_redeem_txid,
}: TauriSwapProgressEventContent<"XmrRedeemInMempool">) {
// TODO: Reimplement this using Tauri
//const additionalContent = swap
// ? `This transaction transfers ${getSwapXmrAmount(swap).toFixed(6)} XMR to ${
// state?.bobXmrRedeemAddress
// }`
// : null;
return (
<Box>
@ -32,16 +27,12 @@ export default function XmrRedeemInMempoolPage({
gap: "0.5rem",
}}
>
{state && (
<>
<MoneroTransactionInfoBox
title="Monero Redeem Transaction"
txId={state.bobXmrRedeemTxId}
additionalContent={additionalContent}
loading={false}
/>
</>
)}
<MoneroTransactionInfoBox
title="Monero Redeem Transaction"
txId={xmr_redeem_txid}
additionalContent={`The funds have been sent to the address ${xmr_redeem_address}`}
loading={false}
/>
<FeedbackInfoBox />
</Box>
</Box>

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 { SwapStateProcessExited } from "models/storeModel";
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import CliLogsBox from "../../../../other/RenderedCliLog";
export default function ProcessExitedAndNotDonePage({
state,
@ -18,7 +18,7 @@ export default function ProcessExitedAndNotDonePage({
const hasRpcError = state.rpcError != null;
const hasSwap = swap != null;
let messages = [];
const messages = [];
messages.push(
isCancelRefund

View File

@ -1,47 +1,41 @@
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";
// eslint-disable-next-line import/no-cycle
import { TauriSwapProgressEvent } from "models/tauriModel";
import SwapStatePage from "../SwapStatePage";
import BitcoinRefundedPage from "../done/BitcoinRefundedPage";
import ProcessExitedAndNotDonePage from "./ProcessExitedAndNotDonePage";
type ProcessExitedPageProps = {
state: SwapStateProcessExited;
};
export default function ProcessExitedPage({ state }: ProcessExitedPageProps) {
const swap = useActiveSwapInfo();
// If we have a swap state, for a "done" state we should use it to display additional information that can't be extracted from the database
export default function ProcessExitedPage({
prevState,
swapId,
}: {
prevState: TauriSwapProgressEvent | null;
swapId: string;
}) {
// 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)
if (
isSwapStateXmrRedeemInMempool(state.prevState) ||
isSwapStateBtcRefunded(state.prevState) ||
isSwapStateBtcPunished(state.prevState)
prevState != null &&
(prevState.type === "XmrRedeemInMempool" ||
prevState.type === "BtcRefunded" ||
prevState.type === "BtcPunished")
) {
return <SwapStatePage swapState={state.prevState} />;
return (
<SwapStatePage
state={{
curr: prevState,
prev: null,
swapId,
}}
/>
);
}
// 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.state_name === SwapStateName.XmrRedeemed) {
return <XmrRedeemInMempoolPage state={null} />;
}
if (swap.state_name === SwapStateName.BtcRefunded) {
return <BitcoinRefundedPage state={null} />;
}
if (swap.state_name === SwapStateName.BtcPunished) {
return <BitcoinPunishedPage />;
}
}
// TODO: Display something useful here
return (
<>
If the swap is not a "done" state (or we don't have a db state because the
swap did complete the SwapSetup yet) we should tell the user and show logs
Not implemented yet
</>
);
// 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} />;
// return <ProcessExitedAndNotDonePage state={state} />;
}

View File

@ -1,19 +1,16 @@
import { Box, DialogContentText } from "@material-ui/core";
import { SwapStateBtcLockInMempool } from "models/storeModel";
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import SwapMightBeCancelledAlert from "../../../../alert/SwapMightBeCancelledAlert";
type BitcoinLockTxInMempoolPageProps = {
state: SwapStateBtcLockInMempool;
};
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
export default function BitcoinLockTxInMempoolPage({
state,
}: BitcoinLockTxInMempoolPageProps) {
btc_lock_confirmations,
btc_lock_txid,
}: TauriSwapProgressEventContent<"BtcLockTxInMempool">) {
return (
<Box>
<SwapMightBeCancelledAlert
bobBtcLockTxConfirmations={state.bobBtcLockTxConfirmations}
bobBtcLockTxConfirmations={btc_lock_confirmations}
/>
<DialogContentText>
The Bitcoin lock transaction has been published. The swap will proceed
@ -22,14 +19,14 @@ export default function BitcoinLockTxInMempoolPage({
</DialogContentText>
<BitcoinTransactionInfoBox
title="Bitcoin Lock Transaction"
txId={state.bobBtcLockTxId}
txId={btc_lock_txid}
loading
additionalContent={
<>
Most swap providers require one confirmation before locking their
Monero
<br />
Confirmations: {state.bobBtcLockTxConfirmations}
Confirmations: {btc_lock_confirmations}
</>
}
/>

View File

@ -1,16 +1,19 @@
import { SwapStateStarted } from "models/storeModel";
import { BitcoinAmount } from "renderer/components/other/Units";
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import { SatsAmount } from "renderer/components/other/Units";
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function StartedPage({ state }: { state: SwapStateStarted }) {
const description = state.txLockDetails ? (
<>
Locking <BitcoinAmount amount={state.txLockDetails.amount} /> with a
network fee of <BitcoinAmount amount={state.txLockDetails.fees} />
</>
) : (
"Locking Bitcoin"
export default function StartedPage({
btc_lock_amount,
btc_tx_lock_fee,
}: TauriSwapProgressEventContent<"Started">) {
return (
<CircularProgressWithSubtitle
description={
<>
Locking <SatsAmount amount={btc_lock_amount} /> with a network fee of{" "}
<SatsAmount amount={btc_tx_lock_fee} />
</>
}
/>
);
return <CircularProgressWithSubtitle description={description} />;
}

View File

@ -1,15 +1,12 @@
import { Box, DialogContentText } from "@material-ui/core";
import { SwapStateXmrLockInMempool } from "models/storeModel";
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
type XmrLockTxInMempoolPageProps = {
state: SwapStateXmrLockInMempool;
};
export default function XmrLockTxInMempoolPage({
state,
}: XmrLockTxInMempoolPageProps) {
const additionalContent = `Confirmations: ${state.aliceXmrLockTxConfirmations}/10`;
xmr_lock_tx_confirmations,
xmr_lock_txid,
}: TauriSwapProgressEventContent<"XmrLockTxInMempool">) {
const additionalContent = `Confirmations: ${xmr_lock_tx_confirmations}/10`;
return (
<Box>
@ -20,7 +17,7 @@ export default function XmrLockTxInMempoolPage({
<MoneroTransactionInfoBox
title="Monero Lock Transaction"
txId={state.aliceXmrLockTxId}
txId={xmr_lock_txid}
additionalContent={additionalContent}
loading
/>

View File

@ -1,8 +1,8 @@
import { useState } from "react";
import { Box, makeStyles, TextField, Typography } from "@material-ui/core";
import { SwapStateWaitingForBtcDeposit } from "models/storeModel";
import { BidQuote } from "models/tauriModel";
import { useState } from "react";
import { useAppSelector } from "store/hooks";
import { satsToBtc } from "utils/conversionUtils";
import { btcToSats, satsToBtc } from "utils/conversionUtils";
import { MoneroAmount } from "../../../../other/Units";
const MONERO_FEE = 0.000016;
@ -29,42 +29,42 @@ function calcBtcAmountWithoutFees(amount: number, fees: number) {
}
export default function DepositAmountHelper({
state,
min_deposit_until_swap_will_start,
max_deposit_until_maximum_amount_is_reached,
min_bitcoin_lock_tx_fee,
quote,
}: {
state: SwapStateWaitingForBtcDeposit;
min_deposit_until_swap_will_start: number;
max_deposit_until_maximum_amount_is_reached: number;
min_bitcoin_lock_tx_fee: number;
quote: BidQuote;
}) {
const classes = useStyles();
const [amount, setAmount] = useState(state.minDeposit);
const [amount, setAmount] = useState(min_deposit_until_swap_will_start);
const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0;
function getTotalAmountAfterDeposit() {
return amount + satsToBtc(bitcoinBalance);
return amount + bitcoinBalance;
}
function hasError() {
return (
amount < state.minDeposit ||
getTotalAmountAfterDeposit() > state.maximumAmount
amount < min_deposit_until_swap_will_start ||
getTotalAmountAfterDeposit() > max_deposit_until_maximum_amount_is_reached
);
}
function calcXMRAmount(): number | null {
if (Number.isNaN(amount)) return null;
if (hasError()) return null;
if (state.price == null) return null;
console.log(
`Calculating calcBtcAmountWithoutFees(${getTotalAmountAfterDeposit()}, ${
state.minBitcoinLockTxFee
}) / ${state.price} - ${MONERO_FEE}`,
);
if (quote.price == null) return null;
return (
calcBtcAmountWithoutFees(
getTotalAmountAfterDeposit(),
state.minBitcoinLockTxFee,
min_bitcoin_lock_tx_fee,
) /
state.price -
quote.price -
MONERO_FEE
);
}
@ -75,9 +75,9 @@ export default function DepositAmountHelper({
Depositing {bitcoinBalance > 0 && <>another</>}
</Typography>
<TextField
error={hasError()}
value={amount}
onChange={(e) => setAmount(parseFloat(e.target.value))}
error={!!hasError()}
value={satsToBtc(amount)}
onChange={(e) => setAmount(btcToSats(parseFloat(e.target.value)))}
size="small"
type="number"
className={classes.textField}

View File

@ -1,5 +1,5 @@
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
import { MoneroWalletRpcUpdateState } from "../../../../../../models/storeModel";
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function DownloadingMoneroWalletRpcPage({
updateState,

View File

@ -1,12 +1,12 @@
import { Box, DialogContentText, makeStyles } from "@material-ui/core";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import { useState } from "react";
import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField";
import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { buyXmr } from "renderer/rpc";
import { useAppSelector } from "store/hooks";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import { isTestnet } from "store/config";
import RemainingFundsWillBeUsedAlert from "../../../../alert/RemainingFundsWillBeUsedAlert";
import IpcInvokeButton from "../../../../IpcInvokeButton";
const useStyles = makeStyles((theme) => ({
initButton: {
@ -29,6 +29,10 @@ export default function InitPage() {
(state) => state.providers.selectedProvider,
);
async function init() {
await buyXmr(selectedProvider, refundAddress, redeemAddress);
}
return (
<Box>
<RemainingFundsWillBeUsedAlert />
@ -58,7 +62,7 @@ export default function InitPage() {
/>
</Box>
<IpcInvokeButton
<PromiseInvokeButton
disabled={
!refundAddressValid || !redeemAddressValid || !selectedProvider
}
@ -67,12 +71,10 @@ export default function InitPage() {
size="large"
className={classes.initButton}
endIcon={<PlayArrowIcon />}
ipcChannel="spawn-buy-xmr"
ipcArgs={[selectedProvider, redeemAddress, refundAddress]}
displayErrorSnackbar={false}
onClick={init}
>
Start swap
</IpcInvokeButton>
</PromiseInvokeButton>
</Box>
);
}

View File

@ -1,5 +1,5 @@
import { useAppSelector } from "store/hooks";
import { SwapSpawnType } from "models/cliModel";
import { useAppSelector } from "store/hooks";
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function InitiatedPage() {

View File

@ -1,14 +1,10 @@
import { Box, makeStyles, Typography } from "@material-ui/core";
import { SwapStateWaitingForBtcDeposit } from "models/storeModel";
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import { useAppSelector } from "store/hooks";
import DepositAddressInfoBox from "../../DepositAddressInfoBox";
import BitcoinIcon from "../../../../icons/BitcoinIcon";
import { MoneroSatsExchangeRate, SatsAmount } from "../../../../other/Units";
import DepositAddressInfoBox from "../../DepositAddressInfoBox";
import DepositAmountHelper from "./DepositAmountHelper";
import {
BitcoinAmount,
MoneroBitcoinExchangeRate,
SatsAmount,
} from "../../../../other/Units";
const useStyles = makeStyles((theme) => ({
amountHelper: {
@ -23,13 +19,13 @@ const useStyles = makeStyles((theme) => ({
},
}));
type WaitingForBtcDepositPageProps = {
state: SwapStateWaitingForBtcDeposit;
};
export default function WaitingForBtcDepositPage({
state,
}: WaitingForBtcDepositPageProps) {
deposit_address,
min_deposit_until_swap_will_start,
max_deposit_until_maximum_amount_is_reached,
min_bitcoin_lock_tx_fee,
quote,
}: TauriSwapProgressEventContent<"WaitingForBtcDeposit">) {
const classes = useStyles();
const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0;
@ -38,7 +34,7 @@ export default function WaitingForBtcDepositPage({
<Box>
<DepositAddressInfoBox
title="Bitcoin Deposit Address"
address={state.depositAddress}
address={deposit_address}
additionalContent={
<Box className={classes.additionalContent}>
<Typography variant="subtitle2">
@ -51,9 +47,11 @@ export default function WaitingForBtcDepositPage({
) : null}
<li>
Send any amount between{" "}
<BitcoinAmount amount={state.minDeposit} /> and{" "}
<BitcoinAmount amount={state.maxDeposit} /> to the address
above
<SatsAmount amount={min_deposit_until_swap_will_start} /> and{" "}
<SatsAmount
amount={max_deposit_until_maximum_amount_is_reached}
/>{" "}
to the address above
{bitcoinBalance > 0 && (
<> (on top of the already deposited funds)</>
)}
@ -61,11 +59,11 @@ export default function WaitingForBtcDepositPage({
<li>
All Bitcoin sent to this this address will converted into
Monero at an exchance rate of{" "}
<MoneroBitcoinExchangeRate rate={state.price} />
<MoneroSatsExchangeRate rate={quote.price} />
</li>
<li>
The network fee of{" "}
<BitcoinAmount amount={state.minBitcoinLockTxFee} /> will
<SatsAmount amount={min_bitcoin_lock_tx_fee} /> will
automatically be deducted from the deposited coins
</li>
<li>
@ -74,7 +72,16 @@ export default function WaitingForBtcDepositPage({
</li>
</ul>
</Typography>
<DepositAmountHelper state={state} />
<DepositAmountHelper
min_deposit_until_swap_will_start={
min_deposit_until_swap_will_start
}
max_deposit_until_maximum_amount_is_reached={
max_deposit_until_maximum_amount_is_reached
}
min_bitcoin_lock_tx_fee={min_bitcoin_lock_tx_fee}
quote={quote}
/>
</Box>
}
icon={<BitcoinIcon />}

View File

@ -1,14 +1,10 @@
import { Button, Dialog, DialogActions } from "@material-ui/core";
import { useAppDispatch, useIsRpcEndpointBusy } from "store/hooks";
import { RpcMethod } from "models/rpcModel";
import { rpcResetWithdrawTxId } from "store/features/rpcSlice";
import WithdrawStatePage from "./WithdrawStatePage";
import DialogHeader from "../DialogHeader";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { useState } from "react";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { withdrawBtc } from "renderer/rpc";
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage";
import DialogHeader from "../DialogHeader";
import AddressInputPage from "./pages/AddressInputPage";
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage";
import WithdrawDialogContent from "./WithdrawDialogContent";
export default function WithdrawDialog({
@ -42,10 +38,7 @@ export default function WithdrawDialog({
setWithdrawAddressValid={setWithdrawAddressValid}
/>
) : (
<BtcTxInMempoolPageContent
withdrawTxId={withdrawTxId}
onCancel={onCancel}
/>
<BtcTxInMempoolPageContent withdrawTxId={withdrawTxId} />
)}
</WithdrawDialogContent>
<DialogActions>

View File

@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { Box, DialogContent, makeStyles } from "@material-ui/core";
import { ReactNode } from "react";
import WithdrawStepper from "./WithdrawStepper";
const useStyles = makeStyles({

View File

@ -1,5 +1,4 @@
import { Step, StepLabel, Stepper } from "@material-ui/core";
import { useAppSelector, useIsRpcEndpointBusy } from "store/hooks";
function getActiveStep(isPending: boolean, withdrawTxId: string | null) {
if (isPending) {

View File

@ -1,8 +1,5 @@
import { useState } from "react";
import { Button, DialogActions, DialogContentText } from "@material-ui/core";
import { DialogContentText } from "@material-ui/core";
import BitcoinAddressTextField from "../../../inputs/BitcoinAddressTextField";
import WithdrawDialogContent from "../WithdrawDialogContent";
import IpcInvokeButton from "../../../IpcInvokeButton";
export default function AddressInputPage({
withdrawAddress,

View File

@ -1,13 +1,10 @@
import { Button, DialogActions, DialogContentText } from "@material-ui/core";
import { DialogContentText } from "@material-ui/core";
import BitcoinTransactionInfoBox from "../../swap/BitcoinTransactionInfoBox";
import WithdrawDialogContent from "../WithdrawDialogContent";
export default function BtcTxInMempoolPageContent({
withdrawTxId,
onCancel,
}: {
withdrawTxId: string;
onCancel: () => void;
}) {
return (
<>

View File

@ -1,6 +1,6 @@
import { Drawer, makeStyles, Box } from "@material-ui/core";
import NavigationHeader from "./NavigationHeader";
import { Box, Drawer, makeStyles } from "@material-ui/core";
import NavigationFooter from "./NavigationFooter";
import NavigationHeader from "./NavigationHeader";
export const drawerWidth = 240;

View File

@ -1,13 +1,12 @@
import RedditIcon from "@material-ui/icons/Reddit";
import GitHubIcon from "@material-ui/icons/GitHub";
import { Box, makeStyles } from "@material-ui/core";
import LinkIconButton from "../icons/LinkIconButton";
import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert";
import GitHubIcon from "@material-ui/icons/GitHub";
import RedditIcon from "@material-ui/icons/Reddit";
import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert";
import RpcStatusAlert from "../alert/RpcStatusAlert";
import DiscordIcon from "../icons/DiscordIcon";
import { DISCORD_URL } from "../pages/help/ContactInfoBox";
import MoneroWalletRpcUpdatingAlert from "../alert/MoneroWalletRpcUpdatingAlert";
import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert";
import DiscordIcon from "../icons/DiscordIcon";
import LinkIconButton from "../icons/LinkIconButton";
import { DISCORD_URL } from "../pages/help/ContactInfoBox";
const useStyles = makeStyles((theme) => ({
outer: {
@ -29,7 +28,11 @@ export default function NavigationFooter() {
<Box className={classes.outer}>
<FundsLeftInWalletAlert />
<UnfinishedSwapsAlert />
<RpcStatusAlert />
{
// TODO: Uncomment when we have implemented a way for the UI to be displayed before the context has been initialized
// <RpcStatusAlert />
}
<MoneroWalletRpcUpdatingAlert />
<Box className={classes.linksOuter}>
<LinkIconButton url="https://reddit.com/r/unstoppableswap">

View File

@ -1,8 +1,8 @@
import { Box, List } from "@material-ui/core";
import SwapHorizOutlinedIcon from "@material-ui/icons/SwapHorizOutlined";
import HistoryOutlinedIcon from "@material-ui/icons/HistoryOutlined";
import AccountBalanceWalletIcon from "@material-ui/icons/AccountBalanceWallet";
import HelpOutlineIcon from "@material-ui/icons/HelpOutline";
import HistoryOutlinedIcon from "@material-ui/icons/HistoryOutlined";
import SwapHorizOutlinedIcon from "@material-ui/icons/SwapHorizOutlined";
import RouteListItemIconButton from "./RouteListItemIconButton";
import UnfinishedSwapsBadge from "./UnfinishedSwapsCountBadge";

View File

@ -1,6 +1,6 @@
import { ListItem, ListItemIcon, ListItemText } from "@material-ui/core";
import { ReactNode } from "react";
import { useNavigate } from "react-router-dom";
import { ListItem, ListItemIcon, ListItemText } from "@material-ui/core";
export default function RouteListItemIconButton({
name,

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import { Box, IconButton, TextField } from "@material-ui/core";
import SearchIcon from "@material-ui/icons/Search";
import CloseIcon from "@material-ui/icons/Close";
import SearchIcon from "@material-ui/icons/Search";
import { useState } from "react";
export function ExpandableSearchBox({
query,

View File

@ -1,7 +1,7 @@
import TreeView from "@material-ui/lab/TreeView";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import TreeItem from "@material-ui/lab/TreeItem";
import TreeView from "@material-ui/lab/TreeView";
import ScrollablePaperTextBox from "./ScrollablePaperTextBox";
interface JsonTreeViewProps {

View File

@ -1,6 +1,6 @@
import React from "react";
import Button, { ButtonProps } from "@material-ui/core/Button";
import CircularProgress from "@material-ui/core/CircularProgress";
import React from "react";
interface LoadingButtonProps extends ButtonProps {
loading: boolean;

View File

@ -1,6 +1,6 @@
import { Box, Chip, Typography } from "@material-ui/core";
import { useMemo, useState } from "react";
import { CliLog } from "models/cliModel";
import { useMemo, useState } from "react";
import { logsToRawString } from "utils/parseUtils";
import ScrollablePaperTextBox from "./ScrollablePaperTextBox";

View File

@ -1,9 +1,9 @@
import { Box, Divider, IconButton, Paper, Typography } from "@material-ui/core";
import { ReactNode, useRef } from "react";
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp";
import { ReactNode, useRef } from "react";
import { VList, VListHandle } from "virtua";
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
import { ExpandableSearchBox } from "./ExpandableSearchBox";
const MIN_HEIGHT = "10rem";

View File

@ -1,6 +1,6 @@
import { piconerosToXmr, satsToBtc } from "utils/conversionUtils";
import { Tooltip } from "@material-ui/core";
import { useAppSelector } from "store/hooks";
import { piconerosToXmr, satsToBtc } from "utils/conversionUtils";
type Amount = number | null | undefined;
@ -64,10 +64,27 @@ export function MoneroAmount({ amount }: { amount: Amount }) {
);
}
export function MoneroBitcoinExchangeRate({ rate }: { rate: Amount }) {
export function MoneroBitcoinExchangeRate(
state: { rate: Amount } | { satsAmount: number; piconeroAmount: number },
) {
if ("rate" in state) {
return (
<AmountWithUnit amount={state.rate} unit="BTC/XMR" fixedPrecision={8} />
);
}
const rate =
satsToBtc(state.satsAmount) / piconerosToXmr(state.piconeroAmount);
return <AmountWithUnit amount={rate} unit="BTC/XMR" fixedPrecision={8} />;
}
export function MoneroSatsExchangeRate({ rate }: { rate: Amount }) {
const btc = satsToBtc(rate);
return <AmountWithUnit amount={btc} unit="BTC/XMR" fixedPrecision={6} />;
}
export function SatsAmount({ amount }: { amount: Amount }) {
const btcAmount = amount == null ? null : satsToBtc(amount);
return <BitcoinAmount amount={btcAmount} />;

View File

@ -1,6 +1,6 @@
import { Typography } from "@material-ui/core";
import DepositAddressInfoBox from "../../modal/swap/DepositAddressInfoBox";
import MoneroIcon from "../../icons/MoneroIcon";
import DepositAddressInfoBox from "../../modal/swap/DepositAddressInfoBox";
const XMR_DONATE_ADDRESS =
"87jS4C7ngk9EHdqFFuxGFgg8AyH63dRUoULshWDybFJaP75UA89qsutG5B1L1QTc4w228nsqsv8EjhL7bz8fB3611Mh98mg";

View File

@ -1,7 +1,7 @@
import { Button, Typography } from "@material-ui/core";
import { useState } from "react";
import InfoBox from "../../modal/swap/InfoBox";
import FeedbackDialog from "../../modal/feedback/FeedbackDialog";
import InfoBox from "../../modal/swap/InfoBox";
export default function FeedbackInfoBox() {
const [showDialog, setShowDialog] = useState(false);

View File

@ -1,9 +1,9 @@
import { Box, makeStyles } from "@material-ui/core";
import ContactInfoBox from "./ContactInfoBox";
import FeedbackInfoBox from "./FeedbackInfoBox";
import DonateInfoBox from "./DonateInfoBox";
import TorInfoBox from "./TorInfoBox";
import FeedbackInfoBox from "./FeedbackInfoBox";
import RpcControlBox from "./RpcControlBox";
import TorInfoBox from "./TorInfoBox";
const useStyles = makeStyles((theme) => ({
outer: {

View File

@ -1,12 +1,12 @@
import { Box, makeStyles } from "@material-ui/core";
import IpcInvokeButton from "renderer/components/IpcInvokeButton";
import { useAppSelector } from "store/hooks";
import StopIcon from "@material-ui/icons/Stop";
import FolderOpenIcon from "@material-ui/icons/FolderOpen";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import StopIcon from "@material-ui/icons/Stop";
import { RpcProcessStateType } from "models/rpcModel";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { useAppSelector } from "store/hooks";
import InfoBox from "../../modal/swap/InfoBox";
import CliLogsBox from "../../other/RenderedCliLog";
import FolderOpenIcon from "@material-ui/icons/FolderOpen";
const useStyles = makeStyles((theme) => ({
actionsOuter: {
@ -36,34 +36,34 @@ export default function RpcControlBox() {
}
additionalContent={
<Box className={classes.actionsOuter}>
<IpcInvokeButton
<PromiseInvokeButton
variant="contained"
ipcChannel="spawn-start-rpc"
ipcArgs={[]}
endIcon={<PlayArrowIcon />}
disabled={isRunning}
requiresRpc={false}
onClick={() => {
throw new Error("Not implemented");
}}
>
Start Daemon
</IpcInvokeButton>
<IpcInvokeButton
</PromiseInvokeButton>
<PromiseInvokeButton
variant="contained"
ipcChannel="stop-cli"
ipcArgs={[]}
endIcon={<StopIcon />}
disabled={!isRunning}
requiresRpc={false}
onClick={() => {
throw new Error("Not implemented");
}}
>
Stop Daemon
</IpcInvokeButton>
<IpcInvokeButton
ipcChannel="open-data-dir-in-file-explorer"
ipcArgs={[]}
</PromiseInvokeButton>
<PromiseInvokeButton
endIcon={<FolderOpenIcon />}
requiresRpc={false}
isIconButton
size="small"
tooltipTitle="Open the data directory of the Swap Daemon in your file explorer"
onClick={() => {
throw new Error("Not implemented");
}}
/>
</Box>
}

View File

@ -1,8 +1,8 @@
import { Box, makeStyles, Typography } from "@material-ui/core";
import IpcInvokeButton from "renderer/components/IpcInvokeButton";
import { useAppSelector } from "store/hooks";
import StopIcon from "@material-ui/icons/Stop";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import StopIcon from "@material-ui/icons/Stop";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { useAppSelector } from "store/hooks";
import InfoBox from "../../modal/swap/InfoBox";
import CliLogsBox from "../../other/RenderedCliLog";
@ -42,26 +42,26 @@ export default function TorInfoBox() {
}
additionalContent={
<Box className={classes.actionsOuter}>
<IpcInvokeButton
<PromiseInvokeButton
variant="contained"
disabled={isTorRunning}
ipcChannel="spawn-tor"
ipcArgs={[]}
endIcon={<PlayArrowIcon />}
requiresRpc={false}
onClick={() => {
throw new Error("Not implemented");
}}
>
Start Tor
</IpcInvokeButton>
<IpcInvokeButton
</PromiseInvokeButton>
<PromiseInvokeButton
variant="contained"
disabled={!isTorRunning}
ipcChannel="stop-tor"
ipcArgs={[]}
endIcon={<StopIcon />}
requiresRpc={false}
onClick={() => {
throw new Error("Not implemented");
}}
>
Stop Tor
</IpcInvokeButton>
</PromiseInvokeButton>
</Box>
}
icon={null}

View File

@ -1,11 +1,11 @@
import { Typography } from "@material-ui/core";
import { useIsSwapRunning } from "store/hooks";
import HistoryTable from "./table/HistoryTable";
import SwapDialog from "../../modal/swap/SwapDialog";
import { useAppSelector } from "store/hooks";
import SwapTxLockAlertsBox from "../../alert/SwapTxLockAlertsBox";
import SwapDialog from "../../modal/swap/SwapDialog";
import HistoryTable from "./table/HistoryTable";
export default function HistoryPage() {
const showDialog = useIsSwapRunning();
const showDialog = useAppSelector((state) => state.swap.state !== null);
return (
<>

View File

@ -6,23 +6,14 @@ import {
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";
import {
getHumanReadableDbStateType,
getSwapBtcAmount,
getSwapXmrAmount,
GetSwapInfoResponse,
} from "../../../../../models/rpcModel";
import { GetSwapInfoResponse } from "models/tauriModel";
import { useState } from "react";
import { PiconeroAmount, SatsAmount } from "../../../other/Units";
import HistoryRowActions from "./HistoryRowActions";
import HistoryRowExpanded from "./HistoryRowExpanded";
import { BitcoinAmount, MoneroAmount } from "../../../other/Units";
type HistoryRowProps = {
swap: GetSwapInfoResponse;
};
const useStyles = makeStyles((theme) => ({
amountTransferContainer: {
@ -43,17 +34,14 @@ function AmountTransfer({
return (
<Box className={classes.amountTransferContainer}>
<BitcoinAmount amount={btcAmount} />
<SatsAmount amount={btcAmount} />
<ArrowForwardIcon />
<MoneroAmount amount={xmrAmount} />
<PiconeroAmount amount={xmrAmount} />
</Box>
);
}
export default function HistoryRow({ swap }: HistoryRowProps) {
const btcAmount = getSwapBtcAmount(swap);
const xmrAmount = getSwapXmrAmount(swap);
export default function HistoryRow(swap: GetSwapInfoResponse) {
const [expanded, setExpanded] = useState(false);
return (
@ -64,13 +52,16 @@ export default function HistoryRow({ swap }: HistoryRowProps) {
{expanded ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell>{swap.swap_id.substring(0, 5)}...</TableCell>
<TableCell>{swap.swap_id}</TableCell>
<TableCell>
<AmountTransfer xmrAmount={xmrAmount} btcAmount={btcAmount} />
<AmountTransfer
xmrAmount={swap.xmr_amount}
btcAmount={swap.btc_amount}
/>
</TableCell>
<TableCell>{getHumanReadableDbStateType(swap.state_name)}</TableCell>
<TableCell>{swap.state_name.toString()}</TableCell>
<TableCell>
<HistoryRowActions swap={swap} />
<HistoryRowActions {...swap} />
</TableCell>
</TableRow>

View File

@ -1,68 +1,65 @@
import { Tooltip } from "@material-ui/core";
import Button, { ButtonProps } from "@material-ui/core/Button/Button";
import { ButtonProps } from "@material-ui/core/Button/Button";
import { green, red } from "@material-ui/core/colors";
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 } from "models/tauriModel";
import {
GetSwapInfoResponse,
SwapStateName,
isSwapStateNamePossiblyCancellableSwap,
isSwapStateNamePossiblyRefundableSwap,
} from "../../../../../models/rpcModel";
BobStateName,
GetSwapInfoResponseExt,
isBobStateNamePossiblyCancellableSwap,
isBobStateNamePossiblyRefundableSwap,
} from "models/tauriModelExt";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { resumeSwap } from "renderer/rpc";
export function SwapResumeButton({
swap,
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
}: ButtonProps & { swap: GetSwapInfoResponse }) {
return (
<IpcInvokeButton
<PromiseInvokeButton
variant="contained"
color="primary"
disabled={swap.completed}
ipcChannel="spawn-resume-swap"
ipcArgs={[swap.swap_id]}
endIcon={<PlayArrowIcon />}
requiresRpc
onClick={() => resumeSwap(swap.swap_id)}
{...props}
>
Resume
</IpcInvokeButton>
</PromiseInvokeButton>
);
}
export function SwapCancelRefundButton({
swap,
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
}: { swap: GetSwapInfoResponseExt } & ButtonProps) {
const cancelOrRefundable =
isSwapStateNamePossiblyCancellableSwap(swap.state_name) ||
isSwapStateNamePossiblyRefundableSwap(swap.state_name);
isBobStateNamePossiblyCancellableSwap(swap.state_name) ||
isBobStateNamePossiblyRefundableSwap(swap.state_name);
if (!cancelOrRefundable) {
return <></>;
}
return (
<IpcInvokeButton
ipcChannel="spawn-cancel-refund"
ipcArgs={[swap.swap_id]}
requiresRpc
<PromiseInvokeButton
displayErrorSnackbar={false}
{...props}
onClick={async () => {
// TODO: Implement this using the Tauri RPC
throw new Error("Not implemented");
}}
>
Attempt manual Cancel & Refund
</IpcInvokeButton>
</PromiseInvokeButton>
);
}
export default function HistoryRowActions({
swap,
}: {
swap: GetSwapInfoResponse;
}) {
if (swap.state_name === SwapStateName.XmrRedeemed) {
export default function HistoryRowActions(swap: GetSwapInfoResponse) {
if (swap.state_name === BobStateName.XmrRedeemed) {
return (
<Tooltip title="The swap is completed because you have redeemed the XMR">
<DoneIcon style={{ color: green[500] }} />
@ -70,7 +67,7 @@ export default function HistoryRowActions({
);
}
if (swap.state_name === SwapStateName.BtcRefunded) {
if (swap.state_name === BobStateName.BtcRefunded) {
return (
<Tooltip title="The swap is completed because your BTC have been refunded">
<DoneIcon style={{ color: green[500] }} />
@ -78,7 +75,9 @@ export default function HistoryRowActions({
);
}
if (swap.state_name === SwapStateName.BtcPunished) {
// TODO: Display a button here to attempt a cooperative redeem
// See this PR: https://github.com/UnstoppableSwap/unstoppableswap-gui/pull/212
if (swap.state_name === BobStateName.BtcPunished) {
return (
<Tooltip title="The swap is completed because you have been punished">
<ErrorIcon style={{ color: red[500] }} />

View File

@ -8,24 +8,15 @@ import {
TableContainer,
TableRow,
} from "@material-ui/core";
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
import { isTestnet } from "store/config";
import { GetSwapInfoResponse } from "models/tauriModel";
import {
getHumanReadableDbStateType,
getSwapBtcAmount,
getSwapExchangeRate,
getSwapTxFees,
getSwapXmrAmount,
GetSwapInfoResponse,
} from "../../../../../models/rpcModel";
import SwapLogFileOpenButton from "./SwapLogFileOpenButton";
import { SwapCancelRefundButton } from "./HistoryRowActions";
import { SwapMoneroRecoveryButton } from "./SwapMoneroRecoveryButton";
import {
BitcoinAmount,
MoneroAmount,
MoneroBitcoinExchangeRate,
PiconeroAmount,
SatsAmount,
} from "renderer/components/other/Units";
import { isTestnet } from "store/config";
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
import SwapLogFileOpenButton from "./SwapLogFileOpenButton";
const useStyles = makeStyles((theme) => ({
outer: {
@ -47,12 +38,6 @@ export default function HistoryRowExpanded({
}) {
const classes = useStyles();
const { seller, start_date: startDate } = swap;
const btcAmount = getSwapBtcAmount(swap);
const xmrAmount = getSwapXmrAmount(swap);
const txFees = getSwapTxFees(swap);
const exchangeRate = getSwapExchangeRate(swap);
return (
<Box className={classes.outer}>
<TableContainer>
@ -60,7 +45,7 @@ export default function HistoryRowExpanded({
<TableBody>
<TableRow>
<TableCell>Started on</TableCell>
<TableCell>{startDate}</TableCell>
<TableCell>{swap.start_date}</TableCell>
</TableRow>
<TableRow>
<TableCell>Swap ID</TableCell>
@ -68,38 +53,39 @@ export default function HistoryRowExpanded({
</TableRow>
<TableRow>
<TableCell>State Name</TableCell>
<TableCell>
{getHumanReadableDbStateType(swap.state_name)}
</TableCell>
<TableCell>{swap.state_name}</TableCell>
</TableRow>
<TableRow>
<TableCell>Monero Amount</TableCell>
<TableCell>
<MoneroAmount amount={xmrAmount} />
<PiconeroAmount amount={swap.xmr_amount} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Bitcoin Amount</TableCell>
<TableCell>
<BitcoinAmount amount={btcAmount} />
<SatsAmount amount={swap.btc_amount} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Exchange Rate</TableCell>
<TableCell>
<MoneroBitcoinExchangeRate rate={exchangeRate} />
<MoneroBitcoinExchangeRate
satsAmount={swap.btc_amount}
piconeroAmount={swap.xmr_amount}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Bitcoin Network Fees</TableCell>
<TableCell>
<BitcoinAmount amount={txFees} />
<SatsAmount amount={swap.tx_lock_fee} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Provider Address</TableCell>
<TableCell>
<Box>{seller.addresses.join(", ")}</Box>
<Box>{swap.seller.addresses.join(", ")}</Box>
</TableCell>
</TableRow>
<TableRow>
@ -122,12 +108,16 @@ export default function HistoryRowExpanded({
variant="outlined"
size="small"
/>
<SwapCancelRefundButton swap={swap} variant="contained" size="small" />
<SwapMoneroRecoveryButton
swap={swap}
variant="contained"
size="small"
/>
{/*
// TOOD: reimplement these buttons using Tauri
<SwapCancelRefundButton swap={swap} variant="contained" size="small" />
<SwapMoneroRecoveryButton
swap={swap}
variant="contained"
size="small"
/>
*/}
</Box>
</Box>
);

View File

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

View File

@ -1,4 +1,3 @@
import { ButtonProps } from "@material-ui/core/Button/Button";
import {
Button,
Dialog,
@ -6,9 +5,10 @@ import {
DialogContent,
DialogTitle,
} from "@material-ui/core";
import { useState } from "react";
import { ButtonProps } from "@material-ui/core/Button/Button";
import { CliLog } from "models/cliModel";
import IpcInvokeButton from "../../../IpcInvokeButton";
import { useState } from "react";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import CliLogsBox from "../../../other/RenderedCliLog";
export default function SwapLogFileOpenButton({
@ -19,16 +19,17 @@ export default function SwapLogFileOpenButton({
return (
<>
<IpcInvokeButton
ipcArgs={[swapId]}
ipcChannel="get-swap-logs"
<PromiseInvokeButton
onSuccess={(data) => {
setLogs(data as CliLog[]);
}}
onClick={async () => {
throw new Error("Not implemented");
}}
{...props}
>
view log
</IpcInvokeButton>
View log
</PromiseInvokeButton>
{logs && (
<Dialog open onClose={() => setLogs(null)} fullWidth maxWidth="lg">
<DialogTitle>Logs of swap {swapId}</DialogTitle>

View File

@ -1,4 +1,3 @@
import { ButtonProps } from "@material-ui/core/Button/Button";
import {
Box,
Button,
@ -8,17 +7,17 @@ import {
DialogContentText,
Link,
} from "@material-ui/core";
import { useAppDispatch, useAppSelector } from "store/hooks";
import { ButtonProps } from "@material-ui/core/Button/Button";
import { GetSwapInfoArgs } from "models/tauriModel";
import { rpcResetMoneroRecoveryKeys } from "store/features/rpcSlice";
import {
GetSwapInfoResponse,
isSwapMoneroRecoverable,
} from "../../../../../models/rpcModel";
import IpcInvokeButton from "../../../IpcInvokeButton";
import { useAppDispatch, useAppSelector } from "store/hooks";
import DialogHeader from "../../../modal/DialogHeader";
import ScrollablePaperTextBox from "../../../other/ScrollablePaperTextBox";
function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) {
function MoneroRecoveryKeysDialog() {
// TODO: Reimplement this using the new Tauri API
return null;
const dispatch = useAppDispatch();
const keys = useAppSelector((s) => s.rpc.state.moneroRecovery);
@ -96,24 +95,28 @@ function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) {
export function SwapMoneroRecoveryButton({
swap,
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
}: { swap: GetSwapInfoArgs } & ButtonProps) {
return <> </>;
/* TODO: Reimplement this using the new Tauri API
const isRecoverable = isSwapMoneroRecoverable(swap.state_name);
if (!isRecoverable) {
return <></>;
}
return (
<>
<IpcInvokeButton
ipcChannel="spawn-monero-recovery"
ipcArgs={[swap.swap_id]}
requiresRpc
<PromiseInvokeButton
onClick={async () => {
throw new Error("Not implemented");
}}
{...props}
>
Display Monero Recovery Keys
</IpcInvokeButton>
</PromiseInvokeButton>
<MoneroRecoveryKeysDialog swap={swap} />
</>
);
*/
}

View File

@ -1,6 +1,6 @@
import { Box, makeStyles } from "@material-ui/core";
import SwapWidget from "./SwapWidget";
import ApiAlertsBox from "./ApiAlertsBox";
import SwapWidget from "./SwapWidget";
const useStyles = makeStyles((theme) => ({
outer: {

View File

@ -1,27 +1,26 @@
import { ChangeEvent, useEffect, useState } from "react";
import {
makeStyles,
Box,
Paper,
Typography,
TextField,
LinearProgress,
Fab,
LinearProgress,
makeStyles,
Paper,
TextField,
Typography,
} from "@material-ui/core";
import InputAdornment from "@material-ui/core/InputAdornment";
import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward";
import SwapHorizIcon from "@material-ui/icons/SwapHoriz";
import { Alert } from "@material-ui/lab";
import { satsToBtc } from "utils/conversionUtils";
import { useAppSelector } from "store/hooks";
import { ExtendedProviderStatus } from "models/apiModel";
import { isSwapState } from "models/storeModel";
import SwapDialog from "../../modal/swap/SwapDialog";
import ProviderSelect from "../../modal/provider/ProviderSelect";
import { ChangeEvent, useEffect, useState } from "react";
import { useAppSelector } from "store/hooks";
import { satsToBtc } from "utils/conversionUtils";
import {
ListSellersDialogOpenButton,
ProviderSubmitDialogOpenButton,
} from "../../modal/provider/ProviderListDialog";
import ProviderSelect from "../../modal/provider/ProviderSelect";
import SwapDialog from "../../modal/swap/SwapDialog";
// After RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN failed reconnection attempts we can assume the public registry is down
const RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN = 1;
@ -84,9 +83,7 @@ function HasProviderSwapWidget({
}) {
const classes = useStyles();
const forceShowDialog = useAppSelector((state) =>
isSwapState(state.swap.state),
);
const forceShowDialog = useAppSelector((state) => state.swap.state !== null);
const [showDialog, setShowDialog] = useState(false);
const [btcFieldValue, setBtcFieldValue] = useState<number | string>(
satsToBtc(selectedProvider.minSwapAmount),
@ -177,9 +174,7 @@ function HasProviderSwapWidget({
}
function HasNoProvidersSwapWidget() {
const forceShowDialog = useAppSelector((state) =>
isSwapState(state.swap.state),
);
const forceShowDialog = useAppSelector((state) => state.swap.state !== null);
const isPublicRegistryDown = useAppSelector((state) =>
isRegistryDown(
state.providers.registry.failedReconnectAttemptsSinceLastSuccess,

View File

@ -1,8 +1,6 @@
import { Button, CircularProgress, IconButton } from "@material-ui/core";
import RefreshIcon from "@material-ui/icons/Refresh";
import IpcInvokeButton from "../../IpcInvokeButton";
import { checkBitcoinBalance } from "renderer/rpc";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { checkBitcoinBalance } from "renderer/rpc";
export default function WalletRefreshButton() {
return (

View File

@ -1,13 +1,13 @@
import { Box, Button, makeStyles, Typography } from "@material-ui/core";
import { useState } from "react";
import SendIcon from "@material-ui/icons/Send";
import { useAppSelector, useIsRpcEndpointBusy } from "store/hooks";
import { RpcMethod } from "models/rpcModel";
import { useState } from "react";
import { SatsAmount } from "renderer/components/other/Units";
import { useAppSelector, useIsRpcEndpointBusy } from "store/hooks";
import BitcoinIcon from "../../icons/BitcoinIcon";
import InfoBox from "../../modal/swap/InfoBox";
import WithdrawDialog from "../../modal/wallet/WithdrawDialog";
import WalletRefreshButton from "./WalletRefreshButton";
import InfoBox from "../../modal/swap/InfoBox";
import { SatsAmount } from "renderer/components/other/Units";
const useStyles = makeStyles((theme) => ({
title: {

View File

@ -1,11 +1,11 @@
import { IconButton, styled } from "@material-ui/core";
import { Close } from "@material-ui/icons";
import {
MaterialDesignContent,
SnackbarKey,
SnackbarProvider,
useSnackbar,
} from "notistack";
import { IconButton, styled } from "@material-ui/core";
import { Close } from "@material-ui/icons";
import { ReactNode } from "react";
const StyledMaterialDesignContent = styled(MaterialDesignContent)(() => ({

View File

@ -1,29 +1,30 @@
import { render } from "react-dom";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { store } from "./store/storeRenderer";
import { setRegistryProviders } from "store/features/providersSlice";
import { setAlerts } from "store/features/alertsSlice";
import { setXmrPrice, setBtcPrice } from "store/features/ratesSlice";
import { setRegistryProviders } from "store/features/providersSlice";
import { setBtcPrice, setXmrPrice } from "store/features/ratesSlice";
import logger from "../utils/logger";
import {
fetchAlertsViaHttp,
fetchBtcPrice,
fetchProvidersViaHttp,
fetchXmrPrice,
} from "./api";
import logger from "../utils/logger";
import App from "./components/App";
import { checkBitcoinBalance, getRawSwapInfos } from "./rpc";
import { store } from "./store/storeRenderer";
setTimeout(() => {
setInterval(() => {
checkBitcoinBalance();
getRawSwapInfos();
}, 10000);
}, 5000);
render(
const container = document.getElementById("root");
const root = createRoot(container!);
root.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root"),
);
async function fetchInitialData() {

View File

@ -1,31 +1,89 @@
import { invoke } from "@tauri-apps/api/core";
import { store } from "./store/storeRenderer";
import { invoke as invokeUnsafe } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import {
BalanceArgs,
BalanceResponse,
BuyXmrArgs,
BuyXmrResponse,
GetSwapInfoResponse,
ResumeSwapArgs,
ResumeSwapResponse,
SuspendCurrentSwapResponse,
TauriSwapProgressEventWrapper,
WithdrawBtcArgs,
WithdrawBtcResponse,
} from "models/tauriModel";
import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice";
import { swapTauriEventReceived } from "store/features/swapSlice";
import { store } from "./store/storeRenderer";
import { Provider } from "models/apiModel";
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
listen<TauriSwapProgressEventWrapper>("swap-progress-update", (event) => {
console.log("Received swap progress event", event.payload);
store.dispatch(swapTauriEventReceived(event.payload));
});
async function invoke<ARGS, RESPONSE>(
command: string,
args: ARGS,
): Promise<RESPONSE> {
return invokeUnsafe(command, {
args: args as Record<string, unknown>,
}) as Promise<RESPONSE>;
}
async function invokeNoArgs<RESPONSE>(command: string): Promise<RESPONSE> {
return invokeUnsafe(command, {}) as Promise<RESPONSE>;
}
export async function checkBitcoinBalance() {
const response = (await invoke("get_balance")) as {
balance: number;
};
const response = await invoke<BalanceArgs, BalanceResponse>("get_balance", {
force_refresh: true,
});
store.dispatch(rpcSetBalance(response.balance));
}
export async function getRawSwapInfos() {
const response = await invoke("get_swap_infos_all");
const response =
await invokeNoArgs<GetSwapInfoResponse[]>("get_swap_infos_all");
(response as any[]).forEach((info) => store.dispatch(rpcSetSwapInfo(info)));
response.forEach((swapInfo) => {
store.dispatch(rpcSetSwapInfo(swapInfo));
});
}
export async function withdrawBtc(address: string): Promise<string> {
const response = (await invoke("withdraw_btc", {
args: {
const response = await invoke<WithdrawBtcArgs, WithdrawBtcResponse>(
"withdraw_btc",
{
address,
amount: null,
},
})) as {
txid: string;
amount: number;
};
);
return response.txid;
}
export async function buyXmr(
seller: Provider,
bitcoin_change_address: string,
monero_receive_address: string,
) {
await invoke<BuyXmrArgs, BuyXmrResponse>("buy_xmr", {
seller: providerToConcatenatedMultiAddr(seller),
bitcoin_change_address,
monero_receive_address,
});
}
export async function resumeSwap(swapId: string) {
await invoke<ResumeSwapArgs, ResumeSwapResponse>("resume_swap", {
swap_id: swapId,
});
}
export async function suspendCurrentSwap() {
await invokeNoArgs<SuspendCurrentSwapResponse>("suspend_current_swap");
}

View File

@ -1,9 +1,9 @@
import swapReducer from "./features/swapSlice";
import providersSlice from "./features/providersSlice";
import torSlice from "./features/torSlice";
import rpcSlice from "./features/rpcSlice";
import alertsSlice from "./features/alertsSlice";
import providersSlice from "./features/providersSlice";
import ratesSlice from "./features/ratesSlice";
import rpcSlice from "./features/rpcSlice";
import swapReducer from "./features/swapSlice";
import torSlice from "./features/torSlice";
export const reducers = {
swap: swapReducer,

View File

@ -2,14 +2,8 @@ import { ExtendedProviderStatus } from "models/apiModel";
export const isTestnet = () => true;
export const isExternalRpc = () => true;
export const isDevelopment = true;
export function getStubTestnetProvider(): ExtendedProviderStatus | null {
return null;
}
export const getPlatform = () => {
return "mac";
};

View File

@ -1,8 +1,8 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
import { sortProviderList } from "utils/sortUtils";
import { isProviderCompatible } from "utils/multiAddrUtils";
import { getStubTestnetProvider } from "store/config";
import { isProviderCompatible } from "utils/multiAddrUtils";
import { sortProviderList } from "utils/sortUtils";
const stubTestnetProvider = getStubTestnetProvider();

View File

@ -1,21 +1,12 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
import { MoneroWalletRpcUpdateState } from "models/storeModel";
import { GetSwapInfoResponse } from "models/tauriModel";
import { CliLog } from "../../models/cliModel";
import {
GetSwapInfoResponse,
MoneroRecoveryResponse,
RpcProcessStateType,
} from "../../models/rpcModel";
import {
CliLog,
isCliLog,
isCliLogDownloadingMoneroWalletRpc,
isCliLogFailedToSyncMoneroWallet,
isCliLogFinishedSyncingMoneroWallet,
isCliLogStartedRpcServer,
isCliLogStartedSyncingMoneroWallet,
} from "../../models/cliModel";
import { getLogsAndStringsFromRawFileString } from "utils/parseUtils";
import { GetSwapInfoResponseExt } from "models/tauriModelExt";
type Process =
| {
@ -41,7 +32,7 @@ interface State {
withdrawTxId: string | null;
rendezvous_discovered_sellers: (ExtendedProviderStatus | ProviderStatus)[];
swapInfos: {
[swapId: string]: GetSwapInfoResponse;
[swapId: string]: GetSwapInfoResponseExt;
};
moneroRecovery: {
swapId: string;
@ -51,7 +42,8 @@ interface State {
isSyncing: boolean;
};
moneroWalletRpc: {
updateState: false | MoneroWalletRpcUpdateState;
// TODO: Reimplement this using Tauri
updateState: false;
};
}
@ -85,44 +77,6 @@ export const rpcSlice = createSlice({
name: "rpc",
initialState,
reducers: {
rpcAddLogs(slice, action: PayloadAction<(CliLog | string)[]>) {
if (
slice.process.type === RpcProcessStateType.STARTED ||
slice.process.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS ||
slice.process.type === RpcProcessStateType.EXITED
) {
const logs = action.payload;
slice.process.logs.push(...logs);
logs.filter(isCliLog).forEach((log) => {
if (
isCliLogStartedRpcServer(log) &&
slice.process.type === RpcProcessStateType.STARTED
) {
slice.process = {
type: RpcProcessStateType.LISTENING_FOR_CONNECTIONS,
logs: slice.process.logs,
address: log.fields.addr,
};
} else if (isCliLogDownloadingMoneroWalletRpc(log)) {
slice.state.moneroWalletRpc.updateState = {
progress: log.fields.progress,
downloadUrl: log.fields.download_url,
};
if (log.fields.progress === "100%") {
slice.state.moneroWalletRpc.updateState = false;
}
} else if (isCliLogStartedSyncingMoneroWallet(log)) {
slice.state.moneroWallet.isSyncing = true;
} else if (isCliLogFinishedSyncingMoneroWallet(log)) {
slice.state.moneroWallet.isSyncing = false;
} else if (isCliLogFailedToSyncMoneroWallet(log)) {
slice.state.moneroWallet.isSyncing = false;
}
});
}
},
rpcInitiate(slice) {
slice.process = {
type: RpcProcessStateType.STARTED,
@ -169,7 +123,8 @@ export const rpcSlice = createSlice({
slice.state.withdrawTxId = null;
},
rpcSetSwapInfo(slice, action: PayloadAction<GetSwapInfoResponse>) {
slice.state.swapInfos[action.payload.swap_id] = action.payload;
slice.state.swapInfos[action.payload.swap_id] =
action.payload as GetSwapInfoResponseExt;
},
rpcSetEndpointBusy(slice, action: PayloadAction<string>) {
if (!slice.busyEndpoints.includes(action.payload)) {
@ -202,7 +157,6 @@ export const rpcSlice = createSlice({
export const {
rpcProcessExited,
rpcAddLogs,
rpcInitiate,
rpcSetBalance,
rpcSetWithdrawTxId,

View File

@ -1,55 +1,12 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { extractAmountFromUnitString } from "utils/parseUtils";
import { Provider } from "models/apiModel";
import {
isSwapStateBtcLockInMempool,
isSwapStateProcessExited,
isSwapStateXmrLockInMempool,
SwapSlice,
SwapStateAttemptingCooperativeRedeeem,
SwapStateBtcCancelled,
SwapStateBtcLockInMempool,
SwapStateBtcPunished,
SwapStateBtcRedemeed,
SwapStateBtcRefunded,
SwapStateInitiated,
SwapStateProcessExited,
SwapStateReceivedQuote,
SwapStateStarted,
SwapStateType,
SwapStateWaitingForBtcDeposit,
SwapStateXmrLocked,
SwapStateXmrLockInMempool,
SwapStateXmrRedeemInMempool,
} from "../../models/storeModel";
import {
isCliLogAliceLockedXmr,
isCliLogBtcTxStatusChanged,
isCliLogPublishedBtcTx,
isCliLogReceivedQuote,
isCliLogReceivedXmrLockTxConfirmation,
isCliLogRedeemedXmr,
isCliLogStartedSwap,
isCliLogWaitingForBtcDeposit,
CliLog,
isCliLogAdvancingState,
SwapSpawnType,
isCliLogBtcTxFound,
isCliLogReleasingSwapLockLog,
isYouHaveBeenPunishedCliLog,
isCliLogAcquiringSwapLockLog,
isCliLogApiCallError,
isCliLogDeterminedSwapAmount,
isCliLogAttemptingToCooperativelyRedeemXmr,
} from "../../models/cliModel";
import logger from "../../utils/logger";
import { TauriSwapProgressEventWrapper } from "models/tauriModel";
import { SwapSlice } from "../../models/storeModel";
const initialState: SwapSlice = {
state: null,
processRunning: false,
swapId: null,
logs: [],
provider: null,
// TODO: Remove this and replace logic entirely with Tauri events
spawnType: null,
};
@ -57,266 +14,27 @@ export const swapSlice = createSlice({
name: "swap",
initialState,
reducers: {
swapAddLog(
slice,
action: PayloadAction<{ logs: CliLog[]; isFromRestore: boolean }>,
swapTauriEventReceived(
swap,
action: PayloadAction<TauriSwapProgressEventWrapper>,
) {
const { logs } = action.payload;
slice.logs.push(...logs);
logs.forEach((log) => {
if (
isCliLogAcquiringSwapLockLog(log) &&
!action.payload.isFromRestore
) {
slice.processRunning = true;
slice.swapId = log.fields.swap_id;
// TODO: Maybe we can infer more info here (state) from the log
} else if (isCliLogReceivedQuote(log)) {
const price = extractAmountFromUnitString(log.fields.price);
const minimumSwapAmount = extractAmountFromUnitString(
log.fields.minimum_amount,
);
const maximumSwapAmount = extractAmountFromUnitString(
log.fields.maximum_amount,
);
if (
price != null &&
minimumSwapAmount != null &&
maximumSwapAmount != null
) {
const nextState: SwapStateReceivedQuote = {
type: SwapStateType.RECEIVED_QUOTE,
price,
minimumSwapAmount,
maximumSwapAmount,
};
slice.state = nextState;
}
} else if (isCliLogWaitingForBtcDeposit(log)) {
const maxGiveable = extractAmountFromUnitString(
log.fields.max_giveable,
);
const minDeposit = extractAmountFromUnitString(
log.fields.min_deposit_until_swap_will_start,
);
const maxDeposit = extractAmountFromUnitString(
log.fields.max_deposit_until_maximum_amount_is_reached,
);
const minimumAmount = extractAmountFromUnitString(
log.fields.minimum_amount,
);
const maximumAmount = extractAmountFromUnitString(
log.fields.maximum_amount,
);
const minBitcoinLockTxFee = extractAmountFromUnitString(
log.fields.min_bitcoin_lock_tx_fee,
);
const price = extractAmountFromUnitString(log.fields.price);
const depositAddress = log.fields.deposit_address;
if (
maxGiveable != null &&
minimumAmount != null &&
maximumAmount != null &&
minDeposit != null &&
maxDeposit != null &&
minBitcoinLockTxFee != null &&
price != null
) {
const nextState: SwapStateWaitingForBtcDeposit = {
type: SwapStateType.WAITING_FOR_BTC_DEPOSIT,
depositAddress,
maxGiveable,
minimumAmount,
maximumAmount,
minDeposit,
maxDeposit,
price,
minBitcoinLockTxFee,
};
slice.state = nextState;
}
} else if (isCliLogDeterminedSwapAmount(log)) {
const amount = extractAmountFromUnitString(log.fields.amount);
const fees = extractAmountFromUnitString(log.fields.fees);
const nextState: SwapStateStarted = {
type: SwapStateType.STARTED,
txLockDetails:
amount != null && fees != null ? { amount, fees } : null,
};
slice.state = nextState;
} else if (isCliLogStartedSwap(log)) {
if (slice.state?.type !== SwapStateType.STARTED) {
const nextState: SwapStateStarted = {
type: SwapStateType.STARTED,
txLockDetails: null,
};
slice.state = nextState;
}
slice.swapId = log.fields.swap_id;
} else if (isCliLogPublishedBtcTx(log)) {
if (log.fields.kind === "lock") {
const nextState: SwapStateBtcLockInMempool = {
type: SwapStateType.BTC_LOCK_TX_IN_MEMPOOL,
bobBtcLockTxId: log.fields.txid,
bobBtcLockTxConfirmations: 0,
};
slice.state = nextState;
} else if (log.fields.kind === "cancel") {
const nextState: SwapStateBtcCancelled = {
type: SwapStateType.BTC_CANCELLED,
btcCancelTxId: log.fields.txid,
};
slice.state = nextState;
} else if (log.fields.kind === "refund") {
const nextState: SwapStateBtcRefunded = {
type: SwapStateType.BTC_REFUNDED,
bobBtcRefundTxId: log.fields.txid,
};
slice.state = nextState;
}
} else if (isCliLogBtcTxStatusChanged(log) || isCliLogBtcTxFound(log)) {
if (isSwapStateBtcLockInMempool(slice.state)) {
if (slice.state.bobBtcLockTxId === log.fields.txid) {
const newStatusText = isCliLogBtcTxStatusChanged(log)
? log.fields.new_status
: log.fields.status;
if (newStatusText.startsWith("confirmed with")) {
const confirmations = Number.parseInt(
newStatusText.split(" ")[2],
10,
);
slice.state.bobBtcLockTxConfirmations = confirmations;
}
}
}
} else if (isCliLogAliceLockedXmr(log)) {
const nextState: SwapStateXmrLockInMempool = {
type: SwapStateType.XMR_LOCK_TX_IN_MEMPOOL,
aliceXmrLockTxId: log.fields.txid,
aliceXmrLockTxConfirmations: 0,
};
slice.state = nextState;
} else if (isCliLogReceivedXmrLockTxConfirmation(log)) {
if (isSwapStateXmrLockInMempool(slice.state)) {
if (slice.state.aliceXmrLockTxId === log.fields.txid) {
slice.state.aliceXmrLockTxConfirmations = Number.parseInt(
log.fields.seen_confirmations,
10,
);
}
}
} else if (isCliLogAdvancingState(log)) {
if (log.fields.state === "xmr is locked") {
const nextState: SwapStateXmrLocked = {
type: SwapStateType.XMR_LOCKED,
};
slice.state = nextState;
} else if (log.fields.state === "btc is redeemed") {
const nextState: SwapStateBtcRedemeed = {
type: SwapStateType.BTC_REDEEMED,
};
slice.state = nextState;
}
} else if (isCliLogRedeemedXmr(log)) {
const nextState: SwapStateXmrRedeemInMempool = {
type: SwapStateType.XMR_REDEEM_IN_MEMPOOL,
bobXmrRedeemTxId: log.fields.txid,
bobXmrRedeemAddress: log.fields.monero_receive_address,
};
slice.state = nextState;
} else if (isYouHaveBeenPunishedCliLog(log)) {
const nextState: SwapStateBtcPunished = {
type: SwapStateType.BTC_PUNISHED,
};
slice.state = nextState;
} else if (isCliLogAttemptingToCooperativelyRedeemXmr(log)) {
const nextState: SwapStateAttemptingCooperativeRedeeem = {
type: SwapStateType.ATTEMPTING_COOPERATIVE_REDEEM,
};
slice.state = nextState;
} else if (
isCliLogReleasingSwapLockLog(log) &&
!action.payload.isFromRestore
) {
const nextState: SwapStateProcessExited = {
type: SwapStateType.PROCESS_EXITED,
prevState: slice.state,
rpcError: null,
};
slice.state = nextState;
slice.processRunning = false;
} else if (isCliLogApiCallError(log) && !action.payload.isFromRestore) {
if (isSwapStateProcessExited(slice.state)) {
slice.state.rpcError = log.fields.err;
}
} else {
logger.debug({ log }, `Swap log was not reduced`);
}
});
if (swap.state === null || action.payload.swap_id !== swap.state.swapId) {
swap.state = {
curr: action.payload.event,
prev: null,
swapId: action.payload.swap_id,
};
} else {
swap.state.prev = swap.state.curr;
swap.state.curr = action.payload.event;
}
},
swapReset() {
return initialState;
},
swapInitiate(
swap,
action: PayloadAction<{
provider: Provider | null;
spawnType: SwapSpawnType;
swapId: string | null;
}>,
) {
const nextState: SwapStateInitiated = {
type: SwapStateType.INITIATED,
};
swap.processRunning = true;
swap.state = nextState;
swap.logs = [];
swap.provider = action.payload.provider;
swap.spawnType = action.payload.spawnType;
swap.swapId = action.payload.swapId;
},
swapProcessExited(swap, action: PayloadAction<string | null>) {
if (!swap.processRunning) {
logger.warn(`swapProcessExited called on a swap that is not running`);
return;
}
const nextState: SwapStateProcessExited = {
type: SwapStateType.PROCESS_EXITED,
prevState: swap.state,
rpcError: action.payload,
};
swap.state = nextState;
swap.processRunning = false;
},
},
});
export const { swapInitiate, swapProcessExited, swapReset, swapAddLog } =
swapSlice.actions;
export const { swapReset, swapTauriEventReceived } = swapSlice.actions;
export default swapSlice.reducer;

View File

@ -1,6 +1,6 @@
import { sortBy } from "lodash";
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`
@ -17,17 +17,20 @@ export function useResumeableSwapsCount() {
}
export function useIsSwapRunning() {
return useAppSelector((state) => state.swap.state !== null);
return useAppSelector(
(state) =>
state.swap.state !== null && state.swap.state.curr.type !== "Released",
);
}
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,
);
}
export function useActiveSwapId() {
return useAppSelector((s) => s.swap.swapId);
return useAppSelector((s) => s.swap.state?.swapId ?? null);
}
export function useActiveSwapInfo() {

View File

@ -1,6 +1,6 @@
import { ExtendedProviderStatus, Provider } from "models/apiModel";
import { Multiaddr } from "multiaddr";
import semver from "semver";
import { ExtendedProviderStatus, Provider } from "models/apiModel";
import { isTestnet } from "store/config";
const MIN_ASB_VERSION = "0.12.0";

View File

@ -1,4 +1,4 @@
import { CliLog, isCliLog } from "models/cliModel";
import { CliLog } from "models/cliModel";
/*
Extract btc amount from string
@ -17,21 +17,28 @@ export function extractAmountFromUnitString(text: string): number | null {
return null;
}
// E.g 2021-12-29 14:25:59.64082 +00:00:00
// E.g: 2024-08-19 6:11:37.475038 +00:00:00
export function parseDateString(str: string): number {
const parts = str.split(" ").slice(0, -1);
if (parts.length !== 2) {
throw new Error(
`Date string does not consist solely of date and time Str: ${str} Parts: ${parts}`,
);
// Split the string and take only the date and time parts
const [datePart, timePart] = str.split(" ");
if (!datePart || !timePart) {
throw new Error(`Invalid date string format: ${str}`);
}
const wholeString = parts.join(" ");
const date = Date.parse(wholeString);
// Parse time part
const [hours, minutes, seconds] = timePart.split(":");
const paddedHours = hours.padStart(2, "0"); // Ensure two-digit hours
// Combine date and time parts, ensuring two-digit hours
const dateTimeString = `${datePart}T${paddedHours}:${minutes}:${seconds.split(".")[0]}Z`;
const date = Date.parse(dateTimeString);
if (Number.isNaN(date)) {
throw new Error(
`Date string could not be parsed Str: ${str} Parts: ${parts}`,
);
throw new Error(`Date string could not be parsed: ${str}`);
}
return date;
}
@ -50,13 +57,15 @@ export function getLogsAndStringsFromRawFileString(
return getLinesOfString(rawFileData).map((line) => {
try {
return JSON.parse(line);
} catch (e) {
} catch {
return line;
}
});
}
export function getLogsFromRawFileString(rawFileData: string): CliLog[] {
// TODO: Reimplement this using Tauri
return [];
return getLogsAndStringsFromRawFileString(rawFileData).filter(isCliLog);
}

View File

@ -1,6 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { internalIpV4 } from "internal-ip";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
// @ts-expect-error process is a nodejs global

Some files were not shown because too many files have changed in this diff Show More