ZaStoGram/TMessagesProj/jni/wallet/WalletEngineBridge.cpp
2026-10-13 13:38:18 +04:00

1395 lines
70 KiB
C++

#include <jni.h>
#include "wallet_engine.hpp"
#include "GramWalletAdapter.h"
#include "WalletJournal.h"
#include <functional>
#include <algorithm>
#include <charconv>
#include <limits>
#include <mutex>
#include <unordered_map>
namespace {
namespace we = wallet_engine;
we::NftTransferIntent nft_transfer_intent(JNIEnv *env, jbyteArray comment_bytes, const std::string &recipient, const std::string &nft,
jlong attached, jlong forward) {
if (!we::is_valid_ton_address(recipient) || !we::is_valid_ton_address(nft))
throw std::invalid_argument("Valid recipient and NFT addresses are required");
if (forward < 0 || attached <= forward)
throw std::invalid_argument("NFT attached amount must exceed its forward amount");
we::NftTransferPayload payload = we::NftTransferPayload::kEmpty{};
if (comment_bytes) {
const auto length = env->GetArrayLength(comment_bytes);
if (length > 960) throw std::invalid_argument("Comment exceeds 960 UTF-8 bytes");
if (length > 0) {
std::string comment(static_cast<size_t>(length), '\0');
env->GetByteArrayRegion(comment_bytes, 0, length, reinterpret_cast<jbyte *>(comment.data()));
if (env->ExceptionCheck()) throw std::runtime_error("Could not read NFT comment");
payload = we::NftTransferPayload::kComment{comment};
}
}
return {nft, recipient,
we::NftTransferFunding::kExact{std::to_string(attached), std::to_string(forward)},
payload, we::SendExpiration::kEngineDefault{}};
}
struct SensitiveString {
std::string value;
explicit SensitiveString(size_t size) : value(size, '\0') {}
SensitiveString(const SensitiveString &) = delete;
SensitiveString &operator=(const SensitiveString &) = delete;
~SensitiveString() {
// Volatile writes keep this cleanup from being optimized away.
volatile char *data = value.data();
for (size_t i = 0; i < value.size(); ++i) data[i] = 0;
}
};
// This host is scoped to a user-requested local operation. Nothing is persisted;
// the mnemonic is released when the synchronous lifecycle calls finish.
class EphemeralWalletHost final : public we::WalletPlatformHost {
std::unique_ptr<SensitiveString> secret_;
std::string secret_ref_;
const we::SecretAccessReason reason_;
[[noreturn]] static void unavailable() {
we::protected_secret_host_error::Failed error;
error.kind = we::ProtectedSecretHostErrorKind::kUnavailable;
error.diagnostic = "No recovery phrase authorized for this operation";
throw error;
}
public:
explicit EphemeralWalletHost(we::SecretAccessReason reason) : reason_(reason) {}
void store_protected_secret(const we::ProtectedSecretStore &request) override {
if (secret_) unavailable();
auto secret = std::make_unique<SensitiveString>(request.bytes.size());
std::copy(request.bytes.begin(), request.bytes.end(), secret->value.begin());
secret_ref_ = request.secret_ref.value;
secret_ = std::move(secret);
}
std::vector<uint8_t> read_protected_secret(const we::ProtectedSecretRead &request) override {
if (!secret_ || request.secret_ref.value != secret_ref_
|| request.reason != reason_) unavailable();
return {secret_->value.begin(), secret_->value.end()};
}
void delete_protected_secret(const we::ProtectedSecretRef &) override { unavailable(); }
std::optional<we::JournalRecord> load_journal(const we::JournalKey &) override {
throw std::logic_error("Local wallet operation does not use a journal");
}
we::JournalCompareExchangeResult compare_exchange_journal(const we::JournalCompareExchange &) override {
throw std::logic_error("Local wallet operation does not use a journal");
}
};
struct ImportProofRequest {
we::ImportWalletRequest value{"import-proof", we::Network::kMainnet, {}};
explicit ImportProofRequest(const std::string &phrase) {
value.recovery_words.reserve(24);
const char *whitespace = " \t\r\n\f\v";
for (size_t start = phrase.find_first_not_of(whitespace); start != std::string::npos;) {
const auto end = phrase.find_first_of(whitespace, start);
value.recovery_words.emplace_back(phrase, start,
end == std::string::npos ? std::string::npos : end - start);
start = end == std::string::npos ? end : phrase.find_first_not_of(whitespace, end);
}
}
~ImportProofRequest() {
for (auto &word : value.recovery_words) {
volatile char *data = word.data();
for (size_t i = 0; i < word.size(); ++i) data[i] = 0;
}
}
};
class OfflineWalletTransport final : public we::WalletStatuslessHost {
public:
std::vector<uint8_t> execute_statusless(const we::HttpRequest &) override {
throw std::logic_error("Local wallet operation cannot access the network");
}
void cancel_statusless(const we::HttpRequestId &) override {}
};
// Host callbacks may run on a Rust worker as well as a Java caller thread.
class JavaEnv {
public:
JNIEnv *env = nullptr;
explicit JavaEnv(JavaVM *vm) : vm_(vm) {
const jint status = vm_->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6);
if (status == JNI_EDETACHED) {
#ifdef __ANDROID__
if (vm_->AttachCurrentThread(&env, nullptr) != JNI_OK)
#else
if (vm_->AttachCurrentThread(reinterpret_cast<void **>(&env), nullptr) != JNI_OK)
#endif
throw std::runtime_error("Could not attach wallet callback thread");
attached_ = true;
} else if (status != JNI_OK) {
throw std::runtime_error("Could not access wallet callback thread");
}
if (env->PushLocalFrame(16) != JNI_OK) {
if (attached_) vm_->DetachCurrentThread();
throw std::runtime_error("Could not allocate wallet callback references");
}
}
~JavaEnv() {
env->PopLocalFrame(nullptr);
if (attached_) vm_->DetachCurrentThread();
}
private:
JavaVM *vm_;
bool attached_ = false;
};
class WalletHosts final : public we::WalletStatuslessHost, public we::WalletPlatformHost {
public:
std::weak_ptr<we::WalletClient> client;
const SensitiveString *secret = nullptr;
std::string secret_ref;
we::SecretAccessReason secret_reason = we::SecretAccessReason::kSignTransfer;
std::function<void(const we::SendSnapshot &)> on_state;
void report_state() {
if (on_state) {
if (auto value = client.lock()) on_state(value->snapshot().send);
}
}
WalletHosts(JNIEnv *env, jobject transport, std::string journal_directory)
: journal_(std::move(journal_directory)) {
if (!transport || env->GetJavaVM(&vm_) != JNI_OK)
throw std::invalid_argument("Wallet transport is required");
jclass cls = env->GetObjectClass(transport);
execute_ = env->GetMethodID(cls, "execute", "(JLjava/lang/String;Z[BJ)[B");
cancel_ = env->GetMethodID(cls, "cancel", "(J)V");
env->DeleteLocalRef(cls);
jclass error_cls = env->FindClass("org/telegram/ui/Wallet/WalletEngine2$HttpTransport$HostException");
if (!error_cls || !execute_ || !cancel_)
throw std::runtime_error("Could not resolve wallet transport callbacks");
error_kind_ = env->GetFieldID(error_cls, "kind", "I");
if (!error_kind_) {
env->DeleteLocalRef(error_cls);
throw std::runtime_error("Could not resolve wallet transport error");
}
error_class_ = static_cast<jclass>(env->NewGlobalRef(error_cls));
env->DeleteLocalRef(error_cls);
if (!error_class_) throw std::bad_alloc();
transport_ = env->NewGlobalRef(transport);
if (!transport_) {
env->DeleteGlobalRef(error_class_);
throw std::bad_alloc();
}
}
~WalletHosts() override {
try {
JavaEnv java(vm_);
java.env->DeleteGlobalRef(transport_);
java.env->DeleteGlobalRef(error_class_);
} catch (...) { }
}
std::vector<uint8_t> execute_statusless(const we::HttpRequest &request) override {
report_state();
JavaEnv java(vm_);
JNIEnv *env = java.env;
jstring url = env->NewStringUTF(request.url.c_str());
if (!url) check_exception(env);
jbyteArray body = env->NewByteArray(static_cast<jsize>(request.body.size()));
if (!body) check_exception(env);
env->SetByteArrayRegion(body, 0, static_cast<jsize>(request.body.size()),
reinterpret_cast<const jbyte *>(request.body.data()));
check_exception(env);
auto response = static_cast<jbyteArray>(env->CallObjectMethod(transport_, execute_,
static_cast<jlong>(request.id.value), url,
static_cast<jboolean>(request.method == we::HttpMethod::kPost), body,
static_cast<jlong>(request.timeout_ms)));
check_exception(env);
if (!response) throw transport_error(7);
std::vector<uint8_t> result(env->GetArrayLength(response));
env->GetByteArrayRegion(response, 0, static_cast<jsize>(result.size()),
reinterpret_cast<jbyte *>(result.data()));
check_exception(env);
return result;
}
void cancel_statusless(const we::HttpRequestId &id) override {
JavaEnv java(vm_);
java.env->CallVoidMethod(transport_, cancel_, static_cast<jlong>(id.value));
// Cancellation is best-effort and must not leave a pending JNI exception.
if (java.env->ExceptionCheck()) java.env->ExceptionClear();
}
std::vector<uint8_t> read_protected_secret(const we::ProtectedSecretRead &request) override {
report_state();
if (!secret || request.secret_ref.value != secret_ref || request.reason != secret_reason)
throw secret_unavailable();
return {secret->value.begin(), secret->value.end()};
}
void store_protected_secret(const we::ProtectedSecretStore &) override {
throw secret_unavailable();
}
void delete_protected_secret(const we::ProtectedSecretRef &) override {
throw secret_unavailable();
}
std::optional<we::JournalRecord> load_journal(const we::JournalKey &key) override {
report_state();
try { return journal_.access(key).current; }
catch (...) { throw journal_unavailable(); }
}
we::JournalCompareExchangeResult compare_exchange_journal(const we::JournalCompareExchange &mutation) override {
report_state();
try { return journal_.access(mutation.key, &mutation); }
catch (...) { throw journal_unavailable(); }
}
private:
WalletJournal journal_;
JavaVM *vm_ = nullptr;
jobject transport_ = nullptr;
jclass error_class_ = nullptr;
jmethodID execute_ = nullptr;
jmethodID cancel_ = nullptr;
jfieldID error_kind_ = nullptr;
static we::statusless_host_error::Failed transport_error(jint kind) {
we::statusless_host_error::Failed error;
error.kind = static_cast<we::StatuslessHostErrorKind>(kind >= 1 && kind <= 7 ? kind : 7);
error.diagnostic = kind == 2 ? "Wallet provider request timed out"
: kind == 6 ? "Wallet provider request was cancelled" : "Wallet provider request failed";
return error;
}
void check_exception(JNIEnv *env) const {
if (!env->ExceptionCheck()) return;
jthrowable exception = env->ExceptionOccurred();
env->ExceptionClear();
jint kind = env->IsInstanceOf(exception, error_class_) ? env->GetIntField(exception, error_kind_) : 7;
env->DeleteLocalRef(exception);
throw transport_error(kind);
}
static we::protected_secret_host_error::Failed secret_unavailable() {
we::protected_secret_host_error::Failed error;
error.kind = we::ProtectedSecretHostErrorKind::kUnavailable;
error.diagnostic = "No recovery phrase authorized for this operation";
return error;
}
static we::journal_host_error::Failed journal_unavailable() {
we::journal_host_error::Failed error;
error.kind = we::JournalHostErrorKind::kUnavailable;
error.diagnostic = "Wallet journal storage failed";
return error;
}
};
std::mutex clients_mutex;
struct Client {
std::shared_ptr<we::WalletClient> wallet;
std::shared_ptr<WalletHosts> hosts;
};
std::unordered_map<jlong, Client> clients;
jlong next_handle = 1;
Client get_client(jlong handle) {
std::lock_guard<std::mutex> lock(clients_mutex);
const auto it = clients.find(handle);
if (it == clients.end()) throw std::runtime_error("NO_WALLET_ENGINE");
return it->second;
}
std::string java_string(JNIEnv *env, jstring value) {
if (!value) throw std::invalid_argument("Required string is missing");
const char *chars = env->GetStringUTFChars(value, nullptr);
if (!chars) throw std::bad_alloc();
try {
std::string result(chars);
env->ReleaseStringUTFChars(value, chars);
return result;
} catch (...) {
env->ReleaseStringUTFChars(value, chars);
throw;
}
}
we::SendIntent transfer_intent(
const std::string &address, jlong amount,
we::SendMessageBody body) {
if (amount < 0) throw std::invalid_argument("Transfer amount must not be negative");
const auto parsed = we::parse_ton_address(address);
const auto friendly = std::get_if<we::TonAddressFormat::kUserFriendly>(&parsed.format.get_variant());
if (friendly != nullptr && friendly->testnet) {
throw std::invalid_argument("Testnet-only recipient is not allowed");
}
// Raw addresses carry no bounce flag; retain the non-bounceable default.
const bool bounce = friendly != nullptr && friendly->bounceable;
return {we::SendExpiration::kEngineDefault{}, {
we::SendMessage{address, we::SendAmount::kExact{std::to_string(amount)},
std::move(body), bounce, std::nullopt}
}};
}
we::SendIntent transfer_intent(const std::string &address, jlong amount) {
return transfer_intent(address, amount, we::SendMessageBody::kEmpty{});
}
// Local references are scoped to the native entry point that creates the result.
class Transaction {
JNIEnv *env_;
jclass cls_;
jclass engine_cls_;
public:
jobject object;
Transaction(JNIEnv *env, const std::string &address, jlong amount) : env_(env) {
engine_cls_ = env_->FindClass("org/telegram/ui/Wallet/WalletEngine2");
if (!engine_cls_) throw std::runtime_error("Could not resolve wallet engine");
auto factory = env_->GetStaticMethodID(engine_cls_, "createTransaction",
"(Ljava/lang/String;J)Lorg/telegram/tgnet/tl/TL_wallet$walletTransaction;");
if (!factory) throw std::runtime_error("Could not resolve wallet transaction factory");
auto destination = env_->NewStringUTF(address.c_str());
if (!destination) throw std::bad_alloc();
object = env_->CallStaticObjectMethod(engine_cls_, factory, destination, amount);
if (env_->ExceptionCheck() || !object) throw std::runtime_error("Could not create wallet transaction");
env_->DeleteLocalRef(destination);
cls_ = env_->GetObjectClass(object);
}
void text(const char *name, const std::string &value) {
auto field = env_->GetFieldID(cls_, name, "Ljava/lang/String;");
if (!field) throw std::runtime_error("Could not resolve transaction field");
auto str = env_->NewStringUTF(value.c_str());
if (!str) throw std::bad_alloc();
env_->SetObjectField(object, field, str);
env_->DeleteLocalRef(str);
}
void number(const char *name, jlong value) {
auto field = env_->GetFieldID(cls_, name, "J");
if (!field) throw std::runtime_error("Could not resolve transaction field");
env_->SetLongField(object, field, value);
}
void boolean(const char *name, bool value) {
auto field = env_->GetFieldID(cls_, name, "Z");
if (!field) throw std::runtime_error("Could not resolve transaction field");
env_->SetBooleanField(object, field, value);
}
void phase(we::SendPhase value) {
auto setter = env_->GetStaticMethodID(engine_cls_, "setTransactionPhase",
"(Lorg/telegram/tgnet/tl/TL_wallet$walletTransaction;I)V");
if (!setter) throw std::runtime_error("Could not resolve transaction phase");
env_->CallStaticVoidMethod(engine_cls_, setter, object, static_cast<jint>(value));
if (env_->ExceptionCheck()) throw std::runtime_error("Could not convert transaction phase");
}
};
jobject preview_transaction(JNIEnv *env, const we::SendPreview &preview,
const std::string &destination, jlong amount) {
Transaction result(env, destination, amount);
result.boolean("preview", true);
const auto &decimal_fee = preview.emulation.wallet_fees_nanograms;
jlong fee = 0;
const auto parsed = std::from_chars(decimal_fee.data(), decimal_fee.data() + decimal_fee.size(), fee);
if (parsed.ec != std::errc() || parsed.ptr != decimal_fee.data() + decimal_fee.size() || fee < 0)
throw std::runtime_error("Estimated fee exceeds the supported range");
result.number("fee", fee);
result.text("traceFeesNanograms", preview.emulation.trace_fees_nanograms);
result.number("validUntil", static_cast<jlong>(preview.valid_until));
result.boolean("traceSucceeded", preview.emulation.trace_succeeded);
result.boolean("traceIncomplete", preview.emulation.is_incomplete);
result.phase(we::SendPhase::kIdle);
return result.object;
}
class SecretScope {
std::shared_ptr<WalletHosts> hosts_;
public:
SecretScope(const Client &client, const SensitiveString &phrase, we::SecretAccessReason reason)
: hosts_(client.hosts) {
hosts_->secret = &phrase;
hosts_->secret_reason = reason;
}
~SecretScope() {
hosts_->secret = nullptr;
hosts_->secret_reason = we::SecretAccessReason::kSignTransfer;
}
};
class SendScope {
JNIEnv *env_;
std::shared_ptr<WalletHosts> hosts_;
jobject callback_ = nullptr;
public:
SendScope(JNIEnv *env, const Client &client, const SensitiveString &phrase,
jobject callback, const std::string &operation_id,
we::SecretAccessReason reason = we::SecretAccessReason::kSignTransfer)
: env_(env), hosts_(client.hosts) {
JavaVM *vm = nullptr;
env->GetJavaVM(&vm);
auto cls = env->GetObjectClass(callback);
auto state = env->GetMethodID(cls, "state", "(ILjava/lang/String;Ljava/lang/String;)V");
env->DeleteLocalRef(cls);
if (!state) throw std::runtime_error("Could not resolve send callback");
callback_ = env->NewGlobalRef(callback);
if (!callback_) throw std::bad_alloc();
try {
hosts_->on_state = [vm, ref = callback_, state, operation_id](const we::SendSnapshot &snapshot) {
if (!snapshot.operation_id || *snapshot.operation_id != operation_id) return;
JavaEnv java(vm);
auto error = snapshot.error_message ? java.env->NewStringUTF(snapshot.error_message->c_str()) : nullptr;
auto transaction_hash = snapshot.resolution && snapshot.resolution->transaction_hash
? java.env->NewStringUTF(snapshot.resolution->transaction_hash->c_str()) : nullptr;
if (!java.env->ExceptionCheck())
java.env->CallVoidMethod(ref, state, static_cast<jint>(snapshot.phase), error, transaction_hash);
// UI delivery cannot change whether a signed transfer was submitted.
if (java.env->ExceptionCheck()) java.env->ExceptionClear();
};
} catch (...) {
env->DeleteGlobalRef(callback_);
throw;
}
hosts_->secret = &phrase;
hosts_->secret_reason = reason;
}
~SendScope() {
hosts_->secret = nullptr;
hosts_->secret_reason = we::SecretAccessReason::kSignTransfer;
try { hosts_->report_state(); } catch (...) { }
hosts_->on_state = nullptr;
env_->DeleteGlobalRef(callback_);
}
};
void throw_java(JNIEnv *env, const char *type, const char *message) {
if (env->ExceptionCheck()) return;
jclass cls = env->FindClass(type);
if (cls) {
env->ThrowNew(cls, message);
env->DeleteLocalRef(cls);
}
}
} // namespace
extern "C" JNIEXPORT jobjectArray JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_mnemonicWordlist(JNIEnv *env, jclass) {
try {
const auto words = we::mnemonic_wordlist();
jclass string_class = env->FindClass("java/lang/String");
if (!string_class) return nullptr;
jobjectArray result = env->NewObjectArray(static_cast<jsize>(words.size()), string_class, nullptr);
env->DeleteLocalRef(string_class);
if (!result) return nullptr;
for (size_t i = 0; i < words.size(); ++i) {
jstring word = env->NewStringUTF(words[i].c_str());
if (!word) return nullptr;
env->SetObjectArrayElement(result, static_cast<jsize>(i), word);
env->DeleteLocalRef(word);
if (env->ExceptionCheck()) return nullptr;
}
return result;
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not load mnemonic word list");
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not load mnemonic word list");
}
return nullptr;
}
extern "C" JNIEXPORT jbyteArray JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeSecretPhraseToPublicKey(
JNIEnv *env, jclass, jbyteArray secret_phrase) {
if (!secret_phrase) {
throw_java(env, "java/lang/IllegalArgumentException", "Recovery phrase is required");
return nullptr;
}
try {
const jsize length = env->GetArrayLength(secret_phrase);
SensitiveString phrase(static_cast<size_t>(length));
env->GetByteArrayRegion(secret_phrase, 0, length, reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return nullptr;
auto key = we::rotation_mnemonic_public_key(phrase.value);
ImportProofRequest parsed(phrase.value);
if (parsed.value.recovery_words.size() == 24) {
SensitiveString signing_phrase(0);
signing_phrase.value.reserve(phrase.value.size());
for (size_t i = 12; i < 24; ++i) {
if (i != 12) signing_phrase.value.push_back(' ');
signing_phrase.value.append(parsed.value.recovery_words[i]);
}
key = we::rotation_mnemonic_public_key(signing_phrase.value);
}
if (key.size() != 32) throw std::runtime_error("Unexpected public key size");
jbyteArray result = env->NewByteArray(32);
if (result) env->SetByteArrayRegion(result, 0, 32, reinterpret_cast<const jbyte *>(key.data()));
return result;
} catch (const we::WalletLifecycleError &) {
throw_java(env, "java/lang/IllegalArgumentException", "Invalid Rotation recovery phrase");
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not derive wallet public key");
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not derive wallet public key");
}
return nullptr;
}
extern "C" JNIEXPORT jstring JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeSecretPhraseToAddress(
JNIEnv *env, jclass, jbyteArray secret_phrase) {
if (!secret_phrase) {
throw_java(env, "java/lang/IllegalArgumentException", "Recovery phrase is required");
return nullptr;
}
try {
const jsize length = env->GetArrayLength(secret_phrase);
SensitiveString phrase(static_cast<size_t>(length));
env->GetByteArrayRegion(secret_phrase, 0, length, reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return nullptr;
// Reuse import's complete mnemonic validation and canonical address derivation.
// The host retains the phrase only for this call and wipes it on destruction.
ImportProofRequest request(phrase.value);
auto host = std::make_shared<EphemeralWalletHost>(we::SecretAccessReason::kSignTonConnectProof);
auto lifecycle = we::WalletLifecycle::init(host);
auto descriptor = lifecycle->import_wallet(request.value);
return env->NewStringUTF(descriptor.address.c_str());
} catch (const we::wallet_lifecycle_error::InvalidRecoveryPhrase &) {
throw_java(env, "java/lang/IllegalArgumentException", "Invalid Rotation recovery phrase");
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not derive wallet address");
} catch (...) {
// Never include phrase contents in errors or logs.
throw_java(env, "java/lang/IllegalStateException", "Could not derive wallet address");
}
return nullptr;
}
extern "C" JNIEXPORT jboolean JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_isValidCellBoc(JNIEnv *env, jclass, jstring boc) {
try {
return boc && gram_adapter::is_valid_cell_boc(java_string(env, boc)) ? JNI_TRUE : JNI_FALSE;
} catch (...) {
return JNI_FALSE;
}
}
extern "C" JNIEXPORT jbyteArray JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeTonConnectCrypto(
JNIEnv *env, jclass, jbyteArray phrase_bytes, jbyteArray parameters_bytes) {
if (!phrase_bytes || !parameters_bytes) {
throw_java(env, "java/lang/IllegalArgumentException", "Missing TON Connect approval data");
return nullptr;
}
try {
SensitiveString phrase(static_cast<size_t>(env->GetArrayLength(phrase_bytes)));
SensitiveString parameters(static_cast<size_t>(env->GetArrayLength(parameters_bytes)));
env->GetByteArrayRegion(phrase_bytes, 0, static_cast<jsize>(phrase.value.size()),
reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return nullptr;
env->GetByteArrayRegion(parameters_bytes, 0, static_cast<jsize>(parameters.value.size()),
reinterpret_cast<jbyte *>(parameters.value.data()));
if (env->ExceptionCheck()) return nullptr;
SensitiveString output(0);
output.value = gram_adapter::ton_connect_crypto(phrase.value, parameters.value);
auto result = env->NewByteArray(static_cast<jsize>(output.value.size()));
if (result) env->SetByteArrayRegion(result, 0, static_cast<jsize>(output.value.size()),
reinterpret_cast<const jbyte *>(output.value.data()));
return result;
} catch (const std::invalid_argument &error) {
throw_java(env, "java/lang/IllegalArgumentException", error.what());
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not prepare TON Connect approval");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not prepare TON Connect approval");
}
return nullptr;
}
extern "C" JNIEXPORT jobject JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeCreateImportProof(
JNIEnv *env, jclass, jbyteArray phrase_bytes, jbyteArray domain_bytes,
jbyteArray payload_bytes, jint timestamp, jboolean for_import) {
if (!phrase_bytes || !domain_bytes || !payload_bytes || timestamp <= 0
|| env->GetArrayLength(phrase_bytes) == 0
|| env->GetArrayLength(domain_bytes) == 0
|| env->GetArrayLength(payload_bytes) == 0) {
throw_java(env, "java/lang/IllegalArgumentException", "Recovery phrase and proof challenge are required");
return nullptr;
}
try {
SensitiveString phrase(static_cast<size_t>(env->GetArrayLength(phrase_bytes)));
env->GetByteArrayRegion(phrase_bytes, 0, static_cast<jsize>(phrase.value.size()),
reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return nullptr;
// Use standard UTF-8 bytes, not JNI's modified UTF-8, for signed fields.
std::string domain(static_cast<size_t>(env->GetArrayLength(domain_bytes)), '\0');
std::string payload(static_cast<size_t>(env->GetArrayLength(payload_bytes)), '\0');
env->GetByteArrayRegion(domain_bytes, 0, static_cast<jsize>(domain.size()),
reinterpret_cast<jbyte *>(domain.data()));
if (env->ExceptionCheck()) return nullptr;
env->GetByteArrayRegion(payload_bytes, 0, static_cast<jsize>(payload.size()),
reinterpret_cast<jbyte *>(payload.data()));
if (env->ExceptionCheck()) return nullptr;
ImportProofRequest request(phrase.value);
auto host = std::make_shared<EphemeralWalletHost>(we::SecretAccessReason::kSignTonConnectProof);
auto lifecycle = we::WalletLifecycle::init(host);
auto descriptor = lifecycle->import_wallet(request.value);
if (for_import && request.value.recovery_words.size() == 24) {
// The complete phrase was validated above. MTProto import verifies
// with the submitted address-derivation key, not the rotated key.
// Importing its first half retains the same address and authorizes
// that key for this one proof. Wipe discarded signing words first.
for (size_t i = 12; i < request.value.recovery_words.size(); ++i) {
auto &word = request.value.recovery_words[i];
volatile char *data = word.data();
for (size_t j = 0; j < word.size(); ++j) data[j] = 0;
}
request.value.recovery_words.resize(12);
host = std::make_shared<EphemeralWalletHost>(we::SecretAccessReason::kSignTonConnectProof);
lifecycle = we::WalletLifecycle::init(host);
descriptor = lifecycle->import_wallet(request.value);
}
auto proof = lifecycle->sign_ton_connect_proof({descriptor, domain,
static_cast<uint64_t>(timestamp), payload});
if (descriptor.public_key.size() != 32 || proof.signature.size() != 64)
throw std::runtime_error("Unexpected ownership proof size");
auto cls = env->FindClass("org/telegram/ui/Wallet/WalletEngine2$ImportedWalletProof");
if (!cls) return nullptr;
auto constructor = env->GetMethodID(cls, "<init>", "([BI[B)V");
if (!constructor) return nullptr;
auto public_key = env->NewByteArray(32);
if (!public_key) return nullptr;
auto signature = env->NewByteArray(64);
if (!signature) return nullptr;
// MTProto import derives the address and verifies the proof with this
// same anchor key. Other ownership proofs retain current-key signing.
env->SetByteArrayRegion(public_key, 0, 32,
reinterpret_cast<const jbyte *>(descriptor.public_key.data()));
if (env->ExceptionCheck()) return nullptr;
env->SetByteArrayRegion(signature, 0, 64,
reinterpret_cast<const jbyte *>(proof.signature.data()));
if (env->ExceptionCheck()) return nullptr;
return env->NewObject(cls, constructor, public_key, timestamp, signature);
} catch (const we::wallet_lifecycle_error::InvalidRecoveryPhrase &) {
throw_java(env, "java/lang/IllegalArgumentException", "Invalid Rotation recovery phrase");
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not create wallet ownership proof");
} catch (...) {
// Never include phrase contents in errors or logs.
throw_java(env, "java/lang/IllegalStateException", "Could not create wallet ownership proof");
}
return nullptr;
}
extern "C" JNIEXPORT jboolean JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeIsValidAddress(
JNIEnv *env, jclass, jstring address) {
if (!address) return JNI_FALSE;
try {
const auto value = java_string(env, address);
// TON addresses are ASCII; reject JNI modified UTF-8 before passing it to Rust.
for (unsigned char c : value) {
if (c >= 0x80) return JNI_FALSE;
}
return we::is_valid_ton_address(value) ? JNI_TRUE : JNI_FALSE;
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not validate TON address");
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not validate TON address");
}
return JNI_FALSE;
}
extern "C" JNIEXPORT jstring JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeTransactionAddress(
JNIEnv *env, jclass, jstring address) {
try {
const auto friendly = we::convert_ton_address(
java_string(env, address), we::TonAddressFormat::kUserFriendly{false, false});
return env->NewStringUTF(friendly.c_str());
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not format transaction address");
} catch (...) {
throw_java(env, "java/lang/IllegalArgumentException", "Invalid transaction address");
}
return nullptr;
}
extern "C" JNIEXPORT jlong JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeCreate(
JNIEnv *env, jclass, jint current_account, jstring address, jbyteArray public_key, jobject transport, jstring journal_directory) {
try {
if (!address || !public_key || env->GetArrayLength(public_key) != 32) {
throw std::invalid_argument("Wallet address and a 32-byte public key are required");
}
const char *chars = env->GetStringUTFChars(address, nullptr);
if (!chars) return 0;
std::string wallet_address;
try {
wallet_address = chars;
} catch (...) {
env->ReleaseStringUTFChars(address, chars);
throw;
}
env->ReleaseStringUTFChars(address, chars);
const auto raw_address = we::convert_ton_address(wallet_address, we::TonAddressFormat::kRaw{});
std::vector<uint8_t> key(32);
env->GetByteArrayRegion(public_key, 0, 32, reinterpret_cast<jbyte *>(key.data()));
if (env->ExceptionCheck()) return 0;
const auto record_id = "telegram-" + std::to_string(current_account) + ":" + raw_address;
we::WalletClientConfig config{
record_id,
wallet_address,
std::move(key),
we::ProtectedSecretRef{record_id + ":mnemonic"},
we::Network::kMainnet,
60,
30,
we::ProviderConfig{"https://toncenter.com", std::nullopt, 15000}
};
auto hosts = std::make_shared<WalletHosts>(env, transport, java_string(env, journal_directory));
hosts->secret_ref = record_id + ":mnemonic";
auto client = we::WalletClient::new_statusless(config, hosts, hosts);
hosts->client = client;
std::lock_guard<std::mutex> lock(clients_mutex);
if (next_handle == std::numeric_limits<jlong>::max()) {
throw std::runtime_error("Wallet engine handles exhausted");
}
const jlong handle = next_handle++;
clients.emplace(handle, Client{std::move(client), std::move(hosts)});
return handle;
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not initialize wallet engine");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalArgumentException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not initialize wallet engine");
}
return 0;
}
extern "C" JNIEXPORT jlong JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeBalance(JNIEnv *env, jclass, jlong handle) {
try {
std::shared_ptr<we::WalletClient> client;
{
std::lock_guard<std::mutex> lock(clients_mutex);
auto it = clients.find(handle);
if (it == clients.end()) throw std::runtime_error("Wallet engine is closed");
client = it->second.wallet;
}
const auto update = client->refresh();
const auto &snapshot = update.snapshot;
if (snapshot.account_resource.phase != we::ResourcePhase::kReady || !snapshot.account) {
throw std::runtime_error(snapshot.account_resource.error
? snapshot.account_resource.error->developer_message : "Could not load wallet balance");
}
const auto &decimal = snapshot.account->balance_nanograms;
jlong balance = 0;
const auto parsed = std::from_chars(decimal.data(), decimal.data() + decimal.size(), balance);
if (parsed.ec != std::errc() || parsed.ptr != decimal.data() + decimal.size() || balance < 0)
throw std::runtime_error("Wallet balance exceeds the supported range");
return balance;
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not load wallet balance");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not load wallet balance");
}
return 0;
}
extern "C" JNIEXPORT jobject JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeEmulateSend(
JNIEnv *env, jclass, jlong handle, jstring address, jlong amount,
jbyteArray comment_bytes, jboolean comment_encrypted) {
try {
auto client = get_client(handle);
auto destination = java_string(env, address);
auto body = we::SendMessageBody(we::SendMessageBody::kEmpty{});
if (comment_bytes) {
std::string comment(static_cast<size_t>(env->GetArrayLength(comment_bytes)), '\0');
env->GetByteArrayRegion(comment_bytes, 0, static_cast<jsize>(comment.size()),
reinterpret_cast<jbyte *>(comment.data()));
if (env->ExceptionCheck()) return nullptr;
// For encrypted previews Java supplies a size-matched placeholder BOC.
body = comment_encrypted
? we::SendMessageBody(we::SendMessageBody::kRawPayload{comment})
: we::SendMessageBody(we::SendMessageBody::kComment{comment});
}
auto preview = client.wallet->preview_send({transfer_intent(destination, amount, std::move(body))});
return preview_transaction(env, preview, destination, amount);
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not preview transfer");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not preview transfer");
}
return nullptr;
}
extern "C" JNIEXPORT jobject JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeEmulateRotateKey(
JNIEnv *env, jclass, jlong handle, jstring address) {
try {
// No SecretScope: preview_key_rotation accepts public state only and
// constructs zero owner/proof signatures before contacting Toncenter.
const auto preview = get_client(handle).wallet->preview_key_rotation();
return preview_transaction(env, preview, java_string(env, address), 0);
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not emulate key rotation");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not emulate key rotation");
}
return nullptr;
}
extern "C" JNIEXPORT jobject JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativePrepareRotateKey(
JNIEnv *env, jclass, jlong handle, jbyteArray phrase_bytes, jstring gasless_address) {
try {
auto client = get_client(handle);
if (!phrase_bytes || env->GetArrayLength(phrase_bytes) == 0)
throw std::invalid_argument("Recovery phrase is required");
if (gasless_address && !we::is_valid_ton_address(java_string(env, gasless_address)))
throw std::invalid_argument("Valid relayer address is required");
SensitiveString phrase(static_cast<size_t>(env->GetArrayLength(phrase_bytes)));
env->GetByteArrayRegion(phrase_bytes, 0, static_cast<jsize>(phrase.value.size()),
reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return nullptr;
SecretScope scope(client, phrase, we::SecretAccessReason::kPrepareKeyRotation);
const auto snapshot = client.wallet->refresh().snapshot;
if (snapshot.account_resource.phase != we::ResourcePhase::kReady || !snapshot.account)
throw std::runtime_error(snapshot.account_resource.error
? snapshot.account_resource.error->developer_message : "Could not load wallet account");
constexpr uint64_t validity_seconds = 120;
if (snapshot.account->sync_utime > std::numeric_limits<uint32_t>::max() - validity_seconds)
throw std::runtime_error("Key rotation expiration exceeds the supported range");
auto prepared = client.wallet->prepare_key_rotation({
snapshot.account->sync_utime + validity_seconds, we::KeyRotationMessageKind::kExternal});
SensitiveString replacement(0);
replacement.value.swap(prepared.replacement_recovery_phrase.phrase);
if (prepared.new_public_key.size() != 32)
throw std::runtime_error("Unexpected signing public key size");
const auto cls = env->FindClass("org/telegram/ui/Wallet/WalletEngine2$PreparedRotation");
if (!cls) return nullptr;
const auto constructor = env->GetMethodID(cls, "<init>",
"(Ljava/lang/String;Ljava/lang/String;[B[B)V");
if (!constructor) return nullptr;
const auto external = env->NewStringUTF(prepared.external_boc.c_str());
if (!external) return nullptr;
const auto internal = gasless_address ? env->NewStringUTF(prepared.internal_boc.c_str()) : nullptr;
if (env->ExceptionCheck()) return nullptr;
const auto public_key = env->NewByteArray(32);
if (!public_key) return nullptr;
env->SetByteArrayRegion(public_key, 0, 32,
reinterpret_cast<const jbyte *>(prepared.new_public_key.data()));
if (env->ExceptionCheck()) return nullptr;
const auto bytes = env->NewByteArray(static_cast<jsize>(replacement.value.size()));
if (!bytes) return nullptr;
env->SetByteArrayRegion(bytes, 0, static_cast<jsize>(replacement.value.size()),
reinterpret_cast<const jbyte *>(replacement.value.data()));
if (env->ExceptionCheck()) return nullptr;
// The Java constructor clears its phrase bytes after creating the result.
return env->NewObject(cls, constructor, external, internal, bytes, public_key);
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not prepare key rotation");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not prepare key rotation");
}
return nullptr;
}
extern "C" JNIEXPORT jbyteArray JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeDecryptRawComment(
JNIEnv *env, jclass, jbyteArray phrase_bytes, jstring sender, jstring body) {
if (!phrase_bytes || !sender || !body || env->GetArrayLength(phrase_bytes) == 0) {
throw_java(env, "java/lang/IllegalArgumentException", "Recovery phrase, sender and message body are required");
return nullptr;
}
try {
SensitiveString phrase(static_cast<size_t>(env->GetArrayLength(phrase_bytes)));
env->GetByteArrayRegion(phrase_bytes, 0, static_cast<jsize>(phrase.value.size()),
reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return nullptr;
ImportProofRequest request(phrase.value);
auto host = std::make_shared<EphemeralWalletHost>(we::SecretAccessReason::kDecryptComment);
auto lifecycle = we::WalletLifecycle::init(host);
auto descriptor = lifecycle->import_wallet(request.value);
we::WalletClientConfig config{
descriptor.record_id, descriptor.address, descriptor.public_key,
descriptor.secret_ref, descriptor.network, 60, 30,
we::ProviderConfig{"https://toncenter.com", std::nullopt, 15000}
};
// The existing engine API takes a client, but decryption uses only local
// key material. A dedicated transport prohibits any provider requests.
auto client = we::WalletClient::new_statusless(config,
std::make_shared<OfflineWalletTransport>(), host);
SensitiveString plaintext(0);
plaintext.value = client->decrypt_comment({java_string(env, sender), java_string(env, body)});
auto result = env->NewByteArray(static_cast<jsize>(plaintext.value.size()));
if (!result) return nullptr;
env->SetByteArrayRegion(result, 0, static_cast<jsize>(plaintext.value.size()),
reinterpret_cast<const jbyte *>(plaintext.value.data()));
return result;
} catch (const we::wallet_lifecycle_error::InvalidRecoveryPhrase &) {
throw_java(env, "java/lang/IllegalArgumentException", "Invalid Rotation recovery phrase");
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not decrypt transaction comment");
} catch (...) {
// Do not expose secret material or unauthenticated plaintext in diagnostics.
throw_java(env, "java/lang/IllegalStateException", "Could not decrypt transaction comment");
}
return nullptr;
}
extern "C" JNIEXPORT jbyteArray JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeDecryptComment(
JNIEnv *env, jclass, jlong handle, jbyteArray phrase_bytes,
jstring sender, jstring body) {
try {
if (!phrase_bytes || !sender || !body)
throw std::invalid_argument("Recovery phrase, sender and message body are required");
auto client = get_client(handle);
SensitiveString phrase(static_cast<size_t>(env->GetArrayLength(phrase_bytes)));
env->GetByteArrayRegion(phrase_bytes, 0, static_cast<jsize>(phrase.value.size()),
reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return nullptr;
SecretScope scope(client, phrase, we::SecretAccessReason::kDecryptComment);
const auto comment = client.wallet->decrypt_comment({
java_string(env, sender), java_string(env, body)});
auto result = env->NewByteArray(static_cast<jsize>(comment.size()));
if (!result) return nullptr;
env->SetByteArrayRegion(result, 0, static_cast<jsize>(comment.size()),
reinterpret_cast<const jbyte *>(comment.data()));
return result;
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not decrypt transaction comment");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not decrypt transaction comment");
}
return nullptr;
}
namespace {
we::SendIntent ton_connect_intent(JNIEnv *env, jobjectArray fields, jlong valid_until) {
const int count = fields ? env->GetArrayLength(fields) : 0;
if (count == 0 || count % 5 || count > 255 * 5 || valid_until < 0)
throw std::invalid_argument("Invalid TON Connect transaction");
we::SendIntent intent{valid_until == 0 ? we::SendExpiration(we::SendExpiration::kEngineDefault{})
: we::SendExpiration(we::SendExpiration::kExact{static_cast<uint64_t>(valid_until)}), {}};
for (int i = 0; i < count; i += 5) {
std::optional<std::string> values[5];
for (int j = 0; j < 5; ++j) {
auto value = static_cast<jstring>(env->GetObjectArrayElement(fields, i + j));
if (value) { values[j] = java_string(env, value); env->DeleteLocalRef(value); }
}
if (!values[0] || !values[1] || !values[4] || (*values[4] != "0" && *values[4] != "1")) throw std::invalid_argument("Missing transfer fields");
auto body = values[2] ? we::SendMessageBody(we::SendMessageBody::kRawPayload{*values[2]})
: we::SendMessageBody(we::SendMessageBody::kEmpty{});
intent.messages.push_back({*values[0], we::SendAmount::kExact{*values[1]}, body, *values[4] == "1", values[3]});
}
return intent;
}
}
extern "C" JNIEXPORT jobject JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativePreviewTonConnect(
JNIEnv *env, jclass, jlong handle, jobjectArray fields, jlong valid_until) {
try {
auto intent = ton_connect_intent(env, fields, valid_until);
auto preview = get_client(handle).wallet->preview_ton_connect({"ton-connect-preview", false, intent});
return preview_transaction(env, preview, intent.messages.front().destination, 0);
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not preview TON Connect transaction");
}
return nullptr;
}
extern "C" JNIEXPORT void JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativePreviewSignMessage(
JNIEnv *env, jclass, jlong handle, jobjectArray fields, jlong valid_until) {
try {
auto intent = ton_connect_intent(env, fields, valid_until);
get_client(handle).wallet->preview_sign_message({intent});
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not preview message signing");
}
}
extern "C" JNIEXPORT jstring JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeSignMessage(
JNIEnv *env, jclass, jlong handle, jbyteArray phrase_bytes, jobjectArray fields,
jlong valid_until, jstring operation_id) {
try {
auto client = get_client(handle);
auto intent = ton_connect_intent(env, fields, valid_until);
if (!phrase_bytes) throw std::invalid_argument("Recovery phrase is required");
SensitiveString phrase(static_cast<size_t>(env->GetArrayLength(phrase_bytes)));
env->GetByteArrayRegion(phrase_bytes, 0, static_cast<jsize>(phrase.value.size()), reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return nullptr;
SecretScope scope(client, phrase, we::SecretAccessReason::kSignTransfer);
// The engine journals the internal message before handoff and never broadcasts it.
const auto signed_message = client.wallet->sign_message({java_string(env, operation_id), false, intent});
return env->NewStringUTF(signed_message.internal_boc.c_str());
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not sign internal message");
}
return nullptr;
}
extern "C" JNIEXPORT jobject JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativePrepareTonConnectTransfer(
JNIEnv *env, jclass, jlong handle, jbyteArray phrase_bytes, jobjectArray fields,
jlong valid_until, jstring operation_id) {
try {
auto client = get_client(handle);
auto intent = ton_connect_intent(env, fields, valid_until);
if (!phrase_bytes) throw std::invalid_argument("Recovery phrase is required");
SensitiveString phrase(static_cast<size_t>(env->GetArrayLength(phrase_bytes)));
env->GetByteArrayRegion(phrase_bytes, 0, static_cast<jsize>(phrase.value.size()), reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return nullptr;
SecretScope scope(client, phrase, we::SecretAccessReason::kSignTransfer);
auto prepared = client.wallet->prepare_transfer({java_string(env, operation_id), intent});
auto cls = env->FindClass("org/telegram/ui/Wallet/WalletEngine2$PreparedSend");
if (!cls) return nullptr;
auto ctor = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
if (!ctor) return nullptr;
return env->NewObject(cls, ctor, env->NewStringUTF(prepared.external_boc.c_str()), nullptr, nullptr);
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not prepare TON Connect transaction");
}
return nullptr;
}
extern "C" JNIEXPORT jobject JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativePrepareSend(
JNIEnv *env, jclass, jlong handle, jbyteArray phrase_bytes, jstring address,
jlong amount, jbyteArray comment_bytes, jbyteArray comment_recipient_public_key,
jstring gasless_address, jstring operation_id) {
try {
auto client = get_client(handle);
const auto destination = java_string(env, address);
if (!we::is_valid_ton_address(destination))
throw std::invalid_argument("Valid destination address is required");
std::optional<std::string> relayer;
if (gasless_address) {
relayer = java_string(env, gasless_address);
if (!we::is_valid_ton_address(*relayer))
throw std::invalid_argument("Valid relayer address is required");
}
if (amount < 0) throw std::invalid_argument("Transfer amount must not be negative");
if (!phrase_bytes) throw std::invalid_argument("Recovery phrase is required");
SensitiveString phrase(static_cast<size_t>(env->GetArrayLength(phrase_bytes)));
env->GetByteArrayRegion(phrase_bytes, 0, static_cast<jsize>(phrase.value.size()),
reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return nullptr;
std::optional<we::Boc> encrypted_comment_body;
auto body = we::SendMessageBody(we::SendMessageBody::kEmpty{});
if (comment_bytes) {
std::string comment(static_cast<size_t>(env->GetArrayLength(comment_bytes)), '\0');
env->GetByteArrayRegion(comment_bytes, 0, static_cast<jsize>(comment.size()),
reinterpret_cast<jbyte *>(comment.data()));
if (env->ExceptionCheck()) return nullptr;
if (comment_recipient_public_key) {
std::vector<uint8_t> recipient_key(static_cast<size_t>(env->GetArrayLength(comment_recipient_public_key)));
if (recipient_key.size() != 32) throw std::invalid_argument("Recipient public key must be 32 bytes");
env->GetByteArrayRegion(comment_recipient_public_key, 0, 32, reinterpret_cast<jbyte *>(recipient_key.data()));
if (env->ExceptionCheck()) return nullptr;
SecretScope scope(client, phrase, we::SecretAccessReason::kEncryptComment);
encrypted_comment_body = client.wallet->create_encrypted_comment({destination, comment, recipient_key});
body = we::SendMessageBody::kRawPayload{*encrypted_comment_body};
} else {
body = we::SendMessageBody::kComment{comment};
}
}
SecretScope scope(client, phrase, we::SecretAccessReason::kSignTransfer);
const auto prepared = client.wallet->prepare_transfer({
java_string(env, operation_id), transfer_intent(destination, amount, std::move(body))});
const auto external_boc = env->NewStringUTF(prepared.external_boc.c_str());
if (!external_boc) return nullptr;
const auto internal_boc = relayer ? env->NewStringUTF(prepared.internal_boc.c_str()) : nullptr;
if (env->ExceptionCheck()) return nullptr;
const auto comment_body = encrypted_comment_body
? env->NewStringUTF(encrypted_comment_body->c_str()) : nullptr;
if (env->ExceptionCheck()) return nullptr;
const auto prepared_cls = env->FindClass("org/telegram/ui/Wallet/WalletEngine2$PreparedSend");
if (!prepared_cls) return nullptr;
const auto prepared_constructor = env->GetMethodID(prepared_cls, "<init>",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
if (!prepared_constructor) return nullptr;
return env->NewObject(prepared_cls, prepared_constructor, external_boc, internal_boc, comment_body);
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not prepare transfer");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not prepare transfer");
}
return nullptr;
}
extern "C" JNIEXPORT jstring JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeMessageBodyHash(
JNIEnv *env, jclass, jstring boc) {
try {
const auto hash = gram_adapter::message_body_hash(java_string(env, boc));
return env->NewStringUTF(hash.c_str());
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not hash message body");
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not hash message body");
}
return nullptr;
}
extern "C" JNIEXPORT jobject JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeSend(
JNIEnv *env, jclass, jlong handle, jbyteArray phrase_bytes, jstring address,
jlong amount, jbyteArray comment_bytes, jbyteArray comment_recipient_public_key,
jstring operation_id, jobject callbacks) {
try {
auto client = get_client(handle);
auto destination = java_string(env, address);
std::optional<std::string> comment;
if (comment_bytes) {
comment.emplace(static_cast<size_t>(env->GetArrayLength(comment_bytes)), '\0');
env->GetByteArrayRegion(comment_bytes, 0, static_cast<jsize>(comment->size()),
reinterpret_cast<jbyte *>(comment->data()));
if (env->ExceptionCheck()) return nullptr;
}
auto id = java_string(env, operation_id);
if (!phrase_bytes || !callbacks) throw std::invalid_argument("Recovery phrase and callbacks are required");
SensitiveString phrase(static_cast<size_t>(env->GetArrayLength(phrase_bytes)));
env->GetByteArrayRegion(phrase_bytes, 0, static_cast<jsize>(phrase.value.size()),
reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return nullptr;
auto body = comment
? we::SendMessageBody(we::SendMessageBody::kComment{*comment})
: we::SendMessageBody(we::SendMessageBody::kEmpty{});
if (comment && comment_recipient_public_key) {
std::vector<uint8_t> recipient_key(static_cast<size_t>(env->GetArrayLength(comment_recipient_public_key)));
if (recipient_key.size() != 32) throw std::invalid_argument("Recipient public key must be 32 bytes");
env->GetByteArrayRegion(comment_recipient_public_key, 0, 32, reinterpret_cast<jbyte *>(recipient_key.data()));
if (env->ExceptionCheck()) return nullptr;
SecretScope scope(client, phrase, we::SecretAccessReason::kEncryptComment);
body = we::SendMessageBody::kRawPayload{
client.wallet->create_encrypted_comment({destination, *comment, recipient_key})
};
}
auto intent = transfer_intent(destination, amount, std::move(body));
SendScope scope(env, client, phrase, callbacks, id);
auto sent = client.wallet->send({id, false, intent});
Transaction result(env, destination, amount);
result.boolean("feeUnknown", true);
result.text("operationId", sent.operation_id);
result.text("messageHash", sent.message_hash);
result.phase(sent.phase);
return result.object;
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not send transfer");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not send transfer");
}
return nullptr;
}
extern "C" JNIEXPORT jobject JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeResolvePending(JNIEnv *env, jclass, jlong handle) {
try {
const auto snapshot = get_client(handle).wallet->resolve_pending();
const auto cls = env->FindClass("org/telegram/ui/Wallet/WalletEngine2$SendResolution");
if (!cls) return nullptr;
const auto constructor = env->GetMethodID(cls, "<init>",
"(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;J)V");
if (!constructor) return nullptr;
const auto operation_id = snapshot.operation_id ? env->NewStringUTF(snapshot.operation_id->c_str()) : nullptr;
const auto transaction_hash = snapshot.resolution && snapshot.resolution->transaction_hash
? env->NewStringUTF(snapshot.resolution->transaction_hash->c_str()) : nullptr;
const auto error = snapshot.error_message ? env->NewStringUTF(snapshot.error_message->c_str()) : nullptr;
const auto retry_after = snapshot.resolution && snapshot.resolution->retry_after_hint_ms
? *snapshot.resolution->retry_after_hint_ms : 4000;
if (env->ExceptionCheck()) return nullptr;
return env->NewObject(cls, constructor, operation_id, static_cast<jint>(snapshot.phase),
transaction_hash, error, static_cast<jlong>(retry_after));
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not resolve wallet send");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not resolve wallet send");
}
return nullptr;
}
extern "C" JNIEXPORT void JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeRotateKey(
JNIEnv *env, jclass, jlong handle, jbyteArray phrase_bytes,
jstring operation_id, jobject callbacks) {
try {
auto client = get_client(handle);
auto id = java_string(env, operation_id);
if (!phrase_bytes || !callbacks)
throw std::invalid_argument("Recovery phrase and callbacks are required");
auto callback_class = env->GetObjectClass(callbacks);
auto replacement_callback = env->GetMethodID(callback_class, "replacement", "([B[B)V");
env->DeleteLocalRef(callback_class);
if (!replacement_callback) return;
SensitiveString phrase(static_cast<size_t>(env->GetArrayLength(phrase_bytes)));
env->GetByteArrayRegion(phrase_bytes, 0, static_cast<jsize>(phrase.value.size()),
reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return;
SendScope scope(env, client, phrase, callbacks, id, we::SecretAccessReason::kPrepareKeyRotation);
// Use provider time for the signature rather than the device clock.
const auto snapshot = client.wallet->refresh().snapshot;
if (snapshot.account_resource.phase != we::ResourcePhase::kReady || !snapshot.account)
throw std::runtime_error(snapshot.account_resource.error
? snapshot.account_resource.error->developer_message : "Could not load wallet account");
constexpr uint64_t validity_seconds = 120;
if (snapshot.account->sync_utime > std::numeric_limits<uint32_t>::max() - validity_seconds)
throw std::runtime_error("Key rotation expiration exceeds the supported range");
auto prepared = client.wallet->prepare_key_rotation({
snapshot.account->sync_utime + validity_seconds, we::KeyRotationMessageKind::kExternal});
client.hosts->secret = nullptr;
{
SensitiveString replacement(0);
replacement.value.swap(prepared.replacement_recovery_phrase.phrase);
if (prepared.new_public_key.size() != 32)
throw std::runtime_error("Unexpected signing public key size");
auto public_key = env->NewByteArray(32);
if (!public_key) return;
env->SetByteArrayRegion(public_key, 0, 32,
reinterpret_cast<const jbyte *>(prepared.new_public_key.data()));
if (env->ExceptionCheck()) return;
auto bytes = env->NewByteArray(static_cast<jsize>(replacement.value.size()));
if (!bytes) return;
env->SetByteArrayRegion(bytes, 0, static_cast<jsize>(replacement.value.size()),
reinterpret_cast<const jbyte *>(replacement.value.data()));
if (env->ExceptionCheck()) return;
// This callback waits for Java to finish saving the phrase. Never submit first.
env->CallVoidMethod(callbacks, replacement_callback, bytes, public_key);
if (env->ExceptionCheck()) return;
env->DeleteLocalRef(bytes);
env->DeleteLocalRef(public_key);
}
client.wallet->send_boc({id, false, prepared.signed_boc, prepared.seqno, prepared.valid_until});
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not rotate wallet key");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not rotate wallet key");
}
}
extern "C" JNIEXPORT void JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeClose(JNIEnv *env, jclass, jlong handle) {
try {
std::shared_ptr<we::WalletClient> client;
{
std::lock_guard<std::mutex> lock(clients_mutex);
auto it = clients.find(handle);
if (it == clients.end()) return;
client = std::move(it->second.wallet);
clients.erase(it);
}
// Never hold the registry lock while entering Rust or running host callbacks.
client->shutdown();
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not close wallet engine");
}
}
extern "C" JNIEXPORT jobject JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativeEmulateSendNFT(
JNIEnv *env, jclass, jlong handle, jstring recipient, jstring nft_address,
jbyteArray comment_bytes, jstring operation_id, jlong attached, jlong forward) {
try {
auto client = get_client(handle);
const auto destination = java_string(env, recipient);
const auto intent = nft_transfer_intent(env, comment_bytes, destination, java_string(env, nft_address), attached, forward);
const auto preview = client.wallet->preview_nft_transfer({java_string(env, operation_id), intent});
// The transferred asset is an NFT, so don't present attached execution funds as a coin payment.
return preview_transaction(env, preview, destination, 0);
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not preview NFT transfer");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not preview NFT transfer");
}
return nullptr;
}
extern "C" JNIEXPORT jobject JNICALL
Java_org_telegram_ui_Wallet_WalletEngine2_nativePrepareSendNFT(
JNIEnv *env, jclass, jlong handle, jbyteArray phrase_bytes, jstring recipient,
jstring nft_address, jbyteArray comment_bytes, jstring operation_id, jlong attached, jlong forward) {
try {
auto client = get_client(handle);
const auto intent = nft_transfer_intent(env, comment_bytes, java_string(env, recipient), java_string(env, nft_address), attached, forward);
if (!phrase_bytes) throw std::invalid_argument("Recovery phrase is required");
SensitiveString phrase(static_cast<size_t>(env->GetArrayLength(phrase_bytes)));
env->GetByteArrayRegion(phrase_bytes, 0, static_cast<jsize>(phrase.value.size()),
reinterpret_cast<jbyte *>(phrase.value.data()));
if (env->ExceptionCheck()) return nullptr;
SecretScope scope(client, phrase, we::SecretAccessReason::kSignTransfer);
const auto prepared = client.wallet->prepare_nft_transfer({java_string(env, operation_id), intent});
const auto external = env->NewStringUTF(prepared.external_boc.c_str());
if (!external) return nullptr;
const auto cls = env->FindClass("org/telegram/ui/Wallet/WalletEngine2$PreparedSend");
if (!cls) return nullptr;
const auto constructor = env->GetMethodID(cls, "<init>",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
if (!constructor) return nullptr;
// The current API requests normal delivery; no relayer/gasless route was selected.
return env->NewObject(cls, constructor, external, nullptr, nullptr);
} catch (const std::bad_alloc &) {
throw_java(env, "java/lang/OutOfMemoryError", "Could not prepare NFT transfer");
} catch (const std::exception &error) {
throw_java(env, "java/lang/IllegalStateException", error.what());
} catch (...) {
throw_java(env, "java/lang/IllegalStateException", "Could not prepare NFT transfer");
}
return nullptr;
}