From f6fdc0e3cbe5bce8f2c22bd297d480f5c6efb89d Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 10 Jun 2026 10:58:17 +0200 Subject: [PATCH 1/6] add TransferFlow engine: issue retry/polling, resume decision logic, transfer event helpers - pollAndIssue: polls the destination mint quote until paid, then issues ecash with retry and exponential backoff on transient errors (~30s budget) - pure resumeAction decision function (destination-first state machine) - classifyMeltFailure: distinguish definitive-unpaid from unknown outcome - Event.transferMints orients from/to via the proofs relationship because SwiftData does not preserve to-many relationship array order - persist mint quote expiry into pending transfer events (existing field) - fix latent index trap in EventList/EventSummary mint labels Co-Authored-By: Claude Fable 5 --- .../PersistentModelV1/EventFactory.swift | 37 +- .../Operations/transfer.swift | 394 ++++++++++++++++++ macadamia/Wallet/Events/EventList.swift | 3 +- macadamia/Wallet/Events/EventSummary.swift | 4 +- 4 files changed, 433 insertions(+), 5 deletions(-) create mode 100644 macadamia/PersistentModelV1/Operations/transfer.swift diff --git a/macadamia/PersistentModelV1/EventFactory.swift b/macadamia/PersistentModelV1/EventFactory.swift index 05c40e9..e6e667b 100644 --- a/macadamia/PersistentModelV1/EventFactory.swift +++ b/macadamia/PersistentModelV1/EventFactory.swift @@ -204,7 +204,9 @@ extension AppSchemaV1.Event { mintQuote: mintQuote, bolt11MeltQuote: meltQuote, amount: amount, - expiration: nil, + // the issuance deadline: mints may refuse to issue ecash on an + // expired quote even when it was paid + expiration: mintQuote.expiry.map { Date(timeIntervalSince1970: TimeInterval($0)) }, longDescription: nil, proofs: proofs, memo: nil, @@ -243,3 +245,36 @@ extension AppSchemaV1.Event { groupingID: groupingID) } } + +extension AppSchemaV1.Event { + /// The endpoints of a transfer event, by convention stored as + /// `mints[0]` = from, `mints[1]` = to. + /// + /// SwiftData does NOT reliably preserve the order of a to-many relationship + /// array, so the positional convention alone can come back flipped after a + /// save. The proofs relationship disambiguates: a pending transfer holds the + /// melt inputs (source mint), a completed transfer holds the newly issued + /// ecash (destination mint). + var transferMints: (from: Mint, to: Mint)? { + guard let mints, mints.count >= 2 else { return nil } + var endpoints = (from: mints[0], to: mints[1]) + + if let proofMint = proofs?.first?.mint { + switch kind { + case .pendingTransfer where proofMint == endpoints.to, + .transfer where proofMint == endpoints.from: + endpoints = (endpoints.to, endpoints.from) + default: + break + } + } + return endpoints + } + + /// Deadline for issuing ecash at the destination mint. Falls back to the + /// expiry inside the stored mint quote for events that predate `expiration` + /// being set on pending transfers. + var mintQuoteExpiry: Date? { + expiration ?? mintQuote?.expiry.map { Date(timeIntervalSince1970: TimeInterval($0)) } + } +} diff --git a/macadamia/PersistentModelV1/Operations/transfer.swift b/macadamia/PersistentModelV1/Operations/transfer.swift new file mode 100644 index 0000000..902ba89 --- /dev/null +++ b/macadamia/PersistentModelV1/Operations/transfer.swift @@ -0,0 +1,394 @@ +// +// transfer.swift +// macadamia +// +// Shared engine for the mint-to-mint transfer flow (melt at source mint, +// issue at destination mint). Used by both SwapManager and InlineSwapManager. +// + +import Foundation +import SwiftData +import CashuSwift +import OSLog + +fileprivate let transferLogger = Logger(subsystem: "macadamia", category: "TransferFlow") + +/// Errors specific to the transfer (melt + issue) flow. +enum TransferError: Error { + case missingData(String) + /// The melt definitively did not happen — the mint reported the quote unpaid. + case meltFailure(Error) + /// The melt request errored in a way that leaves the payment outcome unknown. + case meltOutcomeUnknown + case unknownQuoteState + /// The destination mint reports the quote as already issued. + case alreadyIssued + /// The destination mint refuses to issue because the quote expired. + case mintQuoteExpired + /// The lightning payment at the source mint is still in flight. + case paymentInFlight +} + +extension TransferError: LocalizedError { + var errorDescription: String? { + switch self { + case .missingData(let detail): + return detail + case .meltFailure(let error): + return String(localized: "The payment did not go through: \(error.localizedDescription)") + case .meltOutcomeUnknown: + return String(localized: "The payment outcome could not be determined. Nothing was changed — check again later.") + case .unknownQuoteState: + return String(localized: "The mint returned an unknown quote state.") + case .alreadyIssued: + return String(localized: """ + The destination mint reports that the ecash for this transfer \ + was already issued. If your balance does not reflect it, \ + you can recover it via Settings → Restore. + """) + case .mintQuoteExpired: + return String(localized: """ + The destination mint's quote for this transfer has expired. \ + If the payment already went through, the funds can only be \ + recovered by the mint operator. + """) + case .paymentInFlight: + return String(localized: "The Lightning payment is still in flight. Check again in a moment.") + } + } +} + +enum TransferFlow { + + // MARK: - Error classification + + enum IssueErrorClass: Equatable { + case retryable // transport errors or payment not yet registered + case alreadyIssued // 20002 + case staleOutputs // 10002 — derivation counter desync + case expired // 20007 + case terminal + } + + static func classifyIssueError(_ error: Error) -> IssueErrorClass { + if error is URLError { return .retryable } + guard let cashuError = error as? CashuError else { return .terminal } + switch cashuError { + case .networkError, .quoteNotPaid, .quoteIsPending: + return .retryable + case .proofsAlreadyIssuedForQuote: + return .alreadyIssued + case .blindedMessageAlreadySigned: + return .staleOutputs + case .quoteIsExpired: + return .expired + default: + return .terminal + } + } + + // MARK: - Retry policy + + struct IssueRetryPolicy: Sendable { + let quotePollInterval: TimeInterval + let quotePollMaxAttempts: Int + let mintMaxAttempts: Int + let backoffBase: TimeInterval + let backoffCap: TimeInterval + let totalBudget: TimeInterval + + static let interactive = IssueRetryPolicy(quotePollInterval: 2, + quotePollMaxAttempts: 5, + mintMaxAttempts: 3, + backoffBase: 1, + backoffCap: 4, + totalBudget: 30) + + func backoff(forAttempt attempt: Int) -> TimeInterval { + min(backoffCap, backoffBase * pow(2, Double(attempt - 1))) + } + } + + // MARK: - Poll & issue + + enum IssueOutcome { + case success(CashuSwift.IssueResult) + /// Retries or time budget exhausted without a definitive failure. + /// The pending event stays resumable. + case parkedPending(message: String, lastError: Error?) + case alreadyIssued + case staleOutputs(Error) + case expired + case failed(Error) + } + + /// Quote IDs currently being issued, to prevent the same transfer from + /// being completed twice concurrently (e.g. a double-tapped button). + @MainActor static var activeQuoteIDs = Set() + + /// Polls the destination mint's quote state until it is paid, then issues + /// ecash with retry and backoff on transient errors. + /// + /// Operates on sendable snapshots only and never touches SwiftData models. + /// Never throws — every path maps to an `IssueOutcome`. + static func pollAndIssue(mintQuote: CashuSwift.Bolt11.MintQuote, + on mint: CashuSwift.Mint, + seed: String?, + policy: IssueRetryPolicy = .interactive, + quoteState: (@Sendable (String, CashuSwift.Mint) async throws -> CashuSwift.Bolt11.MintQuote)? = nil, + mintExecutor: (@Sendable (CashuSwift.Bolt11.MintQuote, CashuSwift.Mint, String?) async throws -> CashuSwift.IssueResult)? = nil, + sleeper: (@Sendable (TimeInterval) async -> Void)? = nil) async -> IssueOutcome { + + let fetchQuoteState = quoteState ?? { id, mint in + try await CashuSwift.Bolt11.mintQuoteState(id, from: mint) + } + let executeMint = mintExecutor ?? { quote, mint, seed in + try await CashuSwift.Bolt11.mint(quote: quote, from: mint, seed: seed) + } + let sleep = sleeper ?? { seconds in + try? await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) + } + + let start = Date() + func budgetExhausted() -> Bool { + Date().timeIntervalSince(start) >= policy.totalBudget + } + + // Phase 1: wait for the destination mint to register the payment. + // A state-check failure is never terminal (it is read-only) — only a + // reported state can end the wait early. + var lastKnownState: CashuSwift.QuoteState? = nil + pollLoop: for attempt in 1...policy.quotePollMaxAttempts { + do { + let current = try await fetchQuoteState(mintQuote.quote, mint) + lastKnownState = current.state + transferLogger.debug("quote state poll \(attempt): \(current.state?.rawValue ?? "nil")") + } catch { + transferLogger.warning("quote state poll \(attempt) failed: \(error.localizedDescription)") + if classifyIssueError(error) == .expired { return .expired } + } + + switch lastKnownState { + case .paid: + break pollLoop + case .issued: + return .alreadyIssued + default: + if attempt < policy.quotePollMaxAttempts, !budgetExhausted() { + await sleep(policy.quotePollInterval) + } + } + } + + if lastKnownState == .unpaid || lastKnownState == .pending { + return .parkedPending(message: String(localized: """ + The destination mint has not registered the payment yet. \ + You can complete this transfer later from the transaction list. + """), + lastError: nil) + } + // State .paid or unknown (all polls errored): attempt to mint anyway — + // the mint endpoint is authoritative and its errors are classified below. + + // Phase 2: issue with retry and backoff. + var lastError: Error? = nil + mintLoop: for attempt in 1...policy.mintMaxAttempts { + do { + let result = try await executeMint(mintQuote, mint, seed) + transferLogger.info("issued ecash after \(attempt) attempt(s), DLEQ: \(String(describing: result.dleqResult))") + return .success(result) + } catch { + lastError = error + let classification = classifyIssueError(error) + transferLogger.warning("mint attempt \(attempt) failed (\(String(describing: classification))): \(error.localizedDescription)") + + switch classification { + case .retryable: + guard attempt < policy.mintMaxAttempts, !budgetExhausted() else { + break mintLoop + } + await sleep(policy.backoff(forAttempt: attempt)) + case .alreadyIssued: + return .alreadyIssued + case .staleOutputs: + return .staleOutputs(error) + case .expired: + return .expired + case .terminal: + return .failed(error) + } + } + } + + return .parkedPending(message: String(localized: """ + The destination mint did not respond. \ + You can complete this transfer later from the transaction list. + """), + lastError: lastError) + } + + // MARK: - Resume decision + + /// What we know about a quote at the time of resuming a transfer. + enum QuoteCheck: Equatable { + case state(CashuSwift.QuoteState) + case unavailable + } + + struct ResumeContext: Equatable { + let destination: QuoteCheck // mint quote state at the destination mint + let source: QuoteCheck? // melt quote state at the source mint; nil = not consulted + let mintQuoteExpired: Bool + let hasProofs: Bool + let hasBlankOutputs: Bool + } + + enum ResumeAction: Equatable { + case issue // destination paid → issue now + case informAlreadyIssued + case pollDestinationThenIssue // source paid, destination lagging + case waitSourcePending // lightning payment in flight — change nothing + case remelt // nothing happened yet, inputs available + case missingDataForRemelt + case informExpiredUnpaid // quote expired before payment — reversion allowed + case expiredStranded // paid but expired — operator recovery + case keepPendingUnknown // could not determine states — change nothing + } + + /// Pure, destination-first decision on how to resume a pending transfer. + static func resumeAction(for ctx: ResumeContext) -> ResumeAction { + switch ctx.destination { + case .unavailable: + return .keepPendingUnknown + case .state(.paid): + // Attempt issuance even when expired — some mints allow it, + // a refusal surfaces as IssueOutcome.expired. + return .issue + case .state(.issued): + return .informAlreadyIssued + case .state(.pending): + // Destination is mid-issuance (possibly a concurrent attempt) — do not interfere. + return .keepPendingUnknown + case .state(.unpaid): + guard let source = ctx.source else { return .keepPendingUnknown } + switch source { + case .unavailable, .state(.issued): + return .keepPendingUnknown + case .state(.paid): + return ctx.mintQuoteExpired ? .expiredStranded : .pollDestinationThenIssue + case .state(.pending): + return .waitSourcePending + case .state(.unpaid): + if ctx.mintQuoteExpired { return .informExpiredUnpaid } + return ctx.hasProofs ? .remelt : .missingDataForRemelt + } + } + } + + // MARK: - Melt failure classification + + enum MeltFailureClass: Equatable { + /// The mint reported the quote unpaid (or the request never reached it) — + /// inputs can safely return to the valid state. + case definitelyUnpaid + /// The payment may still settle — inputs must stay pending and the + /// event must keep its data. + case unknownOutcome + } + + static func classifyMeltFailure(error: Error?, reportedState: CashuSwift.QuoteState?) -> MeltFailureClass { + if reportedState == .unpaid { return .definitelyUnpaid } + + if let cashuError = error as? CashuError { + switch cashuError { + // Thrown by client-side validation before the melt request is sent, + // or by the mint refusing to pay an expired quote. + case .quoteIsExpired, .insufficientInputs, .unitError, .noActiveKeysetForUnit: + return .definitelyUnpaid + default: + return .unknownOutcome + } + } + return .unknownOutcome + } + + // MARK: - Source-side recovery + + /// Opportunistically settles the source side of a transfer whose payment is + /// known to have happened: marks the inputs spent and stores any NUT-08 + /// change. Failures are logged and never block issuance at the destination. + @MainActor + static func recoverSourceSide(meltQuote: CashuSwift.Bolt11.MeltQuote?, + from: Mint, + sendableFrom: CashuSwift.Mint, + preFetched: CashuSwift.MeltResult?, + blankOutputSet: BlankOutputSet?, + proofs: [Proof]?, + modelContext: ModelContext) async { + guard let meltQuote else { return } + + let result: CashuSwift.MeltResult? + if let preFetched { + result = preFetched + } else { + let outputs = (blankOutputSet?.outputs.isEmpty == false) ? blankOutputSet?.tuple() : nil + result = try? await CashuSwift.Bolt11.meltState(meltQuote.quote, + from: sendableFrom, + blankOutputs: outputs) + } + + guard let result, result.quote.state == .paid else { return } + + proofs?.setState(.spent) + if let change = result.change, !change.isEmpty { + do { + // re-adding previously stored change is safe: duplicates are skipped by C value + try from.addProofs(change, to: modelContext, increaseDerivationCounter: false) + } catch { + transferLogger.warning("could not store melt change during resume: \(error.localizedDescription)") + } + } + try? modelContext.save() + } + + // MARK: - Finalization + + /// Persists the result of a successful issuance: stores the new proofs + /// (incrementing the destination keyset counter), records the transfer + /// event and hides the pending event. + @MainActor + static func finalizeIssuedTransfer(issueResult: CashuSwift.IssueResult, + mintQuote: CashuSwift.Bolt11.MintQuote, + meltQuote: CashuSwift.Bolt11.MeltQuote?, + from: Mint, + to: Mint, + pendingTransferEvent: Event, + modelContext: ModelContext) throws { + guard let wallet = from.wallet else { + throw macadamiaError.databaseError("mint \(from.url.absoluteString) does not have an associated wallet.") + } + + let internalProofs = try to.addProofs(issueResult.proofs, to: modelContext) + + guard let meltQuote = meltQuote ?? pendingTransferEvent.bolt11MeltQuote else { + // No melt quote to record — still persist the proofs and resolve the event. + transferLogger.warning("finalizing transfer without a melt quote on record") + pendingTransferEvent.visible = false + try modelContext.save() + return + } + + let transferEvent = Event.transferEvent(wallet: wallet, + amount: pendingTransferEvent.amount ?? 0, + from: from, + to: to, + proofs: internalProofs, + meltQuote: meltQuote, + mintQuote: mintQuote, + preImage: meltQuote.paymentPreimage, + groupingID: nil) + modelContext.insert(transferEvent) + pendingTransferEvent.visible = false + try modelContext.save() + } +} diff --git a/macadamia/Wallet/Events/EventList.swift b/macadamia/Wallet/Events/EventList.swift index cd920a2..4938309 100644 --- a/macadamia/Wallet/Events/EventList.swift +++ b/macadamia/Wallet/Events/EventList.swift @@ -199,8 +199,7 @@ struct EventList: View { private var mintLabel: Text { switch eventGroup.events.first?.kind { case .pendingTransfer, .transfer: - if let from = eventGroup.events.first?.mints?[0], - let to = eventGroup.events.first?.mints?[1] { + if let (from, to) = eventGroup.events.first?.transferMints { return Text("\(from.displayName) ") + Text(Image(systemName: "arrow.right")) + Text(" \(to.displayName)") } else { return Text("...") diff --git a/macadamia/Wallet/Events/EventSummary.swift b/macadamia/Wallet/Events/EventSummary.swift index 970a453..2c67394 100644 --- a/macadamia/Wallet/Events/EventSummary.swift +++ b/macadamia/Wallet/Events/EventSummary.swift @@ -323,8 +323,8 @@ struct TransferEventSummary: View { var body: some View { List { Section { - TransferMintLabel(from: event.mints?[0].displayName ?? "Not found", - to: event.mints?[1].displayName ?? "Not found") + TransferMintLabel(from: event.transferMints?.from.displayName ?? "Not found", + to: event.transferMints?.to.displayName ?? "Not found") } header: { Text("Mints") } From fca6d74dbbf0ca0e397af3ed1684821d8472a466 Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 10 Jun 2026 10:58:30 +0200 Subject: [PATCH 2/6] make transfers retryable and resume destination-first in both swap managers - issue step now uses TransferFlow.pollAndIssue instead of a single-shot mint call; failures park the transfer as resumable pending state - melt failures only revert inputs to valid when the mint verifiably reports the quote unpaid; unknown outcomes (timeouts) keep proofs pending and preserve event data instead of wiping blankOutputs/proofs - melt errors trigger a one-shot meltState recheck: a payment that settled despite a thrown error now continues into issuance - resumeTransfer checks the destination mint quote first and no longer requires meltQuote/proofs/blankOutputs for the recovery path - new .pending state handled in SwapView, BalancerView, RedeemView - TransferView: live quote status rows and expiry instead of data- presence checkmarks, alert view was never attached (errors were silently swallowed), removal now verifies unpaid state and per-proof NUT-07 state with the mint before reverting Co-Authored-By: Claude Fable 5 --- macadamia/Mints/BalancerView.swift | 31 +- macadamia/Mints/SwapView.swift | 13 +- .../PersistentModelV1/Operations/swap.swift | 662 +++++++++++------- macadamia/Wallet/Redeem/RedeemView.swift | 4 + macadamia/Wallet/TransferView.swift | 251 +++++-- 5 files changed, 652 insertions(+), 309 deletions(-) diff --git a/macadamia/Mints/BalancerView.swift b/macadamia/Mints/BalancerView.swift index a23b743..e8c53a1 100644 --- a/macadamia/Mints/BalancerView.swift +++ b/macadamia/Mints/BalancerView.swift @@ -246,27 +246,38 @@ struct BalancerView: View { private func handleSwapStateChange(_ states: [SwapManager.TransferState]?) { guard let states else { return } - // Check if all swaps are complete (either success or fail) + // Check if all swaps are complete (success, parked as pending, or fail) let allComplete = states.allSatisfy { transferState in if case .success = transferState.state { return true } + if case .pending = transferState.state { return true } if case .fail = transferState.state { return true } return false } if allComplete { - // Check if all succeeded - let allSucceeded = states.allSatisfy { transferState in - if case .success = transferState.state { return true } + let anyFailed = states.contains { transferState in + if case .fail = transferState.state { return true } + return false + } + let anyPending = states.contains { transferState in + if case .pending = transferState.state { return true } return false } - if allSucceeded { + if anyFailed { + buttonState = .fail() + } else if anyPending { + buttonState = .success() + displayAlert(alert: AlertDetail(title: String(localized: "Transfers Pending"), + description: String(localized: """ + Some transfers could not finish yet. \ + You can complete them from the transaction list. + """))) + } else { buttonState = .success() DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { dismiss() } - } else { - buttonState = .fail() } } } @@ -286,6 +297,9 @@ struct BalancerView: View { case .success: Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) + case .pending: + Image(systemName: "hourglass") + .foregroundColor(.orange) case .fail: Image(systemName: "xmark.circle.fill") .foregroundColor(.red) @@ -307,6 +321,9 @@ struct BalancerView: View { case .success: Text("Complete") .foregroundColor(.green) + case .pending: + Text("Pending") + .foregroundColor(.orange) case .fail(let error): VStack(alignment: .leading) { Text("Failed") diff --git a/macadamia/Mints/SwapView.swift b/macadamia/Mints/SwapView.swift index 2b5347d..5932e7c 100644 --- a/macadamia/Mints/SwapView.swift +++ b/macadamia/Mints/SwapView.swift @@ -6,7 +6,7 @@ import CashuSwift struct SwapView: View { enum PaymentState { - case none, ready, setup, melting, minting, success, fail + case none, ready, setup, melting, minting, success, parked, fail } @State private var state: PaymentState = .none @@ -50,15 +50,15 @@ struct SwapView: View { } var isProgressSectionVisible: Bool { - state == .melting || state == .setup || state == .minting || state == .success + state == .melting || state == .setup || state == .minting || state == .success || state == .parked } var shouldShowSetupCheckmark: Bool { - state == .melting || state == .minting || state == .success + state == .melting || state == .minting || state == .success || state == .parked } var shouldShowMeltingCheckmark: Bool { - state == .minting || state == .success + state == .minting || state == .success || state == .parked } private var selectedMintBalance: Int { @@ -234,6 +234,11 @@ struct SwapView: View { DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { dismiss() } + case .pending(let message): + state = .parked + buttonState = .idle(String(localized: "Close"), action: { dismiss() }) + displayAlert(alert: AlertDetail(title: String(localized: "Transfer Pending"), + description: message)) case .fail(let error): state = .fail buttonState = .fail() diff --git a/macadamia/PersistentModelV1/Operations/swap.swift b/macadamia/PersistentModelV1/Operations/swap.swift index 8abbd0f..88cb4c7 100644 --- a/macadamia/PersistentModelV1/Operations/swap.swift +++ b/macadamia/PersistentModelV1/Operations/swap.swift @@ -8,14 +8,11 @@ fileprivate let swapLogger = Logger(subsystem: "macadamia", category: "SwapOpera @MainActor final class SwapManager: ObservableObject { - enum TransferError: Error { - case missingData(String) - case meltFailure(Error) - case unknownQuoteState - } - enum State: Equatable { case waiting, preparing, melting, minting, success + /// The transfer could not finish yet but remains resumable from the + /// transaction list (e.g. issuance retries exhausted, payment in flight). + case pending(String) case fail(Error) static func == (lhs: State, rhs: State) -> Bool { @@ -25,6 +22,7 @@ final class SwapManager: ObservableObject { case (.melting, .melting): return true case (.minting, .minting): return true case (.success, .success): return true + case (.pending, .pending): return true case (.fail, .fail): return true default: return false } @@ -190,74 +188,107 @@ final class SwapManager: ObservableObject { func resumeTransfer(with pendingTransferEvent: Event, modelContext: ModelContext) { guard let mintQuote = pendingTransferEvent.mintQuote, - let meltQuote = pendingTransferEvent.bolt11MeltQuote, - let mints = pendingTransferEvent.mints, - mints.count >= 2 else { - swapLogger.error("Cannot resume transfer: missing mint quote, melt quote, or mints") - setCurrentSwapState(.fail(TransferError.missingData("Unable to find mint quote, mwlt quote or associated mints for this transfer event."))) + let (from, to) = pendingTransferEvent.transferMints else { + swapLogger.error("Cannot resume transfer: missing mint quote or mints") + setCurrentSwapState(.fail(TransferError.missingData("Unable to find the mint quote or the mints associated with this transfer event."))) return } - let from = mints[0] - let to = mints[1] - - guard let blankOutputSet = pendingTransferEvent.blankOutputs else { - swapLogger.error("Cannot resume transfer: no change outputs assigned") - setCurrentSwapState(.fail(TransferError.missingData("No change outputs where assigned to this operation."))) - return - } + // these are only required for specific resume paths, not up front + let meltQuote = pendingTransferEvent.bolt11MeltQuote + let blankOutputSet = pendingTransferEvent.blankOutputs + let proofs = pendingTransferEvent.proofs + let expired = (pendingTransferEvent.mintQuoteExpiry ?? .distantFuture) < Date() let sendableFrom = CashuSwift.Mint(from) + let sendableTo = CashuSwift.Mint(to) - guard let proofs = pendingTransferEvent.proofs else { - swapLogger.error("Cannot resume transfer: no proofs assigned to operation") - setCurrentSwapState(.fail(TransferError.missingData("Trying to resume transfer, but no ecash was previously assigned to the operation."))) - return - } + setCurrentSwapState(.preparing) Task { - do { - let meltResult = try await CashuSwift.Bolt11.meltState(meltQuote.quote, - from: sendableFrom, - blankOutputs: blankOutputSet.outputs.isEmpty ? nil : blankOutputSet.tuple()) + // destination first: did the payment arrive, was the ecash already issued? + let destination: TransferFlow.QuoteCheck + if let state = (try? await CashuSwift.Bolt11.mintQuoteState(mintQuote.quote, from: sendableTo))?.state { + destination = .state(state) + } else { + destination = .unavailable + } - await MainActor.run { - switch meltResult.quote.state { - case .paid: - - meltDidSucceed(mintQuote: mintQuote, - newMeltQuote: meltResult.quote, - change: meltResult.change ?? [], - proofs: proofs, - from: from, - to: to, - pendingTransferEvent: pendingTransferEvent, - modelContext: modelContext) - - case .pending: - swapLogger.error("Resume transfer: melt quote state is pending (unknown)") - setCurrentSwapState(.fail(TransferError.unknownQuoteState)) - - case .unpaid: - - transferMelt(meltQuote: meltQuote, - mintQuote: mintQuote, - from: from, - to: to, - with: proofs, - pendingTransferEvent: pendingTransferEvent, - modelContext: modelContext) - - case .issued, .none: - swapLogger.error("Resume transfer: melt quote has unknown payment state") - setCurrentSwapState(.fail(TransferError.unknownQuoteState)) - } + // consult the source melt quote only when the destination has not seen the payment + var source: TransferFlow.QuoteCheck? = nil + var sourceMeltResult: CashuSwift.MeltResult? = nil + if destination == .state(.unpaid), let meltQuote { + let outputs = (blankOutputSet?.outputs.isEmpty == false) ? blankOutputSet?.tuple() : nil + sourceMeltResult = try? await CashuSwift.Bolt11.meltState(meltQuote.quote, + from: sendableFrom, + blankOutputs: outputs) + source = sourceMeltResult.flatMap { result in + result.quote.state.map { TransferFlow.QuoteCheck.state($0) } + } ?? .unavailable + } + + let context = TransferFlow.ResumeContext(destination: destination, + source: source, + mintQuoteExpired: expired, + hasProofs: !(proofs ?? []).isEmpty, + hasBlankOutputs: blankOutputSet != nil) + let action = TransferFlow.resumeAction(for: context) + swapLogger.info("resuming transfer: destination \(String(describing: destination)), source \(String(describing: source)) → \(String(describing: action))") + + switch action { + case .issue, .pollDestinationThenIssue: + await TransferFlow.recoverSourceSide(meltQuote: meltQuote, + from: from, + sendableFrom: sendableFrom, + preFetched: sourceMeltResult, + blankOutputSet: blankOutputSet, + proofs: proofs, + modelContext: modelContext) + transferIssue(mintQuote: mintQuote, + meltQuote: sourceMeltResult?.quote ?? meltQuote, + from: from, + to: to, + pendingTransferEvent: pendingTransferEvent, + modelContext: modelContext) + + case .informAlreadyIssued: + setCurrentSwapState(.fail(TransferError.alreadyIssued)) + + case .waitSourcePending: + setCurrentSwapState(.pending(String(localized: "The Lightning payment is still in flight. Check again in a moment."))) + + case .remelt: + guard let meltQuote, let proofs, !proofs.isEmpty else { + setCurrentSwapState(.fail(TransferError.missingData("The original ecash for this transfer is no longer attached to the event."))) + return } - } catch { - swapLogger.error("Resume transfer failed: \(error.localizedDescription)") - await MainActor.run { - setCurrentSwapState(.fail(error)) + if blankOutputSet == nil { + swapLogger.warning("re-melting without blank outputs — overpaid fees will not be returned as change") } + proofs.setState(.pending) + transferMelt(meltQuote: meltQuote, + mintQuote: mintQuote, + from: from, + to: to, + with: proofs, + pendingTransferEvent: pendingTransferEvent, + modelContext: modelContext) + + case .missingDataForRemelt: + setCurrentSwapState(.fail(TransferError.missingData("The payment was never made and the original ecash is no longer attached to this transfer event."))) + + case .informExpiredUnpaid: + // expired before the payment happened — reverting is the right call + setCurrentSwapState(.fail(TransferError.meltFailure(TransferError.mintQuoteExpired))) + + case .expiredStranded: + setCurrentSwapState(.pending(String(localized: """ + The payment went through but the destination mint's quote expired. \ + Contact the mint operator to recover the funds. + """))) + + case .keepPendingUnknown: + setCurrentSwapState(.pending(String(localized: "Could not determine the transfer's state. Nothing was changed — try again later."))) } } } @@ -333,46 +364,71 @@ final class SwapManager: ObservableObject { setCurrentSwapState(.melting) + let sendableFrom = CashuSwift.Mint(from) + let blankOutputSet = pendingTransferEvent.blankOutputs + Task { do { swapLogger.debug("Attempting to melt...") - swapLogger.debug("transfer event has change outputs assigned: \(pendingTransferEvent.blankOutputs?.outputs.isEmpty ?? true ? "empty" : "assigned and populated")") + swapLogger.debug("transfer event has change outputs assigned: \(blankOutputSet?.outputs.isEmpty ?? true ? "empty" : "assigned and populated")") let meltResult = try await CashuSwift.Bolt11.melt(quote: meltQuote, - from: CashuSwift.Mint(from), + from: sendableFrom, proofs: proofs.sendable(), - blankOutputs: pendingTransferEvent.blankOutputs?.tuple()) + blankOutputs: blankOutputSet?.tuple()) swapLogger.info("DLEQ check on melt change proofs: \(String(describing: meltResult.dleqResult))") - await MainActor.run { - if meltResult.quote.state == .paid { - - meltDidSucceed(mintQuote: mintQuote, - newMeltQuote: meltResult.quote, - change: meltResult.change ?? [], - proofs: proofs, - from: from, - to: to, - pendingTransferEvent: pendingTransferEvent, - modelContext: modelContext) - } else { - - swapLogger.info(""" - Melt function returned a quote with state NOT PAID, \ - probably because the lightning payment failed - """) - meltFailed(with: CashuError.unknownError("Transfer did not complete because the melt quote was returned with state UNPAID."), - proofs: proofs, - pendingTransferEvent: pendingTransferEvent, - modelContext: modelContext) - } + if meltResult.quote.state == .paid { + + meltDidSucceed(mintQuote: mintQuote, + newMeltQuote: meltResult.quote, + change: meltResult.change ?? [], + proofs: proofs, + from: from, + to: to, + pendingTransferEvent: pendingTransferEvent, + modelContext: modelContext) + } else { + + swapLogger.info(""" + Melt function returned a quote with state NOT PAID, \ + probably because the lightning payment failed + """) + meltFailed(with: CashuError.unknownError("Transfer did not complete because the melt quote was returned with state UNPAID."), + reportedState: meltResult.quote.state, + proofs: proofs, + pendingTransferEvent: pendingTransferEvent, + modelContext: modelContext) } } catch { swapLogger.error("Melt operation failed: \(error.localizedDescription)") - await MainActor.run { - meltFailed(with: error, proofs: proofs, pendingTransferEvent: pendingTransferEvent, modelContext: modelContext) + + // The thrown error alone cannot tell us whether the payment happened + // (e.g. a timeout while the payment settles). Re-check the quote once + // before deciding what to do with the inputs. + let outputs = (blankOutputSet?.outputs.isEmpty == false) ? blankOutputSet?.tuple() : nil + let recheck = try? await CashuSwift.Bolt11.meltState(meltQuote.quote, + from: sendableFrom, + blankOutputs: outputs) + + if let recheck, recheck.quote.state == .paid { + swapLogger.info("melt threw but the quote is PAID — continuing with issuance") + meltDidSucceed(mintQuote: mintQuote, + newMeltQuote: recheck.quote, + change: recheck.change ?? [], + proofs: proofs, + from: from, + to: to, + pendingTransferEvent: pendingTransferEvent, + modelContext: modelContext) + } else { + meltFailed(with: error, + reportedState: recheck?.quote.state, + proofs: proofs, + pendingTransferEvent: pendingTransferEvent, + modelContext: modelContext) } } } @@ -403,7 +459,7 @@ final class SwapManager: ObservableObject { } transferIssue(mintQuote: mintQuote, - meltResult: newMeltQuote, + meltQuote: newMeltQuote, from: from, to: to, pendingTransferEvent: pendingTransferEvent, @@ -411,20 +467,33 @@ final class SwapManager: ObservableObject { } - private func meltFailed(with error: Error, proofs: [Proof], pendingTransferEvent: Event, modelContext: ModelContext) { - swapLogger.error("Melt failed: \(error.localizedDescription). Reverting proofs to valid state.") - proofs.setState(.valid) - pendingTransferEvent.blankOutputs = nil - pendingTransferEvent.proofs = nil - - try? modelContext.save() - setCurrentSwapState(.fail(error)) + private func meltFailed(with error: Error, + reportedState: CashuSwift.QuoteState?, + proofs: [Proof], + pendingTransferEvent: Event, + modelContext: ModelContext) { + switch TransferFlow.classifyMeltFailure(error: error, reportedState: reportedState) { + case .definitelyUnpaid: + swapLogger.error("Melt definitively unpaid: \(error.localizedDescription). Reverting proofs to valid state.") + proofs.setState(.valid) + // the event keeps its proofs and blank outputs so the melt can be retried + try? modelContext.save() + setCurrentSwapState(.fail(TransferError.meltFailure(error))) + + case .unknownOutcome: + swapLogger.warning("Melt outcome unknown: \(error.localizedDescription). Keeping proofs pending and event data intact.") + try? modelContext.save() + setCurrentSwapState(.pending(String(localized: """ + The payment outcome could not be determined. \ + Check this transfer again later from the transaction list. + """))) + } nextSwapIfPresent() } private func transferIssue(mintQuote: CashuSwift.Bolt11.MintQuote, - meltResult: CashuSwift.Bolt11.MeltQuote, + meltQuote: CashuSwift.Bolt11.MeltQuote?, from: Mint, to: Mint, pendingTransferEvent: Event, @@ -433,49 +502,65 @@ final class SwapManager: ObservableObject { guard let wallet = from.wallet else { swapLogger.error("Mint \(from.url) does not have an associated wallet during issue") setCurrentSwapState(.fail(macadamiaError.databaseError("mint \(from.url.absoluteString) does not have an associated wallet."))) + nextSwapIfPresent() return } setCurrentSwapState(.minting) let sendableMint = CashuSwift.Mint(to) + let seed = wallet.seed + let quoteID = mintQuote.quote Task { - do { - let mintResult = try await CashuSwift.Bolt11.mint(quote: mintQuote, - from: sendableMint, - seed: wallet.seed) - - swapLogger.info("DLEQ check on newly minted proofs: \(String(describing: mintResult.dleqResult))") - - try await MainActor.run { + guard !TransferFlow.activeQuoteIDs.contains(quoteID) else { + swapLogger.warning("issuance already in progress for quote \(quoteID), skipping duplicate run") + return + } + TransferFlow.activeQuoteIDs.insert(quoteID) + defer { TransferFlow.activeQuoteIDs.remove(quoteID) } - let internalProofs = try to.addProofs(mintResult.proofs, to: modelContext) + let outcome = await TransferFlow.pollAndIssue(mintQuote: mintQuote, + on: sendableMint, + seed: seed) - let transferEvent = Event.transferEvent(wallet: wallet, - amount: pendingTransferEvent.amount ?? 0, + switch outcome { + case .success(let result): + do { + try TransferFlow.finalizeIssuedTransfer(issueResult: result, + mintQuote: mintQuote, + meltQuote: meltQuote, from: from, to: to, - proofs: internalProofs, - meltQuote: meltResult, - mintQuote: mintQuote, - preImage: meltResult.paymentPreimage, - groupingID: nil) - modelContext.insert(transferEvent) - pendingTransferEvent.visible = false - try modelContext.save() - + pendingTransferEvent: pendingTransferEvent, + modelContext: modelContext) setCurrentSwapState(.success) - - nextSwapIfPresent() - } - } catch { - swapLogger.error("Mint/issue operation failed: \(error.localizedDescription)") - await MainActor.run { + } catch { + swapLogger.error("Failed to persist issued transfer: \(error.localizedDescription)") setCurrentSwapState(.fail(error)) - nextSwapIfPresent() } + + case .parkedPending(let message, let lastError): + swapLogger.warning("transfer parked as pending. last error: \(String(describing: lastError))") + try? modelContext.save() + setCurrentSwapState(.pending(message)) + + case .alreadyIssued: + setCurrentSwapState(.fail(TransferError.alreadyIssued)) + + case .staleOutputs(let error): + swapLogger.error("stale outputs during issuance: \(error.localizedDescription)") + setCurrentSwapState(.fail(error)) + + case .expired: + setCurrentSwapState(.fail(TransferError.mintQuoteExpired)) + + case .failed(let error): + swapLogger.error("Mint/issue operation failed: \(error.localizedDescription)") + setCurrentSwapState(.fail(error)) } + + nextSwapIfPresent() } } @@ -642,14 +727,11 @@ final class SwapService { @MainActor final class InlineSwapManager: ObservableObject { - enum TransferError: Error { - case missingData(String) - case meltFailure(Error) - case unknownQuoteState - } - enum State { case ready, loading, melting, minting, success + /// The transfer could not finish yet but remains resumable from the + /// transaction list (e.g. issuance retries exhausted, payment in flight). + case pending(message: String) case fail(error: Error?) } @@ -750,67 +832,104 @@ final class InlineSwapManager: ObservableObject { func resumeTransfer(with pendingTransferEvent: Event) { guard let mintQuote = pendingTransferEvent.mintQuote, - let meltQuote = pendingTransferEvent.bolt11MeltQuote, - let mints = pendingTransferEvent.mints, - mints.count >= 2 else { - updateHandler(.fail(error: TransferError.missingData("Unable to find mint quote, mwlt quote or associated mints for this transfer event."))) + let (from, to) = pendingTransferEvent.transferMints else { + updateHandler(.fail(error: TransferError.missingData("Unable to find the mint quote or the mints associated with this transfer event."))) return } - let from = mints[0] - let to = mints[1] - - guard let blankOutputSet = pendingTransferEvent.blankOutputs else { - updateHandler(.fail(error: TransferError.missingData("No change outputs where assigned to this operation."))) - return - } + // these are only required for specific resume paths, not up front + let meltQuote = pendingTransferEvent.bolt11MeltQuote + let blankOutputSet = pendingTransferEvent.blankOutputs + let proofs = pendingTransferEvent.proofs + let expired = (pendingTransferEvent.mintQuoteExpiry ?? .distantFuture) < Date() let sendableFrom = CashuSwift.Mint(from) + let sendableTo = CashuSwift.Mint(to) - guard let proofs = pendingTransferEvent.proofs else { - updateHandler(.fail(error: TransferError.missingData("Trying to resume transfer, but no ecash was previously assigned to the operation."))) - return - } + updateHandler(.loading) Task { - do { - let meltResult = try await CashuSwift.Bolt11.meltState(meltQuote.quote, - from: sendableFrom, - blankOutputs: blankOutputSet.outputs.isEmpty ? nil : blankOutputSet.tuple()) - - await MainActor.run { - switch meltResult.quote.state { - case .paid: - - meltDidSucceed(mintQuote: mintQuote, - newMeltQuote: meltResult.quote, - change: meltResult.change ?? [], - proofs: proofs, - from: from, - to: to, - pendingTransferEvent: pendingTransferEvent) - - case .pending: - - updateHandler(.fail(error: TransferError.unknownQuoteState)) - - case .unpaid: + // destination first: did the payment arrive, was the ecash already issued? + let destination: TransferFlow.QuoteCheck + if let state = (try? await CashuSwift.Bolt11.mintQuoteState(mintQuote.quote, from: sendableTo))?.state { + destination = .state(state) + } else { + destination = .unavailable + } - transferMelt(meltQuote: meltQuote, - mintQuote: mintQuote, - from: from, - to: to, - with: proofs, - pendingTransferEvent: pendingTransferEvent) + // consult the source melt quote only when the destination has not seen the payment + var source: TransferFlow.QuoteCheck? = nil + var sourceMeltResult: CashuSwift.MeltResult? = nil + if destination == .state(.unpaid), let meltQuote { + let outputs = (blankOutputSet?.outputs.isEmpty == false) ? blankOutputSet?.tuple() : nil + sourceMeltResult = try? await CashuSwift.Bolt11.meltState(meltQuote.quote, + from: sendableFrom, + blankOutputs: outputs) + source = sourceMeltResult.flatMap { result in + result.quote.state.map { TransferFlow.QuoteCheck.state($0) } + } ?? .unavailable + } - case .issued, .none: - updateHandler(.fail(error: TransferError.unknownQuoteState)) - } + let context = TransferFlow.ResumeContext(destination: destination, + source: source, + mintQuoteExpired: expired, + hasProofs: !(proofs ?? []).isEmpty, + hasBlankOutputs: blankOutputSet != nil) + let action = TransferFlow.resumeAction(for: context) + swapLogger.info("resuming transfer: destination \(String(describing: destination)), source \(String(describing: source)) → \(String(describing: action))") + + switch action { + case .issue, .pollDestinationThenIssue: + await TransferFlow.recoverSourceSide(meltQuote: meltQuote, + from: from, + sendableFrom: sendableFrom, + preFetched: sourceMeltResult, + blankOutputSet: blankOutputSet, + proofs: proofs, + modelContext: modelContext) + transferIssue(mintQuote: mintQuote, + meltQuote: sourceMeltResult?.quote ?? meltQuote, + from: from, + to: to, + pendingTransferEvent: pendingTransferEvent) + + case .informAlreadyIssued: + updateHandler(.fail(error: TransferError.alreadyIssued)) + + case .waitSourcePending: + updateHandler(.pending(message: String(localized: "The Lightning payment is still in flight. Check again in a moment."))) + + case .remelt: + guard let meltQuote, let proofs, !proofs.isEmpty else { + updateHandler(.fail(error: TransferError.missingData("The original ecash for this transfer is no longer attached to the event."))) + return } - } catch { - await MainActor.run { - updateHandler(.fail(error: error)) + if blankOutputSet == nil { + swapLogger.warning("re-melting without blank outputs — overpaid fees will not be returned as change") } + proofs.setState(.pending) + transferMelt(meltQuote: meltQuote, + mintQuote: mintQuote, + from: from, + to: to, + with: proofs, + pendingTransferEvent: pendingTransferEvent) + + case .missingDataForRemelt: + updateHandler(.fail(error: TransferError.missingData("The payment was never made and the original ecash is no longer attached to this transfer event."))) + + case .informExpiredUnpaid: + // expired before the payment happened — reverting is the right call + updateHandler(.fail(error: TransferError.meltFailure(TransferError.mintQuoteExpired))) + + case .expiredStranded: + updateHandler(.pending(message: String(localized: """ + The payment went through but the destination mint's quote expired. \ + Contact the mint operator to recover the funds. + """))) + + case .keepPendingUnknown: + updateHandler(.pending(message: String(localized: "Could not determine the transfer's state. Nothing was changed — try again later."))) } } } @@ -881,43 +1000,67 @@ final class InlineSwapManager: ObservableObject { updateHandler(.melting) + let sendableFrom = CashuSwift.Mint(from) + let blankOutputSet = pendingTransferEvent.blankOutputs + Task { do { swapLogger.debug("Attempting to melt...") - swapLogger.debug("transfer event has change outputs assigned: \(pendingTransferEvent.blankOutputs?.outputs.isEmpty ?? true ? "empty" : "assigned and populated")") + swapLogger.debug("transfer event has change outputs assigned: \(blankOutputSet?.outputs.isEmpty ?? true ? "empty" : "assigned and populated")") let meltResult = try await CashuSwift.Bolt11.melt(quote: meltQuote, - from: CashuSwift.Mint(from), + from: sendableFrom, proofs: proofs.sendable(), - blankOutputs: pendingTransferEvent.blankOutputs?.tuple()) + blankOutputs: blankOutputSet?.tuple()) swapLogger.info("DLEQ check on melt change proofs: \(String(describing: meltResult.dleqResult))") - await MainActor.run { - if meltResult.quote.state == .paid { - - meltDidSucceed(mintQuote: mintQuote, - newMeltQuote: meltResult.quote, - change: meltResult.change ?? [], - proofs: proofs, - from: from, - to: to, - pendingTransferEvent: pendingTransferEvent) - } else { - - swapLogger.info(""" - Melt function returned a quote with state NOT PAID, \ - probably because the lightning payment failed - """) - meltFailed(with: CashuError.unknownError("Transfer did not complete because the melt quote was returned with state UNPAID."), - proofs: proofs, - pendingTransferEvent: pendingTransferEvent) - } + if meltResult.quote.state == .paid { + + meltDidSucceed(mintQuote: mintQuote, + newMeltQuote: meltResult.quote, + change: meltResult.change ?? [], + proofs: proofs, + from: from, + to: to, + pendingTransferEvent: pendingTransferEvent) + } else { + + swapLogger.info(""" + Melt function returned a quote with state NOT PAID, \ + probably because the lightning payment failed + """) + meltFailed(with: CashuError.unknownError("Transfer did not complete because the melt quote was returned with state UNPAID."), + reportedState: meltResult.quote.state, + proofs: proofs, + pendingTransferEvent: pendingTransferEvent) } } catch { - await MainActor.run { - meltFailed(with: error, proofs: proofs, pendingTransferEvent: pendingTransferEvent) + swapLogger.error("Melt operation failed: \(error.localizedDescription)") + + // The thrown error alone cannot tell us whether the payment happened + // (e.g. a timeout while the payment settles). Re-check the quote once + // before deciding what to do with the inputs. + let outputs = (blankOutputSet?.outputs.isEmpty == false) ? blankOutputSet?.tuple() : nil + let recheck = try? await CashuSwift.Bolt11.meltState(meltQuote.quote, + from: sendableFrom, + blankOutputs: outputs) + + if let recheck, recheck.quote.state == .paid { + swapLogger.info("melt threw but the quote is PAID — continuing with issuance") + meltDidSucceed(mintQuote: mintQuote, + newMeltQuote: recheck.quote, + change: recheck.change ?? [], + proofs: proofs, + from: from, + to: to, + pendingTransferEvent: pendingTransferEvent) + } else { + meltFailed(with: error, + reportedState: recheck?.quote.state, + proofs: proofs, + pendingTransferEvent: pendingTransferEvent) } } } @@ -945,24 +1088,37 @@ final class InlineSwapManager: ObservableObject { } transferIssue(mintQuote: mintQuote, - meltResult: newMeltQuote, + meltQuote: newMeltQuote, from: from, to: to, pendingTransferEvent: pendingTransferEvent) } - private func meltFailed(with error: Error, proofs: [Proof], pendingTransferEvent: Event) { - proofs.setState(.valid) - pendingTransferEvent.blankOutputs = nil - pendingTransferEvent.proofs = nil - - try? modelContext.save() - updateHandler(.fail(error: error)) + private func meltFailed(with error: Error, + reportedState: CashuSwift.QuoteState?, + proofs: [Proof], + pendingTransferEvent: Event) { + switch TransferFlow.classifyMeltFailure(error: error, reportedState: reportedState) { + case .definitelyUnpaid: + swapLogger.error("Melt definitively unpaid: \(error.localizedDescription). Reverting proofs to valid state.") + proofs.setState(.valid) + // the event keeps its proofs and blank outputs so the melt can be retried + try? modelContext.save() + updateHandler(.fail(error: TransferError.meltFailure(error))) + + case .unknownOutcome: + swapLogger.warning("Melt outcome unknown: \(error.localizedDescription). Keeping proofs pending and event data intact.") + try? modelContext.save() + updateHandler(.pending(message: String(localized: """ + The payment outcome could not be determined. \ + Check this transfer again later from the transaction list. + """))) + } } private func transferIssue(mintQuote: CashuSwift.Bolt11.MintQuote, - meltResult: CashuSwift.Bolt11.MeltQuote, + meltQuote: CashuSwift.Bolt11.MeltQuote?, from: Mint, to: Mint, pendingTransferEvent: Event) { @@ -975,43 +1131,55 @@ final class InlineSwapManager: ObservableObject { updateHandler(.minting) let sendableMint = CashuSwift.Mint(to) + let seed = wallet.seed + let quoteID = mintQuote.quote Task { - do { - let mintResult = try await CashuSwift.Bolt11.mint(quote: mintQuote, - from: sendableMint, - seed: wallet.seed) + guard !TransferFlow.activeQuoteIDs.contains(quoteID) else { + swapLogger.warning("issuance already in progress for quote \(quoteID), skipping duplicate run") + return + } + TransferFlow.activeQuoteIDs.insert(quoteID) + defer { TransferFlow.activeQuoteIDs.remove(quoteID) } - swapLogger.info("DLEQ check on newly minted proofs: \(String(describing: mintResult.dleqResult))") + let outcome = await TransferFlow.pollAndIssue(mintQuote: mintQuote, + on: sendableMint, + seed: seed) - await MainActor.run { - do { - let internalProofs = try to.addProofs(mintResult.proofs, to: modelContext) - - let transferEvent = Event.transferEvent(wallet: wallet, - amount: pendingTransferEvent.amount ?? 0, - from: from, - to: to, - proofs: internalProofs, - meltQuote: meltResult, - mintQuote: mintQuote, - preImage: meltResult.paymentPreimage, - groupingID: nil) - modelContext.insert(transferEvent) - pendingTransferEvent.visible = false - try modelContext.save() - - updateHandler(.success) - } catch { - updateHandler(.fail(error: error)) - } + switch outcome { + case .success(let result): + do { + try TransferFlow.finalizeIssuedTransfer(issueResult: result, + mintQuote: mintQuote, + meltQuote: meltQuote, + from: from, + to: to, + pendingTransferEvent: pendingTransferEvent, + modelContext: modelContext) + updateHandler(.success) + } catch { + swapLogger.error("Failed to persist issued transfer: \(error.localizedDescription)") + updateHandler(.fail(error: error)) } - } catch { - DispatchQueue.main.async { - #warning("handle error during minting") - self.updateHandler(.fail(error: error)) - } + case .parkedPending(let message, let lastError): + swapLogger.warning("transfer parked as pending. last error: \(String(describing: lastError))") + try? modelContext.save() + updateHandler(.pending(message: message)) + + case .alreadyIssued: + updateHandler(.fail(error: TransferError.alreadyIssued)) + + case .staleOutputs(let error): + swapLogger.error("stale outputs during issuance: \(error.localizedDescription)") + updateHandler(.fail(error: error)) + + case .expired: + updateHandler(.fail(error: TransferError.mintQuoteExpired)) + + case .failed(let error): + swapLogger.error("Mint/issue operation failed: \(error.localizedDescription)") + updateHandler(.fail(error: error)) } } } diff --git a/macadamia/Wallet/Redeem/RedeemView.swift b/macadamia/Wallet/Redeem/RedeemView.swift index dcb1186..505afbd 100644 --- a/macadamia/Wallet/Redeem/RedeemView.swift +++ b/macadamia/Wallet/Redeem/RedeemView.swift @@ -391,6 +391,10 @@ struct RedeemView: View { if let onSuccess { onSuccess() } + case .pending(let message): + buttonState = .idle(String(localized: "Close"), action: { dismiss() }) + displayAlert(alert: AlertDetail(title: String(localized: "Transfer Pending"), + description: message)) case .fail(let error): buttonState = .fail() redeemLogger.error("could not swap token to mint due to error \(error)") diff --git a/macadamia/Wallet/TransferView.swift b/macadamia/Wallet/TransferView.swift index 3c423bc..784d71a 100644 --- a/macadamia/Wallet/TransferView.swift +++ b/macadamia/Wallet/TransferView.swift @@ -42,13 +42,18 @@ struct TransferView: View { } private var transferMints: (from: Mint, to: Mint)? { - guard let mints = pendingTransferEvent.mints, - mints.count >= 2 - else { return nil } + pendingTransferEvent.transferMints + } - return (mints[0], mints[1]) + private enum RemoteStatus { + case loading + case state(CashuSwift.QuoteState) + case unavailable } + @State private var sourceStatus: RemoteStatus = .loading + @State private var destinationStatus: RemoteStatus = .loading + var body: some View { ZStack { List { @@ -68,32 +73,17 @@ struct TransferView: View { Text("Amount") } } - + Section { - VStack(alignment: .leading) { - HStack { - Image(systemName: pendingTransferEvent.bolt11MeltQuote == nil ? "xmark" : "checkmark") - .frame(width: 20) - Text("Payment Quote") - } - HStack { - Image(systemName: pendingTransferEvent.mintQuote == nil ? "xmark" : "checkmark") - .frame(width: 20) - Text("Ecash Quote") - } - HStack { - Image(systemName: pendingTransferEvent.proofs == nil || pendingTransferEvent.proofs?.isEmpty ?? true ? "xmark" : "checkmark") - .frame(width: 20) - Text("Ecash selected") - } - HStack { - Image(systemName: pendingTransferEvent.blankOutputs == nil ? "xmark" : "checkmark") - .frame(width: 20) - Text("Change created") - } + VStack(alignment: .leading, spacing: 8) { + statusRow(title: String(localized: "Payment"), status: sourceStatus, isMeltSide: true) + statusRow(title: String(localized: "Ecash issued"), status: destinationStatus, isMeltSide: false) + expiryRow } .font(.callout) .foregroundStyle(.secondary) + } header: { + Text("Status") } } VStack { @@ -105,10 +95,112 @@ struct TransferView: View { .onAppear { buttonState = .idle(String(localized: "Complete Transfer"), action: { complete() }) } + .task { + await loadStatus() + } + .alertView(isPresented: $showAlert, currentAlert: currentAlert) } - + + @ViewBuilder + private func statusRow(title: String, status: RemoteStatus, isMeltSide: Bool) -> some View { + HStack { + Group { + switch status { + case .loading: + ProgressView() + .scaleEffect(0.7) + case .state(let state): + switch state { + case .paid: + Image(systemName: isMeltSide ? "checkmark.circle.fill" : "hourglass") + .foregroundStyle(isMeltSide ? .green : .orange) + case .issued: + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(.green) + case .pending: + Image(systemName: "hourglass") + .foregroundStyle(.orange) + case .unpaid: + Image(systemName: "circle") + } + case .unavailable: + Image(systemName: "questionmark.circle") + } + } + .frame(width: 20) + Text(title) + Spacer() + Text(statusDescription(status, isMeltSide: isMeltSide)) + } + } + + private func statusDescription(_ status: RemoteStatus, isMeltSide: Bool) -> String { + switch status { + case .loading: + return "" + case .unavailable: + return String(localized: "Unknown") + case .state(let state): + switch (state, isMeltSide) { + case (.paid, true): return String(localized: "Paid") + case (.paid, false): return String(localized: "Payment received") + case (.pending, true): return String(localized: "In flight") + case (.pending, false): return String(localized: "Processing") + case (.unpaid, true): return String(localized: "Unpaid") + case (.unpaid, false): return String(localized: "Awaiting payment") + case (.issued, _): return String(localized: "Issued") + } + } + } + + @ViewBuilder + private var expiryRow: some View { + if let expiry = pendingTransferEvent.mintQuoteExpiry { + HStack { + Image(systemName: "clock") + .frame(width: 20) + Text("Quote expires") + Spacer() + if expiry < Date() { + Text("expired — completing may fail") + .foregroundStyle(.red) + } else { + Text(expiry, style: .relative) + .foregroundStyle(expiry.timeIntervalSinceNow < 900 ? .orange : .secondary) + } + } + } + } + + private func loadStatus() async { + guard let (from, to) = transferMints else { + sourceStatus = .unavailable + destinationStatus = .unavailable + return + } + + let sendableFrom = CashuSwift.Mint(from) + let sendableTo = CashuSwift.Mint(to) + let mintQuote = pendingTransferEvent.mintQuote + let meltQuote = pendingTransferEvent.bolt11MeltQuote + + if let mintQuote, + let state = (try? await CashuSwift.Bolt11.mintQuoteState(mintQuote.quote, from: sendableTo))?.state { + destinationStatus = .state(state) + } else { + destinationStatus = .unavailable + } + + if let meltQuote, + let state = (try? await CashuSwift.Bolt11.meltQuoteState(meltQuote.quote, from: sendableFrom))?.state { + sourceStatus = .state(state) + } else { + sourceStatus = .unavailable + } + } + private func complete() { - + currentSwapManager = InlineSwapManager(modelContext: modelContext, updateHandler: { state in switch state { case .ready: @@ -124,23 +216,38 @@ struct TransferView: View { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { dismiss() } + case .pending(let message): + buttonState = .idle(String(localized: "Check Again"), action: { complete() }) + displayAlert(alert: AlertDetail(title: String(localized: "Still Pending"), + description: message)) + Task { await loadStatus() } case .fail(let error): buttonState = .fail() - + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + buttonState = .idle(String(localized: "Complete Transfer"), action: { complete() }) + } + Task { await loadStatus() } + switch error { - case let .meltFailure(meltError) as InlineSwapManager.TransferError: - - let primary = AlertButton(title: String(localized: "Remove Payment"), - role: .destructive, - action: { removeEvent() }) - - displayAlert(alert: AlertDetail(title: String(localized: "Unpaid ⚠"), - description: String(localized: "This payment did not go through and one or more parts are marked \"unpaid\". The Ecash reserved for this operation is still valid and can be made available by removing the pending transfer event."), - primaryButton: primary)) - - - case let .missingData(string) as InlineSwapManager.TransferError: - displayAlert(alert: AlertDetail(title: String(localized: "Missing Data"), description: string)) + case let transferError as TransferError: + switch transferError { + case .meltFailure(let meltError): + let primary = AlertButton(title: String(localized: "Remove Payment"), + role: .destructive, + action: { verifyAndRemoveEvent() }) + + displayAlert(alert: AlertDetail(title: String(localized: "Unpaid ⚠"), + description: String(localized: """ + This payment did not go through: \(meltError.localizedDescription) \ + The Ecash reserved for this operation can be made available again \ + by removing the pending transfer event. + """), + primaryButton: primary)) + case .missingData(let string): + displayAlert(alert: AlertDetail(title: String(localized: "Missing Data"), description: string)) + default: + displayAlert(alert: AlertDetail(with: transferError)) + } case let error?: displayAlert(alert: AlertDetail(title: String(localized: "Error"), description: error.localizedDescription)) case nil: @@ -148,18 +255,60 @@ struct TransferView: View { } } }) - + currentSwapManager?.resumeTransfer(with: pendingTransferEvent) - + } - - private func removeEvent() { - pendingTransferEvent.proofs?.setState(.valid) - pendingTransferEvent.proofs = nil - pendingTransferEvent.visible = false - dismiss() + + /// Removes the pending transfer only after verifying with the mint that the + /// payment is unpaid, and reverts only those inputs the mint confirms unspent. + private func verifyAndRemoveEvent() { + buttonState = .loading(String(localized: "Verifying...")) + + guard let (from, _) = transferMints else { + buttonState = .idle(String(localized: "Complete Transfer"), action: { complete() }) + displayAlert(alert: AlertDetail(title: String(localized: "Cannot Remove"), + description: String(localized: "The mints for this transfer could not be found."))) + return + } + + let sendableFrom = CashuSwift.Mint(from) + let meltQuote = pendingTransferEvent.bolt11MeltQuote + let proofs = pendingTransferEvent.proofs ?? [] + + Task { + if let meltQuote { + let state = (try? await CashuSwift.Bolt11.meltQuoteState(meltQuote.quote, from: sendableFrom))?.state + guard state == .unpaid else { + buttonState = .idle(String(localized: "Complete Transfer"), action: { complete() }) + displayAlert(alert: AlertDetail(title: String(localized: "Cannot Remove"), + description: String(localized: """ + The payment could not be verified as unpaid. Removing it now \ + could lose funds — try completing the transfer instead. + """))) + return + } + } + + if !proofs.isEmpty { + guard let states = try? await CashuSwift.check(proofs.sendable(), mint: sendableFrom), + states.count == proofs.count else { + buttonState = .idle(String(localized: "Complete Transfer"), action: { complete() }) + displayAlert(alert: AlertDetail(title: String(localized: "Cannot Remove"), + description: String(localized: "The state of the reserved ecash could not be checked with the mint. Try again later."))) + return + } + for (proof, state) in zip(proofs, states) { + proof.state = (state == .unspent) ? .valid : .spent + } + } + + pendingTransferEvent.visible = false + try? modelContext.save() + dismiss() + } } - + private func displayAlert(alert: AlertDetail) { currentAlert = alert showAlert = true From 49f7dab368646ebd015124974c2fca659b3d8399 Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 10 Jun 2026 10:58:30 +0200 Subject: [PATCH 3/6] add unit tests for TransferFlow engine Co-Authored-By: Claude Fable 5 --- macadamiaTests/TransferFlowTests.swift | 400 +++++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 macadamiaTests/TransferFlowTests.swift diff --git a/macadamiaTests/TransferFlowTests.swift b/macadamiaTests/TransferFlowTests.swift new file mode 100644 index 0000000..dea0b90 --- /dev/null +++ b/macadamiaTests/TransferFlowTests.swift @@ -0,0 +1,400 @@ +@testable import macadamia +import XCTest +import CashuSwift +import SwiftData +import BIP39 + +final class TransferFlowTests: XCTestCase { + + // MARK: - Helpers + + /// Test-only recorder for injected closures. Access is single-threaded in + /// these tests (the engine awaits every closure call). + private final class Recorder: @unchecked Sendable { + var delays: [TimeInterval] = [] + var quoteStateCalls = 0 + var mintCalls = 0 + } + + private func makeMintQuote(state: CashuSwift.QuoteState? = .paid, + expiry: Int? = nil) -> CashuSwift.Bolt11.MintQuote { + CashuSwift.Bolt11.MintQuote(quote: "quote-id-123", + request: "lnbc210n1...", + amount: 21, + unit: "sat", + state: state, + expiry: expiry) + } + + private func makeSendableMint() -> CashuSwift.Mint { + CashuSwift.Mint(url: URL(string: "http://localhost:3338")!, keysets: []) + } + + private func makeIssueResult() throws -> CashuSwift.IssueResult { + // IssueResult has no public initializer but is Codable + let json = #"{"proofs":[],"dleqResult":{"valid":{}}}"# + return try JSONDecoder().decode(CashuSwift.IssueResult.self, from: Data(json.utf8)) + } + + // MARK: - Issue error classification + + func testClassifyIssueError() { + XCTAssertEqual(TransferFlow.classifyIssueError(CashuError.networkError), .retryable) + XCTAssertEqual(TransferFlow.classifyIssueError(CashuError.quoteNotPaid), .retryable) + XCTAssertEqual(TransferFlow.classifyIssueError(CashuError.quoteIsPending), .retryable) + XCTAssertEqual(TransferFlow.classifyIssueError(URLError(.timedOut)), .retryable) + + XCTAssertEqual(TransferFlow.classifyIssueError(CashuError.proofsAlreadyIssuedForQuote), .alreadyIssued) + XCTAssertEqual(TransferFlow.classifyIssueError(CashuError.blindedMessageAlreadySigned), .staleOutputs) + XCTAssertEqual(TransferFlow.classifyIssueError(CashuError.quoteIsExpired), .expired) + + XCTAssertEqual(TransferFlow.classifyIssueError(CashuError.unknownError("weird")), .terminal) + XCTAssertEqual(TransferFlow.classifyIssueError(CashuError.alreadySpent), .terminal) + XCTAssertEqual(TransferFlow.classifyIssueError(macadamiaError.multiMintToken), .terminal) + } + + // MARK: - Melt failure classification + + func testClassifyMeltFailure() { + // the mint reported unpaid — definitive, regardless of the error + XCTAssertEqual(TransferFlow.classifyMeltFailure(error: CashuError.networkError, reportedState: .unpaid), + .definitelyUnpaid) + XCTAssertEqual(TransferFlow.classifyMeltFailure(error: nil, reportedState: .unpaid), + .definitelyUnpaid) + + // errors thrown before the request is sent, or refusal of an expired quote + XCTAssertEqual(TransferFlow.classifyMeltFailure(error: CashuError.quoteIsExpired, reportedState: nil), + .definitelyUnpaid) + XCTAssertEqual(TransferFlow.classifyMeltFailure(error: CashuError.insufficientInputs(""), reportedState: nil), + .definitelyUnpaid) + XCTAssertEqual(TransferFlow.classifyMeltFailure(error: CashuError.unitError(""), reportedState: nil), + .definitelyUnpaid) + + // anything transport-shaped must preserve funds + XCTAssertEqual(TransferFlow.classifyMeltFailure(error: CashuError.networkError, reportedState: nil), + .unknownOutcome) + XCTAssertEqual(TransferFlow.classifyMeltFailure(error: CashuError.quoteIsPending, reportedState: nil), + .unknownOutcome) + XCTAssertEqual(TransferFlow.classifyMeltFailure(error: URLError(.timedOut), reportedState: nil), + .unknownOutcome) + XCTAssertEqual(TransferFlow.classifyMeltFailure(error: nil, reportedState: nil), + .unknownOutcome) + XCTAssertEqual(TransferFlow.classifyMeltFailure(error: nil, reportedState: .pending), + .unknownOutcome) + } + + // MARK: - Resume decision table + + private func ctx(dest: TransferFlow.QuoteCheck, + source: TransferFlow.QuoteCheck? = nil, + expired: Bool = false, + hasProofs: Bool = true, + hasBlankOutputs: Bool = true) -> TransferFlow.ResumeContext { + TransferFlow.ResumeContext(destination: dest, + source: source, + mintQuoteExpired: expired, + hasProofs: hasProofs, + hasBlankOutputs: hasBlankOutputs) + } + + func testResumeActionDecisionTable() { + // destination paid → issue, no matter what else + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.paid))), .issue) + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.paid), expired: true, hasProofs: false, hasBlankOutputs: false)), .issue) + + // destination issued + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.issued))), .informAlreadyIssued) + + // destination mid-issuance → hands off + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.pending))), .keepPendingUnknown) + + // destination unreachable → change nothing + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .unavailable)), .keepPendingUnknown) + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .unavailable, source: .state(.paid))), .keepPendingUnknown) + + // destination unpaid, source paid → settlement lag (or stranded when expired) + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.unpaid), source: .state(.paid))), .pollDestinationThenIssue) + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.unpaid), source: .state(.paid), expired: true)), .expiredStranded) + + // source payment in flight → wait, preserve everything + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.unpaid), source: .state(.pending))), .waitSourcePending) + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.unpaid), source: .state(.pending), expired: true)), .waitSourcePending) + + // nothing happened yet + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.unpaid), source: .state(.unpaid))), .remelt) + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.unpaid), source: .state(.unpaid), hasProofs: false)), .missingDataForRemelt) + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.unpaid), source: .state(.unpaid), expired: true)), .informExpiredUnpaid) + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.unpaid), source: .state(.unpaid), expired: true, hasProofs: false)), .informExpiredUnpaid) + + // source state unknown → change nothing + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.unpaid), source: .unavailable)), .keepPendingUnknown) + XCTAssertEqual(TransferFlow.resumeAction(for: ctx(dest: .state(.unpaid), source: nil)), .keepPendingUnknown) + } + + // MARK: - pollAndIssue + + func testPollAndIssueRetriesThenSucceeds() async throws { + let recorder = Recorder() + let result = try makeIssueResult() + + let outcome = await TransferFlow.pollAndIssue( + mintQuote: makeMintQuote(state: .unpaid), + on: makeSendableMint(), + seed: nil, + quoteState: { [recorder] _, _ in + recorder.quoteStateCalls += 1 + return self.makeMintQuote(state: .paid) + }, + mintExecutor: { [recorder] _, _, _ in + recorder.mintCalls += 1 + if recorder.mintCalls < 3 { throw CashuError.networkError } + return result + }, + sleeper: { [recorder] delay in recorder.delays.append(delay) } + ) + + guard case .success = outcome else { + return XCTFail("expected success, got \(outcome)") + } + XCTAssertEqual(recorder.mintCalls, 3) + // two failed mint attempts → exponential backoff 1s, 2s + XCTAssertEqual(recorder.delays, [1, 2]) + } + + func testPollAndIssueParksWhileUnpaid() async { + let recorder = Recorder() + + let outcome = await TransferFlow.pollAndIssue( + mintQuote: makeMintQuote(state: .unpaid), + on: makeSendableMint(), + seed: nil, + quoteState: { [recorder] _, _ in + recorder.quoteStateCalls += 1 + return self.makeMintQuote(state: .unpaid) + }, + mintExecutor: { [recorder] _, _, _ in + recorder.mintCalls += 1 + XCTFail("must not attempt to mint an unpaid quote") + throw CashuError.quoteNotPaid + }, + sleeper: { [recorder] delay in recorder.delays.append(delay) } + ) + + guard case .parkedPending = outcome else { + return XCTFail("expected parkedPending, got \(outcome)") + } + XCTAssertEqual(recorder.quoteStateCalls, 5) + XCTAssertEqual(recorder.mintCalls, 0) + // sleeps between polls only (none after the final one) + XCTAssertEqual(recorder.delays, [2, 2, 2, 2]) + } + + func testPollAndIssueDetectsAlreadyIssuedFromQuoteState() async { + let recorder = Recorder() + + let outcome = await TransferFlow.pollAndIssue( + mintQuote: makeMintQuote(state: .paid), + on: makeSendableMint(), + seed: nil, + quoteState: { _, _ in self.makeMintQuote(state: .issued) }, + mintExecutor: { [recorder] _, _, _ in + recorder.mintCalls += 1 + throw CashuError.proofsAlreadyIssuedForQuote + }, + sleeper: { _ in } + ) + + guard case .alreadyIssued = outcome else { + return XCTFail("expected alreadyIssued, got \(outcome)") + } + XCTAssertEqual(recorder.mintCalls, 0) + } + + func testPollAndIssueDetectsAlreadyIssuedFromMintError() async { + let outcome = await TransferFlow.pollAndIssue( + mintQuote: makeMintQuote(state: .paid), + on: makeSendableMint(), + seed: nil, + quoteState: { _, _ in self.makeMintQuote(state: .paid) }, + mintExecutor: { _, _, _ in throw CashuError.proofsAlreadyIssuedForQuote }, + sleeper: { _ in } + ) + + guard case .alreadyIssued = outcome else { + return XCTFail("expected alreadyIssued, got \(outcome)") + } + } + + func testPollAndIssueExpired() async { + let outcome = await TransferFlow.pollAndIssue( + mintQuote: makeMintQuote(state: .paid), + on: makeSendableMint(), + seed: nil, + quoteState: { _, _ in self.makeMintQuote(state: .paid) }, + mintExecutor: { _, _, _ in throw CashuError.quoteIsExpired }, + sleeper: { _ in } + ) + + guard case .expired = outcome else { + return XCTFail("expected expired, got \(outcome)") + } + } + + func testPollAndIssueTerminalError() async { + let outcome = await TransferFlow.pollAndIssue( + mintQuote: makeMintQuote(state: .paid), + on: makeSendableMint(), + seed: nil, + quoteState: { _, _ in self.makeMintQuote(state: .paid) }, + mintExecutor: { _, _, _ in throw CashuError.unknownError("broken") }, + sleeper: { _ in } + ) + + guard case .failed = outcome else { + return XCTFail("expected failed, got \(outcome)") + } + } + + func testPollAndIssueParksAfterMintRetriesExhausted() async { + let recorder = Recorder() + + let outcome = await TransferFlow.pollAndIssue( + mintQuote: makeMintQuote(state: .paid), + on: makeSendableMint(), + seed: nil, + quoteState: { _, _ in self.makeMintQuote(state: .paid) }, + mintExecutor: { [recorder] _, _, _ in + recorder.mintCalls += 1 + throw CashuError.networkError + }, + sleeper: { [recorder] delay in recorder.delays.append(delay) } + ) + + guard case .parkedPending(_, let lastError) = outcome else { + return XCTFail("expected parkedPending, got \(outcome)") + } + XCTAssertNotNil(lastError) + XCTAssertEqual(recorder.mintCalls, 3) + XCTAssertEqual(recorder.delays, [1, 2]) + } + + func testPollAndIssueMintsEvenWhenStateChecksFail() async throws { + // the state endpoint being broken must not block issuance — the mint + // endpoint is authoritative + let result = try makeIssueResult() + let recorder = Recorder() + + let outcome = await TransferFlow.pollAndIssue( + mintQuote: makeMintQuote(state: nil), + on: makeSendableMint(), + seed: nil, + quoteState: { _, _ in throw CashuError.networkError }, + mintExecutor: { [recorder] _, _, _ in + recorder.mintCalls += 1 + return result + }, + sleeper: { [recorder] delay in recorder.delays.append(delay) } + ) + + guard case .success = outcome else { + return XCTFail("expected success, got \(outcome)") + } + XCTAssertEqual(recorder.mintCalls, 1) + } + + // MARK: - Event helpers + + @MainActor + func testTransferMintsHelper() throws { + let schema = Schema([Proof.self, Mint.self, Wallet.self, Event.self]) + let configuration = ModelConfiguration(isStoredInMemoryOnly: true) + let container = try ModelContainer(for: schema, configurations: [configuration]) + let context = container.mainContext + + let mnemonic = Mnemonic() + let wallet = Wallet(mnemonic: mnemonic.phrase.joined(separator: " "), + seed: String(bytes: mnemonic.seed)) + context.insert(wallet) + + let mintA = Mint(url: URL(string: "http://localhost:3338")!, keysets: []) + let mintB = Mint(url: URL(string: "http://localhost:3339")!, keysets: []) + context.insert(mintA) + context.insert(mintB) + + // input proofs of a pending transfer always live at the SOURCE mint + let inputProof = Proof(CashuSwift.Proof(keysetID: "009a1f293253e41e", + amount: 21, + secret: "secret", + C: "02abc"), + unit: .sat, + inputFeePPK: 0, + state: .pending, + mint: mintA, + wallet: wallet) + context.insert(inputProof) + + let quote = makeMintQuote(state: .paid, expiry: 1750000000) + + let event = Event(date: Date(), + unit: .sat, + shortDescription: "Pending Transfer", + visible: true, + kind: .pendingTransfer, + wallet: wallet, + mintQuote: quote, + proofs: [inputProof], + mints: [mintA, mintB]) + context.insert(event) + try context.save() + + // SwiftData does not guarantee relationship array order across saves, + // so from/to must be recovered via the proofs' mint — regardless of + // how the mints array comes back. + let endpoints = try XCTUnwrap(event.transferMints) + XCTAssertEqual(endpoints.from.url, mintA.url) + XCTAssertEqual(endpoints.to.url, mintB.url) + + // a completed transfer holds the issued ecash at the DESTINATION mint + let issuedProof = Proof(CashuSwift.Proof(keysetID: "009a1f293253e41e", + amount: 21, + secret: "secret2", + C: "02def"), + unit: .sat, + inputFeePPK: 0, + state: .valid, + mint: mintB, + wallet: wallet) + context.insert(issuedProof) + + let completedEvent = Event(date: Date(), + unit: .sat, + shortDescription: "Transfer", + visible: true, + kind: .transfer, + wallet: wallet, + mintQuote: quote, + proofs: [issuedProof], + mints: [mintA, mintB]) + context.insert(completedEvent) + try context.save() + + let completedEndpoints = try XCTUnwrap(completedEvent.transferMints) + XCTAssertEqual(completedEndpoints.from.url, mintA.url) + XCTAssertEqual(completedEndpoints.to.url, mintB.url) + + // no index trap and nil result for events with fewer than two mints + let singleMintEvent = Event(date: Date(), + unit: .sat, + shortDescription: "Pending Transfer", + visible: true, + kind: .pendingTransfer, + wallet: wallet, + mints: [mintA]) + context.insert(singleMintEvent) + XCTAssertNil(singleMintEvent.transferMints) + + // expiry falls back to the stored quote when `expiration` was never set + XCTAssertNil(event.expiration) + XCTAssertEqual(event.mintQuoteExpiry, Date(timeIntervalSince1970: 1750000000)) + } +} From 856c1350dc90f9ab532c7696b1a3d4e5442eb78b Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 10 Jun 2026 12:38:56 +0200 Subject: [PATCH 4/6] add dedicated fromMint/toMint references to transfer events SwiftData does not preserve to-many relationship array order (proven by test: [A, B] came back [B, A] after a single save), so the positional from/to convention on Event.mints is unreliable. New transfer events now carry unidirectional optional fromMint/toMint relationships, set on creation and preferred by transferMints. Existing rows stay untouched and resolve via the proofs-orientation/positional fallback. Adding optional relationships is a lightweight migration; verified by an on-disk store test (pre-change schema -> current schema -> reopen) and the existing legacy-store migration tests. Co-Authored-By: Claude Fable 5 --- .../PersistentModelV1/EventFactory.swift | 96 ++++---- .../PersistentModelV1/PersistentModelV1.swift | 15 +- macadamiaTests/TransferFlowTests.swift | 229 ++++++++++++++++++ 3 files changed, 295 insertions(+), 45 deletions(-) diff --git a/macadamia/PersistentModelV1/EventFactory.swift b/macadamia/PersistentModelV1/EventFactory.swift index e6e667b..43b4f02 100644 --- a/macadamia/PersistentModelV1/EventFactory.swift +++ b/macadamia/PersistentModelV1/EventFactory.swift @@ -195,25 +195,28 @@ extension AppSchemaV1.Event { meltQuote: CashuSwift.Bolt11.MeltQuote, mintQuote: CashuSwift.Bolt11.MintQuote, groupingID: UUID?) -> Event { - Event(date: Date(), - unit: unit, - shortDescription: "Pending Transfer", - visible: true, - kind: .pendingTransfer, - wallet: wallet, - mintQuote: mintQuote, - bolt11MeltQuote: meltQuote, - amount: amount, - // the issuance deadline: mints may refuse to issue ecash on an - // expired quote even when it was paid - expiration: mintQuote.expiry.map { Date(timeIntervalSince1970: TimeInterval($0)) }, - longDescription: nil, - proofs: proofs, - memo: nil, - mints: [from, to], - preImage: nil, - redeemed: nil, - groupingID: groupingID) + let event = Event(date: Date(), + unit: unit, + shortDescription: "Pending Transfer", + visible: true, + kind: .pendingTransfer, + wallet: wallet, + mintQuote: mintQuote, + bolt11MeltQuote: meltQuote, + amount: amount, + // the issuance deadline: mints may refuse to issue ecash on an + // expired quote even when it was paid + expiration: mintQuote.expiry.map { Date(timeIntervalSince1970: TimeInterval($0)) }, + longDescription: nil, + proofs: proofs, + memo: nil, + mints: [from, to], + preImage: nil, + redeemed: nil, + groupingID: groupingID) + event.fromMint = from + event.toMint = to + return event } static func transferEvent(wallet: Wallet, @@ -226,36 +229,43 @@ extension AppSchemaV1.Event { mintQuote: CashuSwift.Bolt11.MintQuote, preImage: String?, groupingID: UUID?) -> Event { - Event(date: Date(), - unit: unit, shortDescription: "Transfer", - visible: true, - kind: .transfer, - wallet: wallet, - mintQuote: mintQuote, - bolt11MeltQuote: meltQuote, - amount: amount, - token: nil, - expiration: nil, - longDescription: nil, - proofs: proofs, - memo: nil, - mints: [from, to], - preImage: preImage, - redeemed: nil, - groupingID: groupingID) + let event = Event(date: Date(), + unit: unit, shortDescription: "Transfer", + visible: true, + kind: .transfer, + wallet: wallet, + mintQuote: mintQuote, + bolt11MeltQuote: meltQuote, + amount: amount, + token: nil, + expiration: nil, + longDescription: nil, + proofs: proofs, + memo: nil, + mints: [from, to], + preImage: preImage, + redeemed: nil, + groupingID: groupingID) + event.fromMint = from + event.toMint = to + return event } } extension AppSchemaV1.Event { - /// The endpoints of a transfer event, by convention stored as - /// `mints[0]` = from, `mints[1]` = to. + /// The endpoints of a transfer event. /// - /// SwiftData does NOT reliably preserve the order of a to-many relationship - /// array, so the positional convention alone can come back flipped after a - /// save. The proofs relationship disambiguates: a pending transfer holds the - /// melt inputs (source mint), a completed transfer holds the newly issued - /// ecash (destination mint). + /// New events carry dedicated `fromMint`/`toMint` references. Legacy rows + /// fall back to the old positional convention (`mints[0]` = from, + /// `mints[1]` = to) — but SwiftData does NOT reliably preserve the order of + /// a to-many relationship array, so the proofs relationship disambiguates + /// where possible: a pending transfer holds the melt inputs (source mint), + /// a completed transfer holds the newly issued ecash (destination mint). var transferMints: (from: Mint, to: Mint)? { + if let fromMint, let toMint { + return (fromMint, toMint) + } + guard let mints, mints.count >= 2 else { return nil } var endpoints = (from: mints[0], to: mints[1]) diff --git a/macadamia/PersistentModelV1/PersistentModelV1.swift b/macadamia/PersistentModelV1/PersistentModelV1.swift index 2a11f69..348afb0 100644 --- a/macadamia/PersistentModelV1/PersistentModelV1.swift +++ b/macadamia/PersistentModelV1/PersistentModelV1.swift @@ -443,9 +443,20 @@ enum AppSchemaV1: VersionedSchema { private var blankOutputData: Data? var redeemed: Bool? - + var groupingID: UUID? - + + // Dedicated transfer endpoints, set on creation for new transfer events. + // nil on rows written before these fields existed — those resolve via the + // proofs relationship / positional order in `transferMints`. Deliberately + // unidirectional: `mints` keeps the only explicit inverse pair with + // Mint.events, so relationship inference stays unambiguous. + @Relationship(deleteRule: .nullify) + var fromMint: Mint? + + @Relationship(deleteRule: .nullify) + var toMint: Mint? + enum Kind: Codable { case pendingMint case mint diff --git a/macadamiaTests/TransferFlowTests.swift b/macadamiaTests/TransferFlowTests.swift index dea0b90..2094705 100644 --- a/macadamiaTests/TransferFlowTests.swift +++ b/macadamiaTests/TransferFlowTests.swift @@ -396,5 +396,234 @@ final class TransferFlowTests: XCTestCase { // expiry falls back to the stored quote when `expiration` was never set XCTAssertNil(event.expiration) XCTAssertEqual(event.mintQuoteExpiry, Date(timeIntervalSince1970: 1750000000)) + + // dedicated endpoint references always win over the heuristics + event.fromMint = mintB + event.toMint = mintA + let dedicated = try XCTUnwrap(event.transferMints) + XCTAssertEqual(dedicated.from.url, mintB.url) + XCTAssertEqual(dedicated.to.url, mintA.url) + } + + // MARK: - Migration + + /// Writes a store shaped like the schema BEFORE `Event.fromMint`/`toMint` + /// existed, then opens it with the current schema — the exact upgrade + /// existing users perform. Must lightweight-migrate without a throw, keep + /// legacy rows resolvable, and persist dedicated endpoints on new rows. + @MainActor + func testLightweightMigrationAddsDedicatedFromToMints() throws { + let storeURL = FileManager.default.temporaryDirectory + .appendingPathComponent("macadamia-fromto-migration-\(UUID().uuidString).store") + defer { + for suffix in ["", "-wal", "-shm"] { + try? FileManager.default.removeItem(at: URL(fileURLWithPath: storeURL.path + suffix)) + } + } + + let urlA = URL(string: "http://localhost:3338")! + let urlB = URL(string: "http://localhost:3339")! + + // 1) Write the pre-fromMint/toMint store with a full transfer object graph. + do { + let container = try ModelContainer(for: PreFromToSchema.Wallet.self, + PreFromToSchema.Mint.self, + PreFromToSchema.Proof.self, + PreFromToSchema.Event.self, + configurations: ModelConfiguration(url: storeURL)) + let context = ModelContext(container) + + let wallet = PreFromToSchema.Wallet(mnemonic: "m", seed: "s") + context.insert(wallet) + + let mintA = PreFromToSchema.Mint(url: urlA) + let mintB = PreFromToSchema.Mint(url: urlB) + mintA.wallet = wallet + mintB.wallet = wallet + context.insert(mintA) + context.insert(mintB) + + let inputProof = PreFromToSchema.Proof(C: "02abc", mint: mintA, wallet: wallet) + context.insert(inputProof) + + context.insert(PreFromToSchema.Event(shortDescription: "Pending Transfer", + kind: .pendingTransfer, + wallet: wallet, + proofs: [inputProof], + mints: [mintA, mintB])) + try context.save() + } + + // 2) Open the SAME store with the current schema (the production upgrade). + do { + let container = try ModelContainer(for: Wallet.self, Proof.self, Mint.self, Event.self, + configurations: ModelConfiguration(url: storeURL)) + let context = ModelContext(container) + + let legacyEvent = try XCTUnwrap(try context.fetch(FetchDescriptor()).first) + XCTAssertNil(legacyEvent.fromMint, "rows from before the fields existed must read nil") + XCTAssertNil(legacyEvent.toMint) + + // legacy resolution still orients via the proofs relationship + let endpoints = try XCTUnwrap(legacyEvent.transferMints) + XCTAssertEqual(endpoints.from.url, urlA) + XCTAssertEqual(endpoints.to.url, urlB) + + // 3) A new event created through the factory carries dedicated endpoints. + let wallet = try XCTUnwrap(try context.fetch(FetchDescriptor()).first) + let mints = try context.fetch(FetchDescriptor()) + let from = try XCTUnwrap(mints.first(where: { $0.url == urlA })) + let to = try XCTUnwrap(mints.first(where: { $0.url == urlB })) + + let meltQuote = CashuSwift.Bolt11.MeltQuote(quote: "melt-q", + request: "lnbc210n1...", + amount: 21, + unit: "sat", + feeReserve: 1, + state: .unpaid, + expiry: nil) + let pending = Event.pendingTransferEvent(wallet: wallet, + amount: 21, + from: from, + to: to, + proofs: [], + meltQuote: meltQuote, + mintQuote: makeMintQuote(state: .unpaid), + groupingID: nil) + context.insert(pending) + try context.save() + } + + // 4) Reopen once more: the dedicated references must survive on disk — + // unlike the mints array, whose order SwiftData scrambles. + let container = try ModelContainer(for: Wallet.self, Proof.self, Mint.self, Event.self, + configurations: ModelConfiguration(url: storeURL)) + let context = ModelContext(container) + + let events = try context.fetch(FetchDescriptor()) + let newEvent = try XCTUnwrap(events.first(where: { $0.fromMint != nil })) + XCTAssertEqual(newEvent.fromMint?.url, urlA) + XCTAssertEqual(newEvent.toMint?.url, urlB) + + let resolved = try XCTUnwrap(newEvent.transferMints) + XCTAssertEqual(resolved.from.url, urlA) + XCTAssertEqual(resolved.to.url, urlB) + } +} + +/// Minimal stand-in for the persisted schema BEFORE `Event.fromMint`/`Event.toMint` +/// existed, used only to write a pre-migration store. Mirrors the relationship +/// topology between the four entities (same entity names, same inverses) so the +/// current schema recognises the store and lightweight-migrates it. +private enum PreFromToSchema { + + @Model + final class Wallet { + @Attribute(.unique) var walletID: UUID + var mnemonic: String + var seed: String + var active: Bool + var dateCreated: Date + + @Relationship(inverse: \Mint.wallet) + var mints: [Mint] + + var proofs: [Proof] + + @Relationship(deleteRule: .cascade, inverse: \Event.wallet) + var events: [Event] + + init(mnemonic: String, seed: String) { + self.walletID = UUID() + self.mnemonic = mnemonic + self.seed = seed + self.active = true + self.dateCreated = Date() + self.mints = [] + self.proofs = [] + self.events = [] + } + } + + @Model + final class Mint { + @Attribute(.unique) var mintID: UUID + var url: URL + var keysets: [CashuSwift.Keyset] + var dateAdded: Date + var hidden: Bool = false + var wallet: Wallet? + var proofs: [Proof]? + var events: [Event]? + + init(url: URL) { + self.mintID = UUID() + self.url = url + self.keysets = [] + self.dateAdded = Date() + self.proofs = [] + } + } + + @Model + final class Proof { + @Attribute(.unique) var proofID: UUID + var keysetID: String + var C: String + var secret: String + var amount: Int + var state: AppSchemaV1.Proof.State + var inputFeePPK: Int + var dateCreated: Date + + @Relationship(inverse: \Mint.proofs) + var mint: Mint? + + @Relationship(inverse: \Wallet.proofs) + var wallet: Wallet? + + init(C: String, mint: Mint, wallet: Wallet) { + self.proofID = UUID() + self.keysetID = "009a1f293253e41e" + self.C = C + self.secret = "secret-\(C)" + self.amount = 21 + self.state = .pending + self.inputFeePPK = 0 + self.dateCreated = Date() + self.mint = mint + self.wallet = wallet + } + } + + @Model + final class Event { + @Attribute(.unique) var eventID: UUID + var date: Date + var shortDescription: String + var visible: Bool + var kind: AppSchemaV1.Event.Kind + var amount: Int? + var wallet: Wallet? + var proofs: [Proof]? + + @Relationship(deleteRule: .noAction, inverse: \Mint.events) + var mints: [Mint]? + + init(shortDescription: String, + kind: AppSchemaV1.Event.Kind, + wallet: Wallet, + proofs: [Proof], + mints: [Mint]) { + self.eventID = UUID() + self.date = Date() + self.shortDescription = shortDescription + self.visible = true + self.kind = kind + self.amount = 21 + self.wallet = wallet + self.proofs = proofs + self.mints = mints + } } } From 0a73087751598aab7de065dcfaf5868c70efe2d8 Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 10 Jun 2026 17:39:54 +0200 Subject: [PATCH 5/6] pin CashuSwift and Bolt11 to version tags instead of branches CashuSwift: payment-methods branch -> up to next minor from 0.4.1 Bolt11: main branch -> up to next minor from 0.1.3 The branch pins forced a stale Bolt11 revision over cashu-swift's exact 0.1.3 requirement, breaking the build against Bech32Swift 1.0.1. Co-Authored-By: Claude Fable 5 --- macadamia.xcodeproj/project.pbxproj | 8 +++---- .../xcshareddata/swiftpm/Package.resolved | 21 +++++++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/macadamia.xcodeproj/project.pbxproj b/macadamia.xcodeproj/project.pbxproj index 9d640cd..a3fa654 100644 --- a/macadamia.xcodeproj/project.pbxproj +++ b/macadamia.xcodeproj/project.pbxproj @@ -830,8 +830,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/zeugmaster/Bolt11.git"; requirement = { - branch = main; - kind = branch; + kind = upToNextMinorVersion; + minimumVersion = 0.1.3; }; }; 370A84052CC18AEE0039A413 /* XCRemoteSwiftPackageReference "SwiftUI-Flow" */ = { @@ -854,8 +854,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/zeugmaster/cashu-swift.git"; requirement = { - branch = "payment-methods"; - kind = branch; + kind = upToNextMinorVersion; + minimumVersion = 0.4.1; }; }; 377D26242E1FDE7800CFB880 /* XCRemoteSwiftPackageReference "URUI" */ = { diff --git a/macadamia.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macadamia.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0962441..06efeda 100644 --- a/macadamia.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/macadamia.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zeugmaster/Bech32Swift.git", "state" : { - "revision" : "72ff0e36f88290396190bd3067800e44e851a2a4", - "version" : "1.0.0" + "revision" : "09ec6c35a4fbeaac80e709e1808e8af7b8ef038b", + "version" : "1.0.1" } }, { @@ -87,8 +87,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zeugmaster/Bolt11.git", "state" : { - "branch" : "main", - "revision" : "fa8ffb4d3c0d2cc6d7cbdcb84eeb8f5b44e68548" + "revision" : "f93a4edfde69e70d5d58b0199bb4a373aff19b57", + "version" : "0.1.3" + } + }, + { + "identity" : "bolt12", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zeugmaster/Bolt12.git", + "state" : { + "revision" : "0542a679d189b6fb4bb9488e145fe383c1445ddc", + "version" : "0.1.0" } }, { @@ -96,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zeugmaster/cashu-swift.git", "state" : { - "branch" : "payment-methods", - "revision" : "6288917cf1aaeafe451c2c57d77f88f4f02e0044" + "revision" : "873dcf929b2768a0fff4948cf3e767d9e3685a78", + "version" : "0.4.1" } }, { From d30768138285d453cc5a786cfdf9a89f33b146fe Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 10 Jun 2026 18:30:37 +0200 Subject: [PATCH 6/6] complete localizations for all 12 languages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add translations (ar, bn, de, es, fr, hi, ja, ko, pt-BR, ru, tr) for the new transfer-flow strings and for previously untranslated keys — 56 keys, 617 translations. Catalog is now fully translated. Co-Authored-By: Claude Fable 5 --- macadamia/AppAssets/Localizable.xcstrings | 4116 ++++++++++++++++++++- 1 file changed, 3991 insertions(+), 125 deletions(-) diff --git a/macadamia/AppAssets/Localizable.xcstrings b/macadamia/AppAssets/Localizable.xcstrings index 99fac05..3cf5d4f 100644 --- a/macadamia/AppAssets/Localizable.xcstrings +++ b/macadamia/AppAssets/Localizable.xcstrings @@ -22,500 +22,3227 @@ }, "shouldTranslate" : false }, + "%@ Added" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ تمت الإضافة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ যোগ করা হয়েছে" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Hinzugefügt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Añadido" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Ajouté" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ जोड़ा गया" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 追加済み" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 추가됨" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Adicionado" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Добавлено" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Eklendi" + } + } + } + }, + "%@ Redeem Later" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ استرداد لاحقًا" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ পরে রিডিম করুন" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Später einlösen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Canjear después" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Encaisser plus tard" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ बाद में रिडीम करें" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 後で引き換え" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 나중에 사용" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Resgatar depois" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Получить позже" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Sonra kullan" + } + } + } + }, "-" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "-" + "value" : "-" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "-" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "-" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "-" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "-" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "-" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "-" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "-" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "-" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "-" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "-" + } + } + } + }, + "..." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "…" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "…" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "…" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "…" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "…" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "…" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "…" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "…" + } + } + } + }, + "%@ " : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + } + } + }, + "%@ %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %2$@" + } + } + }, + "shouldTranslate" : false + }, + "%lld" : { + "shouldTranslate" : false + }, + "%lld mint%@" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld mint%2$@" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld মিন্ট%2$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld Mint%2$@" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld mint%2$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld mint%2$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld mint%2$@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld मिंट%2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lldミント%2$@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld 민트%2$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld mint%2$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld минт%2$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld mint%2$@" + } + } + } + }, + "%lld Mint%@ selected" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld Mint%2$@ محددة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld Mint%2$@ নির্বাচিত" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld Mint%2$@ ausgewählt" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld Mint%2$@ selected" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld Mint%2$@ seleccionados" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld Mint%2$@ sélectionnés" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld Mint%2$@ चयनित" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lldミント%2$@選択済み" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld 민트%2$@ 선택됨" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld Mint%2$@ selecionados" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld Mint%2$@ выбрано" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld Mint%2$@ seçildi" + } + } + } + }, + "%lld objects" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld عنصر" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld অবজেক্ট" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Objekte" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld objetos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld objets" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld ऑब्जेक्ट" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lldオブジェクト" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 객체" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld objetos" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld объектов" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld nesne" + } + } + } + }, + "%lld ppk" : { + "shouldTranslate" : false + }, + "%lld sats" : { + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld sats" + } + } + }, + "shouldTranslate" : false + }, + "%lld." : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld." + } + } + }, + "shouldTranslate" : false + }, + "%lld/%lld" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld/" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld/%2$lld" + } + } + }, + "shouldTranslate" : false + }, + "%lld%%" : { + "shouldTranslate" : false + }, + "Amount:" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المبلغ:" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "পরিমাণ:" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Betrag:" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cantidad:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Montant :" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "राशि:" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "金額:" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "금액:" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valor:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сумма:" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tutar:" + } + } + } + }, + "Awaiting payment" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "في انتظار الدفع" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "পেমেন্টের অপেক্ষায়" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Warte auf Zahlung" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esperando el pago" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En attente de paiement" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "भुगतान की प्रतीक्षा में" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "支払い待ち" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "결제 대기 중" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aguardando pagamento" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ожидание платежа" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ödeme bekleniyor" + } + } + } + }, + "Cannot Remove" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذرت الإزالة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "সরানো যাচ্ছে না" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entfernen nicht möglich" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se puede eliminar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suppression impossible" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हटाया नहीं जा सकता" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "削除できません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제거할 수 없음" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não é possível remover" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Невозможно удалить" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kaldırılamıyor" + } + } + } + }, + "Check Again" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تحقق مرة أخرى" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "আবার চেক করুন" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erneut prüfen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comprobar de nuevo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vérifier à nouveau" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "फिर से जांचें" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再確認" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다시 확인" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verificar novamente" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проверить снова" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tekrar kontrol et" + } + } + } + }, + "Could not determine the transfer's state. Nothing was changed — try again later." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذر تحديد حالة التحويل. لم يتغير شيء — حاول مرة أخرى لاحقًا." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ট্রান্সফারের অবস্থা নির্ধারণ করা যায়নি। কিছুই পরিবর্তন হয়নি — পরে আবার চেষ্টা করুন।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Der Status des Transfers konnte nicht ermittelt werden. Es wurde nichts verändert — versuche es später erneut." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo determinar el estado de la transferencia. No se cambió nada; inténtalo de nuevo más tarde." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible de déterminer l'état du transfert. Rien n'a été modifié — réessayez plus tard." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ट्रांसफर की स्थिति निर्धारित नहीं की जा सकी। कुछ भी बदला नहीं गया — बाद में पुनः प्रयास करें।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "転送の状態を確認できませんでした。何も変更されていません。後でもう一度お試しください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "전송 상태를 확인할 수 없습니다. 아무것도 변경되지 않았습니다. 나중에 다시 시도하세요." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível determinar o estado da transferência. Nada foi alterado — tente novamente mais tarde." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось определить состояние перевода. Ничего не изменено — попробуйте позже." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transferin durumu belirlenemedi. Hiçbir şey değiştirilmedi — daha sonra tekrar deneyin." + } + } + } + }, + "Ecash issued" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إصدار Ecash" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ecash ইস্যু" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ecash ausgestellt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ecash emitido" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ecash émis" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ecash जारी" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ecash発行" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ecash 발행" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ecash emitido" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ecash выпущен" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ecash oluşturuldu" + } + } + } + }, + "expired — completing may fail" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "منتهي الصلاحية — قد يفشل الإتمام" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "মেয়াদোত্তীর্ণ — সম্পন্ন করা ব্যর্থ হতে পারে" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "abgelaufen — Abschließen könnte fehlschlagen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "expirada — completar puede fallar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "expiré — la finalisation peut échouer" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "समाप्त — पूरा करना विफल हो सकता है" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "期限切れ — 完了できない可能性があります" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "만료됨 — 완료가 실패할 수 있음" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "expirada — concluir pode falhar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "истекло — завершение может не сработать" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "süresi doldu — tamamlama başarısız olabilir" + } + } + } + }, + "In flight" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قيد التنفيذ" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "চলমান" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unterwegs" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "En curso" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En cours" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रगति में" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送金中" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "전송 중" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Em andamento" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "В пути" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yolda" + } + } + } + }, + "Issued" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تم الإصدار" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ইস্যু করা হয়েছে" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgestellt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Emitido" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Émis" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "जारी किया गया" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "発行済み" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "발행됨" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Emitido" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выпущено" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oluşturuldu" + } + } + } + }, + "Paid" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مدفوع" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "পরিশোধিত" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bezahlt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pagado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Payé" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "भुगतान किया गया" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "支払済み" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "지불됨" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pago" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оплачено" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ödendi" + } + } + } + }, + "Payment received" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تم استلام الدفعة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "পেমেন্ট গৃহীত" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zahlung erhalten" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pago recibido" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paiement reçu" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "भुगतान प्राप्त हुआ" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "支払い受領" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "결제 수신됨" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pagamento recebido" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Платёж получен" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ödeme alındı" + } + } + } + }, + "Pending" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "معلق" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "অমীমাংসিত" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausstehend" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pendiente" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En attente" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "लंबित" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "保留中" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "대기 중" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pendente" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "В ожидании" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beklemede" + } + } + } + }, + "Processing" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قيد المعالجة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "প্রক্রিয়াধীন" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wird verarbeitet" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Procesando" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Traitement en cours" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रोसेस हो रहा है" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "処理中" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "처리 중" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Processando" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обработка" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İşleniyor" + } + } + } + }, + "Quote expires" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تنتهي صلاحية عرض السعر" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "কোটেশনের মেয়াদ শেষ হবে" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Angebot läuft ab" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La cotización expira" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le devis expire" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कोटेशन समाप्त होगा" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "見積もりの期限" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "견적 만료" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "A cotação expira" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предложение истекает" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teklif süresi doluyor" + } + } + } + }, + "Some transfers could not finish yet. You can complete them from the transaction list." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذر إتمام بعض التحويلات بعد. يمكنك إتمامها من قائمة المعاملات." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "কিছু ট্রান্সফার এখনও সম্পন্ন হতে পারেনি। আপনি লেনদেন তালিকা থেকে সেগুলি সম্পন্ন করতে পারেন।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einige Transfers konnten noch nicht abgeschlossen werden. Du kannst sie über die Transaktionsliste abschließen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algunas transferencias aún no pudieron finalizar. Puedes completarlas desde la lista de transacciones." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certains transferts n'ont pas encore pu aboutir. Vous pouvez les terminer depuis la liste des transactions." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कुछ ट्रांसफर अभी पूरे नहीं हो सके। आप उन्हें लेन-देन सूची से पूरा कर सकते हैं।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "一部の転送はまだ完了していません。取引リストから完了できます。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "일부 전송이 아직 완료되지 않았습니다. 거래 목록에서 완료할 수 있습니다." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algumas transferências ainda não puderam ser concluídas. Você pode completá-las na lista de transações." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Некоторые переводы пока не удалось завершить. Вы можете завершить их из списка транзакций." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazı transferler henüz tamamlanamadı. Bunları işlem listesinden tamamlayabilirsiniz." + } + } + } + }, + "Status" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الحالة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "স্ট্যাটাস" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Status" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Statut" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "स्थिति" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ステータス" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "상태" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Status" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Статус" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Durum" + } + } + } + }, + "Still Pending" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يزال معلقًا" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "এখনও অমীমাংসিত" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noch ausstehend" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aún pendiente" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toujours en attente" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अभी भी लंबित" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "まだ保留中" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아직 대기 중" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ainda pendente" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Всё ещё в ожидании" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hâlâ beklemede" + } + } + } + }, + "The destination mint did not respond. You can complete this transfer later from the transaction list." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لم يستجب Mint الوجهة. يمكنك إتمام هذا التحويل لاحقًا من قائمة المعاملات." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "গন্তব্য Mint সাড়া দেয়নি। আপনি এই ট্রান্সফারটি পরে লেনদেন তালিকা থেকে সম্পন্ন করতে পারেন।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Ziel-Mint hat nicht geantwortet. Du kannst diesen Transfer später über die Transaktionsliste abschließen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El mint de destino no respondió. Puedes completar esta transferencia más tarde desde la lista de transacciones." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le mint de destination n'a pas répondu. Vous pouvez terminer ce transfert plus tard depuis la liste des transactions." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "गंतव्य Mint ने जवाब नहीं दिया। आप इस ट्रांसफर को बाद में लेन-देन सूची से पूरा कर सकते हैं।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "転送先のミントが応答しませんでした。この転送は後で取引リストから完了できます。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "대상 민트가 응답하지 않았습니다. 이 전송은 나중에 거래 목록에서 완료할 수 있습니다." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "O mint de destino não respondeu. Você pode concluir esta transferência mais tarde na lista de transações." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Минт назначения не ответил. Вы можете завершить этот перевод позже из списка транзакций." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef Mint yanıt vermedi. Bu transferi daha sonra işlem listesinden tamamlayabilirsiniz." + } + } + } + }, + "The destination mint has not registered the payment yet. You can complete this transfer later from the transaction list." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لم يسجّل Mint الوجهة الدفعة بعد. يمكنك إتمام هذا التحويل لاحقًا من قائمة المعاملات." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "গন্তব্য Mint এখনও পেমেন্ট নিবন্ধন করেনি। আপনি এই ট্রান্সফারটি পরে লেনদেন তালিকা থেকে সম্পন্ন করতে পারেন।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Ziel-Mint hat die Zahlung noch nicht erfasst. Du kannst diesen Transfer später über die Transaktionsliste abschließen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El mint de destino aún no ha registrado el pago. Puedes completar esta transferencia más tarde desde la lista de transacciones." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le mint de destination n'a pas encore enregistré le paiement. Vous pouvez terminer ce transfert plus tard depuis la liste des transactions." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "गंतव्य Mint ने अभी तक भुगतान दर्ज नहीं किया है। आप इस ट्रांसफर को बाद में लेन-देन सूची से पूरा कर सकते हैं।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "転送先のミントはまだ支払いを確認していません。この転送は後で取引リストから完了できます。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "대상 민트가 아직 결제를 확인하지 못했습니다. 이 전송은 나중에 거래 목록에서 완료할 수 있습니다." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "O mint de destino ainda não registrou o pagamento. Você pode concluir esta transferência mais tarde na lista de transações." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Минт назначения ещё не зарегистрировал платёж. Вы можете завершить этот перевод позже из списка транзакций." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef Mint ödemeyi henüz kaydetmedi. Bu transferi daha sonra işlem listesinden tamamlayabilirsiniz." + } + } + } + }, + "The destination mint reports that the ecash for this transfer was already issued. If your balance does not reflect it, you can recover it via Settings → Restore." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يفيد Mint الوجهة بأن Ecash لهذا التحويل قد صدر بالفعل. إذا لم يظهر ذلك في رصيدك، يمكنك استعادته عبر الإعدادات ← استعادة." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "গন্তব্য Mint জানাচ্ছে যে এই ট্রান্সফারের Ecash ইতিমধ্যে ইস্যু করা হয়েছে। আপনার ব্যালেন্সে তা প্রতিফলিত না হলে, সেটিংস → পুনরুদ্ধার থেকে তা ফিরে পেতে পারেন।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Ziel-Mint meldet, dass das Ecash für diesen Transfer bereits ausgestellt wurde. Falls Dein Guthaben das nicht widerspiegelt, kannst Du es über Einstellungen → Wiederherstellen zurückholen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El mint de destino informa que el Ecash de esta transferencia ya fue emitido. Si tu saldo no lo refleja, puedes recuperarlo en Ajustes → Restaurar." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le mint de destination indique que l'Ecash de ce transfert a déjà été émis. Si votre solde ne le reflète pas, vous pouvez le récupérer via Réglages → Restaurer." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "गंतव्य Mint के अनुसार इस ट्रांसफर का Ecash पहले ही जारी किया जा चुका है। यदि आपका बैलेंस इसे नहीं दिखाता है, तो आप इसे सेटिंग्स → पुनर्स्थापना से वापस पा सकते हैं।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "転送先のミントによると、この転送のEcashはすでに発行されています。残高に反映されていない場合は、設定 → 復元から回復できます。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "대상 민트가 이 전송의 Ecash가 이미 발행되었다고 보고합니다. 잔액에 반영되지 않았다면 설정 → 복원에서 복구할 수 있습니다." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "O mint de destino informa que o Ecash desta transferência já foi emitido. Se o seu saldo não refletir isso, você pode recuperá-lo em Ajustes → Restaurar." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Минт назначения сообщает, что Ecash для этого перевода уже выпущен. Если баланс этого не отражает, вы можете восстановить его через Настройки → Восстановить." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef Mint, bu transferin Ecash'inin zaten oluşturulduğunu bildiriyor. Bakiyenize yansımadıysa Ayarlar → Geri Yükle üzerinden kurtarabilirsiniz." + } + } + } + }, + "The destination mint's quote for this transfer has expired. If the payment already went through, the funds can only be recovered by the mint operator." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "انتهت صلاحية عرض السعر من Mint الوجهة لهذا التحويل. إذا تم الدفع بالفعل، فلا يمكن استرداد الأموال إلا عن طريق مشغّل الـ Mint." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "এই ট্রান্সফারের জন্য গন্তব্য Mint-এর কোটেশনের মেয়াদ শেষ হয়ে গেছে। পেমেন্ট ইতিমধ্যে হয়ে থাকলে, কেবল Mint অপারেটরই অর্থ পুনরুদ্ধার করতে পারবেন।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Angebot der Ziel-Mint für diesen Transfer ist abgelaufen. Falls die Zahlung bereits durchgegangen ist, kann nur der Betreiber der Mint die Mittel wiederherstellen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La cotización del mint de destino para esta transferencia ha expirado. Si el pago ya se realizó, solo el operador del mint puede recuperar los fondos." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le devis du mint de destination pour ce transfert a expiré. Si le paiement a déjà abouti, seul l'opérateur du mint peut récupérer les fonds." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस ट्रांसफर के लिए गंतव्य Mint का कोटेशन समाप्त हो गया है। यदि भुगतान पहले ही हो चुका है, तो धनराशि केवल Mint ऑपरेटर ही वापस दिला सकता है।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この転送に対する転送先ミントの見積もりが期限切れになりました。支払いがすでに完了している場合、資金はミントの運営者のみが回復できます。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 전송에 대한 대상 민트의 견적이 만료되었습니다. 결제가 이미 완료된 경우 자금은 민트 운영자만 복구할 수 있습니다." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "A cotação do mint de destino para esta transferência expirou. Se o pagamento já foi realizado, apenas o operador do mint pode recuperar os fundos." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предложение минта назначения для этого перевода истекло. Если платёж уже прошёл, вернуть средства может только оператор минта." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef Mint'in bu transfer için verdiği teklifin süresi doldu. Ödeme zaten gerçekleştiyse fonlar yalnızca Mint operatörü tarafından kurtarılabilir." + } + } + } + }, + "The Lightning payment is still in flight. Check again in a moment." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "دفعة Lightning لا تزال قيد التنفيذ. تحقق مرة أخرى بعد قليل." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lightning পেমেন্ট এখনও চলমান। কিছুক্ষণ পরে আবার চেক করুন।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Lightning-Zahlung ist noch unterwegs. Prüfe es gleich noch einmal." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El pago Lightning todavía está en curso. Compruébalo de nuevo en un momento." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le paiement Lightning est toujours en cours. Vérifiez à nouveau dans un instant." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lightning भुगतान अभी भी प्रगति में है। थोड़ी देर में फिर से जांचें।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lightning送金はまだ処理中です。しばらくしてから再確認してください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lightning 결제가 아직 진행 중입니다. 잠시 후 다시 확인하세요." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "O pagamento Lightning ainda está em andamento. Verifique novamente em instantes." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lightning-платёж ещё в пути. Проверьте снова чуть позже." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lightning ödemesi hâlâ yolda. Birazdan tekrar kontrol edin." + } + } + } + }, + "The mint returned an unknown quote state." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أعاد الـ Mint حالة عرض سعر غير معروفة." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mint একটি অজানা কোটেশন স্ট্যাটাস ফেরত দিয়েছে।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Mint hat einen unbekannten Angebotsstatus zurückgegeben." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El mint devolvió un estado de cotización desconocido." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le mint a renvoyé un état de devis inconnu." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mint ने अज्ञात कोटेशन स्थिति लौटाई।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ミントが不明な見積もり状態を返しました。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "민트가 알 수 없는 견적 상태를 반환했습니다." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "O mint retornou um estado de cotação desconhecido." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Минт вернул неизвестный статус предложения." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mint bilinmeyen bir teklif durumu döndürdü." + } + } + } + }, + "The mints for this transfer could not be found." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذر العثور على Mints لهذا التحويل." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "এই ট্রান্সফারের Mints খুঁজে পাওয়া যায়নি।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Mints für diesen Transfer konnten nicht gefunden werden." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudieron encontrar los mints de esta transferencia." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les mints de ce transfert sont introuvables." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस ट्रांसफर के Mints नहीं मिल सके।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この転送のミントが見つかりませんでした。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 전송의 민트를 찾을 수 없습니다." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível encontrar os mints desta transferência." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Минты для этого перевода не найдены." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu transferin Mint'leri bulunamadı." + } + } + } + }, + "The payment could not be verified as unpaid. Removing it now could lose funds — try completing the transfer instead." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذر التحقق من أن الدفعة غير مدفوعة. قد تؤدي إزالتها الآن إلى فقدان الأموال — حاول إتمام التحويل بدلاً من ذلك." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "পেমেন্টটি অপরিশোধিত কিনা যাচাই করা যায়নি। এখন সরালে অর্থ হারাতে পারেন — পরিবর্তে ট্রান্সফারটি সম্পন্ন করার চেষ্টা করুন।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es konnte nicht bestätigt werden, dass die Zahlung unbezahlt ist. Sie jetzt zu entfernen könnte Mittel verlieren — versuche stattdessen, den Transfer abzuschließen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo verificar que el pago esté sin pagar. Eliminarlo ahora podría perder fondos; intenta completar la transferencia en su lugar." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible de vérifier que le paiement est non payé. Le supprimer maintenant pourrait entraîner une perte de fonds — essayez plutôt de terminer le transfert." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "यह सत्यापित नहीं हो सका कि भुगतान नहीं हुआ है। इसे अभी हटाने से धनराशि खो सकती है — इसके बजाय ट्रांसफर पूरा करने का प्रयास करें।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "支払いが未払いであることを確認できませんでした。今削除すると資金を失う可能性があります。代わりに転送の完了を試してください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "결제가 미지불 상태인지 확인할 수 없습니다. 지금 제거하면 자금을 잃을 수 있습니다. 대신 전송 완료를 시도하세요." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível verificar que o pagamento está não pago. Removê-lo agora pode perder fundos — tente concluir a transferência." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось подтвердить, что платёж не оплачен. Удаление сейчас может привести к потере средств — попробуйте вместо этого завершить перевод." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ödemenin ödenmemiş olduğu doğrulanamadı. Şimdi kaldırmak fon kaybına yol açabilir — bunun yerine transferi tamamlamayı deneyin." + } + } + } + }, + "The payment did not go through: %@" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لم تتم الدفعة: %@" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "-" + "value" : "পেমেন্ট সম্পন্ন হয়নি: %@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "-" + "value" : "Die Zahlung ist nicht durchgegangen: %@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "-" + "value" : "El pago no se realizó: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "-" + "value" : "Le paiement n'a pas abouti : %@" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "-" + "value" : "भुगतान नहीं हो सका: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "-" + "value" : "支払いは完了しませんでした:%@" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "-" + "value" : "결제가 완료되지 않았습니다: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "-" + "value" : "O pagamento não foi realizado: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "-" + "value" : "Платёж не прошёл: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "-" + "value" : "Ödeme gerçekleşmedi: %@" } } } }, - "..." : { + "The payment outcome could not be determined. Check this transfer again later from the transaction list." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "…" + "value" : "تعذر تحديد نتيجة الدفعة. تحقق من هذا التحويل لاحقًا من قائمة المعاملات." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "…" + "value" : "পেমেন্টের ফলাফল নির্ধারণ করা যায়নি। এই ট্রান্সফারটি পরে লেনদেন তালিকা থেকে আবার চেক করুন।" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "…" + "value" : "Das Ergebnis der Zahlung konnte nicht ermittelt werden. Prüfe diesen Transfer später über die Transaktionsliste erneut." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "…" + "value" : "No se pudo determinar el resultado del pago. Comprueba esta transferencia más tarde desde la lista de transacciones." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "…" + "value" : "Le résultat du paiement n'a pas pu être déterminé. Vérifiez ce transfert plus tard depuis la liste des transactions." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "…" + "value" : "भुगतान का परिणाम निर्धारित नहीं किया जा सका। इस ट्रांसफर को बाद में लेन-देन सूची से फिर से जांचें।" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "…" + "value" : "支払い結果を確認できませんでした。この転送は後で取引リストから再確認してください。" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "…" + "value" : "결제 결과를 확인할 수 없습니다. 이 전송을 나중에 거래 목록에서 다시 확인하세요." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "…" + "value" : "Não foi possível determinar o resultado do pagamento. Verifique esta transferência mais tarde na lista de transações." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "…" + "value" : "Не удалось определить результат платежа. Проверьте этот перевод позже в списке транзакций." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "…" + "value" : "Ödemenin sonucu belirlenemedi. Bu transferi daha sonra işlem listesinden tekrar kontrol edin." } } } }, - "%@ " : { + "The payment outcome could not be determined. Nothing was changed — check again later." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "%@" + "value" : "تعذر تحديد نتيجة الدفعة. لم يتغير شيء — تحقق مرة أخرى لاحقًا." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "%@" + "value" : "পেমেন্টের ফলাফল নির্ধারণ করা যায়নি। কিছুই পরিবর্তন হয়নি — পরে আবার চেক করুন।" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%@" + "value" : "Das Ergebnis der Zahlung konnte nicht ermittelt werden. Es wurde nichts verändert — prüfe es später erneut." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "%@" + "value" : "No se pudo determinar el resultado del pago. No se cambió nada; comprueba de nuevo más tarde." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%@" + "value" : "Le résultat du paiement n'a pas pu être déterminé. Rien n'a été modifié — vérifiez à nouveau plus tard." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "%@" + "value" : "भुगतान का परिणाम निर्धारित नहीं किया जा सका। कुछ भी बदला नहीं गया — बाद में फिर से जांचें।" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%@" + "value" : "支払い結果を確認できませんでした。何も変更されていません。後で再確認してください。" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "%@" + "value" : "결제 결과를 확인할 수 없습니다. 아무것도 변경되지 않았습니다. 나중에 다시 확인하세요." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "%@" + "value" : "Não foi possível determinar o resultado do pagamento. Nada foi alterado — verifique novamente mais tarde." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "%@" + "value" : "Не удалось определить результат платежа. Ничего не изменено — проверьте снова позже." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%@" + "value" : "Ödemenin sonucu belirlenemedi. Hiçbir şey değiştirilmedi — daha sonra tekrar kontrol edin." } } } }, - "%@ %@" : { + "The payment went through but the destination mint's quote expired. Contact the mint operator to recover the funds." : { "localizations" : { - "en" : { + "ar" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ %2$@" + "value" : "تمت الدفعة لكن انتهت صلاحية عرض السعر من Mint الوجهة. تواصل مع مشغّل الـ Mint لاسترداد الأموال." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "পেমেন্ট সম্পন্ন হয়েছে কিন্তু গন্তব্য Mint-এর কোটেশনের মেয়াদ শেষ হয়ে গেছে। অর্থ পুনরুদ্ধার করতে Mint অপারেটরের সাথে যোগাযোগ করুন।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Zahlung ist durchgegangen, aber das Angebot der Ziel-Mint ist abgelaufen. Wende Dich an den Betreiber der Mint, um die Mittel wiederherzustellen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El pago se realizó, pero la cotización del mint de destino expiró. Contacta al operador del mint para recuperar los fondos." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le paiement a abouti mais le devis du mint de destination a expiré. Contactez l'opérateur du mint pour récupérer les fonds." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "भुगतान हो गया लेकिन गंतव्य Mint का कोटेशन समाप्त हो गया। धनराशि वापस पाने के लिए Mint ऑपरेटर से संपर्क करें।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "支払いは完了しましたが、転送先ミントの見積もりが期限切れになりました。資金を回復するにはミントの運営者に連絡してください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "결제는 완료되었지만 대상 민트의 견적이 만료되었습니다. 자금을 복구하려면 민트 운영자에게 문의하세요." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "O pagamento foi realizado, mas a cotação do mint de destino expirou. Entre em contato com o operador do mint para recuperar os fundos." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Платёж прошёл, но предложение минта назначения истекло. Свяжитесь с оператором минта, чтобы вернуть средства." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ödeme gerçekleşti ancak hedef Mint'in teklifinin süresi doldu. Fonları kurtarmak için Mint operatörüyle iletişime geçin." } } - }, - "shouldTranslate" : false - }, - "%lld" : { - "shouldTranslate" : false + } }, - "%lld mint%@" : { + "The state of the reserved ecash could not be checked with the mint. Try again later." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld mint%2$@" + "value" : "تعذر التحقق من حالة Ecash المحجوز لدى الـ Mint. حاول مرة أخرى لاحقًا." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld মিন্ট%2$@" + "value" : "সংরক্ষিত Ecash-এর অবস্থা Mint-এর সাথে যাচাই করা যায়নি। পরে আবার চেষ্টা করুন।" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld Mint%2$@" + "value" : "Der Status des reservierten Ecash konnte nicht bei der Mint geprüft werden. Versuche es später erneut." } }, - "en" : { + "es" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld mint%2$@" + "value" : "No se pudo comprobar con el mint el estado del Ecash reservado. Inténtalo de nuevo más tarde." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'état de l'Ecash réservé n'a pas pu être vérifié auprès du mint. Réessayez plus tard." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आरक्षित Ecash की स्थिति Mint से जांची नहीं जा सकी। बाद में पुनः प्रयास करें।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "予約されたEcashの状態をミントで確認できませんでした。後でもう一度お試しください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "예약된 Ecash의 상태를 민트에서 확인할 수 없습니다. 나중에 다시 시도하세요." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível verificar com o mint o estado do Ecash reservado. Tente novamente mais tarde." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось проверить у минта состояние зарезервированного Ecash. Попробуйте позже." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayrılan Ecash'in durumu Mint ile doğrulanamadı. Daha sonra tekrar deneyin." + } + } + } + }, + "This payment did not go through: %@ The Ecash reserved for this operation can be made available again by removing the pending transfer event." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لم تتم هذه الدفعة: %@ يمكن إتاحة Ecash المحجوز لهذه العملية مرة أخرى عن طريق إزالة حدث التحويل المعلق." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "এই পেমেন্টটি সম্পন্ন হয়নি: %@ এই অপারেশনের জন্য সংরক্ষিত Ecash অমীমাংসিত ট্রান্সফার ইভেন্টটি সরিয়ে আবার উপলব্ধ করা যেতে পারে।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Zahlung ist nicht durchgegangen: %@ Das für diesen Vorgang reservierte Ecash kann wieder verfügbar gemacht werden, indem Du das ausstehende Transferereignis entfernst." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld mint%2$@" + "value" : "Este pago no se realizó: %@ El Ecash reservado para esta operación puede volver a estar disponible eliminando el evento de transferencia pendiente." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld mint%2$@" + "value" : "Ce paiement n'a pas abouti : %@ L'Ecash réservé pour cette opération peut être de nouveau disponible en supprimant l'événement de transfert en attente." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld मिंट%2$@" + "value" : "यह भुगतान नहीं हो सका: %@ इस ऑपरेशन के लिए आरक्षित Ecash को लंबित ट्रांसफर इवेंट हटाकर फिर से उपलब्ध किया जा सकता है।" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lldミント%2$@" + "value" : "この支払いは完了しませんでした:%@ この操作のために予約されたEcashは、保留中の転送イベントを削除すると再び利用可能になります。" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld 민트%2$@" + "value" : "이 결제는 완료되지 않았습니다: %@ 이 작업을 위해 예약된 Ecash는 대기 중인 전송 이벤트를 제거하면 다시 사용할 수 있습니다." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld mint%2$@" + "value" : "Este pagamento não foi realizado: %@ O Ecash reservado para esta operação pode ficar disponível novamente removendo o evento de transferência pendente." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld минт%2$@" + "value" : "Этот платёж не прошёл: %@ Ecash, зарезервированный для этой операции, можно снова сделать доступным, удалив ожидающее событие перевода." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld mint%2$@" + "value" : "Bu ödeme gerçekleşmedi: %@ Bu işlem için ayrılan Ecash, bekleyen transfer olayını kaldırarak yeniden kullanılabilir hale getirilebilir." } } } }, - "%lld Mint%@ selected" : { + "Transfer Pending" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld Mint%2$@ محددة" + "value" : "التحويل معلق" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld Mint%2$@ নির্বাচিত" + "value" : "ট্রান্সফার অমীমাংসিত" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld Mint%2$@ ausgewählt" + "value" : "Transfer ausstehend" } }, - "en" : { + "es" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld Mint%2$@ selected" + "value" : "Transferencia pendiente" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transfert en attente" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ट्रांसफर लंबित" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "転送は保留中" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "전송 대기 중" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transferência pendente" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перевод в ожидании" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transfer beklemede" + } + } + } + }, + "Transfers Pending" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تحويلات معلقة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ট্রান্সফারগুলি অমীমাংসিত" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transfers ausstehend" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld Mint%2$@ seleccionados" + "value" : "Transferencias pendientes" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld Mint%2$@ sélectionnés" + "value" : "Transferts en attente" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld Mint%2$@ चयनित" + "value" : "ट्रांसफर लंबित हैं" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lldミント%2$@選択済み" + "value" : "保留中の転送あり" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld 민트%2$@ 선택됨" + "value" : "대기 중인 전송" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld Mint%2$@ selecionados" + "value" : "Transferências pendentes" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld Mint%2$@ выбрано" + "value" : "Переводы в ожидании" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld Mint%2$@ seçildi" + "value" : "Transferler beklemede" } } } }, - "%lld objects" : { + "Unknown" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "%lld عنصر" + "value" : "غير معروف" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "%lld অবজেক্ট" + "value" : "অজানা" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%lld Objekte" + "value" : "Unbekannt" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "%lld objetos" + "value" : "Desconocido" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%lld objets" + "value" : "Inconnu" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "%lld ऑब्जेक्ट" + "value" : "अज्ञात" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%lldオブジェクト" + "value" : "不明" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "%lld 객체" + "value" : "알 수 없음" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "%lld objetos" + "value" : "Desconhecido" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "%lld объектов" + "value" : "Неизвестно" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%lld nesne" + "value" : "Bilinmiyor" } } } }, - "%lld ppk" : { - "shouldTranslate" : false - }, - "%lld sats" : { - "extractionState" : "stale", + "Unpaid" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "غير مدفوع" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "অপরিশোধিত" + } + }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%lld sats" + "value" : "Unbezahlt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No pagado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non payé" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "भुगतान नहीं हुआ" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "未払い" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "미지불" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não pago" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не оплачено" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ödenmedi" } } - }, - "shouldTranslate" : false + } }, - "%lld." : { + "Verifying..." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "جارٍ التحقق…" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "যাচাই করা হচ্ছে…" + } + }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%lld." + "value" : "Überprüfe…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verificando…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vérification…" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सत्यापित किया जा रहा है…" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "確認中…" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "확인 중…" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verificando…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проверка…" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Doğrulanıyor…" } } - }, - "shouldTranslate" : false + } }, - "%lld/%lld" : { + "• Allocation:" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "• التخصيص:" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "• বরাদ্দ:" + } + }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld/" + "value" : "• Zuteilung:" } }, - "en" : { + "es" : { "stringUnit" : { "state" : "translated", - "value" : "%1$lld/%2$lld" + "value" : "• Asignación:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "• Répartition :" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "• आवंटन:" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "• 配分:" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "• 할당:" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "• Alocação:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "• Распределение:" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "• Tahsis:" } } - }, - "shouldTranslate" : false - }, - "%lld%%" : { - "shouldTranslate" : false - }, - "• Allocation:" : { - + } }, "⚠️ Encoding Error" : { "localizations" : { @@ -2271,7 +4998,74 @@ } }, "Amount hidden" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المبلغ مخفي" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "পরিমাণ লুকানো" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Betrag verborgen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cantidad oculta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Montant masqué" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "राशि छिपी हुई" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "金額非表示" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "금액 숨김" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valor oculto" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сумма скрыта" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tutar gizli" + } + } + } }, "Amount: " : { "localizations" : { @@ -4795,7 +7589,74 @@ } }, "Close" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إغلاق" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "বন্ধ করুন" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schließen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fermer" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "बंद करें" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "閉じる" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "닫기" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fechar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрыть" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapat" + } + } + } }, "Colombian Peso" : { "localizations" : { @@ -7190,7 +10051,74 @@ } }, "Dismiss this popover." : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إغلاق هذه النافذة المنبثقة." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "এই পপওভারটি বন্ধ করুন।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dieses Popover schließen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar esta ventana emergente." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fermer cette fenêtre contextuelle." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस पॉपओवर को बंद करें।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このポップオーバーを閉じます。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 팝오버를 닫습니다." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fechar este popover." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрыть это всплывающее окно." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu açılır pencereyi kapat." + } + } + } }, "Display" : { "localizations" : { @@ -9296,7 +12224,74 @@ } }, "Fee:" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الرسوم:" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ফি:" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gebühr:" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comisión:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Frais :" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "शुल्क:" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "手数料:" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "수수료:" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taxa:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Комиссия:" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ücret:" + } + } + } }, "Fee: %lld • Allocation: %lld" : { "extractionState" : "stale", @@ -9729,7 +12724,74 @@ } }, "Generic" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "عام" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "সাধারণ" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Generisch" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genérico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Générique" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सामान्य" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "汎用" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "일반" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genérico" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Универсальный" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genel" + } + } + } }, "Get Invoice" : { "localizations" : { @@ -12842,7 +15904,74 @@ } }, "Loading quotes..." : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "جارٍ تحميل عروض الأسعار…" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "কোটেশন লোড হচ্ছে…" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lade Angebote…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cargando cotizaciones…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chargement des devis…" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कोटेशन लोड हो रहे हैं…" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "見積もりを読み込み中…" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "견적 불러오는 중…" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carregando cotações…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Загрузка предложений…" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teklifler yükleniyor…" + } + } + } }, "Lock to public key" : { "localizations" : { @@ -17201,7 +20330,74 @@ } }, "No supported methods" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا توجد طرق مدعومة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "কোনো সমর্থিত পদ্ধতি নেই" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine unterstützten Methoden" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No hay métodos compatibles" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune méthode prise en charge" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कोई समर्थित विधि नहीं" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "対応する方法がありません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "지원되는 방법 없음" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nenhum método compatível" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нет поддерживаемых методов" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desteklenen yöntem yok" + } + } + } }, "No supported payment method found in bitcoin URI" : { "localizations" : { @@ -17414,7 +20610,74 @@ } }, "No units available." : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا توجد وحدات متاحة." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "কোনো ইউনিট উপলব্ধ নেই।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine Einheiten verfügbar." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No hay unidades disponibles." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune unité disponible." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कोई इकाई उपलब्ध नहीं।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "利用可能な単位がありません。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용 가능한 단위가 없습니다." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nenhuma unidade disponível." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нет доступных единиц." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanılabilir birim yok." + } + } + } }, "No wallet initialized yet." : { "localizations" : { @@ -18117,7 +21380,74 @@ } }, "On-chain" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "على السلسلة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "অন-চেইন" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "On-Chain" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "On-chain" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "On-chain" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऑन-चेन" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オンチェーン" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "온체인" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "On-chain" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ончейн" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zincir üstü" + } + } + } }, "On-chain Bitcoin payments are not supported" : { "localizations" : { @@ -20924,7 +24254,74 @@ } }, "Pending Payment Parts" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أجزاء الدفع المعلقة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "অমীমাংসিত পেমেন্ট অংশ" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausstehende Zahlungsteile" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partes del pago pendientes" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parties du paiement en attente" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "लंबित भुगतान भाग" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "保留中の支払い部分" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "대기 중인 결제 부분" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partes do pagamento pendentes" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ожидающие части платежа" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bekleyen ödeme parçaları" + } + } + } }, "Pending Receive" : { "localizations" : { @@ -21347,7 +24744,74 @@ } }, "Please enter each mint URL only once. If no protocol is specified, https:// will be used." : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يرجى إدخال كل عنوان URL لـ Mint مرة واحدة فقط. إذا لم يُحدد بروتوكول، فسيتم استخدام https://." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "প্রতিটি Mint URL শুধুমাত্র একবার লিখুন। কোনো প্রোটোকল উল্লেখ না থাকলে https:// ব্যবহার করা হবে।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitte gib jede Mint-URL nur einmal ein. Wird kein Protokoll angegeben, wird https:// verwendet." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce cada URL de mint solo una vez. Si no se especifica un protocolo, se usará https://." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veuillez saisir chaque URL de mint une seule fois. Si aucun protocole n'est précisé, https:// sera utilisé." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कृपया प्रत्येक Mint URL केवल एक बार दर्ज करें। यदि कोई प्रोटोकॉल निर्दिष्ट नहीं है, तो https:// का उपयोग किया जाएगा।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "各ミントのURLは一度だけ入力してください。プロトコルが指定されていない場合は https:// が使用されます。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "각 민트 URL은 한 번만 입력하세요. 프로토콜을 지정하지 않으면 https://가 사용됩니다." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insira cada URL de mint apenas uma vez. Se nenhum protocolo for especificado, https:// será usado." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вводите каждый URL минта только один раз. Если протокол не указан, будет использован https://." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Her Mint URL'sini yalnızca bir kez girin. Protokol belirtilmezse https:// kullanılır." + } + } + } }, "Polish Złoty" : { "localizations" : { @@ -22061,13 +25525,214 @@ } }, "Quote amount mismatch" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "عدم تطابق مبلغ عرض السعر" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "কোটেশনের পরিমাণ মিলছে না" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Angebotsbetrag stimmt nicht überein" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El importe de la cotización no coincide" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Montant du devis non concordant" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कोटेशन राशि मेल नहीं खाती" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "見積もり金額の不一致" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "견적 금액 불일치" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valor da cotação não corresponde" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сумма предложения не совпадает" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teklif tutarı uyuşmuyor" + } + } + } }, "Quote amounts don't match invoice" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مبالغ عرض السعر لا تطابق الفاتورة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "কোটেশনের পরিমাণ ইনভয়েসের সাথে মিলছে না" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Angebotsbeträge stimmen nicht mit der Rechnung überein" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los importes de la cotización no coinciden con la factura" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les montants du devis ne correspondent pas à la facture" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कोटेशन राशियाँ इनवॉइस से मेल नहीं खातीं" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "見積もり金額が請求書と一致しません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "견적 금액이 인보이스와 일치하지 않습니다" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os valores da cotação não correspondem à fatura" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Суммы предложения не совпадают со счётом" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teklif tutarları faturayla uyuşmuyor" + } + } + } }, "Quote fetch failed" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "فشل جلب عرض السعر" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "কোটেশন আনতে ব্যর্থ" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abruf des Angebots fehlgeschlagen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error al obtener la cotización" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Échec de la récupération du devis" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कोटेशन प्राप्त करने में विफल" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "見積もりの取得に失敗しました" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "견적을 가져오지 못했습니다" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Falha ao obter a cotação" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось получить предложение" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teklif alınamadı" + } + } + } }, "Quote ID" : { "localizations" : { @@ -26353,7 +30018,74 @@ } }, "Tap Next to start using the app." : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اضغط على التالي لبدء استخدام التطبيق." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "অ্যাপ ব্যবহার শুরু করতে পরবর্তী-তে ট্যাপ করুন।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tippe auf Weiter, um die App zu verwenden." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toca Siguiente para empezar a usar la app." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touchez Suivant pour commencer à utiliser l'app." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐप का उपयोग शुरू करने के लिए आगे पर टैप करें।" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "「次へ」をタップしてアプリの使用を開始します。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "앱 사용을 시작하려면 다음을 탭하세요." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toque em Avançar para começar a usar o app." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нажмите «Далее», чтобы начать пользоваться приложением." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulamayı kullanmaya başlamak için İleri'ye dokunun." + } + } + } }, "Tap to select a mint from the list" : { "localizations" : { @@ -29019,7 +32751,74 @@ } }, "Total Lightning Fees:" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إجمالي رسوم Lightning:" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "মোট Lightning ফি:" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lightning-Gebühren gesamt:" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comisiones Lightning totales:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Frais Lightning totaux :" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कुल Lightning शुल्क:" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lightning手数料合計:" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "총 Lightning 수수료:" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taxas Lightning totais:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Всего комиссий Lightning:" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toplam Lightning ücretleri:" + } + } + } }, "Total Lightning Fees: %lld" : { "extractionState" : "stale", @@ -30073,7 +33872,74 @@ } }, "Unit" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الوحدة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ইউনিট" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einheit" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unité" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इकाई" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "単位" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "단위" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unidade" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Единица" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Birim" + } + } + } }, "Unit Error 💵" : { "localizations" : { @@ -32679,4 +36545,4 @@ } }, "version" : "1.1" -} \ No newline at end of file +}