Repository navigation
fix: allowance follow-ups #1141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
b6dd98d
fix: release a failed automatic payment's list once
ovitrif 411b050
fix: keep auto-paid labels and totals past 200 allowance attempts
ovitrif d3ce74b
fix: keep a bitcoin request an allowance can't pay over lightning manual
ovitrif 6f7354a
fix: load allowance contacts and identity once the paykit session app…
ovitrif 114dcaa
fix: announce every allowance the wallet accepts by itself
ovitrif 9d096f6
fix: say an allowance PIN check confirms the allowance change
ovitrif cc24f0e
chore: add changelog fragment for allowance follow-ups
ovitrif 26074ba
fix: release a failed payment's list only after its outcome is saved
ovitrif 2ac0e56
fix: count auto-paid history for the signed-in identity only
ovitrif e641ef2
fix: start allowances again when a session change cuts the start short
ovitrif 29ecfc2
fix: name each contact once in a combined allowance added notice
ovitrif 99b02d1
fix: keep allowance labels and open attempts across a wallet restore
ovitrif 48f34cf
docs: note how allowance journey requests are priced on each platform
ovitrif d0b9084
fix: keep a bad allowance backup entry from failing the wallet backup
ovitrif e4e6b85
chore: cover the restore fix in the allowance changelog entry
ovitrif aa2b648
fix: restart allowances for a session that appeared mid-start and mer…
ovitrif e56f58a
fix: write absent allowance backup values as null so android restores…
ovitrif 27d1675
docs: add the restore keeps history allowance journey
ovitrif 100d7b2
fix: let the node settle a restored allowance attempt
ovitrif 37b2ed7
fix: announce an accepted allowance offer once when its mark fails to…
ovitrif 7296854
fix: retry clearing an announced allowance offer's mark
ovitrif fb62396
fix: leave a restored allowance attempt open until the node can settl…
ovitrif 08225c9
fix: forget announced allowance offers on sign-out or profile switch
ovitrif e71b591
fix: give back a consumed payment list after a restart
ovitrif 3335054
fix: never consume a payment list for an attempt the journal lost
ovitrif 73a4fcf
fix: keep a payment list's owner through the wallet backup
ovitrif f085773
docs: match the ended allowance row in the end journey
ovitrif f3b5825
fix: leave a payment in progress out of recovery
ovitrif File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| import Foundation | ||
|
|
||
| /// The allowance states of a wallet backup by identity. An entry this version can't read is dropped instead of failing the | ||
| /// rest of the wallet backup it travels in. | ||
| struct PaykitAllowanceStateBackups: Codable { | ||
| let byIdentity: [String: PaykitAllowanceStateBackup] | ||
|
|
||
| init(_ byIdentity: [String: PaykitAllowanceStateBackup]) { | ||
| self.byIdentity = byIdentity | ||
| } | ||
|
|
||
| init(from decoder: Decoder) throws { | ||
| let entries = (try? decoder.singleValueContainer().decode([String: Lenient].self)) ?? [:] | ||
| byIdentity = entries.compactMapValues(\.value) | ||
| if byIdentity.count != entries.count { | ||
| Logger.warn("Skipped \(entries.count - byIdentity.count) unreadable allowance states in a backup", context: "PaykitAllowance") | ||
| } | ||
| } | ||
|
|
||
| func encode(to encoder: Encoder) throws { | ||
| var container = encoder.singleValueContainer() | ||
| try container.encode(byIdentity) | ||
| } | ||
|
|
||
| private struct Lenient: Decodable { | ||
| let value: PaykitAllowanceStateBackup? | ||
|
|
||
| init(from decoder: Decoder) throws { | ||
| value = try? PaykitAllowanceStateBackup(from: decoder) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// One identity's local allowance state in the wallet backup, in the format both apps read: the USD labels of each | ||
| /// grant and the attempt journal restart recovery verifies from. Timestamps are RFC 3339, USD amounts decimal strings. | ||
| struct PaykitAllowanceStateBackup: Codable { | ||
| struct Limits: Codable { | ||
| let perPaymentUsd: String | ||
| let monthlyUsd: String | ||
| let perPaymentSats: UInt64 | ||
| let monthlySats: UInt64 | ||
| } | ||
|
|
||
| struct Group: Codable { | ||
| let id: String | ||
| let counterparty: String | ||
| let limits: Limits | ||
| let allowanceIds: [String] | ||
| let createdAt: String | ||
| } | ||
|
|
||
| struct JournalEntry: Codable { | ||
| let attemptId: String | ||
| let isAutomatic: Bool | ||
| let requestId: PaykitPaymentStateBackup.RequestID | ||
| let allowanceId: String? | ||
| let amountSats: UInt64 | ||
| let paymentEndpointIdentifier: String | ||
| let paymentHash: String? | ||
| let onchainAddress: String? | ||
| let transactionId: String? | ||
| let stage: PaykitAllowanceLocalState.Stage | ||
| let createdAt: String | ||
| let paymentListVersion: UInt64? | ||
| let previousPaymentListVersion: UInt64? | ||
|
|
||
| /// Android requires every key, so an absent optional is written as null rather than left out. | ||
| func encode(to encoder: Encoder) throws { | ||
| var container = encoder.container(keyedBy: CodingKeys.self) | ||
| try container.encode(attemptId, forKey: .attemptId) | ||
| try container.encode(isAutomatic, forKey: .isAutomatic) | ||
| try container.encode(requestId, forKey: .requestId) | ||
| try container.encode(allowanceId, forKey: .allowanceId) | ||
| try container.encode(amountSats, forKey: .amountSats) | ||
| try container.encode(paymentEndpointIdentifier, forKey: .paymentEndpointIdentifier) | ||
| try container.encode(paymentHash, forKey: .paymentHash) | ||
| try container.encode(onchainAddress, forKey: .onchainAddress) | ||
| try container.encode(transactionId, forKey: .transactionId) | ||
| try container.encode(stage, forKey: .stage) | ||
| try container.encode(createdAt, forKey: .createdAt) | ||
| try container.encode(paymentListVersion, forKey: .paymentListVersion) | ||
| try container.encode(previousPaymentListVersion, forKey: .previousPaymentListVersion) | ||
| } | ||
| } | ||
|
|
||
| let groups: [Group] | ||
| let journal: [JournalEntry] | ||
| let presentedProposalIds: Set<String> | ||
| let unannouncedOfferIds: Set<String> | ||
| let notifiedRequestIds: Set<String> | ||
| let lastTrustedTime: String? | ||
|
|
||
| func encode(to encoder: Encoder) throws { | ||
| var container = encoder.container(keyedBy: CodingKeys.self) | ||
| try container.encode(groups, forKey: .groups) | ||
| try container.encode(journal, forKey: .journal) | ||
| try container.encode(presentedProposalIds, forKey: .presentedProposalIds) | ||
| try container.encode(unannouncedOfferIds, forKey: .unannouncedOfferIds) | ||
| try container.encode(notifiedRequestIds, forKey: .notifiedRequestIds) | ||
| try container.encode(lastTrustedTime, forKey: .lastTrustedTime) | ||
| } | ||
|
|
||
| init(_ state: PaykitAllowanceLocalState) { | ||
| groups = state.groups.map { group in | ||
| Group( | ||
| id: group.id, | ||
| counterparty: group.counterparty, | ||
| limits: Limits( | ||
| perPaymentUsd: "\(group.limits.perPaymentUsd)", | ||
| monthlyUsd: "\(group.limits.monthlyUsd)", | ||
| perPaymentSats: group.limits.perPaymentSats, | ||
| monthlySats: group.limits.monthlySats | ||
| ), | ||
| allowanceIds: group.allowanceIds, | ||
| createdAt: PaykitAllowanceTime.format(group.createdAt) | ||
| ) | ||
| } | ||
| journal = state.journal.map { entry in | ||
| JournalEntry( | ||
| attemptId: entry.attemptId, | ||
| isAutomatic: entry.isAutomatic, | ||
| requestId: PaykitPaymentStateBackup.RequestID(entry.requestId, billingPeriod: nil), | ||
| allowanceId: entry.allowanceId, | ||
| amountSats: entry.amountSats, | ||
| paymentEndpointIdentifier: entry.paymentEndpointIdentifier, | ||
| paymentHash: entry.paymentHash, | ||
| onchainAddress: entry.onchainAddress, | ||
| transactionId: entry.transactionId, | ||
| stage: entry.stage, | ||
| createdAt: PaykitAllowanceTime.format(entry.createdAt), | ||
| paymentListVersion: entry.paymentListVersion, | ||
| previousPaymentListVersion: entry.previousPaymentListVersion | ||
| ) | ||
| } | ||
| presentedProposalIds = state.presentedProposalIds | ||
| unannouncedOfferIds = state.unannouncedOfferIds | ||
| notifiedRequestIds = state.notifiedRequestIds | ||
| lastTrustedTime = state.lastTrustedTime.map(PaykitAllowanceTime.format) | ||
| } | ||
|
|
||
| func restored() throws -> PaykitAllowanceLocalState { | ||
| try PaykitAllowanceLocalState( | ||
| groups: groups.map { group in | ||
| guard let perPaymentUsd = Decimal(string: group.limits.perPaymentUsd, locale: Locale(identifier: "en_US_POSIX")), | ||
| let monthlyUsd = Decimal(string: group.limits.monthlyUsd, locale: Locale(identifier: "en_US_POSIX")) | ||
| else { throw Self.invalidBackup("Invalid allowance limits") } | ||
| return try PaykitAllowanceLocalState.Group( | ||
| id: group.id, | ||
| counterparty: group.counterparty, | ||
| limits: PaykitAllowanceLimits( | ||
| perPaymentUsd: perPaymentUsd, | ||
| monthlyUsd: monthlyUsd, | ||
| perPaymentSats: group.limits.perPaymentSats, | ||
| monthlySats: group.limits.monthlySats | ||
| ), | ||
| allowanceIds: group.allowanceIds, | ||
| createdAt: Self.date(group.createdAt) | ||
| ) | ||
| }, | ||
| journal: journal.map { entry in | ||
| try PaykitAllowanceLocalState.JournalEntry( | ||
| attemptId: entry.attemptId, | ||
| isAutomatic: entry.isAutomatic, | ||
| requestId: entry.requestId.restored(billingPeriod: nil), | ||
| allowanceId: entry.allowanceId, | ||
| amountSats: entry.amountSats, | ||
| paymentEndpointIdentifier: entry.paymentEndpointIdentifier, | ||
| paymentHash: entry.paymentHash, | ||
| onchainAddress: entry.onchainAddress, | ||
| transactionId: entry.transactionId, | ||
| stage: entry.stage, | ||
| createdAt: Self.date(entry.createdAt), | ||
| paymentListVersion: entry.paymentListVersion, | ||
| previousPaymentListVersion: entry.previousPaymentListVersion | ||
| ) | ||
| }, | ||
| presentedProposalIds: presentedProposalIds, | ||
| unannouncedOfferIds: unannouncedOfferIds, | ||
| notifiedRequestIds: notifiedRequestIds, | ||
| lastTrustedTime: lastTrustedTime.map(Self.date) | ||
| ) | ||
| } | ||
|
|
||
| private static func date(_ value: String) throws -> Date { | ||
| guard let date = PaykitAllowanceTime.parse(value) else { throw invalidBackup("Invalid allowance timestamp") } | ||
| return date | ||
| } | ||
|
|
||
| private static func invalidBackup(_ message: String) -> NSError { | ||
| NSError(domain: "BackupService", code: -1, userInfo: [NSLocalizedDescriptionKey: message]) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.