mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-05-12 21:20:42 +01:00
404 lines
12 KiB
C++
404 lines
12 KiB
C++
#include "wallet.h"
|
|
|
|
#include <cstring>
|
|
#include <chrono>
|
|
|
|
#include <boost/iostreams/device/file_descriptor.hpp>
|
|
#include <boost/iostreams/stream.hpp>
|
|
|
|
#include "common.h"
|
|
#include "jni_cache.h"
|
|
#include "eraser.h"
|
|
#include "fd.h"
|
|
|
|
namespace io = boost::iostreams;
|
|
|
|
namespace monero {
|
|
|
|
using namespace std::chrono_literals;
|
|
|
|
static_assert(COIN == 1e12, "Monero atomic unit must be 1e-12 XMR");
|
|
static_assert(CRYPTONOTE_MAX_BLOCK_NUMBER == 500000000,
|
|
"Min timestamp must be higher than max block height");
|
|
|
|
Wallet::Wallet(
|
|
JNIEnv* env,
|
|
int network_id,
|
|
const JvmRef<jobject>& wallet_native)
|
|
: m_wallet(static_cast<cryptonote::network_type>(network_id),
|
|
0, /* kdf_rounds */
|
|
true, /* unattended */
|
|
std::make_unique<RemoteNodeClientFactory>(env, wallet_native)),
|
|
m_callback(env, wallet_native),
|
|
m_account_ready(false),
|
|
m_blockchain_height(1),
|
|
m_restore_height(0),
|
|
m_refresh_running(false),
|
|
m_refresh_canceled(false) {
|
|
// Use a bogus ipv6 address as a placeholder for the daemon address.
|
|
LOG_FATAL_IF(!m_wallet.init("[100::/64]", {}, {}, 0, false),
|
|
"Init failed");
|
|
m_wallet.stop();
|
|
m_wallet.callback(this);
|
|
}
|
|
|
|
// Generate keypairs deterministically. Account creation time will be set
|
|
// to Monero epoch.
|
|
void generateAccountKeys(cryptonote::account_base& account,
|
|
const std::vector<char>& secret_scalar) {
|
|
crypto::secret_key secret_key;
|
|
LOG_FATAL_IF(secret_scalar.size() != sizeof(secret_key.data),
|
|
"Secret key size mismatch");
|
|
std::copy(secret_scalar.begin(), secret_scalar.end(), secret_key.data);
|
|
crypto::secret_key gen = account.generate(secret_key, true, false);
|
|
LOG_FATAL_IF(gen != secret_key);
|
|
}
|
|
|
|
void Wallet::restoreAccount(const std::vector<char>& secret_scalar, uint64_t restore_point) {
|
|
LOG_FATAL_IF(m_account_ready, "Account should not be reinitialized");
|
|
std::lock_guard<std::mutex> lock(m_wallet_mutex);
|
|
auto& account = m_wallet.get_account();
|
|
generateAccountKeys(account, secret_scalar);
|
|
if (restore_point < CRYPTONOTE_MAX_BLOCK_NUMBER) {
|
|
m_restore_height = restore_point;
|
|
} else {
|
|
if (restore_point > account.get_createtime()) {
|
|
account.set_createtime(restore_point);
|
|
}
|
|
m_restore_height = estimateRestoreHeight(account.get_createtime());
|
|
}
|
|
m_wallet.rescan_blockchain(true, false, false);
|
|
m_account_ready = true;
|
|
}
|
|
|
|
uint64_t Wallet::estimateRestoreHeight(uint64_t timestamp) {
|
|
// Apply -1 month adjustment for fluctuations in block time, just like
|
|
// estimate_blockchain_height() does when node's height is unavailable.
|
|
const int secs_per_month = 60 * 60 * 24 * 30;
|
|
LOG_FATAL_IF(secs_per_month > timestamp);
|
|
return m_wallet.get_approximate_blockchain_height(timestamp - secs_per_month);
|
|
}
|
|
|
|
bool Wallet::parseFrom(std::istream& input) {
|
|
LOG_FATAL_IF(m_account_ready, "Account should not be reinitialized");
|
|
std::ostringstream ss;
|
|
ss << input.rdbuf();
|
|
const std::string buf = ss.str();
|
|
binary_archive<false> ar{epee::strspan<std::uint8_t>(buf)};
|
|
std::lock_guard<std::mutex> lock(m_wallet_mutex);
|
|
if (!serialization::serialize_noeof(ar, *this))
|
|
return false;
|
|
if (!serialization::serialize_noeof(ar, m_wallet.get_account()))
|
|
return false;
|
|
if (!serialization::serialize(ar, m_wallet))
|
|
return false;
|
|
m_blockchain_height = m_wallet.get_blockchain_current_height();
|
|
m_wallet.get_transfers(m_tx_outs);
|
|
m_account_ready = true;
|
|
return true;
|
|
}
|
|
|
|
bool Wallet::writeTo(std::ostream& output) {
|
|
return suspendRefreshAndRunLocked([&]() -> bool {
|
|
binary_archive<true> ar(output);
|
|
if (!serialization::serialize_noeof(ar, *this))
|
|
return false;
|
|
if (!serialization::serialize_noeof(ar, require_account()))
|
|
return false;
|
|
if (!serialization::serialize(ar, m_wallet))
|
|
return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
template<typename Callback>
|
|
void Wallet::getOwnedTxOuts(Callback callback) {
|
|
std::lock_guard<std::mutex> lock(m_tx_outs_mutex);
|
|
callback(m_tx_outs);
|
|
}
|
|
|
|
std::string Wallet::public_address() const {
|
|
auto account = const_cast<Wallet*>(this)->require_account();
|
|
return account.get_public_address_str(m_wallet.nettype());
|
|
}
|
|
|
|
cryptonote::account_base& Wallet::require_account() {
|
|
LOG_FATAL_IF(!m_account_ready, "Account is not initialized");
|
|
return m_wallet.get_account();
|
|
}
|
|
|
|
// Reading m_transfers from wallet2 is not guarded by any lock; call this function only
|
|
// from wallet2's callback thread.
|
|
void Wallet::handleBalanceChanged(uint64_t at_block_height) {
|
|
LOGV("handleBalanceChanged(%lu)", at_block_height);
|
|
m_tx_outs_mutex.lock();
|
|
m_wallet.get_transfers(m_tx_outs);
|
|
m_tx_outs_mutex.unlock();
|
|
m_blockchain_height = at_block_height;
|
|
callOnRefresh(true);
|
|
}
|
|
|
|
void Wallet::handleNewBlock(uint64_t height) {
|
|
m_blockchain_height = height;
|
|
// Notify the blockchain height once every 200 ms if the height is a multiple of 100.
|
|
bool debounce = true;
|
|
if (height % 100 == 0) {
|
|
static std::chrono::steady_clock::time_point last_time;
|
|
auto now = std::chrono::steady_clock::now();
|
|
if (now - last_time >= 200.ms) {
|
|
last_time = now;
|
|
debounce = false;
|
|
}
|
|
}
|
|
if (!debounce) {
|
|
callOnRefresh(false);
|
|
}
|
|
}
|
|
|
|
void Wallet::callOnRefresh(bool balance_changed) {
|
|
m_callback.callVoidMethod(getJniEnv(), WalletNative_onRefresh, m_blockchain_height, balance_changed);
|
|
}
|
|
|
|
Wallet::Status Wallet::nonReentrantRefresh(bool skip_coinbase) {
|
|
LOG_FATAL_IF(m_refresh_running.exchange(true),
|
|
"Refresh should not be called concurrently");
|
|
Status ret;
|
|
std::unique_lock<std::mutex> wallet_lock(m_wallet_mutex);
|
|
m_wallet.set_refresh_type(skip_coinbase ? tools::wallet2::RefreshType::RefreshNoCoinbase
|
|
: tools::wallet2::RefreshType::RefreshDefault);
|
|
while (!m_refresh_canceled) {
|
|
m_wallet.set_refresh_from_block_height(m_restore_height);
|
|
try {
|
|
// refresh() will block until stop() is called or it syncs successfully.
|
|
m_wallet.refresh(false);
|
|
if (!m_wallet.stopped()) {
|
|
m_wallet.stop();
|
|
ret = Status::OK;
|
|
break;
|
|
}
|
|
} catch (const tools::error::no_connection_to_daemon&) {
|
|
ret = Status::NO_NETWORK_CONNECTIVITY;
|
|
break;
|
|
} catch (const tools::error::refresh_error&) {
|
|
ret = Status::REFRESH_ERROR;
|
|
break;
|
|
}
|
|
m_refresh_cond.wait(wallet_lock);
|
|
}
|
|
if (m_refresh_canceled) {
|
|
m_refresh_canceled = false;
|
|
ret = Status::INTERRUPTED;
|
|
}
|
|
m_refresh_running.store(false);
|
|
m_blockchain_height = m_wallet.get_blockchain_current_height();
|
|
// Always notify the last block height.
|
|
callOnRefresh(false);
|
|
return ret;
|
|
}
|
|
|
|
template<typename T>
|
|
auto Wallet::suspendRefreshAndRunLocked(T block) -> decltype(block()) {
|
|
std::unique_lock<std::mutex> wallet_lock(m_wallet_mutex, std::try_to_lock);
|
|
if (!wallet_lock.owns_lock()) {
|
|
JNIEnv* env = getJniEnv();
|
|
for (;;) {
|
|
if (!m_wallet.stopped()) {
|
|
m_wallet.stop();
|
|
m_callback.callVoidMethod(env, WalletNative_onSuspendRefresh, true);
|
|
}
|
|
if (wallet_lock.try_lock()) {
|
|
break;
|
|
}
|
|
std::this_thread::yield();
|
|
}
|
|
m_callback.callVoidMethod(env, WalletNative_onSuspendRefresh, false);
|
|
m_refresh_cond.notify_one();
|
|
}
|
|
return block();
|
|
}
|
|
|
|
void Wallet::cancelRefresh() {
|
|
suspendRefreshAndRunLocked([&]() {
|
|
m_refresh_canceled = true;
|
|
});
|
|
}
|
|
|
|
void Wallet::setRefreshSince(long height_or_timestamp) {
|
|
suspendRefreshAndRunLocked([&]() {
|
|
if (height_or_timestamp < CRYPTONOTE_MAX_BLOCK_NUMBER) {
|
|
m_restore_height = height_or_timestamp;
|
|
} else {
|
|
LOG_FATAL("TODO");
|
|
}
|
|
});
|
|
}
|
|
|
|
extern "C"
|
|
JNIEXPORT jlong JNICALL
|
|
Java_im_molly_monero_WalletNative_nativeCreate(
|
|
JNIEnv* env,
|
|
jobject thiz,
|
|
jint network_id) {
|
|
auto wallet = new Wallet(env, network_id, JvmParamRef<jobject>(thiz));
|
|
return nativeToJvmPointer(wallet);
|
|
}
|
|
|
|
extern "C"
|
|
JNIEXPORT void JNICALL
|
|
Java_im_molly_monero_WalletNative_nativeDispose(
|
|
JNIEnv* env,
|
|
jobject thiz,
|
|
jlong handle) {
|
|
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
|
free(wallet);
|
|
}
|
|
|
|
extern "C"
|
|
JNIEXPORT void JNICALL
|
|
Java_im_molly_monero_WalletNative_nativeRestoreAccount(
|
|
JNIEnv* env,
|
|
jobject thiz,
|
|
jlong handle,
|
|
jbyteArray p_secret_scalar,
|
|
jlong restore_point) {
|
|
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
|
std::vector<char> secret_scalar = jvmToNativeByteArray(
|
|
env, JvmParamRef<jbyteArray>(p_secret_scalar));
|
|
Eraser secret_eraser(secret_scalar);
|
|
wallet->restoreAccount(secret_scalar, restore_point);
|
|
}
|
|
|
|
extern "C"
|
|
JNIEXPORT jboolean JNICALL
|
|
Java_im_molly_monero_WalletNative_nativeLoad(
|
|
JNIEnv* env,
|
|
jobject thiz,
|
|
jlong handle,
|
|
jint fd) {
|
|
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
|
io::stream<io::file_descriptor_source> in_stream(fd, io::never_close_handle);
|
|
return wallet->parseFrom(in_stream);
|
|
}
|
|
|
|
extern "C"
|
|
JNIEXPORT jboolean JNICALL
|
|
Java_im_molly_monero_WalletNative_nativeSave(
|
|
JNIEnv* env,
|
|
jobject thiz,
|
|
jlong handle, jint fd) {
|
|
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
|
io::stream<io::file_descriptor_sink> out_stream(fd, io::never_close_handle);
|
|
return wallet->writeTo(out_stream);
|
|
}
|
|
|
|
extern "C"
|
|
JNIEXPORT jint JNICALL
|
|
Java_im_molly_monero_WalletNative_nativeNonReentrantRefresh
|
|
(JNIEnv* env,
|
|
jobject thiz,
|
|
jlong handle,
|
|
jboolean skip_coinbase) {
|
|
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
|
return wallet->nonReentrantRefresh(skip_coinbase);
|
|
}
|
|
|
|
extern "C"
|
|
JNIEXPORT void JNICALL
|
|
Java_im_molly_monero_WalletNative_nativeCancelRefresh(
|
|
JNIEnv* env,
|
|
jobject thiz,
|
|
jlong handle) {
|
|
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
|
wallet->cancelRefresh();
|
|
}
|
|
|
|
extern "C"
|
|
JNIEXPORT void JNICALL
|
|
Java_im_molly_monero_WalletNative_nativeSetRefreshSince(
|
|
JNIEnv* env,
|
|
jobject thiz,
|
|
jlong handle,
|
|
jlong height_or_timestamp) {
|
|
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
|
wallet->setRefreshSince(height_or_timestamp);
|
|
}
|
|
|
|
//extern "C"
|
|
//JNIEXPORT jbyteArray JNICALL
|
|
//Java_im_molly_monero_Wallet_nativeGetViewPublicKey(
|
|
// JNIEnv* env,
|
|
// jobject thiz,
|
|
// jlong handle) {
|
|
// auto* wallet = reinterpret_cast<Wallet*>(handle);
|
|
// auto* key = &wallet->keys().m_account_address.m_view_public_key;
|
|
// return nativeToJvmByteArray(env, key->data, sizeof(key->data)).Release();
|
|
//}
|
|
//
|
|
//extern "C"
|
|
//JNIEXPORT jbyteArray JNICALL
|
|
//Java_im_molly_monero_Wallet_nativeGetSpendPublicKey(
|
|
// JNIEnv* env,
|
|
// jobject thiz,
|
|
// jlong handle) {
|
|
// auto* wallet = reinterpret_cast<Wallet*>(handle);
|
|
// auto* key = &wallet->keys().m_account_address.m_spend_public_key;
|
|
// return nativeToJvmByteArray(env, key->data, sizeof(key->data)).Release();
|
|
//}
|
|
|
|
extern "C"
|
|
JNIEXPORT jstring JNICALL
|
|
Java_im_molly_monero_WalletNative_nativeGetPrimaryAccountAddress(
|
|
JNIEnv* env,
|
|
jobject thiz,
|
|
jlong handle) {
|
|
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
|
return nativeToJvmString(env, wallet->public_address()).Release();
|
|
}
|
|
|
|
extern "C"
|
|
JNIEXPORT jlong JNICALL
|
|
Java_im_molly_monero_WalletNative_nativeGetCurrentBlockchainHeight(
|
|
JNIEnv* env,
|
|
jobject thiz,
|
|
jlong handle) {
|
|
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
|
uint64_t height = wallet->current_blockchain_height();
|
|
LOG_FATAL_IF(height > std::numeric_limits<jlong>::max(),
|
|
"Blockchain height overflowed jlong");
|
|
return static_cast<jlong>(height);
|
|
}
|
|
|
|
ScopedJvmLocalRef<jobject> nativeToJvmOwnedTxOut(JNIEnv* env,
|
|
const TxOut& tx_out) {
|
|
LOG_FATAL_IF(tx_out.m_spent
|
|
&& (tx_out.m_spent_height == 0 ||
|
|
tx_out.m_spent_height < tx_out.m_block_height),
|
|
"Unexpected spent block height in tx output");
|
|
return {env, OwnedTxOut.newObject(
|
|
env,
|
|
OwnedTxOut_ctor,
|
|
nativeToJvmByteArray(env, tx_out.m_txid.data, sizeof(tx_out.m_txid.data)).obj(),
|
|
tx_out.m_amount,
|
|
tx_out.m_block_height,
|
|
tx_out.m_spent_height)
|
|
};
|
|
}
|
|
|
|
extern "C"
|
|
JNIEXPORT jobjectArray JNICALL
|
|
Java_im_molly_monero_WalletNative_nativeGetOwnedTxOuts(
|
|
JNIEnv* env,
|
|
jobject thiz,
|
|
jlong handle) {
|
|
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
|
ScopedJvmLocalRef<jobjectArray> j_array;
|
|
wallet->getOwnedTxOuts([env, &j_array](std::vector<TxOut> const& tx_outs) {
|
|
j_array = nativeToJvmObjectArray(env,
|
|
tx_outs,
|
|
OwnedTxOut.getClass(),
|
|
&nativeToJvmOwnedTxOut);
|
|
});
|
|
return j_array.Release();
|
|
}
|
|
|
|
} // namespace monero
|