Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli/tauri): Emit events on Monero transaction confirmation update and redeem transaction publication #57

Merged
merged 11 commits into from
Sep 18, 2024
Merged
10 changes: 10 additions & 0 deletions node_modules/.yarn-integrity

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { ReactNode } from "react";
import BitcoinIcon from "renderer/components/icons/BitcoinIcon";
import { isTestnet } from "store/config";
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
import TransactionInfoBox from "./TransactionInfoBox";

type Props = {
title: string;
txId: string;
additionalContent: ReactNode;
loading: boolean;
};

export default function BitcoinTransactionInfoBox({ txId, ...props }: Props) {
const explorerUrl = getBitcoinTxExplorerUrl(txId, isTestnet());
import TransactionInfoBox, {
TransactionInfoBoxProps,
} from "./TransactionInfoBox";

export default function BitcoinTransactionInfoBox({
txId,
...props
}: Omit<TransactionInfoBoxProps, "icon" | "explorerUrlCreator">) {
return (
<TransactionInfoBox
txId={txId}
explorerUrl={explorerUrl}
explorerUrlCreator={(txId) => getBitcoinTxExplorerUrl(txId, isTestnet())}
icon={<BitcoinIcon />}
{...props}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { ReactNode } from "react";
import MoneroIcon from "renderer/components/icons/MoneroIcon";
import { isTestnet } from "store/config";
import { getMoneroTxExplorerUrl } from "utils/conversionUtils";
import TransactionInfoBox from "./TransactionInfoBox";

type Props = {
title: string;
txId: string;
additionalContent: ReactNode;
loading: boolean;
};

export default function MoneroTransactionInfoBox({ txId, ...props }: Props) {
const explorerUrl = getMoneroTxExplorerUrl(txId, isTestnet());
import TransactionInfoBox, {
TransactionInfoBoxProps,
} from "./TransactionInfoBox";

export default function MoneroTransactionInfoBox({
txId,
...props
}: Omit<TransactionInfoBoxProps, "icon" | "explorerUrlCreator">) {
return (
<TransactionInfoBox
txId={txId}
explorerUrl={explorerUrl}
explorerUrlCreator={(txid) => getMoneroTxExplorerUrl(txid, isTestnet())}
icon={<MoneroIcon />}
{...props}
/>
Expand Down
27 changes: 17 additions & 10 deletions src-gui/src/renderer/components/modal/swap/TransactionInfoBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { Link, Typography } from "@material-ui/core";
import { ReactNode } from "react";
import InfoBox from "./InfoBox";

type TransactionInfoBoxProps = {
export type TransactionInfoBoxProps = {
title: string;
txId: string;
explorerUrl: string;
txId: string | null;
explorerUrlCreator: ((txId: string) => string) | null;
additionalContent: ReactNode;
loading: boolean;
icon: JSX.Element;
Expand All @@ -14,24 +14,31 @@ type TransactionInfoBoxProps = {
export default function TransactionInfoBox({
title,
txId,
explorerUrl,
additionalContent,
icon,
loading,
explorerUrlCreator,
}: TransactionInfoBoxProps) {
return (
<InfoBox
title={title}
mainContent={<Typography variant="h5">{txId}</Typography>}
mainContent={
<Typography variant="h5">
{txId ?? "Transaction ID not available"}
</Typography>
}
loading={loading}
additionalContent={
<>
<Typography variant="subtitle2">{additionalContent}</Typography>
<Typography variant="body1">
<Link href={explorerUrl} target="_blank">
View on explorer
</Link>
</Typography>
{explorerUrlCreator != null &&
txId != null && ( // Only show the link if the txId is not null and we have a creator for the explorer URL
<Typography variant="body1">
<Link href={explorerUrlCreator(txId)} target="_blank">
View on explorer
</Link>
</Typography>
)}
</>
}
icon={icon}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,10 @@ import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";

export default function XmrRedeemInMempoolPage({
xmr_redeem_address,
xmr_redeem_txid,
}: TauriSwapProgressEventContent<"XmrRedeemInMempool">) {
// TODO: Reimplement this using Tauri
//const additionalContent = swap
// ? `This transaction transfers ${getSwapXmrAmount(swap).toFixed(6)} XMR to ${
// state?.bobXmrRedeemAddress
// }`
// : null;
export default function XmrRedeemInMempoolPage(
state: TauriSwapProgressEventContent<"XmrRedeemInMempool">,
) {
const xmr_redeem_txid = state.xmr_redeem_txids[0] ?? null;

return (
<Box>
Expand All @@ -30,7 +24,7 @@ export default function XmrRedeemInMempoolPage({
<MoneroTransactionInfoBox
title="Monero Redeem Transaction"
txId={xmr_redeem_txid}
additionalContent={`The funds have been sent to the address ${xmr_redeem_address}`}
additionalContent={`The funds have been sent to the address ${state.xmr_redeem_address}`}
loading={false}
/>
<FeedbackInfoBox />
Expand Down
9 changes: 9 additions & 0 deletions swap/src/bitcoin/timelocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,12 @@ pub enum ExpiredTimelocks {
Cancel { blocks_left: u32 },
Punish,
}

impl ExpiredTimelocks {
/// Check whether the timelock on the cancel transaction has expired.
///
/// Retuns `true` even if the swap has already been canceled or punished.
pub fn cancel_timelock_expired(&self) -> bool {
!matches!(self, ExpiredTimelocks::None { .. })
}
}
4 changes: 2 additions & 2 deletions swap/src/cli/api/tauri_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ pub enum TauriSwapProgressEvent {
XmrLocked,
BtcRedeemed,
XmrRedeemInMempool {
#[typeshare(serialized_as = "string")]
xmr_redeem_txid: monero::TxHash,
#[typeshare(serialized_as = "Vec<string>")]
xmr_redeem_txids: Vec<monero::TxHash>,
#[typeshare(serialized_as = "string")]
xmr_redeem_address: monero::Address,
},
Expand Down
58 changes: 54 additions & 4 deletions swap/src/monero/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use ::monero::{Address, Network, PrivateKey, PublicKey};
use anyhow::{Context, Result};
use monero_rpc::wallet::{BlockHeight, MoneroWalletRpc as _, Refreshed};
use monero_rpc::{jsonrpc, wallet};
use std::future::Future;
use std::ops::Div;
use std::pin::Pin;
use std::str::FromStr;
use std::time::Duration;
use tokio::sync::Mutex;
Expand Down Expand Up @@ -215,7 +217,18 @@ impl Wallet {
))
}

/// Wait until the specified transfer has been completed or failed.
pub async fn watch_for_transfer(&self, request: WatchRequest) -> Result<(), InsufficientFunds> {
self.watch_for_transfer_with(request, None).await
}

/// Wait until the specified transfer has been completed or failed and listen to each new confirmation.
#[allow(clippy::too_many_arguments)]
pub async fn watch_for_transfer_with(
&self,
request: WatchRequest,
listener: Option<ConfirmationListener>,
) -> Result<(), InsufficientFunds> {
let WatchRequest {
conf_target,
public_view_key,
Expand All @@ -236,14 +249,15 @@ impl Wallet {

let check_interval = tokio::time::interval(self.sync_interval.div(10));

wait_for_confirmations(
wait_for_confirmations_with(
&self.inner,
transfer_proof,
address,
expected,
conf_target,
check_interval,
self.name.clone(),
listener,
)
.await?;

Expand Down Expand Up @@ -332,14 +346,21 @@ pub struct WatchRequest {
pub expected: Amount,
}

async fn wait_for_confirmations<C: monero_rpc::wallet::MoneroWalletRpc<reqwest::Client> + Sync>(
type ConfirmationListener =
Box<dyn Fn(u64) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> + Send + 'static>;

#[allow(clippy::too_many_arguments)]
async fn wait_for_confirmations_with<
C: monero_rpc::wallet::MoneroWalletRpc<reqwest::Client> + Sync,
>(
client: &Mutex<C>,
transfer_proof: TransferProof,
to_address: Address,
expected: Amount,
conf_target: u64,
mut check_interval: Interval,
wallet_name: String,
listener: Option<ConfirmationListener>,
) -> Result<(), InsufficientFunds> {
let mut seen_confirmations = 0u64;

Expand Down Expand Up @@ -405,6 +426,11 @@ async fn wait_for_confirmations<C: monero_rpc::wallet::MoneroWalletRpc<reqwest::
needed_confirmations = %conf_target,
"Received new confirmation for Monero lock tx"
);

// notify the listener we received new confirmations
if let Some(listener) = &listener {
listener(seen_confirmations).await;
}
}
}

Expand All @@ -419,6 +445,30 @@ mod tests {
use std::sync::atomic::{AtomicU32, Ordering};
use tracing::metadata::LevelFilter;

async fn wait_for_confirmations<
C: monero_rpc::wallet::MoneroWalletRpc<reqwest::Client> + Sync,
>(
client: &Mutex<C>,
transfer_proof: TransferProof,
to_address: Address,
expected: Amount,
conf_target: u64,
check_interval: Interval,
wallet_name: String,
) -> Result<(), InsufficientFunds> {
wait_for_confirmations_with(
client,
transfer_proof,
to_address,
expected,
conf_target,
check_interval,
wallet_name,
None,
)
.await
}

#[tokio::test]
async fn given_exact_confirmations_does_not_fetch_tx_again() {
let client = Mutex::new(DummyClient::new(vec![Ok(CheckTxKey {
Expand All @@ -435,7 +485,7 @@ mod tests {
Amount::from_piconero(100),
10,
tokio::time::interval(Duration::from_millis(10)),
"foo-wallet".to_owned()
"foo-wallet".to_owned(),
)
.await;

Expand Down Expand Up @@ -533,7 +583,7 @@ mod tests {
Amount::from_piconero(100),
5,
tokio::time::interval(Duration::from_millis(10)),
"foo-wallet".to_owned()
"foo-wallet".to_owned(),
)
.await
.unwrap();
Expand Down
13 changes: 8 additions & 5 deletions swap/src/protocol/bob/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use crate::bitcoin::{
self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
TxLock, Txid,
};
use crate::monero;
use crate::monero::wallet::WatchRequest;
use crate::monero::{self, TxHash};
use crate::monero::{monero_private_key, TransferProof};
use crate::monero_ext::ScalarExt;
use crate::protocol::{Message0, Message1, Message2, Message3, Message4, CROSS_CURVE_PROOF_SYSTEM};
Expand Down Expand Up @@ -627,7 +627,7 @@ impl State5 {
monero_wallet: &monero::Wallet,
wallet_file_name: std::string::String,
monero_receive_address: monero::Address,
) -> Result<()> {
) -> Result<Vec<TxHash>> {
let (spend_key, view_key) = self.xmr_keys();

tracing::info!(%wallet_file_name, "Generating and opening Monero wallet from the extracted keys to redeem the Monero");
Expand All @@ -652,12 +652,15 @@ impl State5 {

// Ensure that the generated wallet is synced so we have a proper balance
monero_wallet.refresh(20).await?;
// Sweep (transfer all funds) to the given address

// Sweep (transfer all funds) to the Bobs Monero redeem address
let tx_hashes = monero_wallet.sweep_all(monero_receive_address).await?;
for tx_hash in tx_hashes {

for tx_hash in &tx_hashes {
tracing::info!(%monero_receive_address, txid=%tx_hash.0, "Successfully transferred XMR to wallet");
}
Ok(())

Ok(tx_hashes)
}
}

Expand Down
Loading
Loading