Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions monero-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ const EMBEDDED_PATCHES: &[EmbeddedPatch] = &[
"Adds balancePerSubaddress() and unlockedBalancePerSubaddress() to wallet::WalletImpl in api/wallet.h",
"patches/eigenwallet_0004_wallet_impl_balance_per_subaddress.patch"
),
embedded_patch!(
"eigenwallet_0005_pending_transaction_raw_tx_hex",
"Adds rawTxHex() to PendingTransaction in wallet2_api.h",
"patches/eigenwallet_0005_pending_transaction_raw_tx_hex.patch"
),
];

/// Find the workspace target directory from OUT_DIR
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
diff --git a/src/wallet/api/pending_transaction.cpp b/src/wallet/api/pending_transaction.cpp
index 84cc8c585..60e242b3f 100644
--- a/src/wallet/api/pending_transaction.cpp
+++ b/src/wallet/api/pending_transaction.cpp
@@ -175,6 +175,24 @@ std::vector<std::tuple<std::string, std::string, std::string>> PendingTransactio
return keys;
}

+std::string PendingTransactionImpl::rawTxHex(const std::string &tx_hash) const
+{
+ if (m_pending_tx.size() != 1)
+ {
+ throw std::runtime_error("Expected exactly one pending transaction, found " + std::to_string(m_pending_tx.size()));
+ }
+
+ const auto &ptx = m_pending_tx.front();
+ const std::string current_tx_hash = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx));
+
+ if (current_tx_hash != tx_hash)
+ {
+ throw std::runtime_error("Requested transaction hash " + tx_hash + " does not match pending transaction hash " + current_tx_hash);
+ }
+
+ return epee::string_tools::buff_to_hex_nodelimer(cryptonote::tx_to_blob(ptx.tx));
+}
+
bool PendingTransactionImpl::commit(const std::string &filename, bool overwrite)
{

diff --git a/src/wallet/api/pending_transaction.h b/src/wallet/api/pending_transaction.h
index 0377eb79c..2c54f53d7 100644
--- a/src/wallet/api/pending_transaction.h
+++ b/src/wallet/api/pending_transaction.h
@@ -52,6 +52,7 @@ public:
uint64_t fee() const override;
std::vector<std::string> txid() const override;
std::vector<std::tuple<std::string, std::string, std::string>> txKeys(const std::string &tx_hash) const override;
+ std::string rawTxHex(const std::string &tx_hash) const override;
uint64_t txCount() const override;
std::vector<uint32_t> subaddrAccount() const override;
std::vector<std::set<uint32_t>> subaddrIndices() const override;
diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h
index 073799c9e..aca4e91d6 100644
--- a/src/wallet/api/wallet2_api.h
+++ b/src/wallet/api/wallet2_api.h
@@ -132,6 +132,7 @@ struct PendingTransaction
virtual uint64_t fee() const = 0;
virtual std::vector<std::string> txid() const = 0;
virtual std::vector<std::tuple<std::string, std::string, std::string>> txKeys(const std::string &tx_hash) const = 0;
+ virtual std::string rawTxHex(const std::string &tx_hash) const = 0;
/*!
* \brief txCount - number of transactions current transaction will be splitted to
* \return
5 changes: 5 additions & 0 deletions monero-sys/src/bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ namespace Monero
return std::make_unique<std::string>(err);
}

inline std::unique_ptr<std::string> pendingTransactionRawTxHex(const PendingTransaction &tx, const std::string &tx_hash)
{
return std::make_unique<std::string>(tx.rawTxHex(tx_hash));
}

/**
* Wrapper for Wallet::checkTxKey to accommodate passing std::string by reference.
* The original API takes the tx_key parameter by value which is not compatible
Expand Down
5 changes: 5 additions & 0 deletions monero-sys/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@ pub mod ffi {
tx_hash: &CxxString,
) -> Result<UniquePtr<CxxVector<TxKey>>>;

fn pendingTransactionRawTxHex(
tx: &PendingTransaction,
tx_hash: &CxxString,
) -> Result<UniquePtr<CxxString>>;

/// Get the fee of a pending transaction.
fn pendingTransactionFee(tx: &PendingTransaction) -> Result<u64>;

Expand Down
73 changes: 69 additions & 4 deletions monero-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ pub enum TransactionDirection {
/// A wrapper around a pending transaction.
///
/// Safety: do _not_ implement copy, send, sync, ...
///
/// Must be manually dropped via FfiWallet::dispose_pending_transaction.
pub struct PendingTransactionHandle(*mut ffi::PendingTransaction);

/// Struct containing a raw pointer to a transaction history.
Expand Down Expand Up @@ -622,6 +624,25 @@ impl WalletHandle {
.map_err(|e| anyhow!("Failed to transfer funds after multiple attempts: {e:?}"))
}

pub async fn construct_multi_destination_tx(
&self,
destinations: &[(monero_address::MoneroAddress, monero_oxide_ext::Amount)],
) -> anyhow::Result<(TxReceipt, String)> {
let destinations = destinations.to_vec();

retry_notify(backoff(None, None), || async {
let destinations = destinations.clone();

self.call(move |wallet| wallet.construct_multi_destination_tx(&destinations))
.await
.map_err(backoff::Error::transient)
}, |error, duration: Duration| {
tracing::error!(error=?error, "Failed to construct transaction, retrying in {} secs", duration.as_secs());
})
.await?
.map_err(|e| anyhow!("Failed to construct transaction after multiple attempts: {e:?}"))
}

/// Sweep all funds to an address.
pub async fn sweep(
&self,
Expand Down Expand Up @@ -2428,6 +2449,39 @@ impl FfiWallet {
result
}

pub fn construct_multi_destination_tx(
&mut self,
destinations: &[(monero_address::MoneroAddress, monero_oxide_ext::Amount)],
) -> anyhow::Result<(TxReceipt, String)> {
self.ensure_synchronized_blocking()
.context("Cannot construct transaction when wallet is not synchronized")?;

let output_addresses = destinations
.iter()
.map(|(address, _)| *address)
.collect::<Vec<_>>();

let mut pending_tx = self.create_pending_transaction_multi_dest(destinations, false)?;

let built = (|| -> anyhow::Result<(TxReceipt, String)> {
let (txid, tx_keys) = pending_tx.validate_single_txid(&output_addresses).context(
"Failed to ensure transaction has one txid and at least one tx key before constructing",
)?;

let height = self.blockchain_height();

let tx_hex = pending_tx.raw_tx_hex(&txid)?;

Ok((TxReceipt { txid, tx_keys, height }, tx_hex))
})();

if let Err(e) = self.dispose_pending_transaction(pending_tx) {
tracing::error!(error=%e, "Failed to dispose pending transaction after constructing");
}

built
}

/// Create a pending transaction without publishing it.
/// Returns the pending transaction that can be inspected before publishing.
fn create_pending_transaction_single_dest(
Expand Down Expand Up @@ -2670,10 +2724,7 @@ impl FfiWallet {
/// Returns an error if the underlying FFI call fails. Callers decide whether
/// to propagate it: where it would mask a more important result (e.g. after a
/// publish attempt) it should be logged instead.
fn dispose_pending_transaction(
&mut self,
tx: PendingTransactionHandle,
) -> anyhow::Result<()> {
fn dispose_pending_transaction(&mut self, tx: PendingTransactionHandle) -> anyhow::Result<()> {
// Safety: we pass a valid pointer and we verified it's not used again since PendingTransaction is moved into this function
unsafe {
self.inner
Expand Down Expand Up @@ -3011,6 +3062,20 @@ impl PendingTransactionHandle {

Ok((txid, keys_map))
}

fn raw_tx_hex(&self, txid: &str) -> anyhow::Result<String> {
self.check_error()
.context("Pending transaction is in an error state")?;

let_cxx_string!(txid_cxx = txid);
let hex = ffi::pendingTransactionRawTxHex(self, &txid_cxx)
.context(
"Failed to get raw transaction hex from pending transaction: FFI call failed with exception",
)?
.to_string();

Ok(hex)
}
}

impl SyncProgress {
Expand Down
17 changes: 17 additions & 0 deletions monero-wallet-ng/src/util.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
use monero_oxide::ed25519::{Point, Scalar};
use monero_oxide_wallet::transaction::{NotPruned, Transaction};

/// Derive the Ed25519 public key for a private scalar.
pub fn public_key(private_key: &Scalar) -> Point {
Point::from(curve25519_dalek::constants::ED25519_BASEPOINT_POINT * (*private_key).into())
}

#[derive(Debug, thiserror::Error)]
pub enum TransactionFromHexError {
#[error("Transaction blob was not valid hex")]
Hex(#[from] hex::FromHexError),
#[error("Failed to deserialize transaction blob")]
Read(#[from] std::io::Error),
}

pub fn transaction_from_hex(
blob_hex: &str,
) -> Result<Transaction<NotPruned>, TransactionFromHexError> {
let bytes = hex::decode(blob_hex)?;
let tx = Transaction::read(&mut bytes.as_slice())?;
Ok(tx)
}
27 changes: 26 additions & 1 deletion monero-wallet/src/wallets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! Mostly we do two things:
//! - wait for transactions to be confirmed
//! - send money from one wallet to another.
pub use monero_sys::{Daemon, WalletHandle as Wallet, WalletHandleListener};
pub use monero_sys::{Daemon, TxReceipt, WalletHandle as Wallet, WalletHandleListener};

use anyhow::{Context, Result};
use monero_address::Network;
Expand Down Expand Up @@ -398,6 +398,31 @@ impl Wallets {
.context("Failed to construct sweep transaction to destination")
}

pub async fn construct_multi_destination_tx(
&self,
destinations: &[(monero_address::MoneroAddress, monero_oxide_ext::Amount)],
) -> Result<(Transaction<NotPruned>, TxReceipt)> {
let (receipt, tx_hex) = self
.main_wallet()
.await
.construct_multi_destination_tx(destinations)
.await
.context("Failed to construct Monero transaction")?;

let tx = monero_wallet_ng::util::transaction_from_hex(&tx_hex)
.context("Failed to parse constructed Monero transaction")?;

let tx_hash = hex::encode(tx.hash());
anyhow::ensure!(
tx_hash == receipt.txid,
"Parsed Monero transaction hash {} does not match wallet-reported txid {}",
tx_hash,
receipt.txid
);

Ok((tx, receipt))
}

/// Verify a transfer using the new monero-wallet-ng implementation.
///
/// This verifies that a transaction sends the expected amount to the given view pair
Expand Down
32 changes: 32 additions & 0 deletions swap-db/src/alice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ pub enum Alice {
BtcLocked {
state3: alice::State3,
},
XmrLockTransactionConstructed {
monero_wallet_restore_blockheight: BlockHeight,
#[serde(with = "swap_serde::monero::transaction")]
xmr_lock_tx: monero_oxide_wallet::transaction::Transaction,
transfer_proof: TransferProof,
state3: alice::State3,
},
XmrLockTransactionSent {
monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
Expand Down Expand Up @@ -169,6 +176,17 @@ impl From<AliceState> for Alice {
Alice::BtcLockTransactionSeen { state3: *state3 }
}
AliceState::BtcLocked { state3 } => Alice::BtcLocked { state3: *state3 },
AliceState::XmrLockTransactionConstructed {
monero_wallet_restore_blockheight,
xmr_lock_tx,
transfer_proof,
state3,
} => Alice::XmrLockTransactionConstructed {
monero_wallet_restore_blockheight,
xmr_lock_tx,
transfer_proof,
state3: *state3,
},
AliceState::XmrLockTransactionSent {
monero_wallet_restore_blockheight,
transfer_proof,
Expand Down Expand Up @@ -344,6 +362,17 @@ impl From<Alice> for AliceState {
Alice::BtcLocked { state3 } => AliceState::BtcLocked {
state3: Box::new(state3),
},
Alice::XmrLockTransactionConstructed {
monero_wallet_restore_blockheight,
xmr_lock_tx,
transfer_proof,
state3,
} => AliceState::XmrLockTransactionConstructed {
monero_wallet_restore_blockheight,
xmr_lock_tx,
transfer_proof,
state3: Box::new(state3),
},
Alice::XmrLockTransactionSent {
monero_wallet_restore_blockheight,
transfer_proof,
Expand Down Expand Up @@ -519,6 +548,9 @@ impl fmt::Display for Alice {
write!(f, "Bitcoin lock transaction in mempool")
}
Alice::BtcLocked { .. } => f.write_str("Bitcoin locked"),
Alice::XmrLockTransactionConstructed { .. } => {
f.write_str("Monero lock transaction constructed")
}
Alice::XmrLockTransactionSent { .. } => f.write_str("Monero lock transaction sent"),
Alice::XmrLocked { .. } => f.write_str("Monero locked"),
Alice::XmrLockTransferProofSent { .. } => {
Expand Down
13 changes: 12 additions & 1 deletion swap-machine/src/alice/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ pub enum AliceState {
BtcEarlyRefundable {
state3: Box<State3>,
},
XmrLockTransactionConstructed {
monero_wallet_restore_blockheight: BlockHeight,
xmr_lock_tx: monero_oxide_wallet::transaction::Transaction,
transfer_proof: TransferProof,
state3: Box<State3>,
},
XmrLockTransactionSent {
monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
Expand Down Expand Up @@ -184,6 +190,9 @@ impl fmt::Display for AliceState {
write!(f, "bitcoin lock transaction in mempool")
}
AliceState::BtcLocked { .. } => write!(f, "btc is locked"),
AliceState::XmrLockTransactionConstructed { .. } => {
write!(f, "xmr lock transaction constructed")
}
AliceState::XmrLockTransactionSent { .. } => write!(f, "xmr lock transaction sent"),
AliceState::XmrLocked { .. } => write!(f, "xmr is locked"),
AliceState::XmrLockTransferProofSent { .. } => {
Expand Down Expand Up @@ -1165,7 +1174,9 @@ impl ReservesMonero for AliceState {
AliceState::Started { .. } => monero::Amount::ZERO,
// These are the only states where we have to assume we will have to lock
// our Monero, and we haven't done so yet.
AliceState::BtcLockTransactionSeen { state3 } | AliceState::BtcLocked { state3 } => {
AliceState::BtcLockTransactionSeen { state3 }
| AliceState::BtcLocked { state3 }
| AliceState::XmrLockTransactionConstructed { state3, .. } => {
// We reserve as much Monero as we need for the output of the lock transaction
// and as we need for the network fee
state3.xmr.min_conservative_balance_to_spend()
Expand Down
3 changes: 2 additions & 1 deletion swap/src/asb/recovery/cancel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ pub async fn cancel(
| AliceState::BtcLockTransactionSeen { .. }
| AliceState::BtcLocked { .. } => bail!("Cannot cancel swap {} because it is in state {} where no XMR was locked.", swap_id, state),

AliceState::XmrLockTransactionSent { monero_wallet_restore_blockheight, transfer_proof, state3, }
AliceState::XmrLockTransactionConstructed { monero_wallet_restore_blockheight, transfer_proof, state3, .. }
| AliceState::XmrLockTransactionSent { monero_wallet_restore_blockheight, transfer_proof, state3, }
| AliceState::XmrLocked { monero_wallet_restore_blockheight, transfer_proof, state3 }
| AliceState::XmrLockTransferProofSent { monero_wallet_restore_blockheight, transfer_proof, state3 }

Expand Down
3 changes: 2 additions & 1 deletion swap/src/asb/recovery/punish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ pub async fn punish(

let (state3, transfer_proof) = match state {
// Punish potentially possible (no knowledge of cancel transaction)
AliceState::XmrLockTransactionSent {state3, transfer_proof, ..}
AliceState::XmrLockTransactionConstructed {state3, transfer_proof, ..}
| AliceState::XmrLockTransactionSent {state3, transfer_proof, ..}
| AliceState::XmrLocked {state3, transfer_proof, ..}
| AliceState::XmrLockTransferProofSent {state3, transfer_proof, ..}
| AliceState::EncSigLearned {state3, transfer_proof, ..}
Expand Down
1 change: 1 addition & 0 deletions swap/src/asb/recovery/redeem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub async fn redeem(
AliceState::Started { .. }
| AliceState::BtcLockTransactionSeen { .. }
| AliceState::BtcLocked { .. }
| AliceState::XmrLockTransactionConstructed { .. }
| AliceState::XmrLockTransactionSent { .. }
| AliceState::XmrLocked { .. }
| AliceState::XmrLockTransferProofSent { .. }
Expand Down
3 changes: 2 additions & 1 deletion swap/src/asb/recovery/refund.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ pub async fn refund(
| AliceState::BtcLocked { .. } => bail!(Error::NoXmrLocked(state)),

// Refund potentially possible (no knowledge of cancel transaction)
AliceState::XmrLockTransactionSent { transfer_proof, state3, .. }
AliceState::XmrLockTransactionConstructed { transfer_proof, state3, .. }
| AliceState::XmrLockTransactionSent { transfer_proof, state3, .. }
| AliceState::XmrLocked { transfer_proof, state3, .. }
| AliceState::XmrLockTransferProofSent { transfer_proof, state3, .. }
| AliceState::EncSigLearned { transfer_proof, state3, .. }
Expand Down
Loading
Loading