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
4 changes: 4 additions & 0 deletions DashWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
1C2D3E4F5A6B7C8D9E0F1A2B /* TransferAmountHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C9D0E1F2A3B4C5D6E7F809 /* TransferAmountHostingController.swift */; };
1C887F21B0826303D34B8D90 /* ContactsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 764BAE3F8A6B30668FEB1FA6 /* ContactsScreen.swift */; };
1CDA90D00719D6249F2761AC /* ContactItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE7E8766B8C1601A85AAB4F /* ContactItem.swift */; };
9E51F3B86C207A15D2B84E60 /* DashPayUserLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D0A7C21B5E39F04A11C2D57 /* DashPayUserLink.swift */; };
1F3FF21B0295D5A326F08B74 /* OrderPreviewFeeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D1A08496F925D17031493E /* OrderPreviewFeeRow.swift */; };
1FAFEA3F516E6CBDD2B49010 /* SwiftDashSDKContactsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02357928871603392E797C9A /* SwiftDashSDKContactsService.swift */; };
215F9BB0CAADF4D47D95C444 /* DWAvatarUploadClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E2D975669CB53883D56803 /* DWAvatarUploadClientTests.swift */; };
Expand Down Expand Up @@ -2544,6 +2545,7 @@
2AFF01DA243F4559003718DC /* DWDPRegistrationStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DWDPRegistrationStatus.m; sourceTree = "<group>"; };
2C4FDA5A52AB80D7F349A7AF /* WalletSendService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletSendService.swift; sourceTree = "<group>"; };
2CE7E8766B8C1601A85AAB4F /* ContactItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContactItem.swift; sourceTree = "<group>"; };
4D0A7C21B5E39F04A11C2D57 /* DashPayUserLink.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DashPayUserLink.swift; sourceTree = "<group>"; };
2DCBA057E553F81F54ACE6E1 /* StorageRecordDetailViews.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StorageRecordDetailViews.swift; sourceTree = "<group>"; };
2E7EE6B2E027DC4062E9983E /* ContactProfileSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContactProfileSheet.swift; sourceTree = "<group>"; };
2F134B2E67D68A97892FD760 /* WalletsScreen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsScreen.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6697,6 +6699,7 @@
isa = PBXGroup;
children = (
2CE7E8766B8C1601A85AAB4F /* ContactItem.swift */,
4D0A7C21B5E39F04A11C2D57 /* DashPayUserLink.swift */,
02357928871603392E797C9A /* SwiftDashSDKContactsService.swift */,
);
path = Contacts;
Expand Down Expand Up @@ -11116,6 +11119,7 @@
E4165429FE629AE7E9CB7E22 /* DWIdentityRegistrationController.swift in Sources */,
A15ED0B9E7AAF29C694E4E16 /* DWIdentityAuthorizer.swift in Sources */,
1CDA90D00719D6249F2761AC /* ContactItem.swift in Sources */,
9E51F3B86C207A15D2B84E60 /* DashPayUserLink.swift in Sources */,
1FAFEA3F516E6CBDD2B49010 /* SwiftDashSDKContactsService.swift in Sources */,
1C887F21B0826303D34B8D90 /* ContactsScreen.swift in Sources */,
07B6BD15F525AAF000B53162 /* AddContactScreen.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// DashPayUserLink.swift
// DashWallet
//
// QR/link payload identifying a DashPay user: the Platform identity
// id (base58) plus the preferred DPNS username, encoded as
//
// dashpay://user?id=<base58-identity-id>&username=<label>
//
// Rendered as "my QR code" on the add-contact screen and parsed back
// when another user scans it. Parsing is pure and offline; it proves
// nothing — the username claim must be verified against Platform
// (DPNS lookup, identity id match) before it is shown as that user.
//

import Foundation
import SwiftDashSDK

struct DashPayUserLink: Equatable {
/// 32-byte Platform identity id.
let identityId: Data
/// Preferred DPNS label, stored without the ".dash" suffix.
let username: String

var identityIdBase58: String { identityId.toBase58String() }

/// Canonical URI form — the QR payload.
var uriString: String {
var components = URLComponents()
components.scheme = "dashpay"
components.host = "user"
components.queryItems = [
URLQueryItem(name: "id", value: identityIdBase58),
URLQueryItem(name: "username", value: username),
]
return components.string ?? "dashpay://user"
}

/// Parse a scanned string, enforcing the canonical `dashpay://user`
/// shape strictly so the cross-platform wire contract stays
/// unambiguous: scheme/host/parameter names are matched
/// case-insensitively, but userinfo, port, path, fragment,
/// duplicate parameters, and unsupported parameters are all
/// rejected. `id` must decode to a 32-byte base58 identifier and
/// `username` must be non-empty (a trailing ".dash" is tolerated
/// and stripped). Anything else — payment URIs, invitation links,
/// bare usernames — returns nil.
static func parse(_ string: String) -> DashPayUserLink? {
let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
guard let components = URLComponents(string: trimmed),
Comment thread
QuantumExplorer marked this conversation as resolved.
components.scheme?.lowercased() == "dashpay",
components.host?.lowercased() == "user",
components.user == nil,
components.password == nil,
components.port == nil,
components.path.isEmpty,
components.fragment == nil
else { return nil }

var identityId: Data?
var username: String?
var seenNames: Set<String> = []
for item in components.queryItems ?? [] {
let name = item.name.lowercased()
guard seenNames.insert(name).inserted else { return nil }
switch name {
case "id": identityId = item.value.flatMap { Data.identifier(fromBase58: $0) }
case "username": username = item.value
default: return nil
}
}

guard let identityId, identityId.count == 32 else { return nil }
let label = (username ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
.withoutDashSuffix
guard !label.isEmpty else { return nil }
return DashPayUserLink(identityId: identityId, username: label)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,22 @@ final class SwiftDashSDKContactsService: ObservableObject {
}
}

/// Exact DPNS resolution on Platform: the identity id currently
/// owning `username` ("alice" or "alice.dash"), or nil when the
/// name is unregistered. Unlike `searchUsernames` this is not a
/// capped prefix page, so it's the right primitive for verifying a
/// scanned QR's username↔identity claim.
func resolveUsername(_ username: String) async throws -> Data? {
guard let wallet = SwiftDashSDKHost.shared.wallet else {
throw ServiceError.noWallet
}
do {
return try await wallet.resolveDpnsName(username)
} catch {
throw ServiceError.sdk(error)
}
}

/// Look up an already-materialized `ContactItem` for `identityId`
/// across the established / incoming / outgoing snapshots. Used by
/// the add-contact preview to show a contact's real profile fields
Expand Down
Loading
Loading