286 lines
12 KiB
Plaintext
286 lines
12 KiB
Plaintext
#import "KoinsMoneroWallet.h"
|
|
|
|
#import <os/log.h>
|
|
|
|
#include "monero_wallet_full.h"
|
|
|
|
static std::string walletPath(NSString *name) {
|
|
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
|
NSString *dir = paths.firstObject ?: NSTemporaryDirectory();
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
if (![fm fileExistsAtPath:dir]) {
|
|
[fm createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil];
|
|
}
|
|
return std::string([dir stringByAppendingPathComponent:name].UTF8String);
|
|
}
|
|
|
|
static monero_wallet_full *g_wallet = nullptr;
|
|
static NSString *g_daemonURL = @"xmr-node.cakewallet.com:18081";
|
|
static NSArray<NSString *> *g_fallbackDaemons = nil;
|
|
static std::atomic<bool> s_cachedConnected(false);
|
|
static std::atomic<unsigned long long> s_cachedDaemonHeight(0);
|
|
static std::atomic<long long> s_lastDaemonProbe(0);
|
|
|
|
static void connectDaemon(monero_wallet_full *w) {
|
|
if (!w) return;
|
|
@autoreleasepool {
|
|
if (!g_fallbackDaemons) {
|
|
g_fallbackDaemons = @[
|
|
@"xmr-node.cakewallet.com:18081",
|
|
@"node.monerodevs.org:18089",
|
|
@"monero.stackwallet.com:18081",
|
|
];
|
|
}
|
|
NSMutableArray<NSString *> *candidates = [NSMutableArray arrayWithObject:g_daemonURL];
|
|
for (NSString *url in g_fallbackDaemons) {
|
|
if (![url isEqualToString:g_daemonURL]) [candidates addObject:url];
|
|
}
|
|
for (NSString *url in candidates) {
|
|
try {
|
|
w->set_daemon_connection(std::string(url.UTF8String));
|
|
if (w->is_connected_to_daemon()) {
|
|
g_daemonURL = url;
|
|
return;
|
|
}
|
|
} catch (...) {
|
|
// Try the next daemon.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@implementation KoinsMoneroWallet
|
|
|
|
+ (void)invoke:(NSString *)jsonRequest
|
|
completion:(void (^)(NSString *response))completion {
|
|
static dispatch_queue_t invokeQueue;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
invokeQueue = dispatch_queue_create("com.koins.monero.invoke", DISPATCH_QUEUE_SERIAL);
|
|
});
|
|
|
|
dispatch_async(invokeQueue, ^{
|
|
NSMutableDictionary *resp = [NSMutableDictionary new];
|
|
try {
|
|
NSData *data = [jsonRequest dataUsingEncoding:NSUTF8StringEncoding];
|
|
NSDictionary *req = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
NSString *op = req[@"op"] ?: @"";
|
|
resp[@"op"] = op;
|
|
|
|
if ([op isEqualToString:@"setDaemon"]) {
|
|
NSString *url = req[@"url"];
|
|
if (url.length > 0) g_daemonURL = url;
|
|
resp[@"url"] = g_daemonURL;
|
|
} else if ([op isEqualToString:@"createWallet"]) {
|
|
NSString *name = req[@"name"];
|
|
std::string path = walletPath(name);
|
|
std::string password = std::string([req[@"password"] UTF8String]);
|
|
bool exists = false;
|
|
try { exists = monero_wallet_full::wallet_exists(path); } catch (...) {}
|
|
if (exists) {
|
|
g_wallet = monero_wallet_full::open_wallet(path, password, monero_network_type::MAINNET, false);
|
|
connectDaemon(g_wallet);
|
|
resp[@"address"] = [NSString stringWithUTF8String:g_wallet->get_address(0, 0).c_str()];
|
|
try { resp[@"mnemonic"] = [NSString stringWithUTF8String:g_wallet->get_seed().c_str()]; } catch (...) {}
|
|
} else {
|
|
monero_wallet_config config;
|
|
config.m_path = path;
|
|
config.m_password = password;
|
|
config.m_network_type = monero_network_type::MAINNET;
|
|
config.m_language = std::string([req[@"language"] ?: @"English" UTF8String]);
|
|
config.m_save_current = false;
|
|
monero_wallet_full *w = monero_wallet_full::create_wallet(config, nullptr);
|
|
g_wallet = w;
|
|
connectDaemon(w);
|
|
resp[@"address"] = [NSString stringWithUTF8String:w->get_address(0, 0).c_str()];
|
|
resp[@"mnemonic"] = [NSString stringWithUTF8String:w->get_seed().c_str()];
|
|
}
|
|
} else if ([op isEqualToString:@"openWallet"] || [op isEqualToString:@"restoreWallet"]) {
|
|
if (g_wallet) {
|
|
try { g_wallet->close(true); } catch (...) {}
|
|
delete g_wallet;
|
|
g_wallet = nullptr;
|
|
}
|
|
if ([op isEqualToString:@"openWallet"]) {
|
|
g_wallet = monero_wallet_full::open_wallet(
|
|
walletPath(req[@"name"]),
|
|
std::string([req[@"password"] UTF8String]),
|
|
monero_network_type::MAINNET, false);
|
|
} else {
|
|
monero_wallet_config config;
|
|
config.m_path = walletPath(req[@"name"]);
|
|
config.m_password = std::string([req[@"password"] UTF8String]);
|
|
config.m_network_type = monero_network_type::MAINNET;
|
|
config.m_seed = std::string([req[@"seed"] UTF8String]);
|
|
config.m_restore_height = (uint64_t)[req[@"restoreHeight"] longLongValue];
|
|
config.m_language = "English";
|
|
config.m_save_current = false;
|
|
g_wallet = monero_wallet_full::create_wallet(config, nullptr);
|
|
}
|
|
connectDaemon(g_wallet);
|
|
resp[@"address"] = [NSString stringWithUTF8String:g_wallet->get_address(0, 0).c_str()];
|
|
} else if ([op isEqualToString:@"getSeed"]) {
|
|
if (g_wallet) {
|
|
resp[@"mnemonic"] = [NSString stringWithUTF8String:g_wallet->get_seed().c_str()];
|
|
} else {
|
|
resp[@"error"] = @"No wallet open";
|
|
}
|
|
} else if ([op isEqualToString:@"removeWallet"]) {
|
|
NSString *name = req[@"name"];
|
|
if (g_wallet) {
|
|
try { g_wallet->close(false); } catch (...) {}
|
|
delete g_wallet;
|
|
g_wallet = nullptr;
|
|
}
|
|
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
|
NSString *dir = paths.firstObject;
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
for (NSString *ext in @[ @"", @".keys", @".address.txt", @".mms" ]) {
|
|
NSString *file = [dir stringByAppendingPathComponent:[name stringByAppendingString:ext]];
|
|
[fm removeItemAtPath:file error:nil];
|
|
}
|
|
} else if ([op isEqualToString:@"closeWallet"]) {
|
|
if (g_wallet) {
|
|
try { g_wallet->close(true); } catch (...) {}
|
|
delete g_wallet;
|
|
g_wallet = nullptr;
|
|
}
|
|
} else if ([op isEqualToString:@"saveWallet"]) {
|
|
// monero-cpp never persists while syncing, so scan progress survives a
|
|
// restart only if this is called explicitly. save() pauses the sync
|
|
// thread while it writes.
|
|
if (g_wallet) {
|
|
g_wallet->save();
|
|
resp[@"saved"] = @YES;
|
|
} else {
|
|
resp[@"error"] = @"No wallet open";
|
|
}
|
|
} else if ([op isEqualToString:@"getBalance"]) {
|
|
if (g_wallet) {
|
|
uint64_t balance = 0, unlocked = 0;
|
|
try {
|
|
balance = g_wallet->get_balance();
|
|
unlocked = g_wallet->get_unlocked_balance();
|
|
} catch (const std::exception &e) {
|
|
os_log_error(OS_LOG_DEFAULT, "[koins] getBalance: %{public}s", e.what());
|
|
}
|
|
resp[@"balance"] = @(balance).stringValue;
|
|
resp[@"unlocked"] = @(unlocked).stringValue;
|
|
} else {
|
|
resp[@"error"] = @"No wallet open";
|
|
}
|
|
} else if ([op isEqualToString:@"getAddress"]) {
|
|
if (g_wallet) {
|
|
resp[@"address"] = [NSString stringWithUTF8String:g_wallet->get_address(0, 0).c_str()];
|
|
} else {
|
|
resp[@"error"] = @"No wallet open";
|
|
}
|
|
} else if ([op isEqualToString:@"getTransactions"]) {
|
|
NSMutableArray *entries = [NSMutableArray new];
|
|
if (!g_wallet) {
|
|
resp[@"transactions"] = entries;
|
|
} else {
|
|
try {
|
|
monero_transfer_query query;
|
|
for (const auto &transfer : g_wallet->get_transfers(query)) {
|
|
const auto &tx = transfer->m_tx;
|
|
const auto isIn = transfer->is_incoming();
|
|
boost::optional<uint64_t> txHeight = tx ? tx->get_height() : boost::none;
|
|
[entries addObject:@{
|
|
@"hash" : [NSString stringWithUTF8String:((tx && tx->m_hash) ? *tx->m_hash : "").c_str()],
|
|
@"amount" : @((transfer->m_amount ? *transfer->m_amount : 0)).stringValue,
|
|
@"timestamp" : @((tx && tx->m_received_timestamp) ? *tx->m_received_timestamp : 0),
|
|
@"direction" : (isIn && *isIn) ? @"in" : @"out",
|
|
@"confirmed" : @((tx && tx->m_is_confirmed) ? *tx->m_is_confirmed : false),
|
|
@"height" : @(txHeight ? *txHeight : 0),
|
|
@"fee" : @((tx && tx->m_fee) ? *tx->m_fee : 0).stringValue,
|
|
}];
|
|
}
|
|
} catch (const std::exception &e) {
|
|
os_log_error(OS_LOG_DEFAULT, "[koins] getTransactions: %{public}s", e.what());
|
|
}
|
|
resp[@"transactions"] = entries;
|
|
}
|
|
} else if ([op isEqualToString:@"startSync"]) {
|
|
if (g_wallet) {
|
|
connectDaemon(g_wallet);
|
|
try {
|
|
if (g_wallet->is_connected_to_daemon()) {
|
|
g_wallet->start_syncing(5000);
|
|
resp[@"syncing"] = @YES;
|
|
} else {
|
|
resp[@"syncing"] = @NO;
|
|
}
|
|
} catch (const std::exception &e) {
|
|
resp[@"syncing"] = @NO;
|
|
}
|
|
} else {
|
|
resp[@"error"] = @"No wallet open";
|
|
}
|
|
} else if ([op isEqualToString:@"stopSync"]) {
|
|
try { if (g_wallet) g_wallet->stop_syncing(); } catch (...) {}
|
|
} else if ([op isEqualToString:@"getSyncStatus"]) {
|
|
uint64_t height = 0, daemonHeight = 0;
|
|
bool connected = false;
|
|
if (g_wallet) {
|
|
try {
|
|
height = g_wallet->get_height();
|
|
// is_connected_to_daemon performs a live round trip; public
|
|
// daemons rate limit aggressive polling, so cache for 10s.
|
|
long long now = (long long)([[NSDate date] timeIntervalSince1970] * 1000);
|
|
if (now - s_lastDaemonProbe.load() > 10000) {
|
|
s_lastDaemonProbe.store(now);
|
|
connected = g_wallet->is_connected_to_daemon();
|
|
s_cachedConnected = connected;
|
|
} else {
|
|
connected = s_cachedConnected;
|
|
}
|
|
daemonHeight = s_cachedDaemonHeight;
|
|
if (connected) {
|
|
daemonHeight = g_wallet->get_daemon_height();
|
|
s_cachedDaemonHeight = daemonHeight;
|
|
}
|
|
} catch (const std::exception &e) {
|
|
os_log_error(OS_LOG_DEFAULT, "[koins] getSyncStatus: %{public}s", e.what());
|
|
}
|
|
}
|
|
resp[@"height"] = @(height);
|
|
resp[@"daemonHeight"] = @(daemonHeight);
|
|
resp[@"syncing"] = @(connected && height < daemonHeight);
|
|
resp[@"connected"] = @(connected);
|
|
} else if ([op isEqualToString:@"transferTo"]) {
|
|
if (!g_wallet) {
|
|
resp[@"error"] = @"No wallet open";
|
|
} else {
|
|
monero_tx_config config;
|
|
auto destination = std::make_shared<monero_destination>();
|
|
destination->m_address = std::string([req[@"address"] UTF8String]);
|
|
destination->m_amount = (uint64_t)[req[@"amount"] longLongValue];
|
|
config.m_destinations = { destination };
|
|
config.m_priority = (monero_tx_priority)[req[@"priority"] integerValue];
|
|
config.m_relay = true;
|
|
auto txs = g_wallet->create_txs(config);
|
|
const auto &txSet = txs[0]->m_tx_set;
|
|
std::string signedHex = (txSet && txSet->m_signed_tx_hex) ? *txSet->m_signed_tx_hex : "";
|
|
g_wallet->submit_txs(signedHex);
|
|
resp[@"txHash"] = [NSString stringWithUTF8String:(txs[0]->m_hash ? *txs[0]->m_hash : "").c_str()];
|
|
resp[@"fee"] = @((txs[0]->m_fee ? *txs[0]->m_fee : 0)).stringValue;
|
|
}
|
|
} else {
|
|
resp[@"error"] = [NSString stringWithFormat:@"Unknown op: %@", op];
|
|
}
|
|
} catch (const std::exception &e) {
|
|
os_log_error(OS_LOG_DEFAULT, "[koins] monero %{public}@ failed: %{public}s",
|
|
resp[@"op"], e.what());
|
|
resp[@"error"] = [NSString stringWithUTF8String:e.what()];
|
|
} catch (...) {
|
|
resp[@"error"] = @"Unknown native exception";
|
|
}
|
|
|
|
NSData *out = [NSJSONSerialization dataWithJSONObject:resp options:0 error:nil];
|
|
NSString *outStr = [[NSString alloc] initWithData:out encoding:NSUTF8StringEncoding];
|
|
completion(outStr ?: @"{\"error\":\"encode failed\"}");
|
|
});
|
|
}
|
|
|
|
@end |