Replace the JavaScript mobile app with a native Swift iOS wallet
This commit is contained in:
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
@@ -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)]))
|
||||
}
|
||||
}
|
||||
@@ -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))))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
@@ -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...])
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user