List transactions newest first on both chains
This commit is contained in:
@@ -174,12 +174,18 @@ struct TransactionRow: View {
|
|||||||
var record: TransactionRecord
|
var record: TransactionRecord
|
||||||
var symbol: String
|
var symbol: String
|
||||||
|
|
||||||
private var formattedDate: String {
|
private var subtitle: String {
|
||||||
guard let timestamp = record.timestamp else { return "Pending" }
|
if let timestamp = record.timestamp {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateStyle = .medium
|
formatter.dateStyle = .medium
|
||||||
formatter.timeStyle = .short
|
formatter.timeStyle = .short
|
||||||
return formatter.string(from: timestamp)
|
return record.confirmed ? formatter.string(from: timestamp) : "Pending confirmation"
|
||||||
|
}
|
||||||
|
// Monero transfers carry no timestamp; the block height is the honest label.
|
||||||
|
if let height = record.height, height > 0 {
|
||||||
|
return record.confirmed ? "Block \(height)" : "In the transaction pool"
|
||||||
|
}
|
||||||
|
return record.confirmed ? "Confirmed" : "Pending"
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -197,7 +203,7 @@ struct TransactionRow: View {
|
|||||||
Text(record.direction == .incoming ? "Received" : "Sent")
|
Text(record.direction == .incoming ? "Received" : "Sent")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.system(size: 14, weight: .semibold))
|
||||||
.foregroundStyle(Theme.text)
|
.foregroundStyle(Theme.text)
|
||||||
Text(record.confirmed ? formattedDate : "Pending confirmation")
|
Text(subtitle)
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundStyle(Theme.muted)
|
.foregroundStyle(Theme.muted)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ struct TransactionDetailView: View {
|
|||||||
var chain: Chain
|
var chain: Chain
|
||||||
var onDone: () -> Void
|
var onDone: () -> Void
|
||||||
|
|
||||||
|
@State private var resolvedDate: Date?
|
||||||
|
@State private var isResolvingDate = false
|
||||||
|
|
||||||
|
private var displayedDate: Date? { record.timestamp ?? resolvedDate }
|
||||||
|
|
||||||
private var dateFormatter: DateFormatter {
|
private var dateFormatter: DateFormatter {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateStyle = .medium
|
formatter.dateStyle = .medium
|
||||||
@@ -59,8 +64,12 @@ struct TransactionDetailView: View {
|
|||||||
|
|
||||||
Card {
|
Card {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
if let timestamp = record.timestamp {
|
if let date = displayedDate {
|
||||||
DetailRow(label: "Date", value: dateFormatter.string(from: timestamp))
|
DetailRow(label: "Date", value: dateFormatter.string(from: date))
|
||||||
|
} else if isResolvingDate {
|
||||||
|
DetailRow(label: "Date", value: "Looking up the block time…")
|
||||||
|
} else if let height = record.height, height > 0 {
|
||||||
|
DetailRow(label: "Block height", value: String(height))
|
||||||
}
|
}
|
||||||
if let fee = record.fee, !fee.isEmpty, record.direction == .outgoing {
|
if let fee = record.fee, !fee.isEmpty, record.direction == .outgoing {
|
||||||
DetailRow(label: "Network fee", value: "\(fee) \(symbol)")
|
DetailRow(label: "Network fee", value: "\(fee) \(symbol)")
|
||||||
@@ -108,6 +117,15 @@ struct TransactionDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
|
.task {
|
||||||
|
guard record.timestamp == nil,
|
||||||
|
chain == .xmr,
|
||||||
|
let height = record.height, height > 0,
|
||||||
|
resolvedDate == nil else { return }
|
||||||
|
isResolvingDate = true
|
||||||
|
resolvedDate = try? await MoneroService.shared.blockTimestamp(height: height)
|
||||||
|
isResolvingDate = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,33 @@ final class MoneroService {
|
|||||||
return response["mnemonic"] as? String ?? ""
|
return response["mnemonic"] as? String ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Block time for a height. wallet2 keeps no timestamp for transfers, so the
|
||||||
|
/// only way to date a Monero transaction is to ask the daemon for the block
|
||||||
|
/// it landed in.
|
||||||
|
func blockTimestamp(height: UInt64) async throws -> Date? {
|
||||||
|
guard let url = URL(string: "http://\(Self.daemonHost):18081/json_rpc") else { return nil }
|
||||||
|
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_block_header_by_height",
|
||||||
|
"params": ["height": height],
|
||||||
|
])
|
||||||
|
|
||||||
|
let (data, response) = try await URLSession.shared.data(for: request)
|
||||||
|
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode),
|
||||||
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||||
|
let result = json["result"] as? [String: Any],
|
||||||
|
let header = result["block_header"] as? [String: Any],
|
||||||
|
let timestamp = (header["timestamp"] as? NSNumber)?.doubleValue, timestamp > 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Date(timeIntervalSince1970: timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Reads
|
// MARK: - Reads
|
||||||
|
|
||||||
func balance() async throws -> Balance {
|
func balance() async throws -> Balance {
|
||||||
@@ -201,6 +228,21 @@ final class MoneroService {
|
|||||||
note: (entry["note"] as? String).flatMap { $0.isEmpty ? nil : $0 },
|
note: (entry["note"] as? String).flatMap { $0.isEmpty ? nil : $0 },
|
||||||
weight: (entry["weight"] as? NSNumber)?.uint64Value)
|
weight: (entry["weight"] as? NSNumber)?.uint64Value)
|
||||||
}
|
}
|
||||||
|
.sorted { newestFirst($0, $1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Newest first. wallet2 gives transfers no timestamp, but the block height is
|
||||||
|
/// chronological and always present, so that is the ordering key; anything
|
||||||
|
/// still in the pool (height 0) belongs at the top.
|
||||||
|
private func newestFirst(_ left: TransactionRecord, _ right: TransactionRecord) -> Bool {
|
||||||
|
func key(_ record: TransactionRecord) -> UInt64 {
|
||||||
|
guard let height = record.height, height > 0 else { return .max }
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
let leftKey = key(left)
|
||||||
|
let rightKey = key(right)
|
||||||
|
if leftKey != rightKey { return leftKey > rightKey }
|
||||||
|
return (left.timestamp ?? .distantFuture) > (right.timestamp ?? .distantFuture)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Accounts
|
// MARK: - Accounts
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ final class XRPWalletService {
|
|||||||
ledgerIndex: (entry["ledger_index"] as? NSNumber)?.uint64Value,
|
ledgerIndex: (entry["ledger_index"] as? NSNumber)?.uint64Value,
|
||||||
sequence: (tx["Sequence"] as? NSNumber)?.uint64Value))
|
sequence: (tx["Sequence"] as? NSNumber)?.uint64Value))
|
||||||
}
|
}
|
||||||
return records
|
// Newest first, matching the Monero list.
|
||||||
|
return records.sorted { ($0.timestamp ?? .distantFuture) > ($1.timestamp ?? .distantFuture) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Sends
|
// MARK: - Sends
|
||||||
|
|||||||
Reference in New Issue
Block a user