Skip to content

Commit 6d296bb

Browse files
committed
add integration test for monero_wallet_ng::verify_transfer
1 parent 81d41a1 commit 6d296bb

File tree

11 files changed

+243
-19
lines changed

11 files changed

+243
-19
lines changed

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ bdk_wallet = "2.0.0"
4040
bitcoin = { version = "0.32", features = ["rand", "serde"] }
4141

4242
# monero-oxide
43+
monero-daemon-rpc = { git = "https://github.com/kayabaNerve/monero-oxide.git", branch = "rpc-rewrite" }
4344
monero-oxide = { git = "https://github.com/kayabaNerve/monero-oxide.git", branch = "rpc-rewrite" }
4445
monero-oxide-wallet = { git = "https://github.com/kayabaNerve/monero-oxide.git", package = "monero-wallet", branch = "rpc-rewrite" }
4546
monero-simple-request-rpc = { git = "https://github.com/kayabaNerve/monero-oxide.git", branch = "rpc-rewrite" }

monero-sys/Cargo.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,22 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7+
# Crypto
8+
curve25519-dalek = { workspace = true }
9+
10+
# Database
11+
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono"] }
12+
13+
# Error handling
714
anyhow = { workspace = true }
15+
thiserror = "2.0.17"
16+
817
backoff = { workspace = true }
918
chrono = { version = "0.4", features = ["serde"] }
1019
cxx = "1.0.137"
20+
hex = "0.4"
1121
monero = { workspace = true }
1222
serde = { workspace = true }
13-
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono"] }
14-
thiserror = "2.0.17"
1523
tokio = { workspace = true, features = ["sync", "time", "rt"] }
1624
tracing = { workspace = true }
1725
typeshare = { workspace = true }

monero-sys/src/bridge.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,22 @@ namespace Monero
303303
return std::make_unique<std::string>(tx_info.hash());
304304
}
305305

306+
/**
307+
* Get the secret view key of the wallet.
308+
*/
309+
inline std::unique_ptr<std::string> secretViewKey(const Wallet &wallet)
310+
{
311+
return std::make_unique<std::string>(wallet.secretViewKey());
312+
}
313+
314+
/**
315+
* Get the public spend key of the wallet.
316+
*/
317+
inline std::unique_ptr<std::string> publicSpendKey(const Wallet &wallet)
318+
{
319+
return std::make_unique<std::string>(wallet.publicSpendKey());
320+
}
321+
306322
/**
307323
* Get the timestamp of a transaction from TransactionInfo.
308324
*/

monero-sys/src/bridge.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,12 @@ pub mod ffi {
385385
total: &mut u64,
386386
spent: &mut u64,
387387
) -> Result<bool>;
388+
389+
/// Get the secret view key of the wallet.
390+
fn secretViewKey(wallet: &Wallet) -> Result<UniquePtr<CxxString>>;
391+
392+
/// Get the public spend key of the wallet.
393+
fn publicSpendKey(wallet: &Wallet) -> Result<UniquePtr<CxxString>>;
388394
}
389395
}
390396

monero-sys/src/lib.rs

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub use bridge::{TraceListener, WalletEventListener, WalletListenerBox};
1818
pub use database::{Database, RecentWallet};
1919
use throttle::Throttle;
2020

