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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/ar.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,7 @@
"subscriptions__allowances" = "المخصصات";
"subscriptions__allowances_empty_headline" = "أعدّ\n<accent>المخصصات</accent>";
"subscriptions__allowances_paused_description" = "سجلات الميزانيات لديك أقدم من سجلات استخدمتها هذه المحفظة من قبل. ستطلب الطلبات موافقتك حتى تستأنف.";
"subscriptions__allowances_paused_restore_description" = "تمت استعادة هذه المحفظة من نسخة احتياطية. ستطلب الطلبات موافقتك حتى تستأنف.";
"subscriptions__allowances_paused_resume" = "استئناف";
"subscriptions__allowances_paused_title" = "تم إيقاف الدفعات التلقائية مؤقتًا";
"subscriptions__allowances_empty_description" = "يمكنك تعيين حدود إنفاق لتلبية طلبات الدفع من جهات الاتصال الموثوقة تلقائيًا.";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/ca.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1701,6 +1701,7 @@
"subscriptions__allowances" = "Assignacions";
"subscriptions__allowances_empty_headline" = "Configura\n<accent>assignacions</accent>";
"subscriptions__allowances_paused_description" = "Els teus registres d'assignacions són més antics que altres que aquest moneder ja ha fet servir. Les sol·licituds demanaran la teva aprovació fins que reprenguis.";
"subscriptions__allowances_paused_restore_description" = "Aquest moneder s'ha restaurat a partir d'una còpia de seguretat. Les sol·licituds demanaran la teva aprovació fins que reprenguis.";
"subscriptions__allowances_paused_resume" = "Reprèn";
"subscriptions__allowances_paused_title" = "Pagaments automàtics en pausa";
"subscriptions__allowances_empty_description" = "Pots establir límits de despesa per pagar automàticament les sol·licituds de pagament de contactes de confiança.";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/cs.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,7 @@
"subscriptions__allowances" = "Povolení plateb";
"subscriptions__allowances_empty_headline" = "Nastavte\n<accent>povolení plateb</accent>";
"subscriptions__allowances_paused_description" = "Záznamy vašich rozpočtů jsou starší než ty, které tato peněženka už použila. Požadavky budou čekat na vaše schválení, dokud platby neobnovíte.";
"subscriptions__allowances_paused_restore_description" = "Tato peněženka byla obnovena ze zálohy. Požadavky budou čekat na vaše schválení, dokud platby neobnovíte.";
"subscriptions__allowances_paused_resume" = "Obnovit";
"subscriptions__allowances_paused_title" = "Automatické platby pozastaveny";
"subscriptions__allowances_empty_description" = "Můžete nastavit limity útraty a automaticky tak vyřizovat žádosti o platbu od důvěryhodných kontaktů.";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1837,6 +1837,7 @@
"subscriptions__allowances" = "Kontingente";
"subscriptions__allowances_empty_headline" = "Kontingente\n<accent>einrichten</accent>";
"subscriptions__allowances_paused_description" = "Deine Budgetdaten sind älter als Daten, die diese Wallet bereits verwendet hat. Anfragen warten auf deine Bestätigung, bis du fortsetzt.";
"subscriptions__allowances_paused_restore_description" = "Diese Wallet wurde aus einem Backup wiederhergestellt. Anfragen warten auf deine Bestätigung, bis du fortsetzt.";
"subscriptions__allowances_paused_resume" = "Fortsetzen";
"subscriptions__allowances_paused_title" = "Automatische Zahlungen pausiert";
"subscriptions__allowances_empty_description" = "Lege Ausgabenlimits fest, um Zahlungsanforderungen vertrauenswürdiger Kontakte automatisch zu bezahlen.";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/el.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1572,6 +1572,7 @@
"subscriptions__allowances" = "Προεγκρίσεις";
"subscriptions__allowances_empty_headline" = "Ρύθμισε\n<accent>προεγκρίσεις</accent>";
"subscriptions__allowances_paused_description" = "Τα αρχεία των προϋπολογισμών σου είναι παλαιότερα από αρχεία που έχει ήδη χρησιμοποιήσει αυτό το πορτοφόλι. Τα αιτήματα θα ζητούν την έγκρισή σου μέχρι να συνεχίσεις.";
"subscriptions__allowances_paused_restore_description" = "Αυτό το πορτοφόλι επαναφέρθηκε από αντίγραφο ασφαλείας. Τα αιτήματα θα ζητούν την έγκρισή σου μέχρι να συνεχίσεις.";
"subscriptions__allowances_paused_resume" = "Συνέχεια";
"subscriptions__allowances_paused_title" = "Οι αυτόματες πληρωμές έχουν παυθεί";
"subscriptions__allowances_empty_description" = "Μπορείς να ορίσεις όρια δαπανών για να εξοφλούνται αυτόματα αιτήματα πληρωμής από αξιόπιστες επαφές.";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1714,6 +1714,7 @@
"subscriptions__allowances" = "Allowances";
"subscriptions__allowances_empty_headline" = "Set up\n<accent>allowances</accent>";
"subscriptions__allowances_paused_description" = "Your allowance records are older than ones this wallet has already used. Requests ask for your approval until you resume.";
"subscriptions__allowances_paused_restore_description" = "This wallet was restored from a backup. Requests ask for your approval until you resume.";
"subscriptions__allowances_paused_resume" = "Resume";
"subscriptions__allowances_paused_title" = "Automatic payments paused";
"subscriptions__allowances_empty_description" = "You can set spending limits to automatically fulfill payment requests from trusted peers.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1843,6 +1843,7 @@
"subscriptions__allowances" = "Asignaciones";
"subscriptions__allowances_empty_headline" = "Configure\n<accent>asignaciones</accent>";
"subscriptions__allowances_paused_description" = "Tus registros de asignaciones son más antiguos que otros que esta billetera ya usó. Las solicitudes pedirán tu aprobación hasta que reanudes.";
"subscriptions__allowances_paused_restore_description" = "Esta billetera se restauró desde una copia de seguridad. Las solicitudes pedirán tu aprobación hasta que reanudes.";
"subscriptions__allowances_paused_resume" = "Reanudar";
"subscriptions__allowances_paused_title" = "Pagos automáticos en pausa";
"subscriptions__allowances_empty_description" = "Puede establecer límites de gasto para pagar automáticamente las solicitudes de pago de contactos de confianza.";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1721,6 +1721,7 @@
"subscriptions__allowances" = "Asignaciones";
"subscriptions__allowances_empty_headline" = "Configure\n<accent>asignaciones</accent>";
"subscriptions__allowances_paused_description" = "Tus registros de asignaciones son más antiguos que otros que este monedero ya ha usado. Las solicitudes pedirán tu aprobación hasta que reanudes.";
"subscriptions__allowances_paused_restore_description" = "Este monedero se ha restaurado desde una copia de seguridad. Las solicitudes pedirán tu aprobación hasta que reanudes.";
"subscriptions__allowances_paused_resume" = "Reanudar";
"subscriptions__allowances_paused_title" = "Pagos automáticos en pausa";
"subscriptions__allowances_empty_description" = "Puede establecer límites de gasto para pagar automáticamente las solicitudes de pago de contactos de confianza.";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1851,6 +1851,7 @@
"subscriptions__allowances" = "Allocations";
"subscriptions__allowances_empty_headline" = "Configurez\n<accent>des allocations</accent>";
"subscriptions__allowances_paused_description" = "Vos données d'allocations sont plus anciennes que celles que ce portefeuille a déjà utilisées. Les demandes attendront votre accord jusqu'à la reprise.";
"subscriptions__allowances_paused_restore_description" = "Ce portefeuille a été restauré à partir d'une sauvegarde. Les demandes attendront votre accord jusqu'à la reprise.";
"subscriptions__allowances_paused_resume" = "Reprendre";
"subscriptions__allowances_paused_title" = "Paiements automatiques en pause";
"subscriptions__allowances_empty_description" = "Vous pouvez définir des limites de dépenses pour régler automatiquement les demandes de paiement de contacts de confiance.";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1810,6 +1810,7 @@
"subscriptions__allowances" = "Plafond";
"subscriptions__allowances_empty_headline" = "Imposta\n<accent>i plafond</accent>";
"subscriptions__allowances_paused_description" = "I dati dei tuoi budget sono più vecchi di quelli che questo wallet ha già usato. Le richieste chiederanno la tua approvazione finché non riprendi.";
"subscriptions__allowances_paused_restore_description" = "Questo wallet è stato ripristinato da un backup. Le richieste chiederanno la tua approvazione finché non riprendi.";
"subscriptions__allowances_paused_resume" = "Riprendi";
"subscriptions__allowances_paused_title" = "Pagamenti automatici in pausa";
"subscriptions__allowances_empty_description" = "Puoi impostare limiti di spesa per pagare automaticamente le richieste di pagamento dei contatti fidati.";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/nl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1845,6 +1845,7 @@
"subscriptions__allowances" = "Toelagen";
"subscriptions__allowances_empty_headline" = "Stel\n<accent>toelagen</accent> in";
"subscriptions__allowances_paused_description" = "Je toelagengegevens zijn ouder dan gegevens die deze wallet al heeft gebruikt. Verzoeken vragen om je goedkeuring tot je hervat.";
"subscriptions__allowances_paused_restore_description" = "Deze wallet is hersteld vanuit een backup. Verzoeken vragen om je goedkeuring tot je hervat.";
"subscriptions__allowances_paused_resume" = "Hervatten";
"subscriptions__allowances_paused_title" = "Automatische betalingen gepauzeerd";
"subscriptions__allowances_empty_description" = "U kunt bestedingslimieten instellen om betaalverzoeken van vertrouwde contacten automatisch te voldoen.";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/pl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1850,6 +1850,7 @@
"subscriptions__allowances" = "Przydziały";
"subscriptions__allowances_empty_headline" = "Ustaw\n<accent>przydziały</accent>";
"subscriptions__allowances_paused_description" = "Dane Twoich budżetów są starsze niż te, których ten portfel już użył. Prośby będą czekać na Twoją zgodę, dopóki nie wznowisz.";
"subscriptions__allowances_paused_restore_description" = "Ten portfel został przywrócony z kopii zapasowej. Prośby będą czekać na Twoją zgodę, dopóki nie wznowisz.";
"subscriptions__allowances_paused_resume" = "Wznów";
"subscriptions__allowances_paused_title" = "Płatności automatyczne wstrzymane";
"subscriptions__allowances_empty_description" = "Możesz ustawić limity wydatków, aby automatycznie realizować żądania płatności od zaufanych kontaktów.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1851,6 +1851,7 @@
"subscriptions__allowances" = "Pré-aprovações";
"subscriptions__allowances_empty_headline" = "Configure\n<accent>pré-aprovações</accent>";
"subscriptions__allowances_paused_description" = "Os registros das suas mesadas são mais antigos do que outros que esta carteira já usou. As solicitações vão pedir sua aprovação até você retomar.";
"subscriptions__allowances_paused_restore_description" = "Esta carteira foi restaurada a partir de um backup. As solicitações vão pedir sua aprovação até você retomar.";
"subscriptions__allowances_paused_resume" = "Retomar";
"subscriptions__allowances_paused_title" = "Pagamentos automáticos pausados";
"subscriptions__allowances_empty_description" = "Você pode definir limites de gastos para pagar automaticamente solicitações de pagamento de contatos confiáveis.";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/pt.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1497,6 +1497,7 @@
"subscriptions__allowances" = "Pré-aprovações";
"subscriptions__allowances_empty_headline" = "Configure\n<accent>pré-aprovações</accent>";
"subscriptions__allowances_paused_description" = "Os registos das suas mesadas são mais antigos do que outros que esta carteira já usou. Os pedidos vão pedir a sua aprovação até retomar.";
"subscriptions__allowances_paused_restore_description" = "Esta carteira foi restaurada a partir de um backup. Os pedidos vão pedir a sua aprovação até retomar.";
"subscriptions__allowances_paused_resume" = "Retomar";
"subscriptions__allowances_paused_title" = "Pagamentos automáticos em pausa";
"subscriptions__allowances_empty_description" = "Pode definir limites de gastos para pagar automaticamente pedidos de pagamento de contactos de confiança.";
Expand Down
1 change: 1 addition & 0 deletions Bitkit/Resources/Localization/ru.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1831,6 +1831,7 @@
"subscriptions__allowances" = "Лимиты автооплаты";
"subscriptions__allowances_empty_headline" = "Настройте\n<accent>лимиты автооплаты</accent>";
"subscriptions__allowances_paused_description" = "Данные ваших бюджетов старше тех, что этот кошелёк уже использовал. Запросы будут ждать вашего подтверждения, пока вы не возобновите платежи.";
"subscriptions__allowances_paused_restore_description" = "Этот кошелёк был восстановлен из резервной копии. Запросы будут ждать вашего подтверждения, пока вы не возобновите платежи.";
"subscriptions__allowances_paused_resume" = "Возобновить";
"subscriptions__allowances_paused_title" = "Автоматические платежи приостановлены";
"subscriptions__allowances_empty_description" = "Вы можете задать лимиты расходов, чтобы автоматически оплачивать запросы на оплату от доверенных контактов.";
Expand Down
21 changes: 15 additions & 6 deletions Bitkit/Services/BackupService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -691,14 +691,10 @@ class BackupService {
try await performRestore(category: .wallet, retainedData: retainedWalletBackup) { dataBytes in
try walletRestoreGate.retain(dataBytes)
let payload = try JSONDecoder().decode(WalletBackupV1.self, from: dataBytes)
if let paymentState = payload.paykitPaymentState {
try await Self.restorePaykitPaymentState(from: payload) { paymentState, hasLedger in
// VssBackupClient's existing custom backup namespace is explicitly wallet index 0.
let backupWallet = try await PaykitPaymentStateBackup.ActiveOnchainAttempt.Wallet.backupNamespace(index: 0)
try await PaykitPaymentProofService.shared.restoreBackup(
paymentState,
wallet: backupWallet,
hasLedger: payload.paykitSdkBackupState != nil
)
try await PaykitPaymentProofService.shared.restoreBackup(paymentState, wallet: backupWallet, hasLedger: hasLedger)
}
try TransferStorage.shared.upsertList(payload.transfers)
await PrivatePaykitAddressReservationStore.shared.restoreBackup(
Expand Down Expand Up @@ -1422,6 +1418,19 @@ class BackupService {
markRestoreComplete(category: category)
}

/// A backup can predate payments made since, so restoring a wallet payload pauses automatic payments before any Paykit
/// state comes back, also for a payload without payment state, until the user resumes them. The pause is kept per
/// device, so it applies even when no allowance this wallet pays through existed at the restore.
static func restorePaykitPaymentState(
from payload: WalletBackupV1,
allowanceStore: any PaykitAllowanceStoring = PaykitAllowanceKeychainStore(),
restore: (_ paymentState: PaykitPaymentStateBackup, _ hasLedger: Bool) async throws -> Void
) async throws {
try allowanceStore.setPausedAfterRestore(true)
guard let paymentState = payload.paykitPaymentState else { return }
try await restore(paymentState, payload.paykitSdkBackupState != nil)
}

func markRestoreComplete(category: BackupCategory) {
let currentTime = UInt64(Date().timeIntervalSince1970)
updateBackupStatus(category: category) { status in
Expand Down
35 changes: 33 additions & 2 deletions Bitkit/Services/PaykitAllowance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ struct PaykitAllowance: Identifiable, Hashable {
let lastEventAt: Date?
/// The proposal's stream item as displayed; acceptance names it so a re-sent proposal asks again.
let proposalStreamItemId: UInt64?
/// Terms with a limit beyond the per-payment cap and the one monthly amount Bitkit checks itself, such as a lifetime
/// amount, a payment count or another period. Bitkit never proposes these, and they never pay automatically.
let hasUncheckedLimits: Bool

var counterparty: String {
id.counterparty
Expand All @@ -65,7 +68,8 @@ struct PaykitAllowance: Identifiable, Hashable {
terms.asset() == PaykitIssuerInterop.bitcoinAsset
else { return nil }

let monthly = terms.periodLimits().first { Self.isMonthly($0.period()) }
let periodLimits = terms.periodLimits()
let monthly = periodLimits.first { Self.isMonthly($0.period()) }
let perPaymentMaxSats = terms.perPaymentAmount().flatMap { Self.sats(fromBitcoinAmount: $0.maximum()) }
let monthlyLimitSats = monthly?.amountLimit().flatMap { Self.sats(fromBitcoinAmount: $0) }
let monthlyAnchor = monthly?.period().anchor().flatMap(PaykitAllowanceTime.parse)
Expand All @@ -85,6 +89,8 @@ struct PaykitAllowance: Identifiable, Hashable {
allowedPaymentEndpointIdentifiers = terms.allowedPaymentEndpointIdentifiers()
lastEventAt = record.lastEventAt.flatMap(PaykitAllowanceTime.parse)
proposalStreamItemId = record.proposalStreamItemId
hasUncheckedLimits = terms.lifetimeAmountLimit() != nil || periodLimits.count > 1 ||
periodLimits.contains { !Self.isMonthly($0.period()) || $0.paymentCountLimit() != nil }
}

init(
Expand All @@ -99,7 +105,8 @@ struct PaykitAllowance: Identifiable, Hashable {
expiresAt: Date? = nil,
allowedPaymentEndpointIdentifiers: [String]? = nil,
lastEventAt: Date? = nil,
proposalStreamItemId: UInt64? = nil
proposalStreamItemId: UInt64? = nil,
hasUncheckedLimits: Bool = false
) {
self.id = id
self.role = role
Expand All @@ -113,6 +120,7 @@ struct PaykitAllowance: Identifiable, Hashable {
self.allowedPaymentEndpointIdentifiers = allowedPaymentEndpointIdentifiers
self.lastEventAt = lastEventAt
self.proposalStreamItemId = proposalStreamItemId
self.hasUncheckedLimits = hasUncheckedLimits
}

func status(at now: Date) -> Status {
Expand Down Expand Up @@ -319,6 +327,29 @@ enum PaykitAllowanceCapacity {
return used + amountSats <= monthlyLimitSats
}

enum Fit: Equatable {
/// The allowances with only the limits this wallet checks that fit the request, in the order given; the request can
/// pay automatically through one of them.
case automatic([PaykitAllowance])
/// Only allowances with limits this wallet does not check fit, so the request asks without Limit Reached.
case uncheckedOnly
/// No allowance fits the request, so it asks and Limit Reached is raised.
case none
}

/// Which of a request's matching allowances can pay it automatically. Every allowance is measured against the
/// per-payment cap and monthly amount this wallet checks; only one without other limits can pay automatically.
static func fit(amountSats: UInt64, allowances: [PaykitAllowance], attempts: [Attempt], now: Date, localAttempts: [Attempt] = []) -> Fit {
let fitting = allowances.filter {
fits(amountSats: amountSats, allowance: $0, attempts: attempts, now: now, localAttempts: localAttempts)
}
let automatic = fitting.filter { !$0.hasUncheckedLimits }
if !automatic.isEmpty {
return .automatic(automatic)
}
return fitting.isEmpty ? .none : .uncheckedOnly
}

static func attempts(from spends: [PaykitAllowanceLocalState.AutomaticSpend]) -> [Attempt] {
spends.map {
Attempt(
Expand Down
Loading
Loading