wip: WithdrawDialog migrated to Tauri IPC

This commit is contained in:
binarybaron 2024-08-09 19:03:28 +02:00
parent 92034a5be8
commit 47821cbe79
No known key found for this signature in database
GPG key ID: 99B75D3E1476A26E
14 changed files with 185 additions and 166 deletions

View file

@ -6,6 +6,7 @@ import { ReactNode, useEffect, useState } from "react";
interface IpcInvokeButtonProps<T> {
onSuccess?: (data: T) => void;
onClick: () => Promise<T>;
onPendingChange?: (bool) => void;
isLoadingOverride?: boolean;
isIconButton?: boolean;
loadIcon?: ReactNode;
@ -24,26 +25,22 @@ export default function PromiseInvokeButton<T>({
isIconButton,
displayErrorSnackbar,
tooltipTitle,
onPendingChange,
...rest
}: IpcInvokeButtonProps<T> & ButtonProps) {
const { enqueueSnackbar } = useSnackbar();
const [isPending, setIsPending] = useState(false);
const [hasMinLoadingTimePassed, setHasMinLoadingTimePassed] = useState(false);
const isLoading = (isPending && hasMinLoadingTimePassed) || isLoadingOverride;
const isLoading = isPending || isLoadingOverride;
const actualEndIcon = isLoading
? loadIcon || <CircularProgress size="1em" />
: endIcon;
useEffect(() => {
setHasMinLoadingTimePassed(false);
setTimeout(() => setHasMinLoadingTimePassed(true), 100);
}, [isPending]);
async function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
if (!isPending) {
try {
onPendingChange?.(true);
setIsPending(true);
let result = await onClick();
onSuccess?.(result);
@ -56,32 +53,23 @@ export default function PromiseInvokeButton<T>({
}
} finally {
setIsPending(false);
onPendingChange?.(false);
}
}
}
const isDisabled = disabled || isLoading;
return (
<Tooltip title={tooltipTitle}>
<span>
{isIconButton ? (
<IconButton
onClick={handleClick}
disabled={isDisabled}
{...(rest as any)}
>
{actualEndIcon}
</IconButton>
) : (
<Button
onClick={handleClick}
disabled={isDisabled}
endIcon={actualEndIcon}
{...rest}
/>
)}
</span>
</Tooltip>
return isIconButton ? (
<IconButton onClick={handleClick} disabled={isDisabled} {...(rest as any)}>
{actualEndIcon}
</IconButton>
) : (
<Button
onClick={handleClick}
disabled={isDisabled}
endIcon={actualEndIcon}
{...rest}
/>
);
}

View file

@ -1,9 +1,14 @@
import { Dialog } 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 { 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 { withdrawBtc } from "renderer/rpc";
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage";
import AddressInputPage from "./pages/AddressInputPage";
export default function WithdrawDialog({
open,
@ -12,23 +17,60 @@ export default function WithdrawDialog({
open: boolean;
onClose: () => void;
}) {
const isRpcEndpointBusy = useIsRpcEndpointBusy(RpcMethod.WITHDRAW_BTC);
const dispatch = useAppDispatch();
const [pending, setPending] = useState(false);
const [withdrawTxId, setWithdrawTxId] = useState<string | null>(null);
const [withdrawAddressValid, setWithdrawAddressValid] = useState(false);
const [withdrawAddress, setWithdrawAddress] = useState<string>("");
function onCancel() {
if (!isRpcEndpointBusy) {
if (!pending) {
setWithdrawTxId(null);
setWithdrawAddress("");
onClose();
dispatch(rpcResetWithdrawTxId());
}
}
// This prevents an issue where the Dialog is shown for a split second without a present withdraw state
if (!open && !isRpcEndpointBusy) return null;
if (!open) return null;
return (
<Dialog open onClose={onCancel} maxWidth="sm" fullWidth>
<DialogHeader title="Withdraw Bitcoin" />
<WithdrawStatePage onCancel={onCancel} />
{withdrawTxId === null ? (
<AddressInputPage
setWithdrawAddress={setWithdrawAddress}
withdrawAddress={withdrawAddress}
setWithdrawAddressValid={setWithdrawAddressValid}
/>
) : (
<BtcTxInMempoolPageContent
withdrawTxId={withdrawTxId}
onCancel={onCancel}
/>
)}
<DialogActions>
{withdrawTxId === null ? (
<PromiseInvokeButton
variant="contained"
color="primary"
disabled={!withdrawAddressValid}
onClick={() => withdrawBtc(withdrawAddress)}
onPendingChange={(pending) => {
console.log("pending", pending);
setPending(pending);
}}
onSuccess={(txId) => {
setWithdrawTxId(txId);
}}
>
Withdraw
</PromiseInvokeButton>
) : (
<Button onClick={onCancel} color="primary" disabled={pending}>
Close
</Button>
)}
</DialogActions>
</Dialog>
);
}

View file

@ -1,17 +1,18 @@
import { useState } from 'react';
import { Button, DialogActions, DialogContentText } from '@material-ui/core';
import BitcoinAddressTextField from '../../../inputs/BitcoinAddressTextField';
import WithdrawDialogContent from '../WithdrawDialogContent';
import IpcInvokeButton from '../../../IpcInvokeButton';
import { useState } from "react";
import { Button, DialogActions, DialogContentText } from "@material-ui/core";
import BitcoinAddressTextField from "../../../inputs/BitcoinAddressTextField";
import WithdrawDialogContent from "../WithdrawDialogContent";
import IpcInvokeButton from "../../../IpcInvokeButton";
export default function AddressInputPage({
onCancel,
withdrawAddress,
setWithdrawAddress,
setWithdrawAddressValid,
}: {
onCancel: () => void;
withdrawAddress: string;
setWithdrawAddress: (address: string) => void;
setWithdrawAddressValid: (valid: boolean) => void;
}) {
const [withdrawAddressValid, setWithdrawAddressValid] = useState(false);
const [withdrawAddress, setWithdrawAddress] = useState('');
return (
<>
<WithdrawDialogContent>
@ -28,22 +29,6 @@ export default function AddressInputPage({
fullWidth
/>
</WithdrawDialogContent>
<DialogActions>
<Button onClick={onCancel} variant="text">
Cancel
</Button>
<IpcInvokeButton
disabled={!withdrawAddressValid}
ipcChannel="spawn-withdraw-btc"
ipcArgs={[withdrawAddress]}
color="primary"
variant="contained"
requiresRpc
>
Withdraw
</IpcInvokeButton>
</DialogActions>
</>
);
}

View file

@ -1,6 +1,6 @@
import { Button, DialogActions, DialogContentText } from '@material-ui/core';
import BitcoinTransactionInfoBox from '../../swap/BitcoinTransactionInfoBox';
import WithdrawDialogContent from '../WithdrawDialogContent';
import { Button, DialogActions, DialogContentText } from "@material-ui/core";
import BitcoinTransactionInfoBox from "../../swap/BitcoinTransactionInfoBox";
import WithdrawDialogContent from "../WithdrawDialogContent";
export default function BtcTxInMempoolPageContent({
withdrawTxId,
@ -23,14 +23,6 @@ export default function BtcTxInMempoolPageContent({
additionalContent={null}
/>
</WithdrawDialogContent>
<DialogActions>
<Button onClick={onCancel} variant="text">
Cancel
</Button>
<Button onClick={onCancel} color="primary" variant="contained">
Done
</Button>
</DialogActions>
</>
);
}

View file

@ -1,21 +0,0 @@
import { Button, DialogActions } from '@material-ui/core';
import CircularProgressWithSubtitle from '../../swap/CircularProgressWithSubtitle';
import WithdrawDialogContent from '../WithdrawDialogContent';
export default function InitiatedPage({ onCancel }: { onCancel: () => void }) {
return (
<>
<WithdrawDialogContent>
<CircularProgressWithSubtitle description="Withdrawing Bitcoin" />
</WithdrawDialogContent>
<DialogActions>
<Button onClick={onCancel} variant="text">
Cancel
</Button>
<Button disabled color="primary" variant="contained">
Done
</Button>
</DialogActions>
</>
);
}

View file

@ -3,7 +3,6 @@ import { store } from "./store/storeRenderer";
import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice";
export async function checkBitcoinBalance() {
// TODO: use tauri-bindgen here
const response = (await invoke("get_balance")) as {
balance: number;
};
@ -16,3 +15,17 @@ export async function getRawSwapInfos() {
(response as any[]).forEach((info) => store.dispatch(rpcSetSwapInfo(info)));
}
export async function withdrawBtc(address: string): Promise<string> {
const response = (await invoke("withdraw_btc", {
args: {
address,
amount: null,
},
})) as {
txid: string;
amount: number;
};
return response.txid;
}

View file

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

View file

@ -1,7 +1,7 @@
[package]
name = "unstoppableswap-gui-rs"
version = "0.0.0"
authors = ["binarybaron", "einliterflasche", "unstoppableswap"]
authors = [ "binarybaron", "einliterflasche", "unstoppableswap" ]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -9,15 +9,15 @@ description = "GUI for XMR<>BTC Atomic Swaps written in Rust"
[lib]
name = "unstoppableswap_gui_rs_lib"
crate-type = ["lib", "cdylib", "staticlib"]
crate-type = [ "lib", "cdylib", "staticlib" ]
[build-dependencies]
tauri-build = { version = "2.0.0-rc.1", features = ["config-json5"] }
tauri-build = { version = "2.0.0-rc.1", features = [ "config-json5" ] }
[dependencies]
anyhow = "1"
once_cell = "1"
serde = { version = "1", features = ["derive"] }
serde = { version = "1", features = [ "derive" ] }
serde_json = "1"
swap = { path = "../swap" }
tauri = { version = "2.0.0-rc.1", features = ["config-json5"] }
tauri = { version = "2.0.0-rc.1", features = [ "config-json5" ] }

View file

@ -1,20 +1,17 @@
use std::sync::Arc;
use once_cell::sync::OnceCell;
use std::result::Result;
use std::sync::Arc;
use swap::{
api::{
request::{
get_balance as get_balance_impl, get_swap_infos_all as get_swap_infos_all_impl,
BalanceArgs, BalanceResponse, GetSwapInfoResponse,
withdraw_btc as withdraw_btc_impl, BalanceArgs, BalanceResponse, GetSwapInfoResponse,
WithdrawBtcArgs, WithdrawBtcResponse,
},
Context,
},
cli::command::{Bitcoin, Monero},
};
// Lazy load the Context
static CONTEXT: OnceCell<Arc<Context>> = OnceCell::new();
use tauri::{Manager, State};
trait ToStringResult<T> {
fn to_string_result(self) -> Result<T, String>;
@ -31,24 +28,46 @@ impl<T, E: ToString> ToStringResult<T> for Result<T, E> {
}
#[tauri::command]
async fn get_balance() -> Result<BalanceResponse, String> {
let context = CONTEXT.get().unwrap();
async fn get_balance(context: State<'_, Arc<Context>>) -> Result<BalanceResponse, String> {
get_balance_impl(
BalanceArgs {
force_refresh: true,
},
context.clone(),
context.inner().clone(),
)
.await
.to_string_result()
}
#[tauri::command]
async fn get_swap_infos_all() -> Result<Vec<GetSwapInfoResponse>, String> {
let context = CONTEXT.get().unwrap();
async fn get_swap_infos_all(
context: State<'_, Arc<Context>>,
) -> Result<Vec<GetSwapInfoResponse>, String> {
get_swap_infos_all_impl(context.inner().clone())
.await
.to_string_result()
}
get_swap_infos_all_impl(context.clone())
/*macro_rules! tauri_command {
($command_name:ident, $command_args:ident, $command_response:ident) => {
#[tauri::command]
async fn $command_name(
context: State<'_, Context>,
args: $command_args,
) -> Result<$command_response, String> {
swap::api::request::$command_name(args, context)
.await
.to_string_result()
}
};
}*/
#[tauri::command]
async fn withdraw_btc(
context: State<'_, Arc<Context>>,
args: WithdrawBtcArgs,
) -> Result<WithdrawBtcResponse, String> {
withdraw_btc_impl(args, context.inner().clone())
.await
.to_string_result()
}
@ -73,9 +92,7 @@ fn setup<'a>(app: &'a mut tauri::App) -> Result<(), Box<dyn std::error::Error>>
.await
.unwrap();
CONTEXT
.set(Arc::new(context))
.expect("Failed to initialize cli context");
app.manage(Arc::new(context));
});
Ok(())
@ -84,7 +101,11 @@ fn setup<'a>(app: &'a mut tauri::App) -> Result<(), Box<dyn std::error::Error>>
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![get_balance, get_swap_infos_all])
.invoke_handler(tauri::generate_handler![
get_balance,
get_swap_infos_all,
withdraw_btc
])
.setup(setup)
.run(tauri::generate_context!())
.expect("error while running tauri application");

View file

@ -21,7 +21,7 @@ use std::future::Future;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug_span, field, Instrument, Span};
use tracing::Instrument;
use uuid::Uuid;
#[derive(PartialEq, Debug)]
@ -53,9 +53,9 @@ pub struct MoneroRecoveryArgs {
pub swap_id: Uuid,
}
#[derive(Debug, Eq, PartialEq)]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct WithdrawBtcArgs {
pub amount: Option<Amount>,
pub amount: Option<u64>,
pub address: bitcoin::Address,
}
@ -119,6 +119,12 @@ pub struct GetSwapInfoResponse {
pub timelock: Option<ExpiredTimelocks>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct WithdrawBtcResponse {
amount: u64,
txid: String,
}
#[derive(Serialize, Deserialize)]
pub struct Seller {
pub peer_id: String,
@ -645,7 +651,7 @@ pub async fn get_config(context: Arc<Context>) -> Result<serde_json::Value> {
pub async fn withdraw_btc(
withdraw_btc: WithdrawBtcArgs,
context: Arc<Context>,
) -> Result<serde_json::Value> {
) -> Result<WithdrawBtcResponse> {
let WithdrawBtcArgs { address, amount } = withdraw_btc;
let bitcoin_wallet = context
.bitcoin_wallet
@ -653,7 +659,7 @@ pub async fn withdraw_btc(
.context("Could not get Bitcoin wallet")?;
let amount = match amount {
Some(amount) => amount,
Some(amount) => Amount::from_sat(amount),
None => {
bitcoin_wallet
.max_giveable(address.script_pubkey().len())
@ -669,11 +675,10 @@ pub async fn withdraw_btc(
.broadcast(signed_tx.clone(), "withdraw")
.await?;
Ok(json!({
"signed_tx": signed_tx,
"amount": amount.to_sat(),
"txid": signed_tx.txid(),
}))
Ok(WithdrawBtcResponse {
txid: signed_tx.txid().to_string(),
amount: amount.to_sat(),
})
}
#[tracing::instrument(fields(method = "start_daemon"), skip(context))]
@ -848,17 +853,7 @@ impl Request {
}
}
async fn handle_cmd(self, context: Arc<Context>) -> Result<Box<dyn erased_serde::Serialize>> {
match self.cmd {
Method::Balance(args) => {
let response = get_balance(args, context).await?;
Ok(Box::new(response) as Box<dyn erased_serde::Serialize>)
}
_ => todo!(),
}
}
pub async fn call(self, context: Arc<Context>) -> Result<JsonValue> {
pub async fn call(self, _: Arc<Context>) -> Result<JsonValue> {
unreachable!("This function should never be called")
}
}

View file

@ -1,7 +1,6 @@
#![warn(
unused_extern_crates,
missing_copy_implementations,
rust_2018_idioms,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::fallible_impl_from,
@ -12,8 +11,10 @@
#![forbid(unsafe_code)]
#![allow(non_snake_case)]
use crate::cli::command::{parse_args_and_apply_defaults, ParseResult};
use crate::common::check_latest_version;
use crate::{
cli::command::{parse_args_and_apply_defaults, ParseResult},
common::check_latest_version,
};
use anyhow::Result;
use std::env;

View file

@ -1,8 +1,8 @@
use crate::api::request::{
buy_xmr, cancel_and_refund, export_bitcoin_wallet, get_balance, get_config, get_history,
list_sellers, monero_recovery, resume_swap, start_daemon, withdraw_btc, BalanceArgs,
BuyXmrArgs, CancelAndRefundArgs, ListSellersArgs, Method, MoneroRecoveryArgs, Request,
ResumeArgs, StartDaemonArgs, WithdrawBtcArgs,
BuyXmrArgs, CancelAndRefundArgs, ListSellersArgs, MoneroRecoveryArgs, ResumeArgs,
StartDaemonArgs, WithdrawBtcArgs,
};
use crate::api::Context;
use crate::bitcoin::{bitcoin_address, Amount};
@ -10,7 +10,6 @@ use crate::monero;
use crate::monero::monero_address;
use anyhow::Result;
use libp2p::core::Multiaddr;
use serde_json::Value;
use std::ffi::OsString;
use std::net::SocketAddr;
use std::path::PathBuf;
@ -193,7 +192,14 @@ where
.await?,
);
withdraw_btc(WithdrawBtcArgs { amount, address }, context).await?;
withdraw_btc(
WithdrawBtcArgs {
amount: amount.map(Amount::to_sat),
address,
},
context,
)
.await?;
Ok(())
}

View file

@ -1,11 +1,11 @@
use crate::api::Context;
use std::{net::SocketAddr, sync::Arc};
use std::net::SocketAddr;
use thiserror::Error;
use tower_http::cors::CorsLayer;
use jsonrpsee::{
core::server::host_filtering::AllowHosts,
server::{RpcModule, ServerBuilder, ServerHandle},
server::{ServerBuilder, ServerHandle},
};
pub mod methods;

View file

@ -13,7 +13,6 @@ use jsonrpsee::server::RpcModule;
use libp2p::core::Multiaddr;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use uuid::Uuid;
trait ConvertToJsonRpseeError<T> {
@ -29,7 +28,7 @@ impl<T> ConvertToJsonRpseeError<T> for Result<T, anyhow::Error> {
pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
let mut module = RpcModule::new(outer_context);
module.register_async_method("suspend_current_swap", |params, context| async move {
module.register_async_method("suspend_current_swap", |_, context| async move {
suspend_current_swap(context).await.to_jsonrpsee_result()
})?;
@ -66,11 +65,11 @@ pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
.to_jsonrpsee_result()
})?;
module.register_async_method("get_history", |params, context| async move {
module.register_async_method("get_history", |_, context| async move {
get_history(context).await.to_jsonrpsee_result()
})?;
module.register_async_method("get_raw_states", |params, context| async move {
module.register_async_method("get_raw_states", |_, context| async move {
get_raw_states(context).await.to_jsonrpsee_result()
})?;
@ -131,7 +130,8 @@ pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
::bitcoin::Amount::from_str_in(amount_str, ::bitcoin::Denomination::Bitcoin)
.map_err(|_| {
jsonrpsee_core::Error::Custom("Unable to parse amount".to_string())
})?,
})?
.to_sat(),
)
} else {
None
@ -224,7 +224,7 @@ pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
.to_jsonrpsee_result()
})?;
module.register_async_method("get_current_swap", |params, context| async move {
module.register_async_method("get_current_swap", |_, context| async move {
get_current_swap(context).await.to_jsonrpsee_result()
})?;