21+
use std::io::Cursor;
2122
use std::panic::{catch_unwind, AssertUnwindSafe};
2223
use std::sync::{Arc, Mutex};
2324
use std::{
@@ -584,6 +585,59 @@ impl WalletHandle {
584585
.context("Couldn't complete wallet call")?
585586
}
586587

588+
/// Get the secret view key of the wallet.
589+
pub async fn secret_view_key(&self) -> anyhow::Result<curve25519_dalek::scalar::Scalar> {
590+
let secret_view_key = self
591+
.call(move |wallet| wallet.secret_view_key())
592+
.await
593+
.context("Couldn't complete wallet call")?
594+
.context("Couldn't get secret view key string from wallet2")?;
595+
596+
let secret_view_key_bytes = hex::decode(&secret_view_key)
597+
.context("Failed to decode secret view key string from wallet2 from hex to bytes")?;
598+
599+
let secret_view_key: [u8; 32] =
600+
secret_view_key_bytes.try_into().map_err(|v: Vec<u8>| {
601+
anyhow!(
602+
"Secret view key has wrong length: expected 32 bytes, got {}",
603+
v.len()
604+
)
605+
})?;
606+
607+
let private_view_key =
608+
curve25519_dalek::scalar::Scalar::from_canonical_bytes(secret_view_key)
609+
.into_option()
610+
.context("Failed to convert secret view key bytes to Scalar")?;
611+
612+
Ok(private_view_key)
613+
}
614+
615+
/// Get the public spend key of the wallet.
616+
pub async fn public_spend_key(
617+
&self,
618+
) -> anyhow::Result<curve25519_dalek::edwards::CompressedEdwardsY> {
619+
let public_spend_key = self
620+
.call(move |wallet| wallet.public_spend_key())
621+
.await
622+
.context("Couldn't complete wallet call")?
623+
.context("Couldn't get public spend key string from wallet2")?;
624+
625+
let public_spend_key_bytes = hex::decode(&public_spend_key)
626+
.context("Failed to decode public spend key string from wallet2 from hex to bytes")?;
627+
628+
let public_spend_key: [u8; 32] =
629+
public_spend_key_bytes.try_into().map_err(|v: Vec<u8>| {
630+
anyhow!(
631+
"Public spend key has wrong length: expected 32 bytes, got {}",
632+
v.len()
633+
)
634+
})?;
635+
636+
Ok(curve25519_dalek::edwards::CompressedEdwardsY(
637+
public_spend_key,
638+
))
639+
}
640+
587641
/// Get the creation height of the wallet.
588642
pub async fn creation_height(&self) -> anyhow::Result<u64> {
589643
self.call(move |wallet| wallet.creation_height())
@@ -946,7 +1000,6 @@ impl WalletHandle {
9461000
Ok(())
9471001
}
9481002

949-
9501003
/// Wait for an incoming transaction with at least the specified amount.
9511004
///
9521005
/// This method polls the wallet's transaction history at the specified interval
@@ -985,7 +1038,7 @@ impl WalletHandle {
9851038
confirmations = tx.confirmations,
9861039
"Found incoming transaction that meets the amount lower bound"
9871040
);
988-
1041+
9891042
return Ok(tx);
9901043
}
9911044
}
@@ -2664,6 +2717,32 @@ impl FfiWallet {
26642717
Ok(seed)
26652718
}
26662719

2720+
/// Get the secret view key of the wallet.
2721+
fn secret_view_key(&self) -> anyhow::Result<String> {
2722+
let key = ffi::secretViewKey(&self.inner)
2723+
.context("Failed to get secret view key")?
2724+
.to_string();
2725+
2726+
if key.is_empty() {
2727+
bail!("Failed to get secret view key");
2728+
}
2729+
2730+
Ok(key)
2731+
}
2732+
2733+
/// Get the public spend key of the wallet.
2734+
fn public_spend_key(&self) -> anyhow::Result<String> {
2735+
let key = ffi::publicSpendKey(&self.inner)
2736+
.context("Failed to get public spend key")?
2737+
.to_string();
2738+
2739+
if key.is_empty() {
2740+
bail!("wallet2 returned an empty public spend key");
2741+
}
2742+
2743+
Ok(key)
2744+
}
2745+
26672746
/// Sign a message with the wallet's private key.
26682747
///
26692748
/// # Arguments

monero-tests/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,21 @@ edition = "2024"
55

66
[dependencies]
77
monero = { workspace = true }
8+
monero-daemon-rpc = { workspace = true }
89
monero-harness = { path = "../monero-harness" }
10+
monero-oxide-wallet = { workspace = true }
11+
monero-simple-request-rpc = { workspace = true }
912
monero-sys = { path = "../monero-sys" }
13+
monero-wallet-ng = { path = "../monero-wallet-ng" }
1014
testcontainers = { workspace = true }
1115

1216
anyhow = { workspace = true }
17+
hex = "0.4"
1318
tokio = { workspace = true, features = ["macros", "rt"] }
1419
tracing = { workspace = true }
1520
tracing-subscriber = { workspace = true }
21+
uuid = { workspace = true }
22+
zeroize = "1.5"
1623

1724
[lints]
1825
workspace = true

