Skip to content

Commit

Permalink
feat(cli, gui, tauri): Emit events on Monero transaction confirmation…
Browse files Browse the repository at this point in the history
… update and redeem transaction publication (#57)

We now,
- emit a Tauri event when the Monero lock transaction receives a new confirmation
- emit a Tauri event with a list of transaction hashes once we have published the Monero redeem transaction 
- gui: display the confirmations and txids

This PR closes #12.
  • Loading branch information
Einliterflasche authored Sep 18, 2024
1 parent a1fd147 commit 9d1151c
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 155 deletions.
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

0 comments on commit 9d1151c

Please sign in to comment.