Replace the JavaScript mobile app with a native Swift iOS wallet

This commit is contained in:
2026-09-19 13:33:11 +08:00
parent d80b58b704
commit 3e1a39d4d7
79 changed files with 25482 additions and 4023 deletions
+10
View File
@@ -8,3 +8,13 @@
- Never publish without explicit permission. Before any publish action — running `bun run build:stable`, uploading a release asset to Gitea (`https://gitea.tzgyn.dev/TZGyn/koins`), updating the Homebrew cask, or pushing the tap repo (`https://gitea.tzgyn.dev/TZGyn/homebrew-tap`) — describe the intended action (what will be built, what SHA, which release) and ask for permission. Once you have asked and received a clear affirmative (any clear yes, "go", "publish", etc.), you may proceed through the whole pipeline without pausing between steps: build, notarize, verify, upload to the Gitea release, update the Homebrew cask, and push the Gitea tap repo. One permission covers the entire publish flow. You must still get a separate permission before starting each new publish cycle.
- Permission to commit ("commit", "commit and push", or similar) authorizes `git commit` and also `git push` of the committed changes. Only push the branches/files described in the permission.
- At the end of every response, always surface a "Pending from you" section listing anything you need from the user (decisions, permissions, credentials, confirming a plan, etc.). If nothing is pending, say "Nothing pending." List each pending item as a numbered entry (`1.`, `2.`, …) so the user can approve or act on them by number (e.g. "go ahead with 1, 2").
## iOS app (`apps/ios`)
Native SwiftUI wallet for Monero and XRP — no JavaScript runtime, no CocoaPods, no Swift packages.
- Regenerate the project after adding or removing files: `xcodegen generate` (from `apps/ios`).
- Simulator build: `xcodebuild -project Koins.xcodeproj -scheme Koins -configuration Debug -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' ARCHS=arm64 ONLY_ACTIVE_ARCH=NO -derivedDataPath /tmp/koins-dd build`.
- Crypto/serialization self-test: launch with `-koinsSelfTest`, then read `log show --predicate 'subsystem == "com.koins.app"'`.
- Live network self-test: launch with `-koinsSelfTestNetwork`.
- See `apps/ios/README.md` for the full layout and verification workflow.
+9
View File
@@ -0,0 +1,9 @@
# Generated by XcodeGen from project.yml
Koins.xcodeproj/
# Build output
build/
DerivedData/
*.xcworkspace
xcuserdata/
*.xcuserstate
+337
View File
@@ -0,0 +1,337 @@
import Foundation
import SwiftUI
@MainActor
final class AppModel: ObservableObject {
@Published var chain: Chain = .xrp
@Published var xrpWallet: WalletRecord?
@Published var xmrWallet: WalletRecord?
@Published var balance = Balance(total: "0", unlocked: "", funded: false)
@Published var transactions: [TransactionRecord] = []
@Published var syncStatus: SyncStatus?
@Published var isBusy = false
@Published var errorMessage: String?
@Published var statusMessage: String?
@Published var moneroReady = false
@Published var moneroAddress: String?
/// Rough time remaining for the Monero sync, derived from observed block rate.
@Published var syncEta: String?
private var moneroOpened = false
private var refreshTick = 0
private var refreshTask: Task<Void, Never>?
private var lastSample: (height: UInt64, date: Date)?
var wallet: WalletRecord? {
chain == .xrp ? xrpWallet : xmrWallet
}
var hasWallet: Bool { wallet != nil }
/// Address for the chain currently on screen. Kept chain-aware because the
/// Monero address stays cached once its wallet is open.
var displayAddress: String {
chain == .xrp ? (xrpWallet?.address ?? "") : (moneroAddress ?? xmrWallet?.address ?? "")
}
var symbol: String { chain.symbol }
// MARK: - Bootstrap
func bootstrap() async {
xrpWallet = WalletStore.wallet(.xrp)
xmrWallet = WalletStore.wallet(.xmr)
if xmrWallet == nil, let orphan = MoneroService.orphanedWalletNames().first {
// Wallet file on disk with no stored record (for example after the
// preferences were lost): re-attach it so funds are not stranded.
await adoptMoneroWallet(named: orphan)
}
if let xmrWallet {
await openMonero(afterRestore: false, wallet: xmrWallet)
}
if xrpWallet != nil || moneroReady {
chain = xrpWallet != nil ? .xrp : .xmr
}
await refresh(force: true)
startAutoRefresh()
}
private func adoptMoneroWallet(named name: String) async {
do {
let address = try await MoneroService.shared.openWallet(name: name)
let record = WalletRecord(id: "xmr_\(Int(Date().timeIntervalSince1970))",
name: name,
address: address,
createdAt: Date())
xmrWallet = record
WalletStore.saveWallet(record, chain: .xmr)
moneroOpened = true
moneroReady = true
moneroAddress = address
statusMessage = "Recovered the existing Monero wallet “\(name)” from this device."
} catch {
errorMessage = "Found a Monero wallet file but could not open it: \(error.localizedDescription)"
}
}
/// Polls the wallet while the app is in the foreground. Monero sync progress
/// changes constantly, so it is refreshed far more often than XRP.
func startAutoRefresh() {
guard refreshTask == nil else { return }
refreshTask = Task { [weak self] in
while !Task.isCancelled {
guard let self else { return }
let seconds: UInt64 = self.chain == .xmr ? 5 : 30
try? await Task.sleep(nanoseconds: seconds * 1_000_000_000)
if Task.isCancelled { return }
await self.refresh()
}
}
}
func select(_ newChain: Chain) async {
chain = newChain
errorMessage = nil
statusMessage = nil
balance = Balance(total: "0", unlocked: "", funded: false)
transactions = []
syncStatus = nil
if newChain == .xmr, let xmrWallet, !moneroOpened {
await openMonero(afterRestore: false, wallet: xmrWallet)
}
await refresh(force: true)
}
// MARK: - Monero lifecycle
private func openMonero(afterRestore: Bool, wallet: WalletRecord) async {
isBusy = true
defer { isBusy = false }
do {
let address = try await MoneroService.shared.openWallet(name: wallet.name)
moneroOpened = true
moneroReady = true
moneroAddress = address.isEmpty ? wallet.address : address
if let address = moneroAddress, address != wallet.address {
var updated = wallet
updated.address = address
xmrWallet = updated
WalletStore.saveWallet(updated, chain: .xmr)
}
// Always try to catch up, not only right after a restore.
_ = try? await MoneroService.shared.startSync()
} catch {
// Keep the stored wallet so a transient daemon failure is not fatal.
moneroOpened = false
moneroReady = false
moneroAddress = nil
errorMessage = "Monero wallet could not be opened: \(error.localizedDescription)"
}
}
// MARK: - Create / import
func createWallet(name: String) async throws -> String {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { throw XRPError.rpc("Enter a wallet name") }
isBusy = true
defer { isBusy = false }
if chain == .xrp {
let (record, seed) = try XRPWalletService.shared.createWallet(name: trimmed)
xrpWallet = record
await refresh()
return seed
}
let result = try await MoneroService.shared.createWallet(name: trimmed)
let record = WalletRecord(id: "xmr_\(Int(Date().timeIntervalSince1970))",
name: trimmed,
address: result.address,
createdAt: Date())
xmrWallet = record
WalletStore.saveWallet(record, chain: .xmr)
if !result.mnemonic.isEmpty {
WalletStore.saveSecret(result.mnemonic, chain: .xmr)
}
moneroOpened = true
moneroReady = true
moneroAddress = result.address
_ = try? await MoneroService.shared.startSync()
await refresh(force: true)
return result.mnemonic
}
func importWallet(name: String, secret: String, restoreHeight: UInt64?) async throws {
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedSecret = secret.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedName.isEmpty else { throw XRPError.rpc("Enter a wallet name") }
guard !trimmedSecret.isEmpty else { throw XRPError.rpc("Enter your seed or secret") }
isBusy = true
defer { isBusy = false }
if chain == .xrp {
xrpWallet = try XRPWalletService.shared.importWallet(name: trimmedName, secret: trimmedSecret)
await refresh(force: true)
return
}
let address = try await MoneroService.shared.restoreWallet(name: trimmedName,
seed: trimmedSecret,
restoreHeight: restoreHeight ?? 0)
let record = WalletRecord(id: "xmr_\(Int(Date().timeIntervalSince1970))",
name: trimmedName,
address: address,
createdAt: Date())
xmrWallet = record
WalletStore.saveWallet(record, chain: .xmr)
WalletStore.saveSecret(trimmedSecret, chain: .xmr)
moneroOpened = true
moneroReady = true
moneroAddress = address
_ = try? await MoneroService.shared.startSync()
await refresh(force: true)
}
// MARK: - Reads
func refresh(force: Bool = false) async {
guard hasWallet else { return }
errorMessage = nil
if chain == .xrp, let wallet = xrpWallet {
do {
async let balanceCall = XRPWalletService.shared.balance(address: wallet.address)
async let txCall = XRPWalletService.shared.transactions(address: wallet.address)
balance = try await balanceCall
transactions = try await txCall
} catch {
errorMessage = error.localizedDescription
}
// The Monero wallet keeps scanning while XRP is on screen, so keep its
// progress current here too.
if moneroReady, let status = try? await MoneroService.shared.syncStatus() {
updateSyncStatus(status)
}
return
}
guard moneroReady else { return }
do {
// Sync progress is cheap and polled every tick; the balance and transfer
// list touch the wallet's data files, so they are refreshed less often.
updateSyncStatus(try await MoneroService.shared.syncStatus())
refreshTick += 1
if force || refreshTick == 1 || refreshTick % 4 == 0 {
async let balanceCall = MoneroService.shared.balance()
async let txCall = MoneroService.shared.transactions()
balance = try await balanceCall
transactions = try await txCall
}
} catch {
errorMessage = error.localizedDescription
}
}
/// Tracks how fast blocks are being scanned so the UI can show an estimate.
private func updateSyncStatus(_ status: SyncStatus) {
syncStatus = status
let now = Date()
defer { lastSample = (status.height, now) }
guard status.syncing,
let sample = lastSample,
status.height > sample.height,
status.daemonHeight > status.height else {
if !status.syncing { syncEta = nil }
return
}
let elapsed = now.timeIntervalSince(sample.date)
guard elapsed > 1 else { return }
let blocksPerSecond = Double(status.height - sample.height) / elapsed
guard blocksPerSecond > 0 else { return }
let remaining = Double(status.daemonHeight - status.height) / blocksPerSecond
syncEta = Self.formatDuration(remaining)
}
private static func formatDuration(_ seconds: Double) -> String {
guard seconds.isFinite, seconds > 0 else { return "unknown" }
let total = Int(seconds.rounded())
let hours = total / 3600
let minutes = (total % 3600) / 60
if hours >= 24 { return "\(hours / 24)d \(hours % 24)h" }
if hours > 0 { return "\(hours)h \(minutes)m" }
if minutes > 0 { return "\(minutes)m" }
return "under a minute"
}
func startMoneroSync() async {
guard moneroReady else {
if let wallet = xmrWallet { await openMonero(afterRestore: true, wallet: wallet) }
return
}
_ = try? await MoneroService.shared.startSync()
await refresh(force: true)
}
func revealSeed() async throws -> String {
if chain == .xrp {
guard let seed = WalletStore.secret(.xrp) else {
throw XRPError.rpc("No stored secret for this wallet")
}
return seed
}
if let stored = WalletStore.secret(.xmr), !stored.isEmpty { return stored }
return try await MoneroService.shared.seed()
}
// MARK: - Sends
func send(to: String, amount: String, tag: String, priority: TransactionPriority) async throws {
let destinationTag = UInt32(tag.trimmingCharacters(in: .whitespaces))
if chain == .xrp {
guard let seed = WalletStore.secret(.xrp) else {
throw XRPError.rpc("Wallet secret is unavailable")
}
statusMessage = nil
_ = try await XRPWalletService.shared.send(secret: seed,
to: to,
amountXRP: amount,
destinationTag: destinationTag)
statusMessage = "Payment sent"
await refresh(force: true)
return
}
_ = try await MoneroService.shared.send(address: to, amountXMR: amount, priority: priority)
statusMessage = "Payment sent"
await refresh(force: true)
}
// MARK: - Delete
func deleteWallet() async {
if chain == .xrp {
WalletStore.removeWallet(.xrp)
xrpWallet = nil
} else {
if let wallet = xmrWallet { await MoneroService.shared.removeWallet(name: wallet.name) }
WalletStore.removeWallet(.xmr)
xmrWallet = nil
moneroOpened = false
moneroReady = false
moneroAddress = nil
syncStatus = nil
}
balance = Balance(total: "0", unlocked: "", funded: false)
transactions = []
errorMessage = nil
statusMessage = nil
}
func clearMessages() {
errorMessage = nil
statusMessage = nil
}
}
@@ -0,0 +1,2 @@
#import "KoinsMoneroWallet.h"
#import "KoinsSecp256k1.h"
+16
View File
@@ -0,0 +1,16 @@
#import <Foundation/Foundation.h>
#import "KoinsSecp256k1.h"
NS_ASSUME_NONNULL_BEGIN
/// Objective-C++ bridge to the on-device monero-cpp wallet. A single JSON
/// in/out entry point keeps the C++ surface out of Swift; work is serialized
/// on a private queue because a monero_wallet_full is not thread safe.
@interface KoinsMoneroWallet : NSObject
+ (void)invoke:(NSString *)jsonRequest
completion:(void (^)(NSString *response))completion;
@end
NS_ASSUME_NONNULL_END
+276
View File
@@ -0,0 +1,276 @@
#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:@"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
+25
View File
@@ -0,0 +1,25 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// secp256k1 helpers used by the XRP wallet. Backed by the OpenSSL build that
/// already ships inside the on-device monero static library, so no extra
/// dependency is linked.
@interface KoinsSecp256k1 : NSObject
/// XRP account private scalar derived from a 16-byte family seed entropy.
+ (nullable NSData *)privateKeyFromEntropy:(NSData *)entropy;
/// 33-byte compressed public key for a 32-byte private scalar.
+ (nullable NSData *)publicKeyFromPrivateKey:(NSData *)privateKey;
/// DER-encoded, low-S ECDSA signature over a 32-byte digest.
+ (nullable NSData *)signDigest:(NSData *)digest
withPrivateKey:(NSData *)privateKey;
/// (left + right) mod n, used by BIP-32 hardened child derivation.
+ (nullable NSData *)addScalar:(NSData *)left toScalar:(NSData *)right;
@end
NS_ASSUME_NONNULL_END
+187
View File
@@ -0,0 +1,187 @@
#import "KoinsSecp256k1.h"
#import <openssl/bn.h>
#import <openssl/ec.h>
#import <openssl/ecdsa.h>
#import <openssl/obj_mac.h>
#import <openssl/sha.h>
// secp256k1 group order.
static const char *kCurveOrderHex =
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141";
static BIGNUM *KoinsCurveOrder(void) {
BIGNUM *order = NULL;
BN_hex2bn(&order, kCurveOrderHex);
return order;
}
static NSData *KoinsSHA512Half(NSData *input) {
unsigned char digest[SHA512_DIGEST_LENGTH];
SHA512((const unsigned char *)input.bytes, (size_t)input.length, digest);
return [NSData dataWithBytes:digest length:32];
}
// Mirror of ripple-keypairs deriveScalar: hash the bytes (optionally with a
// 32-bit discriminator) until the result is a valid non-zero curve scalar.
static BIGNUM *KoinsDeriveScalar(NSData *bytes, BOOL hasDiscriminator,
uint32_t discriminator, const BIGNUM *order) {
for (uint32_t i = 0; i <= 4294967295u; i++) {
NSMutableData *input = [NSMutableData dataWithData:bytes];
if (hasDiscriminator) {
uint32_t d = htonl(discriminator);
[input appendBytes:&d length:4];
}
uint32_t idx = htonl(i);
[input appendBytes:&idx length:4];
NSData *half = KoinsSHA512Half(input);
BIGNUM *scalar = BN_bin2bn((const unsigned char *)half.bytes, (int)half.length, NULL);
if (scalar && !BN_is_zero(scalar) && BN_cmp(scalar, order) < 0) {
return scalar;
}
if (scalar) BN_free(scalar);
}
return NULL;
}
@implementation KoinsSecp256k1
+ (nullable NSData *)privateKeyFromEntropy:(NSData *)entropy {
if (entropy.length != 16) return nil;
BN_CTX *ctx = BN_CTX_new();
BIGNUM *order = KoinsCurveOrder();
EC_GROUP *group = EC_GROUP_new_by_curve_name(NID_secp256k1);
EC_POINT *publicGen = EC_POINT_new(group);
NSData *result = nil;
BIGNUM *privateGen = KoinsDeriveScalar(entropy, NO, 0, order);
if (privateGen && EC_POINT_mul(group, publicGen, privateGen, NULL, NULL, ctx)) {
unsigned char compressed[33];
size_t written = EC_POINT_point2oct(group, publicGen,
POINT_CONVERSION_COMPRESSED,
compressed, sizeof(compressed), ctx);
if (written == sizeof(compressed)) {
NSData *publicGenBytes = [NSData dataWithBytes:compressed length:written];
BIGNUM *offset = KoinsDeriveScalar(publicGenBytes, YES, 0, order);
if (offset) {
BIGNUM *derived = BN_new();
if (BN_mod_add(derived, offset, privateGen, order, ctx)) {
unsigned char out[32] = {0};
BN_bn2binpad(derived, out, sizeof(out));
result = [NSData dataWithBytes:out length:sizeof(out)];
}
BN_free(derived);
BN_free(offset);
}
}
}
if (privateGen) BN_free(privateGen);
if (publicGen) EC_POINT_free(publicGen);
if (group) EC_GROUP_free(group);
if (order) BN_free(order);
if (ctx) BN_CTX_free(ctx);
return result;
}
+ (nullable NSData *)publicKeyFromPrivateKey:(NSData *)privateKey {
if (privateKey.length != 32) return nil;
BN_CTX *ctx = BN_CTX_new();
EC_GROUP *group = EC_GROUP_new_by_curve_name(NID_secp256k1);
EC_POINT *point = EC_POINT_new(group);
BIGNUM *scalar = BN_bin2bn((const unsigned char *)privateKey.bytes, (int)privateKey.length, NULL);
NSData *result = nil;
if (scalar && EC_POINT_mul(group, point, scalar, NULL, NULL, ctx)) {
unsigned char compressed[33];
size_t written = EC_POINT_point2oct(group, point,
POINT_CONVERSION_COMPRESSED,
compressed, sizeof(compressed), ctx);
if (written == sizeof(compressed)) {
result = [NSData dataWithBytes:compressed length:written];
}
}
if (scalar) BN_free(scalar);
if (point) EC_POINT_free(point);
if (group) EC_GROUP_free(group);
if (ctx) BN_CTX_free(ctx);
return result;
}
+ (nullable NSData *)addScalar:(NSData *)left toScalar:(NSData *)right {
if (left.length != 32 || right.length != 32) return nil;
BN_CTX *ctx = BN_CTX_new();
BIGNUM *order = KoinsCurveOrder();
BIGNUM *a = BN_bin2bn((const unsigned char *)left.bytes, (int)left.length, NULL);
BIGNUM *b = BN_bin2bn((const unsigned char *)right.bytes, (int)right.length, NULL);
BIGNUM *sum = BN_new();
NSData *result = nil;
if (a && b && sum && BN_mod_add(sum, a, b, order, ctx)) {
unsigned char out[32] = {0};
BN_bn2binpad(sum, out, sizeof(out));
result = [NSData dataWithBytes:out length:sizeof(out)];
}
if (sum) BN_free(sum);
if (b) BN_free(b);
if (a) BN_free(a);
if (order) BN_free(order);
if (ctx) BN_CTX_free(ctx);
return result;
}
+ (nullable NSData *)signDigest:(NSData *)digest
withPrivateKey:(NSData *)privateKey {
if (digest.length != 32 || privateKey.length != 32) return nil;
BN_CTX *ctx = BN_CTX_new();
EC_KEY *key = EC_KEY_new_by_curve_name(NID_secp256k1);
BIGNUM *scalar = BN_bin2bn((const unsigned char *)privateKey.bytes, (int)privateKey.length, NULL);
NSData *result = nil;
if (key && scalar && EC_KEY_set_private_key(key, scalar)) {
const EC_GROUP *group = EC_KEY_get0_group(key);
EC_POINT *pub = EC_POINT_new(group);
if (pub && EC_POINT_mul(group, pub, scalar, NULL, NULL, ctx) &&
EC_KEY_set_public_key(key, pub)) {
ECDSA_SIG *sig = ECDSA_do_sign((const unsigned char *)digest.bytes, (int)digest.length, key);
if (sig) {
// Ripple only accepts canonical (low-S) signatures.
BIGNUM *order = KoinsCurveOrder();
BIGNUM *halfOrder = BN_dup(order);
BN_rshift1(halfOrder, halfOrder);
const BIGNUM *r = NULL;
const BIGNUM *s = NULL;
ECDSA_SIG_get0(sig, &r, &s);
if (BN_cmp(s, halfOrder) > 0) {
BIGNUM *lowS = BN_new();
BN_sub(lowS, order, s);
ECDSA_SIG_set0(sig, BN_dup(r), lowS);
}
unsigned char *der = NULL;
int derLen = i2d_ECDSA_SIG(sig, &der);
if (derLen > 0) {
result = [NSData dataWithBytes:der length:(NSUInteger)derLen];
OPENSSL_free(der);
}
BN_free(halfOrder);
BN_free(order);
ECDSA_SIG_free(sig);
}
}
if (pub) EC_POINT_free(pub);
}
if (scalar) BN_free(scalar);
if (key) EC_KEY_free(key);
if (ctx) BN_CTX_free(ctx);
return result;
}
@end
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UILaunchScreen</key>
<dict/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
</dict>
</plist>
+30
View File
@@ -0,0 +1,30 @@
import SwiftUI
@main
struct KoinsApp: App {
@StateObject private var model: AppModel
init() {
Secp256k1Backend.derivePrivateKey = { entropy in
KoinsSecp256k1.privateKey(fromEntropy: entropy)
}
Secp256k1Backend.publicKey = { privateKey in
KoinsSecp256k1.publicKey(fromPrivateKey: privateKey)
}
Secp256k1Backend.sign = { digest, privateKey in
KoinsSecp256k1.signDigest(digest, withPrivateKey: privateKey)
}
Secp256k1Backend.addScalar = { left, right in
KoinsSecp256k1.addScalar(left, toScalar: right)
}
_model = StateObject(wrappedValue: AppModel())
SelfTest.runIfRequested()
}
var body: some Scene {
WindowGroup {
RootView()
.environmentObject(model)
}
}
}
+69
View File
@@ -0,0 +1,69 @@
import Foundation
enum Chain: String, Codable, CaseIterable {
case xrp
case xmr
var symbol: String { self == .xrp ? "XRP" : "XMR" }
var title: String { self == .xrp ? "XRP Ledger" : "Monero" }
var subtitle: String {
self == .xrp ? "Fast, low-fee payments" : "Private, untraceable payments"
}
}
struct WalletRecord: Codable, Identifiable, Equatable {
let id: String
var name: String
var address: String
var createdAt: Date
}
enum Direction {
case incoming
case outgoing
}
struct TransactionRecord: Identifiable {
let id: String
let amount: String
let timestamp: Date?
let direction: Direction
let confirmed: Bool
var fee: String?
var tag: Int?
var counterparty: String?
}
struct Balance {
var total: String
var unlocked: String
var funded: Bool
}
struct SyncStatus {
var height: UInt64
var daemonHeight: UInt64
var syncing: Bool
var connected: Bool
var progress: Double {
guard daemonHeight > 0 else { return 0 }
return min(1, Double(height) / Double(daemonHeight))
}
}
enum TransactionPriority: Int {
case defaultPriority = 0
case low = 1
case medium = 2
case high = 3
var label: String {
switch self {
case .defaultPriority: return "Default"
case .low: return "Low"
case .medium: return "Medium"
case .high: return "High"
}
}
}
@@ -0,0 +1,107 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include <exception>
#include <string>
/**
* Public interface for libmonero-cpp library.
*/
namespace monero {
/**
* Exception when interacting with a Monero wallet or daemon.
*/
class monero_error : public std::exception {
public:
std::string message;
monero_error() {}
/**
* Construct the exception.
*
* @param msg is a human-readable description of the error
*/
monero_error(const std::string& msg) : message(msg) {}
const char* what() const noexcept override {
return message.c_str();
}
};
/**
* Exception when interacting with the Monero daemon or wallet RPC API.
*/
class monero_rpc_error : public monero_error {
public:
int code;
/**
* Construct the exception.
*
* @param error_code is the error code (optional)
* @param msg is a human-readable description of the error
*/
monero_rpc_error(int error_code, const std::string& msg) : code(error_code) { message = msg; }
/**
* Construct the exception.
*
* @param msg is a human-readable description of the error
*/
monero_rpc_error(const std::string& msg) : code(-1) { message = msg; }
};
}
@@ -0,0 +1,439 @@
#include "monero_rpc_connection.h"
#include "utils/gen_utils.h"
#include "utils/monero_utils.h"
#include "daemon/monero_daemon_rpc_model.h"
#include "monero_error.h"
static const uint32_t DEFAULT_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
namespace monero {
// --------------------------- MONERO REQUEST PARAMS ---------------------------
rapidjson::Value monero_request_params::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const {
rapidjson::Value root(rapidjson::kObjectType);
return root;
}
// --------------------------- MONERO RPC REQUEST ---------------------------
monero_rpc_request::monero_rpc_request(const std::string& method, const std::shared_ptr<serializable_struct>& params, bool json_rpc): m_method(method), m_params(params) {
if (params == nullptr) m_params = std::make_shared<monero_request_params>();
if (json_rpc) {
m_id = "0";
m_version = "2.0";
}
}
rapidjson::Value monero_rpc_request::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const {
if (!is_json_rpc()) {
if (m_params == nullptr) throw monero_error("No params provided");
return m_params->to_rapidjson_val(allocator);
}
// create root
rapidjson::Value root(rapidjson::kObjectType);
// set string values
rapidjson::Value value_str(rapidjson::kStringType);
if (m_version != boost::none) monero_utils::add_json_member("jsonrpc", m_version.get(), allocator, root, value_str);
if (m_id != boost::none) monero_utils::add_json_member("id", m_id.get(), allocator, root, value_str);
if (m_method != boost::none) monero_utils::add_json_member("method", m_method.get(), allocator, root, value_str);
if (m_params != nullptr) root.AddMember("params", m_params->to_rapidjson_val(allocator), allocator);
// return root
return root;
}
// --------------------------- MONERO RPC RESPONSE ---------------------------
void raise_rpc_error(const boost::property_tree::ptree& error_node, const boost::string_ref uri, const std::string& method) {
std::string err_message = "Unknown error";
int err_code = -1;
for (auto it = error_node.begin(); it != error_node.end(); ++it) {
std::string key_err = it->first;
if (key_err == std::string("message")) {
if (it->second.data().empty()) {
err_message = "Received error response from RPC request with method '" + method + "' to " + uri.to_string();
}
else err_message = it->second.data();
} else if (key_err == std::string("code")) {
try {
err_code = it->second.get_value<int>();
} catch (...) {
MERROR("non-numeric error code from a malformed/unusual RPC response");
err_code = -1;
}
}
}
throw monero_rpc_error(err_code, err_message);
}
// --------------------------- MONERO RPC CONNECTION ---------------------------
bool monero_rpc_connection::compare(int p1, int p2) {
if (p1 == p2) return false;
// 0 alway first
if (p1 == 0) return true;
if (p2 == 0) return false;
return p1 > p2;
}
monero_rpc_connection::monero_rpc_connection(const std::string& uri, const std::string& username, const std::string& password, const std::string& proxy_uri, const std::string& zmq_uri, int priority, const boost::optional<uint32_t>& timeout_ms) {
if (!uri.empty()) m_uri = uri;
else m_uri = boost::none;
if (!proxy_uri.empty()) m_proxy_uri = proxy_uri;
else m_proxy_uri = boost::none;
if (!zmq_uri.empty()) m_zmq_uri = zmq_uri;
else m_zmq_uri = boost::none;
m_priority = priority;
m_timeout_ms = timeout_ms;
set_credentials(username, password);
}
monero_rpc_connection::monero_rpc_connection(const monero_rpc_connection& rpc) {
boost::lock_guard<boost::recursive_mutex> src_lock(rpc.m_mutex);
m_uri = rpc.m_uri;
m_proxy_uri = rpc.m_proxy_uri;
m_zmq_uri = rpc.m_zmq_uri;
m_priority = rpc.m_priority;
m_timeout_ms = rpc.m_timeout_ms;
set_credentials(rpc.m_username.value_or(""), rpc.m_password.value_or(""));
m_is_online = rpc.m_is_online;
m_is_authenticated = rpc.m_is_authenticated;
m_response_time = rpc.m_response_time;
m_attributes = rpc.m_attributes;
}
rapidjson::Value monero_rpc_connection::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
// create root
rapidjson::Value root(rapidjson::kObjectType);
// set string values
rapidjson::Value value_str(rapidjson::kStringType);
if (m_uri != boost::none) monero_utils::add_json_member("uri", m_uri.get(), allocator, root, value_str);
if (m_username != boost::none) monero_utils::add_json_member("username", m_username.get(), allocator, root, value_str);
if (m_password != boost::none) monero_utils::add_json_member("password", m_password.get(), allocator, root, value_str);
if (m_proxy_uri != boost::none) monero_utils::add_json_member("proxyUri", m_proxy_uri.get(), allocator, root, value_str);
if (m_zmq_uri != boost::none) monero_utils::add_json_member("zmqUri", m_zmq_uri.get(), allocator, root, value_str);
// set num values
rapidjson::Value value_num(rapidjson::kNumberType);
monero_utils::add_json_member("priority", m_priority, allocator, root, value_num);
if (m_timeout_ms != boost::none) monero_utils::add_json_member("timeoutMs", m_timeout_ms.get(), allocator, root, value_num);
if (m_response_time != boost::none) monero_utils::add_json_member("responseTime", m_response_time.get(), allocator, root, value_num);
// set bool values
if (m_is_online != boost::none) monero_utils::add_json_member("isOnline", m_is_online.get(), allocator, root);
if (m_is_authenticated != boost::none) monero_utils::add_json_member("isAuthenticated", m_is_authenticated.get(), allocator, root);
return root;
}
bool monero_rpc_connection::is_onion() const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
epee::net_utils::http::url_content parsed{};
if (!epee::net_utils::parse_url(m_uri.value_or(""), parsed)) return false;
return parsed.host.size() >= 6 && parsed.host.compare(parsed.host.size() - 6, 6, ".onion") == 0;
}
bool monero_rpc_connection::is_i2p() const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
epee::net_utils::http::url_content parsed{};
if (!epee::net_utils::parse_url(m_uri.value_or(""), parsed)) return false;
return parsed.host.size() >= 8 && parsed.host.compare(parsed.host.size() - 8, 8, ".b32.i2p") == 0;
}
void monero_rpc_connection::set_credentials(const std::string& username, const std::string& password) {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
// disconnect http client if connected
if (m_http_client != nullptr && m_http_client->is_connected()) m_http_client->disconnect();
bool username_empty = username.empty();
bool password_empty = password.empty();
// check username and password consistency
if (!username_empty || !password_empty) {
if (password_empty) {
throw monero_error("password cannot be empty because username is not empty");
}
if (username_empty) {
throw monero_error("username cannot be empty because password is not empty");
}
}
// check username and password changes
bool username_equals = (m_username == boost::none && username_empty) || (m_username != boost::none && *m_username == username);
bool password_equals = (m_password == boost::none && password_empty) || (m_password != boost::none && *m_password == password);
// connection reset values
if (!username_equals || !password_equals) {
m_is_online = boost::none;
m_is_authenticated = boost::none;
}
// setup username and password
if (!username_empty && !password_empty) {
m_username = username;
m_password = password;
} else {
m_username = boost::none;
m_password = boost::none;
}
}
void monero_rpc_connection::set_attribute(const std::string& key, const std::string& val) {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
m_attributes[key] = val;
}
std::string monero_rpc_connection::get_attribute(const std::string& key) const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
std::unordered_map<std::string, std::string>::const_iterator i = m_attributes.find(key);
if (i == m_attributes.end()) {
return std::string(""); // attribute not found
}
return i->second;
}
boost::optional<bool> monero_rpc_connection::is_online() const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
return m_is_online;
}
boost::optional<bool> monero_rpc_connection::is_authenticated() const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
return m_is_authenticated;
}
boost::optional<bool> monero_rpc_connection::is_connected() const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
if (m_is_online == boost::none) return boost::none;
return m_is_online.get() && (m_is_authenticated == boost::none || m_is_authenticated.get());
}
bool monero_rpc_connection::check_connection(const boost::optional<uint32_t>& timeout_ms) {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
boost::optional<bool> is_online_before = m_is_online;
boost::optional<bool> is_authenticated_before = m_is_authenticated;
boost::optional<std::chrono::high_resolution_clock::time_point> start_time = boost::none;
try {
// assume daemon connection
monero_get_blocks_by_height_request request(100);
start_time = std::chrono::high_resolution_clock::now();
send_binary_request(request, timeout_ms);
// set response time
m_response_time = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start_time.get()).count();
m_is_online = true;
m_is_authenticated = true;
}
catch (const monero_rpc_error& ex) {
m_is_online = false;
m_is_authenticated = boost::none;
m_response_time = boost::none;
if (ex.code == 401 || ex.code == 404) {
// fallback to latency check
m_is_online = true;
m_is_authenticated = ex.code == 404;
}
}
catch (...) {
m_is_online = false;
m_is_authenticated = boost::none;
m_response_time = boost::none;
}
if (gen_utils::bool_equals(true, m_is_online) && start_time != boost::none) {
m_response_time = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start_time.get()).count();
}
return is_online_before != m_is_online || is_authenticated_before != m_is_authenticated;
}
boost::property_tree::ptree monero_rpc_connection::send_json_request(const std::string& path, const std::shared_ptr<serializable_struct>& params, const boost::optional<uint32_t>& timeout_ms) const {
// send JSON-RPC request
monero_rpc_request request(path, params);
auto response = send_json_request(request, timeout_ms);
// assert JSON-RPC response is defined
if (response.m_result == boost::none) throw monero_error("Invalid Monero JSONRPC response");
return response.m_result.get();
}
monero_rpc_response monero_rpc_connection::send_json_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms) const {
// invoke JSON-RPC method
monero_rpc_response response;
send_rpc_request("/json_rpc", request, response, timeout_ms);
// return JSON-RPC response
return response;
}
boost::property_tree::ptree monero_rpc_connection::send_path_request(const std::string& path, const std::shared_ptr<serializable_struct>& params, const boost::optional<uint32_t>& timeout_ms) const {
// send RPC request
monero_rpc_request request(path, params, false);
auto response = send_path_request(request, timeout_ms);
// assert RPC response is defined
if (response.m_response == boost::none) throw monero_error("Invalid Monero RPC response");
return response.m_response.get();
}
monero_rpc_response monero_rpc_connection::send_path_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms) const {
// validate parameters
if (request.m_method == boost::none || request.m_method->empty()) throw monero_error("No RPC method set in path request");
// invoke RPC method
monero_rpc_response response;
send_rpc_request(std::string("/") + request.m_method.get(), request, response, timeout_ms);
// return RPC response
return response;
}
monero_rpc_response monero_rpc_connection::send_binary_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms) const {
// validate parameters
if (request.m_method == boost::none || request.m_method->empty()) throw monero_error("No RPC method set in binary request");
// invoke binary RPC method
monero_rpc_response response;
send_rpc_request(std::string("/") + request.m_method.get(), request, response, timeout_ms, true);
// return binary response
return response;
}
void monero_rpc_connection::ensure_configured() const {
// create http client on first use, so constructing a connection does not require network support
if (m_http_client == nullptr) {
auto factory = std::make_unique<net::http::client_factory>();
m_http_client = factory->create();
m_http_client->set_auto_connect(true);
}
// only reconfigure when uri/creds/proxy changed since last apply
std::string uri = m_uri.value_or("");
std::string username = m_username.value_or("");
std::string password = m_password.value_or("");
std::string proxy_uri = m_proxy_uri.value_or("");
auto key = std::make_tuple(uri, username, password, proxy_uri);
if (m_applied == key) return; // unchanged, so reuse live connection
// setup connection credentials
boost::optional<epee::net_utils::http::login> login;
if(!username.empty() && !password.empty()) {
login = epee::net_utils::http::login();
login->username = username;
login->password = password;
}
// detect ssl
epee::net_utils::ssl_support_t ssl = uri.rfind("https", 0) == 0 ? epee::net_utils::ssl_support_t::e_ssl_support_enabled : epee::net_utils::ssl_support_t::e_ssl_support_disabled;
if (!m_http_client->set_proxy(proxy_uri)) throw monero_error("Could not set proxy uri: " + proxy_uri);
if (!m_http_client->set_server(uri, login, std::move(ssl))) throw monero_error("Could not set uri: " + uri);
if (!m_http_client->connect(std::chrono::milliseconds(DEFAULT_TIMEOUT_MS))) MWARNING("Could not connect to server: " + uri);
m_applied = key;
}
std::string monero_rpc_connection::invoke_post(const boost::string_ref uri, const std::string& body, const boost::optional<uint32_t>& timeout_ms) const {
boost::lock_guard<boost::recursive_mutex> lock(m_mutex);
// request/response timeout: user-configurable, defaults to 3 minutes when not set.
uint32_t timeout = DEFAULT_TIMEOUT_MS;
if (timeout_ms != boost::none) timeout = timeout_ms.get();
else if (m_timeout_ms != boost::none) timeout = m_timeout_ms.get();
if (timeout == 0) timeout = std::numeric_limits<uint32_t>::max(); // timeout of 0 is treated as indefinite
// ensure_configured() bounds only the first connect; epee reconnects with this request timeout
ensure_configured();
const epee::net_utils::http::http_response_info* pri = NULL;
// invoke http json
const uint64_t received_before = m_http_client->get_bytes_received();
if (!m_http_client->invoke_post(uri, body, std::chrono::milliseconds(timeout), std::addressof(pri))) {
// epee never surfaces the 401 status, but a rejected login means the server answered;
// a dead or idle-closed socket yields no response bytes at all
bool received = m_http_client->get_bytes_received() > received_before;
if (received && m_http_client->is_connected()) {
throw monero_rpc_error(401, "Unauthorized");
}
// drop the socket: epee leaves EOF'd sockets marked connected, so without this the next
// call reuses the poisoned socket and hits the same misdiagnosis again
m_http_client->disconnect();
if (m_uri == boost::none || m_uri->empty()) throw monero_error("Cannot send RPC request: uri not set.");
throw monero_error("Network error: " + std::string(received ? "connection dropped mid-response" : "no response") + " from " + m_uri.get() + uri.to_string() + " (timeout: " + std::to_string(timeout) + "ms)");
}
if (!pri) throw monero_error("Could not get response info");
if (pri->m_response_code < 200 || pri->m_response_code > 299) {
std::string err_msg = std::to_string(pri->m_response_code) + " " + pri->m_response_comment + (pri->m_body.empty() ? "" : ": " + pri->m_body);
throw monero_rpc_error(pri->m_response_code, err_msg);
}
// return response info body
return pri->m_body;
}
void deserialize_rpc_response(const std::string& json, monero_rpc_response& response, const boost::string_ref uri, const std::string& method) {
// parse json to property node
boost::property_tree::ptree node;
gen_utils::deserialize(json, node);
// parse JSON-RPC response
for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) {
std::string key = it->first;
if (key == std::string("error")) raise_rpc_error(it->second, uri, method);
else if (key == std::string("jsonrpc")) response.m_jsonrpc = it->second.data();
else if (key == std::string("result")) response.m_result = it->second;
}
// if not JSON-RPC, set RPC response
if (response.m_jsonrpc == boost::none) response.m_response = node;
}
void monero_rpc_connection::send_rpc_request(const boost::string_ref uri, const monero_rpc_request& request, monero_rpc_response& response, const boost::optional<uint32_t>& timeout_ms, bool binary) const {
std::string body;
if (request.m_method == boost::none) throw monero_error("Must provide a valid request method");
if (binary) monero_utils::json_to_binary(request.serialize(), body);
else body = request.serialize();
std::string result = invoke_post(uri, body, timeout_ms);
if (binary) response.m_binary = result;
else deserialize_rpc_response(result, response, uri, request.m_method.get());
}
std::shared_ptr<monero_rpc_connection> monero_rpc_connection::from_property_tree(const boost::property_tree::ptree& node) {
std::shared_ptr<monero_rpc_connection> connection = std::make_shared<monero_rpc_connection>();
for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) {
std::string key = it->first;
if (key == std::string("uri")) connection->m_uri = it->second.data();
else if (key == std::string("username")) connection->m_username = it->second.data();
else if (key == std::string("password")) connection->m_password = it->second.data();
else if (key == std::string("proxyUri") || key == std::string("proxy_uri")) connection->m_proxy_uri = it->second.data();
else if (key == std::string("zmqUri")) connection->m_zmq_uri = it->second.data();
}
return connection;
}
}
@@ -0,0 +1,281 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>
#include <boost/thread/condition_variable.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"
#include "net/http.h"
#include "daemon/monero_daemon_model.h"
/**
* Public interface for libmonero-cpp library.
*/
namespace monero {
/**
* Models parameters for a request to a RPC server.
*/
struct monero_request_params : public serializable_struct {
monero_request_params() { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models a request to a RPC server.
*/
struct monero_rpc_request : public serializable_struct {
boost::optional<std::string> m_id;
boost::optional<std::string> m_version;
boost::optional<std::string> m_method;
std::shared_ptr<serializable_struct> m_params;
monero_rpc_request() { }
monero_rpc_request(const std::string& method, const std::shared_ptr<serializable_struct>& params, bool json_rpc = true);
bool is_json_rpc() const { return m_id != boost::none && m_version != boost::none; }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models a response from a RPC server.
*/
struct monero_rpc_response {
boost::optional<std::string> m_jsonrpc;
boost::optional<boost::property_tree::ptree> m_result;
boost::optional<boost::property_tree::ptree> m_response;
boost::optional<std::string> m_binary;
};
/**
* Maintains a connection and sends requests to a Monero RPC API.
*/
class monero_rpc_connection : public serializable_struct {
public:
boost::optional<std::string> m_uri;
boost::optional<std::string> m_username;
boost::optional<std::string> m_password;
boost::optional<std::string> m_proxy_uri;
boost::optional<std::string> m_zmq_uri; // TODO: implement zmq listener
boost::optional<uint32_t> m_timeout_ms; // RPC request timeout in milliseconds
boost::optional<uint64_t> m_response_time; // automatically set by calling check_connection()
int m_priority; // priority relative to other connections where 1 is highest, then 2, etc, and 0 is lowest (default)
static std::shared_ptr<monero_rpc_connection> from_property_tree(const boost::property_tree::ptree& node);
/**
* Checks RPC connection priority order.
*
* @param c1 first priority to compare
* @param c2 second priority to compare
*/
static bool compare(int p1, int p2);
/**
* Initialize a new RPC connection.
*
* @param uri RPC connection uri
* @param username RPC connection authentication username
* @param password RPC connection authentication password
* @param proxy_uri RPC connection proxy uri
* @param zmq_uri RPC connection zmq uri
* @param priority RPC connection priority
* @param timeout_ms RPC connection timeout in milliseconds
*/
monero_rpc_connection(const std::string& uri = "", const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const std::string& zmq_uri = "", int priority = 0, const boost::optional<uint32_t>& timeout_ms = boost::none);
/**
* Copy a RPC connection.
*
* @param rpc RPC connection to copy
*/
monero_rpc_connection(const monero_rpc_connection& rpc);
/**
* Indicates if the connection uri is a TOR server.
*
* @return true or false to indicate if connection uri is a TOR server
*/
bool is_onion() const;
/**
* Indicates if the connection uri is a I2P server.
*
* @return true or false to indicate if connection uri is a I2P server
*/
bool is_i2p() const;
/**
* Set connection credentials.
*
* @param username username to use in RPC authentication
* @param password password to use in RPC authentication
*/
void set_credentials(const std::string& username, const std::string& password);
/**
* Set connection attribute.
*
* @param key is the attribute key
* @param val is the attribute value
*/
void set_attribute(const std::string& key, const std::string& val);
/**
* Get connection attribute.
*
* @param key is the attribute to get the value of
* @return key's value if set
*/
std::string get_attribute(const std::string& key) const;
/**
* Indicates if the connection is online, which is set automatically by calling check_connection().
*
* @return true or false to indicate if online, or null if check_connection() has not been called
*/
boost::optional<bool> is_online() const;
/**
* Indicates if the connection is authenticated, which is set automatically by calling check_connection().
*
* @return true if authenticated or no authentication, false if not authenticated, or null if not set
*/
boost::optional<bool> is_authenticated() const;
/**
* Indicates if the connection is connected, which is set automatically by calling check_connection().
*
* @return true or false to indicate if connected, or null if check_connection() has not been called
*/
boost::optional<bool> is_connected() const;
/**
* Check the connection and update online, authentication, and response time status.
*
* @param timeout_ms the maximum response time before considered offline
* @return
*/
bool check_connection(const boost::optional<uint32_t>& timeout_ms = boost::none);
/**
* Send a request to the RPC API.
*
* @param path specifies the method to request
* @param params are the request's input parameters
* @return the RPC API response as a map
*/
boost::property_tree::ptree send_json_request(const std::string& path, const std::shared_ptr<serializable_struct>& params = nullptr, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
/**
* Send a request to the RPC API.
*
* @param request specifies the method to request with parameters
* @param timeout request timeout in milliseconds
* @return the RPC API response as a map
*/
monero_rpc_response send_json_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
/**
* Send a RPC request to the given path and with the given paramters.
*
* E.g. "/get_transactions" with params
*
* @param path is the url path of the request to invoke
* @param params are request parameters sent in the body
* @return the RPC API response as a map
*/
boost::property_tree::ptree send_path_request(const std::string& path, const std::shared_ptr<serializable_struct>& params = nullptr, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
/**
* Send a RPC request to the given path and with the given paramters.
*
* @param request specifies the method to request with parameters
* @param timeout request timeout in milliseconds
* @return the request's deserialized response
*/
monero_rpc_response send_path_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
/**
* Send a binary RPC request.
*
* @param request specifies the method to request with paramesters
* @param timeout request timeout in milliseconds
* @return the request's deserialized response
*/
monero_rpc_response send_binary_request(const monero_rpc_request &request, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
protected:
// instance variables
mutable boost::recursive_mutex m_mutex;
boost::optional<bool> m_is_online;
boost::optional<bool> m_is_authenticated;
mutable std::unique_ptr<epee::net_utils::http::abstract_http_client> m_http_client;
std::unordered_map<std::string, std::string> m_attributes;
mutable std::tuple<std::string, std::string, std::string, std::string> m_applied;
void ensure_configured() const;
std::string invoke_post(const boost::string_ref uri, const std::string& body, const boost::optional<uint32_t>& timeout_ms = boost::none) const;
void send_rpc_request(const boost::string_ref uri, const monero_rpc_request& request, monero_rpc_response& response, const boost::optional<uint32_t>& timeout_ms = boost::none, bool binary = false) const;
};
}
@@ -0,0 +1,60 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#include "monero_daemon.h"
/**
* Public library intereface.
*/
namespace monero {
}
@@ -0,0 +1,805 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "common/monero_rpc_connection.h"
#include "monero_daemon_model.h"
/**
* Public library interface.
*/
namespace monero {
/**
* Receives notifications as a daemon is updated.
*/
class monero_daemon_listener {
public:
std::shared_ptr<monero_block_header> m_last_header;
/**
* Called when a new block is added to the chain.
*
* @param header is the header of the block added to the chain
*/
virtual void on_block_header(const std::shared_ptr<monero_block_header>& header) {
m_last_header = header;
}
};
/**
* Monero daemon interface.
*/
class monero_daemon {
public:
/**
* Virtual destructor.
*/
virtual ~monero_daemon() {}
/**
* Register a listener receive daemon notifications.
*
* @param listener is the listener to receive daemon notifications
*/
virtual void add_listener(monero_daemon_listener &listener) {
throw std::runtime_error("monero_daemon::add_listener(): not supported");
}
/**
* Unregister a listener to receive daemon notifications.
*
* @param listener is the listener to unregister
*/
virtual void remove_listener(monero_daemon_listener &listener) {
throw std::runtime_error("monero_daemon::remove_listener(): not supported");
}
/**
* Get the listeners registered with the daemon.
*/
virtual std::set<monero_daemon_listener*> get_listeners() {
throw std::runtime_error("monero_daemon::get_listeners(): not supported");
}
/**
* Remove all listeners registered with the daemon.
*/
virtual void remove_listeners() {
throw std::runtime_error("monero_daemon::remove_listeners(): not supported");
}
/**
* Get the daemon's version.
*
* @return the daemon's version
*/
virtual monero_version get_version() {
throw std::runtime_error("monero_daemon::get_version(): not supported");
}
/**
* Indicates if the daemon is trusted or untrusted.
*
* @return true if the daemon is trusted, false otherwise
*/
virtual bool is_trusted() {
throw std::runtime_error("monero_daemon::is_trusted(): not supported");
}
/**
* Get the number of blocks in the longest chain known to the node.
*
* @return the number of blocks
*/
virtual uint64_t get_height() {
throw std::runtime_error("monero_daemon::get_height(): not supported");
}
/**
* Get a block's hash by its height.
*
* @param height is the height of the block hash to get
* @return tthe block's hash at the given height
*/
virtual std::string get_block_hash(uint64_t height) {
throw std::runtime_error("monero_daemon::get_block_hash(): not supported");
}
/**
* Get a block template for mining a new block.
*
* @param wallet_address is the address of the wallet to receive miner transactions if block is successfully mined
* @param reserve_size is the reserve size (optional)
* @return a block template for mining a new block
*/
virtual std::shared_ptr<monero_block_template> get_block_template(const std::string& wallet_address, const boost::optional<int>& reserve_size = boost::none) {
throw std::runtime_error("monero_daemon::get_block_template(): not supported");
}
/**
* Get the last block's header.
*
* @return the last block's header
*/
virtual std::shared_ptr<monero_block_header> get_last_block_header() {
throw std::runtime_error("monero_daemon::get_last_block_header(): not supported");
}
/**
* Get a block header by its hash.
*
* @param block_hash is the hash of the block to get the header of
* @return the block's header
*/
virtual std::shared_ptr<monero_block_header> get_block_header_by_hash(const std::string& block_hash) {
throw std::runtime_error("monero_daemon::get_block_header_by_hash(): not supported");
}
/**
* Get a block header by its height.
*
* @param height is the height of the block to get the header of
* @return the block's header
*/
virtual std::shared_ptr<monero_block_header> get_block_header_by_height(uint64_t height) {
throw std::runtime_error("monero_daemon::get_block_header_by_height(): not supported");
}
/**
* Get block headers for the given range.
*
* @param start_height is the start height lower bound inclusive (optional)
* @param end_height is the end height upper bound inclusive (optional)
* @return block headers in the given range
*/
virtual std::vector<std::shared_ptr<monero_block_header>> get_block_headers_by_range(uint64_t start_height, uint64_t end_height) {
throw std::runtime_error("monero_daemon::get_block_headers_by_range(): not supported");
}
/**
* Get a block by hash.
*
* @param block_hash is the hash of the block to get
* @return the block with the given hash
*/
virtual std::shared_ptr<monero_block> get_block_by_hash(const std::string& block_hash) {
throw std::runtime_error("monero_daemon::get_block_by_hash(): not supported");
}
/**
* Get blocks by hash.
*
* @param block_hashes are array of hashes; first 10 blocks hash goes sequential,
* next goes in pow(2,n) offset, like 2, 4, 8, 16, 32, 64 and so on,
* and the last one is always genesis block
* @param start_height is the start height to get blocks by hash
* @param prune specifies if returned blocks should be pruned (defaults to false) // TODO: test default
* @return the retrieved blocks
*/
virtual std::vector<std::shared_ptr<monero_block>> get_blocks_by_hash(const std::vector<std::string>& block_hashes, uint64_t start_height, bool prune) {
throw std::runtime_error("monero_daemon::get_blocks_by_hash(): not supported");
}
/**
* Get a block by height.
*
* @param height is the height of the block to get
* @return the block at the given height
*/
virtual std::shared_ptr<monero_block> get_block_by_height(uint64_t height) {
throw std::runtime_error("monero_daemon::get_block_by_height(): not supported");
}
/**
* Get blocks at the given heights.
*
* @param heights are the heights of the blocks to get
* @return blocks at the given heights
*/
virtual std::vector<std::shared_ptr<monero_block>> get_blocks_by_height(const std::vector<uint64_t>& heights) {
throw std::runtime_error("monero_daemon::get_blocks_by_height(): not supported");
}
/**
* Get blocks in the given height range.
*
* @param start_height is the start height lower bound inclusive (optional)
* @param end_height is the end height upper bound inclusive (optional)
* @return blocks in the given height range
*/
virtual std::vector<std::shared_ptr<monero_block>> get_blocks_by_range(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height) {
throw std::runtime_error("monero_daemon::get_blocks_by_range(): not supported");
}
/**
* Get blocks in the given height range as chunked requests so that each request is
* not too big.
*
* @param start_height is the start height lower bound inclusive (optional)
* @param end_height is the end height upper bound inclusive (optional)
* @return blocks in the given height range
*/
virtual std::vector<std::shared_ptr<monero_block>> get_blocks_by_range_chunked(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height, boost::optional<uint64_t> max_chunk_size) {
throw std::runtime_error("monero_daemon::get_blocks_by_range_chunked(): not supported");
}
/**
* Get block hashes as a binary request to the daemon.
*
* @param block_hashes specify block hashes to fetch; first 10 blocks hash goes
* sequential, next goes in pow(2,n) offset, like 2, 4, 8, 16, 32, 64
* and so on, and the last one is always genesis block
* @param start_height is the starting height of block hashes to return
* @return the requested block hashes
*/
virtual std::vector<std::string> get_block_hashes(const std::vector<std::string>& block_hashes, uint64_t start_height) {
throw std::runtime_error("monero_daemon::get_block_hashes(): not supported");
}
/**
* Get a transaction by hash.
*
* @param tx_hash is the hash of the transaction to get
* @param prune specifies if the returned tx should be pruned (defaults to false)
* @return the transaction with the given hash or null if not found
*/
virtual std::shared_ptr<monero_tx> get_tx(const std::string& tx_hash, bool prune = false) {
std::vector<std::string> hashes;
hashes.push_back(tx_hash);
auto txs = get_txs(hashes, prune);
std::shared_ptr<monero_tx> tx;
if (txs.size() > 0) {
tx = txs[0];
}
return tx;
}
/**
* Get transactions by hashes.
*
* @param tx_hashes are hashes of transactions to get
* @param prune specifies if the returned txs should be pruned (defaults to false)
* @return found transactions with the given hashes
*/
virtual std::vector<std::shared_ptr<monero_tx>> get_txs(const std::vector<std::string>& tx_hashes, bool prune = false) {
throw std::runtime_error("monero_daemon::get_txs(): not supported");
}
/**
* Get a transaction hex by hash.
*
* @param tx_hash is the hash of the transaction to get hex from
* @param prune specifies if the returned tx hex should be pruned (defaults to false)
* @return the tx hex with the given hash
*/
virtual boost::optional<std::string> get_tx_hex(const std::string& tx_hash, bool prune = false) {
std::vector<std::string> hashes;
hashes.push_back(tx_hash);
auto hexes = get_tx_hexes(hashes, prune);
boost::optional<std::string> hex;
if (hexes.size() > 0) {
hex = hexes[0];
}
return hex;
}
/**
* Get transaction hexes by hashes.
*
* @param tx_hashes are hashes of transactions to get hexes from
* @param prune specifies if the returned tx hexes should be pruned (defaults to false)
* @return are the tx hexes
*/
virtual std::vector<std::string> get_tx_hexes(const std::vector<std::string>& tx_hashes, bool prune = false) {
throw std::runtime_error("monero_daemon::get_tx_hexes(): not supported");
}
/**
* Gets the total emissions and fees from the genesis block to the current height.
*
* @param height is the height to start computing the miner sum
* @param num_blocks are the number of blocks to include in the sum
* @return the sum emission and fees since the geneis block
*/
virtual std::shared_ptr<monero_miner_tx_sum> get_miner_tx_sum(uint64_t height, uint64_t num_blocks) {
throw std::runtime_error("monero_daemon::get_miner_tx_sum(): not supported");
}
/**
* Get mining fee estimates per kB.
*
* @param grace_blocks TODO
* @return mining fee estimates per kB
*/
virtual std::shared_ptr<monero_fee_estimate> get_fee_estimate(uint64_t grace_blocks = 0) {
throw std::runtime_error("monero_daemon::get_fee_estimate(): not supported");
}
/**
* Submits a transaction to the daemon's pool.
*
* @param tx_hex is the raw transaction hex to submit
* @param do_not_relay specifies if the tx should be relayed (optional)
* @return the submission results
*/
virtual std::shared_ptr<monero_submit_tx_result> submit_tx_hex(const std::string& tx_hex, bool do_not_relay = false) {
throw std::runtime_error("monero_daemon::submit_tx_hex(): not supported");
}
/**
* Relays a transaction by hash.
*
* @param tx_hash identifies the transaction to relay
*/
virtual void relay_tx_by_hash(const std::string& tx_hash) {
std::vector<std::string> tx_hashes;
tx_hashes.push_back(tx_hash);
relay_txs_by_hash(tx_hashes);
}
/**
* Relays transactions by hash.
*
* @param tx_hashes identify the transactions to relay
*/
virtual void relay_txs_by_hash(const std::vector<std::string>& tx_hashes) {
throw std::runtime_error("monero_daemon::relay_txs_by_hash(): not supported");
}
/**
* Get valid transactions seen by the node but not yet mined into a block, as well
* as spent key image information for the tx pool.
*
* @return transactions in the transaction pool
*/
virtual std::vector<std::shared_ptr<monero_tx>> get_tx_pool() {
throw std::runtime_error("monero_daemon::get_tx_pool(): not supported");
}
/**
* Get hashes of transactions in the transaction pool.
*
* @return hashes of transactions in the transaction pool
*/
virtual std::vector<std::string> get_tx_pool_hashes() {
throw std::runtime_error("monero_daemon::get_tx_pool_hashes(): not supported");
}
/**
* Get all transaction pool backlog.
*
* @return transaction pool backlog entries
*/
virtual std::vector<monero_tx_backlog_entry> get_tx_pool_backlog() {
throw std::runtime_error("monero_daemon::get_tx_pool_backlog(): not supported");
}
/**
* Get transaction pool statistics.
*
* @return statistics about the transaction pool
*/
virtual std::shared_ptr<monero_tx_pool_stats> get_tx_pool_stats() {
throw std::runtime_error("monero_daemon::get_tx_pool_stats(): not supported");
}
/**
* Flushes all transactions from the tx pool.
*/
virtual void flush_tx_pool() {
throw std::runtime_error("monero_daemon::flush_tx_pool(): not supported");
}
/**
* Flush transactions from the tx pool.
*
* @param hashes are hashes of transactions to flush
*/
virtual void flush_tx_pool(const std::vector<std::string> &hashes) {
throw std::runtime_error("monero_daemon::flush_tx_pool(): not supported");
}
/**
* Flush a single transaction from the tx pool.
*
* @param hash is the hash of transaction to flush
*/
virtual void flush_tx_pool(const std::string &hash) {
throw std::runtime_error("monero_daemon::flush_tx_pool(): not supported");
}
/**
* Get the spent status of the given key image.
*
* @param key_image is key image hex to get the status of
* @return the status of the key image
*/
virtual monero_key_image_spent_status get_key_image_spent_status(const std::string& key_image) {
std::vector<std::string> key_images;
key_images.push_back(key_image);
auto statuses = get_key_image_spent_statuses(key_images);
if (statuses.empty()) throw std::runtime_error("Could not get key image spent status");
return statuses[0];
}
/**
* Get the spent status of each given key image.
*
* @param key_image are hex key images to get the statuses of
* @return the spent status for each key image
*/
virtual std::vector<monero_key_image_spent_status> get_key_image_spent_statuses(const std::vector<std::string>& key_images) {
throw std::runtime_error("monero_daemon::get_key_image_spent_statuses(): not supported");
}
/**
* Get outputs identified by a list of output amounts and indices as a binary
* request.
*
* @param outputs identify each output by amount and index
* @return the identified outputs
*/
virtual std::vector<std::shared_ptr<monero_output>> get_outputs(const std::vector<monero_output>& outputs) {
throw std::runtime_error("monero_daemon::get_outputs(): not supported");
}
/**
* Get a histogram of output amounts. For all amounts (possibly filtered by
* parameters), gives the number of outputs on the chain for that amount.
* RingCT outputs counts as 0 amount.
*
* @param amounts are amounts of outputs to make the histogram with
* @param min_count TODO
* @param max_count TODO
* @param is_unlocked makes a histogram with outputs with the specified lock state
* @param recent_cutoff TODO
* @return output histogram entries meeting the parameters
*/
virtual std::vector<std::shared_ptr<monero_output_histogram_entry>> get_output_histogram(const std::vector<uint64_t>& amounts, const boost::optional<int>& min_count, const boost::optional<int>& max_count, const boost::optional<bool>& is_unlocked, const boost::optional<int>& recent_cutoff) {
throw std::runtime_error("monero_daemon::get_output_histogram(): not supported");
}
/**
* Creates an output distribution.
*
* @param amounts are amounts of outputs to make the distribution with
* @param is_cumulative specifies if the results should be cumulative (defaults to TODO)
* @param start_height is the start height lower bound inclusive (optional)
* @param end_height is the end height upper bound inclusive (optional)
* @return output distribution entries meeting the parameters
*/
virtual std::vector<std::shared_ptr<monero_output_distribution_entry>> get_output_distribution(const std::vector<uint64_t>& amounts, const boost::optional<bool>& is_cumulative = boost::none, const boost::optional<uint64_t>& start_height = boost::none, const boost::optional<uint64_t>& end_height = boost::none) {
throw std::runtime_error("monero_daemon::get_output_distribution(): not supported");
}
/**
* Get general information about the state of the node and the network.
*
* @return general information about the node and network
*/
virtual std::shared_ptr<monero_daemon_info> get_info() {
throw std::runtime_error("monero_daemon::get_info(): not supported");
}
/**
* Get synchronization information.
*
* @return contains sync information
*/
virtual std::shared_ptr<monero_daemon_sync_info> get_sync_info() {
throw std::runtime_error("monero_daemon::get_sync_info(): not supported");
}
/**
* Look up information regarding hard fork voting and readiness.
*
* @return hard fork information
*/
virtual std::shared_ptr<monero_hard_fork_info> get_hard_fork_info() {
throw std::runtime_error("monero_daemon::get_hard_fork_info(): not supported");
}
/**
* Get alternative chains seen by the node.
*
* @return alternative chains seen by the node
*/
virtual std::vector<std::shared_ptr<monero_alt_chain>> get_alt_chains() {
throw std::runtime_error("monero_daemon::get_alt_chains(): not supported");
}
/**
* Get known block hashes which are not on the main chain.
*
* @return known block hashes which are not on the main chain
*/
virtual std::vector<std::string> get_alt_block_hashes() {
throw std::runtime_error("monero_daemon::get_alt_block_hashes(): not supported");
}
/**
* Get the download bandwidth limit.
*
* @return is the download bandwidth limit
*/
virtual int get_download_limit() {
throw std::runtime_error("monero_daemon::get_download_limit(): not supported");
}
/**
* Set the download bandwidth limit.
*
* @param limit is the download limit to set (-1 to reset to default)
* @return int is the new download limit after setting
*/
virtual int set_download_limit(int limit) {
throw std::runtime_error("monero_daemon::set_download_limit(): not supported");
}
/**
* Reset the download bandwidth limit.
*
* @return the download bandwidth limit after resetting
*/
virtual int reset_download_limit() {
throw std::runtime_error("monero_daemon::reset_download_limit(): not supported");
}
/**
* Get the upload bandwidth limit.
*
* @return is the upload bandwidth limit
*/
virtual int get_upload_limit() {
throw std::runtime_error("monero_daemon::get_upload_limit(): not supported");
}
/**
* Set the upload bandwidth limit.
*
* @param limit is the upload limit to set (-1 to reset to default)
* @return int is the new upload limit after setting
*/
virtual int set_upload_limit(int limit) {
throw std::runtime_error("monero_daemon::set_upload_limit(): not supported");
}
/**
* Reset the upload bandwidth limit.
*
* @return the upload bandwidth limit after resetting
*/
virtual int reset_upload_limit() {
throw std::runtime_error("monero_daemon::reset_upload_limit(): not supported");
}
/**
* Get peers with active incoming or outgoing connections to the node.
*
* @return the daemon's peers
*/
virtual std::vector<std::shared_ptr<monero_peer>> get_peers() {
throw std::runtime_error("monero_daemon::get_peers(): not supported");
}
/**
* Get all known peers including their last known online status.
*
* @return the daemon's known peers
*/
virtual std::vector<std::shared_ptr<monero_peer>> get_known_peers() {
throw std::runtime_error("monero_daemon::get_known_peers(): not supported");
}
/**
* Limit number of outgoing peers.
*
* @param limit is the maximum number of outgoing peers
*/
virtual void set_outgoing_peer_limit(int limit) {
throw std::runtime_error("monero_daemon::set_outgoing_peer_limit(): not supported");
}
/**
* Limit number of incoming peers.
*
* @param limit is the maximum number of incoming peers
*/
virtual void set_incoming_peer_limit(int limit) {
throw std::runtime_error("monero_daemon::set_incoming_peer_limit(): not supported");
}
/**
* Get peer bans.
*
* @return entries about banned peers
*/
virtual std::vector<std::shared_ptr<monero_ban>> get_peer_bans() {
throw std::runtime_error("monero_daemon::get_peer_bans(): not supported");
}
/**
* Ban peers nodes.
*
* @param bans are bans to apply against peer nodes
*/
virtual void set_peer_bans(const std::vector<std::shared_ptr<monero_ban>>& bans) {
throw std::runtime_error("monero_daemon::set_peer_bans(): not supported");
}
/**
* Ban a peer node.
*
* @param ban contains information about a node to ban
*/
virtual void set_peer_ban(const std::shared_ptr<monero_ban>& ban) {
if (ban == nullptr) throw std::runtime_error("Ban is none");
std::vector<std::shared_ptr<monero_ban>> bans;
bans.push_back(ban);
set_peer_bans(bans);
}
/**
* Start mining.
*
* @param address is the address given miner rewards if the daemon mines a block
* @param num_threads is the number of mining threads to run
* @param is_background specifies if the miner should run in the background or not
* @param ignore_battery specifies if the battery state (e.g. on laptop) should be ignored or not
*/
virtual void start_mining(const std::string &address, boost::optional<uint64_t> num_threads, boost::optional<bool> is_background, boost::optional<bool> ignore_battery) {
throw std::runtime_error("monero_daemon::start_mining(): not supported");
}
/**
* Stop mining.
*/
virtual void stop_mining() {
throw std::runtime_error("monero_daemon::stop_mining(): not supported");
}
/**
* Get the daemon's mining status.
*
* @return the daemon's mining status
*/
virtual std::shared_ptr<monero_mining_status> get_mining_status() {
throw std::runtime_error("monero_daemon::get_mining_status(): not supported");
}
/**
* Generate blocks to a wallet address (regtest only).
*
* @param wallet_address is the address of the wallet to receive miner transactions if block is successfully mined
* @param num_blocks is the number of blocks to generate
* @param prev_block_hash is the hash of the previous block to build on top of (optional, builds on the current tip if not given)
* @param starting_nonce is the starting nonce to use (optional)
* @return the result of generating blocks; height is the height of the last block generated
*/
virtual std::shared_ptr<monero_generate_blocks_result> generate_blocks(const std::string& wallet_address, uint64_t num_blocks, const boost::optional<std::string>& prev_block_hash = boost::none, const boost::optional<uint32_t>& starting_nonce = boost::none) {
throw std::runtime_error("monero_daemon::generate_blocks(): not supported");
}
/**
* Submit a mined block to the network.
*
* @param block_blob is the mined block to submit
*/
virtual void submit_block(const std::string& block_blob) {
std::vector<std::string> block_blobs;
block_blobs.push_back(block_blob);
return submit_blocks(block_blobs);
}
/**
* Submit mined blocks to the network.
*
* @param block_blobs are the mined blocks to submit
*/
virtual void submit_blocks(const std::vector<std::string>& block_blobs) {
throw std::runtime_error("monero_daemon::submit_blocks(): not supported");
}
/**
* Prune the blockchain.
*
* @param check specifies to check the pruning (default false)
* @return the prune result
*/
virtual std::shared_ptr<monero_prune_result> prune_blockchain(bool check) {
throw std::runtime_error("monero_daemon::prune_blockchain(): not supported");
}
/**
* Check for update.
*
* @return the result of the update check
*/
virtual std::shared_ptr<monero_daemon_update_check_result> check_for_update() {
throw std::runtime_error("monero_daemon::check_for_update(): not supported");
}
/**
* Download an update.
*
* @param path is the path to download the update (optional)
* @return the result of the update download
*/
virtual std::shared_ptr<monero_daemon_update_download_result> download_update(const std::string& path = "") {
throw std::runtime_error("monero_daemon::download_update(): not supported");
}
/**
* Safely disconnect and shut down the daemon.
*/
virtual void stop() {
throw std::runtime_error("monero_daemon::stop(): not supported");
}
/**
* Get the header of the next block added to the chain.
*
* @return the header of the next block added to the chain
*/
virtual std::shared_ptr<monero_block_header> wait_for_next_block_header() {
throw std::runtime_error("monero_daemon::wait_for_next_block_header(): not supported");
}
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,639 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include <map>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"
/**
* Public interface for libmonero-cpp library.
*/
namespace monero {
/**
* Base struct which can be serialized.
*/
struct serializable_struct {
virtual ~serializable_struct() = default;
/**
* Serializes the struct to a json std::string.
*
* @return the struct serialized to a json std::string
*/
std::string serialize() const;
/**
* Converts the struct to a rapidjson Value.
*
* @param allocator is the rapidjson document allocator
* @return the struct as a rapidjson Value
*/
virtual rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const = 0;
};
/**
* Models connection ssl options.
*/
struct ssl_options : public serializable_struct {
boost::optional<std::string> m_ssl_private_key_path;
boost::optional<std::string> m_ssl_certificate_path;
boost::optional<std::string> m_ssl_ca_file;
std::vector<std::string> m_ssl_allowed_fingerprints;
boost::optional<bool> m_ssl_allow_any_cert;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Enumerates Monero connection types.
*/
enum monero_connection_type : uint8_t {
INVALID = 0,
IPV4,
IPV6,
TOR,
I2P
};
/**
* Enumerates Monero network types.
*/
enum monero_network_type : uint8_t {
MAINNET = 0,
TESTNET,
STAGENET
};
/**
* Models connection bandwidth limits.
*/
struct monero_bandwidth_limits : public serializable_struct {
boost::optional<int> m_up;
boost::optional<int> m_down;
monero_bandwidth_limits() { }
monero_bandwidth_limits(int up, int down): m_up(up), m_down(down) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_bandwidth_limits>& limits);
};
/**
* Models a Monero version.
*/
struct monero_version : public serializable_struct {
boost::optional<uint32_t> m_number;
boost::optional<bool> m_is_release;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_version>& version);
};
// forward declarations
struct monero_tx;
struct monero_output;
/**
* Models a Monero block header which contains information about the block.
*
* TODO: a header that is transmitted may have fewer fields like cryptonote::block_header; separate?
*/
struct monero_block_header : public serializable_struct {
boost::optional<std::string> m_hash;
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_timestamp;
boost::optional<uint64_t> m_size;
boost::optional<uint64_t> m_weight;
boost::optional<uint64_t> m_long_term_weight;
boost::optional<uint64_t> m_depth;
boost::optional<uint64_t> m_difficulty_low;
boost::optional<uint64_t> m_difficulty_high;
boost::optional<uint64_t> m_cumulative_difficulty_low;
boost::optional<uint64_t> m_cumulative_difficulty_high;
boost::optional<uint32_t> m_major_version;
boost::optional<uint32_t> m_minor_version;
boost::optional<uint32_t> m_nonce;
boost::optional<std::string> m_miner_tx_hash;
boost::optional<uint32_t> m_num_txs;
boost::optional<bool> m_orphan_status;
boost::optional<std::string> m_prev_hash;
boost::optional<uint64_t> m_reward;
boost::optional<std::string> m_pow_hash;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
std::shared_ptr<monero_block_header> copy(const std::shared_ptr<monero_block_header>& src, const std::shared_ptr<monero_block_header>& tgt) const;
virtual void merge(const std::shared_ptr<monero_block_header>& self, const std::shared_ptr<monero_block_header>& other);
};
/**
* Models a Monero block in the blockchain.
*/
struct monero_block : public monero_block_header {
boost::optional<std::string> m_hex;
std::shared_ptr<monero_tx> m_miner_tx;
std::vector<std::shared_ptr<monero_tx>> m_txs;
std::vector<std::string> m_tx_hashes;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
std::shared_ptr<monero_block> copy(const std::shared_ptr<monero_block>& src, const std::shared_ptr<monero_block>& tgt) const;
void merge(const std::shared_ptr<monero_block_header>& self, const std::shared_ptr<monero_block_header>& other);
void merge(const std::shared_ptr<monero_block>& self, const std::shared_ptr<monero_block>& other);
};
/**
* Models a Monero transaction on the blockchain.
*/
struct monero_tx : public serializable_struct {
static const std::string DEFAULT_PAYMENT_ID; // default payment id "0000000000000000"
static const std::string DEFAULT_ID;
std::shared_ptr<monero_block> m_block;
boost::optional<std::string> m_hash;
boost::optional<uint32_t> m_version;
boost::optional<bool> m_is_miner_tx;
boost::optional<std::string> m_payment_id;
boost::optional<uint64_t> m_fee;
boost::optional<uint32_t> m_ring_size;
boost::optional<bool> m_relay;
boost::optional<bool> m_is_relayed;
boost::optional<bool> m_is_confirmed;
boost::optional<bool> m_in_tx_pool;
boost::optional<uint64_t> m_num_confirmations;
boost::optional<uint64_t> m_unlock_time;
boost::optional<uint64_t> m_last_relayed_timestamp;
boost::optional<uint64_t> m_received_timestamp;
boost::optional<bool> m_is_double_spend_seen;
boost::optional<std::string> m_key;
boost::optional<std::string> m_full_hex;
boost::optional<std::string> m_pruned_hex;
boost::optional<std::string> m_prunable_hex;
boost::optional<std::string> m_prunable_hash;
boost::optional<uint64_t> m_size;
boost::optional<uint64_t> m_weight;
std::vector<std::shared_ptr<monero_output>> m_inputs;
std::vector<std::shared_ptr<monero_output>> m_outputs;
std::vector<uint64_t> m_output_indices;
boost::optional<std::string> m_metadata;
boost::optional<std::string> m_common_tx_sets;
std::vector<uint8_t> m_extra;
boost::optional<std::string> m_rct_signatures; // TODO: implement
boost::optional<std::string> m_rct_sig_prunable; // TODO: implement
boost::optional<bool> m_is_kept_by_block;
boost::optional<bool> m_is_failed;
boost::optional<uint64_t> m_last_failed_height;
boost::optional<std::string> m_last_failed_hash;
boost::optional<uint64_t> m_max_used_block_height;
boost::optional<std::string> m_max_used_block_hash;
std::vector<std::string> m_signatures;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, std::shared_ptr<monero_tx> tx);
std::shared_ptr<monero_tx> copy(const std::shared_ptr<monero_tx>& src, const std::shared_ptr<monero_tx>& tgt) const;
virtual void merge(const std::shared_ptr<monero_tx>& self, const std::shared_ptr<monero_tx>& other);
boost::optional<uint64_t> get_height() const;
};
/**
* Models a Monero key image.
*/
struct monero_key_image : public serializable_struct {
boost::optional<std::string> m_hex;
boost::optional<std::string> m_signature;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_key_image>& key_image);
static std::vector<std::shared_ptr<monero_key_image>> deserialize_key_images(const std::string& key_images_json); // TODO: remove this specialty util used once
std::shared_ptr<monero_key_image> copy(const std::shared_ptr<monero_key_image>& src, const std::shared_ptr<monero_key_image>& tgt) const;
void merge(const std::shared_ptr<monero_key_image>& self, const std::shared_ptr<monero_key_image>& other);
};
/**
* Models a Monero transaction output.
*/
struct monero_output : public serializable_struct {
std::shared_ptr<monero_tx> m_tx;
std::shared_ptr<monero_key_image> m_key_image;
boost::optional<uint64_t> m_amount;
boost::optional<uint64_t> m_index;
std::vector<uint64_t> m_ring_output_indices;
boost::optional<std::string> m_stealth_public_key;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_output>& output);
std::shared_ptr<monero_output> copy(const std::shared_ptr<monero_output>& src, const std::shared_ptr<monero_output>& tgt) const;
virtual void merge(const std::shared_ptr<monero_output>& self, const std::shared_ptr<monero_output>& other);
};
/**
* Models the status of a Monero key image.
*/
enum monero_key_image_spent_status : uint8_t {
NOT_SPENT = 0,
CONFIRMED,
TX_POOL
};
/**
* Models a Monero RPC payment information.
*/
struct monero_rpc_payment_info : public serializable_struct {
boost::optional<uint64_t> m_credits;
boost::optional<std::string> m_top_block_hash;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_rpc_payment_info>& rpc_payment_info);
};
/**
* Models an alternative chain seen by the node.
*/
struct monero_alt_chain : public serializable_struct {
std::vector<std::string> m_block_hashes;
boost::optional<uint64_t> m_difficulty_low;
boost::optional<uint64_t> m_difficulty_high;
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_length;
boost::optional<std::string> m_main_chain_parent_block_hash;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_alt_chain>& alt_chain);
};
/**
* Monero banhammer.
*/
struct monero_ban : public serializable_struct {
boost::optional<std::string> m_host;
boost::optional<uint32_t> m_ip;
boost::optional<bool> m_is_banned;
boost::optional<uint64_t> m_seconds;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_ban>& ban);
};
/**
* Result of pruning the blockchain.
*/
struct monero_prune_result : public serializable_struct {
boost::optional<bool> m_is_pruned;
boost::optional<int> m_pruning_seed;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_prune_result>& result);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Monero daemon mining status.
*/
struct monero_mining_status : public serializable_struct {
boost::optional<bool> m_is_active;
boost::optional<bool> m_is_background;
boost::optional<std::string> m_address;
boost::optional<uint64_t> m_speed;
boost::optional<int> m_num_threads;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_mining_status>& status);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Model for the summation of miner emissions and fees.
*/
struct monero_miner_tx_sum : public serializable_struct {
boost::optional<uint64_t> m_emission_sum_low;
boost::optional<uint64_t> m_emission_sum_high;
boost::optional<uint64_t> m_fee_sum_low;
boost::optional<uint64_t> m_fee_sum_high;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_miner_tx_sum>& sum);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Monero block template to mine.
*/
struct monero_block_template : public serializable_struct {
boost::optional<std::string> m_block_template_blob;
boost::optional<std::string> m_block_hashing_blob;
boost::optional<std::string> m_prev_hash;
boost::optional<std::string> m_seed_hash;
boost::optional<std::string> m_next_seed_hash;
boost::optional<uint64_t> m_difficulty_low;
boost::optional<uint64_t> m_difficulty_high;
boost::optional<uint64_t> m_expected_reward;
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_reserved_offset;
boost::optional<uint64_t> m_seed_height;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_block_template>& tmplt);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Monero daemon connection span.
*/
struct monero_connection_span : public serializable_struct {
boost::optional<std::string> m_connection_id;
boost::optional<std::string> m_remote_address;
boost::optional<uint64_t> m_num_blocks;
boost::optional<uint64_t> m_rate;
boost::optional<uint64_t> m_speed;
boost::optional<uint64_t> m_size;
boost::optional<uint64_t> m_start_height;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_connection_span>& span);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models a peer to the daemon.
*/
struct monero_peer : public serializable_struct {
boost::optional<std::string> m_id;
boost::optional<std::string> m_address;
boost::optional<std::string> m_host;
boost::optional<int> m_port;
boost::optional<bool> m_is_online;
boost::optional<uint64_t> m_last_seen_timestamp;
boost::optional<int> m_pruning_seed;
boost::optional<int> m_rpc_port;
boost::optional<uint64_t> m_rpc_credits_per_hash;
boost::optional<std::string> m_hash;
boost::optional<uint64_t> m_avg_download;
boost::optional<uint64_t> m_avg_upload;
boost::optional<uint64_t> m_current_download;
boost::optional<uint64_t> m_current_upload;
boost::optional<uint64_t> m_height;
boost::optional<bool> m_is_incoming;
boost::optional<uint64_t> m_live_time;
boost::optional<bool> m_is_local_ip;
boost::optional<bool> m_is_local_host;
boost::optional<uint64_t> m_num_receives;
boost::optional<uint64_t> m_num_sends;
boost::optional<uint64_t> m_receive_idle_time;
boost::optional<uint64_t> m_send_idle_time;
boost::optional<std::string> m_state;
boost::optional<int> m_num_support_flags;
boost::optional<monero_connection_type> m_connection_type;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_peer>& peer);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models the result from submitting a tx to a daemon.
*/
struct monero_submit_tx_result : public monero_rpc_payment_info {
boost::optional<bool> m_has_invalid_input;
boost::optional<bool> m_has_invalid_output;
boost::optional<bool> m_has_too_few_outputs;
boost::optional<bool> m_is_good;
boost::optional<bool> m_is_relayed;
boost::optional<bool> m_is_double_spend;
boost::optional<bool> m_is_fee_too_low;
boost::optional<bool> m_is_mixin_too_low;
boost::optional<bool> m_is_overspend;
boost::optional<bool> m_is_too_big;
boost::optional<bool> m_sanity_check_failed;
boost::optional<bool> m_is_tx_extra_too_big;
boost::optional<bool> m_is_nonzero_unlock_time;
boost::optional<std::string> m_reason;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_submit_tx_result>& result);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* TODO.
*/
struct monero_tx_backlog_entry {
// TODO
};
/**
* Monero output distribution entry.
*/
struct monero_output_distribution_entry : public serializable_struct {
boost::optional<uint64_t> m_amount;
boost::optional<uint64_t> m_base;
std::vector<uint64_t> m_distribution;
boost::optional<uint64_t> m_start_height;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_output_distribution_entry>& entry);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Entry in a Monero output histogram (see get_output_histogram of Daemon RPC documentation).
*/
struct monero_output_histogram_entry : public serializable_struct {
boost::optional<uint64_t> m_amount;
boost::optional<uint64_t> m_num_instances;
boost::optional<uint64_t> m_unlocked_instances;
boost::optional<uint64_t> m_recent_instances;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_output_histogram_entry>& entry);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models transaction pool statistics.
*/
struct monero_tx_pool_stats : public serializable_struct {
boost::optional<int> m_num_txs;
boost::optional<int> m_num_not_relayed;
boost::optional<int> m_num_failing;
boost::optional<int> m_num_double_spends;
boost::optional<int> m_num10m;
boost::optional<uint64_t> m_fee_total;
boost::optional<uint64_t> m_bytes_max;
boost::optional<uint64_t> m_bytes_med;
boost::optional<uint64_t> m_bytes_min;
boost::optional<uint64_t> m_bytes_total;
std::map<uint64_t, uint64_t> m_histo;
boost::optional<uint64_t> m_histo98pc;
boost::optional<uint64_t> m_oldest_timestamp;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_pool_stats>& stats);
};
/**
* Models the result of checking for a daemon update.
*/
struct monero_daemon_update_check_result : public serializable_struct {
boost::optional<bool> m_is_update_available;
boost::optional<std::string> m_version;
boost::optional<std::string> m_hash;
boost::optional<std::string> m_auto_uri;
boost::optional<std::string> m_user_uri;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_update_check_result>& check);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models the result of downloading an update.
*/
struct monero_daemon_update_download_result : public monero_daemon_update_check_result {
boost::optional<std::string> m_download_path;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_update_download_result>& check);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models a Monero fee estimate.
*/
struct monero_fee_estimate : public serializable_struct {
boost::optional<uint64_t> m_quantization_mask;
boost::optional<uint64_t> m_fee;
std::vector<uint64_t> m_fees;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_fee_estimate>& estimate);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Monero daemon info.
*/
struct monero_daemon_info : public monero_rpc_payment_info {
boost::optional<std::string> m_version;
boost::optional<uint64_t> m_num_alt_blocks;
boost::optional<uint64_t> m_block_size_limit;
boost::optional<uint64_t> m_block_size_median;
boost::optional<uint64_t> m_block_weight_limit;
boost::optional<uint64_t> m_block_weight_median;
boost::optional<std::string> m_bootstrap_daemon_address;
boost::optional<uint64_t> m_difficulty_low;
boost::optional<uint64_t> m_difficulty_high;
boost::optional<uint64_t> m_cumulative_difficulty_low;
boost::optional<uint64_t> m_cumulative_difficulty_high;
boost::optional<uint64_t> m_free_space;
boost::optional<int> m_num_offline_peers;
boost::optional<int> m_num_online_peers;
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_height_without_bootstrap;
boost::optional<monero_network_type> m_network_type;
boost::optional<bool> m_is_offline;
boost::optional<int> m_num_incoming_connections;
boost::optional<int> m_num_outgoing_connections;
boost::optional<int> m_num_rpc_connections;
boost::optional<uint64_t> m_start_timestamp;
boost::optional<uint64_t> m_adjusted_timestamp;
boost::optional<uint64_t> m_target;
boost::optional<uint64_t> m_target_height;
boost::optional<int> m_num_txs;
boost::optional<int> m_num_txs_pool;
boost::optional<bool> m_was_bootstrap_ever_used;
boost::optional<uint64_t> m_database_size;
boost::optional<bool> m_update_available;
boost::optional<bool> m_is_busy_syncing;
boost::optional<bool> m_is_synchronized;
boost::optional<bool> m_is_restricted;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_info>& info);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models daemon synchronization information.
*/
struct monero_daemon_sync_info : public monero_rpc_payment_info {
boost::optional<std::string> m_overview;
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_target_height;
boost::optional<int> m_next_needed_pruning_seed;
std::vector<std::shared_ptr<monero_peer>> m_peers;
std::vector<std::shared_ptr<monero_connection_span>> m_spans;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_sync_info>& info);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Monero hard fork info.
*/
struct monero_hard_fork_info : public monero_rpc_payment_info {
boost::optional<bool> m_is_enabled;
boost::optional<uint64_t> m_earliest_height;
boost::optional<int> m_state;
boost::optional<int> m_threshold;
boost::optional<int> m_version;
boost::optional<int> m_num_votes;
boost::optional<int> m_window;
boost::optional<int> m_voting;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_hard_fork_info>& info);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Models the result of generating blocks.
*/
struct monero_generate_blocks_result : public serializable_struct {
std::vector<std::string> m_block_hashes;
boost::optional<uint64_t> m_height;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_generate_blocks_result>& result);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
}
@@ -0,0 +1,892 @@
/**
* Copyright (c) everoddandeven
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2025-2026 woodser
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#include "monero_daemon_rpc.h"
#include "monero_daemon_rpc_model.h"
#include "common/monero_error.h"
#include "utils/gen_utils.h"
#include "utils/monero_utils.h"
static const uint64_t MAX_REQ_SIZE = 3000000;
static const uint64_t NUM_HEADERS_PER_REQ = 750;
namespace monero {
/**
* Polls daemon and sends notifications in order to notify external daemon listeners.
*/
class monero_daemon_poller : public gen_utils::thread_poller {
public:
explicit monero_daemon_poller(monero_daemon* daemon, uint64_t poll_period_ms = 10000): m_daemon(daemon) {
init_common("monero_daemon_rpc");
m_poll_period_ms = poll_period_ms;
}
~monero_daemon_poller() override {
set_is_polling(false);
}
void poll() override {
if (!m_last_header) {
m_last_header = m_daemon->get_last_block_header();
return;
}
auto header = m_daemon->get_last_block_header();
if (header->m_hash != m_last_header->m_hash) {
m_last_header = header;
announce_block_header(header);
}
}
private:
monero_daemon* m_daemon;
std::shared_ptr<monero_block_header> m_last_header;
void announce_block_header(const std::shared_ptr<monero_block_header>& header) {
gen_utils::thread_poller::announce_scope scope(*this); // see wait_for_callbacks_idle()
auto listeners = m_daemon->get_listeners();
for (auto& listener : listeners) {
try {
listener->on_block_header(header);
} catch (const std::exception& e) {
MERROR("Error calling listener on new block header: " << e.what());
}
}
}
};
/**
* Sends a notification on a condition variable when a block is added to blockchain.
*/
class block_notifier : public monero_daemon_listener {
public:
block_notifier(boost::mutex* temp, boost::condition_variable* cv, bool* ready) { this->temp = temp; this->cv = cv; this->ready = ready; }
void on_block_header(const std::shared_ptr<monero_block_header>& header) override {
boost::mutex::scoped_lock lock(*temp);
m_last_header = header;
*ready = true;
cv->notify_one();
}
private:
boost::mutex* temp;
boost::condition_variable* cv;
bool* ready;
};
void monero_daemon_rpc::set_poll_period_in_ms(uint64_t period_ms) {
boost::lock_guard<boost::mutex> poller_lock(m_poller_mutex);
if (!m_poller) m_poller = std::make_unique<monero_daemon_poller>(this);
m_poller->set_period_in_ms(period_ms);
}
/**
* Validate RPC response status.
*/
void check_response_status(const boost::property_tree::ptree& node) {
for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) {
std::string key = it->first;
if (key == std::string("status")) {
std::string status = it->second.data();
// TODO monero-project empty string status is returned for download update response when an update is available
if (status == std::string("OK") || status == std::string("")) {
return;
}
else throw monero_rpc_error(status);
}
}
throw monero_error("Could not get JSON RPC response status");
}
monero_daemon_rpc::monero_daemon_rpc(const std::shared_ptr<monero_rpc_connection>& rpc): m_rpc(rpc) {
if (rpc == nullptr) throw monero_error("Connection cannot be null");
if (!rpc->is_online().value_or(false) && rpc->m_uri != boost::none) rpc->check_connection();
}
monero_daemon_rpc::monero_daemon_rpc(const std::string& uri, const std::string& username, const std::string& password, const std::string& proxy_uri, const std::string& zmq_uri, const boost::optional<uint32_t>& timeout):
m_rpc(std::make_shared<monero_rpc_connection>(uri, username, password, proxy_uri, zmq_uri, 0, timeout)) {
if (!uri.empty()) m_rpc->check_connection();
}
std::set<monero_daemon_listener*> monero_daemon_rpc::get_listeners() {
boost::lock_guard<boost::recursive_mutex> lock(m_listeners_mutex);
return m_listeners;
}
void monero_daemon_rpc::add_listener(monero_daemon_listener &listener) {
{
boost::lock_guard<boost::recursive_mutex> lock(m_listeners_mutex);
m_listeners.insert(&listener);
}
refresh_listening();
}
void monero_daemon_rpc::remove_listener(monero_daemon_listener &listener) {
{
boost::lock_guard<boost::recursive_mutex> lock(m_listeners_mutex);
m_listeners.erase(&listener);
}
refresh_listening();
wait_for_listener_callbacks_idle();
}
void monero_daemon_rpc::remove_listeners() {
{
boost::lock_guard<boost::recursive_mutex> lock(m_listeners_mutex);
m_listeners.clear();
}
refresh_listening();
wait_for_listener_callbacks_idle();
}
void monero_daemon_rpc::wait_for_listener_callbacks_idle() {
// ensures no in-flight callback dispatch can still reference a just-removed listener
monero_daemon_poller* poller;
{
boost::lock_guard<boost::mutex> poller_lock(m_poller_mutex);
poller = m_poller.get();
}
if (poller != nullptr) poller->wait_for_callbacks_idle();
}
std::shared_ptr<monero_rpc_connection> monero_daemon_rpc::get_rpc_connection() const {
MTRACE("monero_daemon_rpc::get_rpc_connection()");
return m_rpc;
}
bool monero_daemon_rpc::is_connected() {
try {
get_version();
return true;
}
catch (...) {
return false;
}
}
monero_version monero_daemon_rpc::get_version() {
auto result = m_rpc->send_json_request("get_version");
check_response_status(result);
monero_version version;
deserialize_version(result, version);
return version;
}
bool monero_daemon_rpc::is_trusted() {
auto result = m_rpc->send_path_request("get_height");
check_response_status(result);
auto get_height_response = std::make_shared<monero_get_block_result>();
monero_get_block_result::from_property_tree(result, get_height_response);
return gen_utils::bool_equals(false, get_height_response->m_untrusted);
}
uint64_t monero_daemon_rpc::get_height() {
auto result = m_rpc->send_json_request("get_block_count");
check_response_status(result);
std::shared_ptr<monero_get_block_result> block_result = std::make_shared<monero_get_block_result>();
monero_get_block_result::from_property_tree(result, block_result);
if (block_result->m_count == boost::none) throw monero_error("Could not get height");
return block_result->m_count.get();
}
std::string monero_daemon_rpc::get_block_hash(uint64_t height) {
std::shared_ptr<monero_get_block_hash_params> params = std::make_shared<monero_get_block_hash_params>(height);
auto result = m_rpc->send_json_request("on_get_block_hash", params);
return result.data();
}
std::shared_ptr<monero_block_template> monero_daemon_rpc::get_block_template(const std::string& wallet_address, const boost::optional<int>& reserve_size) {
MTRACE("monero_daemon_rpc::get_block_template()");
auto params = std::make_shared<monero_get_block_params>(wallet_address, reserve_size);
auto result = m_rpc->send_json_request("get_block_template", params);
check_response_status(result);
std::shared_ptr<monero_block_template> tmplt = std::make_shared<monero_block_template>();
deserialize_block_template(result, tmplt);
return tmplt;
}
std::shared_ptr<monero_block_header> monero_daemon_rpc::get_last_block_header() {
auto result = m_rpc->send_json_request("get_last_block_header");
check_response_status(result);
std::shared_ptr<monero_block_header> header = std::make_shared<monero_block_header>();
deserialize_block_header(result, header);
return header;
}
std::shared_ptr<monero_block_header> monero_daemon_rpc::get_block_header_by_hash(const std::string& hash) {
std::shared_ptr<monero_get_block_params> params = std::make_shared<monero_get_block_params>(hash);
auto result = m_rpc->send_json_request("get_block_header_by_hash", params);
check_response_status(result);
std::shared_ptr<monero_block_header> header = std::make_shared<monero_block_header>();
deserialize_block_header(result, header);
return header;
}
std::shared_ptr<monero_block_header> monero_daemon_rpc::get_block_header_by_height(uint64_t height) {
std::shared_ptr<monero_get_block_params> params = std::make_shared<monero_get_block_params>(height);
auto result = m_rpc->send_json_request("get_block_header_by_height", params);
check_response_status(result);
std::shared_ptr<monero_block_header> header = std::make_shared<monero_block_header>();
deserialize_block_header(result, header);
return header;
}
std::vector<std::shared_ptr<monero_block_header>> monero_daemon_rpc::get_block_headers_by_range(uint64_t start_height, uint64_t end_height) {
auto params = std::make_shared<monero_get_block_params>(start_height, end_height);
auto result = m_rpc->send_json_request("get_block_headers_range", params);
check_response_status(result);
std::vector<std::shared_ptr<monero_block_header>> headers;
deserialize_block_headers(result, headers);
return headers;
}
std::shared_ptr<monero_block> monero_daemon_rpc::get_block_by_hash(const std::string& hash) {
std::shared_ptr<monero_get_block_params> params = std::make_shared<monero_get_block_params>(hash);
auto result = m_rpc->send_json_request("get_block", params);
check_response_status(result);
auto block = std::make_shared<monero_block>();
deserialize_block(result, block);
return block;
}
std::shared_ptr<monero_block_header> monero_daemon_rpc::get_block_header_by_height_cached(uint64_t height, uint64_t max_height, std::unordered_map<uint64_t, std::shared_ptr<monero_block_header>>& header_cache) {
// get header from cache
auto found = header_cache.find(height);
if (found != header_cache.end()) return found->second;
// fetch and cache headers if not in cache
uint64_t end_height = std::min(max_height, height + NUM_HEADERS_PER_REQ - 1);
auto headers = get_block_headers_by_range(height, end_height);
for(const auto& header : headers) {
if (header->m_height == boost::none) throw monero_error("Malformed block header: expected height to be defined");
header_cache[header->m_height.get()] = header;
}
// look up rather than default-construct via operator[]: a daemon response missing the
// requested height must not silently cache (and return) a null header
found = header_cache.find(height);
if (found == header_cache.end()) throw monero_error("Daemon did not return a header for height " + std::to_string(height));
return found->second;
}
std::vector<std::shared_ptr<monero_block>> monero_daemon_rpc::get_blocks_by_hash(const std::vector<std::string>& block_hashes, uint64_t start_height, bool prune) {
throw monero_error("monero_daemon_rpc::get_blocks_by_hash(): not implemented");
}
std::shared_ptr<monero_block> monero_daemon_rpc::get_block_by_height(uint64_t height) {
std::shared_ptr<monero_get_block_params> params = std::make_shared<monero_get_block_params>(height);
auto result = m_rpc->send_json_request("get_block", params);
check_response_status(result);
auto block = std::make_shared<monero_block>();
deserialize_block(result, block);
return block;
}
std::vector<std::shared_ptr<monero_block>> monero_daemon_rpc::get_blocks_by_height(const std::vector<uint64_t>& heights) {
// fetch blocks in binary
monero_get_blocks_by_height_request request(heights);
auto response = m_rpc->send_binary_request(request);
if (response.m_binary == boost::none) throw monero_error("Invalid Monero Binary response");
boost::property_tree::ptree node;
monero_utils::binary_blocks_to_property_tree(response.m_binary.get(), node);
check_response_status(node);
std::vector<std::shared_ptr<monero_block>> blocks;
deserialize_blocks(node, heights, blocks);
return blocks;
}
std::vector<std::shared_ptr<monero_block>> monero_daemon_rpc::get_blocks_by_range(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height) {
if (start_height == boost::none) start_height = 0;
if (end_height == boost::none) {
uint64_t block_height = get_height();
end_height = block_height == 0 ? 0 : block_height - 1;
}
std::vector<uint64_t> heights;
for (uint64_t height = start_height.get(); height <= end_height.get(); height++) heights.push_back(height);
return get_blocks_by_height(heights);
}
std::vector<std::shared_ptr<monero_block>> monero_daemon_rpc::get_blocks_by_range_chunked(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height, boost::optional<uint64_t> max_chunk_size) {
if (start_height == boost::none) start_height = 0;
if (end_height == boost::none) {
uint64_t block_height = get_height();
end_height = block_height == 0 ? 0 : block_height - 1;
}
boost::optional<uint64_t> last_height = boost::none;
std::vector<std::shared_ptr<monero_block>> blocks;
if (start_height.get() > end_height.get()) return blocks;
// scoped to this call: see the comment on get_max_blocks()/get_block_header_by_height_cached() in the header
std::unordered_map<uint64_t, std::shared_ptr<monero_block_header>> header_cache;
while (last_height == boost::none || last_height.get() < end_height.get()) {
uint64_t height_to_get = (last_height == boost::none) ? start_height.get() : last_height.get() + 1;
auto max_blocks = get_max_blocks(height_to_get, end_height, max_chunk_size, header_cache);
if (max_blocks.empty()) throw monero_error("Daemon returned an empty block chunk for a non-empty range at height " + std::to_string(height_to_get));
blocks.insert(blocks.end(), max_blocks.begin(), max_blocks.end());
last_height = blocks[blocks.size() - 1]->m_height.get();
}
return blocks;
}
std::vector<std::shared_ptr<monero_block>> monero_daemon_rpc::get_max_blocks(boost::optional<uint64_t> start_height, boost::optional<uint64_t> max_height, boost::optional<uint64_t> chunk_size, std::unordered_map<uint64_t, std::shared_ptr<monero_block_header>>& header_cache) {
if (start_height == boost::none) start_height = 0;
if (max_height == boost::none) {
uint64_t block_height = get_height();
max_height = block_height == 0 ? 0 : block_height - 1;
}
if (chunk_size == boost::none) chunk_size = MAX_REQ_SIZE;
// determine end height to fetch
uint64_t req_size = 0;
boost::optional<uint64_t> end_height = boost::none; // none = no block accepted yet
uint64_t height_to_get = start_height.get();
while (req_size < chunk_size.get() && height_to_get <= max_height.get()) {
// get header of next block
auto header = get_block_header_by_height_cached(height_to_get, max_height.get(), header_cache);
if (header->m_size == boost::none) throw monero_error("Malformed block header: expected size to be defined");
uint64_t header_size = header->m_size.get();
// block cannot be bigger than max request size
if (header_size > chunk_size.get()) throw monero_error("Block exceeds maximum request size: " + std::to_string(header_size));
// done iterating if fetching block would exceed max request size
if (req_size + header_size > chunk_size.get()) break;
// otherwise block is included
req_size += header_size;
end_height = height_to_get;
height_to_get++;
}
if (end_height != boost::none) {
return get_blocks_by_range(start_height, end_height.get());
}
return std::vector<std::shared_ptr<monero_block>>();
}
std::vector<std::string> monero_daemon_rpc::get_block_hashes(const std::vector<std::string>& block_hashes, uint64_t start_height) {
throw monero_error("monero_daemon_rpc::get_block_hashes(): not implemented");
}
std::vector<std::shared_ptr<monero_tx>> monero_daemon_rpc::get_txs(const std::vector<std::string>& tx_hashes, bool prune) {
MTRACE("monero_daemon_rpc::get_txs()");
if (tx_hashes.empty()) throw monero_error("Must provide an array of transaction hashes");
auto params = std::make_shared<monero_get_txs_params>(tx_hashes, prune);
auto result = m_rpc->send_path_request("get_transactions", params);
try { check_response_status(result); }
catch (const std::exception& ex) {
if (std::string(ex.what()).find("Failed to parse hex representation of transaction hash") != std::string::npos) {
throw monero_error("Invalid transaction hash");
}
throw;
}
std::vector<std::shared_ptr<monero_tx>> txs;
deserialize_txs(result, txs);
return txs;
}
std::vector<std::string> monero_daemon_rpc::get_tx_hexes(const std::vector<std::string>& tx_hashes, bool prune) {
MTRACE("monero_daemon_rpc::get_tx_hexes()");
std::vector<std::string> hexes;
for(const auto& tx : get_txs(tx_hashes, prune)) {
// tx may be pruned regardless of configuration
if (tx->m_pruned_hex == boost::none) {
if (tx->m_full_hex == boost::none) throw monero_error("Tx has no hex");
hexes.push_back(tx->m_full_hex.get());
} else {
hexes.push_back(tx->m_pruned_hex.get());
}
}
return hexes;
}
std::shared_ptr<monero_miner_tx_sum> monero_daemon_rpc::get_miner_tx_sum(uint64_t height, uint64_t num_blocks) {
auto params = std::make_shared<monero_get_miner_tx_sum_params>(height, num_blocks);
auto result = m_rpc->send_json_request("get_coinbase_tx_sum", params);
check_response_status(result);
auto sum = std::make_shared<monero_miner_tx_sum>();
deserialize_miner_tx_sum(result, sum);
return sum;
}
std::shared_ptr<monero_fee_estimate> monero_daemon_rpc::get_fee_estimate(uint64_t grace_blocks) {
auto params = std::make_shared<monero_get_fee_estimate_params>(grace_blocks);
auto result = m_rpc->send_json_request("get_fee_estimate", params);
check_response_status(result);
auto estimate = std::make_shared<monero_fee_estimate>();
deserialize_fee_estimate(result, estimate);
return estimate;
}
std::shared_ptr<monero_submit_tx_result> monero_daemon_rpc::submit_tx_hex(const std::string& tx_hex, bool do_not_relay) {
MTRACE("monero_daemon_rpc::submit_tx_hex()");
auto params = std::make_shared<monero_submit_tx_params>(tx_hex, do_not_relay);
auto result = m_rpc->send_path_request("send_raw_transaction", params);
auto sum = std::make_shared<monero_submit_tx_result>();
deserialize_submit_tx_result(result, sum);
// set m_is_good based on status
try {
check_response_status(result);
sum->m_is_good = true;
} catch (...) {
sum->m_is_good = false;
}
return sum;
}
void monero_daemon_rpc::relay_txs_by_hash(const std::vector<std::string>& tx_hashes) {
MTRACE("monero_daemon_rpc::relay_txs_by_hash()");
auto params = std::make_shared<monero_submit_tx_params>(tx_hashes);
auto result = m_rpc->send_json_request("relay_tx", params);
check_response_status(result);
}
std::shared_ptr<monero_tx_pool_stats> monero_daemon_rpc::get_tx_pool_stats() {
auto result = m_rpc->send_path_request("get_transaction_pool_stats");
check_response_status(result);
auto stats = std::make_shared<monero_tx_pool_stats>();
deserialize_tx_pool_stats(result, stats);
return stats;
}
std::vector<std::shared_ptr<monero_tx>> monero_daemon_rpc::get_tx_pool() {
auto result = m_rpc->send_path_request("get_transaction_pool");
check_response_status(result);
std::vector<std::shared_ptr<monero_tx>> pool;
deserialize_txs(result, pool);
return pool;
}
std::vector<std::string> monero_daemon_rpc::get_tx_pool_hashes() {
auto result = m_rpc->send_path_request("get_transaction_pool_hashes");
check_response_status(result);
std::vector<std::string> tx_hashes;
deserialize_tx_hashes(result, tx_hashes);
return tx_hashes;
}
void monero_daemon_rpc::flush_tx_pool(const std::vector<std::string> &hashes) {
MTRACE("monero_daemon_rpc::flush_tx_pool()");
auto params = std::make_shared<monero_submit_tx_params>(hashes);
auto result = m_rpc->send_json_request("flush_txpool", params);
check_response_status(result);
}
void monero_daemon_rpc::flush_tx_pool() {
std::vector<std::string> hashes;
flush_tx_pool(hashes);
}
void monero_daemon_rpc::flush_tx_pool(const std::string &hash) {
std::vector<std::string> hashes;
hashes.push_back(hash);
flush_tx_pool(hashes);
}
std::vector<monero_key_image_spent_status> monero_daemon_rpc::get_key_image_spent_statuses(const std::vector<std::string>& key_images) {
if (key_images.empty()) throw monero_error("Must provide key images to check the status of");
auto params = std::make_shared<monero_is_key_image_spent_params>(key_images);
auto result = m_rpc->send_path_request("is_key_image_spent", params);
check_response_status(result);
std::vector<monero_key_image_spent_status> statuses;
deserialize_key_image_spent_status(result, statuses);
return statuses;
}
std::vector<std::shared_ptr<monero_output>> monero_daemon_rpc::get_outputs(const std::vector<monero_output>& outputs) {
throw monero_error("monero_daemon_rpc::get_outputs(): not implemented");
}
std::vector<std::shared_ptr<monero_output_histogram_entry>> monero_daemon_rpc::get_output_histogram(const std::vector<uint64_t>& amounts, const boost::optional<int>& min_count, const boost::optional<int>& max_count, const boost::optional<bool>& is_unlocked, const boost::optional<int>& recent_cutoff) {
MTRACE("monero_daemon_rpc::get_output_histogram()");
auto params = std::make_shared<monero_get_output_histogram_params>(amounts, min_count, max_count, is_unlocked, recent_cutoff);
auto result = m_rpc->send_json_request("get_output_histogram", params);
check_response_status(result);
std::vector<std::shared_ptr<monero_output_histogram_entry>> entries;
deserialize_output_histogram_entries(result, entries);
return entries;
}
std::vector<std::shared_ptr<monero_output_distribution_entry>> monero_daemon_rpc::get_output_distribution(const std::vector<uint64_t>& amounts, const boost::optional<bool>& is_cumulative, const boost::optional<uint64_t>& start_height, const boost::optional<uint64_t>& end_height) {
MTRACE("monero_daemon_rpc::get_output_distribution()");
auto params = std::make_shared<monero_get_output_distribution_params>(amounts, is_cumulative, start_height, end_height);
auto result = m_rpc->send_json_request("get_output_distribution", params);
check_response_status(result);
std::vector<std::shared_ptr<monero_output_distribution_entry>> entries;
deserialize_output_distribution_entries(result, entries);
return entries;
}
std::shared_ptr<monero_daemon_info> monero_daemon_rpc::get_info() {
auto result = m_rpc->send_json_request("get_info");
check_response_status(result);
std::shared_ptr<monero_daemon_info> info = std::make_shared<monero_daemon_info>();
deserialize_daemon_info(result, info);
return info;
}
std::shared_ptr<monero_daemon_sync_info> monero_daemon_rpc::get_sync_info() {
auto result = m_rpc->send_json_request("sync_info");
check_response_status(result);
std::shared_ptr<monero_daemon_sync_info> info = std::make_shared<monero_daemon_sync_info>();
deserialize_daemon_sync_info(result, info);
return info;
}
std::shared_ptr<monero_hard_fork_info> monero_daemon_rpc::get_hard_fork_info() {
auto result = m_rpc->send_json_request("hard_fork_info");
check_response_status(result);
std::shared_ptr<monero_hard_fork_info> info = std::make_shared<monero_hard_fork_info>();
deserialize_hard_fork_info(result, info);
return info;
}
std::vector<std::shared_ptr<monero_alt_chain>> monero_daemon_rpc::get_alt_chains() {
auto result = m_rpc->send_json_request("get_alternate_chains");
check_response_status(result);
std::vector<std::shared_ptr<monero_alt_chain>> alt_chains;
deserialize_alt_chains(result, alt_chains);
return alt_chains;
}
std::vector<std::string> monero_daemon_rpc::get_alt_block_hashes() {
auto result = m_rpc->send_path_request("get_alt_blocks_hashes");
check_response_status(result);
std::vector<std::string> hashes;
deserialize_alt_block_hashes(result, hashes);
return hashes;
}
int monero_daemon_rpc::get_download_limit() {
MTRACE("monero_daemon_rpc::get_download_limit()");
auto limits = get_bandwidth_limits();
if (limits->m_down != boost::none) return limits->m_down.get();
throw monero_error("Could not get download limit");
}
int monero_daemon_rpc::set_download_limit(int limit) {
MTRACE("monero_daemon_rpc::set_download_limit()");
if (limit == -1) return reset_download_limit();
if (limit <= 0) throw monero_error("Download limit must be an integer greater than 0");
auto result = set_bandwidth_limits(0, limit);
if (result->m_down != boost::none) return result->m_down.get();
throw monero_error("Could not set download limit");
}
int monero_daemon_rpc::reset_download_limit() {
MTRACE("monero_daemon_rpc::reset_download_limit()");
auto result = set_bandwidth_limits(0, -1);
if (result->m_down != boost::none) return result->m_down.get();
throw monero_error("Could not set download limit");
}
int monero_daemon_rpc::get_upload_limit() {
MTRACE("monero_daemon_rpc::get_upload_limit()");
auto limits = get_bandwidth_limits();
if (limits->m_up != boost::none) return limits->m_up.get();
throw monero_error("Could not get upload limit");
}
int monero_daemon_rpc::set_upload_limit(int limit) {
MTRACE("monero_daemon_rpc::set_upload_limit()");
if (limit == -1) return reset_upload_limit();
if (limit <= 0) throw monero_error("Upload limit must be an integer greater than 0");
auto result = set_bandwidth_limits(limit, 0);
if (result->m_up != boost::none) return result->m_up.get();
throw monero_error("Could not set download limit");
}
int monero_daemon_rpc::reset_upload_limit() {
MTRACE("monero_daemon_rpc::reset_upload_limit()");
auto result = set_bandwidth_limits(-1, 0);
if (result->m_up != boost::none) return result->m_up.get();
throw monero_error("Could not set download limit");
}
std::vector<std::shared_ptr<monero_peer>> monero_daemon_rpc::get_peers() {
auto result = m_rpc->send_json_request("get_connections");
check_response_status(result);
std::vector<std::shared_ptr<monero_peer>> peers;
deserialize_peers(result, peers);
return peers;
}
std::vector<std::shared_ptr<monero_peer>> monero_daemon_rpc::get_known_peers() {
auto result = m_rpc->send_path_request("get_peer_list");
check_response_status(result);
std::vector<std::shared_ptr<monero_peer>> peers;
deserialize_peers(result, peers);
return peers;
}
void monero_daemon_rpc::set_outgoing_peer_limit(int limit) {
MTRACE("monero_daemon_rpc::set_outgoing_peer_limit()");
if (limit < 0) throw monero_error("Outgoing peer limit must be >= 0");
auto params = std::make_shared<monero_peer_limits_params>();
params->m_out_peers = limit;
auto result = m_rpc->send_path_request("out_peers", params);
check_response_status(result);
}
void monero_daemon_rpc::set_incoming_peer_limit(int limit) {
MTRACE("monero_daemon_rpc::set_incoming_peer_limit()");
if (limit < 0) throw monero_error("Incoming peer limit must be >= 0");
auto params = std::make_shared<monero_peer_limits_params>();
params->m_in_peers = limit;
auto result = m_rpc->send_path_request("in_peers", params);
check_response_status(result);
}
std::vector<std::shared_ptr<monero_ban>> monero_daemon_rpc::get_peer_bans() {
MTRACE("monero_daemon_rpc::get_peer_bans()");
auto result = m_rpc->send_json_request("get_bans");
check_response_status(result);
std::vector<std::shared_ptr<monero_ban>> bans;
deserialize_bans(result, bans);
return bans;
}
void monero_daemon_rpc::set_peer_bans(const std::vector<std::shared_ptr<monero_ban>>& bans) {
MTRACE("monero_daemon_rpc::set_peer_bans()");
auto params = std::make_shared<monero_set_bans_params>(bans);
auto result = m_rpc->send_json_request("set_bans", params);
check_response_status(result);
}
void monero_daemon_rpc::start_mining(const std::string &address, boost::optional<uint64_t> num_threads, boost::optional<bool> background_mining, boost::optional<bool> ignore_battery) {
MTRACE("monero_daemon_rpc::start_mining()");
if (address.empty()) throw monero_error("Must provide address to mine to");
if (num_threads != boost::none && num_threads.get() == 0) throw monero_error("Number of threads must be an integer greater than 0");
auto params = std::make_shared<monero_start_mining_params>(address, num_threads, background_mining, ignore_battery);
auto result = m_rpc->send_path_request("start_mining", params);
check_response_status(result);
}
void monero_daemon_rpc::stop_mining() {
MTRACE("monero_daemon_rpc::stop_mining()");
auto result = m_rpc->send_path_request("stop_mining");
check_response_status(result);
}
std::shared_ptr<monero_mining_status> monero_daemon_rpc::get_mining_status() {
MTRACE("monero_daemon_rpc::get_mining_status()");
auto result = m_rpc->send_path_request("mining_status");
check_response_status(result);
auto status = std::make_shared<monero_mining_status>();
deserialize_mining_status(result, status);
return status;
}
std::shared_ptr<monero_generate_blocks_result> monero_daemon_rpc::generate_blocks(const std::string& wallet_address, uint64_t num_blocks, const boost::optional<std::string>& prev_block_hash, const boost::optional<uint32_t>& starting_nonce) {
MTRACE("monero_daemon_rpc::generate_blocks()");
if (wallet_address.empty()) throw monero_error("Must provide an address to mine to");
if (num_blocks == 0) throw monero_error("Must provide a number of blocks to generate greater than 0");
auto params = std::make_shared<monero_generate_blocks_params>(wallet_address, num_blocks, prev_block_hash, starting_nonce);
auto result = m_rpc->send_json_request("generateblocks", params);
check_response_status(result);
auto generate_result = std::make_shared<monero_generate_blocks_result>();
deserialize_generate_blocks_result(result, generate_result);
return generate_result;
}
void monero_daemon_rpc::submit_blocks(const std::vector<std::string>& block_blobs) {
MTRACE("monero_daemon_rpc::submit_blocks()");
if (block_blobs.empty()) throw monero_error("Must provide an array of mined block blobs to submit");
auto params = std::make_shared<monero_submit_blocks_params>(block_blobs);
auto result = m_rpc->send_json_request("submit_block", params);
check_response_status(result);
}
std::shared_ptr<monero_prune_result> monero_daemon_rpc::prune_blockchain(bool check) {
MTRACE("monero_daemon_rpc::prune_blockchain()");
auto params = std::make_shared<monero_prune_blockchain_params>(check);
auto result = m_rpc->send_json_request("prune_blockchain", params);
check_response_status(result);
std::shared_ptr<monero_prune_result> prune_result = std::make_shared<monero_prune_result>();
deserialize_prune_result(result, prune_result);
return prune_result;
}
std::shared_ptr<monero_daemon_update_check_result> monero_daemon_rpc::check_for_update() {
MTRACE("monero_daemon_rpc::check_for_update()");
auto params = std::make_shared<monero_download_update_params>("check");
auto result = m_rpc->send_path_request("update", params);
check_response_status(result);
auto check_result = std::make_shared<monero_daemon_update_check_result>();
deserialize_update_check_result(result, check_result);
return check_result;
}
std::shared_ptr<monero_daemon_update_download_result> monero_daemon_rpc::download_update(const std::string& path) {
MTRACE("monero_daemon_rpc::download_update()");
auto params = std::make_shared<monero_download_update_params>("download", path);
auto result = m_rpc->send_path_request("update", params);
check_response_status(result);
auto download_result = std::make_shared<monero_daemon_update_download_result>();
deserialize_update_download_result(result, download_result);
return download_result;
}
void monero_daemon_rpc::stop() {
MTRACE("monero_daemon_rpc::stop()");
auto result = m_rpc->send_path_request("stop_daemon");
check_response_status(result);
}
std::shared_ptr<monero_block_header> monero_daemon_rpc::wait_for_next_block_header() {
MTRACE("monero_daemon_rpc::wait_for_next_block_header()");
// use mutex and condition variable with predicate to wait for block
boost::mutex temp;
boost::condition_variable cv;
bool ready = false;
// create listener which notifies condition variable when block is added
block_notifier block_listener(&temp, &cv, &ready);
// register the listener, and unregister it unconditionally on the way out
// otherwise m_listeners keeps a pointer into this stack
// frame after it's gone, and the next block dispatch calls through it (UAF)
struct listener_guard {
monero_daemon_rpc* daemon;
monero_daemon_listener& listener;
~listener_guard() {
// never let remove_listener() escape a destructor since if we're
// already unwinding from another exception, that would call std::terminate
try { daemon->remove_listener(listener); } catch (...) {}
}
} unregister_guard{this, block_listener};
add_listener(block_listener);
// guard against an offline/unresponsive daemon (no connection, stalled node, etc.):
// such a daemon will never announce a new block, so bound the wait instead of hanging
// this call forever.
// TODO monero-java waits indefinitely.
bool timed_out;
std::shared_ptr<monero_block_header> header;
{
boost::mutex::scoped_lock lock(temp);
cv.timed_wait(lock, boost::posix_time::minutes(30), [&]() { return ready; });
// check ready directly rather than trust timed_wait's return value, still under the
// lock: avoids any discrepancy if a header arrives right at the timeout boundary
timed_out = !ready;
// copy out while still holding the lock: block_listener stays registered until the
// unregister_guard destructor runs (at function exit, after this point), so
// on_block_header() could still write m_last_header under temp concurrently with an
// unlocked read here
if (!timed_out) header = block_listener.m_last_header;
}
if (timed_out) throw monero_error("Timed out waiting for next block header");
// return last height
if (header == nullptr) throw monero_error("Could not get last block header.");
return header;
}
std::shared_ptr<monero_bandwidth_limits> monero_daemon_rpc::get_bandwidth_limits() {
MTRACE("monero_daemon_rpc::get_bandwidth_limits()");
auto result = m_rpc->send_path_request("get_limit");
check_response_status(result);
auto limits = std::make_shared<monero_bandwidth_limits>();
monero_bandwidth_limits::from_property_tree(result, limits);
return limits;
}
std::shared_ptr<monero_bandwidth_limits> monero_daemon_rpc::set_bandwidth_limits(int up, int down) {
MTRACE("monero_daemon_rpc::set_bandwidth_limits()");
auto limits = std::make_shared<monero_bandwidth_limits>(up, down);
auto result = m_rpc->send_path_request("set_limit", limits);
check_response_status(result);
monero_bandwidth_limits::from_property_tree(result, limits);
return limits;
}
void monero_daemon_rpc::refresh_listening() {
boost::lock_guard<boost::mutex> poller_lock(m_poller_mutex);
bool should_poll;
{
boost::lock_guard<boost::recursive_mutex> lock(m_listeners_mutex);
should_poll = m_listeners.size() > 0;
}
if (!m_poller && should_poll) {
m_poller = std::make_unique<monero_daemon_poller>(this);
}
if (m_poller) m_poller->request_is_polling(should_poll);
}
monero_daemon_rpc::~monero_daemon_rpc() {
MTRACE("~monero_daemon_rpc()");
remove_listeners();
}
}
@@ -0,0 +1,189 @@
/**
* Copyright (c) everoddandeven
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2025-2026 woodser
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "monero_daemon.h"
/**
* Implements a monero_daemon.h using monero-project's monerod.
*/
namespace monero {
// forward declaration of internal daemon poller
class monero_daemon_poller;
/**
* Implements a Monero daemon using monerod-rpc.
*/
class monero_daemon_rpc : public monero_daemon {
public:
/**
* Destruct the daemon.
*/
~monero_daemon_rpc() override;
monero_daemon_rpc(const std::shared_ptr<monero_rpc_connection>& rpc);
monero_daemon_rpc(const std::string& uri, const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const std::string& zmq_uri = "", const boost::optional<uint32_t>& timeout = boost::none);
/**
* Get the daemon's RPC connection.
*
* @return the daemon's rpc connection
*/
std::shared_ptr<monero_rpc_connection> get_rpc_connection() const;
/**
* Indicates if the client is connected to the daemon via RPC.
*
* @return true if the client is connected to the daemon, false otherwise
*/
bool is_connected();
/**
* Set the daemon poll period.
*
* @param period_ms poll period in milliseconds.
*/
void set_poll_period_in_ms(uint64_t period_ms);
/**
* Supported daemon methods.
*/
std::set<monero_daemon_listener*> get_listeners() override;
void add_listener(monero_daemon_listener &listener) override;
void remove_listener(monero_daemon_listener &listener) override;
void remove_listeners() override;
monero_version get_version() override;
bool is_trusted() override;
uint64_t get_height() override;
std::string get_block_hash(uint64_t height) override;
std::shared_ptr<monero_block_template> get_block_template(const std::string& wallet_address, const boost::optional<int>& reserve_size = boost::none) override;
std::shared_ptr<monero_block_header> get_last_block_header() override;
std::shared_ptr<monero_block_header> get_block_header_by_hash(const std::string& hash) override;
std::shared_ptr<monero_block_header> get_block_header_by_height(uint64_t height) override;
std::vector<std::shared_ptr<monero_block_header>> get_block_headers_by_range(uint64_t start_height, uint64_t end_height) override;
std::shared_ptr<monero_block> get_block_by_hash(const std::string& hash) override;
std::vector<std::shared_ptr<monero_block>> get_blocks_by_hash(const std::vector<std::string>& block_hashes, uint64_t start_height, bool prune) override;
std::shared_ptr<monero_block> get_block_by_height(uint64_t height) override;
std::vector<std::shared_ptr<monero_block>> get_blocks_by_height(const std::vector<uint64_t>& heights) override;
std::vector<std::shared_ptr<monero_block>> get_blocks_by_range(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height) override;
std::vector<std::shared_ptr<monero_block>> get_blocks_by_range_chunked(boost::optional<uint64_t> start_height, boost::optional<uint64_t> end_height, boost::optional<uint64_t> max_chunk_size) override;
std::vector<std::string> get_block_hashes(const std::vector<std::string>& block_hashes, uint64_t start_height) override;
std::vector<std::shared_ptr<monero_tx>> get_txs(const std::vector<std::string>& tx_hashes, bool prune = false) override;
std::vector<std::string> get_tx_hexes(const std::vector<std::string>& tx_hashes, bool prune = false) override;
std::shared_ptr<monero_miner_tx_sum> get_miner_tx_sum(uint64_t height, uint64_t num_blocks) override;
std::shared_ptr<monero_fee_estimate> get_fee_estimate(uint64_t grace_blocks = 0) override;
std::shared_ptr<monero_submit_tx_result> submit_tx_hex(const std::string& tx_hex, bool do_not_relay = false) override;
void relay_txs_by_hash(const std::vector<std::string>& tx_hashes) override;
std::shared_ptr<monero_tx_pool_stats> get_tx_pool_stats() override;
std::vector<std::shared_ptr<monero_tx>> get_tx_pool() override;
std::vector<std::string> get_tx_pool_hashes() override;
void flush_tx_pool(const std::vector<std::string> &hashes) override;
void flush_tx_pool() override;
void flush_tx_pool(const std::string &hash) override;
std::vector<monero_key_image_spent_status> get_key_image_spent_statuses(const std::vector<std::string>& key_images) override;
std::vector<std::shared_ptr<monero_output>> get_outputs(const std::vector<monero_output>& outputs) override;
std::vector<std::shared_ptr<monero_output_histogram_entry>> get_output_histogram(const std::vector<uint64_t>& amounts, const boost::optional<int>& min_count, const boost::optional<int>& max_count, const boost::optional<bool>& is_unlocked, const boost::optional<int>& recent_cutoff) override;
std::vector<std::shared_ptr<monero_output_distribution_entry>> get_output_distribution(const std::vector<uint64_t>& amounts, const boost::optional<bool>& is_cumulative = boost::none, const boost::optional<uint64_t>& start_height = boost::none, const boost::optional<uint64_t>& end_height = boost::none) override;
std::shared_ptr<monero_daemon_info> get_info() override;
std::shared_ptr<monero_daemon_sync_info> get_sync_info() override;
std::shared_ptr<monero_hard_fork_info> get_hard_fork_info() override;
std::vector<std::shared_ptr<monero_alt_chain>> get_alt_chains() override;
std::vector<std::string> get_alt_block_hashes() override;
int get_download_limit() override;
int set_download_limit(int limit) override;
int reset_download_limit() override;
int get_upload_limit() override;
int set_upload_limit(int limit) override;
int reset_upload_limit() override;
std::vector<std::shared_ptr<monero_peer>> get_peers() override;
std::vector<std::shared_ptr<monero_peer>> get_known_peers() override;
void set_outgoing_peer_limit(int limit) override;
void set_incoming_peer_limit(int limit) override;
std::vector<std::shared_ptr<monero_ban>> get_peer_bans() override;
void set_peer_bans(const std::vector<std::shared_ptr<monero_ban>>& bans) override;
void start_mining(const std::string &address, boost::optional<uint64_t> num_threads, boost::optional<bool> background_mining, boost::optional<bool> ignore_battery) override;
void stop_mining() override;
std::shared_ptr<monero_mining_status> get_mining_status() override;
std::shared_ptr<monero_generate_blocks_result> generate_blocks(const std::string& wallet_address, uint64_t num_blocks, const boost::optional<std::string>& prev_block_hash = boost::none, const boost::optional<uint32_t>& starting_nonce = boost::none) override;
void submit_blocks(const std::vector<std::string>& block_blobs) override;
std::shared_ptr<monero_prune_result> prune_blockchain(bool check) override;
std::shared_ptr<monero_daemon_update_check_result> check_for_update() override;
std::shared_ptr<monero_daemon_update_download_result> download_update(const std::string& path = "") override;
void stop() override;
std::shared_ptr<monero_block_header> wait_for_next_block_header() override;
// --------------------------------- PRIVATE --------------------------------
private:
friend class monero_daemon_poller;
mutable boost::recursive_mutex m_listeners_mutex;
std::set<monero_daemon_listener*> m_listeners;
std::shared_ptr<monero_rpc_connection> m_rpc;
boost::mutex m_poller_mutex;
std::unique_ptr<monero_daemon_poller> m_poller;
// header_cache is scoped to a single get_blocks_by_range_chunked() call (see below) rather than
// stored as a member: it only helps avoid re-fetching a header already seen earlier in that one
// chunking pass, and keeping it as instance state would let it grow unbounded over the connection's
// lifetime and serve stale headers across reorgs on later calls
std::vector<std::shared_ptr<monero_block>> get_max_blocks(boost::optional<uint64_t> start_height, boost::optional<uint64_t> max_height, boost::optional<uint64_t> chunk_size, std::unordered_map<uint64_t, std::shared_ptr<monero_block_header>>& header_cache);
std::shared_ptr<monero_block_header> get_block_header_by_height_cached(uint64_t height, uint64_t max_height, std::unordered_map<uint64_t, std::shared_ptr<monero_block_header>>& header_cache);
std::shared_ptr<monero_bandwidth_limits> get_bandwidth_limits();
std::shared_ptr<monero_bandwidth_limits> set_bandwidth_limits(int up, int down);
void refresh_listening();
void wait_for_listener_callbacks_idle();
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,288 @@
/**
* Copyright (c) everoddandeven
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2025-2026 woodser
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "common/monero_rpc_connection.h"
/**
* Internal data model for monero_daemon_rpc.h.
*/
namespace monero {
/**
* Models a Monero RPC banhammer.
*/
struct monero_rpc_ban : public monero_ban {
monero_rpc_ban(const std::shared_ptr<monero_ban> &ban);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
// ------------------------------ Binary Request ---------------------------------
/**
* Models paramaters for monerod-rpc `/get_blocks_by_height.bin` method.
*/
struct monero_get_blocks_by_height_request : public monero_rpc_request {
std::vector<uint64_t> m_heights;
monero_get_blocks_by_height_request(uint64_t num_blocks);
monero_get_blocks_by_height_request(const std::vector<uint64_t>& heights): m_heights(heights) { m_method = "get_blocks_by_height.bin"; }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
// ------------------------------ RPC Params ---------------------------------
struct monero_download_update_params : public serializable_struct {
boost::optional<std::string> m_command;
boost::optional<std::string> m_path;
monero_download_update_params(const std::string& command = "download", const std::string& path = "");
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_submit_tx_params : public serializable_struct {
boost::optional<std::string> m_tx_hex;
boost::optional<bool> m_do_not_relay;
std::vector<std::string> m_tx_hashes;
monero_submit_tx_params(const std::vector<std::string>& tx_hashes): m_tx_hashes(tx_hashes) { }
monero_submit_tx_params(const std::string& tx_hex, bool do_not_relay): m_tx_hex(tx_hex), m_do_not_relay(do_not_relay) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_peer_limits_params : public serializable_struct {
boost::optional<int> m_in_peers;
boost::optional<int> m_out_peers;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_txs_params : public serializable_struct {
std::vector<std::string> m_tx_hashes;
boost::optional<bool> m_decode_as_json;
boost::optional<bool> m_prune;
monero_get_txs_params(const std::vector<std::string> &tx_hashes, bool prune, bool decode_as_json = true): m_tx_hashes(tx_hashes), m_prune(prune), m_decode_as_json(decode_as_json) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_is_key_image_spent_params : public serializable_struct {
std::vector<std::string> m_key_images;
monero_is_key_image_spent_params(const std::vector<std::string>& key_images): m_key_images(key_images) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
// ------------------------------ JSON-RPC Params ---------------------------------
struct monero_start_mining_params : public serializable_struct {
boost::optional<std::string> m_miner_address;
boost::optional<uint64_t> m_num_threads;
boost::optional<bool> m_is_background;
boost::optional<bool> m_ignore_battery;
monero_start_mining_params(const boost::optional<std::string>& address, const boost::optional<uint64_t>& num_threads, const boost::optional<bool>& is_background, const boost::optional<bool>& ignore_battery): m_miner_address(address), m_num_threads(num_threads), m_is_background(is_background), m_ignore_battery(ignore_battery) { }
monero_start_mining_params(const boost::optional<uint64_t>& num_threads, const boost::optional<bool>& is_background, const boost::optional<bool>& ignore_battery): m_num_threads(num_threads), m_is_background(is_background), m_ignore_battery(ignore_battery) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_generate_blocks_params : public serializable_struct {
boost::optional<std::string> m_wallet_address;
boost::optional<uint64_t> m_num_blocks;
boost::optional<std::string> m_prev_block_hash;
boost::optional<uint32_t> m_starting_nonce;
monero_generate_blocks_params(const std::string& wallet_address, uint64_t num_blocks, const boost::optional<std::string>& prev_block_hash, const boost::optional<uint32_t>& starting_nonce): m_wallet_address(wallet_address), m_num_blocks(num_blocks), m_prev_block_hash(prev_block_hash), m_starting_nonce(starting_nonce) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_prune_blockchain_params : public serializable_struct {
boost::optional<bool> m_check;
monero_prune_blockchain_params(bool check = true): m_check(check) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_submit_blocks_params : public serializable_struct {
std::vector<std::string> m_block_blobs;
monero_submit_blocks_params(const std::vector<std::string>& block_blobs): m_block_blobs(block_blobs) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_block_params : public serializable_struct {
boost::optional<uint64_t> m_height;
boost::optional<std::string> m_hash;
boost::optional<bool> m_fill_pow_hash;
boost::optional<uint64_t> m_start_height;
boost::optional<uint64_t> m_end_height;
boost::optional<std::string> m_wallet_address;
boost::optional<int> m_reserve_size;
monero_get_block_params(uint64_t height, bool fill_pow_hash = false): m_height(height), m_fill_pow_hash(fill_pow_hash) { }
monero_get_block_params(const std::string& hash, bool fill_pow_hash = false): m_hash(hash), m_fill_pow_hash(fill_pow_hash) { }
monero_get_block_params(uint64_t start_height, uint64_t end_height): m_start_height(start_height), m_end_height(end_height) { }
monero_get_block_params(const std::string& wallet_address, const boost::optional<int>& reserve_size): m_wallet_address(wallet_address), m_reserve_size(reserve_size) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_block_hash_params : public serializable_struct {
boost::optional<uint64_t> m_height;
monero_get_block_hash_params(uint64_t height): m_height(height) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_miner_tx_sum_params : public serializable_struct {
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_count;
monero_get_miner_tx_sum_params(uint64_t height, uint64_t count): m_height(height), m_count(count) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_fee_estimate_params : public serializable_struct {
boost::optional<uint64_t> m_grace_blocks;
monero_get_fee_estimate_params(uint64_t grace_blocks = 0): m_grace_blocks(grace_blocks) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_set_bans_params : public serializable_struct {
std::vector<std::shared_ptr<monero_rpc_ban>> m_bans;
monero_set_bans_params(const std::vector<std::shared_ptr<monero_ban>>& bans);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_output_histogram_params : public serializable_struct {
std::vector<uint64_t> m_amounts;
boost::optional<int> m_min_count;
boost::optional<int> m_max_count;
boost::optional<bool> m_is_unlocked;
boost::optional<int> m_recent_cutoff;
monero_get_output_histogram_params(const std::vector<uint64_t>& amounts, const boost::optional<int>& min_count, const boost::optional<int>& max_count, const boost::optional<bool>& is_unlocked, const boost::optional<int>& recent_cutoff) : m_amounts(amounts), m_min_count(min_count), m_max_count(max_count), m_is_unlocked(is_unlocked), m_recent_cutoff(recent_cutoff) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_output_distribution_params : public serializable_struct {
std::vector<uint64_t> m_amounts;
boost::optional<bool> m_cumulative;
boost::optional<bool> m_binary;
boost::optional<uint64_t> m_from_height;
boost::optional<uint64_t> m_to_height;
monero_get_output_distribution_params(const std::vector<uint64_t>& amounts, const boost::optional<bool>& cumulative, const boost::optional<uint64_t>& from_height, const boost::optional<uint64_t>& to_height) : m_amounts(amounts), m_cumulative(cumulative), m_from_height(from_height), m_to_height(to_height), m_binary(false) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
// ------------------------------ JSON-RPC Response ---------------------------------
struct monero_get_block_result {
boost::optional<uint64_t> m_count;
boost::optional<uint64_t> m_height;
boost::optional<bool> m_untrusted;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_get_block_result>& result);
};
// ------------------------------ RPC Deserialization ---------------------------------
void deserialize_version(const boost::property_tree::ptree& node, monero_version& version);
void deserialize_block_header(const boost::property_tree::ptree& node, const std::shared_ptr<monero_block_header>& header);
void deserialize_block_headers(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_block_header>>& headers);
void deserialize_block(const boost::property_tree::ptree& node, const std::shared_ptr<monero_block>& block, bool is_nested = false);
void deserialize_blocks(const boost::property_tree::ptree& node, const std::vector<uint64_t>& heights, std::vector<std::shared_ptr<monero_block>>& blocks);
void deserialize_alt_block_hashes(const boost::property_tree::ptree& node, std::vector<std::string>& block_hashes);
void deserialize_txs(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_tx>>& txs);
void deserialize_tx_hashes(const boost::property_tree::ptree& node, std::vector<std::string>& tx_hashes);
void deserialize_key_image_spent_status(const boost::property_tree::ptree& node, std::vector<monero_key_image_spent_status>& statuses);
void deserialize_alt_chains(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_alt_chain>>& alt_chains);
void deserialize_bans(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_ban>>& bans);
void deserialize_prune_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_prune_result>& result);
void deserialize_mining_status(const boost::property_tree::ptree& node, const std::shared_ptr<monero_mining_status>& status);
void deserialize_generate_blocks_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_generate_blocks_result>& result);
void deserialize_miner_tx_sum(const boost::property_tree::ptree& node, const std::shared_ptr<monero_miner_tx_sum>& sum);
void deserialize_block_template(const boost::property_tree::ptree& node, const std::shared_ptr<monero_block_template>& tmplt);
void deserialize_peers(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_peer>>& peers);
void deserialize_submit_tx_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_submit_tx_result>& result);
void deserialize_output_distribution_entries(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_output_distribution_entry>>& entries);
void deserialize_output_histogram_entries(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_output_histogram_entry>>& entries);
void deserialize_tx_pool_stats(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_pool_stats>& stats);
void deserialize_update_check_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_update_check_result>& check);
void deserialize_update_download_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_update_download_result>& check);
void deserialize_fee_estimate(const boost::property_tree::ptree& node, const std::shared_ptr<monero_fee_estimate>& estimate);
void deserialize_daemon_info(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_info>& info);
void deserialize_daemon_sync_info(const boost::property_tree::ptree& node, const std::shared_ptr<monero_daemon_sync_info>& info);
void deserialize_hard_fork_info(const boost::property_tree::ptree& node, const std::shared_ptr<monero_hard_fork_info>& info);
}
@@ -0,0 +1,220 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#include "gen_utils.h"
#include "rapidjson/stringbuffer.h"
#include "rapidjson/writer.h"
using namespace gen_utils;
// ------------------------- SERIALIZATION ---------------------------
std::string gen_utils::serialize(const rapidjson::Document& doc) {
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
doc.Accept(writer);
return buffer.GetString();
}
std::string gen_utils::serialize(const boost::property_tree::ptree& node) {
std::stringstream ss;
boost::property_tree::write_json(ss, node, false);
std::string str = ss.str();
return str.substr(0, str.size() - 1); // strip newline
}
void gen_utils::deserialize(const std::string& json, boost::property_tree::ptree& root) {
std::istringstream iss = json.empty() ? std::istringstream() : std::istringstream(json);
try {
boost::property_tree::read_json(iss, root);
} catch (std::exception const& e) {
throw std::runtime_error("Invalid JSON");
}
}
// --------------------------- THREAD POLLER ---------------------------
thread_poller::thread_poller(): m_is_polling(false), m_poll_loop_running(false), m_poll_period_ms(20000) {
}
thread_poller::~thread_poller() {
// safety net only: derived classes must stop polling in their own destructor
// (before their members are torn down) since by the time this base destructor
// runs, the derived part of the object is already gone and a concurrently
// running poll() (a virtual call) would hit a partially-destructed object
set_is_polling(false);
}
namespace {
// stack of poller instances whose announce_scope is currently active on the current
// thread, innermost last. Per-instance so that a callback of poller A calling into
// a *different* poller B's wait_for_callbacks_idle() still waits normally.
thread_local std::vector<const thread_poller*> t_active_dispatch_stack;
}
thread_poller::announce_scope::announce_scope(thread_poller& poller): m_poller(poller) {
t_active_dispatch_stack.push_back(&poller);
boost::lock_guard<boost::mutex> lock(m_poller.m_announce_mutex);
++m_poller.m_announce_count;
}
thread_poller::announce_scope::~announce_scope() {
{
boost::lock_guard<boost::mutex> lock(m_poller.m_announce_mutex);
if (--m_poller.m_announce_count == 0) m_poller.m_announce_cv.notify_all();
}
t_active_dispatch_stack.pop_back();
}
void thread_poller::wait_for_callbacks_idle() {
// skip the wait only if this poller's own dispatch is active on the current thread;
// waiting would deadlock in that case. A different poller's active dispatch must not
// short-circuit this wait.
for (const thread_poller* active : t_active_dispatch_stack) {
if (active == this) return;
}
boost::unique_lock<boost::mutex> lock(m_announce_mutex);
m_announce_cv.wait(lock, [&]() { return m_announce_count == 0; });
}
void thread_poller::init_common(const std::string& name) {
m_name = name;
}
void thread_poller::request_is_polling(bool is_polling) {
boost::lock_guard<boost::mutex> lifecycle_lock(m_lifecycle_mutex);
if (is_polling) {
if (m_is_polling) return;
m_is_polling = true;
run_poll_loop();
return;
}
bool was_polling = m_is_polling.exchange(false);
if (was_polling) {
boost::lock_guard<boost::mutex> polling_lock(m_polling_mutex);
m_poll_cv.notify_one();
}
}
void thread_poller::set_is_polling(bool is_polling) {
boost::unique_lock<boost::mutex> lifecycle_lock(m_lifecycle_mutex);
if (is_polling) {
if (m_is_polling) return;
m_is_polling = true;
run_poll_loop();
return;
}
bool was_polling = m_is_polling.exchange(false);
if (was_polling) {
boost::lock_guard<boost::mutex> polling_lock(m_polling_mutex);
m_poll_cv.notify_one();
}
if (!m_poll_loop_running) return; // loop already fully exited; nothing to wait for
if (boost::this_thread::get_id() == m_poll_thread_id) {
// called from the poll thread itself (e.g. a listener removed itself from within a
// callback invoked by poll()): waiting for our own loop to exit would deadlock. Just
// return -- the loop re-checks m_is_polling itself and will exit and clear
// m_poll_loop_running/m_poll_thread_id on its own (see run_poll_loop).
return;
}
// wait for the loop to actually finish
m_lifecycle_cv.wait(lifecycle_lock, [&]() { return !m_poll_loop_running; });
}
void thread_poller::run_poll_loop() {
// called with m_lifecycle_mutex held
if (m_poll_loop_running) return; // only run one loop at a time
m_poll_loop_running = true;
// TODO: use global threadpool, background sync wasm wallet in c++ thread
try {
boost::thread([this]() {
{
boost::lock_guard<boost::mutex> lifecycle_lock(m_lifecycle_mutex);
m_poll_thread_id = boost::this_thread::get_id();
}
while (true) {
while (m_is_polling) {
try { poll(); }
catch (const std::exception& e) { MERROR(m_name << " failed to background poll: " << e.what()); }
catch (...) { MERROR(m_name << " failed to background poll"); }
// only wait if polling still enabled
if (m_is_polling) {
boost::mutex::scoped_lock lock(m_polling_mutex);
boost::posix_time::milliseconds wait_for_ms(m_poll_period_ms.load());
m_poll_cv.timed_wait(lock, wait_for_ms, [&]() { return !m_is_polling; });
}
}
boost::unique_lock<boost::mutex> lifecycle_lock(m_lifecycle_mutex);
if (m_is_polling) continue;
m_poll_loop_running = false;
m_poll_thread_id = boost::thread::id();
m_lifecycle_cv.notify_all();
return;
}
}).detach();
} catch (...) {
m_poll_loop_running = false;
m_is_polling = false;
throw;
}
}
@@ -0,0 +1,220 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#ifndef gen_utils_h
#define gen_utils_h
#include <boost/lexical_cast.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include "rapidjson/document.h"
#include <boost/thread/recursive_mutex.hpp>
#include <boost/thread/thread.hpp>
#include <boost/thread/condition_variable.hpp>
#include <chrono>
#include <thread>
#include <atomic>
#include "include_base_utils.h"
#include "common/util.h"
/**
* Collection of generic utilities.
*/
namespace gen_utils
{
/**
* Return a unique identifier.
*
* @return a unique id
*/
static std::string get_uuid() {
boost::uuids::random_generator generator;
boost::uuids::uuid uuid = generator();
return boost::uuids::to_string(uuid);
}
/**
* Wait for the given duration.
*
* @param duration_ms the duration to wait in milliseconds
*/
static void wait_for(uint64_t duration_ms) {
std::this_thread::sleep_for(std::chrono::milliseconds(duration_ms));
}
static bool bool_equals(bool val, const boost::optional<bool>& opt_val) {
return opt_val == boost::none ? false : val == *opt_val;
}
// ------------------------- SERIALIZATION ---------------------------
// TODO: fully switch from property trees to rapidjson
std::string serialize(const rapidjson::Document& doc);
std::string serialize(const boost::property_tree::ptree& node);
void deserialize(const std::string& json, boost::property_tree::ptree& root);
// ------------------------- VALUE RECONCILATION ----------------------------
// TODO: refactor common template code
template <class T, typename std::enable_if<std::is_same<T, std::string>::value, T>::type* = nullptr>
boost::optional<T> reconcile(const boost::optional<T>& val1, const boost::optional<T>& val2, boost::optional<bool> resolve_defined, boost::optional<bool> resolve_true, boost::optional<bool> resolve_max, const std::string& err_msg = "") {
// check for equality
if (val1 == val2) return val1;
// resolve one value none
if (val1 == boost::none || val2 == boost::none) {
if (resolve_defined != boost::none && *resolve_defined == false) return boost::none;
else return val1 == boost::none ? val2 : val1;
}
throw std::runtime_error(std::string("Cannot reconcile strings: ") + boost::lexical_cast<std::string>(val1) + std::string(" vs ") + boost::lexical_cast<std::string>(val2) + (!err_msg.empty() ? std::string(". ") + err_msg : std::string("")));
}
template <class T, typename std::enable_if<std::is_same<T, std::string>::value, T>::type* = nullptr>
boost::optional<T> reconcile(const boost::optional<T>& val1, const boost::optional<T>& val2, const std::string& err_msg = "") {
return reconcile(val1, val2, boost::none, boost::none, boost::none, err_msg);
}
template <class T, typename std::enable_if<std::is_integral<T>::value, T>::type* = nullptr>
boost::optional<T> reconcile(const boost::optional<T>& val1, const boost::optional<T>& val2, boost::optional<bool> resolve_defined, boost::optional<bool> resolve_true, boost::optional<bool> resolve_max, const std::string& err_msg = "") {
// check for equality
if (val1 == val2) return val1;
// resolve one value none
if (val1 == boost::none || val2 == boost::none) {
if (resolve_defined != boost::none && *resolve_defined == false) return boost::none;
else return val1 == boost::none ? val2 : val1;
}
// resolve different booleans
if (resolve_true != boost::none) return (bool) val1 == *resolve_true ? val1 : val2; // if resolve true, return true, else return false
// resolve different numbers
if (resolve_max != boost::none) return *resolve_max ? std::max(*val1, *val2) : std::min(*val1, *val2);
// cannot reconcile
throw std::runtime_error(std::string("Cannot reconcile integrals: ") + boost::lexical_cast<std::string>(val1) + std::string(" vs ") + boost::lexical_cast<std::string>(val2) + (!err_msg.empty() ? std::string(". ") + err_msg : std::string("")));
}
template <class T, typename std::enable_if<std::is_integral<T>::value, T>::type* = nullptr>
boost::optional<T> reconcile(const boost::optional<T>& val1, const boost::optional<T>& val2, const std::string& err_msg = "") {
return reconcile(val1, val2, boost::none, boost::none, boost::none, err_msg);
}
template <class T>
std::vector<T> reconcile(const std::vector<T>& v1, const std::vector<T>& v2, const std::string& err_msg = "") {
// check for equality
if (v1 == v2) return v1;
// resolve one vector empty
if (v1.empty()) return v2;
if (v2.empty()) return v1;
// otherwise cannot reconcile
throw std::runtime_error("Cannot reconcile vectors" + (!err_msg.empty() ? std::string(". ") + err_msg : std::string("")));
}
// ------------------------- THREAD POLLER ----------------------------
// WARNING: Only destroy a thread_poller from a thread other than its own pool thread.
// (e.g. a listener callback running on the poll thread, drops the last owning reference/unique_ptr
// to the poller.)
class thread_poller {
public:
thread_poller();
virtual ~thread_poller();
bool is_polling() const { return m_is_polling; }
void set_is_polling(bool is_polling);
void request_is_polling(bool is_polling);
void set_period_in_ms(uint64_t period_ms) { m_poll_period_ms = period_ms; }
virtual void poll() = 0;
void wait_for_callbacks_idle();
class announce_scope {
public:
explicit announce_scope(thread_poller& poller);
~announce_scope();
announce_scope(const announce_scope&) = delete;
announce_scope& operator=(const announce_scope&) = delete;
private:
thread_poller& m_poller;
};
protected:
std::string m_name;
boost::recursive_mutex m_mutex;
boost::mutex m_polling_mutex; // guards the interruptible periodic sleep (m_poll_cv)
boost::mutex m_lifecycle_mutex; // guards m_poll_loop_running/m_poll_thread_id and start/stop coordination
boost::thread::id m_poll_thread_id; // id of the currently running loop thread; guarded by m_lifecycle_mutex
std::atomic<bool> m_is_polling;
bool m_poll_loop_running; // guarded by m_lifecycle_mutex
std::atomic<uint64_t> m_poll_period_ms;
boost::condition_variable m_poll_cv; // periodic-sleep interrupt
boost::condition_variable m_lifecycle_cv; // notified (under m_lifecycle_mutex) when the loop actually exits
boost::mutex m_announce_mutex; // guards m_announce_count
boost::condition_variable m_announce_cv; // notified (under m_announce_mutex) when m_announce_count reaches 0
int m_announce_count = 0; // number of announce_scope instances currently alive, across all threads
void init_common(const std::string& name);
void run_poll_loop();
};
}
#endif /* gen_utils_h */
@@ -0,0 +1,624 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#include "monero_utils.h"
#include "rpc/core_rpc_server_commands_defs.h"
#include "storages/portable_storage_template_helper.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "mnemonics/electrum-words.h"
#include "mnemonics/english.h"
#include "string_tools.h"
#include "byte_stream.h"
#include "gen_utils.h"
using namespace cryptonote;
using namespace monero_utils;
void monero_utils::set_log_level(int level) {
mlog_set_log_level(level);
}
void monero_utils::set_log_categories(const std::string& categories) {
mlog_set_categories(categories.c_str());
}
void monero_utils::configure_logging(const std::string& path, bool console) {
mlog_configure(path, console);
}
// --------------------------- VALIDATION UTILS -------------------------------
monero_integrated_address monero_utils::get_integrated_address(monero_network_type network_type, const std::string& standard_address, const std::string& payment_id) {
// parse and validate address
cryptonote::address_parse_info address_info;
if (!get_account_address_from_str(address_info, static_cast<cryptonote::network_type>(network_type), standard_address)) throw std::runtime_error("Invalid address");
if (address_info.has_payment_id) throw std::runtime_error("The given address already has a payment id");
// randomly generate payment id if not given, else validate
crypto::hash8 payment_id_h8;
if (payment_id.empty()) {
payment_id_h8 = crypto::rand<crypto::hash8>();
} else {
cryptonote::blobdata payment_id_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(payment_id, payment_id_data) || sizeof(crypto::hash8) != payment_id_data.size()) throw std::runtime_error("Invalid payment id");
payment_id_h8 = *reinterpret_cast<const crypto::hash8*>(payment_id_data.data());
}
// build integrated address
monero_integrated_address integrated_address;
integrated_address.m_integrated_address = cryptonote::get_account_integrated_address_as_str(static_cast<cryptonote::network_type>(network_type), address_info.address, payment_id_h8);
integrated_address.m_standard_address = standard_address;
integrated_address.m_payment_id = epee::string_tools::pod_to_hex(payment_id_h8);
return integrated_address;
}
bool monero_utils::is_valid_address(const std::string& address, monero_network_type network_type) {
try {
validate_address(address, network_type);
return true;
} catch (...) {
return false;
}
}
bool monero_utils::is_valid_private_view_key(const std::string& private_view_key) {
try {
validate_private_view_key(private_view_key);
return true;
} catch (...) {
return false;
}
}
bool monero_utils::is_valid_private_spend_key(const std::string& private_spend_key) {
try {
validate_private_spend_key(private_spend_key);
return true;
} catch (...) {
return false;
}
}
bool monero_utils::is_valid_public_view_key(const std::string& public_view_key) {
try {
validate_public_view_key(public_view_key);
return true;
} catch (...) {
return false;
}
}
bool monero_utils::is_valid_public_spend_key(const std::string& public_spend_key) {
try {
validate_public_spend_key(public_spend_key);
return true;
} catch (...) {
return false;
}
}
bool monero_utils::is_valid_payment_id(const std::string& payment_id) {
try {
validate_payment_id(payment_id);
return true;
} catch (...) {
return false;
}
}
bool monero_utils::is_valid_mnemonic(const std::string& mnemonic, const std::string& language) {
try {
validate_mnemonic(mnemonic, language);
return true;
}
catch (...) {
return false;
}
}
void monero_utils::validate_address(const std::string& address, monero_network_type network_type) {
cryptonote::address_parse_info info;
if (!get_account_address_from_str(info, static_cast<cryptonote::network_type>(network_type), address)) throw std::runtime_error("Invalid address");
}
void monero_utils::validate_private_view_key(const std::string& private_view_key) {
if (private_view_key.length() != 64) throw std::runtime_error("private view key expected to be 64 hex characters");
cryptonote::blobdata private_view_key_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(private_view_key, private_view_key_data) || private_view_key_data.size() != sizeof(crypto::secret_key)) {
throw std::runtime_error("private view key expected to be 64 hex characters");
}
}
void monero_utils::validate_private_spend_key(const std::string& private_spend_key) {
if (private_spend_key.length() != 64) throw std::runtime_error("private spend key expected to be 64 hex characters");
cryptonote::blobdata private_spend_key_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(private_spend_key, private_spend_key_data) || private_spend_key_data.size() != sizeof(crypto::secret_key)) {
throw std::runtime_error("private spend key expected to be 64 hex characters");
}
}
void monero_utils::validate_public_view_key(const std::string& public_view_key) {
if (public_view_key.length() != 64) throw std::runtime_error("public view key expected to be 64 hex characters");
cryptonote::blobdata public_view_key_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(public_view_key, public_view_key_data) || public_view_key_data.size() != sizeof(crypto::public_key)) {
throw std::runtime_error("public view key expected to be 64 hex characters");
}
}
void monero_utils::validate_public_spend_key(const std::string& public_spend_key) {
if (public_spend_key.length() != 64) throw std::runtime_error("public spend key expected to be 64 hex characters");
cryptonote::blobdata public_spend_key_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(public_spend_key, public_spend_key_data) || public_spend_key_data.size() != sizeof(crypto::public_key)) {
throw std::runtime_error("public spend key expected to be 64 hex characters");
}
}
void monero_utils::validate_payment_id(const std::string& payment_id) {
if (payment_id.length() != 64 && payment_id.length() != 16) throw std::runtime_error("payment id expected to be 64 or 16 hex characters");
cryptonote::blobdata payment_id_data;
if(!epee::string_tools::parse_hexstr_to_binbuff(payment_id, payment_id_data) || (payment_id_data.size() != sizeof(crypto::hash8) && payment_id_data.size() != sizeof(crypto::hash))) {
throw std::runtime_error("payment id expected to be 64 or 16 hex characters");
}
}
void monero_utils::validate_mnemonic(const std::string& mnemonic, const std::string& language) {
if (mnemonic.empty()) throw std::runtime_error("Mnemonic phrase is empty");
std::string seed_language;
crypto::secret_key recovery_key;
if (crypto::ElectrumWords::get_is_old_style_seed(mnemonic)) throw std::runtime_error("Mnemonic phrased words must be 25");
if(!crypto::ElectrumWords::words_to_bytes(mnemonic, recovery_key, seed_language)) throw std::runtime_error("Invalid mnemonic");
if (seed_language == crypto::ElectrumWords::old_language_name) seed_language = "English";
// validate language
if (!language.empty()) {
if (!crypto::ElectrumWords::is_valid_language(language)) throw std::runtime_error("Invalid language: " + language);
if (language != seed_language) throw std::runtime_error("Seed language mismatch");
}
}
// -------------------------- BINARY SERIALIZATION ----------------------------
void monero_utils::json_to_binary(const std::string &json, std::string &bin) {
epee::serialization::portable_storage ps;
ps.load_from_json(json);
epee::byte_stream bs;
ps.store_to_binary(bs);
bin = std::string((char*) bs.data(), bs.size());
}
void monero_utils::binary_to_json(const std::string &bin, std::string &json) {
epee::serialization::portable_storage ps;
ps.load_from_binary(bin);
ps.dump_as_json(json);
}
void monero_utils::binary_blocks_to_json(const std::string &bin, std::string &json) {
// load binary rpc response to struct
cryptonote::COMMAND_RPC_GET_BLOCKS_BY_HEIGHT::response resp_struct;
epee::serialization::load_t_from_binary(resp_struct, bin);
// build property tree from deserialized blocks and transactions
boost::property_tree::ptree root;
boost::property_tree::ptree blocksNode; // array of block strings
boost::property_tree::ptree txsNodes; // array of txs per block (array of array)
for (int blockIdx = 0; blockIdx < resp_struct.blocks.size(); blockIdx++) {
// parse and validate block
cryptonote::block block;
if (cryptonote::parse_and_validate_block_from_blob(resp_struct.blocks[blockIdx].block, block)) {
// add block node to blocks node
boost::property_tree::ptree blockNode;
blockNode.put("", cryptonote::obj_to_json_str(block)); // TODO: no pretty print
blocksNode.push_back(std::make_pair("", blockNode));
} else {
throw std::runtime_error("failed to parse block blob at index " + std::to_string(blockIdx));
}
// parse and validate txs
boost::property_tree::ptree txs_node;
for (int txIdx = 0; txIdx < resp_struct.blocks[blockIdx].txs.size(); txIdx++) {
cryptonote::transaction tx;
if (cryptonote::parse_and_validate_tx_from_blob(resp_struct.blocks[blockIdx].txs[txIdx].blob, tx)) {
// add tx node to txs node
boost::property_tree::ptree txNode;
//MTRACE("PRUNED:\n" << monero_utils::get_pruned_tx_json(tx));
txNode.put("", monero_utils::get_pruned_tx_json(tx)); // TODO: no pretty print
txs_node.push_back(std::make_pair("", txNode));
} else {
throw std::runtime_error("failed to parse tx blob at index " + std::to_string(txIdx));
}
}
txsNodes.push_back(std::make_pair("", txs_node)); // array of array of transactions, one array per block
}
root.add_child("blocks", blocksNode);
root.add_child("txs", txsNodes);
root.put("status", resp_struct.status);
root.put("untrusted", resp_struct.untrusted); // TODO: loss of ints and bools
// convert root to string // TODO: common utility with serial_bridge
std::stringstream ss;
boost::property_tree::write_json(ss, root, false/*pretty*/);
json = ss.str();
}
// ------------------------------- RAPIDJSON ----------------------------------
void monero_utils::add_json_member(std::string key, std::string val, rapidjson::Document::AllocatorType& allocator, rapidjson::Value& root, rapidjson::Value& field) {
rapidjson::Value field_key(key.c_str(), key.size(), allocator);
field.SetString(val.c_str(), val.size(), allocator);
root.AddMember(field_key, field, allocator);
}
void monero_utils::add_json_member(std::string key, bool val, rapidjson::Document::AllocatorType& allocator, rapidjson::Value& root) {
rapidjson::Value field_key(key.c_str(), key.size(), allocator);
if (val) {
rapidjson::Value field_val(rapidjson::kTrueType);
root.AddMember(field_key, field_val, allocator);
} else {
rapidjson::Value field_val(rapidjson::kFalseType);
root.AddMember(field_key, field_val, allocator);
}
}
rapidjson::Value monero_utils::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<std::string>& strs) {
rapidjson::Value value_arr(rapidjson::kArrayType);
rapidjson::Value value_str(rapidjson::kStringType);
for (const std::string& str : strs) {
value_str.SetString(str.c_str(), str.size(), allocator);
value_arr.PushBack(value_str, allocator);
}
return value_arr;
}
rapidjson::Value monero_utils::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<int>& nums) {
rapidjson::Value value_arr(rapidjson::kArrayType);
rapidjson::Value value_num(rapidjson::kNumberType);
for (const auto& num : nums) {
value_num.SetInt(num);
value_arr.PushBack(value_num, allocator);
}
return value_arr;
}
rapidjson::Value monero_utils::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<uint8_t>& nums) {
rapidjson::Value value_arr(rapidjson::kArrayType);
rapidjson::Value value_num(rapidjson::kNumberType);
for (const auto& num : nums) {
value_num.SetInt(num);
value_arr.PushBack(value_num, allocator);
}
return value_arr;
}
// TODO: remove these redundant implementations for different sizes?
rapidjson::Value monero_utils::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<uint32_t>& nums) {
rapidjson::Value value_arr(rapidjson::kArrayType);
rapidjson::Value value_num(rapidjson::kNumberType);
for (const auto& num : nums) {
value_num.SetUint64(num);
value_arr.PushBack(value_num, allocator);
}
return value_arr;
}
rapidjson::Value monero_utils::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<uint64_t>& nums) {
rapidjson::Value value_arr(rapidjson::kArrayType);
rapidjson::Value value_num(rapidjson::kNumberType);
for (const auto& num : nums) {
value_num.SetUint64(num);
value_arr.PushBack(value_num, allocator);
}
return value_arr;
}
// ----------------------------------------------------------------------------
bool monero_utils::is_valid_language(const std::string& language) {
std::vector<std::string> languages;
crypto::ElectrumWords::get_language_list(languages, false);
std::vector<std::string>::iterator it = std::find(languages.begin(), languages.end(), language);
if (it == languages.end()) {
crypto::ElectrumWords::get_language_list(languages, true);
it = std::find(languages.begin(), languages.end(), language);
}
if (it == languages.end()) return false;
return true;
}
// TODO: this is unused
std::shared_ptr<monero_block> monero_utils::cn_block_to_block(const cryptonote::block& cn_block) {
cryptonote::block temp = cn_block;
std::cout << cryptonote::obj_to_json_str(temp) << std::endl;
std::shared_ptr<monero_block> block = std::make_shared<monero_block>();
block->m_major_version = cn_block.major_version;
block->m_minor_version = cn_block.minor_version;
block->m_timestamp = cn_block.timestamp;
block->m_prev_hash = epee::string_tools::pod_to_hex(cn_block.prev_id);
block->m_nonce = cn_block.nonce;
block->m_miner_tx = monero_utils::cn_tx_to_tx(cn_block.miner_tx);
for (const crypto::hash& tx_hash : cn_block.tx_hashes) {
block->m_tx_hashes.push_back(epee::string_tools::pod_to_hex(tx_hash));
}
return block;
}
std::shared_ptr<monero_tx> monero_utils::cn_tx_to_tx(const cryptonote::transaction& cn_tx, bool init_as_tx_wallet) {
std::shared_ptr<monero_tx> tx = init_as_tx_wallet ? std::make_shared<monero_tx_wallet>() : std::make_shared<monero_tx>();
tx->m_version = cn_tx.version;
tx->m_unlock_time = cn_tx.unlock_time;
tx->m_hash = epee::string_tools::pod_to_hex(cn_tx.hash);
tx->m_extra = cn_tx.extra;
// init inputs
for (const txin_v& cnVin : cn_tx.vin) {
if (cnVin.which() != 0 && cnVin.which() != 3) throw std::runtime_error("Unsupported variant type");
if (tx->m_is_miner_tx == boost::none) tx->m_is_miner_tx = cnVin.which() == 0;
if (cnVin.which() != 3) continue; // only process txin_to_key of variant TODO: support other types, like 0 "gen" which is miner tx?
std::shared_ptr<monero_output> input = init_as_tx_wallet ? std::make_shared<monero_output_wallet>() : std::make_shared<monero_output>();
input->m_tx = tx;
tx->m_inputs.push_back(input);
const txin_to_key& txin = boost::get<txin_to_key>(cnVin);
input->m_amount = txin.amount;
input->m_ring_output_indices = txin.key_offsets;
crypto::key_image cnKeyImage = txin.k_image;
input->m_key_image = std::make_shared<monero_key_image>();
input->m_key_image->m_hex = epee::string_tools::pod_to_hex(cnKeyImage);
}
// init outputs
for (const tx_out& cnVout : cn_tx.vout) {
std::shared_ptr<monero_output> output = init_as_tx_wallet ? std::make_shared<monero_output_wallet>() : std::make_shared<monero_output>();
output->m_tx = tx;
tx->m_outputs.push_back(output);
output->m_amount = cnVout.amount;
// before HF_VERSION_VIEW_TAGS, outputs with public keys are of type txout_to_key
// after HF_VERSION_VIEW_TAGS, outputs with public keys are of type txout_to_tagged_key
crypto::public_key cnStealthPublicKey;
if (cnVout.target.type() == typeid(txout_to_key))
cnStealthPublicKey = boost::get<txout_to_key>(cnVout.target).key;
else if (cnVout.target.type() == typeid(txout_to_tagged_key))
cnStealthPublicKey = boost::get<txout_to_tagged_key>(cnVout.target).key;
else
throw std::runtime_error(std::string("Unexpected output target type found: ") + std::string(cnVout.target.type().name()));
output->m_stealth_public_key = epee::string_tools::pod_to_hex(cnStealthPublicKey);
}
return tx;
// TODO: finish this, cryptonote::transaction has:
// std::vector<std::vector<crypto::signature> > m_signatures;
// rct::rctSig m_rct_signatures;
// mutable size_t blob_size;
}
void monero_utils::binary_blocks_to_property_tree(const std::string &bin, boost::property_tree::ptree &node) {
std::string response_json;
monero_utils::binary_blocks_to_json(bin, response_json);
std::istringstream iss(response_json);
boost::property_tree::read_json(iss, node);
auto blocks = node.get_child("blocks");
boost::property_tree::ptree parsed_blocks;
for (auto &entry : blocks) {
const std::string &block_str = entry.second.get_value<std::string>();
boost::property_tree::ptree block_node;
gen_utils::deserialize(block_str, block_node);
parsed_blocks.push_back(std::make_pair("", block_node));
}
node.put_child("blocks", parsed_blocks);
auto txs = node.get_child("txs");
boost::property_tree::ptree all_txs;
for (auto &rpc_txs_entry : txs) {
boost::property_tree::ptree txs_for_block;
const auto &rpc_txs = rpc_txs_entry.second;
if (!rpc_txs.empty() || !rpc_txs.data().empty()) {
for (auto &tx_entry : rpc_txs) {
std::string tx_str = tx_entry.second.get_value<std::string>();
auto pos = tx_str.find(',');
if (pos != std::string::npos) {
tx_str.replace(pos, 1, "{");
tx_str += "}";
}
boost::property_tree::ptree tx_node;
gen_utils::deserialize(tx_str, tx_node);
txs_for_block.push_back(std::make_pair("", tx_node));
}
}
all_txs.push_back(std::make_pair("", txs_for_block));
}
node.put_child("txs", all_txs);
}
void monero_utils::merge_tx(const std::shared_ptr<monero_tx_wallet>& tx, std::map<std::string, std::shared_ptr<monero_tx_wallet>>& tx_map, std::map<uint64_t, std::shared_ptr<monero_block>>& block_map) {
if (tx->m_hash == boost::none) throw std::runtime_error("Tx hash is not initialized");
// merge tx
std::map<std::string, std::shared_ptr<monero_tx_wallet>>::const_iterator tx_iter = tx_map.find(*tx->m_hash);
if (tx_iter == tx_map.end()) {
tx_map[*tx->m_hash] = tx; // cache new tx
} else {
std::shared_ptr<monero_tx_wallet>& a_tx = tx_map[*tx->m_hash];
a_tx->merge(a_tx, tx); // merge with existing tx
}
// merge tx's block if confirmed
if (tx->get_height() != boost::none) {
std::map<uint64_t, std::shared_ptr<monero_block>>::const_iterator block_iter = block_map.find(tx->get_height().get());
if (block_iter == block_map.end()) {
block_map[tx->get_height().get()] = tx->m_block; // cache new block
} else {
std::shared_ptr<monero_block>& a_block = block_map[tx->get_height().get()];
a_block->merge(a_block, tx->m_block); // merge with existing block
}
}
}
bool monero_utils::tx_height_less_than(const std::shared_ptr<monero_tx>& tx1, const std::shared_ptr<monero_tx>& tx2) {
if (tx1->m_block != nullptr && tx2->m_block != nullptr) return tx1->get_height() < tx2->get_height();
else if (tx1->m_block == nullptr) return false;
else return true;
}
bool monero_utils::incoming_transfer_before(const std::shared_ptr<monero_incoming_transfer>& transfer1, const std::shared_ptr<monero_incoming_transfer>& transfer2) {
// compare by height
if (tx_height_less_than(transfer1->m_tx, transfer2->m_tx)) return true;
// compare by account and subaddress index
if (transfer1->m_account_index.get() < transfer2->m_account_index.get()) return true;
else if (transfer1->m_account_index.get() == transfer2->m_account_index.get()) return transfer1->m_subaddress_index.get() < transfer2->m_subaddress_index.get();
else return false;
}
bool monero_utils::vout_before(const std::shared_ptr<monero_output>& o1, const std::shared_ptr<monero_output>& o2) {
if (o1 == o2) return false; // ignore equal references
std::shared_ptr<monero_output_wallet> ow1 = std::static_pointer_cast<monero_output_wallet>(o1);
std::shared_ptr<monero_output_wallet> ow2 = std::static_pointer_cast<monero_output_wallet>(o2);
// compare by height
if (tx_height_less_than(ow1->m_tx, ow2->m_tx)) return true;
// compare by account index, subaddress index, output index, then key image hex
if (ow1->m_account_index.get() < ow2->m_account_index.get()) return true;
if (ow1->m_account_index.get() == ow2->m_account_index.get()) {
if (ow1->m_subaddress_index.get() < ow2->m_subaddress_index.get()) return true;
if (ow1->m_subaddress_index.get() == ow2->m_subaddress_index.get()) {
if (ow1->m_index.get() < ow2->m_index.get()) return true;
if (ow1->m_index.get() == ow2->m_index.get()) throw std::runtime_error("Should never sort outputs with duplicate indices");
}
}
return false;
}
static std::string make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, monero_network_type network_type) {
cryptonote::address_parse_info info;
if(!get_account_address_from_str(info, static_cast<cryptonote::network_type>(network_type), address)) {
throw std::runtime_error(std::string("Invalid address: ") + address);
}
if (!payment_id.empty()) {
throw std::runtime_error("Standalone payment id deprecated, use integrated address instead");
}
std::string uri = "monero:" + address;
unsigned int n_fields = 0;
if (amount > 0) {
// URI encoded amount is in decimal units, not atomic units
uri += (n_fields++ ? "&" : "?") + std::string("tx_amount=") + cryptonote::print_money(amount);
}
if (!recipient_name.empty()) {
uri += (n_fields++ ? "&" : "?") + std::string("recipient_name=") + epee::net_utils::conver_to_url_format(recipient_name);
}
if (!tx_description.empty()) {
uri += (n_fields++ ? "&" : "?") + std::string("tx_description=") + epee::net_utils::conver_to_url_format(tx_description);
}
return uri;
}
std::string monero_utils::get_payment_uri(const monero_tx_config& config, monero_network_type network_type) {
// validate config
std::vector<std::shared_ptr<monero_destination>> destinations = config.get_normalized_destinations();
if (destinations.size() != 1) throw std::runtime_error("Cannot make URI from supplied parameters: must provide exactly one destination to send funds");
if (destinations.at(0)->m_address == boost::none) throw std::runtime_error("Cannot make URI from supplied parameters: must provide destination address");
if (destinations.at(0)->m_amount == boost::none) throw std::runtime_error("Cannot make URI from supplied parameters: must provide destination amount");
// prepare wallet2 params
std::string address = destinations.at(0)->m_address.get();
std::string payment_id = config.m_payment_id == boost::none ? "" : config.m_payment_id.get();
uint64_t amount = destinations.at(0)->m_amount.get();
std::string note = config.m_note == boost::none ? "" : config.m_note.get();
std::string m_recipient_name = config.m_recipient_name == boost::none ? "" : config.m_recipient_name.get();
// make uri
std::string uri = make_uri(address, payment_id, amount, note, m_recipient_name, network_type);
if (uri.empty()) throw std::runtime_error("Cannot make URI from supplied parameters");
return uri;
}
uint64_t monero_utils::xmr_to_atomic_units(double amount_xmr) {
if (!std::isfinite(amount_xmr) || amount_xmr < 0) throw std::invalid_argument("amount must be a finite, non-negative number");
long double atomic = std::round(static_cast<long double>(amount_xmr) * static_cast<long double>(XMR_AU_MULTIPLIER));
// validate the actual rounded value about to be cast, not a precomputed "amount_xmr" threshold:
// a threshold like UINT64_MAX / XMR_AU_MULTIPLIER carries its own division rounding error and can
// let a value that overflows uint64_t slip through, and casting an out-of-range floating point
// value to uint64_t is undefined behavior (not a wraparound). 2^64 is exactly representable in any
// binary floating point type with enough exponent range (it's a pure power of two), so this bound
// check is exact regardless of long double's precision on a given platform.
static const long double MAX_ATOMIC_UNITS_EXCLUSIVE = std::ldexp(1.0L, 64); // 2^64
if (atomic < 0.0L || atomic >= MAX_ATOMIC_UNITS_EXCLUSIVE) throw std::invalid_argument("amount exceeds maximum representable atomic units");
return static_cast<uint64_t>(atomic);
}
double monero_utils::atomic_units_to_xmr(uint64_t amount_atomic_units) {
return static_cast<double>(amount_atomic_units) / static_cast<double>(XMR_AU_MULTIPLIER);
}
@@ -0,0 +1,322 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#ifndef monero_utils_h
#define monero_utils_h
#include "wallet/monero_wallet_model.h"
#include "cryptonote_basic/cryptonote_basic.h"
#include "serialization/keyvalue_serialization.h" // TODO: consolidate with other binary deps?
#include "storages/portable_storage.h"
/**
* Collection of utilities for the Monero library.
*/
namespace monero_utils
{
using namespace cryptonote;
// ------------------------------ CONSTANTS ---------------------------------
static const int RING_SIZE = 12; // network-enforced ring size
static const uint64_t XMR_AU_MULTIPLIER = 1000000000000ULL;
// -------------------------------- UTILS -----------------------------------
void set_log_level(int level);
void set_log_categories(const std::string& categories);
void configure_logging(const std::string& path, bool console);
monero_integrated_address get_integrated_address(monero_network_type network_type, const std::string& standard_address, const std::string& payment_id);
std::string get_payment_uri(const monero_tx_config& config, monero_network_type network_type);
bool is_valid_address(const std::string& address, monero_network_type network_type);
bool is_valid_private_view_key(const std::string& private_view_key);
bool is_valid_private_spend_key(const std::string& private_spend_key);
bool is_valid_public_view_key(const std::string& public_view_key);
bool is_valid_public_spend_key(const std::string& public_spend_key);
bool is_valid_payment_id(const std::string& payment_id);
bool is_valid_mnemonic(const std::string& mnemonic, const std::string& language = "");
void validate_address(const std::string& address, monero_network_type network_type);
void validate_private_view_key(const std::string& private_view_key);
void validate_private_spend_key(const std::string& private_spend_key);
void validate_public_view_key(const std::string& public_view_key);
void validate_public_spend_key(const std::string& public_spend_key);
void validate_payment_id(const std::string& payment_id);
void validate_mnemonic(const std::string& mnemonic, const std::string& language = "");
void json_to_binary(const std::string &json, std::string &bin);
void binary_to_json(const std::string &bin, std::string &json);
void binary_blocks_to_json(const std::string &bin, std::string &json);
uint64_t xmr_to_atomic_units(double amount_xmr);
double atomic_units_to_xmr(uint64_t amount_atomic_units);
// ------------------------------ RAPIDJSON ---------------------------------
/**
* Add number, string, and boolean json members using template specialization.
*
* TODO: add_json_member("key", "val", ...) treated as integer instead of string literal
*/
template <class T>
void add_json_member(std::string key, T val, rapidjson::Document::AllocatorType& allocator, rapidjson::Value& root, rapidjson::Value& field) {
rapidjson::Value field_key(key.c_str(), key.size(), allocator);
if (std::is_signed<T>::value) field.SetInt64((int64_t) val);
else field.SetUint64((uint64_t) val);
root.AddMember(field_key, field, allocator);
}
void add_json_member(std::string key, std::string val, rapidjson::Document::AllocatorType& allocator, rapidjson::Value& root, rapidjson::Value& field);
void add_json_member(std::string key, bool val, rapidjson::Document::AllocatorType& allocator, rapidjson::Value& root);
// TODO: template implementation here, could move to monero_utils.hpp per https://stackoverflow.com/questions/3040480/c-template-function-compiles-in-header-but-not-implementation
template <class T> rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<std::shared_ptr<T>>& vals) {
rapidjson::Value value_arr(rapidjson::kArrayType);
for (const auto& val : vals) {
value_arr.PushBack(val->to_rapidjson_val(allocator), allocator);
}
return value_arr;
}
// TODO: template implementation here, could move to monero_utils.hpp per https://stackoverflow.com/questions/3040480/c-template-function-compiles-in-header-but-not-implementation
template <class T> rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<T>& vals) {
rapidjson::Value value_arr(rapidjson::kArrayType);
for (const auto& val : vals) {
value_arr.PushBack(val.to_rapidjson_val(allocator), allocator);
}
return value_arr;
}
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<std::string>& strs);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<int>& nums);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<uint8_t>& nums);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<uint32_t>& nums);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator, const std::vector<uint64_t>& nums);
// --------------------------------------------------------------------------
/**
* Indicates if the given language is valid.
*
* @param language is the language to validate
* @return true if the language is valid, false otherwise
*/
bool is_valid_language(const std::string& language);
/**
* Convert a cryptonote::block to a block in this library's native model.
*
* @param cn_block is the block to convert
* @return a block in this library's native model
*/
std::shared_ptr<monero_block> cn_block_to_block(const cryptonote::block& cn_block);
/**
* Convert a cryptonote::transaction to a transaction in this library's
* native model.
*
* @param cn_tx is the transaction to convert
* @param init_as_tx_wallet specifies if a monero_tx xor monero_tx_wallet should be initialized
*/
std::shared_ptr<monero_tx> cn_tx_to_tx(const cryptonote::transaction& cn_tx, bool init_as_tx_wallet = false);
/**
* Modified from core_rpc_server.cpp to return a std::string.
*
* TODO: remove this duplicate, use core_rpc_server instead
*/
static std::string get_pruned_tx_json(cryptonote::transaction &tx)
{
std::stringstream ss;
json_archive<true> ar(ss);
bool r = tx.serialize_base(ar);
CHECK_AND_ASSERT_MES(r, std::string(), "Failed to serialize rct signatures base");
return ss.str();
}
void binary_blocks_to_property_tree(const std::string &bin, boost::property_tree::ptree &node);
/**
* Merges a transaction into a unique set of transactions.
*
* @param tx is the transaction to merge into the existing txs
* @param tx_map maps tx hashes to txs
* @param block_map maps block heights to blocks
*/
void merge_tx(const std::shared_ptr<monero_tx_wallet>& tx, std::map<std::string, std::shared_ptr<monero_tx_wallet>>& tx_map, std::map<uint64_t, std::shared_ptr<monero_block>>& block_map);
/**
* Returns true iff tx1's height is known to be less than tx2's height for sorting.
*/
bool tx_height_less_than(const std::shared_ptr<monero_tx>& tx1, const std::shared_ptr<monero_tx>& tx2);
/**
* Returns true iff transfer1 is ordered before transfer2 by ascending account and subaddress indices.
*/
bool incoming_transfer_before(const std::shared_ptr<monero_incoming_transfer>& transfer1, const std::shared_ptr<monero_incoming_transfer>& transfer2);
/**
* Returns true iff wallet vout1 is ordered before vout2 by ascending account and subaddress indices then index.
*/
bool vout_before(const std::shared_ptr<monero_output>& o1, const std::shared_ptr<monero_output>& o2);
// ----------------------------- GATHER BLOCKS ------------------------------
static std::vector<std::shared_ptr<monero_block>> get_blocks_from_txs(std::vector<std::shared_ptr<monero_tx_wallet>> txs) {
std::shared_ptr<monero_block> unconfirmed_block = nullptr; // placeholder for unconfirmed txs
std::vector<std::shared_ptr<monero_block>> blocks;
std::unordered_set<std::shared_ptr<monero_block>> seen_block_ptrs;
for (const std::shared_ptr<monero_tx_wallet>& tx : txs) {
if (tx->m_block == nullptr) {
if (unconfirmed_block == nullptr) unconfirmed_block = std::make_shared<monero_block>();
tx->m_block = unconfirmed_block;
unconfirmed_block->m_txs.push_back(tx);
}
std::unordered_set<std::shared_ptr<monero_block>>::const_iterator got = seen_block_ptrs.find(tx->m_block);
if (got == seen_block_ptrs.end()) {
seen_block_ptrs.insert(tx->m_block);
blocks.push_back(tx->m_block);
}
}
return blocks;
}
static std::vector<std::shared_ptr<monero_block>> get_blocks_from_transfers(std::vector<std::shared_ptr<monero_transfer>> transfers) {
std::shared_ptr<monero_block> unconfirmed_block = nullptr; // placeholder for unconfirmed txs in return json
std::vector<std::shared_ptr<monero_block>> blocks;
std::unordered_set<std::shared_ptr<monero_block>> seen_block_ptrs;
for (auto const& transfer : transfers) {
std::shared_ptr<monero_tx_wallet> tx = transfer->m_tx;
if (tx->m_block == nullptr) {
if (unconfirmed_block == nullptr) unconfirmed_block = std::make_shared<monero_block>();
tx->m_block = unconfirmed_block;
unconfirmed_block->m_txs.push_back(tx);
}
std::unordered_set<std::shared_ptr<monero_block>>::const_iterator got = seen_block_ptrs.find(tx->m_block);
if (got == seen_block_ptrs.end()) {
seen_block_ptrs.insert(tx->m_block);
blocks.push_back(tx->m_block);
}
}
return blocks;
}
static std::vector<std::shared_ptr<monero_block>> get_blocks_from_outputs(std::vector<std::shared_ptr<monero_output_wallet>> outputs) {
std::vector<std::shared_ptr<monero_block>> blocks;
std::unordered_set<std::shared_ptr<monero_block>> seen_block_ptrs;
for (auto const& output : outputs) {
std::shared_ptr<monero_tx_wallet> tx = std::static_pointer_cast<monero_tx_wallet>(output->m_tx);
if (tx->m_block == nullptr) throw std::runtime_error("Need to handle unconfirmed output");
std::unordered_set<std::shared_ptr<monero_block>>::const_iterator got = seen_block_ptrs.find(tx->m_block);
if (got == seen_block_ptrs.end()) {
seen_block_ptrs.insert(tx->m_block);
blocks.push_back(tx->m_block);
}
}
return blocks;
}
// ------------------------------ FREE MEMORY -------------------------------
static void free(std::shared_ptr<monero_block> block) {
for (std::shared_ptr<monero_tx>& tx : block->m_txs) {
tx->m_block.reset();
monero_tx_wallet* tx_wallet = dynamic_cast<monero_tx_wallet*>(tx.get());
if (tx_wallet != nullptr) {
if (tx_wallet->m_tx_set != nullptr) tx_wallet->m_tx_set.reset();
if (tx_wallet->m_outgoing_transfer != nullptr) tx_wallet->m_outgoing_transfer->m_tx.reset();
for (std::shared_ptr<monero_transfer> transfer : tx_wallet->m_incoming_transfers) transfer->m_tx.reset();
for (std::shared_ptr<monero_output> output : tx_wallet->m_outputs) output->m_tx.reset();
for (std::shared_ptr<monero_output> input : tx_wallet->m_inputs) {
input->m_key_image.reset();
input->m_tx.reset();
}
}
monero_tx_query* tx_query = dynamic_cast<monero_tx_query*>(tx.get());
if (tx_query != nullptr) {
if (tx_query->m_transfer_query != nullptr) {
tx_query->m_transfer_query->m_tx_query.reset();
tx_query->m_transfer_query.reset();
}
if (tx_query->m_output_query != nullptr) {
tx_query->m_output_query->m_tx_query.reset();
tx_query->m_output_query.reset();
}
}
}
block.reset();
}
static void free(std::vector<std::shared_ptr<monero_block>> blocks) {
for (std::shared_ptr<monero_block>& block : blocks) monero_utils::free(block);
}
static void free(std::shared_ptr<monero_tx> tx) {
if (tx->m_block == nullptr) {
std::shared_ptr<monero_block> block = std::make_shared<monero_block>();
tx->m_block = block;
block->m_txs.push_back(tx);
}
monero_utils::free(tx->m_block);
}
static void free(std::vector<std::shared_ptr<monero_tx_wallet>> txs) {
return monero_utils::free(monero_utils::get_blocks_from_txs(txs));
}
static void free(std::vector<std::shared_ptr<monero_transfer>> transfers) {
return monero_utils::free(monero_utils::get_blocks_from_transfers(transfers));
}
static void free(std::vector<std::shared_ptr<monero_output_wallet>> outputs) {
return monero_utils::free(monero_utils::get_blocks_from_outputs(outputs));
}
}
#endif /* monero_utils_h */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,311 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include <boost/optional.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>
#include <boost/thread/condition_variable.hpp>
#include <atomic>
#include "monero_wallet.h"
#include "wallet/wallet2.h"
/**
* Implements a monero_wallet.h by wrapping monero-project's wallet2.
*/
namespace monero {
// -------------------------------- LISTENERS -------------------------------
// forward declaration of internal wallet2 listener
struct wallet2_listener;
// --------------------------- STATIC WALLET UTILS --------------------------
/**
* Monero wallet implementation which uses monero-project's wallet2.
*/
class monero_wallet_full : public monero_wallet {
public:
/**
* Indicates if a wallet exists at the given path.
*
* @param path is the path to check for a wallet
* @return true if a wallet exists at the given path, false otherwise
*/
static bool wallet_exists(const std::string& path);
/**
* Open an existing wallet from disk.
*
* @param path is the path to the wallet file to open
* @param password is the password of the wallet file to open
* @param network_type is the wallet's network type
* @param regtest enable regtest
* @return a pointer to the wallet instance
*/
static monero_wallet_full* open_wallet(const std::string& path, const std::string& password, const monero_network_type network_type, bool regtest = false);
/**
* Open an in-memory wallet from existing data buffers.
*
* @param password is the password of the wallet file to open
* @param network_type is the wallet's network type
* @param keys_data contains the contents of the ".keys" file
* @param cache_data contains the contents of the wallet cache file (no extension)
* @param daemon_connection is connection information to a daemon (default = an unconnected wallet)
* @param http_client_factory allows use of custom http clients
* @param regtest enable regtest
* @return a pointer to the wallet instance
*/
static monero_wallet_full* open_wallet_data(const std::string& password, const monero_network_type, const std::string& keys_data, const std::string& cache_data, const std::shared_ptr<monero_rpc_connection>& daemon_connection = std::make_shared<monero_rpc_connection>(), std::unique_ptr<epee::net_utils::http::http_client_factory> http_client_factory = nullptr, bool regtest = false);
/**
* Create a new wallet with the given configuration.
*
* @param config is the wallet configuration
* @param http_client_factory allows use of custom http clients
* @return a pointer to the wallet instance
*/
static monero_wallet_full* create_wallet(const monero_wallet_config& config, std::unique_ptr<epee::net_utils::http::http_client_factory> http_client_factory = nullptr);
/**
* Get a list of available languages for the wallet's seed.
*
* @return the available languages for the wallet's seed
*/
static std::vector<std::string> get_seed_languages();
// ----------------------------- WALLET METHODS -----------------------------
/**
* Destruct the wallet.
*/
~monero_wallet_full();
/**
* Supported wallet methods.
*/
bool is_view_only() const override { return m_w2->watch_only(); }
void set_daemon_connection(const std::string& uri, const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const boost::optional<bool>& is_trusted = boost::none) override;
void set_daemon_connection(const std::shared_ptr<monero_rpc_connection>& connection, const boost::optional<bool>& is_trusted = boost::none) override;
std::shared_ptr<monero_rpc_connection> get_daemon_connection() const override;
bool is_connected_to_daemon() const override;
bool is_daemon_synced() const override;
bool is_daemon_trusted() const override;
bool is_synced() const override;
monero_version get_version() const override;
std::string get_path() const override;
monero_network_type get_network_type() const override;
std::string get_seed() const override;
std::string get_seed_language() const override;
std::string get_public_view_key() const override;
std::string get_private_view_key() const override;
std::string get_public_spend_key() const override;
std::string get_private_spend_key() const override;
std::string get_address(const uint32_t account_idx, const uint32_t subaddress_idx) const override;
monero_subaddress get_address_index(const std::string& address) const override;
monero_integrated_address get_integrated_address(const std::string& standard_address = "", const std::string& payment_id = "") const override;
monero_integrated_address decode_integrated_address(const std::string& integrated_address) const override;
uint64_t get_height() const override;
uint64_t get_restore_height() const override;
void set_restore_height(uint64_t restore_height) override;
uint64_t get_daemon_height() const override;
uint64_t get_daemon_max_peer_height() const override;
uint64_t get_height_by_date(uint16_t year, uint8_t month, uint8_t day) const override;
void add_listener(monero_wallet_listener& listener) override;
void remove_listener(monero_wallet_listener& listener) override;
std::set<monero_wallet_listener*> get_listeners() override;
monero_sync_result sync() override;
monero_sync_result sync(monero_wallet_listener& listener) override;
monero_sync_result sync(uint64_t start_height) override;
monero_sync_result sync(uint64_t start_height, monero_wallet_listener& listener) override;
void start_syncing(uint64_t sync_period_in_ms) override;
void stop_syncing() override;
void scan_txs(const std::vector<std::string>& tx_hashes) override;
void rescan_spent() override;
void rescan_blockchain() override;
uint64_t get_balance() const override;
uint64_t get_balance(uint32_t account_idx) const override;
uint64_t get_balance(uint32_t account_idx, uint32_t subaddress_idx) const override;
uint64_t get_unlocked_balance() const override;
uint64_t get_unlocked_balance(uint32_t account_idx) const override;
uint64_t get_unlocked_balance(uint32_t account_idx, uint32_t subaddress_idx) const override;
std::vector<monero_account> get_accounts(bool include_subaddresses, const std::string& tag) const override;
monero_account get_account(const uint32_t account_idx, bool include_subaddresses) const override;
monero_account create_account(const std::string& label = "") override;
std::vector<monero_subaddress> get_subaddresses(const uint32_t account_idx, const std::vector<uint32_t>& subaddress_indices) const override;
monero_subaddress create_subaddress(uint32_t account_idx, const std::string& label = "") override;
void set_subaddress_label(uint32_t account_idx, uint32_t subaddress_idx, const std::string& label = "") override;
std::vector<std::shared_ptr<monero_tx_wallet>> get_txs() const override;
std::vector<std::shared_ptr<monero_tx_wallet>> get_txs(const monero_tx_query& query) const override;
std::vector<std::shared_ptr<monero_transfer>> get_transfers(const monero_transfer_query& query) const override;
std::vector<std::shared_ptr<monero_output_wallet>> get_outputs(const monero_output_query& query) const override;
std::string export_outputs(bool all = false) const override;
int import_outputs(const std::string& outputs_hex) override;
std::shared_ptr<monero_key_image_export_result> export_key_images(bool all = false) const override;
std::shared_ptr<monero_key_image_import_result> import_key_images(const std::vector<std::shared_ptr<monero_key_image>>& key_images, uint64_t offset = 0) override;
void freeze_output(const std::string& key_image) override;
void thaw_output(const std::string& key_image) override;
bool is_output_frozen(const std::string& key_image) override;
monero_tx_priority get_default_fee_priority() const override;
std::vector<std::shared_ptr<monero_tx_wallet>> create_txs(const monero_tx_config& config) override;
std::vector<std::shared_ptr<monero_tx_wallet>> sweep_unlocked(const monero_tx_config& config) override;
std::shared_ptr<monero_tx_wallet> sweep_output(const monero_tx_config& config) override;
std::vector<std::shared_ptr<monero_tx_wallet>> sweep_dust(bool relay = false) override;
std::vector<std::string> relay_txs(const std::vector<std::string>& tx_metadatas) override;
monero_tx_set describe_tx_set(const monero_tx_set& tx_set) override;
monero_tx_set sign_txs(const std::string& unsigned_tx_hex) override;
std::vector<std::string> submit_txs(const std::string& signed_tx_hex) override;
std::string sign_message(const std::string& msg, monero_message_signature_type signature_type, uint32_t account_idx = 0, uint32_t subaddress_idx = 0) const override;
monero_message_signature_result verify_message(const std::string& msg, const std::string& address, const std::string& signature) const override;
std::string get_tx_key(const std::string& tx_hash) const override;
std::shared_ptr<monero_check_tx> check_tx_key(const std::string& tx_hash, const std::string& txKey, const std::string& address) const override;
std::string get_tx_proof(const std::string& tx_hash, const std::string& address, const std::string& message) const override;
std::shared_ptr<monero_check_tx> check_tx_proof(const std::string& tx_hash, const std::string& address, const std::string& message, const std::string& signature) const override;
std::string get_spend_proof(const std::string& tx_hash, const std::string& message) const override;
bool check_spend_proof(const std::string& tx_hash, const std::string& message, const std::string& signature) const override;
std::string get_reserve_proof_wallet(const std::string& message) const override;
std::string get_reserve_proof_account(uint32_t account_idx, uint64_t amount, const std::string& message) const override;
std::shared_ptr<monero_check_reserve> check_reserve_proof(const std::string& address, const std::string& message, const std::string& signature) const override;
std::string get_tx_note(const std::string& tx_hash) const override;
std::vector<std::string> get_tx_notes(const std::vector<std::string>& tx_hashes) const override;
void set_tx_note(const std::string& tx_hash, const std::string& note) override;
void set_tx_notes(const std::vector<std::string>& tx_hashes, const std::vector<std::string>& notes) override;
std::vector<monero_address_book_entry> get_address_book_entries(const std::vector<uint64_t>& indices) const override;
uint64_t add_address_book_entry(const std::string& address, const std::string& description) override;
void edit_address_book_entry(uint64_t index, bool set_address, const std::string& address, bool set_description, const std::string& description) override;
void delete_address_book_entry(uint64_t index) override;
std::string get_payment_uri(const monero_tx_config& config) const override;
std::shared_ptr<monero_tx_config> parse_payment_uri(const std::string& uri) const override;
bool get_attribute(const std::string& key, std::string& value) const override;
void set_attribute(const std::string& key, const std::string& val) override;
void start_mining(boost::optional<uint64_t> num_threads, boost::optional<bool> background_mining, boost::optional<bool> ignore_battery) override;
void stop_mining() override;
uint64_t wait_for_next_block() override;
bool is_multisig_import_needed() const override;
monero_multisig_info get_multisig_info() const override;
std::string prepare_multisig() override;
std::string make_multisig(const std::vector<std::string>& multisig_hexes, int threshold, const std::string& password) override;
monero_multisig_init_result exchange_multisig_keys(const std::vector<std::string>& multisig_hexes, const std::string& password) override;
std::string export_multisig_hex() override;
int import_multisig_hex(const std::vector<std::string>& multisig_hexes, const bool refresh_after_import = true) override;
monero_multisig_sign_result sign_multisig_tx_hex(const std::string& multisig_tx_hex) override;
std::vector<std::string> submit_multisig_tx_hex(const std::string& signed_multisig_tx_hex) override;
void change_password(const std::string& old_password, const std::string& new_password) override;
void move_to(const std::string& path, const std::string& password) override;
void save() override;
void close(bool save = false) override;
bool is_closed() const override { return m_is_closed; }
/**
* Wallet import and export using buffers and not the file system.
*/
std::string get_keys_file_buffer(const epee::wipeable_string& password, bool view_only) const;
std::string get_cache_file_buffer() const;
// --------------------------------- PROTECTED --------------------------------
protected:
std::unique_ptr<tools::wallet2> m_w2; // internal wallet implementation
void init_common();
// ---------------------------------- PRIVATE ---------------------------------
private:
friend struct wallet2_listener;
std::unique_ptr<wallet2_listener> m_w2_listener; // internal wallet implementation listener
std::set<monero_wallet_listener*> m_listeners; // external wallet listeners
std::atomic<bool> m_is_closed;
static monero_wallet_full* create_wallet_from_seed(monero_wallet_config& config, std::unique_ptr<epee::net_utils::http::http_client_factory> http_client_factory);
static monero_wallet_full* create_wallet_from_keys(monero_wallet_config& config, std::unique_ptr<epee::net_utils::http::http_client_factory> http_client_factory);
static monero_wallet_full* create_wallet_random(monero_wallet_config& config, std::unique_ptr<epee::net_utils::http::http_client_factory> http_client_factory);
std::vector<monero_subaddress> get_subaddresses_aux(uint32_t account_idx, const std::vector<uint32_t>& subaddress_indices, const std::vector<tools::wallet2::transfer_details>& transfers) const;
std::vector<std::shared_ptr<monero_transfer>> get_transfers_aux(const monero_transfer_query& query) const;
std::vector<std::shared_ptr<monero_output_wallet>> get_outputs_aux(const monero_output_query& query) const;
std::vector<std::shared_ptr<monero_tx_wallet>> sweep_account(const monero_tx_config& config); // sweeps unlocked funds within an account; private helper to sweep_unlocked()
void assert_not_closed() const;
// serializes a wallet operation with background sync: pauses the sync loop, interrupts background refresh, and locks m_sync_mutex
struct sync_op_lock {
sync_op_lock(const monero_wallet_full& wallet);
sync_op_lock(const sync_op_lock&) = delete;
~sync_op_lock();
const monero_wallet_full& m_wallet;
};
// blockchain sync management
mutable std::atomic<bool> m_is_synced; // whether or not wallet is synced
mutable std::atomic<bool> m_is_connected; // cache connection status to avoid unecessary RPC calls
boost::condition_variable m_sync_cv; // to make sync threads woke
mutable boost::mutex m_sync_mutex; // synchronize sync and other wallet operations
mutable std::atomic<uint32_t> m_num_sync_pauses; // number of operations pausing background sync
std::atomic<bool> m_background_syncing; // whether or not a background sync pass is in progress
std::atomic<bool> m_rescan_on_sync; // whether or not to rescan on sync
std::atomic<bool> m_syncing_enabled; // whether or not auto sync is enabled
std::atomic<bool> m_sync_loop_running; // whether or not the syncing thread is shut down
std::atomic<int> m_syncing_interval; // auto sync loop interval in milliseconds
boost::thread m_syncing_thread; // thread for auto sync loop
boost::mutex m_syncing_mutex; // synchronize auto sync loop
void run_sync_loop(); // run the sync loop in a thread
monero_sync_result lock_and_sync(boost::optional<uint64_t> start_height = boost::none, bool background = false); // internal function to synchronize request to sync and rescan
monero_sync_result sync_aux(boost::optional<uint64_t> start_height = boost::none); // internal function to immediately block, sync, and report progress
};
}
@@ -0,0 +1,320 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#include "monero_wallet_keys.h"
#include "utils/monero_utils.h"
#include <chrono>
#include <iostream>
#include "mnemonics/electrum-words.h"
#include "mnemonics/english.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "cryptonote_basic/cryptonote_basic_impl.h"
#include "string_tools.h"
#include "device/device.hpp"
using namespace epee;
using namespace tools;
using namespace crypto;
/**
* Public library interface.
*/
namespace monero {
// ---------------------------- WALLET MANAGEMENT ---------------------------
monero_wallet_keys* monero_wallet_keys::create_wallet_random(const monero_wallet_config& config) {
// validate and normalize config
monero_wallet_config config_normalized = config.copy();
if (config_normalized.m_network_type == boost::none) throw std::runtime_error("Must provide wallet network type");
if (config_normalized.m_language == boost::none || config_normalized.m_language.get().empty()) config_normalized.m_language = "English";
if (!monero_utils::is_valid_language(config_normalized.m_language.get())) throw std::runtime_error("Unknown language: " + config_normalized.m_language.get());
// initialize random wallet account
monero_wallet_keys* wallet = new monero_wallet_keys();
crypto::secret_key spend_key_sk = wallet->m_account.generate();
// initialize remaining wallet
wallet->m_network_type = config_normalized.m_network_type.get();
wallet->m_language = config_normalized.m_language.get();
epee::wipeable_string wipeable_mnemonic;
if (!crypto::ElectrumWords::bytes_to_words(spend_key_sk, wipeable_mnemonic, wallet->m_language)) {
throw std::runtime_error("Failed to create mnemonic from private spend key for language: " + std::string(wallet->m_language));
}
wallet->m_seed = std::string(wipeable_mnemonic.data(), wipeable_mnemonic.size());
wallet->init_common();
return wallet;
}
monero_wallet_keys* monero_wallet_keys::create_wallet_from_seed(const monero_wallet_config& config) {
// validate config
if (config.m_is_multisig != boost::none && config.m_is_multisig.get()) throw std::runtime_error("Restoring from multisig seed not supported");
if (config.m_network_type == boost::none) throw std::runtime_error("Must provide wallet network type");
if (config.m_seed == boost::none || config.m_seed.get().empty()) throw std::runtime_error("Must provide wallet seed");
// validate mnemonic and get recovery key and language
crypto::secret_key spend_key_sk;
std::string language;
bool is_valid = crypto::ElectrumWords::words_to_bytes(config.m_seed.get(), spend_key_sk, language);
if (!is_valid) throw std::runtime_error("Invalid mnemonic");
if (language == crypto::ElectrumWords::old_language_name) language = Language::English().get_language_name();
// apply offset if given
if (config.m_seed_offset != boost::none && !config.m_seed_offset.get().empty()) spend_key_sk = cryptonote::decrypt_key(spend_key_sk, config.m_seed_offset.get());
// initialize wallet account
monero_wallet_keys* wallet = new monero_wallet_keys();
wallet->m_account = cryptonote::account_base{};
wallet->m_account.generate(spend_key_sk, true, false);
// initialize remaining wallet
wallet->m_network_type = config.m_network_type.get();
wallet->m_language = language;
epee::wipeable_string wipeable_mnemonic;
if (!crypto::ElectrumWords::bytes_to_words(spend_key_sk, wipeable_mnemonic, wallet->m_language)) {
throw std::runtime_error("Failed to create mnemonic from private spend key for language: " + std::string(wallet->m_language));
}
wallet->m_seed = std::string(wipeable_mnemonic.data(), wipeable_mnemonic.size());
wallet->init_common();
return wallet;
}
monero_wallet_keys* monero_wallet_keys::create_wallet_from_keys(const monero_wallet_config& config) {
// validate and normalize config
monero_wallet_config config_normalized = config.copy();
if (config.m_network_type == boost::none) throw std::runtime_error("Must provide wallet network type");
if (config.m_language == boost::none || config_normalized.m_language.get().empty()) config_normalized.m_language = "English";
if (config.m_private_spend_key == boost::none) config_normalized.m_private_spend_key = std::string("");
if (config.m_private_view_key == boost::none) config_normalized.m_private_view_key = std::string("");
if (!monero_utils::is_valid_language(config_normalized.m_language.get())) throw std::runtime_error("Unknown language: " + config_normalized.m_language.get());
// parse and validate private spend key
crypto::secret_key spend_key_sk;
bool has_spend_key = false;
if (!config_normalized.m_private_spend_key.get().empty()) {
cryptonote::blobdata spend_key_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(config.m_private_spend_key.get(), spend_key_data) || spend_key_data.size() != sizeof(crypto::secret_key)) {
throw std::runtime_error("failed to parse secret spend key");
}
has_spend_key = true;
spend_key_sk = *reinterpret_cast<const crypto::secret_key*>(spend_key_data.data());
}
// parse and validate private view key
bool has_view_key = true;
crypto::secret_key view_key_sk;
if (config_normalized.m_private_view_key.get().empty()) {
if (has_spend_key) has_view_key = false;
else throw std::runtime_error("Neither spend key nor view key supplied");
}
if (has_view_key) {
cryptonote::blobdata view_key_data;
if (!epee::string_tools::parse_hexstr_to_binbuff(config_normalized.m_private_view_key.get(), view_key_data) || view_key_data.size() != sizeof(crypto::secret_key)) {
throw std::runtime_error("failed to parse secret view key");
}
view_key_sk = *reinterpret_cast<const crypto::secret_key*>(view_key_data.data());
}
// parse and validate address
cryptonote::address_parse_info address_info;
if (config_normalized.m_primary_address.get().empty()) {
if (has_view_key) throw std::runtime_error("must provide address if providing private view key");
} else {
if (!get_account_address_from_str(address_info, static_cast<cryptonote::network_type>(config_normalized.m_network_type.get()), config_normalized.m_primary_address.get())) throw std::runtime_error("failed to parse address");
// check the spend and view keys match the given address
crypto::public_key pkey;
if (has_spend_key) {
if (!crypto::secret_key_to_public_key(spend_key_sk, pkey)) throw std::runtime_error("failed to verify secret spend key");
if (address_info.address.m_spend_public_key != pkey) throw std::runtime_error("spend key does not match address");
}
if (has_view_key) {
if (!crypto::secret_key_to_public_key(view_key_sk, pkey)) throw std::runtime_error("failed to verify secret view key");
if (address_info.address.m_view_public_key != pkey) throw std::runtime_error("view key does not match address");
}
}
// initialize wallet account
monero_wallet_keys* wallet = new monero_wallet_keys();
if (has_spend_key && has_view_key) {
wallet->m_account.create_from_keys(address_info.address, spend_key_sk, view_key_sk);
} else if (has_spend_key) {
wallet->m_account.generate(spend_key_sk, true, false);
} else {
wallet->m_account.create_from_viewkey(address_info.address, view_key_sk);
}
// initialize remaining wallet
wallet->m_is_view_only = !has_spend_key;
wallet->m_network_type = config_normalized.m_network_type.get();
if (!config_normalized.m_private_spend_key.get().empty()) {
wallet->m_language = config_normalized.m_language.get();
epee::wipeable_string wipeable_mnemonic;
if (!crypto::ElectrumWords::bytes_to_words(spend_key_sk, wipeable_mnemonic, wallet->m_language)) {
throw std::runtime_error("Failed to create mnemonic from private spend key for language: " + std::string(wallet->m_language));
}
wallet->m_seed = std::string(wipeable_mnemonic.data(), wipeable_mnemonic.size());
}
wallet->init_common();
return wallet;
}
std::vector<std::string> monero_wallet_keys::get_seed_languages() {
std::vector<std::string> languages;
crypto::ElectrumWords::get_language_list(languages, true); // TODO: support getting names in language
return languages;
}
// ----------------------------- WALLET METHODS -----------------------------
monero_wallet_keys::~monero_wallet_keys() {
MTRACE("~monero_wallet_keys()");
close();
}
monero_version monero_wallet_keys::get_version() const {
monero_version version;
version.m_number = 65552; // same as monero-wallet-rpc v0.15.0.1 release
version.m_is_release = false; // TODO: could pull from MONERO_VERSION_IS_RELEASE in version.cpp
return version;
}
std::string monero_wallet_keys::get_address(uint32_t account_idx, uint32_t subaddress_idx) const {
hw::device &hwdev = m_account.get_device();
cryptonote::subaddress_index index{account_idx, subaddress_idx};
cryptonote::account_public_address address = hwdev.get_subaddress(m_account.get_keys(), index);
return cryptonote::get_account_address_as_str(static_cast<cryptonote::network_type>(m_network_type), !index.is_zero(), address);
}
monero_integrated_address monero_wallet_keys::get_integrated_address(const std::string& standard_address, const std::string& payment_id) const {
MTRACE("monero_wallet_keys::get_integrated_address()");
throw std::runtime_error("monero_wallet_keys::get_integrated_address() not implemented");
}
monero_integrated_address monero_wallet_keys::decode_integrated_address(const std::string& integrated_address) const {
MTRACE("monero_wallet_keys::decode_integrated_address()");
throw std::runtime_error("monero_wallet_keys::decode_integrated_address() not implemented");
}
monero_account monero_wallet_keys::get_account(uint32_t account_idx, bool include_subaddresses) const {
MTRACE("monero_wallet_keys::get_account()");
if (include_subaddresses) {
std::string err = "monero_wallet_keys::get_account(account_idx, include_subaddresses) include_subaddresses must be false";
MERROR(err);
throw std::runtime_error(err);
}
// build and return account
monero_account account;
account.m_index = account_idx;
account.m_primary_address = get_address(account_idx, 0);
return account;
}
std::vector<monero_subaddress> monero_wallet_keys::get_subaddresses(const uint32_t account_idx, const std::vector<uint32_t>& subaddress_indices) const {
// must provide subaddress indices
if (subaddress_indices.empty()) {
std::string err = "Keys-only wallet does not have enumerable set of subaddresses; specific specific subaddresses";
MERROR(err);
throw std::runtime_error(err);
}
// initialize subaddresses at indices
std::vector<monero_subaddress> subaddresses;
for (uint32_t subaddressIndicesIdx = 0; subaddressIndicesIdx < subaddress_indices.size(); subaddressIndicesIdx++) {
monero_subaddress subaddress;
subaddress.m_account_index = account_idx;
uint32_t subaddress_idx = subaddress_indices.at(subaddressIndicesIdx);
subaddress.m_index = subaddress_idx;
subaddress.m_address = get_address(account_idx, subaddress_idx);
subaddresses.push_back(subaddress);
}
return subaddresses;
}
std::string monero_wallet_keys::sign_message(const std::string& msg, monero_message_signature_type signature_type, uint32_t account_idx, uint32_t subaddress_idx) const {
MTRACE("monero_wallet_keys::sign_message()");
throw std::runtime_error("monero_wallet_keys::sign_message() not implemented");
}
monero_message_signature_result monero_wallet_keys::verify_message(const std::string& msg, const std::string& address, const std::string& signature) const {
MTRACE("monero_wallet_keys::verify_message()");
throw std::runtime_error("monero_wallet_keys::verify_message() not implemented");
}
void monero_wallet_keys::close(bool save) {
if (save) throw std::runtime_error("MoneroWalletKeys does not support saving");
// no pointers to destroy
}
// ------------------------------- PRIVATE HELPERS ----------------------------
void monero_wallet_keys::init_common() {
m_primary_address = m_account.get_public_address_str(static_cast<cryptonote::network_type>(m_network_type));
const cryptonote::account_keys& keys = m_account.get_keys();
m_pub_view_key = epee::string_tools::pod_to_hex(keys.m_account_address.m_view_public_key);
m_prv_view_key = epee::string_tools::pod_to_hex(unwrap(unwrap(keys.m_view_secret_key)));
m_pub_spend_key = epee::string_tools::pod_to_hex(keys.m_account_address.m_spend_public_key);
m_prv_spend_key = epee::string_tools::pod_to_hex(unwrap(unwrap(keys.m_spend_secret_key)));
if (m_prv_spend_key == "0000000000000000000000000000000000000000000000000000000000000000") m_prv_spend_key = "";
}
}
@@ -0,0 +1,147 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "monero_wallet.h"
#include "cryptonote_basic/account.h"
using namespace monero;
/**
* Public library interface.
*/
namespace monero {
/**
* Implements a Monero wallet to provide basic key management.
*/
class monero_wallet_keys : public monero_wallet {
public:
// --------------------------- STATIC WALLET UTILS --------------------------
/**
* Create a new wallet with a randomly generated seed.
*
* @param config is the wallet configuration (network type and language)
*/
static monero_wallet_keys* create_wallet_random(const monero_wallet_config& config);
/**
* Create a wallet from an existing mnemonic phrase or seed.
*
* @param config is the wallet configuration (network type, seed, seed offset, isMultisig)
*/
static monero_wallet_keys* create_wallet_from_seed(const monero_wallet_config& config);
/**
* Create a wallet from an address, view key, and spend key.
*
* @param config is the wallet configuration (network type, address, view key, spend key, language)
*/
static monero_wallet_keys* create_wallet_from_keys(const monero_wallet_config& config);
/**
* Get a list of available languages for the wallet's seed.
*
* @return the available languages for the wallet's seed
*/
static std::vector<std::string> get_seed_languages();
// ----------------------------- WALLET METHODS -----------------------------
/**
* Destruct the wallet.
*/
~monero_wallet_keys();
/**
* Supported wallet methods.
*/
bool is_view_only() const override { return m_is_view_only; }
monero_version get_version() const override;
monero_network_type get_network_type() const override { return m_network_type; }
std::string get_seed() const override { return m_seed; }
std::string get_seed_language() const override { return m_language; }
std::string get_private_view_key() const override { return m_prv_view_key; }
std::string get_private_spend_key() const override { return m_prv_spend_key; }
std::string get_public_view_key() const override { return m_pub_view_key; }
std::string get_public_spend_key() const override { return m_pub_spend_key; }
std::string get_primary_address() const override { return m_primary_address; }
std::string get_address(const uint32_t account_idx, const uint32_t subaddress_idx) const override;
monero_integrated_address get_integrated_address(const std::string& standard_address = "", const std::string& payment_id = "") const override;
monero_integrated_address decode_integrated_address(const std::string& integrated_address) const override;
monero_account get_account(const uint32_t account_idx, bool include_subaddresses) const override;
std::vector<monero_subaddress> get_subaddresses(const uint32_t account_idx, const std::vector<uint32_t>& subaddress_indices) const override;
std::string sign_message(const std::string& msg, monero_message_signature_type signature_type, uint32_t account_idx = 0, uint32_t subaddress_idx = 0) const override;
monero_message_signature_result verify_message(const std::string& msg, const std::string& address, const std::string& signature) const override;
void close(bool save = false) override;
// --------------------------------- PRIVATE --------------------------------
private:
bool m_is_view_only;
monero_network_type m_network_type;
cryptonote::account_base m_account;
std::string m_seed;
std::string m_language;
std::string m_pub_view_key;
std::string m_prv_view_key;
std::string m_pub_spend_key;
std::string m_prv_spend_key;
std::string m_primary_address;
void init_common();
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,609 @@
/**
* Copyright (c) woodser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "common/monero_rpc_connection.h"
#include "daemon/monero_daemon_model.h"
using namespace monero;
/**
* Public library interface.
*/
namespace monero {
/**
* Configures a wallet to create.
*/
struct monero_wallet_config : public serializable_struct {
boost::optional<std::string> m_path;
boost::optional<std::string> m_password;
boost::optional<monero_network_type> m_network_type;
std::shared_ptr<monero_rpc_connection> m_server;
boost::optional<bool> m_is_trusted_daemon;
boost::optional<std::string> m_seed;
boost::optional<std::string> m_seed_offset;
boost::optional<std::string> m_primary_address;
boost::optional<std::string> m_private_view_key;
boost::optional<std::string> m_private_spend_key;
boost::optional<uint64_t> m_restore_height;
boost::optional<std::string> m_language;
boost::optional<bool> m_save_current;
boost::optional<uint64_t> m_account_lookahead;
boost::optional<uint64_t> m_subaddress_lookahead;
boost::optional<bool> m_is_multisig;
boost::optional<bool> m_regtest;
monero_wallet_config() {}
monero_wallet_config(const monero_wallet_config& config);
monero_wallet_config copy() const;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static std::shared_ptr<monero_wallet_config> deserialize(const std::string& config_json);
};
/**
* Models a result of syncing a wallet.
*/
struct monero_sync_result : public serializable_struct {
uint64_t m_num_blocks_fetched;
bool m_received_money;
monero_sync_result() {}
monero_sync_result(const uint64_t num_blocks_fetched, const bool received_money) : m_num_blocks_fetched(num_blocks_fetched), m_received_money(received_money) {}
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
};
/**
* Models a Monero subaddress.
*/
struct monero_subaddress : public serializable_struct {
boost::optional<uint32_t> m_account_index;
boost::optional<uint32_t> m_index;
boost::optional<std::string> m_address;
boost::optional<std::string> m_label;
boost::optional<uint64_t> m_balance;
boost::optional<uint64_t> m_unlocked_balance;
boost::optional<uint64_t> m_num_unspent_outputs;
boost::optional<bool> m_is_used;
boost::optional<uint64_t> m_num_blocks_to_unlock;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_subaddress>& subaddress);
};
/**
* Models a Monero account.
*/
struct monero_account : public serializable_struct {
boost::optional<uint32_t> m_index;
boost::optional<std::string> m_primary_address;
boost::optional<uint64_t> m_balance;
boost::optional<uint64_t> m_unlocked_balance;
boost::optional<std::string> m_tag;
std::vector<monero_subaddress> m_subaddresses;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_account>& account);
};
/**
* Models an outgoing transfer destination.
*/
struct monero_destination : public serializable_struct {
boost::optional<std::string> m_address;
boost::optional<uint64_t> m_amount;
monero_destination(boost::optional<std::string> address = boost::none, boost::optional<uint64_t> amount = boost::none) : m_address(address), m_amount(amount) {}
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_destination>& destination);
std::shared_ptr<monero_destination> copy(const std::shared_ptr<monero_destination>& src, const std::shared_ptr<monero_destination>& tgt) const;
};
// forward declarations
struct monero_tx_config;
struct monero_tx_wallet;
struct monero_tx_query;
struct monero_tx_set;
/**
* Models a base transfer of funds to or from the wallet.
*
* TODO: m_is_incoming for api consistency
*/
struct monero_transfer : public serializable_struct {
std::shared_ptr<monero_tx_wallet> m_tx;
boost::optional<uint64_t> m_amount;
boost::optional<uint32_t> m_account_index;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_transfer>& transfer);
virtual boost::optional<bool> is_incoming() const = 0; // derived class must implement
std::shared_ptr<monero_transfer> copy(const std::shared_ptr<monero_transfer>& src, const std::shared_ptr<monero_transfer>& tgt) const;
boost::optional<bool> is_outgoing() const {
if (is_incoming() == boost::none) return boost::none;
return !(*is_incoming());
}
void merge(const std::shared_ptr<monero_transfer>& self, const std::shared_ptr<monero_transfer>& other);
};
/**
* Models an incoming transfer of funds to the wallet.
*/
struct monero_incoming_transfer : public monero_transfer {
boost::optional<uint32_t> m_subaddress_index;
boost::optional<std::string> m_address;
boost::optional<uint64_t> m_num_suggested_confirmations;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
std::shared_ptr<monero_incoming_transfer> copy(const std::shared_ptr<monero_transfer>& src, const std::shared_ptr<monero_transfer>& tgt) const;
std::shared_ptr<monero_incoming_transfer> copy(const std::shared_ptr<monero_incoming_transfer>& src, const std::shared_ptr<monero_incoming_transfer>& tgt) const;
boost::optional<bool> is_incoming() const;
void merge(const std::shared_ptr<monero_transfer>& self, const std::shared_ptr<monero_transfer>& other);
void merge(const std::shared_ptr<monero_incoming_transfer>& self, const std::shared_ptr<monero_incoming_transfer>& other);
};
/**
* Models an outgoing transfer of funds from the wallet.
*/
struct monero_outgoing_transfer : public monero_transfer {
std::vector<uint32_t> m_subaddress_indices;
std::vector<std::string> m_addresses;
std::vector<std::shared_ptr<monero_destination>> m_destinations;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
std::shared_ptr<monero_outgoing_transfer> copy(const std::shared_ptr<monero_transfer>& src, const std::shared_ptr<monero_transfer>& tgt) const;
std::shared_ptr<monero_outgoing_transfer> copy(const std::shared_ptr<monero_outgoing_transfer>& src, const std::shared_ptr<monero_outgoing_transfer>& tgt) const;
boost::optional<bool> is_incoming() const;
void merge(const std::shared_ptr<monero_transfer>& self, const std::shared_ptr<monero_transfer>& other);
void merge(const std::shared_ptr<monero_outgoing_transfer>& self, const std::shared_ptr<monero_outgoing_transfer>& other);
};
/**
* Configures a query to retrieve transfers.
*
* All transfers are returned except those that do not meet the criteria defined in this query.
*/
struct monero_transfer_query : public monero_transfer {
boost::optional<bool> m_is_incoming;
boost::optional<std::string> m_address;
std::vector<std::string> m_addresses;
boost::optional<uint32_t> m_subaddress_index;
std::vector<uint32_t> m_subaddress_indices;
std::vector<std::shared_ptr<monero_destination>> m_destinations;
boost::optional<bool> m_has_destinations;
std::shared_ptr<monero_tx_query> m_tx_query;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_transfer_query>& transfer_query);
static std::shared_ptr<monero_transfer_query> deserialize_from_block(const std::string& transfer_query_json);
static bool is_contextual(const monero_transfer_query& query);
std::shared_ptr<monero_transfer_query> copy(const std::shared_ptr<monero_transfer>& src, const std::shared_ptr<monero_transfer>& tgt) const;
std::shared_ptr<monero_transfer_query> copy(const std::shared_ptr<monero_transfer_query>& src, const std::shared_ptr<monero_transfer_query>& tgt) const;
boost::optional<bool> is_incoming() const;
bool meets_criteria(monero_transfer* transfer, bool query_parent = true) const;
};
/**
* Models a Monero output with wallet extensions.
*/
struct monero_output_wallet : public monero_output {
boost::optional<uint32_t> m_account_index;
boost::optional<uint32_t> m_subaddress_index;
boost::optional<bool> m_is_spent;
boost::optional<bool> m_is_frozen;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_output_wallet>& output_wallet);
std::shared_ptr<monero_output_wallet> copy(const std::shared_ptr<monero_output>& src, const std::shared_ptr<monero_output>& tgt) const;
std::shared_ptr<monero_output_wallet> copy(const std::shared_ptr<monero_output_wallet>& src, const std::shared_ptr<monero_output_wallet>& tgt) const;
void merge(const std::shared_ptr<monero_output>& self, const std::shared_ptr<monero_output>& other);
void merge(const std::shared_ptr<monero_output_wallet>& self, const std::shared_ptr<monero_output_wallet>& other);
};
/**
* Configures a query to retrieve wallet outputs (i.e. outputs that the wallet has or had the
* ability to spend).
*
* All outputs are returned except those that do not meet the criteria defined in this query.
*/
struct monero_output_query : public monero_output_wallet {
std::vector<uint32_t> m_subaddress_indices;
boost::optional<uint64_t> m_min_amount;
boost::optional<uint64_t> m_max_amount;
std::shared_ptr<monero_tx_query> m_tx_query;
//boost::property_tree::ptree to_property_tree() const;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_output_query>& output_query);
static std::shared_ptr<monero_output_query> deserialize_from_block(const std::string& output_query_json);
static bool is_contextual(const monero_output_query& query);
std::shared_ptr<monero_output_query> copy(const std::shared_ptr<monero_output>& src, const std::shared_ptr<monero_output>& tgt) const;
std::shared_ptr<monero_output_query> copy(const std::shared_ptr<monero_output_wallet>& src, const std::shared_ptr<monero_output_wallet>& tgt) const; // TODO: necessary to override all super classes?
std::shared_ptr<monero_output_query> copy(const std::shared_ptr<monero_output_query>& src, const std::shared_ptr<monero_output_query>& tgt) const;
bool meets_criteria(monero_output_wallet* output, bool query_parent = true) const;
};
/**
* Models a Monero transaction in the context of a wallet.
*/
struct monero_tx_wallet : public monero_tx {
std::shared_ptr<monero_tx_set> m_tx_set;
boost::optional<bool> m_is_incoming;
boost::optional<bool> m_is_outgoing;
std::vector<std::shared_ptr<monero_incoming_transfer>> m_incoming_transfers;
std::shared_ptr<monero_outgoing_transfer> m_outgoing_transfer;
boost::optional<std::string> m_note;
boost::optional<bool> m_is_locked;
boost::optional<uint64_t> m_input_sum;
boost::optional<uint64_t> m_output_sum;
boost::optional<std::string> m_change_address;
boost::optional<uint64_t> m_change_amount;
boost::optional<uint32_t> m_num_dummy_outputs;
boost::optional<std::string> m_extra_hex;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_wallet>& tx_wallet);
std::shared_ptr<monero_tx_wallet> copy(const std::shared_ptr<monero_tx>& src, const std::shared_ptr<monero_tx>& tgt) const;
std::shared_ptr<monero_tx_wallet> copy(const std::shared_ptr<monero_tx_wallet>& src, const std::shared_ptr<monero_tx_wallet>& tgt) const;
void merge(const std::shared_ptr<monero_tx>& self, const std::shared_ptr<monero_tx>& other);
void merge(const std::shared_ptr<monero_tx_wallet>& self, const std::shared_ptr<monero_tx_wallet>& other);
std::vector<std::shared_ptr<monero_transfer>> get_transfers() const;
std::vector<std::shared_ptr<monero_transfer>> get_transfers(const monero_transfer_query& query) const;
std::vector<std::shared_ptr<monero_transfer>> filter_transfers(const monero_transfer_query& query);
std::vector<std::shared_ptr<monero_output_wallet>> get_outputs_wallet() const;
std::vector<std::shared_ptr<monero_output_wallet>> get_outputs_wallet(const monero_output_query& query) const;
std::vector<std::shared_ptr<monero_output_wallet>> filter_outputs_wallet(const monero_output_query& query);
};
/**
* Configures a query to retrieve transactions.
*
* All transactions are returned except those that do not meet the criteria defined in this query.
*/
struct monero_tx_query : public monero_tx_wallet {
boost::optional<bool> m_is_outgoing;
boost::optional<bool> m_is_incoming;
std::vector<std::string> m_hashes;
boost::optional<bool> m_has_payment_id;
std::vector<std::string> m_payment_ids;
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_min_height;
boost::optional<uint64_t> m_max_height;
boost::optional<uint64_t> m_include_outputs;
std::shared_ptr<monero_transfer_query> m_transfer_query;
std::shared_ptr<monero_output_query> m_input_query;
std::shared_ptr<monero_output_query> m_output_query;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_query>& tx_query);
static std::shared_ptr<monero_tx_query> deserialize_from_block(const std::string& tx_query_json);
/**
* Remove query criteria which require looking up other transfers/outputs to
* fulfill query.
*
* @param query the query to decontextualize
* @return a reference to the query for convenience
*/
static std::shared_ptr<monero_tx_query> decontextualize(std::shared_ptr<monero_tx_query> query);
std::shared_ptr<monero_tx_query> copy(const std::shared_ptr<monero_tx>& src, const std::shared_ptr<monero_tx>& tgt) const;
std::shared_ptr<monero_tx_query> copy(const std::shared_ptr<monero_tx_wallet>& src, const std::shared_ptr<monero_tx_wallet>& tgt) const; // TODO: necessary to override all super classes?
std::shared_ptr<monero_tx_query> copy(const std::shared_ptr<monero_tx_query>& src, const std::shared_ptr<monero_tx_query>& tgt) const;
bool meets_criteria(monero_tx_wallet* tx, bool query_children = true) const;
};
/**
* Groups transactions who share common hex data which is needed in order to
* sign and submit the transactions.
*
* For example, multisig transactions created from create_txs() share a common
* hex string which is needed in order to sign and submit the multisig
* transactions.
*/
struct monero_tx_set : public serializable_struct {
std::vector<std::shared_ptr<monero_tx_wallet>> m_txs;
boost::optional<std::string> m_signed_tx_hex;
boost::optional<std::string> m_unsigned_tx_hex;
boost::optional<std::string> m_multisig_tx_hex;
//boost::property_tree::ptree to_property_tree() const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_set>& set);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static monero_tx_set deserialize(const std::string& tx_set_json);
};
/**
* Monero integrated address model.
*/
struct monero_integrated_address : public serializable_struct {
std::string m_standard_address;
std::string m_payment_id;
std::string m_integrated_address;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_integrated_address>& subaddress);
};
/**
* Enumerates Monero network types.
*/
enum monero_tx_priority : uint8_t {
DEFAULT = 0,
UNIMPORTANT,
NORMAL,
ELEVATED
};
/**
* Configures a transaction to send, sweep, or create a payment URI.
*/
struct monero_tx_config : public serializable_struct {
boost::optional<std::string> m_address;
boost::optional<uint64_t> m_amount;
std::vector<std::shared_ptr<monero_destination>> m_destinations;
std::vector<uint32_t> m_subtract_fee_from;
boost::optional<std::string> m_payment_id;
boost::optional<monero_tx_priority> m_priority;
boost::optional<uint32_t> m_ring_size;
boost::optional<uint64_t> m_fee;
boost::optional<uint32_t> m_account_index;
std::vector<uint32_t> m_subaddress_indices;
boost::optional<bool> m_can_split;
boost::optional<bool> m_relay;
boost::optional<std::string> m_note;
boost::optional<std::string> m_recipient_name;
boost::optional<uint64_t> m_below_amount;
boost::optional<bool> m_sweep_each_subaddress;
boost::optional<std::string> m_key_image;
monero_tx_config() {}
monero_tx_config(const monero_tx_config& config);
monero_tx_config copy() const;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static std::shared_ptr<monero_tx_config> deserialize(const std::string& config_json);
std::vector<std::shared_ptr<monero_destination>> get_normalized_destinations() const;
};
/**
* Models results from exporting signed key images.
*/
struct monero_key_image_export_result : public serializable_struct {
boost::optional<uint64_t> m_offset;
std::vector<std::shared_ptr<monero_key_image>> m_key_images;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static std::shared_ptr<monero_key_image_export_result> deserialize(const std::string& result_json);
};
/**
* Models results from importing key images.
*/
struct monero_key_image_import_result : public serializable_struct {
boost::optional<uint64_t> m_height;
boost::optional<uint64_t> m_spent_amount;
boost::optional<uint64_t> m_unspent_amount;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_key_image_import_result>& result);
};
/**
* Enumerates message verification results.
*/
enum monero_message_signature_type : uint8_t {
SIGN_WITH_SPEND_KEY = 0,
SIGN_WITH_VIEW_KEY
};
/**
* Models results from message verification.
*/
struct monero_message_signature_result : public serializable_struct {
bool m_is_good;
uint32_t m_version;
bool m_is_old;
monero_message_signature_type m_signature_type;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_message_signature_result> result);
};
/**
* Base class for results from checking a transaction or reserve proof.
*/
struct monero_check : public serializable_struct {
bool m_is_good;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
};
/**
* Results from checking a transaction key.
*/
struct monero_check_tx : public monero_check {
boost::optional<bool> m_in_tx_pool;
boost::optional<uint64_t> m_num_confirmations;
boost::optional<uint64_t> m_received_amount;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_check_tx>& check);
};
/**
* Results from checking a reserve proof.
*/
struct monero_check_reserve : public monero_check {
boost::optional<uint64_t> m_total_amount;
boost::optional<uint64_t> m_unconfirmed_spent_amount;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_check_reserve>& check);
};
/**
* Models information about a multisig wallet.
*/
struct monero_multisig_info : public serializable_struct {
bool m_is_multisig;
bool m_is_ready;
uint32_t m_threshold;
uint32_t m_num_participants;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_multisig_info>& info);
};
/**
* Models the result of initializing a multisig wallet which results in the
* multisig wallet's address xor another multisig hex to share with
* participants to create the wallet.
*/
struct monero_multisig_init_result : public serializable_struct {
boost::optional<std::string> m_address;
boost::optional<std::string> m_multisig_hex;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_multisig_init_result>& res);
};
/**
* Models the result of signing multisig tx hex.
*/
struct monero_multisig_sign_result : public serializable_struct {
boost::optional<std::string> m_signed_multisig_tx_hex;
std::vector<std::string> m_tx_hashes;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_multisig_sign_result>& res);
};
/**
* Monero address book entry model.
*/
struct monero_address_book_entry : public serializable_struct {
boost::optional<uint64_t> m_index; // TODO: not boost::optional
boost::optional<std::string> m_address;
boost::optional<std::string> m_description;
boost::optional<std::string> m_payment_id;
monero_address_book_entry() {}
monero_address_book_entry(uint64_t index, const std::string& address, const std::string& description) : m_index(index), m_address(address), m_description(description) {}
monero_address_book_entry(uint64_t index, const std::string& address, const std::string& description, const std::string& payment_id) : m_index(index), m_address(address), m_description(description), m_payment_id(payment_id) {}
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_address_book_entry>& entry);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const;
};
/**
* Models a Monero address type.
*/
enum monero_address_type : uint8_t {
PRIMARY_ADDRESS = 0,
INTEGRATED_ADDRESS,
SUBADDRESS
};
/**
* Compares two transactions by their height.
*/
struct monero_tx_height_comparator {
bool operator()(const std::shared_ptr<monero_tx>& tx1, const std::shared_ptr<monero_tx>& tx2) const;
};
/**
* Compares two transfers by ascending account and subaddress indices.
*/
struct monero_incoming_transfer_comparator {
bool operator()(const std::shared_ptr<monero_incoming_transfer>& t1, const std::shared_ptr<monero_incoming_transfer>& t2) const;
bool operator()(const monero_incoming_transfer& t1, const monero_incoming_transfer& t2) const;
};
/**
* Compares two outputs by ascending account and subaddress indices.
*/
struct monero_output_comparator {
bool operator()(const monero_output_wallet& o1, const monero_output_wallet& o2) const;
};
/**
* Maintains metadata for a decoded address.
*/
struct monero_decoded_address : public serializable_struct {
std::string m_address;
monero_address_type m_address_type;
monero_network_type m_network_type;
monero_decoded_address(const std::string& address, monero_address_type address_type, monero_network_type network_type);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
/**
* Represents an account tag.
*/
struct monero_account_tag : public serializable_struct {
boost::optional<std::string> m_tag;
boost::optional<std::string> m_label;
std::vector<uint32_t> m_account_indices;
monero_account_tag() { }
monero_account_tag(const std::string& tag, const std::string& label): m_tag(tag), m_label(label) { }
monero_account_tag(const std::string& tag, const std::string& label, const std::vector<uint32_t>& account_indices): m_tag(tag), m_label(label), m_account_indices(account_indices) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_account_tag>& account_tag);
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,282 @@
/**
* Copyright (c) everoddandeven
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2025-2026 woodser
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "monero_wallet.h"
/**
* Implements a monero_wallet.h by wrapping monero-project's monero-wallet-rpc.
*/
namespace monero {
// forward declaration of internal wallet poller
class monero_wallet_poller;
/**
* Implements a Monero wallet using monero-wallet-rpc.
*/
class monero_wallet_rpc : public monero_wallet {
public:
/**
* Destruct the wallet.
*/
~monero_wallet_rpc();
monero_wallet_rpc(const std::shared_ptr<monero_rpc_connection>& rpc_connection);
monero_wallet_rpc(const std::string& uri = "", const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const std::string& zmq_uri = "", const boost::optional<uint32_t>& timeout = boost::none);
/**
* Set the wallet poll period.
*
* @param period_ms poll period in milliseconds.
*/
void set_poll_period_in_ms(uint64_t period_ms);
/**
* Open an existing wallet from rpc server.
*
* @param config is the wallet configuration
* @return a pointer to the wallet instance
*/
monero_wallet_rpc* open_wallet(const std::shared_ptr<monero_wallet_config> &config);
/**
* Open an existing wallet from rpc server.
*
* @param name is the wallet's name to open
* @param password is the password of the wallet file to open
* @return a pointer to the wallet instance
*/
monero_wallet_rpc* open_wallet(const std::string& name, const std::string& password);
/**
* Create a new wallet with the given configuration.
*
* @param config is the wallet configuration
* @return a pointer to the wallet instance
*/
monero_wallet_rpc* create_wallet(const std::shared_ptr<monero_wallet_config> &config);
/**
* Get the wallet's RPC connection.
*
* @return the wallet's rpc connection
*/
std::shared_ptr<monero_rpc_connection> get_rpc_connection() const { return m_rpc; }
/**
* Get a list of available languages for the wallet's seed.
*
* @return the available languages for the wallet's seed
*/
std::vector<std::string> get_seed_languages() const;
/**
* Save and close the current wallet and stop the RPC server.
*/
void stop();
/**
* Supported wallet methods.
*/
void add_listener(monero_wallet_listener& listener) override;
void remove_listener(monero_wallet_listener& listener) override;
std::set<monero_wallet_listener*> get_listeners() override;
bool is_view_only() const override;
std::shared_ptr<monero_rpc_connection> get_daemon_connection() const override;
void set_daemon_connection(const std::shared_ptr<monero_rpc_connection>& connection, bool is_trusted, const boost::optional<ssl_options>& ssl_options);
void set_daemon_connection(const std::shared_ptr<monero_rpc_connection>& connection, const boost::optional<bool>& is_trusted = boost::none) override;
void set_daemon_connection(const std::string& uri, const std::string& username = "", const std::string& password = "", const std::string& proxy_uri = "", const boost::optional<bool>& is_trusted = boost::none) override;
bool is_connected_to_daemon() const override;
monero_version get_version() const override;
std::string get_path() const override;
std::string get_seed() const override;
std::string get_seed_language() const override;
std::string get_public_view_key() const override;
std::string get_private_view_key() const override;
std::string get_public_spend_key() const override;
std::string get_private_spend_key() const override;
std::string get_address(const uint32_t account_idx, const uint32_t subaddress_idx) const override;
monero_subaddress get_address_index(const std::string& address) const override;
monero_integrated_address get_integrated_address(const std::string& standard_address = "", const std::string& payment_id = "") const override;
monero_integrated_address decode_integrated_address(const std::string& integrated_address) const override;
uint64_t get_height() const override;
uint64_t get_daemon_height() const override;
uint64_t get_height_by_date(uint16_t year, uint8_t month, uint8_t day) const override;
monero_sync_result sync() override;
monero_sync_result sync(monero_wallet_listener& listener) override;
monero_sync_result sync(uint64_t start_height, monero_wallet_listener& listener) override;
monero_sync_result sync(uint64_t start_height) override;
void start_syncing(uint64_t sync_period_in_ms = 10000) override;
void stop_syncing() override;
void scan_txs(const std::vector<std::string>& tx_hashes) override;
void rescan_spent() override;
void rescan_blockchain() override;
uint64_t get_balance() const override;
uint64_t get_balance(uint32_t account_index) const override;
uint64_t get_balance(uint32_t account_idx, uint32_t subaddress_idx) const override;
uint64_t get_unlocked_balance() const override;
uint64_t get_unlocked_balance(uint32_t account_index) const override;
uint64_t get_unlocked_balance(uint32_t account_idx, uint32_t subaddress_idx) const override;
monero_account get_account(const uint32_t account_idx, bool include_subaddresses) const override;
monero_account get_account(const uint32_t account_idx, bool include_subaddresses, bool skip_balances) const;
std::vector<monero_account> get_accounts(bool include_subaddresses, const std::string& tag) const override;
std::vector<monero_account> get_accounts(bool include_subaddresses, const std::string& tag, bool skip_balances) const;
monero_account create_account(const std::string& label = "") override;
std::vector<monero_subaddress> get_subaddresses(const uint32_t account_idx, const std::vector<uint32_t>& subaddress_indices, bool skip_balances) const;
std::vector<monero_subaddress> get_subaddresses(uint32_t account_idx, const std::vector<uint32_t>& subaddress_indices) const override;
std::vector<monero_subaddress> get_subaddresses(const uint32_t account_idx) const override;
monero_subaddress get_subaddress(const uint32_t account_idx, const uint32_t subaddress_idx) const override;
monero_subaddress create_subaddress(uint32_t account_idx, const std::string& label = "") override;
void set_subaddress_label(uint32_t account_idx, uint32_t subaddress_idx, const std::string& label = "") override;
std::vector<std::shared_ptr<monero_tx_wallet>> get_txs() const override;
std::vector<std::shared_ptr<monero_tx_wallet>> get_txs(const monero_tx_query& query) const override;
std::vector<std::shared_ptr<monero_transfer>> get_transfers(const monero_transfer_query& query) const override;
std::vector<std::shared_ptr<monero_output_wallet>> get_outputs(const monero_output_query& query) const override;
std::string export_outputs(bool all = false) const override;
int import_outputs(const std::string& outputs_hex) override;
std::shared_ptr<monero_key_image_export_result> export_key_images(bool all = false) const override;
std::shared_ptr<monero_key_image_import_result> import_key_images(const std::vector<std::shared_ptr<monero_key_image>>& key_images, uint64_t offset = 0) override;
void freeze_output(const std::string& key_image) override;
void thaw_output(const std::string& key_image) override;
bool is_output_frozen(const std::string& key_image) override;
monero_tx_priority get_default_fee_priority() const override;
std::vector<std::shared_ptr<monero_tx_wallet>> create_txs(const monero_tx_config& conf) override;
std::shared_ptr<monero_tx_wallet> sweep_output(const monero_tx_config& config) override;
std::vector<std::shared_ptr<monero_tx_wallet>> sweep_dust(bool relay = false) override;
std::vector<std::shared_ptr<monero_tx_wallet>> sweep_unlocked(const monero_tx_config& config) override;
std::vector<std::string> relay_txs(const std::vector<std::string>& tx_metadatas) override;
monero_tx_set describe_tx_set(const monero_tx_set& tx_set) override;
monero_tx_set sign_txs(const std::string& unsigned_tx_hex) override;
std::vector<std::string> submit_txs(const std::string& signed_tx_hex) override;
std::string sign_message(const std::string& msg, monero_message_signature_type signature_type, uint32_t account_idx = 0, uint32_t subaddress_idx = 0) const override;
monero_message_signature_result verify_message(const std::string& msg, const std::string& address, const std::string& signature) const override;
std::string get_tx_key(const std::string& tx_hash) const override;
std::shared_ptr<monero_check_tx> check_tx_key(const std::string& tx_hash, const std::string& tx_key, const std::string& address) const override;
std::string get_tx_proof(const std::string& tx_hash, const std::string& address, const std::string& message) const override;
std::shared_ptr<monero_check_tx> check_tx_proof(const std::string& tx_hash, const std::string& address, const std::string& message, const std::string& signature) const override;
std::string get_spend_proof(const std::string& tx_hash, const std::string& message) const override;
bool check_spend_proof(const std::string& tx_hash, const std::string& message, const std::string& signature) const override;
std::string get_reserve_proof_wallet(const std::string& message) const override;
std::string get_reserve_proof_account(uint32_t account_idx, uint64_t amount, const std::string& message) const override;
std::shared_ptr<monero_check_reserve> check_reserve_proof(const std::string& address, const std::string& message, const std::string& signature) const override;
std::string get_tx_note(const std::string& tx_hash) const override;
std::vector<std::string> get_tx_notes(const std::vector<std::string>& tx_hashes) const override;
void set_tx_note(const std::string& tx_hashes, const std::string& notes) override;
void set_tx_notes(const std::vector<std::string>& tx_hashes, const std::vector<std::string>& notes) override;
std::vector<monero_address_book_entry> get_address_book_entries(const std::vector<uint64_t>& indices) const override;
uint64_t add_address_book_entry(const std::string& address, const std::string& description) override;
void edit_address_book_entry(uint64_t index, bool set_address, const std::string& address, bool set_description, const std::string& description) override;
void delete_address_book_entry(uint64_t index) override;
void tag_accounts(const std::string& tag, const std::vector<uint32_t>& account_indices) override;
void untag_accounts(const std::vector<uint32_t>& account_indices) override;
std::vector<std::shared_ptr<monero_account_tag>> get_account_tags() const override;
void set_account_tag_label(const std::string& tag, const std::string& label) override;
std::string get_payment_uri(const monero_tx_config& config) const override;
std::shared_ptr<monero_tx_config> parse_payment_uri(const std::string& uri) const override;
void set_attribute(const std::string& key, const std::string& val) override;
bool get_attribute(const std::string& key, std::string& value) const override;
void start_mining(boost::optional<uint64_t> num_threads, boost::optional<bool> background_mining, boost::optional<bool> ignore_battery) override;
void stop_mining() override;
bool is_multisig_import_needed() const override;
monero_multisig_info get_multisig_info() const override;
std::string prepare_multisig() override;
std::string make_multisig(const std::vector<std::string>& multisig_hexes, int threshold, const std::string& password) override;
monero_multisig_init_result exchange_multisig_keys(const std::vector<std::string>& multisig_hexes, const std::string& password) override;
std::string export_multisig_hex() override;
int import_multisig_hex(const std::vector<std::string>& multisig_hexes, const bool refresh_after_import = true) override;
monero_multisig_sign_result sign_multisig_tx_hex(const std::string& multisig_tx_hex) override;
std::vector<std::string> submit_multisig_tx_hex(const std::string& signed_multisig_tx_hex) override;
void change_password(const std::string& old_password, const std::string& new_password) override;
void save() override;
bool is_closed() const override;
void close(bool save = false) override;
std::shared_ptr<monero_subaddress> get_balances(const boost::optional<uint32_t>& account_idx, const boost::optional<uint32_t>& subaddress_idx) const;
// --------------------------------- PRIVATE --------------------------------
private:
friend class monero_wallet_poller;
boost::optional<uint64_t> m_sync_period_in_ms;
std::string m_path = "";
std::shared_ptr<monero_rpc_connection> m_rpc;
std::shared_ptr<monero_rpc_connection> m_daemon_connection;
mutable boost::recursive_mutex m_listeners_mutex;
std::set<monero_wallet_listener*> m_listeners;
mutable boost::recursive_mutex m_sync_mutex;
mutable boost::mutex m_address_cache_mutex;
mutable std::unordered_map<uint32_t, std::unordered_map<uint32_t, std::string>> m_address_cache;
boost::mutex m_poller_mutex;
std::unique_ptr<monero_wallet_poller> m_poller;
monero_wallet_rpc* create_wallet_random(const std::shared_ptr<monero_wallet_config> &config);
monero_wallet_rpc* create_wallet_from_seed(const std::shared_ptr<monero_wallet_config> &config);
monero_wallet_rpc* create_wallet_from_keys(const std::shared_ptr<monero_wallet_config> &config);
monero_sync_result refresh(const std::shared_ptr<serializable_struct>& params);
std::map<uint32_t, std::vector<uint32_t>> get_account_indices(bool get_subaddress_indices) const;
std::vector<uint32_t> get_subaddress_indices(uint32_t account_idx) const;
std::vector<std::shared_ptr<monero_output_wallet>> get_outputs_aux(const monero_output_query& query) const;
std::vector<std::shared_ptr<monero_transfer>> get_transfers_aux(const monero_transfer_query& query) const;
std::string query_key(const std::string& key_type) const;
std::vector<std::shared_ptr<monero_tx_wallet>> sweep_account(const monero_tx_config &conf);
void clear_address_cache();
void refresh_listening();
void wait_for_listener_callbacks_idle();
void poll();
void clear();
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,500 @@
/**
* Copyright (c) everoddandeven
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Parts of this file are originally copyright (c) 2025-2026 woodser
*
* Parts of this file are originally copyright (c) 2014-2019, The Monero Project
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* All rights reserved.
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
*/
#pragma once
#include "daemon/monero_daemon_rpc_model.h"
#include "monero_wallet_model.h"
/**
* Internal data model for monero_wallet_rpc.
*/
namespace monero {
/**
* key-value struct which can be serialized.
*/
struct key_value : public serializable_struct {
boost::optional<std::string> m_key;
boost::optional<std::string> m_value;
key_value() { }
key_value(const std::string& key): m_key(key) { }
key_value(const std::string& key, const std::string& value): m_key(key), m_value(value) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<key_value>& attributes);
};
/**
* Models a RPC Monero key image.
*/
struct monero_rpc_key_image : public monero_key_image {
monero_rpc_key_image(const std::shared_ptr<monero_key_image> &key_image);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
// ------------------------------ JSON-RPC Params ---------------------------------
struct monero_query_key_params : public serializable_struct {
boost::optional<std::string> m_key_type;
monero_query_key_params(const std::string& key_type): m_key_type(key_type) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_query_output_params : public serializable_struct {
boost::optional<std::string> m_key_image;
monero_query_output_params(const std::string& key_image): m_key_image(key_image) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_address_params : public serializable_struct {
boost::optional<std::string> m_address;
boost::optional<uint32_t> m_account_index;
std::vector<uint32_t> m_subaddress_indices;
monero_get_address_params(const std::string& address): m_address(address) { }
monero_get_address_params(uint32_t account_index): m_account_index(account_index) { }
monero_get_address_params(uint32_t account_index, const std::vector<uint32_t>& subaddress_indices): m_account_index(account_index), m_subaddress_indices(subaddress_indices) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_integrated_address_params : public serializable_struct {
boost::optional<std::string> m_integrated_address;
boost::optional<std::string> m_standard_address;
boost::optional<std::string> m_payment_id;
monero_integrated_address_params(const std::string& integrated_address): m_integrated_address(integrated_address) {}
monero_integrated_address_params(const std::string& standard_address, const std::string& payment_id): m_standard_address(standard_address), m_payment_id(payment_id) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_multisig_params : public serializable_struct {
// TODO monero-docs document this parameter
boost::optional<bool> m_enable_multisig_experimental;
boost::optional<bool> m_refresh_after_import;
std::vector<std::string> m_multisig_info;
std::vector<std::string> m_multisig_hexes;
boost::optional<std::string> m_multisig_tx_hex;
boost::optional<int> m_threshold;
boost::optional<std::string> m_password;
monero_multisig_params(bool enable_multisig_experimental = true): m_enable_multisig_experimental(enable_multisig_experimental) { }
monero_multisig_params(const std::vector<std::string>& multisig_hexes, const std::string& password): m_multisig_info(multisig_hexes), m_password(password) { }
monero_multisig_params(const std::vector<std::string>& multisig_hexes, const char* password): monero_multisig_params(multisig_hexes, std::string(password)) { }
monero_multisig_params(const std::vector<std::string>& multisig_hexes, int threshold, const std::string& password): m_multisig_info(multisig_hexes), m_threshold(threshold), m_password(password) { }
monero_multisig_params(const std::vector<std::string>& multisig_hexes, const bool refresh_after_import): m_multisig_hexes(multisig_hexes), m_refresh_after_import(refresh_after_import) { }
monero_multisig_params(const std::string& multisig_tx_hex): m_multisig_tx_hex(multisig_tx_hex) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_payment_uri_params : public serializable_struct {
boost::optional<std::string> m_uri;
boost::optional<std::string> m_address;
boost::optional<uint64_t> m_amount;
boost::optional<std::string> m_payment_id;
boost::optional<std::string> m_recipient_name;
boost::optional<std::string> m_tx_description;
monero_payment_uri_params() { }
monero_payment_uri_params(const std::string& uri): m_uri(uri) { }
monero_payment_uri_params(const monero_tx_config& config);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_balance_params : public serializable_struct {
boost::optional<uint32_t> m_account_idx;
std::vector<uint32_t> m_address_indices;
boost::optional<bool> m_all_accounts;
boost::optional<bool> m_strict;
monero_get_balance_params(bool all_accounts, bool strict = false): m_all_accounts(all_accounts), m_strict(strict) { }
monero_get_balance_params(uint32_t account_idx, const std::vector<uint32_t>& address_indices, bool all_accounts = false, bool strict = false): m_account_idx(account_idx), m_address_indices(address_indices), m_all_accounts(all_accounts), m_strict(strict) { }
monero_get_balance_params(uint32_t account_idx, boost::optional<uint32_t> address_idx, bool all_accounts = false, bool strict = false);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_close_wallet_params : public serializable_struct {
boost::optional<bool> m_save;
monero_close_wallet_params(bool save = false): m_save(save) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_change_wallet_password_params : public serializable_struct {
boost::optional<std::string> m_old_password;
boost::optional<std::string> m_new_password;
monero_change_wallet_password_params(const std::string& old_password, const std::string& new_password): m_old_password(old_password), m_new_password(new_password) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_set_daemon_params : public serializable_struct {
boost::optional<std::string> m_address;
boost::optional<std::string> m_username;
boost::optional<std::string> m_password;
boost::optional<std::string> m_proxy;
boost::optional<bool> m_trusted;
boost::optional<std::string> m_ssl_support;
boost::optional<ssl_options> m_ssl_options;
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_account_tag_params : public serializable_struct {
std::vector<uint32_t> m_account_indices;
boost::optional<std::string> m_tag;
boost::optional<std::string> m_label;
boost::optional<std::string> m_description;
monero_account_tag_params() {}
monero_account_tag_params(const std::string& tag, const std::string& description = ""): m_tag(tag), m_description(description) { }
monero_account_tag_params(const std::vector<uint32_t>& account_indices): m_account_indices(account_indices) { }
monero_account_tag_params(const std::string& tag, const std::vector<uint32_t>& account_indices): m_tag(tag), m_account_indices(account_indices) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_tx_notes_params : public serializable_struct {
std::vector<std::string> m_tx_hashes;
std::vector<std::string> m_notes;
monero_tx_notes_params(const std::vector<std::string>& tx_hashes): m_tx_hashes(tx_hashes) { }
monero_tx_notes_params(const std::vector<std::string>& tx_hashes, const std::vector<std::string>& notes): m_tx_hashes(tx_hashes), m_notes(notes) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_address_book_entry_params : public serializable_struct {
boost::optional<uint64_t> m_index; // TODO: not boost::optional
boost::optional<bool> m_set_address;
boost::optional<std::string> m_address;
boost::optional<bool> m_set_description;
boost::optional<std::string> m_description;
std::vector<uint64_t> m_entries;
monero_address_book_entry_params(uint64_t index): m_index(index) { }
monero_address_book_entry_params(const std::vector<uint64_t>& entries): m_entries(entries) { }
monero_address_book_entry_params(uint64_t index, bool set_address, const std::string& address, bool set_description, const std::string& description): m_index(index), m_set_address(set_address), m_address(address), m_set_description(set_description), m_description(description) { }
monero_address_book_entry_params(const std::string& address, const std::string& description): m_address(address), m_description(description) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_verify_sign_message_params : public serializable_struct {
boost::optional<std::string> m_data;
boost::optional<std::string> m_address;
boost::optional<std::string> m_signature;
boost::optional<monero_message_signature_type> m_signature_type;
boost::optional<uint32_t> m_account_index;
boost::optional<uint32_t> m_address_index;
monero_verify_sign_message_params(const std::string &data, const std::string &address, const std::string& signature): m_data(data), m_address(address), m_signature(signature) { }
monero_verify_sign_message_params(const std::string &data, monero_message_signature_type signature_type, uint32_t account_index, uint32_t address_index): m_data(data), m_signature_type(signature_type), m_account_index(account_index), m_address_index(address_index) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_check_tx_key_params : public serializable_struct {
boost::optional<std::string> m_tx_hash;
boost::optional<std::string> m_address;
boost::optional<std::string> m_tx_key;
monero_check_tx_key_params(const std::string &tx_hash): m_tx_hash(tx_hash) { }
monero_check_tx_key_params(const std::string &tx_hash, const std::string &tx_key, const std::string &address): m_tx_hash(tx_hash), m_tx_key(tx_key), m_address(address) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_sign_describe_transfer_params : public serializable_struct {
boost::optional<std::string> m_unsigned_txset;
boost::optional<std::string> m_multisig_txset;
boost::optional<bool> m_export_raw;
boost::optional<bool> m_get_tx_keys;
monero_sign_describe_transfer_params() { }
monero_sign_describe_transfer_params(const std::string &unsigned_txset) : m_unsigned_txset(unsigned_txset) { }
monero_sign_describe_transfer_params(const std::string &unsigned_txset, const std::string &multisig_txset) : m_unsigned_txset(unsigned_txset), m_multisig_txset(multisig_txset) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_wallet_relay_tx_params : public serializable_struct {
boost::optional<std::string> m_hex;
boost::optional<std::string> m_signed_tx_hex;
monero_wallet_relay_tx_params() {}
monero_wallet_relay_tx_params(const std::string &hex): m_hex(hex) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_sweep_params : public serializable_struct {
boost::optional<std::string> m_address;
boost::optional<uint32_t> m_account_index;
std::vector<uint32_t> m_subaddr_indices;
boost::optional<std::string> m_key_image;
boost::optional<bool> m_relay;
boost::optional<monero_tx_priority> m_priority;
boost::optional<std::string> m_payment_id;
boost::optional<uint64_t> m_below_amount;
boost::optional<bool> m_get_tx_key;
boost::optional<bool> m_get_tx_keys;
boost::optional<bool> m_get_tx_hex;
boost::optional<bool> m_get_tx_metadata;
monero_sweep_params(bool relay = false): m_relay(relay) { }
monero_sweep_params(const monero_tx_config& config);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_create_edit_subaddress_params : public serializable_struct {
boost::optional<std::string> m_label;
boost::optional<uint32_t> m_account_index;
boost::optional<uint32_t> m_subaddress_index;
monero_create_edit_subaddress_params(uint32_t account_idx, const std::string& label): m_account_index(account_idx), m_label(label) { }
monero_create_edit_subaddress_params(uint32_t account_idx, uint32_t subaddress_idx, const std::string& label): m_account_index(account_idx), m_subaddress_index(subaddress_idx), m_label(label) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_wallet_data_params : public serializable_struct {
boost::optional<bool> m_all;
std::vector<std::shared_ptr<monero_key_image>> m_key_images;
boost::optional<uint64_t> m_offset;
boost::optional<std::string> m_outputs_hex;
monero_wallet_data_params(const std::vector<std::shared_ptr<monero_key_image>> &key_images, uint64_t offset): m_key_images(key_images), m_offset(offset) { };
monero_wallet_data_params(bool all): m_all(all) { }
monero_wallet_data_params(const std::string& outputs_hex): m_outputs_hex(outputs_hex) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_create_open_wallet_params : public serializable_struct {
boost::optional<std::string> m_filename;
boost::optional<std::string> m_password;
boost::optional<std::string> m_language;
boost::optional<std::string> m_seed;
boost::optional<std::string> m_seed_offset;
boost::optional<uint64_t> m_restore_height;
boost::optional<bool> m_autosave_current;
boost::optional<bool> m_enable_multisig_experimental;
boost::optional<std::string> m_address;
boost::optional<std::string> m_view_key;
boost::optional<std::string> m_spend_key;
monero_create_open_wallet_params(const boost::optional<std::string>& filename, const boost::optional<std::string> &password): m_filename(filename), m_password(password), m_autosave_current(false) { }
monero_create_open_wallet_params(const boost::optional<std::string>& filename, const boost::optional<std::string> &password, const boost::optional<std::string> &language): m_filename(filename), m_password(password), m_language(language), m_autosave_current(false) { }
monero_create_open_wallet_params(const boost::optional<std::string>& filename, const boost::optional<std::string> &password, const boost::optional<std::string> &seed, const boost::optional<std::string> &seed_offset, const boost::optional<uint64_t> &restore_height, const boost::optional<std::string> &language, const boost::optional<bool> &autosave_current, const boost::optional<bool> &enable_multisig_experimental): m_filename(filename), m_password(password), m_seed(seed), m_seed_offset(seed_offset), m_restore_height(restore_height), m_language(language), m_autosave_current(autosave_current), m_enable_multisig_experimental(enable_multisig_experimental) { }
monero_create_open_wallet_params(const boost::optional<std::string>& filename, const boost::optional<std::string> &password, const boost::optional<std::string> &address, const boost::optional<std::string> &view_key, const boost::optional<std::string> &spend_key, const boost::optional<uint64_t> &restore_height, const boost::optional<bool> &autosave_current): m_filename(filename), m_password(password), m_address(address), m_view_key(view_key), m_spend_key(spend_key), m_restore_height(restore_height), m_autosave_current(autosave_current) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_reserve_proof_params : public serializable_struct {
boost::optional<bool> m_all;
boost::optional<std::string> m_message;
boost::optional<std::string> m_tx_hash;
boost::optional<uint32_t> m_account_index;
boost::optional<uint64_t> m_amount;
boost::optional<std::string> m_address;
boost::optional<std::string> m_signature;
monero_reserve_proof_params(const std::string &message, bool all = true): m_all(all), m_message(message) { }
monero_reserve_proof_params(const std::string &address, const std::string &message, const std::string &signature): m_address(address), m_message(message), m_signature(signature) { }
monero_reserve_proof_params(const std::string &tx_hash, const std::string &address, const std::string &message, const std::string &signature): m_tx_hash(tx_hash), m_address(address), m_message(message), m_signature(signature) { }
monero_reserve_proof_params(const std::string &tx_hash, const std::string &message): m_tx_hash(tx_hash), m_message(message) { }
monero_reserve_proof_params(uint32_t account_index, uint64_t amount, const std::string &message): m_account_index(account_index), m_amount(amount), m_message(message) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_wallet_refresh_params : public serializable_struct {
boost::optional<bool> m_enable;
boost::optional<uint64_t> m_period;
boost::optional<uint64_t> m_start_height;
monero_wallet_refresh_params() { }
monero_wallet_refresh_params(bool enable): m_enable(enable) { }
monero_wallet_refresh_params(bool enable, uint64_t period): m_enable(enable), m_period(period) { }
monero_wallet_refresh_params(uint64_t start_height): m_start_height(start_height) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_transfer_params : public serializable_struct {
std::vector<uint32_t> m_subtract_fee_from_outputs;
boost::optional<uint32_t> m_account_index;
std::vector<uint32_t> m_subaddress_indices;
boost::optional<std::string> m_payment_id;
boost::optional<bool> m_do_not_relay;
boost::optional<int> m_priority;
boost::optional<bool> m_get_tx_hex;
boost::optional<bool> m_get_tx_metadata;
boost::optional<bool> m_get_tx_keys;
boost::optional<bool> m_get_tx_key;
std::vector<std::shared_ptr<monero_destination>> m_destinations;
monero_transfer_params(const monero_tx_config &config);
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_transfers_params : public serializable_struct {
boost::optional<bool> m_in;
boost::optional<bool> m_out;
boost::optional<bool> m_pool;
boost::optional<bool> m_pending;
boost::optional<bool> m_failed;
boost::optional<uint64_t> m_min_height;
boost::optional<uint64_t> m_max_height;
boost::optional<bool> m_all_accounts;
boost::optional<uint32_t> m_account_index;
std::vector<uint32_t> m_subaddr_indices;
bool filter_by_height() const { return m_min_height != boost::none || m_max_height != boost::none; }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
struct monero_get_incoming_transfers_params : public serializable_struct {
boost::optional<std::string> m_transfer_type;
boost::optional<bool> m_verbose;
boost::optional<uint32_t> m_account_index;
std::vector<uint32_t> m_subaddr_indices;
monero_get_incoming_transfers_params(const std::string& transfer_type, bool verbose = true): m_transfer_type(transfer_type), m_verbose(verbose) { }
rapidjson::Value to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const override;
};
// ------------------------------ JSON-RPC Response ---------------------------------
struct monero_multisig_response {
boost::optional<int> m_num_outputs;
boost::optional<std::string> m_multisig_info;
std::vector<std::string> m_tx_hashes;
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_multisig_response>& response);
};
struct monero_get_balance_response {
boost::optional<uint64_t> m_balance;
boost::optional<uint64_t> m_unlocked_balance;
boost::optional<bool> m_multisig_import_needed;
boost::optional<uint64_t> m_time_to_unlock;
boost::optional<uint64_t> m_blocks_to_unlock;
std::vector<std::shared_ptr<monero_subaddress>> m_per_subaddress;
monero_get_balance_response(): m_balance(0), m_unlocked_balance(0) { }
static void from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr<monero_get_balance_response>& response);
};
// ------------------------------ RPC Deserialization ---------------------------------
void deserialize_subaddress(const boost::property_tree::ptree& node, const std::shared_ptr<monero_subaddress>& subaddress);
void deserialize_subaddresses(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_subaddress>>& subaddresses);
void deserialize_account(const boost::property_tree::ptree& node, monero_account& account);
void deserialize_accounts(const boost::property_tree::ptree& node, std::vector<monero_account>& accounts);
void init_sent_tx(const monero_tx_config &config, std::shared_ptr<monero_tx_wallet> &tx, bool copy_destinations);
void deserialize_tx_with_transfer_and_merge(const boost::property_tree::ptree& node, std::map<std::string, std::shared_ptr<monero_tx_wallet>>& tx_map, std::map<uint64_t, std::shared_ptr<monero_block>>& block_map);
void deserialize_tx_with_output_and_merge(const boost::property_tree::ptree& node, std::map<std::string, std::shared_ptr<monero_tx_wallet>>& tx_map, std::map<uint64_t, std::shared_ptr<monero_block>>& block_map);
void deserialize_tx_set(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_set>& set);
void deserialize_tx_set(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_set>& set, const std::shared_ptr<monero_tx_wallet> &tx, bool is_outgoing, const monero_tx_config &config);
void deserialize_sent_tx_set(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_set>& set);
void deserialize_sent_tx_set(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_set>& set, std::vector<std::shared_ptr<monero_tx_wallet>> &txs, const boost::optional<monero_tx_config> &conf);
void deserialize_described_tx_set(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_set>& set);
void deserialize_submitted_tx_hashes(const boost::property_tree::ptree& node, std::vector<std::string>& tx_hashes);
void deserialize_relayed_tx_hash(const boost::property_tree::ptree& node, std::vector<std::string>& tx_hashes);
int deserialize_num_created_txs(const boost::property_tree::ptree& node, bool can_split);
std::string deserialize_tx_key(const boost::property_tree::ptree& node);
void deserialize_tx_notes(const boost::property_tree::ptree& node, std::vector<std::string>& tx_notes);
void deserialize_integrated_address(const boost::property_tree::ptree& node, monero_integrated_address& subaddress);
void deserialize_key_image_export_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_key_image_export_result>& result);
void deserialize_key_image_import_result(const boost::property_tree::ptree& node, const std::shared_ptr<monero_key_image_import_result>& result);
void deserialize_message_signature_result(const boost::property_tree::ptree& node, monero_message_signature_result& result);
void deserialize_check_tx(const boost::property_tree::ptree& node, const std::shared_ptr<monero_check_tx>& check);
void deserialize_check_reserve(const boost::property_tree::ptree& node, const std::shared_ptr<monero_check_reserve>& check);
void deserialize_multisig_info(const boost::property_tree::ptree& node, monero_multisig_info& info);
void deserialize_multisig_init_result(const boost::property_tree::ptree& node, monero_multisig_init_result& res);
void deserialize_multisig_sign_result(const boost::property_tree::ptree& node, monero_multisig_sign_result& res);
void deserialize_address_book_entries(const boost::property_tree::ptree& node, std::vector<monero_address_book_entry>& entries);
void deserialize_seed_languages(const boost::property_tree::ptree& node, std::vector<std::string>& languages);
uint64_t deserialize_address_book_index(const boost::property_tree::ptree& node);
void deserialize_account_tags(const boost::property_tree::ptree& node, std::vector<std::shared_ptr<monero_account_tag>>& account_tags);
std::string deserialize_signature(const boost::property_tree::ptree& node);
std::string deserialize_exported_outputs(const boost::property_tree::ptree& node);
uint64_t deserialize_block_height(const boost::property_tree::ptree& node);
int deserialize_num_imported_outputs(const boost::property_tree::ptree& node);
bool deserialize_frozen_output_info(const boost::property_tree::ptree& node);
monero_tx_priority deserialize_tx_priority(const boost::property_tree::ptree& node);
std::string deserialize_payment_uri(const boost::property_tree::ptree& node);
void deserialize_payment_uri(const boost::property_tree::ptree& node, const std::shared_ptr<monero_tx_config>& tx_config);
void deserialize_sync_result(const boost::property_tree::ptree& node, monero_sync_result& sync_result);
}
Binary file not shown.
+290
View File
@@ -0,0 +1,290 @@
import Foundation
import os
/// On-device verification against vectors generated from the reference
/// implementation (xrpl.js + ripple-keypairs + ripple-binary-codec). Runs only
/// when the app is launched with `-koinsSelfTest`.
enum SelfTest {
private static let log = OSLog(subsystem: "com.koins.app", category: "selftest")
/// Writes to both stdout and the unified log so results are visible however
/// the app was launched.
private static func emit(_ line: String) {
print(line)
os_log("%{public}@", log: log, type: .default, line)
}
static func runIfRequested() {
if ProcessInfo.processInfo.arguments.contains("-koinsSelfTest") {
print("KOINS_SELFTEST begin")
run()
print("KOINS_SELFTEST end")
}
if ProcessInfo.processInfo.arguments.contains("-koinsSelfTestNetwork") {
runNetwork()
}
}
/// End-to-end check of the live network paths (XRP RPC + native Monero
/// wallet). Triggered with `-koinsSelfTestNetwork`; cleans up after itself.
static func runNetwork() {
// Detached so the test does not inherit the main actor: the UI thread must
// stay free, otherwise the launch-time call would deadlock.
Task.detached(priority: .userInitiated) {
emit("KOINS_NET begin")
// The test creates temporary wallets, so snapshot the real ones and put
// them back afterwards instead of wiping them.
let backup = (xrp: WalletStore.wallet(.xrp),
xmr: WalletStore.wallet(.xmr),
xrpSecret: WalletStore.secret(.xrp),
xmrSecret: WalletStore.secret(.xmr))
func restoreWallets() {
if let xrp = backup.xrp { WalletStore.saveWallet(xrp, chain: .xrp) } else { WalletStore.removeWallet(.xrp) }
if let secret = backup.xrpSecret { WalletStore.saveSecret(secret, chain: .xrp) }
if let xmr = backup.xmr { WalletStore.saveWallet(xmr, chain: .xmr) } else { WalletStore.removeWallet(.xmr) }
if let secret = backup.xmrSecret { WalletStore.saveSecret(secret, chain: .xmr) }
emit("KOINS_NET restored saved wallets")
}
do {
let (record, phrase) = try XRPWalletService.shared.createWallet(name: "koins-net-selftest")
emit("KOINS_NET xrp address \(record.address) phraseWords \(phrase.split(separator: " ").count)")
let balance = try await XRPWalletService.shared.balance(address: record.address)
emit("KOINS_NET xrp funded=\(balance.funded) balance=\(balance.total)")
let history = try await XRPWalletService.shared.transactions(address: record.address)
emit("KOINS_NET xrp transactions=\(history.count)")
// A known funded account, to exercise the funded-balance path.
for reference in ["rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "rHR6xtDW7BxeQN8VB8R9HzXzyMeS42n7C"] {
let reference = try await XRPWalletService.shared.balance(address: reference)
emit("KOINS_NET xrp reference funded=\(reference.funded) balance=\(reference.total)")
let referenceHistory = try await XRPWalletService.shared.transactions(address: "rHR6xtDW7BxeQN8VB8R9HzXzyMeS42n7C")
emit("KOINS_NET xrp reference transactions=\(referenceHistory.count)")
}
} catch {
emit("KOINS_NET xrp FAIL \(error)")
}
do {
let created = try await MoneroService.shared.createWallet(name: "koins-net-selftest")
let words = created.mnemonic.split(separator: " ").count
emit("KOINS_NET xmr address \(created.address) mnemonicWords \(words)")
let started = try await MoneroService.shared.startSync()
emit("KOINS_NET xmr startedSync \(started)")
for attempt in 1...6 {
try? await Task.sleep(nanoseconds: 5_000_000_000)
let status = try await MoneroService.shared.syncStatus()
emit("KOINS_NET xmr status \(attempt) height=\(status.height) daemon=\(status.daemonHeight) connected=\(status.connected) syncing=\(status.syncing)")
}
let balance = try await MoneroService.shared.balance()
emit("KOINS_NET xmr balance \(balance.total) unlocked \(balance.unlocked)")
await MoneroService.shared.stopSync()
await MoneroService.shared.removeWallet(name: "koins-net-selftest")
} catch {
emit("KOINS_NET xmr FAIL \(error)")
}
restoreWallets()
emit("KOINS_NET end")
}
}
static func run() {
var failures = 0
func expect(_ name: String, _ actual: String, _ wanted: String) {
let ok = actual.lowercased() == wanted.lowercased()
if !ok { failures += 1 }
let line = "KOINS_SELFTEST \(name): \(ok ? "PASS" : "FAIL") \(actual)"
print(line)
os_log("%{public}@", log: log, type: .default, line)
}
expect("ripemd160", Hex.encode(RIPEMD160.hash(Array("abc".utf8))),
"8eb208f7e05d987a9b044a8e98c6b087f15a0bfc")
// ed25519 vector
let edSeed = "sEdSPx8CLbFUna3DJdvJ74aQPLdemqu"
let edAddress = "rEhh6f9rj5UUBhFzGGaxS5zYU2CCqKFXBC"
let edPublicKey = "EDF9096653463E3A0B79A67E69D9E66AB83D93D4E867335ADE5DAFE8467F4F1734"
if let keypair = try? XRPKey.keypair(fromSeed: edSeed) {
expect("ed25519 pubkey", Hex.encode(keypair.publicKey), edPublicKey)
expect("ed25519 address", keypair.address, edAddress)
} else {
failures += 1
print("KOINS_SELFTEST ed25519: FAIL keypair threw")
}
// secp256k1 vector: exercises the OpenSSL-backed derivation on device.
let secpSeed = "spFbtYoPd7Hg7AD9XZn7CBwUKC5Qe"
let secpPublicKey = "0322DC9BB14903F71186BC46B0107D55A069D23CD97BBD205491A992EC68FA0ABE"
let secpAddress = "rpsRYc8DbXzfVN32w3hZjUtyuF1K89hu47"
do {
let keypair = try XRPKey.keypair(fromSeed: secpSeed)
expect("secp256k1 algorithm", keypair.algorithm.rawValue, "secp256k1")
expect("secp256k1 pubkey", Hex.encode(keypair.publicKey), secpPublicKey)
expect("secp256k1 address", keypair.address, secpAddress)
} catch {
failures += 1
print("KOINS_SELFTEST secp256k1: FAIL \(error)")
}
// Serialization + hashing reference vector.
let signingHex = "53545800120000220000000024000030392E00000309201B05E69F3B61400000000016E36068400000000000000C7321EDF9096653463E3A0B79A67E69D9E66AB83D93D4E867335ADE5DAFE8467F4F173481149A26172134BFDA9708A73305AF2776A26DB7B4F5831492891F44F454D156327AA24F4C8BB72540E31BF1"
let referenceSignature = "F560672EA3103D2E43179302CD66ACF563A4EDFBA90ADE24E378B743135FD989EE132FA4B703021C2DD1979E770A6CEBC99556EA330AF9E6E7186673F837CB0D"
let expectedBlob = "120000220000000024000030392E00000309201B05E69F3B61400000000016E36068400000000000000C7321EDF9096653463E3A0B79A67E69D9E66AB83D93D4E867335ADE5DAFE8467F4F17347440F560672EA3103D2E43179302CD66ACF563A4EDFBA90ADE24E378B743135FD989EE132FA4B703021C2DD1979E770A6CEBC99556EA330AF9E6E7186673F837CB0D81149A26172134BFDA9708A73305AF2776A26DB7B4F5831492891F44F454D156327AA24F4C8BB72540E31BF1"
let expectedHash = "F853B5391CD6317EA370830EEA3C047AC8D21E9FA00E4116D94C48E360B0434F"
var payment = XRPPayment(account: edAddress,
destination: "rNMovRR3WPbFLVaSbETCCR71XsqyxhJ9P6",
amountDrops: 1_500_000,
feeDrops: 12,
sequence: 12345,
lastLedgerSequence: 99000123,
destinationTag: 777,
signingPublicKey: Hex.decode(edPublicKey) ?? [],
signature: Hex.decode(referenceSignature))
if let payload = try? payment.signingPayload() {
expect("signing payload", Hex.encode(payload), signingHex)
} else {
failures += 1
}
if let blob = try? payment.txBlob() {
expect("tx blob", Hex.encode(blob), expectedBlob)
if let hash = try? payment.transactionHash() {
expect("tx hash", Hex.encode(hash), expectedHash)
} else {
failures += 1
}
} else {
failures += 1
}
// BIP-39 / BIP-32 (m/44'/144'/0'/0/0): the recovery phrase used for new XRP
// wallets. Vectors come from @scure/bip39 + @scure/bip32 + ripple-keypairs.
let zeroMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
expect("bip39 encode", BIP39.mnemonic(from: [UInt8](repeating: 0, count: 16)), zeroMnemonic)
expect("bip39 entropy", Hex.encode(BIP39.entropy(from: zeroMnemonic) ?? []), String(repeating: "00", count: 16))
expect("bip39 rejects bad checksum",
BIP39.entropy(from: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon") == nil ? "rejected" : "accepted",
"rejected")
// BIP-32 intermediate values, to isolate derivation problems.
if let master = BIP32.masterKey(fromSeed: BIP39.seed(from: zeroMnemonic)) {
expect("bip32 master key", Hex.encode(master.privateKey),
"1837c1be8e2995ec11cda2b066151be2cfb48adf9e47b151d46adab3a21cdf67")
expect("bip32 master chain code", Hex.encode(master.chainCode),
"7923408dadd3c7b56eed15567707ae5e5dca089de972e07f3b860450e2a3b70e")
let H = BIP32.hardenedBit
for (level, path, expected) in [
(1, [H | 44], "cd46049fb82a4fccbd967c5234e61152f4df87d01d1ef833420814c67ee4b7ea"),
(2, [H | 44, H | 144], "a12250f1f38fb41b62f0f1a4ec9d3555d24788843cb30e6d98a777fe540adc1d"),
(3, [H | 44, H | 144, H | 0], "54bac451906ecd52648a49126a2606c9177761f14d31dda079c0f768ac3501fd"),
(4, [H | 44, H | 144, H | 0, 0], "e582ce1724e93eebe3f38f1d42c01620b66d136946fec450dfe71b62e65de54f"),
(5, BIP32.xrpPath, "90802a50aa84efb6cdb225f17c27616ea94048c179142fecf03f4712a07ea7a4"),
] {
if let child = BIP32.derive(path, from: master) {
expect("bip32 level \(level)", Hex.encode(child.privateKey), expected)
} else {
failures += 1
print("KOINS_SELFTEST bip32 level \(level): FAIL derivation returned nil")
}
}
} else {
failures += 1
print("KOINS_SELFTEST bip32 master: FAIL")
}
let mnemonicCases: [(String, String, String)] = [
(zeroMnemonic,
"031d68bc1a142e6766b2bdfb006ccfe135ef2e0e2e94abb5cf5c9ab6104776fbae",
"rHsMGQEkVNJmpGWs8XUBoTBiAAbwxZN5v3"),
("legal winner thank year wave sausage worth useful legal winner thank yellow",
"0283e0926eb0ab7bb8f5c0b5637d9e2526bdc398763af79cd904f013178961692d",
"rpaLPza5oxhJ7V8nFDwPm9F7kmZ16uc8do"),
]
for (position, vector) in mnemonicCases.enumerated() {
do {
let keypair = try XRPKey.keypair(fromMnemonic: vector.0)
expect("bip44 pubkey #\(position)", Hex.encode(keypair.publicKey), vector.1)
expect("bip44 address #\(position)", keypair.address, vector.2)
} catch {
failures += 1
print("KOINS_SELFTEST bip44 #\(position): FAIL \(error)")
}
}
// secp256k1 signing must produce a low-S DER signature.
if let keypair = try? XRPKey.keypair(fromSeed: secpSeed),
let message = Hex.decode(signingHex),
let signature = try? XRPKey.sign(message: message, keypair: keypair) {
let isDer = signature.count > 8 && signature[0] == 0x30
expect("secp256k1 signature is DER", isDer ? "yes" : "no", "yes")
if let order = BigNumber.fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"),
let half = order.dividedByTwo(),
let s = DER.signatureScalar(signature) {
expect("secp256k1 signature is low-S", s.lessThanOrEqual(half) ? "yes" : "no", "yes")
}
print("KOINS_SELFTEST secp256k1 signature len: \(signature.count)")
} else {
failures += 1
print("KOINS_SELFTEST secp256k1 sign: FAIL")
}
let summary = "KOINS_SELFTEST summary: \(failures == 0 ? "ALL PASS" : "\(failures) FAILURE(S)")"
print(summary)
os_log("%{public}@", log: log, type: .default, summary)
}
}
/// Minimal big-endian integer helper used only for the low-S check.
struct BigNumber {
let bytes: [UInt8]
static func fromHex(_ hex: String) -> BigNumber? {
Hex.decode(hex).map { BigNumber(bytes: $0) }
}
func dividedByTwo() -> BigNumber? {
var result: [UInt8] = []
var carry: UInt16 = 0
for byte in bytes {
let value = carry * 256 + UInt16(byte)
result.append(UInt8(value / 2))
carry = value % 2
}
return BigNumber(bytes: result)
}
func lessThanOrEqual(_ other: BigNumber) -> Bool {
let left = trimmed()
let right = other.trimmed()
if left.count != right.count { return left.count < right.count }
for (a, b) in zip(left, right) where a != b { return a < b }
return true
}
private func trimmed() -> [UInt8] {
var value = bytes
while value.first == 0 && value.count > 1 { value.removeFirst() }
return value
}
}
enum DER {
/// Extracts the `s` component from a DER-encoded ECDSA signature.
static func signatureScalar(_ der: [UInt8]) -> BigNumber? {
guard der.count > 8, der[0] == 0x30 else { return nil }
var index = 2
guard der[index] == 0x02 else { return nil }
let rLength = Int(der[index + 1])
index += 2 + rLength
guard index + 2 <= der.count, der[index] == 0x02 else { return nil }
let sLength = Int(der[index + 1])
let start = index + 2
guard start + sLength <= der.count else { return nil }
return BigNumber(bytes: Array(der[start..<(start + sLength)]))
}
}
+92
View File
@@ -0,0 +1,92 @@
import CryptoKit
import Foundation
/// BIP-32 key derivation for the XRP account path `m/44'/144'/0'/0/0`. The
/// first three steps are hardened, the last two are not, so both cases are
/// supported (hardened steps sign with the parent private key, normal steps
/// with the parent public key).
enum BIP32 {
static let hardenedBit: UInt32 = 0x8000_0000
/// Standard XRP derivation path: 44' / 144' / 0' / 0 / 0.
static let xrpPath: [UInt32] = [
hardenedBit | 44,
hardenedBit | 144,
hardenedBit | 0,
0,
0,
]
struct Key {
var privateKey: [UInt8]
var chainCode: [UInt8]
var publicKey: [UInt8]
}
static func masterKey(fromSeed seed: [UInt8]) -> Key? {
let derived = hmac(key: Array("Bitcoin seed".utf8), data: seed)
guard derived.count == 64 else { return nil }
let privateKey = Array(derived[0..<32])
guard isValidScalar(privateKey),
let publicKey = Secp256k1Backend.publicKey?(Data(privateKey)) else { return nil }
return Key(privateKey: privateKey,
chainCode: Array(derived[32..<64]),
publicKey: Array(publicKey))
}
static func derive(_ path: [UInt32], from key: Key) -> Key? {
var current = key
for index in path {
guard let next = deriveChild(current, index: index) else { return nil }
current = next
}
return current
}
private static func deriveChild(_ parent: Key, index: UInt32) -> Key? {
let isHardened = index & hardenedBit != 0
var data: [UInt8] = []
if isHardened {
data.append(0x00)
data += parent.privateKey
} else {
data += parent.publicKey
}
data += [
UInt8((index >> 24) & 0xff),
UInt8((index >> 16) & 0xff),
UInt8((index >> 8) & 0xff),
UInt8(index & 0xff),
]
let derived = hmac(key: parent.chainCode, data: data)
guard derived.count == 64 else { return nil }
let left = Array(derived[0..<32])
guard isValidScalar(left),
let sum = Secp256k1Backend.addScalar?(Data(left), Data(parent.privateKey)) else { return nil }
let privateKey = Array(sum)
// The child public key is the same whether it is computed from the child
// private key or by point addition, so derive it directly.
guard isValidScalar(privateKey),
let publicKey = Secp256k1Backend.publicKey?(Data(privateKey)) else { return nil }
return Key(privateKey: privateKey,
chainCode: Array(derived[32..<64]),
publicKey: Array(publicKey))
}
/// A key is usable when 0 < key < the secp256k1 group order.
private static func isValidScalar(_ bytes: [UInt8]) -> Bool {
guard bytes.count == 32 else { return false }
let orderHex = "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"
guard let order = Hex.decode(orderHex) else { return false }
if bytes.allSatisfy({ $0 == 0 }) { return false }
for (left, right) in zip(bytes, order) where left != right { return left < right }
return false
}
private static func hmac(key: [UInt8], data: [UInt8]) -> [UInt8] {
Array(HMAC<SHA512>.authenticationCode(for: Data(data), using: SymmetricKey(data: Data(key))))
}
}
+94
View File
@@ -0,0 +1,94 @@
import CryptoKit
import Foundation
/// BIP-39 mnemonics. The XRP wallet uses a 12 word phrase as its recovery
/// secret, matching what hardware and mobile wallets expect.
enum BIP39 {
static let wordCount = 12
static let entropyBytes = 16
private static let checksumBits = entropyBytes / 4
private static let index: [String: Int] = {
var map: [String: Int] = [:]
for (position, word) in BIP39Wordlist.words.enumerated() { map[word] = position }
return map
}()
static func generateMnemonic() -> String {
var entropy = [UInt8](repeating: 0, count: entropyBytes)
for position in entropy.indices { entropy[position] = UInt8.random(in: 0...255) }
return mnemonic(from: entropy)
}
/// 16 bytes of entropy plus its 4 bit SHA-256 checksum, as 12 words.
static func mnemonic(from entropy: [UInt8]) -> String {
precondition(entropy.count == entropyBytes, "BIP-39 entropy must be 16 bytes")
var bits = entropy.flatMap { byte in (0..<8).map { (byte >> (7 - $0)) & 1 } }
let checksum = XRPHash.sha256(entropy)
bits += (0..<checksumBits).map { (checksum[0] >> (7 - $0)) & 1 }
var words: [String] = []
for start in stride(from: 0, to: bits.count, by: 11) {
var value = 0
for offset in 0..<11 { value = (value << 1) | Int(bits[start + offset]) }
words.append(BIP39Wordlist.words[value])
}
return words.joined(separator: " ")
}
static func entropy(from mnemonic: String) -> [UInt8]? {
let words = normalize(mnemonic).split(separator: " ").map(String.init)
guard words.count == wordCount else { return nil }
var bits: [Int] = []
for word in words {
guard let position = index[word] else { return nil }
bits += (0..<11).map { (position >> (10 - $0)) & 1 }
}
var entropy: [UInt8] = []
for start in stride(from: 0, to: entropyBytes * 8, by: 8) {
var value: UInt8 = 0
for offset in 0..<8 { value = (value << 1) | UInt8(bits[start + offset]) }
entropy.append(value)
}
let checksum = XRPHash.sha256(entropy)
for offset in 0..<checksumBits {
let expected = (checksum[0] >> (7 - offset)) & 1
guard bits[entropyBytes * 8 + offset] == expected else { return nil }
}
return entropy
}
/// 64 byte BIP-39 seed (PBKDF2-HMAC-SHA512, empty passphrase).
static func seed(from mnemonic: String) -> [UInt8] {
let password = Array(normalize(mnemonic).utf8)
let salt = Array("mnemonic".utf8)
// Derived length equals the hash length, so a single PBKDF2 block is needed.
let block = salt + [0, 0, 0, 1]
var previous = hmacSHA512(key: password, data: block)
var result = previous
for _ in 1..<2_048 {
previous = hmacSHA512(key: password, data: previous)
for position in result.indices { result[position] ^= previous[position] }
}
return result
}
static func isMnemonic(_ secret: String) -> Bool {
normalize(secret).split(separator: " ").count == wordCount
}
private static func normalize(_ text: String) -> String {
text.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.precomposedStringWithCompatibilityMapping
}
private static func hmacSHA512(key: [UInt8], data: [UInt8]) -> [UInt8] {
let authentication = HMAC<SHA512>.authenticationCode(for: Data(data),
using: SymmetricKey(data: Data(key)))
return Array(authentication)
}
}
+263
View File
@@ -0,0 +1,263 @@
import Foundation
/// BIP-39 English word list (2048 words), in the canonical order.
enum BIP39Wordlist {
static let words: [String] = [
"abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract",
"absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid",
"acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual",
"adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance",
"advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent",
"agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album",
"alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone",
"alpha", "already", "also", "alter", "always", "amateur", "amazing", "among",
"amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry",
"animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique",
"anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april",
"arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor",
"army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact",
"artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume",
"asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction",
"audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado",
"avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis",
"baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball",
"bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base",
"basic", "basket", "battle", "beach", "bean", "beauty", "because", "become",
"beef", "before", "begin", "behave", "behind", "believe", "below", "belt",
"bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle",
"bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black",
"blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood",
"blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body",
"boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring",
"borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain",
"brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief",
"bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother",
"brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb",
"bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus",
"business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable",
"cactus", "cage", "cake", "call", "calm", "camera", "camp", "can",
"canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable",
"capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry",
"cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog",
"catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling",
"celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk",
"champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap",
"check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child",
"chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar",
"cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify",
"claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff",
"climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud",
"clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut",
"code", "coffee", "coil", "coin", "collect", "color", "column", "combine",
"come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm",
"congress", "connect", "consider", "control", "convince", "cook", "cool", "copper",
"copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch",
"country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle",
"craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream",
"credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop",
"cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch",
"crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious",
"current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad",
"damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn",
"day", "deal", "debate", "debris", "decade", "december", "decide", "decline",
"decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay",
"deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend",
"deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk",
"despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram",
"dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital",
"dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover",
"disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide",
"divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain",
"donate", "donkey", "donor", "door", "dose", "double", "dove", "draft",
"dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill",
"drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb",
"dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager",
"eagle", "early", "earn", "earth", "easily", "east", "easy", "echo",
"ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight",
"either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator",
"elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ",
"empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy",
"energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough",
"enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode",
"equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt",
"escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil",
"evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude",
"excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit",
"exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend",
"extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint",
"faith", "fall", "false", "fame", "family", "famous", "fan", "fancy",
"fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault",
"favorite", "feature", "february", "federal", "fee", "feed", "feel", "female",
"fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field",
"figure", "file", "film", "filter", "final", "find", "fine", "finger",
"finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness",
"fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight",
"flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly",
"foam", "focus", "fog", "foil", "fold", "follow", "food", "foot",
"force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil",
"foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend",
"fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel",
"fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy",
"gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment",
"gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius",
"genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle",
"ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass",
"glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue",
"goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip",
"govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass",
"gravity", "great", "green", "grid", "grief", "grit", "grocery", "group",
"grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun",
"gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy",
"harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard",
"head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet",
"help", "hen", "hero", "hidden", "high", "hill", "hint", "hip",
"hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow",
"home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital",
"host", "hotel", "hour", "hover", "hub", "huge", "human", "humble",
"humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband",
"hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill",
"illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose",
"improve", "impulse", "inch", "include", "income", "increase", "index", "indicate",
"indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial",
"inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane",
"insect", "inside", "inspire", "install", "intact", "interest", "into", "invest",
"invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory",
"jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel",
"job", "join", "joke", "journey", "joy", "judge", "juice", "jump",
"jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup",
"key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit",
"kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know",
"lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language",
"laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law",
"lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave",
"lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend",
"length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty",
"library", "license", "life", "lift", "light", "like", "limb", "limit",
"link", "lion", "liquid", "list", "little", "live", "lizard", "load",
"loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop",
"lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber",
"lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet",
"maid", "mail", "main", "major", "make", "mammal", "man", "manage",
"mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin",
"marine", "market", "marriage", "mask", "mass", "master", "match", "material",
"math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure",
"meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory",
"mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message",
"metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind",
"minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake",
"mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment",
"monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning",
"mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie",
"much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music",
"must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin",
"narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative",
"neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral",
"never", "news", "next", "nice", "night", "noble", "noise", "nominee",
"noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice",
"novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey",
"object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean",
"october", "odor", "off", "offer", "office", "often", "oil", "okay",
"old", "olive", "olympic", "omit", "once", "one", "onion", "online",
"only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit",
"orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich",
"other", "outdoor", "outer", "output", "outside", "oval", "oven", "over",
"own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page",
"pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper",
"parade", "parent", "park", "parrot", "party", "pass", "patch", "path",
"patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut",
"pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper",
"perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical",
"piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot",
"pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet",
"plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge",
"poem", "poet", "point", "polar", "pole", "police", "pond", "pony",
"pool", "popular", "portion", "position", "possible", "post", "potato", "pottery",
"poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare",
"present", "pretty", "prevent", "price", "pride", "primary", "print", "priority",
"prison", "private", "prize", "problem", "process", "produce", "profit", "program",
"project", "promote", "proof", "property", "prosper", "protect", "proud", "provide",
"public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil",
"puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle",
"pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz",
"quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail",
"rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid",
"rare", "rate", "rather", "raven", "raw", "razor", "ready", "real",
"reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle",
"reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject",
"relax", "release", "relief", "rely", "remain", "remember", "remind", "remove",
"render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report",
"require", "rescue", "resemble", "resist", "resource", "response", "result", "retire",
"retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib",
"ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid",
"ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road",
"roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room",
"rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude",
"rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness",
"safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same",
"sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say",
"scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science",
"scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea",
"search", "season", "seat", "second", "secret", "section", "security", "seed",
"seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence",
"series", "service", "session", "settle", "setup", "seven", "shadow", "shaft",
"shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine",
"ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder",
"shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side",
"siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar",
"simple", "since", "sing", "siren", "sister", "situate", "six", "size",
"skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab",
"slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan",
"slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth",
"snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social",
"sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve",
"someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup",
"source", "south", "space", "spare", "spatial", "spawn", "speak", "special",
"speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin",
"spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray",
"spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium",
"staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay",
"steak", "steel", "stem", "step", "stereo", "stick", "still", "sting",
"stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street",
"strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject",
"submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest",
"suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme",
"sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain",
"swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim",
"swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table",
"tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target",
"task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten",
"tenant", "tennis", "tent", "term", "test", "text", "thank", "that",
"theme", "then", "theory", "there", "they", "thing", "this", "thought",
"three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger",
"tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title",
"toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token",
"tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top",
"topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist",
"toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic",
"train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree",
"trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy",
"trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try",
"tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle",
"twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical",
"ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo",
"unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown",
"unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon",
"upper", "upset", "urban", "urge", "usage", "use", "used", "useful",
"useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley",
"valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle",
"velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very",
"vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view",
"village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual",
"vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote",
"voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want",
"warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave",
"way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding",
"weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat",
"wheel", "when", "where", "whip", "whisper", "wide", "width", "wife",
"wild", "will", "win", "window", "wine", "wing", "wink", "winner",
"winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman",
"wonder", "wood", "wool", "word", "work", "world", "worry", "worth",
"wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year",
"yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo",
]
}
+76
View File
@@ -0,0 +1,76 @@
import Foundation
/// Base58 with the XRP Ledger alphabet and Base58Check helpers.
enum XRPBase58 {
static let alphabet: [Character] = Array("rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz")
private static let valueOf: [Character: Int] = {
var map: [Character: Int] = [:]
for (index, character) in alphabet.enumerated() { map[character] = index }
return map
}()
static func encode(_ bytes: [UInt8]) -> String {
var zeros = 0
for byte in bytes {
if byte == 0 { zeros += 1 } else { break }
}
var digits: [Int] = []
for byte in bytes[zeros...] {
var carry = Int(byte)
for index in digits.indices {
carry += digits[index] << 8
digits[index] = carry % 58
carry /= 58
}
while carry > 0 {
digits.append(carry % 58)
carry /= 58
}
}
var output = String(repeating: String(alphabet[0]), count: zeros)
for digit in digits.reversed() { output.append(alphabet[digit]) }
return output
}
static func decode(_ string: String) throws -> [UInt8] {
let characters = Array(string)
var zeros = 0
for character in characters {
if character == alphabet[0] { zeros += 1 } else { break }
}
var bytes: [Int] = []
for character in characters[zeros...] {
guard let value = valueOf[character] else { throw XRPError.invalidBase58 }
var carry = value
for index in bytes.indices {
carry += bytes[index] * 58
bytes[index] = carry & 0xff
carry >>= 8
}
while carry > 0 {
bytes.append(carry & 0xff)
carry >>= 8
}
}
return [UInt8](repeating: 0, count: zeros) + bytes.reversed().map { UInt8($0) }
}
static func checkEncode(version: [UInt8], payload: [UInt8]) -> String {
let body = version + payload
return encode(body + XRPHash.checksum(body))
}
static func checkDecode(_ string: String, versionLength: Int) throws -> [UInt8] {
let decoded = try decode(string)
guard decoded.count > versionLength + 4 else { throw XRPError.invalidAddress }
let body = Array(decoded[0..<(decoded.count - 4)])
let checksum = Array(decoded[(decoded.count - 4)...])
guard XRPHash.checksum(body) == checksum else { throw XRPError.invalidAddress }
return Array(body[versionLength...])
}
}
+68
View File
@@ -0,0 +1,68 @@
import CryptoKit
import Foundation
enum XRPError: LocalizedError {
case invalidBase58
case invalidAddress
case invalidSeed
case unsupportedSeed
case rpc(String)
case network(String)
var errorDescription: String? {
switch self {
case .invalidBase58: return "Invalid Base58 text"
case .invalidAddress: return "Invalid XRP address"
case .invalidSeed: return "Check the recovery phrase or secret"
case .unsupportedSeed: return "Unsupported XRP secret"
case .rpc(let message): return message
case .network(let message): return message
}
}
}
enum XRPHash {
static func sha256(_ bytes: [UInt8]) -> [UInt8] {
Array(SHA256.hash(data: Data(bytes)))
}
static func sha512(_ bytes: [UInt8]) -> [UInt8] {
Array(SHA512.hash(data: Data(bytes)))
}
/// First 32 bytes of SHA-512, used by the XRP Ledger everywhere.
static func sha512Half(_ bytes: [UInt8]) -> [UInt8] {
Array(sha512(bytes).prefix(32))
}
static func ripemd160(_ bytes: [UInt8]) -> [UInt8] {
RIPEMD160.hash(bytes)
}
/// Base58Check double-SHA-256 checksum.
static func checksum(_ bytes: [UInt8]) -> [UInt8] {
Array(sha256(sha256(bytes)).prefix(4))
}
}
enum Hex {
static func encode(_ bytes: [UInt8]) -> String {
bytes.map { String(format: "%02x", $0) }.joined()
}
static func decode(_ string: String) -> [UInt8]? {
var text = string.trimmingCharacters(in: .whitespacesAndNewlines)
if text.hasPrefix("0x") || text.hasPrefix("0X") { text = String(text.dropFirst(2)) }
guard text.count % 2 == 0 else { return nil }
var bytes: [UInt8] = []
bytes.reserveCapacity(text.count / 2)
var index = text.startIndex
while index < text.endIndex {
let next = text.index(index, offsetBy: 2)
guard let byte = UInt8(text[index..<next], radix: 16) else { return nil }
bytes.append(byte)
index = next
}
return bytes
}
}
+114
View File
@@ -0,0 +1,114 @@
import Foundation
/// RIPEMD-160, needed for XRP account id derivation (there is no CryptoKit /
/// CommonCrypto implementation of this digest on iOS).
enum RIPEMD160 {
private static let roundLeft: [Int] = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13,
]
private static let roundRight: [Int] = [
5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11,
]
private static let shiftsLeft: [UInt32] = [
11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6,
]
private static let shiftsRight: [UInt32] = [
8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11,
]
private static let constantsLeft: [UInt32] = [0x0000_0000, 0x5A82_7999, 0x6ED9_EBA1, 0x8F1B_BCDC, 0xA953_FD4E]
private static let constantsRight: [UInt32] = [0x50A2_8BE6, 0x5C4D_D124, 0x6D70_3EF3, 0x7A6D_76E9, 0x0000_0000]
static func hash(_ message: [UInt8]) -> [UInt8] {
var padded = message
let bitLength = UInt64(message.count) * 8
padded.append(0x80)
while padded.count % 64 != 56 { padded.append(0) }
for shift in stride(from: 0, through: 56, by: 8) {
padded.append(UInt8((bitLength >> UInt64(shift)) & 0xff))
}
var h0: UInt32 = 0x6745_2301
var h1: UInt32 = 0xEFCD_AB89
var h2: UInt32 = 0x98BA_DCFE
var h3: UInt32 = 0x1032_5476
var h4: UInt32 = 0xC3D2_E1F0
var offset = 0
while offset < padded.count {
var words = [UInt32](repeating: 0, count: 16)
for index in 0..<16 {
let base = offset + index * 4
words[index] = UInt32(padded[base])
| (UInt32(padded[base + 1]) << 8)
| (UInt32(padded[base + 2]) << 16)
| (UInt32(padded[base + 3]) << 24)
}
var al = h0, bl = h1, cl = h2, dl = h3, el = h4
var ar = h0, br = h1, cr = h2, dr = h3, er = h4
for step in 0..<80 {
let round = step / 16
var leftTemp = rotate(al &+ f(round, bl, cl, dl) &+ words[roundLeft[step]] &+ constantsLeft[round], by: shiftsLeft[step]) &+ el
al = el; el = dl; dl = rotate(cl, by: 10); cl = bl; bl = leftTemp
let rightRound = 4 - round
leftTemp = rotate(ar &+ f(rightRound, br, cr, dr) &+ words[roundRight[step]] &+ constantsRight[round], by: shiftsRight[step]) &+ er
ar = er; er = dr; dr = rotate(cr, by: 10); cr = br; br = leftTemp
}
let temp = h1 &+ cl &+ dr
h1 = h2 &+ dl &+ er
h2 = h3 &+ el &+ ar
h3 = h4 &+ al &+ br
h4 = h0 &+ bl &+ cr
h0 = temp
offset += 64
}
var digest: [UInt8] = []
for word in [h0, h1, h2, h3, h4] {
digest.append(UInt8(word & 0xff))
digest.append(UInt8((word >> 8) & 0xff))
digest.append(UInt8((word >> 16) & 0xff))
digest.append(UInt8((word >> 24) & 0xff))
}
return digest
}
private static func f(_ round: Int, _ x: UInt32, _ y: UInt32, _ z: UInt32) -> UInt32 {
switch round {
case 0: return x ^ y ^ z
case 1: return (x & y) | (~x & z)
case 2: return (x | ~y) ^ z
case 3: return (x & z) | (y & ~z)
default: return x ^ (y | ~z)
}
}
private static func rotate(_ value: UInt32, by amount: UInt32) -> UInt32 {
(value << amount) | (value >> (32 - amount))
}
}
+76
View File
@@ -0,0 +1,76 @@
import Foundation
import Security
/// Small Keychain wrapper for wallet secrets. Secrets never touch UserDefaults.
enum Keychain {
private static let service = "com.koins.app"
static func set(_ value: String, for key: String) {
guard let data = value.data(using: .utf8) else { return }
remove(key)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
SecItemAdd(query as CFDictionary, nil)
}
static func get(_ key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var item: CFTypeRef?
guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess,
let data = item as? Data,
let value = String(data: data, encoding: .utf8) else {
return nil
}
return value
}
static func remove(_ key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
SecItemDelete(query as CFDictionary)
}
}
enum WalletStore {
private static let defaults = UserDefaults.standard
private static func walletKey(_ chain: Chain) -> String { "koins.wallet.\(chain.rawValue)" }
private static func secretKey(_ chain: Chain) -> String { "koins.secret.\(chain.rawValue)" }
static func wallet(_ chain: Chain) -> WalletRecord? {
guard let data = defaults.data(forKey: walletKey(chain)) else { return nil }
return try? JSONDecoder().decode(WalletRecord.self, from: data)
}
static func saveWallet(_ wallet: WalletRecord, chain: Chain) {
guard let data = try? JSONEncoder().encode(wallet) else { return }
defaults.set(data, forKey: walletKey(chain))
}
static func removeWallet(_ chain: Chain) {
defaults.removeObject(forKey: walletKey(chain))
Keychain.remove(secretKey(chain))
}
static func secret(_ chain: Chain) -> String? {
Keychain.get(secretKey(chain))
}
static func saveSecret(_ secret: String, chain: Chain) {
Keychain.set(secret, for: secretKey(chain))
}
}
+260
View File
@@ -0,0 +1,260 @@
import CoreImage.CIFilterBuiltins
import SwiftUI
enum Theme {
static let background = Color(red: 0.035, green: 0.035, blue: 0.05)
static let surface = Color(red: 0.09, green: 0.09, blue: 0.12)
static let surfaceAlt = Color(red: 0.13, green: 0.13, blue: 0.17)
static let border = Color.white.opacity(0.08)
static let accent = Color(red: 1.0, green: 0.56, blue: 0.16)
static let accentSoft = Color(red: 1.0, green: 0.56, blue: 0.16).opacity(0.15)
static let text = Color.white
static let muted = Color.white.opacity(0.55)
static let danger = Color(red: 1.0, green: 0.36, blue: 0.36)
static let success = Color(red: 0.35, green: 0.85, blue: 0.55)
}
struct PrimaryButton: View {
var title: String
var disabled: Bool = false
var action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.system(size: 16, weight: .semibold))
.frame(maxWidth: .infinity)
.padding(.vertical, 15)
.background(disabled ? Theme.surfaceAlt : Theme.accent)
.foregroundStyle(disabled ? Theme.muted : Color.black)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
}
.disabled(disabled)
}
}
struct SecondaryButton: View {
var title: String
var action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.system(size: 15, weight: .medium))
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(Theme.surface)
.foregroundStyle(Theme.text)
.overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).stroke(Theme.border))
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
}
}
}
struct Card<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
content
.padding(18)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Theme.surface)
.overlay(RoundedRectangle(cornerRadius: 20, style: .continuous).stroke(Theme.border))
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
struct FieldLabel: View {
var text: String
var body: some View {
Text(text)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Theme.muted)
.padding(.leading, 4)
}
}
struct KoinsTextField: View {
var placeholder: String
@Binding var text: String
var multiline = false
var keyboard: UIKeyboardType = .default
var autocapitalization: TextInputAutocapitalization = .never
var body: some View {
Group {
if multiline {
TextEditor(text: $text)
.scrollContentBackground(.hidden)
.frame(minHeight: 96)
.padding(8)
} else {
TextField(placeholder, text: $text)
.keyboardType(keyboard)
.textInputAutocapitalization(autocapitalization)
.autocorrectionDisabled()
.padding(.horizontal, 14)
.padding(.vertical, 13)
}
}
.font(.system(size: 15))
.foregroundStyle(Theme.text)
.background(Theme.surface)
.overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).stroke(Theme.border))
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
}
}
struct MessageBanner: View {
var message: String
var isError: Bool
var body: some View {
Text(message)
.font(.system(size: 13))
.foregroundStyle(isError ? Theme.danger : Theme.success)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background((isError ? Theme.danger : Theme.success).opacity(0.12))
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
}
struct ChainPicker: View {
@Binding var selection: Chain
var body: some View {
HStack(spacing: 6) {
ForEach(Chain.allCases, id: \.self) { chain in
Button {
selection = chain
} label: {
Text(chain.title)
.font(.system(size: 13, weight: .semibold))
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.background(selection == chain ? Theme.accent : Color.clear)
.foregroundStyle(selection == chain ? Color.black : Theme.muted)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
}
}
}
.padding(4)
.background(Theme.surface)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
}
}
struct QRCodeView: View {
var text: String
var body: some View {
if let image = QRCodeView.makeImage(from: text) {
Image(uiImage: image)
.interpolation(.none)
.resizable()
.scaledToFit()
.padding(12)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
} else {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Theme.surfaceAlt)
.aspectRatio(1, contentMode: .fit)
}
}
private static func makeImage(from text: String) -> UIImage? {
guard !text.isEmpty else { return nil }
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(text.utf8)
filter.correctionLevel = "M"
guard let output = filter.outputImage else { return nil }
let scaled = output.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
let context = CIContext()
guard let cgImage = context.createCGImage(scaled, from: scaled.extent) else { return nil }
return UIImage(cgImage: cgImage)
}
}
struct TransactionRow: View {
var record: TransactionRecord
var symbol: String
private var formattedDate: String {
guard let timestamp = record.timestamp else { return "Pending" }
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: timestamp)
}
var body: some View {
HStack(spacing: 12) {
ZStack {
Circle()
.fill(record.direction == .incoming ? Theme.success.opacity(0.15) : Theme.accentSoft)
.frame(width: 38, height: 38)
Image(systemName: record.direction == .incoming ? "arrow.down.left" : "arrow.up.right")
.font(.system(size: 15, weight: .bold))
.foregroundStyle(record.direction == .incoming ? Theme.success : Theme.accent)
}
VStack(alignment: .leading, spacing: 3) {
Text(record.direction == .incoming ? "Received" : "Sent")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(Theme.text)
Text(record.confirmed ? formattedDate : "Pending confirmation")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
Spacer()
VStack(alignment: .trailing, spacing: 3) {
Text("\(record.direction == .incoming ? "+" : "-")\(record.amount) \(symbol)")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(record.direction == .incoming ? Theme.success : Theme.text)
if let fee = record.fee, !fee.isEmpty, record.direction == .outgoing {
Text("fee \(fee)")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
}
}
.padding(.vertical, 6)
}
}
struct ScreenHeader: View {
var title: String
var subtitle: String?
var onBack: (() -> Void)?
var body: some View {
HStack(alignment: .center, spacing: 12) {
if let onBack {
Button(action: onBack) {
Image(systemName: "chevron.left")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(Theme.text)
.frame(width: 34, height: 34)
.background(Theme.surface)
.clipShape(Circle())
}
}
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.system(size: 22, weight: .bold))
.foregroundStyle(Theme.text)
if let subtitle {
Text(subtitle)
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
}
}
Spacer()
}
}
}
+228
View File
@@ -0,0 +1,228 @@
import SwiftUI
struct DashboardView: View {
@EnvironmentObject private var model: AppModel
@Binding var route: Route
@State private var showSeed = false
@State private var revealedSeed: String?
@State private var seedError: String?
var body: some View {
VStack(alignment: .leading, spacing: 18) {
ScreenHeader(title: model.wallet?.name ?? "Wallet",
subtitle: model.chain.title,
onBack: { route = .home })
Card {
VStack(alignment: .leading, spacing: 10) {
Text("Balance")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(Theme.muted)
Text("\(model.balance.total) \(model.symbol)")
.font(.system(size: 32, weight: .bold, design: .rounded))
.foregroundStyle(Theme.text)
if model.chain == .xmr, model.balance.unlocked != model.balance.total {
Text("\(model.balance.unlocked) \(model.symbol) spendable now")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
AddressRow(address: model.displayAddress)
}
}
if model.chain == .xmr {
Card {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("Sync")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Theme.text)
Spacer()
if let status = model.syncStatus { SyncBadge(status: status) }
}
if let status = model.syncStatus {
SyncProgress(status: status, eta: model.syncEta)
} else {
Text("Sync status is not available yet.")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
}
}
}
HStack(spacing: 10) {
PrimaryButton(title: "Send") { route = .send }
SecondaryButton(title: "Receive") { route = .receive }
}
if !model.transactions.isEmpty {
Card {
VStack(alignment: .leading, spacing: 10) {
Text("Activity")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Theme.muted)
ForEach(model.transactions) { record in
TransactionRow(record: record, symbol: model.symbol)
if record.id != model.transactions.last?.id {
Divider().overlay(Theme.border)
}
}
}
}
}
if let message = model.errorMessage {
MessageBanner(message: message, isError: true)
}
if let message = model.statusMessage {
MessageBanner(message: message, isError: false)
}
SecondaryButton(title: "Reveal recovery phrase") {
Task {
seedError = nil
do {
revealedSeed = try await model.revealSeed()
showSeed = true
} catch {
seedError = error.localizedDescription
}
}
}
if let seedError {
MessageBanner(message: seedError, isError: true)
}
}
.sheet(isPresented: $showSeed) {
if let revealedSeed {
SeedRevealView(seed: revealedSeed, symbol: model.symbol) { showSeed = false }
}
}
}
}
struct SendView: View {
@EnvironmentObject private var model: AppModel
@Binding var route: Route
@State private var destination = ""
@State private var amount = ""
@State private var tag = ""
@State private var priority: TransactionPriority = .defaultPriority
@State private var localError: String?
var body: some View {
VStack(alignment: .leading, spacing: 18) {
ScreenHeader(title: "Send \(model.symbol)",
subtitle: "Balance \(model.balance.total) \(model.symbol)",
onBack: { route = .home })
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: model.chain == .xrp ? "Destination address" : "Recipient address")
KoinsTextField(placeholder: model.chain == .xrp ? "r…" : "4…", text: $destination)
}
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: "Amount (\(model.symbol))")
KoinsTextField(placeholder: "0.0", text: $amount, keyboard: .decimalPad)
}
if model.chain == .xrp {
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: "Destination tag (optional)")
KoinsTextField(placeholder: "e.g. 12345", text: $tag, keyboard: .numberPad)
}
}
if model.chain == .xmr {
VStack(alignment: .leading, spacing: 8) {
FieldLabel(text: "Priority")
Picker("Priority", selection: $priority) {
ForEach([TransactionPriority.low, .medium, .high, .defaultPriority], id: \.rawValue) { item in
Text(item.label).tag(item)
}
}
.pickerStyle(.segmented)
}
}
if let message = localError ?? model.errorMessage {
MessageBanner(message: message, isError: true)
}
if let message = model.statusMessage {
MessageBanner(message: message, isError: false)
}
PrimaryButton(title: model.isBusy ? "Sending…" : "Send", disabled: model.isBusy) {
Task {
localError = nil
model.clearMessages()
do {
try await model.send(to: destination, amount: amount, tag: tag, priority: priority)
amount = ""
tag = ""
route = .dashboard
} catch {
localError = error.localizedDescription
}
}
}
Spacer(minLength: 0)
}
}
}
struct ReceiveView: View {
@EnvironmentObject private var model: AppModel
@Binding var route: Route
private var address: String {
model.displayAddress
}
var body: some View {
VStack(alignment: .leading, spacing: 18) {
ScreenHeader(title: "Receive \(model.symbol)",
subtitle: "Share this address to get paid",
onBack: { route = .home })
Card {
VStack(spacing: 16) {
QRCodeView(text: address)
.frame(maxWidth: 240)
Text(address)
.font(.system(size: 12, design: .monospaced))
.foregroundStyle(Theme.muted)
.multilineTextAlignment(.center)
.lineLimit(3)
.truncationMode(.middle)
}
.frame(maxWidth: .infinity)
}
SecondaryButton(title: "Copy address") {
UIPasteboard.general.string = address
}
if model.chain == .xrp {
Card {
Text("XRP accounts must hold at least 1 XRP as a reserve to be active. Only send XRP on the XRP Ledger to this address.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
}
} else {
Card {
Text("Send only Monero (XMR) to this address. Transfers need confirmations before the balance becomes spendable.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
}
}
Spacer(minLength: 0)
}
}
}
+379
View File
@@ -0,0 +1,379 @@
import SwiftUI
enum Route: Equatable {
case home
case create
case importWallet
case dashboard
case send
case receive
}
struct SeedBox: Identifiable {
let id = UUID()
let seed: String
let symbol: String
}
struct RootView: View {
@EnvironmentObject private var model: AppModel
@State private var route: Route = .home
@State private var seedBox: SeedBox?
var body: some View {
ZStack {
Theme.background.ignoresSafeArea()
ScrollView {
VStack(spacing: 18) {
switch route {
case .home:
HomeView(route: $route)
case .create:
CreateWalletView(route: $route, seedBox: $seedBox)
case .importWallet:
ImportWalletView(route: $route)
case .dashboard:
DashboardView(route: $route)
case .send:
SendView(route: $route)
case .receive:
ReceiveView(route: $route)
}
}
.padding(.horizontal, 20)
.padding(.top, 12)
.padding(.bottom, 40)
}
.scrollDismissesKeyboard(.interactively)
}
.preferredColorScheme(.dark)
.task { await model.bootstrap() }
.sheet(item: $seedBox) { box in
SeedRevealView(seed: box.seed, symbol: box.symbol) { seedBox = nil }
}
.onChange(of: model.chain) { _ in
if route == .dashboard { route = .home }
}
}
}
struct HomeView: View {
@EnvironmentObject private var model: AppModel
@Binding var route: Route
@State private var showDeleteConfirm = false
var body: some View {
VStack(spacing: 18) {
header
if model.hasWallet, model.chain == .xrp || model.moneroReady {
walletCard
} else if model.hasWallet {
moneroUnavailableCard
} else {
emptyState
}
if let message = model.errorMessage {
MessageBanner(message: message, isError: true)
}
if let message = model.statusMessage {
MessageBanner(message: message, isError: false)
}
}
.confirmationDialog("Delete this wallet?",
isPresented: $showDeleteConfirm,
titleVisibility: .visible) {
Button("Delete wallet", role: .destructive) {
Task {
await model.deleteWallet()
route = .home
}
}
Button("Cancel", role: .cancel) {}
} message: {
Text("The keys stored on this device are removed. Make sure your recovery seed is backed up.")
}
}
private var header: some View {
VStack(alignment: .leading, spacing: 4) {
Text("Koins")
.font(.system(size: 30, weight: .heavy))
.foregroundStyle(Theme.text)
Text("Monero and XRP wallet")
.font(.system(size: 13))
.foregroundStyle(Theme.muted)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var walletCard: some View {
VStack(spacing: 14) {
ChainPicker(selection: Binding(get: { model.chain },
set: { newValue in Task { await model.select(newValue) } }))
Card {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text(model.wallet?.name ?? "Wallet")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(Theme.text)
Spacer()
if model.chain == .xmr, let status = model.syncStatus {
SyncBadge(status: status)
}
}
VStack(alignment: .leading, spacing: 4) {
Text("Balance")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(Theme.muted)
Text("\(model.balance.total) \(model.symbol)")
.font(.system(size: 30, weight: .bold, design: .rounded))
.foregroundStyle(Theme.text)
if model.chain == .xmr, model.balance.unlocked != model.balance.total {
Text("\(model.balance.unlocked) \(model.symbol) unlocked")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
}
AddressRow(address: model.displayAddress)
if model.chain == .xmr, let status = model.syncStatus {
SyncProgress(status: status, eta: model.syncEta)
}
}
}
HStack(spacing: 10) {
PrimaryButton(title: "Send") { route = .send }
SecondaryButton(title: "Receive") { route = .receive }
}
if model.chain == .xmr {
SecondaryButton(title: model.syncStatus?.syncing == true ? "Syncing…" : "Sync now") {
Task { await model.startMoneroSync() }
}
}
HStack(spacing: 10) {
SecondaryButton(title: "Refresh") { Task { await model.refresh(force: true) } }
SecondaryButton(title: "Delete") { showDeleteConfirm = true }
}
if !model.transactions.isEmpty {
Card {
VStack(alignment: .leading, spacing: 10) {
Text("Activity")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Theme.muted)
ForEach(model.transactions) { record in
TransactionRow(record: record, symbol: model.symbol)
if record.id != model.transactions.last?.id {
Divider().overlay(Theme.border)
}
}
}
}
} else if model.chain == .xrp, model.balance.funded == false {
Text("This account is not funded yet. Send XRP to the address above to activate it.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
}
if model.chain == .xrp, model.moneroReady, let status = model.syncStatus, status.syncing {
Card {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
Circle()
.fill(status.connected ? Theme.success : Theme.danger)
.frame(width: 7, height: 7)
Text("Monero is syncing in the background")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Theme.text)
Spacer()
Text("\(Int(status.progress * 100))%")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(Theme.muted)
}
SyncProgress(status: status, eta: model.syncEta)
}
}
}
}
}
private var moneroUnavailableCard: some View {
VStack(spacing: 14) {
ChainPicker(selection: Binding(get: { model.chain },
set: { newValue in Task { await model.select(newValue) } }))
Card {
VStack(alignment: .leading, spacing: 10) {
Text("Monero wallet")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(Theme.text)
Text("The wallet is on this device but could not be opened. It is safe to retry; nothing was deleted.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
SecondaryButton(title: "Retry") {
Task { await model.select(.xmr) }
}
}
}
}
}
private var emptyState: some View {
VStack(spacing: 14) {
ChainPicker(selection: Binding(get: { model.chain },
set: { newValue in Task { await model.select(newValue) } }))
Card {
VStack(alignment: .leading, spacing: 8) {
Text("\(model.chain.title) wallet")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(Theme.text)
Text("Create a new wallet on this device, or import one with its recovery seed.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
}
}
VStack(spacing: 10) {
PrimaryButton(title: "Create wallet") { route = .create }
SecondaryButton(title: "Import wallet") { route = .importWallet }
}
}
}
}
struct SyncBadge: View {
var status: SyncStatus
private var label: String {
if status.syncing {
let percent = status.progress * 100
return percent < 10 ? String(format: "Syncing %.1f%%", percent) : "Syncing \(Int(percent))%"
}
return status.connected ? "Synced" : "Offline"
}
var body: some View {
HStack(spacing: 6) {
Circle()
.fill(status.connected ? Theme.success : Theme.danger)
.frame(width: 7, height: 7)
Text(label)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(Theme.muted)
}
}
}
/// Live scan progress: how far the wallet has reached, and roughly how long the
/// remaining blocks will take at the observed rate.
struct SyncProgress: View {
var status: SyncStatus
var eta: String?
var body: some View {
VStack(alignment: .leading, spacing: 6) {
ProgressView(value: status.progress)
.tint(Theme.accent)
HStack(spacing: 8) {
Text("Block \(status.height.formatted()) of \(status.daemonHeight.formatted())")
Spacer()
if status.syncing {
Text(eta.map { "about \($0) left" } ?? "estimating…")
} else if status.connected {
Text("up to date")
}
}
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
}
}
struct AddressRow: View {
var address: String
@State private var copied = false
var body: some View {
HStack(spacing: 8) {
Text(address.isEmpty ? "—" : address)
.font(.system(size: 11, design: .monospaced))
.foregroundStyle(Theme.muted)
.lineLimit(1)
.truncationMode(.middle)
Spacer(minLength: 6)
Button {
UIPasteboard.general.string = address
copied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { copied = false }
} label: {
Image(systemName: copied ? "checkmark" : "doc.on.doc")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(Theme.accent)
}
.disabled(address.isEmpty)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(Theme.surfaceAlt)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
}
struct SeedRevealView: View {
var seed: String
var symbol: String
var onDone: () -> Void
@State private var acknowledged = false
var body: some View {
ZStack {
Theme.background.ignoresSafeArea()
VStack(alignment: .leading, spacing: 16) {
Text("Your recovery phrase")
.font(.system(size: 24, weight: .bold))
.foregroundStyle(Theme.text)
Text("Write these words down in order and keep them offline. Anyone with this phrase can spend your \(symbol).")
.font(.system(size: 13))
.foregroundStyle(Theme.muted)
Text(seed)
.font(.system(size: 15, design: .monospaced))
.foregroundStyle(Theme.text)
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Theme.surface)
.overlay(RoundedRectangle(cornerRadius: 16, style: .continuous).stroke(Theme.border))
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
Button {
UIPasteboard.general.string = seed
} label: {
Label("Copy phrase", systemImage: "doc.on.doc")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(Theme.accent)
}
Toggle(isOn: $acknowledged) {
Text("I have written down my phrase")
.font(.system(size: 13))
.foregroundStyle(Theme.muted)
}
.tint(Theme.accent)
Spacer()
PrimaryButton(title: "Continue", disabled: !acknowledged, action: onDone)
}
.padding(22)
}
}
}
+142
View File
@@ -0,0 +1,142 @@
import SwiftUI
struct CreateWalletView: View {
@EnvironmentObject private var model: AppModel
@Binding var route: Route
@Binding var seedBox: SeedBox?
@State private var name = ""
@State private var localError: String?
var body: some View {
VStack(alignment: .leading, spacing: 18) {
ScreenHeader(title: "Create wallet",
subtitle: "A new \(model.chain.title) wallet on this device",
onBack: { route = .home })
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: "Wallet name")
KoinsTextField(placeholder: "My \(model.symbol) wallet", text: $name)
}
if model.chain == .xmr {
Card {
Text("The wallet file is created locally and encrypted on this device. Syncing starts right away and can take a while for a new Monero wallet.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
}
}
if let message = localError ?? model.errorMessage {
MessageBanner(message: message, isError: true)
}
PrimaryButton(title: model.isBusy ? "Generating…" : "Generate wallet",
disabled: model.isBusy) {
Task {
localError = nil
model.clearMessages()
do {
let seed = try await model.createWallet(name: name)
seedBox = SeedBox(seed: seed, symbol: model.symbol)
route = .dashboard
} catch {
localError = error.localizedDescription
}
}
}
Spacer(minLength: 0)
}
}
}
struct ImportWalletView: View {
@EnvironmentObject private var model: AppModel
@Binding var route: Route
@State private var name = ""
@State private var secret = ""
@State private var restoreHeight = ""
@State private var localError: String?
@State private var tipError: String?
@State private var isLoadingTip = false
var body: some View {
VStack(alignment: .leading, spacing: 18) {
ScreenHeader(title: "Import wallet",
subtitle: "Restore a \(model.chain.title) wallet from its seed",
onBack: { route = .home })
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: "Wallet name")
KoinsTextField(placeholder: "My \(model.symbol) wallet", text: $name)
}
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: model.chain == .xrp ? "Recovery phrase" : "Mnemonic seed")
KoinsTextField(placeholder: model.chain == .xrp ? "12 words, or a family seed (sEd…)" : "25 word seed phrase",
text: $secret,
multiline: model.chain == .xmr)
}
if model.chain == .xmr {
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: "Restore height (optional)")
KoinsTextField(placeholder: "e.g. 3200000", text: $restoreHeight, keyboard: .numberPad)
}
Card {
VStack(alignment: .leading, spacing: 10) {
Text("Without a height the wallet scans the whole chain — millions of blocks, which takes hours on a phone. Set the block your wallet was first used, or start from the current tip if you only care about new funds.")
.font(.system(size: 12))
.foregroundStyle(Theme.muted)
Button {
Task {
tipError = nil
isLoadingTip = true
defer { isLoadingTip = false }
do {
let tip = try await MoneroService.shared.daemonHeight()
restoreHeight = String(tip)
} catch {
tipError = error.localizedDescription
}
}
} label: {
Text(isLoadingTip ? "Fetching chain tip…" : "Use the current chain tip")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(Theme.accent)
}
.disabled(isLoadingTip)
if let tipError {
Text(tipError)
.font(.system(size: 11))
.foregroundStyle(Theme.danger)
}
}
}
}
if let message = localError ?? model.errorMessage {
MessageBanner(message: message, isError: true)
}
PrimaryButton(title: model.isBusy ? "Importing…" : "Import wallet",
disabled: model.isBusy) {
Task {
localError = nil
model.clearMessages()
do {
let height = UInt64(restoreHeight.trimmingCharacters(in: .whitespaces))
try await model.importWallet(name: name, secret: secret, restoreHeight: height)
route = .dashboard
} catch {
localError = error.localizedDescription
}
}
}
Spacer(minLength: 0)
}
}
}
+224
View File
@@ -0,0 +1,224 @@
import Foundation
/// Monero amounts are atomic units (1 XMR = 1e12 piconero).
enum MoneroAmount {
static let atomicPerXMR: UInt64 = 1_000_000_000_000
static func xmr(fromAtomic value: String) -> String {
guard let atomic = UInt64(value) else { return "0" }
return xmr(fromAtomic: atomic)
}
static func xmr(fromAtomic value: UInt64) -> String {
let whole = value / atomicPerXMR
let fraction = value % atomicPerXMR
if fraction == 0 { return String(whole) }
var text = String(format: "%012llu", fraction)
while text.hasSuffix("0") { text.removeLast() }
return "\(whole).\(text)"
}
static func atomic(fromXMR value: String) throws -> UInt64 {
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty, !text.hasPrefix("-") else { throw XRPError.rpc("Enter a valid amount") }
let parts = text.split(separator: ".", omittingEmptySubsequences: false)
guard parts.count <= 2 else { throw XRPError.rpc("Enter a valid amount") }
guard let whole = UInt64(parts[0].isEmpty ? "0" : String(parts[0])) else {
throw XRPError.rpc("Enter a valid amount")
}
var fraction: UInt64 = 0
if parts.count == 2 {
let fractionText = String(parts[1])
guard fractionText.count <= 12, fractionText.allSatisfy({ $0.isNumber }) else {
throw XRPError.rpc("XMR supports at most 12 decimal places")
}
let padded = fractionText.padding(toLength: 12, withPad: "0", startingAt: 0)
guard let parsed = UInt64(padded) else { throw XRPError.rpc("Enter a valid amount") }
fraction = parsed
}
let (wholeAtomic, overflow) = whole.multipliedReportingOverflow(by: atomicPerXMR)
guard !overflow else { throw XRPError.rpc("Amount is too large") }
let (total, carry) = wholeAtomic.addingReportingOverflow(fraction)
guard !carry else { throw XRPError.rpc("Amount is too large") }
return total
}
}
/// Swift wrapper around the Objective-C++ monero-cpp bridge.
final class MoneroService {
static let shared = MoneroService()
/// Wallet files that exist on disk without a stored record, so a lost
/// preference file does not strand a wallet.
static func orphanedWalletNames() -> [String] {
let manager = FileManager.default
guard let documents = manager.urls(for: .documentDirectory, in: .userDomainMask).first,
let items = try? manager.contentsOfDirectory(at: documents,
includingPropertiesForKeys: nil) else {
return []
}
let keys = Set(items.filter { $0.pathExtension == "keys" }
.map { $0.deletingPathExtension().lastPathComponent })
return items
.filter { $0.pathExtension.isEmpty && !$0.lastPathComponent.hasPrefix(".") }
.map { $0.lastPathComponent }
.filter { keys.contains($0) }
.sorted()
}
/// The wallet file is encrypted with this password; the recovery seed lives
/// in the Keychain.
static let walletPassword = "koins"
/// Default daemon, kept in sync with the bridge. Used for the cheap
/// daemon-only queries that do not need an open wallet.
static let daemonHost = "xmr-node.cakewallet.com"
/// Current chain tip, so an import can start scanning from now instead of
/// from the genesis block.
func daemonHeight() async throws -> UInt64 {
guard let url = URL(string: "http://\(Self.daemonHost):18081/json_rpc") else {
throw XRPError.network("Invalid Monero daemon address")
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 15
request.httpBody = try JSONSerialization.data(withJSONObject: [
"jsonrpc": "2.0",
"id": "koins",
"method": "get_info",
])
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw XRPError.network("Monero daemon is unreachable")
}
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let result = json["result"] as? [String: Any],
let height = (result["height"] as? NSNumber)?.uint64Value else {
throw XRPError.network("Could not read the Monero chain tip")
}
return height
}
private func invoke(_ request: [String: Any]) async throws -> [String: Any] {
let json = try JSONSerialization.data(withJSONObject: request)
let requestString = String(data: json, encoding: .utf8) ?? "{}"
let responseString: String = await withCheckedContinuation { continuation in
KoinsMoneroWallet.invoke(requestString) { response in
continuation.resume(returning: response)
}
}
guard let data = responseString.data(using: .utf8),
let response = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw XRPError.rpc("Unexpected response from the Monero wallet")
}
if let error = response["error"] as? String, !error.isEmpty {
throw XRPError.rpc(error)
}
return response
}
// MARK: - Wallet lifecycle
func createWallet(name: String) async throws -> (address: String, mnemonic: String) {
let response = try await invoke(["op": "createWallet",
"name": name,
"password": Self.walletPassword,
"language": "English"])
return (response["address"] as? String ?? "", response["mnemonic"] as? String ?? "")
}
func openWallet(name: String) async throws -> String {
let response = try await invoke(["op": "openWallet",
"name": name,
"password": Self.walletPassword])
return response["address"] as? String ?? ""
}
func restoreWallet(name: String,
seed: String,
restoreHeight: UInt64) async throws -> String {
let response = try await invoke(["op": "restoreWallet",
"name": name,
"password": Self.walletPassword,
"seed": seed,
"restoreHeight": restoreHeight])
return response["address"] as? String ?? ""
}
func removeWallet(name: String) async {
_ = try? await invoke(["op": "removeWallet", "name": name])
}
func closeWallet() async {
_ = try? await invoke(["op": "closeWallet"])
}
func seed() async throws -> String {
let response = try await invoke(["op": "getSeed"])
return response["mnemonic"] as? String ?? ""
}
// MARK: - Reads
func balance() async throws -> Balance {
let response = try await invoke(["op": "getBalance"])
let total = response["balance"] as? String ?? "0"
let unlocked = response["unlocked"] as? String ?? total
return Balance(total: MoneroAmount.xmr(fromAtomic: total),
unlocked: MoneroAmount.xmr(fromAtomic: unlocked),
funded: (UInt64(total) ?? 0) > 0)
}
func transactions() async throws -> [TransactionRecord] {
let response = try await invoke(["op": "getTransactions"])
let entries = response["transactions"] as? [[String: Any]] ?? []
return entries.map { entry in
let timestamp = (entry["timestamp"] as? NSNumber)?.doubleValue ?? 0
return TransactionRecord(
id: entry["hash"] as? String ?? UUID().uuidString,
amount: MoneroAmount.xmr(fromAtomic: entry["amount"] as? String ?? "0"),
timestamp: timestamp > 0 ? Date(timeIntervalSince1970: timestamp) : nil,
direction: (entry["direction"] as? String) == "in" ? .incoming : .outgoing,
confirmed: (entry["confirmed"] as? Bool) ?? false,
fee: MoneroAmount.xmr(fromAtomic: entry["fee"] as? String ?? "0"),
tag: nil,
counterparty: nil)
}
}
// MARK: - Sync
func startSync() async throws -> Bool {
let response = try await invoke(["op": "startSync"])
return (response["syncing"] as? Bool) ?? false
}
func stopSync() async {
_ = try? await invoke(["op": "stopSync"])
}
func syncStatus() async throws -> SyncStatus {
let response = try await invoke(["op": "getSyncStatus"])
return SyncStatus(height: (response["height"] as? NSNumber)?.uint64Value ?? 0,
daemonHeight: (response["daemonHeight"] as? NSNumber)?.uint64Value ?? 0,
syncing: (response["syncing"] as? Bool) ?? false,
connected: (response["connected"] as? Bool) ?? false)
}
// MARK: - Sends
func send(address: String, amountXMR: String, priority: TransactionPriority) async throws -> (hash: String, fee: String) {
let atomic = try MoneroAmount.atomic(fromXMR: amountXMR)
let response = try await invoke(["op": "transferTo",
"address": address.trimmingCharacters(in: .whitespacesAndNewlines),
"amount": String(atomic),
"priority": priority.rawValue])
return (response["txHash"] as? String ?? "",
MoneroAmount.xmr(fromAtomic: response["fee"] as? String ?? "0"))
}
}
+53
View File
@@ -0,0 +1,53 @@
import Foundation
/// Exact decimal <-> drops conversion (1 XRP = 1_000_000 drops) without
/// floating point rounding errors.
enum XRPAmount {
static let dropsPerXRP: UInt64 = 1_000_000
static let maxDrops: UInt64 = 100_000_000_000 * dropsPerXRP // 1e11 XRP
static func drops(fromXRP value: String) throws -> UInt64 {
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty, !text.hasPrefix("-"), !text.hasPrefix("+") else {
throw XRPError.rpc("Enter a valid amount")
}
let parts = text.split(separator: ".", omittingEmptySubsequences: false)
guard parts.count <= 2 else { throw XRPError.rpc("Enter a valid amount") }
let wholeText = parts[0].isEmpty ? "0" : String(parts[0])
guard let whole = UInt64(wholeText) else { throw XRPError.rpc("Enter a valid amount") }
var fraction: UInt64 = 0
if parts.count == 2 {
let fractionText = String(parts[1])
guard fractionText.count <= 6, fractionText.allSatisfy({ $0.isNumber }) else {
throw XRPError.rpc("XRP supports at most 6 decimal places")
}
let padded = fractionText.padding(toLength: 6, withPad: "0", startingAt: 0)
guard let parsed = UInt64(padded) else { throw XRPError.rpc("Enter a valid amount") }
fraction = parsed
}
let (wholeDrops, overflow) = whole.multipliedReportingOverflow(by: dropsPerXRP)
guard !overflow else { throw XRPError.rpc("Amount is too large") }
let (total, carry) = wholeDrops.addingReportingOverflow(fraction)
guard !carry, total <= maxDrops else { throw XRPError.rpc("Amount is too large") }
return total
}
static func xrp(fromDrops drops: String) -> String {
guard let value = UInt64(drops.trimmingCharacters(in: .whitespacesAndNewlines)) else {
return "0"
}
return xrp(fromDrops: value)
}
static func xrp(fromDrops value: UInt64) -> String {
let whole = value / dropsPerXRP
let fraction = value % dropsPerXRP
if fraction == 0 { return String(whole) }
var fractionText = String(format: "%06llu", fraction)
while fractionText.hasSuffix("0") { fractionText.removeLast() }
return "\(whole).\(fractionText)"
}
}
+136
View File
@@ -0,0 +1,136 @@
import Foundation
/// Canonical XRP Ledger binary serialization for the Payment transactions this
/// wallet sends, plus the signing / transaction-id hashing around it.
struct XRPPayment {
var account: String
var destination: String
var amountDrops: UInt64
var feeDrops: UInt64
var sequence: UInt32
var lastLedgerSequence: UInt32?
var destinationTag: UInt32?
var signingPublicKey: [UInt8]
var signature: [UInt8]?
private static let typeUInt16: UInt8 = 1
private static let typeUInt32: UInt8 = 2
private static let typeAmount: UInt8 = 6
private static let typeBlob: UInt8 = 7
private static let typeAccountID: UInt8 = 8
func serialized(includingSignature: Bool) throws -> [UInt8] {
var output: [UInt8] = []
// Fields must appear in (type, field) order.
output += header(type: Self.typeUInt16, field: 2) // TransactionType
output += uint16(0) // ttPAYMENT
output += header(type: Self.typeUInt32, field: 2) // Flags
output += uint32(0)
output += header(type: Self.typeUInt32, field: 4) // Sequence
output += uint32(sequence)
if let tag = destinationTag {
output += header(type: Self.typeUInt32, field: 14) // DestinationTag
output += uint32(tag)
}
if let lastLedger = lastLedgerSequence {
output += header(type: Self.typeUInt32, field: 27) // LastLedgerSequence
output += uint32(lastLedger)
}
output += header(type: Self.typeAmount, field: 1) // Amount
output += try amount(amountDrops)
output += header(type: Self.typeAmount, field: 8) // Fee
output += try amount(feeDrops)
output += header(type: Self.typeBlob, field: 3) // SigningPubKey
output += variableLength(signingPublicKey)
if includingSignature, let signature {
output += header(type: Self.typeBlob, field: 4) // TxnSignature
output += variableLength(signature)
}
output += header(type: Self.typeAccountID, field: 1) // Account
output += variableLength(try XRPKey.accountId(fromAddress: account))
output += header(type: Self.typeAccountID, field: 3) // Destination
output += variableLength(try XRPKey.accountId(fromAddress: destination))
return output
}
/// Bytes the private key signs (HashPrefix.transactionSig + tx).
func signingPayload() throws -> [UInt8] {
[0x53, 0x54, 0x58, 0x00] + (try serialized(includingSignature: false))
}
/// The signed transaction blob submitted to the network (no hash prefix).
func txBlob() throws -> [UInt8] {
guard signature != nil else { throw XRPError.rpc("Transaction is not signed") }
return try serialized(includingSignature: true)
}
func transactionHash() throws -> [UInt8] {
guard signature != nil else { throw XRPError.rpc("Transaction is not signed") }
let blob = try serialized(includingSignature: true)
return XRPHash.sha512Half([0x54, 0x58, 0x4e, 0x00] + blob)
}
// MARK: - Encoding helpers
private func header(type: UInt8, field: UInt8) -> [UInt8] {
if field < 16 { return [(type << 4) | field] }
return [type << 4, field]
}
private func uint16(_ value: UInt16) -> [UInt8] {
[UInt8(value >> 8), UInt8(value & 0xff)]
}
private func uint32(_ value: UInt32) -> [UInt8] {
[
UInt8((value >> 24) & 0xff),
UInt8((value >> 16) & 0xff),
UInt8((value >> 8) & 0xff),
UInt8(value & 0xff),
]
}
/// Native (XRP) amount: 8 bytes big-endian drops with the XRP flag bit set.
private func amount(_ drops: UInt64) throws -> [UInt8] {
guard drops <= XRPAmount.maxDrops else { throw XRPError.rpc("Amount is too large") }
var bytes = uint64(drops)
bytes[0] |= 0x40
return bytes
}
private func uint64(_ value: UInt64) -> [UInt8] {
var bytes: [UInt8] = []
for shift in stride(from: 56, through: 0, by: -8) {
bytes.append(UInt8((value >> UInt64(shift)) & 0xff))
}
return bytes
}
private func variableLength(_ bytes: [UInt8]) -> [UInt8] {
var output: [UInt8] = []
let count = bytes.count
if count < 192 {
output.append(UInt8(count))
} else if count <= 12_480 {
output.append(UInt8((count >> 8) + 192))
output.append(UInt8(count & 0xff))
} else {
output.append(UInt8((count >> 16) + 240))
output.append(UInt8((count >> 8) & 0xff))
output.append(UInt8(count & 0xff))
}
return output + bytes
}
}
+72
View File
@@ -0,0 +1,72 @@
import Foundation
/// Minimal XRP Ledger JSON-RPC client. A couple of public endpoints are tried
/// in order so a single outage does not break the wallet.
final class XRPClient {
static let shared = XRPClient()
private let endpoints = [
"https://s1.ripple.com:51234/",
"https://s2.ripple.com:51234/",
"https://xrplcluster.com/",
]
private var preferredEndpoint = 0
private let session: URLSession
private init() {
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 20
configuration.waitsForConnectivity = false
session = URLSession(configuration: configuration)
}
func rpc(_ method: String, _ params: [String: Any] = [:]) async throws -> [String: Any] {
var lastError: Error = XRPError.network("XRP network is unavailable")
for attempt in 0..<endpoints.count {
let index = (preferredEndpoint + attempt) % endpoints.count
guard let url = URL(string: endpoints[index]) else { continue }
do {
let result = try await send(method, params, to: url)
preferredEndpoint = index
return result
} catch {
lastError = error
}
}
throw lastError
}
private func send(_ method: String, _ params: [String: Any], to url: URL) async throws -> [String: Any] {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = ["method": method, "params": [params], "id": 1]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw XRPError.network("Ledger request failed (\(method))")
}
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw XRPError.network("Malformed ledger response")
}
let result = json["result"] as? [String: Any] ?? [:]
if let error = result["error"] as? String {
let message = result["error_message"] as? String ?? error
throw XRPError.rpc(message)
}
if let error = json["error"] as? String {
throw XRPError.rpc(json["error_message"] as? String ?? error)
}
return result
}
static func isAccountNotFound(_ error: Error) -> Bool {
if case XRPError.rpc(let message) = error {
return message == "actNotFound" || message.contains("Account not found")
}
return false
}
}
+148
View File
@@ -0,0 +1,148 @@
import CryptoKit
import Foundation
enum XRPAlgorithm: String {
case ed25519
case secp256k1
}
/// Set by the app target so this file stays compilable outside iOS.
enum Secp256k1Backend {
static var derivePrivateKey: ((Data) -> Data?)?
static var publicKey: ((Data) -> Data?)?
static var sign: ((Data, Data) -> Data?)?
static var addScalar: ((Data, Data) -> Data?)?
}
struct XRPKeypair {
let algorithm: XRPAlgorithm
/// ed25519: 32-byte seed. secp256k1: 32-byte scalar.
let privateKey: [UInt8]
/// ed25519: 0xED + 32-byte key. secp256k1: 33-byte compressed point.
let publicKey: [UInt8]
var address: String { XRPKey.address(fromPublicKey: publicKey) }
var signingPublicKeyHex: String { Hex.encode(publicKey) }
}
enum XRPKey {
static let ed25519SeedVersion: [UInt8] = [0x01, 0xe1, 0x4b]
static let familySeedVersion: [UInt8] = [0x21]
static let accountIdVersion: [UInt8] = [0x00]
static let ed25519Prefix: UInt8 = 0xED
// MARK: - Seeds
static func generateSeed(algorithm: XRPAlgorithm = .ed25519) -> String {
var entropy = [UInt8](repeating: 0, count: 16)
for index in entropy.indices { entropy[index] = UInt8.random(in: 0...255) }
let version = algorithm == .ed25519 ? ed25519SeedVersion : familySeedVersion
return XRPBase58.checkEncode(version: version, payload: entropy)
}
static func decodeSeed(_ seed: String) throws -> (entropy: [UInt8], algorithm: XRPAlgorithm) {
let decoded = try XRPBase58.decode(seed.trimmingCharacters(in: .whitespacesAndNewlines))
guard decoded.count > 4 else { throw XRPError.invalidSeed }
let body = Array(decoded[0..<(decoded.count - 4)])
let checksum = Array(decoded[(decoded.count - 4)...])
guard XRPHash.checksum(body) == checksum else { throw XRPError.invalidSeed }
if body.count == ed25519SeedVersion.count + 16, Array(body.prefix(3)) == ed25519SeedVersion {
return (Array(body.dropFirst(3)), .ed25519)
}
if body.count == familySeedVersion.count + 16, body[0] == familySeedVersion[0] {
return (Array(body.dropFirst(1)), .secp256k1)
}
throw XRPError.unsupportedSeed
}
// MARK: - Keys
/// Recovery phrase for a new wallet: 12 BIP-39 words.
static func generateMnemonic() -> String {
BIP39.generateMnemonic()
}
/// Accepts either a 12 word BIP-39 recovery phrase or a legacy XRP family
/// seed (`sEd…` / `s…`).
static func keypair(fromSecret secret: String) throws -> XRPKeypair {
let trimmed = secret.trimmingCharacters(in: .whitespacesAndNewlines)
if BIP39.isMnemonic(trimmed) {
return try keypair(fromMnemonic: trimmed)
}
return try keypair(fromSeed: trimmed)
}
/// Derives the account at `m/44'/144'/0'/0/0`, the path hardware and mobile
/// wallets use for XRP.
static func keypair(fromMnemonic mnemonic: String) throws -> XRPKeypair {
guard BIP39.entropy(from: mnemonic) != nil else {
throw XRPError.invalidSeed
}
let seed = BIP39.seed(from: mnemonic)
guard let master = BIP32.masterKey(fromSeed: seed),
let derived = BIP32.derive(BIP32.xrpPath, from: master) else {
throw XRPError.unsupportedSeed
}
return XRPKeypair(algorithm: .secp256k1,
privateKey: derived.privateKey,
publicKey: derived.publicKey)
}
static func keypair(fromSeed seed: String) throws -> XRPKeypair {
let (entropy, algorithm) = try decodeSeed(seed)
switch algorithm {
case .ed25519:
let raw = XRPHash.sha512Half(entropy)
let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: Data(raw))
let publicKey = [ed25519Prefix] + Array(privateKey.publicKey.rawRepresentation)
return XRPKeypair(algorithm: .ed25519, privateKey: raw, publicKey: publicKey)
case .secp256k1:
guard let derive = Secp256k1Backend.derivePrivateKey,
let toPublic = Secp256k1Backend.publicKey,
let privateKey = derive(Data(entropy)),
let publicKey = toPublic(privateKey) else {
throw XRPError.unsupportedSeed
}
return XRPKeypair(algorithm: .secp256k1,
privateKey: Array(privateKey),
publicKey: Array(publicKey))
}
}
static func address(fromPublicKey publicKey: [UInt8]) -> String {
let accountId = XRPHash.ripemd160(XRPHash.sha256(publicKey))
return XRPBase58.checkEncode(version: accountIdVersion, payload: accountId)
}
static func isValidAddress(_ address: String) -> Bool {
(try? XRPBase58.checkDecode(address, versionLength: 1)) != nil
}
static func accountId(fromAddress address: String) throws -> [UInt8] {
let payload = try XRPBase58.checkDecode(address, versionLength: 1)
guard payload.count == 20 else { throw XRPError.invalidAddress }
return payload
}
// MARK: - Signing
/// ed25519 returns a 64-byte signature; secp256k1 a DER-encoded one, matching
/// what the XRP Ledger expects for each signing scheme.
static func sign(message: [UInt8], keypair: XRPKeypair) throws -> [UInt8] {
switch keypair.algorithm {
case .ed25519:
let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: Data(keypair.privateKey))
let signature = try privateKey.signature(for: Data(message))
return Array(signature)
case .secp256k1:
let digest = XRPHash.sha512Half(message)
guard let signFn = Secp256k1Backend.sign,
let signature = signFn(Data(digest), Data(keypair.privateKey)) else {
throw XRPError.unsupportedSeed
}
return Array(signature)
}
}
}
@@ -0,0 +1,157 @@
import Foundation
/// High level XRP operations: wallet creation/import, balances, history, sends.
final class XRPWalletService {
static let shared = XRPWalletService()
private let rippleEpoch: TimeInterval = 946_684_800
// MARK: - Wallet lifecycle
func createWallet(name: String) throws -> (WalletRecord, String) {
let mnemonic = XRPKey.generateMnemonic()
let keypair = try XRPKey.keypair(fromMnemonic: mnemonic)
let record = WalletRecord(id: "xrp_\(Int(Date().timeIntervalSince1970))",
name: name,
address: keypair.address,
createdAt: Date())
WalletStore.saveWallet(record, chain: .xrp)
WalletStore.saveSecret(mnemonic, chain: .xrp)
return (record, mnemonic)
}
func importWallet(name: String, secret: String) throws -> WalletRecord {
let trimmed = secret.trimmingCharacters(in: .whitespacesAndNewlines)
let keypair = try XRPKey.keypair(fromSecret: trimmed)
let record = WalletRecord(id: "xrp_\(Int(Date().timeIntervalSince1970))",
name: name,
address: keypair.address,
createdAt: Date())
WalletStore.saveWallet(record, chain: .xrp)
WalletStore.saveSecret(trimmed, chain: .xrp)
return record
}
func address(forSecret secret: String) throws -> String {
try XRPKey.keypair(fromSecret: secret.trimmingCharacters(in: .whitespacesAndNewlines)).address
}
// MARK: - Reads
func balance(address: String) async throws -> Balance {
do {
let result = try await XRPClient.shared.rpc("account_info",
["account": address,
"ledger_index": "current"])
let accountData = result["account_data"] as? [String: Any] ?? [:]
let drops = accountData["Balance"] as? String ?? "0"
return Balance(total: XRPAmount.xrp(fromDrops: drops), unlocked: "", funded: true)
} catch {
if XRPClient.isAccountNotFound(error) {
return Balance(total: "0", unlocked: "", funded: false)
}
throw error
}
}
func transactions(address: String) async throws -> [TransactionRecord] {
let result = try await XRPClient.shared.rpc("account_tx",
["account": address,
"ledger_index_min": -1,
"ledger_index_max": -1,
"limit": 40])
let entries = result["transactions"] as? [[String: Any]] ?? []
var records: [TransactionRecord] = []
for entry in entries {
guard let tx = entry["tx_json"] as? [String: Any] ?? entry["tx"] as? [String: Any] else { continue }
guard (tx["TransactionType"] as? String) == "Payment" else { continue }
let meta = entry["meta"] as? [String: Any] ?? entry["metaData"] as? [String: Any] ?? [:]
if let txResult = meta["TransactionResult"] as? String, txResult != "tesSUCCESS" { continue }
// IOU / MPT deliveries carry an object; only native XRP is shown.
let delivered = (meta["delivered_amount"] as? String)
?? (meta["DeliveredAmount"] as? String)
?? (tx["Amount"] as? String)
guard let deliveredDrops = delivered else { continue }
let destination = tx["Destination"] as? String ?? ""
let isIncoming = destination == address
let date = (tx["date"] as? NSNumber).map { Date(timeIntervalSince1970: $0.doubleValue + rippleEpoch) }
records.append(TransactionRecord(
id: entry["hash"] as? String ?? tx["hash"] as? String ?? UUID().uuidString,
amount: XRPAmount.xrp(fromDrops: deliveredDrops),
timestamp: date,
direction: isIncoming ? .incoming : .outgoing,
confirmed: (entry["validated"] as? Bool) ?? false,
fee: (tx["Fee"] as? String).map { XRPAmount.xrp(fromDrops: $0) },
tag: (tx["DestinationTag"] as? NSNumber)?.intValue,
counterparty: isIncoming ? (tx["Account"] as? String) : destination))
}
return records
}
// MARK: - Sends
func send(secret: String,
to: String,
amountXRP: String,
destinationTag: UInt32?) async throws -> (hash: String, fee: String) {
let destination = to.trimmingCharacters(in: .whitespacesAndNewlines)
guard XRPKey.isValidAddress(destination) else {
throw XRPError.rpc("Enter a valid XRP address")
}
let keypair = try XRPKey.keypair(fromSeed: secret)
let amountDrops = try XRPAmount.drops(fromXRP: amountXRP)
async let infoCall = XRPClient.shared.rpc("account_info",
["account": keypair.address,
"ledger_index": "current"])
async let feeCall = XRPClient.shared.rpc("fee", [:])
async let ledgerCall = XRPClient.shared.rpc("ledger_current", [:])
let info = try await infoCall
let accountData = info["account_data"] as? [String: Any] ?? [:]
guard let sequence = (accountData["Sequence"] as? NSNumber)?.uint32Value else {
throw XRPError.rpc("Could not read the account sequence")
}
let balanceDrops = UInt64((accountData["Balance"] as? String) ?? "0") ?? 0
let feeResult = try await feeCall
let feeDrops = (feeResult["drops"] as? [String: Any])
.flatMap { $0["open_ledger_fee"] as? String }
.flatMap { UInt64($0) } ?? 12
let ledger = try await ledgerCall
let ledgerIndex = (ledger["ledger_current_index"] as? NSNumber)?.uint32Value
guard amountDrops + feeDrops <= balanceDrops else {
throw XRPError.rpc("Insufficient balance to cover the amount and network fee")
}
var payment = XRPPayment(account: keypair.address,
destination: destination,
amountDrops: amountDrops,
feeDrops: feeDrops,
sequence: sequence,
lastLedgerSequence: ledgerIndex.map { $0 + 20 },
destinationTag: destinationTag,
signingPublicKey: keypair.publicKey,
signature: nil)
let signature = try XRPKey.sign(message: payment.signingPayload(), keypair: keypair)
payment.signature = signature
let blob = Hex.encode(try payment.txBlob())
let result = try await XRPClient.shared.rpc("submit", ["tx_blob": blob])
let engineResult = result["engine_result"] as? String ?? ""
guard engineResult.hasPrefix("tes") else {
let message = result["engine_result_message"] as? String ?? "Transaction failed"
throw XRPError.rpc("\(engineResult): \(message)")
}
return (Hex.encode(try payment.transactionHash()), XRPAmount.xrp(fromDrops: feeDrops))
}
}
+91
View File
@@ -0,0 +1,91 @@
# Koins iOS
Native SwiftUI wallet for Monero and XRP. iOS only, no JavaScript runtime.
## Layout
```
Koins/
KoinsApp.swift App entry point
AppModel.swift Observable state, orchestrates both chains
Models.swift Chain / wallet / transaction models
SelfTest.swift Launch-time verification (see below)
Bridge/
Koins-Bridging-Header.h
KoinsMoneroWallet.h/.mm ObjC++ bridge to the monero-cpp wallet
KoinsSecp256k1.h/.mm secp256k1 via the OpenSSL bundled in the monero lib
Monero/
libs/ libkoins_monero_merged.a (device + simulator arm64)
include/ monero-cpp headers
Support/ Base58, RIPEMD-160, hashing, Keychain, wallet store
Wallet/ XRP keys, binary codec, RPC client, services
Views/ SwiftUI screens
```
## How the chains are implemented
- **Monero**: the wallet runs on device through `libkoins_monero_merged.a`
(monero-project `wallet2` + monero-cpp, built for `aarch64-apple-ios` and the
arm64 simulator). `KoinsMoneroWallet` serialises all C++ work on a private
queue and exchanges JSON with Swift. Wallet files live in the app's Documents
directory; the recovery seed is stored in the Keychain.
- **XRP**: implemented directly in Swift, no third-party package.
- Recovery phrase: a 12 word BIP-39 mnemonic, derived to the standard XRP
account path `m/44'/144'/0'/0/0` (BIP-32, secp256k1), so the same words
restore the same account in hardware and other mobile wallets.
- Imports also accept legacy XRP family seeds (`sEd…` / `s…`), including the
Ed25519 and secp256k1 variants.
- Key derivation and signing: Ed25519 via CryptoKit and secp256k1 via the
OpenSSL build embedded in the monero static library.
- Transaction serialization: canonical XRP binary encoding plus the
`STX`/`TXN` hash prefixes, matching `ripple-binary-codec`.
- Network: JSON-RPC against public XRP Ledger endpoints with failover.
## Build
```sh
cd apps/ios
xcodegen generate
xcodebuild -project Koins.xcodeproj -scheme Koins -configuration Debug \
-sdk iphonesimulator -destination 'generic/platform=iOS Simulator' \
ARCHS=arm64 ONLY_ACTIVE_ARCH=NO -derivedDataPath /tmp/koins-dd build
```
Install and run on a booted simulator:
```sh
xcrun simctl install <udid> /tmp/koins-dd/Build/Products/Debug-iphonesimulator/Koins.app
xcrun simctl launch <udid> com.koins.app
```
Re-run `xcodegen generate` after adding or removing source files. No CocoaPods
and no Swift packages are used.
## Verification
The XRP implementation is checked against vectors produced by the reference
JavaScript stack (`xrpl`, `ripple-keypairs`, `ripple-binary-codec`).
Cryptography and serialization (no network):
```sh
xcrun simctl launch <udid> com.koins.app -koinsSelfTest
xcrun simctl spawn <udid> log show --last 1m --predicate 'subsystem == "com.koins.app"' --style compact
```
Live network check (creates and then removes a temporary wallet on both chains,
talks to a Monero daemon and the XRP Ledger):
```sh
xcrun simctl launch <udid> com.koins.app -koinsSelfTestNetwork
```
## Notes
- The Monero daemon default is `xmr-node.cakewallet.com:18081` with two
fallbacks; `MoneroService` reconnects on demand.
- Ed25519 and OpenSSL ECDSA both use randomised nonces, so signatures are valid
but not byte-identical to deterministic implementations such as
`ripple-keypairs`. Signatures are verified rather than compared.
- Refresh the simulator build when `/tmp` is cleared: the intermediate build
products live in `/tmp/koins-dd`.
+70
View File
@@ -0,0 +1,70 @@
name: Koins
options:
bundleIdPrefix: com.koins
deploymentTarget:
iOS: "16.0"
xcodeVersion: "16.0"
settings:
base:
ENABLE_USER_SCRIPT_SANDBOXING: NO
CLANG_CXX_LANGUAGE_STANDARD: "c++14"
GCC_TREAT_WARNINGS_AS_ERRORS: NO
CLANG_CXX_LIBRARY: "libc++"
SWIFT_VERSION: "5.0"
targets:
Koins:
type: application
platform: iOS
sources:
- Koins
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.koins.app
INFOPLIST_FILE: Koins/Info.plist
CODE_SIGN_STYLE: Automatic
CODE_SIGNING_ALLOWED: NO
SWIFT_OBJC_BRIDGING_HEADER: Koins/Bridge/Koins-Bridging-Header.h
OTHER_LDFLAGS:
- "$(inherited)"
- "-lkoins_monero_merged"
LIBRARY_SEARCH_PATHS:
- "$(SRCROOT)/Koins/Monero/libs"
HEADER_SEARCH_PATHS:
- "$(SRCROOT)/Koins/Monero/include/monero-cpp-src"
- "$(SRCROOT)/Koins/Monero/include/monero-cpp-src/wallet"
- "$(SRCROOT)/Koins/Monero/include/monero-cpp-src/common"
- "$(SRCROOT)/Koins/Monero/include/monero-cpp-src/utils"
- "$(SRCROOT)/Koins/Monero/include/monero-cpp-src/daemon"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/src"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/contrib"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/external/rapidjson/include"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/contrib/epee/include"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/external/easylogging++"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/external/boost"
- "$(SRCROOT)/../../build/monero-cpp/external/monero-project/contrib/depends/aarch64-apple-ios/include"
OTHER_CPLUSPLUSFLAGS:
- "$(inherited)"
- "-DKOINS_MONERO_NATIVE=1"
- "-DBOOST_UUID_DISABLE_ALIGNMENT"
GCC_PREPROCESSOR_DEFINITIONS:
- "$(inherited)"
- "KOINS_MONERO_NATIVE=1"
info:
path: Koins/Info.plist
properties:
UILaunchScreen: {}
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
UIUserInterfaceStyle: Dark
NSAppTransportSecurity:
NSAllowsArbitraryLoads: true
schemes:
Koins:
build:
targets:
Koins: all
run:
config: Debug
-16
View File
@@ -1,16 +0,0 @@
# Local
.DS_Store
*.local
*.log*
# Dist
node_modules
dist/
# IDE
.vscode/*
!.vscode/extensions.json
.idea
# TypeScript
*.tsbuildinfo
-3
View File
@@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar"]
}
-24
View File
@@ -1,24 +0,0 @@
# AGENTS.md
You are an expert in JavaScript, Rspeedy, and Vue Lynx application development. You write maintainable, performant, and accessible code.
## Read in Advance
- Vue Lynx: [llms.txt](https://vue.lynxjs.org/llms.txt), **REQUIRED**.
While dealing with a Vue Lynx task, an agent **MUST** read this doc because it is an entry point of all available docs about Vue Lynx.
## Commands
- `npm run dev` - Start the dev server
- `npm run build` - Build the app for production
- `npm run preview` - Preview the production build locally
- `npm exec rspeedy inspect` - Inspect the Rspeedy config and Rspack config of the project.
## Related Docs
- Rsbuild: <https://rsbuild.rs/llms.txt>
- Rspack: <https://rspack.rs/llms.txt>
-27
View File
@@ -1,27 +0,0 @@
## Vue Lynx project
This is a Vue Lynx project bootstrapped with `create-vue-lynx`.
## IDE Setup
This project is pre-configured for [Vue Language Features (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.volar). VS Code will prompt you to install it automatically.
Volar provides prop completions, type checking, and Lynx-specific diagnostics for `.vue` files.
## Getting Started
First, install the dependencies:
```bash
pnpm install
```
Then, run the development server:
```bash
pnpm run dev
```
Scan the QRCode in the terminal with your LynxExplorer App to see the result.
You can start editing the page by modifying `src/App.vue`. The page auto-updates as you edit the file.
-40
View File
@@ -1,40 +0,0 @@
import { defineConfig } from '@lynx-js/rspeedy'
import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin'
import { pluginVueLynx } from 'vue-lynx/plugin'
import { pluginTailwindCSS } from 'rsbuild-plugin-tailwindcss'
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill'
export default defineConfig({
environments: {
lynx: {
resolve: {
alias: {
// crypto-browserify -> create-hash -> require('crypto') forms a
// circular dependency; webpack resolves createHash to undefined in
// the lynx build. Point create-hash at its browser implementation
// (no require('crypto') cycle).
'create-hash$': '/Users/tzgyn/Git/koins/node_modules/.bun/create-hash@1.2.0/node_modules/create-hash/browser.js',
},
},
},
web: {},
},
plugins: [
pluginNodePolyfill(),
pluginQRCode({
schema(url) {
return `${url}?fullscreen=true`
},
}),
pluginVueLynx({
optionsApi: false,
enableCSSInlineVariables: true,
enableCSSInheritance: true,
}),
pluginTailwindCSS({
config: 'tailwind.config.ts',
exclude: [/[\\/]node_modules[\\/]/],
}),
],
})
-28
View File
@@ -1,28 +0,0 @@
{
"name": "mobile",
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "rspeedy build",
"dev": "rspeedy dev",
"preview": "rspeedy preview"
},
"dependencies": {
"vue": "^3.5.0",
"vue-lynx": "^0.5.1",
"xrpl": "^5.0.0"
},
"devDependencies": {
"@lynx-js/qrcode-rsbuild-plugin": "^0.4.6",
"@lynx-js/rspeedy": "^0.13.5",
"@lynx-js/tailwind-preset": "^0.5.1",
"@rsbuild/plugin-node-polyfill": "^1.4.6",
"@rsbuild/plugin-vue": "^1.2.6",
"rsbuild-plugin-tailwindcss": "^0.2.4",
"tailwindcss": "3",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=18"
}
}
-5
View File
@@ -1,5 +0,0 @@
export default {
plugins: {
tailwindcss: {},
},
}
-29
View File
@@ -1,29 +0,0 @@
@tailwind base;
@tailwind utilities;
:root {
background-color: #0a0a0b;
color: #fafafa;
}
page {
background-color: #0a0a0b;
}
input {
background-color: #18181b;
color: #fafafa;
caret-color: #fafafa;
}
scroll-view {
background-color: transparent;
}
view {
background-color: transparent;
}
text {
color: #fafafa;
}
-399
View File
@@ -1,399 +0,0 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue-lynx'
import './App.css'
import {
generateWallet,
walletFromSecret,
isValidAddress,
getWallet,
saveWallet,
deleteWallet,
getSeed,
saveSeed,
getBalance,
getTransactions,
sendPayment,
type XrpWalletInfo,
type XrpBalance,
type XrpTxEntry,
} from './lib/xrp'
type Screen =
| 'welcome'
| 'create'
| 'import'
| 'dashboard'
| 'send'
| 'receive'
const screen = ref<Screen>('welcome')
const wallet = ref<XrpWalletInfo | null>(null)
const seed = ref<string | null>(null)
const balance = ref<XrpBalance | null>(null)
const transactions = ref<XrpTxEntry[]>([])
const loading = ref(false)
const error = ref('')
const success = ref('')
const newName = ref('')
const newMnemonic = ref('')
const importName = ref('')
const importSecret = ref('')
const createdSeed = ref('')
const sendTo = ref('')
const sendAmount = ref('')
const sendTag = ref('')
const sending = ref(false)
const showAddress = computed(() => wallet.value?.address ?? '')
const truncatedAddress = computed(() => {
const a = showAddress.value
if (a.length <= 16) return a
return `${a.slice(0, 8)}...${a.slice(-8)}`
})
onMounted(() => {
wallet.value = getWallet()
if (wallet.value) {
seed.value = getSeed()
screen.value = 'dashboard'
refresh()
}
})
async function refresh() {
const w = wallet.value
if (!w) return
loading.value = true
error.value = ''
try {
const [bal, txs] = await Promise.all([
getBalance(w.address),
getTransactions(w.address),
])
balance.value = bal
transactions.value = txs
} catch (e) {
console.info('[koins] refresh error:', e instanceof Error ? e.message : String(e))
error.value = e instanceof Error ? e.message : 'Failed to load wallet'
} finally {
loading.value = false
}
}
function handleCreate() {
error.value = ''
if (!newName.value.trim()) {
error.value = 'Enter a wallet name'
return
}
try {
const { address, seed: newSeed } = generateWallet()
const w: XrpWalletInfo = {
id: `xrp_${Date.now()}`,
name: newName.value.trim(),
address,
createdAt: new Date().toISOString(),
}
wallet.value = w
seed.value = newSeed
saveWallet(w)
saveSeed(newSeed)
createdSeed.value = newSeed
newName.value = ''
screen.value = 'create'
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to create wallet'
}
}
function finishCreate() {
createdSeed.value = ''
screen.value = 'dashboard'
refresh()
}
function handleImport() {
error.value = ''
if (!importName.value.trim()) {
error.value = 'Enter a wallet name'
return
}
if (!importSecret.value.trim()) {
error.value = 'Enter your seed or secret'
return
}
try {
const { address } = walletFromSecret(importSecret.value)
const w: XrpWalletInfo = {
id: `xrp_${Date.now()}`,
name: importName.value.trim(),
address,
createdAt: new Date().toISOString(),
}
wallet.value = w
seed.value = importSecret.value.trim()
saveWallet(w)
saveSeed(importSecret.value.trim())
importName.value = ''
importSecret.value = ''
screen.value = 'dashboard'
refresh()
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to import wallet'
}
}
function handleSend() {
error.value = ''
if (!isValidAddress(sendTo.value.trim())) {
error.value = 'Invalid XRP address'
return
}
const amount = parseFloat(sendAmount.value)
if (!amount || amount <= 0) {
error.value = 'Enter a valid amount'
return
}
if (!seed.value) {
error.value = 'Wallet is locked'
return
}
const bal = parseFloat(balance.value?.balance ?? '0')
if (amount > bal) {
error.value = 'Insufficient balance'
return
}
sending.value = true
sendPayment(
seed.value,
sendTo.value.trim(),
sendAmount.value,
sendTag.value ? parseInt(sendTag.value, 10) : undefined,
)
.then(() => {
success.value = 'Payment sent'
sendTo.value = ''
sendAmount.value = ''
sendTag.value = ''
refresh()
})
.catch((e) => {
error.value = e instanceof Error ? e.message : 'Failed to send'
})
.finally(() => {
sending.value = false
})
}
function handleDelete() {
if (!wallet.value) return
deleteWallet()
wallet.value = null
seed.value = null
balance.value = null
transactions.value = []
screen.value = 'welcome'
}
function goDashboard() {
screen.value = 'dashboard'
refresh()
}
function formatDate(ts: number): string {
if (!ts) return 'pending'
const d = new Date(ts * 1000)
const month = d.toLocaleDateString(undefined, { month: 'short' })
return `${month} ${d.getDate()}`
}
function truncateHash(hash: string): string {
if (hash.length <= 14) return hash
return `${hash.slice(0, 7)}...${hash.slice(-7)}`
}
</script>
<template>
<view class="app flex-1 bg-background px-5 pt-12 pb-6">
<!-- Welcome -->
<view v-if="screen === 'welcome'" class="flex flex-col items-center">
<text class="mt-12 text-5xl font-bold text-foreground">Koins</text>
<text class="mt-1 mb-6 text-lg text-muted">XRP Wallet</text>
<text class="mb-4 text-sm text-center text-muted">
Create or import your XRP wallet
</text>
<view class="mt-5 flex w-full flex-row gap-3">
<view class="flex-1 bg-primary h-[52px] rounded-xl items-center justify-center" @tap="screen = 'create'">
<text class="text-base font-semibold text-primary-foreground">Create Wallet</text>
</view>
<view class="flex-1 bg-zinc-800 h-[52px] rounded-xl items-center justify-center" @tap="screen = 'import'">
<text class="text-base font-semibold text-foreground">Import</text>
</view>
</view>
</view>
<!-- Create -->
<view v-else-if="screen === 'create'" class="flex flex-col items-center">
<text class="self-start mb-5 text-[26px] font-semibold text-foreground">Create Wallet</text>
<text v-if="!createdSeed" class="mb-4 text-sm text-center text-muted">Choose a name for your new wallet</text>
<view v-if="!createdSeed" class="flex w-full flex-col gap-3">
<input
class="input h-[52px] w-full rounded-xl border border-border bg-card px-4 text-base text-foreground"
:value="newName"
@input="newName = $event.detail.value"
placeholder="Wallet name"
placeholder-class="text-muted"
/>
<view class="h-[52px] bg-primary w-full rounded-xl items-center justify-center" @tap="handleCreate">
<text class="text-base font-semibold text-primary-foreground">Generate Wallet</text>
</view>
</view>
<view v-else class="flex w-full flex-col gap-2.5 rounded-2xl border border-yellow-800 bg-yellow-950 p-5">
<text class="text-lg font-semibold text-yellow-200">Your Seed Phrase</text>
<text class="text-[13px] text-yellow-300">Write this down. It is the only way to recover your wallet. Never share it.</text>
<text class="rounded-[10px] border border-yellow-800 bg-card p-3.5 text-[15px] leading-6 text-foreground">
{{ createdSeed }}
</text>
<view class="h-[52px] bg-primary w-full rounded-xl items-center justify-center" @tap="finishCreate">
<text class="text-base font-semibold text-primary-foreground">I've Saved It</text>
</view>
</view>
<text v-if="error" class="mt-3 text-center text-[13px] text-destructive">{{ error }}</text>
</view>
<!-- Import -->
<view v-else-if="screen === 'import'" class="flex flex-col items-center">
<text class="self-start mb-5 text-[26px] font-semibold text-foreground">Import Wallet</text>
<view class="flex w-full flex-col gap-3">
<input
class="input h-[52px] w-full rounded-xl border border-border bg-card px-4 text-base text-foreground"
:value="importName"
@input="importName = $event.detail.value"
placeholder="Wallet name"
placeholder-class="text-muted"
/>
<input
class="input h-[52px] w-full rounded-xl border border-border bg-card px-4 text-base text-foreground"
:value="importSecret"
@input="importSecret = $event.detail.value"
placeholder="Seed phrase or secret"
placeholder-class="text-muted"
/>
<view class="h-[52px] bg-primary w-full rounded-xl items-center justify-center" @tap="handleImport">
<text class="text-base font-semibold text-primary-foreground">Import Wallet</text>
</view>
</view>
<text v-if="error" class="mt-3 text-center text-[13px] text-destructive">{{ error }}</text>
</view>
<!-- Dashboard -->
<view v-else-if="screen === 'dashboard'" class="flex flex-col items-center">
<view class="flex w-full flex-row items-start justify-between">
<view class="flex flex-col">
<text class="text-[17px] font-semibold text-foreground">{{ wallet?.name ?? 'Wallet' }}</text>
<text class="mt-0.5 text-[13px] text-muted">{{ truncatedAddress }}</text>
</view>
<view class="flex flex-row gap-3">
<text class="text-sm text-primary" @tap="handleDelete">Delete</text>
</view>
</view>
<view class="mt-5 flex w-full flex-col items-center rounded-2xl border border-border bg-card p-7">
<text class="text-sm text-muted">Total Balance</text>
<text class="mt-1.5 text-4xl font-bold text-foreground">{{ loading && !balance ? '...' : (balance?.balance ?? '0') }} XRP</text>
<text v-if="balance && !balance.funded" class="mt-1.5 text-[13px] text-yellow-400">Account not funded yet</text>
<text v-if="error" class="mt-3 text-center text-[13px] text-destructive">{{ error }}</text>
</view>
<view class="mt-5 flex w-full flex-row gap-3">
<view class="flex-1 bg-primary h-[52px] rounded-xl items-center justify-center" @tap="screen = 'send'">
<text class="text-base font-semibold text-primary-foreground">Send</text>
</view>
<view class="flex-1 bg-zinc-800 h-[52px] rounded-xl items-center justify-center" @tap="screen = 'receive'">
<text class="text-base font-semibold text-foreground">Receive</text>
</view>
</view>
<text class="self-start mt-7 mb-2 text-lg font-semibold text-foreground">Transactions</text>
<scroll-view class="tx-list flex w-full flex-col" :scroll-y="true">
<view v-if="transactions.length === 0 && !loading" class="py-6">
<text class="text-sm text-center text-muted">No transactions yet</text>
</view>
<view v-for="tx in transactions" :key="tx.hash" class="mb-2 flex w-full flex-row items-center rounded-xl bg-card p-3.5">
<view
class="mr-3 h-9 w-9 rounded-full items-center justify-center"
:class="tx.direction === 'in' ? 'bg-emerald-950' : 'bg-red-950'"
>
<text class="text-lg font-bold" :class="tx.direction === 'in' ? 'text-emerald-400' : 'text-red-400'">
{{ tx.direction === 'in' ? '↓' : '↑' }}
</text>
</view>
<view class="flex flex-1 flex-col">
<text class="text-[15px] font-medium text-foreground">{{ tx.direction === 'in' ? 'Received' : 'Sent' }}</text>
<text class="mt-0.5 text-xs text-muted">{{ formatDate(tx.timestamp) }} · {{ truncateHash(tx.hash) }}</text>
</view>
<text class="text-[15px] font-semibold" :class="tx.direction === 'in' ? 'text-emerald-400' : 'text-red-400'">
{{ tx.direction === 'in' ? '+' : '-' }}{{ tx.amount }}
</text>
</view>
</scroll-view>
</view>
<!-- Send -->
<view v-else-if="screen === 'send'" class="flex flex-col items-center">
<text class="self-start mb-5 text-[26px] font-semibold text-foreground">Send XRP</text>
<view class="flex w-full flex-col gap-3">
<input
class="input h-[52px] w-full rounded-xl border border-border bg-card px-4 text-base text-foreground"
:value="sendTo"
@input="sendTo = $event.detail.value"
placeholder="Destination address (r...)"
placeholder-class="text-muted"
/>
<input
class="input h-[52px] w-full rounded-xl border border-border bg-card px-4 text-base text-foreground"
:value="sendAmount"
@input="sendAmount = $event.detail.value"
placeholder="Amount (XRP)"
placeholder-class="text-muted"
/>
<input
class="input h-[52px] w-full rounded-xl border border-border bg-card px-4 text-base text-foreground"
:value="sendTag"
@input="sendTag = $event.detail.value"
placeholder="Destination tag (optional)"
placeholder-class="text-muted"
/>
<view class="h-[52px] bg-primary w-full rounded-xl items-center justify-center" @tap="handleSend">
<text class="text-base font-semibold text-primary-foreground">{{ sending ? 'Sending...' : 'Send' }}</text>
</view>
<view class="h-[52px] bg-zinc-800 w-full rounded-xl items-center justify-center" @tap="goDashboard">
<text class="text-base font-semibold text-foreground">Cancel</text>
</view>
</view>
<text v-if="error" class="mt-3 text-center text-[13px] text-destructive">{{ error }}</text>
<text v-if="success" class="mt-3 text-center text-[13px] text-success">{{ success }}</text>
</view>
<!-- Receive -->
<view v-else-if="screen === 'receive'" class="flex flex-col items-center">
<text class="self-start mb-5 text-[26px] font-semibold text-foreground">Receive XRP</text>
<view class="flex w-full flex-col items-center rounded-2xl border border-border bg-card p-7">
<text class="text-sm leading-6 text-center text-foreground">{{ showAddress }}</text>
<text class="mt-4 text-sm text-center text-muted">Share this address to receive XRP</text>
</view>
<view class="mt-5 h-[52px] bg-primary w-full rounded-xl items-center justify-center" @tap="goDashboard">
<text class="text-base font-semibold text-primary-foreground">Done</text>
</view>
</view>
</view>
</template>
-20
View File
@@ -1,20 +0,0 @@
import './lib/text-encoder'
declare const lynxCoreInject: { tt?: Record<string, any> } | undefined
// The LynxExplorer SDK calls tt.callDestroyLifetimeFun() when destroying a
// card. vue-lynx's runtime does not define it, which throws during page
// teardown and can break subsequent loads. Provide the expected no-op hook.
if (typeof lynxCoreInject !== 'undefined' && lynxCoreInject?.tt) {
const tt = lynxCoreInject.tt
if (typeof tt.callDestroyLifetimeFun !== 'function') {
tt.callDestroyLifetimeFun = () => {}
}
}
import { createApp } from 'vue-lynx'
import App from './App.vue'
const app = createApp(App)
app.mount()
-38
View File
@@ -1,38 +0,0 @@
declare const lynx: {
getStorageSync(key: string): string
setStorageSync(key: string, value: string): void
removeStorageSync(key: string): void
} | undefined
function lynxStorageAvailable(): boolean {
return typeof lynx !== 'undefined' && typeof lynx.getStorageSync === 'function'
}
export function getItem(key: string): string | null {
try {
const ls = globalThis.localStorage?.getItem(key)
if (ls !== null && ls !== undefined) return ls
} catch {}
try {
if (lynxStorageAvailable()) return lynx!.getStorageSync(key)
} catch {}
return null
}
export function setItem(key: string, value: string): void {
try {
globalThis.localStorage?.setItem(key, value)
} catch {}
try {
if (lynxStorageAvailable()) lynx!.setStorageSync(key, value)
} catch {}
}
export function removeItem(key: string): void {
try {
globalThis.localStorage?.removeItem(key)
} catch {}
try {
if (lynxStorageAvailable()) lynx!.removeStorageSync(key)
} catch {}
}
-75
View File
@@ -1,75 +0,0 @@
// TextEncoder/TextDecoder polyfill for runtimes that lack them (Lynx PrimJS).
// Needed by @noble/hashes (xrpl crypto stack) and other WASM-adjacent libs.
// Must be imported BEFORE any module that transitively imports xrpl, because
// @noble/curves calls utf8ToBytes at module-evaluation time.
if (typeof globalThis.TextEncoder === 'undefined') {
const utf8Encode = (str: string): Uint8Array => {
const bytes: number[] = []
for (let i = 0; i < str.length; i++) {
let code = str.charCodeAt(i)
if (code < 0x80) {
bytes.push(code)
} else if (code < 0x800) {
bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f))
} else if (code >= 0xd800 && code <= 0xdbff && i + 1 < str.length) {
const low = str.charCodeAt(i + 1)
if (low >= 0xdc00 && low <= 0xdfff) {
i++
code = 0x10000 + ((code - 0xd800) << 10) + (low - 0xdc00)
bytes.push(
0xf0 | (code >> 18),
0x80 | ((code >> 12) & 0x3f),
0x80 | ((code >> 6) & 0x3f),
0x80 | (code & 0x3f),
)
} else {
bytes.push(0xef, 0xbf, 0xbd)
}
} else if (code >= 0xdc00 && code <= 0xdfff) {
bytes.push(0xef, 0xbf, 0xbd)
} else {
bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f))
}
}
return Uint8Array.from(bytes)
}
globalThis.TextEncoder = class TextEncoder {
encoding = 'utf-8'
encode(input: string = ''): Uint8Array {
return utf8Encode(input)
}
} as unknown as typeof TextEncoder
}
if (typeof globalThis.TextDecoder === 'undefined') {
globalThis.TextDecoder = class TextDecoder {
encoding = 'utf-8'
decode(bytes?: Uint8Array): string {
if (!bytes) return ''
let result = ''
for (let i = 0; i < bytes.length; i++) {
const b = bytes[i]
if (b < 0x80) {
result += String.fromCharCode(b)
} else if (b < 0xe0 && i + 1 < bytes.length) {
result += String.fromCharCode(((b & 0x1f) << 6) | (bytes[++i] & 0x3f))
} else if (b < 0xf0 && i + 2 < bytes.length) {
result += String.fromCharCode(
((b & 0x0f) << 12) | ((bytes[++i] & 0x3f) << 6) | (bytes[++i] & 0x3f),
)
} else if (i + 3 < bytes.length) {
const cp =
((b & 0x07) << 18) |
((bytes[++i] & 0x3f) << 12) |
((bytes[++i] & 0x3f) << 6) |
(bytes[++i] & 0x3f)
result += String.fromCodePoint(cp)
}
}
return result
}
} as unknown as typeof TextDecoder
}
export {}
-201
View File
@@ -1,201 +0,0 @@
import { Wallet, xrpToDrops, dropsToXrp, isValidClassicAddress } from 'xrpl'
import { getItem, setItem, removeItem } from './storage'
const RPC_URL = 'https://s1.ripple.com:51234/'
const RIPPLE_EPOCH = 946684800
const WALLET_KEY = 'koins_xrp_wallet'
const SEED_KEY = 'koins_xrp_seed'
export interface XrpWalletInfo {
id: string
name: string
address: string
createdAt: string
}
export interface XrpBalance {
balance: string
funded: boolean
}
export interface XrpTxEntry {
hash: string
amount: string
timestamp: number
direction: 'in' | 'out'
fee?: string
confirmed: boolean
destinationTag?: number
}
async function rpc(command: string, params: Record<string, unknown>): Promise<any> {
const res = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
method: command,
params: [params],
id: 1,
}),
})
if (!res.ok) throw new Error(`RPC ${command} failed: HTTP ${res.status}`)
const data = await res.json()
const err = data?.result?.error ?? data?.error
if (err) {
const msg = data?.result?.error_message ?? data?.error_message ?? err
const e = new Error(String(msg))
;(e as any).code = err
throw e
}
return data.result
}
export function isValidAddress(address: string): boolean {
return isValidClassicAddress(address)
}
export function generateWallet(): { address: string; seed: string } {
const wallet = Wallet.generate()
return { address: wallet.classicAddress, seed: wallet.seed! }
}
export function walletFromSecret(secret: string): { address: string } {
const s = secret.trim()
try {
return { address: Wallet.fromSeed(s).classicAddress }
} catch {
return { address: Wallet.fromMnemonic(s).classicAddress }
}
}
export function walletForSigning(secret: string): Wallet {
const s = secret.trim()
try {
return Wallet.fromSeed(s)
} catch {
return Wallet.fromMnemonic(s)
}
}
export function getWallet(): XrpWalletInfo | null {
const raw = getItem(WALLET_KEY)
if (!raw) return null
try {
return JSON.parse(raw) as XrpWalletInfo
} catch {
return null
}
}
export function saveWallet(wallet: XrpWalletInfo): void {
setItem(WALLET_KEY, JSON.stringify(wallet))
}
export function deleteWallet(): void {
removeItem(WALLET_KEY)
removeItem(SEED_KEY)
}
export function getSeed(): string | null {
return getItem(SEED_KEY)
}
export function saveSeed(seed: string): void {
setItem(SEED_KEY, seed)
}
export async function getBalance(address: string): Promise<XrpBalance> {
try {
const result = await rpc('account_info', {
account: address,
ledger_index: 'current',
})
const balance = (result.account_data?.Balance ?? '0') as string
return { balance: dropsAmount(balance), funded: true }
} catch (e: any) {
if (e?.code === 'actNotFound') {
return { balance: '0', funded: false }
}
throw e
}
}
function dropsAmount(value: unknown): string {
if (typeof value !== 'string') return '0'
return String(dropsToXrp(value))
}
export async function getTransactions(address: string): Promise<XrpTxEntry[]> {
const result = await rpc('account_tx', {
account: address,
ledger_index_min: -1,
ledger_index_max: -1,
limit: 25,
})
const entries: XrpTxEntry[] = []
for (const entry of result.transactions as any[]) {
const tx = entry.tx_json ?? entry.tx
if (!tx || tx.TransactionType !== 'Payment') continue
const meta = entry.meta ?? entry.metaData ?? {}
if (meta.TransactionResult && meta.TransactionResult !== 'tesSUCCESS') continue
const delivered = meta.delivered_amount ?? meta.DeliveredAmount ?? tx.Amount
entries.push({
hash: entry.hash ?? tx.hash,
amount: dropsAmount(delivered),
timestamp: tx.date ? tx.date + RIPPLE_EPOCH : 0,
direction: tx.Destination === address ? 'in' : 'out',
fee: dropsAmount(tx.Fee),
confirmed: entry.validated === true,
destinationTag: tx.DestinationTag,
})
}
return entries
}
export async function sendPayment(
secret: string,
to: string,
amount: string,
destinationTag?: number,
): Promise<{ hash: string; fee: string }> {
if (!isValidClassicAddress(to)) throw new Error('Invalid XRP address')
const wallet = walletForSigning(secret)
const account = wallet.classicAddress
const [info, feeResult] = await Promise.all([
rpc('account_info', { account, ledger_index: 'current' }),
rpc('fee', {}),
])
const accountData = info.account_data ?? {}
const sequence = accountData.Sequence as number
const balanceDrops = BigInt(accountData.Balance ?? '0')
const feeDrops = BigInt(feeResult.drops?.open_ledger_fee ?? '10')
const amountDrops = BigInt(xrpToDrops(amount))
if (amountDrops + feeDrops > balanceDrops) {
throw new Error('Insufficient balance (includes network fee)')
}
const tx: Record<string, unknown> = {
TransactionType: 'Payment',
Account: account,
Destination: to,
Amount: xrpToDrops(amount),
Fee: feeDrops.toString(),
Sequence: sequence,
Flags: 0,
}
if (destinationTag !== undefined) tx.DestinationTag = destinationTag
const signed = wallet.sign(tx)
const result = await rpc('submit', { tx_blob: signed.tx_blob })
const engineResult = result.engine_result
if (engineResult !== 'tesSUCCESS') {
const msg = result.engine_result_message ?? 'Transaction failed'
throw new Error(`${engineResult}: ${msg}`)
}
return {
hash: signed.hash,
fee: dropsAmount(feeDrops.toString()),
}
}
-12
View File
@@ -1,12 +0,0 @@
/// <reference types="@lynx-js/rspeedy/client" />
declare module '@lynx-js/types' {
interface GlobalProps {
/**
* Define your global properties in this interface.
* These types will be accessible through `lynx.__globalProps`.
*/
}
}
export {}
-11
View File
@@ -1,11 +0,0 @@
declare module '*.vue' {
import type { Component } from 'vue-lynx'
const component: Component
export default component
}
declare module '*.png' {
const src: string
export default src
}
-16
View File
@@ -1,16 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["vue-lynx/types"],
"noEmit": true,
},
"vueCompilerOptions": {
"plugins": ["vue-lynx/types/volar-plugin"]
},
"include": ["./**/*.ts", "./**/*.vue"],
}
-32
View File
@@ -1,32 +0,0 @@
import type { Config } from 'tailwindcss'
import preset from '@lynx-js/tailwind-preset'
const config: Config = {
content: ['./src/**/*.{vue,js,ts}'],
presets: [preset],
theme: {
extend: {
colors: {
background: '#0a0a0b',
foreground: '#fafafa',
muted: {
DEFAULT: '#a1a1aa',
foreground: '#71717a',
},
card: {
DEFAULT: '#18181b',
foreground: '#fafafa',
},
border: '#27272a',
primary: {
DEFAULT: '#3b82f6',
foreground: '#ffffff',
},
destructive: '#f87171',
success: '#34d399',
},
},
},
}
export default config
-15
View File
@@ -1,15 +0,0 @@
{
"compilerOptions": {
"strict": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"skipLibCheck": true,
},
"references": [
{ "path": "./tsconfig.node.json" },
{ "path": "./src" },
],
"files": [],
}
-18
View File
@@ -1,18 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"composite": true,
// Rspeedy uses Node.js's module resolution
"module": "node16",
"moduleResolution": "node16",
"erasableSyntaxOnly": true,
// Node.js 18+
"lib": ["es2023"],
"target": "es2022",
"noEmit": true,
},
"include": ["./lynx.config.ts"],
}
+12 -3013
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -4,7 +4,6 @@
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"dev:mobile": "turbo run dev:hmr",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"check-types": "turbo run check-types"