Compare commits

...
Author SHA1 Message Date
Forte11Cuba aafa9f97eb fix: dispose crash and modal overlap 2026-03-30 00:10:34 -06:00
Forte11andGitHub 7d3fe5ac58 Merge pull request #84 from Forte11Cuba/fix/nostr-listener-fail-to-post
fix: reduce Nostr listener cancellation delay from 5s to 1s
2026-03-29 23:53:06 -06:00
Forte11Cuba b9c01ce865 fix: reduce Nostr listener cancellation delay from 5s to 1s 2026-03-29 23:45:36 -06:00
Forte11andGitHub 679aa6be5f Merge pull request #83 from Forte11Cuba/feat/nostr-key-persistence
feat: persist Nostr ephemeral keys for payment request recovery
2026-03-29 23:35:54 -06:00
Forte11Cuba c44a17efbd fix: address CodeRabbit review on Nostr key persistence 2026-03-29 23:30:56 -06:00
Forte11Cuba 27a2baefcc feat: persist Nostr ephemeral keys for payment request recovery 2026-03-29 22:49:13 -06:00
Forte11andGitHub e23c5748ce Merge pull request #82 from Forte11Cuba/fix/qr-logo-overlay
feat: add logo overlay to QR codes in RequestScreen
2026-03-29 21:54:15 -06:00
Forte11Cuba c9781b2ff2 refactor: extract bitcoinOrange to AppColors constants 2026-03-29 21:53:50 -06:00
Forte11Cuba b9485d3296 feat: add logo overlay to QR codes in RequestScreen 2026-03-29 21:31:08 -06:00
Forte11andGitHub c222fff82e Merge pull request #81 from Forte11Cuba/feat/nut18-receiver-ui
feat: add RequestScreen UI with unified payment (Phase 2)
2026-03-29 21:22:01 -06:00
Forte11Cuba f839031340 fix: align scanner invoiceOnly handler with BIP-321 validation 2026-03-29 21:18:16 -06:00
Forte11Cuba 41b8743b29 fix: address CodeRabbit Phase 2 review 2026-03-29 21:05:55 -06:00
Forte11Cuba 056f4c1286 feat: add RequestScreen UI with unified payment (Phase 2) 2026-03-29 20:33:00 -06:00
Forte11andGitHub cfed450521 Merge pull request #79 from Forte11Cuba/feat/nut18-receiver-bridge
feat: expose create_payment_request() and wait_for_nostr_payment() in Rust bridge
2026-03-29 18:56:06 -06:00
Forte11Cuba 438603c97b fix: validate wallet matches handle before starting listener 2026-03-29 18:29:23 -06:00
Forte11Cuba c92c438efe fix: skip spawning listener if Dart stream is already closed 2026-03-29 15:22:23 -06:00
Forte11Cuba 7d5efcfb75 fix: apply nitpick improvements from CodeRabbit 2026-03-29 14:19:04 -06:00
Forte11Cuba bb942ef72c fix: return error on Dart stream cancellation instead of Ok(0) 2026-03-29 14:07:45 -06:00
Forte11Cuba b28a529ffc fix: validate incoming payment and reject empty relay list 2026-03-29 13:45:20 -06:00
Forte11Cuba 1ff3a6ec16 feat: address CodeRabbit review feedback 2026-03-29 13:27:52 -06:00
Forte11Cuba f1b679d912 feat: expose create_payment_request() and wait_for_nostr_payment() in Rust bridge 2026-03-29 13:01:36 -06:00
Forte11andGitHub 8b514337a6 Merge pull request #78 from Forte11Cuba/feat/nut18-payment-requests
feat: implement NUT-18/26 payment request support (payer side)
2026-03-29 04:31:42 -06:00
Forte11Cuba d7f6803f1f fix: normalize mint URLs before acceptance check 2026-03-29 04:25:05 -06:00
Forte11Cuba d91a40c41d fix: address CodeRabbit review feedback 2026-03-29 04:06:56 -06:00
Forte11Cuba 0ed49789d0 feat: implement NUT-18/26 payment request support (payer side) 2026-03-29 03:40:13 -06:00
Forte11andGitHub a41b99644c Merge pull request #76 from Forte11Cuba/fix/p2pk-pending-token-claim
fix: pass P2PK signing key when claiming pending tokens
2026-03-29 02:35:58 -06:00
Forte11Cuba 286eae3812 fix: route pending P2PK claims through receiveToken for full bookkeeping 2026-03-29 01:58:12 -06:00
Forte11Cuba e1e8057be5 fix: pass P2PK signing key when claiming pending tokens (#71) 2026-03-29 01:44:27 -06:00
Forte11andGitHub a1ce50bcd4 Merge pull request #75 from Forte11Cuba/docs/update-readme
docs: update README to reflect current project state
2026-03-29 01:28:57 -06:00
49 changed files with 7277 additions and 236 deletions
+1
View File
@@ -22,6 +22,7 @@ class AppColors {
// === ACCENT COLORS ===
static const Color primaryAction = Color(0xFFE35D33); // Naranja vibrante
static const Color secondaryAction = Color(0xFFFFB74D); // Amarillo pulpa/Bitcoin
static const Color bitcoinOrange = Color(0xFFF7931A); // Bitcoin brand orange
static const Color success = Color(0xFF00E676); // Verde neón
static const Color error = Color(0xFFFF5252); // Rojo suave
static const Color warning = Color(0xFFFFB74D); // Amarillo
+4 -2
View File
@@ -473,11 +473,13 @@ class NfcService {
return null;
}
/// Check if a string looks like a Cashu token.
/// Check if a string looks like a Cashu token or payment request.
static bool _isCashuToken(String text) {
final lower = text.toLowerCase().trim();
return lower.startsWith('cashua') ||
lower.startsWith('cashub') ||
lower.startsWith('creqa');
lower.startsWith('creqa') ||
lower.startsWith('creqb') ||
lower.startsWith('bitcoin:');
}
}
+20
View File
@@ -0,0 +1,20 @@
/// Builds a BIP-321 unified payment URI.
///
/// Format: BITCOIN:?LIGHTNING=LNBC...&CREQ=CREQB1...
/// All uppercase for optimal QR encoding (alphanumeric mode).
///
/// Ref: NUT-26 "BIP-321 Integration" section.
String buildUnifiedUri({
required String creqB,
String? bolt11,
}) {
final buffer = StringBuffer('bitcoin:?');
if (bolt11 != null) {
buffer.write('lightning=');
buffer.write(bolt11.toUpperCase());
buffer.write('&');
}
buffer.write('creq=');
buffer.write(creqB.toUpperCase());
return buffer.toString();
}
+55 -4
View File
@@ -15,7 +15,7 @@ enum IncomingDataType {
cashuToken, // cashuA... / cashuB...
lightningInvoice, // lnbc... / lntb... / lnbcrt...
mintUrl, // https://...
paymentRequest, // creqA... (post-MVP, Cashu payment request)
paymentRequest, // creqA/creqB/bitcoin:?creq= (NUT-18/26 payment request)
unknown,
}
@@ -103,8 +103,31 @@ class IncomingDataParser {
);
}
// Payment Request (creqA...)
if (lower.startsWith('creqa')) {
// BIP-321 unified URI: bitcoin:?lightning=...&creq=...
// Detect as paymentRequest but also extract the lightning invoice
if (lower.startsWith('bitcoin:')) {
final bolt11 = _extractBip321Param(trimmed, 'lightning');
final creq = _extractBip321Param(trimmed, 'creq');
if (creq != null) {
return ParsedData(
type: IncomingDataType.paymentRequest,
raw: trimmed,
invoiceBolt11: bolt11,
);
}
// bitcoin: URI with only lightning= (no creq)
if (bolt11 != null) {
return ParsedData(
type: IncomingDataType.lightningInvoice,
raw: trimmed,
invoiceBolt11: bolt11,
);
}
}
// Payment Request: creqA (NUT-18), creqB/CREQB1 (NUT-26)
if (lower.startsWith('creqa') || lower.startsWith('creqb')) {
return ParsedData(
type: IncomingDataType.paymentRequest,
raw: trimmed,
@@ -193,7 +216,35 @@ class IncomingDataParser {
case ScanMode.cashuOnly:
return data.type == IncomingDataType.cashuToken;
case ScanMode.invoiceOnly:
return data.type == IncomingDataType.lightningInvoice;
// Accept pure invoices and BIP-321 URIs that contain a lightning invoice
return data.type == IncomingDataType.lightningInvoice ||
(data.type == IncomingDataType.paymentRequest &&
data.invoiceBolt11 != null);
}
}
/// Extract a query parameter value from a BIP-321 URI.
/// Case-insensitive key matching, handles percent-encoding.
static String? _extractBip321Param(String uri, String key) {
final qIndex = uri.indexOf('?');
if (qIndex < 0) return null;
final query = uri.substring(qIndex + 1);
for (final param in query.split('&')) {
final eqIndex = param.indexOf('=');
if (eqIndex < 0) continue;
final k = param.substring(0, eqIndex);
if (k.toLowerCase() == key.toLowerCase()) {
final rawValue = param.substring(eqIndex + 1);
if (rawValue.isEmpty) continue;
try {
final value = Uri.decodeComponent(rawValue);
if (value.isEmpty) continue;
return value;
} on ArgumentError {
continue;
}
}
}
return null;
}
}
+27 -2
View File
@@ -502,7 +502,22 @@
"scanLightningInvoiceHint": "Scanne eine Lightning Rechnung (lnbc...)",
"addMintQuestion": "Diesen Mint hinzufügen?",
"cameraPermissionDenied": "Kamera-Berechtigung verweigert",
"paymentRequestNotSupported": "Zahlungsanfragen werden noch nicht unterstützt",
"paymentRequestTitle": "Zahlungsanfrage",
"paymentRequestFrom": "Anfrage von",
"paymentRequestAmount": "Angeforderter Betrag",
"paymentRequestDescription": "Beschreibung",
"paymentRequestMints": "Akzeptierte Mints",
"paymentRequestAnyMint": "Jeder Mint",
"paymentRequestPay": "Bezahlen",
"paymentRequestPaying": "Bezahle...",
"paymentRequestSuccess": "Zahlung erfolgreich gesendet",
"paymentRequestNoTransport": "Diese Anfrage hat keine konfigurierte Zustellmethode",
"paymentRequestTransport": "Transport",
"paymentRequestMintNotAccepted": "Dein aktiver Mint ist nicht in der Liste der akzeptierten Mints",
"paymentRequestUnitMismatch": "Inkompatible Einheit: Anfrage erfordert {unit}",
"paymentRequestInsufficientBalance": "Unzureichendes Guthaben",
"paymentRequestErrorParsing": "Fehler beim Lesen der Zahlungsanfrage",
"p2pkTitle": "P2PK-Schlüssel",
"p2pkSettingsDescription": "Gesperrtes ecash empfangen",
"p2pkExperimental": "P2PK ist experimentell. Mit Vorsicht verwenden.",
@@ -534,5 +549,15 @@
"p2pkErrorInvalidNsec": "Ungültiger nsec",
"p2pkErrorKeyAlreadyExists": "Dieser Schlüssel existiert bereits",
"p2pkErrorKeyNotFound": "Schlüssel nicht gefunden",
"p2pkErrorCannotDeletePrimary": "Primärschlüssel kann nicht gelöscht werden"
"p2pkErrorCannotDeletePrimary": "Primärschlüssel kann nicht gelöscht werden",
"request": "Anfordern",
"requestPayment": "Zahlung anfordern",
"requestPaymentDescription": "Einheitliche Zahlungsanforderung erstellen",
"generateRequest": "Anforderung erstellen",
"generatingRequest": "Anforderung wird erstellt...",
"requestPaymentReceived": "Zahlung erhalten",
"requestDescriptionHint": "Beschreibung (optional)",
"universal": "Universal",
"copiedToClipboard": "In die Zwischenablage kopiert"
}
+31 -2
View File
@@ -390,7 +390,26 @@
"scanLightningInvoiceHint": "Scan a Lightning invoice (lnbc...)",
"addMintQuestion": "Add this mint?",
"cameraPermissionDenied": "Camera permission denied",
"paymentRequestNotSupported": "Payment requests are not yet supported",
"paymentRequestTitle": "Payment Request",
"paymentRequestFrom": "Request from",
"paymentRequestAmount": "Requested amount",
"paymentRequestDescription": "Description",
"paymentRequestMints": "Accepted mints",
"paymentRequestAnyMint": "Any mint",
"paymentRequestPay": "Pay",
"paymentRequestPaying": "Paying...",
"paymentRequestSuccess": "Payment sent successfully",
"paymentRequestNoTransport": "This request has no delivery method configured",
"paymentRequestTransport": "Transport",
"paymentRequestMintNotAccepted": "Your active mint is not in the list of accepted mints",
"paymentRequestUnitMismatch": "Incompatible unit: request requires {unit}",
"@paymentRequestUnitMismatch": {
"placeholders": {
"unit": { "type": "String" }
}
},
"paymentRequestInsufficientBalance": "Insufficient balance",
"paymentRequestErrorParsing": "Error reading payment request",
"p2pkTitle": "P2PK Keys",
"p2pkSettingsDescription": "Receive locked ecash",
@@ -423,5 +442,15 @@
"p2pkErrorInvalidNsec": "Invalid nsec",
"p2pkErrorKeyAlreadyExists": "This key already exists",
"p2pkErrorKeyNotFound": "Key not found",
"p2pkErrorCannotDeletePrimary": "Cannot delete primary key"
"p2pkErrorCannotDeletePrimary": "Cannot delete primary key",
"request": "Request",
"requestPayment": "Request Payment",
"requestPaymentDescription": "Generate unified payment request",
"generateRequest": "Generate Request",
"generatingRequest": "Generating request...",
"requestPaymentReceived": "Payment received",
"requestDescriptionHint": "Description (optional)",
"universal": "Universal",
"copiedToClipboard": "Copied to clipboard"
}
+31 -2
View File
@@ -510,7 +510,26 @@
"scanLightningInvoiceHint": "Escanea un invoice Lightning (lnbc...)",
"addMintQuestion": "¿Agregar este mint?",
"cameraPermissionDenied": "Permiso de cámara denegado",
"paymentRequestNotSupported": "Los payment requests aún no están soportados",
"paymentRequestTitle": "Solicitud de pago",
"paymentRequestFrom": "Solicitud de",
"paymentRequestAmount": "Monto solicitado",
"paymentRequestDescription": "Descripción",
"paymentRequestMints": "Mints aceptados",
"paymentRequestAnyMint": "Cualquier mint",
"paymentRequestPay": "Pagar",
"paymentRequestPaying": "Pagando...",
"paymentRequestSuccess": "Pago enviado correctamente",
"paymentRequestNoTransport": "Esta solicitud no tiene método de entrega configurado",
"paymentRequestTransport": "Transporte",
"paymentRequestMintNotAccepted": "Tu mint activo no está en la lista de mints aceptados",
"paymentRequestUnitMismatch": "Unidad incompatible: la solicitud requiere {unit}",
"@paymentRequestUnitMismatch": {
"placeholders": {
"unit": { "type": "String" }
}
},
"paymentRequestInsufficientBalance": "Balance insuficiente",
"paymentRequestErrorParsing": "Error al leer la solicitud de pago",
"p2pkTitle": "Claves P2PK",
"p2pkSettingsDescription": "Recibir ecash bloqueado",
@@ -543,5 +562,15 @@
"p2pkErrorInvalidNsec": "nsec inválido",
"p2pkErrorKeyAlreadyExists": "Esta clave ya existe",
"p2pkErrorKeyNotFound": "Clave no encontrada",
"p2pkErrorCannotDeletePrimary": "No se puede eliminar la clave principal"
"p2pkErrorCannotDeletePrimary": "No se puede eliminar la clave principal",
"request": "Solicitar",
"requestPayment": "Solicitar pago",
"requestPaymentDescription": "Generar solicitud de pago unificada",
"generateRequest": "Generar solicitud",
"generatingRequest": "Generando solicitud...",
"requestPaymentReceived": "Pago recibido",
"requestDescriptionHint": "Descripción (opcional)",
"universal": "Universal",
"copiedToClipboard": "Copiado al portapapeles"
}
+27 -2
View File
@@ -502,7 +502,22 @@
"scanLightningInvoiceHint": "Scannez une facture Lightning (lnbc...)",
"addMintQuestion": "Ajouter ce mint ?",
"cameraPermissionDenied": "Permission de la caméra refusée",
"paymentRequestNotSupported": "Les demandes de paiement ne sont pas encore prises en charge",
"paymentRequestTitle": "Demande de paiement",
"paymentRequestFrom": "Demande de",
"paymentRequestAmount": "Montant demandé",
"paymentRequestDescription": "Description",
"paymentRequestMints": "Mints acceptés",
"paymentRequestAnyMint": "N'importe quel mint",
"paymentRequestPay": "Payer",
"paymentRequestPaying": "Paiement en cours...",
"paymentRequestSuccess": "Paiement envoyé avec succès",
"paymentRequestNoTransport": "Cette demande n'a pas de méthode de livraison configurée",
"paymentRequestTransport": "Transport",
"paymentRequestMintNotAccepted": "Votre mint actif n'est pas dans la liste des mints acceptés",
"paymentRequestUnitMismatch": "Unité incompatible : la demande nécessite {unit}",
"paymentRequestInsufficientBalance": "Solde insuffisant",
"paymentRequestErrorParsing": "Erreur lors de la lecture de la demande de paiement",
"p2pkTitle": "Clés P2PK",
"p2pkSettingsDescription": "Recevoir ecash verrouillé",
"p2pkExperimental": "P2PK est expérimental. Utiliser avec prudence.",
@@ -534,5 +549,15 @@
"p2pkErrorInvalidNsec": "nsec invalide",
"p2pkErrorKeyAlreadyExists": "Cette clé existe déjà",
"p2pkErrorKeyNotFound": "Clé non trouvée",
"p2pkErrorCannotDeletePrimary": "Impossible de supprimer la clé principale"
"p2pkErrorCannotDeletePrimary": "Impossible de supprimer la clé principale",
"request": "Demander",
"requestPayment": "Demander un paiement",
"requestPaymentDescription": "Générer une demande de paiement unifiée",
"generateRequest": "Générer la demande",
"generatingRequest": "Génération en cours...",
"requestPaymentReceived": "Paiement reçu",
"requestDescriptionHint": "Description (facultatif)",
"universal": "Universel",
"copiedToClipboard": "Copié dans le presse-papiers"
}
+27 -2
View File
@@ -502,7 +502,22 @@
"scanLightningInvoiceHint": "Scansiona una fattura Lightning (lnbc...)",
"addMintQuestion": "Aggiungere questo mint?",
"cameraPermissionDenied": "Permesso fotocamera negato",
"paymentRequestNotSupported": "Le richieste di pagamento non sono ancora supportate",
"paymentRequestTitle": "Richiesta di pagamento",
"paymentRequestFrom": "Richiesta da",
"paymentRequestAmount": "Importo richiesto",
"paymentRequestDescription": "Descrizione",
"paymentRequestMints": "Mint accettati",
"paymentRequestAnyMint": "Qualsiasi mint",
"paymentRequestPay": "Paga",
"paymentRequestPaying": "Pagamento in corso...",
"paymentRequestSuccess": "Pagamento inviato con successo",
"paymentRequestNoTransport": "Questa richiesta non ha un metodo di consegna configurato",
"paymentRequestTransport": "Trasporto",
"paymentRequestMintNotAccepted": "Il tuo mint attivo non è nella lista dei mint accettati",
"paymentRequestUnitMismatch": "Unità incompatibile: la richiesta richiede {unit}",
"paymentRequestInsufficientBalance": "Saldo insufficiente",
"paymentRequestErrorParsing": "Errore nella lettura della richiesta di pagamento",
"p2pkTitle": "Chiavi P2PK",
"p2pkSettingsDescription": "Ricevi ecash bloccato",
"p2pkExperimental": "P2PK è sperimentale. Usare con cautela.",
@@ -534,5 +549,15 @@
"p2pkErrorInvalidNsec": "nsec non valido",
"p2pkErrorKeyAlreadyExists": "Questa chiave esiste già",
"p2pkErrorKeyNotFound": "Chiave non trovata",
"p2pkErrorCannotDeletePrimary": "Impossibile eliminare la chiave principale"
"p2pkErrorCannotDeletePrimary": "Impossibile eliminare la chiave principale",
"request": "Richiedi",
"requestPayment": "Richiedi pagamento",
"requestPaymentDescription": "Genera richiesta di pagamento unificata",
"generateRequest": "Genera richiesta",
"generatingRequest": "Generazione in corso...",
"requestPaymentReceived": "Pagamento ricevuto",
"requestDescriptionHint": "Descrizione (opzionale)",
"universal": "Universale",
"copiedToClipboard": "Copiato negli appunti"
}
+27 -2
View File
@@ -502,7 +502,22 @@
"scanLightningInvoiceHint": "Lightningインボイスをスキャン(lnbc...",
"addMintQuestion": "このMintを追加しますか?",
"cameraPermissionDenied": "カメラの許可が拒否されました",
"paymentRequestNotSupported": "支払いリクエストはまだサポートされていません",
"paymentRequestTitle": "支払いリクエスト",
"paymentRequestFrom": "リクエスト元",
"paymentRequestAmount": "リクエスト金額",
"paymentRequestDescription": "説明",
"paymentRequestMints": "対応ミント",
"paymentRequestAnyMint": "すべてのミント",
"paymentRequestPay": "支払う",
"paymentRequestPaying": "支払い中...",
"paymentRequestSuccess": "支払いが正常に送信されました",
"paymentRequestNoTransport": "このリクエストには配信方法が設定されていません",
"paymentRequestTransport": "トランスポート",
"paymentRequestMintNotAccepted": "アクティブなミントは対応ミントリストにありません",
"paymentRequestUnitMismatch": "単位が互換性がありません:リクエストには{unit}が必要です",
"paymentRequestInsufficientBalance": "残高不足",
"paymentRequestErrorParsing": "支払いリクエストの読み取りエラー",
"p2pkTitle": "P2PK鍵",
"p2pkSettingsDescription": "ロックされたecashを受け取る",
"p2pkExperimental": "P2PKは実験的機能です。注意してご使用ください。",
@@ -534,5 +549,15 @@
"p2pkErrorInvalidNsec": "無効なnsec",
"p2pkErrorKeyAlreadyExists": "この鍵は既に存在します",
"p2pkErrorKeyNotFound": "鍵が見つかりません",
"p2pkErrorCannotDeletePrimary": "プライマリ鍵は削除できません"
"p2pkErrorCannotDeletePrimary": "プライマリ鍵は削除できません",
"request": "リクエスト",
"requestPayment": "支払いをリクエスト",
"requestPaymentDescription": "統合支払いリクエストを生成",
"generateRequest": "リクエストを生成",
"generatingRequest": "生成中...",
"requestPaymentReceived": "支払いを受け取りました",
"requestDescriptionHint": "説明(任意)",
"universal": "ユニバーサル",
"copiedToClipboard": "クリップボードにコピーしました"
}
+27 -2
View File
@@ -502,7 +502,22 @@
"scanLightningInvoiceHint": "Lightning 인보이스 스캔 (lnbc...)",
"addMintQuestion": "이 mint를 추가하시겠습니까?",
"cameraPermissionDenied": "카메라 권한이 거부되었습니다",
"paymentRequestNotSupported": "결제 요청은 아직 지원되지 않습니다",
"paymentRequestTitle": "결제 요청",
"paymentRequestFrom": "요청자",
"paymentRequestAmount": "요청 금액",
"paymentRequestDescription": "설명",
"paymentRequestMints": "허용된 민트",
"paymentRequestAnyMint": "모든 민트",
"paymentRequestPay": "결제",
"paymentRequestPaying": "결제 중...",
"paymentRequestSuccess": "결제가 성공적으로 전송되었습니다",
"paymentRequestNoTransport": "이 요청에는 전달 방법이 설정되지 않았습니다",
"paymentRequestTransport": "전송 방식",
"paymentRequestMintNotAccepted": "활성 민트가 허용된 민트 목록에 없습니다",
"paymentRequestUnitMismatch": "호환되지 않는 단위: 요청에 {unit}이(가) 필요합니다",
"paymentRequestInsufficientBalance": "잔액 부족",
"paymentRequestErrorParsing": "결제 요청을 읽는 중 오류 발생",
"p2pkTitle": "P2PK 키",
"p2pkSettingsDescription": "잠긴 ecash 받기",
"p2pkExperimental": "P2PK는 실험적 기능입니다. 주의하여 사용하세요.",
@@ -534,5 +549,15 @@
"p2pkErrorInvalidNsec": "유효하지 않은 nsec",
"p2pkErrorKeyAlreadyExists": "이 키는 이미 존재합니다",
"p2pkErrorKeyNotFound": "키를 찾을 수 없습니다",
"p2pkErrorCannotDeletePrimary": "기본 키는 삭제할 수 없습니다"
"p2pkErrorCannotDeletePrimary": "기본 키는 삭제할 수 없습니다",
"request": "요청",
"requestPayment": "결제 요청",
"requestPaymentDescription": "통합 결제 요청 생성",
"generateRequest": "요청 생성",
"generatingRequest": "생성 중...",
"requestPaymentReceived": "결제가 수신되었습니다",
"requestDescriptionHint": "설명 (선택사항)",
"universal": "유니버설",
"copiedToClipboard": "클립보드에 복사되었습니다"
}
+141 -3
View File
@@ -2017,11 +2017,95 @@ abstract class L10n {
/// **'Permiso de cámara denegado'**
String get cameraPermissionDenied;
/// No description provided for @paymentRequestNotSupported.
/// No description provided for @paymentRequestTitle.
///
/// In es, this message translates to:
/// **'Los payment requests aún no están soportados'**
String get paymentRequestNotSupported;
/// **'Solicitud de pago'**
String get paymentRequestTitle;
/// No description provided for @paymentRequestFrom.
///
/// In es, this message translates to:
/// **'Solicitud de'**
String get paymentRequestFrom;
/// No description provided for @paymentRequestAmount.
///
/// In es, this message translates to:
/// **'Monto solicitado'**
String get paymentRequestAmount;
/// No description provided for @paymentRequestDescription.
///
/// In es, this message translates to:
/// **'Descripción'**
String get paymentRequestDescription;
/// No description provided for @paymentRequestMints.
///
/// In es, this message translates to:
/// **'Mints aceptados'**
String get paymentRequestMints;
/// No description provided for @paymentRequestAnyMint.
///
/// In es, this message translates to:
/// **'Cualquier mint'**
String get paymentRequestAnyMint;
/// No description provided for @paymentRequestPay.
///
/// In es, this message translates to:
/// **'Pagar'**
String get paymentRequestPay;
/// No description provided for @paymentRequestPaying.
///
/// In es, this message translates to:
/// **'Pagando...'**
String get paymentRequestPaying;
/// No description provided for @paymentRequestSuccess.
///
/// In es, this message translates to:
/// **'Pago enviado correctamente'**
String get paymentRequestSuccess;
/// No description provided for @paymentRequestNoTransport.
///
/// In es, this message translates to:
/// **'Esta solicitud no tiene método de entrega configurado'**
String get paymentRequestNoTransport;
/// No description provided for @paymentRequestTransport.
///
/// In es, this message translates to:
/// **'Transporte'**
String get paymentRequestTransport;
/// No description provided for @paymentRequestMintNotAccepted.
///
/// In es, this message translates to:
/// **'Tu mint activo no está en la lista de mints aceptados'**
String get paymentRequestMintNotAccepted;
/// No description provided for @paymentRequestUnitMismatch.
///
/// In es, this message translates to:
/// **'Unidad incompatible: la solicitud requiere {unit}'**
String paymentRequestUnitMismatch(String unit);
/// No description provided for @paymentRequestInsufficientBalance.
///
/// In es, this message translates to:
/// **'Balance insuficiente'**
String get paymentRequestInsufficientBalance;
/// No description provided for @paymentRequestErrorParsing.
///
/// In es, this message translates to:
/// **'Error al leer la solicitud de pago'**
String get paymentRequestErrorParsing;
/// No description provided for @p2pkTitle.
///
@@ -2214,6 +2298,60 @@ abstract class L10n {
/// In es, this message translates to:
/// **'No se puede eliminar la clave principal'**
String get p2pkErrorCannotDeletePrimary;
/// No description provided for @request.
///
/// In es, this message translates to:
/// **'Solicitar'**
String get request;
/// No description provided for @requestPayment.
///
/// In es, this message translates to:
/// **'Solicitar pago'**
String get requestPayment;
/// No description provided for @requestPaymentDescription.
///
/// In es, this message translates to:
/// **'Generar solicitud de pago unificada'**
String get requestPaymentDescription;
/// No description provided for @generateRequest.
///
/// In es, this message translates to:
/// **'Generar solicitud'**
String get generateRequest;
/// No description provided for @generatingRequest.
///
/// In es, this message translates to:
/// **'Generando solicitud...'**
String get generatingRequest;
/// No description provided for @requestPaymentReceived.
///
/// In es, this message translates to:
/// **'Pago recibido'**
String get requestPaymentReceived;
/// No description provided for @requestDescriptionHint.
///
/// In es, this message translates to:
/// **'Descripción (opcional)'**
String get requestDescriptionHint;
/// No description provided for @universal.
///
/// In es, this message translates to:
/// **'Universal'**
String get universal;
/// No description provided for @copiedToClipboard.
///
/// In es, this message translates to:
/// **'Copiado al portapapeles'**
String get copiedToClipboard;
}
class _L10nDelegate extends LocalizationsDelegate<L10n> {
+76 -2
View File
@@ -1063,8 +1063,54 @@ class L10nDe extends L10n {
String get cameraPermissionDenied => 'Kamera-Berechtigung verweigert';
@override
String get paymentRequestNotSupported =>
'Zahlungsanfragen werden noch nicht unterstützt';
String get paymentRequestTitle => 'Zahlungsanfrage';
@override
String get paymentRequestFrom => 'Anfrage von';
@override
String get paymentRequestAmount => 'Angeforderter Betrag';
@override
String get paymentRequestDescription => 'Beschreibung';
@override
String get paymentRequestMints => 'Akzeptierte Mints';
@override
String get paymentRequestAnyMint => 'Jeder Mint';
@override
String get paymentRequestPay => 'Bezahlen';
@override
String get paymentRequestPaying => 'Bezahle...';
@override
String get paymentRequestSuccess => 'Zahlung erfolgreich gesendet';
@override
String get paymentRequestNoTransport =>
'Diese Anfrage hat keine konfigurierte Zustellmethode';
@override
String get paymentRequestTransport => 'Transport';
@override
String get paymentRequestMintNotAccepted =>
'Dein aktiver Mint ist nicht in der Liste der akzeptierten Mints';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Inkompatible Einheit: Anfrage erfordert $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Unzureichendes Guthaben';
@override
String get paymentRequestErrorParsing =>
'Fehler beim Lesen der Zahlungsanfrage';
@override
String get p2pkTitle => 'P2PK-Schlüssel';
@@ -1167,4 +1213,32 @@ class L10nDe extends L10n {
@override
String get p2pkErrorCannotDeletePrimary =>
'Primärschlüssel kann nicht gelöscht werden';
@override
String get request => 'Anfordern';
@override
String get requestPayment => 'Zahlung anfordern';
@override
String get requestPaymentDescription =>
'Einheitliche Zahlungsanforderung erstellen';
@override
String get generateRequest => 'Anforderung erstellen';
@override
String get generatingRequest => 'Anforderung wird erstellt...';
@override
String get requestPaymentReceived => 'Zahlung erhalten';
@override
String get requestDescriptionHint => 'Beschreibung (optional)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'In die Zwischenablage kopiert';
}
+74 -2
View File
@@ -1050,8 +1050,53 @@ class L10nEn extends L10n {
String get cameraPermissionDenied => 'Camera permission denied';
@override
String get paymentRequestNotSupported =>
'Payment requests are not yet supported';
String get paymentRequestTitle => 'Payment Request';
@override
String get paymentRequestFrom => 'Request from';
@override
String get paymentRequestAmount => 'Requested amount';
@override
String get paymentRequestDescription => 'Description';
@override
String get paymentRequestMints => 'Accepted mints';
@override
String get paymentRequestAnyMint => 'Any mint';
@override
String get paymentRequestPay => 'Pay';
@override
String get paymentRequestPaying => 'Paying...';
@override
String get paymentRequestSuccess => 'Payment sent successfully';
@override
String get paymentRequestNoTransport =>
'This request has no delivery method configured';
@override
String get paymentRequestTransport => 'Transport';
@override
String get paymentRequestMintNotAccepted =>
'Your active mint is not in the list of accepted mints';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Incompatible unit: request requires $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Insufficient balance';
@override
String get paymentRequestErrorParsing => 'Error reading payment request';
@override
String get p2pkTitle => 'P2PK Keys';
@@ -1150,4 +1195,31 @@ class L10nEn extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => 'Cannot delete primary key';
@override
String get request => 'Request';
@override
String get requestPayment => 'Request Payment';
@override
String get requestPaymentDescription => 'Generate unified payment request';
@override
String get generateRequest => 'Generate Request';
@override
String get generatingRequest => 'Generating request...';
@override
String get requestPaymentReceived => 'Payment received';
@override
String get requestDescriptionHint => 'Description (optional)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'Copied to clipboard';
}
+74 -2
View File
@@ -1055,8 +1055,53 @@ class L10nEs extends L10n {
String get cameraPermissionDenied => 'Permiso de cámara denegado';
@override
String get paymentRequestNotSupported =>
'Los payment requests aún no están soportados';
String get paymentRequestTitle => 'Solicitud de pago';
@override
String get paymentRequestFrom => 'Solicitud de';
@override
String get paymentRequestAmount => 'Monto solicitado';
@override
String get paymentRequestDescription => 'Descripción';
@override
String get paymentRequestMints => 'Mints aceptados';
@override
String get paymentRequestAnyMint => 'Cualquier mint';
@override
String get paymentRequestPay => 'Pagar';
@override
String get paymentRequestPaying => 'Pagando...';
@override
String get paymentRequestSuccess => 'Pago enviado correctamente';
@override
String get paymentRequestNoTransport =>
'Esta solicitud no tiene método de entrega configurado';
@override
String get paymentRequestTransport => 'Transporte';
@override
String get paymentRequestMintNotAccepted =>
'Tu mint activo no está en la lista de mints aceptados';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Unidad incompatible: la solicitud requiere $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Balance insuficiente';
@override
String get paymentRequestErrorParsing => 'Error al leer la solicitud de pago';
@override
String get p2pkTitle => 'Claves P2PK';
@@ -1158,4 +1203,31 @@ class L10nEs extends L10n {
@override
String get p2pkErrorCannotDeletePrimary =>
'No se puede eliminar la clave principal';
@override
String get request => 'Solicitar';
@override
String get requestPayment => 'Solicitar pago';
@override
String get requestPaymentDescription => 'Generar solicitud de pago unificada';
@override
String get generateRequest => 'Generar solicitud';
@override
String get generatingRequest => 'Generando solicitud...';
@override
String get requestPaymentReceived => 'Pago recibido';
@override
String get requestDescriptionHint => 'Descripción (opcional)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'Copiado al portapapeles';
}
+76 -2
View File
@@ -1068,8 +1068,54 @@ class L10nFr extends L10n {
String get cameraPermissionDenied => 'Permission de la caméra refusée';
@override
String get paymentRequestNotSupported =>
'Les demandes de paiement ne sont pas encore prises en charge';
String get paymentRequestTitle => 'Demande de paiement';
@override
String get paymentRequestFrom => 'Demande de';
@override
String get paymentRequestAmount => 'Montant demandé';
@override
String get paymentRequestDescription => 'Description';
@override
String get paymentRequestMints => 'Mints acceptés';
@override
String get paymentRequestAnyMint => 'N\'importe quel mint';
@override
String get paymentRequestPay => 'Payer';
@override
String get paymentRequestPaying => 'Paiement en cours...';
@override
String get paymentRequestSuccess => 'Paiement envoyé avec succès';
@override
String get paymentRequestNoTransport =>
'Cette demande n\'a pas de méthode de livraison configurée';
@override
String get paymentRequestTransport => 'Transport';
@override
String get paymentRequestMintNotAccepted =>
'Votre mint actif n\'est pas dans la liste des mints acceptés';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Unité incompatible : la demande nécessite $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Solde insuffisant';
@override
String get paymentRequestErrorParsing =>
'Erreur lors de la lecture de la demande de paiement';
@override
String get p2pkTitle => 'Clés P2PK';
@@ -1172,4 +1218,32 @@ class L10nFr extends L10n {
@override
String get p2pkErrorCannotDeletePrimary =>
'Impossible de supprimer la clé principale';
@override
String get request => 'Demander';
@override
String get requestPayment => 'Demander un paiement';
@override
String get requestPaymentDescription =>
'Générer une demande de paiement unifiée';
@override
String get generateRequest => 'Générer la demande';
@override
String get generatingRequest => 'Génération en cours...';
@override
String get requestPaymentReceived => 'Paiement reçu';
@override
String get requestDescriptionHint => 'Description (facultatif)';
@override
String get universal => 'Universel';
@override
String get copiedToClipboard => 'Copié dans le presse-papiers';
}
+76 -2
View File
@@ -1058,8 +1058,54 @@ class L10nIt extends L10n {
String get cameraPermissionDenied => 'Permesso fotocamera negato';
@override
String get paymentRequestNotSupported =>
'Le richieste di pagamento non sono ancora supportate';
String get paymentRequestTitle => 'Richiesta di pagamento';
@override
String get paymentRequestFrom => 'Richiesta da';
@override
String get paymentRequestAmount => 'Importo richiesto';
@override
String get paymentRequestDescription => 'Descrizione';
@override
String get paymentRequestMints => 'Mint accettati';
@override
String get paymentRequestAnyMint => 'Qualsiasi mint';
@override
String get paymentRequestPay => 'Paga';
@override
String get paymentRequestPaying => 'Pagamento in corso...';
@override
String get paymentRequestSuccess => 'Pagamento inviato con successo';
@override
String get paymentRequestNoTransport =>
'Questa richiesta non ha un metodo di consegna configurato';
@override
String get paymentRequestTransport => 'Trasporto';
@override
String get paymentRequestMintNotAccepted =>
'Il tuo mint attivo non è nella lista dei mint accettati';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Unità incompatibile: la richiesta richiede $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Saldo insufficiente';
@override
String get paymentRequestErrorParsing =>
'Errore nella lettura della richiesta di pagamento';
@override
String get p2pkTitle => 'Chiavi P2PK';
@@ -1160,4 +1206,32 @@ class L10nIt extends L10n {
@override
String get p2pkErrorCannotDeletePrimary =>
'Impossibile eliminare la chiave principale';
@override
String get request => 'Richiedi';
@override
String get requestPayment => 'Richiedi pagamento';
@override
String get requestPaymentDescription =>
'Genera richiesta di pagamento unificata';
@override
String get generateRequest => 'Genera richiesta';
@override
String get generatingRequest => 'Generazione in corso...';
@override
String get requestPaymentReceived => 'Pagamento ricevuto';
@override
String get requestDescriptionHint => 'Descrizione (opzionale)';
@override
String get universal => 'Universale';
@override
String get copiedToClipboard => 'Copiato negli appunti';
}
+72 -1
View File
@@ -1038,7 +1038,51 @@ class L10nJa extends L10n {
String get cameraPermissionDenied => 'カメラの許可が拒否されました';
@override
String get paymentRequestNotSupported => '支払いリクエストはまだサポートされていません';
String get paymentRequestTitle => '支払いリクエスト';
@override
String get paymentRequestFrom => 'リクエスト元';
@override
String get paymentRequestAmount => 'リクエスト金額';
@override
String get paymentRequestDescription => '説明';
@override
String get paymentRequestMints => '対応ミント';
@override
String get paymentRequestAnyMint => 'すべてのミント';
@override
String get paymentRequestPay => '支払う';
@override
String get paymentRequestPaying => '支払い中...';
@override
String get paymentRequestSuccess => '支払いが正常に送信されました';
@override
String get paymentRequestNoTransport => 'このリクエストには配信方法が設定されていません';
@override
String get paymentRequestTransport => 'トランスポート';
@override
String get paymentRequestMintNotAccepted => 'アクティブなミントは対応ミントリストにありません';
@override
String paymentRequestUnitMismatch(String unit) {
return '単位が互換性がありません:リクエストには$unitが必要です';
}
@override
String get paymentRequestInsufficientBalance => '残高不足';
@override
String get paymentRequestErrorParsing => '支払いリクエストの読み取りエラー';
@override
String get p2pkTitle => 'P2PK鍵';
@@ -1136,4 +1180,31 @@ class L10nJa extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => 'プライマリ鍵は削除できません';
@override
String get request => 'リクエスト';
@override
String get requestPayment => '支払いをリクエスト';
@override
String get requestPaymentDescription => '統合支払いリクエストを生成';
@override
String get generateRequest => 'リクエストを生成';
@override
String get generatingRequest => '生成中...';
@override
String get requestPaymentReceived => '支払いを受け取りました';
@override
String get requestDescriptionHint => '説明(任意)';
@override
String get universal => 'ユニバーサル';
@override
String get copiedToClipboard => 'クリップボードにコピーしました';
}
+72 -1
View File
@@ -1040,7 +1040,51 @@ class L10nKo extends L10n {
String get cameraPermissionDenied => '카메라 권한이 거부되었습니다';
@override
String get paymentRequestNotSupported => '결제 요청은 아직 지원되지 않습니다';
String get paymentRequestTitle => '결제 요청';
@override
String get paymentRequestFrom => '요청자';
@override
String get paymentRequestAmount => '요청 금액';
@override
String get paymentRequestDescription => '설명';
@override
String get paymentRequestMints => '허용된 민트';
@override
String get paymentRequestAnyMint => '모든 민트';
@override
String get paymentRequestPay => '결제';
@override
String get paymentRequestPaying => '결제 중...';
@override
String get paymentRequestSuccess => '결제가 성공적으로 전송되었습니다';
@override
String get paymentRequestNoTransport => '이 요청에는 전달 방법이 설정되지 않았습니다';
@override
String get paymentRequestTransport => '전송 방식';
@override
String get paymentRequestMintNotAccepted => '활성 민트가 허용된 민트 목록에 없습니다';
@override
String paymentRequestUnitMismatch(String unit) {
return '호환되지 않는 단위: 요청에 $unit이(가) 필요합니다';
}
@override
String get paymentRequestInsufficientBalance => '잔액 부족';
@override
String get paymentRequestErrorParsing => '결제 요청을 읽는 중 오류 발생';
@override
String get p2pkTitle => 'P2PK 키';
@@ -1138,4 +1182,31 @@ class L10nKo extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => '기본 키는 삭제할 수 없습니다';
@override
String get request => '요청';
@override
String get requestPayment => '결제 요청';
@override
String get requestPaymentDescription => '통합 결제 요청 생성';
@override
String get generateRequest => '요청 생성';
@override
String get generatingRequest => '생성 중...';
@override
String get requestPaymentReceived => '결제가 수신되었습니다';
@override
String get requestDescriptionHint => '설명 (선택사항)';
@override
String get universal => '유니버설';
@override
String get copiedToClipboard => '클립보드에 복사되었습니다';
}
+76 -2
View File
@@ -1057,8 +1057,54 @@ class L10nPt extends L10n {
String get cameraPermissionDenied => 'Permissão de câmera negada';
@override
String get paymentRequestNotSupported =>
'Solicitações de pagamento ainda não são suportadas';
String get paymentRequestTitle => 'Solicitação de pagamento';
@override
String get paymentRequestFrom => 'Solicitação de';
@override
String get paymentRequestAmount => 'Valor solicitado';
@override
String get paymentRequestDescription => 'Descrição';
@override
String get paymentRequestMints => 'Mints aceitos';
@override
String get paymentRequestAnyMint => 'Qualquer mint';
@override
String get paymentRequestPay => 'Pagar';
@override
String get paymentRequestPaying => 'Pagando...';
@override
String get paymentRequestSuccess => 'Pagamento enviado com sucesso';
@override
String get paymentRequestNoTransport =>
'Esta solicitação não tem método de entrega configurado';
@override
String get paymentRequestTransport => 'Transporte';
@override
String get paymentRequestMintNotAccepted =>
'Seu mint ativo não está na lista de mints aceitos';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Unidade incompatível: a solicitação requer $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Saldo insuficiente';
@override
String get paymentRequestErrorParsing =>
'Erro ao ler a solicitação de pagamento';
@override
String get p2pkTitle => 'Chaves P2PK';
@@ -1160,4 +1206,32 @@ class L10nPt extends L10n {
@override
String get p2pkErrorCannotDeletePrimary =>
'Não é possível excluir a chave principal';
@override
String get request => 'Solicitar';
@override
String get requestPayment => 'Solicitar pagamento';
@override
String get requestPaymentDescription =>
'Gerar solicitação de pagamento unificada';
@override
String get generateRequest => 'Gerar solicitação';
@override
String get generatingRequest => 'Gerando solicitação...';
@override
String get requestPaymentReceived => 'Pagamento recebido';
@override
String get requestDescriptionHint => 'Descrição (opcional)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'Copiado para a área de transferência';
}
+75 -2
View File
@@ -1054,8 +1054,53 @@ class L10nRu extends L10n {
String get cameraPermissionDenied => 'Доступ к камере запрещён';
@override
String get paymentRequestNotSupported =>
'Запросы на оплату пока не поддерживаются';
String get paymentRequestTitle => 'Запрос на оплату';
@override
String get paymentRequestFrom => 'Запрос от';
@override
String get paymentRequestAmount => 'Запрошенная сумма';
@override
String get paymentRequestDescription => 'Описание';
@override
String get paymentRequestMints => 'Принимаемые минты';
@override
String get paymentRequestAnyMint => 'Любой минт';
@override
String get paymentRequestPay => 'Оплатить';
@override
String get paymentRequestPaying => 'Оплата...';
@override
String get paymentRequestSuccess => 'Платёж успешно отправлен';
@override
String get paymentRequestNoTransport =>
'В этом запросе не настроен метод доставки';
@override
String get paymentRequestTransport => 'Транспорт';
@override
String get paymentRequestMintNotAccepted =>
'Ваш активный минт не в списке принимаемых';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Несовместимая единица: запрос требует $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Недостаточный баланс';
@override
String get paymentRequestErrorParsing => 'Ошибка чтения запроса на оплату';
@override
String get p2pkTitle => 'Ключи P2PK';
@@ -1157,4 +1202,32 @@ class L10nRu extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => 'Невозможно удалить основной ключ';
@override
String get request => 'Запросить';
@override
String get requestPayment => 'Запросить платёж';
@override
String get requestPaymentDescription =>
'Создать унифицированный запрос на оплату';
@override
String get generateRequest => 'Создать запрос';
@override
String get generatingRequest => 'Создание запроса...';
@override
String get requestPaymentReceived => 'Платёж получен';
@override
String get requestDescriptionHint => 'Описание (необязательно)';
@override
String get universal => 'Универсальный';
@override
String get copiedToClipboard => 'Скопировано в буфер обмена';
}
+75 -1
View File
@@ -1057,7 +1057,54 @@ class L10nSw extends L10n {
String get cameraPermissionDenied => 'Ruhusa ya kamera imekataliwa';
@override
String get paymentRequestNotSupported => 'Maombi ya malipo bado hayatumiki';
String get paymentRequestTitle => 'Ombi la malipo';
@override
String get paymentRequestFrom => 'Ombi kutoka';
@override
String get paymentRequestAmount => 'Kiasi kilichoombwa';
@override
String get paymentRequestDescription => 'Maelezo';
@override
String get paymentRequestMints => 'Mint zinazokubaliwa';
@override
String get paymentRequestAnyMint => 'Mint yoyote';
@override
String get paymentRequestPay => 'Lipa';
@override
String get paymentRequestPaying => 'Inalipa...';
@override
String get paymentRequestSuccess => 'Malipo yametumwa kwa mafanikio';
@override
String get paymentRequestNoTransport =>
'Ombi hili halina njia ya uwasilishaji iliyosanidiwa';
@override
String get paymentRequestTransport => 'Njia ya usafirishaji';
@override
String get paymentRequestMintNotAccepted =>
'Mint yako hai haiko kwenye orodha ya mint zinazokubaliwa';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Kitengo kisichooana: ombi linahitaji $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Salio haitoshi';
@override
String get paymentRequestErrorParsing =>
'Hitilafu wakati wa kusoma ombi la malipo';
@override
String get p2pkTitle => 'Funguo za P2PK';
@@ -1157,4 +1204,31 @@ class L10nSw extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => 'Haiwezekani kufuta ufunguo mkuu';
@override
String get request => 'Omba';
@override
String get requestPayment => 'Omba malipo';
@override
String get requestPaymentDescription => 'Tengeneza ombi la malipo la pamoja';
@override
String get generateRequest => 'Tengeneza ombi';
@override
String get generatingRequest => 'Inatengeneza ombi...';
@override
String get requestPaymentReceived => 'Malipo limepokelewa';
@override
String get requestDescriptionHint => 'Maelezo (si lazima)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'Imenakiliwa kwenye ubao wa kunakili';
}
+72 -1
View File
@@ -1034,7 +1034,51 @@ class L10nZh extends L10n {
String get cameraPermissionDenied => '相机权限被拒绝';
@override
String get paymentRequestNotSupported => '付款请求尚不支持';
String get paymentRequestTitle => '付款请求';
@override
String get paymentRequestFrom => '来自';
@override
String get paymentRequestAmount => '请求金额';
@override
String get paymentRequestDescription => '描述';
@override
String get paymentRequestMints => '接受的铸造厂';
@override
String get paymentRequestAnyMint => '任何铸造厂';
@override
String get paymentRequestPay => '支付';
@override
String get paymentRequestPaying => '支付中...';
@override
String get paymentRequestSuccess => '付款发送成功';
@override
String get paymentRequestNoTransport => '此请求未配置交付方式';
@override
String get paymentRequestTransport => '传输方式';
@override
String get paymentRequestMintNotAccepted => '您的活跃铸造厂不在接受的铸造厂列表中';
@override
String paymentRequestUnitMismatch(String unit) {
return '单位不兼容:请求需要 $unit';
}
@override
String get paymentRequestInsufficientBalance => '余额不足';
@override
String get paymentRequestErrorParsing => '读取付款请求时出错';
@override
String get p2pkTitle => 'P2PK密钥';
@@ -1131,4 +1175,31 @@ class L10nZh extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => '无法删除主密钥';
@override
String get request => '请求';
@override
String get requestPayment => '请求付款';
@override
String get requestPaymentDescription => '生成统一付款请求';
@override
String get generateRequest => '生成请求';
@override
String get generatingRequest => '正在生成请求...';
@override
String get requestPaymentReceived => '已收到付款';
@override
String get requestDescriptionHint => '描述(可选)';
@override
String get universal => '通用';
@override
String get copiedToClipboard => '已复制到剪贴板';
}
+27 -2
View File
@@ -502,7 +502,22 @@
"scanLightningInvoiceHint": "Escaneie um invoice Lightning (lnbc...)",
"addMintQuestion": "Adicionar este mint?",
"cameraPermissionDenied": "Permissão de câmera negada",
"paymentRequestNotSupported": "Solicitações de pagamento ainda não são suportadas",
"paymentRequestTitle": "Solicitação de pagamento",
"paymentRequestFrom": "Solicitação de",
"paymentRequestAmount": "Valor solicitado",
"paymentRequestDescription": "Descrição",
"paymentRequestMints": "Mints aceitos",
"paymentRequestAnyMint": "Qualquer mint",
"paymentRequestPay": "Pagar",
"paymentRequestPaying": "Pagando...",
"paymentRequestSuccess": "Pagamento enviado com sucesso",
"paymentRequestNoTransport": "Esta solicitação não tem método de entrega configurado",
"paymentRequestTransport": "Transporte",
"paymentRequestMintNotAccepted": "Seu mint ativo não está na lista de mints aceitos",
"paymentRequestUnitMismatch": "Unidade incompatível: a solicitação requer {unit}",
"paymentRequestInsufficientBalance": "Saldo insuficiente",
"paymentRequestErrorParsing": "Erro ao ler a solicitação de pagamento",
"p2pkTitle": "Chaves P2PK",
"p2pkSettingsDescription": "Receber ecash bloqueado",
"p2pkExperimental": "P2PK é experimental. Use com cautela.",
@@ -534,5 +549,15 @@
"p2pkErrorInvalidNsec": "nsec inválido",
"p2pkErrorKeyAlreadyExists": "Esta chave já existe",
"p2pkErrorKeyNotFound": "Chave não encontrada",
"p2pkErrorCannotDeletePrimary": "Não é possível excluir a chave principal"
"p2pkErrorCannotDeletePrimary": "Não é possível excluir a chave principal",
"request": "Solicitar",
"requestPayment": "Solicitar pagamento",
"requestPaymentDescription": "Gerar solicitação de pagamento unificada",
"generateRequest": "Gerar solicitação",
"generatingRequest": "Gerando solicitação...",
"requestPaymentReceived": "Pagamento recebido",
"requestDescriptionHint": "Descrição (opcional)",
"universal": "Universal",
"copiedToClipboard": "Copiado para a área de transferência"
}
+27 -2
View File
@@ -502,7 +502,22 @@
"scanLightningInvoiceHint": "Сканируйте Lightning счёт (lnbc...)",
"addMintQuestion": "Добавить этот mint?",
"cameraPermissionDenied": "Доступ к камере запрещён",
"paymentRequestNotSupported": "Запросы на оплату пока не поддерживаются",
"paymentRequestTitle": "Запрос на оплату",
"paymentRequestFrom": "Запрос от",
"paymentRequestAmount": "Запрошенная сумма",
"paymentRequestDescription": "Описание",
"paymentRequestMints": "Принимаемые минты",
"paymentRequestAnyMint": "Любой минт",
"paymentRequestPay": "Оплатить",
"paymentRequestPaying": "Оплата...",
"paymentRequestSuccess": "Платёж успешно отправлен",
"paymentRequestNoTransport": "В этом запросе не настроен метод доставки",
"paymentRequestTransport": "Транспорт",
"paymentRequestMintNotAccepted": "Ваш активный минт не в списке принимаемых",
"paymentRequestUnitMismatch": "Несовместимая единица: запрос требует {unit}",
"paymentRequestInsufficientBalance": "Недостаточный баланс",
"paymentRequestErrorParsing": "Ошибка чтения запроса на оплату",
"p2pkTitle": "Ключи P2PK",
"p2pkSettingsDescription": "Получить заблокированный ecash",
"p2pkExperimental": "P2PK экспериментальный. Используйте с осторожностью.",
@@ -534,5 +549,15 @@
"p2pkErrorInvalidNsec": "Недействительный nsec",
"p2pkErrorKeyAlreadyExists": "Этот ключ уже существует",
"p2pkErrorKeyNotFound": "Ключ не найден",
"p2pkErrorCannotDeletePrimary": "Невозможно удалить основной ключ"
"p2pkErrorCannotDeletePrimary": "Невозможно удалить основной ключ",
"request": "Запросить",
"requestPayment": "Запросить платёж",
"requestPaymentDescription": "Создать унифицированный запрос на оплату",
"generateRequest": "Создать запрос",
"generatingRequest": "Создание запроса...",
"requestPaymentReceived": "Платёж получен",
"requestDescriptionHint": "Описание (необязательно)",
"universal": "Универсальный",
"copiedToClipboard": "Скопировано в буфер обмена"
}
+27 -2
View File
@@ -502,7 +502,22 @@
"scanLightningInvoiceHint": "Changanua ankara ya Lightning (lnbc...)",
"addMintQuestion": "Ongeza mint hii?",
"cameraPermissionDenied": "Ruhusa ya kamera imekataliwa",
"paymentRequestNotSupported": "Maombi ya malipo bado hayatumiki",
"paymentRequestTitle": "Ombi la malipo",
"paymentRequestFrom": "Ombi kutoka",
"paymentRequestAmount": "Kiasi kilichoombwa",
"paymentRequestDescription": "Maelezo",
"paymentRequestMints": "Mint zinazokubaliwa",
"paymentRequestAnyMint": "Mint yoyote",
"paymentRequestPay": "Lipa",
"paymentRequestPaying": "Inalipa...",
"paymentRequestSuccess": "Malipo yametumwa kwa mafanikio",
"paymentRequestNoTransport": "Ombi hili halina njia ya uwasilishaji iliyosanidiwa",
"paymentRequestTransport": "Njia ya usafirishaji",
"paymentRequestMintNotAccepted": "Mint yako hai haiko kwenye orodha ya mint zinazokubaliwa",
"paymentRequestUnitMismatch": "Kitengo kisichooana: ombi linahitaji {unit}",
"paymentRequestInsufficientBalance": "Salio haitoshi",
"paymentRequestErrorParsing": "Hitilafu wakati wa kusoma ombi la malipo",
"p2pkTitle": "Funguo za P2PK",
"p2pkSettingsDescription": "Pokea ecash iliyofungwa",
"p2pkExperimental": "P2PK ni ya majaribio. Tumia kwa uangalifu.",
@@ -534,5 +549,15 @@
"p2pkErrorInvalidNsec": "nsec batili",
"p2pkErrorKeyAlreadyExists": "Ufunguo huu tayari upo",
"p2pkErrorKeyNotFound": "Ufunguo haujapatikana",
"p2pkErrorCannotDeletePrimary": "Haiwezekani kufuta ufunguo mkuu"
"p2pkErrorCannotDeletePrimary": "Haiwezekani kufuta ufunguo mkuu",
"request": "Omba",
"requestPayment": "Omba malipo",
"requestPaymentDescription": "Tengeneza ombi la malipo la pamoja",
"generateRequest": "Tengeneza ombi",
"generatingRequest": "Inatengeneza ombi...",
"requestPaymentReceived": "Malipo limepokelewa",
"requestDescriptionHint": "Maelezo (si lazima)",
"universal": "Universal",
"copiedToClipboard": "Imenakiliwa kwenye ubao wa kunakili"
}
+27 -2
View File
@@ -502,7 +502,22 @@
"scanLightningInvoiceHint": "扫描闪电发票(lnbc...",
"addMintQuestion": "添加此铸造厂?",
"cameraPermissionDenied": "相机权限被拒绝",
"paymentRequestNotSupported": "付款请求尚不支持",
"paymentRequestTitle": "付款请求",
"paymentRequestFrom": "来自",
"paymentRequestAmount": "请求金额",
"paymentRequestDescription": "描述",
"paymentRequestMints": "接受的铸造厂",
"paymentRequestAnyMint": "任何铸造厂",
"paymentRequestPay": "支付",
"paymentRequestPaying": "支付中...",
"paymentRequestSuccess": "付款发送成功",
"paymentRequestNoTransport": "此请求未配置交付方式",
"paymentRequestTransport": "传输方式",
"paymentRequestMintNotAccepted": "您的活跃铸造厂不在接受的铸造厂列表中",
"paymentRequestUnitMismatch": "单位不兼容:请求需要 {unit}",
"paymentRequestInsufficientBalance": "余额不足",
"paymentRequestErrorParsing": "读取付款请求时出错",
"p2pkTitle": "P2PK密钥",
"p2pkSettingsDescription": "接收锁定的ecash",
"p2pkExperimental": "P2PK是实验性功能。请谨慎使用。",
@@ -534,5 +549,15 @@
"p2pkErrorInvalidNsec": "无效的nsec",
"p2pkErrorKeyAlreadyExists": "此密钥已存在",
"p2pkErrorKeyNotFound": "未找到密钥",
"p2pkErrorCannotDeletePrimary": "无法删除主密钥"
"p2pkErrorCannotDeletePrimary": "无法删除主密钥",
"request": "请求",
"requestPayment": "请求付款",
"requestPaymentDescription": "生成统一付款请求",
"generateRequest": "生成请求",
"generatingRequest": "正在生成请求...",
"requestPaymentReceived": "已收到付款",
"requestDescriptionHint": "描述(可选)",
"universal": "通用",
"copiedToClipboard": "已复制到剪贴板"
}
+168 -5
View File
@@ -7,13 +7,16 @@ import '../src/rust/api/wallet.dart';
import '../src/rust/api/token.dart';
import '../src/rust/api/mint_info.dart';
import '../src/rust/api/keys.dart';
import '../src/rust/api/payment_request.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:uuid/uuid.dart';
import '../data/transaction_meta_storage.dart';
import '../data/pending_token.dart';
import '../data/pending_token_storage.dart';
import '../core/utils/keyset_debug.dart';
import '../core/utils/p2pk_utils.dart';
import '../widgets/effects/cashu_confetti.dart';
/// Helper class para info de token parseado
@@ -76,6 +79,7 @@ class WalletProvider extends ChangeNotifier {
static const _activeMintKey = 'wallet_active_mint';
static const _activeUnitKey = 'wallet_active_unit';
static const _pendingMintInvoicesKey = 'pending_mint_invoices';
static const _pendingNostrRequestKey = 'pending_nostr_request';
/// Mint de Cuba Bitcoin - siempre aparece primero en la lista
static const cubaBitcoinMint = 'https://mint.cubabitcoin.org';
@@ -1062,6 +1066,27 @@ class WalletProvider extends ChangeNotifier {
}
}
// ============================================================
// PAYMENT REQUEST (NUT-18/26)
// ============================================================
/// Paga un Payment Request (NUT-18 creqA / NUT-26 creqB / BIP-321).
/// CDK maneja: NUT-10 spending conditions, transporte (Nostr/HTTP POST),
/// preparación del token y entrega automática.
Future<void> payPaymentRequest(
String encodedRequest, {
BigInt? customAmount,
}) async {
final wallet = await getActiveWallet();
debugPrint('[PAY_REQUEST] Paying payment request via ${wallet.mintUrl}');
await wallet.payPaymentRequest(
encoded: encodedRequest,
customAmount: customAmount,
);
debugPrint('[PAY_REQUEST] Payment delivered successfully');
notifyListeners();
}
// ============================================================
// MINT (Depositar via Lightning)
// ============================================================
@@ -1223,6 +1248,130 @@ class WalletProvider extends ChangeNotifier {
return null;
}
// ============================================================
// PENDING NOSTR PAYMENT REQUESTS
// ============================================================
/// TTL for pending Nostr requests (24 hours).
static const _pendingNostrRequestTtl = Duration(hours: 24);
StreamSubscription<NostrPaymentEvent>? _pendingNostrSubscription;
bool _pendingNostrResumeAttempted = false;
static const _pendingNostrSecretKey = 'pending_nostr_secret';
static const _secureStorage = FlutterSecureStorage();
/// Save a pending Nostr payment request for recovery after app restart.
/// Secret key goes to FlutterSecureStorage, metadata to SharedPreferences.
Future<void> savePendingNostrRequest(PersistedRequestData data) async {
try {
// Secret key in encrypted storage
await _secureStorage.write(
key: _pendingNostrSecretKey,
value: data.secretHex,
);
// Metadata in SharedPreferences (no secrets)
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_pendingNostrRequestKey, jsonEncode({
'pubkeyHex': data.pubkeyHex,
'relays': data.relays,
'amount': data.amount?.toString(),
'unit': data.unit,
'mintUrl': data.mintUrl,
'createdAt': DateTime.now().toIso8601String(),
}));
} catch (e) {
debugPrint('Error saving pending Nostr request: $e');
}
}
/// Remove the pending Nostr payment request (payment received or cancelled).
Future<void> removePendingNostrRequest() async {
try {
await _secureStorage.delete(key: _pendingNostrSecretKey);
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_pendingNostrRequestKey);
} catch (e) {
debugPrint('Error removing pending Nostr request: $e');
}
}
/// Check for a pending Nostr payment request and resume listening.
/// Called automatically during app startup. Guarded against duplicate calls.
Future<void> resumePendingNostrRequest() async {
if (_pendingNostrResumeAttempted) return;
_pendingNostrResumeAttempted = true;
await _pendingNostrSubscription?.cancel();
_pendingNostrSubscription = null;
try {
final prefs = await SharedPreferences.getInstance();
final jsonStr = prefs.getString(_pendingNostrRequestKey);
if (jsonStr == null) return;
final map = Map<String, dynamic>.from(jsonDecode(jsonStr));
// Check TTL
final createdAt = DateTime.tryParse(map['createdAt'] ?? '');
if (createdAt == null ||
DateTime.now().difference(createdAt) > _pendingNostrRequestTtl) {
await removePendingNostrRequest();
debugPrint('Pending Nostr request expired, removed');
return;
}
// Read secret from secure storage
final secretHex = await _secureStorage.read(key: _pendingNostrSecretKey);
if (secretHex == null) {
await prefs.remove(_pendingNostrRequestKey);
debugPrint('Pending Nostr request has no secret key, removed');
return;
}
// Reconstruct handle
final data = PersistedRequestData(
secretHex: secretHex,
pubkeyHex: map['pubkeyHex'] as String,
relays: List<String>.from(map['relays']),
amount: map['amount'] != null
? BigInt.tryParse(map['amount'] as String)
: null,
unit: map['unit'] as String,
mintUrl: map['mintUrl'] as String,
);
final handle = NostrListenerHandle.fromPersisted(data: data);
// Get or create the wallet for this mint+unit (lazy instantiation)
Wallet wallet;
try {
wallet = await getWallet(data.mintUrl, data.unit);
} catch (e) {
debugPrint('No wallet for pending Nostr request: $e');
await removePendingNostrRequest();
return;
}
// Resume listening in background
debugPrint('Resuming pending Nostr payment request...');
_pendingNostrSubscription = wallet.waitForNostrPayment(handle: handle).listen(
(event) {
if (event.state == NostrPaymentState.received) {
debugPrint('Pending Nostr payment received: ${event.amount}');
removePendingNostrRequest();
confettiController.fire();
_pendingNostrSubscription?.cancel();
_pendingNostrSubscription = null;
}
},
onError: (error) {
debugPrint('Pending Nostr listener error (kept for retry): $error');
_pendingNostrSubscription = null;
_pendingNostrResumeAttempted = false; // Allow retry on next startup
},
);
} catch (e) {
debugPrint('Error resuming pending Nostr request: $e');
}
}
/// Guarda metadata para una transacción de mint (Lightning deposit).
Future<void> _saveMintMetadata(Wallet wallet, String invoice) async {
try {
@@ -1417,6 +1566,9 @@ class WalletProvider extends ChangeNotifier {
// Vincular transacciones incoming sin metadata con pending invoices
await _matchPendingMintInvoices();
// Resume pending Nostr payment request if app was killed mid-wait
await resumePendingNostrRequest();
}
/// Busca transacciones incoming sin metadata y las vincula con
@@ -1701,7 +1853,8 @@ class WalletProvider extends ChangeNotifier {
/// Retorna el monto recibido si tiene éxito, o lanza excepción.
/// Si el token está gastado o es inválido, lo elimina automáticamente.
/// Verifica conectividad al mint antes de intentar reclamar.
Future<BigInt> claimPendingToken(String id) async {
/// [p2pkPrivateKey] clave privada para desbloquear tokens P2PK.
Future<BigInt> claimPendingToken(String id, {String? p2pkPrivateKey}) async {
final pending = _pendingTokenStorage.get(id);
if (pending == null) {
throw Exception('Token pendiente no encontrado');
@@ -1714,8 +1867,16 @@ class WalletProvider extends ChangeNotifier {
}
try {
// Intentar reclamar usando el método existente
final amount = await receiveToken(pending.encoded);
BigInt amount;
if (P2PKUtils.isP2PKLocked(pending.encoded) && p2pkPrivateKey == null) {
throw Exception('P2PK token requires a private key to claim');
}
amount = await receiveToken(
pending.encoded,
p2pkPrivateKey: p2pkPrivateKey,
);
// Éxito: eliminar de pending
await _pendingTokenStorage.remove(id);
@@ -1744,7 +1905,8 @@ class WalletProvider extends ChangeNotifier {
/// Verifica y reclama automáticamente tokens pendientes.
/// Retorna un mapa con estadísticas: claimed, failed, removed, totalClaimed, unit.
Future<Map<String, dynamic>> checkPendingTokens() async {
/// [p2pkKeyResolver] función que dado un token encoded retorna la clave privada P2PK (o null).
Future<Map<String, dynamic>> checkPendingTokens({String? Function(String encodedToken)? p2pkKeyResolver}) async {
final tokens = _pendingTokenStorage.listValid();
if (tokens.isEmpty) {
return {'claimed': 0, 'failed': 0, 'removed': 0, 'totalClaimed': BigInt.zero, 'unit': _activeUnit};
@@ -1758,7 +1920,8 @@ class WalletProvider extends ChangeNotifier {
for (final token in tokens) {
try {
final amount = await claimPendingToken(token.id);
final p2pkKey = p2pkKeyResolver?.call(token.encoded);
final amount = await claimPendingToken(token.id, p2pkPrivateKey: p2pkKey);
claimed++;
totalClaimed += amount;
claimedUnit ??= token.unit; // Usar la unidad del primer token reclamado
+16 -4
View File
@@ -8,6 +8,7 @@ import '../../widgets/scanner/qr_scanner_widget.dart';
import '../../providers/wallet_provider.dart';
import '../4_receive/receive_screen.dart';
import '../7_melt/melt_screen.dart';
import '../11_payment_request/payment_request_screen.dart';
/// Pantalla de escaneo QR con soporte para diferentes modos
class ScanScreen extends StatefulWidget {
@@ -195,8 +196,17 @@ class _ScanScreenState extends State<ScanScreen> {
break;
case IncomingDataType.paymentRequest:
// TODO: Implementar manejo de payment requests (post-MVP)
_showError(l10n.paymentRequestNotSupported);
if (mounted) {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PaymentRequestScreen(
encodedRequest: data.raw,
),
),
);
}
break;
case IncomingDataType.unknown:
@@ -221,8 +231,10 @@ class _ScanScreenState extends State<ScanScreen> {
}
void _handleInvoiceOnlyMode(ParsedData data) {
if (data.type == IncomingDataType.lightningInvoice) {
// Retornar el invoice vía callback
if (data.type == IncomingDataType.lightningInvoice ||
(data.type == IncomingDataType.paymentRequest &&
data.invoiceBolt11 != null)) {
// Return the invoice — works for pure BOLT11 and BIP-321 URIs with lightning=
Navigator.pop(context, data.invoiceBolt11 ?? data.raw);
widget.onDataScanned?.call(data.invoiceBolt11 ?? data.raw);
} else {
@@ -0,0 +1,338 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
import '../../src/rust/api/payment_request.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../providers/wallet_provider.dart';
/// Pantalla de confirmación para pagar un Payment Request (NUT-18/26)
class PaymentRequestScreen extends StatefulWidget {
/// Raw encoded payment request (creqA, CREQB1, or bitcoin:?creq=)
final String encodedRequest;
const PaymentRequestScreen({super.key, required this.encodedRequest});
@override
State<PaymentRequestScreen> createState() => _PaymentRequestScreenState();
}
class _PaymentRequestScreenState extends State<PaymentRequestScreen> {
PaymentRequestInfo? _info;
String? _parseError;
bool _isPaying = false;
String? _payError;
@override
void initState() {
super.initState();
_parseRequest();
}
void _parseRequest() {
try {
final info = PaymentRequestInfo.parse(encoded: widget.encodedRequest);
setState(() => _info = info);
} catch (e) {
setState(() => _parseError = e.toString());
}
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text(
l10n.paymentRequestTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
),
),
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft),
onPressed: () => Navigator.pop(context),
),
),
body: _parseError != null
? _buildError(l10n)
: _info == null
? const Center(child: CircularProgressIndicator())
: _buildContent(l10n),
),
);
}
Widget _buildError(L10n l10n) {
return Center(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: GlassCard(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(LucideIcons.alertCircle, color: Colors.redAccent, size: 48),
const SizedBox(height: 16),
Text(
l10n.paymentRequestErrorParsing,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
_parseError!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 13,
color: Colors.white.withValues(alpha: 0.6),
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
Widget _buildContent(L10n l10n) {
final info = _info!;
final walletProvider = context.watch<WalletProvider>();
final activeUnit = walletProvider.activeUnit;
final requestUnit = info.unit ?? 'sat';
// Validaciones
final unitMismatch = info.unit != null && info.unit != activeUnit;
String stripSlash(String url) =>
url.endsWith('/') ? url.substring(0, url.length - 1) : url;
final normalizedMints = info.mints.map(stripSlash).toSet();
final activeMint = walletProvider.activeMintUrl;
final mintNotAccepted = normalizedMints.isNotEmpty &&
(activeMint == null || !normalizedMints.contains(stripSlash(activeMint)));
final hasTransport = info.transports.isNotEmpty;
return SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Monto
if (info.amount != null)
_buildAmountCard(info.amount!, requestUnit, l10n),
const SizedBox(height: 16),
// Detalles
GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Descripción
if (info.description != null && info.description!.isNotEmpty) ...[
_buildDetailRow(
LucideIcons.fileText,
l10n.paymentRequestDescription,
info.description!,
),
const SizedBox(height: 12),
],
// Mints aceptados
_buildDetailRow(
LucideIcons.server,
l10n.paymentRequestMints,
info.mints.isEmpty
? l10n.paymentRequestAnyMint
: info.mints.map((m) => Uri.parse(m).host).join(', '),
),
// Transporte (solo mostrar si hay transporte configurado)
if (info.transports.isNotEmpty) ...[
const SizedBox(height: 12),
_buildDetailRow(
LucideIcons.send,
l10n.paymentRequestTransport,
info.transports
.map((t) => t.transportType == 'nostr' ? 'Nostr (NIP-17)' : 'HTTP POST')
.join(', '),
),
],
],
),
),
const SizedBox(height: 16),
// Warnings
if (unitMismatch)
_buildWarning(
LucideIcons.alertTriangle,
l10n.paymentRequestUnitMismatch(requestUnit),
),
if (mintNotAccepted)
_buildWarning(
LucideIcons.alertTriangle,
l10n.paymentRequestMintNotAccepted,
),
if (!hasTransport)
_buildWarning(
LucideIcons.info,
l10n.paymentRequestNoTransport,
),
if (_payError != null) ...[
const SizedBox(height: 12),
_buildWarning(LucideIcons.alertCircle, _payError!),
],
const SizedBox(height: 32),
// Botón pagar
PrimaryButton(
text: _isPaying ? l10n.paymentRequestPaying : l10n.paymentRequestPay,
icon: LucideIcons.zap,
isLoading: _isPaying,
onPressed: (!hasTransport || _isPaying)
? null
: _pay,
),
],
),
);
}
Widget _buildAmountCard(BigInt amount, String unit, L10n l10n) {
return GlassCard(
child: Column(
children: [
Text(
l10n.paymentRequestAmount,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white.withValues(alpha: 0.7),
),
),
const SizedBox(height: 8),
Text(
UnitFormatter.formatBalanceWithUnit(amount, unit),
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 36,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
],
),
);
}
Widget _buildDetailRow(IconData icon, String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: AppColors.primaryAction),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white.withValues(alpha: 0.6),
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
),
],
),
),
],
);
}
Widget _buildWarning(IconData icon, String text) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: GlassCard(
child: Row(
children: [
Icon(icon, size: 20, color: Colors.amber),
const SizedBox(width: 10),
Expanded(
child: Text(
text,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 13,
color: Colors.amber,
),
),
),
],
),
),
);
}
Future<void> _pay() async {
setState(() {
_isPaying = true;
_payError = null;
});
try {
final walletProvider = context.read<WalletProvider>();
await walletProvider.payPaymentRequest(
widget.encodedRequest,
customAmount: _info!.amount,
);
if (!mounted) return;
// Éxito — mostrar snackbar y volver
final l10n = L10n.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.paymentRequestSuccess),
backgroundColor: Colors.green.shade700,
),
);
Navigator.pop(context, true);
} catch (e) {
if (!mounted) return;
setState(() {
_isPaying = false;
_payError = e.toString();
});
}
}
}
+895
View File
@@ -0,0 +1,895 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:share_plus/share_plus.dart';
import '../../src/rust/api/wallet.dart';
import '../../src/rust/api/payment_request.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
import '../../core/utils/bip321_builder.dart';
import '../../core/services/nfc_service.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/common/numpad_widget.dart';
import '../../providers/wallet_provider.dart';
enum RequestStatus { input, generating, waiting, received, error }
enum QrMode { universal, cashu, lightning }
/// Pantalla para solicitar pagos via Payment Request unificado.
/// Genera creqB (Cashu/Nostr) + Lightning invoice en paralelo.
class RequestScreen extends StatefulWidget {
const RequestScreen({super.key});
@override
State<RequestScreen> createState() => _RequestScreenState();
}
class _RequestScreenState extends State<RequestScreen> {
final TextEditingController _descriptionController = TextEditingController();
// Input state
String _amountValue = '';
late String _activeUnit;
// Request state
RequestStatus _status = RequestStatus.input;
String? _errorMessage;
// Payment data
String? _creqB;
String? _bolt11;
QrMode _activeMode = QrMode.cashu;
bool _paymentHandled = false;
// Listeners
StreamSubscription<NostrPaymentEvent>? _nostrSubscription;
StreamSubscription<MintQuote>? _mintSubscription;
// NFC
NfcState _nfcState = NfcState.unsupported;
bool _nfcEmulating = false;
// Success
BigInt _receivedAmount = BigInt.zero;
late final WalletProvider _walletProvider;
@override
void initState() {
super.initState();
_walletProvider = context.read<WalletProvider>();
_activeUnit = _walletProvider.activeUnit;
_checkNfc();
}
@override
void dispose() {
_descriptionController.dispose();
_nostrSubscription?.cancel();
_mintSubscription?.cancel();
if (_nfcEmulating) NfcService.stopEmulating();
// Clear persisted request if user abandoned without receiving payment
if (!_paymentHandled && _creqB != null) {
_walletProvider.removePendingNostrRequest();
}
super.dispose();
}
Future<void> _checkNfc() async {
final state = await NfcService.checkState();
if (mounted) setState(() => _nfcState = state);
}
String get _unitLabel => UnitFormatter.getUnitLabel(_activeUnit);
BigInt get _amount => UnitFormatter.parseRawDigits(_amountValue, _activeUnit);
bool get _isValidAmount => _amount > BigInt.zero;
// ─── Build ───
@override
Widget build(BuildContext context) {
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
L10n.of(context)!.requestPayment,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
actions: [
if (_status == RequestStatus.waiting)
IconButton(
icon: const Icon(LucideIcons.share2, color: Colors.white),
onPressed: _shareRequest,
),
],
),
body: SafeArea(child: _buildBody()),
),
);
}
Widget _buildBody() {
switch (_status) {
case RequestStatus.input:
return _buildInputView();
case RequestStatus.generating:
return _buildGeneratingView();
case RequestStatus.waiting:
return _buildWaitingView();
case RequestStatus.received:
return _buildSuccessView();
case RequestStatus.error:
return _buildErrorView();
}
}
// ─── Input View ───
Widget _buildInputView() {
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildAmountSection(),
const SizedBox(height: AppDimensions.paddingLarge),
_buildDescriptionSection(),
const SizedBox(height: AppDimensions.paddingMedium),
if (_errorMessage != null) _buildErrorMessage(),
],
),
),
),
Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: PrimaryButton(
text: L10n.of(context)!.generateRequest,
onPressed: _isValidAmount ? _generateRequest : null,
),
),
],
);
}
Widget _buildAmountSection() {
final displayAmount =
UnitFormatter.formatRawDigitsForDisplay(_amountValue, _activeUnit);
return Column(
children: [
Text(
displayAmount,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 48,
fontWeight: FontWeight.bold,
color: _isValidAmount || _amountValue.isEmpty
? Colors.white
: AppColors.error,
),
),
const SizedBox(height: 4),
Text(
_unitLabel,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 18,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
NumpadWidget(
value: _amountValue,
onChanged: (v) => setState(() {
_errorMessage = null;
_amountValue = v;
}),
),
],
);
}
Widget _buildDescriptionSection() {
final l10n = L10n.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.descriptionOptional,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
child: TextField(
controller: _descriptionController,
maxLines: 2,
maxLength: 100,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white,
),
decoration: InputDecoration(
hintText: l10n.requestDescriptionHint,
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white.withValues(alpha: 0.3),
),
border: InputBorder.none,
counterStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
),
),
],
);
}
Widget _buildErrorMessage() {
return Container(
margin: const EdgeInsets.only(bottom: AppDimensions.paddingMedium),
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.error.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
Icon(LucideIcons.alertCircle, color: AppColors.error, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.error,
),
),
),
],
),
);
}
// ─── Generating View ───
Widget _buildGeneratingView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(color: AppColors.primaryAction),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
L10n.of(context)!.generatingRequest,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary,
),
),
],
),
);
}
// ─── Waiting View (QR + sub-toggle + actions) ───
Widget _buildWaitingView() {
final l10n = L10n.of(context)!;
final formattedAmount = UnitFormatter.formatBalance(_amount, _activeUnit);
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
children: [
// Amount header
Text(
'$formattedAmount $_unitLabel',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
// QR Code with logo overlay
Center(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Stack(
alignment: Alignment.center,
children: [
QrImageView(
data: _getActiveQrContent(),
version: QrVersions.auto,
size: 260,
backgroundColor: Colors.white,
errorCorrectionLevel: _activeMode == QrMode.universal
? QrErrorCorrectLevel.M
: QrErrorCorrectLevel.H,
),
if (_activeMode == QrMode.cashu)
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white, width: 4),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.asset(
'assets/img/cashu.png',
fit: BoxFit.contain,
),
),
),
if (_activeMode == QrMode.lightning)
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.bitcoinOrange,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white, width: 4),
),
child: const Icon(
LucideIcons.zap,
color: Colors.white,
size: 28,
),
),
],
),
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Sub-toggle: Universal / Cashu / Lightning
_buildModeToggle(),
const SizedBox(height: AppDimensions.paddingMedium),
// Action buttons: Copy, NFC, Share
_buildActionButtons(),
const SizedBox(height: AppDimensions.paddingMedium),
// Status indicator
_buildStatusIndicator(),
],
),
),
),
// Cancel button
Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
l10n.cancel,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
),
],
);
}
Widget _buildModeToggle() {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
_buildToggleButton(
label: L10n.of(context)!.universal,
mode: QrMode.universal,
enabled: _bolt11 != null,
),
_buildToggleButton(
label: 'Cashu',
mode: QrMode.cashu,
enabled: true,
),
_buildToggleButton(
label: 'Lightning',
mode: QrMode.lightning,
enabled: _bolt11 != null,
),
],
),
);
}
Widget _buildToggleButton({
required String label,
required QrMode mode,
required bool enabled,
}) {
final isActive = _activeMode == mode;
return Expanded(
child: GestureDetector(
onTap: enabled
? () {
setState(() => _activeMode = mode);
_updateNfcPayload();
}
: null,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
gradient: isActive
? const LinearGradient(colors: AppColors.buttonGradient)
: null,
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
label,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 13,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
color: enabled
? (isActive ? Colors.white : AppColors.textSecondary)
: Colors.white.withValues(alpha: 0.3),
),
),
),
),
),
);
}
Widget _buildActionButtons() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildActionButton(
icon: LucideIcons.copy,
label: L10n.of(context)!.copy,
onTap: _copyRequest,
),
if (_nfcState == NfcState.enabled) ...[
const SizedBox(width: 16),
_buildActionButton(
icon: _nfcEmulating ? LucideIcons.wifiOff : LucideIcons.wifi,
label: 'NFC',
onTap: _toggleNfc,
active: _nfcEmulating,
),
],
const SizedBox(width: 16),
_buildActionButton(
icon: LucideIcons.share2,
label: L10n.of(context)!.share,
onTap: _shareRequest,
),
],
);
}
Widget _buildActionButton({
required IconData icon,
required String label,
required VoidCallback onTap,
bool active = false,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: active
? AppColors.primaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: active
? Border.all(color: AppColors.primaryAction.withValues(alpha: 0.5))
: null,
),
child: Row(
children: [
Icon(icon, color: active ? AppColors.primaryAction : AppColors.textSecondary, size: 18),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: active ? AppColors.primaryAction : AppColors.textSecondary,
),
),
],
),
),
);
}
Widget _buildStatusIndicator() {
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.textSecondary),
),
),
const SizedBox(width: 12),
Text(
L10n.of(context)!.waitingForPayment,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
],
),
);
}
// ─── Success View ───
Widget _buildSuccessView() {
final l10n = L10n.of(context)!;
final formattedAmount =
'+${UnitFormatter.formatBalance(_receivedAmount, _activeUnit)}';
final unitLabel = UnitFormatter.getUnitLabel(_activeUnit);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.checkCircle2,
color: AppColors.success,
size: 40,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
'$formattedAmount $unitLabel',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
Text(
l10n.requestPaymentReceived,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingLarge * 2),
PrimaryButton(
text: l10n.backToHome,
onPressed: () =>
Navigator.popUntil(context, (route) => route.isFirst),
),
],
),
);
}
// ─── Error View ───
Widget _buildErrorView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.alertCircle,
color: AppColors.error,
size: 40,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
L10n.of(context)!.error,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
_errorMessage ?? L10n.of(context)!.unknownError,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
PrimaryButton(
text: L10n.of(context)!.back,
onPressed: () => setState(() {
_status = RequestStatus.input;
_errorMessage = null;
}),
),
],
),
);
}
// ─── Logic ───
static const List<String> _defaultNostrRelays = [
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://nos.lol',
];
Future<void> _generateRequest() async {
final walletProvider = context.read<WalletProvider>();
final wallet = walletProvider.activeWallet;
// Clean up previous request state — await to ensure Rust locks are released
await _nostrSubscription?.cancel();
_nostrSubscription = null;
await _mintSubscription?.cancel();
_mintSubscription = null;
if (_nfcEmulating) {
NfcService.stopEmulating();
_nfcEmulating = false;
}
_paymentHandled = false;
_creqB = null;
_bolt11 = null;
_activeMode = QrMode.cashu;
setState(() => _status = RequestStatus.generating);
if (wallet == null) {
setState(() {
_status = RequestStatus.error;
_errorMessage = 'No active wallet';
});
return;
}
try {
final description = _descriptionController.text.isNotEmpty
? _descriptionController.text
: null;
// 1. Create payment request (creqB + Nostr keys)
final request = await wallet.createPaymentRequest(
params: CreateRequestParams(
amount: _amount,
unit: _activeUnit,
description: description,
nostrRelays: _defaultNostrRelays,
),
);
_creqB = request.creqB;
// Persist handle for recovery if app is killed
await walletProvider.savePendingNostrRequest(
request.listenerHandle.toPersisted(),
);
// 2. Start Nostr listener
_nostrSubscription = wallet
.waitForNostrPayment(handle: request.listenerHandle)
.listen(_onNostrEvent);
// 3. Start Lightning invoice generation directly via CDK
_mintSubscription = wallet
.mint(amount: _amount, description: description)
.listen(_onMintEvent);
setState(() => _status = RequestStatus.waiting);
} catch (e) {
setState(() {
_status = RequestStatus.error;
_errorMessage = e.toString();
});
}
}
void _onNostrEvent(NostrPaymentEvent event) {
if (!mounted || _paymentHandled) return;
if (event.state == NostrPaymentState.received) {
_paymentHandled = true;
_mintSubscription?.cancel();
_onPaymentSuccess(event.amount ?? _amount);
}
}
void _onMintEvent(MintQuote quote) {
if (!mounted) return;
switch (quote.state) {
case MintQuoteState.unpaid:
if (_bolt11 == null) {
setState(() {
_bolt11 = quote.request;
_activeMode = QrMode.universal;
});
_updateNfcPayload();
}
break;
case MintQuoteState.issued:
if (!_paymentHandled) {
_paymentHandled = true;
_nostrSubscription?.cancel();
_onPaymentSuccess(quote.amount ?? _amount);
}
break;
case MintQuoteState.error:
// Lightning failed, but Nostr listener continues
break;
default:
break;
}
}
Future<void> _onPaymentSuccess(BigInt amount) async {
if (_nfcEmulating) NfcService.stopEmulating();
setState(() {
_status = RequestStatus.received;
_receivedAmount = amount;
});
final walletProvider = context.read<WalletProvider>();
walletProvider.confettiController.fire();
await walletProvider.removePendingNostrRequest();
}
// ─── QR Content ───
String _getActiveQrContent() {
switch (_activeMode) {
case QrMode.universal:
return buildUnifiedUri(creqB: _creqB!, bolt11: _bolt11);
case QrMode.cashu:
return _creqB!.toUpperCase();
case QrMode.lightning:
return _bolt11?.toUpperCase() ?? '';
}
}
// ─── Actions ───
Future<void> _copyRequest() async {
final content = _getActiveQrContent();
await Clipboard.setData(ClipboardData(text: content));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.copiedToClipboard),
backgroundColor: AppColors.success,
duration: const Duration(seconds: 2),
),
);
}
}
void _toggleNfc() {
if (_nfcEmulating) {
NfcService.stopEmulating();
setState(() => _nfcEmulating = false);
} else {
NfcService.startEmulating(_getActiveQrContent());
setState(() => _nfcEmulating = true);
}
}
void _updateNfcPayload() {
if (_nfcEmulating) {
NfcService.startEmulating(_getActiveQrContent());
}
}
void _shareRequest() async {
final content = _getActiveQrContent();
final formattedAmount =
UnitFormatter.formatBalance(_amount, _activeUnit);
final unitLabel = UnitFormatter.getUnitLabel(_activeUnit);
await SharePlus.instance.share(
ShareParams(
text: '$formattedAmount $unitLabel\n\n$content',
subject: 'Payment Request - $formattedAmount $unitLabel',
),
);
}
}
+10 -6
View File
@@ -11,9 +11,10 @@ import '../../widgets/common/glass_card.dart';
import '../../widgets/common/animated_action_button.dart';
import '../../providers/wallet_provider.dart';
import '../../providers/settings_provider.dart';
import '../../providers/p2pk_provider.dart';
import '../4_receive/receive_screen.dart';
import '../5_send/send_screen.dart';
import '../6_mint/mint_screen.dart';
import '../12_request/request_screen.dart';
import '../7_melt/melt_screen.dart';
import '../8_settings/settings_screen.dart';
import '../8_settings/mints_screen.dart';
@@ -51,7 +52,10 @@ class _HomeScreenState extends State<HomeScreen> {
if (!walletProvider.hasPendingTokens) return;
try {
final result = await walletProvider.checkPendingTokens();
final p2pkProvider = context.read<P2PKProvider>();
final result = await walletProvider.checkPendingTokens(
p2pkKeyResolver: p2pkProvider.getPrivateKeyForToken,
);
final claimed = (result['claimed'] as int?) ?? 0;
final totalClaimed = result['totalClaimed'] as BigInt? ?? BigInt.zero;
final unit = (result['unit'] as String?) ?? walletProvider.activeUnit;
@@ -433,14 +437,14 @@ class _HomeScreenState extends State<HomeScreen> {
},
),
_MethodOption(
icon: LucideIcons.zap,
label: l10n.lightning,
description: l10n.generateInvoiceToDeposit,
icon: LucideIcons.bellRing,
label: l10n.request,
description: l10n.requestPaymentDescription,
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const MintScreen()),
MaterialPageRoute(builder: (context) => const RequestScreen()),
);
},
),
+20
View File
@@ -16,6 +16,7 @@ import '../../widgets/common/primary_button.dart';
import '../../providers/wallet_provider.dart';
import '../../providers/p2pk_provider.dart';
import '../10_scanner/scan_screen.dart';
import '../11_payment_request/payment_request_screen.dart';
/// Pantalla para recibir tokens Cashu
class ReceiveScreen extends StatefulWidget {
@@ -993,6 +994,25 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
return;
}
// Detectar payment request y redirigir
final parsed = IncomingDataParser.parse(tokenValue);
if (parsed.type == IncomingDataType.paymentRequest) {
_isValidToken = false;
_tokenInfo = null;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => PaymentRequestScreen(
encodedRequest: tokenValue,
),
),
);
});
return;
}
// Parsear token real con cdk-flutter
final tokenInfo = walletProvider.parseToken(tokenValue);
+1
View File
@@ -581,6 +581,7 @@ class _SendScreenState extends State<SendScreen> {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => _ConfirmationModal(
amount: _amount,
unit: _activeUnit,
+2 -3
View File
@@ -80,10 +80,9 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
_onMintCompleted();
break;
default:
// Cualquier otro estado se trata como error
case MintQuoteState.error:
_status = MintStatus.error;
_errorMessage = L10n.of(context)!.unknownState;
_errorMessage = quote.error ?? L10n.of(context)!.unknownError;
}
});
},
+1
View File
@@ -249,6 +249,7 @@ class _MintScreenState extends State<MintScreen> {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => _ConfirmationModal(
amount: _amount,
unit: _activeUnit,
+15 -2
View File
@@ -594,15 +594,28 @@ class _MeltScreenState extends State<MeltScreen> {
}
});
// Extract lightning invoice from BIP-321 URI if present
var inputValue = value;
if (value.trim().toLowerCase().startsWith('bitcoin:')) {
final parsed = IncomingDataParser.parse(value.trim());
if (parsed.invoiceBolt11 != null) {
inputValue = parsed.invoiceBolt11!;
_invoiceController.text = inputValue;
_invoiceController.selection = TextSelection.fromPosition(
TextPosition(offset: inputValue.length),
);
}
}
// Detectar tipo de input (sin mostrar error, solo detectar)
final inputType = LnurlService.detectType(value);
final inputType = LnurlService.detectType(inputValue);
setState(() => _inputType = inputType);
// Solo procesar automáticamente invoices BOLT11
// LNURL y Lightning Address requieren botón explícito
if (inputType == LnInputType.bolt11Invoice) {
// BOLT11 invoices son 200+ chars; evitar llamar API con input parcial
final cleaned = LnurlService.cleanInput(value);
final cleaned = LnurlService.cleanInput(inputValue);
if (cleaned.length > 50) {
_getQuote(cleaned);
}
+10 -1
View File
@@ -14,6 +14,8 @@ import '../../core/utils/formatters.dart';
import '../../data/transaction_meta_storage.dart';
import '../../data/pending_token.dart';
import '../../providers/wallet_provider.dart';
import '../../providers/p2pk_provider.dart';
import '../../core/utils/p2pk_utils.dart';
import '../../widgets/common/gradient_background.dart';
/// Filtros disponibles para el historial
@@ -222,9 +224,16 @@ class _HistoryScreenState extends State<HistoryScreen> {
Future<void> _claimPendingToken(PendingToken token) async {
final l10n = L10n.of(context)!;
final walletProvider = context.read<WalletProvider>();
final p2pkProvider = context.read<P2PKProvider>();
try {
final amount = await walletProvider.claimPendingToken(token.id);
// Detectar P2PK y obtener clave privada si es necesario
String? p2pkKey;
if (P2PKUtils.isP2PKLocked(token.encoded)) {
p2pkKey = p2pkProvider.getPrivateKeyForToken(token.encoded);
}
final amount = await walletProvider.claimPendingToken(token.id, p2pkPrivateKey: p2pkKey);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
+251
View File
@@ -0,0 +1,251 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'error.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `extract_creq_from_uri`, `parse_payment_request_inner`, `percent_decode`, `wait_for_nostr_payment_inner`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<CreatedPaymentRequest>>
abstract class CreatedPaymentRequest implements RustOpaqueInterface {
String get creqA;
String get creqB;
NostrListenerHandle get listenerHandle;
set creqA(String creqA);
set creqB(String creqB);
set listenerHandle(NostrListenerHandle listenerHandle);
}
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<NostrListenerHandle>>
abstract class NostrListenerHandle implements RustOpaqueInterface {
/// Reconstruct a handle from persisted data (e.g. after app restart).
static NostrListenerHandle fromPersisted({
required PersistedRequestData data,
}) => RustLib.instance.api
.crateApiPaymentRequestNostrListenerHandleFromPersisted(data: data);
/// Export handle data for persistence (e.g. SharedPreferences).
/// The secret key is exposed as hex — the caller is responsible
/// for storing it securely.
PersistedRequestData toPersisted();
}
/// Parameters for creating a payment request.
class CreateRequestParams {
/// Amount to request (in smallest unit, e.g. sats)
final BigInt? amount;
/// Currency unit ("sat", "usd", etc.)
final String unit;
/// Human-readable description
final String? description;
/// Nostr relay URLs for the transport
final List<String> nostrRelays;
const CreateRequestParams({
this.amount,
required this.unit,
this.description,
required this.nostrRelays,
});
@override
int get hashCode =>
amount.hashCode ^
unit.hashCode ^
description.hashCode ^
nostrRelays.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CreateRequestParams &&
runtimeType == other.runtimeType &&
amount == other.amount &&
unit == other.unit &&
description == other.description &&
nostrRelays == other.nostrRelays;
}
/// Event emitted by the Nostr payment listener.
class NostrPaymentEvent {
final NostrPaymentState state;
final BigInt? amount;
final String? error;
const NostrPaymentEvent({required this.state, this.amount, this.error});
@override
int get hashCode => state.hashCode ^ amount.hashCode ^ error.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NostrPaymentEvent &&
runtimeType == other.runtimeType &&
state == other.state &&
amount == other.amount &&
error == other.error;
}
/// State of a Nostr payment listener.
enum NostrPaymentState {
/// Connected to relays, waiting for payment
waiting,
/// Payment received and tokens claimed
received,
/// Error occurred
error,
}
/// Parsed payment request info exposed to Flutter.
/// CDK's FromStr auto-detects creqA (CBOR) vs creqB (Bech32m).
class PaymentRequestInfo {
/// Raw encoded string (creqA... or CREQB1...)
final String raw;
/// Payment id
final String? paymentId;
/// Requested amount (in base unit)
final BigInt? amount;
/// Currency unit ("sat", "usd", etc.)
final String? unit;
/// Whether this is a single-use request
final bool? singleUse;
/// Accepted mints (empty = any mint)
final List<String> mints;
/// Human-readable description
final String? description;
/// Available transports
final List<TransportInfo> transports;
/// Whether NUT-10 spending conditions are required
final bool hasNut10;
const PaymentRequestInfo({
required this.raw,
this.paymentId,
this.amount,
this.unit,
this.singleUse,
required this.mints,
this.description,
required this.transports,
required this.hasNut10,
});
/// Parse a payment request string (creqA, CREQB1, or bitcoin:?creq=).
/// Supports NUT-18 (CBOR+base64) and NUT-26 (Bech32m) automatically.
static PaymentRequestInfo parse({required String encoded}) => RustLib
.instance
.api
.crateApiPaymentRequestPaymentRequestInfoParse(encoded: encoded);
@override
int get hashCode =>
raw.hashCode ^
paymentId.hashCode ^
amount.hashCode ^
unit.hashCode ^
singleUse.hashCode ^
mints.hashCode ^
description.hashCode ^
transports.hashCode ^
hasNut10.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PaymentRequestInfo &&
runtimeType == other.runtimeType &&
raw == other.raw &&
paymentId == other.paymentId &&
amount == other.amount &&
unit == other.unit &&
singleUse == other.singleUse &&
mints == other.mints &&
description == other.description &&
transports == other.transports &&
hasNut10 == other.hasNut10;
}
/// Serializable data for persisting a NostrListenerHandle across app restarts.
class PersistedRequestData {
final String secretHex;
final String pubkeyHex;
final List<String> relays;
final BigInt? amount;
final String unit;
final String mintUrl;
const PersistedRequestData({
required this.secretHex,
required this.pubkeyHex,
required this.relays,
this.amount,
required this.unit,
required this.mintUrl,
});
@override
int get hashCode =>
secretHex.hashCode ^
pubkeyHex.hashCode ^
relays.hashCode ^
amount.hashCode ^
unit.hashCode ^
mintUrl.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PersistedRequestData &&
runtimeType == other.runtimeType &&
secretHex == other.secretHex &&
pubkeyHex == other.pubkeyHex &&
relays == other.relays &&
amount == other.amount &&
unit == other.unit &&
mintUrl == other.mintUrl;
}
/// Transport info exposed to Flutter
class TransportInfo {
/// "nostr" or "post"
final String transportType;
/// Target (nprofile or URL)
final String target;
const TransportInfo({required this.transportType, required this.target});
@override
int get hashCode => transportType.hashCode ^ target.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TransportInfo &&
runtimeType == other.runtimeType &&
transportType == other.transportType &&
target == other.target;
}
+28
View File
@@ -7,6 +7,7 @@ import '../frb_generated.dart';
import 'error.dart';
import 'mint_info.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'payment_request.dart';
import 'token.dart';
// These functions are ignored because they are not marked as `pub`: `mint_url`, `unit`, `update_balance_streams`
@@ -49,6 +50,15 @@ abstract class Wallet implements RustOpaqueInterface {
Future<void> checkPendingTransactions();
/// Create a NUT-18 payment request with Nostr transport.
///
/// Builds a PaymentRequest with the wallet's mint URL and unit,
/// generates ephemeral Nostr keys, and returns both creqA and creqB
/// encodings plus the keys needed for the Nostr listener.
Future<CreatedPaymentRequest> createPaymentRequest({
required CreateRequestParams params,
});
Future<void> finalizePendingMelts();
Future<MintInfo?> getMint();
@@ -77,6 +87,16 @@ abstract class Wallet implements RustOpaqueInterface {
db: db,
);
/// Pay a NUT-18 payment request.
/// Uses CDK's pay_request() which handles:
/// - NUT-10 spending conditions
/// - Transport selection (Nostr preferred, HTTP POST fallback)
/// - Token preparation and delivery
Future<void> payPaymentRequest({
required String encoded,
BigInt? customAmount,
});
Future<PreparedSend> prepareSend({required BigInt amount, SendOptions? opts});
Future<BigInt> receive({required Token token, ReceiveOptions? opts});
@@ -92,6 +112,14 @@ abstract class Wallet implements RustOpaqueInterface {
});
Stream<BigInt> streamBalance();
/// Wait for an incoming Nostr payment (NIP-17 gift-wrap).
///
/// Takes the opaque NostrListenerHandle returned by create_payment_request().
/// The secret key never leaves Rust memory.
Stream<NostrPaymentEvent> waitForNostrPayment({
required NostrListenerHandle handle,
});
}
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<WalletDatabase>>
File diff suppressed because it is too large Load Diff
+347
View File
@@ -6,6 +6,7 @@
import 'api/error.dart';
import 'api/keys.dart';
import 'api/mint_info.dart';
import 'api/payment_request.dart';
import 'api/token.dart';
import 'api/wallet.dart';
import 'dart:async';
@@ -22,6 +23,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
required super.portManager,
});
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_CreatedPaymentRequestPtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequestPtr;
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_NostrListenerHandlePtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandlePtr;
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_PreparedSendPtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSendPtr;
@@ -41,6 +50,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
CreatedPaymentRequest
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
NostrListenerHandle
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
dynamic raw,
);
@protected
PreparedSend
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -65,6 +86,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
CreatedPaymentRequest
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
PreparedSend
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -83,6 +110,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
CreatedPaymentRequest
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
NostrListenerHandle
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
dynamic raw,
);
@protected
PreparedSend
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -110,6 +149,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Map<String, String> dco_decode_Map_String_String_None(dynamic raw);
@protected
CreatedPaymentRequest
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
NostrListenerHandle
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
dynamic raw,
);
@protected
PreparedSend
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -137,6 +188,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RustStreamSink<MintQuote> dco_decode_StreamSink_mint_quote_Sse(dynamic raw);
@protected
RustStreamSink<NostrPaymentEvent>
dco_decode_StreamSink_nostr_payment_event_Sse(dynamic raw);
@protected
RustStreamSink<BigInt> dco_decode_StreamSink_u_64_Sse(dynamic raw);
@@ -149,6 +204,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
bool dco_decode_box_autoadd_bool(dynamic raw);
@protected
CreateRequestParams dco_decode_box_autoadd_create_request_params(dynamic raw);
@protected
MeltQuote dco_decode_box_autoadd_melt_quote(dynamic raw);
@@ -158,6 +216,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion dco_decode_box_autoadd_mint_version(dynamic raw);
@protected
PersistedRequestData dco_decode_box_autoadd_persisted_request_data(
dynamic raw,
);
@protected
ReceiveOptions dco_decode_box_autoadd_receive_options(dynamic raw);
@@ -181,6 +244,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ContactInfo dco_decode_contact_info(dynamic raw);
@protected
CreateRequestParams dco_decode_create_request_params(dynamic raw);
@protected
Error dco_decode_error(dynamic raw);
@@ -217,6 +283,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
List<Transaction> dco_decode_list_transaction(dynamic raw);
@protected
List<TransportInfo> dco_decode_list_transport_info(dynamic raw);
@protected
MeltMethodSettings dco_decode_melt_method_settings(dynamic raw);
@@ -238,6 +307,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion dco_decode_mint_version(dynamic raw);
@protected
NostrPaymentEvent dco_decode_nostr_payment_event(dynamic raw);
@protected
NostrPaymentState dco_decode_nostr_payment_state(dynamic raw);
@protected
Nut04Settings dco_decode_nut_04_settings(dynamic raw);
@@ -288,6 +363,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected
PaymentRequestInfo dco_decode_payment_request_info(dynamic raw);
@protected
PersistedRequestData dco_decode_persisted_request_data(dynamic raw);
@protected
ReceiveOptions dco_decode_receive_options(dynamic raw);
@@ -312,6 +393,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
TransactionStatus dco_decode_transaction_status(dynamic raw);
@protected
TransportInfo dco_decode_transport_info(dynamic raw);
@protected
BigInt dco_decode_u_64(dynamic raw);
@@ -327,6 +411,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
CreatedPaymentRequest
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
NostrListenerHandle
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -351,6 +447,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
CreatedPaymentRequest
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -369,6 +471,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
CreatedPaymentRequest
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
NostrListenerHandle
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -398,6 +512,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
CreatedPaymentRequest
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
NostrListenerHandle
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -427,6 +553,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
RustStreamSink<NostrPaymentEvent>
sse_decode_StreamSink_nostr_payment_event_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<BigInt> sse_decode_StreamSink_u_64_Sse(
SseDeserializer deserializer,
@@ -441,6 +571,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
bool sse_decode_box_autoadd_bool(SseDeserializer deserializer);
@protected
CreateRequestParams sse_decode_box_autoadd_create_request_params(
SseDeserializer deserializer,
);
@protected
MeltQuote sse_decode_box_autoadd_melt_quote(SseDeserializer deserializer);
@@ -450,6 +585,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion sse_decode_box_autoadd_mint_version(SseDeserializer deserializer);
@protected
PersistedRequestData sse_decode_box_autoadd_persisted_request_data(
SseDeserializer deserializer,
);
@protected
ReceiveOptions sse_decode_box_autoadd_receive_options(
SseDeserializer deserializer,
@@ -475,6 +615,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ContactInfo sse_decode_contact_info(SseDeserializer deserializer);
@protected
CreateRequestParams sse_decode_create_request_params(
SseDeserializer deserializer,
);
@protected
Error sse_decode_error(SseDeserializer deserializer);
@@ -517,6 +662,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
List<Transaction> sse_decode_list_transaction(SseDeserializer deserializer);
@protected
List<TransportInfo> sse_decode_list_transport_info(
SseDeserializer deserializer,
);
@protected
MeltMethodSettings sse_decode_melt_method_settings(
SseDeserializer deserializer,
@@ -542,6 +692,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion sse_decode_mint_version(SseDeserializer deserializer);
@protected
NostrPaymentEvent sse_decode_nostr_payment_event(
SseDeserializer deserializer,
);
@protected
NostrPaymentState sse_decode_nostr_payment_state(
SseDeserializer deserializer,
);
@protected
Nut04Settings sse_decode_nut_04_settings(SseDeserializer deserializer);
@@ -600,6 +760,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
PaymentRequestInfo sse_decode_payment_request_info(
SseDeserializer deserializer,
);
@protected
PersistedRequestData sse_decode_persisted_request_data(
SseDeserializer deserializer,
);
@protected
ReceiveOptions sse_decode_receive_options(SseDeserializer deserializer);
@@ -628,6 +798,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
TransactionStatus sse_decode_transaction_status(SseDeserializer deserializer);
@protected
TransportInfo sse_decode_transport_info(SseDeserializer deserializer);
@protected
BigInt sse_decode_u_64(SseDeserializer deserializer);
@@ -646,6 +819,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
NostrListenerHandle self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -674,6 +861,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -695,6 +889,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
NostrListenerHandle self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -729,6 +937,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
NostrListenerHandle self,
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -763,6 +985,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_nostr_payment_event_Sse(
RustStreamSink<NostrPaymentEvent> self,
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_u_64_Sse(
RustStreamSink<BigInt> self,
@@ -778,6 +1006,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_box_autoadd_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_create_request_params(
CreateRequestParams self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_melt_quote(
MeltQuote self,
@@ -796,6 +1030,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_persisted_request_data(
PersistedRequestData self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_receive_options(
ReceiveOptions self,
@@ -826,6 +1066,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_contact_info(ContactInfo self, SseSerializer serializer);
@protected
void sse_encode_create_request_params(
CreateRequestParams self,
SseSerializer serializer,
);
@protected
void sse_encode_error(Error self, SseSerializer serializer);
@@ -883,6 +1129,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_list_transport_info(
List<TransportInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_melt_method_settings(
MeltMethodSettings self,
@@ -913,6 +1165,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_mint_version(MintVersion self, SseSerializer serializer);
@protected
void sse_encode_nostr_payment_event(
NostrPaymentEvent self,
SseSerializer serializer,
);
@protected
void sse_encode_nostr_payment_state(
NostrPaymentState self,
SseSerializer serializer,
);
@protected
void sse_encode_nut_04_settings(Nut04Settings self, SseSerializer serializer);
@@ -982,6 +1246,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_payment_request_info(
PaymentRequestInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_persisted_request_data(
PersistedRequestData self,
SseSerializer serializer,
);
@protected
void sse_encode_receive_options(
ReceiveOptions self,
@@ -1021,6 +1297,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_transport_info(TransportInfo self, SseSerializer serializer);
@protected
void sse_encode_u_64(BigInt self, SseSerializer serializer);
@@ -1048,6 +1327,74 @@ class RustLibWire implements BaseWire {
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
ptr,
);
}
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequestPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_elcaju_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest',
);
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequestPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
ptr,
);
}
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequestPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_elcaju_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest',
);
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequestPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
ptr,
);
}
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandlePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_elcaju_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle',
);
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandlePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
ptr,
);
}
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandlePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_elcaju_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle',
);
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandlePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
ffi.Pointer<ffi.Void> ptr,
+331
View File
@@ -9,6 +9,7 @@
import 'api/error.dart';
import 'api/keys.dart';
import 'api/mint_info.dart';
import 'api/payment_request.dart';
import 'api/token.dart';
import 'api/wallet.dart';
import 'dart:async';
@@ -24,6 +25,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
required super.portManager,
});
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_CreatedPaymentRequestPtr => wire
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest;
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_NostrListenerHandlePtr => wire
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle;
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_PreparedSendPtr => wire
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend;
@@ -43,6 +52,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
CreatedPaymentRequest
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
NostrListenerHandle
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
dynamic raw,
);
@protected
PreparedSend
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -67,6 +88,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
CreatedPaymentRequest
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
PreparedSend
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -85,6 +112,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
CreatedPaymentRequest
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
NostrListenerHandle
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
dynamic raw,
);
@protected
PreparedSend
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -112,6 +151,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Map<String, String> dco_decode_Map_String_String_None(dynamic raw);
@protected
CreatedPaymentRequest
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
NostrListenerHandle
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
dynamic raw,
);
@protected
PreparedSend
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -139,6 +190,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RustStreamSink<MintQuote> dco_decode_StreamSink_mint_quote_Sse(dynamic raw);
@protected
RustStreamSink<NostrPaymentEvent>
dco_decode_StreamSink_nostr_payment_event_Sse(dynamic raw);
@protected
RustStreamSink<BigInt> dco_decode_StreamSink_u_64_Sse(dynamic raw);
@@ -151,6 +206,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
bool dco_decode_box_autoadd_bool(dynamic raw);
@protected
CreateRequestParams dco_decode_box_autoadd_create_request_params(dynamic raw);
@protected
MeltQuote dco_decode_box_autoadd_melt_quote(dynamic raw);
@@ -160,6 +218,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion dco_decode_box_autoadd_mint_version(dynamic raw);
@protected
PersistedRequestData dco_decode_box_autoadd_persisted_request_data(
dynamic raw,
);
@protected
ReceiveOptions dco_decode_box_autoadd_receive_options(dynamic raw);
@@ -183,6 +246,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ContactInfo dco_decode_contact_info(dynamic raw);
@protected
CreateRequestParams dco_decode_create_request_params(dynamic raw);
@protected
Error dco_decode_error(dynamic raw);
@@ -219,6 +285,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
List<Transaction> dco_decode_list_transaction(dynamic raw);
@protected
List<TransportInfo> dco_decode_list_transport_info(dynamic raw);
@protected
MeltMethodSettings dco_decode_melt_method_settings(dynamic raw);
@@ -240,6 +309,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion dco_decode_mint_version(dynamic raw);
@protected
NostrPaymentEvent dco_decode_nostr_payment_event(dynamic raw);
@protected
NostrPaymentState dco_decode_nostr_payment_state(dynamic raw);
@protected
Nut04Settings dco_decode_nut_04_settings(dynamic raw);
@@ -290,6 +365,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected
PaymentRequestInfo dco_decode_payment_request_info(dynamic raw);
@protected
PersistedRequestData dco_decode_persisted_request_data(dynamic raw);
@protected
ReceiveOptions dco_decode_receive_options(dynamic raw);
@@ -314,6 +395,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
TransactionStatus dco_decode_transaction_status(dynamic raw);
@protected
TransportInfo dco_decode_transport_info(dynamic raw);
@protected
BigInt dco_decode_u_64(dynamic raw);
@@ -329,6 +413,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
CreatedPaymentRequest
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
NostrListenerHandle
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -353,6 +449,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
CreatedPaymentRequest
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -371,6 +473,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
CreatedPaymentRequest
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
NostrListenerHandle
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -400,6 +514,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
CreatedPaymentRequest
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
NostrListenerHandle
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -429,6 +555,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
RustStreamSink<NostrPaymentEvent>
sse_decode_StreamSink_nostr_payment_event_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<BigInt> sse_decode_StreamSink_u_64_Sse(
SseDeserializer deserializer,
@@ -443,6 +573,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
bool sse_decode_box_autoadd_bool(SseDeserializer deserializer);
@protected
CreateRequestParams sse_decode_box_autoadd_create_request_params(
SseDeserializer deserializer,
);
@protected
MeltQuote sse_decode_box_autoadd_melt_quote(SseDeserializer deserializer);
@@ -452,6 +587,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion sse_decode_box_autoadd_mint_version(SseDeserializer deserializer);
@protected
PersistedRequestData sse_decode_box_autoadd_persisted_request_data(
SseDeserializer deserializer,
);
@protected
ReceiveOptions sse_decode_box_autoadd_receive_options(
SseDeserializer deserializer,
@@ -477,6 +617,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ContactInfo sse_decode_contact_info(SseDeserializer deserializer);
@protected
CreateRequestParams sse_decode_create_request_params(
SseDeserializer deserializer,
);
@protected
Error sse_decode_error(SseDeserializer deserializer);
@@ -519,6 +664,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
List<Transaction> sse_decode_list_transaction(SseDeserializer deserializer);
@protected
List<TransportInfo> sse_decode_list_transport_info(
SseDeserializer deserializer,
);
@protected
MeltMethodSettings sse_decode_melt_method_settings(
SseDeserializer deserializer,
@@ -544,6 +694,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion sse_decode_mint_version(SseDeserializer deserializer);
@protected
NostrPaymentEvent sse_decode_nostr_payment_event(
SseDeserializer deserializer,
);
@protected
NostrPaymentState sse_decode_nostr_payment_state(
SseDeserializer deserializer,
);
@protected
Nut04Settings sse_decode_nut_04_settings(SseDeserializer deserializer);
@@ -602,6 +762,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
PaymentRequestInfo sse_decode_payment_request_info(
SseDeserializer deserializer,
);
@protected
PersistedRequestData sse_decode_persisted_request_data(
SseDeserializer deserializer,
);
@protected
ReceiveOptions sse_decode_receive_options(SseDeserializer deserializer);
@@ -630,6 +800,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
TransactionStatus sse_decode_transaction_status(SseDeserializer deserializer);
@protected
TransportInfo sse_decode_transport_info(SseDeserializer deserializer);
@protected
BigInt sse_decode_u_64(SseDeserializer deserializer);
@@ -648,6 +821,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
NostrListenerHandle self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -676,6 +863,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -697,6 +891,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
NostrListenerHandle self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -731,6 +939,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
NostrListenerHandle self,
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -765,6 +987,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_nostr_payment_event_Sse(
RustStreamSink<NostrPaymentEvent> self,
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_u_64_Sse(
RustStreamSink<BigInt> self,
@@ -780,6 +1008,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_box_autoadd_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_create_request_params(
CreateRequestParams self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_melt_quote(
MeltQuote self,
@@ -798,6 +1032,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_persisted_request_data(
PersistedRequestData self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_receive_options(
ReceiveOptions self,
@@ -828,6 +1068,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_contact_info(ContactInfo self, SseSerializer serializer);
@protected
void sse_encode_create_request_params(
CreateRequestParams self,
SseSerializer serializer,
);
@protected
void sse_encode_error(Error self, SseSerializer serializer);
@@ -885,6 +1131,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_list_transport_info(
List<TransportInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_melt_method_settings(
MeltMethodSettings self,
@@ -915,6 +1167,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_mint_version(MintVersion self, SseSerializer serializer);
@protected
void sse_encode_nostr_payment_event(
NostrPaymentEvent self,
SseSerializer serializer,
);
@protected
void sse_encode_nostr_payment_state(
NostrPaymentState self,
SseSerializer serializer,
);
@protected
void sse_encode_nut_04_settings(Nut04Settings self, SseSerializer serializer);
@@ -984,6 +1248,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_payment_request_info(
PaymentRequestInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_persisted_request_data(
PersistedRequestData self,
SseSerializer serializer,
);
@protected
void sse_encode_receive_options(
ReceiveOptions self,
@@ -1023,6 +1299,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_transport_info(TransportInfo self, SseSerializer serializer);
@protected
void sse_encode_u_64(BigInt self, SseSerializer serializer);
@@ -1041,6 +1320,38 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
class RustLibWire implements BaseWire {
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
int ptr,
) => wasmModule
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
ptr,
);
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
int ptr,
) => wasmModule
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
ptr,
);
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
int ptr,
) => wasmModule
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
ptr,
);
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
int ptr,
) => wasmModule
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
ptr,
);
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
int ptr,
@@ -1112,6 +1423,26 @@ external RustLibWasmModule get wasmModule;
@JS()
@anonymous
extension type RustLibWasmModule._(JSObject _) implements JSObject {
external void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
int ptr,
);
external void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
int ptr,
);
external void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
int ptr,
);
external void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
int ptr,
);
external void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
int ptr,
+354 -2
View File
@@ -17,6 +17,27 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "ahash"
version = "0.8.12"
@@ -184,12 +205,49 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "async-utility"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151"
dependencies = [
"futures-util",
"gloo-timers",
"tokio",
"wasm-bindgen-futures",
]
[[package]]
name = "async-wsocket"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069"
dependencies = [
"async-utility",
"futures",
"futures-util",
"js-sys",
"tokio",
"tokio-rustls",
"tokio-socks",
"tokio-tungstenite",
"url",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "atomic"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]]
name = "atomic-destructor"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4"
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -239,6 +297,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bc-ur"
version = "0.12.0"
@@ -354,6 +418,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
[[package]]
name = "brotli"
version = "8.0.2"
@@ -425,6 +498,7 @@ dependencies = [
"ciborium",
"lightning",
"lightning-invoice",
"nostr-sdk",
"once_cell",
"serde",
"serde_json",
@@ -439,6 +513,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher",
]
[[package]]
name = "cbor-diag"
version = "0.1.12"
@@ -490,6 +573,7 @@ dependencies = [
"jsonwebtoken",
"lightning",
"lightning-invoice",
"nostr-sdk",
"regex",
"ring",
"rustls",
@@ -628,6 +712,30 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "chacha20poly1305"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"cipher",
"poly1305",
"zeroize",
]
[[package]]
name = "chrono"
version = "0.4.44"
@@ -669,6 +777,17 @@ dependencies = [
"half",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
"zeroize",
]
[[package]]
name = "clap"
version = "4.6.0"
@@ -807,6 +926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"typenum",
]
@@ -915,6 +1035,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
@@ -940,6 +1061,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "elcaju_core"
version = "0.1.0"
@@ -953,6 +1080,7 @@ dependencies = [
"flutter_rust_bridge",
"getrandom 0.3.4",
"log",
"nostr-sdk",
"oslog",
"reqwest",
"serde_json",
@@ -1313,6 +1441,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.12"
@@ -1397,7 +1534,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
"webpki-roots 1.0.6",
]
[[package]]
@@ -1584,6 +1721,28 @@ dependencies = [
"serde_core",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"block-padding",
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "ipnet"
version = "2.12.0"
@@ -1754,6 +1913,12 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
[[package]]
name = "lru-slab"
version = "0.1.2"
@@ -1832,6 +1997,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "negentropy"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d"
[[package]]
name = "nom"
version = "7.1.3"
@@ -1842,6 +2013,84 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nostr"
version = "0.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1"
dependencies = [
"aes",
"base64 0.22.1",
"bech32",
"bip39",
"bitcoin_hashes 0.14.1",
"cbc",
"chacha20",
"chacha20poly1305",
"getrandom 0.2.17",
"hex",
"instant",
"scrypt",
"secp256k1",
"serde",
"serde_json",
"unicode-normalization",
"url",
]
[[package]]
name = "nostr-database"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1"
dependencies = [
"lru",
"nostr",
"tokio",
]
[[package]]
name = "nostr-gossip"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6"
dependencies = [
"nostr",
]
[[package]]
name = "nostr-relay-pool"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b"
dependencies = [
"async-utility",
"async-wsocket",
"atomic-destructor",
"hex",
"lru",
"negentropy",
"nostr",
"nostr-database",
"tokio",
"tracing",
]
[[package]]
name = "nostr-sdk"
version = "0.44.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393"
dependencies = [
"async-utility",
"nostr",
"nostr-database",
"nostr-gossip",
"nostr-relay-pool",
"tokio",
"tracing",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -1927,6 +2176,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl-probe"
version = "0.2.1"
@@ -1967,12 +2222,33 @@ dependencies = [
"windows-link",
]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
name = "pem"
version = "3.0.6"
@@ -2069,6 +2345,17 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -2365,7 +2652,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
"webpki-roots 1.0.6",
]
[[package]]
@@ -2467,6 +2754,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
]
[[package]]
name = "schannel"
version = "0.1.29"
@@ -2506,6 +2802,18 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scrypt"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [
"password-hash",
"pbkdf2",
"salsa20",
"sha2",
]
[[package]]
name = "secp256k1"
version = "0.29.1"
@@ -2659,6 +2967,17 @@ dependencies = [
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@@ -2960,6 +3279,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-socks"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
dependencies = [
"either",
"futures-util",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.18"
@@ -2985,6 +3316,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tungstenite",
"webpki-roots 0.26.11",
]
[[package]]
@@ -3184,6 +3516,16 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -3213,6 +3555,7 @@ dependencies = [
"idna",
"percent-encoding",
"serde",
"serde_derive",
]
[[package]]
@@ -3405,6 +3748,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.6",
]
[[package]]
name = "webpki-roots"
version = "1.0.6"
+5 -2
View File
@@ -8,8 +8,8 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
# Cashu — acceso directo, sin intermediarios
cdk = { version = "0.15.1", default-features = false, features = ["wallet"] }
cdk-common = { version = "0.15.1", default-features = false }
cdk = { version = "0.15.1", default-features = false, features = ["wallet", "nostr"] }
cdk-common = { version = "0.15.1", default-features = false, features = ["nostr"] }
cdk-sqlite = { version = "0.15.1", default-features = false, features = ["wallet"] }
# Bridge Flutter <-> Rust
@@ -31,6 +31,9 @@ bip39 = { version = "2.1", default-features = false, features = ["std"] }
# Entropy
getrandom = { version = "0.3", default-features = false, features = ["std"] }
# Nostr keys (same version as CDK 0.15.1 — CDK does not re-export Keys)
nostr-sdk = { version = "0.44.1", default-features = false, features = ["nip04", "nip44", "nip59"] }
# Async runtime
tokio = { version = "1", default-features = false }
+1
View File
@@ -1,5 +1,6 @@
pub mod error;
pub mod keys;
pub mod mint_info;
pub mod payment_request;
pub mod token;
pub mod wallet;
+591
View File
@@ -0,0 +1,591 @@
use std::str::FromStr;
use cdk::{
amount::Amount,
mint_url::MintUrl,
nuts::{
CurrencyUnit, PaymentRequest as CdkPaymentRequest, Transport, TransportType,
},
wallet::ReceiveOptions as CdkReceiveOptions,
};
use flutter_rust_bridge::frb;
use nostr_sdk::{
nips::nip19::{Nip19Profile, ToBech32},
Keys as NostrKeys, PublicKey, RelayUrl,
};
use crate::frb_generated::StreamSink;
use super::{error::Error, wallet::Wallet};
// ========================================================================
// Payment Request parsing (NUT-18 creqA + NUT-26 creqB)
// ========================================================================
/// Parsed payment request info exposed to Flutter.
/// CDK's FromStr auto-detects creqA (CBOR) vs creqB (Bech32m).
pub struct PaymentRequestInfo {
/// Raw encoded string (creqA... or CREQB1...)
pub raw: String,
/// Payment id
pub payment_id: Option<String>,
/// Requested amount (in base unit)
pub amount: Option<u64>,
/// Currency unit ("sat", "usd", etc.)
pub unit: Option<String>,
/// Whether this is a single-use request
pub single_use: Option<bool>,
/// Accepted mints (empty = any mint)
pub mints: Vec<String>,
/// Human-readable description
pub description: Option<String>,
/// Available transports
pub transports: Vec<TransportInfo>,
/// Whether NUT-10 spending conditions are required
pub has_nut10: bool,
}
/// Transport info exposed to Flutter
pub struct TransportInfo {
/// "nostr" or "post"
pub transport_type: String,
/// Target (nprofile or URL)
pub target: String,
}
impl PaymentRequestInfo {
/// Parse a payment request string (creqA, CREQB1, or bitcoin:?creq=).
/// Supports NUT-18 (CBOR+base64) and NUT-26 (Bech32m) automatically.
#[frb(sync)]
pub fn parse(encoded: String) -> Result<PaymentRequestInfo, Error> {
parse_payment_request_inner(encoded)
}
}
/// Inner parsing logic, outside of frb macro scope to avoid iterator issues.
fn parse_payment_request_inner(encoded: String) -> Result<PaymentRequestInfo, Error> {
let creq_str = extract_creq_from_uri(&encoded).unwrap_or(encoded);
let pr = CdkPaymentRequest::from_str(&creq_str)
.map_err(|e| Error::Cdk(format!("Invalid payment request: {e}")))?;
let mints: Vec<String> = match &pr.mints {
Some(list) => {
let mut v = Vec::new();
for m in list {
v.push(format!("{}", m));
}
v
}
None => Vec::new(),
};
let mut transports = Vec::new();
for t in &pr.transports {
transports.push(TransportInfo {
transport_type: match t._type {
TransportType::Nostr => "nostr".into(),
TransportType::HttpPost => "post".into(),
},
target: t.target.clone(),
});
}
let has_nut10 = pr.nut10.is_some();
Ok(PaymentRequestInfo {
raw: creq_str,
payment_id: pr.payment_id,
amount: pr.amount.map(|a| a.into()),
unit: pr.unit.map(|u| format!("{}", u)),
single_use: pr.single_use,
mints,
description: pr.description,
transports,
has_nut10,
})
}
/// Extract creq parameter from a BIP-321 bitcoin: URI.
/// e.g. "bitcoin:?creq=CREQB1...&lightning=lnbc..." → "CREQB1..."
/// Handles percent-encoding and case-insensitive key matching.
fn extract_creq_from_uri(input: &str) -> Option<String> {
if !input
.get(..8)
.is_some_and(|scheme| scheme.eq_ignore_ascii_case("bitcoin:"))
{
return None;
}
let query = input.split_once('?')?.1;
for param in query.split('&') {
let (key, value) = param.split_once('=')?;
if key.eq_ignore_ascii_case("creq") {
return Some(percent_decode(value));
}
}
None
}
/// Simple percent-decoding for URI query values.
fn percent_decode(value: &str) -> String {
let bytes = value.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let Ok(byte) = u8::from_str_radix(
std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""),
16,
) {
out.push(byte);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8(out).unwrap_or_else(|_| value.to_string())
}
// ========================================================================
// Payment execution (payer side)
// ========================================================================
impl Wallet {
/// Pay a NUT-18 payment request.
/// Uses CDK's pay_request() which handles:
/// - NUT-10 spending conditions
/// - Transport selection (Nostr preferred, HTTP POST fallback)
/// - Token preparation and delivery
pub async fn pay_payment_request(
&self,
encoded: String,
custom_amount: Option<u64>,
) -> Result<(), Error> {
let creq_str = extract_creq_from_uri(&encoded).unwrap_or(encoded);
let pr = CdkPaymentRequest::from_str(&creq_str)
.map_err(|e| Error::Cdk(format!("Invalid payment request: {e}")))?;
self.inner
.pay_request(pr, custom_amount.map(Amount::from))
.await?;
self.update_balance_streams().await;
Ok(())
}
}
// ========================================================================
// Payment Request creation (payee/receiver side) — NUT-18/26
// ========================================================================
/// Parameters for creating a payment request.
pub struct CreateRequestParams {
/// Amount to request (in smallest unit, e.g. sats)
pub amount: Option<u64>,
/// Currency unit ("sat", "usd", etc.)
pub unit: String,
/// Human-readable description
pub description: Option<String>,
/// Nostr relay URLs for the transport
pub nostr_relays: Vec<String>,
}
/// Result of creating a payment request.
pub struct CreatedPaymentRequest {
/// NUT-18 encoding: creqA... (CBOR+base64url)
pub creq_a: String,
/// NUT-26 encoding: CREQB1... (Bech32m, uppercase for QR)
pub creq_b: String,
/// Opaque handle holding the ephemeral Nostr keys — pass to wait_for_nostr_payment().
/// The secret key never leaves Rust.
pub listener_handle: NostrListenerHandle,
}
/// Opaque handle that keeps Nostr ephemeral keys in Rust memory.
/// Dart receives this as an opaque reference and passes it back
/// to wait_for_nostr_payment() without ever seeing the secret key.
/// Also carries the expected payment parameters for validation.
#[derive(Clone)]
pub struct NostrListenerHandle {
keys: NostrKeys,
pubkey: PublicKey,
relays: Vec<String>,
/// Expected amount (None = any amount accepted)
expected_amount: Option<Amount>,
/// Expected currency unit
expected_unit: CurrencyUnit,
/// This wallet's mint URL
mint_url: MintUrl,
}
/// Serializable data for persisting a NostrListenerHandle across app restarts.
pub struct PersistedRequestData {
pub secret_hex: String,
pub pubkey_hex: String,
pub relays: Vec<String>,
pub amount: Option<u64>,
pub unit: String,
pub mint_url: String,
}
impl NostrListenerHandle {
/// Export handle data for persistence (e.g. SharedPreferences).
/// The secret key is exposed as hex — the caller is responsible
/// for storing it securely.
#[frb(sync)]
pub fn to_persisted(&self) -> PersistedRequestData {
PersistedRequestData {
secret_hex: self.keys.secret_key().to_secret_hex(),
pubkey_hex: self.pubkey.to_hex(),
relays: self.relays.clone(),
amount: self.expected_amount.map(|a| a.into()),
unit: self.expected_unit.to_string(),
mint_url: self.mint_url.to_string(),
}
}
/// Reconstruct a handle from persisted data (e.g. after app restart).
#[frb(sync)]
pub fn from_persisted(data: PersistedRequestData) -> Result<NostrListenerHandle, Error> {
let secret_key = nostr_sdk::SecretKey::from_hex(&data.secret_hex)
.map_err(|e| Error::Cdk(format!("Invalid persisted secret key: {e}")))?;
let keys = NostrKeys::new(secret_key);
// Derive pubkey from secret — don't trust the persisted copy
let pubkey = keys.public_key;
let unit = CurrencyUnit::from_str(&data.unit)
.unwrap_or(CurrencyUnit::Custom(data.unit));
let mint_url = MintUrl::from_str(&data.mint_url)
.map_err(|e| Error::Cdk(format!("Invalid persisted mint URL: {e}")))?;
Ok(NostrListenerHandle {
keys,
pubkey,
relays: data.relays,
expected_amount: data.amount.map(Amount::from),
expected_unit: unit,
mint_url,
})
}
}
/// State of a Nostr payment listener.
pub enum NostrPaymentState {
/// Connected to relays, waiting for payment
Waiting,
/// Payment received and tokens claimed
Received,
/// Error occurred
Error,
}
/// Event emitted by the Nostr payment listener.
pub struct NostrPaymentEvent {
pub state: NostrPaymentState,
pub amount: Option<u64>,
pub error: Option<String>,
}
impl Wallet {
/// Create a NUT-18 payment request with Nostr transport.
///
/// Builds a PaymentRequest with the wallet's mint URL and unit,
/// generates ephemeral Nostr keys, and returns both creqA and creqB
/// encodings plus the keys needed for the Nostr listener.
pub async fn create_payment_request(
&self,
params: CreateRequestParams,
) -> Result<CreatedPaymentRequest, Error> {
// Generate ephemeral Nostr keys for this request
let keys = NostrKeys::generate();
// Parse relay URLs for nprofile
if params.nostr_relays.is_empty() {
return Err(Error::InvalidInput);
}
let relay_urls: Vec<RelayUrl> = params
.nostr_relays
.iter()
.map(|r| RelayUrl::parse(r))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| Error::Cdk(format!("Invalid relay URL: {e}")))?;
let nprofile = Nip19Profile::new(keys.public_key, relay_urls);
let nprofile_bech32 = nprofile
.to_bech32()
.map_err(|e| Error::Cdk(format!("nprofile encoding failed: {e}")))?;
// Build the Nostr transport
let nostr_transport = Transport {
_type: TransportType::Nostr,
target: nprofile_bech32,
tags: Some(vec![vec!["n".to_string(), "17".to_string()]]),
};
// Build the PaymentRequest with this wallet's mint and unit
let mint_url = self.mint_url()?;
if params.unit != self.unit {
return Err(Error::InvalidInput);
}
let unit = CurrencyUnit::from_str(&self.unit)
.unwrap_or(CurrencyUnit::Custom(self.unit.clone()));
let pr = CdkPaymentRequest {
payment_id: None,
amount: params.amount.map(Amount::from),
unit: Some(unit.clone()),
single_use: Some(true),
mints: Some(vec![mint_url.clone()]),
description: params.description,
transports: vec![nostr_transport],
nut10: None,
};
// Encode both formats
let creq_a = pr.to_string(); // NUT-18: creqA...
let creq_b = pr
.to_bech32_string()
.map_err(|e| Error::Cdk(format!("Bech32m encoding failed: {e}")))?;
let pubkey = keys.public_key;
Ok(CreatedPaymentRequest {
creq_a,
creq_b,
listener_handle: NostrListenerHandle {
keys,
pubkey,
relays: params.nostr_relays,
expected_amount: params.amount.map(Amount::from),
expected_unit: unit,
mint_url,
},
})
}
/// Wait for an incoming Nostr payment (NIP-17 gift-wrap).
///
/// Takes the opaque NostrListenerHandle returned by create_payment_request().
/// The secret key never leaves Rust memory.
pub async fn wait_for_nostr_payment(
&self,
handle: NostrListenerHandle,
sink: StreamSink<NostrPaymentEvent>,
) -> Result<(), Error> {
let NostrListenerHandle {
keys,
pubkey,
relays,
expected_amount,
expected_unit,
mint_url,
} = handle;
// Fail fast if the handle was created for a different wallet
let current_mint = self.mint_url()?;
let current_unit = CurrencyUnit::from_str(&self.unit)
.unwrap_or(CurrencyUnit::Custom(self.unit.clone()));
if mint_url != current_mint || expected_unit != current_unit {
return Err(Error::InvalidInput);
}
if sink
.add(NostrPaymentEvent {
state: NostrPaymentState::Waiting,
amount: None,
error: None,
})
.is_err()
{
return Ok(());
}
let _self = self.clone();
flutter_rust_bridge::spawn(async move {
match wait_for_nostr_payment_inner(
&_self, keys, pubkey, relays, expected_amount, expected_unit, mint_url, &sink,
)
.await
{
Ok(amount) => {
let _ = sink.add(NostrPaymentEvent {
state: NostrPaymentState::Received,
amount: Some(amount.into()),
error: None,
});
_self.update_balance_streams().await;
}
Err(e) => {
let _ = sink.add(NostrPaymentEvent {
state: NostrPaymentState::Error,
amount: None,
error: Some(e.to_string()),
});
}
}
});
Ok(())
}
}
/// Internal: connect to relays, listen for gift-wrapped payment, receive tokens.
/// Validates incoming payments against expected amount/unit/mint before accepting.
/// Periodically checks if the Dart stream is still alive and disconnects if not.
#[allow(clippy::too_many_arguments)]
async fn wait_for_nostr_payment_inner(
wallet: &Wallet,
keys: NostrKeys,
pubkey: PublicKey,
relays: Vec<String>,
expected_amount: Option<Amount>,
expected_unit: CurrencyUnit,
expected_mint: MintUrl,
sink: &StreamSink<NostrPaymentEvent>,
) -> Result<Amount, Error> {
use cdk::nuts::{nut00::ProofsMethods, PaymentRequestPayload, Token as CdkToken};
use nostr_sdk::{Client, Filter, Kind};
use std::time::Duration;
// Create Nostr client with ephemeral keys
let client = Client::new(keys.clone());
for relay in &relays {
client
.add_read_relay(relay)
.await
.map_err(|e| Error::Cdk(format!("Failed to add relay {relay}: {e}")))?;
}
// Connect with timeout so we fail fast if relays are unreachable
tokio::time::timeout(Duration::from_secs(10), client.connect())
.await
.map_err(|_| Error::Network("Relay connection timed out".to_string()))?;
// Subscribe to NIP-17 gift-wrap events (kind 1059) addressed to our ephemeral pubkey
let filter = Filter::new().pubkey(pubkey).kind(Kind::GiftWrap);
client
.subscribe(filter, None)
.await
.map_err(|e| Error::Cdk(format!("Subscription failed: {e}")))?;
// Listen for notifications with periodic cancellation check.
// Every 1s we check if the Dart stream is still alive by attempting
// a sink.add(). If it fails, the Dart side disposed the stream and
// we disconnect cleanly. Short interval minimizes "Fail to post" spam.
let mut notifications = client.notifications();
loop {
match tokio::time::timeout(Duration::from_secs(1), notifications.recv()).await {
Ok(Ok(notification)) => {
// Check sink is alive before processing
if sink
.add(NostrPaymentEvent {
state: NostrPaymentState::Waiting,
amount: None,
error: None,
})
.is_err()
{
client.disconnect().await;
return Err(Error::Network("Listener cancelled by client".to_string()));
}
if let nostr_sdk::RelayPoolNotification::Event { event, .. } = notification {
// Try to unwrap NIP-17 gift-wrap
let unwrapped = match client.unwrap_gift_wrap(&event).await {
Ok(rumor) => rumor,
Err(_) => continue,
};
// Parse PaymentRequestPayload from the rumor content
let payload: PaymentRequestPayload =
match serde_json::from_str(&unwrapped.rumor.content) {
Ok(p) => p,
Err(_) => continue,
};
// Validate unit matches
if payload.unit != expected_unit {
log::warn!(
"Ignoring payment: unit mismatch (expected {}, got {})",
expected_unit,
payload.unit
);
continue;
}
// Validate mint matches
if payload.mint != expected_mint {
log::warn!(
"Ignoring payment: mint mismatch (expected {}, got {})",
expected_mint,
payload.mint
);
continue;
}
// Validate amount if a specific amount was requested
if let Some(expected) = expected_amount {
let received_amount = payload.proofs.total_amount().unwrap_or_default();
if received_amount < expected {
log::warn!(
"Ignoring payment: underpayment (expected {}, got {})",
expected,
received_amount
);
continue;
}
}
// Build a token from the validated payload and receive it
let token = CdkToken::new(
payload.mint,
payload.proofs,
payload.memo,
payload.unit,
);
let token_str = token.to_string();
// Continue listening on receive errors (e.g. already-spent proofs)
match wallet
.inner
.receive(&token_str, CdkReceiveOptions::default())
.await
{
Ok(received) => {
client.disconnect().await;
return Ok(received);
}
Err(e) => {
log::warn!("Failed to receive token, continuing: {e}");
continue;
}
}
}
}
Ok(Err(_)) => {
// Notification channel closed
break;
}
Err(_) => {
// Timeout — check if the Dart stream is still alive
if sink
.add(NostrPaymentEvent {
state: NostrPaymentState::Waiting,
amount: None,
error: None,
})
.is_err()
{
// Dart side cancelled the stream, clean up
client.disconnect().await;
return Err(Error::Network("Listener cancelled by client".to_string()));
}
}
}
}
client.disconnect().await;
Err(Error::Network(
"Nostr listener ended before a payment was received".to_string(),
))
}
+25 -4
View File
@@ -45,7 +45,7 @@ pub struct Wallet {
pub unit: String,
balance_broadcast: broadcast::Sender<u64>,
inner: CdkWallet,
pub(crate) inner: CdkWallet,
seed: [u8; 64],
}
@@ -179,12 +179,33 @@ impl Wallet {
.inner
.mint_quote(PaymentMethod::BOLT11, Some(amount.into()), description, None)
.await?;
let _ = sink.add(MintQuote::from(quote.clone()));
if sink.add(MintQuote::from(quote.clone())).is_err() {
return Ok(());
}
let _self = self.clone();
flutter_rust_bridge::spawn(async move {
loop {
sleep(Duration::from_secs(3)).await;
// Check if the Dart stream is still alive before polling
if sink
.add(MintQuote {
id: quote.id.clone(),
request: quote.request.clone(),
amount: quote.amount.map(|a| a.into()),
expiry: Some(quote.expiry),
state: MintQuoteState::Unpaid,
token: None,
error: None,
})
.is_err()
{
info!("Mint polling stopped: Dart stream closed for {}", quote.id);
break;
}
info!("Checking mint quote state for {}", quote.id);
match _self.inner.check_mint_quote_status(&quote.id).await {
Ok(state_res) => match state_res.state {
@@ -383,7 +404,7 @@ impl Wallet {
// === Internal helpers ===
fn mint_url(&self) -> Result<MintUrl, Error> {
pub(crate) fn mint_url(&self) -> Result<MintUrl, Error> {
Ok(MintUrl::from_str(&self.mint_url)?)
}
@@ -391,7 +412,7 @@ impl Wallet {
CurrencyUnit::from_str(&self.unit).unwrap_or(CurrencyUnit::Custom(self.unit.clone()))
}
async fn update_balance_streams(&self) {
pub(crate) async fn update_balance_streams(&self) {
let balance = self
.inner
.total_balance()
+1223 -108
View File
File diff suppressed because it is too large Load Diff