support regtest wallet config for jni wallet (#139)
What changed, and why it matters
This commit adds support for a 'regtest' (regression test) mode flag when opening or creating full wallets through the Java JNI bridge. Regtest is a private, sandboxed Monero network used for testing, similar to a practice environment. The change also explicitly blocks the regtest option for RPC wallets, where it is not supported. There is no direct security vulnerability in the diff itself; it is a feature addition for testing configuration. The main security consideration is that if a user mistakenly enables regtest on a mainnet wallet, it could cause confusion or incorrect behavior, but the code enforces that regtest can only be used with mainnet network type.
No immediate security action required. Treat as a normal feature commit. Reviewers may want to confirm that the underlying C++ `monero_wallet_full::open_wallet` and `open_wallet_data` functions handle the regtest flag safely and that the new JNI boolean is correctly marshaled on all supported platforms.
Security signals we found
New JNI parameter added to native wallet open paths
Validation added to reject regtest for RPC wallet open/create
Test enforces regtest only with mainnet network type
Backward-compatible overloads default regtest to false
Evidence from the diff
The patch threads a new boolean regtest parameter through the JNI boundary for openWallet and openWalletData in MoneroWalletFull, matching an updated underlying C++ API signature. It adds regtest to MoneroWalletConfig, preserves backward compatibility via overloaded methods defaulting to false, and rejects regtest in MoneroWalletRpc open/create paths with explicit validation errors. A test asserts that regtest requires mainnet network type. No memory-management, input-validation, or cryptographic flaws are evident in the diff.
Changed components
src/main/cpp/monero_jni_bridge.cppsrc/main/cpp/monero_jni_bridge.hsrc/main/java/monero/wallet/MoneroWalletFull.javasrc/main/java/monero/wallet/MoneroWalletRpc.javasrc/main/java/monero/wallet/model/MoneroWalletConfig.javasrc/test/java/TestMoneroWalletFull.javaInspect captured patch +37 / −12
diff --git a/src/main/cpp/monero_jni_bridge.cpp b/src/main/cpp/monero_jni_bridge.cpp
index 101f82d..7727edf 100644
--- a/src/main/cpp/monero_jni_bridge.cpp
+++ b/src/main/cpp/monero_jni_bridge.cpp
@@ -426,7 +426,7 @@ JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletFull_walletExistsJni(J
return static_cast<jboolean>(wallet_exists);
}
-JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletJni(JNIEnv *env, jclass clazz, jstring jpath, jstring jpassword, jint jnetwork_type) {
+JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletJni(JNIEnv *env, jclass clazz, jstring jpath, jstring jpassword, jint jnetwork_type, jboolean regtest) {
MTRACE("Java_monero_wallet_MoneroWalletFull_openWalletJni");
const char* _path = env->GetStringUTFChars(jpath, NULL);
const char* _password = env->GetStringUTFChars(jpassword, NULL);
@@ -437,7 +437,7 @@ JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletJni(JNIEnv
// load wallet from file
try {
- monero_wallet* wallet = monero_wallet_full::open_wallet(path, password, static_cast<monero_network_type>(jnetwork_type));
+ monero_wallet* wallet = monero_wallet_full::open_wallet(path, password, static_cast<monero_network_type>(jnetwork_type), regtest);
return reinterpret_cast<jlong>(wallet);
} catch (...) {
rethrow_cpp_exception_as_java_exception(env);
@@ -445,7 +445,7 @@ JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletJni(JNIEnv
}
}
-JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletDataJni(JNIEnv *env, jclass clazz, jstring jpassword, jint jnetwork_type, jbyteArray jkeys_data, jbyteArray jcache_data) {
+JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletDataJni(JNIEnv *env, jclass clazz, jstring jpassword, jint jnetwork_type, jbyteArray jkeys_data, jbyteArray jcache_data, jboolean regtest) {
MTRACE("Java_monero_wallet_MoneroWalletFull_openWalletDataJni()");
// convert password to string
@@ -468,7 +468,7 @@ JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletDataJni(JN
// load wallet from data
try {
- monero_wallet* wallet = monero_wallet_full::open_wallet_data(password, static_cast<monero_network_type>(jnetwork_type), keys_data, cache_data);
+ monero_wallet* wallet = monero_wallet_full::open_wallet_data(password, static_cast<monero_network_type>(jnetwork_type), keys_data, cache_data, monero_rpc_connection(), nullptr, regtest);
return reinterpret_cast<jlong>(wallet);
} catch (...) {
rethrow_cpp_exception_as_java_exception(env);
diff --git a/src/main/cpp/monero_jni_bridge.h b/src/main/cpp/monero_jni_bridge.h
index aa4e84c..0449131 100644
--- a/src/main/cpp/monero_jni_bridge.h
+++ b/src/main/cpp/monero_jni_bridge.h
@@ -61,9 +61,9 @@ JNIEXPORT void JNICALL Java_monero_common_MoneroUtils_configureLoggingJni(JNIEnv
JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletFull_walletExistsJni(JNIEnv *, jclass, jstring);
-JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletJni(JNIEnv *, jclass, jstring, jstring, jint);
+JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletJni(JNIEnv *, jclass, jstring, jstring, jint, jboolean);
-JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletDataJni(JNIEnv *, jclass, jstring, jint, jbyteArray, jbyteArray);
+JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletDataJni(JNIEnv *, jclass, jstring, jint, jbyteArray, jbyteArray, jboolean);
JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_createWalletJni(JNIEnv *, jclass, jstring);
diff --git a/src/main/java/monero/wallet/MoneroWalletFull.java b/src/main/java/monero/wallet/MoneroWalletFull.java
index 13a275a..85d7855 100644
--- a/src/main/java/monero/wallet/MoneroWalletFull.java
+++ b/src/main/java/monero/wallet/MoneroWalletFull.java
@@ -123,18 +123,20 @@ public class MoneroWalletFull extends MoneroWalletDefault {
* @param password is the password of the wallet file to open
* @param networkType is the wallet's network type
* @param daemonConnection is connection configuration to a daemon (default = an unconnected wallet)
+ * @param regtest indicates if the wallet should be opened in regtest mode
* @return the opened wallet
*/
- public static MoneroWalletFull openWallet(String path, String password, MoneroNetworkType networkType, MoneroRpcConnection daemonConnection) {
+ public static MoneroWalletFull openWallet(String path, String password, MoneroNetworkType networkType, MoneroRpcConnection daemonConnection, boolean regtest) {
if (!walletExistsJni(path)) throw new MoneroError("Wallet does not exist at path: " + path);
if (networkType == null) throw new MoneroError("Must provide a network type");
- long jniWalletHandle = openWalletJni(path, password, networkType.ordinal());
+ long jniWalletHandle = openWalletJni(path, password, networkType.ordinal(), regtest);
MoneroWalletFull wallet = new MoneroWalletFull(jniWalletHandle, password);
if (daemonConnection != null) wallet.setDaemonConnection(daemonConnection);
return wallet;
}
public static MoneroWalletFull openWallet(String path, String password, MoneroNetworkType networkType) { return openWallet(path, password, networkType, (MoneroRpcConnection) null); }
public static MoneroWalletFull openWallet(String path, String password, MoneroNetworkType networkType, String daemonUri) { return openWallet(path, password, networkType, daemonUri == null ? null : new MoneroRpcConnection(daemonUri)); }
+ public static MoneroWalletFull openWallet(String path, String password, MoneroNetworkType networkType, MoneroRpcConnection daemonConnection) { return openWallet(path, password, networkType, daemonConnection, false); }
/**
* Open an existing wallet from byte[] data using JNI bindings to wallet2.h
@@ -144,15 +146,17 @@ public class MoneroWalletFull extends MoneroWalletDefault {
* @param keysData the wallet's keys data
* @param cacheData the wallet's cache data
* @param daemonConnection connection configuration to a daemon (default = an unconnected wallet)
+ * @param regtest indicates if the wallet should be opened in regtest mode
* @return the opened wallet
*/
- public static MoneroWalletFull openWalletData(String password, MoneroNetworkType networkType, byte[] keysData, byte[] cacheData, MoneroRpcConnection daemonConnection) {
+ public static MoneroWalletFull openWalletData(String password, MoneroNetworkType networkType, byte[] keysData, byte[] cacheData, MoneroRpcConnection daemonConnection, boolean regtest) {
if (networkType == null) throw new MoneroError("Must provide a network type");
- long jniWalletHandle = openWalletDataJni(password, networkType.ordinal(), keysData == null ? new byte[0] : keysData, cacheData == null ? new byte[0] : cacheData);
+ long jniWalletHandle = openWalletDataJni(password, networkType.ordinal(), keysData == null ? new byte[0] : keysData, cacheData == null ? new byte[0] : cacheData, regtest);
MoneroWalletFull wallet = new MoneroWalletFull(jniWalletHandle, password);
if (daemonConnection != null) wallet.setDaemonConnection(daemonConnection);
return wallet;
}
+ public static MoneroWalletFull openWalletData(String password, MoneroNetworkType networkType, byte[] keysData, byte[] cacheData, MoneroRpcConnection daemonConnection) { return openWalletData(password, networkType, keysData, cacheData, daemonConnection, false); }
/**
* <p>Open an existing wallet using JNI bindings to wallet2.h.</p>
@@ -1399,9 +1403,9 @@ public class MoneroWalletFull extends MoneroWalletDefault {
private native static boolean walletExistsJni(String path);
- private native static long openWalletJni(String path, String password, int networkType);
+ private native static long openWalletJni(String path, String password, int networkType, boolean regtest);
- private native static long openWalletDataJni(String password, int networkType, byte[] keysData, byte[] cacheData);
+ private native static long openWalletDataJni(String password, int networkType, byte[] keysData, byte[] cacheData, boolean regtest);
private native static long createWalletJni(String walletConfigJson);
diff --git a/src/main/java/monero/wallet/MoneroWalletRpc.java b/src/main/java/monero/wallet/MoneroWalletRpc.java
index 7a13f0f..041dfa1 100644
--- a/src/main/java/monero/wallet/MoneroWalletRpc.java
+++ b/src/main/java/monero/wallet/MoneroWalletRpc.java
@@ -291,6 +291,7 @@ public class MoneroWalletRpc extends MoneroWalletDefault {
// validate config
if (config == null) throw new MoneroError("Must provide configuration of wallet to open");
if (config.getPath() == null || config.getPath().isEmpty()) throw new MoneroError("Filename is not initialized");
+ if (config.getRegtest() != null) throw new MoneroError("Cannot specify regtest mode when opening RPC wallet");
// TODO: ensure other fields are uninitialized?
// open wallet on rpc server
@@ -352,6 +353,7 @@ public class MoneroWalletRpc extends MoneroWalletDefault {
// validate config
if (config == null) throw new MoneroError("Must specify config to create wallet");
+ if (config.getRegtest() != null) throw new MoneroError("Cannot specify regtest mode when creating RPC wallet");
if (config.getNetworkType() != null) throw new MoneroError("Cannot specify network type when creating RPC wallet");
if (config.getSeed() != null && (config.getPrimaryAddress() != null || config.getPrivateViewKey() != null || config.getPrivateSpendKey() != null)) {
throw new MoneroError("Wallet can be initialized with a seed or keys but not both");
diff --git a/src/main/java/monero/wallet/model/MoneroWalletConfig.java b/src/main/java/monero/wallet/model/MoneroWalletConfig.java
index 29c1605..cdc91e2 100644
--- a/src/main/java/monero/wallet/model/MoneroWalletConfig.java
+++ b/src/main/java/monero/wallet/model/MoneroWalletConfig.java
@@ -34,6 +34,7 @@ public class MoneroWalletConfig {
private byte[] keysData;
private byte[] cacheData;
private Boolean isMultisig;
+ private Boolean regtest;
public MoneroWalletConfig() {
// default constructor
@@ -58,6 +59,7 @@ public class MoneroWalletConfig {
keysData = config.getKeysData();
cacheData = config.getCacheData();
isMultisig = config.isMultisig();
+ regtest = config.getRegtest();
}
public MoneroWalletConfig copy() {
@@ -293,6 +295,15 @@ public class MoneroWalletConfig {
return this;
}
+ public Boolean getRegtest() {
+ return regtest;
+ }
+
+ public MoneroWalletConfig setRegtest(Boolean regtest) {
+ this.regtest = regtest;
+ return this;
+ }
+
@Override
public String toString() {
return JsonUtils.serialize(this);
diff --git a/src/test/java/TestMoneroWalletFull.java b/src/test/java/TestMoneroWalletFull.java
index 92638a7..be51dc3 100644
--- a/src/test/java/TestMoneroWalletFull.java
+++ b/src/test/java/TestMoneroWalletFull.java
@@ -279,6 +279,14 @@ public class TestMoneroWalletFull extends TestMoneroWalletCommon {
if (daemon.isConnected()) assertEquals(daemon.getHeight(), wallet.getRestoreHeight());
else assertTrue(wallet.getRestoreHeight() >= 0);
wallet.close();
+
+ // test error creating wallet with regtest mode
+ try {
+ createWallet(new MoneroWalletConfig().setNetworkType(MoneroNetworkType.TESTNET).setRegtest(true));
+ throw new RuntimeException("Should have thrown exception");
+ } catch (Exception e) {
+ assertEquals("Network type must be mainnet when using regtest option", e.getMessage());
+ }
}
// Can create a full wallet from seed
Why this scored 20/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.