monero-tests/tests/transaction_keys.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ async fn monero_transfers() -> anyhow::Result<()> {
1212

1313
let cli = Cli::default();
1414
let wallets = vec!["alice", "bob", "candice"];
15-
// Disbale background sync for these wallet -- this way we _have_ to use the transfer proof to discover the transactions.
15+
16+
// Disable background sync for these wallet -- this way we _have_ to use the transfer proof to discover the transactions.
1617
let (monero, _container, _wallet_conainers) =
1718
monero_harness::Monero::new_with_sync_specified(&cli, wallets, false).await?;
1819

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use monero_daemon_rpc::MoneroDaemon;
2+
use monero_harness::image::RPC_PORT;
3+
use monero_harness::Cli;
4+
use monero_simple_request_rpc::SimpleRequestTransport;
5+
use monero_sys::{TxReceipt, WalletHandle};
6+
use monero_wallet_ng::verify_transfer;
7+
use testcontainers::Container;
8+
use zeroize::Zeroizing;
9+
10+
type TestSetup<'a> = (
11+
WalletHandle,
12+
TxReceipt,
13+
u64,
14+
MoneroDaemon<SimpleRequestTransport>,
15+
Container<'a, monero_harness::image::Monerod>,
16+
Vec<Container<'a, monero_harness::image::MoneroWalletRpc>>,
17+
);
18+
19+
async fn setup(cli: &Cli) -> anyhow::Result<TestSetup<'_>> {
20+
tracing_subscriber::fmt()
21+
.with_env_filter(
22+
"info,test=debug,monero_harness=debug,monero_rpc=debug,monero_sys=trace,verify_transfer=trace",
23+
)
24+
.try_init()
25+
.ok();
26+
27+
let wallets = vec!["alice"];
28+
let (monero, monerod_container, wallet_containers) =
29+
monero_harness::Monero::new(cli, wallets).await?;
30+
31+
monero.init_and_start_miner().await?;
32+
33+
let miner_wallet = monero.wallet("miner")?;
34+
let alice = monero.wallet("alice")?;
35+
36+
assert!(miner_wallet.balance().await? > 0);
37+
38+
let alice_address = alice.address().await?;
39+
40+
let transfer_amount: u64 = 1_000_000_000_000; // 1 XMR in piconero
41+
42+
let tx_receipt = miner_wallet
43+
.transfer(&alice_address, transfer_amount)
44+
.await?;
45+
46+
monero.generate_block().await?;
47+
48+
let alice_wallet = alice.wallet().clone();
49+
50+
let rpc_port = monerod_container.get_host_port_ipv4(RPC_PORT);
51+
let rpc_url = format!("http://127.0.0.1:{}", rpc_port);
52+
let daemon = SimpleRequestTransport::new(rpc_url)
53+
.await
54+
.expect("failed to create RPC client");
55+
56+
Ok((
57+
alice_wallet,
58+
tx_receipt,
59+
transfer_amount,
60+
daemon,
61+
monerod_container,
62+
wallet_containers,
63+
))
64+
}
65+
66+
/// Tests that `monero_wallet_ng::verify_transfer` correctly verifies a transaction sent to a wallet.
67+
#[tokio::test]
68+
async fn verify_transfer_succeeds_for_valid_transfer() -> anyhow::Result<()> {
69+
let cli = Cli::default();
70+
let (alice_wallet, tx_receipt, transfer_amount, daemon, _c, _wc) = setup(&cli).await?;
71+
72+
let tx_id: [u8; 32] = hex::decode(&tx_receipt.txid)?
73+
.try_into()
74+
.map_err(|_| anyhow::anyhow!("Invalid tx_id length"))?;
75+
76+
let secret_view_scalar = alice_wallet
77+
.secret_view_key()
78+
.await
79+
.expect("Failed to get secret view key");
80+
81+
let public_spend_compressed = alice_wallet
82+
.public_spend_key()
83+
.await
84+
.expect("Failed to get public spend key");
85+
86+
let private_view_key = Zeroizing::new(secret_view_scalar);
87+
let public_spend_key = public_spend_compressed
88+
.decompress()
89+
.expect("Failed to decompress public spend key");
90+
91+
let transfer_verified = verify_transfer(
92+
&daemon,
93+
tx_id,
94+
public_spend_key,
95+
private_view_key,
96+
transfer_amount,
97+
)
98+
.await?;
99+
100+
assert!(
101+
transfer_verified,
102+
"verify_transfer should confirm the transfer amount"
103+
);
104+
105+
Ok(())
106+
}

monero-wallet-ng/examples/mainnet_verify_transfer.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ use monero_oxide_wallet::ed25519::{CompressedPoint, Scalar};
66
use monero_simple_request_rpc::SimpleRequestTransport;
77
use zeroize::Zeroizing;
88

9-
use monero_wallet3::verify_transfer;
9+
use monero_wallet_ng::verify_transfer;
1010

11-
/// Default Monero node URL if none provided via CLI
1211
const DEFAULT_NODE_URL: &str = "http://xmr-node.cakewallet.com:18081";
1312

14-
/// Transaction ID to verify (hex encoded)
13+
/// Transaction ID to verify
1514
/// https://xmrchain.net/tx/fdd9ce7194cd8e6d14ecf7c5f49e7882be7248cde26215bb0bd9e20de1791b8e
1615
const TX_ID_HEX: &str = "fdd9ce7194cd8e6d14ecf7c5f49e7882be7248cde26215bb0bd9e20de1791b8e";
1716

@@ -23,8 +22,8 @@ const SECRET_VIEW_KEY_HEX: &str =
2322
const PUBLIC_SPEND_KEY_HEX: &str =
2423
"1031c07d9b5772d14797826587d23135f2495a2bf173a5ae802f90d8fd1625be";
2524

26-
/// Expected amount: 0.001 XMR = 1_000_000_000 piconero
27-
const EXPECTED_AMOUNT: u64 = 1_000_000_000;
25+
/// Expected amount (0.001 XMR)
26+
const EXPECTED_AMOUNT: u64 = 1_00_000_000;
2827

2928
fn hex_to_bytes<const N: usize>(hex: &str) -> [u8; N] {
3029
let mut bytes = [0u8; N];

0 commit comments

Comments
 (0)