feat(gui): Migrate to Tauri events

- Replace Electron IPC with Tauri invoke() for API calls
- Implement TauriSwapProgressEvent for state management
- Remove IpcInvokeButton, replace with PromiseInvokeButton
- Update models: new tauriModel.ts, refactor rpcModel.ts
- Simplify SwapSlice state, remove processRunning flag
- Refactor SwapStatePage to use TauriSwapProgressEvent
- Update HistoryRow and HistoryRowActions for new data structures
- Remove unused Electron-specific components (e.g., RpcStatusAlert)
- Update dependencies: React 18, Material-UI v4 to v5
- Implement typeshare for Rust/TypeScript type synchronization
- Add BobStateName enum for more precise swap state tracking
- Refactor utility functions for Tauri compatibility
- Remove JSONStream and other Electron-specific dependencies
This commit is contained in:
binarybaron 2024-08-26 15:32:28 +02:00
parent d54f5c6c77
commit cf641bc8bb
No known key found for this signature in database
GPG key ID: 99B75D3E1476A26E
77 changed files with 2484 additions and 2167 deletions

View file

@ -18,396 +18,3 @@ export interface CliLog {
[index: string]: unknown; [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 { export enum RpcMethod {
GET_BTC_BALANCE = "get_bitcoin_balance", GET_BTC_BALANCE = "get_bitcoin_balance",
WITHDRAW_BTC = "withdraw_btc", WITHDRAW_BTC = "withdraw_btc",
@ -110,227 +107,9 @@ export type SwapSellerInfo = {
addresses: string[]; 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 = { export type MoneroRecoveryResponse = {
address: string; address: string;
spend_key: string; spend_key: string;
view_key: string; view_key: string;
restore_height: number; 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 { CliLog, SwapSpawnType } from "./cliModel";
import { Provider } from "./apiModel"; import { TauriSwapProgressEvent } from "./tauriModel";
export interface SwapSlice { export interface SwapSlice {
state: SwapState | null; state: {
logs: CliLog[]; curr: TauriSwapProgressEvent;
processRunning: boolean; prev: TauriSwapProgressEvent | null;
provider: Provider | null; swapId: string;
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;
} | null; } | null;
} logs: CliLog[];
spawnType: SwapSpawnType | 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;
} }

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

View file

@ -1,8 +1,11 @@
import { Alert } from "@material-ui/lab";
import { Box, LinearProgress } from "@material-ui/core"; import { Box, LinearProgress } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";
export default function MoneroWalletRpcUpdatingAlert() { export default function MoneroWalletRpcUpdatingAlert() {
// TODO: Reimplement this using Tauri Events
return <></>;
const updateState = useAppSelector( const updateState = useAppSelector(
(s) => s.rpc.state.moneroWalletRpc.updateState, (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 { Box, makeStyles } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";
import WalletRefreshButton from "../pages/wallet/WalletRefreshButton";
import { SatsAmount } from "../other/Units"; import { SatsAmount } from "../other/Units";
import WalletRefreshButton from "../pages/wallet/WalletRefreshButton";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
outer: { outer: {

View file

@ -1,8 +1,11 @@
import { Alert } from "@material-ui/lab";
import { CircularProgress } from "@material-ui/core"; import { CircularProgress } from "@material-ui/core";
import { useAppSelector } from "store/hooks"; import { Alert } from "@material-ui/lab";
import { RpcProcessStateType } from "models/rpcModel"; 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() { export default function RpcStatusAlert() {
const rpcProcess = useAppSelector((s) => s.rpc.process); const rpcProcess = useAppSelector((s) => s.rpc.process);
if (rpcProcess.type === RpcProcessStateType.STARTED) { if (rpcProcess.type === RpcProcessStateType.STARTED) {

View file

@ -1,10 +1,10 @@
import { makeStyles } from "@material-ui/core"; import { makeStyles } from "@material-ui/core";
import { Alert, AlertTitle } from "@material-ui/lab"; import { Alert, AlertTitle } from "@material-ui/lab";
import { useActiveSwapInfo } from "store/hooks";
import { import {
isSwapTimelockInfoCancelled, isSwapTimelockInfoCancelled,
isSwapTimelockInfoNone, isSwapTimelockInfoNone,
} from "models/rpcModel"; } from "models/rpcModel";
import { useActiveSwapInfo } from "store/hooks";
import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration"; import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
@ -21,6 +21,9 @@ export default function SwapMightBeCancelledAlert({
}: { }: {
bobBtcLockTxConfirmations: number; bobBtcLockTxConfirmations: number;
}) { }) {
// TODO: Reimplement this using Tauri
return <></>;
const classes = useStyles(); const classes = useStyles();
const swap = useActiveSwapInfo(); const swap = useActiveSwapInfo();

View file

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

View file

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

View file

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

View file

@ -3,7 +3,6 @@ import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function MoneroIcon(props: SvgIconProps) { export default function MoneroIcon(props: SvgIconProps) {
return ( return (
// eslint-disable-next-line react/jsx-props-no-spreading
<SvgIcon {...props}> <SvgIcon {...props}>
<svg <svg
xmlns="http://www.w3.org/2000/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) { export default function TorIcon(props: SvgIconProps) {
return ( return (
// eslint-disable-next-line react/jsx-props-no-spreading
<SvgIcon {...props}> <SvgIcon {...props}>
<svg <svg
xmlns="http://www.w3.org/2000/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 { TextField } from "@material-ui/core";
import { TextFieldProps } from "@material-ui/core/TextField/TextField"; import { TextFieldProps } from "@material-ui/core/TextField/TextField";
import { isBtcAddressValid } from "utils/conversionUtils"; import { useEffect } from "react";
import { isTestnet } from "store/config"; import { isTestnet } from "store/config";
import { isBtcAddressValid } from "utils/conversionUtils";
export default function BitcoinAddressTextField({ export default function BitcoinAddressTextField({
address, address,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
import { ReactNode } from "react";
import { Box, Typography } from "@material-ui/core"; import { Box, Typography } from "@material-ui/core";
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined"; import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
import InfoBox from "./InfoBox"; import { ReactNode } from "react";
import ClipboardIconButton from "./ClipbiardIconButton";
import BitcoinQrCode from "./BitcoinQrCode"; import BitcoinQrCode from "./BitcoinQrCode";
import ClipboardIconButton from "./ClipbiardIconButton";
import InfoBox from "./InfoBox";
type Props = { type Props = {
title: string; 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 { isTestnet } from "store/config";
import { getMoneroTxExplorerUrl } from "utils/conversionUtils"; import { getMoneroTxExplorerUrl } from "utils/conversionUtils";
import MoneroIcon from "renderer/components/icons/MoneroIcon";
import { ReactNode } from "react";
import TransactionInfoBox from "./TransactionInfoBox"; import TransactionInfoBox from "./TransactionInfoBox";
type Props = { type Props = {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,43 +1,28 @@
import { Box } from "@material-ui/core"; import { Box } from "@material-ui/core";
import { useAppSelector } from "store/hooks"; import { SwapSlice } from "models/storeModel";
import { import CircularProgressWithSubtitle from "../CircularProgressWithSubtitle";
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 BitcoinPunishedPage from "./done/BitcoinPunishedPage"; 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({ export default function SwapStatePage({
swapState, state,
}: { }: {
swapState: SwapState | null; state: SwapSlice["state"];
}) { }) {
// TODO: Reimplement this using tauri events
/*
const isSyncingMoneroWallet = useAppSelector( const isSyncingMoneroWallet = useAppSelector(
(state) => state.rpc.state.moneroWallet.isSyncing, (state) => state.rpc.state.moneroWallet.isSyncing,
); );
@ -45,62 +30,57 @@ export default function SwapStatePage({
if (isSyncingMoneroWallet) { if (isSyncingMoneroWallet) {
return <SyncingMoneroWalletPage />; return <SyncingMoneroWalletPage />;
} }
*/
if (swapState === null) { if (state === null) {
return <InitPage />; return <InitPage />;
} }
if (isSwapStateInitiated(swapState)) { switch (state.curr.type) {
return <InitiatedPage />; 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 { Box, DialogContentText } from "@material-ui/core";
import { SwapStateBtcRefunded } from "models/storeModel"; import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import { useActiveSwapInfo } from "store/hooks"; import { useActiveSwapInfo } from "store/hooks";
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox"; import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
export default function BitcoinRefundedPage({ export default function BitcoinRefundedPage({
state, btc_refund_txid,
}: { }: TauriSwapProgressEventContent<"BtcRefunded">) {
state: SwapStateBtcRefunded | null; // TODO: Reimplement this using Tauri
}) {
const swap = useActiveSwapInfo(); const swap = useActiveSwapInfo();
const additionalContent = swap const additionalContent = swap
? `Refund address: ${swap.btc_refund_address}` ? `Refund address: ${swap.btc_refund_address}`
@ -28,14 +27,15 @@ export default function BitcoinRefundedPage({
gap: "0.5rem", gap: "0.5rem",
}} }}
> >
{state && ( {
<BitcoinTransactionInfoBox // TODO: We should display the confirmation count here
title="Bitcoin Refund Transaction" }
txId={state.bobBtcRefundTxId} <BitcoinTransactionInfoBox
loading={false} title="Bitcoin Refund Transaction"
additionalContent={additionalContent} txId={btc_refund_txid}
/> loading={false}
)} additionalContent={additionalContent}
/>
<FeedbackInfoBox /> <FeedbackInfoBox />
</Box> </Box>
</Box> </Box>

View file

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

View file

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

View file

@ -1,47 +1,41 @@
import { useActiveSwapInfo } from "store/hooks"; import { TauriSwapProgressEvent } from "models/tauriModel";
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 SwapStatePage from "../SwapStatePage"; import SwapStatePage from "../SwapStatePage";
import BitcoinRefundedPage from "../done/BitcoinRefundedPage";
import ProcessExitedAndNotDonePage from "./ProcessExitedAndNotDonePage";
type ProcessExitedPageProps = { export default function ProcessExitedPage({
state: SwapStateProcessExited; prevState,
}; swapId,
}: {
export default function ProcessExitedPage({ state }: ProcessExitedPageProps) { prevState: TauriSwapProgressEvent | null;
const swap = useActiveSwapInfo(); swapId: string;
}) {
// 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 // 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 ( if (
isSwapStateXmrRedeemInMempool(state.prevState) || prevState != null &&
isSwapStateBtcRefunded(state.prevState) || (prevState.type === "XmrRedeemInMempool" ||
isSwapStateBtcPunished(state.prevState) 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 // TODO: Display something useful here
if (swap) { return (
if (swap.state_name === SwapStateName.XmrRedeemed) { <>
return <XmrRedeemInMempoolPage state={null} />; 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
if (swap.state_name === SwapStateName.BtcRefunded) { Not implemented yet
return <BitcoinRefundedPage state={null} />; </>
} );
if (swap.state_name === SwapStateName.BtcPunished) {
return <BitcoinPunishedPage />;
}
}
// 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 // 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 { Box, DialogContentText } from "@material-ui/core";
import { SwapStateBtcLockInMempool } from "models/storeModel"; import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
import SwapMightBeCancelledAlert from "../../../../alert/SwapMightBeCancelledAlert"; import SwapMightBeCancelledAlert from "../../../../alert/SwapMightBeCancelledAlert";
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
type BitcoinLockTxInMempoolPageProps = {
state: SwapStateBtcLockInMempool;
};
export default function BitcoinLockTxInMempoolPage({ export default function BitcoinLockTxInMempoolPage({
state, btc_lock_confirmations,
}: BitcoinLockTxInMempoolPageProps) { btc_lock_txid,
}: TauriSwapProgressEventContent<"BtcLockTxInMempool">) {
return ( return (
<Box> <Box>
<SwapMightBeCancelledAlert <SwapMightBeCancelledAlert
bobBtcLockTxConfirmations={state.bobBtcLockTxConfirmations} bobBtcLockTxConfirmations={btc_lock_confirmations}
/> />
<DialogContentText> <DialogContentText>
The Bitcoin lock transaction has been published. The swap will proceed The Bitcoin lock transaction has been published. The swap will proceed
@ -22,14 +19,14 @@ export default function BitcoinLockTxInMempoolPage({
</DialogContentText> </DialogContentText>
<BitcoinTransactionInfoBox <BitcoinTransactionInfoBox
title="Bitcoin Lock Transaction" title="Bitcoin Lock Transaction"
txId={state.bobBtcLockTxId} txId={btc_lock_txid}
loading loading
additionalContent={ additionalContent={
<> <>
Most swap providers require one confirmation before locking their Most swap providers require one confirmation before locking their
Monero Monero
<br /> <br />
Confirmations: {state.bobBtcLockTxConfirmations} Confirmations: {btc_lock_confirmations}
</> </>
} }
/> />

View file

@ -1,16 +1,19 @@
import { SwapStateStarted } from "models/storeModel"; import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import { BitcoinAmount } from "renderer/components/other/Units"; import { SatsAmount } from "renderer/components/other/Units";
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle"; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function StartedPage({ state }: { state: SwapStateStarted }) { export default function StartedPage({
const description = state.txLockDetails ? ( btc_lock_amount,
<> btc_tx_lock_fee,
Locking <BitcoinAmount amount={state.txLockDetails.amount} /> with a }: TauriSwapProgressEventContent<"Started">) {
network fee of <BitcoinAmount amount={state.txLockDetails.fees} /> return (
</> <CircularProgressWithSubtitle
) : ( description={
"Locking Bitcoin" <>
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 { Box, DialogContentText } from "@material-ui/core";
import { SwapStateXmrLockInMempool } from "models/storeModel"; import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox"; import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
type XmrLockTxInMempoolPageProps = {
state: SwapStateXmrLockInMempool;
};
export default function XmrLockTxInMempoolPage({ export default function XmrLockTxInMempoolPage({
state, xmr_lock_tx_confirmations,
}: XmrLockTxInMempoolPageProps) { xmr_lock_txid,
const additionalContent = `Confirmations: ${state.aliceXmrLockTxConfirmations}/10`; }: TauriSwapProgressEventContent<"XmrLockTxInMempool">) {
const additionalContent = `Confirmations: ${xmr_lock_tx_confirmations}/10`;
return ( return (
<Box> <Box>
@ -20,7 +17,7 @@ export default function XmrLockTxInMempoolPage({
<MoneroTransactionInfoBox <MoneroTransactionInfoBox
title="Monero Lock Transaction" title="Monero Lock Transaction"
txId={state.aliceXmrLockTxId} txId={xmr_lock_txid}
additionalContent={additionalContent} additionalContent={additionalContent}
loading loading
/> />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,10 @@
import { Button, Dialog, DialogActions } from "@material-ui/core"; 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 { useState } from "react";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { withdrawBtc } from "renderer/rpc"; import { withdrawBtc } from "renderer/rpc";
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage"; import DialogHeader from "../DialogHeader";
import AddressInputPage from "./pages/AddressInputPage"; import AddressInputPage from "./pages/AddressInputPage";
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage";
import WithdrawDialogContent from "./WithdrawDialogContent"; import WithdrawDialogContent from "./WithdrawDialogContent";
export default function WithdrawDialog({ export default function WithdrawDialog({
@ -42,10 +38,7 @@ export default function WithdrawDialog({
setWithdrawAddressValid={setWithdrawAddressValid} setWithdrawAddressValid={setWithdrawAddressValid}
/> />
) : ( ) : (
<BtcTxInMempoolPageContent <BtcTxInMempoolPageContent withdrawTxId={withdrawTxId} />
withdrawTxId={withdrawTxId}
onCancel={onCancel}
/>
)} )}
</WithdrawDialogContent> </WithdrawDialogContent>
<DialogActions> <DialogActions>

View file

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

View file

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

View file

@ -1,8 +1,5 @@
import { useState } from "react"; import { DialogContentText } from "@material-ui/core";
import { Button, DialogActions, DialogContentText } from "@material-ui/core";
import BitcoinAddressTextField from "../../../inputs/BitcoinAddressTextField"; import BitcoinAddressTextField from "../../../inputs/BitcoinAddressTextField";
import WithdrawDialogContent from "../WithdrawDialogContent";
import IpcInvokeButton from "../../../IpcInvokeButton";
export default function AddressInputPage({ export default function AddressInputPage({
withdrawAddress, 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 BitcoinTransactionInfoBox from "../../swap/BitcoinTransactionInfoBox";
import WithdrawDialogContent from "../WithdrawDialogContent";
export default function BtcTxInMempoolPageContent({ export default function BtcTxInMempoolPageContent({
withdrawTxId, withdrawTxId,
onCancel,
}: { }: {
withdrawTxId: string; withdrawTxId: string;
onCancel: () => void;
}) { }) {
return ( return (
<> <>

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { piconerosToXmr, satsToBtc } from "utils/conversionUtils";
import { Tooltip } from "@material-ui/core"; import { Tooltip } from "@material-ui/core";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";
import { piconerosToXmr, satsToBtc } from "utils/conversionUtils";
type Amount = number | null | undefined; 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} />; 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 }) { export function SatsAmount({ amount }: { amount: Amount }) {
const btcAmount = amount == null ? null : satsToBtc(amount); const btcAmount = amount == null ? null : satsToBtc(amount);
return <BitcoinAmount amount={btcAmount} />; return <BitcoinAmount amount={btcAmount} />;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { render } from "react-dom"; import { createRoot } from "react-dom/client";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { setAlerts } from "store/features/alertsSlice"; import { setAlerts } from "store/features/alertsSlice";
import { setRegistryProviders } from "store/features/providersSlice"; import { setRegistryProviders } from "store/features/providersSlice";
@ -14,16 +14,17 @@ import App from "./components/App";
import { checkBitcoinBalance, getRawSwapInfos } from "./rpc"; import { checkBitcoinBalance, getRawSwapInfos } from "./rpc";
import { store } from "./store/storeRenderer"; import { store } from "./store/storeRenderer";
setTimeout(() => { setInterval(() => {
checkBitcoinBalance(); checkBitcoinBalance();
getRawSwapInfos(); getRawSwapInfos();
}, 10000); }, 5000);
render( const container = document.getElementById("root");
const root = createRoot(container!);
root.render(
<Provider store={store}> <Provider store={store}>
<App /> <App />
</Provider>, </Provider>,
document.getElementById("root"),
); );
async function fetchInitialData() { async function fetchInitialData() {

View file

@ -1,31 +1,89 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke as invokeUnsafe } from "@tauri-apps/api/core";
import { store } from "./store/storeRenderer"; 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 { 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() { export async function checkBitcoinBalance() {
const response = (await invoke("get_balance")) as { const response = await invoke<BalanceArgs, BalanceResponse>("get_balance", {
balance: number; force_refresh: true,
}; });
store.dispatch(rpcSetBalance(response.balance)); store.dispatch(rpcSetBalance(response.balance));
} }
export async function getRawSwapInfos() { 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> { export async function withdrawBtc(address: string): Promise<string> {
const response = (await invoke("withdraw_btc", { const response = await invoke<WithdrawBtcArgs, WithdrawBtcResponse>(
args: { "withdraw_btc",
{
address, address,
amount: null, amount: null,
}, },
})) as { );
txid: string;
amount: number;
};
return response.txid; 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

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

View file

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

View file

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

View file

@ -1,55 +1,12 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { extractAmountFromUnitString } from "utils/parseUtils"; import { TauriSwapProgressEventWrapper } from "models/tauriModel";
import { Provider } from "models/apiModel"; import { SwapSlice } from "../../models/storeModel";
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";
const initialState: SwapSlice = { const initialState: SwapSlice = {
state: null, state: null,
processRunning: false,
swapId: null,
logs: [], logs: [],
provider: null,
// TODO: Remove this and replace logic entirely with Tauri events
spawnType: null, spawnType: null,
}; };
@ -57,266 +14,27 @@ export const swapSlice = createSlice({
name: "swap", name: "swap",
initialState, initialState,
reducers: { reducers: {
swapAddLog( swapTauriEventReceived(
slice, swap,
action: PayloadAction<{ logs: CliLog[]; isFromRestore: boolean }>, action: PayloadAction<TauriSwapProgressEventWrapper>,
) { ) {
const { logs } = action.payload; if (swap.state === null || action.payload.swap_id !== swap.state.swapId) {
slice.logs.push(...logs); swap.state = {
curr: action.payload.event,
logs.forEach((log) => { prev: null,
if ( swapId: action.payload.swap_id,
isCliLogAcquiringSwapLockLog(log) && };
!action.payload.isFromRestore } else {
) { swap.state.prev = swap.state.curr;
slice.processRunning = true; swap.state.curr = action.payload.event;
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`);
}
});
}, },
swapReset() { swapReset() {
return initialState; 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 } = export const { swapReset, swapTauriEventReceived } = swapSlice.actions;
swapSlice.actions;
export default swapSlice.reducer; export default swapSlice.reducer;

View file

@ -17,17 +17,20 @@ export function useResumeableSwapsCount() {
} }
export function useIsSwapRunning() { 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) { export function useSwapInfo(swapId: string | null) {
return useAppSelector((state) => return useAppSelector((state) =>
swapId ? state.rpc.state.swapInfos[swapId] ?? null : null, swapId ? (state.rpc.state.swapInfos[swapId] ?? null) : null,
); );
} }
export function useActiveSwapId() { export function useActiveSwapId() {
return useAppSelector((s) => s.swap.swapId); return useAppSelector((s) => s.swap.state?.swapId ?? null);
} }
export function useActiveSwapInfo() { export function useActiveSwapInfo() {

View file

@ -1,6 +1,6 @@
import { ExtendedProviderStatus, Provider } from "models/apiModel";
import { Multiaddr } from "multiaddr"; import { Multiaddr } from "multiaddr";
import semver from "semver"; import semver from "semver";
import { ExtendedProviderStatus, Provider } from "models/apiModel";
import { isTestnet } from "store/config"; import { isTestnet } from "store/config";
const MIN_ASB_VERSION = "0.12.0"; 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 Extract btc amount from string
@ -17,21 +17,28 @@ export function extractAmountFromUnitString(text: string): number | null {
return 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 { export function parseDateString(str: string): number {
const parts = str.split(" ").slice(0, -1); // Split the string and take only the date and time parts
if (parts.length !== 2) { const [datePart, timePart] = str.split(" ");
throw new Error(
`Date string does not consist solely of date and time Str: ${str} Parts: ${parts}`, 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)) { if (Number.isNaN(date)) {
throw new Error( throw new Error(`Date string could not be parsed: ${str}`);
`Date string could not be parsed Str: ${str} Parts: ${parts}`,
);
} }
return date; return date;
} }
@ -50,13 +57,15 @@ export function getLogsAndStringsFromRawFileString(
return getLinesOfString(rawFileData).map((line) => { return getLinesOfString(rawFileData).map((line) => {
try { try {
return JSON.parse(line); return JSON.parse(line);
} catch (e) { } catch {
return line; return line;
} }
}); });
} }
export function getLogsFromRawFileString(rawFileData: string): CliLog[] { export function getLogsFromRawFileString(rawFileData: string): CliLog[] {
// TODO: Reimplement this using Tauri
return [];
return getLogsAndStringsFromRawFileString(rawFileData).filter(isCliLog); return getLogsAndStringsFromRawFileString(rawFileData).filter(isCliLog);
} }

File diff suppressed because it is too large Load diff