Skip to content
Open
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
88 changes: 81 additions & 7 deletions DashWallet/Sources/Models/Transactions/WalletSendService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,19 +144,37 @@ final class PreparedStandardSend: NSObject {
txidWire: Data(txHash.reversed()), address: address, amount: amount, fee: fee)

case .rejected(_, let reason):
// Nothing reached the network, so the money is provably still
// here — say that, because "wasn't sent" alone reads as a loss.
let error = WalletSendService.makeError(
code: .broadcastRejected,
description: "The transaction wasn't sent. You can try again. \(reason)"
description: WalletSendService.BroadcastOutcomeCopy.rejected,
diagnostic: reason
)
claimLock.lock()
broadcastState = .ready
claimLock.unlock()
throw error

case .unknown(_, let reason):
// The old copy told the user to "wait for wallet synchronization"
// without saying that the wallet is the thing doing the waiting —
// and it appended the SDK's internal reason, so the dialog ended
// in "SPV broadcast saw no acceptance signal before dash-spv's
// acceptance timeout". Neither was actionable, and neither was
// localized, so a customer on a non-English device got a wall of
// English (support ticket 32189).
//
// What the copy promises is what the shipped SDK does: dash-spv
// keeps retrying while the wallet is open. It deliberately does
// NOT promise that retries survive closing the app —
// launch-time re-registration is dashpay/platform#4659 and is
// not in the SDK this builds against. See
// `BroadcastOutcomeCopy.unknown` for when that qualifier can go.
let error = WalletSendService.makeError(
code: .broadcastUnknown,
description: "We couldn't confirm whether the transaction was accepted. Don't send it again; wait for wallet synchronization. \(reason)"
description: WalletSendService.BroadcastOutcomeCopy.unknown,
diagnostic: reason
)
claimLock.lock()
broadcastState = .unknown(error)
Expand Down Expand Up @@ -217,6 +235,15 @@ final class WalletSendService: NSObject {
subsystem: "org.dashfoundation.dash",
category: "swift-sdk-migration.wallet-send-service")

/// `userInfo` key carrying the SDK's own explanation of a broadcast
/// outcome.
///
/// Deliberately not `NSLocalizedDescriptionKey`: it is engineer-facing
/// text and must never reach a dialog. Internal rather than file-private
/// because the point of keeping it is that logging, error inspection and
/// tests in other files can read it back.
static let diagnosticKey = "org.dashfoundation.dash.send.diagnostic"

/// See `RecentSendsRegistry` — the send-success screen's fallback source.
let recentSends = RecentSendsRegistry()

Expand Down Expand Up @@ -313,12 +340,14 @@ final class WalletSendService: NSObject {
} catch SwiftDashSDKTransactionSender.SendError.transactionRejected(_, let reason) {
throw Self.makeError(
code: .broadcastRejected,
description: "The transaction wasn't sent. You can try again. \(reason)"
description: BroadcastOutcomeCopy.rejected,
diagnostic: reason
)
} catch SwiftDashSDKTransactionSender.SendError.transactionStatusUnknown(_, let reason) {
throw Self.makeError(
code: .broadcastUnknown,
description: "We couldn't confirm whether the transaction was accepted. Don't send it again; wait for wallet synchronization. \(reason)"
description: BroadcastOutcomeCopy.unknown,
diagnostic: reason
)
}
}
Expand Down Expand Up @@ -727,11 +756,56 @@ private extension WalletSendService {
comment: "DashPay Contacts"))
}

