Backlog mediums (display/gating/caching only — no send/sweep/encrypt call or args changed): Resilience: - Gate Send/Review on a KNOWN peer-derived tip (SyncInfo::sendTipReady): a fresh reconnect or peerless node has tip_known=false, so its local height isn't a trustworthy network tip — sending is disabled with a clear tooltip until it is. Lite is exempt (its own sync gate). The z_sendmany call is untouched. - History now shows "Dropped" for a conflicted tx (confirmations < 0) and "Expired?" for one unconfirmed > 1h, instead of "Pending" forever (new TransactionInfo::isConflicted/isStalePending; the tab inlines the check on its DisplayTx fields). - A cached z_viewtransaction row's confirmations now advance by the blocks mined since it was sampled (observed_height), instead of freezing at the cached value; never goes backwards; pending (0) isn't extrapolated. Performance: - Explorer block list uses loadRangeCached() (memoized on the cache revision) instead of loadRange(), so it only hits SQLite after the cache changes, not every frame. Tests: testResilienceMediums (tip gate, confirmations-advance, dropped/stale predicates) + loadRangeCached memoization in testExplorerBlockCache (isolated on its own cache). Held: Overview/Balance per-frame list cache, peers clippers, off-thread tx-history cache load (larger refactors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
473 lines
21 KiB
C++
473 lines
21 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#pragma once
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include <cstdint>
|
|
#include <chrono>
|
|
#include <ctime>
|
|
#include <cstring>
|
|
#include <utility>
|
|
|
|
#include "exchange_info.h"
|
|
#include "candle.h"
|
|
|
|
namespace dragonx {
|
|
|
|
/**
|
|
* @brief Represents an address with its balance
|
|
*/
|
|
struct AddressInfo {
|
|
std::string address;
|
|
double balance = 0.0; // DISPLAY total incl. pending 0-conf change (minconf=0)
|
|
std::string type; // "shielded" or "transparent"
|
|
bool has_spending_key = true; // false for view-only (imported via z_importviewingkey)
|
|
|
|
// For display
|
|
std::string label;
|
|
|
|
// CONFIRMED balance (minconf>=1) — what z_sendmany can actually spend now. Kept last so positional
|
|
// brace-init of the leading fields (used in tests) still compiles.
|
|
double spendableBalance = 0.0;
|
|
|
|
// Derived
|
|
bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
|
|
bool isShielded() const { return type == "shielded"; }
|
|
bool isSpendable() const { return has_spending_key; }
|
|
};
|
|
|
|
std::vector<size_t> sortedSpendableAddressIndices(const std::vector<AddressInfo>& addresses,
|
|
bool requirePositiveBalance = true);
|
|
int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses);
|
|
// Index of the address whose string equals `address` (exact match), or -1 (empty string / not listed).
|
|
int addressIndexByString(const std::vector<AddressInfo>& addresses, const std::string& address);
|
|
|
|
// The Send tab's source-address resolution, pure. `current` (s_from_address) is the single source of truth:
|
|
// the one-shot auto-pick (highest confirmed balance) only fills an EMPTY source, and never replaces one the
|
|
// user - or another tab's "Send from this address" - already set. A source that is not in the list (the list
|
|
// can be transiently short) is kept as is with index -1; it must NOT trigger a re-pick.
|
|
struct SourceSelection {
|
|
std::string address; // what s_from_address must hold afterwards
|
|
int index = -1; // its row in `addresses`, -1 if not listed
|
|
bool autoLatched = false; // new value of the one-shot latch
|
|
};
|
|
SourceSelection resolveSourceSelection(const std::vector<AddressInfo>& addresses, const std::string& current,
|
|
bool autoLatched, bool connected);
|
|
|
|
/**
|
|
* @brief Represents a wallet transaction
|
|
*/
|
|
struct TransactionInfo {
|
|
std::string txid;
|
|
std::string type; // "send", "receive", "mined"
|
|
double amount = 0.0;
|
|
int64_t timestamp = 0; // Unix timestamp
|
|
int confirmations = 0;
|
|
std::string address; // destination (send) or source (receive)
|
|
std::string from_address; // source address for sends
|
|
std::string memo;
|
|
|
|
// Computed fields
|
|
std::string getTimeString() const;
|
|
std::string getTypeDisplay() const;
|
|
bool isConfirmed() const { return confirmations >= 1; }
|
|
bool isMature() const { return confirmations >= 100; }
|
|
// gettransaction reports confirmations < 0 for a tx conflicted with the chain (double-spent / reorged out).
|
|
bool isConflicted() const { return confirmations < 0; }
|
|
// Unconfirmed for longer than maxAgeSec since the wallet first saw it: most likely dropped from the
|
|
// mempool or expired. A display heuristic only (timestamp 0 = unknown -> never flagged).
|
|
bool isStalePending(int64_t now, int64_t maxAgeSec = 3600) const {
|
|
return confirmations == 0 && timestamp > 0 && now - timestamp > maxAgeSec;
|
|
}
|
|
};
|
|
|
|
// Exact field-wise equality: decides whether a refreshed list really differs. `amount` is compared
|
|
// BIT-exactly (like the old History fingerprint): -0.0 and +0.0 render differently ("+-0.0" vs "+0.0").
|
|
// Must list EVERY TransactionInfo field (tests mutate each one).
|
|
inline bool sameTransactionInfo(const TransactionInfo& a, const TransactionInfo& b) {
|
|
return a.timestamp == b.timestamp && a.confirmations == b.confirmations &&
|
|
std::memcmp(&a.amount, &b.amount, sizeof(double)) == 0 &&
|
|
a.txid == b.txid && a.type == b.type && a.address == b.address &&
|
|
a.from_address == b.from_address && a.memo == b.memo;
|
|
}
|
|
// Same row except the confirmation COUNT: the pending (confirmations==0) state must still match because it drives the
|
|
// History tab's pending-first sort and shield-leg merge. A delta for which every row passes this changes nothing the
|
|
// tab's merged/sorted display list is built from, so it can patch confirmations in place instead of rebuilding.
|
|
inline bool sameTransactionLayout(const TransactionInfo& a, const TransactionInfo& b) {
|
|
return a.timestamp == b.timestamp && (a.confirmations == 0) == (b.confirmations == 0) &&
|
|
std::memcmp(&a.amount, &b.amount, sizeof(double)) == 0 &&
|
|
a.txid == b.txid && a.type == b.type && a.address == b.address &&
|
|
a.from_address == b.from_address && a.memo == b.memo;
|
|
}
|
|
inline bool sameTransactionList(const std::vector<TransactionInfo>& a, const std::vector<TransactionInfo>& b) {
|
|
if (a.size() != b.size()) return false;
|
|
for (std::size_t i = 0; i < a.size(); ++i)
|
|
if (!sameTransactionInfo(a[i], b[i])) return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Represents a connected peer
|
|
*/
|
|
struct PeerInfo {
|
|
int id = 0;
|
|
std::string addr;
|
|
std::string subver;
|
|
std::string services;
|
|
int version = 0;
|
|
int64_t conntime = 0;
|
|
int banscore = 0;
|
|
double pingtime = 0.0;
|
|
int64_t bytessent = 0;
|
|
int64_t bytesrecv = 0;
|
|
int startingheight = 0;
|
|
int synced_headers = 0;
|
|
int synced_blocks = 0;
|
|
bool inbound = false;
|
|
|
|
// TLS info
|
|
std::string tls_cipher;
|
|
bool tls_verified = false;
|
|
|
|
std::string getConnectionTime() const;
|
|
};
|
|
|
|
/**
|
|
* @brief Represents a banned peer
|
|
*/
|
|
struct BannedPeer {
|
|
std::string address;
|
|
std::string subnet;
|
|
int64_t banned_until = 0;
|
|
|
|
std::string getBannedUntilString() const;
|
|
};
|
|
|
|
/**
|
|
* @brief Mining statistics
|
|
*/
|
|
struct MiningInfo {
|
|
bool generate = false;
|
|
int genproclimit = 0; // -1 means max CPUs
|
|
double localHashrate = 0.0; // Local hashrate (H/s) from getlocalsolps RPC (RandomX)
|
|
double networkHashrate = 0.0; // Network hashrate (H/s)
|
|
int blocks = 0;
|
|
double difficulty = 0.0;
|
|
std::string chain;
|
|
|
|
double daemon_memory_mb = 0.0; // Daemon process RSS in MB
|
|
|
|
// History for chart
|
|
std::vector<double> hashrate_history; // Last N samples
|
|
std::vector<std::time_t> hashrate_history_t; // wall-clock stamp per sample (lockstep with hashrate_history)
|
|
static constexpr int MAX_HISTORY = 300; // 5 minutes at ~1s intervals
|
|
|
|
// Recent daemon log lines for the mining log panel
|
|
std::vector<std::string> log_lines;
|
|
};
|
|
|
|
/**
|
|
* @brief Blockchain synchronization info
|
|
*/
|
|
struct SyncInfo {
|
|
int blocks = 0;
|
|
int headers = 0;
|
|
double verification_progress = 0.0;
|
|
bool syncing = false;
|
|
std::string best_blockhash;
|
|
std::string lite_error; // lite: last sync failure text, "" = none
|
|
|
|
// Rescan state (detected from daemon output)
|
|
bool rescanning = false;
|
|
float rescan_progress = 0.0f; // 0.0 - 1.0
|
|
std::string rescan_status; // e.g. "Rescanning... 25%"
|
|
|
|
// Sapling note witness rebuild — a distinct, often-long phase after a rescan/zap. The daemon
|
|
// reports it in TWO sub-phases with different signals, so we track which is active:
|
|
// 1 = initial pass ("Setting Initial Sapling Witness for tx <hash>, <i> of <N>") — progress
|
|
// is distinct-txs-witnessed / N (the <i> bounces, so it can't be used directly).
|
|
// 2 = witness-cache walk ("Building Witnesses for block <h> <frac> complete, <n> remaining")
|
|
// — progress derived from how far "remaining" has fallen from its per-phase peak.
|
|
// The two are sequential with different scales, so progress is NOT carried across the boundary
|
|
// (that would pin the bar at the initial pass's ~100% through the whole cache walk).
|
|
bool building_witnesses = false;
|
|
int witness_phase = 0; // 0 none, 1 initial-witness pass, 2 witness-cache walk
|
|
float witness_progress = 0.0f; // 0.0 - 1.0, within the current sub-phase
|
|
int witness_remaining = 0; // blocks left in the cache walk (0 if unknown / phase 1)
|
|
|
|
// True only when the daemon reports a positive peer-derived tip (longestchain > 0). A node with
|
|
// no peers reports longestchain == 0, so `blocks` is then only the LOCAL best height and cannot be
|
|
// trusted as "at the network tip". isSynced() gates on this so a peerless / cold-started node is
|
|
// never reported synced (which would enable Send against a stale balance). (audit: peerless-synced)
|
|
bool tip_known = false;
|
|
|
|
bool isSynced() const { return tip_known && !syncing && blocks > 0 && blocks >= headers - 2; }
|
|
|
|
// Send precondition: the wallet is caught up to a KNOWN peer-derived tip. A fresh (re)connect or a
|
|
// peerless node has tip_known == false, so its local height can't be trusted as the network tip.
|
|
// The lite backend never populates tip_known, so lite is exempt (it has its own sync gate).
|
|
bool sendTipReady(bool liteBuild) const { return liteBuild || (tip_known && !syncing && blocks > 0); }
|
|
};
|
|
|
|
/**
|
|
* @brief Market/price information
|
|
*/
|
|
struct MarketInfo {
|
|
double price_usd = 0.0;
|
|
double price_btc = 0.0;
|
|
double volume_24h = 0.0;
|
|
double change_24h = 0.0;
|
|
double market_cap = 0.0;
|
|
std::string last_updated;
|
|
std::chrono::steady_clock::time_point last_fetch_time{};
|
|
bool price_loading = false;
|
|
std::string price_error;
|
|
|
|
// Live in-session price history: ~1 sample/minute, capped at MAX_HISTORY samples.
|
|
// Backs the main chart and the "minute" portfolio-sparkline interval.
|
|
std::vector<double> price_history;
|
|
static constexpr int MAX_HISTORY = 24; // 24 samples (~24 minutes at the 60s refresh)
|
|
|
|
// Historical USD price series fetched from CoinGecko market_chart, so the portfolio-group
|
|
// sparklines can show real hour/day/week/month trends instead of resampling the ~24-minute
|
|
// in-session buffer. Timestamped (unix seconds), oldest->newest; empty until the first fetch.
|
|
// Refreshed on a slow cadence (~30 min) since historical data moves slowly.
|
|
std::vector<std::pair<std::time_t, double>> price_chart_intraday; // ~24h @ 5-minute granularity
|
|
std::vector<std::pair<std::time_t, double>> price_chart_daily; // ~1yr @ daily granularity
|
|
std::chrono::steady_clock::time_point chart_last_fetch_time{};
|
|
bool chart_loaded = false;
|
|
|
|
// Per-EXCHANGE candle series for the SELECTED pair, fetched from that venue's own API (see
|
|
// data/exchange_candles.h). When exchange_chart_active is true the Market chart draws these instead
|
|
// of the CoinGecko cross-exchange aggregate above; it's set false while switching pairs or when the
|
|
// selected venue has no adapter / its fetch failed, so the chart gracefully falls back to aggregate.
|
|
std::vector<std::pair<std::time_t, double>> exchange_chart_intraday;
|
|
std::vector<std::pair<std::time_t, double>> exchange_chart_daily;
|
|
bool exchange_chart_active = false;
|
|
// Full OHLC for the same per-exchange candles, backing the candlestick rendering (the aggregate
|
|
// CoinGecko series is close-only, so it stays a line). Parallel to exchange_chart_* above.
|
|
std::vector<data::Candle> exchange_ohlc_intraday;
|
|
std::vector<data::Candle> exchange_ohlc_daily;
|
|
|
|
// Exchanges/pairs fetched live from CoinGecko (empty until fetched; the Market tab
|
|
// falls back to data::getExchangeRegistry() while empty).
|
|
std::vector<data::ExchangeInfo> exchanges;
|
|
};
|
|
|
|
/**
|
|
* @brief Pool mining state (from xmrig HTTP API)
|
|
*/
|
|
struct PoolMiningState {
|
|
bool pool_mode = false; // UI toggle: solo vs pool
|
|
bool xmrig_running = false;
|
|
std::string pool_url;
|
|
std::string algo;
|
|
std::string version; // running miner's version (from its API)
|
|
|
|
double hashrate_10s = 0;
|
|
double hashrate_60s = 0;
|
|
double hashrate_15m = 0;
|
|
int64_t accepted = 0;
|
|
int64_t rejected = 0;
|
|
int64_t uptime_sec = 0;
|
|
double pool_diff = 0;
|
|
bool connected = false;
|
|
std::time_t running_since = 0; // wall-clock the miner was first seen running this session (0 = not running)
|
|
std::string last_error; // newest xmrig log line that looks like a connection error ("" = none)
|
|
|
|
// Memory/thread usage (bytes for memory)
|
|
int64_t memory_used = 0;
|
|
int threads_active = 0;
|
|
|
|
// Pool-side hashrate (from pool stats API)
|
|
double pool_hashrate = 0;
|
|
|
|
// Hashrate history for chart (mirrors MiningInfo::hashrate_history)
|
|
std::vector<double> hashrate_history;
|
|
std::vector<std::time_t> hashrate_history_t; // wall-clock stamp per sample (lockstep with hashrate_history)
|
|
static constexpr int MAX_HISTORY = 60; // ~2 min at the ~2s pool poll
|
|
|
|
// Recent log lines for the log panel
|
|
std::vector<std::string> log_lines;
|
|
};
|
|
|
|
/**
|
|
* @brief Complete wallet state - all data fetched from daemon
|
|
*/
|
|
struct WalletState {
|
|
// Connection
|
|
bool connected = false;
|
|
bool warming_up = false; // daemon reachable but in RPC warmup (error -28)
|
|
// True when the daemon is up/launching but not yet answering RPC (e.g. the connect probe
|
|
// times out because the node is loading the block index). Distinct from warming_up, which
|
|
// needs a JSON-RPC -28 reply; here getinfo never returns, so we infer the state from the
|
|
// daemon's launch state + its own console output. Drives the same loading overlay so the
|
|
// user sees WHAT the node is doing instead of a bare "Connection failed".
|
|
bool daemon_initializing = false;
|
|
std::string warmup_status; // user-friendly title, e.g. "Processing blocks..."
|
|
std::string warmup_description; // subtitle explaining the stage
|
|
int daemon_version = 0;
|
|
std::string daemon_subversion;
|
|
int protocol_version = 0;
|
|
int p2p_port = 0;
|
|
int longestchain = 0;
|
|
int notarized = 0;
|
|
|
|
// Sync status
|
|
SyncInfo sync;
|
|
|
|
// Balances (named to match UI usage). These are the DISPLAY totals — minconf=0, so they include the
|
|
// user's own pending change and don't crater during an unconfirmed send.
|
|
double privateBalance = 0.0; // shielded balance (display, incl. pending change)
|
|
double transparentBalance = 0.0;
|
|
double totalBalance = 0.0;
|
|
double unconfirmedBalance = 0.0; // = totalBalance - spendableTotalBalance (the pending portion)
|
|
// CONFIRMED / spendable totals (minconf>=1) — what can actually be sent right now. z_sendmany runs at
|
|
// minconf=1, so the Send form / Max / spend validation must size off these, never the display totals.
|
|
double spendablePrivateBalance = 0.0;
|
|
double spendableTransparentBalance = 0.0;
|
|
double spendableTotalBalance = 0.0;
|
|
|
|
// Aliases for backward compatibility
|
|
double& shielded_balance = privateBalance;
|
|
double& transparent_balance = transparentBalance;
|
|
double& total_balance = totalBalance;
|
|
double& unconfirmed_balance = unconfirmedBalance;
|
|
|
|
// Addresses - combined list for UI convenience
|
|
std::vector<AddressInfo> addresses;
|
|
|
|
// Also keep separate lists for legacy code
|
|
std::vector<AddressInfo> z_addresses;
|
|
std::vector<AddressInfo> t_addresses;
|
|
|
|
// Transactions
|
|
std::vector<TransactionInfo> transactions;
|
|
|
|
// Peers
|
|
std::vector<PeerInfo> peers;
|
|
std::vector<BannedPeer> bannedPeers;
|
|
|
|
// Aliases for banned_peers
|
|
std::vector<BannedPeer>& banned_peers = bannedPeers;
|
|
|
|
// Mining
|
|
MiningInfo mining;
|
|
|
|
// Pool mining (xmrig)
|
|
PoolMiningState pool_mining;
|
|
|
|
// Market
|
|
MarketInfo market;
|
|
|
|
// Wallet encryption state (populated from getwalletinfo)
|
|
bool encrypted = false; // true if wallet has ever been encrypted
|
|
bool locked = false; // true if encrypted && unlocked_until <= now
|
|
int64_t unlocked_until = 0; // 0 = locked, >0 = unix timestamp when auto-lock fires
|
|
bool encryption_state_known = false; // true once first getwalletinfo response processed
|
|
|
|
bool isEncrypted() const { return encrypted; }
|
|
bool isLocked() const { return encrypted && locked; }
|
|
bool isUnlocked() const { return encrypted && !locked; }
|
|
|
|
// Timestamps for refresh logic
|
|
int64_t last_balance_update = 0;
|
|
// A Core poll that skipped the balance confirmed it is still current (nothing it depends on changed):
|
|
// the staleness badge counts from the later of the two.
|
|
int64_t balance_verified_at = 0;
|
|
// The last balance call that was sent failed (or timed out): the staleness badge then shows at its usual
|
|
// thresholds even while the scan budget is holding the next one.
|
|
bool balance_last_attempt_failed = false;
|
|
int wallet_tx_count = -1; // getwalletinfo's txcount; -1 = unknown
|
|
int64_t last_address_update = 0; // set when an address-list refresh applies; 0 = never loaded yet
|
|
int64_t last_tx_update = 0;
|
|
|
|
// Change counter for `transactions` (main thread only). Starts at 1 so a UI cache's 0 sentinel never matches;
|
|
// only ever increases (clear() bumps, never resets) so a wallet switch can't alias an old cache key. EVERY
|
|
// write to `transactions` (replace, insert/erase, in-place field edit) must use the helpers below or call
|
|
// touchTransactions() afterwards. UI caches (History tab, ...) key on this, NOT on last_tx_update (a
|
|
// wall-clock refresh stamp that lite never sets) nor App::tx_dirty_generation_ (a refresh-needed flag).
|
|
std::uint64_t tx_revision = 1;
|
|
// Bumps with tx_revision EXCEPT for a pure confirmation-count change (see sameTransactionLayout): the History tab's
|
|
// O(N) merge+sort cache keys on this and just patches confirmations when only tx_revision moved. Every writer that
|
|
// is not setTransactionsIfChanged goes through touchTransactions()/replaceTransactions() and bumps both (safe default).
|
|
std::uint64_t tx_structure_revision = 1;
|
|
void touchTransactions() { ++tx_revision; ++tx_structure_revision; }
|
|
void replaceTransactions(std::vector<TransactionInfo>&& next) { transactions = std::move(next); touchTransactions(); }
|
|
// Replace only when the content differs; returns true (and bumps) if it did. Keeps the old list when identical.
|
|
bool setTransactionsIfChanged(std::vector<TransactionInfo>&& next) {
|
|
if (sameTransactionList(transactions, next)) return false;
|
|
bool layoutSame = transactions.size() == next.size();
|
|
for (std::size_t i = 0; layoutSame && i < next.size(); ++i)
|
|
layoutSame = sameTransactionLayout(transactions[i], next[i]);
|
|
transactions = std::move(next);
|
|
++tx_revision;
|
|
if (!layoutSame) ++tx_structure_revision;
|
|
return true;
|
|
}
|
|
int64_t last_peer_update = 0;
|
|
int64_t last_mining_update = 0;
|
|
|
|
// Helper methods
|
|
int getAddressCount() const { return addresses.size(); }
|
|
double getBalanceUSD() const { return totalBalance * market.price_usd; }
|
|
|
|
void clear() {
|
|
connected = false;
|
|
warming_up = false;
|
|
daemon_initializing = false;
|
|
warmup_status.clear();
|
|
warmup_description.clear();
|
|
daemon_version = 0;
|
|
daemon_subversion.clear();
|
|
protocol_version = 0;
|
|
p2p_port = 0;
|
|
longestchain = 0;
|
|
notarized = 0;
|
|
sync = SyncInfo{};
|
|
privateBalance = transparentBalance = totalBalance = 0.0;
|
|
unconfirmedBalance = 0.0;
|
|
spendablePrivateBalance = spendableTransparentBalance = spendableTotalBalance = 0.0;
|
|
encrypted = false;
|
|
locked = false;
|
|
unlocked_until = 0;
|
|
encryption_state_known = false;
|
|
addresses.clear();
|
|
z_addresses.clear();
|
|
t_addresses.clear();
|
|
transactions.clear();
|
|
touchTransactions(); // never reset: a post-switch revision must differ from every cached key
|
|
peers.clear();
|
|
bannedPeers.clear();
|
|
// W6-1: reset node-level mining state too — the daemon restarts on a wallet switch (mining
|
|
// stops), so leaving the previous wallet's hashrate/blocks would show stale mining stats.
|
|
mining = MiningInfo{};
|
|
pool_mining = PoolMiningState{};
|
|
// After a disconnect / wallet switch nothing is freshly known, so drop the "last successful
|
|
// refresh" stamps. Otherwise the pre-teardown time survives and, on reconnect, the staleness
|
|
// badge (and any "updated X ago" reader) briefly reports it as current until the first refresh
|
|
// re-stamps it. All readers treat 0 as "never" (formatTimeAgoShort/timeAgo return "").
|
|
last_balance_update = last_address_update = last_tx_update = last_peer_update = last_mining_update = 0;
|
|
balance_verified_at = 0;
|
|
balance_last_attempt_failed = false;
|
|
wallet_tx_count = -1;
|
|
}
|
|
|
|
// Rebuild combined addresses list from z/t lists
|
|
void rebuildAddressList() {
|
|
addresses.clear();
|
|
addresses.reserve(z_addresses.size() + t_addresses.size());
|
|
for (const auto& addr : z_addresses) {
|
|
addresses.push_back(addr);
|
|
}
|
|
for (const auto& addr : t_addresses) {
|
|
addresses.push_back(addr);
|
|
}
|
|
}
|
|
};
|
|
|
|
} // namespace dragonx
|