Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -195,17 +195,41 @@ public final class PersistentTransaction {
typedKind == .assetUnlock
}

/// `true` for masternode provider special transactions (ProRegTx
/// and the three ProUp*Tx kinds). Like asset locks, these get
/// classified `Internal` by the wallet's direction logic (the
/// wallet only sees its own owner/voting/payout keys referenced
/// in the payload), so direction-derived labels like
/// "Self-Transfer" are misleading for them.
public var isProviderSpecial: Bool {
providerSpecialName != nil
}

/// Human-readable name for provider special transactions, `nil`
/// for every other kind.
public var providerSpecialName: String? {
switch typedKind {
case .providerRegistration: return "Provider Registration"
case .providerUpdateRegistrar: return "Provider Update Registrar"
case .providerUpdateService: return "Provider Update Service"
case .providerUpdateRevocation: return "Provider Update Revocation"
default: return nil
Comment thread
QuantumExplorer marked this conversation as resolved.
}
}

/// Direction text for UI surfaces, overridden for asset-lock /
/// asset-unlock txs where the raw `Internal` direction is
/// misleading (the L1 DASH isn't going "to myself" — it's being
/// converted to / from L2 platform credits).
/// asset-unlock txs (the L1 DASH isn't going "to myself" — it's
/// being converted to / from L2 platform credits) and for
/// provider special txs (the payload references our keys but no
/// value moves "to myself").
///
/// Use this anywhere a human-readable "what happened" label is
/// needed; fall back to [`directionName`] only when the consumer
/// genuinely needs the raw direction (e.g. the filter dropdown).
public var displayDirection: String {
if isAssetLock { return "Asset Lock" }
if isAssetUnlock { return "Asset Unlock" }
if let name = providerSpecialName { return name }
return directionName
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,30 @@ struct TransactionDetailView: View {
/// have them, else an explicit "amount unknown" label for the
/// historical-asset-lock case (rather than the misleading
/// `+0.00000000 DASH` from `transaction.formattedAmount`).
private var displayAmount: String {
/// `nil` for a payload-only provider special tx — a ProRegTx
/// observed via the owner/voting keys moves no wallet balance,
/// and `+0.00000000 DASH` reads as a broken zero-value receive.
private var displayAmount: String? {
if transaction.isAssetLock {
if let duffs = assetLockAmountDuffs {
let dash = Double(duffs) / 100_000_000.0
return String(format: "-%.8f DASH", dash)
}
return "Asset Lock (amount unknown)"
}
if transaction.isProviderSpecial && transaction.netAmount == 0 {
return nil
}
return transaction.formattedAmount
}

private var typeDescription: String {
if transaction.isAssetLock { return "Asset Lock" }
if transaction.isAssetUnlock { return "Asset Unlock" }
// Special kinds (asset lock/unlock, provider txs) take their
// label from the model so it can't drift from the list rows.
if transaction.isAssetLock || transaction.isAssetUnlock
|| transaction.isProviderSpecial {
return transaction.displayDirection
}
switch transaction.netAmount {
case let amount where amount > 0:
return "Received"
Expand All @@ -45,6 +55,7 @@ struct TransactionDetailView: View {
private var typeIcon: String {
if transaction.isAssetLock { return "lock.fill" }
if transaction.isAssetUnlock { return "lock.open.fill" }
if transaction.isProviderSpecial { return "server.rack" }
switch transaction.netAmount {
case let amount where amount > 0:
return "arrow.down.circle.fill"
Expand All @@ -59,6 +70,9 @@ struct TransactionDetailView: View {
if transaction.isAssetLock || transaction.isAssetUnlock {
return .purple
}
if transaction.isProviderSpecial {
return .orange
}
switch transaction.netAmount {
case let amount where amount > 0:
return .green
Expand Down Expand Up @@ -102,9 +116,11 @@ struct TransactionDetailView: View {
.font(.headline)
.foregroundColor(.secondary)

Text(displayAmount)
.font(.system(size: 32, weight: .bold, design: .rounded))
.foregroundColor(typeColor)
if let displayAmount {
Text(displayAmount)
.font(.system(size: 32, weight: .bold, design: .rounded))
.foregroundColor(typeColor)
}
}
.padding(.top, 20)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ struct TransactionRowView: View {
// "receive" applies cleanly.
if transaction.isAssetLock { return "lock.fill" }
if transaction.isAssetUnlock { return "lock.open.fill" }
// Provider special txs (ProRegTx / ProUp*Tx) also classify as
// `Internal` — the wallet just sees its own owner/voting/payout
// keys in the payload — so the self-transfer arrows would lie.
if transaction.isProviderSpecial { return "server.rack" }
// direction: 0=incoming, 1=outgoing, 2=internal, 3=coinJoin
switch transaction.direction {
case 0: return "arrow.down.circle.fill"
Expand All @@ -249,6 +253,11 @@ struct TransactionRowView: View {
if transaction.isAssetLock || transaction.isAssetUnlock {
return .purple
}
// Provider special txs get their own axis too — orange, so a
// masternode registration doesn't scan as a red "sent" row.
if transaction.isProviderSpecial {
return .orange
}
switch transaction.direction {
case 0: return .green
case 1, 2: return .red
Expand Down Expand Up @@ -409,6 +418,14 @@ struct TransactionRowView: View {
}
return "Asset Lock (amount unknown)"
}
// A payload-only provider special tx moves no wallet balance;
// `+0.00000000 DASH` reads as a broken zero-value receive, so
// put the tx kind in the amount slot instead. A provider tx
// that DOES move value (e.g. this wallet funded the collateral)
// falls through and shows the real signed amount.
if transaction.isProviderSpecial && transaction.netAmount == 0 {
return transaction.providerSpecialName ?? transaction.transactionType
}
return transaction.formattedAmount
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import XCTest
@testable import SwiftDashSDK

/// Pins the `transactionTypeKind` discriminant → display mapping on
/// `PersistentTransaction`. The raw bytes mirror Rust's
/// `transaction_type_to_u8` (rs-platform-wallet-ffi
/// `core_wallet_types.rs`); a drift there, a `TransactionTypeKind`
/// case renumbering, or a label typo shows up here without UI
/// automation.
final class PersistentTransactionDisplayTests: XCTestCase {

/// Direction 2 = internal — the raw classification every special
/// tx gets (asset locks, provider txs), which the display helpers
/// exist to override.
private func makeTransaction(kind: UInt8, direction: UInt32 = 2) -> PersistentTransaction {
let tx = PersistentTransaction(
txid: Data(repeating: 0xAB, count: 32),
transactionData: Data(),
direction: direction
)
tx.transactionTypeKind = kind
return tx
}

func testProviderSpecialNameForProviderKinds() {
let expected: [UInt8: String] = [
2: "Provider Registration",
3: "Provider Update Registrar",
4: "Provider Update Service",
5: "Provider Update Revocation",
]
for (kind, name) in expected {
let tx = makeTransaction(kind: kind)
XCTAssertEqual(tx.providerSpecialName, name, "kind \(kind)")
XCTAssertTrue(tx.isProviderSpecial, "kind \(kind)")
XCTAssertEqual(tx.displayDirection, name, "kind \(kind)")
}
}

func testNonProviderKindsAreNotProviderSpecial() {
// Every other discriminant, plus the 0xFF "not populated"
// sentinel and an out-of-range future byte.
for kind: UInt8 in [0, 1, 6, 7, 8, 9, 0xFF, 42] {
let tx = makeTransaction(kind: kind)
XCTAssertNil(tx.providerSpecialName, "kind \(kind)")
XCTAssertFalse(tx.isProviderSpecial, "kind \(kind)")
}
}

func testDisplayDirectionPrecedenceUnchangedForOtherKinds() {
// Asset lock/unlock keep their existing overrides…
XCTAssertEqual(makeTransaction(kind: 6).displayDirection, "Asset Lock")
XCTAssertEqual(makeTransaction(kind: 7).displayDirection, "Asset Unlock")
// …and non-special kinds fall through to the raw direction.
XCTAssertEqual(makeTransaction(kind: 0).displayDirection, "Internal")
XCTAssertEqual(makeTransaction(kind: 0, direction: 0).displayDirection, "Incoming")
XCTAssertEqual(makeTransaction(kind: 0xFF).displayDirection, "Internal")
}
}
Loading