monero-wallet-sdk/lib/android/src/main/cpp/wallet.cc
2023-10-11 00:58:13 +02:00

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