static func makeError(code: ErrorCode, description: String) -> NSError {
NSError(
/// The two broadcast outcomes a user can be shown, in one place.
///
/// Every send route ends in one of these, and there is more than one route
/// — the prepared standard send and the selected-input / sweep path, which
/// broadcasts inside `buildAndSignFromAddress` and never reaches
/// `PreparedStandardSend.broadcast()`. They used to build the strings
/// independently and drifted apart, so one route kept showing unlocalized
/// English with the SDK's internal reason appended.
enum BroadcastOutcomeCopy {
/// Nothing reached the network, so the money is provably still here —
/// say that, because "wasn't sent" alone reads as a loss.
static var rejected: String {
NSLocalizedString(
"The transaction wasn't sent, so nothing left your wallet. You can try again.",
comment: "Send failed before any bytes reached the network")
}

/// The transaction went out and no acceptance signal came back.
///
/// The retry this promises is the one the shipped SDK actually
/// performs: dash-spv keeps rebroadcasting a transaction it is
/// tracking, for as long as the process lives. That is also why the
/// copy says "while it's open" rather than making an unqualified
/// promise — closing the app ends the retry today, and telling the
/// user otherwise would be worse than the old wording, because they
/// would close it believing the wallet had the situation in hand.
///
/// dashpay/platform#4659 re-registers unconfirmed sends for rebroadcast
/// at every launch, which makes closing the app harmless. This sentence
/// stays true either way; it can lose the qualifier once that ships.
static var unknown: String {
NSLocalizedString(
"We couldn't confirm the transaction reached the network. Don't send it again — the wallet keeps trying while it's open, and your balance will update as soon as it goes through.",
comment: "Send dispatched but no network acceptance signal arrived")
Comment on lines +770 to +792

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Add translations for the new broadcast-outcome strings

Both new NSLocalizedString keys are introduced here, but the PR contains no corresponding entries in the app's .strings catalogs. As a result, non-English devices—including the Russian-locale device from support ticket 32189—fall back to the English development strings. Add the source keys to the localization pipeline/catalogs and provide translations where available so the localization portion of the fix is effective.

source: gpt-6-astra (phase2-reviewer: general)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 62701467a — and thank you, this was the half of the change that actually mattered for the reporter on 32189, and I had broken it myself without noticing.

The keys were missing because the extraction that adds them runs during the build and rewrites all 43 catalogs, and I had been reverting that churn wholesale after every build. That is the right habit for keeping a pristine checkout clean and exactly the wrong one on a branch that introduces strings — so the code shipped localized and the catalog never heard about it.

Both entries are now in en.lproj, in alphabetical position with their translator comments. Added by hand rather than by committing the build output, so the diff is two entries instead of a four-thousand-line reshuffle; plutil -lint passes.

English only, which looks like the convention here — recently added keys such as "The operator key is missing from the keychain…" exist in en.lproj and nowhere else, and the other 42 locales are filled by the periodic translation pass (#1076 did 376 strings in one go). I have not invented translations: a wrong Russian string would be worse for that customer than an English one he can at least paste into a translator. If this repo expects the new keys seeded into every locale as untranslated copies, say so and I will add them.

🤖 Reviewed with Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved (re-reviewed at 62701467): The two broadcast-outcome keys are now present in DashWallet/en.lproj/Localizable.strings with matching comments and exact source strings. Other locales remain for the normal translation pipeline rather than being populated with unverified translations.

}
}

static func makeError(
code: ErrorCode,
description: String,
diagnostic: String? = nil
) -> NSError {
var userInfo: [String: Any] = [NSLocalizedDescriptionKey: description]
if let diagnostic, !diagnostic.isEmpty {
userInfo[diagnosticKey] = diagnostic
}
return NSError(
domain: errorDomain,
code: code.rawValue,
userInfo: [NSLocalizedDescriptionKey: description]
userInfo: userInfo
)
}
}
6 changes: 6 additions & 0 deletions DashWallet/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,9 @@
"The operator key is missing from the keychain — re-add it and try again." = "The operator key is missing from the keychain — re-add it and try again.";
"The temporary username must not itself require voting." = "The temporary username must not itself require voting.";

/* Send failed before any bytes reached the network */
"The transaction wasn't sent, so nothing left your wallet. You can try again." = "The transaction wasn't sent, so nothing left your wallet. You can try again.";

/* Usernames */
"The transfer settles through the network's withdrawal queue — usually a few minutes. You can close this sheet and come back; the unban resumes where it left off." = "The transfer settles through the network's withdrawal queue — usually a few minutes. You can close this sheet and come back; the unban resumes where it left off.";
"The unban was sent but its result couldn't be confirmed. Don't retry — check the masternode's status again in a few minutes." = "The unban was sent but its result couldn't be confirmed. Don't retry — check the masternode's status again in a few minutes.";
Expand Down Expand Up @@ -2859,6 +2862,9 @@
/* DashPay: banner identity row subtitle */
"View profile" = "View profile";

/* Send dispatched but no network acceptance signal arrived */
"We couldn't confirm the transaction reached the network. Don't send it again — the wallet keeps trying while it's open, and your balance will update as soon as it goes through." = "We couldn't confirm the transaction reached the network. Don't send it again — the wallet keeps trying while it's open, and your balance will update as soon as it goes through.";

/* DashPay FAQ */
"What about Unstoppable Domains and similar name services?" = "What about Unstoppable Domains and similar name services?";

Expand Down
Loading