From 260251789a628da8b679aa3d1db5613e1741a561 Mon Sep 17 00:00:00 2001 From: everoddandeven Date: Mon, 9 Sep 2024 13:19:59 +0200 Subject: [PATCH] MoneroWalletLight implementation --- src/main/cpp/monero_jni_bridge.cpp | 1831 +++++++++++- src/main/cpp/monero_jni_bridge.h | 220 +- .../java/monero/wallet/MoneroWalletFull.java | 220 +- .../java/monero/wallet/MoneroWalletLight.java | 380 +++ src/test/java/TestMoneroWalletCommon.java | 23 +- src/test/java/TestMoneroWalletFull.java | 12 +- src/test/java/TestMoneroWalletLight.java | 2626 +++++++++++++++++ src/test/java/utils/TestUtils.java | 53 +- 8 files changed, 5236 insertions(+), 129 deletions(-) create mode 100644 src/main/java/monero/wallet/MoneroWalletLight.java create mode 100644 src/test/java/TestMoneroWalletLight.java diff --git a/src/main/cpp/monero_jni_bridge.cpp b/src/main/cpp/monero_jni_bridge.cpp index e07f3115..909b2ade 100644 --- a/src/main/cpp/monero_jni_bridge.cpp +++ b/src/main/cpp/monero_jni_bridge.cpp @@ -37,6 +37,7 @@ #include #include "monero_jni_bridge.h" #include "wallet/monero_wallet_full.h" +#include "wallet/monero_wallet_light.h" #include "utils/monero_utils.h" using namespace std; @@ -416,7 +417,7 @@ JNIEXPORT void JNICALL Java_monero_common_MoneroUtils_configureLoggingJni(JNIEnv monero_utils::configure_logging(path, console); } -// --------------------------- STATIC WALLET UTILS ---------------------------- +// --------------------------- STATIC FULL WALLET UTILS ---------------------------- JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletFull_walletExistsJni(JNIEnv *env, jclass clazz, jstring jpath) { MTRACE("Java_monero_wallet_MoneroWalletFull_walletExistsJni"); @@ -518,7 +519,111 @@ JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletFull_getSeedLangua return jlanguages; } -// ------------------------ WALLET INSTANCE METHODS -------------------------- + +// --------------------------- STATIC LIGHT WALLET UTILS ---------------------------- + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_walletExistsJni(JNIEnv *env, jclass clazz, jstring jpath) { + MTRACE("Java_monero_wallet_MoneroWalletLight_walletExistsJni"); + const char* _path = env->GetStringUTFChars(jpath, NULL); + string path = string(_path); + env->ReleaseStringUTFChars(jpath, _path); + bool wallet_exists = monero_wallet_light::wallet_exists(path); + return static_cast(wallet_exists); +} + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_openWalletJni(JNIEnv *env, jclass clazz, jstring jpath, jstring jpassword, jint jnetwork_type) { + MTRACE("Java_monero_wallet_MoneroWalletLight_openWalletJni"); + const char* _path = env->GetStringUTFChars(jpath, NULL); + const char* _password = env->GetStringUTFChars(jpassword, NULL); + string path = string(_path); + string password = string(_password ? _password : ""); + env->ReleaseStringUTFChars(jpath, _path); + env->ReleaseStringUTFChars(jpassword, _password); + + // load wallet from file + try { + monero_wallet* wallet = monero_wallet_light::open_wallet(path, password, static_cast(jnetwork_type)); + return reinterpret_cast(wallet); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_openWalletDataJni(JNIEnv *env, jclass clazz, jstring jpassword, jint jnetwork_type, jbyteArray jkeys_data, jbyteArray jcache_data) { + MTRACE("Java_monero_wallet_MoneroWalletLight_openWalletDataJni()"); + + // convert password to string + const char* _password = env->GetStringUTFChars(jpassword, NULL); + string password = string(_password ? _password : ""); + env->ReleaseStringUTFChars(jpassword, _password); + + // convert keys bytes to string + int keys_length = env->GetArrayLength(jkeys_data); + jboolean is_copy; + jbyte* _keys_data = env->GetByteArrayElements(jkeys_data, &is_copy); + string keys_data = string((char*) _keys_data, keys_length); + env->ReleaseByteArrayElements(jkeys_data, _keys_data, JNI_ABORT); + + // convert cache bytes to string + int cache_length = env->GetArrayLength(jcache_data); + jbyte* _cache_data = env->GetByteArrayElements(jcache_data, &is_copy); + string cache_data = string((char*) _cache_data, cache_length); + env->ReleaseByteArrayElements(jcache_data, _cache_data, JNI_ABORT); + + // load wallet from data + try { + monero_wallet* wallet = monero_wallet_light::open_wallet_data(password, static_cast(jnetwork_type), keys_data, cache_data); + return reinterpret_cast(wallet); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_createWalletJni(JNIEnv *env, jclass clazz, jstring jconfig) { + MTRACE("Java_monero_wallet_MoneroWalletLight_createWalletJni"); + + // get config as json string + const char* _config = jconfig ? env->GetStringUTFChars(jconfig, NULL) : nullptr; + string config_json = string(_config ? _config : ""); + env->ReleaseStringUTFChars(jconfig, _config); + + // deserialize wallet config + shared_ptr config = monero_wallet_config::deserialize(config_json); + + // construct wallet + try { + monero_wallet* wallet = monero_wallet_light::create_wallet(*config); + return reinterpret_cast(wallet); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_getSeedLanguagesJni(JNIEnv *env, jclass clazz) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getLanguagesJni"); + + // get languages + vector languages; + try { + languages = monero_wallet_light::get_seed_languages(); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } + + // build java string array + jobjectArray jlanguages = env->NewObjectArray(languages.size(), env->FindClass("java/lang/String"), nullptr); + for (int i = 0; i < languages.size(); i++) { + env->SetObjectArrayElement(jlanguages, i, env->NewStringUTF(languages[i].c_str())); + } + return jlanguages; +} + + +// ------------------------ FULL WALLET INSTANCE METHODS -------------------------- JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletFull_isViewOnlyJni(JNIEnv *env, jobject instance) { MTRACE("Java_monero_wallet_MoneroWalletFull_isViewOnlyJni"); @@ -2239,6 +2344,1728 @@ JNIEXPORT jbyteArray JNICALL Java_monero_wallet_MoneroWalletFull_getCacheFileBuf } } + +// ------------------------ FULL WALLET INSTANCE METHODS -------------------------- + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_isViewOnlyJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_isViewOnlyJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + return wallet->is_view_only(); +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_setDaemonConnectionJni(JNIEnv *env, jobject instance, jstring juri, jstring jusername, jstring jpassword) { + MTRACE("Java_monero_wallet_MoneroWalletLight_setDaemonConnectionJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + set_daemon_connection(env, wallet, juri, jusername, jpassword); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_setProxyJni(JNIEnv *env, jobject instance, jstring juri) { + MTRACE("Java_monero_wallet_MoneroWalletLight_setProxyJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _uri = juri ? env->GetStringUTFChars(juri, NULL) : nullptr; + string uri = string(_uri ? _uri : ""); + env->ReleaseStringUTFChars(juri, _uri); + try { + wallet->set_daemon_proxy(uri); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_getDaemonConnectionJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getDaemonConnectionJni()"); + + // get wallet + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // get daemon connection + try { + boost::optional daemon_connection = wallet->get_daemon_connection(); + if (daemon_connection == boost::none) return 0; + + // return string[uri, username, password] + jobjectArray vals = env->NewObjectArray(3, env->FindClass("java/lang/String"), nullptr); + if (daemon_connection->m_uri != boost::none && !daemon_connection->m_uri.get().empty()) env->SetObjectArrayElement(vals, 0, env->NewStringUTF(daemon_connection->m_uri.get().c_str())); + if (daemon_connection->m_username != boost::none && !daemon_connection->m_username.get().empty()) env->SetObjectArrayElement(vals, 1, env->NewStringUTF(daemon_connection->m_username.get().c_str())); + if (daemon_connection->m_password != boost::none && !daemon_connection->m_password.get().empty()) env->SetObjectArrayElement(vals, 2, env->NewStringUTF(daemon_connection->m_password.get().c_str())); + return vals; + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_isConnectedToDaemonJni(JNIEnv* env, jobject instance) { + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return static_cast(wallet->is_connected_to_daemon()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_isDaemonSyncedJni(JNIEnv* env, jobject instance) { + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return wallet->is_daemon_synced(); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_isSyncedJni(JNIEnv* env, jobject instance) { + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return wallet->is_synced(); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getVersionJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getVersionJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return env->NewStringUTF(wallet->get_version().serialize().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getPathJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getPathJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + return env->NewStringUTF(wallet->get_path().c_str()); +} + +JNIEXPORT jint JNICALL Java_monero_wallet_MoneroWalletLight_getNetworkTypeJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getNetworkTypeJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + return wallet->get_network_type(); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getSeedJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getSeedJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return env->NewStringUTF(wallet->get_seed().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getSeedLanguageJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getSeedLanguageJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return env->NewStringUTF(wallet->get_seed_language().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getPublicViewKeyJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getPublicViewKeyJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return env->NewStringUTF(wallet->get_public_view_key().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getPrivateViewKeyJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getPrivateViewKeyJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return env->NewStringUTF(wallet->get_private_view_key().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getPublicSpendKeyJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getPublicSpendKeyJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return env->NewStringUTF(wallet->get_public_spend_key().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getPrivateSpendKeyJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getPrivateSpendKeyJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return env->NewStringUTF(wallet->get_private_spend_key().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getAddressJni(JNIEnv *env, jobject instance, jint account_idx, jint subaddress_idx) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getAddressJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + string address = wallet->get_address((uint32_t) account_idx, (uint32_t) subaddress_idx); + return env->NewStringUTF(address.c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getAddressIndexJni(JNIEnv *env, jobject instance, jstring jaddress) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getAddressIndexJni"); + + // collect and release string param + const char* _address = jaddress ? env->GetStringUTFChars(jaddress, NULL) : nullptr; + string address = string(_address ? _address : ""); + env->ReleaseStringUTFChars(jaddress, _address); + + // get indices of addresse's subaddress + try { + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + monero_subaddress subaddress = wallet->get_address_index(address); + string subaddress_json = subaddress.serialize(); + return env->NewStringUTF(subaddress_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +/** + * Only one listener needs to subscribe over JNI, so this removes the previously registered listener + * and registers the new listener. + */ +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_setListenerJni(JNIEnv *env, jobject instance, jobject jlistener) { + MTRACE("Java_monero_wallet_MoneroWalletLight_setListenerJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // remove old listener + wallet_jni_listener* old_listener = get_handle(env, instance, JNI_LISTENER_HANDLE); + if (old_listener != nullptr) { + wallet->remove_listener(*old_listener); + delete old_listener; + } + + // add new listener + if (jlistener == nullptr) return 0; + wallet_jni_listener* listener = new wallet_jni_listener(env, jlistener); + wallet->add_listener(*listener); + return reinterpret_cast(listener); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getIntegratedAddressJni(JNIEnv *env, jobject instance, jstring jstandard_address, jstring jpayment_id) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getIntegratedAddressJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // collect and release string params + const char* _standardAddress = jstandard_address ? env->GetStringUTFChars(jstandard_address, NULL) : nullptr; + const char* _paymentId = jpayment_id ? env->GetStringUTFChars(jpayment_id, NULL) : nullptr; + string standard_address = string(_standardAddress ? _standardAddress : ""); + string payment_id = string(_paymentId ? _paymentId : ""); + env->ReleaseStringUTFChars(jstandard_address, _standardAddress); + env->ReleaseStringUTFChars(jpayment_id, _paymentId); + + // get and serialize integrated address + try { + monero_integrated_address integrated_address = wallet->get_integrated_address(standard_address, payment_id); + string integrated_address_json = integrated_address.serialize(); + return env->NewStringUTF(integrated_address_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_decodeIntegratedAddressJni(JNIEnv *env, jobject instance, jstring jintegrated_address) { + MTRACE("Java_monero_wallet_MoneroWalletLight_decodeIntegratedAddressJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _integratedAddress = jintegrated_address ? env->GetStringUTFChars(jintegrated_address, NULL) : nullptr; + string integrated_address_str = string(_integratedAddress ? _integratedAddress : ""); + env->ReleaseStringUTFChars(jintegrated_address, _integratedAddress); + + // serialize and return decoded integrated address + try { + monero_integrated_address integrated_address = wallet->decode_integrated_address(integrated_address_str); + string integrated_address_json = integrated_address.serialize(); + return env->NewStringUTF(integrated_address_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_getHeightJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getHeightJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + return wallet->get_height(); +} + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_getChainHeightJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getChainHeightJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return wallet->get_daemon_height(); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_getRestoreHeightJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getRestoreHeightJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + return wallet->get_restore_height(); +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_setRestoreHeightJni(JNIEnv *env, jobject instance, jlong restore_height) { + MTRACE("Java_monero_wallet_MoneroWalletLight_setRestoreHeightJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + wallet->set_restore_height(restore_height); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_getDaemonHeightJni(JNIEnv* env, jobject instance) { + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return wallet->get_daemon_height(); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_getDaemonMaxPeerHeightJni(JNIEnv* env, jobject instance) { + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return wallet->get_daemon_max_peer_height(); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_getHeightByDateJni(JNIEnv* env, jobject instance, jint year, jint month, jint day) { + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return wallet->get_height_by_date(year, month, day); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_syncJni(JNIEnv *env, jobject instance, jlong start_height) { + MTRACE("Java_monero_wallet_MoneroWalletLight_syncJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + + // sync wallet + monero_sync_result result = wallet->sync(start_height); + + // build and return results as Object[2]{(long) num_blocks_fetched, (boolean) received_money} + jobjectArray results = env->NewObjectArray(2, env->FindClass("java/lang/Object"), nullptr); + jclass longClass = env->FindClass("java/lang/Long"); + jmethodID longConstructor = env->GetMethodID(longClass, "", "(J)V"); + jobject numBlocksFetchedWrapped = env->NewObject(longClass, longConstructor, static_cast(result.m_num_blocks_fetched)); + env->SetObjectArrayElement(results, 0, numBlocksFetchedWrapped); + jclass booleanClass = env->FindClass("java/lang/Boolean"); + jmethodID booleanConstructor = env->GetMethodID(booleanClass, "", "(Z)V"); + jobject receivedMoneyWrapped = env->NewObject(booleanClass, booleanConstructor, static_cast(result.m_received_money)); + env->SetObjectArrayElement(results, 1, receivedMoneyWrapped); + return results; + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_startSyncingJni(JNIEnv *env, jobject instance, jlong sync_period_in_ms) { + MTRACE("Java_monero_wallet_MoneroWalletLight_startSyncingJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + wallet->start_syncing(sync_period_in_ms); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_stopSyncingJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_stopSyncingJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + wallet->stop_syncing(); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_scanTxsJni(JNIEnv* env, jobject instance, jobjectArray jtx_hashes) { + MTRACE("Java_monero_wallet_MoneroWalletLight_scanTxsJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // get tx hashes from jobjectArray to vector + vector tx_hashes; + if (jtx_hashes != nullptr) { + jsize size = env->GetArrayLength(jtx_hashes); + for (int idx = 0; idx < size; idx++) { + jstring jstr = (jstring) env->GetObjectArrayElement(jtx_hashes, idx); + const char* _str = jstr ? env->GetStringUTFChars(jstr, NULL) : nullptr; + string str = string(_str ? _str : ""); + env->ReleaseStringUTFChars(jstr, _str); + env->DeleteLocalRef(jstr); + tx_hashes.push_back(str); + } + env->DeleteLocalRef(jtx_hashes); + } + + // scan txs + try { + wallet->scan_txs(tx_hashes); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_rescanSpentJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_rescanSpentJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + wallet->rescan_spent(); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_rescanBlockchainJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_rescanBlockchainJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + wallet->rescan_blockchain(); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getBalanceWalletJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getBalanceWalletJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + uint64_t balance = wallet->get_balance(); + return env->NewStringUTF(boost::lexical_cast(balance).c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getBalanceAccountJni(JNIEnv *env, jobject instance, jint account_idx) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getBalanceAccountJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + uint64_t balance = wallet->get_balance(account_idx); + return env->NewStringUTF(boost::lexical_cast(balance).c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getBalanceSubaddressJni(JNIEnv *env, jobject instance, jint account_idx, jint subaddress_idx) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getBalanceSubaddressJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + uint64_t balance = wallet->get_balance(account_idx, subaddress_idx); + return env->NewStringUTF(boost::lexical_cast(balance).c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getUnlockedBalanceWalletJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getUnlockedBalanceWalletJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + uint64_t balance = wallet->get_unlocked_balance(); + return env->NewStringUTF(boost::lexical_cast(balance).c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getUnlockedBalanceAccountJni(JNIEnv *env, jobject instance, jint account_idx) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getUnlockedBalanceAccountJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + uint64_t balance = wallet->get_unlocked_balance(account_idx); + return env->NewStringUTF(boost::lexical_cast(balance).c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getUnlockedBalanceSubaddressJni(JNIEnv *env, jobject instance, jint account_idx, jint subaddress_idx) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getUnlockedBalanceSubaddressJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + uint64_t balance = wallet->get_unlocked_balance(account_idx, subaddress_idx); + return env->NewStringUTF(boost::lexical_cast(balance).c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getAccountsJni(JNIEnv* env, jobject instance, jboolean include_subaddresses, jstring jtag) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getAccountsJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _tag = jtag ? env->GetStringUTFChars(jtag, NULL) : nullptr; + string tag = string(_tag ? _tag : ""); + env->ReleaseStringUTFChars(jtag, _tag); + + // get accounts + vector accounts = wallet->get_accounts(include_subaddresses, tag); + + // wrap and serialize accounts + rapidjson::Document doc; + doc.SetObject(); + doc.AddMember("accounts", monero_utils::to_rapidjson_val(doc.GetAllocator(), accounts), doc.GetAllocator()); + string accounts_json = monero_utils::serialize(doc); + return env->NewStringUTF(accounts_json.c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getAccountJni(JNIEnv* env, jobject instance, jint account_idx, jboolean include_subaddresses) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getAccountJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // get account + monero_account account = wallet->get_account(account_idx, include_subaddresses); + + // serialize and return account + string account_json = account.serialize(); + return env->NewStringUTF(account_json.c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_createAccountJni(JNIEnv* env, jobject instance, jstring jlabel) { + MTRACE("Java_monero_wallet_MoneroWalletLight_createAccountJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _label = jlabel ? env->GetStringUTFChars(jlabel, NULL) : nullptr; + string label = string(_label ? _label : ""); + env->ReleaseStringUTFChars(jlabel, _label); + + // create account + monero_account account = wallet->create_account(label); + + // serialize and return account + string account_json = account.serialize(); + return env->NewStringUTF(account_json.c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getSubaddressesJni(JNIEnv* env, jobject instance, jint account_idx, jintArray jsubaddressIndices) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getSubaddressesJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // convert subaddress indices from jintArray to vector + vector subaddress_indices; + if (jsubaddressIndices != nullptr) { + jsize numSubaddressIndices = env->GetArrayLength(jsubaddressIndices); + jint* intArr = env->GetIntArrayElements(jsubaddressIndices, 0); + for (int subaddressIndicesIdx = 0; subaddressIndicesIdx < numSubaddressIndices; subaddressIndicesIdx++) { + subaddress_indices.push_back(intArr[subaddressIndicesIdx]); + } + } + + // get subaddresses + vector subaddresses = wallet->get_subaddresses(account_idx, subaddress_indices); + + // wrap and serialize subaddresses + rapidjson::Document doc; + doc.SetObject(); + doc.AddMember("subaddresses", monero_utils::to_rapidjson_val(doc.GetAllocator(), subaddresses), doc.GetAllocator()); + string subaddresses_json = monero_utils::serialize(doc); + return env->NewStringUTF(subaddresses_json.c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_createSubaddressJni(JNIEnv* env, jobject instance, jint account_idx, jstring jlabel) { + MTRACE("Java_monero_wallet_MoneroWalletLight_createSubaddressJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _label = jlabel ? env->GetStringUTFChars(jlabel, NULL) : nullptr; + string label = string(_label ? _label : ""); + env->ReleaseStringUTFChars(jlabel, _label); + + // create subaddress + monero_subaddress subaddress = wallet->create_subaddress(account_idx, label); + + // serialize and return subaddress + string subaddress_json = subaddress.serialize(); + return env->NewStringUTF(subaddress_json.c_str()); +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_setSubaddressLabelJni(JNIEnv* env, jobject instance, jint account_idx, jint subaddress_idx, jstring jlabel) { + MTRACE("Java_monero_wallet_MoneroWalletLight_setSubaddressLabelJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _label = jlabel ? env->GetStringUTFChars(jlabel, NULL) : nullptr; + string label = string(_label ? _label : ""); + env->ReleaseStringUTFChars(jlabel, _label); + try { + wallet->set_subaddress_label(account_idx, subaddress_idx, label); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getTxsJni(JNIEnv* env, jobject instance, jstring jtx_query) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getTxsJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _tx_query = jtx_query ? env->GetStringUTFChars(jtx_query, NULL) : nullptr; + string tx_query_json = string(_tx_query ? _tx_query : ""); + env->ReleaseStringUTFChars(jtx_query, _tx_query); + try { + + // deserialize tx query + shared_ptr tx_query = monero_tx_query::deserialize_from_block(tx_query_json); + //cout << "Fetching txs with query: " << tx_query->serialize() << endl; + + // get txs + vector> txs = wallet->get_txs(*tx_query); + MTRACE("Got " << txs.size() << " txs"); + + // wrap and serialize blocks + vector> blocks = monero_utils::get_blocks_from_txs(txs); + rapidjson::Document doc; + doc.SetObject(); + doc.AddMember("blocks", monero_utils::to_rapidjson_val(doc.GetAllocator(), blocks), doc.GetAllocator()); + string blocks_json = monero_utils::serialize(doc); + + // free memory + monero_utils::free(blocks); + monero_utils::free(tx_query); + return env->NewStringUTF(blocks_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getTransfersJni(JNIEnv* env, jobject instance, jstring jtransfer_query) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getTransfersJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _transfer_query = jtransfer_query ? env->GetStringUTFChars(jtransfer_query, NULL) : nullptr; + string transfer_query_json = string(_transfer_query ? _transfer_query : ""); + env->ReleaseStringUTFChars(jtransfer_query, _transfer_query); + try { + + // deserialize transfer query + shared_ptr transfer_query = monero_transfer_query::deserialize_from_block(transfer_query_json); + +// // log query +// if (transfer_query->m_tx_query != boost::none) { +// if ((*transfer_query->m_tx_query)->m_block == boost::none) cout << "Transfer query's tx query rooted at [tx]:" << (*transfer_query->m_tx_query)->serialize() << endl; +// else cout << "Transfer query's tx query rooted at [block]: " << (*(*transfer_query->m_tx_query)->m_block)->serialize() << endl; +// } else cout << "Transfer query: " << transfer_query->serialize() << endl; + + // get transfers + vector> transfers = wallet->get_transfers(*transfer_query); + + // wrap and serialize blocks + vector> blocks = monero_utils::get_blocks_from_transfers(transfers); + rapidjson::Document doc; + doc.SetObject(); + doc.AddMember("blocks", monero_utils::to_rapidjson_val(doc.GetAllocator(), blocks), doc.GetAllocator()); + string blocks_json = monero_utils::serialize(doc); + + // free memory + monero_utils::free(blocks); + monero_utils::free(transfer_query->m_tx_query.get()); + return env->NewStringUTF(blocks_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getOutputsJni(JNIEnv* env, jobject instance, jstring joutput_query) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getOutputsJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _output_query = joutput_query ? env->GetStringUTFChars(joutput_query, NULL) : nullptr; + string output_query_json = string(_output_query ? _output_query : ""); + env->ReleaseStringUTFChars(joutput_query, _output_query); + try { + + // deserialize output query + shared_ptr output_query = monero_output_query::deserialize_from_block(output_query_json); + MTRACE("Fetching outputs with request: " << output_query->serialize()); + + // get outputs + vector> outputs = wallet->get_outputs(*output_query); + MTRACE("Got " << outputs.size() << " outputs"); + + // wrap and serialize blocks + vector> blocks = monero_utils::get_blocks_from_outputs(outputs); + rapidjson::Document doc; + doc.SetObject(); + doc.AddMember("blocks", monero_utils::to_rapidjson_val(doc.GetAllocator(), blocks), doc.GetAllocator()); + string blocks_json = monero_utils::serialize(doc); + + // free memory + monero_utils::free(blocks); + monero_utils::free(output_query->m_tx_query.get()); + return env->NewStringUTF(blocks_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_exportOutputsJni(JNIEnv* env, jobject instance, jboolean all) { + MTRACE("Java_monero_wallet_MoneroWalletLight_exportOutputsJni()"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + return env->NewStringUTF(wallet->export_outputs(all).c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jint JNICALL Java_monero_wallet_MoneroWalletLight_importOutputsJni(JNIEnv* env, jobject instance, jstring joutputs_hex) { + MTRACE("Java_monero_wallet_MoneroWalletLight_exportOutputsJni()"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _outputs_hex = joutputs_hex ? env->GetStringUTFChars(joutputs_hex, NULL) : nullptr; + string outputs_hex = string(_outputs_hex ? _outputs_hex : ""); + env->ReleaseStringUTFChars(joutputs_hex, _outputs_hex); + try { + return wallet->import_outputs(outputs_hex); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_exportKeyImagesJni(JNIEnv* env, jobject instance, jboolean all) { + MTRACE("Java_monero_wallet_MoneroWalletLight_exportKeyImagesJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + + // fetch key images + vector> key_images = wallet->export_key_images(all); + MTRACE("Fetched " << key_images.size() << " key images"); + + // wrap and serialize key images + rapidjson::Document doc; + doc.SetObject(); + doc.AddMember("keyImages", monero_utils::to_rapidjson_val(doc.GetAllocator(), key_images), doc.GetAllocator()); + string key_images_json = monero_utils::serialize(doc); + return env->NewStringUTF(key_images_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_importKeyImagesJni(JNIEnv* env, jobject instance, jstring jkey_images_json) { + MTRACE("Java_monero_wallet_MoneroWalletLight_importKeyImagesJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _key_images_json = jkey_images_json ? env->GetStringUTFChars(jkey_images_json, NULL) : nullptr; + string key_images_json = string(_key_images_json ? _key_images_json : ""); + env->ReleaseStringUTFChars(jkey_images_json, _key_images_json); + + // deserialize key images to import + vector> key_images = monero_key_image::deserialize_key_images(key_images_json); + //MTRACE("Deserialized " << key_images.size() << " key images from java json"); + + // import key images + shared_ptr result; + try { + result = wallet->import_key_images(key_images); + return env->NewStringUTF(result->serialize().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_freezeOutputJni(JNIEnv* env, jobject instance, jstring jkey_image) { + MTRACE("Java_monero_wallet_MoneroWalletLight_freezeJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _key_image = jkey_image ? env->GetStringUTFChars(jkey_image, NULL) : nullptr; + string key_image = string(_key_image ? _key_image : ""); + env->ReleaseStringUTFChars(jkey_image, _key_image); + + // freeze output + try { + wallet->freeze_output(key_image); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_thawOutputJni(JNIEnv* env, jobject instance, jstring jkey_image) { + MTRACE("Java_monero_wallet_MoneroWalletLight_thawJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _key_image = jkey_image ? env->GetStringUTFChars(jkey_image, NULL) : nullptr; + string key_image = string(_key_image ? _key_image : ""); + env->ReleaseStringUTFChars(jkey_image, _key_image); + + // thaw output + try { + wallet->thaw_output(key_image); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT bool JNICALL Java_monero_wallet_MoneroWalletLight_isOutputFrozenJni(JNIEnv* env, jobject instance, jstring jkey_image) { + MTRACE("Java_monero_wallet_MoneroWalletLight_isFrozenJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _key_image = jkey_image ? env->GetStringUTFChars(jkey_image, NULL) : nullptr; + string key_image = string(_key_image ? _key_image : ""); + env->ReleaseStringUTFChars(jkey_image, _key_image); + + // check if output is frozen + try { + return wallet->is_output_frozen(key_image); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return false; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_createTxsJni(JNIEnv* env, jobject instance, jstring jconfig) { + MTRACE("Java_monero_wallet_MoneroWalletLight_sendTxsJni(request)"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _config = jconfig ? env->GetStringUTFChars(jconfig, NULL) : nullptr; + string config_json = string(_config ? _config : ""); + env->ReleaseStringUTFChars(jconfig, _config); + + // deserialize tx config + shared_ptr config = monero_tx_config::deserialize(config_json); + //MTRACE("Deserialized tx config, re-serialized: " << config->serialize()); + + // create txs + try { + vector> txs = wallet->create_txs(*config); + string tx_set_json = txs[0]->m_tx_set.get()->serialize(); + monero_utils::free(txs); + return env->NewStringUTF(tx_set_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_sweepUnlockedJni(JNIEnv* env, jobject instance, jstring jconfig) { + MTRACE("Java_monero_wallet_MoneroWalletLight_sweepUnlockedJni(config)"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _config = jconfig ? env->GetStringUTFChars(jconfig, NULL) : nullptr; + string config_json = string(_config ? _config : ""); + env->ReleaseStringUTFChars(jconfig, _config); + + // deserialize tx config + shared_ptr config = monero_tx_config::deserialize(config_json); + //MTRACE("Deserialized tx config, re-serialized: " << config->serialize()); + + try { + + // create txs + vector> txs = wallet->sweep_unlocked(*config); + + // collect tx sets + vector> tx_sets; + for (int i = 0; i < txs.size(); i++) { + if (std::find(tx_sets.begin(), tx_sets.end(), txs[i]->m_tx_set.get()) == tx_sets.end()) { + tx_sets.push_back(txs[i]->m_tx_set.get()); + } + } + + // wrap and serialize tx sets + rapidjson::Document doc; + doc.SetObject(); + doc.AddMember("txSets", monero_utils::to_rapidjson_val(doc.GetAllocator(), tx_sets), doc.GetAllocator()); + string tx_sets_json = monero_utils::serialize(doc); + + // free and return + monero_utils::free(txs); + return env->NewStringUTF(tx_sets_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_sweepOutputJni(JNIEnv* env, jobject instance, jstring jconfig) { + MTRACE("Java_monero_wallet_MoneroWalletLight_sweepOutputJni(request)"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _config = jconfig ? env->GetStringUTFChars(jconfig, NULL) : nullptr; + string config_json = string(_config); + env->ReleaseStringUTFChars(jconfig, _config); + + MTRACE("Send request json: " << config_json); + + // deserialize tx config + shared_ptr config = monero_tx_config::deserialize(config_json); + MTRACE("Deserialized send request, re-serialized: " << config->serialize()); + + // submit request with configuration + try { + shared_ptr tx = wallet->sweep_output(*config); + string tx_set_json = tx->m_tx_set.get()->serialize(); + monero_utils::free(tx); + return env->NewStringUTF(tx_set_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_sweepDustJni(JNIEnv* env, jobject instance, jboolean relay) { + MTRACE("Java_monero_wallet_MoneroWalletLight_sweepDustJni(request)"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // sweep dust + try { + vector> txs = wallet->sweep_dust(relay); + string tx_set_json = txs.empty() ? string("{}") : txs[0]->m_tx_set.get()->serialize(); + monero_utils::free(txs); + return env->NewStringUTF(tx_set_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_describeTxSetJni(JNIEnv* env, jobject instance, jstring jtx_set_json) { + MTRACE("Java_monero_wallet_MoneroWalletLight_describeTxSetJson(tx_set_json)"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // get tx set json string + const char* _tx_set_json = jtx_set_json ? env->GetStringUTFChars(jtx_set_json, NULL) : nullptr; + string tx_set_json = string(_tx_set_json); + env->ReleaseStringUTFChars(jtx_set_json, _tx_set_json); + + try { + + // deserialize tx set to describe + monero_tx_set tx_set = monero_tx_set::deserialize(tx_set_json); + + // describe tx set + monero_tx_set described_tx_set = wallet->describe_tx_set(tx_set); + + // serialize, free, and return + std::string monero_tx_set_json = described_tx_set.serialize(); + monero_utils::free(described_tx_set.m_txs); + return env->NewStringUTF(monero_tx_set_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_signTxsJni(JNIEnv* env, jobject instance, jstring junsigned_tx_hex) { + MTRACE("Java_monero_wallet_MoneroWalletLight_signTxsJni()"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // get unsigned tx set as string + const char* _unsigned_tx_hex = junsigned_tx_hex ? env->GetStringUTFChars(junsigned_tx_hex, NULL) : nullptr; + string unsigned_tx_hex = string(_unsigned_tx_hex ? _unsigned_tx_hex : ""); + env->ReleaseStringUTFChars(junsigned_tx_hex, _unsigned_tx_hex); + + try { + + // sign txs + monero_tx_set signed_tx_set = wallet->sign_txs(unsigned_tx_hex); + + // serialize, free, and return + std::string monero_tx_set_json = signed_tx_set.serialize(); + monero_utils::free(signed_tx_set.m_txs); + return env->NewStringUTF(monero_tx_set_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_submitTxsJni(JNIEnv* env, jobject instance, jstring jsigned_tx_hex) { + MTRACE("Java_monero_wallet_MoneroWalletLight_submitTxsJni()"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // get signed tx set as string + const char* _signed_tx_hex = jsigned_tx_hex ? env->GetStringUTFChars(jsigned_tx_hex, NULL) : nullptr; + string signed_tx_hex = string(_signed_tx_hex ? _signed_tx_hex : ""); + env->ReleaseStringUTFChars(jsigned_tx_hex, _signed_tx_hex); + + try { + + // submit signed txs + vector tx_hashes = wallet->submit_txs(signed_tx_hex); + + // return tx hashes as jobjectArray + jobjectArray jtx_hashes = env->NewObjectArray(tx_hashes.size(), env->FindClass("java/lang/String"), nullptr); + for (int i = 0; i < tx_hashes.size(); i++) env->SetObjectArrayElement(jtx_hashes, i, env->NewStringUTF(tx_hashes[i].c_str())); + return jtx_hashes; + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_relayTxsJni(JNIEnv* env, jobject instance, jobjectArray jtx_metadatas) { + MTRACE("Java_monero_wallet_MoneroWalletLight_relayTxsJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // get tx metadatas from jobjectArray to vector + vector tx_metadatas; + if (jtx_metadatas != nullptr) { + jsize size = env->GetArrayLength(jtx_metadatas); + for (int idx = 0; idx < size; idx++) { + jstring jstr = (jstring) env->GetObjectArrayElement(jtx_metadatas, idx); + const char* _str = jstr ? env->GetStringUTFChars(jstr, NULL) : nullptr; + string str = string(_str ? _str : ""); + env->ReleaseStringUTFChars(jstr, _str); + env->DeleteLocalRef(jstr); + tx_metadatas.push_back(str); + } + env->DeleteLocalRef(jtx_metadatas); + } + + // relay tx metadata + vector tx_hashes; + try { + tx_hashes = wallet->relay_txs(tx_metadatas); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } + + // return tx hashes as jobjectArray + jobjectArray jtx_hashes = env->NewObjectArray(tx_hashes.size(), env->FindClass("java/lang/String"), nullptr); + for (int i = 0; i < tx_hashes.size(); i++) env->SetObjectArrayElement(jtx_hashes, i, env->NewStringUTF(tx_hashes[i].c_str())); + return jtx_hashes; +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_signMessageJni(JNIEnv* env, jobject instance, jstring jmsg, jint message_signature_type, jint account_idx, jint subaddress_idx) { + MTRACE("Java_monero_wallet_MoneroWalletLight_signMessageJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _msg = jmsg ? env->GetStringUTFChars(jmsg, NULL) : nullptr; + string msg = string(_msg ? _msg : ""); + env->ReleaseStringUTFChars(jmsg, _msg); + monero_message_signature_type signature_type = message_signature_type == 0 ? monero_message_signature_type::SIGN_WITH_SPEND_KEY : monero_message_signature_type::SIGN_WITH_VIEW_KEY; + try { + string signature = wallet->sign_message(msg, signature_type, account_idx, subaddress_idx); + return env->NewStringUTF(signature.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_verifyMessageJni(JNIEnv* env, jobject instance, jstring jmsg, jstring jaddress, jstring jsignature) { + MTRACE("Java_monero_wallet_MoneroWalletLight_verifyMessageJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _msg = jmsg ? env->GetStringUTFChars(jmsg, NULL) : nullptr; + const char* _address = jaddress ? env->GetStringUTFChars(jaddress, NULL) : nullptr; + const char* _signature = jsignature ? env->GetStringUTFChars(jsignature, NULL) : nullptr; + string msg = string(_msg ? _msg : ""); + string address = string(_address ? _address : ""); + string signature = string(_signature ? _signature : ""); + env->ReleaseStringUTFChars(jmsg, _msg); + env->ReleaseStringUTFChars(jaddress, _address); + env->ReleaseStringUTFChars(jsignature, _signature); + try { + monero_message_signature_result result = wallet->verify_message(msg, address, signature); + return env->NewStringUTF(result.serialize().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getTxKeyJni(JNIEnv* env, jobject instance, jstring jtx_hash) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getTxKeyJniJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _tx_hash = jtx_hash ? env->GetStringUTFChars(jtx_hash, NULL) : nullptr; + string tx_hash = string(_tx_hash == nullptr ? "" : _tx_hash); + env->ReleaseStringUTFChars(jtx_hash, _tx_hash); + try { + return env->NewStringUTF(wallet->get_tx_key(tx_hash).c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_checkTxKeyJni(JNIEnv* env, jobject instance, jstring jtx_hash, jstring jtx_key, jstring jaddress) { + MTRACE("Java_monero_wallet_MoneroWalletLight_checktx_keyJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _tx_hash = jtx_hash ? env->GetStringUTFChars(jtx_hash, NULL) : nullptr; + const char* _tx_key = jtx_key ? env->GetStringUTFChars(jtx_key, NULL) : nullptr; + const char* _address = jaddress ? env->GetStringUTFChars(jaddress, NULL) : nullptr; + string tx_hash = string(_tx_hash == nullptr ? "" : _tx_hash); + string tx_key = string(_tx_key == nullptr ? "" : _tx_key); + string address = string(_address == nullptr ? "" : _address); + env->ReleaseStringUTFChars(jtx_hash, _tx_hash); + env->ReleaseStringUTFChars(jtx_key, _tx_key); + env->ReleaseStringUTFChars(jaddress, _address); + try { + return env->NewStringUTF(wallet->check_tx_key(tx_hash, tx_key, address)->serialize().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getTxProofJni(JNIEnv* env, jobject instance, jstring jtx_hash, jstring jaddress, jstring jmessage) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getTxProofJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _tx_hash = jtx_hash ? env->GetStringUTFChars(jtx_hash, NULL) : nullptr; + const char* _address = jaddress ? env->GetStringUTFChars(jaddress, NULL) : nullptr; + const char* _message = jmessage ? env->GetStringUTFChars(jmessage, NULL) : nullptr; + string tx_hash = string(_tx_hash == nullptr ? "" : _tx_hash); + string address = string(_address == nullptr ? "" : _address); + string message = string(_message == nullptr ? "" : _message); + env->ReleaseStringUTFChars(jtx_hash, _tx_hash); + env->ReleaseStringUTFChars(jaddress, _address); + env->ReleaseStringUTFChars(jmessage, _message); + try { + return env->NewStringUTF(wallet->get_tx_proof(tx_hash, address, message).c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_checkTxProofJni(JNIEnv* env, jobject instance, jstring jtx_hash, jstring jaddress, jstring jmessage, jstring jsignature) { + MTRACE("Java_monero_wallet_MoneroWalletLight_checkTxProofJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _tx_hash = jtx_hash ? env->GetStringUTFChars(jtx_hash, NULL) : nullptr; + const char* _address = jaddress ? env->GetStringUTFChars(jaddress, NULL) : nullptr; + const char* _message = jmessage ? env->GetStringUTFChars(jmessage, NULL) : nullptr; + const char* _signature = jsignature ? env->GetStringUTFChars(jsignature, NULL) : nullptr; + string tx_hash = string(_tx_hash == nullptr ? "" : _tx_hash); + string address = string(_address == nullptr ? "" : _address); + string message = string(_message == nullptr ? "" : _message); + string signature = string(_signature == nullptr ? "" : _signature); + env->ReleaseStringUTFChars(jtx_hash, _tx_hash); + env->ReleaseStringUTFChars(jaddress, _address); + env->ReleaseStringUTFChars(jmessage, _message); + env->ReleaseStringUTFChars(jsignature, _signature); + try { + return env->NewStringUTF(wallet->check_tx_proof(tx_hash, address, message, signature)->serialize().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getSpendProofJni(JNIEnv* env, jobject instance, jstring jtx_hash, jstring jmessage) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getSpendProofJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _tx_hash = jtx_hash ? env->GetStringUTFChars(jtx_hash, NULL) : nullptr; + const char* _message = jmessage ? env->GetStringUTFChars(jmessage, NULL) : nullptr; + string tx_hash = string(_tx_hash == nullptr ? "" : _tx_hash); + string message = string(_message == nullptr ? "" : _message); + env->ReleaseStringUTFChars(jtx_hash, _tx_hash); + env->ReleaseStringUTFChars(jmessage, _message); + try { + return env->NewStringUTF(wallet->get_spend_proof(tx_hash, message).c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_checkSpendProofJni(JNIEnv* env, jobject instance, jstring jtx_hash, jstring jmessage, jstring jsignature) { + MTRACE("Java_monero_wallet_MoneroWalletLight_checkSpendProofJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _tx_hash = jtx_hash ? env->GetStringUTFChars(jtx_hash, NULL) : nullptr; + const char* _message = jmessage ? env->GetStringUTFChars(jmessage, NULL) : nullptr; + const char* _signature = jsignature ? env->GetStringUTFChars(jsignature, NULL) : nullptr; + string tx_hash = string(_tx_hash == nullptr ? "" : _tx_hash); + string message = string(_message == nullptr ? "" : _message); + string signature = string(_signature == nullptr ? "" : _signature); + env->ReleaseStringUTFChars(jtx_hash, _tx_hash); + env->ReleaseStringUTFChars(jmessage, _message); + env->ReleaseStringUTFChars(jsignature, _signature); + try { + return static_cast(wallet->check_spend_proof(tx_hash, message, signature)); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getReserveProofWalletJni(JNIEnv* env, jobject instance, jstring jmessage) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getReserveProofWalletJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _message = jmessage ? env->GetStringUTFChars(jmessage, NULL) : nullptr; + string message = string(_message == nullptr ? "" : _message); + env->ReleaseStringUTFChars(jmessage, _message); + try { + return env->NewStringUTF(wallet->get_reserve_proof_wallet(message).c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getReserveProofAccountJni(JNIEnv* env, jobject instance, jint account_idx, jstring jamount_str, jstring jmessage) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getReserveProofWalletJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _amount_str = jamount_str ? env->GetStringUTFChars(jamount_str, NULL) : nullptr; + const char* _message = jmessage ? env->GetStringUTFChars(jmessage, NULL) : nullptr; + string amount_str = string(_amount_str == nullptr ? "" : _amount_str); + string message = string(_message == nullptr ? "" : _message); + env->ReleaseStringUTFChars(jamount_str, _amount_str); + env->ReleaseStringUTFChars(jmessage, _message); + uint64_t amount = boost::lexical_cast(amount_str); + try { + return env->NewStringUTF(wallet->get_reserve_proof_account(account_idx, amount, message).c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_checkReserveProofJni(JNIEnv* env, jobject instance, jstring jaddress, jstring jmessage, jstring jsignature) { + MTRACE("Java_monero_wallet_MoneroWalletLight_checkReserveProofAccountJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _address = jaddress ? env->GetStringUTFChars(jaddress, NULL) : nullptr; + const char* _message = jmessage ? env->GetStringUTFChars(jmessage, NULL) : nullptr; + const char* _signature = jsignature ? env->GetStringUTFChars(jsignature, NULL) : nullptr; + string address = string(_address == nullptr ? "" : _address); + string message = string(_message == nullptr ? "" : _message); + string signature = string(_signature == nullptr ? "" : _signature); + env->ReleaseStringUTFChars(jaddress, _address); + env->ReleaseStringUTFChars(jmessage, _message); + env->ReleaseStringUTFChars(jsignature, _signature); + try { + return env->NewStringUTF(wallet->check_reserve_proof(address, message, signature)->serialize().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_getTxNotesJni(JNIEnv* env, jobject instance, jobjectArray jtx_hashes) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getTxNotesJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // get tx hashes from jobjectArray to vector + vector tx_hashes; + if (jtx_hashes != nullptr) { + jsize size = env->GetArrayLength(jtx_hashes); + for (int idx = 0; idx < size; idx++) { + jstring jstr = (jstring) env->GetObjectArrayElement(jtx_hashes, idx); + const char* _str = jstr ? env->GetStringUTFChars(jstr, NULL) : nullptr; + string str = string(_str ? _str : ""); + env->ReleaseStringUTFChars(jstr, _str); + env->DeleteLocalRef(jstr); + tx_hashes.push_back(str); + } + env->DeleteLocalRef(jtx_hashes); + } + + // get tx notes + vector notes; + try { + notes = wallet->get_tx_notes(tx_hashes); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } + + // convert and return tx notes as jobjectArray + jobjectArray jtx_notes = env->NewObjectArray(notes.size(), env->FindClass("java/lang/String"), nullptr); + for (int i = 0; i < notes.size(); i++) { + env->SetObjectArrayElement(jtx_notes, i, env->NewStringUTF(notes[i].c_str())); + } + return jtx_notes; +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_setTxNotesJni(JNIEnv* env, jobject instance, jobjectArray jtx_hashes, jobjectArray jtx_notes) { + MTRACE("Java_monero_wallet_MoneroWalletLight_setTxNotesJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // get tx hashes from jobjectArray to vector + vector tx_hashes; + if (jtx_hashes != nullptr) { + jsize size = env->GetArrayLength(jtx_hashes); + for (int idx = 0; idx < size; idx++) { + jstring jstr = (jstring) env->GetObjectArrayElement(jtx_hashes, idx); + const char* _str = jstr ? env->GetStringUTFChars(jstr, NULL) : nullptr; + string str = string(_str ? _str : ""); + env->ReleaseStringUTFChars(jstr, _str); + env->DeleteLocalRef(jstr); + tx_hashes.push_back(str); + } + env->DeleteLocalRef(jtx_hashes); + } + + // get tx notes from jobjectArray to vector + vector notes; + if (jtx_notes != nullptr) { + jsize size = env->GetArrayLength(jtx_notes); + for (int idx = 0; idx < size; idx++) { + jstring jstr = (jstring) env->GetObjectArrayElement(jtx_notes, idx); + const char* _str = jstr ? env->GetStringUTFChars(jstr, NULL) : nullptr; + string str = string(_str ? _str : ""); + env->ReleaseStringUTFChars(jstr, _str); + env->DeleteLocalRef(jstr); + notes.push_back(str); + } + env->DeleteLocalRef(jtx_notes); + } + + // set tx notes + try { + wallet->set_tx_notes(tx_hashes, notes); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getAddressBookEntriesJni(JNIEnv* env, jobject instance, jintArray jindices) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getAddressBookEntriesJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // convert subaddress indices from jintArray to vector + vector indices; + if (jindices != nullptr) { + jsize numIndices = env->GetArrayLength(jindices); + jint* intArr = env->GetIntArrayElements(jindices, 0); + for (int indicesIdx = 0; indicesIdx < numIndices; indicesIdx++) { + indices.push_back(intArr[indicesIdx]); + } + } + + try { + + // get address book entries + vector entries = wallet->get_address_book_entries(indices); + + // wrap and serialize entries + rapidjson::Document doc; + doc.SetObject(); + doc.AddMember("entries", monero_utils::to_rapidjson_val(doc.GetAllocator(), entries), doc.GetAllocator()); + string entries_json = monero_utils::serialize(doc); + return env->NewStringUTF(entries_json.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +// TODO: return jlong for uint64_t +JNIEXPORT jint JNICALL Java_monero_wallet_MoneroWalletLight_addAddressBookEntryJni(JNIEnv* env, jobject instance, jstring jaddress, jstring jdescription) { + MTRACE("Java_monero_wallet_MoneroWalletLight_addAddressBookEntryJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // collect string params + const char* _address = jaddress ? env->GetStringUTFChars(jaddress, NULL) : nullptr; + const char* _description = jdescription ? env->GetStringUTFChars(jdescription, NULL) : nullptr; + string address = string(_address == nullptr ? "" : _address); + string description = string(_description == nullptr ? "" : _description); + env->ReleaseStringUTFChars(jaddress, _address); + env->ReleaseStringUTFChars(jdescription, _description); + + // add address book entry + try { + return wallet->add_address_book_entry(address, description); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_editAddressBookEntryJni(JNIEnv* env, jobject instance, jint index, jboolean set_address, jstring jaddress, jboolean set_description, jstring jdescription) { + MTRACE("Java_monero_wallet_MoneroWalletLight_editAddressBookEntryJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // collect string params + const char* _address = jaddress ? env->GetStringUTFChars(jaddress, NULL) : nullptr; + const char* _description = jdescription ? env->GetStringUTFChars(jdescription, NULL) : nullptr; + string address = string(_address == nullptr ? "" : _address); + string description = string(_description == nullptr ? "" : _description); + env->ReleaseStringUTFChars(jaddress, _address); + env->ReleaseStringUTFChars(jdescription, _description); + + // edit address book entry + try { + wallet->edit_address_book_entry(index, set_address, address, set_description, description); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_deleteAddressBookEntryJni(JNIEnv* env, jobject instance, jint index) { + MTRACE("Java_monero_wallet_MoneroWalletLight_deleteAddressBookEntryJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + + // delete address book entry + try { + wallet->delete_address_book_entry(index); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getPaymentUriJni(JNIEnv* env, jobject instance, jstring jconfig) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getPaymentUriJni()"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _config = jconfig ? env->GetStringUTFChars(jconfig, NULL) : nullptr; + string config_json = string(_config ? _config : ""); + env->ReleaseStringUTFChars(jconfig, _config); + + // deserialize send request + shared_ptr config = monero_tx_config::deserialize(config_json); + MTRACE("Fetching payment uri with : " << config->serialize()); + + // get payment uri + string payment_uri; + try { + payment_uri = wallet->get_payment_uri(*config.get()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } + + // release and return + return env->NewStringUTF(payment_uri.c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_parsePaymentUriJni(JNIEnv* env, jobject instance, jstring juri) { + MTRACE("Java_monero_wallet_MoneroWalletLight_parsePaymentUriJni()"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _uri = juri ? env->GetStringUTFChars(juri, NULL) : nullptr; + string uri = string(_uri ? _uri : ""); + env->ReleaseStringUTFChars(juri, _uri); + + // parse uri to send request + shared_ptr config; + try { + config = wallet->parse_payment_uri(uri); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } + + // return serialized request + return env->NewStringUTF(config->serialize().c_str()); +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getAttributeJni(JNIEnv* env, jobject instance, jstring jkey) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getAttribute()"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _key = jkey ? env->GetStringUTFChars(jkey, NULL) : nullptr; + string key = string(_key); + env->ReleaseStringUTFChars(jkey, _key); + try { + string value; + if (!wallet->get_attribute(key, value)) return 0; + return env->NewStringUTF(value.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_setAttributeJni(JNIEnv* env, jobject instance, jstring jkey, jstring jval) { + MTRACE("Java_monero_wallet_MoneroWalletLight_setAttribute()"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _key = jkey ? env->GetStringUTFChars(jkey, NULL) : nullptr; + const char* _val = jval ? env->GetStringUTFChars(jval, NULL) : nullptr; + string key = string(_key); + string val = string(_val); + env->ReleaseStringUTFChars(jkey, _key); + env->ReleaseStringUTFChars(jval, _val); + try { + wallet->set_attribute(key, val); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_startMiningJni(JNIEnv* env, jobject instance, jlong num_threads, jboolean background_mining, jboolean ignore_battery) { + MTRACE("Java_monero_wallet_MoneroWalletLight_startMiningJni()"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + wallet->start_mining(num_threads, background_mining, ignore_battery); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_stopMiningJni(JNIEnv* env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_startMiningJni()"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + wallet->stop_mining(); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_isMultisigImportNeededJni(JNIEnv* env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_isMultisigImportNeededJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + bool is_multisig_import_needed = wallet->is_multisig_import_needed(); + return static_cast(is_multisig_import_needed); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getMultisigInfoJni(JNIEnv* env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getMultisigInfoJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + monero_multisig_info info = wallet->get_multisig_info(); + return env->NewStringUTF(info.serialize().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_prepareMultisigJni(JNIEnv* env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_prepareMultisigJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + string multisig_hex = wallet->prepare_multisig(); + return env->NewStringUTF(multisig_hex.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_makeMultisigJni(JNIEnv* env, jobject instance, jobjectArray jmultisig_hexes, jint threshold, jstring jpassword) { + MTRACE("Java_monero_wallet_MoneroWalletLight_makeMultisigJni"); + + // get multisig hex as vector + vector multisig_hexes; + if (jmultisig_hexes != nullptr) { + jsize size = env->GetArrayLength(jmultisig_hexes); + for (int idx = 0; idx < size; idx++) { + jstring jstr = (jstring) env->GetObjectArrayElement(jmultisig_hexes, idx); + const char* _str = jstr ? env->GetStringUTFChars(jstr, NULL) : nullptr; + string str = string(_str ? _str : ""); + env->ReleaseStringUTFChars(jstr, _str); + env->DeleteLocalRef(jstr); + multisig_hexes.push_back(str); + } + env->DeleteLocalRef(jmultisig_hexes); + } + + // get password as string + const char* _password = jpassword ? env->GetStringUTFChars(jpassword, NULL) : nullptr; + string password = string(_password ? _password : ""); + env->ReleaseStringUTFChars(jpassword, _password); + + // make the wallet multisig and return result + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + string multisig_hex = wallet->make_multisig(multisig_hexes, threshold, password); + return env->NewStringUTF(multisig_hex.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_exchangeMultisigKeysJni(JNIEnv* env, jobject instance, jobjectArray jmultisig_hexes, jstring jpassword) { + MTRACE("Java_monero_wallet_MoneroWalletLight_exchangeMultisigKeysJni"); + + // get multisig hex as vector + vector multisig_hexes; + if (jmultisig_hexes != nullptr) { + jsize size = env->GetArrayLength(jmultisig_hexes); + for (int idx = 0; idx < size; idx++) { + jstring jstr = (jstring) env->GetObjectArrayElement(jmultisig_hexes, idx); + const char* _str = jstr ? env->GetStringUTFChars(jstr, NULL) : nullptr; + string str = string(_str ? _str : ""); + env->ReleaseStringUTFChars(jstr, _str); + env->DeleteLocalRef(jstr); + multisig_hexes.push_back(str); + } + env->DeleteLocalRef(jmultisig_hexes); + } + + // get password as string + const char* _password = jpassword ? env->GetStringUTFChars(jpassword, NULL) : nullptr; + string password = string(_password ? _password : ""); + env->ReleaseStringUTFChars(jpassword, _password); + + // import peer multisig keys and export result with address xor multisig hex for next round + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + monero_multisig_init_result result = wallet->exchange_multisig_keys(multisig_hexes, password); + return env->NewStringUTF(result.serialize().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_exportMultisigHexJni(JNIEnv* env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_exportMultisigHexJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + string multisig_hex = wallet->export_multisig_hex(); + return env->NewStringUTF(multisig_hex.c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jint JNICALL Java_monero_wallet_MoneroWalletLight_importMultisigHexJni(JNIEnv* env, jobject instance, jobjectArray jmultisig_hexes) { + MTRACE("Java_monero_wallet_MoneroWalletLight_importMultisigHexJni"); + + // get peer multisig hex as vector + vector multisig_hexes; + if (jmultisig_hexes != nullptr) { + jsize size = env->GetArrayLength(jmultisig_hexes); + for (int idx = 0; idx < size; idx++) { + jstring jstr = (jstring) env->GetObjectArrayElement(jmultisig_hexes, idx); + const char* _str = jstr ? env->GetStringUTFChars(jstr, NULL) : nullptr; + string str = string(_str ? _str : ""); + env->ReleaseStringUTFChars(jstr, _str); + env->DeleteLocalRef(jstr); + multisig_hexes.push_back(str); + } + env->DeleteLocalRef(jmultisig_hexes); + } + + // import peer multisig hex and return the number of outputs they signed + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + int num_outputs = wallet->import_multisig_hex(multisig_hexes); + return num_outputs; + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_signMultisigTxHexJni(JNIEnv* env, jobject instance, jstring jmultisig_tx_hex) { + MTRACE("Java_monero_wallet_MoneroWalletLight_signMultisigTxHexJni"); + + // get multisig tx hex as string + const char* _multisig_tx_hex = jmultisig_tx_hex ? env->GetStringUTFChars(jmultisig_tx_hex, NULL) : nullptr; + string multisig_tx_hex = string(_multisig_tx_hex ? _multisig_tx_hex : ""); + env->ReleaseStringUTFChars(jmultisig_tx_hex, _multisig_tx_hex); + + // sign multisig tx hex and return result + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + monero_multisig_sign_result result = wallet->sign_multisig_tx_hex(multisig_tx_hex); + return env->NewStringUTF(result.serialize().c_str()); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_submitMultisigTxHexJni(JNIEnv* env, jobject instance, jstring jsigned_multisig_tx_hex) { + MTRACE("Java_monero_wallet_MoneroWalletLight_submitMultisigTxHexJni"); + + // get signed multisig tx hex as string + const char* _signed_multisig_tx_hex = jsigned_multisig_tx_hex ? env->GetStringUTFChars(jsigned_multisig_tx_hex, NULL) : nullptr; + string signed_multisig_tx_hex = string(_signed_multisig_tx_hex ? _signed_multisig_tx_hex : ""); + env->ReleaseStringUTFChars(jsigned_multisig_tx_hex, _signed_multisig_tx_hex); + + // submit signed multisig tx hex and return the resulting tx hashes + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + vector tx_hashes = wallet->submit_multisig_tx_hex(signed_multisig_tx_hex); + jobjectArray jtx_hashes = env->NewObjectArray(tx_hashes.size(), env->FindClass("java/lang/String"), nullptr); + for (int i = 0; i < tx_hashes.size(); i++) env->SetObjectArrayElement(jtx_hashes, i, env->NewStringUTF(tx_hashes[i].c_str())); + return jtx_hashes; + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_changePasswordJni(JNIEnv* env, jobject instance, jstring jold_password, jstring jnew_password) { + MTRACE("Java_monero_wallet_MoneroWalletLight_changePasswordJni(oldPassword, newPassword)"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _old_password = jold_password ? env->GetStringUTFChars(jold_password, NULL) : nullptr; + const char* _new_password = jnew_password ? env->GetStringUTFChars(jnew_password, NULL) : nullptr; + string old_password = string(_old_password ? _old_password : ""); + string new_password = string(_new_password ? _new_password : ""); + try { + wallet->change_password(old_password, new_password); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_moveToJni(JNIEnv* env, jobject instance, jstring jpath, jstring jpassword) { + MTRACE("Java_monero_wallet_MoneroWalletLight_moveToJni(path, password)"); + const char* _path = jpath ? env->GetStringUTFChars(jpath, NULL) : nullptr; + const char* _password = jpassword ? env->GetStringUTFChars(jpassword, NULL) : nullptr; + string path = string(_path ? _path : ""); + env->ReleaseStringUTFChars(jpath, _path); + string password = string(_password ? _password : ""); + env->ReleaseStringUTFChars(jpassword, _password); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + wallet->move_to(path, password); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_saveJni(JNIEnv* env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_saveJni(path, password)"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + wallet->save(); + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + } +} + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_closeJni(JNIEnv* env, jobject instance, jboolean save) { + MTRACE("Java_monero_wallet_MoneroWalletLight_CloseJni"); + monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + if (save) wallet->save(); + delete wallet; + wallet = nullptr; +} + +JNIEXPORT jbyteArray JNICALL Java_monero_wallet_MoneroWalletLight_getKeysFileBufferJni(JNIEnv* env, jobject instance, jstring jpassword, jboolean view_only) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getKeysFileBufferJni(password, view_only)"); + monero_wallet_light* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + const char* _password = jpassword ? env->GetStringUTFChars(jpassword, NULL) : nullptr; + string password = string(_password ? _password : ""); + env->ReleaseStringUTFChars(jpassword, _password); + try { + + // get keys buffer + std::string keys_buf = wallet->get_keys_file_buffer(password, view_only); + + // create java object + jbyteArray jkeys_buf = env->NewByteArray(keys_buf.length()); + env->SetByteArrayRegion(jkeys_buf, 0, keys_buf.length(), (jbyte*) keys_buf.c_str()); + return jkeys_buf; + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + +JNIEXPORT jbyteArray JNICALL Java_monero_wallet_MoneroWalletLight_getCacheFileBufferJni(JNIEnv* env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletLight_getCacheFileBufferJni()"); + monero_wallet_light* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); + try { + + // get cache buffer + std::string cache_buf = wallet->get_cache_file_buffer(); + + // create java object + jbyteArray jcache_buf = env->NewByteArray(cache_buf.length()); + env->SetByteArrayRegion(jcache_buf, 0, cache_buf.length(), (jbyte*) cache_buf.c_str()); + return jcache_buf; + } catch (...) { + rethrow_cpp_exception_as_java_exception(env); + return 0; + } +} + #ifdef __cplusplus } #endif diff --git a/src/main/cpp/monero_jni_bridge.h b/src/main/cpp/monero_jni_bridge.h index 9c7bd424..204663c5 100644 --- a/src/main/cpp/monero_jni_bridge.h +++ b/src/main/cpp/monero_jni_bridge.h @@ -57,7 +57,7 @@ JNIEXPORT void JNICALL Java_monero_common_MoneroUtils_setLogLevelJni(JNIEnv *, j JNIEXPORT void JNICALL Java_monero_common_MoneroUtils_configureLoggingJni(JNIEnv *, jclass, jstring jpath, jboolean); -// --------------------------- STATIC WALLET UTILS ---------------------------- +// --------------------------- STATIC FULL WALLET UTILS ---------------------------- JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletFull_walletExistsJni(JNIEnv *, jclass, jstring); @@ -69,7 +69,19 @@ JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_createWalletJni(JNIE JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletFull_getSeedLanguagesJni(JNIEnv *, jclass); -// ----------------------------- INSTANCE METHODS ----------------------------- +// --------------------------- STATIC LIGHT WALLET UTILS ---------------------------- + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_walletExistsJni(JNIEnv *, jclass, jstring); + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_openWalletJni(JNIEnv *, jclass, jstring, jstring, jint); + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_openWalletDataJni(JNIEnv *, jclass, jstring, jint, jbyteArray, jbyteArray); + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_createWalletJni(JNIEnv *, jclass, jstring); + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_getSeedLanguagesJni(JNIEnv *, jclass); + +// ----------------------------- FULL WALLET INSTANCE METHODS ----------------------------- JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletFull_isViewOnlyJni(JNIEnv *, jobject); @@ -273,6 +285,210 @@ JNIEXPORT jbyteArray JNICALL Java_monero_wallet_MoneroWalletFull_getKeysFileBuff JNIEXPORT jbyteArray JNICALL Java_monero_wallet_MoneroWalletFull_getCacheFileBufferJni(JNIEnv *, jobject); +// ----------------------------- LIGHT WALLET INSTANCE METHODS ----------------------------- + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_isViewOnlyJni(JNIEnv *, jobject); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_setDaemonConnectionJni(JNIEnv *, jobject, jstring, jstring, jstring); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_setProxyJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_getDaemonConnectionJni(JNIEnv *, jobject); + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_isConnectedToDaemonJni(JNIEnv *, jobject); + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_isDaemonSyncedJni(JNIEnv *, jobject); + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_isSyncedJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getVersionJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getPathJni(JNIEnv *, jobject); + +JNIEXPORT jint JNICALL Java_monero_wallet_MoneroWalletLight_getNetworkTypeJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getSeedJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getSeedLanguageJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getPublicViewKeyJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getPrivateViewKeyJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getPublicSpendKeyJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getPrivateSpendKeyJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getAddressJni(JNIEnv *, jobject, jint, jint); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getAddressIndexJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getIntegratedAddressJni(JNIEnv *, jobject, jstring, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_decodeIntegratedAddressJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_getHeightJni(JNIEnv *, jobject); + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_getRestoreHeightJni(JNIEnv *, jobject); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_setRestoreHeightJni(JNIEnv *, jobject, jlong); + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_getDaemonHeightJni(JNIEnv *, jobject); + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_getDaemonMaxPeerHeightJni(JNIEnv *, jobject); + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_getHeightByDateJni(JNIEnv *, jobject, jint, jint, jint); + +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletLight_setListenerJni(JNIEnv *, jobject, jobject); + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_syncJni(JNIEnv *, jobject, jlong); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_startSyncingJni(JNIEnv *, jobject, jlong); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_stopSyncingJni(JNIEnv *, jobject); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_scanTxsJni(JNIEnv *, jobject, jobjectArray); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_rescanSpentJni(JNIEnv *, jobject); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_rescanBlockchainJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getBalanceWalletJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getBalanceAccountJni(JNIEnv *, jobject, jint); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getBalanceSubaddressJni(JNIEnv *, jobject, jint, jint); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getUnlockedBalanceWalletJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getUnlockedBalanceAccountJni(JNIEnv *, jobject, jint); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getUnlockedBalanceSubaddressJni(JNIEnv *, jobject, jint, jint); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getAccountsJni(JNIEnv *, jobject, jboolean, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getAccountJni(JNIEnv *, jobject, jint, jboolean); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_createAccountJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getSubaddressesJni(JNIEnv *, jobject, jint, jintArray); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_createSubaddressJni(JNIEnv *, jobject, jint, jstring); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_setSubaddressLabelJni(JNIEnv *, jobject, jint, jint, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getTxsJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getTransfersJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getOutputsJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_exportKeyImagesJni(JNIEnv *, jobject, jboolean); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_importKeyImagesJni(JNIEnv *, jobject, jstring); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_freezeOutputJni(JNIEnv *, jobject, jstring); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_thawOutputJni(JNIEnv *, jobject, jstring); + +JNIEXPORT bool JNICALL Java_monero_wallet_MoneroWalletLight_isOutputFrozenJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_createTxsJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_sweepUnlockedJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_sweepOutputJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_sweepDustJni(JNIEnv *, jobject, jboolean); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_describeTxSetJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_signTxsJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_submitTxsJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_relayTxsJni(JNIEnv *, jobject, jobjectArray); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_signMessageJni(JNIEnv *, jobject, jstring, jint, jint, jint); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_verifyMessageJni(JNIEnv *, jobject, jstring, jstring, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getTxKeyJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_checkTxKeyJni(JNIEnv *, jobject, jstring, jstring, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getTxProofJni(JNIEnv *, jobject, jstring, jstring, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_checkTxProofJni(JNIEnv *, jobject, jstring, jstring, jstring, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getSpendProofJni(JNIEnv *, jobject, jstring, jstring); + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_checkSpendProofJni(JNIEnv *, jobject, jstring, jstring, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getReserveProofWalletJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getReserveProofAccountJni(JNIEnv *, jobject, jint, jstring, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_checkReserveProofJni(JNIEnv *, jobject, jstring, jstring, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getPaymentUriJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_parsePaymentUriJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_exportOutputsJni(JNIEnv *, jobject, jboolean); + +JNIEXPORT jint JNICALL Java_monero_wallet_MoneroWalletLight_importOutputsJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_getTxNotesJni(JNIEnv *, jobject, jobjectArray); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_setTxNotesJni(JNIEnv *, jobject, jobjectArray, jobjectArray); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getAddressBookEntriesJni(JNIEnv *, jobject, jintArray); + +JNIEXPORT jint JNICALL Java_monero_wallet_MoneroWalletLight_addAddressBookEntryJni(JNIEnv *, jobject, jstring, jstring); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_editAddressBookEntryJni(JNIEnv *, jobject, jint, jboolean, jstring, jboolean, jstring); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_deleteAddressBookEntryJni(JNIEnv *, jobject, jint); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getAttributeJni(JNIEnv *, jobject, jstring); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_setAttributeJni(JNIEnv *, jobject, jstring, jstring); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_startMiningJni(JNIEnv *, jobject, jlong, jboolean, jboolean); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_stopMiningJni(JNIEnv *, jobject); + +JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletLight_isMultisigImportNeededJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_getMultisigInfoJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_prepareMultisigJni(JNIEnv *, jobject); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_makeMultisigJni(JNIEnv *, jobject, jobjectArray, jint, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_exchangeMultisigKeysJni(JNIEnv *, jobject, jobjectArray, jstring); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_exportMultisigHexJni(JNIEnv *, jobject); + +JNIEXPORT jint JNICALL Java_monero_wallet_MoneroWalletLight_importMultisigHexJni(JNIEnv *, jobject, jobjectArray); + +JNIEXPORT jstring JNICALL Java_monero_wallet_MoneroWalletLight_signMultisigTxHexJni(JNIEnv *, jobject, jstring); + +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletLight_submitMultisigTxHexJni(JNIEnv *, jobject, jstring); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_changePasswordJniJni(JNIEnv *, jobject, jstring, jstring); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_moveToJni(JNIEnv *, jobject, jstring, jstring); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_saveJni(JNIEnv *, jobject); + +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletLight_closeJni(JNIEnv *, jobject, jboolean); + +JNIEXPORT jbyteArray JNICALL Java_monero_wallet_MoneroWalletLight_getKeysFileBufferJni(JNIEnv *, jobject, jstring, jboolean); + +JNIEXPORT jbyteArray JNICALL Java_monero_wallet_MoneroWalletLight_getCacheFileBufferJni(JNIEnv *, jobject); + #ifdef __cplusplus } #endif diff --git a/src/main/java/monero/wallet/MoneroWalletFull.java b/src/main/java/monero/wallet/MoneroWalletFull.java index 399a1463..5827c0c6 100644 --- a/src/main/java/monero/wallet/MoneroWalletFull.java +++ b/src/main/java/monero/wallet/MoneroWalletFull.java @@ -97,7 +97,7 @@ public class MoneroWalletFull extends MoneroWalletDefault { * @param jniWalletHandle memory address of the wallet in c++ * @param password password of the wallet instance */ - private MoneroWalletFull(long jniWalletHandle, String password) { + protected MoneroWalletFull(long jniWalletHandle, String password) { this.jniWalletHandle = jniWalletHandle; this.jniListener = new WalletJniListener(); this.password = password; @@ -327,7 +327,7 @@ private static MoneroWalletFull createWalletRandom(MoneroWalletConfig config) { return new MoneroWalletFull(jniWalletHandle, config.getPassword()); } - private static String serializeWalletConfig(MoneroWalletConfig config) { + protected static String serializeWalletConfig(MoneroWalletConfig config) { Map configMap = JsonUtils.toMap(config); configMap.put("networkType", config.getNetworkType().ordinal()); return JsonUtils.serialize(configMap); @@ -1397,217 +1397,217 @@ public void close(boolean save) { // ------------------------------ NATIVE METHODS ---------------------------- - private native static boolean walletExistsJni(String path); + protected native static boolean walletExistsJni(String path); - private native static long openWalletJni(String path, String password, int networkType); + protected native static long openWalletJni(String path, String password, int networkType); - private native static long openWalletDataJni(String password, int networkType, byte[] keysData, byte[] cacheData); + protected native static long openWalletDataJni(String password, int networkType, byte[] keysData, byte[] cacheData); - private native static long createWalletJni(String walletConfigJson); + protected native static long createWalletJni(String walletConfigJson); - private native long getHeightJni(); + protected native long getHeightJni(); - private native long getRestoreHeightJni(); + protected native long getRestoreHeightJni(); - private native void setRestoreHeightJni(long height); + protected native void setRestoreHeightJni(long height); - private native long getDaemonHeightJni(); + protected native long getDaemonHeightJni(); - private native long getDaemonMaxPeerHeightJni(); + protected native long getDaemonMaxPeerHeightJni(); - private native long getHeightByDateJni(int year, int month, int day); + protected native long getHeightByDateJni(int year, int month, int day); - private native boolean isViewOnlyJni(); + protected native boolean isViewOnlyJni(); - private native void setDaemonConnectionJni(String uri, String username, String password); + protected native void setDaemonConnectionJni(String uri, String username, String password); - private native void setProxyJni(String uri); + protected native void setProxyJni(String uri); - private native String[] getDaemonConnectionJni(); // returns [uri, username, password] + protected native String[] getDaemonConnectionJni(); // returns [uri, username, password] - private native boolean isConnectedToDaemonJni(); + protected native boolean isConnectedToDaemonJni(); - private native boolean isDaemonSyncedJni(); + protected native boolean isDaemonSyncedJni(); - private native boolean isSyncedJni(); + protected native boolean isSyncedJni(); - private native int getNetworkTypeJni(); + protected native int getNetworkTypeJni(); - private native String getVersionJni(); + protected native String getVersionJni(); - private native String getPathJni(); + protected native String getPathJni(); - private native String getSeedJni(); + protected native String getSeedJni(); - private native String getSeedLanguageJni(); + protected native String getSeedLanguageJni(); - private static native String[] getSeedLanguagesJni(); + protected static native String[] getSeedLanguagesJni(); - private native String getPublicViewKeyJni(); + protected native String getPublicViewKeyJni(); - private native String getPrivateViewKeyJni(); + protected native String getPrivateViewKeyJni(); - private native String getPublicSpendKeyJni(); + protected native String getPublicSpendKeyJni(); - private native String getPrivateSpendKeyJni(); + protected native String getPrivateSpendKeyJni(); - private native String getAddressJni(int accountIdx, int subaddressIdx); + protected native String getAddressJni(int accountIdx, int subaddressIdx); - private native String getAddressIndexJni(String address); + protected native String getAddressIndexJni(String address); - private native String getIntegratedAddressJni(String standardAddress, String paymentId); + protected native String getIntegratedAddressJni(String standardAddress, String paymentId); - private native String decodeIntegratedAddressJni(String integratedAddress); + protected native String decodeIntegratedAddressJni(String integratedAddress); - private native long setListenerJni(WalletJniListener listener); + protected native long setListenerJni(WalletJniListener listener); - private native Object[] syncJni(long startHeight); + protected native Object[] syncJni(long startHeight); - private native void startSyncingJni(long syncPeriodInMs); + protected native void startSyncingJni(long syncPeriodInMs); - private native void stopSyncingJni(); + protected native void stopSyncingJni(); - private native void scanTxsJni(String[] txHashes); + protected native void scanTxsJni(String[] txHashes); - private native void rescanSpentJni(); + protected native void rescanSpentJni(); - private native void rescanBlockchainJni(); + protected native void rescanBlockchainJni(); - private native String getBalanceWalletJni(); + protected native String getBalanceWalletJni(); - private native String getBalanceAccountJni(int accountIdx); + protected native String getBalanceAccountJni(int accountIdx); - private native String getBalanceSubaddressJni(int accountIdx, int subaddressIdx); + protected native String getBalanceSubaddressJni(int accountIdx, int subaddressIdx); - private native String getUnlockedBalanceWalletJni(); + protected native String getUnlockedBalanceWalletJni(); - private native String getUnlockedBalanceAccountJni(int accountIdx); + protected native String getUnlockedBalanceAccountJni(int accountIdx); - private native String getUnlockedBalanceSubaddressJni(int accountIdx, int subaddressIdx); + protected native String getUnlockedBalanceSubaddressJni(int accountIdx, int subaddressIdx); - private native String getAccountsJni(boolean includeSubaddresses, String tag); + protected native String getAccountsJni(boolean includeSubaddresses, String tag); - private native String getAccountJni(int accountIdx, boolean includeSubaddresses); + protected native String getAccountJni(int accountIdx, boolean includeSubaddresses); - private native String createAccountJni(String label); + protected native String createAccountJni(String label); - private native String getSubaddressesJni(int accountIdx, int[] subaddressIndices); + protected native String getSubaddressesJni(int accountIdx, int[] subaddressIndices); - private native String createSubaddressJni(int accountIdx, String label); + protected native String createSubaddressJni(int accountIdx, String label); - private native void setSubaddressLabelJni(int accountIdx, int subaddressIdx, String label); + protected native void setSubaddressLabelJni(int accountIdx, int subaddressIdx, String label); - private native String getTxsJni(String txQueryJson); + protected native String getTxsJni(String txQueryJson); - private native String getTransfersJni(String transferQueryJson); + protected native String getTransfersJni(String transferQueryJson); - private native String getOutputsJni(String outputQueryJson); + protected native String getOutputsJni(String outputQueryJson); - private native String exportOutputsJni(boolean all); + protected native String exportOutputsJni(boolean all); - private native int importOutputsJni(String outputsHex); + protected native int importOutputsJni(String outputsHex); - private native String exportKeyImagesJni(boolean all); + protected native String exportKeyImagesJni(boolean all); - private native String importKeyImagesJni(String keyImagesJson); + protected native String importKeyImagesJni(String keyImagesJson); - private native String[] relayTxsJni(String[] txMetadatas); + protected native String[] relayTxsJni(String[] txMetadatas); - private native void freezeOutputJni(String KeyImage); + protected native void freezeOutputJni(String KeyImage); - private native void thawOutputJni(String keyImage); + protected native void thawOutputJni(String keyImage); - private native boolean isOutputFrozenJni(String keyImage); + protected native boolean isOutputFrozenJni(String keyImage); - private native String createTxsJni(String txConfigJson); + protected native String createTxsJni(String txConfigJson); - private native String sweepUnlockedJni(String txConfigJson); + protected native String sweepUnlockedJni(String txConfigJson); - private native String sweepOutputJni(String txConfigJson); + protected native String sweepOutputJni(String txConfigJson); - private native String sweepDustJni(boolean doNotRelay); + protected native String sweepDustJni(boolean doNotRelay); - private native String describeTxSetJni(String txSetJson); + protected native String describeTxSetJni(String txSetJson); - private native String signTxsJni(String unsignedTxHex); + protected native String signTxsJni(String unsignedTxHex); - private native String[] submitTxsJni(String signedTxHex); + protected native String[] submitTxsJni(String signedTxHex); - private native String[] getTxNotesJni(String[] txHashes); + protected native String[] getTxNotesJni(String[] txHashes); - private native void setTxNotesJni(String[] txHashes, String[] notes); + protected native void setTxNotesJni(String[] txHashes, String[] notes); - private native String signMessageJni(String msg, int signatureType, int accountIdx, int subaddressIdx); + protected native String signMessageJni(String msg, int signatureType, int accountIdx, int subaddressIdx); - private native String verifyMessageJni(String msg, String address, String signature); + protected native String verifyMessageJni(String msg, String address, String signature); - private native String getTxKeyJni(String txHash); + protected native String getTxKeyJni(String txHash); - private native String checkTxKeyJni(String txHash, String txKey, String address); + protected native String checkTxKeyJni(String txHash, String txKey, String address); - private native String getTxProofJni(String txHash, String address, String message); + protected native String getTxProofJni(String txHash, String address, String message); - private native String checkTxProofJni(String txHash, String address, String message, String signature); + protected native String checkTxProofJni(String txHash, String address, String message, String signature); - private native String getSpendProofJni(String txHash, String message); + protected native String getSpendProofJni(String txHash, String message); - private native boolean checkSpendProofJni(String txHash, String message, String signature); + protected native boolean checkSpendProofJni(String txHash, String message, String signature); - private native String getReserveProofWalletJni(String message); + protected native String getReserveProofWalletJni(String message); - private native String getReserveProofAccountJni(int accountIdx, String amount, String message); + protected native String getReserveProofAccountJni(int accountIdx, String amount, String message); - private native String checkReserveProofJni(String address, String message, String signature); + protected native String checkReserveProofJni(String address, String message, String signature); - private native String getAddressBookEntriesJni(int[] indices); + protected native String getAddressBookEntriesJni(int[] indices); - private native int addAddressBookEntryJni(String address, String description); + protected native int addAddressBookEntryJni(String address, String description); - private native void editAddressBookEntryJni(int index, boolean setAddress, String address, boolean setDescription, String description); + protected native void editAddressBookEntryJni(int index, boolean setAddress, String address, boolean setDescription, String description); - private native void deleteAddressBookEntryJni(int entryIdx); + protected native void deleteAddressBookEntryJni(int entryIdx); - private native String getPaymentUriJni(String sendRequestJson); + protected native String getPaymentUriJni(String sendRequestJson); - private native String parsePaymentUriJni(String uri); + protected native String parsePaymentUriJni(String uri); - private native String getAttributeJni(String key); + protected native String getAttributeJni(String key); - private native void setAttributeJni(String key, String val); + protected native void setAttributeJni(String key, String val); - private native void startMiningJni(long numThreads, boolean backgroundMining, boolean ignoreBattery); + protected native void startMiningJni(long numThreads, boolean backgroundMining, boolean ignoreBattery); - private native void stopMiningJni(); + protected native void stopMiningJni(); - private native boolean isMultisigImportNeededJni(); + protected native boolean isMultisigImportNeededJni(); - private native String getMultisigInfoJni(); + protected native String getMultisigInfoJni(); - private native String prepareMultisigJni(); + protected native String prepareMultisigJni(); - private native String makeMultisigJni(String[] multisigHexes, int threshold, String password); + protected native String makeMultisigJni(String[] multisigHexes, int threshold, String password); - private native String exchangeMultisigKeysJni(String[] multisigHexes, String password); + protected native String exchangeMultisigKeysJni(String[] multisigHexes, String password); - private native String exportMultisigHexJni(); + protected native String exportMultisigHexJni(); - private native int importMultisigHexJni(String[] multisigHexes); + protected native int importMultisigHexJni(String[] multisigHexes); - private native String signMultisigTxHexJni(String multisigTxHex); + protected native String signMultisigTxHexJni(String multisigTxHex); - private native String[] submitMultisigTxHexJni(String signedMultisigTxHex); + protected native String[] submitMultisigTxHexJni(String signedMultisigTxHex); - private native byte[] getKeysFileBufferJni(String password, boolean viewOnly); + protected native byte[] getKeysFileBufferJni(String password, boolean viewOnly); - private native byte[] getCacheFileBufferJni(); + protected native byte[] getCacheFileBufferJni(); - private native void changePasswordJni(String oldPassword, String newPassword); + protected native void changePasswordJni(String oldPassword, String newPassword); - private native void moveToJni(String path, String password); + protected native void moveToJni(String path, String password); - private native void saveJni(); + protected native void saveJni(); - private native void closeJni(boolean save); + protected native void closeJni(boolean save); // -------------------------------- LISTENER -------------------------------- @@ -1830,7 +1830,7 @@ private static List deserializeTransfers(MoneroTransferQuery que return transfers; } - private static List deserializeOutputs(MoneroOutputQuery query, String blocksJson) { + protected static List deserializeOutputs(MoneroOutputQuery query, String blocksJson) { // deserialize blocks DeserializedBlocksContainer deserializedBlocks = deserializeBlocks(blocksJson); @@ -1862,7 +1862,7 @@ private void refreshListening() { jniListenerHandle = setListenerJni(isEnabled ? jniListener : null); } - private void assertNotClosed() { + protected void assertNotClosed() { if (isClosed) throw new MoneroError("Wallet is closed"); } diff --git a/src/main/java/monero/wallet/MoneroWalletLight.java b/src/main/java/monero/wallet/MoneroWalletLight.java new file mode 100644 index 00000000..26f20095 --- /dev/null +++ b/src/main/java/monero/wallet/MoneroWalletLight.java @@ -0,0 +1,380 @@ +package monero.wallet; + +import java.util.List; + +import common.utils.GenUtils; +import common.utils.JsonUtils; +import monero.common.MoneroError; +import monero.common.MoneroRpcConnection; +import monero.daemon.model.MoneroBlock; +import monero.daemon.model.MoneroNetworkType; +import monero.wallet.model.MoneroOutputQuery; +import monero.wallet.model.MoneroOutputWallet; +import monero.wallet.model.MoneroTxQuery; +import monero.wallet.model.MoneroWalletConfig; + +public class MoneroWalletLight extends MoneroWalletFull { + + public static MoneroWalletLight openWallet(String path, String password, MoneroNetworkType networkType, MoneroRpcConnection daemonConnection) { + 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()); + MoneroWalletLight wallet = new MoneroWalletLight(jniWalletHandle, password); + if (daemonConnection != null) wallet.setDaemonConnection(daemonConnection); + return wallet; + } + public static MoneroWalletLight openWallet(String path, String password, MoneroNetworkType networkType) { return openWallet(path, password, networkType, (MoneroRpcConnection) null); } + public static MoneroWalletLight openWallet(String path, String password, MoneroNetworkType networkType, String daemonUri) { return openWallet(path, password, networkType, daemonUri == null ? null : new MoneroRpcConnection(daemonUri)); } + + public static MoneroWalletLight openWalletData(String password, MoneroNetworkType networkType, byte[] keysData, byte[] cacheData, MoneroRpcConnection daemonConnection) { + 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); + MoneroWalletLight wallet = new MoneroWalletLight(jniWalletHandle, password); + if (daemonConnection != null) wallet.setDaemonConnection(daemonConnection); + return wallet; + } + + public static MoneroWalletLight openWallet(MoneroWalletConfig config) { + + // validate config + if (config == null) throw new MoneroError("Must specify config to open wallet"); + if (config.getPassword() == null) throw new MoneroError("Must specify password to decrypt wallet"); + if (config.getNetworkType() == null) throw new MoneroError("Must specify a network type: 'mainnet', 'testnet' or 'stagenet'"); + if (config.getSeed() != null) throw new MoneroError("Cannot specify seed when opening wallet"); + if (config.getSeedOffset() != null) throw new MoneroError("Cannot specify seed offset when opening wallet"); + if (config.getPrimaryAddress() != null) throw new MoneroError("Cannot specify primary address when opening wallet"); + if (config.getPrivateViewKey() != null) throw new MoneroError("Cannot specify private view key when opening wallet"); + if (config.getPrivateSpendKey() != null) throw new MoneroError("Cannot specify private spend key when opening wallet"); + if (config.getRestoreHeight() != null) throw new MoneroError("Cannot specify restore height when opening wallet"); + if (config.getLanguage() != null) throw new MoneroError("Cannot specify language when opening wallet"); + if (Boolean.TRUE.equals(config.getSaveCurrent())) throw new MoneroError("Cannot save current wallet when opening full wallet"); + + // set server from connection manager if provided + if (config.getConnectionManager() != null) { + if (config.getServer() != null) throw new MoneroError("Wallet can be opened with a server or connection manager but not both"); + config.setServer(config.getConnectionManager().getConnection()); + } + + // read wallet data from disk unless provided + MoneroWalletLight wallet; + if (config.getKeysData() == null) { + wallet = openWallet(config.getPath(), config.getPassword(), config.getNetworkType(), config.getServer()); + } else { + wallet = openWalletData(config.getPassword(), config.getNetworkType(), config.getKeysData(), config.getCacheData(), config.getServer()); + } + + // set connection manager + wallet.setConnectionManager(config.getConnectionManager()); + return wallet; + } + + public static MoneroWalletLight createWallet(MoneroWalletConfig config) { + + // validate config + if (config == null) throw new MoneroError("Must specify config to open wallet"); + if (config.getNetworkType() == null) throw new MoneroError("Must specify a network type: 'mainnet', 'testnet' or 'stagenet'"); + if (config.getPath() != null && !config.getPath().isEmpty() && MoneroWalletLight.walletExists(config.getPath())) throw new MoneroError("Wallet already exists: " + config.getPath()); + if (config.getSeed() != null && (config.getPrimaryAddress() != null || config.getPrivateViewKey() != null || config.getPrivateSpendKey() != null)) { + throw new MoneroError("Wallet may be initialized with a seed or keys but not both"); + } + if (Boolean.TRUE.equals(config.getSaveCurrent() != null)) throw new MoneroError("Cannot save current wallet when creating full wallet"); + + // set server from connection manager if provided + if (config.getConnectionManager() != null) { + if (config.getServer() != null) throw new MoneroError("Wallet can be created with a server or connection manager but not both"); + config.setServer(config.getConnectionManager().getConnection()); + } + + // create wallet + MoneroWalletLight wallet; + if (config.getSeed() != null) { + if (config.getLanguage() != null) throw new MoneroError("Cannot specify language when creating wallet from seed"); + wallet = createWalletFromSeed(config); + } else if (config.getPrimaryAddress() != null || config.getPrivateSpendKey() != null) { + if (config.getSeedOffset() != null) throw new MoneroError("Cannot specify seed offset when creating wallet from keys"); + wallet = createWalletFromKeys(config); + } else { + if (config.getSeedOffset() != null) throw new MoneroError("Cannot specify seed offset when creating random wallet"); + if (config.getRestoreHeight() != null) throw new MoneroError("Cannot specify restore height when creating random wallet"); + wallet = createWalletRandom(config); + } + + // set connection manager + wallet.setConnectionManager(config.getConnectionManager()); + return wallet; + } + + private static MoneroWalletLight createWalletFromSeed(MoneroWalletConfig config) { + if (config.getRestoreHeight() == null) config.setRestoreHeight(0l); + long jniWalletHandle = createWalletJni(serializeWalletConfig(config)); + MoneroWalletLight wallet = new MoneroWalletLight(jniWalletHandle, config.getPassword()); + return wallet; + } + + private static MoneroWalletLight createWalletFromKeys(MoneroWalletConfig config) { + if (config.getRestoreHeight() == null) config.setRestoreHeight(0l); + if (config.getLanguage() == null) config.setLanguage(DEFAULT_LANGUAGE); + try { + long jniWalletHandle = createWalletJni(serializeWalletConfig(config)); + MoneroWalletLight wallet = new MoneroWalletLight(jniWalletHandle, config.getPassword()); + return wallet; + } catch (Exception e) { + throw new MoneroError(e.getMessage()); + } + } + + private static MoneroWalletLight createWalletRandom(MoneroWalletConfig config) { + if (config.getLanguage() == null) config.setLanguage(DEFAULT_LANGUAGE); + long jniWalletHandle = createWalletJni(serializeWalletConfig(config)); + return new MoneroWalletLight(jniWalletHandle, config.getPassword()); + } + + + private MoneroWalletLight(long jniWalletHandle, String password) { + super(jniWalletHandle, password); + } + + @Override + public List getOutputs(MoneroOutputQuery query) { + assertNotClosed(); + + // copy and normalize query up to block + if (query == null) query = new MoneroOutputQuery(); + else { + if (query.getTxQuery() == null) query = query.copy(); + else { + MoneroTxQuery txQuery = query.getTxQuery().copy(); + if (query.getTxQuery().getOutputQuery() == query) query = txQuery.getOutputQuery(); + else { + GenUtils.assertNull("Output query's tx query must be circular reference or null", query.getTxQuery().getOutputQuery()); + query = query.copy(); + query.setTxQuery(txQuery); + } + } + } + if (query.getTxQuery() == null) query.setTxQuery(new MoneroTxQuery()); + query.getTxQuery().setOutputQuery(query); + if (query.getTxQuery().getBlock() == null) query.getTxQuery().setBlock(new MoneroBlock().setTxs(query.getTxQuery())); + + // serialize query from block and fetch outputs from jni + String blocksJson = getOutputsJni(JsonUtils.serialize(query.getTxQuery().getBlock())); + + // deserialize and return outputs + return deserializeOutputs(query, blocksJson); + } + + // ------------------------------ NATIVE METHODS ---------------------------- + + protected native static boolean walletExistsJni(String path); + + protected native static long openWalletJni(String path, String password, int networkType); + + protected native static long openWalletDataJni(String password, int networkType, byte[] keysData, byte[] cacheData); + + protected native static long createWalletJni(String walletConfigJson); + + @Override + protected native long getHeightJni(); + @Override + protected native long getRestoreHeightJni(); + @Override + protected native void setRestoreHeightJni(long height); + @Override + protected native long getDaemonHeightJni(); + @Override + protected native long getDaemonMaxPeerHeightJni(); + @Override + protected native long getHeightByDateJni(int year, int month, int day); + @Override + protected native boolean isViewOnlyJni(); + @Override + protected native void setDaemonConnectionJni(String uri, String username, String password); + @Override + protected native void setProxyJni(String uri); + @Override + protected native String[] getDaemonConnectionJni(); // returns [uri, username, password] + @Override + protected native boolean isConnectedToDaemonJni(); + @Override + protected native boolean isDaemonSyncedJni(); + @Override + protected native boolean isSyncedJni(); + @Override + protected native int getNetworkTypeJni(); + @Override + protected native String getVersionJni(); + @Override + protected native String getPathJni(); + @Override + protected native String getSeedJni(); + @Override + protected native String getSeedLanguageJni(); + + protected static native String[] getSeedLanguagesJni(); + @Override + protected native String getPublicViewKeyJni(); + @Override + protected native String getPrivateViewKeyJni(); + @Override + protected native String getPublicSpendKeyJni(); + @Override + protected native String getPrivateSpendKeyJni(); + @Override + protected native String getAddressJni(int accountIdx, int subaddressIdx); + @Override + protected native String getAddressIndexJni(String address); + @Override + protected native String getIntegratedAddressJni(String standardAddress, String paymentId); + @Override + protected native String decodeIntegratedAddressJni(String integratedAddress); + + //protected native long setListenerJni(WalletJniListener listener); + @Override + protected native Object[] syncJni(long startHeight); + @Override + protected native void startSyncingJni(long syncPeriodInMs); + @Override + protected native void stopSyncingJni(); + @Override + protected native void scanTxsJni(String[] txHashes); + @Override + protected native void rescanSpentJni(); + @Override + protected native void rescanBlockchainJni(); + @Override + protected native String getBalanceWalletJni(); + @Override + protected native String getBalanceAccountJni(int accountIdx); + @Override + protected native String getBalanceSubaddressJni(int accountIdx, int subaddressIdx); + @Override + protected native String getUnlockedBalanceWalletJni(); + @Override + protected native String getUnlockedBalanceAccountJni(int accountIdx); + @Override + protected native String getUnlockedBalanceSubaddressJni(int accountIdx, int subaddressIdx); + @Override + protected native String getAccountsJni(boolean includeSubaddresses, String tag); + @Override + protected native String getAccountJni(int accountIdx, boolean includeSubaddresses); + @Override + protected native String createAccountJni(String label); + @Override + protected native String getSubaddressesJni(int accountIdx, int[] subaddressIndices); + @Override + protected native String createSubaddressJni(int accountIdx, String label); + @Override + protected native void setSubaddressLabelJni(int accountIdx, int subaddressIdx, String label); + @Override + protected native String getTxsJni(String txQueryJson); + @Override + protected native String getTransfersJni(String transferQueryJson); + @Override + protected native String getOutputsJni(String outputQueryJson); + @Override + protected native String exportOutputsJni(boolean all); + @Override + protected native int importOutputsJni(String outputsHex); + @Override + protected native String exportKeyImagesJni(boolean all); + @Override + protected native String importKeyImagesJni(String keyImagesJson); + @Override + protected native String[] relayTxsJni(String[] txMetadatas); + @Override + protected native void freezeOutputJni(String KeyImage); + @Override + protected native void thawOutputJni(String keyImage); + @Override + protected native boolean isOutputFrozenJni(String keyImage); + @Override + protected native String createTxsJni(String txConfigJson); + @Override + protected native String sweepUnlockedJni(String txConfigJson); + @Override + protected native String sweepOutputJni(String txConfigJson); + @Override + protected native String sweepDustJni(boolean doNotRelay); + @Override + protected native String describeTxSetJni(String txSetJson); + @Override + protected native String signTxsJni(String unsignedTxHex); + @Override + protected native String[] submitTxsJni(String signedTxHex); + @Override + protected native String[] getTxNotesJni(String[] txHashes); + @Override + protected native void setTxNotesJni(String[] txHashes, String[] notes); + @Override + protected native String signMessageJni(String msg, int signatureType, int accountIdx, int subaddressIdx); + @Override + protected native String verifyMessageJni(String msg, String address, String signature); + @Override + protected native String getTxKeyJni(String txHash); + @Override + protected native String checkTxKeyJni(String txHash, String txKey, String address); + @Override + protected native String getTxProofJni(String txHash, String address, String message); + @Override + protected native String checkTxProofJni(String txHash, String address, String message, String signature); + @Override + protected native String getSpendProofJni(String txHash, String message); + @Override + protected native boolean checkSpendProofJni(String txHash, String message, String signature); + @Override + protected native String getReserveProofWalletJni(String message); + @Override + protected native String getReserveProofAccountJni(int accountIdx, String amount, String message); + @Override + protected native String checkReserveProofJni(String address, String message, String signature); + @Override + protected native String getAddressBookEntriesJni(int[] indices); + @Override + protected native int addAddressBookEntryJni(String address, String description); + @Override + protected native void editAddressBookEntryJni(int index, boolean setAddress, String address, boolean setDescription, String description); + @Override + protected native void deleteAddressBookEntryJni(int entryIdx); + @Override + protected native String getPaymentUriJni(String sendRequestJson); + @Override + protected native String parsePaymentUriJni(String uri); + @Override + protected native String getAttributeJni(String key); + @Override + protected native void setAttributeJni(String key, String val); + @Override + protected native void startMiningJni(long numThreads, boolean backgroundMining, boolean ignoreBattery); + @Override + protected native void stopMiningJni(); + @Override + protected native boolean isMultisigImportNeededJni(); + @Override + protected native String getMultisigInfoJni(); + @Override + protected native String prepareMultisigJni(); + @Override + protected native String makeMultisigJni(String[] multisigHexes, int threshold, String password); + @Override + protected native String exchangeMultisigKeysJni(String[] multisigHexes, String password); + @Override + protected native String exportMultisigHexJni(); + @Override + protected native int importMultisigHexJni(String[] multisigHexes); + @Override + protected native String signMultisigTxHexJni(String multisigTxHex); + @Override + protected native String[] submitMultisigTxHexJni(String signedMultisigTxHex); + @Override + protected native byte[] getKeysFileBufferJni(String password, boolean viewOnly); + @Override + protected native byte[] getCacheFileBufferJni(); + @Override + protected native void changePasswordJni(String oldPassword, String newPassword); + @Override + protected native void moveToJni(String path, String password); + @Override + protected native void saveJni(); + @Override + protected native void closeJni(boolean save); +} \ No newline at end of file diff --git a/src/test/java/TestMoneroWalletCommon.java b/src/test/java/TestMoneroWalletCommon.java index e0ada136..8b7602a1 100644 --- a/src/test/java/TestMoneroWalletCommon.java +++ b/src/test/java/TestMoneroWalletCommon.java @@ -40,6 +40,7 @@ import monero.daemon.model.MoneroTx; import monero.daemon.model.MoneroVersion; import monero.wallet.MoneroWallet; +import monero.wallet.MoneroWalletLight; import monero.wallet.MoneroWalletRpc; import monero.wallet.model.MoneroAccount; import monero.wallet.model.MoneroAddressBookEntry; @@ -96,10 +97,10 @@ public abstract class TestMoneroWalletCommon { protected static final boolean TEST_RELAYS = true; protected static final boolean TEST_NOTIFICATIONS = true; protected static final boolean TEST_RESETS = false; - private static final int MAX_TX_PROOFS = 25; // maximum number of transactions to check for each proof, undefined to check all - private static final int SEND_MAX_DIFF = 60; - private static final int SEND_DIVISOR = 10; - private static final int NUM_BLOCKS_LOCKED = 10; + protected static final int MAX_TX_PROOFS = 25; // maximum number of transactions to check for each proof, undefined to check all + protected static final int SEND_MAX_DIFF = 60; + protected static final int SEND_DIVISOR = 10; + protected static final int NUM_BLOCKS_LOCKED = 10; // instance variables protected MoneroWallet wallet; // wallet instance to test @@ -3361,7 +3362,7 @@ public void testCreateThenRelaySplit() { testSendToSingle(new MoneroTxConfig().setCanSplit(true)); } - private void testSendToSingle(MoneroTxConfig config) { + protected void testSendToSingle(MoneroTxConfig config) { TestUtils.WALLET_TX_TRACKER.waitForWalletTxsToClearPool(wallet); if (config == null) config = new MoneroTxConfig(); @@ -5379,7 +5380,7 @@ public void testProveUnrelayedTxs() { * * TODO: ensure each tx passes query filter, same with testGetTransfer and getAndTestOutputs */ - private List getAndTestTxs(MoneroWallet wallet, MoneroTxQuery query, TxContext ctx, Boolean isExpected) { + protected List getAndTestTxs(MoneroWallet wallet, MoneroTxQuery query, TxContext ctx, Boolean isExpected) { MoneroTxQuery copy = null; if (query != null) copy = query.copy(); List txs = wallet.getTxs(query); @@ -5477,7 +5478,7 @@ protected void testSignatureHeaderCheckError(MoneroError e) { assertEquals("Signature header check error", e.getMessage()); } - private static void testAccount(MoneroAccount account) { + protected void testAccount(MoneroAccount account) { // test account assertNotNull(account); @@ -5506,7 +5507,7 @@ private static void testAccount(MoneroAccount account) { assertTrue(tag == null || tag.length() > 0); } - private static void testSubaddress(MoneroSubaddress subaddress) { + protected void testSubaddress(MoneroSubaddress subaddress) { assertTrue(subaddress.getAccountIndex() >= 0); assertTrue(subaddress.getIndex() >= 0); assertNotNull(subaddress.getAddress()); @@ -5913,7 +5914,7 @@ private static void testOutgoingTransfer(MoneroOutgoingTransfer transfer, TxCont } } - private static void testDestination(MoneroDestination destination) { + protected static void testDestination(MoneroDestination destination) { MoneroUtils.validateAddress(destination.getAddress(), TestUtils.NETWORK_TYPE); TestUtils.testUnsignedBigInteger(destination.getAmount(), true); } @@ -5973,7 +5974,7 @@ private static List getRandomTransactions(MoneroWallet wallet, M else return txs.subList(0, Math.min(maxTxs, txs.size())); } - private static void testCommonTxSets(List txs, boolean hasSigned, boolean hasUnsigned, boolean hasMultisig) { + protected static void testCommonTxSets(List txs, boolean hasSigned, boolean hasUnsigned, boolean hasMultisig) { assertTrue(txs.size() > 0); // assert that all sets are same reference @@ -6209,7 +6210,7 @@ protected class WalletNotificationCollector extends MoneroWalletListener { @Override public void onNewBlock(long height) { assertTrue(listening); - if (blockNotifications.size() > 0) assertTrue(height == blockNotifications.get(blockNotifications.size() - 1) + 1); + if (blockNotifications.size() > 0 && !(wallet instanceof MoneroWalletLight)) assertTrue(height == blockNotifications.get(blockNotifications.size() - 1) + 1); blockNotifications.add(height); } diff --git a/src/test/java/TestMoneroWalletFull.java b/src/test/java/TestMoneroWalletFull.java index 58874df4..1c2baa8e 100644 --- a/src/test/java/TestMoneroWalletFull.java +++ b/src/test/java/TestMoneroWalletFull.java @@ -23,6 +23,7 @@ import monero.daemon.model.MoneroNetworkType; import monero.wallet.MoneroWallet; import monero.wallet.MoneroWalletFull; +import monero.wallet.MoneroWalletLight; import monero.wallet.MoneroWalletRpc; import monero.wallet.model.MoneroMultisigInfo; import monero.wallet.model.MoneroMultisigInitResult; @@ -593,8 +594,8 @@ public void testSyncSeedStartHeightGTRestoreHeight() { testSyncSeed(TestUtils.FIRST_RECEIVE_HEIGHT + 3l, TestUtils.FIRST_RECEIVE_HEIGHT); } - private void testSyncSeed(Long startHeight, Long restoreHeight) { testSyncSeed(startHeight, restoreHeight, false, false); } - private void testSyncSeed(Long startHeight, Long restoreHeight, boolean skipGtComparison, boolean testPostSyncNotifications) { + protected void testSyncSeed(Long startHeight, Long restoreHeight) { testSyncSeed(startHeight, restoreHeight, false, false); } + protected void testSyncSeed(Long startHeight, Long restoreHeight, boolean skipGtComparison, boolean testPostSyncNotifications) { assertTrue(daemon.isConnected(), "Not connected to daemon"); if (startHeight != null && restoreHeight != null) assertTrue(startHeight <= TestUtils.FIRST_RECEIVE_HEIGHT || restoreHeight <= TestUtils.FIRST_RECEIVE_HEIGHT); @@ -1354,6 +1355,7 @@ public synchronized void onSyncProgress(long height, long startHeight, long endH double expectedPercentDone = (double) (height - startHeight + 1) / (double) (endHeight - startHeight); assertTrue(Double.compare(expectedPercentDone, percentDone) == 0); if (prevHeight == null) assertEquals(startHeight, height); + else if (wallet instanceof MoneroWalletLight) assertTrue(height > prevHeight); else assertEquals(height, prevHeight + 1); prevHeight = height; } @@ -1364,7 +1366,11 @@ public void onDone(long chainHeight) { if (prevHeight == null) { assertNull(prevCompleteHeight); assertEquals(chainHeight, startHeight); - } else { + } + else if (wallet instanceof MoneroWalletLight) { + assertTrue(chainHeight > prevHeight); + } + else { assertEquals(chainHeight - 1, (long) prevHeight); // otherwise last height is chain height - 1 assertEquals(chainHeight, (long) prevCompleteHeight); } diff --git a/src/test/java/TestMoneroWalletLight.java b/src/test/java/TestMoneroWalletLight.java new file mode 100644 index 00000000..9d27cb41 --- /dev/null +++ b/src/test/java/TestMoneroWalletLight.java @@ -0,0 +1,2626 @@ +import monero.common.MoneroConnectionManager; +import monero.common.MoneroError; +import monero.common.MoneroRpcConnection; +import monero.common.MoneroUtils; +import monero.daemon.model.MoneroKeyImage; +import monero.daemon.model.MoneroNetworkType; +import monero.wallet.MoneroWallet; +import monero.wallet.MoneroWalletFull; +import monero.wallet.MoneroWalletLight; +import monero.wallet.MoneroWalletRpc; +import monero.wallet.model.MoneroAccount; +import monero.wallet.model.MoneroMultisigInfo; +import monero.wallet.model.MoneroMultisigInitResult; +import monero.wallet.model.MoneroOutputQuery; +import monero.wallet.model.MoneroOutputWallet; +import monero.wallet.model.MoneroSyncResult; +import monero.wallet.model.MoneroTransfer; +import monero.wallet.model.MoneroTransferQuery; +import monero.wallet.model.MoneroTxConfig; +import monero.wallet.model.MoneroTxQuery; +import monero.wallet.model.MoneroTxWallet; +import monero.wallet.model.MoneroWalletConfig; +import monero.wallet.model.MoneroWalletListener; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestInstance.Lifecycle; + +import common.utils.GenUtils; + +import org.junit.jupiter.api.TestInstance; + +import utils.StartMining; +import utils.TestUtils; +import utils.WalletEqualityUtils; +import utils.WalletSyncPrinter; + +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@TestInstance(Lifecycle.PER_CLASS) // so @BeforeAll and @AfterAll can be used on non-static functions +public class TestMoneroWalletLight extends TestMoneroWalletFull { + + public TestMoneroWalletLight() { + super(); + } + + @Override + @BeforeAll + public void beforeAll() { + System.out.println("Starting Light Wallet Tests"); + super.beforeAll(); + } + + @Override + @AfterAll + public void afterAll() { + System.out.println("End Light Wallet Tests"); + super.afterAll(); + } + + @Override + protected MoneroWalletLight getTestWallet() { + return TestUtils.getWalletLight(); + } + + @Override + @BeforeEach + public void beforeEach(TestInfo testInfo) { + System.out.println("Before test " + testInfo.getDisplayName()); + + // stop mining + //MoneroMiningStatus status = daemon.getMiningStatus(); + //if (status.isActive()) daemon.stopMining(); + } + + public MoneroRpcConnection getRpcConnection() + { + return new MoneroRpcConnection(TestUtils.WALLET_LWS_URI); + } + + @Override + protected MoneroWalletLight openWallet(MoneroWalletConfig config, boolean startSyncing) { + + // assign defaults + if (config == null) config = new MoneroWalletConfig(); + if (config.getPassword() == null) config.setPassword(TestUtils.WALLET_PASSWORD); + if (config.getNetworkType() == null) config.setNetworkType(TestUtils.NETWORK_TYPE); + if (config.getServer() == null && config.getConnectionManager() == null) config.setServerUri(TestUtils.WALLET_LWS_URI); + + // open wallet + MoneroWalletLight wallet = MoneroWalletLight.openWallet(config); + if (startSyncing != false && wallet.isConnectedToDaemon()) { + wallet.startSyncing(TestUtils.SYNC_PERIOD_IN_MS); + wallet.sync(); + } + return wallet; + } + + @Override + protected MoneroWalletLight createWallet(MoneroWalletConfig config) { + return createWallet(config, true); + } + + @Override + protected MoneroWalletLight createWallet(MoneroWalletConfig config, boolean startSyncing) { + + // assign defaults + if (config == null) config = new MoneroWalletConfig(); + boolean random = config.getSeed() == null && config.getPrimaryAddress() == null; + if (config.getPath() == null) config.setPath(TestUtils.TEST_WALLETS_DIR + "/" + UUID.randomUUID().toString()); + if (config.getPassword() == null) config.setPassword(TestUtils.WALLET_PASSWORD); + if (config.getNetworkType() == null) config.setNetworkType(TestUtils.NETWORK_TYPE); + if (config.getServer() == null && config.getConnectionManager() == null) config.setServerUri(TestUtils.WALLET_LWS_URI); + if (config.getRestoreHeight() == null && !random) config.setRestoreHeight(0l); + + // create wallet + MoneroWalletLight wallet = MoneroWalletLight.createWallet(config); + if (!random) assertEquals(config.getRestoreHeight() == null ? 0l : config.getRestoreHeight(), wallet.getRestoreHeight()); + if (startSyncing != false && wallet.isConnectedToDaemon()) { + wallet.startSyncing(TestUtils.SYNC_PERIOD_IN_MS); + wallet.sync(); + } + return wallet; + } + + protected MoneroWalletLight createWallet() { + return createWallet(TestUtils.getWalletLightConfig()); + } + + protected MoneroWalletFull createWalletFull(MoneroWalletConfig config) { + return createWalletFull(config, true); + } + + protected MoneroWalletFull createWalletFull(MoneroWalletConfig config, boolean startSyncing) { + + // assign defaults + if (config == null) config = new MoneroWalletConfig(); + boolean random = config.getSeed() == null && config.getPrimaryAddress() == null; + if (config.getPath() == null) config.setPath(TestUtils.TEST_WALLETS_DIR + "/" + UUID.randomUUID().toString()); + if (config.getPassword() == null) config.setPassword(TestUtils.WALLET_PASSWORD); + if (config.getNetworkType() == null) config.setNetworkType(TestUtils.NETWORK_TYPE); + if (config.getServer() == null && config.getConnectionManager() == null) config.setServer(getRpcConnection()); + if (config.getRestoreHeight() == null && !random) config.setRestoreHeight(0l); + + // create wallet + MoneroWalletFull wallet = MoneroWalletFull.createWallet(config); + if (!random) assertEquals(config.getRestoreHeight() == null ? 0l : config.getRestoreHeight(), wallet.getRestoreHeight()); + if (startSyncing != false && wallet.isConnectedToDaemon()) wallet.startSyncing(TestUtils.SYNC_PERIOD_IN_MS); + return wallet; + } + + @Override + public void closeWallet(MoneroWallet wallet, boolean save) { + wallet.close(save); + } + + /** + * Get the wallet's supported languages for the seed. This is an + * instance method for wallet rpc and a static utility for other wallets. + * + * @return List are the wallet's supported languages + */ + @Override + protected List getSeedLanguages() { + return MoneroWalletLight.getSeedLanguages(); + } + + @Override + protected void testAccount(MoneroAccount account) { + + // test account + assertNotNull(account); + assertTrue(account.getIndex() >= 0); + MoneroUtils.validateAddress(account.getPrimaryAddress(), TestUtils.NETWORK_TYPE); + TestUtils.testUnsignedBigInteger(account.getBalance()); + TestUtils.testUnsignedBigInteger(account.getUnlockedBalance()); + + // if given, test subaddresses and that their balances add up to account balances + if (account.getSubaddresses() != null) { + BigInteger balance = BigInteger.valueOf(0); + BigInteger unlockedBalance = BigInteger.valueOf(0); + for (int i = 0; i < account.getSubaddresses().size(); i++) { + testSubaddress(account.getSubaddresses().get(i)); + assertEquals(account.getIndex(), account.getSubaddresses().get(i).getAccountIndex()); + assertEquals(i, (int) account.getSubaddresses().get(i).getIndex()); + balance = balance.add(account.getSubaddresses().get(i).getBalance()); + unlockedBalance = unlockedBalance.add(account.getSubaddresses().get(i).getUnlockedBalance()); + } + assertTrue(account.getBalance().equals(balance), "Subaddress balances " + balance + " != account " + account.getIndex() + " balance " + account.getBalance()); + assertTrue(account.getUnlockedBalance().equals(unlockedBalance), "Subaddress unlocked balances " + unlockedBalance + " != account " + account.getIndex() + " unlocked balance " + account.getUnlockedBalance()); + } + + // tag must be undefined or non-empty + String tag = account.getTag(); + assertTrue(tag == null || tag.length() > 0); + } + + + // --------------- DEMONSTRATION OF MONERO PROJECT ISSUES ---------------------- + + /** + * This test demonstrates that importing key images erases incoming transfers. + */ + @Disabled // TODO monero-project: fix this https://github.com/monero-project/monero/issues/5812 + @Test + @Override + public void testImportKeyImagesAndTransfers() { + MoneroWalletFull wallet = null; // create a wallet for this test since it becomes corrupt TODO: use common wallet and move to common tests when fixed + try { + + // create and sync a new wallet + String path = getRandomWalletPath(); + wallet = createWallet(new MoneroWalletConfig().setPath(path).setSeed(TestUtils.SEED).setRestoreHeight(TestUtils.FIRST_RECEIVE_HEIGHT)); + wallet.sync(); + + // repeatedly export and re-import key images and test transfer size + for (int i = 0; i < 3; i++) { + + // get incoming transfers before importing + List inTransfers1 = wallet.getTransfers(new MoneroTransferQuery().setIsIncoming(true)); + + // export and re-import key images + List keyImages = wallet.exportKeyImages(); + wallet.importKeyImages(keyImages); + + // get incoming transfers after importing + List inTransfers2 = wallet.getTransfers(new MoneroTransferQuery().setIsIncoming(true)); + + // incoming transfers should be equal + assertEquals(inTransfers1.size(), inTransfers2.size()); + assertEquals(inTransfers1, inTransfers2); + } + } finally { + wallet.close(); + } + } + + /** + * Test the daemon's ability to not hang from wallets which are continuously + * syncing, have registered listeners, and which are not closed. + */ + @Test + @Disabled // TODO monero-project: disabled because observing memory leak behavior when all tests run together + @Override + public void testCreateWalletsWithoutClose() { + + // lets make some wallets and then go away + for (int i = 0; i < 20; i++) { + String path = getRandomWalletPath(); + MoneroWalletFull willLeaveYouHanging = createWallet(new MoneroWalletConfig().setPath(path)); + willLeaveYouHanging.startSyncing(); + willLeaveYouHanging.addListener(new MoneroWalletListener()); // listen for wallet events which could aggrevate hanging + } + + // check in on the daemon + daemon.getHeight(); + + // start mining + try { StartMining.startMining(); } + catch (MoneroError e) { } + + // wait for a block + daemon.waitForNextBlockHeader(); + + // stop mining + try { daemon.stopMining(); } + catch (MoneroError e) { } + + // check in on the daemon + daemon.getHeight(); + + // wallet's intentionally not closed (daemon da man) + } + + // ------------------------------- BEGIN TESTS ------------------------------ + // ------------------------------- BEGIN TESTS ------------------------------ + + // Can get the daemon's height + @Test + @Override + public void testDaemon() { + assumeTrue(TEST_NON_RELAYS); + assertTrue(wallet.isConnectedToDaemon()); + long daemonHeight = wallet.getDaemonHeight(); + assertTrue(daemonHeight > 0); + } + + // Can get the daemon's max peer height + @Test + @Override + public void testGetDaemonMaxPeerHeight() { + assumeTrue(TEST_NON_RELAYS); + long height = ((MoneroWalletFull) wallet).getDaemonMaxPeerHeight(); + assertTrue(height > 0); + } + +// @Test +// public void getApproximateChainHeight() { +// long height = wallet.getApproximateChainHeight(); +// assertTrue(height > 0); +// } + + // Can create a random full wallet + @Test + @Override + public void testCreateWalletRandomFull() { + assumeTrue(TEST_NON_RELAYS); + /* + // create random wallet with defaults + String path = getRandomWalletPath(); + MoneroWalletFull wallet = createWallet(new MoneroWalletConfig().setPath(path).setNetworkType(MoneroNetworkType.MAINNET).setServerUri(TestUtils.OFFLINE_SERVER_URI)); + MoneroUtils.validateMnemonic(wallet.getSeed()); + MoneroUtils.validateAddress(wallet.getPrimaryAddress(), MoneroNetworkType.MAINNET); + assertEquals(MoneroNetworkType.MAINNET, wallet.getNetworkType()); + assertEquals(new MoneroRpcConnection(TestUtils.OFFLINE_SERVER_URI), wallet.getDaemonConnection()); + assertFalse(wallet.isConnectedToDaemon()); + assertEquals("English", wallet.getSeedLanguage()); + assertEquals(path, wallet.getPath()); + assertFalse(wallet.isSynced()); + assertEquals(1, wallet.getHeight()); // TODO monero-project: why does height of new unsynced wallet start at 1? + assertTrue(wallet.getRestoreHeight() >= 0); + + // cannot get daemon chain height + try { + wallet.getDaemonHeight(); + } catch (MoneroError e) { + assertEquals("Wallet is not connected to daemon", e.getMessage()); + } + + // set daemon connection and check chain height + wallet.setDaemonConnection(getRpcConnection()); + assertEquals(daemon.getHeight(), wallet.getDaemonHeight()); + + // close wallet which releases resources + wallet.close(); + */ + // create random wallet with non defaults + String path = getRandomWalletPath(); + MoneroWalletLight wallet = createWallet(new MoneroWalletConfig().setPath(path).setNetworkType(MoneroNetworkType.TESTNET).setLanguage("Spanish"), false); + MoneroUtils.validateMnemonic(wallet.getSeed()); + MoneroUtils.validateAddress(wallet.getPrimaryAddress(), MoneroNetworkType.TESTNET); + assertEquals(MoneroNetworkType.TESTNET, wallet.getNetworkType()); + assertNotNull(wallet.getDaemonConnection()); + assertTrue(getRpcConnection() != wallet.getDaemonConnection()); + assertTrue(getRpcConnection().equals(wallet.getDaemonConnection())); + assertTrue(wallet.isConnectedToDaemon()); + assertEquals("Spanish", wallet.getSeedLanguage()); + assertEquals(path, wallet.getPath()); + assertFalse(wallet.isSynced()); + assertEquals(1, wallet.getHeight()); // TODO monero-project: why is height of unsynced wallet 1? + if (daemon.isConnected()) assertEquals(daemon.getHeight(), wallet.getRestoreHeight()); + else assertTrue(wallet.getRestoreHeight() >= 0); + wallet.close(); + } + + // Can create a full wallet from seed + @Test + @Override + public void testCreateWalletFromSeedFull() { + assumeTrue(TEST_NON_RELAYS); + + // create unconnected wallet with seed + String path = getRandomWalletPath(); + MoneroWalletFull wallet = createWallet(new MoneroWalletConfig().setPath(path).setSeed(TestUtils.SEED).setServerUri(TestUtils.OFFLINE_SERVER_URI)); + assertEquals(TestUtils.SEED, wallet.getSeed()); + assertEquals(TestUtils.ADDRESS, wallet.getPrimaryAddress()); + assertEquals(TestUtils.NETWORK_TYPE, wallet.getNetworkType()); + assertEquals(new MoneroRpcConnection(TestUtils.OFFLINE_SERVER_URI), wallet.getDaemonConnection()); + assertFalse(wallet.isConnectedToDaemon()); + assertEquals("English", wallet.getSeedLanguage()); + assertEquals(path, wallet.getPath()); + assertFalse(wallet.isSynced()); + assertEquals(1, wallet.getHeight()); + assertEquals(0, wallet.getRestoreHeight()); + try { wallet.startSyncing(); } catch (MoneroError e) { assertEquals("Wallet is not connected to daemon", e.getMessage()); } + wallet.close(); + + // create wallet without restore height + path = getRandomWalletPath(); + wallet = createWallet(new MoneroWalletConfig().setPath(path).setSeed(TestUtils.SEED), false); + assertEquals(TestUtils.SEED, wallet.getSeed()); + assertEquals(TestUtils.ADDRESS, wallet.getPrimaryAddress()); + assertEquals(TestUtils.NETWORK_TYPE, wallet.getNetworkType()); + assertNotNull(wallet.getDaemonConnection()); + assertTrue(getRpcConnection() != wallet.getDaemonConnection()); + assertTrue(getRpcConnection().equals(wallet.getDaemonConnection())); + assertTrue(wallet.isConnectedToDaemon()); + assertEquals("English", wallet.getSeedLanguage()); + assertEquals(path, wallet.getPath()); + assertFalse(wallet.isSynced()); + assertEquals(1, wallet.getHeight()); + assertEquals(0, wallet.getRestoreHeight()); // TODO: restore height is lost after closing only in JNI + wallet.close(); + + // create wallet with seed, no connection, and restore height + long restoreHeight = 10000; + path = getRandomWalletPath(); + wallet = createWallet(new MoneroWalletConfig().setPath(path).setSeed(TestUtils.SEED).setRestoreHeight(restoreHeight).setServerUri(TestUtils.OFFLINE_SERVER_URI)); + assertEquals(TestUtils.SEED, wallet.getSeed()); + assertEquals(TestUtils.ADDRESS, wallet.getPrimaryAddress()); + assertEquals(TestUtils.NETWORK_TYPE, wallet.getNetworkType()); + assertEquals(new MoneroRpcConnection(TestUtils.OFFLINE_SERVER_URI), wallet.getDaemonConnection()); + assertFalse(wallet.isConnectedToDaemon()); + assertEquals("English", wallet.getSeedLanguage()); + assertEquals(1, wallet.getHeight()); // TODO monero-project: why does height of new unsynced wallet start at 1? + assertEquals(restoreHeight, wallet.getRestoreHeight()); + assertEquals(path, wallet.getPath()); + wallet.close(true); + wallet = openWallet(new MoneroWalletConfig().setPath(path).setServerUri(TestUtils.OFFLINE_SERVER_URI)); + assertFalse(wallet.isConnectedToDaemon()); + assertFalse(wallet.isSynced()); + assertEquals(1, wallet.getHeight()); + assertEquals(0, wallet.getRestoreHeight()); // restore height is lost after closing + wallet.close(); + + // create wallet with seed, connection, and restore height + path = getRandomWalletPath(); + wallet = createWallet(new MoneroWalletConfig().setPath(path).setSeed(TestUtils.SEED).setRestoreHeight(restoreHeight), false); + assertEquals(TestUtils.SEED, wallet.getSeed()); + assertEquals(TestUtils.ADDRESS, wallet.getPrimaryAddress()); + assertEquals(TestUtils.NETWORK_TYPE, wallet.getNetworkType()); + assertNotNull(wallet.getDaemonConnection()); + assertTrue(getRpcConnection() != wallet.getDaemonConnection()); + assertTrue(getRpcConnection().equals(wallet.getDaemonConnection())); + assertTrue(wallet.isConnectedToDaemon()); + assertEquals("English", wallet.getSeedLanguage()); + assertEquals(path, wallet.getPath()); + assertFalse(wallet.isSynced()); + assertEquals(1, wallet.getHeight()); // TODO monero-project: why does height of new unsynced wallet start at 1? + assertEquals(restoreHeight, wallet.getRestoreHeight()); + wallet.close(); + } + + // Can create a full wallet from keys + @Test + @Override + public void testCreateWalletFromKeysJni() { + assumeTrue(TEST_NON_RELAYS); + + // recreate test wallet from keys + String path = getRandomWalletPath(); + MoneroWalletLight walletKeys = createWallet(new MoneroWalletConfig().setPath(path).setPrimaryAddress(wallet.getPrimaryAddress()).setPrivateViewKey(wallet.getPrivateViewKey()).setPrivateSpendKey(wallet.getPrivateSpendKey()).setRestoreHeight(TestUtils.FIRST_RECEIVE_HEIGHT), false); + try { + assertEquals(wallet.getSeed(), walletKeys.getSeed()); + assertEquals(wallet.getPrimaryAddress(), walletKeys.getPrimaryAddress()); + assertEquals(wallet.getPrivateViewKey(), walletKeys.getPrivateViewKey()); + assertEquals(wallet.getPublicViewKey(), walletKeys.getPublicViewKey()); + assertEquals(wallet.getPrivateSpendKey(), walletKeys.getPrivateSpendKey()); + assertEquals(wallet.getPublicSpendKey(), walletKeys.getPublicSpendKey()); + assertEquals(TestUtils.FIRST_RECEIVE_HEIGHT, walletKeys.getRestoreHeight()); + assertTrue(walletKeys.isConnectedToDaemon()); + assertFalse(walletKeys.isSynced()); + } finally { + walletKeys.close(); + } + } + + // Is compatible with monero-wallet-rpc wallet files + @Test + @Override + public void testWalletFileCompatibility() { + assumeTrue(TEST_NON_RELAYS); + + // create wallet using monero-wallet-rpc + String walletName = GenUtils.getUUID(); + MoneroWalletRpc walletRpc = TestUtils.getWalletRpc(); + walletRpc.createWallet(new MoneroWalletConfig().setPath(walletName).setPassword(TestUtils.WALLET_PASSWORD).setSeed(TestUtils.SEED).setRestoreHeight(TestUtils.FIRST_RECEIVE_HEIGHT)); + walletRpc.sync(); + BigInteger balance = walletRpc.getBalance(); + String outputsHex = walletRpc.exportOutputs(); + walletRpc.close(true); + + // open as full wallet + MoneroWalletLight walletFull = MoneroWalletLight.openWallet(new MoneroWalletConfig().setPath(TestUtils.WALLET_RPC_LOCAL_WALLET_DIR + "/" + walletName).setPassword(TestUtils.WALLET_PASSWORD).setNetworkType(TestUtils.NETWORK_TYPE).setServerUri(TestUtils.DAEMON_RPC_URI).setServerUsername(TestUtils.DAEMON_RPC_USERNAME).setServerPassword(TestUtils.DAEMON_RPC_PASSWORD)); + walletFull.sync(); + assertEquals(TestUtils.SEED, walletFull.getSeed()); + assertEquals(TestUtils.ADDRESS, walletFull.getPrimaryAddress()); + assertEquals(balance, walletFull.getBalance()); + assertEquals(outputsHex.length(), walletFull.exportOutputs().length()); + walletFull.close(true); + + // create full wallet + walletName = GenUtils.getUUID(); + String path = TestUtils.WALLET_RPC_LOCAL_WALLET_DIR + "/" + walletName; + walletFull = MoneroWalletLight.createWallet(new MoneroWalletConfig().setPath(path).setPassword(TestUtils.WALLET_PASSWORD).setNetworkType(TestUtils.NETWORK_TYPE).setSeed(TestUtils.SEED).setRestoreHeight(TestUtils.FIRST_RECEIVE_HEIGHT).setServerUri(TestUtils.DAEMON_RPC_URI).setServerUsername(TestUtils.DAEMON_RPC_USERNAME).setServerPassword(TestUtils.DAEMON_RPC_PASSWORD)); + walletFull.sync(); + balance = walletFull.getBalance(); + outputsHex = walletFull.exportOutputs(); + walletFull.close(true); + + // rebuild wallet cache using full wallet + new File(path).delete(); + walletFull = MoneroWalletLight.openWallet(new MoneroWalletConfig().setPath(path).setPassword(TestUtils.WALLET_PASSWORD).setNetworkType(TestUtils.NETWORK_TYPE).setServerUri(TestUtils.DAEMON_RPC_URI).setServerUsername(TestUtils.DAEMON_RPC_USERNAME).setServerPassword(TestUtils.DAEMON_RPC_PASSWORD)); + walletFull.close(true); + + // open wallet using monero-wallet-rpc + walletRpc.openWallet(new MoneroWalletConfig().setPath(walletName).setPassword(TestUtils.WALLET_PASSWORD)); + walletRpc.sync(); + assertEquals(TestUtils.SEED, walletRpc.getSeed()); + assertEquals(TestUtils.ADDRESS, walletRpc.getPrimaryAddress()); + assertEquals(balance, walletRpc.getBalance()); + assertEquals(outputsHex.length(), walletRpc.exportOutputs().length()); + walletRpc.close(true); + } + + // Is compatible with monero-wallet-rpc outputs and offline transaction signing + @SuppressWarnings("unused") + @Test + @Override + public void testViewOnlyAndOfflineWalletCompatibility() throws InterruptedException, IOException { + assumeTrue(!LITE_MODE && (TEST_NON_RELAYS || TEST_RELAYS)); + + // create view-only wallet in wallet rpc process + MoneroWalletLight viewOnlyWallet = createWallet(new MoneroWalletConfig().setPrimaryAddress(wallet.getPrimaryAddress()).setPrivateViewKey(wallet.getPrivateViewKey()).setServerUri(TestUtils.WALLET_LWS_URI)); + viewOnlyWallet.sync(); + + // create offline full wallet + MoneroWalletFull offlineWallet = createWalletFull(new MoneroWalletConfig().setPrimaryAddress(wallet.getPrimaryAddress()).setPrivateViewKey(wallet.getPrivateViewKey()).setPrivateSpendKey(wallet.getPrivateSpendKey()).setServerUri(TestUtils.OFFLINE_SERVER_URI).setRestoreHeight(0l)); + + // test tx signing with wallets + try { + testViewOnlyAndOfflineWallets(viewOnlyWallet, offlineWallet); + } finally { + closeWallet(offlineWallet); + } + }; + + // Is compatible with monero-wallet-rpc multisig wallets + @Test + @Disabled + @Override + public void testMultisigCompatibility() throws InterruptedException, IOException { + assumeTrue(!LITE_MODE); + + // create participants with full wallet and monero-wallet-rpc + List participants = new ArrayList(); + participants.add(TestUtils.startWalletRpcProcess().createWallet(new MoneroWalletConfig().setPath(GenUtils.getUUID()).setPassword(TestUtils.WALLET_PASSWORD))); + participants.add(TestUtils.startWalletRpcProcess().createWallet(new MoneroWalletConfig().setPath(GenUtils.getUUID()).setPassword(TestUtils.WALLET_PASSWORD))); + participants.add(createWallet(new MoneroWalletConfig())); + + // test multisig + try { + testMultisig(participants, 3, 3, true); + } finally { + + // stop mining at end of test + try { daemon.stopMining(); } + catch (MoneroError e) { } + + // save and close participants + if (participants.get(0) instanceof MoneroWalletRpc) TestUtils.stopWalletRpcProcess((MoneroWalletRpc) participants.get(0)); + else participants.get(0).close(true); // multisig tests might restore wallet from seed + TestUtils.stopWalletRpcProcess((MoneroWalletRpc) participants.get(1)); + closeWallet(participants.get(2), true); + } + }; + + // Can re-sync an existing wallet from scratch + @Test + public void testResyncExisting() { + assertTrue(MoneroWalletLight.walletExists(TestUtils.WALLET_LIGHT_PATH)); + MoneroWalletLight wallet = openWallet(new MoneroWalletConfig().setPath(TestUtils.WALLET_LIGHT_PATH).setServerUri(TestUtils.OFFLINE_SERVER_URI), false); + wallet.setDaemonConnection(getRpcConnection()); + //long startHeight = TestUtils.TEST_RESTORE_HEIGHT; + long startHeight = wallet.getRestoreHeight(); + SyncProgressTester progressTester = new SyncProgressTester(wallet, startHeight, wallet.getDaemonHeight()); + + MoneroSyncResult result = wallet.sync(progressTester); + progressTester.onDone(wallet.getDaemonHeight()); + + // test result after syncing + assertTrue(wallet.isConnectedToDaemon()); + assertTrue(wallet.isSynced()); + assertEquals(wallet.getDaemonHeight() - startHeight - 1, (long) result.getNumBlocksFetched()); + assertTrue(result.getReceivedMoney()); + assertEquals(daemon.getHeight(), wallet.getHeight()); + wallet.close(true); + } + + // Can sync a wallet with a randomly generated seed + @Test + public void testSyncRandom() { + assumeTrue(TEST_NON_RELAYS); + assertTrue(daemon.isConnected(), "Not connected to daemon"); + + // create test wallet + MoneroWalletLight wallet = createWallet(new MoneroWalletConfig(), false); + long restoreHeight = daemon.getHeight(); + + // test wallet's height before syncing + assertEquals(TestUtils.getWalletLight().getDaemonConnection(), wallet.getDaemonConnection()); + // why test current daemon.getHeight() before syncing? + //assertEquals(restoreHeight, wallet.getDaemonHeight()); + assertTrue(wallet.isConnectedToDaemon()); + assertFalse(wallet.isSynced()); + assertEquals(1, wallet.getHeight()); + assertEquals(wallet.getDaemonHeight(), wallet.getRestoreHeight()); + //assertEquals(daemon.getHeight(), wallet.getDaemonHeight()); + + // sync the wallet + //SyncProgressTester progressTester = new SyncProgressTester(wallet, wallet.getRestoreHeight(), wallet.getDaemonHeight()); + MoneroSyncResult result = wallet.sync(); + //progressTester.onDone(wallet.getDaemonHeight()); + + // test result after syncing + MoneroWalletFull walletGt = TestUtils.createWalletGroundTruth(TestUtils.NETWORK_TYPE, wallet.getSeed(), null, restoreHeight); + walletGt.sync(); + try { + assertTrue(wallet.isConnectedToDaemon()); + assertTrue(wallet.isSynced()); + assertEquals(0, (long) result.getNumBlocksFetched()); + assertFalse(result.getReceivedMoney()); + assertEquals(daemon.getHeight(), wallet.getHeight()); + + // sync the wallet with default params + wallet.sync(); + assertTrue(wallet.isSynced()); + assertEquals(daemon.getHeight(), wallet.getHeight()); + + // compare wallet to ground truth + testWalletEqualityOnChain(walletGt, wallet); + } finally { + if (walletGt != null) walletGt.close(false); + wallet.close(); + } + + // attempt to sync unconnected wallet + wallet = createWallet(new MoneroWalletConfig().setServerUri(TestUtils.OFFLINE_SERVER_URI)); + try { + wallet.sync(); + fail("Should have thrown exception"); + } catch (MoneroError e) { + assertEquals("Wallet is not connected to daemon", e.getMessage()); + } finally { + wallet.close(); + } + } + + @Override + protected void testSyncSeed(Long startHeight, Long restoreHeight, boolean skipGtComparison, boolean testPostSyncNotifications) { + assertTrue(daemon.isConnected(), "Not connected to daemon"); + if (startHeight != null && restoreHeight != null) assertTrue(startHeight <= TestUtils.FIRST_RECEIVE_HEIGHT || restoreHeight <= TestUtils.FIRST_RECEIVE_HEIGHT); + // sanitize expected sync bounds + if (restoreHeight == null) restoreHeight = TestUtils.FIRST_RECEIVE_HEIGHT; + + // create wallet from seed + MoneroWalletFull wallet = createWallet(new MoneroWalletConfig().setSeed(TestUtils.SEED).setRestoreHeight(restoreHeight), false); + + + long startHeightExpected = startHeight == null ? restoreHeight : startHeight; + if (startHeightExpected == 0) startHeightExpected = TestUtils.FIRST_RECEIVE_HEIGHT; + long endHeightExpected = wallet.getDaemonMaxPeerHeight(); + + // test wallet and close as final step + MoneroWalletFull walletGt = null; + try { + + // test wallet's height before syncing + assertTrue(wallet.isConnectedToDaemon()); + assertFalse(wallet.isSynced()); + assertEquals(1, wallet.getHeight()); + assertEquals((long) restoreHeight, wallet.getRestoreHeight()); + + // register a wallet listener which tests notifications throughout the sync + WalletSyncTester walletSyncTester = new WalletSyncTester(wallet, startHeightExpected, endHeightExpected); + wallet.addListener(walletSyncTester); + + // sync the wallet with a listener which tests sync notifications + SyncProgressTester progressTester = new SyncProgressTester(wallet, startHeightExpected, endHeightExpected); + MoneroSyncResult result = wallet.sync(startHeight, progressTester); + + // test completion of the wallet and sync listeners + progressTester.onDone(wallet.getDaemonHeight()); + walletSyncTester.onDone(wallet.getDaemonHeight()); + + // test result after syncing + assertTrue(wallet.isSynced()); + assertEquals(wallet.getDaemonHeight() - 1 - startHeightExpected, (long) result.getNumBlocksFetched()); + assertTrue(result.getReceivedMoney()); + if (wallet.getHeight() != daemon.getHeight()) System.out.println("WARNING: wallet height " + wallet.getHeight() + " is not synced with daemon height " + daemon.getHeight()); // TODO: height may not be same after long sync + assertEquals(daemon.getHeight(), wallet.getDaemonHeight(), "Daemon heights are not equal: " + wallet.getDaemonHeight() + " vs " + daemon.getHeight()); + if (startHeightExpected > TestUtils.FIRST_RECEIVE_HEIGHT) assertTrue(wallet.getTxs().get(0).getHeight() > TestUtils.FIRST_RECEIVE_HEIGHT); // wallet is partially synced so first tx happens after true restore height + else assertEquals(TestUtils.FIRST_RECEIVE_HEIGHT, (long) wallet.getTxs().get(0).getHeight()); // wallet should be fully synced so first tx happens on true restore height + + // sync the wallet with default params + result = wallet.sync(); + assertTrue(wallet.isSynced()); + assertEquals(daemon.getHeight(), wallet.getHeight()); + assertTrue(result.getNumBlocksFetched() == 0 || result.getNumBlocksFetched() == 1); // block might be added to chain + assertFalse(result.getReceivedMoney()); + + // compare with ground truth + if (!skipGtComparison) { + walletGt = TestUtils.createWalletGroundTruth(TestUtils.NETWORK_TYPE, wallet.getSeed(), startHeight, restoreHeight); + testWalletEqualityOnChain(walletGt, wallet); + } + + // if testing post-sync notifications, wait for a block to be added to the chain + // then test that sync arg listener was not invoked and registered wallet listener was invoked + if (testPostSyncNotifications) { + + // start automatic syncing + wallet.startSyncing(TestUtils.SYNC_PERIOD_IN_MS); + + // attempt to start mining to push the network along // TODO: TestUtils.tryStartMining() : reqId, TestUtils.tryStopMining(reqId) + boolean startedMining = false; + try { + StartMining.startMining(); + startedMining = true; + } catch (Exception e) { + // no problem + } + + try { + + // wait for block + System.out.println("Waiting for next block to test post sync notifications"); + daemon.waitForNextBlockHeader(); + + // ensure wallet has time to detect new block + try { + TimeUnit.MILLISECONDS.sleep(TestUtils.SYNC_PERIOD_IN_MS + 3000); // sleep for wallet interval + time to sync + } catch (InterruptedException e) { + e.printStackTrace(); + throw new RuntimeException(e.getMessage()); + } + + // test that wallet listener's onSyncProgress() and onNewBlock() were invoked after previous completion + assertTrue(walletSyncTester.getOnSyncProgressAfterDone()); + assertTrue(walletSyncTester.getOnNewBlockAfterDone()); + } finally { + if (startedMining) wallet.stopMining(); + } + } + } finally { + if (walletGt != null) walletGt.close(false); + wallet.close(); + } + } + + + // Can sync a wallet created from seed from the genesis + @Test + @Override + public void testSyncSeedFromGenesis() { + assumeTrue(TEST_NON_RELAYS && !LITE_MODE); + testSyncSeed(null, null, true, false); + } + + // Can sync a wallet created from seed from a restore height + @Test + @Override + public void testSyncSeedFromRestoreHeight() { + assumeTrue(TEST_NON_RELAYS); + testSyncSeed(null, TestUtils.FIRST_RECEIVE_HEIGHT); + } + + // Can sync a wallet created from seed from a start height. + @Test + @Override + public void testSyncSeedFromStartHeight() { + assumeTrue(TEST_NON_RELAYS && !LITE_MODE); + testSyncSeed(TestUtils.FIRST_RECEIVE_HEIGHT, null, false, true); + } + + // Cannot sync a wallet created from seed from a start height less than the restore height, since start/restore height is defined by first login on lws + @Test + @Override + @Disabled + public void testSyncSeedStartHeightLTRestoreHeight() { + super.testSyncSeedStartHeightLTRestoreHeight(); + } + + // Cannot sync a wallet created from seed from a start height greater than the restore height, since start/restore height is defined by first login on lws + @Test + @Override + @Disabled + public void testSyncSeedStartHeightGTRestoreHeight() { + super.testSyncSeedStartHeightGTRestoreHeight(); + } + + // Can sync a wallet created from keys + @Test + public void testSyncWalletFromKeys() { + assumeTrue(TEST_NON_RELAYS); + + // recreate test wallet from keys + String path = getRandomWalletPath(); + MoneroWalletLight walletKeys = createWallet(new MoneroWalletConfig().setPath(path).setPrimaryAddress(wallet.getPrimaryAddress()).setPrivateViewKey(wallet.getPrivateViewKey()).setPrivateSpendKey(wallet.getPrivateSpendKey()).setRestoreHeight(TestUtils.FIRST_RECEIVE_HEIGHT), false); + + // create ground truth wallet for comparison + MoneroWalletFull walletGt = TestUtils.createWalletGroundTruth(TestUtils.NETWORK_TYPE, TestUtils.SEED, null, TestUtils.FIRST_RECEIVE_HEIGHT); + + // test wallet and close as final step + try { + assertEquals(walletKeys.getSeed(), walletGt.getSeed()); + assertEquals(walletKeys.getPrimaryAddress(), walletGt.getPrimaryAddress()); + assertEquals(walletKeys.getPrivateViewKey(), walletGt.getPrivateViewKey()); + assertEquals(walletKeys.getPublicViewKey(), walletGt.getPublicViewKey()); + assertEquals(walletKeys.getPrivateSpendKey(), walletGt.getPrivateSpendKey()); + assertEquals(walletKeys.getPublicSpendKey(), walletGt.getPublicSpendKey()); + assertEquals(TestUtils.FIRST_RECEIVE_HEIGHT, walletGt.getRestoreHeight()); + assertTrue(walletKeys.isConnectedToDaemon()); + assertFalse(walletKeys.isSynced()); + + // sync the wallet + SyncProgressTester progressTester = new SyncProgressTester(walletKeys, TestUtils.FIRST_RECEIVE_HEIGHT, walletKeys.getDaemonMaxPeerHeight()); + MoneroSyncResult result = walletKeys.sync(progressTester); + progressTester.onDone(walletKeys.getDaemonHeight()); + + // test result after syncing + assertTrue(walletKeys.isSynced()); + assertEquals(walletKeys.getDaemonHeight() - TestUtils.FIRST_RECEIVE_HEIGHT, (long) result.getNumBlocksFetched()); + assertTrue(result.getReceivedMoney()); + assertEquals(daemon.getHeight(), walletKeys.getHeight()); + assertEquals(daemon.getHeight(), walletKeys.getDaemonHeight()); + assertEquals(TestUtils.FIRST_RECEIVE_HEIGHT, (long) walletKeys.getTxs().get(0).getHeight()); // wallet should be fully synced so first tx happens on true restore height + + // compare with ground truth + testWalletEqualityOnChain(walletGt, walletKeys); + } finally { + walletGt.close(true); + walletKeys.close(); + } + + // TODO monero-project: importing key images can cause erasure of incoming transfers per wallet2.cpp:11957 which causes this test to fail +// // sync the wallets until same height +// while (wallet.getHeight() != walletKeys.getHeight()) { +// wallet.sync(); +// walletKeys.sync(new WalletSyncPrinter()); +// } +// +// List keyImages = walletKeys.exportKeyImages(); +// walletKeys.importKeyImages(keyImages); + } + + // TODO: test start syncing, notification of syncs happening, stop syncing, no notifications, etc + // Can start and stop syncing + @Test + public void testStartStopSyncing() { + assumeTrue(TEST_NON_RELAYS); + + // test unconnected wallet + String path = getRandomWalletPath(); + MoneroWalletFull wallet = createWallet(new MoneroWalletConfig().setServerUri(TestUtils.OFFLINE_SERVER_URI)); + try { + assertNotNull(wallet.getSeed()); + assertEquals(1, wallet.getHeight()); + assertEquals(BigInteger.valueOf(0), wallet.getBalance()); + wallet.startSyncing(); + } catch (MoneroError e) { + assertEquals("Wallet is not connected to daemon", e.getMessage()); + } finally { + wallet.close(); + } + + // test connecting wallet + path = getRandomWalletPath(); + wallet = createWallet(new MoneroWalletConfig().setPath(path).setServerUri(TestUtils.OFFLINE_SERVER_URI)); + try { + assertNotNull(wallet.getSeed()); + wallet.setDaemonConnection(TestUtils.getWalletLight().getDaemonConnection()); + assertEquals(1, wallet.getHeight()); + assertFalse(wallet.isSynced()); + assertEquals(BigInteger.valueOf(0), wallet.getBalance()); + //long chainHeight = wallet.getDaemonHeight(); + // wallet light doesn't support restore height if not admin + //wallet.setRestoreHeight(chainHeight - 3); + wallet.startSyncing(); + //assertEquals(chainHeight - 3, wallet.getRestoreHeight()); + assertEquals(getRpcConnection(), wallet.getDaemonConnection()); + wallet.stopSyncing(); + wallet.sync(); + wallet.stopSyncing(); + wallet.stopSyncing(); + } finally { + wallet.close(); + } + + // test that sync starts automatically + long restoreHeight = daemon.getHeight() - 100; + path = getRandomWalletPath(); + wallet = createWallet(new MoneroWalletConfig().setPath(path).setSeed(TestUtils.SEED).setRestoreHeight(restoreHeight), false); + try { + + // start syncing + assertEquals(1, wallet.getHeight()); + assertEquals(restoreHeight, wallet.getRestoreHeight()); + assertFalse(wallet.isSynced()); + assertEquals(BigInteger.valueOf(0), wallet.getBalance()); + wallet.startSyncing(TestUtils.SYNC_PERIOD_IN_MS); + + // pause for sync to start + try { + System.out.println("Sleeping to test that sync starts automatically..."); + TimeUnit.MILLISECONDS.sleep(TestUtils.SYNC_PERIOD_IN_MS + 1000); + } catch (InterruptedException e) { + e.printStackTrace(); + throw new RuntimeException(e.getMessage()); + } + + // test that wallet has started syncing + assertTrue(wallet.getHeight() > 1); + + // stop syncing + wallet.stopSyncing(); + + // TODO monero-project: wallet.cpp m_synchronized only ever set to true, never false +// // wait for block to be added to chain +// daemon.getNextBlockHeader(); +// +// // wallet is no longer synced +// assertFalse(wallet.isSynced()); + } finally { + wallet.close(); + } + } + + // Does not interfere with other wallet notifications + @Test + @Disabled + public void testWalletsDoNotInterfere() { + + // create 2 wallets with a recent restore height + long height = daemon.getHeight(); + long restoreHeight = height - 5; + MoneroWalletFull wallet1 = createWallet(new MoneroWalletConfig().setSeed(TestUtils.SEED).setRestoreHeight(restoreHeight), false); + MoneroWalletFull wallet2 = createWallet(new MoneroWalletConfig().setSeed(TestUtils.SEED).setRestoreHeight(restoreHeight), false); + + // track notifications of each wallet + SyncProgressTester tester1 = new SyncProgressTester(wallet1, restoreHeight, height); + SyncProgressTester tester2 = new SyncProgressTester(wallet1, restoreHeight, height); + wallet1.addListener(tester1); + wallet2.addListener(tester2); + + // sync first wallet and test that 2nd is not notified + wallet1.sync(); + assertTrue(tester1.isNotified()); + assertFalse(tester2.isNotified()); + + // sync 2nd wallet and test that 1st is not notified + SyncProgressTester tester3 = new SyncProgressTester(wallet1, restoreHeight, height); + wallet1.addListener(tester3); + wallet2.sync(); + assertTrue(tester2.isNotified()); + assertFalse(tester3.isNotified()); + } + + // Is equal to the RPC wallet. + @Test + @Disabled + public void testWalletEqualityRpc() { + WalletEqualityUtils.testWalletEqualityOnChain(TestUtils.getWalletRpc(), wallet); + } + + // Is equal to the RPC wallet with a seed offset + @Test + @Disabled + public void testWalletEqualityRpcWithOffset() { + + // use common offset to compare wallet implementations + String seedOffset = "my super secret offset!"; + + // create rpc wallet with offset + MoneroWalletRpc walletRpc = TestUtils.getWalletRpc(); + walletRpc.createWallet(new MoneroWalletConfig().setPath(UUID.randomUUID().toString()).setPassword(TestUtils.WALLET_PASSWORD).setSeed(walletRpc.getSeed()).setRestoreHeight(TestUtils.FIRST_RECEIVE_HEIGHT).setSeedOffset(seedOffset)); + + // create full wallet with offset + MoneroWalletFull walletFull = createWallet(new MoneroWalletConfig() + .setPath(getRandomWalletPath()) + .setPassword(TestUtils.WALLET_PASSWORD) + .setNetworkType(TestUtils.NETWORK_TYPE) + .setSeed(TestUtils.SEED) + .setServer(TestUtils.getDaemonRpc().getRpcConnection()) + .setRestoreHeight(TestUtils.FIRST_RECEIVE_HEIGHT) + .setSeedOffset(seedOffset)); + + // deep compare + try { + WalletEqualityUtils.testWalletEqualityOnChain(walletRpc, walletFull); + } finally { + walletFull.close(); + } + } + + // Can be saved + @Test + @Override + public void testSave() { + assumeTrue(TEST_NON_RELAYS); + + // create unique path for new test wallet + String path = getRandomWalletPath(); + + // wallet does not exist + assertFalse(MoneroWalletLight.walletExists(path)); + + // cannot open non-existent wallet + try { + openWallet(new MoneroWalletConfig().setPath(path).setPassword(TestUtils.WALLET_PASSWORD).setNetworkType(TestUtils.NETWORK_TYPE)); + fail("Cannot open non-existent wallet"); + } catch (MoneroError e) { + assertEquals("Wallet does not exist at path: " + path, e.getMessage()); + } + + // create wallet at the path + long restoreHeight = daemon.getHeight() - 200; + MoneroWalletFull wallet = createWallet(new MoneroWalletConfig().setPath(path).setSeed(TestUtils.SEED).setRestoreHeight(restoreHeight).setServerUri(TestUtils.OFFLINE_SERVER_URI)); + + // test wallet at newly created state + try { + + assertTrue(MoneroWalletFull.walletExists(path)); + assertEquals(TestUtils.SEED, wallet.getSeed()); + assertEquals(TestUtils.NETWORK_TYPE, wallet.getNetworkType()); + assertEquals(new MoneroRpcConnection(TestUtils.OFFLINE_SERVER_URI), wallet.getDaemonConnection()); + assertEquals(restoreHeight, wallet.getRestoreHeight()); + assertEquals("English", wallet.getSeedLanguage()); + assertEquals(1, wallet.getHeight()); + assertEquals(restoreHeight, wallet.getRestoreHeight()); + + // set the wallet's connection and sync + wallet.setDaemonConnection(getRpcConnection()); + wallet.sync(); + assertEquals(wallet.getDaemonHeight(), wallet.getHeight()); + + // close the wallet without saving + wallet.close(); + + // re-open the wallet + wallet = openWallet(new MoneroWalletConfig().setPath(path).setServerUri(TestUtils.OFFLINE_SERVER_URI)); + + // test wallet is at newly created state + assertTrue(MoneroWalletFull.walletExists(path)); + assertEquals(TestUtils.SEED, wallet.getSeed()); + assertEquals(TestUtils.NETWORK_TYPE, wallet.getNetworkType()); + assertEquals(new MoneroRpcConnection(TestUtils.OFFLINE_SERVER_URI), wallet.getDaemonConnection()); + assertFalse(wallet.isConnectedToDaemon()); + assertEquals("English", wallet.getSeedLanguage()); + assertFalse(wallet.isSynced()); + assertEquals(1, wallet.getHeight()); + assertEquals(0, wallet.getRestoreHeight()); // TODO monero-project: restoreHeight is reset to 0 after closing + + // set the wallet's connection and sync + wallet.setDaemonConnection(getRpcConnection()); + assertTrue(wallet.isConnectedToDaemon()); + wallet.setRestoreHeight(restoreHeight); + wallet.sync(); + assertTrue(wallet.isSynced()); + assertEquals(wallet.getDaemonHeight(), wallet.getHeight()); + long prevHeight = wallet.getHeight(); + + // save and close the wallet + wallet.save(); + wallet.close(); + + // re-open the wallet + wallet = openWallet(new MoneroWalletConfig().setPath(path).setServerUri(TestUtils.OFFLINE_SERVER_URI)); + + // test wallet state is saved + assertFalse(wallet.isConnectedToDaemon()); + wallet.setDaemonConnection(getRpcConnection()); // TODO monero-project: daemon connection not stored in wallet files so must be explicitly set each time + assertEquals(getRpcConnection(), wallet.getDaemonConnection()); + assertTrue(wallet.isConnectedToDaemon()); + //assertEquals(prevHeight, wallet.getHeight()); height is given by lws server, not by wallet + assertEquals(0, wallet.getRestoreHeight()); // TODO monero-project: restoreHeight is reset to 0 after closing + assertTrue(MoneroWalletFull.walletExists(path)); + assertEquals(TestUtils.SEED, wallet.getSeed()); + assertEquals(TestUtils.NETWORK_TYPE, wallet.getNetworkType()); + assertEquals("English", wallet.getSeedLanguage()); + + // sync + wallet.sync(); + } finally { + wallet.close(); + } + } + + // Can export and import wallet files + @Test + public void testGetData() { + assumeTrue(TEST_NON_RELAYS); + MoneroWalletFull wallet = null; + MoneroWalletFull wallet2 = null; + try { + + // create random wallet + wallet = MoneroWalletLight.createWallet(new MoneroWalletConfig() + .setNetworkType(MoneroNetworkType.MAINNET) + .setPassword("password123")); + + // export wallet files + byte[][] walletData = wallet.getData(); + byte[] keysData = walletData[0]; + byte[] cacheData = walletData[1]; + + // import keys file without cache + wallet2 = MoneroWalletFull.openWallet(new MoneroWalletConfig() + .setNetworkType(MoneroNetworkType.MAINNET) + .setPassword("password123") + .setKeysData(keysData)); + + // import keys and cache files + wallet2 = MoneroWalletFull.openWallet(new MoneroWalletConfig() + .setNetworkType(MoneroNetworkType.MAINNET) + .setPassword("password123") + .setKeysData(keysData) + .setCacheData(cacheData)); + + // test that wallets are equal + assertEquals(wallet2.getSeed(), wallet.getSeed()); + testWalletEqualityOnChain(wallet, wallet2); + } finally { + if (wallet != null) wallet.close(); + if (wallet2 != null) wallet2.close(); + } + } + + // Can be moved + @Test + @Override + public void testMoveTo() { + assumeTrue(TEST_NON_RELAYS); + MoneroWalletFull wallet = null; + Exception err = null; + try { + + // create random in-memory wallet with defaults + wallet = createWallet(new MoneroWalletConfig().setPath("")); + String seed = wallet.getSeed(); + wallet.setAttribute("mykey", "myval1"); + + // change password of in-memory wallet + String password2 = "abc123"; + wallet.changePassword(TestUtils.WALLET_PASSWORD, password2); + + // move wallet from memory to disk + String path1 = TestUtils.TEST_WALLETS_DIR + "/" + GenUtils.getUUID(); + assertTrue(!MoneroWalletFull.walletExists(path1)); + wallet.moveTo(path1); + assertTrue(MoneroWalletFull.walletExists(path1)); + assertEquals(seed, wallet.getSeed()); + assertEquals(wallet.getAttribute("mykey"), "myval1"); + + // move to same path which is same as saving + wallet.setAttribute("mykey", "myval2"); + wallet.moveTo(path1); + wallet.close(); + assertTrue(MoneroWalletFull.walletExists(path1)); + wallet = openWallet(new MoneroWalletConfig().setPath(path1).setPassword(password2)); + assertEquals(seed, wallet.getSeed()); + assertEquals(wallet.getAttribute("mykey"), "myval2"); + + // move wallet to new directory + String path2 = TestUtils.TEST_WALLETS_DIR + "/moved/" + GenUtils.getUUID(); + wallet.setAttribute("mykey", "myval3"); + wallet.moveTo(path2); + assertFalse(MoneroWalletFull.walletExists(path1)); + assertTrue(MoneroWalletFull.walletExists(path2)); + assertEquals(seed, wallet.getSeed()); + + // re-open and test wallet + wallet.close(); + wallet = openWallet(new MoneroWalletConfig().setPath(path2).setPassword(password2)); + wallet.sync(); + assertEquals(seed, wallet.getSeed()); + assertEquals(wallet.getAttribute("mykey"), "myval3"); + } catch (Exception e) { + err = e; + } + + // final cleanup + if (wallet != null) wallet.close(); + if (err != null) throw new RuntimeException(err); + } + + // TODO: this version assumes a wallet can be saved after creation which is not currently supported in wallet2 +// // Can save the wallet +// @Test +// public void testSave() { +// assumeTrue(TEST_NON_RELAYS); +// +// // create unique path for new test wallet +// String path = TestUtils.TEST_WALLETS_DIR + "/test_wallet_" + UUID.randomUUID().toString(); +// +// // wallet does not exist +// assertFalse(MoneroWalletFull.walletExists(path)); +// +// // cannot open non-existent wallet +// try { +// new MoneroWalletFull(path, TestUtils.WALLET_FULL_PW, TestUtils.NETWORK_TYPE); +// fail("Cannot open non-existent wallet"); +// } catch (MoneroException e) { +// assertEquals("Wallet does not exist at path: " + path, e.getMessage()); +// } +// +// // create in-memory wallet to test (no connection, english) +// MoneroWalletFull walletMemory = new MoneroWalletFull(TestUtils.NETWORK_TYPE, null, null); +// assertEquals(TestUtils.NETWORK_TYPE, walletMemory.getNetworkType()); +// assertNull(walletMemory.getDaemonConnection()); +// assertEquals("English", walletMemory.getSeedLanguage()); +// assertEquals(1, walletMemory.getHeight()); +// //assertEquals(0, walletMemory.getChainHeight()); // TODO: this causes dylib runtime_error; test default state of unconnected wallet +// //assertEquals(1, walletMemory.getRestoreHeight()); // TODO; new wallet() without connection but restoreHeight is checkpointed; where is that data cached? +// // attempt to save wallet without a path which hasn't been saved before +// try { +// walletMemory.save(); +// fail("Must specify path to save wallet because wallet has not been previously saved"); +// } catch (MoneroException e) { +// assertEquals("Must specify path to save wallet because wallet has not been previously saved", e.getMessage()); +// } +// +// // save wallet to test_wallets directory +// walletMemory.save(path, TestUtils.WALLET_FULL_PW); +// +// // read wallet saved to disk +// System.out.println("Attempting to read at path: " + path); +// MoneroWalletFull walletDisk1 = new MoneroWalletFull(path, TestUtils.WALLET_FULL_PW, TestUtils.NETWORK_TYPE); +// testJniWalletEquality(walletMemory, walletDisk1); +// +// // sync wallet which isn't connected +// try { +// walletDisk1.sync(); +// } catch (MoneroException e) { +// assertEquals("Wallet has no daemon connection", e.getMessage()); +// assertEquals(0, walletMemory.getHeight()); +// assertEquals(1, walletMemory.getRestoreHeight()); +// } +// +// // set daemon connection +// walletDisk1.setDaemonConnection(TestUtils.getDaemonRpc().getRpcConnection()); +// assertNull(walletMemory.getDaemonConnection()); +// +// // save wallet to default path +// wallet.save(); +// +// // read wallet saved to disk +// MoneroWalletFull walletDisk2 = new MoneroWalletFull(path, TestUtils.WALLET_FULL_PW, TestUtils.NETWORK_TYPE); +// testJniWalletEquality(walletDisk1, walletDisk2); +// +// // sync wallet +// long chainHeight = daemon.getHeight(); +// walletDisk2.sync(); +// assertEquals(chainHeight, walletDisk2.getHeight()); +// assertEquals(chainHeight, walletDisk2.getRestoreHeight()); +// +// // save and re-open wallet +// walletDisk2.save(); +// MoneroWalletFull walletDisk3 = new MoneroWalletFull(path, TestUtils.WALLET_FULL_PW, TestUtils.NETWORK_TYPE); +// testJniWalletEquality(walletDisk2, walletDisk3); +// +// // close wallets to release (c++) resources +// walletMemory.close(); +// walletDisk1.close(); +// walletDisk2.close(); +// walletDisk3.close(); +// } + + // Can be closed + @Test + public void testClose() { + assumeTrue(TEST_NON_RELAYS); + + // create a test wallet + String path = getRandomWalletPath(); + MoneroWalletFull wallet = createWallet(new MoneroWalletConfig().setPath(path)); + wallet.sync(); + assertTrue(wallet.getHeight() > 1); + assertTrue(wallet.isSynced()); + assertFalse(wallet.isClosed()); + + // close the wallet + wallet.close(); + assertTrue(wallet.isClosed()); + + // attempt to interact with the wallet + try { wallet.getHeight(); } + catch (MoneroError e) { assertEquals("Wallet is closed", e.getMessage()); } + try { wallet.getSeed(); } + catch (MoneroError e) { assertEquals("Wallet is closed", e.getMessage()); } + try { wallet.sync(); } + catch (MoneroError e) { assertEquals("Wallet is closed", e.getMessage()); } + try { wallet.startSyncing(); } + catch (MoneroError e) { assertEquals("Wallet is closed", e.getMessage()); } + try { wallet.stopSyncing(); } + catch (MoneroError e) { assertEquals("Wallet is closed", e.getMessage()); } + + // re-open the wallet + wallet = openWallet(new MoneroWalletConfig().setPath(path)); + wallet.sync(); + assertEquals(wallet.getDaemonHeight(), wallet.getHeight()); + assertFalse(wallet.isClosed()); + + // close the wallet + wallet.close(); + assertTrue(wallet.isClosed()); + } + + // Supports multisig sample code + @Test + @Disabled + @Override + public void testMultisigSample() { + testCreateMultisigWallet(1, 2); + testCreateMultisigWallet(1, 4); + testCreateMultisigWallet(2, 2); + testCreateMultisigWallet(2, 3); + testCreateMultisigWallet(2, 4); + } + + private void testCreateMultisigWallet(int M, int N) { + System.out.println("Creating " + M + "/" + N + " multisig wallet"); + + // create participating wallets + List wallets = new ArrayList(); + for (int i = 0; i < N; i++) { + wallets.add(createWallet(new MoneroWalletConfig())); + } + + // prepare and collect multisig hex from each participant + List preparedMultisigHexes = new ArrayList(); + for (MoneroWallet wallet : wallets) preparedMultisigHexes.add(wallet.prepareMultisig()); + + // make each wallet multisig and collect results + List madeMultisigHexes = new ArrayList(); + for (int i = 0; i < wallets.size(); i++) { + + // collect prepared multisig hexes from wallet's peers + List peerMultisigHexes = new ArrayList(); + for (int j = 0; j < wallets.size(); j++) if (j != i) peerMultisigHexes.add(preparedMultisigHexes.get(j)); + + // make wallet multisig and collect result hex + String multisigHex = wallets.get(i).makeMultisig(peerMultisigHexes, M, TestUtils.WALLET_PASSWORD); + madeMultisigHexes.add(multisigHex); + } + + // exchange multisig keys N - M + 1 times + List multisigHexes = madeMultisigHexes; + for (int i = 0; i < N - M + 1; i++) { + + // exchange multisig keys among participants and collect results for next round if applicable + List resultMultisigHexes = new ArrayList(); + for (MoneroWallet wallet : wallets) { + + // import the multisig hex of other participants and collect results + MoneroMultisigInitResult result = wallet.exchangeMultisigKeys(multisigHexes, TestUtils.WALLET_PASSWORD); + resultMultisigHexes.add(result.getMultisigHex()); + } + + // use resulting multisig hex for next round of exchange if applicable + multisigHexes = resultMultisigHexes; + } + + // wallets are now multisig + for (MoneroWallet wallet : wallets) { + String primaryAddress = wallet.getAddress(0, 0); + MoneroUtils.validateAddress(primaryAddress, TestUtils.NETWORK_TYPE); // TODO: replace with MoneroWallet.getNetworkType() when all methods defined in interface + MoneroMultisigInfo info = wallet.getMultisigInfo(); + assertTrue(info.isMultisig()); + assertTrue(info.isReady()); + assertEquals(M, (int) info.getThreshold()); + assertEquals(N, (int) info.getNumParticipants()); + wallet.close(true); + } + } + + // Does not leak memory + @Test + @Disabled + public void testMemoryLeak() { + System.out.println("Infinite loop starting, monitor memory in OS process manager..."); + System.gc(); + int i = 0; + boolean closeWallet = false; + long time = System.currentTimeMillis(); + if (closeWallet) wallet.close(true); + while (true) { + if (closeWallet) wallet = TestUtils.getWalletFull(); + wallet.sync(); + wallet.getTxs(); + wallet.getTransfers(); + wallet.getOutputs(new MoneroOutputQuery().setIsSpent(false)); + if (i % 100 == 0) { + System.out.println("Garbage collecting on iteration " + i); + System.gc(); + System.out.println((System.currentTimeMillis() - time) + " ms since last GC"); + time = System.currentTimeMillis(); + } + if (closeWallet) wallet.close(true); + i++; + } + } + + // ---------------------------------- HELPERS ------------------------------- + + public static String getRandomWalletPath() { + return TestUtils.TEST_WALLETS_DIR + "/test_wallet_" + System.currentTimeMillis(); + } + + /** + * Internal class to test progress updates. + */ + private class SyncProgressTester extends WalletSyncPrinter { + + protected MoneroWalletFull wallet; + private Long prevHeight; + private long startHeight; + private long prevEndHeight; + private Long prevCompleteHeight; + protected boolean isDone; + private Boolean onSyncProgressAfterDone; + + public SyncProgressTester(MoneroWalletFull wallet, long startHeight, long endHeight) { + this.wallet = wallet; + assertTrue(startHeight >= 0); + assertTrue(endHeight >= 0); + this.startHeight = startHeight; + this.prevEndHeight = endHeight; + this.isDone = false; + } + + @Override + public synchronized void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { + super.onSyncProgress(height, startHeight, endHeight, percentDone, message); + + // registered wallet listeners will continue to get sync notifications after the wallet's initial sync + if (isDone) { + assertTrue(wallet.getListeners().contains(this), "Listener has completed and is not registered so should not be called again"); + onSyncProgressAfterDone = true; + } + + // update tester's start height if new sync session + if (prevCompleteHeight != null && startHeight == prevCompleteHeight) this.startHeight = startHeight; + + // if sync is complete, record completion height for subsequent start heights + if (Double.compare(percentDone, 1) == 0) prevCompleteHeight = endHeight; + + // otherwise start height is equal to previous completion height + else if (prevCompleteHeight != null) assertEquals((long) prevCompleteHeight, startHeight); + + assertTrue(endHeight > startHeight, "end height > start height"); + assertEquals(this.startHeight, startHeight); + assertTrue(endHeight >= prevEndHeight); // chain can grow while syncing + prevEndHeight = endHeight; + assertTrue(height >= startHeight); + assertTrue(height < endHeight); + double expectedPercentDone = (double) (height - startHeight + 1) / (double) (endHeight - startHeight); + assertTrue(Double.compare(expectedPercentDone, percentDone) == 0); + if (prevHeight != null) assertTrue(height > prevHeight); + + prevHeight = height; + } + + public void onDone(long chainHeight) { + assertFalse(isDone); + this.isDone = true; + if (prevHeight != null) { + assertTrue(chainHeight > prevHeight); + assertEquals(chainHeight - 1, (long) prevHeight); // otherwise last height is chain height - 1 + } + else { + assertEquals(chainHeight, (long) prevCompleteHeight); + } + } + + public Boolean isNotified() { + return prevHeight != null; + } + + public Boolean getOnSyncProgressAfterDone() { + return onSyncProgressAfterDone; + } + } + + /** + * Internal class to test all wallet notifications on sync. + */ + private class WalletSyncTester extends SyncProgressTester { + + private Long walletTesterPrevHeight; // renamed from prevHeight to not interfere with super's prevHeight + private MoneroOutputWallet prevOutputReceived; + private MoneroOutputWallet prevOutputSpent; + private BigInteger incomingTotal; + private BigInteger outgoingTotal; + private Boolean onNewBlockAfterDone; + private BigInteger prevBalance; + private BigInteger prevUnlockedBalance; + + public WalletSyncTester(MoneroWalletFull wallet, long startHeight, long endHeight) { + super(wallet, startHeight, endHeight); + assertTrue(startHeight >= 0); + assertTrue(endHeight >= 0); + incomingTotal = BigInteger.valueOf(0); + outgoingTotal = BigInteger.valueOf(0); + } + + @Override + public synchronized void onNewBlock(long height) { + if (isDone) { + assertTrue(wallet.getListeners().contains(this), "Listener has completed and is not registered so should not be called again"); + onNewBlockAfterDone = true; + } + if (walletTesterPrevHeight != null) assertEquals(walletTesterPrevHeight + 1, height); + assertTrue(height >= super.startHeight); + walletTesterPrevHeight = height; + } + + @Override + public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { + if (this.prevBalance != null) assertTrue(!newBalance.equals(this.prevBalance) || !newUnlockedBalance.equals(this.prevUnlockedBalance)); + this.prevBalance = newBalance; + this.prevUnlockedBalance = newUnlockedBalance; + } + + @Override + public void onOutputReceived(MoneroOutputWallet output) { + assertNotNull(output); + prevOutputReceived = output; + + // test output + assertNotNull(output.getAmount()); + assertTrue(output.getAccountIndex() >= 0); + assertTrue(output.getSubaddressIndex() >= 0); + + // test output's tx + assertNotNull(output.getTx()); + assertNotNull(output.getTx().getHash()); + assertEquals(64, output.getTx().getHash().length()); + assertTrue(output.getTx().getVersion() >= 0); + assertTrue(output.getTx().getUnlockTime().compareTo(BigInteger.valueOf(0)) >= 0); + assertNull(output.getTx().getInputs()); + assertEquals(1, output.getTx().getOutputs().size()); + assertTrue(output.getTx().getOutputs().get(0) == output); + + // extra is not sent over the jni bridge + assertNull(output.getTx().getExtra()); + + // add incoming amount to running total + if (output.isLocked()) incomingTotal = incomingTotal.add(output.getAmount()); // TODO: only add if not unlocked, test unlocked received + } + + @Override + public void onOutputSpent(MoneroOutputWallet output) { + assertNotNull(output); + prevOutputSpent = output; + + // test output + assertNotNull(output.getAmount()); + assertTrue(output.getAccountIndex() >= 0); + if (output.getSubaddressIndex() != null) assertTrue(output.getSubaddressIndex() >= 0); // TODO (monero-project): can be undefined because inputs not provided so one created from outgoing transfer + + // test output's tx + assertNotNull(output.getTx()); + assertNotNull(output.getTx().getHash()); + assertEquals(64, output.getTx().getHash().length()); + assertTrue(output.getTx().getVersion() >= 0); + assertTrue(output.getTx().getUnlockTime().compareTo(BigInteger.valueOf(0)) >= 0); + assertEquals(1, output.getTx().getInputs().size()); + assertTrue(output.getTx().getInputs().get(0) == output); + assertNull(output.getTx().getOutputs()); + + // extra is not sent over the jni bridge + assertNull(output.getTx().getExtra()); + + // add outgoing amount to running total + if (output.isLocked()) outgoingTotal = outgoingTotal.add(output.getAmount()); + } + + @Override + public void onDone(long chainHeight) { + super.onDone(chainHeight); + //assertNotNull(walletTesterPrevHeight); + //assertNotNull(prevOutputReceived); + //assertNotNull(prevOutputSpent); + //BigInteger balance = incomingTotal.subtract(outgoingTotal); + //assertEquals(balance, wallet.getBalance()); + //assertEquals(prevBalance, wallet.getBalance()); + //assertEquals(prevUnlockedBalance, wallet.getUnlockedBalance()); + } + + public Boolean getOnNewBlockAfterDone() { + return onNewBlockAfterDone; + } + } + + // jni-specific tx tests + @Override + protected void testTxWallet(MoneroTxWallet tx, TxContext ctx) { + if (ctx == null) ctx = new TxContext(); + + // run common tests + super.testTxWallet(tx, ctx); + } + + // possible configuration: on chain xor local wallet data ("strict"), txs ordered same way? TBD + protected static void testWalletEqualityOnChain(MoneroWalletFull wallet1, MoneroWalletFull wallet2) { + WalletEqualityUtils.testWalletEqualityOnChain(wallet1, wallet2); + assertEquals(wallet1.getNetworkType(), wallet2.getNetworkType()); + // why cacheData modifies MoneroWalletFull.getRestoreHeight() ? + assertEquals(wallet1.getRestoreHeight(), wallet2.getRestoreHeight()); + assertEquals(wallet1.getDaemonConnection(), wallet2.getDaemonConnection()); + assertEquals(wallet1.getSeedLanguage(), wallet2.getSeedLanguage()); + // TODO: more jni-specific extensions + } + + // possible configuration: on chain xor local wallet data ("strict"), txs ordered same way? TBD + protected static void testWalletEqualityOnChain(MoneroWalletFull wallet1, MoneroWalletLight wallet2) { + WalletEqualityUtils.testWalletEqualityOnChain(wallet1, wallet2); + assertEquals(wallet1.getNetworkType(), wallet2.getNetworkType()); + assertEquals(wallet1.getRestoreHeight(), wallet2.getRestoreHeight()); + assertEquals(wallet1.getSeedLanguage(), wallet2.getSeedLanguage()); + // TODO: more jni-specific extensions + } + + protected static void testWalletEqualityOnChain(MoneroWalletLight wallet1, MoneroWalletLight wallet2) { + WalletEqualityUtils.testWalletEqualityOnChain(wallet1, wallet2); + assertEquals(wallet1.getNetworkType(), wallet2.getNetworkType()); + assertEquals(wallet1.getRestoreHeight(), wallet2.getRestoreHeight()); + assertEquals(wallet1.getSeedLanguage(), wallet2.getSeedLanguage()); + // TODO: more jni-specific extensions + } + + protected static void testWalletEqualityOnChain(MoneroWalletLight wallet1, MoneroWalletFull wallet2) { + WalletEqualityUtils.testWalletEqualityOnChain(wallet1, wallet2); + assertEquals(wallet1.getNetworkType(), wallet2.getNetworkType()); + assertEquals(wallet1.getRestoreHeight() + 1, wallet2.getRestoreHeight()); + assertEquals(wallet1.getSeedLanguage(), wallet2.getSeedLanguage()); + // TODO: more jni-specific extensions + } + + // -------------------- OVERRIDES TO BE DIRECTLY RUNNABLE ------------------- + + @Override + @Test + public void testCreateWalletRandom() { + super.testCreateWalletRandom(); + } + + @Override + @Test + public void testCreateWalletFromSeed() { + super.testCreateWalletFromSeed(); + } + + @Override + @Test + public void testCreateWalletFromSeedWithOffset() { + super.testCreateWalletFromSeedWithOffset(); + } + + @Override + @Test + public void testCreateWalletFromKeys() { + super.testCreateWalletFromKeys(); + } + + // Can create wallets with subaddress lookahead + @Override + @Test + public void testSubaddressLookahead() { + assumeTrue(TEST_NON_RELAYS); + Exception e1 = null; // emulating Java "finally" but compatible with other languages + MoneroWallet receiver = null; + try { + + // create wallet with high subaddress lookahead + receiver = createWallet(new MoneroWalletConfig().setAccountLookahead(1).setSubaddressLookahead(50)); + + // transfer funds to subaddress with high index // high index may not be supported by lws + wallet.createTx(new MoneroTxConfig() + .setAccountIndex(0) + .addDestination(receiver.getSubaddress(0, 10).getAddress(), TestUtils.MAX_FEE) + .setRelay(true)); + + // observe unconfirmed funds + //GenUtils.waitFor(1000); + daemon.waitForNextBlockHeader(); + + receiver.sync(); + assert(receiver.getBalance().compareTo(new BigInteger("0")) > 0); + } catch (Exception e) { + e1 = e; + } + + if (receiver != null) closeWallet(receiver); + if (e1 != null) throw new RuntimeException(e1); + } + + + @Override + @Test + public void testGetVersion() { + super.testGetVersion(); + } + + @Override + @Test + public void testGetPath() { + super.testGetPath(); + } + + @Override + @Test + public void testSetDaemonConnection() { + + // create random wallet with default daemon connection + MoneroWallet wallet = createWallet(new MoneroWalletConfig()); + assertEquals(new MoneroRpcConnection(TestUtils.WALLET_LWS_URI, TestUtils.DAEMON_RPC_USERNAME, TestUtils.DAEMON_RPC_PASSWORD), wallet.getDaemonConnection()); + assertTrue(wallet.isConnectedToDaemon()); // uses default localhost connection + + // set empty server uri + wallet.setDaemonConnection(""); + assertEquals(null, wallet.getDaemonConnection()); + assertFalse(wallet.isConnectedToDaemon()); + + // set offline server uri + wallet.setDaemonConnection(TestUtils.OFFLINE_SERVER_URI); + assertEquals(new MoneroRpcConnection(TestUtils.OFFLINE_SERVER_URI, "", ""), wallet.getDaemonConnection()); + assertFalse(wallet.isConnectedToDaemon()); + + // set daemon with wrong credentials + wallet.setDaemonConnection(TestUtils.WALLET_LWS_URI, "wronguser", "wrongpass"); + assertEquals(new MoneroRpcConnection(TestUtils.WALLET_LWS_URI, "wronguser", "wrongpass"), wallet.getDaemonConnection()); + if ("".equals(TestUtils.DAEMON_RPC_USERNAME) || TestUtils.DAEMON_RPC_USERNAME == null) assertTrue(wallet.isConnectedToDaemon()); // TODO: monerod without authentication works with bad credentials? + else assertFalse(wallet.isConnectedToDaemon()); + + // set daemon with authentication + wallet.setDaemonConnection(TestUtils.WALLET_LWS_URI, TestUtils.DAEMON_RPC_USERNAME, TestUtils.DAEMON_RPC_PASSWORD); + assertEquals(new MoneroRpcConnection(TestUtils.WALLET_LWS_URI, TestUtils.DAEMON_RPC_USERNAME, TestUtils.DAEMON_RPC_PASSWORD), wallet.getDaemonConnection()); + assertTrue(wallet.isConnectedToDaemon()); + + // nullify daemon connection + wallet.setDaemonConnection((String) null); + assertEquals(null, wallet.getDaemonConnection()); + wallet.setDaemonConnection(TestUtils.WALLET_LWS_URI); + assertEquals(new MoneroRpcConnection(TestUtils.WALLET_LWS_URI), wallet.getDaemonConnection()); + wallet.setDaemonConnection((MoneroRpcConnection) null); + assertEquals(null, wallet.getDaemonConnection()); + + // set daemon uri to non-daemon + wallet.setDaemonConnection("www.getmonero.org"); + assertEquals(new MoneroRpcConnection("www.getmonero.org"), wallet.getDaemonConnection()); + assertFalse(wallet.isConnectedToDaemon()); + + // set daemon to invalid uri + wallet.setDaemonConnection("abc123"); + assertFalse(wallet.isConnectedToDaemon()); + + // attempt to sync + try { + wallet.sync(); + fail("Exception expected"); + } catch (MoneroError e) { + assertEquals("Wallet is not connected to daemon", e.getMessage()); + } finally { + closeWallet(wallet); + } + } + + // Can use a connection manager + @Test + @Override + public void testConnectionManager() { + + // create connection manager with monerod connections + MoneroConnectionManager connectionManager = new MoneroConnectionManager(); + MoneroRpcConnection connection1 = new MoneroRpcConnection(wallet.getDaemonConnection()).setPriority(1); + MoneroRpcConnection connection2 = new MoneroRpcConnection("localhost:48081").setPriority(2); + connectionManager.setConnection(connection1); + connectionManager.addConnection(connection2); + + // create wallet with connection manager + MoneroWallet wallet = createWallet(new MoneroWalletConfig().setServerUri("").setConnectionManager(connectionManager)); + assertEquals(getRpcConnection(), wallet.getDaemonConnection()); + assertTrue(wallet.isConnectedToDaemon()); + + // set manager's connection + connectionManager.setConnection(connection2); + GenUtils.waitFor(TestUtils.AUTO_CONNECT_TIMEOUT_MS); + assertEquals(connection2, wallet.getDaemonConnection()); + + // reopen wallet with connection manager + String path = wallet.getPath(); + closeWallet(wallet); + wallet = openWallet(new MoneroWalletConfig().setServerUri("").setConnectionManager(connectionManager).setPath(path)); + assertEquals(connection2, wallet.getDaemonConnection()); + + // disconnect + connectionManager.setConnection((String) null); + assertEquals(null, wallet.getDaemonConnection()); + assertFalse(wallet.isConnectedToDaemon()); + + // start polling connections + connectionManager.startPolling(TestUtils.SYNC_PERIOD_IN_MS); + + // test that wallet auto connects + GenUtils.waitFor(TestUtils.AUTO_CONNECT_TIMEOUT_MS); + assertEquals(connection1, wallet.getDaemonConnection()); + assertTrue(wallet.isConnectedToDaemon()); + + // test override with bad connection + wallet.addListener(new MoneroWalletListener()); + connectionManager.setAutoSwitch(false); + connectionManager.setConnection("http://foo.bar.xyz"); + assertEquals("http://foo.bar.xyz", wallet.getDaemonConnection().getUri()); + assertEquals(wallet.isConnectedToDaemon(), false); + GenUtils.waitFor(5000); + assertEquals(wallet.isConnectedToDaemon(), false); + + // set to another connection manager + MoneroConnectionManager connectionManager2 = new MoneroConnectionManager(); + connectionManager2.setConnection(connection2); + wallet.setConnectionManager(connectionManager2); + assertEquals(connection2, wallet.getDaemonConnection()); + + // unset connection manager + wallet.setConnectionManager(null); + assertEquals(null, wallet.getConnectionManager()); + assertEquals(connection2, wallet.getDaemonConnection()); + + // stop polling and close + connectionManager.stopPolling(); + closeWallet(wallet); + } + + + @Override + @Test + public void testGetHeight() { + super.testGetHeight(); + } + + @Override + @Test + @Disabled + public void testGetHeightByDate() { + super.testGetHeightByDate(); + } + + @Override + @Test + public void testGetSeed() { + super.testGetSeed(); + } + + @Override + @Test + public void testGetSeedLanguages() { + super.testGetSeedLanguages(); + } + + @Override + @Test + public void testGetPrivateViewKey() { + super.testGetPrivateViewKey(); + } + + @Override + @Test + public void testGetPrivateSpendKey() { + super.testGetPrivateSpendKey(); + } + + @Override + @Test + public void testGetPublicViewKey() { + super.testGetPublicViewKey(); + } + + @Override + @Test + public void testGetPublicSpendKey() { + super.testGetPublicSpendKey(); + } + + @Override + @Test + public void testGetPrimaryAddress() { + super.testGetPrimaryAddress(); + } + + @Override + @Test + public void testGetIntegratedAddress() { + super.testGetIntegratedAddress(); + } + + @Override + @Test + public void testDecodeIntegratedAddress() { + super.testDecodeIntegratedAddress(); + } + + @Override + @Test + public void testSyncWithoutProgress() { + super.testSyncWithoutProgress(); + } + + @Override + @Test + public void testWalletEqualityGroundTruth() { + super.testWalletEqualityGroundTruth(); + } + + @Override + @Test + public void testGetAccountsWithoutSubaddresses() { + super.testGetAccountsWithoutSubaddresses(); + } + + @Override + @Test + public void testGetAccountsWithSubaddresses() { + super.testGetAccountsWithSubaddresses(); + } + + @Override + @Test + public void testGetAccount() { + super.testGetAccount(); + } + + @Override + @Test + public void testCreateAccountWithoutLabel() { + super.testCreateAccountWithoutLabel(); + } + + @Override + @Test + public void testCreateAccountWithLabel() { + super.testCreateAccountWithLabel(); + } + + @Override + @Test + public void testSetAccountLabel() { + super.testSetAccountLabel(); + } + + @Override + @Test + public void testGetSubaddresses() { + super.testGetSubaddresses(); + } + + @Override + @Test + public void testGetSubaddressesByIndices() { + super.testGetSubaddressesByIndices(); + } + + @Override + @Test + public void testGetSubaddressByIndex() { + super.testGetSubaddressByIndex(); + } + + @Override + @Test + public void testCreateSubaddress() { + super.testCreateSubaddress(); + } + + @Override + @Test + public void testSetSubaddressLabel() { + super.testSetSubaddressLabel(); + } + + @Override + @Test + public void testGetSubaddressAddress() { + super.testGetSubaddressAddress(); + } + + @Override + @Test + public void testGetAddressIndices() { + super.testGetAddressIndices(); + } + + @Override + @Test + public void testGetAllBalances() { + super.testGetAllBalances(); + } + + @Override + @Test + public void testGetTxsWallet() { + super.testGetTxsWallet(); + } + + @Override + @Test + public void testGetTxsByHash() { + super.testGetTxsByHash(); + } + + @Override + @Test + public void testGetTxsWithQuery() { + super.testGetTxsWithQuery(); + } + + @Override + @Test + public void testGetTxsByHeight() { + super.testGetTxsByHeight(); + } + + @Override + @Test + public void testGetTxsWithPaymentIds() { + /* LITE_MODE enabled */ + super.testGetTxsWithPaymentIds(); + } + + @Override + @Test + public void testGetTxsFieldsWithFiltering() { + super.testGetTxsFieldsWithFiltering(); + } + + @Override + @Test + public void testValidateInputsGetTxs() { + super.testValidateInputsGetTxs(); + } + + @Override + @Test + public void testGetTransfers() { + super.testGetTransfers(); + } + + @Override + @Test + public void testGetTransfersWithQuery() { + super.testGetTransfersWithQuery(); + } + + @Override + @Test + public void testValidateInputsGetTransfers() { + super.testValidateInputsGetTransfers(); + } + + @Override + @Test + public void testGetIncomingOutgoingTransfers() { + super.testGetIncomingOutgoingTransfers(); + } + + @Override + @Test + public void testGetOutputs() { + super.testGetOutputs(); + } + + @Override + @Test + public void testGetOutputsWithQuery() { + super.testGetOutputsWithQuery(); + } + + @Override + @Test + public void testValidateInputsGetOutputs() { + super.testValidateInputsGetOutputs(); + } + + @Override + @Test + public void testAccounting() { + super.testAccounting(); + } + + // Light wallet doesn't support check tx key + @Override + @Test + @Disabled + public void testCheckTxKey() { + super.testCheckTxKey(); + } + + // Light wallet doesn't support check tx proof + @Override + @Test + @Disabled + public void testCheckTxProof() { + super.testCheckTxProof(); + } + + // Light wallet doesn't support check spend proof + @Override + @Test + @Disabled + public void testCheckSpendProof() { + super.testCheckSpendProof(); + } + + // TODO monero-project: still no way to get rpc_version from lws + @Override + @Test + @Disabled + public void testGetReserveProofWallet() { + super.testGetReserveProofWallet(); + } + + // TODO monero-project: still no way to get rpc_version from lws + @Override + @Test + @Disabled + public void testGetReserveProofAccount() { + super.testGetReserveProofAccount(); + } + + @Override + @Test + public void testSetTxNote() { + super.testSetTxNote(); + } + + @Override + @Test + public void testSetTxNotes() { + super.testSetTxNotes(); + } + + @Override + @Test + public void testExportOutputs() { + super.testExportOutputs(); + } + + @Override + @Test + public void testImportOutputs() { + super.testImportOutputs(); + } + + @Override + @Test + public void testExportKeyImages() { + super.testExportKeyImages(); + } + + @Override + @Test + @Disabled + public void testGetNewKeyImagesFromLastImport() { + super.testGetNewKeyImagesFromLastImport(); + } + + // Supports view-only and offline wallets to create, sign, and submit transactions + @SuppressWarnings("unused") + @Test + @Override + public void testViewOnlyAndOfflineWallets() { + assumeTrue(!LITE_MODE && (TEST_NON_RELAYS || TEST_RELAYS)); + + // create view-only and offline wallets + MoneroWallet viewOnlyWallet = createWallet(new MoneroWalletConfig().setPrimaryAddress(wallet.getPrimaryAddress()).setPrivateViewKey(wallet.getPrivateViewKey()).setRestoreHeight(TestUtils.FIRST_RECEIVE_HEIGHT)); + MoneroWallet offlineWallet = createWalletFull(new MoneroWalletConfig().setPrimaryAddress(wallet.getPrimaryAddress()).setPrivateViewKey(wallet.getPrivateViewKey()).setPrivateSpendKey(wallet.getPrivateSpendKey()).setServerUri(TestUtils.OFFLINE_SERVER_URI).setRestoreHeight(0l)); + assertFalse(offlineWallet.isConnectedToDaemon()); + viewOnlyWallet.sync(); + + // test tx signing with wallets + try { + testViewOnlyAndOfflineWallets(viewOnlyWallet, offlineWallet); + } finally { + closeWallet(viewOnlyWallet); + closeWallet(offlineWallet); + } + } + + @Override + @Test + public void testSignAndVerifyMessages() { + super.testSignAndVerifyMessages(); + } + + @Override + @Test + public void testAddressBook() { + super.testAddressBook(); + } + + @Override + @Test + public void testSetAttributes() { + super.testSetAttributes(); + } + + @Override + @Test + public void testGetPaymentUri() { + super.testGetPaymentUri(); + } + + // Light wallet has no mining support + @Override + @Test + @Disabled + public void testMining() { + super.testMining(); + } + + @Override + @Test + public void testValidateInputsSendingFunds() { + super.testValidateInputsSendingFunds(); + } + + // Light wallet cannot be aware of txs in pool + @Override + @Test + @Disabled + public void testSyncWithPoolSameAccounts() { + super.testSyncWithPoolSameAccounts(); + } + + // Light wallet cannot be aware of txs in pool + @Override + @Test + @Disabled + public void testSyncWithPoolSubmitAndDiscard() { + super.testSyncWithPoolSubmitAndDiscard(); + } + + // Light wallet cannot be aware of txs in pool + @Override + @Test + @Disabled + public void testSyncWithPoolSubmitAndRelay() { + super.testSyncWithPoolSubmitAndRelay(); + } + + // Light wallet cannot be aware of txs in pool + @Override + @Test + @Disabled + public void testSyncWithPoolRelay() { + super.testSyncWithPoolRelay(); + } + + @Override + @Test + public void testSendToSelf() { + super.testSendToSelf(); + } + +// Can send to an external address + @Override + @Test + public void testSendToExternal() { + assumeTrue(TEST_RELAYS); + MoneroWalletLight recipient = null; + try { + + // wait for txs to confirm and for sufficient unlocked balance + TestUtils.WALLET_TX_TRACKER.waitForWalletTxsToClearPool(wallet); + BigInteger amount = TestUtils.MAX_FEE.multiply(BigInteger.valueOf(3)); + TestUtils.WALLET_TX_TRACKER.waitForUnlockedBalance(wallet, 0, null, amount); + + // create recipient wallet + recipient = createWallet(new MoneroWalletConfig()); + + // collect sender balances before + BigInteger balance1 = wallet.getBalance(); + BigInteger unlockedBalance1 = wallet.getUnlockedBalance(); + + // send funds to recipient + MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig() + .setAccountIndex(0) + .setAddress(wallet.getIntegratedAddress(recipient.getPrimaryAddress(), "54491f3bb3572a37").getIntegratedAddress()) + .setAmount(amount) + .setRelay(true)); + + // test sender balances after + BigInteger balance2 = wallet.getBalance(); + BigInteger unlockedBalance2 = wallet.getUnlockedBalance(); + assertTrue(unlockedBalance2.compareTo(unlockedBalance1) < 0); // unlocked balance should decrease + BigInteger expectedBalance = balance1.subtract(tx.getOutgoingAmount()).subtract(tx.getFee()); + assertEquals(expectedBalance, balance2, "Balance after send was not balance before - net tx amount - fee (5 - 1 != 4 test)"); + + // test recipient balance after + // lws may not support unconfirmed txs, so it's better to wait for block confirmation + daemon.waitForNextBlockHeader(); + + recipient.sync(); + + assertTrue(recipient.isSynced()); + //assertFalse(wallet.getTxs(new MoneroTxQuery().setIsConfirmed(false)).isEmpty()); + assertFalse(wallet.getTxs(new MoneroTxQuery().setIsConfirmed(true)).isEmpty()); + + assertEquals(amount, recipient.getBalance()); + } finally { + if (recipient != null && !recipient.isClosed()) closeWallet(recipient); + } + } + + @Override + @Test + public void testSendFromSubaddresses() { + super.testSendFromSubaddresses(); + } + + @Override + @Test + public void testSendFromSubaddressesSplit() { + super.testSendFromSubaddressesSplit(); + } + + @Override + @Test + public void testSend() { + super.testSend(); + } + + @Override + @Test + public void testSendWithPaymentId() { + super.testSendWithPaymentId(); + } + + @Override + @Test + public void testSendSplit() { + super.testSendSplit(); + } + + @Override + @Test + public void testCreateThenRelay() { + super.testCreateThenRelay(); + } + + @Override + @Test + public void testCreateThenRelaySplit() { + super.testCreateThenRelaySplit(); + } + + @Override + @Test + public void testSendToMultiple() { + super.testSendToMultiple(); + } + + @Override + @Test + public void testSendToMultipleSplit() { + super.testSendToMultipleSplit(); + } + + @Override + @Test + public void testSendDustToMultipleSplit() { + super.testSendDustToMultipleSplit(); + } + + @Override + @Test + @Disabled + public void testSubtractFeeFrom() { + super.testSubtractFeeFrom(); + } + + @Override + @Test + @Disabled + public void testSubtractFeeFromSplit() { + super.testSubtractFeeFromSplit(); + } + + @Override + @Test + @Disabled + public void testUpdateLockedSameAccount() { + super.testUpdateLockedSameAccount(); + } + + @Override + @Test + @Disabled + public void testUpdateLockedSameAccountSplit() { + super.testUpdateLockedSameAccountSplit(); + } + + @Override + @Test + @Disabled + public void testUpdateLockedDifferentAccounts() { + super.testUpdateLockedDifferentAccounts(); + } + + @Override + @Test + @Disabled + public void testUpdateLockedDifferentAccountsSplit() { + super.testUpdateLockedDifferentAccountsSplit(); + } + + @Override + @Test + public void testSweepOutputs() { + super.testSweepOutputs(); + } + + @Override + @Test + public void testSweepSubaddresses() { + super.testSweepSubaddresses(); + } + + @Override + @Test + public void testSweepAccounts() { + super.testSweepAccounts(); + } + + @Override + @Test + public void testSweepWalletByAccounts() { + super.testSweepWalletByAccounts(); + } + + @Override + @Test + public void testSweepWalletBySubaddresses() { + super.testSweepWalletBySubaddresses(); + } + + @Override + @Test + public void testSweepDustNoRelay() { + super.testSweepDustNoRelay(); + } + + @Override + @Test + public void testSweepDust() { + super.testSweepDust(); + } + + // Light wallet doesn't support scan txs + @Override + @Test + @Disabled + public void testScanTxs() { + super.testScanTxs(); + } + + @Override + @Test + @Disabled + public void testRescanBlockchain() { + super.testRescanBlockchain(); + } + + // Light wallet doesn't support multisign + @Override + @Test + @Disabled + public void testMultisig() { + super.testMultisig(); + } + + // Light wallet doesn't support multisign + + @Override + @Test + @Disabled + public void testMultisigStress() { + super.testMultisigStress(); + } + + @Override + @Test + public void testChangePassword() { + super.testChangePassword(); + } + + @Override + @Test + public void testSaveAndClose() { + super.testSaveAndClose(); + } + + @Override + @Test + @Tag("NotificationTest") + @Disabled + public void testNotificationsDifferentWallet() { + super.testNotificationsDifferentWallet(); + } + + @Override + @Test + @Tag("NotificationTest") + @Disabled + public void testNotificationsDifferentWalletWhenRelayed() { + super.testNotificationsDifferentWalletWhenRelayed(); + } + + @Override + @Test + @Tag("NotificationTest") + @Disabled + public void testNotificationsDifferentAccounts() { + super.testNotificationsDifferentAccounts(); + } + + @Override + @Test + @Tag("NotificationTest") + @Disabled + public void testNotificationsSameAccount() { + super.testNotificationsSameAccount(); + } + + @Override + @Test + @Tag("NotificationTest") + @Disabled + public void testNotificationsDifferentAccountSweepOutput() { + super.testNotificationsDifferentAccountSweepOutput(); + } + + @Override + @Test + @Tag("NotificationTest") + @Disabled + public void testNotificationsSameAccountSweepOutputWhenRelayed() { + super.testNotificationsSameAccountSweepOutputWhenRelayed(); + } + + @Override + @Test + public void testStopListening() { + super.testStopListening(); + } + + // Can be created and receive funds + @Override + @Test + public void testCreateAndReceive() { + assumeTrue(TEST_NOTIFICATIONS); + + // create random wallet + MoneroWallet receiver = createWallet(new MoneroWalletConfig()); + try { + + // listen for received outputs + WalletNotificationCollector myListener = new WalletNotificationCollector(); + receiver.addListener(myListener); + + // wait for txs to confirm and for sufficient unlocked balance + TestUtils.WALLET_TX_TRACKER.waitForWalletTxsToClearPool(wallet); + TestUtils.WALLET_TX_TRACKER.waitForUnlockedBalance(wallet, 0, null, TestUtils.MAX_FEE); + + // send funds to the receiver + MoneroTxWallet sentTx = wallet.createTx(new MoneroTxConfig().setAccountIndex(0).setAddress(receiver.getPrimaryAddress()).setAmount(TestUtils.MAX_FEE).setRelay(true)); + + // wait for funds to confirm + try { StartMining.startMining(); } catch (Exception e) { } + while (!(wallet.getTx(sentTx.getHash())).isConfirmed()) { + if (wallet.getTx(sentTx.getHash()).isFailed()) throw new Error("Tx failed in mempool: " + sentTx.getHash()); + daemon.waitForNextBlockHeader(); + } + + // receiver should have notified listeners of received outputs + try { TimeUnit.MILLISECONDS.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } // zmq notifications received within 1 second + // wallet2 doesn't notify outputs received when it is light mode + //assertFalse(myListener.getOutputsReceived().isEmpty()); + } finally { + closeWallet(receiver); + try { daemon.stopMining(); } catch (Exception e) { } + } + } + + @Override + @Test + public void testFreezeOutputs() { + super.testFreezeOutputs(); + } + + @Override + @Test + @Disabled + public void testInputKeyImages() { + super.testInputKeyImages(); + } + + // Light wallet cannot prove unrelayed txs + @Override + @Test + @Disabled + public void testProveUnrelayedTxs() { + super.testProveUnrelayedTxs(); + } + +} diff --git a/src/test/java/utils/TestUtils.java b/src/test/java/utils/TestUtils.java index c17e6a6e..5da6f067 100644 --- a/src/test/java/utils/TestUtils.java +++ b/src/test/java/utils/TestUtils.java @@ -21,6 +21,7 @@ import monero.daemon.MoneroDaemonRpc; import monero.daemon.model.MoneroNetworkType; import monero.wallet.MoneroWalletFull; +import monero.wallet.MoneroWalletLight; import monero.wallet.MoneroWalletRpc; import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroWalletConfig; @@ -67,18 +68,27 @@ public class TestUtils { public static final String WALLET_RPC_LOCAL_WALLET_DIR = MONERO_BINS_DIR; public static final String WALLET_RPC_ACCESS_CONTROL_ORIGINS = "http://localhost:8080"; // cors access from web browser + // monero wallet lws configuration (change per your configuration) + public static final int WALLET_LWS_PORT_START = 8443; + public static final int WALLET_LWS_ADMIN_PORT_START = 8444; + public static final String WALLET_LWS_DOMAIN = "localhost"; + public static final String WALLET_LWS_ADMIN_DOMAIN = "localhost"; + public static final String WALLET_LWS_URI = WALLET_LWS_DOMAIN + ":" + WALLET_LWS_PORT_START; + public static final String WALLET_LWS_ADMIN_URI = WALLET_LWS_ADMIN_DOMAIN + ":" + WALLET_LWS_ADMIN_PORT_START; + // test wallet config public static final String WALLET_NAME = "test_wallet_1"; public static final String WALLET_PASSWORD = "supersecretpassword123"; public static final String TEST_WALLETS_DIR = "./test_wallets"; public static final String WALLET_FULL_PATH = TEST_WALLETS_DIR + "/" + WALLET_NAME; - + public static final String WALLET_LIGHT_PATH = TEST_WALLETS_DIR + "/test_wallet_light"; // test wallet constants public static final BigInteger MAX_FEE = BigInteger.valueOf(7500000).multiply(BigInteger.valueOf(10000)); public static final MoneroNetworkType NETWORK_TYPE = MoneroNetworkType.TESTNET; public static final String LANGUAGE = "English"; public static final String SEED = "silk mocked cucumber lettuce hope adrenalin aching lush roles fuel revamp baptism wrist long tender teardrop midst pastry pigment equip frying inbound pinched ravine frying"; public static final String ADDRESS = "A1y9sbVt8nqhZAVm3me1U18rUVXcjeNKuBd1oE2cTs8biA9cozPMeyYLhe77nPv12JA3ejJN3qprmREriit2fi6tJDi99RR"; + public static final String PRIVATE_VIEW_KEY = "198820da9166ee114203eb38c29e00b0e8fc7df508aa632d56ead849093d3808"; public static final long FIRST_RECEIVE_HEIGHT = 171; // NOTE: this value must be the height of the wallet's first tx for tests public static final long SYNC_PERIOD_IN_MS = 5000; // period between wallet syncs in milliseconds public static final String OFFLINE_SERVER_URI = "offline_server_uri"; // dummy server uri to remain offline because wallet2 connects to default if not given @@ -240,6 +250,47 @@ public static MoneroWalletFull getWalletFull() { return walletFull; } + public static MoneroWalletConfig getWalletLightConfig() { + MoneroWalletConfig config = new MoneroWalletConfig(); + return config.setRestoreHeight(TestUtils.FIRST_RECEIVE_HEIGHT).setNetworkType(MoneroNetworkType.TESTNET).setServerUri(TestUtils.WALLET_LWS_URI).setSeed(TestUtils.SEED); + } + + private static MoneroWalletLight walletLight; + public static MoneroWalletLight getWalletLight() { + + if (walletLight == null || walletLight.isClosed()) { + // create wallet from seed if it doesn't exist + if (!MoneroWalletLight.walletExists(WALLET_LIGHT_PATH)) { + + // create directory for test wallets if it doesn't exist + File testWalletsDir = new File(TestUtils.TEST_WALLETS_DIR); + if (!testWalletsDir.exists()) testWalletsDir.mkdirs(); + + // create wallet with connection + MoneroRpcConnection daemonConnection = new MoneroRpcConnection(WALLET_LWS_URI, "", ""); + walletLight = MoneroWalletLight.createWallet(new MoneroWalletConfig().setPath(TestUtils.WALLET_LIGHT_PATH).setPassword(TestUtils.WALLET_PASSWORD).setNetworkType(NETWORK_TYPE).setSeed(TestUtils.SEED).setServer(daemonConnection).setRestoreHeight(FIRST_RECEIVE_HEIGHT)); + assertEquals(TestUtils.FIRST_RECEIVE_HEIGHT, walletLight.getRestoreHeight()); + assertEquals(daemonConnection, walletLight.getDaemonConnection()); + } + // otherwise open existing wallet and update daemon connection + else { + walletLight = MoneroWalletLight.openWallet(WALLET_LIGHT_PATH, WALLET_PASSWORD, TestUtils.NETWORK_TYPE); + walletLight.setDaemonConnection(new MoneroRpcConnection(WALLET_LWS_URI, "", "")); + } + } + + assertEquals(TestUtils.PRIVATE_VIEW_KEY, walletLight.getPrivateViewKey()); + assertEquals(TestUtils.ADDRESS, walletLight.getPrimaryAddress()); + assertEquals(TestUtils.SEED, walletLight.getSeed()); + + // sync and save wallet + walletLight.sync(new WalletSyncPrinter()); + walletLight.save(); + walletLight.startSyncing(TestUtils.SYNC_PERIOD_IN_MS); + + return walletLight; + } + /** * Creates a new wallet considered to be "ground truth". *