diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index c688f66c5..827926935 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -1209,7 +1209,7 @@ repositoryURL = "https://github.com/pubky/paykit-rs"; requirement = { kind = exactVersion; - version = "0.1.1-rc2"; + version = "0.1.1-rc6"; }; }; 18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */ = { diff --git a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d0490865b..faccbba44 100644 --- a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pubky/paykit-rs", "state" : { - "revision" : "01de5dd232d2b22f132d066697cce041700e7136", - "version" : "0.1.1-rc2" + "revision" : "9919198a8a810652945f7366defacae8c3135632", + "version" : "0.1.1-rc6" } }, { diff --git a/Bitkit/Models/PaykitUsdtStateBackup.swift b/Bitkit/Models/PaykitUsdtStateBackup.swift index 4aeef57a2..2c4f72180 100644 --- a/Bitkit/Models/PaykitUsdtStateBackup.swift +++ b/Bitkit/Models/PaykitUsdtStateBackup.swift @@ -60,6 +60,7 @@ struct PaykitUsdtStateBackup: Codable { let receivedAtMillis: Int64 let underpaid: Bool let afterExpiry: Bool + let beforeRequest: Bool? init(_ value: PaykitUsdtPaymentService.Receipt) { wallet = value.wallet @@ -76,6 +77,7 @@ struct PaykitUsdtStateBackup: Codable { receivedAtMillis = Int64(value.receivedAt.timeIntervalSince1970 * 1000) underpaid = value.underpaid afterExpiry = value.afterExpiry + beforeRequest = value.beforeRequest } func restored() throws -> PaykitUsdtPaymentService.Receipt { @@ -89,7 +91,8 @@ struct PaykitUsdtStateBackup: Codable { billingPeriodStartsAt: periodStart), paymentId: paymentId, proofEventId: proofEventId, verified: verified, transferId: transferId, amount: PaykitAmount(asset: .usdt, atomic: amountAtomic), - receivedAt: Date(timeIntervalSince1970: Double(receivedAtMillis) / 1000), underpaid: underpaid, afterExpiry: afterExpiry + receivedAt: Date(timeIntervalSince1970: Double(receivedAtMillis) / 1000), underpaid: underpaid, afterExpiry: afterExpiry, + beforeRequest: beforeRequest ?? false ) } } diff --git a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings index 0316d2a6b..361e6ca6b 100644 --- a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings @@ -572,6 +572,8 @@ "pubky_auth__homeserver" = "الخادم الرئيسي"; "pubky_auth__signup_description" = "أنشئ هوية Pubky جديدة على هذا الخادم الرئيسي. تابع فقط إذا كنت تثق به."; "pubky_auth__approval_failed" = "فشل التفويض"; +"pubky_auth__identity_unavailable_desc" = "تحقق من اتصالك وحاول مرة أخرى."; +"profile__auth_error_signup_code_rejected" = "لم يقبل الخادم الرئيسي رمز التسجيل هذا. حاول مرة أخرى أو استخدم رابط تسجيل آخر."; "wallet__onchain_abandon_transfer_note" = "لم تعد التحويلات إلى رصيد الإنفاق متاحة. يتحقق Bitkit أولًا من الشبكة، ثم يعيد عملات هذا التحويل إلى محفظتك حتى لا يتأكد التحويل الأصلي أبدًا. يكلّف ذلك رسوم شبكة."; "watch_only_accounts__active_section" = "الحسابات النشطة"; "watch_only_accounts__copy_xpub" = "نسخ xpub"; @@ -1229,6 +1231,7 @@ "wallet__payment_request_payment_verified" = "تم التحقق من الدفع"; "wallet__payment_request_underpaid" = "مستلم · دفع ناقص"; "wallet__payment_request_after_expiry" = "مستلم · بعد انتهاء الصلاحية"; +"wallet__payment_request_before_request" = "مستلم · قبل الطلب"; "pubky_auth__usdt_intro_description" = "لتربح، تحتاج إلى مشاركة عنوان USDT الخاص بك مع Paykit. يمكنه عرض نشاط المبيعات، لكنه لا يستطيع إنفاق الأموال."; "pubky_auth__usdt_intro_optional" = "يمكنك مشاركة عنوان USDT الخاص بك مع Paykit لاستلام الأرباح. يمكنه عرض نشاط المبيعات، لكنه لا يستطيع إنفاق الأموال."; "pubky_auth__usdt_unavailable" = "عنوان USDT الخاص بك غير متاح. يرجى المحاولة مرة أخرى."; @@ -1288,6 +1291,7 @@ "usdt__deposit_refund_address_network" = "عنوان USDT على {network}"; "wallet__payment_request" = "طلب دفع"; "wallet__payment_request_unavailable" = "لم يعد طلب الدفع متاحًا."; +"wallet__payment_request_note_too_long" = "اختصر الملاحظة وحاول مرة أخرى."; "wallet__payment_request_expired" = "انتهت صلاحية طلب الدفع."; "wallet__payment_request_waiting_for_details" = "لا تزال تفاصيل الدفع قيد الاستعادة. حاول مرة أخرى قريبًا."; "wallet__payment_request_in_progress" = "طلب الدفع قيد المعالجة بالفعل."; diff --git a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings index b73fe7057..2515533ac 100644 --- a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings @@ -656,6 +656,8 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Crea una nova identitat Pubky en aquest homeserver. Continua només si hi confies."; "pubky_auth__approval_failed" = "Ha fallat l'autorització"; +"pubky_auth__identity_unavailable_desc" = "Comprova la connexió i torna-ho a provar."; +"profile__auth_error_signup_code_rejected" = "L'homeserver no ha acceptat aquest codi de registre. Torna-ho a provar o utilitza un altre enllaç de registre."; "wallet__onchain_abandon_transfer_note" = "Les transferències al saldo de despesa ja no estan disponibles. Bitkit primer comprova la xarxa i després retorna les monedes d'aquesta transferència a la teva cartera perquè la transferència original no es pugui confirmar mai. Això té una comissió de xarxa."; "watch_only_accounts__active_section" = "Comptes actius"; "watch_only_accounts__copy_xpub" = "Copiar xpub"; @@ -1327,6 +1329,7 @@ "wallet__payment_received" = "Rebut Bitcoin"; "wallet__payment_request" = "Sol·licitud de pagament"; "wallet__payment_request_unavailable" = "La sol·licitud de pagament ja no està disponible."; +"wallet__payment_request_note_too_long" = "Escurça la nota i torna-ho a provar."; "wallet__payment_request_expired" = "La sol·licitud de pagament ha caducat."; "wallet__payment_request_waiting_for_details" = "Encara s'estan recuperant les dades de pagament. Torna-ho a provar d'aquí a poc."; "wallet__payment_request_in_progress" = "La sol·licitud de pagament ja s'està processant."; @@ -1649,6 +1652,7 @@ "wallet__payment_request_payment_verified" = "Pagament verificat"; "wallet__payment_request_underpaid" = "Rebut · Pagament insuficient"; "wallet__payment_request_after_expiry" = "Rebut · Després del venciment"; +"wallet__payment_request_before_request" = "Rebut · Abans de la sol·licitud"; "pubky_auth__usdt_intro_description" = "Per guanyar, has de compartir amb Paykit la teva adreça USDT. Pot veure l'activitat de vendes, però no pot gastar fons."; "pubky_auth__usdt_intro_optional" = "Pots compartir amb Paykit la teva adreça USDT per rebre guanys. Pot veure l'activitat de vendes, però no pot gastar fons."; "pubky_auth__usdt_unavailable" = "La teva adreça USDT no està disponible. Torna-ho a provar."; diff --git a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings index bf8012df4..a17a829d8 100644 --- a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings @@ -716,6 +716,8 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Vytvořte si na tomto homeserveru novou identitu Pubky. Pokračujte, jen pokud mu důvěřujete."; "pubky_auth__approval_failed" = "Autorizace se nezdařila"; +"pubky_auth__identity_unavailable_desc" = "Zkontrolujte připojení a zkuste to znovu."; +"profile__auth_error_signup_code_rejected" = "Homeserver tento registrační kód nepřijal. Zkuste to znovu nebo použijte jiný registrační odkaz."; "wallet__onchain_abandon_transfer_note" = "Převody na útratový zůstatek už nejsou dostupné. Bitkit nejprve zkontroluje síť a pak vrátí mince tohoto převodu do vaší peněženky, aby se původní převod nikdy nepotvrdil. Stojí to síťový poplatek."; "watch_only_accounts__active_section" = "Aktivní účty"; "watch_only_accounts__copy_xpub" = "Kopírovat xpub"; @@ -1450,6 +1452,7 @@ "wallet__payment_received" = "bitcoin přijatý"; "wallet__payment_request" = "Žádost o platbu"; "wallet__payment_request_unavailable" = "Žádost o platbu již není k dispozici."; +"wallet__payment_request_note_too_long" = "Zkraťte poznámku a zkuste to znovu."; "wallet__payment_request_expired" = "Platnost žádosti o platbu vypršela."; "wallet__payment_request_waiting_for_details" = "Platební údaje se stále obnovují. Zkuste to za chvíli znovu."; "wallet__payment_request_in_progress" = "Žádost o platbu se již zpracovává."; @@ -1789,6 +1792,7 @@ "wallet__payment_request_payment_verified" = "Platba ověřena"; "wallet__payment_request_underpaid" = "Přijato · Nedoplatek"; "wallet__payment_request_after_expiry" = "Přijato · Po vypršení"; +"wallet__payment_request_before_request" = "Přijato · Před žádostí"; "pubky_auth__usdt_intro_description" = "Abyste mohli vydělávat, musíte s Paykit sdílet svou adresu USDT. Může zobrazovat prodejní aktivitu, ale nemůže utrácet prostředky."; "pubky_auth__usdt_intro_optional" = "Svou adresu USDT můžete sdílet s Paykit a přijímat tak výdělky. Může zobrazovat prodejní aktivitu, ale nemůže utrácet prostředky."; "pubky_auth__usdt_unavailable" = "Vaše adresa USDT není k dispozici. Zkuste to prosím znovu."; diff --git a/Bitkit/Resources/Localization/de.lproj/Localizable.strings b/Bitkit/Resources/Localization/de.lproj/Localizable.strings index edaa4d881..b9d971d7e 100644 --- a/Bitkit/Resources/Localization/de.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/de.lproj/Localizable.strings @@ -714,6 +714,8 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Erstelle eine neue Pubky-Identität auf diesem Homeserver. Fahre nur fort, wenn du ihm vertraust."; "pubky_auth__approval_failed" = "Autorisierung fehlgeschlagen"; +"pubky_auth__identity_unavailable_desc" = "Überprüfe deine Verbindung und versuche es erneut."; +"profile__auth_error_signup_code_rejected" = "Der Homeserver hat diesen Registrierungscode nicht akzeptiert. Versuche es erneut oder verwende einen anderen Registrierungslink."; "wallet__onchain_abandon_transfer_note" = "Überweisungen auf dein Ausgabenguthaben sind nicht mehr verfügbar. Bitkit prüft zuerst das Netzwerk und schickt die Coins dieser Überweisung dann zurück in deine Wallet, damit die ursprüngliche Überweisung nie bestätigt wird. Dafür fällt eine Netzwerkgebühr an."; "watch_only_accounts__active_section" = "Aktive Konten"; "watch_only_accounts__copy_xpub" = "xpub kopieren"; @@ -1448,6 +1450,7 @@ "wallet__payment_received" = "Bitcoin empfangen"; "wallet__payment_request" = "Zahlungsanforderung"; "wallet__payment_request_unavailable" = "Die Zahlungsanforderung ist nicht mehr verfügbar."; +"wallet__payment_request_note_too_long" = "Kürze die Notiz und versuche es erneut."; "wallet__payment_request_expired" = "Die Zahlungsanforderung ist abgelaufen."; "wallet__payment_request_waiting_for_details" = "Die Zahlungsdetails werden noch wiederhergestellt. Versuche es in Kürze erneut."; "wallet__payment_request_in_progress" = "Die Zahlungsanforderung wird bereits bearbeitet."; @@ -1786,6 +1789,7 @@ "wallet__payment_request_payment_verified" = "Zahlung verifiziert"; "wallet__payment_request_underpaid" = "Empfangen · Unterbezahlt"; "wallet__payment_request_after_expiry" = "Empfangen · Nach Ablauf"; +"wallet__payment_request_before_request" = "Empfangen · Vor der Anforderung"; "pubky_auth__usdt_intro_description" = "Um zu verdienen, musst du deine USDT-Adresse mit Paykit teilen. Paykit kann Verkaufsaktivitäten sehen, aber kein Guthaben ausgeben."; "pubky_auth__usdt_intro_optional" = "Du kannst deine USDT-Adresse mit Paykit teilen, um Einnahmen zu empfangen. Paykit kann Verkaufsaktivitäten sehen, aber kein Guthaben ausgeben."; "pubky_auth__usdt_unavailable" = "Deine USDT-Adresse ist nicht verfügbar. Bitte versuche es erneut."; diff --git a/Bitkit/Resources/Localization/el.lproj/Localizable.strings b/Bitkit/Resources/Localization/el.lproj/Localizable.strings index 0e22742d1..e2d85e3ea 100644 --- a/Bitkit/Resources/Localization/el.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/el.lproj/Localizable.strings @@ -603,6 +603,8 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Δημιούργησε μια νέα ταυτότητα Pubky σε αυτόν τον homeserver. Συνέχισε μόνο αν τον εμπιστεύεσαι."; "pubky_auth__approval_failed" = "Η Εξουσιοδότηση Απέτυχε"; +"pubky_auth__identity_unavailable_desc" = "Έλεγξε τη σύνδεσή σου και δοκίμασε ξανά."; +"profile__auth_error_signup_code_rejected" = "Ο homeserver δεν αποδέχτηκε αυτόν τον κωδικό εγγραφής. Δοκίμασε ξανά ή χρησιμοποίησε άλλο σύνδεσμο εγγραφής."; "wallet__onchain_abandon_transfer_note" = "Οι μεταφορές στο υπόλοιπο δαπανών δεν είναι πλέον διαθέσιμες. Το Bitkit ελέγχει πρώτα το δίκτυο και μετά επιστρέφει τα νομίσματα αυτής της μεταφοράς στο πορτοφόλι σας, ώστε η αρχική μεταφορά να μην επιβεβαιωθεί ποτέ. Αυτό έχει χρέωση δικτύου."; "watch_only_accounts__active_section" = "Ενεργοί λογαριασμοί"; "watch_only_accounts__copy_xpub" = "Αντιγραφή xpub"; @@ -1199,6 +1201,7 @@ "wallet__payment_received" = "Bitcoin Λήφθηκε"; "wallet__payment_request" = "Αίτημα πληρωμής"; "wallet__payment_request_unavailable" = "Το αίτημα πληρωμής δεν είναι πλέον διαθέσιμο."; +"wallet__payment_request_note_too_long" = "Συντόμευσε τη σημείωση και δοκίμασε ξανά."; "wallet__payment_request_expired" = "Το αίτημα πληρωμής έληξε."; "wallet__payment_request_waiting_for_details" = "Τα στοιχεία πληρωμής ανακτώνται ακόμα. Δοκιμάστε ξανά σε λίγο."; "wallet__payment_request_in_progress" = "Το αίτημα πληρωμής βρίσκεται ήδη σε επεξεργασία."; @@ -1520,6 +1523,7 @@ "wallet__payment_request_payment_verified" = "Η πληρωμή επαληθεύτηκε"; "wallet__payment_request_underpaid" = "Ελήφθη · Ελλιπής πληρωμή"; "wallet__payment_request_after_expiry" = "Ελήφθη · Μετά τη λήξη"; +"wallet__payment_request_before_request" = "Ελήφθη · Πριν από το αίτημα"; "pubky_auth__usdt_intro_description" = "Για να κερδίζετε, πρέπει να μοιραστείτε τη διεύθυνση USDT σας με το Paykit. Μπορεί να βλέπει τη δραστηριότητα πωλήσεων, αλλά δεν μπορεί να ξοδέψει κεφάλαια."; "pubky_auth__usdt_intro_optional" = "Μπορείτε να μοιραστείτε τη διεύθυνση USDT σας με το Paykit για να λαμβάνετε τα κέρδη σας. Μπορεί να βλέπει τη δραστηριότητα πωλήσεων, αλλά δεν μπορεί να ξοδέψει κεφάλαια."; "pubky_auth__usdt_unavailable" = "Η διεύθυνση USDT σας δεν είναι διαθέσιμη. Δοκιμάστε ξανά."; diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 84b738a51..b836146a5 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -704,6 +704,8 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Create a new Pubky identity on this homeserver. Only continue if you trust it."; "pubky_auth__approval_failed" = "Authorization Failed"; +"pubky_auth__identity_unavailable_desc" = "Check your connection and try again."; +"profile__auth_error_signup_code_rejected" = "The homeserver did not accept this signup code. Try again or use another signup link."; "watch_only_accounts__active_section" = "Active accounts"; "watch_only_accounts__copy_xpub" = "Copy xpub"; "watch_only_accounts__description" = "Each approved Paykit server gets a separate Bitcoin account. Turn tracking off to unload an account without deleting its wallet history."; @@ -1462,6 +1464,7 @@ "wallet__payment_received" = "Received Bitcoin"; "wallet__payment_request" = "Payment Request"; "wallet__payment_request_unavailable" = "The payment request is no longer available."; +"wallet__payment_request_note_too_long" = "Shorten the note and try again."; "wallet__payment_request_expired" = "The payment request has expired."; "wallet__payment_request_waiting_for_details" = "Payment details are still being recovered. Try again shortly."; "wallet__payment_request_in_progress" = "The payment request is already being processed."; @@ -1907,6 +1910,7 @@ "wallet__payment_request_underpaid" = "Received · Underpaid"; "wallet__payment_request_after_expiry" = "Received · After expiry"; +"wallet__payment_request_before_request" = "Received · Before request"; "pubky_auth__usdt_intro_description" = "To earn, you need to share your USDT address with Paykit. It can view sales activity, but cannot spend funds."; "pubky_auth__usdt_intro_optional" = "You can share your USDT address with Paykit to receive earnings. It can view sales activity, but cannot spend funds."; diff --git a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings index 0b954a90f..984b032f9 100644 --- a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings @@ -717,6 +717,8 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Cree una nueva identidad Pubky en este homeserver. Continúe solo si confía en él."; "pubky_auth__approval_failed" = "Falló la autorización"; +"pubky_auth__identity_unavailable_desc" = "Revise su conexión y vuelva a intentarlo."; +"profile__auth_error_signup_code_rejected" = "El homeserver no aceptó este código de registro. Vuelva a intentarlo o use otro enlace de registro."; "wallet__onchain_abandon_transfer_note" = "Las transferencias al saldo de gasto ya no están disponibles. Bitkit primero revisa la red y luego devuelve las monedas de esta transferencia a tu billetera para que la transferencia original nunca se confirme. Esto tiene una comisión de red."; "watch_only_accounts__active_section" = "Cuentas activas"; "watch_only_accounts__copy_xpub" = "Copiar xpub"; @@ -1453,6 +1455,7 @@ "wallet__payment_received" = "Bitcoin recibido"; "wallet__payment_request" = "Solicitud de pago"; "wallet__payment_request_unavailable" = "La solicitud de pago ya no está disponible."; +"wallet__payment_request_note_too_long" = "Acorte la nota y vuelva a intentarlo."; "wallet__payment_request_expired" = "La solicitud de pago venció."; "wallet__payment_request_waiting_for_details" = "Los datos de pago aún se están recuperando. Vuelva a intentarlo en breve."; "wallet__payment_request_in_progress" = "La solicitud de pago ya se está procesando."; @@ -1792,6 +1795,7 @@ "wallet__payment_request_payment_verified" = "Pago verificado"; "wallet__payment_request_underpaid" = "Recibido · Pago insuficiente"; "wallet__payment_request_after_expiry" = "Recibido · Tras vencer"; +"wallet__payment_request_before_request" = "Recibido · Antes de la solicitud"; "pubky_auth__usdt_intro_description" = "Para ganar, debe compartir su dirección USDT con Paykit. Puede ver la actividad de ventas, pero no puede gastar fondos."; "pubky_auth__usdt_intro_optional" = "Puede compartir su dirección USDT con Paykit para recibir sus ganancias. Paykit podrá ver la actividad de ventas, pero no gastar fondos."; "pubky_auth__usdt_unavailable" = "Su dirección USDT no está disponible. Vuelva a intentarlo."; diff --git a/Bitkit/Resources/Localization/es.lproj/Localizable.strings b/Bitkit/Resources/Localization/es.lproj/Localizable.strings index 2e7d27918..95a81cf15 100644 --- a/Bitkit/Resources/Localization/es.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es.lproj/Localizable.strings @@ -664,6 +664,8 @@ "pubky_auth__homeserver" = "Servidor principal"; "pubky_auth__signup_description" = "Cree una nueva identidad Pubky en este servidor principal. Continúe solo si confía en él."; "pubky_auth__approval_failed" = "Error de autorización"; +"pubky_auth__identity_unavailable_desc" = "Compruebe su conexión e inténtelo de nuevo."; +"profile__auth_error_signup_code_rejected" = "El servidor principal no ha aceptado este código de registro. Inténtelo de nuevo o use otro enlace de registro."; "wallet__onchain_abandon_transfer_note" = "Las transferencias al saldo de gasto ya no están disponibles. Bitkit primero comprueba la red y después devuelve las monedas de esta transferencia a tu billetera para que la transferencia original nunca se confirme. Esto tiene una comisión de red."; "watch_only_accounts__active_section" = "Cuentas activas"; "watch_only_accounts__copy_xpub" = "Copiar xpub"; @@ -1346,6 +1348,7 @@ "wallet__payment_received" = "Bitcoin recibido"; "wallet__payment_request" = "Solicitud de pago"; "wallet__payment_request_unavailable" = "La solicitud de pago ya no está disponible."; +"wallet__payment_request_note_too_long" = "Acorte la nota e inténtelo de nuevo."; "wallet__payment_request_expired" = "La solicitud de pago ha caducado."; "wallet__payment_request_waiting_for_details" = "Los datos de pago aún se están recuperando. Inténtelo de nuevo en breve."; "wallet__payment_request_in_progress" = "La solicitud de pago ya se está procesando."; @@ -1670,6 +1673,7 @@ "wallet__payment_request_payment_verified" = "Pago verificado"; "wallet__payment_request_underpaid" = "Recibido · Pago insuficiente"; "wallet__payment_request_after_expiry" = "Recibido · Tras caducar"; +"wallet__payment_request_before_request" = "Recibido · Antes de la solicitud"; "pubky_auth__usdt_intro_description" = "Para ganar, necesita compartir con Paykit su dirección USDT. Puede ver la actividad de ventas, pero no puede gastar fondos."; "pubky_auth__usdt_intro_optional" = "Puede compartir con Paykit su dirección USDT para recibir sus ganancias. Paykit podrá ver la actividad de ventas, pero no gastar fondos."; "pubky_auth__usdt_unavailable" = "Su dirección USDT no está disponible. Inténtelo de nuevo."; diff --git a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings index 4fac9e1da..80af76841 100644 --- a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings @@ -717,6 +717,8 @@ "pubky_auth__homeserver" = "Serveur domestique"; "pubky_auth__signup_description" = "Créez une nouvelle identité Pubky sur ce serveur domestique. Ne continuez que si vous lui faites confiance."; "pubky_auth__approval_failed" = "Échec de l'autorisation"; +"pubky_auth__identity_unavailable_desc" = "Vérifiez votre connexion et réessayez."; +"profile__auth_error_signup_code_rejected" = "Le serveur domestique n'a pas accepté ce code d'inscription. Réessayez ou utilisez un autre lien d'inscription."; "wallet__onchain_abandon_transfer_note" = "Les transferts vers le solde de dépenses ne sont plus disponibles. Bitkit vérifie d'abord le réseau, puis renvoie les pièces de ce transfert vers votre portefeuille afin que le transfert d'origine ne soit jamais confirmé. Cela coûte des frais de réseau."; "watch_only_accounts__active_section" = "Comptes actifs"; "watch_only_accounts__copy_xpub" = "Copier la xpub"; @@ -1461,6 +1463,7 @@ "wallet__payment_received" = "Bitcoin reçus"; "wallet__payment_request" = "Demande de paiement"; "wallet__payment_request_unavailable" = "La demande de paiement n'est plus disponible."; +"wallet__payment_request_note_too_long" = "Raccourcissez la note et réessayez."; "wallet__payment_request_expired" = "La demande de paiement a expiré."; "wallet__payment_request_waiting_for_details" = "Les informations de paiement sont encore en cours de récupération. Réessayez sous peu."; "wallet__payment_request_in_progress" = "La demande de paiement est déjà en cours de traitement."; @@ -1800,6 +1803,7 @@ "wallet__payment_request_payment_verified" = "Paiement vérifié"; "wallet__payment_request_underpaid" = "Reçu · Insuffisant"; "wallet__payment_request_after_expiry" = "Reçu · Après expiration"; +"wallet__payment_request_before_request" = "Reçu · Avant la demande"; "pubky_auth__usdt_intro_description" = "Pour gagner, vous devez partager votre adresse USDT avec Paykit. Il peut voir l'activité des ventes, mais ne peut pas dépenser de fonds."; "pubky_auth__usdt_intro_optional" = "Vous pouvez partager votre adresse USDT avec Paykit pour recevoir vos gains. Il peut voir l'activité des ventes, mais ne peut pas dépenser de fonds."; "pubky_auth__usdt_unavailable" = "Votre adresse USDT est indisponible. Veuillez réessayer."; diff --git a/Bitkit/Resources/Localization/it.lproj/Localizable.strings b/Bitkit/Resources/Localization/it.lproj/Localizable.strings index 43eefe7f5..c65484054 100644 --- a/Bitkit/Resources/Localization/it.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/it.lproj/Localizable.strings @@ -697,6 +697,8 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Crea una nuova identità Pubky su questo homeserver. Continua solo se ti fidi."; "pubky_auth__approval_failed" = "Autorizzazione Non Riuscita"; +"pubky_auth__identity_unavailable_desc" = "Controlla la connessione e riprova."; +"profile__auth_error_signup_code_rejected" = "L'homeserver non ha accettato questo codice di registrazione. Riprova o usa un altro link di registrazione."; "wallet__onchain_abandon_transfer_note" = "I trasferimenti al saldo di spesa non sono più disponibili. Bitkit controlla prima la rete, poi rimanda le monete di questo trasferimento al tuo wallet, così il trasferimento originale non potrà mai essere confermato. Questo costa una commissione di rete."; "watch_only_accounts__active_section" = "Account attivi"; "watch_only_accounts__copy_xpub" = "Copia xpub"; @@ -1423,6 +1425,7 @@ "wallet__payment_received" = "Bitcoin ricevuti"; "wallet__payment_request" = "Richiesta di pagamento"; "wallet__payment_request_unavailable" = "La richiesta di pagamento non è più disponibile."; +"wallet__payment_request_note_too_long" = "Accorcia la nota e riprova."; "wallet__payment_request_expired" = "La richiesta di pagamento è scaduta."; "wallet__payment_request_waiting_for_details" = "I dati di pagamento sono ancora in fase di recupero. Riprova a breve."; "wallet__payment_request_in_progress" = "La richiesta di pagamento è già in elaborazione."; @@ -1759,6 +1762,7 @@ "wallet__payment_request_payment_verified" = "Pagamento verificato"; "wallet__payment_request_underpaid" = "Ricevuto · Insufficiente"; "wallet__payment_request_after_expiry" = "Ricevuto · Dopo la scadenza"; +"wallet__payment_request_before_request" = "Ricevuto · Prima della richiesta"; "pubky_auth__usdt_intro_description" = "Per guadagnare, devi condividere con Paykit il tuo indirizzo USDT. Può vedere l'attività di vendita, ma non può spendere fondi."; "pubky_auth__usdt_intro_optional" = "Puoi condividere con Paykit il tuo indirizzo USDT per ricevere i guadagni. Può vedere l'attività di vendita, ma non può spendere fondi."; "pubky_auth__usdt_unavailable" = "Il tuo indirizzo USDT non è disponibile. Riprova."; diff --git a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings index 2b892c34d..05af3c534 100644 --- a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings @@ -714,6 +714,8 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Maak een nieuwe Pubky-identiteit aan op deze homeserver. Ga alleen verder als u deze vertrouwt."; "pubky_auth__approval_failed" = "Autorisatie mislukt"; +"pubky_auth__identity_unavailable_desc" = "Controleer uw verbinding en probeer het opnieuw."; +"profile__auth_error_signup_code_rejected" = "De homeserver heeft deze aanmeldcode niet geaccepteerd. Probeer het opnieuw of gebruik een andere aanmeldlink."; "wallet__onchain_abandon_transfer_note" = "Overboekingen naar je bestedingssaldo zijn niet meer beschikbaar. Bitkit controleert eerst het netwerk en stuurt de munten van deze overboeking dan terug naar je wallet, zodat de oorspronkelijke overboeking nooit bevestigd wordt. Dit kost netwerkkosten."; "watch_only_accounts__active_section" = "Actieve accounts"; "watch_only_accounts__copy_xpub" = "xpub kopiëren"; @@ -1456,6 +1458,7 @@ "wallet__payment_received" = "Bitcoin Ontvangen"; "wallet__payment_request" = "Betaalverzoek"; "wallet__payment_request_unavailable" = "Het betaalverzoek is niet meer beschikbaar."; +"wallet__payment_request_note_too_long" = "Maak de opmerking korter en probeer het opnieuw."; "wallet__payment_request_expired" = "Het betaalverzoek is verlopen."; "wallet__payment_request_waiting_for_details" = "Betaalgegevens worden nog hersteld. Probeer het zo opnieuw."; "wallet__payment_request_in_progress" = "Het betaalverzoek wordt al verwerkt."; @@ -1794,6 +1797,7 @@ "wallet__payment_request_payment_verified" = "Betaling geverifieerd"; "wallet__payment_request_underpaid" = "Ontvangen · Onderbetaald"; "wallet__payment_request_after_expiry" = "Ontvangen · Na vervaldatum"; +"wallet__payment_request_before_request" = "Ontvangen · Vóór het verzoek"; "pubky_auth__usdt_intro_description" = "Om te verdienen, moet u uw USDT-adres delen met Paykit. Dit kan verkoopactiviteit zien, maar kan geen geld uitgeven."; "pubky_auth__usdt_intro_optional" = "U kunt uw USDT-adres delen met Paykit om inkomsten te ontvangen. Dit kan verkoopactiviteit zien, maar kan geen geld uitgeven."; "pubky_auth__usdt_unavailable" = "Uw USDT-adres is niet beschikbaar. Probeer het opnieuw."; diff --git a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings index 4515ca72e..17f8568a8 100644 --- a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings @@ -716,6 +716,8 @@ "pubky_auth__homeserver" = "Serwer domowy"; "pubky_auth__signup_description" = "Utwórz nową tożsamość Pubky na tym serwerze domowym. Kontynuuj tylko, jeśli mu ufasz."; "pubky_auth__approval_failed" = "Autoryzacja nie powiodła się"; +"pubky_auth__identity_unavailable_desc" = "Sprawdź połączenie i spróbuj ponownie."; +"profile__auth_error_signup_code_rejected" = "Serwer domowy nie zaakceptował tego kodu rejestracyjnego. Spróbuj ponownie lub użyj innego linku rejestracyjnego."; "wallet__onchain_abandon_transfer_note" = "Przelewy na saldo wydatków nie są już dostępne. Bitkit najpierw sprawdza sieć, a potem odsyła monety z tego przelewu do Twojego portfela, aby pierwotny przelew nigdy nie został potwierdzony. Wiąże się to z opłatą sieciową."; "watch_only_accounts__active_section" = "Aktywne konta"; "watch_only_accounts__copy_xpub" = "Kopiuj xpub"; @@ -1460,6 +1462,7 @@ "wallet__payment_received" = "Otrzymano Bitcoinów"; "wallet__payment_request" = "Żądanie płatności"; "wallet__payment_request_unavailable" = "Żądanie płatności nie jest już dostępne."; +"wallet__payment_request_note_too_long" = "Skróć notatkę i spróbuj ponownie."; "wallet__payment_request_expired" = "Żądanie płatności wygasło."; "wallet__payment_request_waiting_for_details" = "Dane płatności są wciąż odzyskiwane. Spróbuj ponownie za chwilę."; "wallet__payment_request_in_progress" = "Żądanie płatności jest już przetwarzane."; @@ -1799,6 +1802,7 @@ "wallet__payment_request_payment_verified" = "Płatność zweryfikowana"; "wallet__payment_request_underpaid" = "Otrzymano · Niedopłata"; "wallet__payment_request_after_expiry" = "Otrzymano · Po terminie"; +"wallet__payment_request_before_request" = "Otrzymano · Przed żądaniem"; "pubky_auth__usdt_intro_description" = "Aby zarabiać, musisz udostępnić Paykit swój adres USDT. Paykit może wyświetlać aktywność sprzedaży, ale nie może wydawać środków."; "pubky_auth__usdt_intro_optional" = "Możesz udostępnić Paykit swój adres USDT, aby otrzymywać zarobki. Paykit może wyświetlać aktywność sprzedaży, ale nie może wydawać środków."; "pubky_auth__usdt_unavailable" = "Twój adres USDT jest niedostępny. Spróbuj ponownie."; diff --git a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings index 488220de6..4d94e061e 100644 --- a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -716,6 +716,8 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Crie uma nova identidade Pubky neste homeserver. Só continue se você confiar nele."; "pubky_auth__approval_failed" = "Falha na Autorização"; +"pubky_auth__identity_unavailable_desc" = "Verifique sua conexão e tente novamente."; +"profile__auth_error_signup_code_rejected" = "O homeserver não aceitou este código de cadastro. Tente novamente ou use outro link de cadastro."; "wallet__onchain_abandon_transfer_note" = "As transferências para o saldo de gastos não estão mais disponíveis. O Bitkit primeiro verifica a rede e depois devolve as moedas desta transferência para sua carteira, para que a transferência original nunca seja confirmada. Isso tem uma taxa de rede."; "watch_only_accounts__active_section" = "Contas ativas"; "watch_only_accounts__copy_xpub" = "Copiar xpub"; @@ -1461,6 +1463,7 @@ "wallet__payment_received" = "Bitcoin Recebido"; "wallet__payment_request" = "Solicitação de pagamento"; "wallet__payment_request_unavailable" = "A solicitação de pagamento não está mais disponível."; +"wallet__payment_request_note_too_long" = "Encurte a nota e tente novamente."; "wallet__payment_request_expired" = "A solicitação de pagamento expirou."; "wallet__payment_request_waiting_for_details" = "Os dados de pagamento ainda estão sendo recuperados. Tente novamente em breve."; "wallet__payment_request_in_progress" = "A solicitação de pagamento já está sendo processada."; @@ -1800,6 +1803,7 @@ "wallet__payment_request_payment_verified" = "Pagamento verificado"; "wallet__payment_request_underpaid" = "Recebido · Pago a menor"; "wallet__payment_request_after_expiry" = "Recebido · Após o vencimento"; +"wallet__payment_request_before_request" = "Recebido · Antes da solicitação"; "pubky_auth__usdt_intro_description" = "Para ganhar, você precisa compartilhar seu endereço USDT com o Paykit. Ele pode ver a atividade de vendas, mas não pode gastar fundos."; "pubky_auth__usdt_intro_optional" = "Você pode compartilhar seu endereço USDT com o Paykit para receber ganhos. Ele pode ver a atividade de vendas, mas não pode gastar fundos."; "pubky_auth__usdt_unavailable" = "Seu endereço USDT não está disponível. Tente novamente."; diff --git a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings index c860dd592..779a1e395 100644 --- a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings @@ -620,6 +620,8 @@ "pubky_auth__homeserver" = "Homeserver"; "pubky_auth__signup_description" = "Crie uma nova identidade Pubky neste homeserver. Continue apenas se confiar nele."; "pubky_auth__approval_failed" = "Falha na autorização"; +"pubky_auth__identity_unavailable_desc" = "Verifique a ligação e tente novamente."; +"profile__auth_error_signup_code_rejected" = "O homeserver não aceitou este código de registo. Tente novamente ou utilize outra ligação de registo."; "watch_only_accounts__active_section" = "Contas ativas"; "watch_only_accounts__copy_xpub" = "Copiar xpub"; "watch_only_accounts__description" = "Cada servidor Paykit aprovado recebe uma conta Bitcoin separada. Desative o acompanhamento para descarregar uma conta sem eliminar o histórico da carteira."; @@ -1155,6 +1157,7 @@ "wallet__boost_swipe" = "Arraste para acelerar"; "wallet__payment_received" = "Bitcoin Recebido"; "wallet__payment_request_unavailable" = "O pedido de pagamento já não está disponível."; +"wallet__payment_request_note_too_long" = "Encurte a nota e tente novamente."; "wallet__payment_request_expired" = "O pedido de pagamento expirou."; "wallet__payment_request_waiting_for_details" = "Os dados de pagamento ainda estão a ser recuperados. Tente novamente daqui a pouco."; "wallet__payment_request_in_progress" = "O pedido de pagamento já está a ser processado."; @@ -1446,6 +1449,7 @@ "wallet__payment_request_payment_verified" = "Pagamento verificado"; "wallet__payment_request_underpaid" = "Recebido · Pago a menos"; "wallet__payment_request_after_expiry" = "Recebido · Após expirar"; +"wallet__payment_request_before_request" = "Recebido · Antes do pedido"; "pubky_auth__usdt_intro_description" = "Para ganhar, tem de partilhar o seu endereço USDT com o Paykit. Este pode ver a atividade de vendas, mas não pode gastar fundos."; "pubky_auth__usdt_intro_optional" = "Pode partilhar o seu endereço USDT com o Paykit para receber ganhos. Este pode ver a atividade de vendas, mas não pode gastar fundos."; "pubky_auth__usdt_unavailable" = "O seu endereço USDT não está disponível. Tente novamente."; diff --git a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings index 59b25a886..077f03cc4 100644 --- a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings @@ -711,6 +711,8 @@ "pubky_auth__homeserver" = "Домашний сервер"; "pubky_auth__signup_description" = "Создайте новый идентификатор Pubky на этом домашнем сервере. Продолжайте, только если доверяете ему."; "pubky_auth__approval_failed" = "Ошибка Авторизации"; +"pubky_auth__identity_unavailable_desc" = "Проверьте подключение и попробуйте снова."; +"profile__auth_error_signup_code_rejected" = "Домашний сервер не принял этот код регистрации. Попробуйте снова или используйте другую ссылку для регистрации."; "wallet__onchain_abandon_transfer_note" = "Переводы на баланс расходов больше недоступны. Bitkit сначала проверяет сеть, а затем возвращает монеты этого перевода в ваш кошелёк, чтобы исходный перевод никогда не подтвердился. За это взимается сетевая комиссия."; "watch_only_accounts__active_section" = "Активные аккаунты"; "watch_only_accounts__copy_xpub" = "Копировать xpub"; @@ -1444,6 +1446,7 @@ "wallet__payment_received" = "Получен Биткоин"; "wallet__payment_request" = "Запрос на оплату"; "wallet__payment_request_unavailable" = "Запрос на оплату больше недоступен."; +"wallet__payment_request_note_too_long" = "Сократите заметку и попробуйте снова."; "wallet__payment_request_expired" = "Срок действия запроса на оплату истёк."; "wallet__payment_request_waiting_for_details" = "Платёжные данные ещё восстанавливаются. Попробуйте чуть позже."; "wallet__payment_request_in_progress" = "Запрос на оплату уже обрабатывается."; @@ -1780,6 +1783,7 @@ "wallet__payment_request_payment_verified" = "Платёж подтверждён"; "wallet__payment_request_underpaid" = "Получено · Недоплата"; "wallet__payment_request_after_expiry" = "Получено · После истечения"; +"wallet__payment_request_before_request" = "Получено · До запроса"; "pubky_auth__usdt_intro_description" = "Чтобы зарабатывать, нужно предоставить Paykit ваш адрес USDT. Paykit может видеть активность продаж, но не может тратить средства."; "pubky_auth__usdt_intro_optional" = "Вы можете предоставить Paykit ваш адрес USDT, чтобы получать заработок. Paykit может видеть активность продаж, но не может тратить средства."; "pubky_auth__usdt_unavailable" = "Ваш адрес USDT недоступен. Попробуйте ещё раз."; diff --git a/Bitkit/Services/PaykitAllowanceExecutor.swift b/Bitkit/Services/PaykitAllowanceExecutor.swift index 3a743e6a9..7bcede912 100644 --- a/Bitkit/Services/PaykitAllowanceExecutor.swift +++ b/Bitkit/Services/PaykitAllowanceExecutor.swift @@ -1050,6 +1050,20 @@ actor PaykitAllowanceExecutor { let endpointIdentifier = payment.endpoint.methodId.rawValue let paymentAppId = try payment.context.paymentAppId(for: endpointIdentifier) + do { + try PaykitPaymentProofSize.validate( + paymentReference: request.paymentReference, + paymentAppId: paymentAppId, + paymentEndpointIdentifier: endpointIdentifier, + billingPeriod: request.billingPeriod?.sdkValue, + allowanceId: candidate.allowanceId, + conversionQuoteId: try? request.payment(using: payment.endpoint.methodId, at: now()).quoteId, + kind: .lightning + ) + } catch { + Logger.info("An incoming request's payment proof cannot be sent in one message; it stays on the manual flow", context: "PaykitAllowance") + return .manual + } // The SDK requires this app to hold the shared execution claim before it queues an automatic Acceptance. try await payer.claimForExecution(request) let recordedAcceptanceIntent = try await payer.recordAcceptanceIntent(request, identity: identity) diff --git a/Bitkit/Services/PaykitPaymentProofService.swift b/Bitkit/Services/PaykitPaymentProofService.swift index 94037be5f..3b74a2d55 100644 --- a/Bitkit/Services/PaykitPaymentProofService.swift +++ b/Bitkit/Services/PaykitPaymentProofService.swift @@ -16,6 +16,61 @@ enum PaykitPaymentProofKind: String, Codable { } } +/// Checks that the Payment Proof Event Message a payment would send fits one Paykit message, so a request whose proof +/// could never be delivered is refused before anything is paid. +enum PaykitPaymentProofSize { + /// The largest log index an ERC-20 receipt can carry, the 78-digit maximum of a uint256. + static let longestUsdtLogIndex = "115792089237316195423570985008687907853269984665640564039457584007913129639935" + + /// The longest proof each rail produces: a 32-byte preimage or txid, or an ERC-20 proof with the largest log index. + static func placeholderProof(for kind: PaykitPaymentProofKind) throws -> String { + let values: [String: String] = switch kind { + case .lightning, .onchain: + ["data": String(repeating: "0", count: 64), "type": kind.rawValue] + case .usdt: + [ + "type": kind.rawValue, + "chain_id": PaykitUsdt.chainId, + "transaction_hash": "0x" + String(repeating: "0", count: 64), + "receipt_log_index": longestUsdtLogIndex, + "signature": "0x" + String(repeating: "0", count: 130), + ] + } + return try String(decoding: JSONSerialization.data(withJSONObject: values, options: [.sortedKeys]), as: UTF8.self) + } + + static func validate( + paymentReference: String, + paymentAppId: String, + paymentEndpointIdentifier: String, + billingPeriod: Paykit.BillingPeriod?, + allowanceId: String?, + conversionQuoteId: String?, + kind: PaykitPaymentProofKind + ) throws { + let length: UInt32 + do { + length = try Paykit.paymentProofEventLen( + localAppId: PaykitSdkService.appId, + paymentReference: Paykit.PaymentReference(text: paymentReference), + submission: Paykit.PaymentProofSubmission( + billingPeriod: billingPeriod, + paymentAppId: paymentAppId, + paymentEndpointIdentifier: paymentEndpointIdentifier, + allowanceId: allowanceId, + conversionQuoteId: conversionQuoteId, + proof: Paykit.PrivateJsonObject(text: placeholderProof(for: kind)) + ) + ) + } catch { + throw PaykitPaymentRequestError.requestUnavailable + } + guard Int(length) <= PaykitSdkService.maximumMessageBytes else { + throw PaykitPaymentRequestError.requestUnavailable + } + } +} + struct PaykitOnchainPaymentResolution: Hashable { let identity: String let requestId: PaykitPaymentRequest.ID @@ -394,6 +449,15 @@ actor PaykitPaymentProofService { proof.onchainAmountSats = payment.amount.atomic } proof.allowanceId = allowanceId + try PaykitPaymentProofSize.validate( + paymentReference: request.paymentReference, + paymentAppId: proof.paymentAppId, + paymentEndpointIdentifier: proof.paymentEndpointIdentifier, + billingPeriod: proof.billingPeriod?.sdkValue, + allowanceId: proof.allowanceId, + conversionQuoteId: proof.conversionQuoteId, + kind: kind + ) try await mutationLock.withLock { try await prepareLocked(request: request, proof: proof) } } diff --git a/Bitkit/Services/PaykitPaymentRequestService.swift b/Bitkit/Services/PaykitPaymentRequestService.swift index 4a319d888..723ad1cbd 100644 --- a/Bitkit/Services/PaykitPaymentRequestService.swift +++ b/Bitkit/Services/PaykitPaymentRequestService.swift @@ -595,6 +595,7 @@ enum PaykitPaymentRequestError: LocalizedError, Equatable { case operationInProgress case amountMismatch case subscriptionTooLong + case noteTooLong var errorDescription: String? { switch self { @@ -608,6 +609,8 @@ enum PaykitPaymentRequestError: LocalizedError, Equatable { t("wallet__payment_request_mismatch") case .subscriptionTooLong: t("subscriptions__content_too_long") + case .noteTooLong: + t("wallet__payment_request_note_too_long") } } } @@ -907,10 +910,11 @@ struct PaykitPaymentRequestService { paymentDeadline: .at(timestamp: Self.timestamp(draft.expiresAt)), metadata: Paykit.PrivateJsonObject(text: metadataText) ) - let record = try await sdk.proposePaymentRequest( - counterparty: target.publicKey, + let record = try await proposePaymentRequest( + to: target, terms: terms, - expectedIdentity: expectedIdentity + expectedIdentity: expectedIdentity, + tooLarge: .noteTooLong ) let wasSent = await proposalWasSent(record, to: target.publicKey, expectedIdentity: expectedIdentity) let deliveryStatus = wasSent ? PaykitPaymentRequest.DeliveryStatus.sent : .queued @@ -991,10 +995,11 @@ struct PaykitPaymentRequestService { proposalDate: proposalDate ) try PaykitSubscriptionProposal.validate(terms) - let record = try await sdk.proposePaymentRequest( - counterparty: target.publicKey, + let record = try await proposePaymentRequest( + to: target, terms: terms, - expectedIdentity: expectedIdentity + expectedIdentity: expectedIdentity, + tooLarge: .subscriptionTooLong ) let deliveryStatus = await proposalWasSent(record, to: target.publicKey, expectedIdentity: expectedIdentity) ? PaykitPaymentRequest.DeliveryStatus.sent @@ -1005,6 +1010,22 @@ struct PaykitPaymentRequestService { return subscription } + /// The SDK refuses a proposal that does not fit one Paykit message before queueing anything. + private func proposePaymentRequest( + to target: PaykitPaymentRequestTarget, + terms: Paykit.PaymentRequestTerms, + expectedIdentity: String, + tooLarge: PaykitPaymentRequestError + ) async throws -> Paykit.PaymentRequestRecord { + do { + return try await sdk.proposePaymentRequest(counterparty: target.publicKey, terms: terms, expectedIdentity: expectedIdentity) + } catch let PaykitError.Protocol(code, _) where code == Self.messageTooLargeCode { + throw tooLarge + } + } + + static let messageTooLargeCode = "message_too_large" + private func subscriptionTerms( _ draft: PaykitSubscriptionDraft, iconURI: String?, diff --git a/Bitkit/Services/PaykitUsdtPaymentService.swift b/Bitkit/Services/PaykitUsdtPaymentService.swift index 537954cb2..872608bed 100644 --- a/Bitkit/Services/PaykitUsdtPaymentService.swift +++ b/Bitkit/Services/PaykitUsdtPaymentService.swift @@ -61,11 +61,22 @@ final class PaykitUsdtPaymentService { let receivedAt: Date let underpaid: Bool let afterExpiry: Bool + /// Transferred before this wallet recorded its request, so it does not pay the request. + var beforeRequest: Bool? = false var satisfied: Bool { - verified && !underpaid && !afterExpiry + verified && !underpaid && !afterExpiry && beforeRequest != true } } + /// Arbitrum's sequencer may set a block timestamp up to a day behind real time. + static let transferTimeTolerance: TimeInterval = 24 * 60 * 60 + + /// A transfer is checked against the time this wallet stored its proposal, not the payer's own times. + static func isBeforeRequest(receivedAt: Date, proposalRecordedAt: String?) -> Bool { + guard let recordedAt = proposalRecordedAt.flatMap(PaykitPaymentRequest.parseDate) else { return false } + return receivedAt < recordedAt.addingTimeInterval(-transferTimeTolerance) + } + private struct State: Codable { var attempts: [Attempt] = [] var receipts: [Receipt] = [] @@ -307,26 +318,17 @@ final class PaykitUsdtPaymentService { /// Check the complete wire message, including its outer request fields, before spending funds. static func validateProofSize(binding: UsdtPaymentProofBinding) throws { - let placeholder = Proof(UsdtPaymentProof(chainId: "42161", transactionHash: "0x" + String(repeating: "0", count: 64), - receiptLogIndex: String(repeating: "9", count: 78), - signature: "0x" + String(repeating: "0", count: 130))) - var message: [String: Any] = try [ - "version": 1, "kind": "paykit.payment_proof", "app_id": "bitkit", "event_id": UUID().uuidString.lowercased(), - "payment_request_id": binding.paymentRequestId, "payment_reference": binding.paymentReference, - "payment_app_id": binding.paymentAppId, - "payment_endpoint_identifier": binding.paymentEndpointIdentifier, - "billing_period": NSNull(), "proof": JSONSerialization.jsonObject(with: JSONEncoder().encode(placeholder)), - ] - if !binding.periodStartsAt.isEmpty { - message["billing_period"] = ["starts_at": binding.periodStartsAt, "ends_at": binding.periodEndsAt] - } - if !binding.conversionQuoteId.isEmpty { - message["conversion_quote_id"] = binding.conversionQuoteId - } - guard try JSONSerialization.data(withJSONObject: message, options: [.withoutEscapingSlashes]).count <= PaykitSdkService.maximumMessageBytes - else { - throw PaykitPaymentRequestError.requestUnavailable - } + try PaykitPaymentProofSize.validate( + paymentReference: binding.paymentReference, + paymentAppId: binding.paymentAppId, + paymentEndpointIdentifier: binding.paymentEndpointIdentifier, + billingPeriod: binding.periodStartsAt.isEmpty + ? nil + : Paykit.BillingPeriod(startsAt: binding.periodStartsAt, endsAt: binding.periodEndsAt), + allowanceId: nil, + conversionQuoteId: binding.conversionQuoteId.isEmpty ? nil : binding.conversionQuoteId, + kind: .usdt + ) } func prepare(context: ContactPaymentContext, quote: UsdtQuote, wallet: UsdtWalletManager, @@ -582,7 +584,9 @@ final class PaykitUsdtPaymentService { paymentId: payment.paymentId, proofEventId: submission.eventId, verified: true, transferId: payment.transferId, amount: received, receivedAt: receivedAt, underpaid: received.atomic < terms.amount.atomic, - afterExpiry: !terms.isValid(at: receivedAt)) + afterExpiry: !terms.isValid(at: receivedAt), + beforeRequest: Self.isBeforeRequest(receivedAt: receivedAt, + proposalRecordedAt: record.proposalRecordedAt)) guard !current.receipts.contains(receipt) else { continue } current.receipts.removeAll { $0.wallet == owner && $0.identity == identity && $0.paymentId == payment.paymentId } current.receipts.append(receipt) diff --git a/Bitkit/Services/PubkyService.swift b/Bitkit/Services/PubkyService.swift index 4cc1a2674..9902711a2 100644 --- a/Bitkit/Services/PubkyService.swift +++ b/Bitkit/Services/PubkyService.swift @@ -447,6 +447,8 @@ actor PaykitSdkService { static let shared = PaykitSdkService() /// Maximum plaintext size accepted by Paykit's pubky-noise transport. static let maximumMessageBytes = 1000 + /// The Paykit App ID this SDK sends from. + static let appId = "bitkit" private static let walletBackupDataChangedSubject = PassthroughSubject() nonisolated static var walletBackupDataChangedPublisher: AnyPublisher { @@ -2065,7 +2067,7 @@ actor PaykitSdkService { } private nonisolated static func config() throws -> PaykitSdkConfig { - var config = try Paykit.defaultConfig(appId: "bitkit") + var config = try Paykit.defaultConfig(appId: appId) config.publicContactSharing = .privateOnly return config } diff --git a/Bitkit/Views/PaymentRequests/PaymentRequestsView.swift b/Bitkit/Views/PaymentRequests/PaymentRequestsView.swift index ff0493f44..c17d8ed4c 100644 --- a/Bitkit/Views/PaymentRequests/PaymentRequestsView.swift +++ b/Bitkit/Views/PaymentRequests/PaymentRequestsView.swift @@ -774,6 +774,7 @@ private extension PaykitPaymentRequest { private extension PaykitUsdtPaymentService.Receipt { var statusText: String { let key = !verified ? "wallet__payment_request_status_pending" : + beforeRequest == true ? "wallet__payment_request_before_request" : underpaid ? "wallet__payment_request_underpaid" : afterExpiry ? "wallet__payment_request_after_expiry" : "wallet__payment_request_payment_verified" return t(key) + " · " + usdtExactAmount(amount.atomic) + " USDT" diff --git a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift index ac561374e..5de86a668 100644 --- a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift +++ b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift @@ -1,3 +1,4 @@ +import Paykit import SwiftUI // MARK: - Config & Sheet Item @@ -45,6 +46,18 @@ func pubkyAuthLiteralText(_ value: String) -> String { } } +/// A homeserver that refuses the signup code and a failed connection get their own messages, since retrying is safe for both. +func pubkyAuthApprovalFailureDescription(_ error: Error) -> String { + switch error { + case let PaykitError.Identity(code, _) where code == "signup_code_rejected": + t("profile__auth_error_signup_code_rejected") + case PaykitError.Transport: + t("pubky_auth__identity_unavailable_desc") + default: + error.localizedDescription + } +} + /// Fills each `{placeholder}` from the template only, so request data that itself contains a placeholder stays as it is. func pubkyAuthFilledText(_ template: String, variables: [String: String]) -> String { var result = "" @@ -707,7 +720,7 @@ struct PubkyAuthApprovalSheet: View { } else { Logger.error("Failed to approve pubky auth: \(error)", context: "PubkyAuthApprovalSheet") } - app.toast(type: .error, title: t("pubky_auth__approval_failed"), description: error.localizedDescription) + app.toast(type: .error, title: t("pubky_auth__approval_failed"), description: pubkyAuthApprovalFailureDescription(error)) state = .authorize } } diff --git a/BitkitTests/ContactPaymentsServiceTests.swift b/BitkitTests/ContactPaymentsServiceTests.swift index 0be8dfc8b..63033ef52 100644 --- a/BitkitTests/ContactPaymentsServiceTests.swift +++ b/BitkitTests/ContactPaymentsServiceTests.swift @@ -970,7 +970,7 @@ final class ContactPaymentsServiceTests: XCTestCase { buildEndpoints: { _ in [endpoint] }, syncPaymentLists: { _ in writes.append("private:published") - return PrivatePaymentListDeliveryReport(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return PrivatePaymentListDeliveryReport(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } ) let reachedHeldPoint = expectation(description: "\(testCase.name): publication reached its held point") diff --git a/BitkitTests/HwFundingSignerTests.swift b/BitkitTests/HwFundingSignerTests.swift index ce70534b4..321df22b0 100644 --- a/BitkitTests/HwFundingSignerTests.swift +++ b/BitkitTests/HwFundingSignerTests.swift @@ -24,6 +24,7 @@ final class HwFundingSignerTests: XCTestCase { proposalOutboundStatus: nil, proposalEventId: "650e8400-e29b-41d4-a716-446655440000", proposalAppId: "bitkit", + proposalRecordedAt: nil, payerAppId: nil, executionClaimAppId: nil, terms: PaymentRequestTerms( @@ -40,6 +41,7 @@ final class HwFundingSignerTests: XCTestCase { ), acceptedEventId: nil, acceptedOutboundStatus: nil, + acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, diff --git a/BitkitTests/PaykitAllowanceExecutorTests.swift b/BitkitTests/PaykitAllowanceExecutorTests.swift index 2d6a4fcaf..5a532e8a0 100644 --- a/BitkitTests/PaykitAllowanceExecutorTests.swift +++ b/BitkitTests/PaykitAllowanceExecutorTests.swift @@ -97,6 +97,22 @@ final class PaykitAllowanceExecutorTests: XCTestCase { XCTAssertFalse(isHandling) } + func testRequestWhoseProofCannotFitOneMessageStaysManualWithoutAcceptance() async throws { + let harness = AllowanceHarness() + let request = try Fixtures.paymentRequest(paymentReference: String(repeating: "💜", count: 256)) + + let result = await harness.executor.autoPay(request, allowances: [Fixtures.allowance()], identity: Fixtures.identityKey) + + XCTAssertEqual(result, .manual) + let log = harness.log.entries + XCTAssertTrue(log.contains("resolve")) + XCTAssertFalse(log.contains("claimForExecution")) + XCTAssertFalse(log.contains("acceptPaymentRequestAutomatically")) + XCTAssertFalse(log.contains("payLightning")) + let intents = await harness.payer.acceptanceIntents + XCTAssertTrue(intents.isEmpty) + } + func testRequestWaitsWithoutAcceptanceWhileThePayeesPaymentListIsPending() async throws { let harness = AllowanceHarness() await harness.payer.setResolveError(PaykitAllowanceError.paymentListPending) @@ -690,7 +706,7 @@ final class PaykitAllowanceExecutorTests: XCTestCase { func testACheckedAllowanceTheSdkBlocksStaysManualWithoutLimitReachedWhenItFits() async throws { let harness = AllowanceHarness() - let lowCapId = "allowance-low-cap" + let lowCapId = "10000000-0000-4000-8000-000000000000" await harness.sdk.setCandidates([ AllowanceHarness.candidate(blocked: .sharedRule(code: "no_eligible_endpoint")), Paykit.AllowanceCandidate(allowanceId: lowCapId, eligiblePaymentEndpointIdentifiers: [Fixtures.lightningIdentifier], blocked: nil), diff --git a/BitkitTests/PaykitAllowanceTests.swift b/BitkitTests/PaykitAllowanceTests.swift index 62676a27f..db93b8176 100644 --- a/BitkitTests/PaykitAllowanceTests.swift +++ b/BitkitTests/PaykitAllowanceTests.swift @@ -695,8 +695,8 @@ enum PaykitAllowanceFixtures { static let identityKey = "pubky\(String(repeating: "z", count: 52))" static let counterpartyKey = "pubky\(String(repeating: "y", count: 52))" static let otherCounterpartyKey = "pubky\(String(repeating: "x", count: 52))" - static let walletAllowanceId = "allowance-wallet" - static let otherAllowanceId = "allowance-other" + static let walletAllowanceId = "30000000-0000-4000-8000-000000000000" + static let otherAllowanceId = "20000000-0000-4000-8000-000000000000" static let lightningIdentifier = PublicPaykitService.MethodId.bitcoinLightningBolt11.rawValue static let onchainIdentifier = PublicPaykitService.MethodId.bitcoinOnchainP2wpkh.rawValue static let now = utc("2026-09-24T12:00:00Z") @@ -921,7 +921,8 @@ enum PaykitAllowanceFixtures { asset: String = PaykitIssuerInterop.bitcoinAsset, conversion: Paykit.PaymentConversion? = nil, createdAt: String = "2026-09-24T11:00:00Z", - acceptedEndpoints: [String] = [lightningIdentifier] + acceptedEndpoints: [String] = [lightningIdentifier], + paymentReference: String = "invoice-123" ) throws -> PaykitPaymentRequest { let record = try Paykit.PaymentRequestRecord( counterparty: counterparty, @@ -933,11 +934,12 @@ enum PaykitAllowanceFixtures { proposalOutboundStatus: nil, proposalEventId: "650e8400-e29b-41d4-a716-446655440000", proposalAppId: "bitkit", + proposalRecordedAt: nil, payerAppId: nil, executionClaimAppId: nil, terms: Paykit.PaymentRequestTerms( amount: Paykit.PaymentRequestAmount(value: amount, asset: asset), - paymentReference: Paykit.PaymentReference(text: "invoice-123"), + paymentReference: Paykit.PaymentReference(text: paymentReference), proposalExpiresAt: nil, recurrence: nil, acceptedPaymentEndpointIdentifiers: acceptedEndpoints, @@ -949,6 +951,7 @@ enum PaykitAllowanceFixtures { ), acceptedEventId: nil, acceptedOutboundStatus: nil, + acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, diff --git a/BitkitTests/PaykitContactLifecycleTests.swift b/BitkitTests/PaykitContactLifecycleTests.swift index 4639062c7..a713ca585 100644 --- a/BitkitTests/PaykitContactLifecycleTests.swift +++ b/BitkitTests/PaykitContactLifecycleTests.swift @@ -77,9 +77,9 @@ final class PaykitContactLifecycleTests: XCTestCase { counterparty: sdk.publicKey, paymentRequestId: "550e8400-e29b-41d4-a716-446655440000", localRole: testCase.role, state: .activeRecurring, proposalStreamItemId: nil, proposalOutboundMessageId: nil, proposalOutboundStatus: nil, - proposalEventId: nil, proposalAppId: testCase.appId, + proposalEventId: nil, proposalAppId: testCase.appId, proposalRecordedAt: nil, payerAppId: testCase.role == .payer ? testCase.appId : nil, executionClaimAppId: nil, - terms: terms, acceptedEventId: nil, acceptedOutboundStatus: nil, + terms: terms, acceptedEventId: nil, acceptedOutboundStatus: nil, acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, canceledOutboundStatus: nil, conversionQuotes: [], paymentProofs: [], lastStreamItemId: nil, lastOutboundMessageId: nil, lastOutboundStatus: nil, @@ -128,8 +128,8 @@ final class PaykitContactLifecycleTests: XCTestCase { counterparty: publicKey, paymentRequestId: UUID().uuidString, localRole: .payer, state: .activeRecurring, proposalStreamItemId: nil, proposalOutboundMessageId: nil, proposalOutboundStatus: nil, - proposalEventId: nil, proposalAppId: "paykit-server", payerAppId: "paykit-server", executionClaimAppId: nil, - terms: terms, acceptedEventId: nil, acceptedOutboundStatus: nil, + proposalEventId: nil, proposalAppId: "paykit-server", proposalRecordedAt: nil, payerAppId: "paykit-server", executionClaimAppId: nil, + terms: terms, acceptedEventId: nil, acceptedOutboundStatus: nil, acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, canceledOutboundStatus: nil, conversionQuotes: [], paymentProofs: [], lastStreamItemId: nil, lastOutboundMessageId: nil, lastOutboundStatus: nil, lastEventAt: nil, invalidReason: nil @@ -527,6 +527,7 @@ private final class ContactLifecycleSdk: PaykitSdk, @unchecked Sendable { queued: [], cleared: updates.filter { update in !failed.contains { $0.counterparty == update.counterparty } } .map { .init(counterparty: $0.counterparty, outboundMessageId: 1, error: nil) }, + acknowledged: [], failedToQueue: failed.map { .init(counterparty: $0.counterparty, outboundMessageId: nil, error: nil) }, failedToDeliver: [] ) @@ -540,7 +541,7 @@ private final class ContactLifecycleSdk: PaykitSdk, @unchecked Sendable { if failWithdrawal { throw PubkyServiceError.sessionNotActive } - return PrivatePaymentListDeliveryReport(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return PrivatePaymentListDeliveryReport(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } override func blockPeer(counterparty: String) async throws -> LinkedPeerRecord { diff --git a/BitkitTests/PaykitIssuerInteropTests.swift b/BitkitTests/PaykitIssuerInteropTests.swift index 0754b5bfc..c99c8e880 100644 --- a/BitkitTests/PaykitIssuerInteropTests.swift +++ b/BitkitTests/PaykitIssuerInteropTests.swift @@ -100,6 +100,7 @@ final class PaykitIssuerInteropTests: XCTestCase { proposalOutboundStatus: nil, proposalEventId: "71300000-0000-4000-8000-000000000002", proposalAppId: "bitkit", + proposalRecordedAt: nil, payerAppId: nil, executionClaimAppId: nil, terms: PaymentRequestTerms( @@ -116,6 +117,7 @@ final class PaykitIssuerInteropTests: XCTestCase { ), acceptedEventId: nil, acceptedOutboundStatus: nil, + acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, diff --git a/BitkitTests/PaykitPaymentProofServiceTests.swift b/BitkitTests/PaykitPaymentProofServiceTests.swift index e78b9ca58..f76144cf9 100644 --- a/BitkitTests/PaykitPaymentProofServiceTests.swift +++ b/BitkitTests/PaykitPaymentProofServiceTests.swift @@ -1550,6 +1550,61 @@ final class PaykitPaymentProofServiceTests: XCTestCase { XCTAssertEqual(priorities.delivery, [.background]) } + func testPreparationAcceptsAProofThatFillsOneMessageExactly() async throws { + for (kind, endpoint) in proofSizeCases { + let reference = try paymentReference(fillingProofOf: kind, endpoint: endpoint, extraBytes: 0) + let record = try paymentRequestRecord(endpoints: [endpoint], paymentReference: reference) + let request = try XCTUnwrap(PaykitPaymentRequest(record: record, now: Date())) + let store = PaymentProofMemoryStore() + let service = paymentProofService(sdk: PaymentProofSdkMock(identity: identity, records: [record]), store: store) + + try await service.prepare(request: request, paymentAppId: "bitkit", paymentEndpointIdentifier: endpoint, kind: kind) + + let prepared = await store.snapshot() + XCTAssertEqual(prepared.map(\.requestId), [request.id], "\(kind)") + } + } + + func testPreparationRefusesARequestWhoseProofCannotFitOneMessage() async throws { + for (kind, endpoint) in proofSizeCases { + let reference = try paymentReference(fillingProofOf: kind, endpoint: endpoint, extraBytes: 1) + let record = try paymentRequestRecord(endpoints: [endpoint], paymentReference: reference) + let request = try XCTUnwrap(PaykitPaymentRequest(record: record, now: Date())) + let store = PaymentProofMemoryStore() + let service = paymentProofService(sdk: PaymentProofSdkMock(identity: identity, records: [record]), store: store) + + do { + try await service.prepare(request: request, paymentAppId: "bitkit", paymentEndpointIdentifier: endpoint, kind: kind) + XCTFail("A request whose proof cannot be delivered must not be paid: \(kind)") + } catch { + XCTAssertEqual(error as? PaykitPaymentRequestError, .requestUnavailable) + } + + let prepared = await store.snapshot() + XCTAssertTrue(prepared.isEmpty, "\(kind)") + } + } + + func testPreparationMeasuresTheAllowanceIdTheProofWillCarry() async throws { + let endpoint = PublicPaykitService.MethodId.bitcoinLightningBolt11.rawValue + let reference = try paymentReference(fillingProofOf: .lightning, endpoint: endpoint, extraBytes: 0) + let record = try paymentRequestRecord(endpoints: [endpoint], paymentReference: reference) + let request = try XCTUnwrap(PaykitPaymentRequest(record: record, now: Date())) + let store = PaymentProofMemoryStore() + let service = paymentProofService(sdk: PaymentProofSdkMock(identity: identity, records: [record]), store: store) + + do { + try await service.prepare(request: request, paymentAppId: "bitkit", paymentEndpointIdentifier: endpoint, kind: .lightning, + allowanceId: "30000000-0000-4000-8000-000000000000") + XCTFail("The Allowance ID makes this proof too long for one message") + } catch { + XCTAssertEqual(error as? PaykitPaymentRequestError, .requestUnavailable) + } + + let prepared = await store.snapshot() + XCTAssertTrue(prepared.isEmpty) + } + func testMismatchedLightningPreimageIsNotSubmitted() async throws { let record = try paymentRequestRecord() let request = try XCTUnwrap(PaykitPaymentRequest(record: record, now: Date())) @@ -3415,12 +3470,35 @@ final class PaykitPaymentProofServiceTests: XCTestCase { return service } + private var proofSizeCases: [(PaykitPaymentProofKind, String)] { + [ + (.lightning, PublicPaykitService.MethodId.bitcoinLightningBolt11.rawValue), + (.onchain, PublicPaykitService.MethodId.regtestOnchainP2wpkh.rawValue), + ] + } + + /// A Payment Reference that makes the Payment Proof Event Message of `kind` exactly `1000 + extraBytes` bytes long. + private func paymentReference(fillingProofOf kind: PaykitPaymentProofKind, endpoint: String, extraBytes: Int) throws -> String { + let baseLength = try Paykit.paymentProofEventLen( + localAppId: "bitkit", + paymentReference: PaymentReference(text: "a"), + submission: PaymentProofSubmission( + billingPeriod: nil, paymentAppId: "bitkit", paymentEndpointIdentifier: endpoint, allowanceId: nil, + conversionQuoteId: nil, proof: PrivateJsonObject(text: PaykitPaymentProofSize.placeholderProof(for: kind)) + ) + ) + let referenceBytes = PaykitSdkService.maximumMessageBytes - (Int(baseLength) - 1) + extraBytes + let wideCharacters = referenceBytes / 4 + return String(repeating: "💜", count: wideCharacters) + String(repeating: "a", count: referenceBytes - wideCharacters * 4) + } + private func paymentRequestRecord( endpoints: [String] = [PublicPaykitService.MethodId.bitcoinLightningBolt11.rawValue], paymentProofs: [PaymentProofRecord] = [], paymentRequestId: String = "550e8400-e29b-41d4-a716-446655440000", state: PaymentRequestLifecycleState = .proposed, - recurrence: PaymentRequestRecurrence? = nil + recurrence: PaymentRequestRecurrence? = nil, + paymentReference: String = "invoice-123" ) throws -> PaymentRequestRecord { try PaymentRequestRecord( counterparty: counterparty, @@ -3432,11 +3510,12 @@ final class PaykitPaymentProofServiceTests: XCTestCase { proposalOutboundStatus: nil, proposalEventId: "650e8400-e29b-41d4-a716-446655440000", proposalAppId: "bitkit", + proposalRecordedAt: nil, payerAppId: nil, executionClaimAppId: nil, terms: PaymentRequestTerms( amount: PaymentRequestAmount(value: "0.00001", asset: "btc"), - paymentReference: PaymentReference(text: "invoice-123"), + paymentReference: PaymentReference(text: paymentReference), proposalExpiresAt: nil, recurrence: recurrence, acceptedPaymentEndpointIdentifiers: endpoints, @@ -3448,6 +3527,7 @@ final class PaykitPaymentProofServiceTests: XCTestCase { ), acceptedEventId: nil, acceptedOutboundStatus: nil, + acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, diff --git a/BitkitTests/PaykitPaymentRequestServiceTests.swift b/BitkitTests/PaykitPaymentRequestServiceTests.swift index c146130f2..c723c26d4 100644 --- a/BitkitTests/PaykitPaymentRequestServiceTests.swift +++ b/BitkitTests/PaykitPaymentRequestServiceTests.swift @@ -3174,6 +3174,76 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { XCTAssertFalse(manager.isCreatingRequest) } + func testProposalTooLargeForOneMessageAsksToShortenWhatTheUserWrote() async throws { + let now = Date(timeIntervalSince1970: 1_800_000_000) + let publicKey = "pubky\(String(repeating: "y", count: 52))" + let sdk = PaymentRequestSdkMock(records: []) + await sdk.configureRecipients( + peers: [linkedPeer(counterparty: publicKey, state: .linked)], + requestCapabilitiesByPublicKey: [publicKey: true] + ) + let tooLarge: PaykitError = .Protocol( + code: "message_too_large", + context: "Private Application Message (1024 bytes) exceeds pubky-noise message size (1000 bytes)" + ) + await sdk.setProposalError(tooLarge) + let manager = paymentRequestManager(sdk: sdk, clock: PaymentRequestTestClock(now)) + await manager.refreshEligibleTargets(savedPublicKeys: [publicKey]) + let target = try XCTUnwrap(manager.eligibleTargets.first) + + do { + _ = try await manager.propose( + PaykitPaymentRequestDraft(amount: PaykitAmount(asset: .btc, atomic: 1), note: String(repeating: "💜", count: 256), + expiresAt: now.addingTimeInterval(60)), + to: target + ) + XCTFail("A proposal that does not fit one message must not be created") + } catch { + XCTAssertEqual(error as? PaykitPaymentRequestError, .noteTooLong) + } + do { + _ = try await manager.proposeSubscription( + PaykitSubscriptionDraft(amount: PaykitAmount(asset: .btc, atomic: 1000), name: "Support", description: "Monthly", + frequency: .month, expiresAt: now.addingTimeInterval(60), iconData: nil), + to: target + ) + XCTFail("A subscription that does not fit one message must not be created") + } catch { + XCTAssertEqual(error as? PaykitPaymentRequestError, .subscriptionTooLong) + } + + let snapshot = await sdk.snapshot() + XCTAssertTrue(snapshot.proposedRequests.isEmpty) + XCTAssertTrue(manager.historyRequests.isEmpty) + XCTAssertTrue(manager.subscriptions.isEmpty) + XCTAssertFalse(manager.isCreatingRequest) + } + + func testOtherProposalProtocolErrorsAreNotReportedAsTooLong() async throws { + let now = Date(timeIntervalSince1970: 1_800_000_000) + let publicKey = "pubky\(String(repeating: "y", count: 52))" + let sdk = PaymentRequestSdkMock(records: []) + await sdk.configureRecipients( + peers: [linkedPeer(counterparty: publicKey, state: .linked)], + requestCapabilitiesByPublicKey: [publicKey: true] + ) + let refusal: PaykitError = .Protocol(code: "protocol_error", context: "Invalid terms") + await sdk.setProposalError(refusal) + let manager = paymentRequestManager(sdk: sdk, clock: PaymentRequestTestClock(now)) + await manager.refreshEligibleTargets(savedPublicKeys: [publicKey]) + let target = try XCTUnwrap(manager.eligibleTargets.first) + + do { + _ = try await manager.propose( + PaykitPaymentRequestDraft(amount: PaykitAmount(asset: .btc, atomic: 1), note: "Coffee", expiresAt: now.addingTimeInterval(60)), + to: target + ) + XCTFail("The SDK refusal must reach the caller") + } catch { + XCTAssertEqual(error as? PaykitError, refusal) + } + } + func testCreatorPendingProposalCanBeDeletedButFixedEndProposalCannot() throws { let now = try XCTUnwrap(ISO8601DateFormatter().date(from: "2027-01-15T08:00:00Z")) let openEnded = try XCTUnwrap(PaykitSubscription(record: paymentRequestRecord( @@ -10456,6 +10526,78 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { } } + func testUsdtTransferMadeBeforeTheRequestWasRecordedDoesNotPayIt() async throws { + defer { try? Keychain.delete(key: .paykitUsdtPayments) } + let identity = "pubky1rsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg" + let contact = "pubky3rsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg" + let endpoint = PublicPaykitService.MethodId.usdtArbitrum.rawValue + let proof = UsdtPaymentProof(chainId: "42161", transactionHash: "transaction", receiptLogIndex: "0", signature: "signature") + let receivedProof = try PaymentProofRecord( + eventId: "750e8400-e29b-41d4-a716-446655440000", outboundMessageId: nil, outboundStatus: nil, streamItemId: 2, + paymentReference: PaymentReference(text: "invoice-123"), billingPeriod: nil, paymentAppId: "bitkit", + paymentEndpointIdentifier: endpoint, allowanceId: nil, conversionQuoteId: nil, + proof: PrivateJsonObject(text: String(decoding: JSONEncoder().encode(PaykitUsdtPaymentService.Proof(proof)), as: UTF8.self)), + recordedAt: "2027-01-15T08:01:00Z" + ) + let transferredAt = Date(timeIntervalSince1970: 1_800_000_000) + let tolerance = PaykitUsdtPaymentService.transferTimeTolerance + // The SDK writes RFC 3339 with an offset and up to nine fraction digits. + func sdkTimestamp(_ date: Date) -> String { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime] + return formatter.string(from: date).replacingOccurrences(of: "Z", with: ".123456789+00:00") + } + let cases: [(proposalRecordedAt: String?, isBeforeRequest: Bool)] = [ + (timestamp(transferredAt.addingTimeInterval(tolerance + 60)), true), + (sdkTimestamp(transferredAt.addingTimeInterval(tolerance + 60)), true), + (timestamp(transferredAt.addingTimeInterval(tolerance - 60)), false), + (sdkTimestamp(transferredAt.addingTimeInterval(tolerance - 60)), false), + (timestamp(transferredAt.addingTimeInterval(-60)), false), + (nil, false), + ("not a time", false), + ] + for testCase in cases { + try Keychain.delete(key: .paykitUsdtPayments) + var received = try paymentRequestRecord(id: "550e8400-e29b-41d4-a716-446655440002", counterparty: contact, + state: .proofSubmitted, role: .payee, amount: "0.05", asset: "usdt", + endpoints: [endpoint], paymentProofs: [receivedProof]) + received.proposalRecordedAt = testCase.proposalRecordedAt + let service = PaykitUsdtPaymentService() + await service.reconcile( + context: { ("wallet", identity) }, + storedTransfer: { id in self.usdtTransfer(id, status: .confirmed) }, + paymentProof: { _, _ in nil }, + submitProof: { _ in false }, + paymentRequests: { [received] }, + verifyPayment: { _, _ in + UsdtVerifiedPayment(paymentId: "42161:transaction:0", transferId: "transaction:0", sender: contact, + recipient: "wallet", amount: 50000, timestamp: UInt64(transferredAt.timeIntervalSince1970)) + } + ) + + let receipt = try XCTUnwrap(service.receipts.first, "\(testCase)") + XCTAssertEqual(receipt.beforeRequest, testCase.isBeforeRequest, "\(testCase)") + XCTAssertEqual(receipt.satisfied, !testCase.isBeforeRequest, "\(testCase)") + XCTAssertEqual(try service.satisfiedProofs(identity: identity).isEmpty, testCase.isBeforeRequest, "\(testCase)") + let request = try XCTUnwrap(PaykitPaymentRequest(historyRecord: received, now: transferredAt)) + let status = PaymentRequestDisplay.status(for: request, isActionable: false, receipt: receipt) + XCTAssertEqual( + status.hasPrefix(t(testCase.isBeforeRequest ? "wallet__payment_request_before_request" : "wallet__payment_request_payment_verified")), + true, + "\(testCase): \(status)" + ) + if testCase.isBeforeRequest { + let alsoLate = PaykitUsdtPaymentService.Receipt( + wallet: receipt.wallet, identity: receipt.identity, requestId: receipt.requestId, paymentId: receipt.paymentId, + proofEventId: receipt.proofEventId, verified: true, transferId: receipt.transferId, amount: receipt.amount, + receivedAt: receipt.receivedAt, underpaid: true, afterExpiry: true, beforeRequest: true + ) + XCTAssertTrue(PaymentRequestDisplay.status(for: request, isActionable: false, receipt: alsoLate) + .hasPrefix(t("wallet__payment_request_before_request"))) + } + } + } + func testUsdtProofCancellationStopsLaterAttemptsAndReceiptVerification() async throws { defer { try? Keychain.delete(key: .paykitUsdtPayments) } let identity = "pubky1rsduhcxpw74snwyct86m38c63j3pq8x4ycqikxg64roik8yw5xg" @@ -10521,6 +10663,7 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { proposalOutboundStatus: proposalOutboundStatus, proposalEventId: "650e8400-e29b-41d4-a716-446655440000", proposalAppId: proposalAppId, + proposalRecordedAt: nil, payerAppId: nil, executionClaimAppId: nil, terms: PaymentRequestTerms( @@ -10537,6 +10680,7 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { ), acceptedEventId: acceptedEventId, acceptedOutboundStatus: nil, + acceptanceRecordedAt: nil, rejectedEventId: rejectedEventId, rejectedOutboundStatus: nil, canceledEventId: canceledEventId, @@ -10673,6 +10817,7 @@ private actor PaymentRequestSdkMock: PaykitPaymentRequestSdkHandling, PaykitPaym private var failingCapabilityKeys: Set = [] private var capabilityLookupPriorities: [PaykitPublicReadPriority] = [] private var proposalResult: PaymentRequestRecord? + private var proposalError: Error? private var uploadCount = 0 private var shouldPauseNextUpload = false private var isUploadPaused = false @@ -10948,6 +11093,9 @@ private actor PaymentRequestSdkMock: PaykitPaymentRequestSdkHandling, PaykitPaym guard PubkyPublicKeyFormat.matches(activeIdentity, expectedIdentity) else { throw PaykitPaymentRequestError.requestUnavailable } + if let proposalError { + throw proposalError + } guard var result = proposalResult else { throw PaymentRequestSdkMockError.requestMissing } @@ -11190,6 +11338,10 @@ private actor PaymentRequestSdkMock: PaykitPaymentRequestSdkHandling, PaykitPaym proposalResult = record } + func setProposalError(_ error: Error?) { + proposalError = error + } + func setProcessReports(_ reports: [OutboundPrivateCounterpartySendReport]) { processReports = reports } diff --git a/BitkitTests/PaykitPaymentStateBackupTests.swift b/BitkitTests/PaykitPaymentStateBackupTests.swift index 821dbf3dc..39ec92f12 100644 --- a/BitkitTests/PaykitPaymentStateBackupTests.swift +++ b/BitkitTests/PaykitPaymentStateBackupTests.swift @@ -1202,12 +1202,38 @@ final class PaykitPaymentStateBackupTests: XCTestCase { XCTAssertEqual(attempt.billingPeriod?.sdkValue.startsAt, "2026-10-01T00:00:00.125Z") XCTAssertEqual(receipt.amount.atomic, 50000) XCTAssertEqual(receipt.requestId, attempt.requestId) + XCTAssertEqual(receipt.beforeRequest, false) + XCTAssertTrue(receipt.satisfied) let encoded = try JSONEncoder().encode(PaykitUsdtStateBackup(attempts: [.init(attempt)], receipts: [.init(receipt)])) let restored = try JSONDecoder().decode(PaykitUsdtStateBackup.self, from: encoded) XCTAssertEqual(try restored.attempts.first?.restored(), attempt) XCTAssertEqual(try restored.receipts.first?.restored(), receipt) } + @MainActor + func testUsdtReceiptPaidBeforeItsRequestStaysUnsatisfiedAcrossABackup() throws { + let requestId = PaykitPaymentRequest.ID(paymentRequestId: "request", counterparty: "pubky" + String(repeating: "y", count: 52)) + let receipt = PaykitUsdtPaymentService.Receipt(wallet: "wallet", identity: identity, requestId: requestId, + paymentId: "payment", proofEventId: "proof", verified: true, + transferId: "transfer", amount: PaykitAmount(asset: .usdt, atomic: 50000), + receivedAt: Date(timeIntervalSince1970: 1000), underpaid: false, afterExpiry: false, + beforeRequest: true) + XCTAssertFalse(receipt.satisfied) + + let encoded = try JSONEncoder().encode(PaykitUsdtStateBackup(attempts: [], receipts: [.init(receipt)])) + let restored = try XCTUnwrap(JSONDecoder().decode(PaykitUsdtStateBackup.self, from: encoded).receipts.first).restored() + + XCTAssertEqual(restored, receipt) + XCTAssertFalse(restored.satisfied) + + var stored = try XCTUnwrap(JSONSerialization.jsonObject(with: JSONEncoder().encode(receipt)) as? [String: Any]) + stored.removeValue(forKey: "beforeRequest") + let earlierReceipt = try JSONDecoder().decode(PaykitUsdtPaymentService.Receipt.self, + from: JSONSerialization.data(withJSONObject: stored)) + XCTAssertNil(earlierReceipt.beforeRequest) + XCTAssertTrue(earlierReceipt.satisfied, "A receipt saved before the flag existed keeps counting as paid") + } + @MainActor func testUsdtRestorePreservesStartedPaymentsWhenSnapshotsOverlap() throws { let service = PaykitUsdtPaymentService() diff --git a/BitkitTests/PaykitReceivedPaymentContactsTests.swift b/BitkitTests/PaykitReceivedPaymentContactsTests.swift index 2d5ed36a5..ddf51348f 100644 --- a/BitkitTests/PaykitReceivedPaymentContactsTests.swift +++ b/BitkitTests/PaykitReceivedPaymentContactsTests.swift @@ -303,7 +303,7 @@ final class PaykitReceivedPaymentContactsTests: XCTestCase { try PaymentRequestRecord( counterparty: counterparty, paymentRequestId: "550e8400-e29b-41d4-a716-446655440000", localRole: .payee, state: .proposed, proposalStreamItemId: 1, proposalOutboundMessageId: nil, proposalOutboundStatus: nil, - proposalEventId: "650e8400-e29b-41d4-a716-446655440000", proposalAppId: "paykit-server", payerAppId: nil, + proposalEventId: "650e8400-e29b-41d4-a716-446655440000", proposalAppId: "paykit-server", proposalRecordedAt: nil, payerAppId: nil, executionClaimAppId: nil, terms: PaymentRequestTerms( amount: PaymentRequestAmount(value: "0.001", asset: "btc"), paymentReference: PaymentReference(text: "invoice-123"), @@ -311,7 +311,7 @@ final class PaykitReceivedPaymentContactsTests: XCTestCase { paymentEndpoints: [identifier: PublicPaykitService.serializePayload(value: address)], requiredAppId: nil, conversion: nil, paymentDeadline: nil, metadata: PrivateJsonObject(text: "{}") ), - acceptedEventId: nil, acceptedOutboundStatus: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, + acceptedEventId: nil, acceptedOutboundStatus: nil, acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, canceledOutboundStatus: nil, conversionQuotes: [], paymentProofs: [], lastStreamItemId: 1, lastOutboundMessageId: nil, lastOutboundStatus: nil, lastEventAt: "2027-01-15T08:00:00Z", invalidReason: nil ) diff --git a/BitkitTests/PaykitRequestPricingTests.swift b/BitkitTests/PaykitRequestPricingTests.swift index 8a63d2c36..bfabcb78e 100644 --- a/BitkitTests/PaykitRequestPricingTests.swift +++ b/BitkitTests/PaykitRequestPricingTests.swift @@ -113,6 +113,51 @@ final class PaykitRequestPricingTests: XCTestCase { } } + @MainActor + func testUsdtProofSizeCountsTheBillingPeriodAndQuote() throws { + let placeholder = try PaykitPaymentProofSize.placeholderProof(for: .usdt) + let baseLength = try Paykit.paymentProofEventLen( + localAppId: "bitkit", + paymentReference: PaymentReference(text: "a"), + submission: PaymentProofSubmission(billingPeriod: nil, paymentAppId: "bitkit", + paymentEndpointIdentifier: "usdt-arbitrum-address", allowanceId: nil, + conversionQuoteId: nil, proof: PrivateJsonObject(text: placeholder)) + ) + let fillingBytes = PaykitSdkService.maximumMessageBytes - (Int(baseLength) - 1) + let reference = String(repeating: "💜", count: fillingBytes / 4) + String(repeating: "a", count: fillingBytes % 4) + let withPeriod = UsdtPaymentProofBinding(payer: String(repeating: "y", count: 52), payee: String(repeating: "y", count: 52), + paymentAppId: "bitkit", paymentRequestId: UUID().uuidString.lowercased(), + paymentReference: reference, paymentEndpointIdentifier: "usdt-arbitrum-address", + periodStartsAt: "2027-01-01T00:00:00Z", periodEndsAt: "2027-02-01T00:00:00Z", + conversionQuoteId: "") + var withQuote = withPeriod + withQuote.periodStartsAt = "" + withQuote.periodEndsAt = "" + withQuote.conversionQuoteId = UUID().uuidString.lowercased() + + for binding in [withPeriod, withQuote] { + XCTAssertThrowsError(try PaykitUsdtPaymentService.validateProofSize(binding: binding)) + } + } + + func testBitkitRequestsLeaveRoomForTheirLongestProof() throws { + let period = BillingPeriod(startsAt: "2027-01-01T00:00:00.123456789Z", endsAt: "2027-02-01T00:00:00.123456789Z") + let endpoints: [(PaykitPaymentProofKind, PublicPaykitService.MethodId)] = [ + (.lightning, .bitcoinLightningBolt11), (.onchain, .bitcoinOnchainP2wpkh), (.usdt, .usdtArbitrum), + ] + for (kind, endpoint) in endpoints { + XCTAssertNoThrow(try PaykitPaymentProofSize.validate( + paymentReference: "bitkit-\(UUID().uuidString)", + paymentAppId: "bitkit", + paymentEndpointIdentifier: endpoint.rawValue, + billingPeriod: period, + allowanceId: UUID().uuidString.lowercased(), + conversionQuoteId: UUID().uuidString.lowercased(), + kind: kind + ), "\(kind)") + } + } + func testRatesAreRequiredOnlyWhenIssuingBitcoinConversions() throws { let usd = PublicPaykitService.MethodId.usdtArbitrum.rawValue XCTAssertEqual(try PaykitRequestPricing.rates(requested: .usd, endpoints: [usd], market: nil, at: now), @@ -136,6 +181,33 @@ final class PaykitRequestPricingTests: XCTestCase { XCTAssertThrowsError(try PaykitUsdtPaymentService.validateProofSize(binding: oversized)) } + @MainActor + func testUsdtProofSizeIsMeasuredExactlyWithTheLongestValidProof() throws { + let placeholder = try PaykitPaymentProofSize.placeholderProof(for: .usdt) + let proof = try JSONDecoder().decode(PaykitUsdtPaymentService.Proof.self, from: Data(placeholder.utf8)) + XCTAssertEqual(proof.type, PaykitPaymentProofKind.usdt.rawValue) + XCTAssertEqual(proof.receiptLogIndex.count, 78) + + var binding = UsdtPaymentProofBinding(payer: String(repeating: "y", count: 52), payee: String(repeating: "y", count: 52), + paymentAppId: "bitkit", paymentRequestId: UUID().uuidString.lowercased(), + paymentReference: "a", paymentEndpointIdentifier: "usdt-arbitrum-address", + periodStartsAt: "", periodEndsAt: "", conversionQuoteId: "") + let baseLength = try Paykit.paymentProofEventLen( + localAppId: "bitkit", + paymentReference: PaymentReference(text: "a"), + submission: PaymentProofSubmission(billingPeriod: nil, paymentAppId: "bitkit", + paymentEndpointIdentifier: "usdt-arbitrum-address", allowanceId: nil, + conversionQuoteId: nil, proof: PrivateJsonObject(text: placeholder)) + ) + let fillingBytes = PaykitSdkService.maximumMessageBytes - (Int(baseLength) - 1) + binding.paymentReference = String(repeating: "💜", count: fillingBytes / 4) + String(repeating: "a", count: fillingBytes % 4) + XCTAssertNoThrow(try PaykitUsdtPaymentService.validateProofSize(binding: binding)) + binding.paymentReference += "a" + XCTAssertThrowsError(try PaykitUsdtPaymentService.validateProofSize(binding: binding)) { + XCTAssertEqual($0 as? PaykitPaymentRequestError, .requestUnavailable) + } + } + func testRecurringPaymentsKeepTheirChosenQuoteAndInclusiveDeadline() throws { let period = PaykitBillingPeriod(startsAt: now, endsAt: now.addingTimeInterval(86400)) let first = PaymentConversionQuoteRecord(eventId: "first", billingPeriod: period.sdkValue, diff --git a/BitkitTests/PaykitSdkIdentityCheckTests.swift b/BitkitTests/PaykitSdkIdentityCheckTests.swift index e1d57c9b5..d4b3754be 100644 --- a/BitkitTests/PaykitSdkIdentityCheckTests.swift +++ b/BitkitTests/PaykitSdkIdentityCheckTests.swift @@ -907,7 +907,7 @@ final class PaykitSdkIdentityCheckTests: XCTestCase { localRole: .payee, state: .proposed, proposalStreamItemId: 1, proposalOutboundMessageId: nil, proposalOutboundStatus: nil, proposalEventId: "71300000-0000-4000-8000-000000000002", - proposalAppId: appId, payerAppId: nil, executionClaimAppId: nil, + proposalAppId: appId, proposalRecordedAt: nil, payerAppId: nil, executionClaimAppId: nil, terms: PaymentRequestTerms( amount: PaymentRequestAmount(value: "0.001", asset: "btc"), paymentReference: PaymentReference(text: "invoice-123"), @@ -915,7 +915,7 @@ final class PaykitSdkIdentityCheckTests: XCTestCase { paymentEndpoints: nil, requiredAppId: "bitkit", conversion: nil, paymentDeadline: nil, metadata: PrivateJsonObject(text: "{}") ), - acceptedEventId: nil, acceptedOutboundStatus: nil, + acceptedEventId: nil, acceptedOutboundStatus: nil, acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, canceledOutboundStatus: nil, conversionQuotes: [], paymentProofs: [], lastStreamItemId: 42, @@ -1407,6 +1407,6 @@ private final class AbandonedCleanupSdk: PaykitSdk, @unchecked Sendable { updates _: [PrivatePaymentListReservationUpdateInput], clearUnlistedLinkedPeers _: Bool ) async throws -> PrivatePaymentListDeliveryReport { mutations.append("privateEndpoints") - return PrivatePaymentListDeliveryReport(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return PrivatePaymentListDeliveryReport(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } } diff --git a/BitkitTests/PaykitSdkOperationLockTests.swift b/BitkitTests/PaykitSdkOperationLockTests.swift index cddac524b..9874ab1f5 100644 --- a/BitkitTests/PaykitSdkOperationLockTests.swift +++ b/BitkitTests/PaykitSdkOperationLockTests.swift @@ -1336,7 +1336,7 @@ private final class PublicReadSdk: PaykitSdk, @unchecked Sendable { updates _: [PrivatePaymentListReservationUpdateInput], clearUnlistedLinkedPeers _: Bool ) async throws -> PrivatePaymentListDeliveryReport { await intake() - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } override func submitPaymentProof( diff --git a/BitkitTests/PaymentNavigationHelperTests.swift b/BitkitTests/PaymentNavigationHelperTests.swift index f0f0afb21..606445af6 100644 --- a/BitkitTests/PaymentNavigationHelperTests.swift +++ b/BitkitTests/PaymentNavigationHelperTests.swift @@ -502,6 +502,7 @@ final class PaymentNavigationHelperTests: XCTestCase { proposalOutboundStatus: nil, proposalEventId: "650e8400-e29b-41d4-a716-446655440000", proposalAppId: "bitkit", + proposalRecordedAt: nil, payerAppId: nil, executionClaimAppId: nil, terms: PaymentRequestTerms( @@ -518,6 +519,7 @@ final class PaymentNavigationHelperTests: XCTestCase { ), acceptedEventId: nil, acceptedOutboundStatus: nil, + acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, diff --git a/BitkitTests/PrivatePaykitServiceTests.swift b/BitkitTests/PrivatePaykitServiceTests.swift index 554cc702c..d98106360 100644 --- a/BitkitTests/PrivatePaykitServiceTests.swift +++ b/BitkitTests/PrivatePaykitServiceTests.swift @@ -283,7 +283,7 @@ final class PrivatePaykitServiceTests: XCTestCase { throw PaykitError.Transport(code: "offline", context: "Unavailable homeserver") }, buildEndpoints: { _ in XCTFail("Failed links must not reserve addresses"); return [] }, - syncPaymentLists: { _ in .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) }, + syncPaymentLists: { _ in .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) }, linkedPeers: { [] } ) ) @@ -1049,7 +1049,7 @@ final class PrivatePaykitServiceTests: XCTestCase { } syncedUpdates = updates published.fulfill() - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } )) @@ -1093,7 +1093,7 @@ final class PrivatePaykitServiceTests: XCTestCase { currentPublicKey: { "pubkylocal" }, ensureLink: { linked.append($0); return .linked }, buildEndpoints: { _ in [] }, - syncPaymentLists: { _ in .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) } + syncPaymentLists: { _ in .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } )) var sessionCurrent = true var prune: Task? @@ -1154,7 +1154,7 @@ final class PrivatePaykitServiceTests: XCTestCase { buildEndpoints: { _ in XCTFail("Ended sessions must not build endpoints"); return [] }, syncPaymentLists: { _ in XCTFail("Ended sessions must not publish") - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } )) @@ -1196,7 +1196,7 @@ final class PrivatePaykitServiceTests: XCTestCase { buildEndpoints: { _ in [endpoint] }, syncPaymentLists: { updates in published += updates.map(\.counterparty) - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } )) @@ -2200,7 +2200,7 @@ final class PrivatePaykitServiceTests: XCTestCase { currentPublicKey: { nil }, ensureLink: { _ in .notLinked }, buildEndpoints: { _ in [] }, - syncPaymentLists: { _ in .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) } + syncPaymentLists: { _ in .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } ), messageRetryOperations: .init(drain: { _ in .init(ensureLink: { _ in }, pendingOutbound: { [] }, linkedPeers: { [] }, processPending: { _ in }, receive: { _ in }) @@ -2220,7 +2220,7 @@ final class PrivatePaykitServiceTests: XCTestCase { buildEndpoints: { _ in [] }, syncPaymentLists: { updates in published += updates.map(\.counterparty) - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } )) _ = await service.rememberSavedContacts([publicKey], replacing: true) @@ -2266,7 +2266,7 @@ final class PrivatePaykitServiceTests: XCTestCase { publishing.fulfill() for await _ in resume {} } - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } )) let wallet = WalletViewModel() @@ -2869,6 +2869,7 @@ final class PrivatePaykitServiceTests: XCTestCase { withdrawals += 1 return .init( queued: [], cleared: [.init(counterparty: key, outboundMessageId: 1, error: nil)], + acknowledged: [], failedToQueue: [], failedToDeliver: [] ) }, @@ -2920,12 +2921,14 @@ final class PrivatePaykitServiceTests: XCTestCase { if shouldFail, failureStage == "queue" { return PrivatePaymentListDeliveryReport( queued: [], cleared: [], + acknowledged: [], failedToQueue: [PrivatePaymentListSyncChange(counterparty: publicKey, outboundMessageId: nil, error: nil)], failedToDeliver: [] ) } return PrivatePaymentListDeliveryReport( queued: [], cleared: [.init(counterparty: publicKey, outboundMessageId: 1, error: nil)], + acknowledged: [], failedToQueue: [], failedToDeliver: [] ) }, @@ -2994,6 +2997,7 @@ final class PrivatePaykitServiceTests: XCTestCase { cleared.append(contentsOf: $0) return PrivatePaymentListDeliveryReport( queued: [], cleared: $0.map { .init(counterparty: $0, outboundMessageId: 1, error: nil) }, + acknowledged: [], failedToQueue: [], failedToDeliver: [] ) }, @@ -3056,6 +3060,7 @@ final class PrivatePaykitServiceTests: XCTestCase { XCTAssertEqual(Set(keys), [failed, successful]) return .init( queued: [], cleared: [.init(counterparty: successful, outboundMessageId: 1, error: nil)], + acknowledged: [], failedToQueue: failQueue ? [.init(counterparty: failed, outboundMessageId: nil, error: nil)] : [], failedToDeliver: failQueue ? [] : [.init( counterparty: failed, outboundMessageId: 2, reservationId: nil, @@ -3109,7 +3114,7 @@ final class PrivatePaykitServiceTests: XCTestCase { }, syncPaymentLists: { _ in XCTFail("An unsupported contact should not receive a private list") - return PrivatePaymentListDeliveryReport(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return PrivatePaymentListDeliveryReport(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } ) @@ -3151,6 +3156,7 @@ final class PrivatePaykitServiceTests: XCTestCase { return PrivatePaymentListDeliveryReport( queued: [], cleared: [], + acknowledged: [], failedToQueue: [], failedToDeliver: [] ) @@ -3223,6 +3229,7 @@ final class PrivatePaykitServiceTests: XCTestCase { return PrivatePaymentListDeliveryReport( queued: [], cleared: [], + acknowledged: [], failedToQueue: [], failedToDeliver: [] ) @@ -3277,7 +3284,7 @@ final class PrivatePaykitServiceTests: XCTestCase { } XCTAssertEqual(updates.map(\.counterparty), [key]) publications += 1 - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) }, linkedPeers: { try await deferOnce("peers"); return [] }, canPublish: { try await deferOnce("availability"); return true } @@ -3329,7 +3336,7 @@ final class PrivatePaykitServiceTests: XCTestCase { syncPaymentLists: { updates in XCTAssertEqual(updates.map(\.counterparty), [key]) publications += 1 - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) }, canPublish: { canPublish } )) @@ -3471,7 +3478,7 @@ final class PrivatePaykitServiceTests: XCTestCase { deferred.fulfill() throw PaykitPaymentRequestError.operationInProgress } - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) }, canPublish: { if isPaused { @@ -3547,7 +3554,7 @@ final class PrivatePaykitServiceTests: XCTestCase { deferred.fulfill() throw PaykitPaymentRequestError.operationInProgress } - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) }, canPublish: { isActive && sharingEnabled }, isApplicationActive: { @@ -4066,7 +4073,7 @@ final class PrivatePaykitServiceTests: XCTestCase { XCTAssertFalse(Task.isCancelled) XCTAssertEqual(updates.map(\.counterparty), [key]) published = true - return PrivatePaymentListDeliveryReport(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return PrivatePaymentListDeliveryReport(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } ) _ = await service.syncLocalEndpointPublication( @@ -4090,7 +4097,10 @@ final class PrivatePaykitServiceTests: XCTestCase { throw PaykitError.Transport(code: "offline", context: "Unavailable homeserver") }, buildEndpoints: { _ in XCTFail("No prepared link"); return [] }, - syncPaymentLists: { _ in XCTFail("No prepared link"); return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) }, + syncPaymentLists: { _ in + XCTFail("No prepared link") + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) + }, linkedPeers: { reads += 1; return [] } ) let task = Task { @@ -4250,7 +4260,7 @@ final class PrivatePaykitServiceTests: XCTestCase { buildEndpoints: { _ in [endpoint] }, syncPaymentLists: { _ in publications += 1 - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } )) var contactState = PrivatePaykitService.ContactState() @@ -4272,6 +4282,7 @@ final class PrivatePaykitServiceTests: XCTestCase { } return .init( queued: [], cleared: [.init(counterparty: publicKey, outboundMessageId: 1, error: nil)], + acknowledged: [], failedToQueue: [], failedToDeliver: [] ) }, @@ -4353,7 +4364,7 @@ final class PrivatePaykitServiceTests: XCTestCase { buildEndpoints: { _ in XCTFail("Cleanup invalidates preparation"); return [] }, syncPaymentLists: { _ in XCTFail("Cleanup invalidates publication") - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } ) ) @@ -4394,7 +4405,7 @@ final class PrivatePaykitServiceTests: XCTestCase { buildEndpoints: { _ in XCTFail("Removed contacts must not reserve addresses"); return [] }, syncPaymentLists: { _ in XCTFail("Removed contacts must not receive publications") - return .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) + return .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } ) ) @@ -4413,7 +4424,7 @@ final class PrivatePaykitServiceTests: XCTestCase { throw PaykitError.NotFound(code: "not_found", context: "No App Registry") }, buildEndpoints: { _ in XCTFail("Unavailable contacts must not reserve addresses"); return [] }, - syncPaymentLists: { _ in .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) } + syncPaymentLists: { _ in .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } ) for _ in 0 ..< 3 { _ = await service.syncLocalEndpointPublication( @@ -4454,7 +4465,7 @@ final class PrivatePaykitServiceTests: XCTestCase { throw PaykitError.Transport(code: "offline", context: "Unavailable homeserver") }, buildEndpoints: { _ in XCTFail("Failed links must not reserve addresses"); return [] }, - syncPaymentLists: { _ in .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) }, + syncPaymentLists: { _ in .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) }, linkedPeers: peers ) _ = await service.syncLocalEndpointPublication( @@ -4491,7 +4502,7 @@ final class PrivatePaykitServiceTests: XCTestCase { throw PaykitError.NotFound(code: "not_found", context: "No App Registry") }, buildEndpoints: { _ in XCTFail("Unavailable contacts must not reserve addresses"); return [] }, - syncPaymentLists: { _ in .init(queued: [], cleared: [], failedToQueue: [], failedToDeliver: []) } + syncPaymentLists: { _ in .init(queued: [], cleared: [], acknowledged: [], failedToQueue: [], failedToDeliver: []) } ) let removals: [(PrivatePaykitService) async -> Void] = [ { await $0.clearContactState(publicKey: publicKey) }, diff --git a/BitkitTests/PubkyAuthApprovalSheetTests.swift b/BitkitTests/PubkyAuthApprovalSheetTests.swift index 1249f1145..d5e98448a 100644 --- a/BitkitTests/PubkyAuthApprovalSheetTests.swift +++ b/BitkitTests/PubkyAuthApprovalSheetTests.swift @@ -236,6 +236,28 @@ final class PubkyAuthApprovalSheetTests: XCTestCase { } } + func testApprovalFailureExplainsARejectedSignupCodeAndAFailedConnection() { + XCTAssertEqual( + pubkyAuthApprovalFailureDescription(PaykitError.Identity(code: "signup_code_rejected", context: "Token already used")), + t("profile__auth_error_signup_code_rejected") + ) + XCTAssertEqual( + pubkyAuthApprovalFailureDescription(PaykitError.Transport(code: "transport_error", context: "homeserver answered 503")), + t("pubky_auth__identity_unavailable_desc") + ) + } + + func testOtherApprovalFailuresKeepTheirOwnDescription() { + let errors: [Error] = [ + PaykitError.Identity(code: "identity_error", context: "Clock skew"), + PaykitError.Policy(code: "policy_error", context: "Identity uses another homeserver"), + PubkyAuthRequestError.invalidPaymentDetails, + ] + for error in errors { + XCTAssertEqual(pubkyAuthApprovalFailureDescription(error), error.localizedDescription) + } + } + func testAuthDisplayPublicKeyOmitsPubkyPrefix() { XCTAssertEqual(pubkyAuthDisplayPublicKey("pubky3rsd123456789w5xg"), "3rsd...w5xg") XCTAssertEqual(pubkyAuthDisplayPublicKey("3rsd123456789w5xg"), "3rsd...w5xg") diff --git a/BitkitTests/ReceiveSheetSessionTests.swift b/BitkitTests/ReceiveSheetSessionTests.swift index 99c9a04db..e9275733e 100644 --- a/BitkitTests/ReceiveSheetSessionTests.swift +++ b/BitkitTests/ReceiveSheetSessionTests.swift @@ -67,11 +67,13 @@ final class ReceiveSheetSessionTests: XCTestCase { proposalOutboundStatus: nil, proposalEventId: nil, proposalAppId: "bitkit", + proposalRecordedAt: nil, payerAppId: nil, executionClaimAppId: nil, terms: nil, acceptedEventId: nil, acceptedOutboundStatus: nil, + acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, diff --git a/BitkitTests/SendConfirmationLifecycleTests.swift b/BitkitTests/SendConfirmationLifecycleTests.swift index a6d960d90..0d4702ce1 100644 --- a/BitkitTests/SendConfirmationLifecycleTests.swift +++ b/BitkitTests/SendConfirmationLifecycleTests.swift @@ -1198,6 +1198,7 @@ final class SendConfirmationLifecycleTests: XCTestCase { proposalOutboundStatus: nil, proposalEventId: "650e8400-e29b-41d4-a716-446655440000", proposalAppId: "bitkit", + proposalRecordedAt: nil, payerAppId: nil, executionClaimAppId: nil, terms: PaymentRequestTerms( @@ -1217,6 +1218,7 @@ final class SendConfirmationLifecycleTests: XCTestCase { ), acceptedEventId: nil, acceptedOutboundStatus: nil, + acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, diff --git a/BitkitTests/SendSheetLifecycleTests.swift b/BitkitTests/SendSheetLifecycleTests.swift index d108f5773..c79af1aba 100644 --- a/BitkitTests/SendSheetLifecycleTests.swift +++ b/BitkitTests/SendSheetLifecycleTests.swift @@ -135,7 +135,7 @@ final class SendSheetLifecycleTests: XCTestCase { counterparty: "pubkypayee", paymentRequestId: UUID().uuidString, localRole: .payer, state: .proposed, proposalStreamItemId: 1, proposalOutboundMessageId: nil, proposalOutboundStatus: nil, - proposalEventId: UUID().uuidString, proposalAppId: "bitkit", + proposalEventId: UUID().uuidString, proposalAppId: "bitkit", proposalRecordedAt: nil, payerAppId: nil, executionClaimAppId: nil, terms: PaymentRequestTerms( amount: PaymentRequestAmount(value: "0.00001", asset: "btc"), @@ -145,7 +145,7 @@ final class SendSheetLifecycleTests: XCTestCase { paymentEndpoints: nil, requiredAppId: "bitkit", conversion: nil, paymentDeadline: nil, metadata: PrivateJsonObject(text: "{}") ), - acceptedEventId: nil, acceptedOutboundStatus: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, + acceptedEventId: nil, acceptedOutboundStatus: nil, acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, canceledOutboundStatus: nil, conversionQuotes: [], paymentProofs: [], lastStreamItemId: 1, lastOutboundMessageId: nil, lastOutboundStatus: nil, lastEventAt: nil, invalidReason: nil diff --git a/BitkitTests/TransferServiceActivityTests.swift b/BitkitTests/TransferServiceActivityTests.swift index 16492304e..48c00c10a 100644 --- a/BitkitTests/TransferServiceActivityTests.swift +++ b/BitkitTests/TransferServiceActivityTests.swift @@ -57,6 +57,7 @@ final class TransferServiceActivityTests: XCTestCase { proposalOutboundStatus: nil, proposalEventId: "650e8400-e29b-41d4-a716-446655440000", proposalAppId: "bitkit", + proposalRecordedAt: nil, payerAppId: nil, executionClaimAppId: nil, terms: PaymentRequestTerms( @@ -73,6 +74,7 @@ final class TransferServiceActivityTests: XCTestCase { ), acceptedEventId: nil, acceptedOutboundStatus: nil, + acceptanceRecordedAt: nil, rejectedEventId: nil, rejectedOutboundStatus: nil, canceledEventId: nil, diff --git a/changelog.d/next/1237.fixed.md b/changelog.d/next/1237.fixed.md new file mode 100644 index 000000000..598544a10 --- /dev/null +++ b/changelog.d/next/1237.fixed.md @@ -0,0 +1 @@ +Payment requests that cannot fit one Paykit message are now refused before anything is sent or paid, a note that is too long asks to be shortened, Pubky authorization explains a refused signup code or a failed connection, and USDT received before a request was made no longer counts as paying it. diff --git a/journeys/payment-requests/README.md b/journeys/payment-requests/README.md index 23783dcb5..dcb4caf49 100644 --- a/journeys/payment-requests/README.md +++ b/journeys/payment-requests/README.md @@ -161,6 +161,10 @@ check uses a supported 375-point-wide device with enlarged Dynamic Type, while A `contact-request-or-pay.xml` also needs a third linked contact that publishes no payment endpoints, so the failure and retry steps are reproducible. +`request-note-too-long.xml` checks that a request whose note does not fit one Paykit message is +refused with "Shorten the note and try again." before anything is sent, and sends after the note is +shortened. It uses the same two-instance setup. + `success-dismissal.xml` matches Android's journey: after paying a Lightning request, closing success must leave no error toast or duplicate payment; closing a later unpaid request must not send it. Use two linked disposable regtest wallets and observe or record dismissal to catch short-lived toasts. diff --git a/journeys/payment-requests/request-note-too-long.xml b/journeys/payment-requests/request-note-too-long.xml new file mode 100644 index 000000000..ff428a22d --- /dev/null +++ b/journeys/payment-requests/request-note-too-long.xml @@ -0,0 +1,18 @@ + + + Verifies that a Payment Request whose note does not fit one Paykit message is refused before anything is sent, with a message asking to shorten the note, and that the same request sends once the note is shortened. Requires two Bitkit instances saved as each other's contacts and linked as identities; see README.md. Paste the long note from the clipboard (xcrun simctl pbcopy) rather than typing it. + + + Copy a note of 256 "💜" characters to the requester's clipboard + On the requester, open the payer contact, tap Pay (id "ContactPay") and tap Request on the sheet + Enter 1,000 sats on the Payment Request amount screen and tap Continue (id "PaymentRequestAmountContinue") + Paste the copied note into the note field (id "PaymentRequestNote") + Tap Send (id "PaymentRequestSend") + Verify an error toast says "Shorten the note and try again." and the request screen stays open with Send (id "PaymentRequestSend") visible + On the payer, verify no new Payment Request appears + On the requester, replace the note with "Coffee" and tap Send (id "PaymentRequestSend") + Verify completion (id "PaymentRequestSent") is shown + On the payer, verify the Payment Request confirmation screen (id "PaymentRequestConfirm") appears with 1,000 sats and For (id "PaymentRequestFor") shows "Coffee" + Close the Payment Request confirmation screen without paying + + diff --git a/journeys/pubky-auth/README.md b/journeys/pubky-auth/README.md index b715cd24f..66e388e29 100644 --- a/journeys/pubky-auth/README.md +++ b/journeys/pubky-auth/README.md @@ -17,10 +17,13 @@ The journey uses a syntactically valid dummy request and does not contact its re - `grant-signup.xml` checks consent, cancellation, homeserver registration, and profile setup for a fresh identity. - `grant-signup-existing-identity.xml` checks approval with an existing identity without opening profile setup. +- `grant-signup-rejected-code.xml` checks the message for a signup code the homeserver refuses, and that no identity is created. -Both require a fresh `pubkyauth://signup_grant` URL from a controlled requesting app, a reachable +The first two require a fresh `pubkyauth://signup_grant` URL from a controlled requesting app, a reachable homeserver and authorization relay, and a valid invite when required. The new-identity fixture -uses ordinary app permissions without a Bitkit companion claim. +uses ordinary app permissions without a Bitkit companion claim. The rejected-code journey instead +needs a URL whose signup token that homeserver refuses, such as the token of a link another wallet +already completed. Deliver each grant signup request through the main wallet scanner: copy the complete URL to the device clipboard, tap **Scan**, then **Paste**, and allow clipboard access if prompted. Camera diff --git a/journeys/pubky-auth/grant-signup-rejected-code.xml b/journeys/pubky-auth/grant-signup-rejected-code.xml new file mode 100644 index 000000000..4a9af7699 --- /dev/null +++ b/journeys/pubky-auth/grant-signup-rejected-code.xml @@ -0,0 +1,17 @@ + + + Verifies that a signup link whose signup code the homeserver refuses shows a specific message, keeps the authorization sheet open for another try, and creates no identity. Precondition: an isolated unlocked wallet with Paykit enabled and no Pubky identity. A controlled requesting app supplies a signup_grant URL for a reachable homeserver and relay whose signup token that homeserver has already used or does not know, for example the token of a signup_grant URL another wallet already completed. The URL requests ordinary app permissions without a Bitkit companion claim. + + + Copy the prepared signup_grant URL with the refused signup token to the device clipboard + Tap the main wallet Scan button (id "Scan") + If camera permission is requested, deny it to use clipboard input + Tap Paste + Allow clipboard access if prompted + Tap Authorize (id "PubkyAuthAuthorize") and complete PIN or biometric approval when enabled + Verify an error toast says "The homeserver did not accept this signup code. Try again or use another signup link." + Verify the authorization sheet is still open with Authorize (id "PubkyAuthAuthorize") enabled + Verify the Create Profile screen (id "CreateProfileSave") is not shown + Close the authorization sheet and verify the wallet still has no Pubky identity + +